Author SHA1 Message Date
swaits 8066f5135e release: firmware v0.2.0
Major architecture reorganization, Wio Tracker L1 support, LED blink
on packet activity, native USB for Heltec V3 (hardware mod required).
See CHANGELOG.md for full details.
2026-04-07 12:46:58 -06:00
swaits 1c93b77d0b refactor: unify SimpleLed via embedded-hal, remove Option<L> from BoardParts
- Added embedded-hal 1.0 as direct dep (already transitive via embedded-hal-async)
- SimpleLed<P: OutputPin> now generic over any HAL — single definition in driver/
- Removed duplicate SimpleLed from board/esp32s3.rs and board/wio_tracker_l1.rs
- Changed BoardParts.led from Option<L> to L — boards with no LED use ()
- Display task calls led.set_rgb() unconditionally (no-op for () boards)
- RAK board: led: () instead of led: None
2026-04-07 12:40:55 -06:00
swaits e60465f181 fix: Wio Tracker LED on P1.01, move SimpleLed to board layer
Per the Wio Tracker L1 schematic, it has a green LED on P1.01.
Moved SimpleLed from driver/ to board layer (esp32s3.rs and
wio_tracker_l1.rs) since the impl needs HAL-specific Output types.
Removed driver/simple_led.rs — each board layer defines its own.
RAK WisBlock remains LedDriver=() (no user LED on the core module).
2026-04-07 12:20:20 -06:00
swaits 35aecf07fc fix: V4 also has plain LED on GPIO35, not WS2812 on GPIO38
Per the official V4 schematic (HTIT-WB32LAF_V4.3.pdf), the V4 has
an orange LED on GPIO35 with a series resistor — same setup as V3.
GPIO38 is GNSS_RX on V4, not an LED. Neither V3 nor V4 has a WS2812.

- V4: switched from WS2812/GPIO38 to SimpleLed/GPIO35
- Removed ws2812.rs driver (no current board uses addressable LEDs)
- All ESP boards now use the same SimpleLed driver on GPIO35
2026-04-07 11:59:05 -06:00
swaits c78e2ceaa9 fix: switch WS2812 driver to blocking RMT transmit
Async RMT had peripheral lifetime issues causing silent failures.
Blocking transmit is ~31µs per update (negligible) and avoids the
async complexity. Also removes the ManuallyDrop/unsafe workaround.
2026-04-07 11:51:18 -06:00
swaits d67c0699c9 fix: prevent RMT peripheral drop on V4, log WS2812 errors
The Rmt struct was being dropped after extracting channel0, which
likely disabled the RMT peripheral. Use ManuallyDrop to keep it alive.
Also log WS2812 transmit errors instead of silently discarding them.
2026-04-07 11:48:34 -06:00
swaits adc76921e1 fix: short 50ms LED blink, RX brightness scaled by SNR
- LED blink is now 50ms (was waiting for 1s timer tick)
- RX blink: green brightness scaled by SNR (-20dB→dim, +15dB→bright)
- TX blink: fixed red brightness
- Blink is self-contained (on→50ms delay→off) instead of timer-based
- V3 white LED: any non-zero = on, so SNR scaling is invisible but
  V4 WS2812 will show green intensity varying with signal quality
2026-04-07 11:34:51 -06:00
swaits acd97966aa fix: V3 has white LED on GPIO35, not WS2812B on GPIO38
The Heltec V3 has a plain white LED on GPIO35 (simple GPIO, not
addressable). Only the V4 has a WS2812B RGB LED on GPIO38.

- Created driver/simple_led.rs for single-color GPIO LEDs
- V3/V3-UART boards: SimpleLed on GPIO35 (white blink on RX/TX)
- V4 board: unchanged (WS2812 on GPIO38, green/red colors)
- nRF boards: unchanged (no LED, type LedDriver = ())
2026-04-07 11:30:16 -06:00
swaits 317236acd1 feat: RGB LED support — blinks green on RX, red on TX
- Added RgbLed trait with impl on () for boards without LED
- Added WS2812 driver (driver/ws2812.rs) using ESP32-S3 RMT peripheral
  - 80MHz clock, 12.5ns resolution, 24-bit GRB encoding
  - Zero extra crate dependencies
