Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8066f5135e
|
||
|
|
1c93b77d0b
|
||
|
|
e60465f181
|
||
|
|
35aecf07fc
|
||
|
|
c78e2ceaa9
|
||
|
|
d67c0699c9
|
||
|
|
adc76921e1
|
||
|
|
acd97966aa
|
||
|
|
317236acd1
|
No files matched your search
+36
-1
@@ -5,7 +5,42 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.1.0] - Unreleased
|
||||
## [0.2.0] - 2026-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- Wio Tracker L1 board support (nRF52840 + SX1262, SH1106 OLED)
|
||||
- LED blink on packet activity: brief flash on RX and TX when display is on
|
||||
- RX brightness scales with SNR (stronger signal = brighter)
|
||||
- Works on all boards with a user LED (Heltec V3/V4, Wio Tracker)
|
||||
- Native USB CDC-ACM support for Heltec V3 (requires hardware mod: solder R29/R3)
|
||||
- `heltec_v3_uart` feature for stock (unmodified) V3 boards via CP2102 bridge
|
||||
- `RgbLed` trait and `SimpleLed` driver for GPIO-based LEDs
|
||||
- `LoRaBoard` trait with full associated types (`RadioParts`, `CommParts`,
|
||||
`DisplayParts`, `DisplayDriver`, `LedDriver`) and `BoardParts<R, C, D, L>` struct
|
||||
|
||||
### Changed
|
||||
|
||||
- Major firmware architecture reorganization:
|
||||
- `hal/` module for MCU-specific primitives (ESP32-S3, nRF52840)
|
||||
- `driver/` module for hardware peripheral drivers (SH1106, SimpleLed)
|
||||
- `host/` module for unified USB/UART communication with internal cfg dispatch
|
||||
- `board/` contains only board definitions — no MCU helpers or drivers mixed in
|
||||
- Board files are declarative wiring diagrams calling hal primitives
|
||||
- `run()` function has zero board-specific conditional compilation
|
||||
- Display init moved to board layer (`create_display()`) — display task has no cfg
|
||||
- Flattened `radio/` and `display/` directories (no single-file directories)
|
||||
- `build.rs` auto-discovers boards by content (implements `LoRaBoard`) and
|
||||
helper modules by `use super::` imports — no exclusion lists
|
||||
- `check` and `clippy` commands now use `--release` profile
|
||||
- Deduplicated USB/UART protocol framing (shared `CobsDecoder` + `route_command`)
|
||||
- Removed `ErrorCode::CrcError` (SX1262 silently drops bad-CRC packets)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Display stuck on splash screen for UART boards (disconnected flag started true)
|
||||
|
||||
## [0.1.0] - 2026-03-15
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
Generated
+2
-1
@@ -372,7 +372,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donglora"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cortex-m",
|
||||
@@ -390,6 +390,7 @@ dependencies = [
|
||||
"embassy-usb",
|
||||
"embassy-usb-driver",
|
||||
"embedded-graphics",
|
||||
"embedded-hal 1.0.0",
|
||||
"embedded-hal-async",
|
||||
"embedded-io-async 0.7.0",
|
||||
"esp-backtrace",
|
||||
|
||||
+2
-1
@@ -3,7 +3,7 @@ members = ["."]
|
||||
|
||||
[package]
|
||||
name = "donglora"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
authors = ["Stephen Waits <steve@waits.net>"]
|
||||
@@ -21,6 +21,7 @@ categories = ["embedded", "hardware-support", "no-std"]
|
||||
cfg-if = "1.0"
|
||||
critical-section = "1.2"
|
||||
defmt = "1.0"
|
||||
embedded-hal = "1.0"
|
||||
embedded-hal-async = "1.0"
|
||||
embassy-embedded-hal = "0.6"
|
||||
embassy-executor = "0.9"
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
- get the code better organized
|
||||
- firmware file naming, no "release", add version
|
||||
- LED blinking on TX/RX? colored?
|
||||
- display having board specific stuff in it
|
||||
- publish new client, mux, bridge (updates in client for UART crap)
|
||||
- try to get heltec v3 back to the right USB port+VID:PID
|
||||
- split out mux and client repos
|
||||
- move to GH organization
|
||||
@@ -11,6 +11,7 @@ use static_cell::StaticCell;
|
||||
|
||||
use super::esp32s3;
|
||||
use super::traits::{BoardParts, LoRaBoard};
|
||||
use crate::driver::simple_led::SimpleLed;
|
||||
use crate::hal::esp32s3 as mcu;
|
||||
|
||||
pub use super::esp32s3::{
|
||||
@@ -18,6 +19,7 @@ pub use super::esp32s3::{
|
||||
};
|
||||
|
||||
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||
pub type LedDriver = SimpleLed<Output<'static>>;
|
||||
|
||||
pub struct UsbParts {
|
||||
pub driver: UsbDriver,
|
||||
@@ -35,6 +37,7 @@ impl LoRaBoard for Board {
|
||||
type CommParts = UsbParts;
|
||||
type DisplayParts = DisplayParts;
|
||||
type DisplayDriver = DisplayDriver;
|
||||
type LedDriver = LedDriver;
|
||||
|
||||
fn init() -> Self {
|
||||
let p = esp_hal::init(esp_hal::Config::default());
|
||||
@@ -45,7 +48,7 @@ impl LoRaBoard for Board {
|
||||
mcu::mac_address()
|
||||
}
|
||||
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> {
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
|
||||
let p = self.p;
|
||||
|
||||
mcu::start_timer(p.TIMG0);
|
||||
@@ -79,10 +82,15 @@ impl LoRaBoard for Board {
|
||||
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
||||
let display = Some(DisplayParts { i2c });
|
||||
|
||||
// White LED on GPIO35
|
||||
let led_pin = Output::new(p.GPIO35, Level::Low, OutputConfig::default());
|
||||
let led = SimpleLed(led_pin);
|
||||
|
||||
BoardParts {
|
||||
radio,
|
||||
host,
|
||||
display,
|
||||
led,
|
||||
mac: Self::mac_address(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||
|
||||
use super::esp32s3;
|
||||
use super::traits::{BoardParts, LoRaBoard};
|
||||
use crate::driver::simple_led::SimpleLed;
|
||||
use crate::hal::esp32s3 as mcu;
|
||||
|
||||
pub use super::esp32s3::{
|
||||
@@ -17,6 +18,7 @@ pub use super::esp32s3::{
|
||||
};
|
||||
|
||||
pub type UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>;
|
||||
pub type LedDriver = SimpleLed<Output<'static>>;
|
||||
|
||||
pub struct UartParts {
|
||||
pub driver: UartDriver,
|
||||
@@ -34,6 +36,7 @@ impl LoRaBoard for Board {
|
||||
type CommParts = UartParts;
|
||||
type DisplayParts = DisplayParts;
|
||||
type DisplayDriver = DisplayDriver;
|
||||
type LedDriver = LedDriver;
|
||||
|
||||
fn init() -> Self {
|
||||
let p = esp_hal::init(esp_hal::Config::default());
|
||||
@@ -44,7 +47,7 @@ impl LoRaBoard for Board {
|
||||
mcu::mac_address()
|
||||
}
|
||||
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UartParts, DisplayParts> {
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UartParts, DisplayParts, LedDriver> {
|
||||
let p = self.p;
|
||||
|
||||
mcu::start_timer(p.TIMG0);
|
||||
@@ -75,10 +78,15 @@ impl LoRaBoard for Board {
|
||||
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
||||
let display = Some(DisplayParts { i2c });
|
||||
|
||||
// White LED on GPIO35
|
||||
let led_pin = Output::new(p.GPIO35, Level::Low, OutputConfig::default());
|
||||
let led = SimpleLed(led_pin);
|
||||
|
||||
BoardParts {
|
||||
radio,
|
||||
host,
|
||||
display,
|
||||
led,
|
||||
mac: Self::mac_address(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use static_cell::StaticCell;
|
||||
|
||||
use super::esp32s3;
|
||||
use super::traits::{BoardParts, LoRaBoard};
|
||||
use crate::driver::simple_led::SimpleLed;
|
||||
use crate::hal::esp32s3 as mcu;
|
||||
|
||||
pub use super::esp32s3::{
|
||||
@@ -11,6 +12,8 @@ pub use super::esp32s3::{
|
||||
|
||||
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||
|
||||
pub type LedDriver = SimpleLed<Output<'static>>;
|
||||
|
||||
pub struct UsbParts {
|
||||
pub driver: UsbDriver,
|
||||
}
|
||||
@@ -27,6 +30,7 @@ impl LoRaBoard for Board {
|
||||
type CommParts = UsbParts;
|
||||
type DisplayParts = DisplayParts;
|
||||
type DisplayDriver = DisplayDriver;
|
||||
type LedDriver = LedDriver;
|
||||
|
||||
fn init() -> Self {
|
||||
let p = esp_hal::init(esp_hal::Config::default());
|
||||
@@ -37,7 +41,7 @@ impl LoRaBoard for Board {
|
||||
mcu::mac_address()
|
||||
}
|
||||
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> {
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
|
||||
let p = self.p;
|
||||
|
||||
mcu::start_timer(p.TIMG0);
|
||||
@@ -71,10 +75,15 @@ impl LoRaBoard for Board {
|
||||
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
||||
let display = Some(DisplayParts { i2c });
|
||||
|
||||
// Orange LED on GPIO35
|
||||
let led_pin = Output::new(p.GPIO35, Level::Low, OutputConfig::default());
|
||||
let led = SimpleLed(led_pin);
|
||||
|
||||
BoardParts {
|
||||
radio,
|
||||
host,
|
||||
display,
|
||||
led,
|
||||
mac: Self::mac_address(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>;
|
||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||
pub type UsbDriver = mcu::UsbNrfDriver;
|
||||
pub type DisplayI2c = mcu::I2cBus;
|
||||
pub type LedDriver = ();
|
||||
|
||||
pub type DisplayDriver = ssd1306::Ssd1306Async<
|
||||
ssd1306::prelude::I2CInterface<DisplayI2c>,
|
||||
@@ -77,6 +78,7 @@ impl LoRaBoard for Board {
|
||||
type CommParts = UsbParts;
|
||||
type DisplayParts = DisplayParts;
|
||||
type DisplayDriver = DisplayDriver;
|
||||
type LedDriver = ();
|
||||
|
||||
fn init() -> Self {
|
||||
let p = embassy_nrf::init(Default::default());
|
||||
@@ -87,7 +89,7 @@ impl LoRaBoard for Board {
|
||||
mcu::mac_address()
|
||||
}
|
||||
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> {
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, ()> {
|
||||
let p = self.p;
|
||||
|
||||
// ── SPI bus for SX1262 ──────────────────────────────────
|
||||
@@ -135,6 +137,7 @@ impl LoRaBoard for Board {
|
||||
radio,
|
||||
host,
|
||||
display,
|
||||
led: (),
|
||||
mac: Self::mac_address(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
//! Board abstraction trait.
|
||||
//!
|
||||
//! Every board must implement [`LoRaBoard`] with concrete associated types
|
||||
//! for its radio, host communication, and display peripherals. The compiler
|
||||
//! enforces completeness — missing types or methods are compile errors.
|
||||
//! for its radio, host communication, display, and LED peripherals. The
|
||||
//! compiler enforces completeness — missing types or methods are compile errors.
|
||||
//!
|
||||
//! See `src/board/PORTING.md` for a step-by-step guide to adding a new board.
|
||||
|
||||
use embedded_graphics::draw_target::DrawTarget;
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
|
||||
/// Async RGB LED control. Boards without an LED use `()` which is a no-op.
|
||||
pub trait RgbLed {
|
||||
fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> impl core::future::Future<Output = ()>;
|
||||
}
|
||||
|
||||
impl RgbLed for () {
|
||||
async fn set_rgb(&mut self, _r: u8, _g: u8, _b: u8) {}
|
||||
}
|
||||
|
||||
/// Compile-time contract for a DongLoRa board.
|
||||
pub trait LoRaBoard: Sized {
|
||||
/// Human-readable board name (shown on display splash screen).
|
||||
@@ -29,6 +38,9 @@ pub trait LoRaBoard: Sized {
|
||||
/// Concrete display driver type (must implement DrawTarget for rendering).
|
||||
type DisplayDriver: DrawTarget<Color = BinaryColor>;
|
||||
|
||||
/// RGB LED driver. Boards without an LED use `()`.
|
||||
type LedDriver: RgbLed;
|
||||
|
||||
/// Initialize the board hardware.
|
||||
fn init() -> Self;
|
||||
|
||||
@@ -36,13 +48,14 @@ pub trait LoRaBoard: Sized {
|
||||
fn mac_address() -> [u8; 6];
|
||||
|
||||
/// Decompose initialized board into peripheral bundles for each task.
|
||||
fn into_parts(self) -> BoardParts<Self::RadioParts, Self::CommParts, Self::DisplayParts>;
|
||||
fn into_parts(self) -> BoardParts<Self::RadioParts, Self::CommParts, Self::DisplayParts, Self::LedDriver>;
|
||||
}
|
||||
|
||||
/// Peripheral bundles for each firmware task, produced by [`LoRaBoard::into_parts`].
|
||||
pub struct BoardParts<R, C, D> {
|
||||
pub struct BoardParts<R, C, D, L> {
|
||||
pub radio: R,
|
||||
pub host: C,
|
||||
pub display: Option<D>,
|
||||
pub led: L,
|
||||
pub mac: [u8; 6],
|
||||
}
|
||||
@@ -21,6 +21,8 @@ pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||
pub type UsbDriver = mcu::UsbNrfDriver;
|
||||
pub type DisplayI2c = mcu::I2cBus;
|
||||
|
||||
pub type LedDriver = crate::driver::simple_led::SimpleLed<Output<'static>>;
|
||||
|
||||
pub type DisplayDriver = crate::driver::sh1106::Sh1106<DisplayI2c>;
|
||||
|
||||
// ── Peripheral bundles ──────────────────────────────────────────────
|
||||
@@ -66,6 +68,7 @@ impl LoRaBoard for Board {
|
||||
type CommParts = UsbParts;
|
||||
type DisplayParts = DisplayParts;
|
||||
type DisplayDriver = DisplayDriver;
|
||||
type LedDriver = LedDriver;
|
||||
|
||||
fn init() -> Self {
|
||||
let mut config = embassy_nrf::config::Config::default();
|
||||
@@ -79,7 +82,7 @@ impl LoRaBoard for Board {
|
||||
mcu::mac_address()
|
||||
}
|
||||
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> {
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
|
||||
let p = self.p;
|
||||
|
||||
// ── SPI bus for SX1262 ──────────────────────────────────
|
||||
@@ -129,10 +132,15 @@ impl LoRaBoard for Board {
|
||||
);
|
||||
let display = Some(DisplayParts { i2c });
|
||||
|
||||
// Green LED on P1.01
|
||||
let led_pin = Output::new(p.P1_01, Level::Low, OutputDrive::Standard);
|
||||
let led = crate::driver::simple_led::SimpleLed(led_pin);
|
||||
|
||||
BoardParts {
|
||||
radio,
|
||||
host,
|
||||
display,
|
||||
led,
|
||||
mac: Self::mac_address(),
|
||||
}
|
||||
}
|
||||
|
||||
+30
-5
@@ -6,11 +6,9 @@ use embassy_time::Timer;
|
||||
use embedded_graphics::draw_target::DrawTarget;
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
|
||||
use crate::board::DisplayParts;
|
||||
use crate::board::{Board, DisplayParts, LedDriver, LoRaBoard, RgbLed};
|
||||
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch};
|
||||
|
||||
use crate::board::{Board, LoRaBoard};
|
||||
|
||||
use render::RSSI_HISTORY_LEN;
|
||||
|
||||
const BOARD_NAME: &str = Board::NAME;
|
||||
@@ -78,6 +76,14 @@ impl DisplayState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Map SNR (dB) to LED brightness (4..64). Stronger signal = brighter.
|
||||
fn snr_brightness(snr: Option<i16>) -> u8 {
|
||||
let snr = snr.unwrap_or(-10);
|
||||
let clamped = snr.clamp(-20, 15) as i32;
|
||||
// -20 → 4, +15 → 64
|
||||
((clamped + 20) * 60 / 35 + 4) as u8
|
||||
}
|
||||
|
||||
/// Render the appropriate screen for the current state into the display buffer.
|
||||
fn render_current(
|
||||
display: &mut impl DrawTarget<Color = BinaryColor>,
|
||||
@@ -103,6 +109,7 @@ fn render_current(
|
||||
#[task]
|
||||
pub async fn display_task(
|
||||
parts: DisplayParts,
|
||||
mut led: LedDriver,
|
||||
status: &'static StatusWatch,
|
||||
display_commands: &'static DisplayCommandChannel,
|
||||
) {
|
||||
@@ -150,19 +157,35 @@ pub async fn display_task(
|
||||
if state.disconnected {
|
||||
continue;
|
||||
}
|
||||
let rx_packet = radio_status.rx_count != state.last_status.rx_count;
|
||||
let tx_packet = radio_status.tx_count != state.last_status.tx_count;
|
||||
|
||||
if let Some(rssi) = radio_status.last_rssi {
|
||||
if radio_status.rx_count != state.last_status.rx_count {
|
||||
if rx_packet {
|
||||
state.record_rssi(rssi);
|
||||
}
|
||||
}
|
||||
if radio_status.tx_count != state.last_status.tx_count {
|
||||
if tx_packet {
|
||||
state.record_tx();
|
||||
}
|
||||
let last_snr = radio_status.last_snr;
|
||||
state.last_status = radio_status;
|
||||
|
||||
if state.display_on {
|
||||
render_current(&mut display, &state, &board_info);
|
||||
let _ = display.flush().await;
|
||||
|
||||
// Brief LED blink: red on TX, green scaled by SNR on RX
|
||||
if tx_packet {
|
||||
led.set_rgb(32, 0, 0).await;
|
||||
Timer::after_millis(50).await;
|
||||
led.set_rgb(0, 0, 0).await;
|
||||
} else if rx_packet {
|
||||
let b = snr_brightness(last_snr);
|
||||
led.set_rgb(0, b, 0).await;
|
||||
Timer::after_millis(50).await;
|
||||
led.set_rgb(0, 0, 0).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Either3::Second(cmd) => match cmd {
|
||||
@@ -171,6 +194,7 @@ pub async fn display_task(
|
||||
state.display_on = false;
|
||||
render::blank(&mut display);
|
||||
let _ = display.flush().await;
|
||||
led.set_rgb(0, 0, 0).await;
|
||||
}
|
||||
DisplayCommand::On => {
|
||||
state.disconnected = false;
|
||||
@@ -185,6 +209,7 @@ pub async fn display_task(
|
||||
state = DisplayState::new();
|
||||
render::splash(&mut display, &board_info);
|
||||
let _ = display.flush().await;
|
||||
led.set_rgb(0, 0, 0).await;
|
||||
}
|
||||
},
|
||||
Either3::Third(()) => {
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
#[cfg(feature = "wio_tracker_l1")]
|
||||
pub mod sh1106;
|
||||
|
||||
#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "wio_tracker_l1"))]
|
||||
pub mod simple_led;
|
||||
@@ -0,0 +1,20 @@
|
||||
//! Simple single-color LED driver (GPIO on/off).
|
||||
//!
|
||||
//! Generic over any `OutputPin`. Any non-zero RGB value turns the LED on;
|
||||
//! all zeros turns it off.
|
||||
|
||||
use embedded_hal::digital::OutputPin;
|
||||
|
||||
use crate::board::RgbLed;
|
||||
|
||||
pub struct SimpleLed<P>(pub P);
|
||||
|
||||
impl<P: OutputPin> RgbLed for SimpleLed<P> {
|
||||
async fn set_rgb(&mut self, r: u8, g: u8, b: u8) {
|
||||
if r > 0 || g > 0 || b > 0 {
|
||||
let _ = self.0.set_high();
|
||||
} else {
|
||||
let _ = self.0.set_low();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ async fn run(spawner: Spawner) {
|
||||
|
||||
if let Some(dp) = parts.display {
|
||||
spawner
|
||||
.spawn(display::display_task(dp, &STATUS, &DISPLAY_COMMANDS))
|
||||
.spawn(display::display_task(dp, parts.led, &STATUS, &DISPLAY_COMMANDS))
|
||||
.expect("spawn display_task");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user