timed_fsm/machine.rs
1use crate::response::Response;
2
3/// A timed finite state machine.
4///
5/// Unlike a classic FSM where transitions depend only on `(State, Event)`,
6/// a `TimedStateMachine` can express transitions that depend on the
7/// **absence** of events within a time window. This is achieved by
8/// including [`TimerCommand`](crate::response::TimerCommand)s in the
9/// [`Response`], which the runtime interprets to set or kill timers.
10///
11/// The state machine itself has **no side effects**. It never calls
12/// platform timer APIs directly. Instead, it returns a `Response`
13/// containing timer commands, and the runtime executes them.
14///
15/// # Two kinds of input
16///
17/// | Method | Meaning |
18/// |--------|---------|
19/// | [`on_event`](Self::on_event) | An external event arrived (key press, byte received, …) |
20/// | [`on_timeout`](Self::on_timeout) | A previously requested timer fired — the absence of events was detected |
21///
22/// Both methods return the same [`Response`] type, so the runtime can
23/// handle them uniformly via [`dispatch`](crate::dispatch::dispatch).
24///
25/// # Timer IDs
26///
27/// The [`TimerId`](Self::TimerId) type identifies timers within the
28/// state machine. Choose the type that fits your needs:
29///
30/// | `TimerId` | When to use |
31/// |-----------|-------------|
32/// | `()` | Exactly one timer — simplest case |
33/// | `enum Timer { … }` | Multiple named timers with distinct roles |
34/// | `u8` / `u32` | Indexed timers generated dynamically |
35///
36/// # Invariants
37///
38/// - `on_event` and `on_timeout` **must not produce side effects**.
39/// All effects are expressed through the returned [`Response`].
40/// - A `Response` with `consumed = false` means the event was not
41/// handled by this machine and should be forwarded to the next
42/// handler in the chain. The machine's internal state must remain
43/// unchanged in that case (as if `on_event` was never called).
44/// - Implementations should be deterministic: the same sequence of
45/// inputs always produces the same sequence of outputs.
46///
47/// # Example: single timer
48///
49/// ```
50/// use std::time::Duration;
51/// use timed_fsm::{TimedStateMachine, Response};
52///
53/// struct DebounceFilter {
54/// pending: Option<bool>,
55/// }
56///
57/// impl DebounceFilter {
58/// fn new() -> Self { Self { pending: None } }
59/// }
60///
61/// impl TimedStateMachine for DebounceFilter {
62/// type Event = bool; // GPIO level: true = high, false = low
63/// type Action = bool; // Confirmed level
64/// type TimerId = (); // One debounce timer
65///
66/// fn on_event(&mut self, event: bool) -> Response<bool, ()> {
67/// self.pending = Some(event);
68/// Response::consume()
69/// .with_timer((), Duration::from_millis(20))
70/// }
71///
72/// fn on_timeout(&mut self, _: ()) -> Response<bool, ()> {
73/// match self.pending.take() {
74/// Some(level) => Response::emit_one(level),
75/// None => Response::pass_through(),
76/// }
77/// }
78/// }
79/// ```
80///
81/// # Example: multiple timers
82///
83/// When a state machine needs more than one concurrent timer, use an
84/// enum as `TimerId` so each timer has a descriptive name.
85///
86/// ```
87/// use std::time::Duration;
88/// use timed_fsm::{TimedStateMachine, Response};
89///
90/// #[derive(Clone, Copy, Debug, PartialEq, Eq)]
91/// enum Timer { Debounce, RepeatDelay, RepeatInterval }
92///
93/// struct KeyRepeater { key: Option<u8> }
94///
95/// impl KeyRepeater {
96/// fn new() -> Self { Self { key: None } }
97/// }
98///
99/// impl TimedStateMachine for KeyRepeater {
100/// type Event = Option<u8>; // Some(key) = pressed, None = released
101/// type Action = u8;
102/// type TimerId = Timer;
103///
104/// fn on_event(&mut self, event: Option<u8>) -> Response<u8, Timer> {
105/// match event {
106/// Some(key) => {
107/// self.key = Some(key);
108/// Response::consume()
109/// .with_timer(Timer::Debounce, Duration::from_millis(10))
110/// .with_kill_timer(Timer::RepeatDelay)
111/// .with_kill_timer(Timer::RepeatInterval)
112/// }
113/// None => {
114/// self.key = None;
115/// Response::consume()
116/// .with_kill_timer(Timer::Debounce)
117/// .with_kill_timer(Timer::RepeatDelay)
118/// .with_kill_timer(Timer::RepeatInterval)
119/// }
120/// }
121/// }
122///
123/// fn on_timeout(&mut self, id: Timer) -> Response<u8, Timer> {
124/// match id {
125/// Timer::Debounce => match self.key {
126/// Some(k) => Response::emit_one(k)
127/// .with_timer(Timer::RepeatDelay, Duration::from_millis(500)),
128/// None => Response::pass_through(),
129/// },
130/// Timer::RepeatDelay => match self.key {
131/// Some(k) => Response::emit_one(k)
132/// .with_timer(Timer::RepeatInterval, Duration::from_millis(50)),
133/// None => Response::pass_through(),
134/// },
135/// Timer::RepeatInterval => match self.key {
136/// Some(k) => Response::emit_one(k)
137/// .with_timer(Timer::RepeatInterval, Duration::from_millis(50)),
138/// None => Response::pass_through(),
139/// },
140/// }
141/// }
142/// }
143/// ```
144pub trait TimedStateMachine {
145 /// The event type fed into the state machine via [`on_event`](Self::on_event).
146 type Event;
147
148 /// The action type produced by transitions.
149 ///
150 /// Actions are collected in [`Response::actions`](crate::Response::actions)
151 /// and forwarded to [`ActionExecutor::execute`](crate::ActionExecutor::execute)
152 /// by the runtime.
153 type Action;
154
155 /// The timer identifier type.
156 ///
157 /// Use `()` if only one timer is needed.
158 /// Use an enum or integer for multiple concurrent timers.
159 ///
160 /// Must implement `Copy + Eq + Debug` so the runtime and assertion
161 /// helpers can compare and display timer IDs.
162 type TimerId: Copy + Eq + core::fmt::Debug;
163
164 /// Process an external event and return the transition result.
165 ///
166 /// The returned [`Response`] describes what actions to emit and
167 /// which timers to set or kill. The runtime then executes those
168 /// effects via [`dispatch`](crate::dispatch::dispatch).
169 fn on_event(&mut self, event: Self::Event) -> Response<Self::Action, Self::TimerId>;
170
171 /// Process a timer timeout and return the transition result.
172 ///
173 /// Called by the runtime when a timer previously requested via
174 /// [`TimerCommand::Set`](crate::response::TimerCommand::Set) fires.
175 /// The `timer_id` identifies which timer expired, allowing the state
176 /// machine to distinguish multiple concurrent timers.
177 fn on_timeout(&mut self, timer_id: Self::TimerId) -> Response<Self::Action, Self::TimerId>;
178}