| Crates.io | typed-fsm |
| lib.rs | typed-fsm |
| version | 0.4.8 |
| created_at | 2025-11-22 01:52:19.52158+00 |
| updated_at | 2025-12-02 22:34:18.371779+00 |
| description | Event-driven FSM with ISR-safe dispatch for embedded systems. Zero-cost, no_std, type-safe state machines with interrupt and concurrency support. |
| homepage | |
| repository | https://github.com/afmiguel/typed-fsm |
| max_upload_size | |
| id | 1944613 |
| size | 412,367 |
A lightweight, zero-cost, event-driven finite state machine microframework for Rust with ISR and concurrency support.
typed-fsm provides a declarative macro-based approach to building type-safe, event-driven state machines. Perfect for embedded systems with interrupt handlers, real-time applications, protocol implementations, and any scenario requiring robust state management with thread-safe or ISR-safe dispatch.
dispatch() from interrupt service routines (optional concurrent feature)Box, dyn, or heap)#![no_std] compatible with zero dependencies by defaultentry, process, and exit actions per statelog or tracing crates (zero-cost when disabled)Enable the concurrent feature for interrupt-safe and thread-safe dispatch:
dropped_events_count() API for production monitoringportable-atomic.| Feature | typed-fsm | statig | smlang | rust-fsm |
|---|---|---|---|---|
| Event-driven | ✓ | ✓ | ✓ | ✓ |
| Zero-cost | ✓ | ~ | ~ | ~ |
| no_std | ✓ | ✓ | ✓ | ✓ |
| Stateful states | ✓ | ✓ | ✓ | ✗¹ |
| Lifecycle hooks | ✓ | ✓ | ~² | ✗ |
| Hierarchical FSM | ~³ | ✓ | ✗ | ✗ |
| Thread-safe (Send+Sync) | ✓ | ? | ? | ? |
| ISR/Concurrency support | ✓⁴ | ✗⁵ | ✗⁵ | ✗⁵ |
| Macro-based DSL | ✓ | ✓ | ✓ | ✓ |
| Type-safe | ✓ | ✓ | ✓ | ✓ |
| Dependencies | 0⁶ | 3⁷ | 1 | 2⁷ |
| Async support | ✗⁸ | ✓ | ✓ | ✗ |
| Diagram generation | ✗ | ✗ | ✗ | ✓ |
¹ rust-fsm: States cannot carry data in DSL (manual implementation possible)
² smlang: Has guards/actions, but not explicit entry/exit hooks per state
³ typed-fsm: Via nested FSMs in context (compositional, not native like statig)
⁴ typed-fsm: Native support via concurrent feature. Atomic protection, event queuing, ISR-safe dispatch
⁵ No native ISR/concurrency support. Manual synchronization required (Arc<Mutex<>>, critical sections)
⁶ typed-fsm: Zero dependencies by default. Optional dependencies when features enabled: logging (+1 dep), concurrent (+3 deps)
⁷ Optional dependencies (can be disabled with feature flags)
⁸ typed-fsm: Can be used within async code, but hooks are synchronous (no async fn support)
Choose typed-fsm if you need:
dispatch() from interrupt handlers (timer, UART, GPIO interrupts)Choose statig if you need:
Choose smlang if you need:
Choose rust-fsm if you need:
Don't choose typed-fsm if you need:
dispatch() from interrupt handlers (timer, UART, GPIO) or multiple threads with atomic protection and lock-free event queuing. Perfect for embedded systems and RTOS environments.logging: +1, concurrent: +3)#![no_std] supportAdd this to your Cargo.toml:
[dependencies]
typed-fsm = "0.4"
A minimal LED blink state machine:
use typed_fsm::{state_machine, Transition};
// Context: Shared state
struct LedContext {
tick_count: u32,
}
// Event: Simple tick
enum Event {
Tick,
}
// State machine: On ↔ Off
state_machine! {
Name: BlinkFSM,
Context: LedContext,
Event: Event,
States: {
On => {
entry: |ctx| {
ctx.tick_count += 1;
println!("LED ON");
}
process: |_ctx, event| {
match event {
Event::Tick => Transition::To(BlinkFSM::Off),
}
}
},
Off => {
entry: |ctx| {
ctx.tick_count += 1;
println!("LED OFF");
}
process: |_ctx, event| {
match event {
Event::Tick => Transition::To(BlinkFSM::On),
}
}
}
}
}
fn main() {
let mut ctx = LedContext { tick_count: 0 };
let mut led = BlinkFSM::On;
// ⚠️ CRITICAL: Must call init() before event loop!
led.init(&mut ctx);
// Dispatch events
led.dispatch(&mut ctx, &Event::Tick); // On → Off
led.dispatch(&mut ctx, &Event::Tick); // Off → On
println!("Total ticks: {}", ctx.tick_count);
}
The process hook must return a Transition enum to tell the state machine what to do next:
Transition::None - Stay in Current StateUse when an event should be handled but doesn't require changing states:
process: |ctx, evt| {
match evt {
MyEvent::UpdateData(value) => {
ctx.data = *value; // Update context
Transition::None // Stay in same state
}
}
}
When to use:
What happens:
process executesexit does NOT execute (no state change)entry does NOT execute (no state change)Transition::To(State) - Move to New StateUse when an event should trigger a state change:
process: |ctx, evt| {
match evt {
MyEvent::Start => {
Transition::To(MyFSM::Running { speed: 100 })
}
}
}
When to use:
What happens:
process executes and returns new stateexit executes (if defined)entry executes (if defined)state_machine! {
Name: DoorFSM,
Context: DoorContext,
Event: DoorEvent,
States: {
Closed => {
process: |ctx, evt| {
match evt {
DoorEvent::Open => {
// Change state
Transition::To(DoorFSM::Open)
},
DoorEvent::Lock => {
// Stay in same state but update context
ctx.locked = true;
Transition::None
},
DoorEvent::Close => {
// Already closed, do nothing
Transition::None
}
}
}
},
Open => {
process: |ctx, evt| {
match evt {
DoorEvent::Close => {
// Change state
Transition::To(DoorFSM::Closed)
},
DoorEvent::Open | DoorEvent::Lock => {
// Invalid in this state, ignore
Transition::None
}
}
}
}
}
}
Key Points:
process block must return a TransitionTransition::None for events that don't change stateTransition::To(State) for events that trigger transitionsprocess before returningexit/entry hooks runYou MUST call .init(&mut ctx) before dispatching any events!
The init() method executes the entry action of the initial state. Forgetting to call it will cause:
// 1. Create context
let mut ctx = MyContext { /* ... */ };
// 2. Create state machine
let mut fsm = MyFSM::InitialState;
// 3. ⚠️ CRITICAL: Initialize BEFORE event loop
fsm.init(&mut ctx);
// 4. Now safe to dispatch events
loop {
fsm.dispatch(&mut ctx, &event);
}
let mut ctx = MyContext { /* ... */ };
let mut fsm = MyFSM::InitialState;
// ❌ WRONG: Forgot to call init()!
// Entry action will NEVER execute!
fsm.dispatch(&mut ctx, &event); // Silent failure
See the blink example for a complete demonstration.
Copy and paste this template to start building your state machine. Replace the UPPERCASE placeholders with your actual names:
use typed_fsm::{state_machine, Transition};
// 1. Define your context (shared state across all states)
struct MY_CONTEXT {
MY_FIELD: MY_TYPE,
}
// 2. Define your events (what can happen to trigger transitions)
enum MY_EVENT {
MY_EVENT_1,
MY_EVENT_2,
}
// 3. Create your state machine
state_machine! {
Name: MY_FSM,
Context: MY_CONTEXT,
Event: MY_EVENT,
States: {
MY_STATE_1 => {
entry: |ctx| {
// Runs once when entering this state
}
process: |ctx, evt| {
match evt {
MY_EVENT::MY_EVENT_1 => Transition::To(MY_FSM::MY_STATE_2),
MY_EVENT::MY_EVENT_2 => Transition::None
}
}
exit: |ctx| {
// Runs once when leaving this state
}
},
MY_STATE_2 => {
process: |ctx, evt| {
match evt {
MY_EVENT::MY_EVENT_1 => Transition::To(MY_FSM::MY_STATE_1),
_ => Transition::None
}
}
}
}
}
fn main() {
// 1. Create context
let mut ctx = MY_CONTEXT {
MY_FIELD: MY_VALUE,
};
// 2. Create state machine (start in initial state)
let mut fsm = MY_FSM::MY_STATE_1;
// 3. ⚠️ CRITICAL: Initialize before event loop!
fsm.init(&mut ctx);
// 4. Event loop - dispatch events
fsm.dispatch(&mut ctx, &MY_EVENT::MY_EVENT_1);
fsm.dispatch(&mut ctx, &MY_EVENT::MY_EVENT_2);
}
States can carry typed data:
state_machine! {
Name: MotorFSM,
Context: MotorContext,
Event: MotorEvent,
States: {
Running { target_speed: u32 } => {
entry: |_ctx| {
println!("Target speed: {} RPM", target_speed);
}
process: |ctx, evt| {
// Access target_speed with *target_speed
match evt {
MotorEvent::SetSpeed(speed) => {
*target_speed = *speed;
Transition::None
},
_ => Transition::None
}
}
}
}
}
Each state supports three lifecycle hooks:
entry (optional) - Executed once when entering the stateprocess (required) - Handles events, returns Transition<S>exit (optional) - Executed once when leaving the statestate_machine! {
Name: MyFSM,
Context: MyContext,
Event: MyEvent,
States: {
Active => {
entry: |ctx| {
ctx.resource.acquire();
}
process: |ctx, evt| {
Transition::None
}
exit: |ctx| {
ctx.resource.release();
}
}
}
}
typed-fsm supports concurrent state machines through composition with Rust's standard concurrency primitives.
FSMs are automatically Send + Sync if their fields are Send + Sync. This allows safe sharing across threads using Arc<Mutex<>>.
use std::sync::{Arc, Mutex};
use std::thread;
use typed_fsm::{state_machine, Transition};
// FSM is automatically Send + Sync
let fsm = Arc::new(Mutex::new(MyFSM::Initial));
let ctx = Arc::new(Mutex::new(MyContext { /* ... */ }));
// Clone for another thread
let fsm_clone = Arc::clone(&fsm);
let ctx_clone = Arc::clone(&ctx);
thread::spawn(move || {
let mut fsm_lock = fsm_clone.lock().unwrap();
let mut ctx_lock = ctx_clone.lock().unwrap();
fsm_lock.dispatch(&mut *ctx_lock, &event);
});
Use channels to distribute events to multiple FSMs:
use std::sync::mpsc::channel;
let (tx, rx1) = channel();
let rx2 = tx.clone();
// Broadcast events to multiple FSMs
tx.send(Event::Tick).unwrap();
Multiple FSMs can coordinate through shared state:
struct SharedState {
lock_a: bool,
lock_b: bool,
}
let shared = Arc::new(Mutex::new(SharedState {
lock_a: false,
lock_b: false
}));
// FSM A and FSM B coordinate via shared state
// See examples/traffic_intersection.rs for complete example
The core framework remains #![no_std] compatible. Concurrency examples use std::sync and std::thread, but the generated FSM code has zero dependencies and works in no_std environments.
For embedded systems without std:
spin::Mutex instead of std::sync::Mutexalloc::sync::Arc instead of std::sync::ArcWhile typed-fsm does not have native async/await support in lifecycle hooks, it can be used within async contexts. The state machine methods are synchronous but can be called from async functions.
use typed_fsm::{state_machine, Transition};
// Standard synchronous FSM
state_machine! {
Name: MyFSM,
Context: MyContext,
Event: MyEvent,
States: {
Active => {
entry: |ctx| {
ctx.status = "active".to_string();
}
process: |ctx, evt| {
match evt {
MyEvent::Stop => Transition::To(MyFSM::Idle),
_ => Transition::None
}
}
},
Idle => {
process: |ctx, evt| {
match evt {
MyEvent::Start => Transition::To(MyFSM::Active),
_ => Transition::None
}
}
}
}
}
// Use within async context
async fn process_events(mut fsm: MyFSM, mut ctx: MyContext) {
fsm.init(&mut ctx);
loop {
// Async operations between dispatches
let event = receive_event_async().await;
// Synchronous dispatch
fsm.dispatch(&mut ctx, &event);
// More async work
if matches!(fsm, MyFSM::Active) {
send_status_update(&ctx).await;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
This is an intentional design decision to:
If your use case requires async fn in entry/exit/process hooks, consider:
These crates sacrifice zero-cost abstraction and require a runtime, but provide first-class async support.
Guards are boolean conditions that must evaluate to true for a state transition to occur. They act as gatekeepers, validating data or checking preconditions before allowing state changes.
Guards allow you to implement conditional logic that determines whether an event should trigger a state transition. This is essential for:
Guards are implemented using standard Rust conditionals (if/else) within the process block. No special syntax is needed - just return Transition::None when the guard condition fails.
use typed_fsm::{state_machine, Transition};
struct ATMContext {
correct_pin: u32,
attempts: u32,
}
enum ATMEvent {
EnterPIN { pin: u32 },
}
state_machine! {
Name: ATM,
Context: ATMContext,
Event: ATMEvent,
States: {
WaitingPIN => {
process: |ctx, evt| {
match evt {
ATMEvent::EnterPIN { pin } => {
// Guard 1: Check if PIN is correct
if *pin == ctx.correct_pin {
println!("PIN accepted");
Transition::To(ATM::Authenticated)
} else {
ctx.attempts += 1;
// Guard 2: Block after 3 attempts
if ctx.attempts >= 3 {
Transition::To(ATM::Blocked)
} else {
Transition::None
}
}
}
}
}
},
Authenticated => {
process: |_ctx, _evt| { Transition::None }
},
Blocked => {
process: |_ctx, _evt| { Transition::None }
}
}
}
Guards can combine multiple conditions:
process: |ctx, evt| {
match evt {
OrderEvent::Submit => {
// Guard 1: Check stock
if !ctx.items_in_stock {
return Transition::None;
}
// Guard 2: Check credit limit
if ctx.order_value > ctx.customer_credit {
return Transition::None;
}
// All guards passed
Transition::To(Order::Submitted)
}
}
}
See examples/guards.rs for a comprehensive example demonstrating:
Run with:
cargo run --example guards
typed-fsm supports time-based state transitions through the Timer trait abstraction pattern. This pattern maintains no_std compatibility while providing a flexible, platform-agnostic way to implement timeouts, retries, and time-based behaviors.
Timeouts allow states to automatically transition after a specified time duration. They're essential for:
Unlike some FSM libraries that provide built-in timer functionality (which would break no_std compatibility), typed-fsm uses a trait abstraction pattern:
Timer trait (user-provided or use the example)This pattern is:
pub trait Timer {
fn start(&mut self, duration_ms: u64);
fn is_expired(&self) -> bool;
fn reset(&mut self);
}
For std (Desktop/Server):
use std::time::{Duration, Instant};
struct StdTimer {
start_time: Option<Instant>,
duration: Duration,
}
impl Timer for StdTimer {
fn start(&mut self, duration_ms: u64) {
self.start_time = Some(Instant::now());
self.duration = Duration::from_millis(duration_ms);
}
fn is_expired(&self) -> bool {
if let Some(start) = self.start_time {
start.elapsed() >= self.duration
} else {
false
}
}
fn reset(&mut self) {
self.start_time = None;
}
}
For Embedded (no_std):
// Example for embedded HAL timer
struct EmbeddedTimer<'a> {
timer: &'a mut dyn embedded_hal::timer::CountDown,
is_running: bool,
}
impl Timer for EmbeddedTimer<'_> {
fn start(&mut self, duration_ms: u64) {
self.timer.start(duration_ms.millis());
self.is_running = true;
}
fn is_expired(&self) -> bool {
self.is_running && self.timer.wait().is_ok()
}
fn reset(&mut self) {
self.is_running = false;
}
}
For Testing (Mock):
struct MockTimer {
remaining_ms: u64,
}
impl Timer for MockTimer {
fn start(&mut self, duration_ms: u64) {
self.remaining_ms = duration_ms;
}
fn is_expired(&self) -> bool {
self.remaining_ms == 0
}
fn reset(&mut self) {
self.remaining_ms = 0;
}
}
// In tests, manually decrement remaining_ms to simulate time
use typed_fsm::{state_machine, Transition};
struct WiFiContext {
timer: StdTimer,
retry_count: u32,
connection_timeout_ms: u64,
}
enum WiFiEvent {
Connect,
Connected,
CheckTimeout, // Polled event to check timeout
}
state_machine! {
Name: WiFi,
Context: WiFiContext,
Event: WiFiEvent,
States: {
Connecting => {
entry: |ctx| {
println!("Connecting... (timeout: {}ms)", ctx.connection_timeout_ms);
// Start timeout timer
ctx.timer.start(ctx.connection_timeout_ms);
}
process: |ctx, evt| {
match evt {
WiFiEvent::Connected => {
println!("Connected!");
ctx.timer.reset();
Transition::To(WiFi::Active)
}
WiFiEvent::CheckTimeout => {
// Check if timeout expired
if ctx.timer.is_expired() {
println!("Timeout!");
ctx.timer.reset();
Transition::To(WiFi::Failed)
} else {
Transition::None
}
}
_ => Transition::None
}
}
exit: |ctx| {
ctx.timer.reset();
}
},
Active => { /* ... */ },
Failed => { /* ... */ }
}
}
let mut ctx = WiFiContext {
timer: StdTimer::new(),
retry_count: 0,
connection_timeout_ms: 5000,
};
let mut wifi = WiFi::Connecting;
wifi.init(&mut ctx);
// In your event loop:
loop {
// Poll for events
if let Some(event) = get_event() {
wifi.dispatch(&mut ctx, &event);
}
// Periodically check for timeouts
wifi.dispatch(&mut ctx, &WiFiEvent::CheckTimeout);
thread::sleep(Duration::from_millis(100));
}
CheckTimeout event in your event loopSee examples/timeouts.rs for comprehensive examples demonstrating:
Run with:
cargo run --example timeouts
concurrent)NEW in v0.4.0: Built-in support for safe dispatch from interrupt service routines (ISRs) and multiple threads.
Enable the concurrent feature when you need to call dispatch() from:
The concurrent feature adds atomic protection to prevent re-entrant dispatch calls:
portable-atomic for broad architecture support (AVR, Cortex-M, RISC-V, Desktop).[dependencies]
typed-fsm = { version = "0.4", features = ["concurrent"] }
# Requires critical-section implementation for your platform:
# - For std: critical-section with "std" feature (included automatically)
# - For embedded: Use your HAL's critical-section implementation
Dependencies added by concurrent feature:
critical-section v1.1 - Portable critical sections (interrupt-safe primitives)heapless v0.8 - No-alloc data structures (event queue)paste v1.0 - Macro hygiene (static variable name generation)portable-atomic v1.0 - Portable atomic types for all architectures// Embedded system with timer interrupt
static mut FSM: Option<SensorFSM> = None;
static mut CTX: Option<SensorContext> = None;
#[interrupt]
fn TIMER_IRQ() {
unsafe {
if let (Some(fsm), Some(ctx)) = (FSM.as_mut(), CTX.as_mut()) {
// ✅ Safe with `concurrent` feature!
// Event is queued if main loop is active
fsm.dispatch(ctx, SensorEvent::TimerTick);
}
}
}
fn main() {
// Main loop processing
loop {
fsm.dispatch(&mut ctx, user_event);
// Automatically processes ISR events from queue
}
}
use std::sync::{Arc, Mutex};
use std::thread;
let fsm = Arc::new(Mutex::new(TaskFSM::Idle));
let ctx = Arc::new(Mutex::new(TaskContext::new()));
// Thread 1: Producer
let (fsm1, ctx1) = (Arc::clone(&fsm), Arc::clone(&ctx));
thread::spawn(move || {
let mut fsm = fsm1.lock().unwrap();
let mut ctx = ctx1.lock().unwrap();
fsm.dispatch(&mut ctx, TaskEvent::NewTask); // ✅ Thread-safe
});
// Thread 2: Consumer
let (fsm2, ctx2) = (Arc::clone(&fsm), Arc::clone(&ctx));
thread::spawn(move || {
let mut fsm = fsm2.lock().unwrap();
let mut ctx = ctx2.lock().unwrap();
fsm.dispatch(&mut ctx, TaskEvent::Process); // ✅ Thread-safe
});
# ISR simulation with event queuing
cargo run --example concurrent_isr --features concurrent
# Multithreading with concurrent dispatch
cargo run --example concurrent_threads --features concurrent
# Run concurrency tests
cargo test --features concurrent --test concurrent_tests
See:
examples/concurrent_isr.rs - Simulated ISR with atomic event queuingexamples/concurrent_threads.rs - Multi-threaded task processortests/concurrent_tests.rs - Comprehensive concurrency testsThis library has comprehensive test coverage (~100%) with 88+ tests covering:
Transition enum functionalityRun all tests:
# Run all tests (without concurrent feature)
cargo test
# Run tests with concurrent feature
cargo test --features concurrent
# Run tests with output
cargo test -- --nocapture
# Run specific test suites
cargo test --test integration_tests # 13 tests
cargo test --test coverage_tests # 10 tests
cargo test --test edge_cases_tests # 8 tests
cargo test --test guards_tests # 14 tests (v0.3.0)
cargo test --test logging_tests # 9 tests (v0.3.0)
cargo test --test timeouts_tests # 11 tests (v0.3.0)
cargo test --test concurrent_tests --features concurrent # 9 tests (v0.4.0)
See the examples/ directory for complete examples:
motor.rs: Motor control system (Complex, Event-Driven) - Start here!traffic_light.rs: Traffic light controller (Simple, Event-Driven)guards.rs: Conditional transitions (ATM, Door Lock, Shop Orders)logging.rs: FSM with logging instrumentationtimeouts.rs: Timer patterns (WiFi Connection, Session Timeout, Debouncing)concurrent_isr.rs: ISR-safe dispatch (requires concurrent feature)concurrent_threads.rs: Thread-safe dispatch (requires concurrent feature)The state_machine! macro generates:
pub enum with your states as variantsinit(), dispatch(), and internal lifecycle methodsThe generated code uses:
dyn Trait)#[inline(always)] for zero-cost abstractionThis library is designed for performance-critical applications:
#![no_std] compatibleQ: Can I use typed-fsm in no_std environments?
A: Yes! typed-fsm is #![no_std] compatible and has zero dependencies, making it perfect for embedded systems.
Q: What's the performance overhead? A: Zero! The macro compiles to simple enum pattern matching. State transitions are just enum assignments. No heap allocations, no dynamic dispatch.
Q: Can states hold data?
A: Yes! States can carry typed fields, for example: Running { speed: u32, mode: Mode }.
Q: How does this compare to manual enum matching? A: It generates the same code you would write manually, but with better organization, lifecycle hooks, and less boilerplate.
Q: Does this work with async/await? A: Yes! The context and events can be async-friendly. The state machine itself is synchronous, but you can use async operations in your entry/exit/process handlers.
Q: Can I serialize the state machine?
A: The generated enum can derive Serialize/Deserialize if you enable the serde feature (future enhancement).
Q: How do I handle errors in state transitions?
A: You can include error information in events or state data. For example: Error { code: u32, message: String }.
Q: Can I have nested state machines? A: Yes! A state's context can contain another state machine. This allows hierarchical state machines.
Q: Is this library safe?
A: Yes! The library contains zero unsafe blocks and has zero dependencies by default. It's been thoroughly tested with 100+ tests covering 100% of code paths.
Q: Can invalid states occur? A: No! Rust's type system prevents invalid states at compile time. If it compiles, the state transitions are valid.
Q: Is this production-ready? A: Yes! The library has comprehensive tests, documentation, and follows Rust best practices. It's ready for production use.
Q: How do I implement conditional transitions (guards)?
A: Use if conditions inside your process handler:
process: |ctx, evt| {
match evt {
Event::Unlock(code) => {
if ctx.authorized_codes.contains(code) {
Transition::To(DoorFSM::Unlocked)
} else {
ctx.failed_attempts += 1;
Transition::None // Stay locked
}
}
}
}
Q: Can I have complex guard conditions?
A: Yes! Use any Rust expression. Guards can check context state, event data, external conditions, etc. For multiple conditions, use if/else if/else chains or helper functions.
Q: Do I need a special feature for guards?
A: No! Guards are just normal Rust code in your process handler. No special syntax or features required.
Q: How do I add logging to my FSM?
A: Enable the logging feature:
[dependencies]
typed-fsm = { version = "0.4", features = ["logging"] }
This automatically logs all state transitions, entry/exit actions, and events using the log crate.
Q: Does logging add overhead when disabled?
A: Zero overhead! When the logging feature is disabled, no logging code is generated at all. It's a true zero-cost abstraction.
Q: Can I use tracing instead of log?
A: Not yet, but it's planned. Currently only the log crate is supported via the logging feature.
Q: How do I implement timeouts?
A: Implement a Timer trait and check elapsed time in your process handler:
process: |ctx, evt| {
match evt {
Event::Tick => {
if ctx.timer.elapsed() > timeout {
Transition::To(WiFiFSM::TimedOut)
} else {
Transition::None
}
}
}
}
See examples/timeouts.rs for complete implementations.
Q: Do I need a specific timer library?
A: No! The timer pattern is platform-agnostic. Use std::time::Instant for std environments, or your HAL's timer for embedded systems.
Q: When do I need the concurrent feature?
A: Enable concurrent when you need to call dispatch() from:
Q: What's the difference between Arc<Mutex
Q: How does ISR-safe dispatch work? A: When dispatch is called from an ISR while another dispatch is active:
Q: Can I use dispatch() directly in an interrupt handler?
A: Yes, with the concurrent feature enabled:
#[interrupt]
fn TIMER_IRQ() {
unsafe {
if let Some(fsm) = FSM.as_mut() {
fsm.dispatch(&mut ctx, &Event::TimerTick); // ✅ Safe!
}
}
}
Q: What's the performance overhead of the concurrent feature? A: ~10-15% overhead when enabled with no contention. Zero overhead when the feature is disabled (default).
Q: Do I need critical-section for the concurrent feature?
A: Yes. For std environments, it's automatically included. For embedded/no_std, you need to provide a critical-section implementation from your HAL.
Q: Can events be dropped?
A: Yes, if the queue is full (default: 16 events). Use dropped_events_count() to monitor:
if MyFSM::dropped_events_count() > 0 {
log::warn!("Queue overflow detected!");
}
Q: How do I configure queue size?
A: Use the QueueCapacity parameter:
state_machine! {
Name: MyFSM,
Context: Ctx,
Event: Event,
QueueCapacity: 64, // Increase to 64 events
States: { ... }
}
Q: What happens in debug vs release builds when queue overflows? A:
Q: My events need to be Clone for concurrent feature. Why? A: Events are cloned when queued. This allows the ISR/thread to return immediately without waiting. Most event types are small and cheap to clone.
Full API documentation is available at docs.rs/typed-fsm.
Licensed under either of:
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.