ESP32-S3 + SX1262 + SH1106 OLED + AXP2101 PMIC, native USB-C. The AXP2101 PMIC gates power to the LoRa chip and the OLED. Without its init sequence (ALDO3 = 3.3 V for LoRa, ALDO1 = 3.3 V for OLED) the SX1262 never responds on SPI and radio init fails. Pin map verified against MeshCore's variants/lilygo_tbeam_supreme_SX1262. 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 at 1.6 V via DIO3. New src/driver/axp.rs holds the AXP2101 init sequence — kept minimal (only the LoRa + OLED rails), since DongLoRa is a LoRa-over-USB bridge and every other rail (GPS, BME280, IMU, SD, batt ADC) is out of scope.
17 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]
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
[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