refactor(radio): chart-first restructure on hsmc 0.3 + soak hardening

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.
This commit is contained in:
swaits committed 2026-04-28 10:59:28 -06:00
1 parent 481712930c
commit 276f941fbb
16 files changed
+2417 -262

No files matched your search

+67 -3
View File
@@ -9,9 +9,66 @@
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel;
use portable_atomic::AtomicBool;
use crate::protocol::{Command, DeviceMessage, LoRaConfig};
/// True while a Reset has been queued in `CommandChannel` but not yet
/// consumed by the radio task. Dedupes injections from any host chart
/// (USB DTR-driven OR UART inactivity-driven) that fires `reset_radio`
/// repeatedly during quiet/inactive periods. Without this dedup, a slow
/// or wedged radio causes the host chart's exit action to inject Reset
/// every cycle, filling the commands channel and producing a "queue
/// full" warn flood. Cleared by the radio chart's Reset handler
/// (`reset_to_unconfigured`).
///
/// Uses `portable_atomic::AtomicBool` (not `core::sync::atomic`) because
/// Cortex-M0+ targets (RP2040) lack native CAS for `swap`. portable-atomic
/// transparently uses native atomics on M3+/M4 and critical-section
/// emulation on M0/M0+.
pub static RESET_PENDING: AtomicBool = AtomicBool::new(false);
/// True while the host transport's USB IN endpoint is observed-dead
/// (write_packet returned Err). Set by `host_task::protocol_loop` on the
/// first failed write; cleared on the next successful write. Producers of
/// **async** D→H events — `radio_task::record_packet` (RX) and
/// `host::framing::emit_async_err` — check this flag before pushing into
/// `OutboundChannel` and silently skip when set, so the queue does not
/// fill while the host is gone.
///
/// Tag-correlated responses (OK / sync ERR / TX_DONE) are NOT gated by
/// this flag: they're a small, bounded volume and the protocol contract
/// requires them to be delivered if we ever reach the host again.
///
/// Same `portable_atomic::AtomicBool` rationale as `RESET_PENDING`.
///
/// **Initial value `true`**: at boot we have no proof the host can
/// drain the outbound queue. Until host_task observes its first
/// successful `write_packet` (and clears this), async-RX producers
/// silently skip pushing. Otherwise — at boot, with no client
/// attached or with mux-rs idle — RX events would fill the queue
/// faster than host_task could drain it (which it can't: `write_packet`
/// blocks until the host actually polls), and the depth-64 ring would
/// flood `[WARN] RX dropped: outbound queue full` hundreds of times
/// per second. Pre-throttle is the structural fix.
pub static RADIO_THROTTLED: AtomicBool = AtomicBool::new(true);
/// Set by `radio::lora_setup` / `lora_setup_with` when a SHORT-DURATION
/// lora-phy op blew through its wall-clock budget (almost always BUSY
/// stuck high — chip is wedged). Read by chart actions and `during:`
/// activities that called the helper: when they observe a downstream
/// `Err(_)` AND see this flag set, they emit `WedgeDetected` (which
/// drives the radio chart into `Recovering` for an in-place NRESET +
/// `lora.init()` retry) instead of the per-action failure event. The
/// flag is consumed (`swap(false)`) by whoever reads it so the next
/// independent op gets a fresh signal.
///
/// This is the cleanest plumbing path for "wedge timeout vs ordinary
/// driver error" given the helper sits below the chart-action layer
/// (free function, no chart-emit access). One atomic, one concern,
/// matching the established `RESET_PENDING` / `RADIO_THROTTLED` pattern.
pub static LORA_WEDGED: AtomicBool = AtomicBool::new(false);
// ── Host → Radio: tag-correlated commands ───────────────────────────
/// An event flowing from the host task into the radio task.
@@ -42,9 +99,16 @@ pub struct OutboundFrame {
pub msg: DeviceMessage,
}
/// Radio-to-host outbound channel (depth 32: enough to hold a burst of
/// RX events + queued command responses without stalling the radio ISR).
pub type OutboundChannel = Channel<CriticalSectionRawMutex, OutboundFrame, 32>;
/// Radio-to-host outbound channel.
///
/// Depth 64: absorbs bursts of RX events plus queued command responses
/// plus async ERRs while the host transport drains. Each slot is ~280 B
/// (max RxPayload + envelope), so 64 slots is ~18 KB — well within RAM
/// budget on every supported MCU. The previous depth-32 was provoking
/// "outbound queue full" floods during host stalls; the larger queue
/// plus `RADIO_THROTTLED` backpressure flag (set by host_task on a
/// failed write_packet) together stop the cascade.
pub type OutboundChannel = Channel<CriticalSectionRawMutex, OutboundFrame, 64>;
// ── Internal: display commands (no longer wire-visible) ────────────