Cap the splash board-name on a 128 px-wide OLED: every name must fit
the FONT_5X8 fallback (25 ASCII chars) or the const_assert at the
BOARD_NAME definition fails the build. At runtime, names that fit
FONT_6X10 (21 chars) keep the primary font; longer names switch to
FONT_5X8 instead of overflowing. Also drop the URL-line font on the
"learn more" screen from FONT_5X8 to FONT_4X6 so "donglora.com" sits
inside the 64 px column with breathing room instead of touching both
edges.
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.
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.
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).
- Set disconnected flag on DisplayCommand::Reset to block stale radio
status updates from re-rendering dashboard over splash screen
- Send StopRx to radio when USB DTR drops so radio stops generating
packets after host disconnects
- Use try_send for unsolicited responses (RxPacket, error recovery,
StopRx Ok) to prevent radio task deadlock on full response channel
- Fall back to LED-only event loop when display init fails instead of
returning early and orphaning the LED
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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
- LED blink is now 50ms (was waiting for 1s timer tick)
- RX blink: green brightness scaled by SNR (-20dB→dim, +15dB→bright)
- TX blink: fixed red brightness
- Blink is self-contained (on→50ms delay→off) instead of timer-based
- V3 white LED: any non-zero = on, so SNR scaling is invisible but
V4 WS2812 will show green intensity varying with signal quality
- 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 display task started with disconnected=true, waiting for a
DisplayCommand::On that the USB task sends on DTR connect. The UART
task has no DTR, so the display never left the "disconnected" state
and ignored all radio status updates. Start connected instead — the
USB task will send Reset on actual disconnect.
- 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>
- Each board now exports DisplayDriver type and create_display() async fn
- ESP32-S3 boards share SSD1306 create_display in board/esp32s3.rs
- RAK WisBlock has its own SSD1306 create_display (different I2C type)
- Wio Tracker has SH1106 create_display (different display IC)
- Removed all cfg_if from display.rs — calls board::create_display()
- Removed display init retry logic (single attempt, return None on failure)
- ZERO board-specific #[cfg(feature)] outside board/, host/mod.rs, and hal/
- Flattened radio/ directory to radio.rs (was just one file)
- Flattened display/ to display.rs (combined task + rendering as inline module)
- Moved SH1106 display driver reference from display/ to driver/ (done in Phase 2)
- Suppressed dead_code warnings for conditional USB/UART types in hal
- All boards: zero warnings, zero cfg in application code