Files
firmware/src/host/framing.rs
T
swaits 276f941fbb 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.
2026-04-28 10:59:28 -06:00

147 lines
5.3 KiB
Rust

//! Shared framing helpers for USB and UART host transports.
//!
//! Ingress path: raw bytes → `FrameDecoder` → `(type_id, tag, payload)`
//! → `Command::parse` → forwarded to radio task along with the tag. On
//! parse failure we echo `ERR(ELENGTH/EPARAM/EMODULATION/EUNKNOWN_CMD)`
//! with the received tag. On CRC/COBS failure we emit async
//! `ERR(EFRAME)` with tag `0x0000` (`PROTOCOL.md §2.5`).
//!
//! Egress path: `OutboundFrame` (tag + DeviceMessage) → `encode_frame`
//! → bytes ready for transport-level write.
use defmt::warn;
use crate::channel::{CommandChannel, InboundEvent, OutboundChannel, OutboundFrame};
use crate::protocol::{
encode_frame, Command, CommandParseError, DeviceMessage, ErrorCode, FrameEncodeError,
MAX_PAYLOAD_FIELD, MAX_WIRE_FRAME,
};
/// Dispatch a frame-level decode into the appropriate downstream queue.
///
/// - `type_id`, `tag`, and `payload` come from `FrameDecoder`.
/// - Known commands are forwarded to the radio task via `commands`.
/// - Parse failures produce a synchronous `ERR` with the echoed tag on
/// `outbound`, so the host sees a concrete answer instead of timing out.
/// - The caller is responsible for rejecting `tag == 0` before calling
/// this; the spec says hosts MUST NOT send tag 0, and we classify such
/// frames as framing errors upstream.
pub async fn dispatch_frame(
type_id: u8,
tag: u16,
payload: &[u8],
commands: &CommandChannel,
outbound: &OutboundChannel,
) {
if tag == 0 {
// Hosts MUST NOT use tag 0 on outbound commands. Treat as
// framing error; do not route to radio. (Spec §2.2)
emit_async_err(outbound, ErrorCode::EFrame).await;
return;
}
match Command::parse(type_id, payload) {
Ok(cmd) => {
let envelope = InboundEvent::Command { tag, cmd };
if commands.try_send(envelope).is_err() {
emit_sync_err(outbound, tag, ErrorCode::EBusy).await;
}
}
Err(e) => {
let code = parse_err_to_wire_code(e);
emit_sync_err(outbound, tag, code).await;
}
}
}
/// Emit an async framing error (tag = 0x0000) per spec §2.5.
///
/// Backpressure: if the host transport's IN endpoint is observed-dead
/// (RADIO_THROTTLED set by host_task), drop silently. Without this gate,
/// a bad-frame burst from a wedged host fills `OutboundChannel` faster
/// than host_task can drain it (because it can't drain it at all when
/// write_packet keeps failing), producing the warn flood seen in soak.
pub async fn emit_async_err(outbound: &OutboundChannel, code: ErrorCode) {
if crate::channel::RADIO_THROTTLED.load(portable_atomic::Ordering::Acquire) {
return;
}
let frame = OutboundFrame {
tag: 0,
msg: DeviceMessage::Err(code),
};
if outbound.try_send(frame).is_err() {
warn!("async ERR dropped: outbound queue full");
}
}
/// Emit a synchronous error response echoing the originating tag.
async fn emit_sync_err(outbound: &OutboundChannel, tag: u16, code: ErrorCode) {
let frame = OutboundFrame {
tag,
msg: DeviceMessage::Err(code),
};
if outbound.try_send(frame).is_err() {
warn!("sync ERR dropped: outbound queue full");
}
}
fn parse_err_to_wire_code(e: CommandParseError) -> ErrorCode {
match e {
CommandParseError::UnknownType => ErrorCode::EUnknownCmd,
CommandParseError::WrongLength => ErrorCode::ELength,
CommandParseError::InvalidField => ErrorCode::EParam,
CommandParseError::ReservedBitSet => ErrorCode::EParam,
CommandParseError::UnknownModulation => ErrorCode::EModulation,
}
}
/// Encode an outbound frame into a single wire-ready byte buffer.
///
/// Returns the number of bytes written to `out`, or `None` on buffer
/// overflow / encode failure (caller may log and drop).
pub fn encode_outbound(frame: &OutboundFrame, out: &mut [u8; MAX_WIRE_FRAME]) -> Option<usize> {
let mut payload_buf = [0u8; MAX_PAYLOAD_FIELD];
let payload_len = match frame.msg.encode_payload(&mut payload_buf) {
Ok(n) => n,
Err(e) => {
warn!("encode_payload failed: {:?}", defmt::Debug2Format(&e));
return None;
}
};
match encode_frame(
frame.msg.type_id(),
frame.tag,
&payload_buf[..payload_len],
out.as_mut_slice(),
) {
Ok(n) => Some(n),
Err(FrameEncodeError::BufferTooSmall) => {
warn!(
"encode_frame: output buffer too small (type={=u8}, tag={=u16}, payload={=usize}B, out_buf={=usize}B)",
frame.msg.type_id(),
frame.tag,
payload_len,
out.len(),
);
None
}
Err(FrameEncodeError::PayloadTooLarge) => {
warn!(
"encode_frame: payload exceeds MAX_PAYLOAD_FIELD (type={=u8}, tag={=u16}, payload={=usize}B)",
frame.msg.type_id(),
frame.tag,
payload_len,
);
None
}
Err(FrameEncodeError::CobsEncode) => {
warn!(
"encode_frame: COBS encode refused (type={=u8}, tag={=u16}, payload={=usize}B)",
frame.msg.type_id(),
frame.tag,
payload_len,
);
None
}
}
}