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:
swaits committed 2026-04-07 09:40:59 -06:00
1 parent 82701c183b
commit c7f145c787
7 files changed
+36 -71

No files matched your search

File renamed without changes.
+16
View File
@@ -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;
}
}
@@ -1,18 +1,16 @@
//! UART task: COBS-framed fixed-size LE command/response protocol. //! UART host task: COBS-framed fixed-size LE command/response protocol.
//! //!
//! Reads COBS frames from UART, decodes them, parses commands, routes them //! Used for boards with USB-UART bridge chips (e.g. CP2102 on Heltec V3).
//! to the radio or display task, and sends COBS-framed responses back.
//! Used for boards with USB-UART bridge chips (e.g. CP2102 on some Heltec V3).
use defmt::warn; use defmt::warn;
use embassy_executor::task; use embassy_executor::task;
use embedded_io_async::Write; use embedded_io_async::Write;
use crate::channel::{CommandChannel, DisplayCommandChannel, ResponseChannel}; use crate::channel::{CommandChannel, DisplayCommandChannel, ResponseChannel};
use crate::protocol_io::{self, CobsDecoder, MAX_FRAME}; use super::framing::{self, CobsDecoder, MAX_FRAME};
#[task] #[task]
pub async fn uart_task( pub async fn host_task(
parts: crate::board::UartParts, parts: crate::board::UartParts,
commands: &'static CommandChannel, commands: &'static CommandChannel,
responses: &'static ResponseChannel, responses: &'static ResponseChannel,
@@ -28,8 +26,7 @@ pub async fn uart_task(
let mut decoder = CobsDecoder::new(); let mut decoder = CobsDecoder::new();
loop { loop {
use embassy_futures::select::select; use embassy_futures::select::{select, Either};
use embassy_futures::select::Either;
match select( match select(
embedded_io_async::Read::read(&mut rx, &mut read_buf), embedded_io_async::Read::read(&mut rx, &mut read_buf),
@@ -53,7 +50,7 @@ pub async fn uart_task(
let _ = cmds.push(cmd); let _ = cmds.push(cmd);
}); });
for cmd in cmds { for cmd in cmds {
protocol_io::route_command( framing::route_command(
cmd, commands, responses, display_commands, has_display, mac, cmd, commands, responses, display_commands, has_display, mac,
) )
.await; .await;
@@ -61,7 +58,7 @@ pub async fn uart_task(
} }
Either::Second(response) => { Either::Second(response) => {
if let Some(frame) = if let Some(frame) =
protocol_io::cobs_encode_response(response, &mut write_buf, &mut cobs_encode_buf) framing::cobs_encode_response(response, &mut write_buf, &mut cobs_encode_buf)
{ {
if tx.write_all(frame).await.is_err() { if tx.write_all(frame).await.is_err() {
warn!("UART write failed, response dropped"); warn!("UART write failed, response dropped");
@@ -1,7 +1,4 @@
//! USB CDC-ACM task: COBS-framed fixed-size LE command/response protocol. //! USB CDC-ACM host task: COBS-framed fixed-size LE command/response protocol.
//!
//! Reads COBS frames from USB, decodes them, parses commands, routes them
//! to the radio or display task, and sends COBS-framed responses back.
use defmt::warn; use defmt::warn;
use embassy_executor::task; use embassy_executor::task;
@@ -11,12 +8,12 @@ use embassy_usb::Builder;
use static_cell::StaticCell; use static_cell::StaticCell;
use crate::channel::{CommandChannel, DisplayCommand, DisplayCommandChannel, ResponseChannel}; use crate::channel::{CommandChannel, DisplayCommand, DisplayCommandChannel, ResponseChannel};
use crate::protocol_io::{self, CobsDecoder, MAX_FRAME}; use super::framing::{self, CobsDecoder, MAX_FRAME};
const MAX_PACKET: usize = 64; const MAX_PACKET: usize = 64;
#[task] #[task]
pub async fn usb_task( pub async fn host_task(
parts: crate::board::UsbParts, parts: crate::board::UsbParts,
commands: &'static CommandChannel, commands: &'static CommandChannel,
responses: &'static ResponseChannel, responses: &'static ResponseChannel,
@@ -24,10 +21,6 @@ pub async fn usb_task(
has_display: bool, has_display: bool,
mac: [u8; 6], mac: [u8; 6],
) { ) {
// ── USB device configuration ────────────────────────────────────
// VID 0x1209: pid.codes open-source USB vendor ID
// PID 0x5741: DongLoRa product ID
// Class 0xEF/0x02/0x01: Miscellaneous / Interface Association Descriptor
let mut config = embassy_usb::Config::new(0x1209, 0x5741); let mut config = embassy_usb::Config::new(0x1209, 0x5741);
config.manufacturer = Some("DongLoRa"); config.manufacturer = Some("DongLoRa");
config.product = Some("DongLoRa LoRa Radio"); config.product = Some("DongLoRa LoRa Radio");
@@ -36,7 +29,6 @@ pub async fn usb_task(
config.device_protocol = 0x01; config.device_protocol = 0x01;
config.composite_with_iads = true; config.composite_with_iads = true;
// Use MAC address as USB serial number for unique device identification.
static SERIAL_BUF: StaticCell<[u8; 12]> = StaticCell::new(); static SERIAL_BUF: StaticCell<[u8; 12]> = StaticCell::new();
let serial_buf = SERIAL_BUF.init([0u8; 12]); let serial_buf = SERIAL_BUF.init([0u8; 12]);
const HEX: &[u8; 16] = b"0123456789ABCDEF"; const HEX: &[u8; 16] = b"0123456789ABCDEF";
@@ -64,18 +56,9 @@ pub async fn usb_task(
let mut usb_dev = builder.build(); let mut usb_dev = builder.build();
let (sender, receiver) = class.split(); let (sender, receiver) = class.split();
// ── Run USB device + protocol loop concurrently ────────────────
join( join(
usb_dev.run(), usb_dev.run(),
protocol_loop( protocol_loop(sender, receiver, commands, responses, display_commands, has_display, mac),
sender,
receiver,
commands,
responses,
display_commands,
has_display,
mac,
),
) )
.await; .await;
} }
@@ -89,8 +72,7 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
has_display: bool, has_display: bool,
mac: [u8; 6], mac: [u8; 6],
) { ) {
use embassy_futures::select::select3; use embassy_futures::select::{select3, Either3};
use embassy_futures::select::Either3;
let mut read_buf = [0u8; MAX_PACKET]; let mut read_buf = [0u8; MAX_PACKET];
let mut write_buf = [0u8; MAX_FRAME]; let mut write_buf = [0u8; MAX_FRAME];
@@ -102,7 +84,7 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
match select3( match select3(
receiver.read_packet(&mut read_buf), receiver.read_packet(&mut read_buf),
responses.receive(), responses.receive(),
embassy_time::Timer::after_millis(250), // DTR poll interval for disconnect detection embassy_time::Timer::after_millis(250),
) )
.await .await
{ {
@@ -122,7 +104,7 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
let _ = cmds.push(cmd); let _ = cmds.push(cmd);
}); });
for cmd in cmds { for cmd in cmds {
protocol_io::route_command( framing::route_command(
cmd, commands, responses, display_commands, has_display, mac, cmd, commands, responses, display_commands, has_display, mac,
) )
.await; .await;
@@ -130,7 +112,7 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
} }
Either3::Second(response) => { Either3::Second(response) => {
if let Some(frame) = if let Some(frame) =
protocol_io::cobs_encode_response(response, &mut write_buf, &mut cobs_encode_buf) framing::cobs_encode_response(response, &mut write_buf, &mut cobs_encode_buf)
{ {
for chunk in frame.chunks(MAX_PACKET) { for chunk in frame.chunks(MAX_PACKET) {
if sender.write_packet(chunk).await.is_err() { if sender.write_packet(chunk).await.is_err() {
@@ -140,12 +122,9 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
} }
} }
} }
Either3::Third(()) => { Either3::Third(()) => {}
// Poll DTR every 250ms to detect host disconnect
}
} }
// Check DTR after every select wake
let connected = receiver.dtr(); let connected = receiver.dtr();
if was_connected && !connected && has_display { if was_connected && !connected && has_display {
display_commands.send(DisplayCommand::Reset).await; display_commands.send(DisplayCommand::Reset).await;
+4 -23
View File
@@ -25,15 +25,11 @@ mod display;
mod driver; mod driver;
#[cfg(not(test))] #[cfg(not(test))]
mod hal; mod hal;
#[cfg(not(test))]
mod host;
mod protocol; mod protocol;
#[cfg(not(test))] #[cfg(not(test))]
mod radio; mod radio;
#[cfg(not(test))]
mod protocol_io;
#[cfg(all(not(test), feature = "heltec_v3"))]
mod uart;
#[cfg(all(not(test), not(feature = "heltec_v3")))]
mod usb;
#[cfg(not(test))] #[cfg(not(test))]
use embassy_executor::Spawner; use embassy_executor::Spawner;
@@ -90,10 +86,8 @@ async fn run(spawner: Spawner) {
.spawn(radio::radio_task(radio, &COMMANDS, &RESPONSES, &STATUS)) .spawn(radio::radio_task(radio, &COMMANDS, &RESPONSES, &STATUS))
.expect("spawn radio_task"); .expect("spawn radio_task");
cfg_if::cfg_if! {
if #[cfg(feature = "heltec_v3")] {
spawner spawner
.spawn(uart::uart_task( .spawn(host::host_task(
comm, comm,
&COMMANDS, &COMMANDS,
&RESPONSES, &RESPONSES,
@@ -101,20 +95,7 @@ async fn run(spawner: Spawner) {
has_display, has_display,
mac, mac,
)) ))
.expect("spawn uart_task"); .expect("spawn host_task");
} else {
spawner
.spawn(usb::usb_task(
comm,
&COMMANDS,
&RESPONSES,
&DISPLAY_COMMANDS,
has_display,
mac,
))
.expect("spawn usb_task");
}
}
if let Some(dp) = display { if let Some(dp) = display {
spawner spawner
-4
View File
@@ -1,4 +0,0 @@
//! UART interface with COBS-framed command/response protocol.
mod task;
pub use task::uart_task;
-4
View File
@@ -1,4 +0,0 @@
//! USB CDC-ACM interface with COBS-framed command/response protocol.
mod task;
pub use task::usb_task;