diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index 34559ee..317232d 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -68,12 +68,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bit-set" version = "0.8.0" @@ -750,11 +744,26 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "hexa-tune-proto" +version = "0.1.0" +dependencies = [ + "defmt", +] + +[[package]] +name = "hexa-tune-proto-embedded" +version = "0.1.0" +dependencies = [ + "defmt", + "heapless 0.9.1", + "hexa-tune-proto", +] + [[package]] name = "hexagenmini" version = "0.1.0" dependencies = [ - "base64", "cortex-m", "cortex-m-rt", "critical-section", @@ -770,9 +779,10 @@ dependencies = [ "embassy-usb", "embedded-storage", "heapless 0.9.1", + "hexa-tune-proto", + "hexa-tune-proto-embedded", "panic-probe", "portable-atomic", - "sha2", "smart-leds", "static_cell", ] @@ -1253,17 +1263,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2-const-stable" version = "0.1.0" diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml index c192c6f..e2878ad 100644 --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -61,8 +61,8 @@ heapless = "0.9.1" static_cell = "2.1" portable-atomic = { version = "1.5", features = ["critical-section"] } -sha2 = { version = "0.10", default-features = false } -base64 = { version = "0.22.1", default-features = false } +hexa-tune-proto = { path = "../../hexaTuneProto/crates/hexa-tune-proto", default-features = false } +hexa-tune-proto-embedded = { path = "../../hexaTuneProto/crates/hexa-tune-proto-embedded", default-features = false, features = ["defmt"] } [profile.release] # Enable generation of debug symbols even on release builds diff --git a/firmware/src/at/at_task.rs b/firmware/src/at/at_task.rs index 5580a73..5da51af 100644 --- a/firmware/src/at/at_task.rs +++ b/firmware/src/at/at_task.rs @@ -1,53 +1,52 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT +use cortex_m::peripheral::SCB; use defmt::{error, info}; use embassy_executor::Spawner; use heapless::String; use {defmt_rtt as _, panic_probe as _}; +use hexa_tune_proto_embedded::command::HexaCommand; +use hexa_tune_proto_embedded::HexaError; + use crate::AT_CH; use crate::USB_CH; use crate::at::*; use crate::channel::*; +use crate::error::FirmwareError; use crate::hexa_config::*; #[embassy_executor::task] -pub async fn at_task(dispatcher: &'static AtDispatcher, spawner: Spawner) { +pub async fn at_task(spawner: Spawner) { info!("Starting AT task"); let mut last_operation_status: String<64> = String::new(); loop { match AT_CH.receive().await { Msg::AtRxLine(line) => { - if let Some(e) = dispatcher.dispatch(spawner, &line) { - let compiled = compile_at_error(get_empty_id(), e); + if let Err((id, e)) = dispatch_and_spawn(spawner, line.as_bytes()) { + let compiled = encode_error_response(id, &e); error!("Dispatch error: {:?}", compiled.as_str()); USB_CH.send(Msg::UsbTxLine(compiled)).await; } } - Msg::AtCmdOutput(at_command) => { - let compiled = at_command.compile(); - info!("Sending output: {}", compiled.as_str()); - USB_CH.send(Msg::UsbTxLine(compiled)).await; + Msg::AtCmdResponse(line) => { + info!("Sending response: {}", line.as_str()); + USB_CH.send(Msg::UsbTxLine(line)).await; } Msg::Done(msg_id) => { - let compiled = compile_at_done(msg_id); + let compiled = encode_done(msg_id); info!("Sending done: {}", compiled.as_str()); USB_CH.send(Msg::UsbTxLine(compiled)).await; } Msg::Err(msg_id, e) => { - let compiled = compile_at_error(msg_id, e); + let compiled = encode_error_response(msg_id, &e); error!("Sending error: {}", compiled.as_str()); USB_CH.send(Msg::UsbTxLine(compiled)).await; } Msg::SetDdsAvailable(status) => { set_dds_available(status); } - Msg::Completed(at_command) => { - let compiled = compile_at_completed(at_command); - info!("Sending done: {}", compiled.as_str()); - USB_CH.send(Msg::UsbTxLine(compiled)).await; - } Msg::SetOperationStatus(status) => { last_operation_status = status; } @@ -60,3 +59,62 @@ pub async fn at_task(dispatcher: &'static AtDispatcher, spawner: Spawner) { } } } + +fn dispatch_and_spawn(spawner: Spawner, payload: &[u8]) -> Result<(), (u32, FirmwareError)> { + let cmd = dispatch_at_payload(payload).map_err(|e| (0u32, e))?; + let id = command_id(&cmd); + + match cmd { + HexaCommand::VersionQuery => { + info!("Dispatching VERSION query"); + spawner.spawn(version_task()).ok(); + } + HexaCommand::SetRgb { id, r, g, b } => { + info!("Dispatching SETRGB command"); + spawner.spawn(setrgb_task(id, r, g, b)).ok(); + } + HexaCommand::Reset { .. } => { + info!("Dispatching RESET command"); + SCB::sys_reset(); + } + HexaCommand::FwUpdate { .. } => { + info!("Dispatching FWUPDATE command"); + spawner.spawn(fwupdate_task()).ok(); + } + HexaCommand::Freq { id, freq, time_ms } => { + if !is_dds_available() { + error!("DDS busy, cannot set FREQ"); + return Err((id, FirmwareError::Hexa(HexaError::DdsBusy))); + } + info!("Dispatching FREQ command"); + spawner.spawn(freq_task(id, freq, time_ms)).ok(); + } + HexaCommand::Operation { id, sub } => { + if !is_dds_available() { + error!("DDS busy, cannot set OPERATION"); + return Err((id, FirmwareError::Hexa(HexaError::DdsBusy))); + } + info!("Dispatching OPERATION command"); + spawner.spawn(operation_task(id, sub)).ok(); + } + HexaCommand::OperationQuery => { + info!("Dispatching OPERATION query"); + spawner.spawn(operation_status_task()).ok(); + } + _ => { + return Err((id, FirmwareError::Hexa(HexaError::UnknownCommand))); + } + } + Ok(()) +} + +fn command_id(cmd: &HexaCommand) -> u32 { + match cmd { + HexaCommand::SetRgb { id, .. } + | HexaCommand::Reset { id } + | HexaCommand::FwUpdate { id } + | HexaCommand::Freq { id, .. } + | HexaCommand::Operation { id, .. } => *id, + _ => 0, + } +} diff --git a/firmware/src/at/command.rs b/firmware/src/at/command.rs deleted file mode 100644 index 6318713..0000000 --- a/firmware/src/at/command.rs +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-FileCopyrightText: 2025 hexaTune LLC -// SPDX-License-Identifier: MIT - -use defmt::info; -use heapless::{String, Vec}; - -use crate::error::Error; - -#[derive(Debug, Clone)] -pub struct AtCommand { - pub id: String<16>, - pub name: String<16>, - pub params: Vec, 8>, - pub is_query: bool, -} - -impl AtCommand { - pub fn compile(&self) -> String<64> { - let mut s = String::<64>::new(); - s.push_str("AT+").unwrap(); - s.push_str(self.name.as_str()).unwrap(); - if self.is_query { - s.push('?').unwrap(); - } else { - s.push('=').unwrap(); - s.push_str(self.id.as_str()).unwrap(); - if !self.params.is_empty() { - for p in self.params.iter() { - s.push('#').unwrap(); - s.push_str(p.as_str()).unwrap(); - } - } - } - s - } -} - -pub fn parse(input: &str) -> Result { - info!("Received input: {}", input); - let input = input.trim(); - if !input.starts_with("AT+") { - return Err(Error::InvalidCommand); - } - let cmd = &input[3..]; - if let Some(eq_pos) = cmd.find('=') { - let (name, param_str) = cmd.split_at(eq_pos); - let mut id: String<16> = { - let mut id = String::<16>::new(); - id.push_str("0").map_err(|_| Error::InvalidUtf8)?; - id - }; - let param_str = ¶m_str[1..]; - let mut params = Vec::, 8>::new(); - for (i, p) in param_str - .split('#') - .map(|p| p.trim()) - .filter(|p| !p.is_empty()) - .enumerate() - { - info!("Parsing param: {}", p); - let mut s_hl = String::<16>::new(); - s_hl.push_str(p).map_err(|_| Error::InvalidUtf8)?; - if i == 0 { - id = s_hl; - } else { - params.push(s_hl).map_err(|_| Error::ParamCount)?; - } - } - Ok(AtCommand { - id, - name: { - let mut n = String::<16>::new(); - n.push_str(name).map_err(|_| Error::InvalidUtf8)?; - n - }, - params, - is_query: false, - }) - } else if let Some(name) = cmd.strip_suffix('?') { - Ok(AtCommand { - id: { - let mut id = String::<16>::new(); - id.push_str("0").map_err(|_| Error::InvalidUtf8)?; - id - }, - name: { - let mut n = String::<16>::new(); - n.push_str(name).map_err(|_| Error::InvalidUtf8)?; - n - }, - params: Vec::, 8>::new(), - is_query: true, - }) - } else { - Ok(AtCommand { - id: { - let mut id = String::<16>::new(); - id.push_str("0").map_err(|_| Error::InvalidUtf8)?; - id - }, - name: { - let mut n = String::<16>::new(); - n.push_str(cmd).map_err(|_| Error::InvalidUtf8)?; - n - }, - params: Vec::, 8>::new(), - is_query: false, - }) - } -} - -pub fn get_empty_id() -> String<16> { - let mut id = String::<16>::new(); - id.push_str("0").unwrap(); - id -} - -pub fn compile_at_done(id: String<16>) -> String<64> { - let at_cmd = AtCommand { - id, - name: { - let mut n = String::<16>::new(); - n.push_str("DONE").unwrap(); - n - }, - params: Vec::, 8>::new(), - is_query: false, - }; - at_cmd.compile() -} - -pub fn compile_at_completed(at_command: AtCommand) -> String<64> { - let mut at_cmd = AtCommand { - id: at_command.id, - name: at_command.name, - params: at_command.params, - is_query: false, - }; - let completede_param = String::<16>::try_from("COMPLETED").unwrap(); - at_cmd.params.push(completede_param).ok(); - at_cmd.compile() -} - -pub fn compile_at_error(id: String<16>, e: Error) -> String<64> { - let at_cmd = AtCommand { - id, - name: { - let mut n = String::<16>::new(); - n.push_str("ERROR").unwrap(); - n - }, - params: { - let mut p = Vec::, 8>::new(); - let mut code = String::<16>::new(); - code.push_str(e.code()).unwrap(); - p.push(code).unwrap(); - p - }, - is_query: false, - }; - at_cmd.compile() -} diff --git a/firmware/src/at/dispatcher.rs b/firmware/src/at/dispatcher.rs index 935341e..20ef041 100644 --- a/firmware/src/at/dispatcher.rs +++ b/firmware/src/at/dispatcher.rs @@ -1,60 +1,76 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use defmt::{error, info}; -use embassy_executor::Spawner; -use heapless::{LinearMap, String}; +use hexa_tune_proto::at::{self, AtOp}; +use hexa_tune_proto_embedded::command::HexaCommand; +use hexa_tune_proto_embedded::dispatch::resolve; -use crate::at::handler::{AtHandler, Handler, register_all}; -use crate::at::parse; -use crate::error::Error; +use crate::channel::MsgString; +use crate::error::FirmwareError; -pub struct AtDispatcher { - handlers: LinearMap, Handler, 8>, +/// Parse an AT payload and resolve it to a typed HexaCommand. +pub fn dispatch_at_payload(payload: &[u8]) -> Result { + let msg = at::parse(payload).map_err(FirmwareError::Proto)?; + resolve(&msg).map_err(FirmwareError::Hexa) } -impl AtDispatcher { - pub fn new() -> Self { - Self { - handlers: register_all(), +/// Encode an AT response (name=id#params...) into a MsgString. +pub fn encode_response(name: &[u8], id: u32, params: &[&[u8]]) -> MsgString { + let mut buf = [0u8; 64]; + if let Ok(n) = at::encode(name, id, AtOp::Response, params, &mut buf) { + if let Ok(s) = core::str::from_utf8(&buf[..n]) { + if let Ok(line) = MsgString::try_from(s) { + return line; + } } } + MsgString::new() +} - pub fn dispatch(&self, spawner: Spawner, input: &str) -> Option { - match parse(input) { - Ok(cmd) => match self.handlers.get(&cmd.name) { - Some(handler) => match handler { - Handler::Version(h) => { - info!("Dispatching VERSION command"); - h.handle(spawner, cmd) - } - Handler::SetRgb(h) => { - info!("Dispatching SETRGB command"); - h.handle(spawner, cmd) - } - Handler::Reset(h) => { - info!("Dispatching RESET command"); - h.handle(spawner, cmd) - } - Handler::FwUpdate(h) => { - info!("Dispatching FWUPDATE command"); - h.handle(spawner, cmd) - } - Handler::Freq(h) => { - info!("Dispatching FREQ command"); - h.handle(spawner, cmd) - } - Handler::Operation(h) => { - info!("Dispatching OPERATION command"); - h.handle(spawner, cmd) - } - }, - None => Some(Error::UnknownCommand), - }, - Err(e) => { - error!("Failed to parse command: {}", &e.code()); - Some(e) - } - } +/// Encode an AT+DONE=id response. +pub fn encode_done(id: u32) -> MsgString { + encode_response(b"DONE", id, &[]) +} + +/// Encode an AT+ERROR=id#code response using u8 error code. +pub fn encode_error_response(id: u32, e: &FirmwareError) -> MsgString { + let code = e.error_code(); + let mut code_buf = [0u8; 3]; + let code_len = u8_to_ascii(code, &mut code_buf); + encode_response(b"ERROR", id, &[&code_buf[..code_len]]) +} + +/// Convert a u32 value to ASCII decimal bytes in a buffer. +pub fn u32_to_ascii_buf(val: u32, buf: &mut [u8; 10]) -> usize { + if val == 0 { + buf[0] = b'0'; + return 1; + } + let mut tmp = [0u8; 10]; + let mut n = val; + let mut i = 10usize; + while n > 0 { + i -= 1; + tmp[i] = b'0' + (n % 10) as u8; + n /= 10; + } + let len = 10 - i; + buf[..len].copy_from_slice(&tmp[i..]); + len +} + +fn u8_to_ascii(val: u8, buf: &mut [u8; 3]) -> usize { + if val >= 100 { + buf[0] = b'0' + val / 100; + buf[1] = b'0' + (val / 10) % 10; + buf[2] = b'0' + val % 10; + 3 + } else if val >= 10 { + buf[0] = b'0' + val / 10; + buf[1] = b'0' + val % 10; + 2 + } else { + buf[0] = b'0' + val; + 1 } } diff --git a/firmware/src/at/handler.rs b/firmware/src/at/handler.rs deleted file mode 100644 index 8567778..0000000 --- a/firmware/src/at/handler.rs +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2025 hexaTune LLC -// SPDX-License-Identifier: MIT - -use embassy_executor::Spawner; -use heapless::{LinearMap, String}; - -use crate::at::*; -use crate::error::Error; - -pub trait AtHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option; -} - -pub enum Handler { - Version(VersionHandler), - SetRgb(SetRgbHandler), - Reset(ResetHandler), - FwUpdate(FwUpdateHandler), - Freq(FreqHandler), - Operation(OperationHandler), -} - -pub fn register_all() -> LinearMap, Handler, 8> { - let mut map: LinearMap, Handler, 8> = LinearMap::new(); - - map.insert( - String::<16>::try_from("VERSION").unwrap(), - Handler::Version(VersionHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("SETRGB").unwrap(), - Handler::SetRgb(SetRgbHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("RESET").unwrap(), - Handler::Reset(ResetHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("FWUPDATE").unwrap(), - Handler::FwUpdate(FwUpdateHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("FREQ").unwrap(), - Handler::Freq(FreqHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("OPERATION").unwrap(), - Handler::Operation(OperationHandler), - ) - .ok(); - - map -} diff --git a/firmware/src/at/handlers/freq_handler.rs b/firmware/src/at/handlers/freq_handler.rs index 7d5ea45..9e770df 100644 --- a/firmware/src/at/handlers/freq_handler.rs +++ b/firmware/src/at/handlers/freq_handler.rs @@ -1,36 +1,14 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use defmt::{error, info}; -use embassy_executor::Spawner; +use defmt::info; use crate::DDS_CH; -use crate::at::{AtCommand, AtHandler}; use crate::channel::*; -use crate::error::Error; -use crate::hexa_config::*; - -pub struct FreqHandler; - -impl AtHandler for FreqHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option { - if is_dds_available() { - info!("Setting FREQ: {:?}", at_command.id.as_str()); - let _ = spawner.spawn(freq_task(at_command)); - None - } else { - error!("DDS busy, cannot set FREQ ({})", at_command.id.as_str()); - Some(Error::DdsBusy) - } - } -} #[embassy_executor::task] -async fn freq_task(at_command: AtCommand) { - info!( - "Sending FREQ command to DDS task: {}", - at_command.id.as_str() - ); - DDS_CH.send(Msg::FreqWithValue(at_command)).await; +pub async fn freq_task(id: u32, freq: u32, time_ms: u32) { + info!("Sending FREQ command to DDS task"); + DDS_CH.send(Msg::FreqSet { id, freq, time_ms }).await; info!("FREQ command sent to DDS task"); } diff --git a/firmware/src/at/handlers/fwupdate_handler.rs b/firmware/src/at/handlers/fwupdate_handler.rs index 4effdae..0ab7c87 100644 --- a/firmware/src/at/handlers/fwupdate_handler.rs +++ b/firmware/src/at/handlers/fwupdate_handler.rs @@ -2,23 +2,9 @@ // SPDX-License-Identifier: MIT use defmt::info; -use embassy_executor::Spawner; - -use crate::at::*; -use crate::error::Error; - -pub struct FwUpdateHandler; - -impl AtHandler for FwUpdateHandler { - fn handle(&self, spawner: Spawner, _at_command: AtCommand) -> Option { - info!("Firmware update requested - spawning BOOTSEL task"); - let _ = spawner.spawn(fwupdate_task()); - None - } -} #[embassy_executor::task] -async fn fwupdate_task() { +pub async fn fwupdate_task() { info!("Entering BOOTSEL mode for firmware update"); embassy_time::Timer::after_millis(100).await; embassy_rp::rom_data::reset_to_usb_boot(0, 0); diff --git a/firmware/src/at/handlers/mod.rs b/firmware/src/at/handlers/mod.rs index 7c4329a..239d6d4 100644 --- a/firmware/src/at/handlers/mod.rs +++ b/firmware/src/at/handlers/mod.rs @@ -7,8 +7,6 @@ mod setrgb_handler; pub use setrgb_handler::*; mod fwupdate_handler; pub use fwupdate_handler::*; -mod reset_handler; -pub use reset_handler::*; mod freq_handler; pub use freq_handler::*; mod operation_handler; diff --git a/firmware/src/at/handlers/operation_handler.rs b/firmware/src/at/handlers/operation_handler.rs index 564dcc4..cc4f6c8 100644 --- a/firmware/src/at/handlers/operation_handler.rs +++ b/firmware/src/at/handlers/operation_handler.rs @@ -1,52 +1,23 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use defmt::{error, info}; -use embassy_executor::Spawner; +use defmt::info; + +use hexa_tune_proto_embedded::command::OperationSub; use crate::AT_CH; use crate::DDS_CH; -use crate::at::{AtCommand, AtHandler}; use crate::channel::*; -use crate::error::Error; -use crate::hexa_config::*; - -pub struct OperationHandler; - -impl AtHandler for OperationHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option { - if at_command.is_query { - let _ = spawner.spawn(operation_status_task()); - None - } else { - info!("Handling OPERATION command"); - if is_dds_available() { - info!("Setting OPERATION: {:?}", at_command.id.as_str()); - let _ = spawner.spawn(operation_task(at_command)); - None - } else { - error!( - "DDS busy, cannot set OPERATION ({})", - at_command.id.as_str() - ); - Some(Error::DdsBusy) - } - } - } -} #[embassy_executor::task] -async fn operation_task(at_command: AtCommand) { - info!( - "Sending OPERATION command to DDS task: {}", - at_command.id.as_str() - ); - DDS_CH.send(Msg::Operation(at_command)).await; +pub async fn operation_task(id: u32, sub: OperationSub) { + info!("Sending OPERATION command to DDS task"); + DDS_CH.send(Msg::OperationCmd { id, sub }).await; info!("OPERATION command sent to DDS task"); } #[embassy_executor::task] -async fn operation_status_task() { +pub async fn operation_status_task() { info!("Requesting OPERATION status"); AT_CH.send(Msg::GetOperationStatus).await; } diff --git a/firmware/src/at/handlers/reset_handler.rs b/firmware/src/at/handlers/reset_handler.rs deleted file mode 100644 index f3785fc..0000000 --- a/firmware/src/at/handlers/reset_handler.rs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2025 hexaTune LLC -// SPDX-License-Identifier: MIT - -use cortex_m::peripheral::SCB; -use embassy_executor::Spawner; - -use crate::at::*; -use crate::error::Error; - -pub struct ResetHandler; - -impl AtHandler for ResetHandler { - fn handle(&self, _spawner: Spawner, _at_command: AtCommand) -> Option { - SCB::sys_reset(); - } -} diff --git a/firmware/src/at/handlers/setrgb_handler.rs b/firmware/src/at/handlers/setrgb_handler.rs index f6413f8..bb2f83a 100644 --- a/firmware/src/at/handlers/setrgb_handler.rs +++ b/firmware/src/at/handlers/setrgb_handler.rs @@ -1,23 +1,10 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use embassy_executor::Spawner; - use crate::RGB_CH; -use crate::at::{AtCommand, AtHandler}; use crate::channel::*; -use crate::error::Error; - -pub struct SetRgbHandler; - -impl AtHandler for SetRgbHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option { - let _ = spawner.spawn(setrgb_task(at_command)); - None - } -} #[embassy_executor::task] -async fn setrgb_task(at_command: AtCommand) { - RGB_CH.send(Msg::RgbWithValue(at_command)).await; +pub async fn setrgb_task(id: u32, r: u8, g: u8, b: u8) { + RGB_CH.send(Msg::RgbSet { id, r, g, b }).await; } diff --git a/firmware/src/at/handlers/version_handler.rs b/firmware/src/at/handlers/version_handler.rs index 4a2849b..b024aa3 100644 --- a/firmware/src/at/handlers/version_handler.rs +++ b/firmware/src/at/handlers/version_handler.rs @@ -1,44 +1,13 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use defmt::error; -use embassy_executor::Spawner; -use heapless::{String, Vec}; - use crate::AT_CH; -use crate::at::*; +use crate::at::encode_response; use crate::channel::*; -use crate::error::Error; use crate::hexa_config::CONF_VERSION; -pub struct VersionHandler; - -impl AtHandler for VersionHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option { - if at_command.is_query { - let _ = spawner.spawn(version_task()); - None - } else { - error!("VERSION command is a query only"); - Some(Error::NotAQuery) - } - } -} - #[embassy_executor::task] -async fn version_task() { - let mut name = String::<16>::new(); - name.push_str("VERSION").unwrap(); - - let mut params = Vec::, 8>::new(); - let version_param = String::<16>::try_from(CONF_VERSION).unwrap(); - params.push(version_param).ok(); - - let at_command = AtCommand { - id: get_empty_id(), - name, - params, - is_query: false, - }; - AT_CH.send(Msg::AtCmdOutput(at_command)).await; +pub async fn version_task() { + let line = encode_response(b"VERSION", 0, &[CONF_VERSION.as_bytes()]); + AT_CH.send(Msg::AtCmdResponse(line)).await; } diff --git a/firmware/src/at/mod.rs b/firmware/src/at/mod.rs index 902ff41..d44969d 100644 --- a/firmware/src/at/mod.rs +++ b/firmware/src/at/mod.rs @@ -1,12 +1,8 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -mod command; -pub use command::*; mod dispatcher; pub use dispatcher::*; -mod handler; -pub use handler::*; mod at_task; pub use at_task::*; mod handlers; diff --git a/firmware/src/channel/message_type.rs b/firmware/src/channel/message_type.rs index b4b42ff..2fca2a9 100644 --- a/firmware/src/channel/message_type.rs +++ b/firmware/src/channel/message_type.rs @@ -3,24 +3,22 @@ use heapless::String; -use crate::at::AtCommand; -use crate::error::Error; +use crate::error::FirmwareError; +use hexa_tune_proto_embedded::command::OperationSub; -pub type MsgId = String<16>; +pub type MsgId = u32; pub type MsgString = String<64>; -pub type IsDdsAvailable = bool; pub enum Msg { AtRxLine(MsgString), - AtCmdOutput(AtCommand), + AtCmdResponse(MsgString), Done(MsgId), - Completed(AtCommand), - Err(MsgId, Error), + Err(MsgId, FirmwareError), UsbTxLine(MsgString), - RgbWithValue(AtCommand), - FreqWithValue(AtCommand), - SetDdsAvailable(IsDdsAvailable), + RgbSet { id: u32, r: u8, g: u8, b: u8 }, + FreqSet { id: u32, freq: u32, time_ms: u32 }, + SetDdsAvailable(bool), SetOperationStatus(MsgString), GetOperationStatus, - Operation(AtCommand), + OperationCmd { id: u32, sub: OperationSub }, } diff --git a/firmware/src/dds/ad985x.rs b/firmware/src/dds/ad985x.rs index e1c4b98..90b798b 100644 --- a/firmware/src/dds/ad985x.rs +++ b/firmware/src/dds/ad985x.rs @@ -5,7 +5,7 @@ use defmt::*; use embassy_rp::gpio::Output; use embassy_time::{Duration, Timer}; -use crate::error::Error; +use crate::error::FirmwareError; const CTRL_PWRDOWN: u8 = 1 << 7; const CTRL_SLEEP: u8 = 1 << 6; @@ -76,7 +76,7 @@ impl Ad985x { ((num + den / 2) / den) as u32 } - pub async fn reset(&mut self) -> Option { + pub async fn reset(&mut self) -> Option { self.rst.set_high(); Timer::after(Duration::from_micros(5)).await; self.rst.set_low(); @@ -93,23 +93,23 @@ impl Ad985x { None } - /*pub async fn sleep(&mut self) -> Option { + /*pub async fn sleep(&mut self) -> Option { self.write_ftw_ctrl(0, self.ctrl_base | CTRL_SLEEP).await; None }*/ - pub async fn down(&mut self) -> Option { + pub async fn down(&mut self) -> Option { self.write_ftw_ctrl(0, self.ctrl_base | CTRL_PWRDOWN).await; None } - pub async fn up(&mut self) -> Option { + pub async fn up(&mut self) -> Option { self.write_ftw_ctrl(0, self.ctrl_base & !(CTRL_SLEEP | CTRL_PWRDOWN)) .await; None } - pub async fn set_freq(&mut self, freq_hz: u32, dwell_ms: u32) -> Option { + pub async fn set_freq(&mut self, freq_hz: u32, dwell_ms: u32) -> Option { if let Some(e) = self.down().await { return Some(e); } @@ -135,7 +135,7 @@ impl Ad985x { None } - async fn set_freq_immediate(&mut self, freq_hz: u32) -> Option { + async fn set_freq_immediate(&mut self, freq_hz: u32) -> Option { let ftw = self.hz_to_ftw(freq_hz); self.write_ftw_ctrl(ftw, self.ctrl_base | CTRL_PHASE0).await; None diff --git a/firmware/src/dds/dds_task.rs b/firmware/src/dds/dds_task.rs index a2886d2..7134e5c 100644 --- a/firmware/src/dds/dds_task.rs +++ b/firmware/src/dds/dds_task.rs @@ -6,13 +6,14 @@ use defmt::*; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex as Cs; use embassy_sync::mutex::Mutex; -use heapless::{String, Vec}; use {defmt_rtt as _, panic_probe as _}; -use crate::at::*; +use hexa_tune_proto_embedded::command::OperationSub; + +use crate::at::{encode_error_response, encode_response, u32_to_ascii_buf}; use crate::channel::*; use crate::dds::*; -use crate::error::Error; +use crate::error::FirmwareError; use crate::{AT_CH, DDS_CH}; static OPERATION: Mutex> = Mutex::new(RefCell::new(Operation::new())); @@ -22,136 +23,86 @@ pub async fn dds_task(mut ad985x: Ad985x) { info!("Starting DDS task"); loop { match DDS_CH.receive().await { - Msg::Operation(at_command) => { - info!( - "Received OPERATION command in DDS task: {}", - at_command.id.as_str() - ); - info!("Parsing OPERATION command parameters"); - if at_command.params.len() != 1 { - error!( - "OPERATION command requires 1 parameters, got {}", - at_command.params.len() - ); - AT_CH.send(Msg::Err(at_command.id, Error::ParamCount)).await; - continue; - } - info!("Parameters parsed successfully"); - info!("Extracting command_type values"); - let command_type = match at_command.params[0].parse::>() { - Ok(v) => v, - Err(_) => { - error!("Invalid command_type value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - continue; - } - }; + Msg::OperationCmd { id, sub } => { + info!("Received OPERATION command in DDS task: {}", id); - match command_type.as_str() { - "PREPARE" => { + match sub { + OperationSub::Prepare => { info!("Preparing DDS operation"); - // Reset the operation by replacing the inner value let operation = OPERATION.lock().await; *operation.borrow_mut() = Operation::new(); - { let mut guard = operation.borrow_mut(); - guard.set_id(at_command.id.clone()); + guard.set_id(id); } drop(operation); info!("DDS operation prepared"); - AT_CH.send(Msg::Completed(at_command.clone())).await; + let completed = + encode_response(b"OPERATION", id, &[b"PREPARE", b"COMPLETED"]); + AT_CH.send(Msg::AtCmdResponse(completed.clone())).await; info!("Completed sent for PREPARE command"); - let compiled_status = compile_at_completed(at_command.clone()); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; + AT_CH.send(Msg::SetOperationStatus(completed)).await; } - "GENERATE" => { + OperationSub::Generate => { info!("Starting DDS operation"); - let mut result: Option = None; + let mut result: Option = None; info!("Setting Device Available to false"); AT_CH.send(Msg::SetDdsAvailable(false)).await; info!("Set Device Available to false"); - let compiled_status = compile_at_completed(at_command.clone()); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; - - let steps = { + let gen_completed = + encode_response(b"OPERATION", id, &[b"GENERATE", b"COMPLETED"]); + AT_CH + .send(Msg::SetOperationStatus(gen_completed.clone())) + .await; + + // Clone steps out of the mutex + let step_count; + let mut step_ids = [0u32; 64]; + let mut step_freqs = [0u32; 64]; + let mut step_times = [0u32; 64]; + { let operation = OPERATION.lock().await; let guard = operation.borrow(); - guard.get_steps().clone() - }; - - let mut at_command_status = at_command.clone(); - at_command_status.params.clear(); - - for at_command_step in steps.iter() { - let mut params = Vec::, 8>::new(); - let generating_param = String::<16>::try_from("GENERATING").unwrap(); - params.push(generating_param).ok(); - - at_command_status.params = params; - - info!("Parsing FREQ command parameters"); - if at_command_step.params.len() != 2 { - error!( - "FREQ command requires 2 parameters, got {}", - at_command_step.params.len() - ); - result = Some(Error::ParamCount); - break; + let steps = guard.get_steps(); + step_count = steps.len(); + for (i, s) in steps.iter().enumerate() { + step_ids[i] = s.id; + step_freqs[i] = s.freq; + step_times[i] = s.time_ms; } - info!("Parameters parsed successfully"); - - info!("Extracting frequency and time_ms values"); - let freq = match at_command_step.params[0].parse::() { - Ok(v) => v, - Err(_) => { - error!( - "Invalid frequency value: {}", - at_command_step.id.as_str() - ); - result = Some(Error::ParamValue); - break; - } - }; - info!("Frequency value extracted: {}", freq); - let time_ms = match at_command_step.params[1].parse::() { - Ok(v) => v, - Err(_) => { - error!( - "Invalid time_ms value: {}", - at_command_step.id.as_str() - ); - result = Some(Error::ParamValue); - break; - } - }; - info!("time_ms value extracted: {}", time_ms); - - info!( - "Setting FREQ to ({}) over {} ms", - at_command_step.id.as_str(), - time_ms - ); + } - let step_id_param = - String::<16>::try_from(at_command_step.id.as_str()).unwrap(); - at_command_status.params.push(step_id_param).ok(); - let compiled_status = compile_at_completed(at_command_status.clone()); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; + for i in 0..step_count { + let step_id = step_ids[i]; + let freq = step_freqs[i]; + let time_ms = step_times[i]; + + // Build status: AT+OPERATION=id#GENERATING#step_id#COMPLETED + let mut sid_buf = [0u8; 10]; + let sid_len = u32_to_ascii_buf(step_id, &mut sid_buf); + let status = encode_response( + b"OPERATION", + id, + &[b"GENERATING", &sid_buf[..sid_len], b"COMPLETED"], + ); + AT_CH.send(Msg::SetOperationStatus(status)).await; - info!("Starting frequency set..."); + info!("Setting FREQ to {} over {} ms", freq, time_ms); let err = ad985x.set_freq(freq, time_ms).await; info!("Frequency set complete."); if let Some(err) = err { - error!("Error setting FREQ: {:?}", err.code()); - result = Some(err); + error!("Error setting FREQ"); + result = Some(FirmwareError::Hexa( + hexa_tune_proto_embedded::HexaError::InvalidParam, + )); + let _ = err; break; } else { info!("FREQ set successfully"); @@ -163,46 +114,47 @@ pub async fn dds_task(mut ad985x: Ad985x) { info!("Set Device Available to true"); if let Some(err) = result { - error!("DDS operation failed: {:?}", err.code()); - AT_CH - .send(Msg::Err(at_command.id.clone(), err.clone())) - .await; + error!("DDS operation failed"); + AT_CH.send(Msg::Err(id, err)).await; - let compiled_status = compile_at_error(at_command.id.clone(), err); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; + let error_status = encode_error_response(id, &err); + AT_CH.send(Msg::SetOperationStatus(error_status)).await; } else { - let compiled_status = compile_at_completed(at_command.clone()); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; + AT_CH.send(Msg::SetOperationStatus(gen_completed)).await; } } - _ => { - error!("Unknown command_type value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - continue; - } } } - Msg::FreqWithValue(at_command) => { - info!( - "Received FREQ command in DDS task: {}", - at_command.id.as_str() - ); + Msg::FreqSet { id, freq, time_ms } => { + info!("Received FREQ command in DDS task: {}", id); - info!("Adding FREQ command to operation"); + info!("Adding FREQ step to operation"); let operation = OPERATION.lock().await; + let step = FreqStep { id, freq, time_ms }; let add_result = { let mut guard = operation.borrow_mut(); - guard.add_step(at_command.clone()) + guard.add_step(step) }; drop(operation); if let Err(e) = add_result { error!("Failed to add step: operation is full"); - AT_CH.send(Msg::Err(at_command.id, e)).await; + AT_CH.send(Msg::Err(id, e)).await; } else { - info!("FREQ command added to operation"); - AT_CH.send(Msg::Completed(at_command)).await; + info!("FREQ step added to operation"); + + // Build completed response: AT+FREQ=id#freq#time_ms#COMPLETED + let mut freq_buf = [0u8; 10]; + let freq_len = u32_to_ascii_buf(freq, &mut freq_buf); + let mut time_buf = [0u8; 10]; + let time_len = u32_to_ascii_buf(time_ms, &mut time_buf); + let completed = encode_response( + b"FREQ", + id, + &[&freq_buf[..freq_len], &time_buf[..time_len], b"COMPLETED"], + ); + AT_CH.send(Msg::AtCmdResponse(completed)).await; info!("Completed sent for FREQ command"); } } diff --git a/firmware/src/dds/dds_type.rs b/firmware/src/dds/dds_type.rs index b68532a..d247a4b 100644 --- a/firmware/src/dds/dds_type.rs +++ b/firmware/src/dds/dds_type.rs @@ -1,40 +1,45 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use heapless::{String, Vec}; +use heapless::Vec; -use crate::at::AtCommand; -use crate::error::Error; +use crate::error::FirmwareError; -pub type OperationId = String<16>; +pub struct FreqStep { + pub id: u32, + pub freq: u32, + pub time_ms: u32, +} pub struct Operation { - id: OperationId, - steps: Vec, + id: u32, + steps: Vec, } impl Operation { pub const fn new() -> Self { Self { - id: String::new(), + id: 0, steps: Vec::new(), } } #[allow(dead_code)] - pub fn get_id(&self) -> &OperationId { - &self.id + pub fn get_id(&self) -> u32 { + self.id } - pub fn set_id(&mut self, id: OperationId) { + pub fn set_id(&mut self, id: u32) { self.id = id; } - pub fn get_steps(&self) -> &heapless::Vec { + pub fn get_steps(&self) -> &Vec { &self.steps } - pub fn add_step(&mut self, step: AtCommand) -> Result<(), Error> { - self.steps.push(step).map_err(|_| Error::OperationStepsFull) + pub fn add_step(&mut self, step: FreqStep) -> Result<(), FirmwareError> { + self.steps + .push(step) + .map_err(|_| FirmwareError::OperationStepsFull) } } diff --git a/firmware/src/error/mod.rs b/firmware/src/error/mod.rs index 40c79a3..46e700f 100644 --- a/firmware/src/error/mod.rs +++ b/firmware/src/error/mod.rs @@ -1,34 +1,48 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -#[derive(Debug, Clone)] -pub enum Error { - InvalidCommand, // "Not an AT command" - DdsBusy, // "DDS is busy" - InvalidUtf8, // "Invalid UTF-8 in param" - InvalidSysEx, // "Invalid SysEx" - InvalidDataLength, // "Invalid data length" - ParamCount, // "Param count" - ParamValue, // "Param value" - NotAQuery, // "Not a query" - UnknownCommand, // "Unknown command" - OperationStepsFull, // "Operation steps full" +pub use hexa_tune_proto::ProtoError; +pub use hexa_tune_proto_embedded::HexaError; + +#[derive(Debug, Clone, Copy)] +pub enum FirmwareError { + Proto(ProtoError), + Hexa(HexaError), + OperationStepsFull, +} + +impl From for FirmwareError { + fn from(e: ProtoError) -> Self { + Self::Proto(e) + } +} + +impl From for FirmwareError { + fn from(e: HexaError) -> Self { + Self::Hexa(e) + } } -impl Error { - pub fn code(&self) -> &'static str { +impl FirmwareError { + /// Returns a numeric error code (u8) for wire-format encoding. + pub fn error_code(&self) -> u8 { match self { - // Command errors - Error::InvalidCommand => "E001001", - Error::DdsBusy => "E001002", - Error::InvalidUtf8 => "E001003", - Error::InvalidSysEx => "E001004", - Error::InvalidDataLength => "E001005", - Error::ParamCount => "E001006", - Error::ParamValue => "E001007", - Error::NotAQuery => "E001008", - Error::UnknownCommand => "E001009", - Error::OperationStepsFull => "E001010", + FirmwareError::Proto(e) => e.code(), + FirmwareError::Hexa(e) => match e { + HexaError::Proto(pe) => pe.code(), + HexaError::UnknownCommand => 11, + HexaError::DdsBusy => 12, + HexaError::NotAQuery => 13, + HexaError::MissingParam => 14, + HexaError::InvalidParam => 15, + }, + FirmwareError::OperationStepsFull => 20, } } } + +impl defmt::Format for FirmwareError { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "FirmwareError({})", self.error_code()); + } +} diff --git a/firmware/src/main.rs b/firmware/src/main.rs index 6f425bb..5dc62cd 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -16,7 +16,6 @@ mod dds; mod error; mod hexa_config; mod rgb; -mod sysex; mod usb; use crate::channel::*; @@ -51,8 +50,6 @@ async fn main(spawner: embassy_executor::Spawner) { //AT module info!("Initializing AT command dispatcher"); - static DISPATCHER: static_cell::StaticCell = static_cell::StaticCell::new(); - let dispatcher = DISPATCHER.init(at::AtDispatcher::new()); //Led module info!("Initializing RGB LED"); @@ -82,7 +79,7 @@ async fn main(spawner: embassy_executor::Spawner) { //Dummy Led let led = embassy_rp::gpio::Output::new(p.PIN_25, embassy_rp::gpio::Level::Low); - spawner.spawn(at::at_task(dispatcher, spawner)).unwrap(); + spawner.spawn(at::at_task(spawner)).unwrap(); spawner.spawn(rgb::rgb_task(rgb_led)).unwrap(); spawner.spawn(usb::dev_task(device)).unwrap(); spawner.spawn(usb::usb_io_task(midi_mutex)).unwrap(); diff --git a/firmware/src/rgb/rgb_task.rs b/firmware/src/rgb/rgb_task.rs index b3bce79..2d2c48f 100644 --- a/firmware/src/rgb/rgb_task.rs +++ b/firmware/src/rgb/rgb_task.rs @@ -7,53 +7,18 @@ use {defmt_rtt as _, panic_probe as _}; use crate::AT_CH; use crate::RGB_CH; use crate::channel::*; -use crate::error::Error; use crate::rgb::RgbLed; #[embassy_executor::task] pub async fn rgb_task(mut rgb_led: RgbLed) { info!("Starting RGB task"); loop { - if let Msg::RgbWithValue(at_command) = RGB_CH.receive().await { - if at_command.params.len() != 3 { - error!( - "SETRGB command {} requires 3 parameters, got {}", - at_command.id.as_str(), - at_command.params.len() - ); - AT_CH.send(Msg::Err(at_command.id, Error::ParamCount)).await; - return; - } - // check params are u8 - let r = match at_command.params[0].parse::() { - Ok(v) => v, - Err(_) => { - error!("Invalid R value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - return; - } - }; - let g = match at_command.params[1].parse::() { - Ok(v) => v, - Err(_) => { - error!("Invalid G value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - return; - } - }; - let b = match at_command.params[2].parse::() { - Ok(v) => v, - Err(_) => { - error!("Invalid B value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - return; - } - }; + if let Msg::RgbSet { id, r, g, b } = RGB_CH.receive().await { info!("Setting RGB to ({}, {}, {})", r, g, b); rgb_led.set_rgb(r, g, b).await; info!("RGB set"); info!("Sending DONE from RGB task"); - AT_CH.send(Msg::Done(at_command.id)).await; + AT_CH.send(Msg::Done(id)).await; } } } diff --git a/firmware/src/sysex/mod.rs b/firmware/src/sysex/mod.rs deleted file mode 100644 index 11eb6a3..0000000 --- a/firmware/src/sysex/mod.rs +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-FileCopyrightText: 2025 hexaTune LLC -// SPDX-License-Identifier: MIT - -use heapless::Vec; - -pub fn build_sysex(payload: &str) -> Option> { - let mut out: Vec = Vec::new(); - - // SysEx start - out.push(0xF0).ok()?; - - // Payload - for b in payload.as_bytes() { - out.push(*b).ok()?; - } - - // SysEx end - out.push(0xF7).ok()?; - - Some(out) -} - -pub fn sysex_to_usb_midi_packets(sysex: &[u8]) -> Vec<[u8; 4], M> { - let mut out: Vec<[u8; 4], M> = Vec::new(); - - let mut i = 0usize; - while i < sysex.len() { - let rem = sysex.len() - i; - if rem >= 3 { - if rem == 3 && sysex[sysex.len() - 1] == 0xF7 { - out.push([0x07, sysex[i], sysex[i + 1], sysex[i + 2]]).ok(); // end with 3 - i += 3; - } else { - out.push([0x04, sysex[i], sysex[i + 1], sysex[i + 2]]).ok(); // start/continue - i += 3; - } - } else if rem == 2 { - out.push([0x06, sysex[i], sysex[i + 1], 0x00]).ok(); // end with 2 - i += 2; - } else { - out.push([0x05, sysex[i], 0x00, 0x00]).ok(); // end with 1 - i += 1; - } - } - out -} - -pub fn extract_sysex_payload(data: &[u8]) -> Option> { - let mut out: Vec = Vec::new(); - - for chunk in data.chunks_exact(4) { - let cin = chunk[0] & 0x0F; - let b1 = chunk[1]; - let b2 = chunk[2]; - let b3 = chunk[3]; - - match cin { - 0x4 => { - // SysEx continue/start (3 byte data) - for &b in &[b1, b2, b3] { - if b != 0 { - out.push(b).ok(); - } - } - } - 0x5 => { - if b1 != 0 { - out.push(b1).ok(); - } - } // end with 1 - 0x6 => { - for &b in &[b1, b2] { - if b != 0 { - out.push(b).ok(); - } - } - } // end with 2 - 0x7 => { - for &b in &[b1, b2, b3] { - if b != 0 { - out.push(b).ok(); - } - } - } // end with 3 - _ => {} - } - } - - if out.first().copied() == Some(0xF0) && out.last().copied() == Some(0xF7) { - let mut payload: Vec = Vec::new(); - payload.extend_from_slice(&out[1..out.len() - 1]).ok()?; - Some(payload) - } else { - None - } -} diff --git a/firmware/src/usb/usb_task.rs b/firmware/src/usb/usb_task.rs index 32bd300..30304f3 100644 --- a/firmware/src/usb/usb_task.rs +++ b/firmware/src/usb/usb_task.rs @@ -6,12 +6,13 @@ use embassy_futures::select::{Either, select}; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex as Cs; use embassy_sync::mutex::Mutex; +use hexa_tune_proto::sysex; +use hexa_tune_proto::usb_midi; + use crate::AT_CH; use crate::USB_CH; -use crate::at::get_empty_id; use crate::channel::*; -use crate::error::Error; -use crate::sysex::{build_sysex, extract_sysex_payload, sysex_to_usb_midi_packets}; +use crate::error::FirmwareError; use crate::usb::{MyMidiClass, MyUsbDevice}; #[embassy_executor::task] @@ -43,48 +44,108 @@ pub async fn usb_io_task(midi: &'static Mutex>) { let data = &buf[..n]; info!("Received MIDI packet: {:?}", data); - if let Some(payload) = extract_sysex_payload(data) { - if let Ok(input) = core::str::from_utf8(&payload) { - match heapless::String::<64>::try_from(input) { - Ok(line) => { - AT_CH.send(Msg::AtRxLine(line)).await; - } - Err(_) => { - error!("Error Code: {}", Error::InvalidDataLength.code()); - AT_CH - .send(Msg::Err(get_empty_id(), Error::InvalidDataLength)) - .await; - } + let num_packets = n / 4; + if num_packets == 0 { + continue; + } + + // Collect 4-byte packets from raw data + let mut packets = [[0u8; 4]; 16]; + for i in 0..num_packets.min(16) { + packets[i] = [ + data[i * 4], + data[i * 4 + 1], + data[i * 4 + 2], + data[i * 4 + 3], + ]; + } + + // Depacketize USB MIDI → SysEx + let mut sysex_buf = [0u8; 128]; + let sysex_len = match usb_midi::depacketize( + &packets[..num_packets.min(16)], + &mut sysex_buf, + ) { + Ok(len) => len, + Err(e) => { + error!("USB MIDI depacketize error"); + AT_CH + .send(Msg::Err(0, FirmwareError::Proto(e))) + .await; + continue; + } + }; + + // Unframe SysEx → payload + let payload = match sysex::unframe(&sysex_buf[..sysex_len]) { + Ok(p) => p, + Err(e) => { + error!("SysEx unframe error"); + AT_CH + .send(Msg::Err(0, FirmwareError::Proto(e))) + .await; + continue; + } + }; + + // Convert to UTF-8 string and send to AT channel + match core::str::from_utf8(payload) { + Ok(input) => match MsgString::try_from(input) { + Ok(line) => { + AT_CH.send(Msg::AtRxLine(line)).await; + } + Err(_) => { + error!("AT payload too long for buffer"); + AT_CH + .send(Msg::Err( + 0, + FirmwareError::Proto( + hexa_tune_proto::ProtoError::BufferTooSmall, + ), + )) + .await; } - } else { - error!("Error Code: {}", Error::InvalidUtf8.code()); + }, + Err(_) => { + error!("Invalid UTF-8 in payload"); AT_CH - .send(Msg::Err(get_empty_id(), Error::InvalidUtf8)) + .send(Msg::Err( + 0, + FirmwareError::Proto(hexa_tune_proto::ProtoError::InvalidUtf8), + )) .await; } - } else { - error!("Error Code: {}", Error::InvalidSysEx.code()); - /*AT_CH - .send(Msg::Err(get_empty_id(), Error::InvalidSysEx)) - .await;*/ } } Either::Second(msg) => match msg { Msg::UsbTxLine(line) => { - if let Some(sysex) = build_sysex::<64>(&line) { - let packets = sysex_to_usb_midi_packets::<64>(&sysex); - info!("Sending {} MIDI packets", packets.len()); - - let mut m = midi.lock().await; - for pkt in packets.iter() { - //info!("Sending MIDI packet: {:?}", pkt); - if let Err(e) = m.write_packet(pkt).await { - error!("USB write error: {:?}", e); + let line_bytes = line.as_bytes(); + let mut sysex_buf = [0u8; 128]; + match sysex::frame(line_bytes, &mut sysex_buf) { + Ok(sysex_len) => { + let mut packets = [[0u8; 4]; 32]; + match usb_midi::packetize( + &sysex_buf[..sysex_len], + &mut packets, + ) { + Ok(np) => { + info!("Sending {} MIDI packets", np); + let mut m = midi.lock().await; + for pkt in packets[..np].iter() { + if let Err(e) = m.write_packet(pkt).await { + error!("USB write error: {:?}", e); + } + } + } + Err(_) => { + error!("USB MIDI packetize error"); + } } } - } else { - error!("UsbTxLine too long to fit into SysEx"); + Err(_) => { + error!("UsbTxLine too long to fit into SysEx"); + } } } _ => {