diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index b67cb28..2d758af 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -1814,6 +1814,7 @@ dependencies = [ "embedded-graphics", "heapless 0.9.2", "prost", + "serde", ] [[package]] @@ -2132,6 +2133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" dependencies = [ "hash32", + "serde_core", "stable_deref_trait", ] diff --git a/firmware/firmware/Cargo.toml b/firmware/firmware/Cargo.toml index c27f080..720c194 100644 --- a/firmware/firmware/Cargo.toml +++ b/firmware/firmware/Cargo.toml @@ -2,7 +2,7 @@ name = "firmware" version = "0.1.0" edition = "2024" -rust-version = "1.88" +rust-version = "1.94.0" [[bin]] name = "firmware" diff --git a/firmware/firmware/src/main.rs b/firmware/firmware/src/main.rs index 772ad80..56c74ec 100644 --- a/firmware/firmware/src/main.rs +++ b/firmware/firmware/src/main.rs @@ -8,6 +8,8 @@ #![deny(clippy::large_stack_frames)] extern crate alloc; +mod midi; +mod storage; include!(concat!(env!("OUT_DIR"), "/version.rs")); @@ -22,44 +24,33 @@ use esp_alloc as _; )] use esp_backtrace as _; +use crate::storage::FakeStorageManager; + use core::cell::RefCell; -use core::marker::PhantomData; use core::ops::Add; use core::str::FromStr; use embassy_embedded_hal::shared_bus::blocking::spi::SpiDevice; -use embassy_executor::{Spawner, task}; -use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; -use embassy_sync::{ - blocking_mutex::{Mutex, raw::NoopRawMutex}, - channel::Channel, -}; +use embassy_executor::Spawner; +use embassy_sync::blocking_mutex::{Mutex, raw::NoopRawMutex}; use embassy_time::Delay; -use embassy_time::Timer; use embedded_graphics::draw_target::DrawTarget; use embedded_graphics::mono_font::{MonoTextStyleBuilder, ascii::FONT_10X20}; use embedded_graphics::prelude::*; use embedded_graphics::primitives::PrimitiveStyleBuilder; use embedded_graphics::text::{Alignment, Baseline, TextStyleBuilder}; use embedded_graphics::{pixelcolor::Rgb565, text::Text}; -use esp_hal::Async; use esp_hal::clock::CpuClock; use esp_hal::gpio::{Level, Output, OutputConfig}; use esp_hal::spi::master::Spi; use esp_hal::time::Rate; use esp_hal::timer::timg::TimerGroup; -use esp_hal::uart::{ - Config as UartConfig, DataBits, Parity, RxError, StopBits, TxError, Uart, UartRx, UartTx, -}; -use esp_println::println; +use esp_hal::uart::{Config as UartConfig, DataBits, Parity, StopBits, Uart}; +use foundation::application::channels; +use foundation::application::state::ApplicationBuilder; use foundation::layout::DisplayLayout; -use foundation::storage::StorageManager; -use foundation::storage::state::Presets; -use foundation::{ - application::state::ApplicationBuilder, - midi::{MidiPacket, MidiParser, MidiReader, MidiWriter}, -}; use heapless::String; use log::info; +use midi::{UartMidiReader, UartMidiWriter}; use mipidsi::models::ST7789; use mipidsi::options::Rotation::Deg270; use mipidsi::options::{ColorInversion, Orientation}; @@ -68,89 +59,6 @@ use mipidsi::options::{ColorInversion, Orientation}; // For more information see: esp_bootloader_esp_idf::esp_app_desc!(); -static MIDI_OUT_CHANNEL: Channel = Channel::new(); - -struct UartMidiReader<'a, 'b> { - uart: &'a mut UartRx<'b, Async>, - parser: MidiParser, -} - -impl<'a, 'b> UartMidiReader<'a, 'b> { - fn new(uart: &'a mut UartRx<'b, Async>) -> Self { - Self { - uart, - parser: MidiParser::default(), - } - } -} - -impl<'a, 'b> MidiReader for UartMidiReader<'a, 'b> { - type Error = RxError; - - async fn read_midi_packet(&mut self) -> Result, Self::Error> { - let mut buf = [0u8; 1]; - self.uart.read_async(&mut buf).await?; - - Ok(self.parser.feed(buf[0])) - } -} - -struct UartMidiWriter<'a, 'b> { - uart: &'a mut UartTx<'b, Async>, -} - -impl<'a, 'b> UartMidiWriter<'a, 'b> { - fn new(uart: &'a mut UartTx<'b, Async>) -> Self { - Self { uart } - } -} - -impl<'a, 'b> MidiWriter for UartMidiWriter<'a, 'b> { - type Error = TxError; - - async fn write_midi_packet(&mut self, packet: &MidiPacket) -> Result<(), Self::Error> { - self.uart - .write_async(&packet.data[..packet.len as usize]) - .await?; - Ok(()) - } -} - -#[derive(Default)] -struct FakeStorageManager<'a> { - phantom_data: PhantomData<&'a ()>, -} - -impl<'a> StorageManager for FakeStorageManager<'a> { - fn load_presets(&self) -> Presets { - heapless::Vec::new() - } - - fn save_presets(&mut self, _presets: &Presets) { - // Do nothing - } -} - -/// Forward MIDI messages IN to the MIDI_OUT_CHANNEL -#[task] -async fn midi_thru_task(mut reader: UartMidiReader<'static, 'static>) { - loop { - if let Some(packet) = reader.read_midi_packet().await.unwrap() { - MIDI_OUT_CHANNEL.send(packet).await; - } - } -} - -/// Read MIDI messages from the MIDI_OUT_CHANNEL and send them out over UART -#[task] -async fn midi_out_task(mut writer: UartMidiWriter<'static, 'static>) { - loop { - let packet = MIDI_OUT_CHANNEL.receive().await; - let res: Result<(), TxError> = writer.write_midi_packet(&packet).await; - res.unwrap(); - } -} - #[allow( clippy::large_stack_frames, reason = "it's not unusual to allocate larger buffers etc. in main" @@ -259,15 +167,6 @@ async fn main(spawner: Spawner) -> ! { .into_async(); let (mut rx, mut tx) = uart.split(); - // spawner - // .spawn(midi_thru_task(rx)) - // .expect("Unable to spawn MIDI thru task"); - // info!("MIDI thru task spawned"); - // spawner - // .spawn(midi_out_task(tx)) - // .expect("Unable to spawn MIDI out task"); - // info!("MIDI out task spawned"); - info!("Startup complete."); let mut midi_reader = UartMidiReader::new(&mut rx); @@ -279,10 +178,15 @@ async fn main(spawner: Spawner) -> ! { .with_midi_reader(&mut midi_reader) .with_midi_writer(&mut midi_writer) .with_storage_manager(&mut storage_manager) + .with_channels( + &mut channels::MidiOutChannel::new(), + &mut channels::DisplayStateUpdateChannel::new(), + &mut channels::StorageStateUpdateChannel::new(), + &mut channels::ButtonEventChannel::new(), + ) .build(); - loop { - Timer::after_secs(5).await; - println!("Heartbeat"); - } + // Start app tasks here + + core::future::pending().await } diff --git a/firmware/firmware/src/midi.rs b/firmware/firmware/src/midi.rs new file mode 100644 index 0000000..d664618 --- /dev/null +++ b/firmware/firmware/src/midi.rs @@ -0,0 +1,49 @@ +use esp_hal::Async; +use esp_hal::uart::{RxError, TxError, UartRx, UartTx}; +use foundation::midi::{MidiPacket, MidiParser, MidiReader, MidiWriter}; + +pub struct UartMidiReader<'a, 'b> { + uart: &'a mut UartRx<'b, Async>, + parser: MidiParser, +} + +impl<'a, 'b> UartMidiReader<'a, 'b> { + pub fn new(uart: &'a mut UartRx<'b, Async>) -> Self { + Self { + uart, + parser: MidiParser::default(), + } + } +} + +impl<'a, 'b> MidiReader for UartMidiReader<'a, 'b> { + type Error = RxError; + + async fn read_midi_packet(&mut self) -> Result, Self::Error> { + let mut buf = [0u8; 1]; + self.uart.read_async(&mut buf).await?; + + Ok(self.parser.feed(buf[0])) + } +} + +pub struct UartMidiWriter<'a, 'b> { + uart: &'a mut UartTx<'b, Async>, +} + +impl<'a, 'b> UartMidiWriter<'a, 'b> { + pub fn new(uart: &'a mut UartTx<'b, Async>) -> Self { + Self { uart } + } +} + +impl<'a, 'b> MidiWriter for UartMidiWriter<'a, 'b> { + type Error = TxError; + + async fn write_midi_packet(&mut self, packet: &MidiPacket) -> Result<(), Self::Error> { + self.uart + .write_async(&packet.data[..packet.len as usize]) + .await?; + Ok(()) + } +} diff --git a/firmware/firmware/src/storage.rs b/firmware/firmware/src/storage.rs new file mode 100644 index 0000000..917aca1 --- /dev/null +++ b/firmware/firmware/src/storage.rs @@ -0,0 +1,16 @@ +use foundation::storage::state::Presets; +use foundation::storage::{StorageManager, StorageManagerLoadError, StorageManagerSaveError}; + +#[derive(Default)] +pub struct FakeStorageManager; + +impl StorageManager for FakeStorageManager { + fn load_presets(&self) -> Result { + Ok(heapless::Vec::new()) + } + + fn save_presets(&mut self, presets: &Presets) -> Result<(), StorageManagerSaveError> { + // Do nothing + Ok(()) + } +} diff --git a/firmware/foundation/Cargo.toml b/firmware/foundation/Cargo.toml index 1806cfe..f277d76 100644 --- a/firmware/foundation/Cargo.toml +++ b/firmware/foundation/Cargo.toml @@ -2,7 +2,7 @@ name = "foundation" version = "0.1.0" edition = "2024" -rust-version = "1.88" +rust-version = "1.94.0" [lib] name = "foundation" @@ -11,5 +11,6 @@ path = "src/lib.rs" [dependencies] embedded-graphics = { version = "0.8.2" } prost = { version = "0.14.3", default-features = false, features = ["derive"] } -heapless = { version = "0.9.2" } +heapless = { version = "0.9.2", features = ["serde"] } embassy-sync = { version = "0.7.2", features = [] } +serde = { version = "1.0.228", default-features = false, features = ["derive"] } diff --git a/firmware/foundation/src/application/channels.rs b/firmware/foundation/src/application/channels.rs index 5ee8e47..fa999ea 100644 --- a/firmware/foundation/src/application/channels.rs +++ b/firmware/foundation/src/application/channels.rs @@ -25,4 +25,34 @@ pub enum ButtonEvent { pub type ButtonEventChannel = Channel; // TODO: Add channel for state updates +pub enum StorageStateEvent { + PresetUpdate { + preset_name: DisplayText, + // Display 1 + display_1_top_row_text: Option, + display_1_top_row_color: Option, + display_1_bottom_row_text: Option, + display_1_bottom_row_color: Option, + // Display 2 + display_2_top_row_text: Option, + display_2_top_row_color: Option, + display_2_bottom_row_text: Option, + display_2_bottom_row_color: Option, + // Display 3 + display_3_top_row_text: Option, + display_3_top_row_color: Option, + display_3_bottom_row_text: Option, + display_3_bottom_row_color: Option, + // Display 4 + display_4_top_row_text: Option, + display_4_top_row_color: Option, + display_4_bottom_row_text: Option, + display_4_bottom_row_color: Option, + // TODO: Button actions + }, + SavePreset, +} + +pub type StorageStateUpdateChannel = Channel; + // TODO: Add channel for button events diff --git a/firmware/foundation/src/application/mod.rs b/firmware/foundation/src/application/mod.rs index a2f8129..6755b82 100644 --- a/firmware/foundation/src/application/mod.rs +++ b/firmware/foundation/src/application/mod.rs @@ -1,20 +1,3 @@ -mod button_task; -mod channels; -mod display_task; +pub mod channels; pub mod state; - -use crate::midi::{MidiReader, MidiWriter}; -use embedded_graphics::draw_target::DrawTarget; -use embedded_graphics::pixelcolor::Rgb565; - -struct Displays<'a, D: DrawTarget> { - display_1: &'a mut D, - display_2: &'a mut D, - display_3: &'a mut D, - display_4: &'a mut D, -} - -struct MidiStreams<'a, MR: MidiReader, MW: MidiWriter> { - reader: &'a mut MR, - writer: &'a mut MW, -} +mod tasks; diff --git a/firmware/foundation/src/application/state.rs b/firmware/foundation/src/application/state.rs index b92cc2f..092f1a3 100644 --- a/firmware/foundation/src/application/state.rs +++ b/firmware/foundation/src/application/state.rs @@ -1,12 +1,28 @@ -use crate::application::channels::MidiOutChannel; -use crate::application::{Displays, MidiStreams}; +use crate::application::channels::{ + ButtonEventChannel, DisplayStateUpdateChannel, MidiOutChannel, StorageStateUpdateChannel, +}; use crate::midi::{MidiReader, MidiWriter}; use crate::storage::StorageManager; use embedded_graphics::draw_target::DrawTarget; use embedded_graphics::pixelcolor::Rgb565; -struct InternalChannels<'a> { +pub(crate) struct Displays<'a, D: DrawTarget> { + pub(crate) display_1: &'a mut D, + pub(crate) display_2: &'a mut D, + pub(crate) display_3: &'a mut D, + pub(crate) display_4: &'a mut D, +} + +pub(crate) struct MidiStreams<'a, MR: MidiReader, MW: MidiWriter> { + reader: &'a mut MR, + writer: &'a mut MW, +} + +pub(crate) struct InternalChannels<'a> { midi_out: &'a mut MidiOutChannel, + display_state_update: &'a mut DisplayStateUpdateChannel, + storage_state_update: &'a mut StorageStateUpdateChannel, + button_event: &'a mut ButtonEventChannel, } pub struct Application< @@ -16,7 +32,6 @@ pub struct Application< MW: MidiWriter, SM: StorageManager, > { - // TODO: Make these no longer public eventually? pub(crate) displays: Displays<'a, D>, pub(crate) midi_streams: MidiStreams<'a, MR, MW>, pub(crate) channels: InternalChannels<'a>, @@ -35,8 +50,11 @@ impl<'a, D: DrawTarget, MR: MidiReader, MW: MidiWriter, SM: Stor display_4: &'a mut D, midi_reader: &'a mut MR, midi_writer: &'a mut MW, - midi_out_channel: &'a mut MidiOutChannel, storage_manager: &'a mut SM, + midi_out_channel: &'a mut MidiOutChannel, + display_state_update_channel: &'a mut DisplayStateUpdateChannel, + storage_state_update_channel: &'a mut StorageStateUpdateChannel, + button_event_channel: &'a mut ButtonEventChannel, ) -> Self { // Maybe a good idea to create the channels here? Self { @@ -52,6 +70,9 @@ impl<'a, D: DrawTarget, MR: MidiReader, MW: MidiWriter, SM: Stor }, channels: InternalChannels { midi_out: midi_out_channel, + display_state_update: display_state_update_channel, + storage_state_update: storage_state_update_channel, + button_event: button_event_channel, }, storage_manager, } @@ -72,8 +93,11 @@ pub struct ApplicationBuilder< display_4: Option<&'a mut D>, midi_reader: Option<&'a mut MR>, midi_writer: Option<&'a mut MW>, - midi_out_channel: Option<&'a mut MidiOutChannel>, storage_manager: Option<&'a mut SM>, + midi_out_channel: Option<&'a mut MidiOutChannel>, + display_state_update_channel: Option<&'a mut DisplayStateUpdateChannel>, + storage_state_update_channel: Option<&'a mut StorageStateUpdateChannel>, + button_event_channel: Option<&'a mut ButtonEventChannel>, } impl<'a, D: DrawTarget, MR: MidiReader, MW: MidiWriter, SM: StorageManager> @@ -88,7 +112,10 @@ impl<'a, D: DrawTarget, MR: MidiReader, MW: MidiWriter, SM: Stor midi_reader: None, midi_writer: None, midi_out_channel: None, + display_state_update_channel: None, + storage_state_update_channel: None, storage_manager: None, + button_event_channel: None, } } @@ -117,8 +144,17 @@ impl<'a, D: DrawTarget, MR: MidiReader, MW: MidiWriter, SM: Stor self } - pub fn with_midi_out_channel(mut self, channel: &'a mut MidiOutChannel) -> Self { - self.midi_out_channel = Some(channel); + pub fn with_channels( + mut self, + midi_out_channel: &'a mut MidiOutChannel, + display_state_update_channel: &'a mut DisplayStateUpdateChannel, + storage_state_update_channel: &'a mut StorageStateUpdateChannel, + button_event_channel: &'a mut ButtonEventChannel, + ) -> Self { + self.midi_out_channel = Some(midi_out_channel); + self.display_state_update_channel = Some(display_state_update_channel); + self.storage_state_update_channel = Some(storage_state_update_channel); + self.button_event_channel = Some(button_event_channel); self } @@ -135,8 +171,14 @@ impl<'a, D: DrawTarget, MR: MidiReader, MW: MidiWriter, SM: Stor self.display_4.expect("Display 4 is required"), self.midi_reader.expect("MIDI reader is required"), self.midi_writer.expect("MIDI writer is required"), - self.midi_out_channel.expect("MIDI out channel is required"), self.storage_manager.expect("Storage manager is required"), + self.midi_out_channel.expect("MIDI out channel required"), + self.display_state_update_channel + .expect("Display state update channel required"), + self.storage_state_update_channel + .expect("Storage state update channel required"), + self.button_event_channel + .expect("Button event channel required"), ) } } diff --git a/firmware/foundation/src/application/button_task.rs b/firmware/foundation/src/application/tasks/button.rs similarity index 100% rename from firmware/foundation/src/application/button_task.rs rename to firmware/foundation/src/application/tasks/button.rs diff --git a/firmware/foundation/src/application/display_task.rs b/firmware/foundation/src/application/tasks/display.rs similarity index 96% rename from firmware/foundation/src/application/display_task.rs rename to firmware/foundation/src/application/tasks/display.rs index f499e84..d6bdb12 100644 --- a/firmware/foundation/src/application/display_task.rs +++ b/firmware/foundation/src/application/tasks/display.rs @@ -31,5 +31,6 @@ pub async fn display_task< 3 => &mut display_4_layout, _ => continue, // Invalid display index, ignore the message }; + // TODO: Update layout for display } } diff --git a/firmware/foundation/src/application/tasks/midi.rs b/firmware/foundation/src/application/tasks/midi.rs new file mode 100644 index 0000000..f5ecedf --- /dev/null +++ b/firmware/foundation/src/application/tasks/midi.rs @@ -0,0 +1,18 @@ +use crate::application::channels::MidiOutChannel; +use crate::midi::{MidiReader, MidiWriter}; + +async fn midi_thru_task(mut reader: MR, midi_out_channel: MidiOutChannel) -> ! { + loop { + if let Some(packet) = reader.read_midi_packet().await.unwrap() { + // TODO: If we decide to support MIDI command input in future, this would be a good place to process those + midi_out_channel.send(packet).await; + } + } +} + +async fn midi_out_task(mut writer: MW, midi_out_channel: MidiOutChannel) -> ! { + loop { + let packet = midi_out_channel.receive().await; + writer.write_midi_packet(&packet).await.unwrap(); + } +} diff --git a/firmware/foundation/src/application/tasks/mod.rs b/firmware/foundation/src/application/tasks/mod.rs new file mode 100644 index 0000000..e997d52 --- /dev/null +++ b/firmware/foundation/src/application/tasks/mod.rs @@ -0,0 +1,4 @@ +pub mod button; +pub mod display; +pub mod midi; +pub mod storage; diff --git a/firmware/foundation/src/application/tasks/storage.rs b/firmware/foundation/src/application/tasks/storage.rs new file mode 100644 index 0000000..4f716ad --- /dev/null +++ b/firmware/foundation/src/application/tasks/storage.rs @@ -0,0 +1,18 @@ +use crate::application::channels::{StorageStateEvent, StorageStateUpdateChannel}; +use crate::storage::StorageManager; + +async fn storage_read_task( + storage_manager: SM, + storage_state_event_channel: StorageStateUpdateChannel, +) -> ! { + let _presets = storage_manager + .load_presets() + .expect("Failed to load presets from storage"); + loop { + let event = storage_state_event_channel.receive().await; + match event { + StorageStateEvent::PresetUpdate { .. } => todo!(), + StorageStateEvent::SavePreset => todo!(), + } + } +} diff --git a/firmware/foundation/src/lib.rs b/firmware/foundation/src/lib.rs index f8f82a9..6746f02 100644 --- a/firmware/foundation/src/lib.rs +++ b/firmware/foundation/src/lib.rs @@ -12,3 +12,12 @@ pub mod layout; pub mod midi; pub mod protocol; pub mod storage; + +/// A trait for types that can be converted to and from another type `T` +pub trait Convertible: Sized { + /// Convert `Self` into `T` + fn to(self) -> T; + + /// Convert `T` into `Self` + fn from(value: T) -> Self; +} diff --git a/firmware/foundation/src/midi.rs b/firmware/foundation/src/midi.rs index a5d9a95..846d612 100644 --- a/firmware/foundation/src/midi.rs +++ b/firmware/foundation/src/midi.rs @@ -1,3 +1,5 @@ +use core::fmt::Debug; + #[derive(Copy, Clone, Debug)] pub enum NoteName { C, @@ -200,7 +202,7 @@ impl MidiParser { } pub trait MidiReader { - type Error; + type Error: Debug; /// Asynchronously read a MIDI packet, returning None if the stream ends fn read_midi_packet(&mut self) @@ -208,7 +210,7 @@ pub trait MidiReader { } pub trait MidiWriter { - type Error; + type Error: Debug; /// Asynchronously write a MIDI packet fn write_midi_packet( diff --git a/firmware/foundation/src/protocol.rs b/firmware/foundation/src/protocol.rs index 25fbb01..66e8ee5 100644 --- a/firmware/foundation/src/protocol.rs +++ b/firmware/foundation/src/protocol.rs @@ -1,7 +1,10 @@ +use crate::generated::device_v1 as pb; +use crate::generated::device_v1::Envelope; +use crate::midi::MidiPacket; use alloc::string::String; use alloc::vec::Vec; - -use crate::generated::device_v1 as pb; +use core::fmt::Debug; +use serde::{Deserialize, Serialize}; const PROTOCOL_VERSION: u32 = 1; @@ -22,10 +25,11 @@ pub struct Capabilities { #[derive(Debug)] pub struct MidiCommand { + // TODO: Fix placeholder: String, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub enum Colour { Red = 1, Green = 2, @@ -177,3 +181,143 @@ impl Message { } } } + +enum RxState { + ReadingLength, + ReadingPayload { len: usize, buf: Vec }, +} + +enum ReadError { + ErrorReadingLength, + ErrorReadingPayload, +} + +trait ProtocolReader { + /// Asynchronously read a payload, returning None if the stream ends + fn read_payload(&mut self) -> impl Future, ReadError>>; +} + +enum ParseState { + Idle, + ReadingLength, + ReadingPayload, +} + +struct ProtocolParser { + state: Option, + data: Vec, + index: usize, +} + +struct DefaultProtocolReader { + parser: ProtocolParser, +} + +impl ProtocolReader for DefaultProtocolReader { + async fn read_payload(&mut self) -> Result, ReadError> { + Ok(None) + } +} + +/// Slop + +/// CRC8 using a simple polynomial +fn crc8(data: &[u8]) -> u8 { + let mut crc = 0u8; + for &b in data { + crc ^= b; + for _ in 0..8 { + crc = if (crc & 0x80) != 0 { + crc << 1 ^ 0x07 + } else { + crc << 1 + }; + } + } + crc +} + +pub struct FrameDecoder { + state: State, + len_buf: [u8; 2], + len_pos: usize, + payload: [u8; N], + payload_pos: usize, + expected_len: usize, +} + +#[derive(Debug)] +enum State { + Sync1, + Sync2, + Len, + Payload, + Crc, +} + +impl FrameDecoder { + pub const fn new() -> Self { + Self { + state: State::Sync1, + len_buf: [0; 2], + len_pos: 0, + payload: [0; N], + payload_pos: 0, + expected_len: 0, + } + } + + /// Push a single byte + /// Returns Some(&payload) when a full valid frame is received + pub fn push(&mut self, byte: u8) -> Option<&[u8]> { + match self.state { + State::Sync1 => { + if byte == 0xAA { + self.state = State::Sync2; + } + } + State::Sync2 => { + if byte == 0x55 { + self.state = State::Len; + self.len_pos = 0; + } else { + // Stay in sync1 if second header byte fails + self.state = State::Sync1; + } + } + State::Len => { + self.len_buf[self.len_pos] = byte; + self.len_pos += 1; + if self.len_pos == 2 { + self.expected_len = u16::from_le_bytes(self.len_buf) as usize; + if self.expected_len == 0 || self.expected_len > N { + // Invalid length - resync + self.state = State::Sync1; + } else { + self.payload_pos = 0; + self.state = State::Payload; + } + } + } + State::Payload => { + self.payload[self.payload_pos] = byte; + self.payload_pos += 1; + if self.payload_pos == self.expected_len { + self.state = State::Crc; + } + } + State::Crc => { + let calc_crc = crc8(&self.payload[..self.expected_len]); + if calc_crc == byte { + // Success + self.state = State::Sync1; + return Some(&self.payload[..self.expected_len]); + } else { + // CRC failed - discard frame + self.state = State::Sync1; + } + } + } + None + } +} diff --git a/firmware/foundation/src/storage/mod.rs b/firmware/foundation/src/storage/mod.rs index 1e8bd83..99e005c 100644 --- a/firmware/foundation/src/storage/mod.rs +++ b/firmware/foundation/src/storage/mod.rs @@ -1,8 +1,22 @@ use crate::storage::state::Presets; +use core::fmt::Debug; pub mod state; +#[derive(Debug)] +pub enum StorageManagerLoadError { + ErrorReadingFromStorage, + NoValueStored, + ErrorDeserializingData, +} + +#[derive(Debug)] +pub enum StorageManagerSaveError { + ErrorDeserializingData, + ErrorWritingToStorage, +} + pub trait StorageManager { - fn load_presets(&self) -> Presets; - fn save_presets(&mut self, presets: &Presets); + fn load_presets(&self) -> Result; + fn save_presets(&mut self, presets: &Presets) -> Result<(), StorageManagerSaveError>; } diff --git a/firmware/foundation/src/storage/state.rs b/firmware/foundation/src/storage/state.rs index c726f1a..7d332a9 100644 --- a/firmware/foundation/src/storage/state.rs +++ b/firmware/foundation/src/storage/state.rs @@ -1,13 +1,14 @@ use crate::midi::MidiPacket; use crate::protocol::Colour; use heapless::{String, Vec}; +use serde::{Deserialize, Serialize}; pub const MAX_PRESETS: usize = 128; pub const MAX_STRING_LENGTH: usize = 16; pub const NUM_OF_BUTTONS: usize = 8; pub const MAX_BUTTON_ACTIONS: usize = 8; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] pub enum MidiCommand { ProgramChange { channel: u8, @@ -55,13 +56,13 @@ impl Into for MidiCommand { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] pub enum ButtonType { Momentary, Toggle, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct ButtonConfig { pub name: String, pub button_type: ButtonType, @@ -71,7 +72,7 @@ pub struct ButtonConfig { pub off_actions: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct StoredPreset { pub name: String, pub buttons: Vec, diff --git a/firmware/simulator/src/storage.rs b/firmware/simulator/src/storage.rs index ffe37bc..16e2b09 100644 --- a/firmware/simulator/src/storage.rs +++ b/firmware/simulator/src/storage.rs @@ -1,8 +1,13 @@ +use foundation::Convertible; +use foundation::storage::state::{Presets, StoredPreset}; +use foundation::storage::{StorageManager, StorageManagerLoadError, StorageManagerSaveError}; use serde::{Deserialize, Serialize}; use std::str::FromStr; +use std::vec::Vec; +use web_sys::Storage; #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] -pub enum MidiCommand { +enum MidiCommand { ProgramChange { channel: u8, program: u8, @@ -57,18 +62,25 @@ pub struct Preset { buttons: Vec, } -impl From for foundation::storage::state::ButtonType { - fn from(value: ButtonType) -> Self { - match value { +impl Convertible for ButtonType { + fn to(self) -> foundation::storage::state::ButtonType { + match self { ButtonType::Momentary => foundation::storage::state::ButtonType::Momentary, ButtonType::Toggle => foundation::storage::state::ButtonType::Toggle, } } -} -impl From for foundation::protocol::Colour { - fn from(value: Colour) -> Self { + fn from(value: foundation::storage::state::ButtonType) -> Self { match value { + foundation::storage::state::ButtonType::Momentary => ButtonType::Momentary, + foundation::storage::state::ButtonType::Toggle => ButtonType::Toggle, + } + } +} + +impl Convertible for Colour { + fn to(self) -> foundation::protocol::Colour { + match self { Colour::Red => foundation::protocol::Colour::Red, Colour::Green => foundation::protocol::Colour::Green, Colour::Blue => foundation::protocol::Colour::Blue, @@ -79,11 +91,24 @@ impl From for foundation::protocol::Colour { Colour::White => foundation::protocol::Colour::White, } } -} -impl From for foundation::storage::state::MidiCommand { - fn from(value: MidiCommand) -> Self { + fn from(value: foundation::protocol::Colour) -> Self { match value { + foundation::protocol::Colour::Red => Colour::Red, + foundation::protocol::Colour::Green => Colour::Green, + foundation::protocol::Colour::Blue => Colour::Blue, + foundation::protocol::Colour::Yellow => Colour::Yellow, + foundation::protocol::Colour::Orange => Colour::Orange, + foundation::protocol::Colour::Purple => Colour::Purple, + foundation::protocol::Colour::Cyan => Colour::Cyan, + foundation::protocol::Colour::White => Colour::White, + } + } +} + +impl Convertible for MidiCommand { + fn to(self) -> foundation::storage::state::MidiCommand { + match self { MidiCommand::ProgramChange { channel, program } => { foundation::storage::state::MidiCommand::ProgramChange { channel, program } } @@ -116,28 +141,133 @@ impl From for foundation::storage::state::MidiCommand { }, } } + + fn from(value: foundation::storage::state::MidiCommand) -> Self { + match value { + foundation::storage::state::MidiCommand::ProgramChange { channel, program } => { + MidiCommand::ProgramChange { channel, program } + } + foundation::storage::state::MidiCommand::ControllerChange { + channel, + controller, + value, + } => MidiCommand::ControllerChange { + channel, + controller, + value, + }, + foundation::storage::state::MidiCommand::NoteOn { + channel, + note, + velocity, + } => MidiCommand::NoteOn { + channel, + note, + velocity, + }, + foundation::storage::state::MidiCommand::NoteOff { + channel, + note, + velocity, + } => MidiCommand::NoteOff { + channel, + note, + velocity, + }, + } + } } -impl From for foundation::storage::state::ButtonConfig { - fn from(value: ButtonConfig) -> Self { +impl Convertible for ButtonConfig { + fn to(self) -> foundation::storage::state::ButtonConfig { foundation::storage::state::ButtonConfig { - name: heapless::String::from_str(value.name.as_str()).unwrap(), - button_type: value.button_type.into(), - colour: value.colour.into(), + name: heapless::String::from_str(self.name.as_str()).unwrap(), + button_type: self.button_type.to(), + colour: self.colour.to(), on_actions: heapless::Vec::from_iter( - value - .on_actions - .iter() - .map(|m| m.clone().into()) - .collect::>(), + self.on_actions.iter().map(|m| m.to()).collect::>(), ), off_actions: heapless::Vec::from_iter( - value - .off_actions + self.off_actions.iter().map(|m| m.to()).collect::>(), + ), + } + } + + fn from(value: foundation::storage::state::ButtonConfig) -> Self { + ButtonConfig { + name: value.name.to_string(), + button_type: Convertible::from(value.button_type), + colour: Convertible::from(value.colour), + on_actions: value + .on_actions + .into_iter() + .map(|m| Convertible::from(m)) + .collect(), + off_actions: value + .off_actions + .into_iter() + .map(|m| Convertible::from(m)) + .collect(), + } + } +} + +impl Convertible for Preset { + fn to(self) -> StoredPreset { + foundation::storage::state::StoredPreset { + name: heapless::String::from_str(self.name.as_str()).unwrap(), + buttons: heapless::Vec::from_iter( + self.buttons .iter() - .map(|m| m.clone().into()) + .map(|b| b.clone().to()) .collect::>(), ), } } + + fn from(value: StoredPreset) -> Self { + Preset { + name: value.name.to_string(), + buttons: value + .buttons + .into_iter() + .map(|b| Convertible::from(b)) + .collect(), + } + } +} + +pub struct LocalStorageManager<'a> { + local_storage: &'a mut Storage, +} + +const STORAGE_KEY_PRESETS: &str = "presets"; +const STORAGE_KEY_PRESET_ID: &str = "preset_id"; + +impl StorageManager for LocalStorageManager<'_> { + fn load_presets(&self) -> Result { + let value = self + .local_storage + .get_item(STORAGE_KEY_PRESETS) + .map_err(|_| StorageManagerLoadError::ErrorReadingFromStorage)? + .ok_or(StorageManagerLoadError::NoValueStored)?; + + let deserialized: Vec = serde_json::from_slice(value.as_bytes()) + .map_err(|_| StorageManagerLoadError::ErrorDeserializingData)?; + let mapped = deserialized.into_iter().map(|p| p.to()).collect(); + Ok(mapped) + } + + fn save_presets(&mut self, presets: &Presets) -> Result<(), StorageManagerSaveError> { + let mapped: Vec = presets + .into_iter() + .map(|p| Convertible::from(p.clone())) + .collect(); + let serialized = serde_json::to_string(&mapped) + .map_err(|_| StorageManagerSaveError::ErrorDeserializingData)?; + + self.local_storage + .set_item(STORAGE_KEY_PRESETS, serialized.as_str()) + .map_err(|_| StorageManagerSaveError::ErrorWritingToStorage) + } }