Files
firmware/src/host/framing.rs
T
2026-04-16 13:25:19 -06:00

124 lines
4.0 KiB
Rust

//! Shared COBS protocol helpers for USB and UART transports.
//!
//! Both `usb_task` and `uart_task` use these helpers for frame
//! accumulation, response encoding, and command routing.
use defmt::warn;
use crate::channel::{CommandChannel, DisplayCommand, DisplayCommandChannel, ResponseChannel};
use crate::protocol::{Command, ErrorCode, Response};
/// Maximum COBS frame size (bytes).
pub const MAX_FRAME: usize = 512;
// Worst case: tag(1) + rssi(2) + snr(2) + len(2) + payload(256) = 263 bytes.
// COBS adds ceil(263/254)+1 = 3 bytes. 266 < 512. Plenty of room.
const _: () = assert!(
MAX_FRAME >= crate::protocol::MAX_PAYLOAD + 64,
"MAX_FRAME too small for max payload + COBS overhead"
);
/// Decodes COBS-framed commands from a byte stream.
pub struct CobsDecoder {
buf: [u8; MAX_FRAME],
len: usize,
}
impl CobsDecoder {
pub const fn new() -> Self {
Self {
buf: [0u8; MAX_FRAME],
len: 0,
}
}
/// Reset the accumulator (discard partial frame).
pub fn reset(&mut self) {
self.len = 0;
}
/// Feed a chunk of bytes, calling `on_command` for each decoded command.
pub fn feed(&mut self, data: &[u8], mut on_command: impl FnMut(Command)) {
for &byte in data {
if byte == 0x00 {
// End of COBS frame — decode and dispatch
if self.len > 0 {
let mut decode_buf = [0u8; MAX_FRAME];
if let Some(decoded_len) = ucobs::decode(&self.buf[..self.len], &mut decode_buf)
{
if let Some(cmd) = Command::from_bytes(&decode_buf[..decoded_len]) {
on_command(cmd);
}
}
}
self.len = 0;
} else if self.len < MAX_FRAME {
self.buf[self.len] = byte;
self.len += 1;
} else {
// Frame too large — discard
self.len = 0;
}
}
}
}
/// COBS-encode a response into `encode_buf` with trailing 0x00 sentinel.
///
/// Returns the slice to send (encoded frame + sentinel), or `None` on overflow.
pub fn cobs_encode_response<'a>(
response: Response,
write_buf: &mut [u8; MAX_FRAME],
encode_buf: &'a mut [u8; MAX_FRAME],
) -> Option<&'a [u8]> {
let raw_len = response.write_to(write_buf);
let encoded_len = ucobs::encode(&write_buf[..raw_len], encode_buf).unwrap_or(0);
if encoded_len < encode_buf.len() {
encode_buf[encoded_len] = 0x00;
Some(&encode_buf[..encoded_len + 1])
} else {
warn!("COBS encode buffer overflow");
None
}
}
/// Route a parsed command to the appropriate handler.
///
/// Display and MAC commands are handled locally (response sent immediately).
/// All other commands are forwarded to the radio task (response sent later).
/// Both paths feed the same `ResponseChannel`, so the one-outstanding-command
/// rule in PROTOCOL.md is required to keep solicited responses ordered.
pub async fn route_command(
cmd: Command,
commands: &CommandChannel,
responses: &ResponseChannel,
display_commands: &DisplayCommandChannel,
has_display: bool,
mac: [u8; 6],
) {
match cmd {
Command::DisplayOn => {
if has_display {
display_commands.send(DisplayCommand::On).await;
responses.send(Response::Ok).await;
} else {
responses.send(Response::Error(ErrorCode::NoDisplay)).await;
}
}
Command::DisplayOff => {
if has_display {
display_commands.send(DisplayCommand::Off).await;
responses.send(Response::Ok).await;
} else {
responses.send(Response::Error(ErrorCode::NoDisplay)).await;
}
}
Command::GetMac => {
responses.send(Response::MacAddress(mac)).await;
}
other => {
commands.send(other).await;
}
}
}