refactor: unified host/ communication module, cfg-free run()
- Created host/ module with internal cfg dispatch (USB vs UART) - Moved usb/task.rs → host/usb.rs, uart/task.rs → host/uart.rs - Moved protocol_io.rs → host/framing.rs - Renamed usb_task/uart_task to host_task (uniform name) - Removed all host-comm cfg from main.rs - The run() function now has ZERO board-specific conditional compilation - Deleted old usb/, uart/, protocol_io.rs modules
This commit is contained in:
1 parent
fb334118fd
commit
2c852b9f45
7 files changed
+44
-79
No files matched your search
@@ -0,0 +1,124 @@
|
||||
//! 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Host communication: unified USB/UART transport.
|
||||
//!
|
||||
//! Exports a single `host_task` regardless of transport.
|
||||
//! The cfg dispatch is contained entirely within this module.
|
||||
|
||||
pub mod framing;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "heltec_v3")] {
|
||||
mod uart;
|
||||
pub use uart::host_task;
|
||||
} else {
|
||||
mod usb;
|
||||
pub use usb::host_task;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//! UART host task: COBS-framed fixed-size LE command/response protocol.
|
||||
//!
|
||||
//! Used for boards with USB-UART bridge chips (e.g. CP2102 on Heltec V3).
|
||||
|
||||
use defmt::warn;
|
||||
use embassy_executor::task;
|
||||
use embedded_io_async::Write;
|
||||
|
||||
use crate::channel::{CommandChannel, DisplayCommandChannel, ResponseChannel};
|
||||
use super::framing::{self, CobsDecoder, MAX_FRAME};
|
||||
|
||||
#[task]
|
||||
pub async fn host_task(
|
||||
parts: crate::board::UartParts,
|
||||
commands: &'static CommandChannel,
|
||||
responses: &'static ResponseChannel,
|
||||
display_commands: &'static DisplayCommandChannel,
|
||||
has_display: bool,
|
||||
mac: [u8; 6],
|
||||
) {
|
||||
let (mut rx, mut tx) = parts.driver.split();
|
||||
|
||||
let mut read_buf = [0u8; 64];
|
||||
let mut write_buf = [0u8; MAX_FRAME];
|
||||
let mut cobs_encode_buf = [0u8; MAX_FRAME];
|
||||
let mut decoder = CobsDecoder::new();
|
||||
|
||||
loop {
|
||||
use embassy_futures::select::{select, Either};
|
||||
|
||||
match select(
|
||||
embedded_io_async::Read::read(&mut rx, &mut read_buf),
|
||||
responses.receive(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either::First(result) => {
|
||||
let n = match result {
|
||||
Ok(0) => continue,
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
decoder.reset();
|
||||
embassy_time::Timer::after_millis(100).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut cmds = heapless::Vec::<_, 4>::new();
|
||||
decoder.feed(&read_buf[..n], |cmd| {
|
||||
let _ = cmds.push(cmd);
|
||||
});
|
||||
for cmd in cmds {
|
||||
framing::route_command(
|
||||
cmd, commands, responses, display_commands, has_display, mac,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Either::Second(response) => {
|
||||
if let Some(frame) =
|
||||
framing::cobs_encode_response(response, &mut write_buf, &mut cobs_encode_buf)
|
||||
{
|
||||
if tx.write_all(frame).await.is_err() {
|
||||
warn!("UART write failed, response dropped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
//! USB CDC-ACM host task: COBS-framed fixed-size LE command/response protocol.
|
||||
|
||||
use defmt::warn;
|
||||
use embassy_executor::task;
|
||||
use embassy_futures::join::join;
|
||||
use embassy_usb::class::cdc_acm::{CdcAcmClass, Receiver, Sender, State};
|
||||
use embassy_usb::Builder;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
use crate::channel::{CommandChannel, DisplayCommand, DisplayCommandChannel, ResponseChannel};
|
||||
use super::framing::{self, CobsDecoder, MAX_FRAME};
|
||||
|
||||
const MAX_PACKET: usize = 64;
|
||||
|
||||
#[task]
|
||||
pub async fn host_task(
|
||||
parts: crate::board::UsbParts,
|
||||
commands: &'static CommandChannel,
|
||||
responses: &'static ResponseChannel,
|
||||
display_commands: &'static DisplayCommandChannel,
|
||||
has_display: bool,
|
||||
mac: [u8; 6],
|
||||
) {
|
||||
let mut config = embassy_usb::Config::new(0x1209, 0x5741);
|
||||
config.manufacturer = Some("DongLoRa");
|
||||
config.product = Some("DongLoRa LoRa Radio");
|
||||
config.device_class = 0xEF;
|
||||
config.device_sub_class = 0x02;
|
||||
config.device_protocol = 0x01;
|
||||
config.composite_with_iads = true;
|
||||
|
||||
static SERIAL_BUF: StaticCell<[u8; 12]> = StaticCell::new();
|
||||
let serial_buf = SERIAL_BUF.init([0u8; 12]);
|
||||
const HEX: &[u8; 16] = b"0123456789ABCDEF";
|
||||
for (i, &byte) in mac.iter().enumerate() {
|
||||
serial_buf[i * 2] = HEX[(byte >> 4) as usize];
|
||||
serial_buf[i * 2 + 1] = HEX[(byte & 0x0F) as usize];
|
||||
}
|
||||
config.serial_number = Some(core::str::from_utf8(serial_buf).expect("MAC hex is valid UTF-8"));
|
||||
|
||||
static DESC_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||
static CONF_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||
static BOS_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||
static CTRL_BUF: StaticCell<[u8; 64]> = StaticCell::new();
|
||||
static CDC_STATE: StaticCell<State> = StaticCell::new();
|
||||
|
||||
let desc_buf = DESC_BUF.init([0; 256]);
|
||||
let conf_buf = CONF_BUF.init([0; 256]);
|
||||
let bos_buf = BOS_BUF.init([0; 256]);
|
||||
let ctrl_buf = CTRL_BUF.init([0; 64]);
|
||||
let cdc_state = CDC_STATE.init(State::new());
|
||||
|
||||
let mut builder = Builder::new(parts.driver, config, desc_buf, conf_buf, bos_buf, ctrl_buf);
|
||||
|
||||
let class = CdcAcmClass::new(&mut builder, cdc_state, MAX_PACKET as u16);
|
||||
let mut usb_dev = builder.build();
|
||||
let (sender, receiver) = class.split();
|
||||
|
||||
join(
|
||||
usb_dev.run(),
|
||||
protocol_loop(sender, receiver, commands, responses, display_commands, has_display, mac),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
|
||||
mut sender: Sender<'d, D>,
|
||||
mut receiver: Receiver<'d, D>,
|
||||
commands: &'static CommandChannel,
|
||||
responses: &'static ResponseChannel,
|
||||
display_commands: &'static DisplayCommandChannel,
|
||||
has_display: bool,
|
||||
mac: [u8; 6],
|
||||
) {
|
||||
use embassy_futures::select::{select3, Either3};
|
||||
|
||||
let mut read_buf = [0u8; MAX_PACKET];
|
||||
let mut write_buf = [0u8; MAX_FRAME];
|
||||
let mut cobs_encode_buf = [0u8; MAX_FRAME];
|
||||
let mut decoder = CobsDecoder::new();
|
||||
let mut was_connected = false;
|
||||
|
||||
loop {
|
||||
match select3(
|
||||
receiver.read_packet(&mut read_buf),
|
||||
responses.receive(),
|
||||
embassy_time::Timer::after_millis(250),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either3::First(result) => {
|
||||
let n = match result {
|
||||
Ok(0) => continue,
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
decoder.reset();
|
||||
embassy_time::Timer::after_millis(100).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut cmds = heapless::Vec::<_, 4>::new();
|
||||
decoder.feed(&read_buf[..n], |cmd| {
|
||||
let _ = cmds.push(cmd);
|
||||
});
|
||||
for cmd in cmds {
|
||||
framing::route_command(
|
||||
cmd, commands, responses, display_commands, has_display, mac,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Either3::Second(response) => {
|
||||
if let Some(frame) =
|
||||
framing::cobs_encode_response(response, &mut write_buf, &mut cobs_encode_buf)
|
||||
{
|
||||
for chunk in frame.chunks(MAX_PACKET) {
|
||||
if sender.write_packet(chunk).await.is_err() {
|
||||
warn!("USB write failed, response dropped");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Either3::Third(()) => {}
|
||||
}
|
||||
|
||||
let connected = receiver.dtr();
|
||||
if was_connected && !connected && has_display {
|
||||
display_commands.send(DisplayCommand::Reset).await;
|
||||
}
|
||||
if !was_connected && connected && has_display {
|
||||
display_commands.send(DisplayCommand::On).await;
|
||||
}
|
||||
was_connected = connected;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user