- Heltec V3/V3-UART/V4: WS2812B on GPIO38 via RMT channel 0
- RAK WisBlock/Wio Tracker: type LedDriver = (), led = None (no-op)
- Display task: green blink on RX packet, red blink on TX packet
- LED auto-off on timer tick (~1s), off when display is off/reset
- LoRaBoard trait gains type LedDriver: RgbLed associated type
- BoardParts gains led: Option<L> field
2026-04-07 11:18:30 -06:00
14 changed files with 152 additions and 22 deletions

No files matched your search

+36 -1
View File
@@ -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/), The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/). 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 ### Added
+2 -1
View File
@@ -372,7 +372,7 @@ dependencies = [
[[package]] [[package]]
name = "donglora" name = "donglora"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cortex-m", "cortex-m",
@@ -390,6 +390,7 @@ dependencies = [
"embassy-usb", "embassy-usb",
"embassy-usb-driver", "embassy-usb-driver",
"embedded-graphics", "embedded-graphics",
"embedded-hal 1.0.0",
"embedded-hal-async", "embedded-hal-async",
"embedded-io-async 0.7.0", "embedded-io-async 0.7.0",
"esp-backtrace", "esp-backtrace",
+2 -1
View File
@@ -3,7 +3,7 @@ members = ["."]
[package] [package]
name = "donglora" name = "donglora"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
authors = ["Stephen Waits <steve@waits.net>"] authors = ["Stephen Waits <steve@waits.net>"]
@@ -21,6 +21,7 @@ categories = ["embedded", "hardware-support", "no-std"]
cfg-if = "1.0" cfg-if = "1.0"
critical-section = "1.2" critical-section = "1.2"
defmt = "1.0" defmt = "1.0"
embedded-hal = "1.0"
embedded-hal-async = "1.0" embedded-hal-async = "1.0"
embassy-embedded-hal = "0.6" embassy-embedded-hal = "0.6"
embassy-executor = "0.9" embassy-executor = "0.9"
-4
View File
@@ -1,8 +1,4 @@
- get the code better organized
- firmware file naming, no "release", add version
- LED blinking on TX/RX? colored? - LED blinking on TX/RX? colored?
- display having board specific stuff in it
- publish new client, mux, bridge (updates in client for UART crap) - 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 - split out mux and client repos
- move to GH organization - move to GH organization
+9 -1
View File
@@ -11,6 +11,7 @@ use static_cell::StaticCell;
use super::esp32s3; use super::esp32s3;
use super::traits::{BoardParts, LoRaBoard}; use super::traits::{BoardParts, LoRaBoard};
use crate::driver::simple_led::SimpleLed;
use crate::hal::esp32s3 as mcu; use crate::hal::esp32s3 as mcu;
pub use super::esp32s3::{ pub use super::esp32s3::{
@@ -18,6 +19,7 @@ pub use super::esp32s3::{
}; };
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>; pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
pub type LedDriver = SimpleLed<Output<'static>>;
pub struct UsbParts { pub struct UsbParts {
pub driver: UsbDriver, pub driver: UsbDriver,
@@ -35,6 +37,7 @@ impl LoRaBoard for Board {
type CommParts = UsbParts; type CommParts = UsbParts;
type DisplayParts = DisplayParts; type DisplayParts = DisplayParts;
type DisplayDriver = DisplayDriver; type DisplayDriver = DisplayDriver;
type LedDriver = LedDriver;
fn init() -> Self { fn init() -> Self {
let p = esp_hal::init(esp_hal::Config::default()); let p = esp_hal::init(esp_hal::Config::default());
@@ -45,7 +48,7 @@ impl LoRaBoard for Board {
mcu::mac_address() mcu::mac_address()
} }
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> { fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
let p = self.p; let p = self.p;
mcu::start_timer(p.TIMG0); 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 i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
let display = Some(DisplayParts { i2c }); 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 { BoardParts {
radio, radio,
host, host,
display, display,
led,
mac: Self::mac_address(), mac: Self::mac_address(),
} }
} }
+9 -1
View File
@@ -10,6 +10,7 @@ use esp_hal::gpio::{Level, Output, OutputConfig};
use super::esp32s3; use super::esp32s3;
use super::traits::{BoardParts, LoRaBoard}; use super::traits::{BoardParts, LoRaBoard};
use crate::driver::simple_led::SimpleLed;
use crate::hal::esp32s3 as mcu; use crate::hal::esp32s3 as mcu;
pub use super::esp32s3::{ 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 UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>;
pub type LedDriver = SimpleLed<Output<'static>>;
pub struct UartParts { pub struct UartParts {
pub driver: UartDriver, pub driver: UartDriver,
@@ -34,6 +36,7 @@ impl LoRaBoard for Board {
type CommParts = UartParts; type CommParts = UartParts;
type DisplayParts = DisplayParts; type DisplayParts = DisplayParts;
type DisplayDriver = DisplayDriver; type DisplayDriver = DisplayDriver;
type LedDriver = LedDriver;
fn init() -> Self { fn init() -> Self {
let p = esp_hal::init(esp_hal::Config::default()); let p = esp_hal::init(esp_hal::Config::default());
@@ -44,7 +47,7 @@ impl LoRaBoard for Board {
mcu::mac_address() mcu::mac_address()
} }
fn into_parts(self) -> BoardParts<RadioParts, UartParts, DisplayParts> { fn into_parts(self) -> BoardParts<RadioParts, UartParts, DisplayParts, LedDriver> {
let p = self.p; let p = self.p;
mcu::start_timer(p.TIMG0); 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 i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
let display = Some(DisplayParts { i2c }); 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 { BoardParts {
radio, radio,
host, host,
display, display,
led,
mac: Self::mac_address(), mac: Self::mac_address(),
} }
} }
+10 -1
View File
@@ -3,6 +3,7 @@ use static_cell::StaticCell;
use super::esp32s3; use super::esp32s3;
use super::traits::{BoardParts, LoRaBoard}; use super::traits::{BoardParts, LoRaBoard};
use crate::driver::simple_led::SimpleLed;
use crate::hal::esp32s3 as mcu; use crate::hal::esp32s3 as mcu;
pub use super::esp32s3::{ pub use super::esp32s3::{
@@ -11,6 +12,8 @@ pub use super::esp32s3::{
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>; pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
pub type LedDriver = SimpleLed<Output<'static>>;
pub struct UsbParts { pub struct UsbParts {
pub driver: UsbDriver, pub driver: UsbDriver,
} }
@@ -27,6 +30,7 @@ impl LoRaBoard for Board {
type CommParts = UsbParts; type CommParts = UsbParts;
type DisplayParts = DisplayParts; type DisplayParts = DisplayParts;
type DisplayDriver = DisplayDriver; type DisplayDriver = DisplayDriver;
type LedDriver = LedDriver;
fn init() -> Self { fn init() -> Self {
let p = esp_hal::init(esp_hal::Config::default()); let p = esp_hal::init(esp_hal::Config::default());
@@ -37,7 +41,7 @@ impl LoRaBoard for Board {
mcu::mac_address() mcu::mac_address()
} }
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> { fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
let p = self.p; let p = self.p;
mcu::start_timer(p.TIMG0); 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 i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
let display = Some(DisplayParts { i2c }); 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 { BoardParts {
radio, radio,
host, host,
display, display,
led,
mac: Self::mac_address(), mac: Self::mac_address(),
} }
} }
+4 -1
View File
@@ -20,6 +20,7 @@ type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>;
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>; pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
pub type UsbDriver = mcu::UsbNrfDriver; pub type UsbDriver = mcu::UsbNrfDriver;
pub type DisplayI2c = mcu::I2cBus; pub type DisplayI2c = mcu::I2cBus;
pub type LedDriver = ();
pub type DisplayDriver = ssd1306::Ssd1306Async< pub type DisplayDriver = ssd1306::Ssd1306Async<
ssd1306::prelude::I2CInterface<DisplayI2c>, ssd1306::prelude::I2CInterface<DisplayI2c>,
@@ -77,6 +78,7 @@ impl LoRaBoard for Board {
type CommParts = UsbParts; type CommParts = UsbParts;
type DisplayParts = DisplayParts; type DisplayParts = DisplayParts;
type DisplayDriver = DisplayDriver; type DisplayDriver = DisplayDriver;
type LedDriver = ();
fn init() -> Self { fn init() -> Self {
let p = embassy_nrf::init(Default::default()); let p = embassy_nrf::init(Default::default());
@@ -87,7 +89,7 @@ impl LoRaBoard for Board {
mcu::mac_address() mcu::mac_address()
} }
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> { fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, ()> {
let p = self.p; let p = self.p;
// ── SPI bus for SX1262 ────────────────────────────────── // ── SPI bus for SX1262 ──────────────────────────────────
@@ -135,6 +137,7 @@ impl LoRaBoard for Board {
radio, radio,
host, host,
display, display,
led: (),
mac: Self::mac_address(), mac: Self::mac_address(),
} }
} }
+17 -4
View File
@@ -1,14 +1,23 @@
//! Board abstraction trait. //! Board abstraction trait.
//! //!
//! Every board must implement [`LoRaBoard`] with concrete associated types //! Every board must implement [`LoRaBoard`] with concrete associated types
//! for its radio, host communication, and display peripherals. The compiler //! for its radio, host communication, display, and LED peripherals. The
//! enforces completeness — missing types or methods are compile errors. //! 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. //! 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::draw_target::DrawTarget;
use embedded_graphics::pixelcolor::BinaryColor; 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. /// Compile-time contract for a DongLoRa board.
pub trait LoRaBoard: Sized { pub trait LoRaBoard: Sized {
/// Human-readable board name (shown on display splash screen). /// 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). /// Concrete display driver type (must implement DrawTarget for rendering).
type DisplayDriver: DrawTarget<Color = BinaryColor>; type DisplayDriver: DrawTarget<Color = BinaryColor>;
/// RGB LED driver. Boards without an LED use `()`.
type LedDriver: RgbLed;
/// Initialize the board hardware. /// Initialize the board hardware.
fn init() -> Self; fn init() -> Self;
@@ -36,13 +48,14 @@ pub trait LoRaBoard: Sized {
fn mac_address() -> [u8; 6]; fn mac_address() -> [u8; 6];
/// Decompose initialized board into peripheral bundles for each task. /// 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`]. /// 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 radio: R,
pub host: C, pub host: C,
pub display: Option<D>, pub display: Option<D>,
pub led: L,
pub mac: [u8; 6], pub mac: [u8; 6],
} }
+9 -1
View File
@@ -21,6 +21,8 @@ pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
pub type UsbDriver = mcu::UsbNrfDriver; pub type UsbDriver = mcu::UsbNrfDriver;
pub type DisplayI2c = mcu::I2cBus; pub type DisplayI2c = mcu::I2cBus;
pub type LedDriver = crate::driver::simple_led::SimpleLed<Output<'static>>;
pub type DisplayDriver = crate::driver::sh1106::Sh1106<DisplayI2c>; pub type DisplayDriver = crate::driver::sh1106::Sh1106<DisplayI2c>;
// ── Peripheral bundles ────────────────────────────────────────────── // ── Peripheral bundles ──────────────────────────────────────────────
@@ -66,6 +68,7 @@ impl LoRaBoard for Board {
type CommParts = UsbParts; type CommParts = UsbParts;
type DisplayParts = DisplayParts; type DisplayParts = DisplayParts;
type DisplayDriver = DisplayDriver; type DisplayDriver = DisplayDriver;
type LedDriver = LedDriver;
fn init() -> Self { fn init() -> Self {
let mut config = embassy_nrf::config::Config::default(); let mut config = embassy_nrf::config::Config::default();
@@ -79,7 +82,7 @@ impl LoRaBoard for Board {
mcu::mac_address() mcu::mac_address()
} }
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> { fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
let p = self.p; let p = self.p;
// ── SPI bus for SX1262 ────────────────────────────────── // ── SPI bus for SX1262 ──────────────────────────────────
@@ -129,10 +132,15 @@ impl LoRaBoard for Board {
); );
let display = Some(DisplayParts { i2c }); 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 { BoardParts {
radio, radio,
host, host,
display, display,
led,
mac: Self::mac_address(), mac: Self::mac_address(),
} }
} }
+30 -5
View File
@@ -6,11 +6,9 @@ use embassy_time::Timer;
use embedded_graphics::draw_target::DrawTarget; use embedded_graphics::draw_target::DrawTarget;
use embedded_graphics::pixelcolor::BinaryColor; 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::channel::{DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch};
use crate::board::{Board, LoRaBoard};
use render::RSSI_HISTORY_LEN; use render::RSSI_HISTORY_LEN;
const BOARD_NAME: &str = Board::NAME; 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. /// Render the appropriate screen for the current state into the display buffer.
fn render_current( fn render_current(
display: &mut impl DrawTarget<Color = BinaryColor>, display: &mut impl DrawTarget<Color = BinaryColor>,
@@ -103,6 +109,7 @@ fn render_current(
#[task] #[task]
pub async fn display_task( pub async fn display_task(
parts: DisplayParts, parts: DisplayParts,
mut led: LedDriver,
status: &'static StatusWatch, status: &'static StatusWatch,
display_commands: &'static DisplayCommandChannel, display_commands: &'static DisplayCommandChannel,
) { ) {
@@ -150,19 +157,35 @@ pub async fn display_task(
if state.disconnected { if state.disconnected {
continue; 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 let Some(rssi) = radio_status.last_rssi {
if radio_status.rx_count != state.last_status.rx_count { if rx_packet {
state.record_rssi(rssi); state.record_rssi(rssi);
} }
} }
if radio_status.tx_count != state.last_status.tx_count { if tx_packet {
state.record_tx(); state.record_tx();
} }
let last_snr = radio_status.last_snr;
state.last_status = radio_status; state.last_status = radio_status;
if state.display_on { if state.display_on {
render_current(&mut display, &state, &board_info); render_current(&mut display, &state, &board_info);
let _ = display.flush().await; 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 { Either3::Second(cmd) => match cmd {
@@ -171,6 +194,7 @@ pub async fn display_task(
state.display_on = false; state.display_on = false;
render::blank(&mut display); render::blank(&mut display);
let _ = display.flush().await; let _ = display.flush().await;
led.set_rgb(0, 0, 0).await;
} }
DisplayCommand::On => { DisplayCommand::On => {
state.disconnected = false; state.disconnected = false;
@@ -185,6 +209,7 @@ pub async fn display_task(
state = DisplayState::new(); state = DisplayState::new();
render::splash(&mut display, &board_info); render::splash(&mut display, &board_info);
let _ = display.flush().await; let _ = display.flush().await;
led.set_rgb(0, 0, 0).await;
} }
}, },
Either3::Third(()) => { Either3::Third(()) => {
+3
View File
@@ -1,2 +1,5 @@
#[cfg(feature = "wio_tracker_l1")] #[cfg(feature = "wio_tracker_l1")]
pub mod sh1106; pub mod sh1106;
#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "wio_tracker_l1"))]
pub mod simple_led;
+20
View File
@@ -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();
}
}
}
+1 -1
View File
@@ -96,7 +96,7 @@ async fn run(spawner: Spawner) {
if let Some(dp) = parts.display { if let Some(dp) = parts.display {
spawner spawner
.spawn(display::display_task(dp, &STATUS, &DISPLAY_COMMANDS)) .spawn(display::display_task(dp, parts.led, &STATUS, &DISPLAY_COMMANDS))
.expect("spawn display_task"); .expect("spawn display_task");
} }
} }