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).
mise.toml now owns the ESP cross-toolchain end to end:
- [env] pins PATH (xtensa-esp-elf-gcc bin) and LIBCLANG_PATH so any
`mise exec` (which the justfile already uses for `set shell`)
resolves the linker without sourcing espup's stray ~/export-esp.sh.
- [tasks.esp-install] installs the matching crosstool version via
espup (espup hardcodes ~/.rustup/toolchains/esp/ -- there is no
--prefix flag -- so the binaries necessarily live there, but the
project now declaratively owns the version + path knowledge).
- cargo:espup pinned to 0.17.1 to match the [env] paths.
Triggered by a real failure: ~/export-esp.sh was moved to Trash and
the ESP build broke with `linker xtensa-esp32s3-elf-gcc not found`.
The previous setup silently no-op'd `[ -f $HOME/export-esp.sh ] && .
$HOME/export-esp.sh` in justfile, leaving the build with no PATH for
the cross-toolchain. Encoding the paths in mise.toml removes the
stray-file dependency.
justfile changes:
- _cargo: drop the `[ -f $HOME/export-esp.sh ] && . ...` source.
- setup / _require_esp_toolchain: install via `mise run esp-install`
instead of bare `espup install`, so the version pin and the [env]
paths stay in sync from a single source.
Bumping ESP versions remains a two-touch change: edit the version
dirs in BOTH [tasks.esp-install] and [env], then re-run the task.
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.
Appends comma-separated extra features to the board feature when
building. Enables iteration workflows like:
just flash wio_tracker_l1 debug-checkpoint
without having to drop to raw cargo + rust-objcopy + uf2 cp.
RAK4631 board:
- SX126X_POWER_EN on P1.05 driven HIGH at init (radio is power-gated;
without this the chip has no power and SPI hangs)
- HFXO + LFXO external crystals 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 needing a
CLOCK_POWER interrupt handler
- Green LED on P1.03 via SimpleLed (RAK19007 Base LED1, active HIGH)
- driver/mod.rs: open simple_led feature gate for rak_wisblock_4631
- Spim::new pin-order comment on SPI setup (arg order is sck/miso/mosi,
not sck/mosi/miso — easy to get wrong)
debug-checkpoint instrumentation (feature-gated, zero cost in release):
- radio.rs: debug_blink::set(N) at each TX-phase boundary, filling in
the long-aspirational blink-map doc
- debug_blink.rs: checkpoint 19 between prepare_for_cad and cad(), so
future bring-ups can distinguish modem-setup hang from DIO1-wait hang
justfile + mise.toml:
- set shell := ["mise", "exec", "--", "sh", "-c"] so recipes
always resolve tools through mise, not whatever happens to be on PATH
- replace per-board := vars + long if/elif _info chain with a single
case statement — one place per board, two lists (names + metadata)
- new flash-dfu recipe using adafruit-nrfutil via uvx, with VID:PID
(239a:0029) port autodetect for the Adafruit nRF52 bootloader
- mise.toml: rust pins now carry ARM targets + clippy, so 'mise install'
is sufficient bootstrapping (no extra rustup target add steps)
Verified TX on real hardware (RAK19007 Base + RAK4631 Core + RAK1921
OLED, US915 band): ADVERT round-trip with an external receiver.
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.
The shipping revision of this board has a WCH CH340K USB-UART bridge
(1a86:7522), not a Silicon Labs CP2102 as the original board-module
comment claimed. Update the doc header, and add the CH340K +
CH340-standard VID:PIDs to the `just flash` port list so we stop
falling through to espflash auto-detect on these boards.
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.
Generate .bin firmware images for ESP32 boards alongside .elf, matching
the existing .hex/.uf2 flow for nRF52840 boards.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- heltec_v3 now uses USB OTG (requires hardware mod: solder R29/R3)
- heltec_v3_uart added as fallback for stock boards
- Removed every #[allow(dead_code)] — fixed root causes instead:
- Comm types (UsbParts/UartParts) moved to individual board files
- USB/UART init removed from hal (boards call esp-hal directly)
- SH1106 driver gated on boards that use it
- UART task sends DisplayCommand::Reset at startup (variant now used)
- ErrorCode::CrcError removed (SX1262 drops bad-CRC silently)
- check/clippy now use --release (no more Xtensa dev-mode warnings)
- Updated README with hardware mod docs and board table
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