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.
147 lines
5.3 KiB
Rust
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
|
|
}
|
|
}
|
|
}
|