Extends the v1.1.0 "SET_CONFIG during RX auto-re-arms" invariant to
every failure path that can fire while a client has RX active:
* `Receiving.on(RxFailed)` now transitions to `ResumingRx` instead of
`Idle`. A transient `next_packet` error re-arms the chip rather than
ending the session.
* `TransmittingFromRx.on(TransmitFailed)` now transitions to
`ResumingRx` instead of `Idle`. TX failure during RX still responds
`TX_FAILED` to the caller, then returns to RX — matching the shape
of the adjacent `ChannelBusy` handler, which already did this.
* `ResumingRx.on(StartRxFailed)` is now a bounded retry (5 attempts,
25 ms backoff) rather than an immediate drop to `Idle`. Only after
the retry budget is exhausted does the chip actually land in `Idle`,
at which point it's genuinely wedged and needs reconfiguring.
Observable effect: the OLED dashboard no longer bounces to Splash
when a client (e.g. ai-bot) TXes while bridges are receiving.
Previously any single hiccup in the RX/TX/ResumeRX cycle would
announce `RadioEvent::Idle`, bouncing the OLED, and the radio would
sit in Idle with no client aware enough to re-issue RX_START —
stranding the session. The invariant should have held for the whole
RX window between `CMD_RX_START` and `CMD_RX_STOP`; now it does.
Implementation notes:
* Two new internal `RadioInput` variants: `ResumeRxRetry` drives the
`ResumingRx` self-transition; `ResumeRxExhausted` breaks out to
Idle after too many failed tries.
* New `resume_retries: u8` field on `RadioContext`, reset to 0 on
every successful `StartRxOk`, bumped by `bump_resume_retries`.
* Constants `MAX_RESUME_RETRIES = 5` and `RESUME_RX_RETRY_BACKOFF_MS
= 25` at the top of `radio.rs`.
* Removed the dead `announce_rx_failed` helper — its only caller was
the now-deleted `Receiving.on(RxFailed) => Idle` edge.
No behavior change for the three paths where `Idle` remains the
correct landing state: initial `CmdRxStart` failure in `StartingRx`,
`CmdRxStop` in `Receiving`, and all three `TransmittingFromIdle`
outcomes (where RX wasn't running to begin with).
Inside state Receiving, override the parent Configured state chart handler on(ConfigApplied) => Idle with on(ConfigApplied) => ResumingRx. ResumingRx entry (try_start_rx_hw) re-enables RX on the chip with the new modulation once apply_set_config has finished the SPI write + calibration, and transitions back to Receiving on StartRxOk.
Net effect: SET_CONFIG issued while the radio is in continuous RX no longer kicks the radio (and therefore the OLED) out to idle. The RX ring is still cleared by apply_set_config; callers never need to re-issue RX_START after SET_CONFIG.
This matches donglora-protocol 1.1.0 §3.5 and §6.3 normative behavior.
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 imperative Mode enum + if-branches in the radio loop
with a declarative hsmc statechart mirroring the display:
Radio
├── Unconfigured
└── Configured
├── Idle
├── Receiving
└── Transmitting
├── FromIdle
└── FromRx
The `TransmittingFrom{Idle,Rx}` substates encode "where to return after
TX" as state identity, killing the `was_receiving` bookkeeping. Entry
actions announce `RadioEvent`s on the display channel; RF I/O stays in
the task loop because `rx_once` blocks indefinitely and cannot live
inside an action. Radio-init failure runs a Ping-only degraded loop
without ever instantiating the machine.
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>
- 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