feat: initial lora-dongle firmware scaffold
Transparent LoRa-over-USB dongle firmware supporting Heltec V3 (ESP32-S3) and RAK 4631 (nRF52840) boards. Actor-model architecture with typed channels between USB, radio, and display tasks. - Protocol: Command/Response enums with COBS+postcard framing design - Board abstraction: auto-discovered via build.rs Jinja2 codegen - Radio task: SX1262 state machine (Idle → Receiving ↔ Transmitting) - Display task: optional SSD1306 dashboard via StatusWatch - RAK 4631: fully wired SPI/USB/I2C peripherals, clippy-clean - Heltec V3: structure in place, awaits espup Xtensa toolchain
This commit is contained in:
20 files changed
+2599
No files matched your search
@@ -0,0 +1,70 @@
|
||||
use esp_hal::gpio::{GpioPin, Input, Output, PullDown, PushPull};
|
||||
use esp_hal::peripherals::SPI2;
|
||||
use esp_hal::spi::master::Spi;
|
||||
use esp_hal::spi::SpiMode;
|
||||
use esp_hal::i2c::I2c;
|
||||
use esp_hal::peripherals::I2C0;
|
||||
use esp_hal::Delay;
|
||||
|
||||
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
||||
use lora_phy::LoRa;
|
||||
|
||||
/// Board name shown on the display dashboard.
|
||||
pub const BOARD_NAME: &str = "Heltec V3";
|
||||
|
||||
// ── Concrete peripheral types ────────────────────────────────────────
|
||||
|
||||
type Iv = GenericSx126xInterfaceVariant<Output<'static, GpioPin<8>>, Input<'static, GpioPin<14>>>;
|
||||
type RadioSpi = Spi<'static, SPI2>;
|
||||
type RadioDriver = Sx126x<RadioSpi, Iv, Sx1262>;
|
||||
|
||||
/// Fully-constructed LoRa radio driver.
|
||||
pub type Radio = LoRa<RadioDriver, Delay>;
|
||||
|
||||
/// USB driver for CDC-ACM.
|
||||
// TODO: define concrete USB OTG type once esp-hal USB support is wired up
|
||||
pub type UsbDriver = ();
|
||||
|
||||
/// I2C bus for the built-in SSD1306 OLED.
|
||||
pub type DisplayI2c = I2c<'static, I2C0>;
|
||||
|
||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||
|
||||
pub struct RadioParts {
|
||||
pub radio: Radio,
|
||||
}
|
||||
|
||||
pub struct UsbParts {
|
||||
pub driver: UsbDriver,
|
||||
}
|
||||
|
||||
pub struct DisplayParts {
|
||||
pub i2c: DisplayI2c,
|
||||
}
|
||||
|
||||
// ── Board init ───────────────────────────────────────────────────────
|
||||
|
||||
pub struct Board {
|
||||
p: esp_hal::peripherals::Peripherals,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn init() -> Self {
|
||||
let p = esp_hal::init(esp_hal::Config::default());
|
||||
Self { p }
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
||||
let _p = self.p;
|
||||
|
||||
// TODO: implement Heltec V3 peripheral init
|
||||
// - SPI2 for SX1262 (SCK=9, MOSI=10, MISO=11, NSS=8)
|
||||
// - GPIO for radio control (RST=12, DIO1=14, BUSY=13)
|
||||
// - Vext pin (GPIO21) LOW to power OLED
|
||||
// - I2C0 for SSD1306 (SDA=17, SCL=18)
|
||||
// - USB OTG for CDC-ACM
|
||||
|
||||
todo!("Heltec V3 peripheral init — requires espup toolchain to develop")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// @generated by build.rs — DO NOT EDIT
|
||||
cfg_if::cfg_if! {
|
||||
{%- for board in boards %}
|
||||
{% if loop.first %}if{% else %}else if{% endif %} #[cfg(feature = "{{ board }}")] {
|
||||
mod {{ board }};
|
||||
pub use {{ board }}::*;
|
||||
}
|
||||
{%- endfor %}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||
use embassy_nrf::gpio::{AnyPin, Input, Level, Output, OutputDrive, Pull};
|
||||
use embassy_nrf::peripherals::USBD;
|
||||
use embassy_nrf::spim::{self, Spim};
|
||||
use embassy_nrf::twim::{self, Twim};
|
||||
use embassy_nrf::usb::Driver;
|
||||
use embassy_nrf::{bind_interrupts, Peripherals};
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use embassy_time::Delay;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
USBD => embassy_nrf::usb::InterruptHandler<USBD>;
|
||||
SPIM3 => embassy_nrf::spim::InterruptHandler<embassy_nrf::peripherals::SPI3>;
|
||||
SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0 => embassy_nrf::twim::InterruptHandler<embassy_nrf::peripherals::TWISPI0>;
|
||||
});
|
||||
|
||||
/// Board name shown on the display dashboard.
|
||||
#[allow(dead_code)]
|
||||
pub const BOARD_NAME: &str = "RAK 4631";
|
||||
|
||||
// ── Concrete peripheral types ────────────────────────────────────────
|
||||
|
||||
type SpiBus = Spim<'static, embassy_nrf::peripherals::SPI3>;
|
||||
type Nss = Output<'static>;
|
||||
type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
||||
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
||||
/// SX126x driver (pre-LoRa construction — LoRa::new is async).
|
||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||
|
||||
/// USB driver for CDC-ACM.
|
||||
pub type UsbDriver = Driver<'static, USBD, &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect>;
|
||||
|
||||
/// I2C bus for an optional SSD1306 OLED.
|
||||
pub type DisplayI2c = Twim<'static, embassy_nrf::peripherals::TWISPI0>;
|
||||
|
||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||
|
||||
pub struct RadioParts {
|
||||
pub driver: RadioDriver,
|
||||
pub delay: Delay,
|
||||
}
|
||||
|
||||
pub struct UsbParts {
|
||||
pub driver: UsbDriver,
|
||||
}
|
||||
|
||||
pub struct DisplayParts {
|
||||
pub i2c: DisplayI2c,
|
||||
}
|
||||
|
||||
// ── Board init ───────────────────────────────────────────────────────
|
||||
|
||||
pub struct Board {
|
||||
p: Peripherals,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn init() -> Self {
|
||||
let p = embassy_nrf::init(Default::default());
|
||||
Self { p }
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
||||
let p = self.p;
|
||||
|
||||
// ── SPI bus for SX1262 ───────────────────────────────────
|
||||
let mut spi_cfg = spim::Config::default();
|
||||
spi_cfg.frequency = spim::Frequency::M1;
|
||||
let spi = Spim::new(p.SPI3, Irqs, p.P1_11, p.P1_13, p.P1_12, spi_cfg);
|
||||
|
||||
static SPI_BUS: StaticCell<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
|
||||
StaticCell::new();
|
||||
let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi));
|
||||
|
||||
let nss = Output::new(AnyPin::from(p.P1_10), Level::High, OutputDrive::Standard);
|
||||
let spi_device = SpiDevice::new(spi_bus, nss);
|
||||
|
||||
// ── SX1262 control pins ──────────────────────────────────
|
||||
let reset = Output::new(AnyPin::from(p.P1_06), Level::High, OutputDrive::Standard);
|
||||
let dio1 = Input::new(AnyPin::from(p.P1_15), Pull::Down);
|
||||
let busy = Input::new(AnyPin::from(p.P1_14), Pull::Down);
|
||||
|
||||
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None).unwrap();
|
||||
|
||||
let sx_config = sx126x::Config {
|
||||
chip: Sx1262,
|
||||
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7),
|
||||
use_dcdc: true,
|
||||
rx_boost: false,
|
||||
};
|
||||
|
||||
let radio = RadioParts {
|
||||
driver: Sx126x::new(spi_device, iv, sx_config),
|
||||
delay: Delay,
|
||||
};
|
||||
|
||||
// ── USB ──────────────────────────────────────────────────
|
||||
static VBUS: StaticCell<embassy_nrf::usb::vbus_detect::SoftwareVbusDetect> =
|
||||
StaticCell::new();
|
||||
let vbus = VBUS.init(embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new(true, false));
|
||||
let usb = UsbParts {
|
||||
driver: Driver::new(p.USBD, Irqs, vbus),
|
||||
};
|
||||
|
||||
// ── Display (optional RAK1921 SSD1306 OLED on I2C) ──────
|
||||
// TODO: probe I2C address 0x3C to detect presence
|
||||
let i2c_cfg = twim::Config::default();
|
||||
let i2c = Twim::new(p.TWISPI0, Irqs, p.P0_13, p.P0_14, i2c_cfg);
|
||||
let display = Some(DisplayParts { i2c });
|
||||
|
||||
(radio, usb, display)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::channel::Channel;
|
||||
use embassy_sync::watch::Watch;
|
||||
|
||||
use crate::protocol::{Command, RadioConfig, Response};
|
||||
|
||||
/// Host-to-radio command channel.
|
||||
pub type CommandChannel = Channel<CriticalSectionRawMutex, Command, 16>;
|
||||
|
||||
/// Radio-to-host response channel.
|
||||
pub type ResponseChannel = Channel<CriticalSectionRawMutex, Response, 32>;
|
||||
|
||||
/// Observable radio status for the display task.
|
||||
pub type StatusWatch = Watch<CriticalSectionRawMutex, RadioStatus, 2>;
|
||||
|
||||
/// Current radio state exposed to observers (e.g. display).
|
||||
#[derive(Debug, Clone, defmt::Format)]
|
||||
pub struct RadioStatus {
|
||||
pub state: RadioState,
|
||||
pub config: Option<RadioConfig>,
|
||||
pub rx_count: u32,
|
||||
pub tx_count: u32,
|
||||
pub last_rssi: Option<i16>,
|
||||
pub last_snr: Option<i8>,
|
||||
}
|
||||
|
||||
/// Radio state machine states.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
|
||||
pub enum RadioState {
|
||||
Idle,
|
||||
Receiving,
|
||||
Transmitting,
|
||||
}
|
||||
|
||||
impl Default for RadioStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: RadioState::Idle,
|
||||
config: None,
|
||||
rx_count: 0,
|
||||
tx_count: 0,
|
||||
last_rssi: None,
|
||||
last_snr: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod render;
|
||||
mod task;
|
||||
pub use task::display_task;
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::channel::{RadioState, RadioStatus};
|
||||
|
||||
/// Render the status dashboard to a framebuffer.
|
||||
///
|
||||
/// Pure logic — no hardware dependency. Takes a status snapshot and produces
|
||||
/// draw calls that the display task flushes to the screen.
|
||||
pub fn dashboard(status: &RadioStatus) {
|
||||
// TODO: use embedded-graphics to render:
|
||||
// Line 1: board name + radio state
|
||||
// Line 2: freq / BW / SF
|
||||
// Line 3: TX power / coding rate
|
||||
// Line 4: RX count / TX count
|
||||
// Line 5: last RSSI / SNR
|
||||
let _ = status;
|
||||
}
|
||||
|
||||
/// Format radio state as a short display string.
|
||||
#[allow(dead_code)]
|
||||
pub fn state_label(state: RadioState) -> &'static str {
|
||||
match state {
|
||||
RadioState::Idle => "IDLE",
|
||||
RadioState::Receiving => "RX",
|
||||
RadioState::Transmitting => "TX",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use embassy_executor::task;
|
||||
|
||||
use crate::board::DisplayParts;
|
||||
use crate::channel::StatusWatch;
|
||||
|
||||
use super::render;
|
||||
|
||||
#[task]
|
||||
pub async fn display_task(parts: DisplayParts, status: &'static StatusWatch) {
|
||||
let _i2c = parts.i2c;
|
||||
|
||||
// TODO: init SSD1306 driver from I2C handle
|
||||
// TODO: initial render of empty dashboard
|
||||
|
||||
let mut receiver = status.receiver().unwrap();
|
||||
|
||||
loop {
|
||||
let state = receiver.changed().await;
|
||||
render::dashboard(&state);
|
||||
// TODO: flush framebuffer to display
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/// Radio hardware errors.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, defmt::Format)]
|
||||
pub enum RadioError {
|
||||
InitFailed,
|
||||
TxTimeout,
|
||||
InvalidConfig,
|
||||
}
|
||||
|
||||
/// USB protocol framing errors.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, defmt::Format)]
|
||||
pub enum ProtocolError {
|
||||
DeserializeFailed,
|
||||
BufferOverflow,
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
mod board;
|
||||
mod channel;
|
||||
mod display;
|
||||
mod error;
|
||||
mod protocol;
|
||||
mod radio;
|
||||
mod usb;
|
||||
|
||||
use embassy_executor::Spawner;
|
||||
|
||||
use crate::channel::{CommandChannel, ResponseChannel, StatusWatch};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "rak_4631")] {
|
||||
use panic_probe as _;
|
||||
} else if #[cfg(feature = "heltec_v3")] {
|
||||
use esp_backtrace as _;
|
||||
}
|
||||
}
|
||||
|
||||
static COMMANDS: CommandChannel = CommandChannel::new();
|
||||
static RESPONSES: ResponseChannel = ResponseChannel::new();
|
||||
static STATUS: StatusWatch = StatusWatch::new();
|
||||
|
||||
#[embassy_executor::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
let board = board::Board::init();
|
||||
let (radio, usb, display) = board.into_parts();
|
||||
|
||||
spawner.spawn(radio::radio_task(radio, &COMMANDS, &RESPONSES, &STATUS)).unwrap();
|
||||
spawner.spawn(usb::usb_task(usb, &COMMANDS, &RESPONSES)).unwrap();
|
||||
|
||||
if let Some(dp) = display {
|
||||
spawner.spawn(display::display_task(dp, &STATUS)).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use heapless::Vec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Maximum LoRa payload size in bytes.
|
||||
pub const MAX_PAYLOAD: usize = 256;
|
||||
|
||||
/// Protocol version for host compatibility checks.
|
||||
#[allow(dead_code)]
|
||||
pub const PROTOCOL_VERSION: u8 = 1;
|
||||
|
||||
/// LoRa signal bandwidth.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
|
||||
#[repr(u8)]
|
||||
pub enum Bandwidth {
|
||||
Khz7 = 0,
|
||||
Khz10 = 1,
|
||||
Khz15 = 2,
|
||||
Khz20 = 3,
|
||||
Khz31 = 4,
|
||||
Khz41 = 5,
|
||||
Khz62 = 6,
|
||||
Khz125 = 7,
|
||||
Khz250 = 8,
|
||||
Khz500 = 9,
|
||||
}
|
||||
|
||||
/// LoRa forward error correction coding rate.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
|
||||
#[repr(u8)]
|
||||
pub enum CodingRate {
|
||||
Cr4_5 = 1,
|
||||
Cr4_6 = 2,
|
||||
Cr4_7 = 3,
|
||||
Cr4_8 = 4,
|
||||
}
|
||||
|
||||
/// Complete LoRa radio configuration.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
|
||||
pub struct RadioConfig {
|
||||
pub freq_hz: u32,
|
||||
pub bw: Bandwidth,
|
||||
pub sf: u8,
|
||||
pub cr: CodingRate,
|
||||
pub sync_word: u16,
|
||||
pub tx_power_dbm: i8,
|
||||
}
|
||||
|
||||
/// Host → firmware commands.
|
||||
// Payload variant is intentionally large (inline heapless::Vec) — no allocator available.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub enum Command {
|
||||
Ping,
|
||||
GetConfig,
|
||||
SetConfig(RadioConfig),
|
||||
StartRx,
|
||||
StopRx,
|
||||
Transmit {
|
||||
config: Option<RadioConfig>,
|
||||
payload: Vec<u8, MAX_PAYLOAD>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Firmware → host responses.
|
||||
// Payload variant is intentionally large (inline heapless::Vec) — no allocator available.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub enum Response {
|
||||
Pong,
|
||||
Config(RadioConfig),
|
||||
RxPacket {
|
||||
rssi: i16,
|
||||
snr: i8,
|
||||
payload: Vec<u8, MAX_PAYLOAD>,
|
||||
},
|
||||
TxDone,
|
||||
Error(ErrorCode),
|
||||
}
|
||||
|
||||
/// Error codes reported to the host.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
|
||||
#[repr(u8)]
|
||||
pub enum ErrorCode {
|
||||
InvalidConfig = 1,
|
||||
RadioBusy = 2,
|
||||
TxTimeout = 3,
|
||||
CrcError = 4,
|
||||
NotConfigured = 5,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod task;
|
||||
pub use task::radio_task;
|
||||
@@ -0,0 +1,95 @@
|
||||
use defmt::error;
|
||||
use embassy_executor::task;
|
||||
use lora_phy::LoRa;
|
||||
|
||||
use crate::board::RadioParts;
|
||||
use crate::channel::{CommandChannel, RadioState, RadioStatus, ResponseChannel, StatusWatch};
|
||||
use crate::protocol::{Command, ErrorCode, Response};
|
||||
|
||||
#[task]
|
||||
pub async fn radio_task(
|
||||
parts: RadioParts,
|
||||
commands: &'static CommandChannel,
|
||||
responses: &'static ResponseChannel,
|
||||
status: &'static StatusWatch,
|
||||
) {
|
||||
let mut state = RadioStatus::default();
|
||||
status.sender().send(state.clone());
|
||||
|
||||
let lora = LoRa::new(parts.driver, false, parts.delay).await;
|
||||
let mut lora = match lora {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("radio init failed: {}", e);
|
||||
responses.send(Response::Error(ErrorCode::InvalidConfig)).await;
|
||||
// Cannot proceed without radio — idle forever.
|
||||
loop {
|
||||
let cmd = commands.receive().await;
|
||||
if let Command::Ping = cmd {
|
||||
responses.send(Response::Pong).await;
|
||||
} else {
|
||||
responses.send(Response::Error(ErrorCode::InvalidConfig)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
let cmd = commands.receive().await;
|
||||
match cmd {
|
||||
Command::Ping => {
|
||||
responses.send(Response::Pong).await;
|
||||
}
|
||||
Command::GetConfig => {
|
||||
if let Some(cfg) = state.config {
|
||||
responses.send(Response::Config(cfg)).await;
|
||||
} else {
|
||||
responses.send(Response::Error(ErrorCode::NotConfigured)).await;
|
||||
}
|
||||
}
|
||||
Command::SetConfig(cfg) => {
|
||||
state.config = Some(cfg);
|
||||
// TODO: apply config to lora hardware
|
||||
status.sender().send(state.clone());
|
||||
}
|
||||
Command::StartRx => {
|
||||
if state.config.is_some() {
|
||||
state.state = RadioState::Receiving;
|
||||
// TODO: start continuous RX via lora
|
||||
status.sender().send(state.clone());
|
||||
} else {
|
||||
responses.send(Response::Error(ErrorCode::NotConfigured)).await;
|
||||
}
|
||||
}
|
||||
Command::StopRx => {
|
||||
state.state = RadioState::Idle;
|
||||
// TODO: put lora radio into standby
|
||||
status.sender().send(state.clone());
|
||||
}
|
||||
Command::Transmit { config, payload } => {
|
||||
let tx_config = config.or(state.config);
|
||||
if let Some(_cfg) = tx_config {
|
||||
let was_receiving = state.state == RadioState::Receiving;
|
||||
state.state = RadioState::Transmitting;
|
||||
status.sender().send(state.clone());
|
||||
|
||||
// TODO: transmit payload via lora
|
||||
let _ = &mut lora;
|
||||
let _ = payload;
|
||||
|
||||
state.tx_count = state.tx_count.wrapping_add(1);
|
||||
responses.send(Response::TxDone).await;
|
||||
|
||||
state.state = if was_receiving {
|
||||
RadioState::Receiving
|
||||
} else {
|
||||
RadioState::Idle
|
||||
};
|
||||
status.sender().send(state.clone());
|
||||
} else {
|
||||
responses.send(Response::Error(ErrorCode::NotConfigured)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod task;
|
||||
pub use task::usb_task;
|
||||
@@ -0,0 +1,26 @@
|
||||
use embassy_executor::task;
|
||||
|
||||
use crate::board::UsbParts;
|
||||
use crate::channel::{CommandChannel, ResponseChannel};
|
||||
|
||||
#[task]
|
||||
pub async fn usb_task(
|
||||
parts: UsbParts,
|
||||
commands: &'static CommandChannel,
|
||||
responses: &'static ResponseChannel,
|
||||
) {
|
||||
let _driver = parts.driver;
|
||||
let _ = commands;
|
||||
let _ = responses;
|
||||
|
||||
// TODO: build embassy_usb CDC-ACM class from driver
|
||||
// TODO: COBS-framed postcard ser/de loop:
|
||||
// select! {
|
||||
// bytes = cdc_read() => deserialize Command → commands.send()
|
||||
// resp = responses.receive() => serialize Response → cdc_write()
|
||||
// }
|
||||
|
||||
loop {
|
||||
embassy_time::Timer::after_secs(1).await;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user