Hierarchical statechart codegen for Rust with: - during: activities for state-scoped concurrent I/O - field-list borrowing (split-borrow inside the run loop) - embassy-first async with tokio support - STATE_CHART const for tooling/inspection - examples: microwave, microwave_tui, embassy_full, during_radio
1977 lines
83 KiB
Rust
1977 lines
83 KiB
Rust
//! Code generation for the `statechart!` macro.
|
|
|
|
use proc_macro2::{Span, TokenStream};
|
|
use quote::{format_ident, quote};
|
|
use syn::Ident;
|
|
|
|
use crate::ir::{HandlerKindIr, Ir, TriggerIr, VariantKind};
|
|
use crate::parse::{During, PayloadKind};
|
|
|
|
/// For each user-declared leaf state that has one or more `during:` activities
|
|
/// active (either declared on it, on any ancestor, or on root), return its
|
|
/// state id and the flattened list of durings in leaf-first order. Durings
|
|
/// declared on the same state follow their declaration order.
|
|
///
|
|
/// Leaf states with no active durings are omitted — the run loop uses its
|
|
/// default (channel+timer) path for those.
|
|
fn active_durings_per_leaf(ir: &Ir) -> Vec<(u16, Vec<During>)> {
|
|
let mut out = Vec::new();
|
|
for s in &ir.states {
|
|
if !s.children.is_empty() {
|
|
continue;
|
|
}
|
|
if s.parent.is_none() {
|
|
continue; // synthetic __Root
|
|
}
|
|
let mut chain: Vec<During> = Vec::new();
|
|
let mut cur = Some(s.id);
|
|
while let Some(id) = cur {
|
|
for d in &ir.states[id as usize].durings {
|
|
chain.push(d.clone());
|
|
}
|
|
cur = ir.states[id as usize].parent;
|
|
}
|
|
if !chain.is_empty() {
|
|
out.push((s.id, chain));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Emit `let dN = fn(&mut self.ctx.fN_0, &mut self.ctx.fN_1, ...);` for each
|
|
/// during in `chain`. Returns the list of binding identifiers in order so the
|
|
/// call site can reference them in the `select!`/`select_N` arguments.
|
|
fn emit_during_bindings(chain: &[During]) -> (Vec<Ident>, TokenStream) {
|
|
let mut idents = Vec::with_capacity(chain.len());
|
|
let mut stmts: Vec<TokenStream> = Vec::with_capacity(chain.len());
|
|
for (i, d) in chain.iter().enumerate() {
|
|
let bind = format_ident!("__hsmc_d{}", i);
|
|
let fn_name = &d.fn_name;
|
|
let field_refs = d.fields.iter().map(|f| quote! { &mut self.ctx.#f });
|
|
stmts.push(quote! {
|
|
let #bind = #fn_name(#(#field_refs),*);
|
|
});
|
|
idents.push(bind);
|
|
}
|
|
(idents, quote! { #(#stmts)* })
|
|
}
|
|
|
|
/// Emit the per-leaf match arms for the tokio run loop. Each arm constructs
|
|
/// the active during: futures (split-borrowing context fields), then races
|
|
/// them against the event channel and the next-timer sleep via
|
|
/// `tokio::select!`. The default arm — used for leaves with no durings — is
|
|
/// emitted by the caller.
|
|
fn emit_tokio_race_arms_with_channel(ir: &Ir) -> Vec<TokenStream> {
|
|
let mut arms = Vec::new();
|
|
for (sid, chain) in active_durings_per_leaf(ir) {
|
|
let (binds, stmts) = emit_during_bindings(&chain);
|
|
let select_arms = binds.iter().map(|b| {
|
|
quote! { ev = #b => __HsmcRace::Event(ev), }
|
|
});
|
|
arms.push(quote! {
|
|
Some(#sid) => {
|
|
#stmts
|
|
let sleep = ::tokio::time::sleep(sleep_dur);
|
|
::tokio::pin!(sleep);
|
|
let rx = self.__tokio_rx.as_mut().expect("hsmc: tokio channel");
|
|
::tokio::select! {
|
|
biased;
|
|
#(#select_arms)*
|
|
maybe = rx.recv() => match maybe {
|
|
Some(ev) => __HsmcRace::Event(ev),
|
|
None => __HsmcRace::ChannelClosed,
|
|
},
|
|
_ = &mut sleep => __HsmcRace::Timer,
|
|
}
|
|
}
|
|
});
|
|
}
|
|
arms
|
|
}
|
|
|
|
/// Same as `emit_tokio_race_arms_with_channel` but for timer-only machines
|
|
/// (no `events:` declared, no channel). The default arm races only the timer.
|
|
fn emit_tokio_race_arms_timer_only(ir: &Ir) -> Vec<TokenStream> {
|
|
let mut arms = Vec::new();
|
|
for (sid, chain) in active_durings_per_leaf(ir) {
|
|
let (binds, stmts) = emit_during_bindings(&chain);
|
|
let select_arms = binds.iter().map(|b| {
|
|
quote! { ev = #b => __HsmcRace::Event(ev), }
|
|
});
|
|
arms.push(quote! {
|
|
Some(#sid) => {
|
|
#stmts
|
|
let sleep = ::tokio::time::sleep(sleep_dur);
|
|
::tokio::pin!(sleep);
|
|
::tokio::select! {
|
|
biased;
|
|
#(#select_arms)*
|
|
_ = &mut sleep => __HsmcRace::Timer,
|
|
}
|
|
}
|
|
});
|
|
}
|
|
arms
|
|
}
|
|
|
|
/// Build a human-readable ASCII tree of the machine's hierarchy, suitable
|
|
/// for emitting as a `STATE_CHART` const. Each state shows its default-child
|
|
/// marker, entry/exit actions (if any), during activities, and handler
|
|
/// triggers. Used for `defmt::info!(M::STATE_CHART)` or panic messages.
|
|
fn emit_state_chart_string(ir: &Ir) -> String {
|
|
let mut out = String::new();
|
|
// Start with the machine name as the root label (root state name is
|
|
// synthetic `__Root` — don't surface it).
|
|
out.push_str(&ir.name.to_string());
|
|
out.push('\n');
|
|
// Top-level children of __Root: walk via __Root's children list.
|
|
let root_children: Vec<u16> = ir.states[0].children.clone();
|
|
let default_child_of_root = ir.states[0].default_child;
|
|
for (i, cid) in root_children.iter().enumerate() {
|
|
let last = i + 1 == root_children.len();
|
|
let is_default = Some(*cid) == default_child_of_root;
|
|
render_state(ir, *cid, "", last, is_default, &mut out);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn render_state(
|
|
ir: &Ir,
|
|
sid: u16,
|
|
prefix: &str,
|
|
is_last: bool,
|
|
is_default: bool,
|
|
out: &mut String,
|
|
) {
|
|
let s = &ir.states[sid as usize];
|
|
let branch = if is_last { "└── " } else { "├── " };
|
|
out.push_str(prefix);
|
|
out.push_str(branch);
|
|
if is_default {
|
|
out.push_str("[default] ");
|
|
}
|
|
out.push_str(&s.name.to_string());
|
|
out.push('\n');
|
|
let child_prefix = format!("{}{}", prefix, if is_last { " " } else { "│ " });
|
|
// Entry actions
|
|
for (_i, aid) in s.entries.iter().enumerate() {
|
|
let name = &ir.actions[*aid as usize].name;
|
|
out.push_str(&child_prefix);
|
|
out.push_str("• entry: ");
|
|
out.push_str(&name.to_string());
|
|
out.push('\n');
|
|
}
|
|
// Exit actions
|
|
for aid in &s.exits {
|
|
let name = &ir.actions[*aid as usize].name;
|
|
out.push_str(&child_prefix);
|
|
out.push_str("• exit: ");
|
|
out.push_str(&name.to_string());
|
|
out.push('\n');
|
|
}
|
|
// Durings
|
|
for d in &s.durings {
|
|
out.push_str(&child_prefix);
|
|
out.push_str("• during: ");
|
|
out.push_str(&d.fn_name.to_string());
|
|
out.push('(');
|
|
for (i, f) in d.fields.iter().enumerate() {
|
|
if i > 0 {
|
|
out.push_str(", ");
|
|
}
|
|
out.push_str(&f.to_string());
|
|
}
|
|
out.push_str(")\n");
|
|
}
|
|
// Handlers (summary)
|
|
for h in &s.handlers {
|
|
let trig = match &h.trigger {
|
|
crate::ir::TriggerIr::Event(_, name, _) => format!("on({})", name),
|
|
crate::ir::TriggerIr::Duration(tid) => {
|
|
let d = &ir.duration_triggers[*tid as usize];
|
|
let verb = if d.repeat { "every" } else { "after" };
|
|
format!("on({} {})", verb, d.key.split(':').next().unwrap_or("?"))
|
|
}
|
|
};
|
|
let target = match &h.kind {
|
|
crate::ir::HandlerKindIr::Transition(tgt, _) => format!("→ {}", tgt),
|
|
crate::ir::HandlerKindIr::Action(_, name) => format!("⇒ {}", name),
|
|
};
|
|
out.push_str(&child_prefix);
|
|
out.push_str("• ");
|
|
out.push_str(&trig);
|
|
out.push(' ');
|
|
out.push_str(&target);
|
|
out.push('\n');
|
|
}
|
|
// Child states
|
|
let default_child_of_s = s.default_child;
|
|
for (i, cid) in s.children.iter().enumerate() {
|
|
let last = i + 1 == s.children.len();
|
|
let is_def = Some(*cid) == default_child_of_s;
|
|
render_state(ir, *cid, &child_prefix, last, is_def, out);
|
|
}
|
|
}
|
|
|
|
/// Return (select_fn_ident, either_variant_idents) for embassy_futures::select
|
|
/// with the given total arity. `arity` must be one of 2, 3, 4, 5, 6.
|
|
fn embassy_select_for_arity(arity: usize) -> (Ident, Vec<Ident>) {
|
|
match arity {
|
|
2 => (
|
|
format_ident!("select"),
|
|
vec![format_ident!("First"), format_ident!("Second")],
|
|
),
|
|
3 => (
|
|
format_ident!("select3"),
|
|
vec![
|
|
format_ident!("First"),
|
|
format_ident!("Second"),
|
|
format_ident!("Third"),
|
|
],
|
|
),
|
|
4 => (
|
|
format_ident!("select4"),
|
|
vec![
|
|
format_ident!("First"),
|
|
format_ident!("Second"),
|
|
format_ident!("Third"),
|
|
format_ident!("Fourth"),
|
|
],
|
|
),
|
|
5 => (
|
|
format_ident!("select5"),
|
|
vec![
|
|
format_ident!("First"),
|
|
format_ident!("Second"),
|
|
format_ident!("Third"),
|
|
format_ident!("Fourth"),
|
|
format_ident!("Fifth"),
|
|
],
|
|
),
|
|
6 => (
|
|
format_ident!("select6"),
|
|
vec![
|
|
format_ident!("First"),
|
|
format_ident!("Second"),
|
|
format_ident!("Third"),
|
|
format_ident!("Fourth"),
|
|
format_ident!("Fifth"),
|
|
format_ident!("Sixth"),
|
|
],
|
|
),
|
|
_ => unreachable!("embassy_select_for_arity called with unsupported arity"),
|
|
}
|
|
}
|
|
|
|
/// Emit embassy run-loop match arms for machines with a bound channel. Arity
|
|
/// per state = active_durings + 2 (channel + timer). Embassy caps at `select6`
|
|
/// → max 4 concurrent durings on a path with events. Over-limit paths emit
|
|
/// a `compile_error!` inside the embassy-only arm so tokio codegen still
|
|
/// compiles unchanged.
|
|
fn emit_embassy_race_arms_with_channel(ir: &Ir) -> Vec<TokenStream> {
|
|
let mut arms = Vec::new();
|
|
for (sid, chain) in active_durings_per_leaf(ir) {
|
|
let n = chain.len();
|
|
if n + 2 > 6 {
|
|
let msg = format!(
|
|
"too many concurrent `during:` activities on the active path for state id {} \
|
|
under the embassy feature ({} + channel + timer > 6); embassy_futures supports \
|
|
at most 4 concurrent durings on a path with events. Combine activities into a \
|
|
single during that uses `select` internally.",
|
|
sid, n
|
|
);
|
|
arms.push(quote! {
|
|
Some(#sid) => { ::core::compile_error!(#msg); unreachable!() }
|
|
});
|
|
continue;
|
|
}
|
|
let (binds, stmts) = emit_during_bindings(&chain);
|
|
let (sel_fn, variants) = embassy_select_for_arity(n + 2);
|
|
let ch_variant = &variants[n];
|
|
let tm_variant = &variants[n + 1];
|
|
let call_args = binds.iter()
|
|
.map(|b| quote! { #b })
|
|
.chain(std::iter::once(quote! { rx.receive() }))
|
|
.chain(std::iter::once(quote! { timer_fut }));
|
|
let either_path = if n + 2 == 2 {
|
|
quote! { ::embassy_futures::select::Either }
|
|
} else {
|
|
let either_ty = format_ident!("Either{}", (n + 2).to_string());
|
|
quote! { ::embassy_futures::select::#either_ty }
|
|
};
|
|
let during_match = binds.iter().enumerate().map(|(i, _b)| {
|
|
let v = &variants[i];
|
|
quote! { #either_path::#v(ev) => __HsmcRace::Event(ev), }
|
|
});
|
|
arms.push(quote! {
|
|
Some(#sid) => {
|
|
#stmts
|
|
let timer_fut = async {
|
|
match next {
|
|
Some(d) => {
|
|
::embassy_time::Timer::after(
|
|
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
|
).await;
|
|
}
|
|
None => ::core::future::pending::<()>().await,
|
|
}
|
|
};
|
|
let rx = self.__embassy_rx.as_ref().expect("hsmc: embassy channel");
|
|
match ::embassy_futures::select::#sel_fn(#(#call_args),*).await {
|
|
#(#during_match)*
|
|
#either_path::#ch_variant(ev) => __HsmcRace::Event(ev),
|
|
#either_path::#tm_variant(()) => __HsmcRace::Timer,
|
|
}
|
|
}
|
|
});
|
|
}
|
|
arms
|
|
}
|
|
|
|
/// Emit embassy run-loop match arms for timer-only machines (no channel).
|
|
/// Arity per state = active_durings + 1 (timer only). Max 5 concurrent
|
|
/// durings on a timer-only path.
|
|
fn emit_embassy_race_arms_timer_only(ir: &Ir) -> Vec<TokenStream> {
|
|
let mut arms = Vec::new();
|
|
for (sid, chain) in active_durings_per_leaf(ir) {
|
|
let n = chain.len();
|
|
if n == 0 {
|
|
continue; // falls through to default (just timer)
|
|
}
|
|
if n + 1 > 6 {
|
|
let msg = format!(
|
|
"too many concurrent `during:` activities on the active path for state id {} \
|
|
under the embassy feature ({} + timer > 6); embassy_futures supports at most 5 \
|
|
concurrent durings on a timer-only path.",
|
|
sid, n
|
|
);
|
|
arms.push(quote! {
|
|
Some(#sid) => { ::core::compile_error!(#msg); unreachable!() }
|
|
});
|
|
continue;
|
|
}
|
|
let (binds, stmts) = emit_during_bindings(&chain);
|
|
let (sel_fn, variants) = embassy_select_for_arity(n + 1);
|
|
let call_args = binds.iter()
|
|
.map(|b| quote! { #b })
|
|
.chain(std::iter::once(quote! { timer_fut }));
|
|
let either_path = if n + 1 == 2 {
|
|
quote! { ::embassy_futures::select::Either }
|
|
} else {
|
|
let either_ty = format_ident!("Either{}", (n + 1).to_string());
|
|
quote! { ::embassy_futures::select::#either_ty }
|
|
};
|
|
let tm_variant = &variants[n];
|
|
let during_match = binds.iter().enumerate().map(|(i, _b)| {
|
|
let v = &variants[i];
|
|
quote! { #either_path::#v(ev) => __HsmcRace::Event(ev), }
|
|
});
|
|
arms.push(quote! {
|
|
Some(#sid) => {
|
|
#stmts
|
|
let timer_fut = async {
|
|
match next {
|
|
Some(d) => {
|
|
::embassy_time::Timer::after(
|
|
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
|
).await;
|
|
}
|
|
None => ::core::future::pending::<()>().await,
|
|
}
|
|
};
|
|
match ::embassy_futures::select::#sel_fn(#(#call_args),*).await {
|
|
#(#during_match)*
|
|
#either_path::#tm_variant(()) => __HsmcRace::Timer,
|
|
}
|
|
}
|
|
});
|
|
}
|
|
arms
|
|
}
|
|
|
|
pub fn generate(ir: &Ir) -> TokenStream {
|
|
let machine_name = &ir.name;
|
|
let state_enum_name = format_ident!("{}State", machine_name);
|
|
let actions_trait_name = format_ident!("{}Actions", machine_name);
|
|
let action_ctx_name = format_ident!("{}ActionContext", machine_name);
|
|
let ctx_alias_name = format_ident!("{}Ctx", machine_name);
|
|
let sender_name = format_ident!("{}Sender", machine_name);
|
|
let ctx_ty = &ir.context_ty;
|
|
let ev_ty = &ir.event_ty;
|
|
|
|
let n_states = ir.states.len();
|
|
let n_actions = ir.actions.len();
|
|
let n_dur_triggers = ir.duration_triggers.len();
|
|
let max_timers = n_dur_triggers.max(1);
|
|
|
|
// Public state enum — only user-declared states (skip __Root).
|
|
let state_variants: Vec<_> = ir
|
|
.states
|
|
.iter()
|
|
.filter(|s| s.parent.is_some())
|
|
.map(|s| s.name.clone())
|
|
.collect();
|
|
|
|
let entries_of_arms: Vec<_> = ir.states.iter().map(|s| {
|
|
let sid = s.id;
|
|
let entries_tok: Vec<_> = s.entries.iter().map(|a| quote! { #a }).collect();
|
|
quote! { #sid => &[#(#entries_tok),*], }
|
|
}).collect();
|
|
let exits_of_arms: Vec<_> = ir.states.iter().map(|s| {
|
|
let sid = s.id;
|
|
let exits_tok: Vec<_> = s.exits.iter().map(|a| quote! { #a }).collect();
|
|
quote! { #sid => &[#(#exits_tok),*], }
|
|
}).collect();
|
|
|
|
let parent_table = ir.states.iter().map(|s| match s.parent {
|
|
Some(p) => quote! { Some(#p) },
|
|
None => quote! { None },
|
|
}).collect::<Vec<_>>();
|
|
let depth_table = ir.states.iter().map(|s| {
|
|
let d = s.depth;
|
|
quote! { #d }
|
|
}).collect::<Vec<_>>();
|
|
let default_child_table = ir.states.iter().map(|s| match s.default_child {
|
|
Some(c) => quote! { Some(#c) },
|
|
None => quote! { None },
|
|
}).collect::<Vec<_>>();
|
|
let state_id_to_variant_arms = ir.states.iter().filter(|s| s.parent.is_some()).map(|s| {
|
|
let id = s.id;
|
|
let nm = &s.name;
|
|
quote! { #id => #state_enum_name::#nm, }
|
|
}).collect::<Vec<_>>();
|
|
// Trait methods, possibly with typed params for payload-bearing actions.
|
|
let action_methods_sync = ir.actions.iter().map(|a| {
|
|
let name = &a.name;
|
|
let params = a.params.iter().map(|f| {
|
|
let n = &f.name;
|
|
let t = &f.ty;
|
|
quote! { #n: #t }
|
|
});
|
|
quote! { fn #name(&mut self, #(#params),*); }
|
|
}).collect::<Vec<_>>();
|
|
let action_methods_async = ir.actions.iter().map(|a| {
|
|
let name = &a.name;
|
|
let params = a.params.iter().map(|f| {
|
|
let n = &f.name;
|
|
let t = &f.ty;
|
|
quote! { #n: #t }
|
|
});
|
|
quote! {
|
|
fn #name(&mut self, #(#params),*) -> impl ::core::future::Future<Output = ()>;
|
|
}
|
|
}).collect::<Vec<_>>();
|
|
|
|
// Dispatch arms: for unit-signature actions call straight through; for
|
|
// payload-bearing actions, re-match on the passed event and destructure.
|
|
// Payload dispatch binds each field by reference (scrutinee is `&Ev`)
|
|
// then `.clone()`s it to produce an owned value matching the handler's
|
|
// declared signature. For `Copy` types this optimizes away.
|
|
let run_action_arms_sync = ir.actions.iter().enumerate().map(|(i, a)| {
|
|
let i = i as u16;
|
|
let name = &a.name;
|
|
if a.params.is_empty() {
|
|
quote! { #i => <Ctx as #actions_trait_name>::#name(ctx), }
|
|
} else {
|
|
let field_idents: Vec<_> = a.params.iter().map(|f| f.name.clone()).collect();
|
|
let clone_args = field_idents.iter().map(|n| quote! { #n.clone() });
|
|
let variant_arms = a.bound_variants.iter().map(|bv| {
|
|
let vname = &bv.name;
|
|
let binds = &field_idents;
|
|
let call = {
|
|
let clones = clone_args.clone();
|
|
quote! { <Ctx as #actions_trait_name>::#name(ctx, #(#clones),*); }
|
|
};
|
|
match bv.kind {
|
|
PayloadKind::Tuple => quote! {
|
|
#ev_ty::#vname(#(#binds),*) => { #call }
|
|
},
|
|
PayloadKind::Struct => quote! {
|
|
#ev_ty::#vname { #(#binds),* } => { #call }
|
|
},
|
|
}
|
|
});
|
|
quote! {
|
|
#i => {
|
|
if let Some(__ev) = __event {
|
|
match __ev {
|
|
#(#variant_arms)*
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}).collect::<Vec<_>>();
|
|
let run_action_arms_async = ir.actions.iter().enumerate().map(|(i, a)| {
|
|
let i = i as u16;
|
|
let name = &a.name;
|
|
if a.params.is_empty() {
|
|
quote! { #i => <Ctx as #actions_trait_name>::#name(ctx).await, }
|
|
} else {
|
|
let field_idents: Vec<_> = a.params.iter().map(|f| f.name.clone()).collect();
|
|
let clone_args = field_idents.iter().map(|n| quote! { #n.clone() });
|
|
let variant_arms = a.bound_variants.iter().map(|bv| {
|
|
let vname = &bv.name;
|
|
let binds = &field_idents;
|
|
let call = {
|
|
let clones = clone_args.clone();
|
|
quote! { <Ctx as #actions_trait_name>::#name(ctx, #(#clones),*).await; }
|
|
};
|
|
match bv.kind {
|
|
PayloadKind::Tuple => quote! {
|
|
#ev_ty::#vname(#(#binds),*) => { #call }
|
|
},
|
|
PayloadKind::Struct => quote! {
|
|
#ev_ty::#vname { #(#binds),* } => { #call }
|
|
},
|
|
}
|
|
});
|
|
quote! {
|
|
#i => {
|
|
if let Some(__ev) = __event {
|
|
match __ev {
|
|
#(#variant_arms)*
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}).collect::<Vec<_>>();
|
|
let duration_expr_arms = ir.duration_triggers.iter().enumerate().map(|(i, d)| {
|
|
let i = i as u16;
|
|
let expr = &d.expr;
|
|
quote! { #i => { let __d: ::hsmc::Duration = #expr; __d }, }
|
|
}).collect::<Vec<_>>();
|
|
let duration_repeat_arms = ir.duration_triggers.iter().enumerate().map(|(i, d)| {
|
|
let i = i as u16;
|
|
let repeat = d.repeat;
|
|
quote! { #i => #repeat, }
|
|
}).collect::<Vec<_>>();
|
|
let event_variant_arms = ir.event_variants.iter().enumerate().map(|(i, v)| {
|
|
let i = i as u16;
|
|
let name = &v.name;
|
|
let pat = match v.kind {
|
|
VariantKind::Unit => quote! { #ev_ty::#name },
|
|
VariantKind::Tuple => quote! { #ev_ty::#name(..) },
|
|
VariantKind::Struct => quote! { #ev_ty::#name { .. } },
|
|
};
|
|
quote! { #pat => #i, }
|
|
}).collect::<Vec<_>>();
|
|
let state_handler_arms = ir.states.iter().map(|s| {
|
|
use std::collections::BTreeMap;
|
|
#[derive(Default)]
|
|
struct Group { action_ids: Vec<u16>, transition_target: Option<u16> }
|
|
let mut groups: BTreeMap<(u8, u16), Group> = BTreeMap::new();
|
|
let mut handlers = s.handlers.iter().collect::<Vec<_>>();
|
|
handlers.sort_by_key(|h| h.decl_index);
|
|
for h in handlers {
|
|
let key = match &h.trigger {
|
|
TriggerIr::Event(id, _, _) => (0u8, *id),
|
|
TriggerIr::Duration(id) => (1u8, *id),
|
|
};
|
|
let g = groups.entry(key).or_default();
|
|
match &h.kind {
|
|
HandlerKindIr::Action(aid, _) => g.action_ids.push(*aid),
|
|
HandlerKindIr::Transition(_, Some(tid)) => g.transition_target = Some(*tid),
|
|
HandlerKindIr::Transition(_, None) => {}
|
|
}
|
|
}
|
|
let sid = s.id;
|
|
let inner_arms = groups.iter().map(|((kind, tid), g)| {
|
|
let kind = *kind;
|
|
let tid = *tid;
|
|
let actions = g.action_ids.iter().map(|a| quote! { #a });
|
|
let transition_tok = match g.transition_target {
|
|
Some(t) => quote! { Some(#t) },
|
|
None => quote! { None },
|
|
};
|
|
quote! {
|
|
(#kind, #tid) => {
|
|
let actions: &'static [u16] = &[#(#actions),*];
|
|
let target: Option<u16> = #transition_tok;
|
|
return Some((actions, target));
|
|
}
|
|
}
|
|
}).collect::<Vec<_>>();
|
|
quote! {
|
|
#sid => {
|
|
match (trigger_kind, trigger_id) {
|
|
#(#inner_arms)*
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}).collect::<Vec<_>>();
|
|
let owned_timers_arms = ir.states.iter().map(|s| {
|
|
let sid = s.id;
|
|
let ids = s.owned_timers.iter().map(|t| quote! { #t });
|
|
quote! { #sid => &[#(#ids),*], }
|
|
}).collect::<Vec<_>>();
|
|
let terminate_match = if let Some(tev) = &ir.terminate_event {
|
|
// Use the kind recorded for this variant (unit/tuple/struct).
|
|
let kind = ir
|
|
.event_variants
|
|
.iter()
|
|
.find(|v| v.name == *tev)
|
|
.map(|v| v.kind)
|
|
.unwrap_or(VariantKind::Unit);
|
|
let pat = match kind {
|
|
VariantKind::Unit => quote! { #ev_ty::#tev },
|
|
VariantKind::Tuple => quote! { #ev_ty::#tev(..) },
|
|
VariantKind::Struct => quote! { #ev_ty::#tev { .. } },
|
|
};
|
|
quote! {
|
|
if matches!(__ev, #pat) { return TerminateCheck::Yes; }
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
let _ = n_actions;
|
|
|
|
// Doc strings as literals for quoting.
|
|
let state_enum_doc = format!(
|
|
"Enum of every state declared in the `{}` statechart. Each variant names a user-declared state; the root is implicit and never surfaced.",
|
|
machine_name
|
|
);
|
|
let actions_trait_doc = format!(
|
|
"Trait implemented by the user to provide action function bodies for the `{}` statechart. One method per unique action name.",
|
|
machine_name
|
|
);
|
|
let action_ctx_doc = format!(
|
|
"Wrapper passed to action methods of the `{}` statechart. `Deref`s to the user's context and exposes [`emit()`](#method.emit) for re-entrant event emission (§4.3).",
|
|
machine_name
|
|
);
|
|
let ctx_alias_doc = format!(
|
|
"Convenience type alias for [`{}ActionContext`] (spec §12.4).",
|
|
machine_name
|
|
);
|
|
let machine_doc = format!(
|
|
"The `{}` state machine. Drive it with [`step()`](Self::step) or, under an async feature, `run().await`. See the crate docs for the generated API (§4.4).",
|
|
machine_name
|
|
);
|
|
let sender_doc = format!(
|
|
"Handle for sending events into a running `{}` from other threads, tasks, or ISRs (§4.5).",
|
|
machine_name
|
|
);
|
|
|
|
// Optional-events branch: decide whether to emit Sender / sender() / channel
|
|
// plumbing at all. When events: was omitted the machine is timer-only and
|
|
// has no Sender type.
|
|
let emit_sender = !ir.events_omitted;
|
|
|
|
// Synthesize the empty event type when events: was omitted.
|
|
let no_events_enum = if ir.events_omitted {
|
|
quote! {
|
|
#[doc(hidden)]
|
|
#[allow(dead_code)]
|
|
pub enum __NoEvents {}
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
// Race-loop match arms for each run() variant. When the active state has
|
|
// `during:` activities on its path, the arm constructs those futures by
|
|
// split-borrowing context fields and races them against the channel and
|
|
// next-timer sleep via `tokio::select!` / `embassy_futures::select`.
|
|
let tokio_race_arms_with_ch = emit_tokio_race_arms_with_channel(ir);
|
|
let tokio_race_arms_timer_only = emit_tokio_race_arms_timer_only(ir);
|
|
let embassy_race_arms_with_ch = emit_embassy_race_arms_with_channel(ir);
|
|
let embassy_race_arms_timer_only = emit_embassy_race_arms_timer_only(ir);
|
|
|
|
// Whether any state in the machine declares at least one `during:`. When
|
|
// false, run() uses the original v0.1 pre-during code path verbatim,
|
|
// which keeps backward compatibility bit-for-bit for machines that
|
|
// never opted into the new feature.
|
|
let has_any_durings = ir.states.iter().any(|s| !s.durings.is_empty());
|
|
|
|
// The tokio with-channel run() body. v0.1 verbatim when no durings are
|
|
// declared; otherwise a per-state match dispatching to `select!` with
|
|
// the appropriate active during set.
|
|
let tokio_run_with_ch_body = if has_any_durings {
|
|
quote! {
|
|
#[allow(dead_code)]
|
|
enum __HsmcRace<E> { Event(E), ChannelClosed, Timer }
|
|
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
|
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
let mut last_instant = ::tokio::time::Instant::now();
|
|
while !self.terminated {
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
if !self.queue.is_empty() {
|
|
let now = ::tokio::time::Instant::now();
|
|
let elapsed = now.duration_since(last_instant);
|
|
last_instant = now;
|
|
next = self.step(elapsed).await;
|
|
continue;
|
|
}
|
|
let sleep_dur = next.unwrap_or(::hsmc::Duration::from_secs(3600));
|
|
let race: __HsmcRace<#ev_ty> = match self.current {
|
|
#(#tokio_race_arms_with_ch)*
|
|
_ => {
|
|
let sleep = ::tokio::time::sleep(sleep_dur);
|
|
::tokio::pin!(sleep);
|
|
let rx = self.__tokio_rx.as_mut().expect("hsmc: tokio channel");
|
|
::tokio::select! {
|
|
biased;
|
|
maybe = rx.recv() => match maybe {
|
|
Some(ev) => __HsmcRace::Event(ev),
|
|
None => __HsmcRace::ChannelClosed,
|
|
},
|
|
_ = &mut sleep => __HsmcRace::Timer,
|
|
}
|
|
}
|
|
};
|
|
match race {
|
|
__HsmcRace::Event(ev) => { let _ = self.queue.push_back(ev); }
|
|
__HsmcRace::ChannelClosed => { if next.is_none() { break; } }
|
|
__HsmcRace::Timer => {}
|
|
}
|
|
let now = ::tokio::time::Instant::now();
|
|
let elapsed = now.duration_since(last_instant);
|
|
last_instant = now;
|
|
next = self.step(elapsed).await;
|
|
}
|
|
Ok(())
|
|
}
|
|
} else {
|
|
quote! {
|
|
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
|
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
let mut last_instant = ::tokio::time::Instant::now();
|
|
while !self.terminated {
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
if !self.queue.is_empty() {
|
|
let now = ::tokio::time::Instant::now();
|
|
let elapsed = now.duration_since(last_instant);
|
|
last_instant = now;
|
|
next = self.step(elapsed).await;
|
|
continue;
|
|
}
|
|
let sleep_dur = next.unwrap_or(::hsmc::Duration::from_secs(3600));
|
|
let sleep = ::tokio::time::sleep(sleep_dur);
|
|
::tokio::pin!(sleep);
|
|
let rx = self.__tokio_rx.as_mut().expect("tokio channel");
|
|
::tokio::select! {
|
|
maybe_ev = rx.recv() => {
|
|
match maybe_ev {
|
|
Some(ev) => { let _ = self.queue.push_back(ev); }
|
|
None => {
|
|
if next.is_none() { break; }
|
|
}
|
|
}
|
|
let now = ::tokio::time::Instant::now();
|
|
let elapsed = now.duration_since(last_instant);
|
|
last_instant = now;
|
|
next = self.step(elapsed).await;
|
|
}
|
|
_ = &mut sleep => {
|
|
let now = ::tokio::time::Instant::now();
|
|
let elapsed = now.duration_since(last_instant);
|
|
last_instant = now;
|
|
next = self.step(elapsed).await;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
};
|
|
|
|
// The embassy with-channel run() body. v0.1 verbatim when no durings
|
|
// are declared; otherwise a per-state match dispatching to
|
|
// embassy_futures::select_N with the appropriate active during set.
|
|
let embassy_run_with_ch_body = if has_any_durings {
|
|
quote! {
|
|
#[allow(dead_code)]
|
|
enum __HsmcRace<E> { Event(E), ChannelClosed, Timer }
|
|
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
|
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
let mut last_instant = ::embassy_time::Instant::now();
|
|
while !self.terminated {
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
if !self.queue.is_empty() {
|
|
let now = ::embassy_time::Instant::now();
|
|
let elapsed_us = now.duration_since(last_instant).as_micros();
|
|
last_instant = now;
|
|
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
|
continue;
|
|
}
|
|
let race: __HsmcRace<#ev_ty> = match self.current {
|
|
#(#embassy_race_arms_with_ch)*
|
|
_ => {
|
|
let timer_fut = async {
|
|
match next {
|
|
Some(d) => {
|
|
::embassy_time::Timer::after(
|
|
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
|
).await;
|
|
}
|
|
None => ::core::future::pending::<()>().await,
|
|
}
|
|
};
|
|
let rx = self.__embassy_rx.as_ref().expect("hsmc: embassy channel");
|
|
match ::embassy_futures::select::select(rx.receive(), timer_fut).await {
|
|
::embassy_futures::select::Either::First(ev) => __HsmcRace::Event(ev),
|
|
::embassy_futures::select::Either::Second(()) => __HsmcRace::Timer,
|
|
}
|
|
}
|
|
};
|
|
match race {
|
|
__HsmcRace::Event(ev) => { let _ = self.queue.push_back(ev); }
|
|
__HsmcRace::ChannelClosed => {}
|
|
__HsmcRace::Timer => {}
|
|
}
|
|
let now = ::embassy_time::Instant::now();
|
|
let elapsed_us = now.duration_since(last_instant).as_micros();
|
|
last_instant = now;
|
|
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
|
}
|
|
Ok(())
|
|
}
|
|
} else {
|
|
quote! {
|
|
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
|
let rx = self.__embassy_rx
|
|
.expect("hsmc: embassy channel (populated by Machine::new)");
|
|
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
let mut last_instant = ::embassy_time::Instant::now();
|
|
while !self.terminated {
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
if !self.queue.is_empty() {
|
|
let now = ::embassy_time::Instant::now();
|
|
let elapsed_us = now.duration_since(last_instant).as_micros();
|
|
last_instant = now;
|
|
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
|
continue;
|
|
}
|
|
let timer_fut = async {
|
|
match next {
|
|
Some(d) => {
|
|
::embassy_time::Timer::after(
|
|
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
|
).await;
|
|
}
|
|
None => ::core::future::pending::<()>().await,
|
|
}
|
|
};
|
|
match ::embassy_futures::select::select(rx.receive(), timer_fut).await {
|
|
::embassy_futures::select::Either::First(ev) => {
|
|
let _ = self.queue.push_back(ev);
|
|
}
|
|
::embassy_futures::select::Either::Second(()) => {}
|
|
}
|
|
let now = ::embassy_time::Instant::now();
|
|
let elapsed_us = now.duration_since(last_instant).as_micros();
|
|
last_instant = now;
|
|
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
|
}
|
|
Ok(())
|
|
}
|
|
};
|
|
|
|
// State ids whose active `during:` chain is non-empty. Used by timer-only
|
|
// run() bodies to decide whether to exit when no timers are pending: if
|
|
// the current state has an active during, the run loop should keep
|
|
// awaiting rather than break (the during might yet produce an event).
|
|
let states_with_active_durings: Vec<u16> = active_durings_per_leaf(ir)
|
|
.into_iter()
|
|
.map(|(sid, _)| sid)
|
|
.collect();
|
|
let has_active_during_arms: Vec<TokenStream> = states_with_active_durings
|
|
.iter()
|
|
.map(|sid| quote! { Some(#sid) => true, })
|
|
.collect();
|
|
|
|
// Human-readable ASCII diagram of the machine's hierarchy. Emitted as a
|
|
// `pub const STATE_CHART: &str` on the machine struct. Useful for
|
|
// `defmt::info!`, panic messages, and documentation.
|
|
let state_chart_str = emit_state_chart_string(ir);
|
|
|
|
// Sender / sender() under tokio. With eager channel creation in
|
|
// `with_queue_capacity_internal()`, `sender()` is `&self`.
|
|
let tokio_sender_impl = if emit_sender {
|
|
quote! {
|
|
#[cfg(feature = "tokio")]
|
|
#[doc = #sender_doc]
|
|
#[derive(Clone)]
|
|
pub struct #sender_name {
|
|
tx: ::tokio::sync::mpsc::UnboundedSender<#ev_ty>,
|
|
}
|
|
#[cfg(feature = "tokio")]
|
|
impl #sender_name {
|
|
/// Send an event to the machine. Returns `Err(AlreadyTerminated)` if
|
|
/// the machine has terminated and the receiver has been dropped.
|
|
pub fn send(&self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
self.tx.send(event).map_err(|_| ::hsmc::HsmcError::AlreadyTerminated)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "tokio")]
|
|
impl<const __QN: usize> #machine_name<__QN> {
|
|
/// Obtain a clonable, `Send` handle for pushing events into this
|
|
/// machine. The channel is eagerly created in `new()` (§4.4).
|
|
pub fn sender(&self) -> #sender_name {
|
|
#sender_name { tx: self.__tokio_tx.as_ref().expect("hsmc: tokio channel").clone() }
|
|
}
|
|
|
|
/// Run the machine to completion on the current Tokio runtime.
|
|
pub async fn run(&mut self) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
#tokio_run_with_ch_body
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Timer-only: no Sender, no tokio channel, but keep a timer-only run().
|
|
quote! {
|
|
#[cfg(feature = "tokio")]
|
|
impl<const __QN: usize> #machine_name<__QN> {
|
|
/// Run the machine to completion, honouring timers and
|
|
/// `during:` activities. No event channel is generated
|
|
/// because this statechart declares no `events:` (§12.2).
|
|
pub async fn run(&mut self) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
#[allow(dead_code)]
|
|
enum __HsmcRace<E> { Event(E), ChannelClosed, Timer }
|
|
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
|
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
let mut last_instant = ::tokio::time::Instant::now();
|
|
while !self.terminated {
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
if !self.queue.is_empty() {
|
|
let now = ::tokio::time::Instant::now();
|
|
let elapsed = now.duration_since(last_instant);
|
|
last_instant = now;
|
|
next = self.step(elapsed).await;
|
|
continue;
|
|
}
|
|
let sleep_dur = match next {
|
|
Some(d) => d,
|
|
None => {
|
|
// No timers pending. If the current state
|
|
// has no `during:` activities either, there
|
|
// is nothing left to wait on — exit.
|
|
let has_any_during: bool = match self.current {
|
|
#(#has_active_during_arms)*
|
|
_ => false,
|
|
};
|
|
if !has_any_during { break; }
|
|
::hsmc::Duration::from_secs(3600)
|
|
}
|
|
};
|
|
let race: __HsmcRace<#ev_ty> = match self.current {
|
|
#(#tokio_race_arms_timer_only)*
|
|
_ => {
|
|
::tokio::time::sleep(sleep_dur).await;
|
|
__HsmcRace::Timer
|
|
}
|
|
};
|
|
match race {
|
|
__HsmcRace::Event(ev) => { let _ = self.queue.push_back(ev); }
|
|
__HsmcRace::ChannelClosed => {}
|
|
__HsmcRace::Timer => {}
|
|
}
|
|
let now = ::tokio::time::Instant::now();
|
|
let elapsed = now.duration_since(last_instant);
|
|
last_instant = now;
|
|
next = self.step(elapsed).await;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Default (no feature): stub sender that documents the limitation. We
|
|
// intentionally keep it non-functional — a proper `Send + Clone` handle
|
|
// without `alloc` or a platform primitive is out of scope for v0.1. See
|
|
// the `tokio` or `embassy` features for a working `sender()`.
|
|
let default_sender_impl = if emit_sender {
|
|
quote! {
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
#[doc = #sender_doc]
|
|
///
|
|
/// **Limitation:** without the `tokio` or `embassy` feature, the
|
|
/// default sender is a non-functional stub and
|
|
/// [`send()`](Self::send) always returns
|
|
/// [`HsmcError::AlreadyTerminated`]. Use the `tokio` or `embassy`
|
|
/// feature to get a working cross-thread sender.
|
|
#[derive(Clone)]
|
|
pub struct #sender_name { _priv: () }
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
impl #sender_name {
|
|
/// Always returns [`HsmcError::AlreadyTerminated`] under
|
|
/// default features — enable `tokio` or `embassy` for a
|
|
/// functional implementation.
|
|
pub fn send(&self, _event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
Err(::hsmc::HsmcError::AlreadyTerminated)
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
impl<const __QN: usize> #machine_name<__QN> {
|
|
/// Non-functional stub under default features — see
|
|
/// [`#sender_name`] for the limitation.
|
|
pub fn sender(&self) -> #sender_name { #sender_name { _priv: () } }
|
|
}
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
// Embassy: first-class support. `Machine::new(ctx, &'static CHAN)` stores
|
|
// type-erased send/receive endpoints so the Machine struct stays free of
|
|
// a `M: RawMutex` generic. `sender()` hands out a `Copy + Send + Sync`
|
|
// newtype usable from ISRs (`try_send`) and tasks (`send().await`).
|
|
let send_assert_name = format_ident!("__hsmc_embassy_ev_send_assert_{}", machine_name);
|
|
let embassy_sender_impl = if emit_sender {
|
|
quote! {
|
|
// Compile-time assertion: the event type must be `Send` under the
|
|
// embassy feature (its sender handle is `Send + Sync`, which
|
|
// requires `E: Send`). Fails with a pointer to `#ev_ty`.
|
|
#[cfg(feature = "embassy")]
|
|
#[allow(dead_code, non_upper_case_globals)]
|
|
const #send_assert_name: fn() = || {
|
|
fn __assert_send<T: ::core::marker::Send>() {}
|
|
__assert_send::<#ev_ty>();
|
|
};
|
|
|
|
#[cfg(feature = "embassy")]
|
|
#[doc = #sender_doc]
|
|
///
|
|
/// `Copy + Send + Sync`-able handle around a
|
|
/// [`embassy_sync::channel::SendDynamicSender`]. Safe to capture in
|
|
/// a `static` for ISR use and to clone across Embassy tasks.
|
|
#[derive(Clone, Copy)]
|
|
pub struct #sender_name {
|
|
inner: ::embassy_sync::channel::SendDynamicSender<'static, #ev_ty>,
|
|
}
|
|
#[cfg(feature = "embassy")]
|
|
impl #sender_name {
|
|
/// Non-blocking send. Returns
|
|
/// [`HsmcError::QueueFull`] if the channel has no room. Safe
|
|
/// from ISR context when the channel is backed by
|
|
/// `CriticalSectionRawMutex`.
|
|
#[track_caller]
|
|
pub fn try_send(self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
self.inner.try_send(event).map_err(|_| ::hsmc::HsmcError::QueueFull)
|
|
}
|
|
/// Async send. Backpressures (awaits) when the channel is
|
|
/// full. Use from Embassy tasks that must not drop events.
|
|
pub async fn send(self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
self.inner.send(event).await;
|
|
Ok(())
|
|
}
|
|
/// Build a sender directly from a user-declared static
|
|
/// channel — convenient for ISR shims that run before the
|
|
/// machine task has spawned. The channel's event type and
|
|
/// capacity must match the machine.
|
|
pub fn from_channel<__M, const __N: usize>(
|
|
channel: &'static ::embassy_sync::channel::Channel<__M, #ev_ty, __N>,
|
|
) -> Self
|
|
where
|
|
__M: ::embassy_sync::blocking_mutex::raw::RawMutex
|
|
+ ::core::marker::Sync
|
|
+ ::core::marker::Send
|
|
+ 'static,
|
|
#ev_ty: ::core::marker::Send,
|
|
{
|
|
Self { inner: channel.sender().into() }
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "embassy")]
|
|
impl #machine_name<8> {
|
|
/// Construct a new machine with the default event-queue
|
|
/// capacity of 8, bound to a user-declared static
|
|
/// `embassy_sync::channel::Channel`. The channel's capacity
|
|
/// must match the machine's `__QN` — here, 8.
|
|
pub fn new<__M>(
|
|
ctx: #ctx_ty,
|
|
channel: &'static ::embassy_sync::channel::Channel<__M, #ev_ty, 8>,
|
|
) -> Self
|
|
where
|
|
__M: ::embassy_sync::blocking_mutex::raw::RawMutex
|
|
+ ::core::marker::Sync
|
|
+ ::core::marker::Send
|
|
+ 'static,
|
|
#ev_ty: ::core::marker::Send,
|
|
{
|
|
let mut __self = <#machine_name<8>>::with_queue_capacity_internal(ctx);
|
|
__self.__embassy_rx = Some(channel.receiver().into());
|
|
__self.__embassy_tx = Some(channel.sender().into());
|
|
__self
|
|
}
|
|
/// Construct a machine with a custom event-queue capacity
|
|
/// `__N`. The channel's capacity must also be `__N`.
|
|
pub fn with_queue_capacity<__M, const __N: usize>(
|
|
ctx: #ctx_ty,
|
|
channel: &'static ::embassy_sync::channel::Channel<__M, #ev_ty, __N>,
|
|
) -> #machine_name<__N>
|
|
where
|
|
__M: ::embassy_sync::blocking_mutex::raw::RawMutex
|
|
+ ::core::marker::Sync
|
|
+ ::core::marker::Send
|
|
+ 'static,
|
|
#ev_ty: ::core::marker::Send,
|
|
{
|
|
let mut __self = <#machine_name<__N>>::with_queue_capacity_internal(ctx);
|
|
__self.__embassy_rx = Some(channel.receiver().into());
|
|
__self.__embassy_tx = Some(channel.sender().into());
|
|
__self
|
|
}
|
|
/// Construct a machine that is driven only via `send()` +
|
|
/// `step().await`. No external `Channel` is wired up, so
|
|
/// [`sender()`](Self::sender) and [`run()`](Self::run)
|
|
/// panic if called. Use this when the single task that
|
|
/// owns the machine is also its only event producer —
|
|
/// the internal queue is sufficient and a
|
|
/// `&'static Channel` would be dead weight.
|
|
pub fn new_local(ctx: #ctx_ty) -> Self {
|
|
<#machine_name<8>>::with_queue_capacity_internal(ctx)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "embassy")]
|
|
impl<const __QN: usize> #machine_name<__QN> {
|
|
/// Channel-less variant of
|
|
/// [`with_queue_capacity`](Self::with_queue_capacity)
|
|
/// — see [`new_local`](Self::new_local).
|
|
pub fn with_queue_capacity_local(ctx: #ctx_ty) -> #machine_name<__QN> {
|
|
<#machine_name<__QN>>::with_queue_capacity_internal(ctx)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "embassy")]
|
|
impl<const __QN: usize> #machine_name<__QN> {
|
|
/// Obtain a clonable, `Send` handle for pushing events into
|
|
/// this machine from ISRs or other Embassy tasks. The channel
|
|
/// was provided at construction.
|
|
pub fn sender(&self) -> #sender_name {
|
|
#sender_name {
|
|
inner: self.__embassy_tx
|
|
.expect("hsmc: embassy channel (populated by Machine::new)"),
|
|
}
|
|
}
|
|
|
|
/// Run the machine to completion, awaiting events from the
|
|
/// bound channel, firing timers via `embassy_time`, and
|
|
/// polling any active `during:` activities for the current
|
|
/// state path. Returns `Ok(())` on clean termination;
|
|
/// `Err(HsmcError::QueueFull)` if an action's `emit()`
|
|
/// overflowed the internal queue.
|
|
pub async fn run(&mut self) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
#embassy_run_with_ch_body
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Timer-only machines (no `events:` declared) have no channel. Keep
|
|
// the existing timer-driven run().
|
|
quote! {
|
|
#[cfg(feature = "embassy")]
|
|
impl #machine_name<8> {
|
|
/// Construct a timer-only machine (no `events:` declared).
|
|
pub fn new(ctx: #ctx_ty) -> Self {
|
|
<#machine_name<8>>::with_queue_capacity_internal(ctx)
|
|
}
|
|
/// Construct a timer-only machine with custom queue capacity.
|
|
pub fn with_queue_capacity<const __N: usize>(ctx: #ctx_ty) -> #machine_name<__N> {
|
|
<#machine_name<__N>>::with_queue_capacity_internal(ctx)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "embassy")]
|
|
impl<const __QN: usize> #machine_name<__QN> {
|
|
/// Timer-only run loop (no `events:` declared) using
|
|
/// `embassy_time::Timer`, plus any `during:` activities
|
|
/// declared on the active state path.
|
|
pub async fn run(&mut self) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
#[allow(dead_code)]
|
|
enum __HsmcRace<E> { Event(E), ChannelClosed, Timer }
|
|
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
|
let mut next = self.step(::hsmc::Duration::ZERO).await;
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
let mut last_instant = ::embassy_time::Instant::now();
|
|
while !self.terminated {
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
if !self.queue.is_empty() {
|
|
let now = ::embassy_time::Instant::now();
|
|
let elapsed_us = now.duration_since(last_instant).as_micros();
|
|
last_instant = now;
|
|
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
|
continue;
|
|
}
|
|
// No queued events. Decide whether to wait.
|
|
let has_any_during: bool = match self.current {
|
|
#(#has_active_during_arms)*
|
|
_ => false,
|
|
};
|
|
if next.is_none() && !has_any_during { break; }
|
|
let race: __HsmcRace<#ev_ty> = match self.current {
|
|
#(#embassy_race_arms_timer_only)*
|
|
_ => {
|
|
// No durings active; just await the timer.
|
|
match next {
|
|
Some(d) => {
|
|
::embassy_time::Timer::after(
|
|
::embassy_time::Duration::from_micros(d.as_micros() as u64)
|
|
).await;
|
|
}
|
|
None => ::core::future::pending::<()>().await,
|
|
}
|
|
__HsmcRace::Timer
|
|
}
|
|
};
|
|
match race {
|
|
__HsmcRace::Event(ev) => { let _ = self.queue.push_back(ev); }
|
|
__HsmcRace::ChannelClosed => {}
|
|
__HsmcRace::Timer => {}
|
|
}
|
|
let now = ::embassy_time::Instant::now();
|
|
let elapsed_us = now.duration_since(last_instant).as_micros();
|
|
last_instant = now;
|
|
next = self.step(::hsmc::Duration::from_micros(elapsed_us)).await;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Embassy stored receiver/sender fields. Populated at construction time
|
|
// when the user provides an `&'static embassy_sync::channel::Channel`.
|
|
let embassy_fields = if emit_sender {
|
|
quote! {
|
|
#[cfg(feature = "embassy")]
|
|
__embassy_rx: Option<::embassy_sync::channel::SendDynamicReceiver<'static, #ev_ty>>,
|
|
#[cfg(feature = "embassy")]
|
|
__embassy_tx: Option<::embassy_sync::channel::SendDynamicSender<'static, #ev_ty>>,
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
let embassy_ctor = if emit_sender {
|
|
quote! {
|
|
#[cfg(feature = "embassy")]
|
|
__embassy_rx: None,
|
|
#[cfg(feature = "embassy")]
|
|
__embassy_tx: None,
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
// Eager tokio channel fields. Always emitted when `tokio` feature is
|
|
// compiled in AND this machine has events.
|
|
let tokio_fields = if emit_sender {
|
|
quote! {
|
|
#[cfg(feature = "tokio")]
|
|
__tokio_tx: Option<::tokio::sync::mpsc::UnboundedSender<#ev_ty>>,
|
|
#[cfg(feature = "tokio")]
|
|
__tokio_rx: Option<::tokio::sync::mpsc::UnboundedReceiver<#ev_ty>>,
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
let tokio_ctor = if emit_sender {
|
|
quote! {
|
|
#[cfg(feature = "tokio")]
|
|
__tokio_tx: { let (tx, _rx) = ::tokio::sync::mpsc::unbounded_channel(); Some(tx) },
|
|
#[cfg(feature = "tokio")]
|
|
__tokio_rx: None,
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
// We need to create tx+rx together; do it via a small helper.
|
|
let tokio_ctor_setup = if emit_sender {
|
|
quote! {
|
|
#[cfg(feature = "tokio")]
|
|
{
|
|
let (tx, rx) = ::tokio::sync::mpsc::unbounded_channel();
|
|
__self.__tokio_tx = Some(tx);
|
|
__self.__tokio_rx = Some(rx);
|
|
}
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
quote! {
|
|
#no_events_enum
|
|
|
|
#[doc = #state_enum_doc]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[allow(non_camel_case_types, dead_code)]
|
|
pub enum #state_enum_name {
|
|
#(#state_variants),*
|
|
}
|
|
|
|
#[doc = #actions_trait_doc]
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
#[allow(non_snake_case)]
|
|
pub trait #actions_trait_name {
|
|
#(#action_methods_sync)*
|
|
}
|
|
|
|
#[doc = #actions_trait_doc]
|
|
///
|
|
/// Under the `tokio` or `embassy` feature, action methods are
|
|
/// `async fn` so that entry/exit/event-action bodies can `.await`
|
|
/// peripheral calls (I2C flush, radio ops, etc.). Returned futures
|
|
/// must be `Send` for compatibility with multi-threaded / Send
|
|
/// executors.
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
#[allow(non_snake_case)]
|
|
pub trait #actions_trait_name {
|
|
#(#action_methods_async)*
|
|
}
|
|
|
|
#[doc = #action_ctx_doc]
|
|
pub struct #action_ctx_name<'a> {
|
|
ctx: &'a mut #ctx_ty,
|
|
__queue: &'a mut (dyn ::hsmc::__private::QueuePush<#ev_ty> + ::core::marker::Send),
|
|
__terminated: &'a bool,
|
|
}
|
|
|
|
#[doc = #ctx_alias_doc]
|
|
pub type #ctx_alias_name<'a> = #action_ctx_name<'a>;
|
|
|
|
impl<'a> ::core::ops::Deref for #action_ctx_name<'a> {
|
|
type Target = #ctx_ty;
|
|
fn deref(&self) -> &#ctx_ty { self.ctx }
|
|
}
|
|
|
|
impl<'a> ::core::ops::DerefMut for #action_ctx_name<'a> {
|
|
fn deref_mut(&mut self) -> &mut #ctx_ty { self.ctx }
|
|
}
|
|
|
|
impl<'a> #action_ctx_name<'a> {
|
|
/// Push an event into the internal queue. It will be processed
|
|
/// after the current event's handling completes (§2.12). Returns
|
|
/// [`HsmcError::QueueFull`] if the queue is full, or
|
|
/// [`HsmcError::AlreadyTerminated`] if the machine is shutting
|
|
/// down.
|
|
pub fn emit(&mut self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
if *self.__terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
|
self.__queue.push(event)
|
|
}
|
|
|
|
/// Convenience wrapper around [`emit()`](Self::emit) that panics on
|
|
/// failure (§12.1). Use in contexts where a full queue is a bug
|
|
/// rather than a recoverable condition.
|
|
pub fn emit_or_panic(&mut self, event: #ev_ty) {
|
|
self.emit(event).expect("hsmc: event queue full");
|
|
}
|
|
}
|
|
|
|
#[doc = #machine_doc]
|
|
#[allow(non_snake_case, non_camel_case_types)]
|
|
pub struct #machine_name<const __QN: usize = 8> {
|
|
ctx: #ctx_ty,
|
|
current: Option<u16>,
|
|
queue: ::hsmc::__private::heapless::Deque<#ev_ty, __QN>,
|
|
timers: ::hsmc::__private::TimerTable<#max_timers>,
|
|
terminated: bool,
|
|
__overflow: bool,
|
|
#tokio_fields
|
|
#embassy_fields
|
|
}
|
|
|
|
const __PARENT: [Option<u16>; #n_states] = [#(#parent_table),*];
|
|
const __DEPTH: [u8; #n_states] = [#(#depth_table),*];
|
|
const __DEFAULT_CHILD: [Option<u16>; #n_states] = [#(#default_child_table),*];
|
|
|
|
#[allow(dead_code, unreachable_patterns)]
|
|
fn __state_id_to_public(id: u16) -> #state_enum_name {
|
|
match id {
|
|
#(#state_id_to_variant_arms)*
|
|
_ => panic!("internal: state id {} has no public variant", id),
|
|
}
|
|
}
|
|
|
|
#[allow(unreachable_code, unused_variables)]
|
|
fn __duration_for(tid: u16) -> ::hsmc::Duration {
|
|
match tid {
|
|
#(#duration_expr_arms)*
|
|
_ => ::hsmc::Duration::ZERO,
|
|
}
|
|
}
|
|
|
|
#[allow(unreachable_code, unused_variables)]
|
|
fn __duration_repeats(tid: u16) -> bool {
|
|
match tid {
|
|
#(#duration_repeat_arms)*
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
#[allow(unreachable_patterns, unused_variables)]
|
|
fn __event_to_id(ev: &#ev_ty) -> u16 {
|
|
match ev {
|
|
#(#event_variant_arms)*
|
|
_ => u16::MAX,
|
|
}
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
fn __handler_lookup(
|
|
state_id: u16,
|
|
trigger_kind: u8,
|
|
trigger_id: u16,
|
|
) -> Option<(&'static [u16], Option<u16>)> {
|
|
match state_id {
|
|
#(#state_handler_arms)*
|
|
_ => {}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
fn __owned_timers(state_id: u16) -> &'static [u16] {
|
|
match state_id {
|
|
#(#owned_timers_arms)*
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
enum TerminateCheck { Yes, No }
|
|
|
|
#[allow(unused_variables)]
|
|
fn __check_terminate(__ev: &#ev_ty) -> TerminateCheck {
|
|
#terminate_match
|
|
TerminateCheck::No
|
|
}
|
|
|
|
impl #machine_name<8> {
|
|
/// Construct a new machine with the default event-queue capacity of 8 (§4.4).
|
|
#[cfg(not(feature = "embassy"))]
|
|
pub fn new(ctx: #ctx_ty) -> Self {
|
|
<#machine_name<8>>::with_queue_capacity_internal(ctx)
|
|
}
|
|
/// Construct a machine with a custom event-queue capacity `__N` (§5.2).
|
|
#[cfg(not(feature = "embassy"))]
|
|
pub fn with_queue_capacity<const __N: usize>(ctx: #ctx_ty) -> #machine_name<__N> {
|
|
<#machine_name<__N>>::with_queue_capacity_internal(ctx)
|
|
}
|
|
}
|
|
|
|
impl<const __QN: usize> #machine_name<__QN> {
|
|
/// ASCII diagram of the state hierarchy, default children, entry/
|
|
/// exit actions, `during:` activities, and handler triggers.
|
|
/// Intended for `defmt::info!`, panic messages, and docs.
|
|
///
|
|
/// Accessible as `Self::STATE_CHART` from any impl block on the
|
|
/// machine; externally requires the turbofish (for example
|
|
/// `Machine::<8>::STATE_CHART`) since the machine carries a const
|
|
/// generic queue capacity.
|
|
pub const STATE_CHART: &'static str = #state_chart_str;
|
|
|
|
fn with_queue_capacity_internal(ctx: #ctx_ty) -> Self {
|
|
#[allow(unused_mut)]
|
|
let mut __self = Self {
|
|
ctx,
|
|
current: None,
|
|
queue: ::hsmc::__private::heapless::Deque::new(),
|
|
timers: ::hsmc::__private::TimerTable::new(),
|
|
terminated: false,
|
|
__overflow: false,
|
|
#tokio_ctor
|
|
#embassy_ctor
|
|
};
|
|
#tokio_ctor_setup
|
|
__self
|
|
}
|
|
|
|
/// Returns and clears the internal-emit overflow flag. Set by
|
|
/// the generated action dispatch when `emit()` inside an action
|
|
/// fails to enqueue — `run()` consults this to surface
|
|
/// [`HsmcError::QueueFull`] out of the async loop rather than
|
|
/// silently dropping the event. Exposed for callers driving the
|
|
/// machine via `step()` directly.
|
|
pub fn take_overflow(&mut self) -> bool {
|
|
let v = self.__overflow;
|
|
self.__overflow = false;
|
|
v
|
|
}
|
|
|
|
/// Returns `true` once the machine has processed its `terminate` event (§4.4).
|
|
pub fn is_terminated(&self) -> bool { self.terminated }
|
|
|
|
/// Return the current leaf state. Panics if called before the
|
|
/// first `step()` (§4.4).
|
|
pub fn current_state(&self) -> #state_enum_name {
|
|
__state_id_to_public(self.current.expect("machine not started; call step() first"))
|
|
}
|
|
|
|
/// Returns `true` when internal event queue has pending events
|
|
/// (§12.3). Useful for schedulers that want to poll `step()`
|
|
/// again eagerly.
|
|
pub fn has_pending_events(&self) -> bool {
|
|
!self.queue.is_empty()
|
|
}
|
|
|
|
/// Consume the machine and yield the user-provided context (§4.4).
|
|
pub fn into_context(self) -> #ctx_ty { self.ctx }
|
|
|
|
/// Borrow the user-provided context (extension beyond spec §4.4; provided for test convenience).
|
|
pub fn context(&self) -> &#ctx_ty { &self.ctx }
|
|
/// Mutably borrow the user-provided context (extension beyond spec §4.4; provided for test convenience).
|
|
pub fn context_mut(&mut self) -> &mut #ctx_ty { &mut self.ctx }
|
|
|
|
/// Advance the machine by `elapsed` wall-clock time. Returns the
|
|
/// duration until the next timer fires, or `None` if the machine
|
|
/// has no live timers (§4.4).
|
|
///
|
|
/// Under the `tokio` or `embassy` feature, `step()` is `async`
|
|
/// because action methods are `async fn` and must be `.await`ed
|
|
/// during dispatch. Under default features it stays sync.
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
pub fn step(&mut self, elapsed: ::hsmc::Duration) -> Option<::hsmc::Duration> {
|
|
if self.terminated { return None; }
|
|
|
|
if self.current.is_none() {
|
|
self.enter_path(0);
|
|
return self.timers.min_remaining();
|
|
}
|
|
|
|
self.timers.decrement(elapsed);
|
|
|
|
if let Some((state, trig)) = self.timers.pop_expired(&__DEPTH) {
|
|
// Re-arm repeating timers before dispatch. If the
|
|
// handler transitions out of `state`, the state's exit
|
|
// path will cancel the freshly-armed timer.
|
|
if __duration_repeats(trig) {
|
|
let d = __duration_for(trig);
|
|
self.timers.start(state, trig, d);
|
|
}
|
|
self.dispatch_trigger(state, 1, trig);
|
|
return self.timers.min_remaining();
|
|
}
|
|
|
|
if let Some(ev) = self.queue.pop_front() {
|
|
if matches!(__check_terminate(&ev), TerminateCheck::Yes) {
|
|
self.do_terminate();
|
|
return None;
|
|
}
|
|
let eid = __event_to_id(&ev);
|
|
if eid != u16::MAX {
|
|
let mut cur = self.current;
|
|
while let Some(s) = cur {
|
|
if let Some((actions, target)) = __handler_lookup(s, 0, eid) {
|
|
self.run_handlers(s, actions, target, Some(&ev));
|
|
break;
|
|
}
|
|
cur = __PARENT[s as usize];
|
|
}
|
|
}
|
|
return self.timers.min_remaining();
|
|
}
|
|
|
|
self.timers.min_remaining()
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
pub async fn step(&mut self, elapsed: ::hsmc::Duration) -> Option<::hsmc::Duration> {
|
|
if self.terminated { return None; }
|
|
|
|
if self.current.is_none() {
|
|
self.enter_path(0).await;
|
|
return self.timers.min_remaining();
|
|
}
|
|
|
|
self.timers.decrement(elapsed);
|
|
|
|
if let Some((state, trig)) = self.timers.pop_expired(&__DEPTH) {
|
|
if __duration_repeats(trig) {
|
|
let d = __duration_for(trig);
|
|
self.timers.start(state, trig, d);
|
|
}
|
|
self.dispatch_trigger(state, 1, trig).await;
|
|
return self.timers.min_remaining();
|
|
}
|
|
|
|
if let Some(ev) = self.queue.pop_front() {
|
|
if matches!(__check_terminate(&ev), TerminateCheck::Yes) {
|
|
self.do_terminate().await;
|
|
return None;
|
|
}
|
|
let eid = __event_to_id(&ev);
|
|
if eid != u16::MAX {
|
|
let mut cur = self.current;
|
|
while let Some(s) = cur {
|
|
if let Some((actions, target)) = __handler_lookup(s, 0, eid) {
|
|
self.run_handlers(s, actions, target, Some(&ev)).await;
|
|
break;
|
|
}
|
|
cur = __PARENT[s as usize];
|
|
}
|
|
}
|
|
return self.timers.min_remaining();
|
|
}
|
|
|
|
self.timers.min_remaining()
|
|
}
|
|
|
|
/// Push an event directly into the machine's queue (synchronous,
|
|
/// single-threaded). Extension beyond spec §4.4 — the canonical
|
|
/// cross-thread path is [`sender().send()`](Self::sender). Useful
|
|
/// for tests and tight `step()` driver loops.
|
|
#[doc(hidden)]
|
|
pub fn send(&mut self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
|
self.queue.push_back(event).map_err(|_| ::hsmc::HsmcError::QueueFull)
|
|
}
|
|
|
|
/// Push an event into the internal queue and drain the queue to
|
|
/// quiescence before returning. Under an async feature the step
|
|
/// loop awaits action futures; after `dispatch()` returns,
|
|
/// `current_state()` reflects the post-dispatch state. Use this
|
|
/// from the task that owns the machine when you want "event
|
|
/// processed" semantics (a drop-in replacement for the
|
|
/// `send + drain` pattern). For cross-task / ISR injection use
|
|
/// [`sender()`](Self::sender) instead.
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
pub async fn dispatch(&mut self, event: #ev_ty) -> ::core::result::Result<(), ::hsmc::HsmcError> {
|
|
if self.terminated { return Err(::hsmc::HsmcError::AlreadyTerminated); }
|
|
self.queue.push_back(event).map_err(|_| ::hsmc::HsmcError::QueueFull)?;
|
|
// Prime state on first call.
|
|
if self.current.is_none() {
|
|
let _ = self.step(::hsmc::Duration::ZERO).await;
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
if self.terminated { return Ok(()); }
|
|
}
|
|
while !self.queue.is_empty() {
|
|
let _ = self.step(::hsmc::Duration::ZERO).await;
|
|
if self.take_overflow() { return Err(::hsmc::HsmcError::QueueFull); }
|
|
if self.terminated { return Ok(()); }
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
fn run_handlers(
|
|
&mut self,
|
|
source_state: u16,
|
|
actions: &'static [u16],
|
|
target: Option<u16>,
|
|
event: Option<&#ev_ty>,
|
|
) {
|
|
for &aid in actions {
|
|
self.run_action(aid, event);
|
|
if self.terminated { return; }
|
|
}
|
|
if let Some(tgt) = target {
|
|
self.transition(source_state, tgt);
|
|
}
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
async fn run_handlers(
|
|
&mut self,
|
|
source_state: u16,
|
|
actions: &'static [u16],
|
|
target: Option<u16>,
|
|
event: Option<&#ev_ty>,
|
|
) {
|
|
for &aid in actions {
|
|
self.run_action(aid, event).await;
|
|
if self.terminated { return; }
|
|
}
|
|
if let Some(tgt) = target {
|
|
self.transition(source_state, tgt).await;
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
fn run_action(&mut self, aid: u16, event: Option<&#ev_ty>) {
|
|
let ctx = &mut self.ctx;
|
|
let mut __proxy = ::hsmc::__private::EmitProxy {
|
|
queue: &mut self.queue,
|
|
overflow: &mut self.__overflow,
|
|
};
|
|
let queue: &mut (dyn ::hsmc::__private::QueuePush<#ev_ty> + ::core::marker::Send) = &mut __proxy;
|
|
let terminated = &self.terminated;
|
|
let mut actx = #action_ctx_name {
|
|
ctx,
|
|
__queue: queue,
|
|
__terminated: terminated,
|
|
};
|
|
fn __dispatch<Ctx: #actions_trait_name>(
|
|
ctx: &mut Ctx,
|
|
aid: u16,
|
|
__event: Option<&#ev_ty>,
|
|
) {
|
|
let _ = __event;
|
|
match aid {
|
|
#(#run_action_arms_sync)*
|
|
_ => {}
|
|
}
|
|
}
|
|
__dispatch(&mut actx, aid, event);
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
async fn run_action(&mut self, aid: u16, event: Option<&#ev_ty>) {
|
|
let ctx = &mut self.ctx;
|
|
let mut __proxy = ::hsmc::__private::EmitProxy {
|
|
queue: &mut self.queue,
|
|
overflow: &mut self.__overflow,
|
|
};
|
|
let queue: &mut (dyn ::hsmc::__private::QueuePush<#ev_ty> + ::core::marker::Send) = &mut __proxy;
|
|
let terminated = &self.terminated;
|
|
let mut actx = #action_ctx_name {
|
|
ctx,
|
|
__queue: queue,
|
|
__terminated: terminated,
|
|
};
|
|
async fn __dispatch<Ctx: #actions_trait_name>(
|
|
ctx: &mut Ctx,
|
|
aid: u16,
|
|
__event: Option<&#ev_ty>,
|
|
) {
|
|
let _ = __event;
|
|
match aid {
|
|
#(#run_action_arms_async)*
|
|
_ => {}
|
|
}
|
|
}
|
|
__dispatch(&mut actx, aid, event).await;
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
fn transition(&mut self, source_matched_state: u16, target: u16) {
|
|
let mut lca = Self::lca(source_matched_state, target);
|
|
if let Some(l) = lca {
|
|
if l == target {
|
|
lca = __PARENT[l as usize];
|
|
}
|
|
}
|
|
if let Some(mut cur) = self.current {
|
|
loop {
|
|
if Some(cur) == lca { break; }
|
|
self.exit_state(cur);
|
|
match __PARENT[cur as usize] {
|
|
Some(p) => cur = p,
|
|
None => break,
|
|
}
|
|
}
|
|
}
|
|
let mut path: ::hsmc::__private::heapless::Vec<u16, 16> =
|
|
::hsmc::__private::heapless::Vec::new();
|
|
let mut node = Some(target);
|
|
while node != lca && node.is_some() {
|
|
let n = node.unwrap();
|
|
let _ = path.push(n);
|
|
node = __PARENT[n as usize];
|
|
}
|
|
for i in (0..path.len()).rev() {
|
|
let s = path[i];
|
|
self.enter_state_no_descent(s);
|
|
}
|
|
self.descend_defaults(target);
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
async fn transition(&mut self, source_matched_state: u16, target: u16) {
|
|
let mut lca = Self::lca(source_matched_state, target);
|
|
if let Some(l) = lca {
|
|
if l == target {
|
|
lca = __PARENT[l as usize];
|
|
}
|
|
}
|
|
if let Some(mut cur) = self.current {
|
|
loop {
|
|
if Some(cur) == lca { break; }
|
|
self.exit_state(cur).await;
|
|
match __PARENT[cur as usize] {
|
|
Some(p) => cur = p,
|
|
None => break,
|
|
}
|
|
}
|
|
}
|
|
let mut path: ::hsmc::__private::heapless::Vec<u16, 16> =
|
|
::hsmc::__private::heapless::Vec::new();
|
|
let mut node = Some(target);
|
|
while node != lca && node.is_some() {
|
|
let n = node.unwrap();
|
|
let _ = path.push(n);
|
|
node = __PARENT[n as usize];
|
|
}
|
|
for i in (0..path.len()).rev() {
|
|
let s = path[i];
|
|
self.enter_state_no_descent(s).await;
|
|
}
|
|
self.descend_defaults(target).await;
|
|
}
|
|
|
|
fn lca(a: u16, b: u16) -> Option<u16> {
|
|
let mut set: ::hsmc::__private::heapless::Vec<u16, 32> =
|
|
::hsmc::__private::heapless::Vec::new();
|
|
let mut cur = Some(a);
|
|
while let Some(x) = cur {
|
|
let _ = set.push(x);
|
|
cur = __PARENT[x as usize];
|
|
}
|
|
let mut cur = Some(b);
|
|
while let Some(x) = cur {
|
|
if set.contains(&x) { return Some(x); }
|
|
cur = __PARENT[x as usize];
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
fn enter_path(&mut self, root: u16) {
|
|
self.enter_state_no_descent(root);
|
|
self.descend_defaults(root);
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
async fn enter_path(&mut self, root: u16) {
|
|
self.enter_state_no_descent(root).await;
|
|
self.descend_defaults(root).await;
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
fn enter_state_no_descent(&mut self, sid: u16) {
|
|
let entries = Self::entries_of(sid);
|
|
for &aid in entries {
|
|
self.run_action(aid, None);
|
|
if self.terminated { return; }
|
|
}
|
|
for &tid in __owned_timers(sid) {
|
|
let d = __duration_for(tid);
|
|
self.timers.start(sid, tid, d);
|
|
}
|
|
self.current = Some(sid);
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
async fn enter_state_no_descent(&mut self, sid: u16) {
|
|
let entries = Self::entries_of(sid);
|
|
for &aid in entries {
|
|
self.run_action(aid, None).await;
|
|
if self.terminated { return; }
|
|
}
|
|
for &tid in __owned_timers(sid) {
|
|
let d = __duration_for(tid);
|
|
self.timers.start(sid, tid, d);
|
|
}
|
|
self.current = Some(sid);
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
fn descend_defaults(&mut self, sid: u16) {
|
|
let mut cur = sid;
|
|
while let Some(child) = __DEFAULT_CHILD[cur as usize] {
|
|
self.enter_state_no_descent(child);
|
|
cur = child;
|
|
}
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
async fn descend_defaults(&mut self, sid: u16) {
|
|
let mut cur = sid;
|
|
while let Some(child) = __DEFAULT_CHILD[cur as usize] {
|
|
self.enter_state_no_descent(child).await;
|
|
cur = child;
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
fn exit_state(&mut self, sid: u16) {
|
|
self.timers.cancel_state(sid);
|
|
let exits = Self::exits_of(sid);
|
|
for &aid in exits {
|
|
self.run_action(aid, None);
|
|
if self.terminated { return; }
|
|
}
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
async fn exit_state(&mut self, sid: u16) {
|
|
self.timers.cancel_state(sid);
|
|
let exits = Self::exits_of(sid);
|
|
for &aid in exits {
|
|
self.run_action(aid, None).await;
|
|
if self.terminated { return; }
|
|
}
|
|
}
|
|
|
|
fn entries_of(sid: u16) -> &'static [u16] {
|
|
match sid {
|
|
#(#entries_of_arms)*
|
|
_ => &[],
|
|
}
|
|
}
|
|
fn exits_of(sid: u16) -> &'static [u16] {
|
|
match sid {
|
|
#(#exits_of_arms)*
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
fn do_terminate(&mut self) {
|
|
if let Some(mut cur) = self.current {
|
|
loop {
|
|
self.exit_state(cur);
|
|
match __PARENT[cur as usize] {
|
|
Some(p) => cur = p,
|
|
None => break,
|
|
}
|
|
}
|
|
}
|
|
self.terminated = true;
|
|
self.current = None;
|
|
while self.queue.pop_front().is_some() {}
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
async fn do_terminate(&mut self) {
|
|
if let Some(mut cur) = self.current {
|
|
loop {
|
|
self.exit_state(cur).await;
|
|
match __PARENT[cur as usize] {
|
|
Some(p) => cur = p,
|
|
None => break,
|
|
}
|
|
}
|
|
}
|
|
self.terminated = true;
|
|
self.current = None;
|
|
while self.queue.pop_front().is_some() {}
|
|
}
|
|
|
|
#[cfg(not(any(feature = "tokio", feature = "embassy")))]
|
|
fn dispatch_trigger(&mut self, state: u16, kind: u8, trig: u16) {
|
|
if let Some((actions, target)) = __handler_lookup(state, kind, trig) {
|
|
// Timer-driven dispatch: no event payload in scope.
|
|
self.run_handlers(state, actions, target, None);
|
|
}
|
|
}
|
|
|
|
#[cfg(any(feature = "tokio", feature = "embassy"))]
|
|
async fn dispatch_trigger(&mut self, state: u16, kind: u8, trig: u16) {
|
|
if let Some((actions, target)) = __handler_lookup(state, kind, trig) {
|
|
self.run_handlers(state, actions, target, None).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
#default_sender_impl
|
|
#tokio_sender_impl
|
|
#embassy_sender_impl
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn _unused(_: Span, _: &Ident) {}
|