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.
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.
The classic T-Beam's red LED on GPIO4 is wired active-low (anode to V+,
cathode to GPIO). Release v1.3.0 drove it as active-high, so the LED sat
lit at idle and dimmed briefly on TX/RX instead of flashing on activity.
Express the polarity where it physically lives — at the pin — via a new
generic InvertedPin<P> wrapper that swaps set_high/set_low. SimpleLed
stays polarity-agnostic and every other board's code path is
byte-for-byte unchanged from v1.3.0.
Empirically verified against hardware on both T-Beam boards: classic red
LED is now dark at idle and flashes on TX/RX; Supreme blue LED behaves
unchanged.
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.
Splash holds bright for 10 s on (re-)entry, then drops to dim while
LearnMore/Info keep alternating. The dashboard stays bright for 3 s
after the last packet or mode change, then dims in place without
disturbing the Listening/Transmitting state or the 1 s sparkline
timer. Cross-leaf TX↔RX hops stay bright throughout — `set_dim` is
fired by the 3 s timer handler as an action rather than as a sibling
`exit:` action, so mode-change leaf-crossings no longer flash
dim→bright on every swap.
Two driver families (`ssd1306::Ssd1306Async` and the in-tree
`Sh1106`) wrapped behind a single `DisplayBrightness` trait in
`src/driver/mod.rs`; the board trait's `DisplayDriver` associated
type now requires it. Dim/bright contrast values centralized as
`CONTRAST_BRIGHT` / `CONTRAST_DIM` constants — tune in one place.
`On.on(CmdReset)` now targets `LearnMore` directly rather than the
ancestor `Splash`. Under hsmc 0.2 up-transition semantics, targeting
an already-active ancestor unwinds only below it without re-descent;
the USB-disconnect reset path was previously relying on the old
re-enter-and-descend behavior and would have parked at `Splash`
with a dead alternation timer.
Bumps `hsmc` 0.1.0 → 0.2 for the up-transition semantic fix.
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.
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 embedded-hal 1.0 as direct dep (already transitive via embedded-hal-async)
- SimpleLed<P: OutputPin> now generic over any HAL — single definition in driver/
- Removed duplicate SimpleLed from board/esp32s3.rs and board/wio_tracker_l1.rs
- Changed BoardParts.led from Option<L> to L — boards with no LED use ()
- Display task calls led.set_rgb() unconditionally (no-op for () boards)
- RAK board: led: () instead of led: None
Per the Wio Tracker L1 schematic, it has a green LED on P1.01.
Moved SimpleLed from driver/ to board layer (esp32s3.rs and
wio_tracker_l1.rs) since the impl needs HAL-specific Output types.
Removed driver/simple_led.rs — each board layer defines its own.
RAK WisBlock remains LedDriver=() (no user LED on the core module).
Per the official V4 schematic (HTIT-WB32LAF_V4.3.pdf), the V4 has
an orange LED on GPIO35 with a series resistor — same setup as V3.
GPIO38 is GNSS_RX on V4, not an LED. Neither V3 nor V4 has a WS2812.
- V4: switched from WS2812/GPIO38 to SimpleLed/GPIO35
- Removed ws2812.rs driver (no current board uses addressable LEDs)
- All ESP boards now use the same SimpleLed driver on GPIO35
The Heltec V3 has a plain white LED on GPIO35 (simple GPIO, not
addressable). Only the V4 has a WS2812B RGB LED on GPIO38.
- Created driver/simple_led.rs for single-color GPIO LEDs
- V3/V3-UART boards: SimpleLed on GPIO35 (white blink on RX/TX)
- V4 board: unchanged (WS2812 on GPIO38, green/red colors)
- nRF boards: unchanged (no LED, type LedDriver = ())
- 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
- 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
- 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