35 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[1.5.2] - 2026-05-11
Changed
- Refreshed
Cargo.lockto pull in the latest compatible patch/minor releases of dependencies. Notable bumps:donglora-protocol1.1.0 → 1.2.0esp-hal1.1.0 → 1.1.1heapless0.9.2 → 0.9.3 (propagates through embassy + esp crates)embassy-usb-driver0.2.0 → 0.2.1embassy-usb-synopsys-otg0.3.2 → 0.3.3enumset1.1.10 → 1.1.12digest0.11.2 → 0.11.3,hashbrown0.17.0 → 0.17.1,hybrid-array0.4.11 → 0.4.12,cc1.2.61 → 1.2.62 No source changes; all 10 board targets compile clean and host-side protocol tests pass.
[1.5.1] - 2026-05-11
Fixed
LoRaConfig.sync_wordis now actually applied to the SX126x.reconfigure_radioaccepted the host'ssync_wordfield in name only — the value was stored inself.configbut never written to the chip'sLoRaSyncwordMSB/LoRaSyncwordLSBregister pair (0x0740/0x0741, SX1261/2 datasheet Rev 2.2 §13.4.4). The chip sat on whateverLoRa::new(_, false, _)set at cold init —LORAWAN_PRIVATE_SYNCWORD(byte0x12, register pair0x1424). Everyone got byte0x12regardless of the requested value. Worked by accident for MeshCore (which uses byte0x12); silently broke any host trying to talk to a non-MeshCore network. Fix wirescfg.sync_wordthrough tolora.set_lora_sync_word(byte)afterenter_standby, using the new lora-phy API from lora-rs/lora-rs#433. Theu16protocol field is decoded back to its underlying byte (byte = (hi & 0xF0) | (lo >> 4)afterto_be_bytes()), the inverse of lora-phy'sconvert_sync_wordnibble-expansion. Honors SX1261/2 §8.3.1 (BUSY-low precondition) via the existingensure_ready + conditional set_standbypreamble inLoRa::set_lora_sync_word.- First TX after
SET_CONFIGno longer fails withERR(ERadio)on a freshly-booted chip. The chart'sprep_for_cad_act(TxCadPrepentry) andprep_for_tx_act(TxPrepentry) both readctx.mdltnand emitTransmitFailed→ERR(ERadio)if it'sNone. Butctx.mdltnwas only ever assigned on the RX path —try_start_rx_hw(RxArmingentry) andcache_rx_pkt(RxSessionentry).reconfigure_radioalready calledto_lora_mod_params(lora, cfg)duringSET_CONFIGbut discarded the result vialet _ = …?;(the dropped comment claimed image calibration; the function is pure compute on SX126x). Hidden in practice by any priorRX_START(ai-bot,ping_pong --role rx, etc.) leavingctx.mdltnpopulated until the next reflash. Fix threads theModulationParamsout ofreconfigure_radioas its return value, andapply_set_configcaches it inself.mdltnalongsideself.config. First TX on a fresh boot now works without a precedingRX_START.
Changed
lora-physwaits/lora-rs#fixedbumped frome8a52ebd→043d7659. The fork'sfixedmerge now includes a newfeat/set-sync-wordbranch alongside the three pre-existingfix/*branches. lora-rs/lora-rs#433 adds the runtimeLoRa::set_lora_sync_word(u8)API the radio fix above depends on (was unavailable in stock lora-phy; the only public sync-word entry points werewith_syncword/new(_, public_network: bool, _)at construction time). Strictly additive in lora-phy: pure addition, no behavioral change toinit_lora/with_syncword/ any existing path.hsmc0.5 → 0.6. Patch bump tracked viacargo update -p hsmc; no behavior change in this crate's usage. Pulled in separately from the radio fixes so the dep churn doesn't blur the fix commits.
[1.5.0] - 2026-04-30
Added
-
Heltec Mesh Node T114 board support (nRF52840 + SX1262 + 1.14" 240×135 ST7789 color TFT). The first color display board in the project, and the third nRF52840 + SX1262 board alongside the RAK4631 and Wio Tracker L1.
VEXT_ENABLE(P0.21) driven HIGH at boot to power the SX1262 + TFT rail. Without this, lora-phy init succeeds at the SPI level but TX_DONE never fires.VTFT_CTRLon P0.03 (active-LOW); PWM backlight on P0.15 (active-LOW, inverted) for smooth dim/bright transitions.- TFT SPI2 (SCK P1.08 / MOSI P1.09) at 8 MHz over EasyDMA, async
ExclusiveDevicefromembedded-hal-bus. - Linker uses the shared
ld/nrf52840-memory.xso the S140 SoftDevice region (0x01000–0x26000) is preserved per project policy.
-
BoardDisplaytrait (src/display.rs) bundlesrender_*,present(),set_bright(), andset_dim(). Replaces the previousDrawTarget<Color = BinaryColor>+DisplayBrightnesscombo as theLoRaBoard::DisplayDriverbound, so each board renders in its native pixel format and resolution without aliasing or color-quantization round-trips. Existing mono OLED drivers (Sh1106,Ssd1306Async) get adapter impls that delegate to the existingdisplay::rendermodule — no behavior change for any previously supported board. -
Async ST7789 driver (
src/driver/st7789.rs) wrappinglcd-async(async fork ofmipidsi) with a 64,800-byte raw-byte framebuffer in.bss. Renders vialcd_async::raw_framebuf::RawFrameBuf<Rgb565, _>and presents via a single async DMA blit (panel.show_raw_data(...).await); the await yields the executor for the ~65 ms SPI transfer so the radio task isn't starved during repaints. -
Native 240×135 RGB565 renderer (
src/display/render_color.rs) with splash, dashboard, and sparkline parity with the mono path, plus a shared QR-code blit (src/display/qr.rs) generic overPixelColor. The blit re-centers the 64×64 bitmap (cells in cols 0..50 with ~14 px right-side padding), so both mono and color splash now show the QR with 7 px quiet zones on all four sides (was visibly left-shifted on mono). -
Shared content types (
BoardInfo,DashboardCtx) decouple what to draw from how each board renders it.create_display()now takes the board's fullDisplayPartsbundle (was just the I2C bus), so non-I2C displays can carry SPI + control pins through. -
IDF app descriptor on every ESP image. New
esp-bootloader-esp-idf0.5 dependency (chip-feature-gated like the rest of theesp-*stack) plusesp_app_desc!()insrc/main.rs, which emits the.flash.appdescsection thatespflashv4 requires in every ESP binary.
Changed
-
ESP build modernized to upstream crates.io. Removed the
[patch.crates-io]block that pinnedesp-hal,esp-rtos,esp-backtrace, andesp-printlntoesp-rs/esp-hal#main. The versions tracked there (esp-hal 1.1,esp-rtos 0.3,esp-backtrace 0.19,esp-println 0.17) have all reached crates.io with theembassy-executor0.10 compatibility we needed, so the local fork pins are no longer required.cargo:espflashbumped to v4 inmise.tomlto match. -
hsmc0.5.0 → 0.5.1. Patch bump pulled in transparently with the lockfile refresh; no behavior change. -
Documentation consolidated.
PROTOCOL.mdremoved from the firmware repo (it now lives canonically in the spec repo, linked fromREADME.md).BLE_MUX_DESIGN_DOC.mdmoved todocs/ble_mux_design.mdso all firmware design docs sit underdocs/.
[1.4.1] - 2026-04-29
Fixed
-
Major LoRa reliability boost across every radio family. Switched
lora-phyfrom crates.io to theswaits/lora-rs#fixedfork branch — a merge of three fixes awaiting upstream PR acceptance, pinned ate8a52ebdviaCargo.lock:- #428 — sx126x: pre-command BUSY check + standby/IRQ-clear hygiene around CAD.
- #431 — sx127x:
clear stale IRQ flags in
do_rxbefore mode change. - #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_THROTTLEDwas 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 successfultx.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 forlast_frame_atto age past 1000 ms, cascading a Reset to radio (Unconfigured) and display (splash) and leaving subsequent host TX commands timing out. Restructuredhost_taskinto two sub-loops joined in the same task:read_loopowns rx + watchdog,write_loopowns tx + throttle. Same executor, disjoint borrows on the split rx/tx halves, no Mutex needed. Also reset theFrameDecoderon watchdog fire so a host that resumes mid-frame resyncs cleanly on the next0x00sentinel (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 toFONT_5X8(25 chars). Aconst_assertat theBOARD_NAMEdefinition rejects names that overflow even the fallback, so the board porter shortensNAMEat compile time rather than getting silent overflow at runtime. Also dropped the URL-line font on the "learn more" screen fromFONT_5X8toFONT_4X6sodonglora.comsits inside the 64 px column with breathing room instead of touching both edges.
Changed
hsmc0.4 → 0.5.default(...)is optional on every state in 0.5; collapsed theWaitingForFirstFramesubstate that existed only because 0.4 requiredConnectedto declare a default child.Connecteditself is now the resting "DTR up, no traffic yet" state, withon(FrameReceived) => Activedescending 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 fewerdefault(). 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-gccbin) andLIBCLANG_PATH, so anymise execresolves the linker without sourcing espup's stray~/export-esp.sh. New[tasks.esp-install]invokes espup with the matching crosstool version pin.cargo:espuppinned to0.17.1to match the[env]paths. Triggered by a real failure where~/export-esp.shwas moved to Trash and the ESP build broke withlinker 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-halmain 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 updatehad 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 pinembassy-executor 0.9— mismatched with our0.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 aCLOCK_POWERinterrupt 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-checkpointblink-map coverage at every TX-phase boundary inradio.rs, including a new checkpoint betweenprepare_for_cadandcad()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 everySET_CONFIG. - Retry count =
clamp(floor(BUDGET/T)+1, 1, 10)withBUDGET = 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.rsmodule — host-testable viaDONGLORA_HOST_TEST=1 cargo test(13 tests). - Validated on-device: 20 iterations of the AI Bot test flow that
previously saw ~2
ChannelBusyerrors now sees zero at the same SF/BW.
- 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
Fixed
- First-TX-after-boot reliability on SX1262 boards. Picked up the
upstream
lora-phyfix 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]againstswaits/lora-rs#fix/sx126x-busy-raceuntil 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_readyto 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. Perlora-rs/lora-rs#260, switched toPull::Noneon 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, andjust buildaccept an optional comma-separatedextra_featuresargument that is appended to the board feature, e.g.just flash wio_tracker_l1 debug-checkpoint. No more dropping to rawcargo + rust-objcopy + uf2 cpfor iteration loops. justrecipes now resolve all tools throughmiseviaset shell := ["mise", "exec", "--", "sh", "-c"].cargo,espflash,rust-objcopy,cargo-hex-to-uf2etc. always pick up the versions pinned inmise.tomlregardless ofPATHordering.mise.tomlrust pins now carry ARM targets + clippy, so a freshmise installis sufficient bootstrapping — no manualrustup target addforthumbv6m-none-eabi/thumbv7em-none-eabihfand no separate clippy install.- New
just flash-dfurecipe for the Adafruit nRF52 bootloader (VID:PID239a:0029) viaadafruit-nrfutilunderuvx, with port autodetect. - Per-board
_infochain in the justfile collapsed from a long per-boardif/elifladder + per-board:=variables into a singlecasestatement — 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<P>, a genericOutputPinwrapper that swapsset_high/set_low. Polarity is expressed at the pin (where the physical wiring lives), soSimpleLedstays 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 atLevel::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.rsmodule 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).
- Pin map verified against MeshCore's
- 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 viaDMA_SPI2) andsrc/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<Sx1276>+GenericSx127xInterfaceVariant(DIO0 as the sole IRQ; no BUSY line on SX127x). XTAL clocked (no TCXO),tx_boost = truefor the PA_BOOST path. LoRaBoardtrait gainssupported_sf_bitmap()andsupported_bw_bitmap()methods with SX126x defaults (0x1FE0,0x03FF). T-Beam classic overrides SF to0x1FC0(SF6–SF12) — the SX1276 has no SF5.radio_chip_id()reports0x0011(RadioChipId::Sx1276) andfreq_range_hz()widens to 137 MHz–1020 MHz per the SX1276 datasheet.
- Pin map verified against MeshCore's
Changed
Cargo.toml: theesp32s3chip feature has been moved out of theesp-hal/esp-rtos/esp-backtrace/esp-printlndependency lines and into each ESP board's feature list (asesp-hal/esp32s3, etc.). This letslilygo_tbeamselect the classicesp32chip 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_CONFIGduring RX auto-re-arms") to all failure paths that can occur while a client has RX active:Receiving.on(RxFailed) => Idlenow transitions toResumingRxinstead ofIdle. A transientnext_packeterror re-arms the chip rather than ending the RX session.TransmittingFromRx.on(TransmitFailed) => Idlenow transitions toResumingRx. TX failure during an active RX session returnsTX_FAILEDto the caller and resumes RX, matching the shape of the adjacentChannelBusyhandler.ResumingRx.on(StartRxFailed) => Idleis now a bounded retry (5 × 25 ms backoff via a newbump_resume_retriesaction and aResumeRxRetry/ResumeRxExhaustedpair of internal events). The chip only lands inIdleafter 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 fireRadioEvent::Idle, which the display uses as itsDashboard → Splashtrigger; the radio would land inIdlewith no client aware enough to re-sendRX_START, stranding the dashboard until the session was manually torn down and restarted. -
Removed
announce_rx_failedhelper action: its only purpose was emittingRadioEvent::Idleon the now-deletedReceiving.on(RxFailed) => Idleedge. No replacement needed —ResumingRxhandles 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_dimis fired as an action by the 3 s timer handler rather than as a siblingexit:action, so mode-change leaf-crossings never see a flash. DisplayBrightnesstrait insrc/driver/mod.rsunifies the two-level backlight API across thessd1306crate'sSsd1306Asyncand the in-treeSh1106driver. Contrast values centralized asCONTRAST_BRIGHT(0xFF) andCONTRAST_DIM(0x08) constants so the two levels can be tuned in one place.
Changed
src/display.rsstatechart restructured:Splashgainsentry: set_brightplus an action-onlyon(after 10s) => set_dimhandler so the splash bright window is time-scoped without disturbing the LM↔Info alternation.DashboardListeningandTransmittingeach gain a nested*Brightsubstate owning theentry: set_brightaction. 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 firesset_dimas an action and up-transitions to the leaf, parking the machine at the leaf with no active child.
On.on(CmdReset)now targetsLearnMoredirectly rather thanSplash. Under hsmc 0.2 up-transition semantics, targetingSplashfrom withinSplash.LearnMore(USB-disconnect race path) would only unwind belowSplashand park with no active child, leaving the LM↔Info alternation timer dead and the QR frozen on screen.LoRaBoard::DisplayDrivertrait bound extended with+ DisplayBrightnessso brightness is part of the board contract alongsideDrawTarget<Color = BinaryColor>.
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_m2now matches the CH340K / CH340 VID:PIDs directly instead of falling through to espflash auto-detect.
Dependencies
hsmcbumped to0.2to 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: insidestate Receiving, addon(ConfigApplied) => ResumingRxto shadow the parentConfiguredstate'son(ConfigApplied) => Idlehandler. The reconfigure path now runsapply_set_config→ConfigApplied→ResumingRx.entry: try_start_rx_hw→StartRxOk→Receivingwithout ever enteringIdle.- The OLED's
Dashboard → Splashtransition was previously driven byRadioEvent::Idlefiring onConfigured.Idle.entry. That event no longer fires during a SET_CONFIG-from-Receiving sequence, so the display stays on Dashboard.
Dependencies
donglora-protocolbumped to1.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_STARTafter everySET_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 followsPROTOCOL.mdv1.0. - Host framing replaced.
src/host/framing.rsnow drives a streamingFrameDecoderfromdonglora-protocoland emits(type_id, tag, payload)frames; the 0.x COBS-per-response shim is gone. - Radio task reshape.
src/radio.rsemits v2TX_DONE/SetConfigResult/RxPayload/RxOriginshapes directly; no 0.x translation layer remains.
Changed
hsmcswitched from path-dep on the local workspace toversion = "0.1.0"from crates.io.donglora-protocolswitched from path-dep on../protocoltoversion = "1.0.0"from crates.io; firmware builds are now reproducible from a clean checkout with no sibling repo required.PROTOCOL.mdupdated to the DongLoRa Protocol v2 normative spec, kept byte-for-byte in sync with thedonglora-protocolcrate's copy.
Infrastructure
- Cargo.toml
repository+homepagenow point at the canonicalhttps://github.com/donglora/firmware(theswaits/donglorarepo 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-checkpointcargo feature with a dedicateddebug_blinktask 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
hsmcstatechart (Unconfigured/Configured.{Idle, StartingRx, Receiving, ResumingRx, Transmitting.{FromIdle, FromRx}}). The main loop is now a singlemachine.run().await; command reading, packet RX, and TX are state-scopedduring:activities. Post-TX RX re-arm is modeled as state identity (TransmittingFromRx→ResumingRx) instead of awas_receivingflag. - Display task rewritten as an
hsmcstatechart (Off/On.{Splash.{LearnMore, Info}, Dashboard.{Listening, Transmitting}}). The 5 s splash alternation and 1 s sparkline tick useon(after …)/on(every …)transitions — no external timer task. - USB host task now carries a small
hsmcstatechart (Disconnected/Connected) whose entry/exit actions handle display wake, radioStopRx, and display reset — replacing the open-codedwas_connectededge detector. - Radio → Display channel switched from a
RadioStatuswatch to a typedRadioEventqueue. The radio emits discreteEnteredRx/EnteredTx/Idle/PacketRx/PacketTx/ConfigChangedevents; downstream consumers observe transitions directly rather than diffing snapshots. - Wire protocol types and COBS framing moved out of firmware into the
shared
donglora-protocolcrate (path dep on../protocol). Firmwaresrc/protocol.rsis now a re-export shim preserving every call site;src/host/framing.rskeeps only the embassy-specificroute_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
- Pin map verified against Meshtastic's
Changed
hal::esp32s3::{init_spi, init_i2c}andboard::esp32s3::init_radioare now generic over GPIO type, so ESP32-S3 boards with different pin assignments can share the helpers.board::esp32s3::init_radiotakes aTcxoCtrlVoltageparameter instead of hardcodingCtrl1V8— needed for ThinkNode-M2's 3.3 V TCXO.- SSD1306 display items in
board::esp32s3are now cfg-gated to Heltec boards (ThinkNode-M2 supplies its own SH1106create_display). justfileusescargo +espfor Xtensa targets instead ofcargo +nightlywith aRUSTC=espoverride — 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<SpawnToken, SpawnError>,Spawner::spawnno longer returns Result) - Adapted to esp-hal main branch API changes:
efuse::base_mac_address(),esp_rtos::startnow requiresSoftwareInterrupt, embassy-rp SPI constructor now requires DMA interrupt bindings just checknow runs clippy with-D warningsinstead of plaincargo check- Rust toolchain bumped to 1.94 in mise.toml
Fixed
core::mem::forget(output)pattern in ESP32-S3 board files is nowlet _vext = Output::new(...)— current esp-hal makesOutputnon-Drop, which madeforgeta no-op and tripped clippy'sforget_non_droplint.- UART boards (heltec_v3_uart) display stuck on splash screen after boot.
Host task sent
DisplayCommand::Reset(which sets disconnected=true) instead ofDisplayCommand::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.rsmodule 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-vtormoved from global dependency to per-board feature (Cortex-M0+ has no VTOR register)build.rsmemory.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
disconnectedflag was not set in theResethandler) - Radio left running after host disconnect, now sends
StopRxwhen DTR drops - System deadlock when host app closes serial port while radio is receiving
(unsolicited responses in radio task now use
try_sendto 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_uartfeature for stock (unmodified) V3 boards via CP2102 bridgeRgbLedtrait andSimpleLeddriver for GPIO-based LEDsLoRaBoardtrait with full associated types (RadioParts,CommParts,DisplayParts,DisplayDriver,LedDriver) andBoardParts<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 dispatchboard/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/anddisplay/directories (no single-file directories) build.rsauto-discovers boards by content (implementsLoRaBoard) and helper modules byuse super::imports — no exclusion listscheckandclippycommands now use--releaseprofile- 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, PID5741) - 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_MAXsentinel (-128): auto-use the board's maximum TX power- Board support: Heltec V3, Heltec V4, RAK WisBlock 4631
- Python host library (
dongloraon PyPI) withpip installanddonglora-muxCLI - 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
.rsfile + one Cargo feature per board