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

+33 -19
View File
@@ -22,11 +22,18 @@ pub type UsbDriver = mcu::UsbNrfDriver;
pub type DisplayI2c = mcu::I2cBus;
pub type LedDriver = crate::driver::simple_led::SimpleLed<Output<'static>>;
pub type DisplayDriver = ssd1306::Ssd1306Async<
ssd1306::prelude::I2CInterface<DisplayI2c>,
ssd1306::size::DisplaySize128x64,
ssd1306::mode::BufferedGraphicsModeAsync<ssd1306::size::DisplaySize128x64>,
>;
// SH1106 hand-rolled driver instead of the ssd1306 crate. The RAK1921
// OLED ships under various Chinese-OEM packaging (e.g. JMD0.96C) that
// can be either SSD1306 or SH1106 silicon — physically and pin
// identical, but use different addressing modes. The bigger reason
// though: ssd1306 0.10's display-interface-i2c emits I2C transaction
// patterns (consecutive writes, 3+ ops) that embassy_nrf::twim's
// `unreachable!()` rejects, so even on real SSD1306 silicon the panic
// would fire. Our hand-rolled SH1106 driver issues only single writes
// per logical op and is twim-safe. SH1106 init opcodes are similar
// enough that real SSD1306 silicon mostly works (worst case: mirrored
// display until proper runtime detection lands).
pub type DisplayDriver = crate::driver::sh1106::Sh1106<DisplayI2c>;
// ── Peripheral bundles ──────────────────────────────────────────────
@@ -46,22 +53,29 @@ pub struct DisplayParts {
// ── Display init ────────────────────────────────────────────────────
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
use ssd1306::mode::DisplayConfigAsync;
use ssd1306::prelude::{Brightness, DisplayRotation};
use ssd1306::size::DisplaySize128x64;
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
let mut display = crate::driver::sh1106::Sh1106::new(i2c, 0x3C);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
embassy_time::Timer::after_millis(100).await;
if display.init().await.is_err() {
defmt::error!("SSD1306 display init failed");
return None;
// Retry 3× with 100ms backoff. The SH1106's first transaction
// after power-on can return Err if the chip's internal voltage
// converter hasn't fully settled — observed empirically on
// RAK4631/RAK1921. Each attempt is independent (full init
// sequence). Display works visibly even if the FIRST attempt
// failed, because subsequent attempts succeed.
for attempt in 0..3u8 {
embassy_time::Timer::after_millis(100).await;
if display.init().await.is_ok() {
let _ = display.set_brightness(0xFF).await;
return Some(display);
}
if attempt < 2 {
defmt::warn!(
"SH1106 display init attempt {=u8}/3 failed; retrying",
attempt + 1
);
}
}
let _ = display.set_brightness(Brightness::BRIGHTEST).await;
Some(display)
defmt::error!("SH1106 display init failed after 3 attempts");
None
}
// ── Board init ──────────────────────────────────────────────────────