diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index 4aea81c..081c4f8 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -70,6 +70,12 @@ dependencies = [ "as-slice", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "allocator-api2" version = "0.3.1" @@ -602,17 +608,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "console_log" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" -dependencies = [ - "log", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "const-default" version = "1.0.0" @@ -1489,7 +1484,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "641e43d6a60244429117ef2fa7a47182120c7561336ea01f6fb08d634f46bae1" dependencies = [ - "allocator-api2", + "allocator-api2 0.3.1", "cfg-if", "document-features", "enumset", @@ -1863,6 +1858,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1879,6 +1880,7 @@ dependencies = [ "async-channel", "embassy-sync 0.7.2", "embedded-graphics", + "hashbrown 0.17.0", "heapless 0.9.2", "log", "prost", @@ -2175,7 +2177,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -2184,6 +2186,18 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "allocator-api2 0.2.21", + "equivalent", + "foldhash 0.2.0", + "rustc-std-workspace-alloc", +] + [[package]] name = "heapless" version = "0.8.0" @@ -4151,6 +4165,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc-std-workspace-alloc" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d441c3b2ebf55cebf796bfdc265d67fa09db17b7bb6bd4be75c509e1e8fec3" + [[package]] name = "rustc_version" version = "0.4.1" @@ -4529,9 +4549,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" name = "simulator" version = "0.1.0" dependencies = [ + "async-channel", "async_wasm_task", "console_error_panic_hook", - "console_log", "embedded-graphics", "embedded-graphics-web-simulator", "foundation", @@ -4542,6 +4562,8 @@ dependencies = [ "static_cell", "trunk", "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-logger", "web-sys", ] @@ -5491,6 +5513,17 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-metadata" version = "0.244.0" diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml index 7dbd477..de5b1b0 100644 --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -30,3 +30,4 @@ heapless = { version = "0.9.2" } static_cell = { version = "2.1.1" } serde = { version = "1.0.228", default-features = false } embassy-sync = { version = "0.7.2" } +async-channel = { version = "2.5.0" } diff --git a/firmware/firmware/src/main.rs b/firmware/firmware/src/main.rs index 1076f08..feae5da 100644 --- a/firmware/firmware/src/main.rs +++ b/firmware/firmware/src/main.rs @@ -10,6 +10,7 @@ extern crate alloc; mod midi; mod storage; +mod time; include!(concat!(env!("OUT_DIR"), "/version.rs")); @@ -26,11 +27,14 @@ use esp_backtrace as _; use crate::storage::FakeStorageManager; +use crate::time::EmbassyTimeSource; use core::cell::RefCell; use display_interface_spi::SPIInterface; use embassy_embedded_hal::shared_bus::blocking::spi::SpiDevice; use embassy_executor::Spawner; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::{Mutex, raw::NoopRawMutex}; +use embassy_sync::channel::{Channel, Sender}; use embassy_time::Delay; use embedded_graphics::draw_target::DrawTarget; use embedded_graphics::pixelcolor::Rgb565; @@ -42,6 +46,7 @@ use esp_hal::time::Rate; use esp_hal::timer::timg::TimerGroup; use esp_hal::uart::{Config as UartConfig, DataBits, Parity, StopBits, Uart, UartRx, UartTx}; use esp_hal::{Async, Blocking}; +use foundation::application::channels::{ButtonEvent, ButtonEventReceiver}; use foundation::application::state::{Application, ApplicationBuilder, Displays}; use log::info; use midi::{UartMidiReader, UartMidiWriter}; @@ -64,8 +69,11 @@ type FirmwareApplication = Application< UartMidiReader<'static, 'static>, UartMidiWriter<'static, 'static>, FakeStorageManager, + EmbassyTimeSource, >; +type ButtonEventChannel = Channel; + static RX: StaticCell> = StaticCell::new(); static TX: StaticCell> = StaticCell::new(); static DISPLAY_1: StaticCell = StaticCell::new(); @@ -76,7 +84,12 @@ static DISPLAYS: StaticCell> = StaticCell::new(); static UART_MIDI_READER: StaticCell = StaticCell::new(); static UART_MIDI_WRITER: StaticCell = StaticCell::new(); static STORAGE_MANAGER: StaticCell = StaticCell::new(); +static BUTTON_EVENT_CHANNEL: StaticCell = StaticCell::new(); +static BUTTON_EVENT_SENDER: StaticCell> = + StaticCell::new(); +static BUTTON_EVENT_RECEIVER: StaticCell = StaticCell::new(); static SPI_BUS: StaticCell>>> = StaticCell::new(); +static TIME_SOURCE: StaticCell = StaticCell::new(); static APP: StaticCell = StaticCell::new(); #[embassy_executor::task] @@ -217,12 +230,20 @@ async fn main(spawner: Spawner) -> ! { let midi_reader = UART_MIDI_READER.init(UartMidiReader::new(rx)); let midi_writer = UART_MIDI_WRITER.init(UartMidiWriter::new(tx)); let storage_manager = STORAGE_MANAGER.init(FakeStorageManager::default()); + let button_event_channel = + BUTTON_EVENT_CHANNEL.init(Channel::::new()); + // TODO: Use this in tasks later + let _button_event_sender = BUTTON_EVENT_SENDER.init(button_event_channel.sender()); + let button_event_receiver = BUTTON_EVENT_RECEIVER.init(button_event_channel.receiver()); + let time_source = TIME_SOURCE.init(EmbassyTimeSource::default()); let app = APP.init( ApplicationBuilder::new() .with_midi_reader(midi_reader) .with_midi_writer(midi_writer) .with_storage_manager(storage_manager) + .with_button_event_receiver(button_event_receiver) + .with_time_source(time_source) .build(), ); diff --git a/firmware/firmware/src/time.rs b/firmware/firmware/src/time.rs new file mode 100644 index 0000000..c1c9444 --- /dev/null +++ b/firmware/firmware/src/time.rs @@ -0,0 +1,10 @@ +use foundation::application::time::TimeSource; + +#[derive(Default)] +pub struct EmbassyTimeSource; + +impl TimeSource for EmbassyTimeSource { + fn now(&self) -> u64 { + embassy_time::Instant::now().as_millis() + } +} diff --git a/firmware/foundation/Cargo.toml b/firmware/foundation/Cargo.toml index c63cfb2..675d4cb 100644 --- a/firmware/foundation/Cargo.toml +++ b/firmware/foundation/Cargo.toml @@ -14,9 +14,10 @@ prost = { version = "0.14.3", default-features = false, features = ["derive"] } heapless = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } log = { workspace = true } +hashbrown = { version = "0.17.0", features = ["alloc"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] embassy-sync = { workspace = true, features = ["log"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -async-channel = { version = "2.5.0" } +async-channel = { workspace = true } diff --git a/firmware/foundation/src/application/channels.rs b/firmware/foundation/src/application/channels.rs index 3cc2c0f..594fd49 100644 --- a/firmware/foundation/src/application/channels.rs +++ b/firmware/foundation/src/application/channels.rs @@ -52,8 +52,15 @@ impl AppChannel { pub type MidiOutChannel = AppChannel; +pub enum DisplayIdentifier { + Display1, + Display2, + Display3, + Display4, +} + pub struct DisplayStateUpdateMessage { - pub(crate) display_index: i8, + pub(crate) display_identifier: DisplayIdentifier, pub(crate) top_row_text: DisplayText, pub(crate) top_row_color: Colour, pub(crate) bottom_row_text: DisplayText, @@ -62,12 +69,35 @@ pub struct DisplayStateUpdateMessage { pub type DisplayStateUpdateChannel = AppChannel; +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ButtonIdentifier { + Button1, + Button2, + Button3, + Button4, + Button5, + Button6, + Button7, + Button8, +} + +#[derive(Debug, Copy, Clone)] pub enum ButtonEvent { - Pressed { button_index: i8 }, - Released { button_index: i8 }, + Pressed { button_identifier: ButtonIdentifier }, + Released { button_identifier: ButtonIdentifier }, } -pub type ButtonEventChannel = AppChannel; +#[cfg(target_arch = "wasm32")] +type Receiver<'a, T, const N: usize> = async_channel::Receiver; +#[cfg(not(target_arch = "wasm32"))] +type Receiver<'a, T, const N: usize> = embassy_sync::channel::Receiver< + 'a, + embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + T, + N, +>; + +pub type ButtonEventReceiver<'a> = Receiver<'a, ButtonEvent, 64>; // TODO: Add channel for state updates pub enum StorageStateEvent { diff --git a/firmware/foundation/src/application/mod.rs b/firmware/foundation/src/application/mod.rs index 9290e5e..43d14f6 100644 --- a/firmware/foundation/src/application/mod.rs +++ b/firmware/foundation/src/application/mod.rs @@ -1,2 +1,3 @@ pub mod channels; pub mod state; +pub mod time; diff --git a/firmware/foundation/src/application/state.rs b/firmware/foundation/src/application/state.rs index 9f4898d..c4f3c28 100644 --- a/firmware/foundation/src/application/state.rs +++ b/firmware/foundation/src/application/state.rs @@ -1,13 +1,15 @@ use crate::application::channels::{ - ButtonEventChannel, DisplayStateUpdateChannel, MidiOutChannel, StorageStateEvent, - StorageStateUpdateChannel, + ButtonEvent, ButtonEventReceiver, ButtonIdentifier, DisplayIdentifier, + DisplayStateUpdateChannel, MidiOutChannel, StorageStateEvent, StorageStateUpdateChannel, }; +use crate::application::time::TimeSource; use crate::layout::DisplayLayout; use crate::midi::{MidiReader, MidiWriter}; use crate::storage::StorageManager; use core::cell::RefCell; use embedded_graphics::draw_target::DrawTarget; use embedded_graphics::pixelcolor::Rgb565; +use hashbrown::HashMap; use log::info; pub struct Displays<'a, D: DrawTarget> { @@ -38,26 +40,30 @@ pub(crate) struct MidiStreams<'a, MR: MidiReader, MW: MidiWriter> { writer: RefCell<&'a mut MW>, } -pub(crate) struct InternalChannels { +pub(crate) struct InternalChannels<'a> { midi_out: MidiOutChannel, display_state_update: DisplayStateUpdateChannel, storage_state_update: StorageStateUpdateChannel, - button_event: ButtonEventChannel, + button_event: &'a ButtonEventReceiver<'a>, } -pub struct Application<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> { +pub struct Application<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager, TS: TimeSource> { pub(crate) midi_streams: MidiStreams<'a, MR, MW>, - pub(crate) channels: InternalChannels, + pub(crate) channels: InternalChannels<'a>, pub(crate) storage_manager: &'a mut SM, - // TODO: Add protocol streams - // TODO: Add buttons + pub(crate) time_source: &'a TS, // TODO: Add protocol streams + // TODO: Add buttons } -impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> Application<'a, MR, MW, SM> { +impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager, TS: TimeSource> + Application<'a, MR, MW, SM, TS> +{ pub fn new( midi_reader: &'a mut MR, midi_writer: &'a mut MW, storage_manager: &'a mut SM, + button_event_receiver: &'a mut ButtonEventReceiver<'a>, + time_source: &'a TS, ) -> Self { Self { midi_streams: MidiStreams { @@ -68,9 +74,10 @@ impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> Application<'a, MR, midi_out: MidiOutChannel::new(), display_state_update: DisplayStateUpdateChannel::new(), storage_state_update: StorageStateUpdateChannel::new(), - button_event: ButtonEventChannel::new(), + button_event: button_event_receiver, }, storage_manager, + time_source, } } @@ -103,8 +110,38 @@ impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> Application<'a, MR, } pub async fn button_task(&self) -> ! { + info!("Starting button task (inside task)"); + + // TODO: Maybe create an enum to represent a single button's state machine + let mut button_pressed_at = HashMap::::with_capacity(8); + let mut button_released_at = HashMap::::with_capacity(8); + loop { + #[cfg(target_arch = "wasm32")] + let button_event = self.channels.button_event.recv().await.unwrap(); + #[cfg(not(target_arch = "wasm32"))] let button_event = self.channels.button_event.receive().await; + info!("Received button event: {:?}", button_event); + match button_event { + ButtonEvent::Pressed { button_identifier } => { + // TODO: We can do some logic here to register double-presses + button_released_at.remove(&button_identifier); + button_pressed_at.insert(button_identifier, self.time_source.now()); + // TODO: Trigger momentary button pressed event? + } + ButtonEvent::Released { button_identifier } => { + if let Some(pressed_at) = button_pressed_at.remove(&button_identifier) { + let duration_pressed = self + .time_source + .duration(pressed_at, self.time_source.now()); + if duration_pressed.as_secs().ge(&1) { + // TODO: Long press? + } + } + // TODO: Trigger momentary button released event? + // TODO: Trigger regular button press event? + } + } } } @@ -112,7 +149,10 @@ impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> Application<'a, MR, pub async fn display_task>( &self, displays: &mut Displays<'_, D>, - ) -> ! { + ) -> ! + where + ::Error: core::fmt::Debug, + { let mut display_1_layout = DisplayLayout::new(displays.display_1); let mut display_2_layout = DisplayLayout::new(displays.display_2); let mut display_3_layout = DisplayLayout::new(displays.display_3); @@ -120,14 +160,19 @@ impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> Application<'a, MR, loop { let update_message = self.channels.display_state_update.receive().await; - let target = match update_message.display_index { - 0 => &mut display_1_layout, - 1 => &mut display_2_layout, - 2 => &mut display_3_layout, - 3 => &mut display_4_layout, - _ => continue, // Invalid display index, ignore the message + let target = match update_message.display_identifier { + DisplayIdentifier::Display1 => &mut display_1_layout, + DisplayIdentifier::Display2 => &mut display_2_layout, + DisplayIdentifier::Display3 => &mut display_3_layout, + DisplayIdentifier::Display4 => &mut display_4_layout, }; - // TODO: Update layout for display + target.set_top_box_colour(update_message.top_row_color.into()); + target.set_bottom_box_colour(update_message.bottom_row_color.into()); + + target.set_top_text(update_message.top_row_text); + target.set_bottom_text(update_message.bottom_row_text); + + target.draw().unwrap(); } } @@ -148,18 +193,30 @@ impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> Application<'a, MR, } #[derive(Default)] -pub struct ApplicationBuilder<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> { +pub struct ApplicationBuilder< + 'a, + MR: MidiReader, + MW: MidiWriter, + SM: StorageManager, + TS: TimeSource, +> { midi_reader: Option<&'a mut MR>, midi_writer: Option<&'a mut MW>, storage_manager: Option<&'a mut SM>, + button_event_receiver: Option<&'a mut ButtonEventReceiver<'a>>, + time_source: Option<&'a TS>, } -impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> ApplicationBuilder<'a, MR, MW, SM> { +impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager, TS: TimeSource> + ApplicationBuilder<'a, MR, MW, SM, TS> +{ pub fn new() -> Self { Self { midi_reader: None, midi_writer: None, storage_manager: None, + button_event_receiver: None, + time_source: None, } } @@ -178,11 +235,27 @@ impl<'a, MR: MidiReader, MW: MidiWriter, SM: StorageManager> ApplicationBuilder< self } - pub fn build(self) -> Application<'a, MR, MW, SM> { + pub fn with_button_event_receiver( + mut self, + button_event_receiver: &'a mut ButtonEventReceiver<'a>, + ) -> Self { + self.button_event_receiver = Some(button_event_receiver); + self + } + + pub fn with_time_source(mut self, time_source: &'a TS) -> Self { + self.time_source = Some(time_source); + self + } + + pub fn build(self) -> Application<'a, MR, MW, SM, TS> { Application::new( self.midi_reader.expect("MIDI reader is required"), self.midi_writer.expect("MIDI writer is required"), self.storage_manager.expect("Storage manager is required"), + self.button_event_receiver + .expect("Button event channel is required"), + self.time_source.expect("Time source is required"), ) } } diff --git a/firmware/foundation/src/application/time.rs b/firmware/foundation/src/application/time.rs new file mode 100644 index 0000000..28b9d35 --- /dev/null +++ b/firmware/foundation/src/application/time.rs @@ -0,0 +1,9 @@ +use core::time::Duration; + +pub trait TimeSource { + fn now(&self) -> u64; + + fn duration(&self, from: u64, to: u64) -> Duration { + Duration::from_millis(to - from) + } +} diff --git a/firmware/foundation/src/protocol.rs b/firmware/foundation/src/protocol.rs index 1f7aebe..5b63d12 100644 --- a/firmware/foundation/src/protocol.rs +++ b/firmware/foundation/src/protocol.rs @@ -3,6 +3,7 @@ use crate::generated::device_v1::Envelope; use alloc::string::String; use alloc::vec::Vec; use core::fmt::Debug; +use embedded_graphics::pixelcolor::{Rgb565, WebColors}; use serde::{Deserialize, Serialize}; const PROTOCOL_VERSION: u32 = 1; @@ -40,6 +41,21 @@ pub enum Colour { White = 8, } +impl Into for Colour { + fn into(self) -> Rgb565 { + match self { + Colour::Red => Rgb565::CSS_RED, + Colour::Green => Rgb565::CSS_GREEN, + Colour::Blue => Rgb565::CSS_BLUE, + Colour::Yellow => Rgb565::CSS_YELLOW, + Colour::Orange => Rgb565::CSS_ORANGE, + Colour::Purple => Rgb565::CSS_MAGENTA, + Colour::Cyan => Rgb565::CSS_CYAN, + Colour::White => Rgb565::CSS_WHITE, + } + } +} + impl From> for pb::Colour { fn from(colour: Option) -> Self { match colour { diff --git a/firmware/simulator/Cargo.toml b/firmware/simulator/Cargo.toml index 9d81dfb..2bf3dc9 100644 --- a/firmware/simulator/Cargo.toml +++ b/firmware/simulator/Cargo.toml @@ -9,8 +9,10 @@ crate-type = ["cdylib", "rlib"] [dependencies] foundation = { path = "../foundation" } wasm-bindgen = { version = "0.2.114" } +wasm-bindgen-futures = { version = "0.4.68" } async_wasm_task = { version = "0.2.3" } -web-sys = { version = "0.3.91", features = ["console", "CanvasRenderingContext2d", "Document", "HtmlButtonElement", "Element", "HtmlCanvasElement", "Window", "Text", "Storage"] } +async-channel = { workspace = true } +web-sys = { version = "0.3.91", features = ["console", "CanvasRenderingContext2d", "Document", "HtmlButtonElement", "Element", "HtmlCanvasElement", "Window", "Text", "Storage", "EventListener", "EventListenerOptions", "Event", "EventTarget"] } console_error_panic_hook = { version = "0.1.7" } embedded-graphics = { workspace = true } @@ -22,8 +24,8 @@ serde_json = { version = "1.0.149" } heapless = { workspace = true } static_cell = { workspace = true } -console_log = { version = "1.0.0", features = ["wasm-bindgen"] } log = { workspace = true } +wasm-logger = { version = "0.2.0" } [dev-dependencies] trunk = { version = "0.21.14" } diff --git a/firmware/simulator/src/lib.rs b/firmware/simulator/src/lib.rs index 9b6366c..cfc5767 100644 --- a/firmware/simulator/src/lib.rs +++ b/firmware/simulator/src/lib.rs @@ -1,14 +1,15 @@ mod midi; mod sleep; mod storage; +mod time; use crate::midi::{FakeMidiReader, FakeMidiWriter}; -use crate::storage::{LocalStorageManager, Preset}; +use crate::storage::LocalStorageManager; +use crate::time::BrowserTimeSource; use embedded_graphics::draw_target::DrawTarget; use embedded_graphics::mono_font::ascii::FONT_10X20; use embedded_graphics::prelude::Primitive; use embedded_graphics::prelude::RgbColor; -use embedded_graphics::primitives::{PrimitiveStyleBuilder, StyledDrawable}; use embedded_graphics::{ Drawable, mono_font::MonoTextStyle, @@ -19,30 +20,101 @@ use embedded_graphics::{ use embedded_graphics_web_simulator::{ display::WebSimulatorDisplay, output_settings::OutputSettingsBuilder, }; +use foundation::application::channels::{ButtonEvent, ButtonEventReceiver, ButtonIdentifier}; use foundation::application::state::{Application, ApplicationBuilder, Displays}; use log::{Level, info}; use static_cell::StaticCell; use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; -use web_sys::{Element, HtmlButtonElement, Storage, window}; +use web_sys::js_sys::Date; +use web_sys::js_sys::futures::spawn_local; +use web_sys::{Document, Element, EventListener, HtmlButtonElement, Storage, window}; const STORAGE_KEY_PRESETS: &str = "presets"; const STORAGE_KEY_PRESET_ID: &str = "preset_id"; +type ButtonEventSender = async_channel::Sender; + static LOCAL_STORAGE: StaticCell = StaticCell::new(); static MIDI_READER: StaticCell = StaticCell::new(); static MIDI_WRITER: StaticCell = StaticCell::new(); static STORAGE_MANAGER: StaticCell = StaticCell::new(); -static APP: StaticCell> = - StaticCell::new(); +static BUTTON_EVENT_SENDER: StaticCell = StaticCell::new(); +static BUTTON_EVENT_RECEIVER: StaticCell = StaticCell::new(); +static TIME_SOURCE: StaticCell = StaticCell::new(); +static APP: StaticCell< + Application, +> = StaticCell::new(); static DISPLAY_1: StaticCell> = StaticCell::new(); static DISPLAY_2: StaticCell> = StaticCell::new(); static DISPLAY_3: StaticCell> = StaticCell::new(); static DISPLAY_4: StaticCell> = StaticCell::new(); static DISPLAYS: StaticCell>> = StaticCell::new(); -pub fn init_logging() { - console_log::init_with_level(Level::Debug).expect("logger init failed"); +fn init_logging() { + wasm_logger::init(wasm_logger::Config::default()); +} + +fn create_button_element( + document: &Document, + parent: &Element, +) -> Result { + parent + .append_child(&document.create_element("button")?.into()) + .and_then(|el| Ok(el.unchecked_into::())) +} + +fn create_display_element(document: &Document, parent: &Element) -> Result { + parent + .append_child(&document.create_element("div")?.into()) + .and_then(|el| Ok(el.dyn_into::()?)) + .and_then(|el| { + el.set_attribute("style", "display: flex; justify-content: center;")?; + Ok(el) + }) +} + +fn setup_event_listeners( + button_event_sender: &'static ButtonEventSender, + button_element: &HtmlButtonElement, + button_identifier: ButtonIdentifier, +) { + let press_listener = EventListener::new(); + let press_handler = Closure::wrap(Box::new(move |_event: web_sys::Event| { + spawn_local(async move { + let id = button_identifier.clone(); + button_event_sender + .send(ButtonEvent::Pressed { + button_identifier: id, + }) + .await + .unwrap(); + }); + }) as Box); + press_listener.set_handle_event(press_handler.as_ref().unchecked_ref()); + press_handler.forget(); + + let release_listener = EventListener::new(); + let release_handler = Closure::wrap(Box::new(move |_event: web_sys::Event| { + let id = button_identifier.clone(); + spawn_local(async move { + button_event_sender + .send(ButtonEvent::Released { + button_identifier: id, + }) + .await + .unwrap(); + }); + }) as Box); + release_listener.set_handle_event(release_handler.as_ref().unchecked_ref()); + release_handler.forget(); + + button_element + .add_event_listener_with_event_listener("mousedown", &press_listener) + .unwrap(); + button_element + .add_event_listener_with_event_listener("mouseup", &release_listener) + .unwrap(); } #[wasm_bindgen] @@ -69,15 +141,13 @@ pub fn main() { .expect("Failed to access localStorage") .expect("No localStorage"), ); + let midi_reader = MIDI_READER.init(FakeMidiReader::default()); + let midi_writer = MIDI_WRITER.init(FakeMidiWriter::default()); + let storage_manager = STORAGE_MANAGER.init(LocalStorageManager::new(local_storage)); - let _initial_preset_id: u8 = local_storage - .get_item(STORAGE_KEY_PRESET_ID) - .expect("Failed to get item from localStorage") - .map(|v| { - v.parse::() - .expect("Failed to parse item from localStorage") - }) - .unwrap_or(0); + let (_button_event_sender, _button_event_receiver) = async_channel::bounded(64); + let button_event_sender = BUTTON_EVENT_SENDER.init(_button_event_sender); + let button_event_receiver = BUTTON_EVENT_RECEIVER.init(_button_event_receiver); let document = window() .and_then(|win| win.document()) @@ -90,75 +160,81 @@ pub fn main() { }) .expect("Could not find root element with id 'simulator-root'"); + // Buttons 1 3 5 7 + // Displays 1 2 3 4 + // Buttons 2 4 6 8 + root_element - .set_attribute("style", "display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: 2em auto 2em; gap: 1rem;") - .unwrap(); - let _button_1_element = root_element - .append_child(&document.create_element("button").unwrap()) - .and_then(|el| Ok(el.unchecked_into::())) - .expect("Failed to create button 1 element"); - let _button_3_element = root_element - .append_child(&document.create_element("button").unwrap()) - .and_then(|el| Ok(el.unchecked_into::())) - .expect("Failed to create button 3 element"); - let _button_5_element = root_element - .append_child(&document.create_element("button").unwrap()) - .and_then(|el| Ok(el.unchecked_into::())) - .expect("Failed to create button 5 element"); - let _button_6_element = root_element - .append_child(&document.create_element("button").unwrap()) - .and_then(|el| Ok(el.unchecked_into::())) - .expect("Failed to create button 6 element"); + .set_attribute("style", "display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: 2em auto 2em; gap: 1rem;") + .unwrap(); - let display_1_element = root_element - .append_child(&document.create_element("div").unwrap()) - .and_then(|el| Ok(el.dyn_into::()?)) - .and_then(|el| { - el.set_attribute("style", "display: flex; justify-content: center;")?; - Ok(el) - }) - .expect("Failed to create display-1 element"); - let display_2_element = root_element - .append_child(&document.create_element("div").unwrap()) - .and_then(|el| Ok(el.dyn_into::()?)) - .and_then(|el| { - el.set_attribute("style", "display: flex; justify-content: center;")?; - Ok(el) - }) - .expect("Failed to create display-2 element"); - let display_3_element = root_element - .append_child(&document.create_element("div").unwrap()) - .and_then(|el| Ok(el.dyn_into::()?)) - .and_then(|el| { - el.set_attribute("style", "display: flex; justify-content: center;")?; - Ok(el) - }) - .expect("Failed to create display-3 element"); - let display_4_element = root_element - .append_child(&document.create_element("div").unwrap()) - .and_then(|el| Ok(el.dyn_into::()?)) - .and_then(|el| { - el.set_attribute("style", "display: flex; justify-content: center;")?; - Ok(el) - }) - .expect("Failed to create display-4 element"); + let button_1_element = + create_button_element(&document, &root_element).expect("Failed to create button 1 element"); + let button_3_element = + create_button_element(&document, &root_element).expect("Failed to create button 3 element"); + let button_5_element = + create_button_element(&document, &root_element).expect("Failed to create button 5 element"); + let button_7_element = + create_button_element(&document, &root_element).expect("Failed to create button 7 element"); - let _button_2_element = root_element - .append_child(&document.create_element("button").unwrap()) - .and_then(|el| Ok(el.unchecked_into::())) - .expect("Failed to create button 2 element"); - let _button_4_element = root_element - .append_child(&document.create_element("button").unwrap()) - .and_then(|el| Ok(el.unchecked_into::())) - .expect("Failed to create button 4 element"); - let _button_6_element = root_element - .append_child(&document.create_element("button").unwrap()) - .and_then(|el| Ok(el.unchecked_into::())) - .expect("Failed to create button 6 element"); - let _button_8_element = root_element - .append_child(&document.create_element("button").unwrap()) - .and_then(|el| Ok(el.unchecked_into::())) - .expect("Failed to create button 8 element"); + let display_1_element = create_display_element(&document, &root_element) + .expect("Failed to create display 1 element"); + let display_2_element = create_display_element(&document, &root_element) + .expect("Failed to create display 2 element"); + let display_3_element = create_display_element(&document, &root_element) + .expect("Failed to create display 3 element"); + let display_4_element = create_display_element(&document, &root_element) + .expect("Failed to create display 4 element"); + + let button_2_element = + create_button_element(&document, &root_element).expect("Failed to create button 2 element"); + let button_4_element = + create_button_element(&document, &root_element).expect("Failed to create button 4 element"); + let button_6_element = + create_button_element(&document, &root_element).expect("Failed to create button 6 element"); + let button_8_element = + create_button_element(&document, &root_element).expect("Failed to create button 8 element"); + + setup_event_listeners( + button_event_sender, + &button_1_element, + ButtonIdentifier::Button1, + ); + setup_event_listeners( + button_event_sender, + &button_2_element, + ButtonIdentifier::Button2, + ); + setup_event_listeners( + button_event_sender, + &button_3_element, + ButtonIdentifier::Button3, + ); + setup_event_listeners( + button_event_sender, + &button_4_element, + ButtonIdentifier::Button4, + ); + setup_event_listeners( + button_event_sender, + &button_5_element, + ButtonIdentifier::Button5, + ); + setup_event_listeners( + button_event_sender, + &button_6_element, + ButtonIdentifier::Button6, + ); + setup_event_listeners( + button_event_sender, + &button_7_element, + ButtonIdentifier::Button7, + ); + setup_event_listeners( + button_event_sender, + &button_8_element, + ButtonIdentifier::Button8, + ); let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::CSS_ORANGE); let display_output_settings = OutputSettingsBuilder::new() @@ -211,15 +287,15 @@ pub fn main() { display_1.flush().unwrap(); display_2.flush().unwrap(); - let midi_reader = MIDI_READER.init(FakeMidiReader::default()); - let midi_writer = MIDI_WRITER.init(FakeMidiWriter::default()); - let storage_manager = STORAGE_MANAGER.init(LocalStorageManager::new(local_storage)); + let time_source = TIME_SOURCE.init(BrowserTimeSource::default()); let app = APP.init( ApplicationBuilder::new() .with_midi_reader(midi_reader) .with_midi_writer(midi_writer) .with_storage_manager(storage_manager) + .with_button_event_receiver(button_event_receiver) + .with_time_source(time_source) .build(), ); let displays = DISPLAYS.init(Displays::new(display_1, display_2, display_3, display_4)); diff --git a/firmware/simulator/src/time.rs b/firmware/simulator/src/time.rs new file mode 100644 index 0000000..882a5dc --- /dev/null +++ b/firmware/simulator/src/time.rs @@ -0,0 +1,11 @@ +use foundation::application::time::TimeSource; +use web_sys::js_sys::Date; + +#[derive(Default)] +pub struct BrowserTimeSource; + +impl TimeSource for BrowserTimeSource { + fn now(&self) -> u64 { + Date::now() as u64 + } +}