Adds nRF52840 + SX1262 + 1.14" 240×135 ST7789 color TFT support. First
color display board in the project; introduces a BoardDisplay trait so
each board renders to its native pixel format and resolution without
aliasing or color-quantization round-trips.
Architecture:
- New BoardDisplay trait (src/display.rs) bundles render_*, present(),
set_bright/set_dim. Replaces the old DrawTarget<Color=BinaryColor>
+ DisplayBrightness combo as the LoRaBoard::DisplayDriver bound.
- Mono OLED drivers (Sh1106, Ssd1306Async) get adapter impls that
delegate to the existing display::render module — no behavior change
for any existing board.
- T114 ST7789 driver (src/driver/st7789.rs) wraps lcd-async (async fork
of mipidsi) with a 64,800-byte raw byte framebuffer in .bss. Renders
via lcd_async::raw_framebuf::RawFrameBuf<Rgb565, _>, 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.
- PWM backlight on PWM0 / P0.15 (active-LOW, inverted polarity) for
smooth dim/bright transitions instead of GPIO toggle.
- Native 240×135 Rgb565 renderer (src/display/render_color.rs) with
splash + dashboard + sparkline.
- Shared QR-code blit (src/display/qr.rs), generic over PixelColor,
re-centers the 64×64 bitmap which has cells in cols 0..50 and ~14 px
of right-side padding — both mono and color splash now show the QR
with 7 px quiet zones on all four sides (was visibly left-shifted).
- Shared content types (BoardInfo, DashboardCtx) decouple what to draw
from how each board renders it.
- create_display() now takes the board's full DisplayParts bundle
(was just the I2C bus) so non-I2C displays can carry SPI + control
pins through.
Hardware:
- VEXT_ENABLE on P0.21 driven HIGH at boot — powers the SX1262 + TFT
rail. Without this, lora-phy init succeeds at the SPI level but
TX_DONE never fires.
- VTFT_CTRL on P0.03, active LOW; backlight on P0.15, active LOW.
- TFT SPI2 (SCK P1.08 / MOSI P1.09) at 8 MHz over EasyDMA. Async
ExclusiveDevice from embedded-hal-bus (with the 'async' feature).
- lcd-async builder at display_size(135, 240), display_offset(52, 40),
Deg90 rotation, ColorInversion::Inverted — standard 1.14" ST7789
die mapping.
- Linker: shared ld/nrf52840-memory.x (S140 SoftDevice preserved at
0x01000-0x26000 per project policy).
Migrate from hsmc 0.2 (local-patched) to hsmc 0.3.0 (crates.io, with
embassy + trace-defmt features). Restructure radio chart and bundle
several adjacent stability changes that need to land together for
soak runs.
Radio chart:
- Booting (default, no handlers) -> Operational (default Unconfigured;
super-state owning CmdPing/CmdGetInfo/Reset/WedgeDetected handlers
+ Recovering + CheckRecoveryBudget) -> Dead (terminal).
- Cold init's during is structurally un-cancellable.
- radio_task is a 30-line shim that panics if run() returns (chart is
non-terminating).
- Internal queue capacity bumped 8 -> 32 to cover action-emit bursts.
Host chart:
- Connected splits into WaitingForFirstFrame (default, no timer) +
Active (chart-owned 1s inactivity timer via on(after) + self-
transition on FrameReceived). Manual Timer::after + last_frame_at
removed.
Soak hardening:
- ld/nrf52840-memory.x: app start 0x1000 -> 0x26000 to leave the
factory s140 SoftDevice dormant in flash.
- hal/nrf52840.rs: dump POWER.RESETREAS on boot -- WARN on DOG/LOCKUP,
INFO on RESETPIN/SREQ/wake-from-OFF.
- host/usb.rs: read_packet errors discriminated -- Disabled is debug
(normal during USB enum), BufferOverflow stays warn.
- COBS encode warns now carry type/tag/payload-length for diagnosis.
- RADIO_THROTTLED static initializes to true (no proof of host drain
at boot).
- driver/sh1106.rs: init retries 3x with 100ms backoff (intermittent
first-write race on power-up).
- Cargo.toml: unexpected_cfgs allowlist for hsmc's journal/trace-*
features.
docs/semtech_lora_cad_lbt_guidebook.md: vendor the Semtech CAD/LBT
reference for offline lookup.
The CAD pre-TX loop previously ran up to 10 retries with a fixed 10 ms
sleep — roughly one max-length packet only at SF7/BW500. At SF10/BW125
the 100 ms total was ~4% of one packet; at SF12/BW125 LBT was a no-op.
Now the per-retry sleep is sized to one time-on-air of an assumed-peer
80-byte packet under the active config, cached on RadioContext and
recomputed on every SET_CONFIG. Retries scale as
clamp(floor(BUDGET/T)+1, 1, 10) with BUDGET=2.5 s, so fast configs
still get up to 10 retries while SF11/12 degrade to single-CAD-and-bail
(logged at config-apply). Sleeps are jittered ±20% per iteration to
decorrelate simultaneously-backing-off contenders.
The ToA math, CR→{1..4} mapping, retry-count derivation, and jitter
helper live in a pure src/lbt.rs module so they're 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 see zero at the same SF/BW.
First classic-ESP32 (non-S3) board in the firmware, and first SX127x
radio. ESP32 + SX1276 + SSD1306 OLED + AXP192 PMIC, CP2102 USB-UART
bridge. Pin map verified from MeshCore variants/lilygo_tbeam_SX1276.
The AXP192 gates power to the LoRa chip and OLED. Its I²C bus is
shared with the display, so the firmware builds I²C0 in blocking mode
first for the PMIC init, then converts the same handle to async and
hands it to the display task — avoids double-consuming the peripheral.
Lora-phy's Sx127x support drives DIO0 as the sole IRQ (SX127x has no
BUSY line); the chip runs from a crystal, tx_boost routes +20 dBm
through PA_BOOST.
The `esp-hal` / `esp-rtos` / `esp-backtrace` / `esp-println` deps have
been restructured so the chip flavour (esp32 vs esp32s3) is selected
at the per-board feature level instead of baked into the dep lines.
This keeps the existing S3 boards building on esp32s3 while the new
T-Beam feature selects classic esp32 via the same dependency set.
LoRaBoard gains supported_sf_bitmap() / supported_bw_bitmap() methods
(with SX126x defaults) so the T-Beam can report SF6–SF12 via GET_INFO
— the SX1276 has no SF5. radio_chip_id() reports 0x0011 per
donglora-protocol RadioChipId::Sx1276. freq_range_hz widens to the
SX1276 datasheet span of 137 MHz–1020 MHz.
New modules:
- src/hal/esp32.rs — classic-ESP32 MCU primitives
- src/board/esp32.rs — Sx127x radio + SSD1306 display pipe
- src/board/lilygo_tbeam.rs — the board file
- src/driver/axp.rs — AXP192 init added alongside AXP2101
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.
Wire-incompatible break with the 0.x protocol. Every host-side path (UART, USB CDC, framing, radio dispatcher, display update channel) speaks the v2 wire format defined in PROTOCOL.md.
* Replace the host framing path with streaming FrameDecoder + encode_frame from donglora-protocol, matching the new (type_id, tag, payload) shape.
* Update src/channel.rs and src/host/{uart,usb,framing}.rs for the new frame semantics: per-request tag echo, CRC checked in the decoder, async ERR fan-out on tag 0.
* Rework src/radio.rs to emit RX / TX_DONE / OK / ERR messages in v2 shape, including RxOrigin, TxDonePayload, and SetConfigResult codes.
* Update src/protocol.rs glue to re-export v2 types from donglora-protocol and drop the legacy 0.x shim surface.
* Refresh PROTOCOL.md to the v2 normative spec (kept in sync with the donglora-protocol crate).
* Minor display and main-loop cleanups needed for the new radio event shape.
Finishes the hsmc migration that was staged across the prior two
commits:
- Radio task is now a single `machine.run().await`; all I/O lives in
state-scoped `during:` activities (`next_command`, `next_packet`,
`perform_tx`). The old `push/drain` helpers, the imperative
top-level `select`, `handle_cmd`, `handle_rx_result`, and
`execute_transmit` are gone. `StartingRx` / `ResumingRx` intermediate
states model the host-visible difference between a responding and a
silent RX re-arm.
- Host/USB task is now an hsmc statechart (Disconnected/Connected)
with entry/exit actions handling display wake, radio StopRx, and
display reset — replacing the open-coded `was_connected` edge
detector. The `protocol_loop` keeps its imperative read path because
`read_packet` blocks.
- New `debug-checkpoint` cargo feature ships a dedicated
`debug_blink.rs` task and a shared atomic counter that code along
the boot/dispatch/TX path bumps at notable points. A ladder-latching
write policy preserves the deepest TX phase reached on the first
attempt until reboot — diagnostic output for UART-only boards where
defmt would corrupt the host protocol. Zero footprint when the
feature is off.
- Silence a stray `clippy::unit_arg` on `led_off` / `led_blip` in the
display task for boards with `LedDriver = ()`.
Replaces the ad-hoc display loop with a declarative hsmc statechart
(Off/On.{Splash.{LearnMore,Info}, Dashboard.{Listening,Transmitting}})
and swaps the Radio→Display channel from a RadioStatus watch to a
typed RadioEvent queue. The radio task now emits discrete mode and
packet events instead of publishing full status snapshots; the
display consumes them through the statechart, which also handles the
5 s splash alternation and 1 s sparkline tick via `on(after …)` /
`on(every …)` transitions.
Also:
- Add hsmc dep and embassy/tokio feature shims (hsmc proc macro
evaluates `cfg(feature = "embassy")` in this crate).
- Fix USB disconnect path in `host/usb.rs` so the DTR branch always
runs on read error (was early-continuing, starving display/radio
reset).
ESP32-S3 + SX1262 + 1.3" SH1106 OLED + CP2102 UART bridge +
1000 mAh Li-Po. Pin map verified against Meshtastic's
variants/esp32s3/ELECROW-ThinkNode-M2. DIO3 TCXO at 3.3 V (vs 1.8 V
on Heltec), VEXT_ENABLE active HIGH (opposite polarity), dedicated
LORA_POWER_EN on GPIO48 sequenced before SPI init. Reuses the
existing async SH1106 driver at I2C address 0x3C.
Refactors hal::esp32s3::{init_spi,init_i2c} and board::esp32s3::init_radio
to accept generic GPIO bounds so ESP32-S3 boards with different pin
assignments can share the helpers. init_radio now takes a
TcxoCtrlVoltage parameter instead of hardcoding Ctrl1V8.
Incidental cleanups needed to make `just check-all` green:
- Replace the core::mem::forget(output) pattern with let _foo = ...;
current esp-hal makes Output non-Drop, so forget is a no-op that
trips clippy's forget_non_drop lint.
- Switch justfile xtensa builds from `cargo +nightly` with a RUSTC
override to `cargo +esp` so the compiler and rust-src versions stay
aligned. Public nightly's rust-src had drifted two months ahead of
the esp fork and broke -Zbuild-std on core.
Bump embassy-executor 0.9 -> 0.10 and embassy-rp 0.9 -> 0.10 from crates.io.
Patch esp-hal, esp-rtos, esp-backtrace, esp-println from esp-rs/esp-hal main
branch (esp-rtos 0.2.0 on crates.io pins embassy-executor ^0.9).
API migrations:
- embassy-executor: arch-cortex-m -> platform-cortex-m feature rename
- embassy-executor: SpawnToken now returned as Result from task functions
- esp-hal main: efuse::Efuse::mac_address() -> efuse::base_mac_address()
- esp-hal main: esp_rtos::start() now requires SoftwareInterrupt<0>
- embassy-rp 0.10: SPI constructor requires DMA interrupt bindings
Also fixes UART display stuck on splash (sent Reset instead of On at boot)
and switches just check to clippy -D warnings.
First RP2040 board and third MCU family (after ESP32-S3 and nRF52840).
Hardware: RP2040 + SX1262 radio, 850-930 MHz HF, PE4259 antenna switch,
LED on GPIO25, no OLED, USB-C via FPC ribbon cable to adapter board.
New files:
- src/hal/rp2040.rs: MCU primitives (SPI bus, USB driver, flash unique ID MAC)
- src/board/waveshare_rp2040_lora.rs: board implementation
- ld/rp2040-memory.x: linker memory layout
Also fixes cortex-m-rt/set-vtor (moved from global dep to per-board feature,
since Cortex-M0+ has no VTOR register) and makes build.rs memory.x selection
target-aware.
- Added RgbLed trait with impl on () for boards without LED
- Added WS2812 driver (driver/ws2812.rs) using ESP32-S3 RMT peripheral
- 80MHz clock, 12.5ns resolution, 24-bit GRB encoding
- Zero extra crate dependencies
- Heltec V3/V3-UART/V4: WS2812B on GPIO38 via RMT channel 0
- RAK WisBlock/Wio Tracker: type LedDriver = (), led = None (no-op)
- Display task: green blink on RX packet, red blink on TX packet
- LED auto-off on timer tick (~1s), off when display is off/reset
- LoRaBoard trait gains type LedDriver: RgbLed associated type
- BoardParts gains led: Option<L> field
The default heltec_v3 feature now uses native USB OTG (same as V4),
appearing as /dev/ttyACM0 with the DongLoRa VID:PID (1209:5741).
This requires a hardware mod: solder R29/R3 and disconnect the CP2102
from the USB data lines.
Stock (unmodified) V3 boards should use the new heltec_v3_uart feature
which preserves the original UART-over-CP2102 behavior (/dev/ttyUSB0).
- heltec_v3: switched from UART to USB OTG
- heltec_v3_uart: new feature, UART fallback for stock boards
- Removed #[cfg] gates on UsbParts/UartParts (both always defined)
- Updated README with hardware mod instructions and board table
- Updated justfile with heltec_v3_uart board definition
- LoRaBoard trait now has associated types: RadioParts, CommParts, DisplayParts, DisplayDriver
- into_parts() is on the trait (enforced by compiler, not implicit convention)
- BoardParts<R, C, D> struct replaces tuple return from into_parts()
- mac_address() stays on trait, MAC also in BoardParts for convenience
- Removed mac field from DisplayParts (display task calls Board::mac_address())
- DisplayDriver associated type requires DrawTarget<Color = BinaryColor>
- Created host/ module with internal cfg dispatch (USB vs UART)
- Moved usb/task.rs → host/usb.rs, uart/task.rs → host/uart.rs
- Moved protocol_io.rs → host/framing.rs
- Renamed usb_task/uart_task to host_task (uniform name)
- Removed all host-comm cfg from main.rs
- The run() function now has ZERO board-specific conditional compilation
- Deleted old usb/, uart/, protocol_io.rs modules
- Created hal/esp32s3.rs with MCU primitives (SPI, I2C, USB, UART, timer, MAC)
- Created hal/nrf52840.rs with shared nRF boilerplate (interrupts, bus wrapping, VBUS, MAC)
- Moved SH1106 OLED driver from display/ to driver/ (clear separation from app code)
- Board esp32s3.rs now delegates MCU init to hal, keeps only peripheral wiring (RadioParts)
- nRF board files use hal for shared StaticCell patterns, mac_address, interrupt bindings
- Board files still own pin assignments and peripheral construction
- Made protocol_io.rs compile for all boards (not just heltec_v3)
- Renamed FrameAccumulator → CobsDecoder, encode_response → cobs_encode_response
- Refactored USB task to use shared CobsDecoder and route_command from protocol_io
- Removed ~80 lines of duplicated COBS framing and command routing from usb/task.rs
- Single canonical MAX_FRAME definition in protocol_io.rs