# Changelog
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/).
## [Unreleased]
## [1.4.1] - 2026-04-29
### Fixed
- **Major LoRa reliability boost across every radio family.** Switched
`lora-phy` from crates.io to the
[`swaits/lora-rs#fixed`](https://github.com/swaits/lora-rs/tree/fixed)
fork branch — a merge of three fixes awaiting upstream PR acceptance,
pinned at `e8a52ebd` via `Cargo.lock`:
- [#428](https://github.com/lora-rs/lora-rs/pull/428) — sx126x:
pre-command BUSY check + standby/IRQ-clear hygiene around CAD.
- [#431](https://github.com/lora-rs/lora-rs/pull/431) — sx127x:
clear stale IRQ flags in `do_rx` before mode change.
- [#432](https://github.com/lora-rs/lora-rs/pull/432) — sx127x:
clear stale IRQ flags before reconfiguring DIO mapping in
`set_irq_params`.
Stability gain across the fleet: the sx126x BUSY-race fix lands on
every nRF / RP2040 board, and the sx127x IRQ-clear fixes unblock the
LilyGo T-Beam (classic) in particular.
- **UART host RX delivery on UART-only boards.** `RADIO_THROTTLED` was
never cleared on UART (`heltec_v3_uart`, `elecrow_thinknode_m2`,
`lilygo_tbeam`) — the throttle gate added in v1.3.0's chart-first
restructure silently dropped every async RX from boot. Mirrored the
USB pattern: clear the throttle on successful `tx.write_all`, re-arm
on failure.
- **UART host watchdog spurious-trip under saturated bidirectional
load.** The `select3`-based loop serialised read and write;
`tx.write_all` (~25 ms per max frame at 115200 baud) blocked the read
poll long enough for `last_frame_at` to age past 1000 ms, cascading a
Reset to radio (`Unconfigured`) and display (splash) and leaving
subsequent host TX commands timing out. Restructured `host_task` into
two sub-loops joined in the same task: `read_loop` owns rx +
watchdog, `write_loop` owns tx + throttle. Same executor, disjoint
borrows on the split rx/tx halves, no Mutex needed. Also reset the
`FrameDecoder` on watchdog fire so a host that resumes mid-frame
resyncs cleanly on the next `0x00` sentinel (USB gets this for free
via DTR-drop reset; UART has no DTR).
- **Splash board-name overflow on long names.** Names that don't fit
`FONT_6X10` (21 ASCII chars on a 128 px-wide OLED) now fall back to
`FONT_5X8` (25 chars). A `const_assert` at the `BOARD_NAME`
definition rejects names that overflow even the fallback, so the
board porter shortens `NAME` at compile time rather than getting
silent overflow at runtime. Also dropped the URL-line font on the
"learn more" screen from `FONT_5X8` to `FONT_4X6` so `donglora.com`
sits inside the 64 px column with breathing room instead of touching
both edges.
### Changed
- **`hsmc` 0.4 → 0.5.** `default(...)` is optional on every state in
0.5; collapsed the `WaitingForFirstFrame` substate that existed only
because 0.4 required `Connected` to declare a default child.
`Connected` itself is now the resting "DTR up, no traffic yet" state,
with `on(FrameReceived) => Active` descending into the
inactivity-timer state on the first byte. Behavior is identical
(entry/exit chain math unchanged); the host/usb chart has one fewer
state and one fewer `default()`. Display and Radio charts left as-is
— their defaults serve real grouping/lifecycle purposes, not 0.4
artifacts.
- **ESP cross-toolchain pinned in `mise.toml`.** `[env]` now owns the
PATH (`xtensa-esp-elf-gcc` bin) and `LIBCLANG_PATH`, so any `mise
exec` resolves the linker without sourcing espup's stray
`~/export-esp.sh`. New `[tasks.esp-install]` invokes espup with the
matching crosstool version pin. `cargo:espup` pinned to `0.17.1` to
match the `[env]` paths. Triggered by a real failure where
`~/export-esp.sh` was moved to Trash and the ESP build broke with
`linker xtensa-esp32s3-elf-gcc not found`. Bumping ESP versions
remains a two-touch change: edit version dirs in BOTH
`[tasks.esp-install]` and `[env]`.
- **ESP dependency requirements bumped** to track the upstream
`esp-rs/esp-hal` main branch: `esp-hal 1.0 → 1.1`, `esp-rtos 0.2 →
0.3`, `esp-backtrace 0.18 → 0.19`, `esp-println 0.16 → 0.17`. `cargo
update` had silently invalidated the `[patch.crates-io]` entries (the
patched versions were no longer semver-compatible with the old
top-level requirements), dropping the patches and falling back to
crates.io releases that pin `embassy-executor 0.9` — mismatched with
our `0.10`. Catching this keeps the patch suite consistent until the
planned `[patch.crates-io]`-removal pass.
## [1.4.0] - 2026-04-25
### Added
- **RAK4631 WisBlock board support** (nRF52840 + SX1262, RAK19007 base
+ RAK4631 Core + RAK1921 OLED). The second nRF52840 + SX1262 board
alongside the Wio Tracker L1.
- `SX126X_POWER_EN` (P1.05) driven HIGH at init — the radio is
power-gated on this module; without this the chip has no power
and SPI hangs.
- HFXO + LFXO external crystals selected in `embassy_nrf::Config`.
USB CDC-ACM needs ±500 ppm which the internal RC oscillator
can't provide.
- `SoftwareVbusDetect(true, true)` so USB enumerates without a
`CLOCK_POWER` interrupt handler.
- Green LED on P1.03 (RAK19007 Base LED1, active-HIGH) via
`SimpleLed`.
- Hardware-validated end-to-end: ADVERT round-trip with an external
receiver on US915.
- Added `debug-checkpoint` blink-map coverage at every TX-phase
boundary in `radio.rs`, including a new checkpoint between
`prepare_for_cad` and `cad()` so future bring-ups can distinguish
a modem-setup hang from a DIO1-wait hang.
- **Adaptive listen-before-talk.** The CAD pre-TX retry loop scales to
the active LoRa config instead of running a fixed 10 retries × 10 ms
(which was roughly one max-length packet at SF7/BW500 and a no-op at
SF12/BW125).
- Per-retry sleep is sized to the time-on-air of an assumed-peer
80-byte packet under the current SF/BW/CR — cached on
`RadioContext`, recomputed on every `SET_CONFIG`.
- Retry count = `clamp(floor(BUDGET/T)+1, 1, 10)` with `BUDGET = 2.5
s`. Fast configs still get up to 10 retries; SF11/12 degrade to
single-CAD-and-bail (logged at config-apply).
- Sleeps jittered ±20% per iteration to decorrelate
simultaneously-backing-off contenders.
- ToA math, CR→{1..4} mapping, retry-count derivation, and jitter
helper live in a pure `src/lbt.rs` module — host-testable via
`DONGLORA_HOST_TEST=1 cargo test` (13 tests).
- Validated on-device: 20 iterations of the AI Bot test flow that
previously saw ~2 `ChannelBusy` errors now sees zero at the same
SF/BW.
### Fixed
- **First-TX-after-boot reliability on SX1262 boards.** Picked up the
upstream `lora-phy` fix that moves the BUSY-handshake check from
*after* to *before* each SPI op, per Semtech SX1261/2 §8.3.1, LR1121
UM §3, and LR1110 DS §1.2.5. The existing post-op wait is preserved
as the belt that closes the BUSY-assertion latency window
(T_SW ≤ 600 ns) that a single level-poll can miss. Consumed via
`[patch.crates-io]` against `swaits/lora-rs#fix/sx126x-busy-race`
until the upstream PR merges.
- Hardware-verified on Seeed Wio Tracker L1: 20/20 clean-boot TX
(vs ~50% before).
- Renames `InterfaceVariant::wait_on_busy` → `wait_until_ready` to
match the new semantics.
- **BUSY pin pull-down → none on nRF SX1262 boards.** SX1262's BUSY
pin has an internal 20 kΩ pull-up (datasheet p. 53); our
`Input::new(..., Pull::Down)` was contending with it, leaving BUSY
at an indeterminate voltage and intermittently mis-reading the
chip's ready state. Per `lora-rs/lora-rs#260`, switched to
`Pull::None` on both Wio Tracker L1 and RAK4631. The RAK4631
appeared to work before, but was affected by the same class of bug
per the upstream issue — now deterministically correct rather than
"happens to work".
### Changed
- **Build-system DX.** `just flash`, `just flash-probe`, and
`just build` accept an optional comma-separated `extra_features`
argument that is appended to the board feature, e.g. `just flash
wio_tracker_l1 debug-checkpoint`. No more dropping to raw
`cargo + rust-objcopy + uf2 cp` for iteration loops.
- **`just` recipes now resolve all tools through `mise`** via
`set shell := ["mise", "exec", "--", "sh", "-c"]`. `cargo`,
`espflash`, `rust-objcopy`, `cargo-hex-to-uf2` etc. always pick up
the versions pinned in `mise.toml` regardless of `PATH` ordering.
- **`mise.toml` rust pins now carry ARM targets + clippy**, so a fresh
`mise install` is sufficient bootstrapping — no manual
`rustup target add` for `thumbv6m-none-eabi` /
`thumbv7em-none-eabihf` and no separate clippy install.
- **New `just flash-dfu` recipe** for the Adafruit nRF52 bootloader
(VID:PID `239a:0029`) via `adafruit-nrfutil` under `uvx`, with port
autodetect.
- **Per-board `_info` chain in the justfile** collapsed from a long
per-board `if/elif` ladder + per-board `:=` variables into a single
`case` statement — one place per board, two lists (names +
metadata).
## [1.3.1] - 2026-04-23
### Fixed
- **LilyGo T-Beam (classic) red LED now flashes on TX/RX** instead of
dimming off. The red LED under the OLED is wired active-LOW on this
board (anode to V+, cathode to GPIO4); v1.3.0 drove it as active-HIGH,
so the LED sat lit at idle and briefly dimmed during activity.
- Empirically verified on hardware against both T-Beam variants.
- Introduces `src/driver/inverted_pin.rs::InvertedPin
`, a generic
`OutputPin` wrapper that swaps `set_high`/`set_low`. Polarity is
expressed at the pin (where the physical wiring lives), so
`SimpleLed` stays polarity-agnostic and every other board's code
path is byte-for-byte unchanged.
- Classic T-Beam now constructs its LED as
`SimpleLed(InvertedPin(led_pin))` with pin init at `Level::High`
(dark at boot).
## [1.3.0] - 2026-04-23
### Added
- LilyGo T-Beam S3 Supreme board support (ESP32-S3 + SX1262 + SH1106
OLED + AXP2101 PMIC, native USB-C).
- Pin map verified against MeshCore's
`variants/lilygo_tbeam_supreme_SX1262`.
- New `src/driver/axp.rs` module with an AXP2101 init sequence
(ALDO3 = 3.3 V LoRa, ALDO1 = 3.3 V OLED). The PMIC gates LoRa and
display power — without it the SX1262 never responds on SPI.
- PMIC lives on a dedicated I²C1 bus (SDA=42, SCL=41), separate
from the SH1106 on I²C0 (SDA=17, SCL=18). I²C1 is used blocking
for a one-shot init and then dropped.
- TCXO driven at 1.6 V via DIO3 (MeshCore uses the same).
- LilyGo T-Beam (classic) board support (ESP32 + SX1276 + SSD1306
OLED + AXP192 PMIC, CP2102 USB-UART bridge). This is the first
board in the firmware on classic ESP32 (non-S3) and the first on
the SX127x radio family.
- Pin map verified against MeshCore's `variants/lilygo_tbeam_SX1276`.
- New `src/hal/esp32.rs` (classic-ESP32 MCU primitives, peripheral
DMA via `DMA_SPI2`) and `src/board/esp32.rs` (SX1276 radio helper
+ SSD1306 display pipe).
- AXP192 init added to `src/driver/axp.rs`: DCDC1 = 3.3 V for the
OLED, LDO2 = 3.3 V for the SX1276 PA_BOOST rail. Shares the
OLED I²C bus (SDA=21, SCL=22); the I²C instance is built in
blocking mode for PMIC init and converted to async for the
display task.
- SX127x support via lora-phy 3's `Sx127x` +
`GenericSx127xInterfaceVariant` (DIO0 as the sole IRQ; no
BUSY line on SX127x). XTAL clocked (no TCXO), `tx_boost = true`
for the PA_BOOST path.
- `LoRaBoard` trait gains `supported_sf_bitmap()` and
`supported_bw_bitmap()` methods with SX126x defaults
(`0x1FE0`, `0x03FF`). T-Beam classic overrides SF to `0x1FC0`
(SF6–SF12) — the SX1276 has no SF5.
- `radio_chip_id()` reports `0x0011` (`RadioChipId::Sx1276`) and
`freq_range_hz()` widens to 137 MHz–1020 MHz per the SX1276
datasheet.
### Changed
- `Cargo.toml`: the `esp32s3` chip feature has been moved out of
the `esp-hal` / `esp-rtos` / `esp-backtrace` / `esp-println`
dependency lines and into each ESP board's feature list (as
`esp-hal/esp32s3`, etc.). This lets `lilygo_tbeam` select the
classic `esp32` chip flavour from the same dependency set.
## [1.2.1] - 2026-04-23
### Fixed
- **Radio no longer drops to Idle mid-RX-session on transient failures.**
Extends the v1.1.0 invariant ("`SET_CONFIG` during RX auto-re-arms")
to all failure paths that can occur while a client has RX active:
- `Receiving.on(RxFailed) => Idle` now transitions to `ResumingRx`
instead of `Idle`. A transient `next_packet` error re-arms the chip
rather than ending the RX session.
- `TransmittingFromRx.on(TransmitFailed) => Idle` now transitions to
`ResumingRx`. TX failure during an active RX session returns
`TX_FAILED` to the caller and resumes RX, matching the shape of the
adjacent `ChannelBusy` handler.
- `ResumingRx.on(StartRxFailed) => Idle` is now a bounded retry
(5 × 25 ms backoff via a new `bump_resume_retries` action and a
`ResumeRxRetry` / `ResumeRxExhausted` pair of internal events).
The chip only lands in `Idle` after exhausting the retry budget —
which means it's genuinely wedged and needs reconfiguring.
Visible effect: the OLED dashboard no longer bounces to Splash when
`ai-bot` (or any other client that TXes while RX is running) sends a
packet. Previously any of the three transitions above would fire
`RadioEvent::Idle`, which the display uses as its `Dashboard → Splash`
trigger; the radio would land in `Idle` with no client aware enough
to re-send `RX_START`, stranding the dashboard until the session was
manually torn down and restarted.
- Removed `announce_rx_failed` helper action: its only purpose was
emitting `RadioEvent::Idle` on the now-deleted `Receiving.on(RxFailed)
=> Idle` edge. No replacement needed — `ResumingRx` handles recovery
silently.
## [1.2.0] - 2026-04-22
### Added
- **OLED auto-dim after inactivity.** The display is bright on any
activity — splash entry, mode change into a dashboard leaf, packet
RX/TX — and dims after a short idle window. Splash holds bright for
10 s, then drops to dim while the LearnMore / Info carousel keeps
alternating. The dashboard stays bright for 3 s after the last
packet or mode change, then dims in place without changing
Listening/Transmitting state. Cross-leaf TX↔RX hops transition
bright→bright with no intermediate dim — `set_dim` is fired as an
action by the 3 s timer handler rather than as a sibling `exit:`
action, so mode-change leaf-crossings never see a flash.
- `DisplayBrightness` trait in `src/driver/mod.rs` unifies the
two-level backlight API across the `ssd1306` crate's `Ssd1306Async`
and the in-tree `Sh1106` driver. Contrast values centralized as
`CONTRAST_BRIGHT` (`0xFF`) and `CONTRAST_DIM` (`0x08`) constants so
the two levels can be tuned in one place.
### Changed
- `src/display.rs` statechart restructured:
- `Splash` gains `entry: set_bright` plus an action-only
`on(after 10s) => set_dim` handler so the splash bright window
is time-scoped without disturbing the LM↔Info alternation.
- `Dashboard` `Listening` and `Transmitting` each gain a nested
`*Bright` substate owning the `entry: set_bright` action. Packet
handlers moved into the leaves and target the leaf's `*Bright` —
sibling-to-sibling within the leaf restarts the 3 s timer without
re-entering the leaf itself (paint / 1 s sparkline timer
preserved). The 3 s timeout handler fires `set_dim` as an action
and up-transitions to the leaf, parking the machine at the leaf
with no active child.
- `On.on(CmdReset)` now targets `LearnMore` directly rather than
`Splash`. Under hsmc 0.2 up-transition semantics, targeting
`Splash` from within `Splash.LearnMore` (USB-disconnect race path)
would only unwind below `Splash` and park with no active child,
leaving the LM↔Info alternation timer dead and the QR frozen on
screen.
- `LoRaBoard::DisplayDriver` trait bound extended with
`+ DisplayBrightness` so brightness is part of the board contract
alongside `DrawTarget`.
### Fixed
- Elecrow ThinkNode-M2 board doc corrected: the shipping revision
carries a WCH CH340K USB-UART bridge (USB `1a86:7522`), not a
Silicon Labs CP2102. `just flash elecrow_thinknode_m2` now matches
the CH340K / CH340 VID:PIDs directly instead of falling through
to espflash auto-detect.
### Dependencies
- `hsmc` bumped to `0.2` to pick up the up-transition fix (transition
to an already-active ancestor unwinds only the subtree strictly
below the target; no exit/entry on the target; no default descent).
## [1.1.0] - 2026-04-22
Small but high-value behavior change: `SET_CONFIG` issued while the
radio is in continuous RX no longer drops the radio (and the OLED)
back to idle. The radio now auto-re-arms RX with the new modulation
once reconfiguration completes, matching DongLoRa Protocol 1.1's
§3.5 / §6.3 amendments.
### Changed
- `src/radio.rs`: inside `state Receiving`, add `on(ConfigApplied)
=> ResumingRx` to shadow the parent `Configured` state's
`on(ConfigApplied) => Idle` handler. The reconfigure path now
runs `apply_set_config` → `ConfigApplied` → `ResumingRx.entry:
try_start_rx_hw` → `StartRxOk` → `Receiving` without ever
entering `Idle`.
- The OLED's `Dashboard → Splash` transition was previously driven
by `RadioEvent::Idle` firing on `Configured.Idle.entry`. That
event no longer fires during a SET_CONFIG-from-Receiving
sequence, so the display stays on Dashboard.
### Dependencies
- `donglora-protocol` bumped to `1.1.0` (spec-only change; matches
the new normative text the firmware now implements).
### Use-case unlock
- Scanner / time-division / dynamic-modulation workflows that
rotate LoRa parameters mid-RX no longer need to re-issue
`RX_START` after every `SET_CONFIG`. One call, radio continues
to receive.
## [1.0.0] - 2026-04-22
The 1.0 release. Firmware now speaks DongLoRa Protocol v2 end to end,
depends on the freshly-published `donglora-protocol` 1.0.0 and `hsmc`
0.1.0 from crates.io, and ships GitHub Release binaries for every
supported board.
### Breaking
- **DongLoRa Protocol v2 wire format** (wire-incompatible with 0.x
clients and firmware). Every `type_id`, tag-correlation rule, and
field layout follows `PROTOCOL.md` v1.0.
- **Host framing replaced.** `src/host/framing.rs` now drives a
streaming `FrameDecoder` from `donglora-protocol` and emits
`(type_id, tag, payload)` frames; the 0.x COBS-per-response shim is
gone.
- **Radio task reshape.** `src/radio.rs` emits v2 `TX_DONE` /
`SetConfigResult` / `RxPayload` / `RxOrigin` shapes directly; no 0.x
translation layer remains.
### Changed
- `hsmc` switched from path-dep on the local workspace to
`version = "0.1.0"` from crates.io.
- `donglora-protocol` switched from path-dep on `../protocol` to
`version = "1.0.0"` from crates.io; firmware builds are now
reproducible from a clean checkout with no sibling repo required.
- `PROTOCOL.md` updated to the DongLoRa Protocol v2 normative spec,
kept byte-for-byte in sync with the `donglora-protocol` crate's
copy.
### Infrastructure
- Cargo.toml `repository` + `homepage` now point at the canonical
`https://github.com/donglora/firmware` (the `swaits/donglora`
repo is archived).
## [0.5.0] - 2026-04-16
### Added
- BLE soft-mesh multiplexer design document (`BLE_MUX_DESIGN_DOC.md`) —
specification for turning a dongle from a 1:1 USB device into a shared
near-field radio gateway for multiple BLE-attached phones. Three-phase
rollout (v1 MVP, v2 hardening, v3 multi-dongle mesh) with regulatory
duty-cycle and TX-access-control enforcement from v1. Targets nRF52840
first; ESP32-S3 reserved for v2.
- `debug-checkpoint` cargo feature with a dedicated `debug_blink` task and
a latching atomic checkpoint — LED-pattern diagnostic for UART-only
boards where defmt output would corrupt the host protocol. Zero
footprint when the feature is off.
### Changed
- Radio task rewritten as an `hsmc` statechart (`Unconfigured` /
`Configured.{Idle, StartingRx, Receiving, ResumingRx,
Transmitting.{FromIdle, FromRx}}`). The main loop is now a single
`machine.run().await`; command reading, packet RX, and TX are
state-scoped `during:` activities. Post-TX RX re-arm is modeled as
state identity (`TransmittingFromRx` → `ResumingRx`) instead of a
`was_receiving` flag.
- Display task rewritten as an `hsmc` statechart (`Off` /
`On.{Splash.{LearnMore, Info}, Dashboard.{Listening, Transmitting}}`).
The 5 s splash alternation and 1 s sparkline tick use `on(after …)` /
`on(every …)` transitions — no external timer task.
- USB host task now carries a small `hsmc` statechart (`Disconnected` /
`Connected`) whose entry/exit actions handle display wake, radio
`StopRx`, and display reset — replacing the open-coded `was_connected`
edge detector.
- Radio → Display channel switched from a `RadioStatus` watch to a typed
`RadioEvent` queue. The radio emits discrete `EnteredRx` / `EnteredTx`
/ `Idle` / `PacketRx` / `PacketTx` / `ConfigChanged` events; downstream
consumers observe transitions directly rather than diffing snapshots.
- Wire protocol types and COBS framing moved out of firmware into the
shared `donglora-protocol` crate (path dep on `../protocol`). Firmware
`src/protocol.rs` is now a re-export shim preserving every call site;
`src/host/framing.rs` keeps only the embassy-specific `route_command`.
### Fixed
- USB disconnect recovery: on a read error the DTR-edge-detect branch
now runs unconditionally (was early-continuing in some paths, starving
the display-reset / radio-stop handoff).
## [0.4.0] - 2026-04-11
### Added
- ELECROW ThinkNode-M2 board support (ESP32-S3 + SX1262 + 1.3" SH1106 OLED
via CP2102 UART bridge, 1000 mAh Li-Po handheld)
- Pin map verified against Meshtastic's
`variants/esp32s3/ELECROW-ThinkNode-M2`
- DIO3 TCXO driven at 3.3 V (not 1.8 V like Heltec)
- Dedicated `LORA_POWER_EN` (GPIO48) sequenced HIGH before SPI init
- `VEXT_ENABLE` (GPIO46) is active HIGH — opposite polarity from Heltec
- Reuses the existing async SH1106 driver at I2C address 0x3C
### Changed
- `hal::esp32s3::{init_spi, init_i2c}` and `board::esp32s3::init_radio` are
now generic over GPIO type, so ESP32-S3 boards with different pin
assignments can share the helpers.
- `board::esp32s3::init_radio` takes a `TcxoCtrlVoltage` parameter instead
of hardcoding `Ctrl1V8` — needed for ThinkNode-M2's 3.3 V TCXO.
- SSD1306 display items in `board::esp32s3` are now cfg-gated to Heltec
boards (ThinkNode-M2 supplies its own SH1106 `create_display`).
- `justfile` uses `cargo +esp` for Xtensa targets instead of `cargo +nightly`
with a `RUSTC=esp` override — the self-contained esp toolchain keeps its
cargo, rustc, and rust-src versions aligned. Public nightly's rust-src
had drifted ahead of the esp fork, breaking `-Zbuild-std`.
- Migrated to embassy-executor 0.10 and embassy-rp 0.10
- ESP crates (esp-hal, esp-rtos, esp-backtrace, esp-println) patched from
esp-rs/esp-hal main branch until esp-rtos publishes with 0.10 support
- Adapted to embassy-executor 0.10 SpawnToken API (task functions now return
`Result`, `Spawner::spawn` no longer returns Result)
- Adapted to esp-hal main branch API changes: `efuse::base_mac_address()`,
`esp_rtos::start` now requires `SoftwareInterrupt`, embassy-rp SPI
constructor now requires DMA interrupt bindings
- `just check` now runs clippy with `-D warnings` instead of plain `cargo check`
- Rust toolchain bumped to 1.94 in mise.toml
### Fixed
- `core::mem::forget(output)` pattern in ESP32-S3 board files is now
`let _vext = Output::new(...)` — current esp-hal makes `Output` non-Drop,
which made `forget` a no-op and tripped clippy's `forget_non_drop` lint.
- UART boards (heltec_v3_uart) display stuck on splash screen after boot.
Host task sent `DisplayCommand::Reset` (which sets disconnected=true) instead
of `DisplayCommand::On`. UART has no DTR, so nothing ever cleared it.
## [0.3.0] - 2026-04-10
### Added
- Waveshare RP2040-LoRa board support (RP2040 + SX1262, no OLED, LED on GPIO25)
- First RP2040 board — third MCU family after ESP32-S3 and nRF52840
- New `hal/rp2040.rs` module with MCU primitives (SPI, USB, flash unique ID)
- USB-C via FPC ribbon cable to adapter board
- PE4259 antenna switch on GPIO17, 850-930 MHz (HF)
### Changed
- `cortex-m-rt/set-vtor` moved from global dependency to per-board feature
(Cortex-M0+ has no VTOR register)
- `build.rs` memory.x selection is now target-aware (thumbv6m vs thumbv7em)
## [0.2.1] - 2026-04-07
### Fixed
- Display stuck on status screen after USB disconnect (stale radio status
updates re-rendered dashboard over splash because `disconnected` flag was
not set in the `Reset` handler)
- Radio left running after host disconnect, now sends `StopRx` when DTR drops
- System deadlock when host app closes serial port while radio is receiving
(unsolicited responses in radio task now use `try_send` to avoid blocking
on a full response channel)
- LED orphaned if OLED display initialization fails (display task now falls
back to a LED-only event loop instead of returning early)
## [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` 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
- Transparent LoRa radio over USB CDC-ACM (VID `1209`, PID `5741`)
- COBS-framed fixed-size LE binary protocol (9 commands, 7 responses)
- Radio configuration: frequency, bandwidth, spreading factor, coding rate, TX power
- Continuous RX mode with RSSI/SNR per packet
- Single-shot TX with optional per-packet config override
- SSD1306 OLED display support with status dashboard
- Radio state, frequency, modulation parameters
- Packet counters (RX/TX)
- RSSI sparkline (1-minute history, TX shown as dotted bars)
- Combined splash/waiting screen on disconnect
- Auto-detect USB host connect/disconnect via DTR
- Config validation against per-board hardware limits
- `TX_POWER_MAX` sentinel (`-128`): auto-use the board's maximum TX power
- Board support: Heltec V3, Heltec V4, RAK WisBlock 4631
- Python host library (`donglora` on PyPI) with `pip install` and `donglora-mux` CLI
- USB multiplexer for sharing one dongle with multiple applications
- Two-way LoRa bridge over TCP (`examples/lora_bridge.py`)
### Technical
- Embassy async runtime (no threads, CPU sleeps when idle)
- Zero-config: boots and waits for host commands
- Protocol-agnostic: firmware has no opinion about what you transmit
- Tasks never panic: all errors reported to host or logged
- Board abstraction: one `.rs` file + one Cargo feature per board