chore(release): 0.5.1
This commit is contained in:
12 files changed
+392
-2198
No files matched your search
@@ -4,6 +4,56 @@ All notable changes to this workspace are documented here. The format
|
|||||||
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
||||||
this project adheres to [Semantic Versioning](https://semver.org/).
|
this project adheres to [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## 0.5.1 — 2026-04-29
|
||||||
|
|
||||||
|
Docs and licensing cleanup. No code or behavior changes.
|
||||||
|
|
||||||
|
- **License simplified to MIT.** The crate was previously dual-licensed
|
||||||
|
MIT OR Apache-2.0; it is now MIT-only. `LICENSE-APACHE` removed,
|
||||||
|
`LICENSE-MIT` renamed to `LICENSE`, workspace `license` field is
|
||||||
|
now `"MIT"`. Copyright (c) 2026 Stephen Waits <steve@waits.net>.
|
||||||
|
- **README rewritten reader-first.** Restructured top-to-bottom so a
|
||||||
|
reader who's never used statecharts can follow the document linearly:
|
||||||
|
a working Blinker chart is now the first concrete artifact; the
|
||||||
|
Quickstart sits directly under it; the larger Radio example
|
||||||
|
(events / payloads / `during:`) follows; the formal eight-rule
|
||||||
|
semantics live below all of that as reference rather than as a
|
||||||
|
prerequisite. The "What it does" feature list is rewritten in
|
||||||
|
user-benefit-first language. The "What `hsmc` deliberately does NOT
|
||||||
|
have" table is promoted from a sub-section to its own top-level
|
||||||
|
section. No semantic changes — the formal rules' content is
|
||||||
|
untouched, only repositioned.
|
||||||
|
- **Tokio integration story corrected.** The README previously implied
|
||||||
|
driving a chart under tokio required `tokio::task::LocalSet`. The
|
||||||
|
normal path is `#[tokio::main(flavor = "current_thread")]` — one
|
||||||
|
line, no extra wrapping, and how the existing examples have always
|
||||||
|
worked. `LocalSet` no longer appears in the README at all.
|
||||||
|
- **`microwave_tui` example dropped.** The ratatui/crossterm TUI
|
||||||
|
variant of the microwave was overkill for a library example and
|
||||||
|
pulled in three dev-dependencies (`ratatui`, `crossterm`, `futures`)
|
||||||
|
that nothing else used. The `microwave` example covers the same
|
||||||
|
feature surface in plain Rust.
|
||||||
|
- **Observability framed honestly.** Enabling `defmt` / `log` /
|
||||||
|
`tracing` output for a chart is one `trace-*` Cargo feature on
|
||||||
|
hsmc's side, plus whatever your downstream crate already does to
|
||||||
|
init its logger (e.g. `tracing_subscriber::fmt::init()` or
|
||||||
|
`env_logger::init()`) — hsmc deliberately doesn't init a logger,
|
||||||
|
per the standard Rust library convention. The Observability
|
||||||
|
section now says so directly and includes a runnable end-to-end
|
||||||
|
snippet using `tracing-subscriber`. The `trace-*` features are
|
||||||
|
also listed in the main feature-flags table so they're
|
||||||
|
discoverable at a glance.
|
||||||
|
- **Inline examples corrected.** The Quickstart Blinker now has both
|
||||||
|
`light_on` and `light_off` actions (it previously had only
|
||||||
|
`light_on`, which left the LED on the whole time). The Embassy
|
||||||
|
snippet no longer declares an unused event channel for what is a
|
||||||
|
timer-only chart.
|
||||||
|
- **Eight-rule order finalized.** The "How a chart behaves" rules are
|
||||||
|
now strictly bottom-up: active path → transitions (LCA-aware) →
|
||||||
|
up-transition rule → entry/exit ordering → `default(...)` (a
|
||||||
|
transition fired on entry) → event bubbling → timers →
|
||||||
|
emit/during/termination. Cross-references updated.
|
||||||
|
|
||||||
## 0.5.0 — 2026-04-28
|
## 0.5.0 — 2026-04-28
|
||||||
|
|
||||||
### `default(...)` is a real LCA-aware transition
|
### `default(...)` is a real LCA-aware transition
|
||||||
|
|||||||
Generated
+6
-595
@@ -2,12 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "allocator-api2"
|
|
||||||
version = "0.2.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -29,53 +23,18 @@ version = "1.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "2.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cassowary"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "castaway"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "compact_str"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
|
||||||
dependencies = [
|
|
||||||
"castaway",
|
|
||||||
"cfg-if",
|
|
||||||
"itoa",
|
|
||||||
"rustversion",
|
|
||||||
"ryu",
|
|
||||||
"static_assertions",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "creusot-std"
|
name = "creusot-std"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -103,66 +62,6 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm"
|
|
||||||
version = "0.28.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.1",
|
|
||||||
"crossterm_winapi",
|
|
||||||
"futures-core",
|
|
||||||
"mio",
|
|
||||||
"parking_lot",
|
|
||||||
"rustix",
|
|
||||||
"signal-hook",
|
|
||||||
"signal-hook-mio",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm_winapi"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling"
|
|
||||||
version = "0.23.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"darling_macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_core"
|
|
||||||
version = "0.23.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
|
||||||
dependencies = [
|
|
||||||
"ident_case",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"strsim",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_macro"
|
|
||||||
version = "0.23.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "defmt"
|
name = "defmt"
|
||||||
version = "0.3.100"
|
version = "0.3.100"
|
||||||
@@ -178,7 +77,7 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78"
|
checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags",
|
||||||
"defmt-macros",
|
"defmt-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -234,12 +133,6 @@ dependencies = [
|
|||||||
"litrs",
|
"litrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "either"
|
|
||||||
version = "1.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embassy-futures"
|
name = "embassy-futures"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -331,110 +224,18 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "errno"
|
|
||||||
version = "0.3.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foldhash"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-executor",
|
|
||||||
"futures-io",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-channel"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-sink",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-executor"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-io"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-macro"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-task"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-util"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-io",
|
|
||||||
"futures-macro",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"memchr",
|
|
||||||
"pin-project-lite",
|
|
||||||
"slab",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -450,17 +251,6 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.15.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
|
||||||
dependencies = [
|
|
||||||
"allocator-api2",
|
|
||||||
"equivalent",
|
|
||||||
"foldhash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
@@ -487,27 +277,18 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "heck"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsmc"
|
name = "hsmc"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm",
|
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
"embassy-sync",
|
"embassy-sync",
|
||||||
"embassy-time",
|
"embassy-time",
|
||||||
"futures",
|
|
||||||
"heapless 0.8.0",
|
"heapless 0.8.0",
|
||||||
"hsmc-macros",
|
"hsmc-macros",
|
||||||
"iai-callgrind",
|
"iai-callgrind",
|
||||||
"log",
|
"log",
|
||||||
"ratatui",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"trybuild",
|
"trybuild",
|
||||||
@@ -515,7 +296,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsmc-macros"
|
name = "hsmc-macros"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hsmc",
|
"hsmc",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -526,7 +307,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsmc-verification"
|
name = "hsmc-verification"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"creusot-std",
|
"creusot-std",
|
||||||
"hsmc",
|
"hsmc",
|
||||||
@@ -568,12 +349,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ident_case"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
@@ -581,38 +356,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.17.0",
|
"hashbrown",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indoc"
|
|
||||||
version = "2.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "instability"
|
|
||||||
version = "0.3.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
|
|
||||||
dependencies = [
|
|
||||||
"darling",
|
|
||||||
"indoc",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -621,66 +365,24 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.185"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linux-raw-sys"
|
|
||||||
version = "0.4.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litrs"
|
name = "litrs"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lock_api"
|
|
||||||
version = "0.4.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
|
||||||
dependencies = [
|
|
||||||
"scopeguard",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru"
|
|
||||||
version = "0.12.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown 0.15.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mio"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"wasi",
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nb"
|
name = "nb"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@@ -735,35 +437,6 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot"
|
|
||||||
version = "0.12.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.9.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"smallvec",
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paste"
|
|
||||||
version = "1.0.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -810,36 +483,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ratatui"
|
|
||||||
version = "0.28.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.1",
|
|
||||||
"cassowary",
|
|
||||||
"compact_str",
|
|
||||||
"crossterm",
|
|
||||||
"instability",
|
|
||||||
"itertools",
|
|
||||||
"lru",
|
|
||||||
"paste",
|
|
||||||
"strum",
|
|
||||||
"strum_macros",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-truncate",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -849,37 +492,6 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustix"
|
|
||||||
version = "0.38.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.1",
|
|
||||||
"errno",
|
|
||||||
"libc",
|
|
||||||
"linux-raw-sys",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustversion"
|
|
||||||
version = "1.0.22"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ryu"
|
|
||||||
version = "1.0.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
@@ -938,89 +550,12 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signal-hook"
|
|
||||||
version = "0.3.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"signal-hook-registry",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signal-hook-mio"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"mio",
|
|
||||||
"signal-hook",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signal-hook-registry"
|
|
||||||
version = "1.4.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
|
||||||
dependencies = [
|
|
||||||
"errno",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slab"
|
|
||||||
version = "0.4.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smallvec"
|
|
||||||
version = "1.15.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum"
|
|
||||||
version = "0.26.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
|
||||||
dependencies = [
|
|
||||||
"strum_macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum_macros"
|
|
||||||
version = "0.26.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
|
||||||
dependencies = [
|
|
||||||
"heck",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"rustversion",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@@ -1164,87 +699,27 @@ version = "1.0.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-segmentation"
|
|
||||||
version = "1.13.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-truncate"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
|
||||||
dependencies = [
|
|
||||||
"itertools",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.1.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "void"
|
name = "void"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasi"
|
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi"
|
|
||||||
version = "0.3.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-i686-pc-windows-gnu",
|
|
||||||
"winapi-x86_64-pc-windows-gnu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-i686-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.59.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -1254,70 +729,6 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm",
|
|
||||||
"windows_aarch64_msvc",
|
|
||||||
"windows_i686_gnu",
|
|
||||||
"windows_i686_gnullvm",
|
|
||||||
"windows_i686_msvc",
|
|
||||||
"windows_x86_64_gnu",
|
|
||||||
"windows_x86_64_gnullvm",
|
|
||||||
"windows_x86_64_msvc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|||||||
+2
-2
@@ -3,9 +3,9 @@ resolver = "2"
|
|||||||
members = ["hsmc", "hsmc-macros", "verification"]
|
members = ["hsmc", "hsmc-macros", "verification"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT"
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
repository = "https://github.com/swaits/hsmc"
|
repository = "https://github.com/swaits/hsmc"
|
||||||
homepage = "https://github.com/swaits/hsmc"
|
homepage = "https://github.com/swaits/hsmc"
|
||||||
File renamed without changes.
-201
@@ -1,201 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for describing the origin of the Work and
|
|
||||||
reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Support. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or support.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2026 Stephen Waits
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied. See the License for the specific language governing permissions
|
|
||||||
and limitations under the License.
|
|
||||||
@@ -1,10 +1,98 @@
|
|||||||
# hsmc — Hierarchical State Machines for Rust
|
# hsmc — Hierarchical State Machines for Rust
|
||||||
|
|
||||||
`hsmc` is a declarative, proc-macro-based statechart crate for Rust. It targets
|
`hsmc` lets you write the control logic of a device — a microwave, a
|
||||||
embedded Rust (embassy) as the primary runtime and supports `tokio` via
|
LoRa radio, a sensor, a UI flow — as a **statechart**: nested states
|
||||||
`LocalSet`. Machines are compiled from a `statechart!` block into
|
with entry/exit actions, transitions, timers, and async work, all
|
||||||
monomorphized code with no dynamic dispatch, no interior mutability, and no
|
declared in one block. The proc macro turns it into Rust code with no
|
||||||
heap allocations by default.
|
heap allocations, no dynamic dispatch, and no interior mutability.
|
||||||
|
|
||||||
|
It targets embedded Rust on `embassy` first, with full tokio support
|
||||||
|
on the side. Same chart, both runtimes.
|
||||||
|
|
||||||
|
## A first chart — a blinker
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use hsmc::{statechart, Duration};
|
||||||
|
|
||||||
|
pub struct Ctx {
|
||||||
|
pub on: bool, // mirrors the LED's current state
|
||||||
|
pub blinks: u32, // count of times we've turned it on
|
||||||
|
}
|
||||||
|
|
||||||
|
statechart! {
|
||||||
|
Blinker {
|
||||||
|
context: Ctx;
|
||||||
|
default(Off); // start with the LED off
|
||||||
|
|
||||||
|
state Off {
|
||||||
|
entry: light_off;
|
||||||
|
on(after Duration::from_millis(500)) => On;
|
||||||
|
}
|
||||||
|
state On {
|
||||||
|
entry: light_on;
|
||||||
|
on(after Duration::from_millis(500)) => Off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlinkerActions for BlinkerActionContext<'_> {
|
||||||
|
async fn light_off(&mut self) { self.on = false; }
|
||||||
|
async fn light_on(&mut self) { self.on = true; self.blinks += 1; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Two states, two timer-driven transitions, two entry actions. The
|
||||||
|
runtime arms each state's timer when you enter it and cancels it
|
||||||
|
when you leave; `default(Off)` picks the initial state. Land in
|
||||||
|
`Off`, fire `light_off`, arm the 500 ms timer; when it elapses,
|
||||||
|
transition to `On`, fire `light_on`, arm the next 500 ms timer.
|
||||||
|
Repeat.
|
||||||
|
|
||||||
|
That's `hsmc` at its smallest: states, timers, and one entry action
|
||||||
|
per state. No event channel, no async I/O, no hierarchy yet.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
Add to `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
hsmc = { version = "0.5", features = ["embassy"] } # firmware
|
||||||
|
# or
|
||||||
|
hsmc = { version = "0.5", features = ["tokio"] } # desktop / tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the Blinker under tokio:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
let mut m = Blinker::new(Ctx { on: false, blinks: 0 });
|
||||||
|
let _ = m.run().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That's the whole driver. A chart runs on a single task — `flavor =
|
||||||
|
"current_thread"` keeps tokio out of the multi-threaded scheduler so
|
||||||
|
you don't have to think about `Send` bounds on your context, your
|
||||||
|
events, or your `during:` futures. Normal Rust, no extra wrapping.
|
||||||
|
|
||||||
|
Run it under embassy:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn blink_task() {
|
||||||
|
let mut m = Blinker::new(Ctx { on: false, blinks: 0 });
|
||||||
|
let _ = m.run().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## A bigger chart — events, payloads, async I/O
|
||||||
|
|
||||||
|
The Blinker doesn't talk to anything outside itself. A real device
|
||||||
|
does. Here's a radio that receives commands from somewhere
|
||||||
|
(button press, RPC call, ISR), continuously listens for packets while
|
||||||
|
in the `Receiving` state, and stops when told:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
statechart! {
|
statechart! {
|
||||||
@@ -25,41 +113,73 @@ statechart! {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## What it does
|
Three things here that the Blinker didn't have:
|
||||||
|
|
||||||
- **Harel-style hierarchical states.** Nested states with `entry:`/`exit:`
|
- **Events.** `events: Ev;` declares the input enum. External code
|
||||||
actions, LCA-aware transitions, optional `default(...)` (a transition
|
drives the chart with `m.sender().send(Ev::StartRx)`; in-chart
|
||||||
that may target any state — descend, redirect to a sibling, bounce
|
actions can `emit(Ev::Whatever)` to queue a follow-up. Handlers
|
||||||
to an ancestor — with a compile-time cycle check), `terminate`.
|
pattern-match the variant and bind its payload directly:
|
||||||
- **Event-driven transitions and actions.** Typed payload bindings:
|
`on(PacketRx { rssi, snr }) => record_packet;` makes `rssi` and
|
||||||
`on(PacketRx { rssi: i16 }) => handler;`.
|
`snr` available to the action.
|
||||||
- **Timer triggers.** `on(after Duration::from_secs(5)) => Next;` and
|
- **`during:` activities.** `next_packet(lora, rx_buf)` is an async
|
||||||
`on(every Duration::from_millis(100)) => tick;`.
|
fn that runs while `Receiving` is active and is dropped when the
|
||||||
- **`during:` activities.** Async functions that run while a state is
|
state exits. It gets `&mut` borrows of just the `lora` and
|
||||||
active and produce events. Multiple durings can borrow disjoint fields
|
`rx_buf` fields of the context — Rust's split-borrow rules let
|
||||||
of the context concurrently via Rust's native split borrow — no
|
several `during:`s on the active path run concurrently without a
|
||||||
`RefCell`, no `Mutex`, no `UnsafeCell` in user code.
|
`RefCell` or `Mutex` in sight.
|
||||||
- **`m.run().await` is the whole task body.** Races active durings, the
|
- **The runtime races them all.** `m.run()` simultaneously polls the
|
||||||
external event channel, and timer deadlines. The MCU sleeps between
|
`during:`, listens on the event channel, and waits for the next
|
||||||
events (embassy's cooperative executor parks all futures).
|
timer deadline; whichever fires first drives the next transition.
|
||||||
- **Unified observability.** One observation per atom of execution
|
The MCU sleeps in between.
|
||||||
fans out at compile time to a journal (`Vec<TraceEvent>`) and/or
|
|
||||||
textual trace backends (`defmt`, `log`, `tracing`) — they share a
|
|
||||||
single vocabulary, so they cannot diverge. With no sink feature on,
|
|
||||||
every site expands to `()` (zero overhead). See
|
|
||||||
[Observability](#observability--one-journal-multiple-outputs).
|
|
||||||
- **Deterministic + mechanically verified.** The `journal` feature
|
|
||||||
records every action, transition, timer, and event in order — same
|
|
||||||
inputs, byte-identical journal. The runtime queue and timer table
|
|
||||||
carry Pearlite specs that Creusot + Why3 + alt-ergo + z3 discharge
|
|
||||||
mechanically (`just verify`). The integration suite pins every
|
|
||||||
spec rule with byte-equal expected-vs-actual journals, trace-format
|
|
||||||
regression checks, and per-feature behavior tests.
|
|
||||||
|
|
||||||
## How a chart behaves — the building blocks
|
For nested-state hierarchies, `emit(...)`, `terminate`, and
|
||||||
|
`on(every D)`, see the [microwave example](hsmc/examples/microwave.rs).
|
||||||
|
|
||||||
Eight rules, each building on the previous. Internalize these and every
|
## What you get
|
||||||
edge case below falls out — you don't have to memorize anything else.
|
|
||||||
|
- **Nested states with proper transitions.** Exit a deeply nested
|
||||||
|
state and every enclosing state's exits run in the right order, no
|
||||||
|
manual bookkeeping. A transition is just `=> Target;` and the
|
||||||
|
runtime walks the tree (LCA dispatch). Includes `entry:`/`exit:`
|
||||||
|
actions, optional `default(...)` initial transitions, and
|
||||||
|
`terminate`.
|
||||||
|
- **Events with typed payloads.** Pattern-match a variant and bind
|
||||||
|
its fields right in the chart: `on(PacketRx { rssi: i16 }) =>
|
||||||
|
record_packet;`. The compiler type-checks the binding against your
|
||||||
|
event enum.
|
||||||
|
- **Timers as first-class triggers.** `on(after 5s) => Next;` for a
|
||||||
|
one-shot, `on(every 100ms) => tick;` for periodic. Armed when the
|
||||||
|
declaring state is entered, cancelled when it's exited — no
|
||||||
|
`select!` plumbing in your code.
|
||||||
|
- **Async work scoped to a state's lifetime.** A `during:` is an
|
||||||
|
async fn that runs while a state is active and is dropped when you
|
||||||
|
leave. Multiple durings on the active path race concurrently and
|
||||||
|
split-borrow disjoint context fields — no `RefCell`, no `Mutex`,
|
||||||
|
no `unsafe`.
|
||||||
|
- **`m.run().await` is the whole task.** It races durings, the event
|
||||||
|
channel, and timer deadlines, and parks the executor between them.
|
||||||
|
No manual loop, no busy-waits, no `Send` headaches under
|
||||||
|
`current_thread` tokio or embassy.
|
||||||
|
- **Observability that's one feature flag away.** Add
|
||||||
|
`features = ["trace-log"]` (or `trace-defmt` / `trace-tracing`) and
|
||||||
|
every action, transition, timer, and event becomes a log line in
|
||||||
|
whichever framework your crate already uses — `defmt` for firmware,
|
||||||
|
`log` for desktop, `tracing` for structured spans. With no sink
|
||||||
|
feature, the observation calls compile to nothing.
|
||||||
|
- **Deterministic + machine-checked.** The `journal` feature records
|
||||||
|
every action, transition, timer, and event in order; identical
|
||||||
|
input produces a byte-identical journal. The runtime's event
|
||||||
|
queue and timer table are formally verified — Creusot + Why3 +
|
||||||
|
alt-ergo + z3 discharge the proofs (`just verify`).
|
||||||
|
- **No heap, no `unsafe`, no dynamic dispatch.** Pure monomorphized
|
||||||
|
code. `no_std` clean by default. Designed to fit on a Cortex-M0+
|
||||||
|
under embassy.
|
||||||
|
|
||||||
|
## How a chart behaves — the rules
|
||||||
|
|
||||||
|
Eight rules, each building on the previous. Internalize these and
|
||||||
|
every edge case below falls out — you don't have to memorize anything
|
||||||
|
else.
|
||||||
|
|
||||||
The full canonical reference lives at
|
The full canonical reference lives at
|
||||||
[`docs/002. hsmc-semantics-formal.md`](docs/002.%20hsmc-semantics-formal.md);
|
[`docs/002. hsmc-semantics-formal.md`](docs/002.%20hsmc-semantics-formal.md);
|
||||||
@@ -73,20 +193,70 @@ you're **simultaneously** in `Root`, `A`, `B`, and `C`. The root is
|
|||||||
always active until termination.
|
always active until termination.
|
||||||
|
|
||||||
The innermost active state is usually a leaf, but it can also be a
|
The innermost active state is usually a leaf, but it can also be a
|
||||||
composite that has children but no `default(...)` — see rule 2.
|
composite that has children but no `default(...)` — see rule 5.
|
||||||
|
|
||||||
This is the most important rule. Every rule below is downstream of it.
|
This is the most important rule. Every rule below is downstream of it.
|
||||||
If a behavior somewhere else seems weird, come back here — it's
|
If a behavior somewhere else seems weird, come back here — it's
|
||||||
probably the resolution.
|
probably the resolution.
|
||||||
|
|
||||||
### 2. `default(...)` — a transition that fires immediately on entry
|
### 2. Transitions — exit to LCA, enter to target
|
||||||
|
|
||||||
|
A transition moves the active path from the current innermost state to
|
||||||
|
some target state. The algorithm is uniform regardless of what
|
||||||
|
triggered it (event, timer, or default — see later rules):
|
||||||
|
|
||||||
|
1. Find the LCA (lowest common ancestor) of current and target.
|
||||||
|
2. Walk **up** from current, exiting each state, until you hit the LCA.
|
||||||
|
The LCA itself is **not** exited.
|
||||||
|
3. Walk **down** from the LCA to target, entering each state. The LCA
|
||||||
|
is **not** re-entered.
|
||||||
|
|
||||||
|
The LCA always exists because root is always an upper bound. State
|
||||||
|
names are globally unique across the chart, so a transition can target
|
||||||
|
**any** state — siblings, cousins, ancestors, the root itself
|
||||||
|
(`on(Trig) => MyChart;`).
|
||||||
|
|
||||||
|
### 3. The up-transition rule — never re-enter what you never left
|
||||||
|
|
||||||
|
If your transition target is **already on the active path** (i.e., it's
|
||||||
|
an ancestor of the innermost state), then step 3 of the transition
|
||||||
|
algorithm does nothing. You exit the states strictly between current
|
||||||
|
and target; the target itself is **not** re-entered.
|
||||||
|
|
||||||
|
So from leaf `C` inside `B` inside `A`, an `on(Up) => A;` exits `C`
|
||||||
|
then `B`. `A`'s entry actions don't fire. `A`'s timers don't restart.
|
||||||
|
`A`'s durings keep running.
|
||||||
|
|
||||||
|
You cannot enter a state you never left.
|
||||||
|
|
||||||
|
### 4. Entry / exit ordering
|
||||||
|
|
||||||
|
Direction is set by the path:
|
||||||
|
|
||||||
|
- **Entries** fire **outer-to-inner**: when entering down through a
|
||||||
|
path, the outermost state's entries fire first, then the next, then
|
||||||
|
the innermost's. Within a single state, entry actions fire in
|
||||||
|
declaration order. After a state's entries finish, its `during:`
|
||||||
|
activities start.
|
||||||
|
- **Exits** fire **inner-to-outer**: innermost first, then parent, then
|
||||||
|
grandparent. Within a single state, exit actions fire in declaration
|
||||||
|
order. Before a state's exits begin, its `during:` activities are
|
||||||
|
cancelled.
|
||||||
|
|
||||||
|
So a full transition reads: cancel durings on the way out → run exits
|
||||||
|
on the way out → run entries on the way in → start durings on the way
|
||||||
|
in.
|
||||||
|
|
||||||
|
### 5. `default(...)` — a transition that fires immediately on entry
|
||||||
|
|
||||||
Any state **may** declare one `default(...)`. It's a transition — same
|
Any state **may** declare one `default(...)`. It's a transition — same
|
||||||
LCA-aware algorithm as any event-triggered transition (rule 4) — that
|
LCA-aware algorithm as rule 2 — that fires **immediately** after the
|
||||||
fires **immediately** after the declaring state's entry actions finish
|
declaring state's entry actions finish (and its durings start). The
|
||||||
(and its durings start). The target may be **any** state in the chart:
|
target may be **any** state in the chart: a child, a sibling, an
|
||||||
a child, a sibling, an ancestor, anywhere. The compiler rejects cycles
|
ancestor, anywhere. Defaults chain: if the resulting target also has
|
||||||
in the default graph, so the chain is always finite.
|
a `default(...)`, it fires too, and so on until a state with no
|
||||||
|
default is reached. The compiler rejects cycles in the default graph,
|
||||||
|
so the chain is always finite.
|
||||||
|
|
||||||
The classic case is a composite descending to a leaf:
|
The classic case is a composite descending to a leaf:
|
||||||
```
|
```
|
||||||
@@ -109,12 +279,17 @@ the state itself is the resting innermost active state until something
|
|||||||
explicitly transitions away. This is the "microwave" pattern: a
|
explicitly transitions away. This is the "microwave" pattern: a
|
||||||
`Standby` state with sub-modes that the chart enters only on demand.
|
`Standby` state with sub-modes that the chart enters only on demand.
|
||||||
|
|
||||||
### 3. Event bubbling — innermost first, first handler wins
|
A consequence of rule 3 (up-transitions): an ancestor's `default(...)`
|
||||||
|
only fires when that ancestor is freshly entered. Transitioning *up*
|
||||||
|
to an ancestor does not re-enter it, so its default does not re-fire.
|
||||||
|
|
||||||
|
### 6. Event bubbling — innermost first, first handler wins
|
||||||
|
|
||||||
An event arrives. Search starts at the **innermost** active state. If
|
An event arrives. Search starts at the **innermost** active state. If
|
||||||
that state has a handler, it runs and the event is consumed. If not,
|
that state has a handler, it runs and the event is consumed (the
|
||||||
walk up to the parent and check there. Repeat. If you reach the root
|
handler may execute a transition per rule 2). If not, walk up to the
|
||||||
with no handler, the event is silently discarded.
|
parent and check there. Repeat. If you reach the root with no handler,
|
||||||
|
the event is silently discarded.
|
||||||
|
|
||||||
Three corollaries:
|
Three corollaries:
|
||||||
|
|
||||||
@@ -124,58 +299,6 @@ Three corollaries:
|
|||||||
- **Timers don't bubble.** A timer belongs to the state that declared
|
- **Timers don't bubble.** A timer belongs to the state that declared
|
||||||
it; it fires only in that state's scope.
|
it; it fires only in that state's scope.
|
||||||
|
|
||||||
### 4. Transitions — exit to LCA, enter to target
|
|
||||||
|
|
||||||
Any transition's algorithm is uniform:
|
|
||||||
|
|
||||||
1. Find the LCA (lowest common ancestor) of current and target.
|
|
||||||
2. Walk **up** from current, exiting each state, until you hit the LCA.
|
|
||||||
The LCA itself is **not** exited.
|
|
||||||
3. Walk **down** from the LCA to target, entering each state. The LCA
|
|
||||||
is **not** re-entered.
|
|
||||||
4. If the target declares a `default(...)`, fire it (rule 2). Defaults
|
|
||||||
chain — each link is itself a full transition — until you reach a
|
|
||||||
state with no default. The compiler rejects cycles.
|
|
||||||
|
|
||||||
The LCA always exists because root is always an upper bound. State
|
|
||||||
names are globally unique across the chart, so a transition can target
|
|
||||||
**any** state — siblings, cousins, ancestors, the root itself
|
|
||||||
(`on(Trig) => MyChart;`).
|
|
||||||
|
|
||||||
### 5. The up-transition rule — never re-enter what you never left
|
|
||||||
|
|
||||||
If your transition target is **already on the active path** (i.e., it's
|
|
||||||
an ancestor of the innermost state), then steps 3 and 4 of the
|
|
||||||
transition algorithm do nothing. You exit the states strictly between
|
|
||||||
current and target; the target itself is **not** re-entered.
|
|
||||||
|
|
||||||
So from leaf `C` inside `B` inside `A`, an `on(Up) => A;` exits `C`
|
|
||||||
then `B`. `A`'s entry actions don't fire. `A`'s `default` does not
|
|
||||||
fire (it only fires when `A` is freshly entered, which it wasn't).
|
|
||||||
`A`'s timers don't restart. `A`'s durings keep running.
|
|
||||||
|
|
||||||
You cannot enter a state you never left.
|
|
||||||
|
|
||||||
### 6. Entry / exit ordering
|
|
||||||
|
|
||||||
Direction is set by the path:
|
|
||||||
|
|
||||||
- **Entries** fire **outer-to-inner**: when entering down through a
|
|
||||||
path, the outermost state's entries fire first, then the next, then
|
|
||||||
the innermost's. Within a single state, entry actions fire in
|
|
||||||
declaration order. After a state's entries finish, its `during:`
|
|
||||||
activities start. Then, if the state declared a `default(...)`,
|
|
||||||
it fires as a transition (rule 2) — chains continue until a state
|
|
||||||
with no default.
|
|
||||||
- **Exits** fire **inner-to-outer**: innermost first, then parent, then
|
|
||||||
grandparent. Within a single state, exit actions fire in declaration
|
|
||||||
order. Before a state's exits begin, its `during:` activities are
|
|
||||||
cancelled.
|
|
||||||
|
|
||||||
So a full transition reads: cancel durings on the way out → run exits
|
|
||||||
on the way out → run entries on the way in → start durings on the way
|
|
||||||
in.
|
|
||||||
|
|
||||||
### 7. Timers — armed by state lifecycle
|
### 7. Timers — armed by state lifecycle
|
||||||
|
|
||||||
A timer (`on(after D) => …` or `on(every D) => …`) is **armed** when
|
A timer (`on(after D) => …` or `on(every D) => …`) is **armed** when
|
||||||
@@ -196,7 +319,7 @@ restarts its timers from zero.
|
|||||||
durings on the active path race; each borrows disjoint `&mut` fields
|
durings on the active path race; each borrows disjoint `&mut` fields
|
||||||
of the context (Rust's split-borrow checker enforces this at compile
|
of the context (Rust's split-borrow checker enforces this at compile
|
||||||
time).
|
time).
|
||||||
- **Termination** is just rules 6 + 4 with target = "outside the
|
- **Termination** is just rules 2 + 4 with target = "outside the
|
||||||
chart": exit the entire active path inner-to-outer, then stop.
|
chart": exit the entire active path inner-to-outer, then stop.
|
||||||
Pending events drop. In-flight durings drop at their next `.await`.
|
Pending events drop. In-flight durings drop at their next `.await`.
|
||||||
|
|
||||||
@@ -206,78 +329,6 @@ spells out edge cases (queue overflow surfacing, same-tick timer ties,
|
|||||||
self-transitions on composites) but the rules above cover the
|
self-transitions on composites) but the rules above cover the
|
||||||
overwhelming majority of charts.
|
overwhelming majority of charts.
|
||||||
|
|
||||||
### What `hsmc` deliberately does NOT have
|
|
||||||
|
|
||||||
The bet is on **simple, consistent semantics**. These features are
|
|
||||||
omitted on purpose; for each, what to do instead:
|
|
||||||
|
|
||||||
| Missing feature | Use instead |
|
|
||||||
| ------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
||||||
| Guard conditions on transitions | Split source into two states, or have an action `emit()` a follow-up event for the chart to branch on |
|
|
||||||
| Orthogonal / parallel regions | Two charts as two tasks connected by channels, or multiple `during:` activities in one state |
|
|
||||||
| History states | Store last child explicitly in your context; transition to it on re-entry |
|
|
||||||
| Deferred events | Handle where they arrive (drop/log if not relevant), or have producer check state |
|
|
||||||
| Internal transitions | `action(Trig) => fn;` (no transition) — exactly that |
|
|
||||||
| State-local context | All state in the root context; durings borrow disjoint fields |
|
|
||||||
| Event priorities beyond FIFO | Higher-priority events on a separate channel into a separate, faster machine |
|
|
||||||
| Runtime statechart modification | Design every variation up front as states |
|
|
||||||
|
|
||||||
## Quickstart
|
|
||||||
|
|
||||||
Add to `Cargo.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
hsmc = { version = "0.5", features = ["embassy"] } # or ["tokio"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Minimal timer-only machine:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use hsmc::{statechart, Duration};
|
|
||||||
|
|
||||||
pub struct Ctx { pub ticks: u32 }
|
|
||||||
|
|
||||||
statechart! {
|
|
||||||
Blinker {
|
|
||||||
context: Ctx;
|
|
||||||
default(On);
|
|
||||||
|
|
||||||
state On {
|
|
||||||
entry: light_on;
|
|
||||||
on(after Duration::from_millis(500)) => Off;
|
|
||||||
}
|
|
||||||
state Off {
|
|
||||||
on(after Duration::from_millis(500)) => On;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlinkerActions for BlinkerActionContext<'_> {
|
|
||||||
async fn light_on(&mut self) { self.ticks += 1; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Drive it under tokio:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let mut m = Blinker::new(Ctx { ticks: 0 });
|
|
||||||
tokio::task::LocalSet::new().run_until(m.run()).await.unwrap();
|
|
||||||
```
|
|
||||||
|
|
||||||
Or under embassy:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
static CHANNEL: Channel<CriticalSectionRawMutex, __NoEvents, 8> = Channel::new();
|
|
||||||
// (timer-only machines don't need a channel; omit `events:` and use `Blinker::new(ctx)`.)
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
async fn blink_task() {
|
|
||||||
let mut m = Blinker::new(Ctx { ticks: 0 });
|
|
||||||
let _ = m.run().await;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `during:` — async activities scoped to a state
|
## `during:` — async activities scoped to a state
|
||||||
|
|
||||||
A `during:` runs while its state is on the active path. It's a plain async
|
A `during:` runs while its state is on the active path. It's a plain async
|
||||||
@@ -395,58 +446,58 @@ For `statechart! Foo { ... }` the macro emits:
|
|||||||
## Feature flags
|
## Feature flags
|
||||||
|
|
||||||
| Feature | Effect |
|
| Feature | Effect |
|
||||||
| --------- | -------------------------------------------------------------------------- |
|
| --------------- | -------------------------------------------------------------------------- |
|
||||||
| (default) | `no_std`, no async runtime. Drive via `step(Duration)` + `send(ev)`. |
|
| (default) | `no_std`, no async runtime. Drive via `step(Duration)` + `send(ev)`. |
|
||||||
| `tokio` | Async actions + `run()`. Targets single-thread via `LocalSet`. |
|
| `tokio` | Async actions + `run()`. Single-thread (`flavor = "current_thread"`). |
|
||||||
| `embassy` | Async actions + `run()`. Targets embassy's cooperative executor. `no_std`. |
|
| `embassy` | Async actions + `run()`. Embassy's cooperative executor. `no_std`. |
|
||||||
|
| `journal` | In-memory `Vec<TraceEvent>` of every observation (replay, byte-deterministic). |
|
||||||
|
| `trace-defmt` | Auto-emit `defmt::*` log lines for every observation. |
|
||||||
|
| `trace-log` | Auto-emit `log::*` log lines for every observation. |
|
||||||
|
| `trace-tracing` | Auto-emit `tracing::*` events with structured fields. |
|
||||||
|
|
||||||
`embassy` and `tokio` are mutually exclusive — enabling both is a compile
|
`embassy` and `tokio` are mutually exclusive — enabling both is a compile
|
||||||
error.
|
error. The `trace-*` and `journal` features are independently composable
|
||||||
|
with each other and with the runtime feature; with no sink feature on,
|
||||||
## Examples
|
the trace points compile to nothing (zero overhead).
|
||||||
|
|
||||||
| Example | Purpose |
|
|
||||||
| --------------- | --------------------------------------------------------------------- |
|
|
||||||
| `microwave` | Full-feature pure-event machine: entry/exit, timers, emit, terminate. |
|
|
||||||
| `microwave_tui` | TUI variant of microwave. |
|
|
||||||
| `during_radio` | `during:` activities simulating a radio task with RX + TX loops. |
|
|
||||||
| `embassy_full` | Embassy feature demonstration. |
|
|
||||||
|
|
||||||
Run `cargo run --example during_radio --features tokio` to see the
|
|
||||||
`during:` pattern end-to-end.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
`hsmc` ships with a Creusot deductive-verification crate
|
|
||||||
(`verification/`) that proves the runtime's `EventQueue` and
|
|
||||||
`TimerTable` correct against Pearlite specs. Why3 + alt-ergo + z3
|
|
||||||
discharge every VC mechanically. Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
just verify
|
|
||||||
```
|
|
||||||
|
|
||||||
The recipe is self-installing (mise pulls in the pinned nightly,
|
|
||||||
opam, Why3, the SMT solvers, and `cargo-creusot`) so a fresh clone
|
|
||||||
gets to a green proof in one command. See
|
|
||||||
[`verification/INVARIANTS.md`](verification/INVARIANTS.md) for the
|
|
||||||
spec-rule → proof mapping.
|
|
||||||
|
|
||||||
Beyond Creusot, the suite includes:
|
|
||||||
|
|
||||||
- **Integration tests** — full chart behavior and per-feature
|
|
||||||
determinism, with byte-equal expected-vs-actual journals on the
|
|
||||||
`det_*` deterministic-flow tests, plus trace-format regression
|
|
||||||
tests pinning every observation verb's textual rendering. Run
|
|
||||||
with `cargo test --workspace --features tokio,journal`.
|
|
||||||
- **`cargo mutants`** — full coverage under `cargo mutants --features
|
|
||||||
tokio,journal,trace-log`.
|
|
||||||
- **Miri** — clean, no UB on the runtime crates.
|
|
||||||
|
|
||||||
## Observability — one journal, multiple outputs
|
## Observability — one journal, multiple outputs
|
||||||
|
|
||||||
Every chart emits a single observation per atom of execution. That
|
On hsmc's side, turning on observability is a single Cargo feature.
|
||||||
observation fans out to whichever sinks you compile in:
|
On your side, it's whatever you'd already do to wire `defmt` / `log`
|
||||||
|
/ `tracing` to an output — hsmc does not initialize a logger
|
||||||
|
(libraries shouldn't; `main` owns that decision).
|
||||||
|
|
||||||
|
If your firmware crate already uses `defmt`, add the feature and
|
||||||
|
you're done — chart events interleave with the rest of your defmt
|
||||||
|
output:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
hsmc = { version = "0.5", features = ["embassy", "trace-defmt"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
For a desktop crate, pick `log` or `tracing` and do that framework's
|
||||||
|
normal one-line init in `main`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
hsmc = { version = "0.5", features = ["tokio", "trace-tracing"] }
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
let mut m = Blinker::new(Ctx { on: false, blinks: 0 });
|
||||||
|
let _ = m.run().await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Swap `trace-tracing` for `trace-log` and `tracing_subscriber::fmt::init()`
|
||||||
|
for `env_logger::init()` if you prefer the `log` ecosystem; the rest is
|
||||||
|
identical. With no sink feature enabled at all, the observation calls
|
||||||
|
compile to `()` — the runtime is byte-identical to a chart with no
|
||||||
|
instrumentation.
|
||||||
|
|
||||||
| Feature | Sink |
|
| Feature | Sink |
|
||||||
| --------------- | -------------------------------------------------------- |
|
| --------------- | -------------------------------------------------------- |
|
||||||
@@ -455,10 +506,9 @@ observation fans out to whichever sinks you compile in:
|
|||||||
| `trace-log` | `log::*` log lines |
|
| `trace-log` | `log::*` log lines |
|
||||||
| `trace-tracing` | `tracing::*` events with structured fields |
|
| `trace-tracing` | `tracing::*` events with structured fields |
|
||||||
|
|
||||||
With **no** sink feature on, observation is fully elided at compile
|
Multiple sinks can be enabled simultaneously; the journal IS the
|
||||||
time — zero overhead. Multiple sinks can be enabled simultaneously;
|
observation stream and trace backends are textual renderings of it,
|
||||||
the journal IS the observation stream and trace backends are textual
|
so they cannot diverge.
|
||||||
renderings of it, so they cannot diverge.
|
|
||||||
|
|
||||||
The textual format is logfmt-style — greppable and regex-parseable:
|
The textual format is logfmt-style — greppable and regex-parseable:
|
||||||
|
|
||||||
@@ -509,19 +559,70 @@ transition: an event (`event:VariantName`), a timer
|
|||||||
(`timer:State/timer-id`), or `internal`. Real-life captures can be
|
(`timer:State/timer-id`), or `internal`. Real-life captures can be
|
||||||
diffed against expected behavior the same way the journal can.
|
diffed against expected behavior the same way the journal can.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
| Example | Purpose |
|
||||||
|
| --------------- | --------------------------------------------------------------------- |
|
||||||
|
| `microwave` | Full-feature pure-event machine: entry/exit, timers, emit, terminate. |
|
||||||
|
| `during_radio` | `during:` activities simulating a radio task with RX + TX loops. |
|
||||||
|
| `embassy_full` | Embassy feature demonstration. |
|
||||||
|
|
||||||
|
Run `cargo run --example during_radio --features tokio` to see the
|
||||||
|
`during:` pattern end-to-end.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
`hsmc` ships with a Creusot deductive-verification crate
|
||||||
|
(`verification/`) that proves the runtime's `EventQueue` and
|
||||||
|
`TimerTable` correct against Pearlite specs. Why3 + alt-ergo + z3
|
||||||
|
discharge every VC mechanically. Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just verify
|
||||||
|
```
|
||||||
|
|
||||||
|
The recipe is self-installing (mise pulls in the pinned nightly,
|
||||||
|
opam, Why3, the SMT solvers, and `cargo-creusot`) so a fresh clone
|
||||||
|
gets to a green proof in one command. See
|
||||||
|
[`verification/INVARIANTS.md`](verification/INVARIANTS.md) for the
|
||||||
|
spec-rule → proof mapping.
|
||||||
|
|
||||||
|
Beyond Creusot, the suite includes:
|
||||||
|
|
||||||
|
- **Integration tests** — full chart behavior and per-feature
|
||||||
|
determinism, with byte-equal expected-vs-actual journals on the
|
||||||
|
`det_*` deterministic-flow tests, plus trace-format regression
|
||||||
|
tests pinning every observation verb's textual rendering. Run
|
||||||
|
with `cargo test --workspace --features tokio,journal`.
|
||||||
|
- **`cargo mutants`** — full coverage under `cargo mutants --features
|
||||||
|
tokio,journal,trace-log`.
|
||||||
|
- **Miri** — clean, no UB on the runtime crates.
|
||||||
|
|
||||||
|
## What `hsmc` deliberately does NOT have
|
||||||
|
|
||||||
|
The bet is on **simple, consistent semantics**. These features are
|
||||||
|
omitted on purpose; for each, what to do instead:
|
||||||
|
|
||||||
|
| Missing feature | Use instead |
|
||||||
|
| ------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||||
|
| Guard conditions on transitions | Split source into two states, or have an action `emit()` a follow-up event for the chart to branch on |
|
||||||
|
| Orthogonal / parallel regions | Two charts as two tasks connected by channels, or multiple `during:` activities in one state |
|
||||||
|
| History states | Store last child explicitly in your context; transition to it on re-entry |
|
||||||
|
| Deferred events | Handle where they arrive (drop/log if not relevant), or have producer check state |
|
||||||
|
| Internal transitions | `action(Trig) => fn;` (no transition) — exactly that |
|
||||||
|
| State-local context | All state in the root context; durings borrow disjoint fields |
|
||||||
|
| Event priorities beyond FIFO | Higher-priority events on a separate channel into a separate, faster machine |
|
||||||
|
| Runtime statechart modification | Design every variation up front as states |
|
||||||
|
| Multi-threaded tokio | `flavor = "current_thread"` is the supported path — one task per chart |
|
||||||
|
|
||||||
|
See [`docs/002. hsmc-semantics-formal.md`](docs/002.%20hsmc-semantics-formal.md)
|
||||||
|
§ "What `hsmc` deliberately does NOT have" for the rationale on each.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Pre-1.0. Per-release breaking changes are documented in
|
Pre-1.0. Per-release breaking changes are documented in
|
||||||
[`CHANGELOG.md`](CHANGELOG.md).
|
[`CHANGELOG.md`](CHANGELOG.md).
|
||||||
|
|
||||||
Out of scope (deliberate): guard conditions, orthogonal/parallel
|
|
||||||
regions, history states, deferred events, internal transitions,
|
|
||||||
state-local context, event priorities beyond FIFO, runtime
|
|
||||||
statechart modification, multi-threaded tokio. See
|
|
||||||
[`docs/002. hsmc-semantics-formal.md`](docs/002.%20hsmc-semantics-formal.md)
|
|
||||||
§ "What `hsmc` deliberately does NOT have" for the rationale and
|
|
||||||
what to use instead.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Dual-licensed MIT OR Apache-2.0.
|
MIT. Copyright (c) 2026 Stephen Waits <steve@waits.net>. See [LICENSE](LICENSE).
|
||||||
File renamed without changes.
@@ -1,201 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for describing the origin of the Work and
|
|
||||||
reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Support. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or support.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2026 Stephen Waits
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied. See the License for the specific language governing permissions
|
|
||||||
and limitations under the License.
|
|
||||||
+1
-5
@@ -11,7 +11,6 @@ readme = "../README.md"
|
|||||||
keywords = ["statechart", "state-machine", "hsm", "embassy", "embedded"]
|
keywords = ["statechart", "state-machine", "hsm", "embassy", "embedded"]
|
||||||
categories = ["embedded", "no-std", "asynchronous", "rust-patterns"]
|
categories = ["embedded", "no-std", "asynchronous", "rust-patterns"]
|
||||||
autoexamples = false
|
autoexamples = false
|
||||||
exclude = ["examples/microwave_tui.rs"]
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
@@ -40,7 +39,7 @@ trace-tracing = ["dep:tracing"]
|
|||||||
journal = []
|
journal = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hsmc-macros = { path = "../hsmc-macros", version = "0.5.0" }
|
hsmc-macros = { path = "../hsmc-macros", version = "0.5.1" }
|
||||||
heapless = "0.8"
|
heapless = "0.8"
|
||||||
tokio = { version = "1", features = ["sync", "time", "rt", "macros"], optional = true }
|
tokio = { version = "1", features = ["sync", "time", "rt", "macros"], optional = true }
|
||||||
embassy-sync = { version = "0.8", optional = true }
|
embassy-sync = { version = "0.8", optional = true }
|
||||||
@@ -53,9 +52,6 @@ tracing = { version = "0.1", default-features = false, optional = true }
|
|||||||
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))'.dev-dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))'.dev-dependencies]
|
||||||
trybuild = "1"
|
trybuild = "1"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "test-util"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "test-util"] }
|
||||||
ratatui = "0.28"
|
|
||||||
crossterm = { version = "0.28", features = ["event-stream"] }
|
|
||||||
futures = "0.3"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||||
iai-callgrind = "0.16"
|
iai-callgrind = "0.16"
|
||||||
|
|||||||
File renamed without changes.
@@ -1,201 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for describing the origin of the Work and
|
|
||||||
reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Support. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or support.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2026 Stephen Waits
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied. See the License for the specific language governing permissions
|
|
||||||
and limitations under the License.
|
|
||||||
@@ -1,761 +0,0 @@
|
|||||||
//! Interactive microwave oven TUI driven by `hsmc`.
|
|
||||||
//!
|
|
||||||
//! Run it:
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! cargo run --example microwave_tui --features tokio
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Keys:
|
|
||||||
//! S — Start cook O — Open door
|
|
||||||
//! X — Stop / cancel C — Close door
|
|
||||||
//! K — Simulate power failure (terminates the machine)
|
|
||||||
//! Q — Quit
|
|
||||||
//!
|
|
||||||
//! This demonstrates the same statechart as `microwave.rs` but hooked up to
|
|
||||||
//! live keyboard input and a real-time ratatui UI. The state machine runs
|
|
||||||
//! on `oven.run()` in one task; a second task reads keys and forwards them
|
|
||||||
//! via a cloned `sender()`; a third task redraws the screen 30×/sec using
|
|
||||||
//! state published by entry/exit actions.
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
collections::VecDeque,
|
|
||||||
io,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crossterm::{
|
|
||||||
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode, KeyEventKind},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
};
|
|
||||||
use futures::StreamExt;
|
|
||||||
use hsmc::{statechart, Duration};
|
|
||||||
use ratatui::{
|
|
||||||
backend::CrosstermBackend,
|
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
|
||||||
style::{Color, Modifier, Style, Stylize},
|
|
||||||
text::{Line, Span},
|
|
||||||
widgets::{Block, Borders, Gauge, Paragraph, Wrap},
|
|
||||||
Terminal,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------- Shared UI state ----------
|
|
||||||
//
|
|
||||||
// The actions write into this through the user context. The render task
|
|
||||||
// reads it. `Arc<Mutex<_>>` keeps it cheap and thread-safe.
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct UiState {
|
|
||||||
state_path: Vec<&'static str>,
|
|
||||||
display: String,
|
|
||||||
magnetron: bool,
|
|
||||||
heater: bool,
|
|
||||||
door_open: bool,
|
|
||||||
blink: bool,
|
|
||||||
log: VecDeque<String>,
|
|
||||||
/// Instant at which the cook should complete. Advances by the pause
|
|
||||||
/// duration on resume so the progress bar freezes while paused.
|
|
||||||
cook_deadline: Option<Instant>,
|
|
||||||
cook_total: Duration,
|
|
||||||
/// Set when we enter Paused; cleared on resume. Freezes the gauge.
|
|
||||||
paused_at: Option<Instant>,
|
|
||||||
terminated: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UiState {
|
|
||||||
async fn log(&mut self, msg: impl Into<String>) {
|
|
||||||
self.log.push_back(msg.into());
|
|
||||||
while self.log.len() > 50 {
|
|
||||||
self.log.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async fn set_path(&mut self, path: &[&'static str]) {
|
|
||||||
self.state_path = path.to_vec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- User context ----------
|
|
||||||
|
|
||||||
pub struct OvenContext {
|
|
||||||
pub ui: Arc<Mutex<UiState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Events ----------
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum OvenEvent {
|
|
||||||
StartPressed,
|
|
||||||
StopPressed,
|
|
||||||
DoorOpened,
|
|
||||||
DoorClosed,
|
|
||||||
Reset,
|
|
||||||
/// Sent by the external watchdog when the adjusted cook deadline is met.
|
|
||||||
/// Using an event instead of a static `Duration` transition lets us pause
|
|
||||||
/// and resume the cook clock — something hsmc timers can't do on their own.
|
|
||||||
CookDone,
|
|
||||||
PowerFailure,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Statechart ----------
|
|
||||||
//
|
|
||||||
// Same shape as microwave.rs (every feature exercised) with a longer cook
|
|
||||||
// time so the user has time to interact. The cook timer lives on `Running`
|
|
||||||
// so it survives Heating⇄Paused transitions.
|
|
||||||
|
|
||||||
const COOK_MS: u64 = 8_000;
|
|
||||||
|
|
||||||
statechart! {
|
|
||||||
Oven {
|
|
||||||
context: OvenContext;
|
|
||||||
events: OvenEvent;
|
|
||||||
|
|
||||||
entry: boot_self_test;
|
|
||||||
entry: log_boot_done;
|
|
||||||
exit: save_last_state;
|
|
||||||
exit: cut_all_power;
|
|
||||||
default(Idle);
|
|
||||||
terminate(PowerFailure);
|
|
||||||
|
|
||||||
// Root-level door tracking. Any state whose local handler shadows these
|
|
||||||
// must repeat `mark_door_*` alongside its own handlers.
|
|
||||||
on(DoorOpened) => mark_door_open;
|
|
||||||
on(DoorClosed) => mark_door_closed;
|
|
||||||
|
|
||||||
// Idle is split so Start can only fire when the door is closed. This is
|
|
||||||
// the HSM equivalent of a guard — a structural check rather than a
|
|
||||||
// boolean predicate.
|
|
||||||
state Idle {
|
|
||||||
// `sync_door` runs after the default descent completes handling, then
|
|
||||||
// emits DoorOpened if the door is physically open. That re-aligns the
|
|
||||||
// machine with reality when we land in Idle from Running (e.g. Stop
|
|
||||||
// pressed while door is open). The emitted event is processed after
|
|
||||||
// the entry cascade, so DoorClosed → DoorOpen happens cleanly.
|
|
||||||
entry: enter_idle, sync_door;
|
|
||||||
default(DoorClosed);
|
|
||||||
|
|
||||||
state DoorClosed {
|
|
||||||
entry: show_ready;
|
|
||||||
// Local handlers, so we must repeat mark_door_open here.
|
|
||||||
on(DoorOpened) => mark_door_open;
|
|
||||||
on(DoorOpened) => DoorOpen;
|
|
||||||
|
|
||||||
on(StartPressed) => record_start;
|
|
||||||
on(StartPressed) => prime_cook;
|
|
||||||
on(StartPressed) => Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
state DoorOpen {
|
|
||||||
entry: show_close_door;
|
|
||||||
on(DoorClosed) => mark_door_closed;
|
|
||||||
on(DoorClosed) => DoorClosed;
|
|
||||||
// Pressing Start while door is open: beep + log, no transition.
|
|
||||||
on(StartPressed) => warn_door_open;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state Running {
|
|
||||||
entry: enter_running, start_cook_clock;
|
|
||||||
exit: stop_cook_clock;
|
|
||||||
default(Cooking);
|
|
||||||
|
|
||||||
on(after Duration::from_millis(250)) => heartbeat;
|
|
||||||
on(StopPressed) => Idle;
|
|
||||||
on(CookDone) => Done;
|
|
||||||
|
|
||||||
state Cooking {
|
|
||||||
entry: enter_cooking, show_cooking;
|
|
||||||
default(Heating);
|
|
||||||
|
|
||||||
state Heating {
|
|
||||||
// Magnetron + heater live on Heating so they turn off the
|
|
||||||
// instant we leave for Paused.
|
|
||||||
entry: enter_heating, start_magnetron, heater_on;
|
|
||||||
exit: heater_off, stop_magnetron;
|
|
||||||
on(DoorOpened) => mark_door_open;
|
|
||||||
on(DoorOpened) => pause_cook_clock;
|
|
||||||
on(DoorOpened) => Suspended;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suspended = "cook is paused". Two substates express the classic
|
|
||||||
// microwave rule: closing the door alone does NOT resume cooking
|
|
||||||
// — the user must press Start. Pressing Stop (handled by Running)
|
|
||||||
// cancels entirely back to Idle.
|
|
||||||
state Suspended {
|
|
||||||
entry: enter_suspended, start_pause_blink;
|
|
||||||
exit: stop_pause_blink;
|
|
||||||
default(DoorAjar);
|
|
||||||
|
|
||||||
state DoorAjar {
|
|
||||||
entry: show_paused;
|
|
||||||
on(DoorClosed) => mark_door_closed;
|
|
||||||
on(DoorClosed) => AwaitingResume;
|
|
||||||
}
|
|
||||||
|
|
||||||
state AwaitingResume {
|
|
||||||
entry: show_press_start;
|
|
||||||
on(DoorOpened) => mark_door_open;
|
|
||||||
on(DoorOpened) => DoorAjar;
|
|
||||||
|
|
||||||
// Actions fire before the transition, so the clock is
|
|
||||||
// resumed and the log line written before we re-enter
|
|
||||||
// Heating (which turns the magnetron back on).
|
|
||||||
on(StartPressed) => log_resume;
|
|
||||||
on(StartPressed) => resume_cook_clock;
|
|
||||||
on(StartPressed) => Heating;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state Done {
|
|
||||||
entry: enter_done, beep, show_done;
|
|
||||||
exit: exit_done;
|
|
||||||
|
|
||||||
on(after Duration::from_millis(2000)) => schedule_auto_reset;
|
|
||||||
on(StartPressed) => Done;
|
|
||||||
on(Reset) => Idle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Actions ----------
|
|
||||||
//
|
|
||||||
// Every method is either publishing to the shared UI state or emitting an
|
|
||||||
// event back into the machine.
|
|
||||||
|
|
||||||
impl OvenActions for OvenActionContext<'_> {
|
|
||||||
// Root
|
|
||||||
async fn boot_self_test(&mut self) { self.ui.lock().unwrap().log("BOOT self-test ok"); }
|
|
||||||
async fn log_boot_done(&mut self) { self.ui.lock().unwrap().log("BOOT ready"); }
|
|
||||||
async fn save_last_state(&mut self){ self.ui.lock().unwrap().log("SHUTDOWN persisted state"); }
|
|
||||||
async fn cut_all_power(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.magnetron = false;
|
|
||||||
ui.heater = false;
|
|
||||||
ui.display = "-- OFF --".into();
|
|
||||||
ui.terminated = true;
|
|
||||||
ui.log("SHUTDOWN power cut");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Door tracking (root-level)
|
|
||||||
async fn mark_door_open(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.door_open = true;
|
|
||||||
}
|
|
||||||
async fn mark_door_closed(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.door_open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Idle
|
|
||||||
async fn enter_idle(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Idle"]);
|
|
||||||
}
|
|
||||||
async fn sync_door(&mut self) {
|
|
||||||
let door_open = self.ui.lock().unwrap().door_open;
|
|
||||||
if door_open {
|
|
||||||
self.ui.lock().unwrap().log("IDLE door still open — syncing");
|
|
||||||
let _ = self.emit(OvenEvent::DoorOpened);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async fn show_ready(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Idle", "DoorClosed"]);
|
|
||||||
ui.display = "READY".into();
|
|
||||||
ui.log("IDLE ready");
|
|
||||||
}
|
|
||||||
async fn show_close_door(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Idle", "DoorOpen"]);
|
|
||||||
ui.display = "CLOSE DOOR".into();
|
|
||||||
ui.log("IDLE door is open");
|
|
||||||
}
|
|
||||||
async fn warn_door_open(&mut self) {
|
|
||||||
self.ui.lock().unwrap().log("WARN can't start — door open");
|
|
||||||
}
|
|
||||||
async fn record_start(&mut self) { self.ui.lock().unwrap().log("BTN start pressed"); }
|
|
||||||
async fn prime_cook(&mut self) { self.ui.lock().unwrap().log("BTN primed cook"); }
|
|
||||||
async fn beep(&mut self) { self.ui.lock().unwrap().log("*BEEP*"); }
|
|
||||||
|
|
||||||
// Running
|
|
||||||
async fn enter_running(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Running"]);
|
|
||||||
}
|
|
||||||
async fn start_magnetron(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.magnetron = true;
|
|
||||||
ui.log("MAG on");
|
|
||||||
}
|
|
||||||
async fn stop_magnetron(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.magnetron = false;
|
|
||||||
ui.log("MAG off");
|
|
||||||
}
|
|
||||||
async fn start_cook_clock(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
let total = Duration::from_millis(COOK_MS);
|
|
||||||
ui.cook_total = total;
|
|
||||||
ui.cook_deadline = Some(Instant::now() + total);
|
|
||||||
ui.paused_at = None;
|
|
||||||
}
|
|
||||||
async fn stop_cook_clock(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.cook_deadline = None;
|
|
||||||
ui.paused_at = None;
|
|
||||||
}
|
|
||||||
async fn pause_cook_clock(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.paused_at = Some(Instant::now());
|
|
||||||
ui.log("CLOCK paused");
|
|
||||||
}
|
|
||||||
async fn resume_cook_clock(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
if let (Some(paused_at), Some(deadline)) = (ui.paused_at, ui.cook_deadline) {
|
|
||||||
let skipped = paused_at.elapsed();
|
|
||||||
ui.cook_deadline = Some(deadline + skipped);
|
|
||||||
ui.log(format!("CLOCK resume (+{}ms)", skipped.as_millis()));
|
|
||||||
}
|
|
||||||
ui.paused_at = None;
|
|
||||||
}
|
|
||||||
async fn heartbeat(&mut self) { /* UI gauge animates on its own */ }
|
|
||||||
|
|
||||||
// Cooking / Heating
|
|
||||||
async fn enter_cooking(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Running", "Cooking"]);
|
|
||||||
}
|
|
||||||
async fn show_cooking(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.display = "COOKING".into();
|
|
||||||
ui.blink = false;
|
|
||||||
}
|
|
||||||
async fn enter_heating(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Running", "Cooking", "Heating"]);
|
|
||||||
ui.blink = false;
|
|
||||||
ui.display = "COOKING".into();
|
|
||||||
}
|
|
||||||
async fn heater_on(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.heater = true;
|
|
||||||
ui.log("HEAT on");
|
|
||||||
}
|
|
||||||
async fn heater_off(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.heater = false;
|
|
||||||
ui.log("HEAT off");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suspended (paused)
|
|
||||||
async fn enter_suspended(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Running", "Cooking", "Suspended"]);
|
|
||||||
}
|
|
||||||
async fn show_paused(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Running", "Cooking", "Suspended", "DoorAjar"]);
|
|
||||||
ui.display = "PAUSED".into();
|
|
||||||
}
|
|
||||||
async fn show_press_start(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Running", "Cooking", "Suspended", "AwaitingResume"]);
|
|
||||||
ui.display = "PRESS START".into();
|
|
||||||
ui.log("PAUSE door closed — press Start to resume");
|
|
||||||
}
|
|
||||||
async fn start_pause_blink(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.blink = true;
|
|
||||||
ui.log("PAUSE blink on");
|
|
||||||
}
|
|
||||||
async fn stop_pause_blink(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.blink = false;
|
|
||||||
ui.log("PAUSE blink off");
|
|
||||||
}
|
|
||||||
async fn log_resume(&mut self) { self.ui.lock().unwrap().log("PAUSE resuming"); }
|
|
||||||
|
|
||||||
// Done
|
|
||||||
async fn enter_done(&mut self) {
|
|
||||||
let mut ui = self.ui.lock().unwrap();
|
|
||||||
ui.set_path(&["Oven", "Done"]);
|
|
||||||
}
|
|
||||||
async fn show_done(&mut self) {
|
|
||||||
self.ui.lock().unwrap().display = "DONE!".into();
|
|
||||||
}
|
|
||||||
async fn exit_done(&mut self) { self.ui.lock().unwrap().log("DONE exit"); }
|
|
||||||
async fn schedule_auto_reset(&mut self) {
|
|
||||||
self.ui.lock().unwrap().log("DONE auto-reset queued");
|
|
||||||
self.emit(OvenEvent::Reset).expect("queue has room");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Rendering ----------
|
|
||||||
|
|
||||||
fn draw(frame: &mut ratatui::Frame<'_>, ui: &UiState) {
|
|
||||||
let size = frame.area();
|
|
||||||
let root = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(16),
|
|
||||||
Constraint::Length(10),
|
|
||||||
Constraint::Length(3),
|
|
||||||
])
|
|
||||||
.split(size);
|
|
||||||
|
|
||||||
// --- Title bar ---
|
|
||||||
let title = if ui.terminated {
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(" MICROWAVE ", Style::new().bg(Color::Red).fg(Color::White).bold()),
|
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled("TERMINATED", Style::new().fg(Color::Red).bold()),
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(" MICROWAVE ", Style::new().bg(Color::Yellow).fg(Color::Black).bold()),
|
|
||||||
Span::raw(" hsmc statechart demo · press Q to quit"),
|
|
||||||
])
|
|
||||||
};
|
|
||||||
frame.render_widget(Paragraph::new(title), root[0]);
|
|
||||||
|
|
||||||
// --- Main area: oven on the left, status on the right ---
|
|
||||||
let main = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
|
|
||||||
.split(root[1]);
|
|
||||||
|
|
||||||
draw_oven(frame, main[0], ui);
|
|
||||||
draw_status(frame, main[1], ui);
|
|
||||||
|
|
||||||
// --- Log ---
|
|
||||||
let log_lines: Vec<Line> = ui.log.iter().rev().take(8).rev()
|
|
||||||
.map(|s| Line::from(s.as_str()))
|
|
||||||
.collect();
|
|
||||||
let log = Paragraph::new(log_lines)
|
|
||||||
.block(Block::default().title(" Event log ").borders(Borders::ALL))
|
|
||||||
.wrap(Wrap { trim: false });
|
|
||||||
frame.render_widget(log, root[2]);
|
|
||||||
|
|
||||||
// --- Keys ---
|
|
||||||
let keys = Line::from(vec![
|
|
||||||
key_hint("S", "Start"),
|
|
||||||
Span::raw(" "),
|
|
||||||
key_hint("X", "Stop"),
|
|
||||||
Span::raw(" "),
|
|
||||||
key_hint("O", "Open door"),
|
|
||||||
Span::raw(" "),
|
|
||||||
key_hint("C", "Close door"),
|
|
||||||
Span::raw(" "),
|
|
||||||
key_hint("K", "Kill (power fail)"),
|
|
||||||
Span::raw(" "),
|
|
||||||
key_hint("Q", "Quit"),
|
|
||||||
]);
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(keys).block(Block::default().borders(Borders::ALL).title(" Controls ")),
|
|
||||||
root[3],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key_hint(k: &str, label: &str) -> Span<'static> {
|
|
||||||
Span::from(format!("[{}] {}", k, label)).fg(Color::Cyan)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_oven(frame: &mut ratatui::Frame<'_>, area: Rect, ui: &UiState) {
|
|
||||||
let block = Block::default().borders(Borders::ALL).title(" Oven ");
|
|
||||||
let inner = block.inner(area);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let rows = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(5),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(2),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(inner);
|
|
||||||
|
|
||||||
// Door indicator line
|
|
||||||
let door_line = if ui.door_open {
|
|
||||||
Span::styled(" ┌───── DOOR OPEN ─────┐", Style::new().fg(Color::Yellow).bold())
|
|
||||||
} else {
|
|
||||||
Span::styled(" ┌───── door closed ─────┐", Style::new().fg(Color::DarkGray))
|
|
||||||
};
|
|
||||||
frame.render_widget(Paragraph::new(Line::from(door_line)), rows[0]);
|
|
||||||
|
|
||||||
// Big display
|
|
||||||
let display_style = if ui.terminated {
|
|
||||||
Style::new().fg(Color::Red).bold()
|
|
||||||
} else if ui.blink && (Instant::now().elapsed().as_millis() / 300) % 2 == 0 {
|
|
||||||
Style::new().fg(Color::Yellow).bold()
|
|
||||||
} else if ui.display == "DONE!" {
|
|
||||||
Style::new().fg(Color::Green).bold()
|
|
||||||
} else if ui.display == "COOKING" {
|
|
||||||
Style::new().fg(Color::LightRed).bold()
|
|
||||||
} else {
|
|
||||||
Style::new().fg(Color::Cyan).bold()
|
|
||||||
};
|
|
||||||
let big = Paragraph::new(vec![
|
|
||||||
Line::from(""),
|
|
||||||
Line::from(Span::styled(
|
|
||||||
format!(" ┃ {:^14} ┃", ui.display),
|
|
||||||
display_style,
|
|
||||||
)),
|
|
||||||
Line::from(Span::styled(" ┃ ┃", Style::new().fg(Color::DarkGray))),
|
|
||||||
Line::from(Span::styled(" ┗━━━━━━━━━━━━━━━━━━┛", Style::new().fg(Color::DarkGray))),
|
|
||||||
]);
|
|
||||||
frame.render_widget(big, rows[1]);
|
|
||||||
|
|
||||||
// Progress gauge for cook — freezes while paused.
|
|
||||||
let (ratio, label, color) = if let Some(deadline) = ui.cook_deadline {
|
|
||||||
let now = ui.paused_at.unwrap_or_else(Instant::now);
|
|
||||||
let total = ui.cook_total;
|
|
||||||
let remaining = deadline.saturating_duration_since(now);
|
|
||||||
let elapsed = total.saturating_sub(remaining);
|
|
||||||
let r = (elapsed.as_secs_f64() / total.as_secs_f64()).clamp(0.0, 1.0);
|
|
||||||
let label = format!("{:.1}s / {:.1}s{}",
|
|
||||||
elapsed.as_secs_f64(),
|
|
||||||
total.as_secs_f64(),
|
|
||||||
if ui.paused_at.is_some() { " (paused)" } else { "" },
|
|
||||||
);
|
|
||||||
let color = if ui.paused_at.is_some() { Color::Yellow } else { Color::LightRed };
|
|
||||||
(r, label, color)
|
|
||||||
} else {
|
|
||||||
(0.0, "—".to_string(), Color::DarkGray)
|
|
||||||
};
|
|
||||||
let gauge = Gauge::default()
|
|
||||||
.block(Block::default().borders(Borders::NONE))
|
|
||||||
.gauge_style(Style::new().fg(color))
|
|
||||||
.ratio(ratio)
|
|
||||||
.label(label);
|
|
||||||
frame.render_widget(gauge, rows[3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_status(frame: &mut ratatui::Frame<'_>, area: Rect, ui: &UiState) {
|
|
||||||
let block = Block::default().borders(Borders::ALL).title(" Status ");
|
|
||||||
let inner = block.inner(area);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let path = if ui.state_path.is_empty() {
|
|
||||||
"—".to_string()
|
|
||||||
} else {
|
|
||||||
ui.state_path.join(" › ")
|
|
||||||
};
|
|
||||||
|
|
||||||
let bullet = |on: bool| {
|
|
||||||
if on { Span::styled("●", Style::new().fg(Color::Green).bold()) }
|
|
||||||
else { Span::styled("○", Style::new().fg(Color::DarkGray)) }
|
|
||||||
};
|
|
||||||
|
|
||||||
let lines = vec![
|
|
||||||
Line::from(""),
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(" State path: ", Style::new().fg(Color::DarkGray)),
|
|
||||||
Span::styled(path, Style::new().fg(Color::White).bold()),
|
|
||||||
]),
|
|
||||||
Line::from(""),
|
|
||||||
Line::from(vec![
|
|
||||||
Span::raw(" Magnetron "),
|
|
||||||
bullet(ui.magnetron),
|
|
||||||
Span::raw(if ui.magnetron { " ON " } else { " off" }),
|
|
||||||
]),
|
|
||||||
Line::from(vec![
|
|
||||||
Span::raw(" Heater "),
|
|
||||||
bullet(ui.heater),
|
|
||||||
Span::raw(if ui.heater { " ON " } else { " off" }),
|
|
||||||
]),
|
|
||||||
Line::from(vec![
|
|
||||||
Span::raw(" Door "),
|
|
||||||
if ui.door_open {
|
|
||||||
Span::styled("OPEN ", Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
|
||||||
} else {
|
|
||||||
Span::styled("CLOSED", Style::new().fg(Color::DarkGray))
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
Line::from(""),
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(" hsmc tip: ", Style::new().fg(Color::DarkGray)),
|
|
||||||
Span::raw("events bubble · timers don't"),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
frame.render_widget(Paragraph::new(lines), inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Main ----------
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
async fn main() -> io::Result<()> {
|
|
||||||
// Shared UI state.
|
|
||||||
let ui = Arc::new(Mutex::new(UiState::default()));
|
|
||||||
|
|
||||||
// Build the machine.
|
|
||||||
let ctx = OvenContext { ui: Arc::clone(&ui) };
|
|
||||||
let mut oven = Oven::with_queue_capacity::<16>(ctx);
|
|
||||||
let sender = oven.sender();
|
|
||||||
let fault_sender = oven.sender();
|
|
||||||
let cook_sender = oven.sender();
|
|
||||||
|
|
||||||
// Raw-mode terminal.
|
|
||||||
enable_raw_mode()?;
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
|
|
||||||
// Render loop — 30 fps.
|
|
||||||
let ui_for_render = Arc::clone(&ui);
|
|
||||||
let (render_stop_tx, mut render_stop_rx) = tokio::sync::oneshot::channel::<()>();
|
|
||||||
let render_task = tokio::spawn(async move {
|
|
||||||
let mut interval = tokio::time::interval(Duration::from_millis(33));
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
_ = &mut render_stop_rx => break,
|
|
||||||
_ = interval.tick() => {
|
|
||||||
let snapshot = {
|
|
||||||
let g = ui_for_render.lock().unwrap();
|
|
||||||
UiSnapshot::from(&*g)
|
|
||||||
};
|
|
||||||
let _ = terminal.draw(|f| draw(f, &snapshot.into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Restore terminal.
|
|
||||||
let _ = disable_raw_mode();
|
|
||||||
let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture);
|
|
||||||
terminal
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cook-clock watchdog. Polls shared UI state; when we're not paused and
|
|
||||||
// the deadline has passed, fires a single CookDone event and then sleeps
|
|
||||||
// until the deadline is re-armed. The machine discards CookDone if the
|
|
||||||
// current state doesn't handle it (e.g. after StopPressed).
|
|
||||||
let ui_for_watchdog = Arc::clone(&ui);
|
|
||||||
let watchdog_task = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
||||||
let should_fire = {
|
|
||||||
let u = ui_for_watchdog.lock().unwrap();
|
|
||||||
if u.terminated { return; }
|
|
||||||
match (u.cook_deadline, u.paused_at) {
|
|
||||||
(Some(deadline), None) => Instant::now() >= deadline,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if should_fire {
|
|
||||||
let _ = cook_sender.send(OvenEvent::CookDone);
|
|
||||||
// Give the machine a beat to process and clear cook_deadline
|
|
||||||
// (via Running's exit → stop_cook_clock) before we check again.
|
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Input loop.
|
|
||||||
let ui_for_input = Arc::clone(&ui);
|
|
||||||
let input_task = tokio::spawn(async move {
|
|
||||||
let mut events = EventStream::new();
|
|
||||||
while let Some(Ok(ev)) = events.next().await {
|
|
||||||
if let Event::Key(k) = ev {
|
|
||||||
if k.kind != KeyEventKind::Press { continue; }
|
|
||||||
let code = k.code;
|
|
||||||
let msg = match code {
|
|
||||||
KeyCode::Char('s') | KeyCode::Char('S') => Some(OvenEvent::StartPressed),
|
|
||||||
KeyCode::Char('x') | KeyCode::Char('X') => Some(OvenEvent::StopPressed),
|
|
||||||
KeyCode::Char('o') | KeyCode::Char('O') => Some(OvenEvent::DoorOpened),
|
|
||||||
KeyCode::Char('c') | KeyCode::Char('C') => Some(OvenEvent::DoorClosed),
|
|
||||||
KeyCode::Char('k') | KeyCode::Char('K') => Some(OvenEvent::PowerFailure),
|
|
||||||
KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => {
|
|
||||||
// Quit: cleanly terminate the machine via PowerFailure.
|
|
||||||
let _ = fault_sender.send(OvenEvent::PowerFailure);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
if let Some(m) = msg {
|
|
||||||
ui_for_input.lock().unwrap().log(format!("KEY -> {:?}", m));
|
|
||||||
let _ = sender.send(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drive the state machine on the main task.
|
|
||||||
let _ = oven.run().await;
|
|
||||||
|
|
||||||
// Let the UI show the TERMINATED banner briefly.
|
|
||||||
tokio::time::sleep(Duration::from_millis(700)).await;
|
|
||||||
|
|
||||||
// Stop the render loop.
|
|
||||||
let _ = render_stop_tx.send(());
|
|
||||||
input_task.abort();
|
|
||||||
watchdog_task.abort();
|
|
||||||
let mut terminal = render_task.await.expect("render task");
|
|
||||||
|
|
||||||
// Belt-and-braces teardown in case the render loop didn't get to it.
|
|
||||||
let _ = disable_raw_mode();
|
|
||||||
let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture);
|
|
||||||
|
|
||||||
// Final transcript.
|
|
||||||
let ui = ui.lock().unwrap();
|
|
||||||
println!("\nFinal state: {}", ui.state_path.join(" › "));
|
|
||||||
println!("Events: {}", ui.log.len());
|
|
||||||
for line in ui.log.iter() {
|
|
||||||
println!(" {}", line);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot so we don't hold the Mutex across the render.
|
|
||||||
struct UiSnapshot {
|
|
||||||
state_path: Vec<&'static str>,
|
|
||||||
display: String,
|
|
||||||
magnetron: bool,
|
|
||||||
heater: bool,
|
|
||||||
door_open: bool,
|
|
||||||
blink: bool,
|
|
||||||
log: VecDeque<String>,
|
|
||||||
cook_deadline: Option<Instant>,
|
|
||||||
cook_total: Duration,
|
|
||||||
paused_at: Option<Instant>,
|
|
||||||
terminated: bool,
|
|
||||||
}
|
|
||||||
impl From<&UiState> for UiSnapshot {
|
|
||||||
fn from(u: &UiState) -> Self {
|
|
||||||
Self {
|
|
||||||
state_path: u.state_path.clone(),
|
|
||||||
display: u.display.clone(),
|
|
||||||
magnetron: u.magnetron,
|
|
||||||
heater: u.heater,
|
|
||||||
door_open: u.door_open,
|
|
||||||
blink: u.blink,
|
|
||||||
log: u.log.clone(),
|
|
||||||
cook_deadline: u.cook_deadline,
|
|
||||||
cook_total: u.cook_total,
|
|
||||||
paused_at: u.paused_at,
|
|
||||||
terminated: u.terminated,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<UiSnapshot> for UiState {
|
|
||||||
fn from(s: UiSnapshot) -> Self {
|
|
||||||
Self {
|
|
||||||
state_path: s.state_path,
|
|
||||||
display: s.display,
|
|
||||||
magnetron: s.magnetron,
|
|
||||||
heater: s.heater,
|
|
||||||
door_open: s.door_open,
|
|
||||||
blink: s.blink,
|
|
||||||
log: s.log,
|
|
||||||
cook_deadline: s.cook_deadline,
|
|
||||||
cook_total: s.cook_total,
|
|
||||||
paused_at: s.paused_at,
|
|
||||||
terminated: s.terminated,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user