Rewrites lingering github.com / *.sr.ht references to this Gitea instance, translating GitHub and Sourcehut URL shapes to Gitea's (/blob/<ref>/ and sr.ht /tree/<ref>/item/ -> /src/branch/<ref>/, /tree/<tag> -> /src/tag/<tag>, /actions/workflows/X -> /actions?workflow=X, /edit/ -> /_edit/). Left pointing at the old hosts on purpose: pull-request links (PRs were not migrated), forks that were never migrated, and GitHub-only surfaces Gitea lacks. OpenPGP identity proofs are untouched by design.
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://git.swaits.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