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
+32 -35

No files matched your search

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