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

+48 -10
View File
@@ -19,7 +19,7 @@ categories = ["embedded", "hardware-support", "no-std"]
[dependencies]
cfg-if = "1.0"
critical-section = "1.2"
defmt = "1.0"
defmt = "0.3"
embedded-hal = "1.0"
embedded-hal-async = "1.0"
embassy-embedded-hal = "0.6"
@@ -31,12 +31,11 @@ embassy-usb-driver = "0.2"
ucobs = { version = "0.3", features = ["legacy-msrv"] }
heapless = "0.9"
donglora-protocol = "1"
hsmc = { version = "0.2", default-features = false, features = ["embassy"] }
# Pulled from swaits/lora-rs fork until lora-rs/lora-rs#428 (pre-command
# BUSY check) is merged + released on crates.io. Switch back to a normal
# crates.io version then.
lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fix/sx126x-busy-race", features = ["defmt-03"] }
portable-atomic = { version = "1", optional = true, default-features = false, features = ["critical-section"] }
hsmc = { version = "0.3", default-features = false, features = ["embassy", "trace-defmt"] }
# Local path while iterating on CAD IRQ-clear / standby-after-CAD fixes.
# Switch back to git fork (then crates.io) once changes are upstreamed.
lora-phy = { path = "../../lora-rs/lora-phy", features = ["defmt-03"] }
portable-atomic = { version = "1", default-features = false, features = ["critical-section"] }
static_cell = "2.1"
# Display rendering
@@ -70,12 +69,22 @@ cortex-m = { version = "0.7", optional = true, features = [
"critical-section-single-core",
] }
cortex-m-rt = { version = "0.7", optional = true }
defmt-rtt = { version = "1.0", optional = true }
defmt-rtt = { version = "0.4", optional = true }
embassy-nrf = { version = "0.10", optional = true, features = [
"nrf52840",
"gpiote",
"time-driver-rtc1",
"defmt",
# UICR pin-config flags — must match what the Adafruit nRF52 bootloader
# actually programmed into UICR on this board. Verified empirically on
# RAK4631 via `probe-rs read b32 0x10001200 4`:
# PSELRESET[0/1] = 0x12 (P0.18 connected as RESET — NOT gpio)
# NFCPINS = 0xFFFFFFFE (bit 0 = 0 → NFC pins as GPIO)
# So `nfc-pins-as-gpio` matches; `reset-pin-as-gpio` would NOT match
# (would request GPIO when UICR has RESET enabled). The bootloader
# uses NRESET for its physical button-reset path, so leaving the pin
# as RESET is the right state.
"nfc-pins-as-gpio",
] }
panic-probe = { version = "1.0", optional = true, features = ["print-defmt"] }
@@ -105,6 +114,21 @@ tokio = []
# a human watches the pattern and reports N to determine where execution
# reached / stopped.
debug-checkpoint = []
# NOTE: chart auto-tracing is wired in unconditionally via the
# `trace;` keyword in each `statechart!` and the `hsmc/trace-defmt`
# feature on the dependency. Whether the trace lines actually emit at
# runtime is controlled by defmt's standard compile-time level filter
# (`DEFMT_LOG` env var). Release/shipping builds with `DEFMT_LOG`
# unset (or `off`) compile every chart trace call out to nothing —
# zero overhead, no separate cargo feature needed.
# When enabled, the SH1106 OLED driver emits a defmt::info! line for
# every I2C transaction it issues — bytes, lengths, addresses. Used to
# debug the embassy_nrf::twim assertion panic at twim.rs:541 (cannot
# happen via the documented call path; instrumenting the SH1106 path
# captures the actual transaction sequence preceding the panic so we
# can root-cause). Off by default; opt in via
# `just flash-attach rak_wisblock_4631 debug-twim`.
debug-twim = []
heltec_v3 = [
"embassy",
"dep:esp-backtrace",
@@ -200,7 +224,6 @@ waveshare_rp2040_lora = [
"dep:defmt-rtt",
"dep:embassy-rp",
"dep:panic-probe",
"dep:portable-atomic",
"embassy-executor/platform-cortex-m",
"embassy-executor/executor-thread",
]
@@ -215,6 +238,17 @@ esp-rtos = { git = "https://github.com/esp-rs/esp-hal.git", branch = "main"
esp-backtrace = { git = "https://github.com/esp-rs/esp-hal.git", branch = "main" }
esp-println = { git = "https://github.com/esp-rs/esp-hal.git", branch = "main" }
[lints.rust]
# hsmc 0.3's `statechart!` macro emits `#[cfg(feature = "journal")]`
# attributes (and others). check-cfg evaluates those in the CALLER's
# crate context, so unless donglora declares the same feature names
# (we don't — they're hsmc's, not ours), rustc lints them. Per hsmc's
# README, the documented fix is to allow the lint with an explicit
# check-cfg list so other unexpected cfgs still surface as warnings.
unexpected_cfgs = { level = "allow", check-cfg = [
'cfg(feature, values("tokio", "embassy", "journal", "trace-defmt", "trace-log", "trace-tracing"))',
] }
[lints.clippy]
undocumented_unsafe_blocks = "deny"
@@ -223,4 +257,8 @@ codegen-units = 1
lto = "fat"
opt-level = 3 # "s"/"z" break Xtensa (l32r literal range exceeded)
panic = "abort"
strip = true
# `strip = true` was killing defmt's `_defmt_version_` / `_defmt_encoding_`
# symbols, breaking RTT log decoding via probe-rs. Keep symbols + DWARF
# so probe-rs can show source locations for log lines.
strip = false
debug = 2