refactor(host/usb): bump hsmc to 0.5 and collapse WaitingForFirstFrame into Connected

hsmc 0.5 makes default(...) optional on every state. Drop the
WaitingForFirstFrame substate that existed only because 0.4 required
Connected to declare a default child — Connected itself is now the
resting "DTR up, no traffic yet" state, and on(FrameReceived) => Active
descends into the inactivity-timer state on the first byte. Behavior is
identical (entry/exit chain math unchanged); the chart has one fewer
state and one fewer default().

Display and Radio charts left as-is — their defaults are all classic
descents serving real grouping/lifecycle purposes (Splash grouping,
Bright sub-state isolation, RX/TX session lifecycle), not 0.4 artifacts.
No journal-replay tests in this repo, so the 0.5 journal-format change
does not apply.
This commit is contained in:
swaits committed 2026-04-29 22:26:16 -06:00
1 parent f68e5d9be5
commit b56c044d57
3 files changed
+38 -41

No files matched your search

Generated
+4 -4
View File
@@ -1550,9 +1550,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hsmc"
version = "0.4.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adbdeff427849daf4d03395027c8538dcdd02a0a26c6e59745f8647f5e657527"
checksum = "a3951a3170ce7aa0f0bf4627ac4cc650857f15449c98e97647fa29fcd19c6d33"
dependencies = [
"defmt 0.3.100",
"embassy-futures",
@@ -1564,9 +1564,9 @@ dependencies = [
[[package]]
name = "hsmc-macros"
version = "0.4.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66ae4d525e3047234d69597f616c89b9ad49ac6a851e4522322a9d74fc2cc1bf"
checksum = "4b6d97dd1d31fba9e2f96c65c0ce38d7bc1ff59abe775a0ae18e14dfae01d1d1"
dependencies = [
"proc-macro2",
"quote",
+1 -1
View File
@@ -33,7 +33,7 @@ embassy-usb-driver = "0.2"
ucobs = { version = "0.3", features = ["legacy-msrv"] }
heapless = "0.9"
donglora-protocol = "1"
hsmc = { version = "0.4", default-features = false, features = ["embassy", "trace-defmt"] }
hsmc = { version = "0.5", 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"] }
+33 -36
View File
@@ -2,24 +2,24 @@
//!
//! ```text
//! Host
//! ├── Disconnected (default) no DTR
//! └── Connected host attached
//! ├── WaitingForFirstFrame (default) DTR up, no traffic yet
//! │ NO inactivity timer (boot quiet
//! │ is normal — chart structurally
//! │ blocks the spurious 1s fire).
//! └── Active ≥1 frame seen since connect
//! inactivity timer (1s) armed and
//! restarted by every frame via
//! self-transition.
//! ├── Disconnected (default) no DTR
//! └── Connected host attached, DTR up
//! │ resting here means no traffic yet —
//! │ NO inactivity timer (boot quiet is
//! │ normal; chart structurally blocks the
//! │ spurious 1s fire).
//! └── Active ≥1 frame seen since connect
//! inactivity timer (1s) armed and
//! restarted by every frame via
//! self-transition.
//! ```
//!
//! Two chart-level invariants made structurally impossible by the shape:
//! 1. **No spurious inactivity at boot.** The 1-second timer only exists
//! in `Active`, which requires a `FrameReceived` event to enter.
//! A freshly-connected host that holds DTR high but sends nothing
//! sits in `WaitingForFirstFrame` indefinitely — no timer, no warn,
//! no Reset cascade.
//! rests at `Connected` indefinitely (hsmc 0.5 optional-default
//! "microwave Standby" pattern) — no timer, no warn, no Reset cascade.
//! 2. **Chart-owned timer scope.** Per hsmc README rule §7 (timers
//! armed by state lifecycle), the inactivity timer is cancelled on
//! every exit of `Active`, restarted on every entry. No global
@@ -58,8 +58,8 @@ const POLL_INTERVAL_MS: u64 = 100;
pub enum HostInput {
DtrRaised,
DtrDropped,
/// A USB read returned ≥1 byte. Drives `WaitingForFirstFrame →
/// Active` (first frame after connect) and the `Active → Active`
/// A USB read returned ≥1 byte. Drives `Connected → Active` on
/// the first frame after DTR up, and the `Active → Active`
/// self-transition that restarts the inactivity timer per
/// hsmc README rule §4 (self-transition exits + re-enters target,
/// re-arming its timer).
@@ -122,24 +122,21 @@ statechart! {
entry: wake_display;
exit: reset_radio, reset_display;
on(DtrDropped) => Disconnected;
default(WaitingForFirstFrame);
// Default child of Connected. No inactivity timer yet —
// a freshly-connected host that hasn't sent its first byte
// is normal (mux-rs holds DTR high before any client
// attaches). Stays here indefinitely until the first
// frame arrives.
state WaitingForFirstFrame {
on(FrameReceived) => Active;
}
// hsmc 0.5 microwave-Standby pattern: Connected itself is the
// resting "DTR up, no traffic yet" state. No inactivity timer
// here — boot quiet is normal (mux-rs holds DTR high before
// any client attaches), so the chart structurally cannot fire
// a 1 s warn before the first byte. The first FrameReceived
// descends into Active, which owns the inactivity timer.
on(FrameReceived) => Active;
// Inactivity timer is chart-scoped: armed on every entry to
// Active (whether from WaitingForFirstFrame or self-loop
// after a frame), cancelled on every exit (per hsmc README
// rule §7). The self-transition `on(FrameReceived) =>
// Active` re-enters Active per rule §4, re-arming the
// timer — equivalent to the "reset last_frame_at on each
// byte" pattern but expressed in chart shape.
// Active (from Connected on the first frame, or from the
// self-transition below on each subsequent frame), cancelled
// on every exit (per hsmc README rule §7). The self-transition
// re-enters Active per rule §4, re-arming the timer —
// equivalent to the "reset last_frame_at on each byte" pattern
// but expressed in chart shape.
//
// hsmc's `after` triggers want `hsmc::Duration` (re-export
// of `core::time::Duration`), not `embassy_time::Duration`.
@@ -275,9 +272,9 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
n,
receiver.dtr(),
);
// Drives WaitingForFirstFrame → Active on first byte
// and Active → Active (timer-restart) on every byte
// after. The chart owns inactivity from here.
// Drives Connected → Active on first byte and
// Active → Active (timer-restart) on every byte after.
// The chart owns inactivity from here.
let _ = machine.dispatch(HostInput::FrameReceived).await;
dispatch_bytes(&mut decoder, &read_buf[..n], commands, outbound).await;
}
@@ -385,9 +382,9 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
// DTR edge detection — drives Connected / Disconnected
// transitions. The chart's `current_state()` may be
// Disconnected, Connected/WaitingForFirstFrame, or
// Connected/Active per the new hierarchy. We're connected if
// current state is anywhere inside Connected.
// Disconnected, Connected (resting), or Connected/Active.
// We're connected if current state is anywhere inside
// Connected.
let dtr_high = receiver.dtr();
let was_connected = !matches!(
machine.current_state(),