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:
swaits committed 2026-03-26 15:16:42 -06:00
commit c30f480c34
20 files changed
+2599

No files matched your search

+89
View File
@@ -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,
}