Skip to main content

autocore_std/fb/
state_machine.rs

1use std::time::{Duration, Instant};
2
3/// State Machine Helper (FB_StateMachine)
4///
5/// A state machine helper with automatic timer management and error tracking.
6/// Provides two timers that automatically reset when the state index changes:
7///
8/// - **Timer** (`timer_done()`) - General purpose timer for delays and debouncing
9/// - **Timeout** (`timed_out()`) - For detecting stuck states
10///
11/// This is equivalent to the IEC 61131-3 FB_StateMachine function block.
12///
13/// # Automatic Timer Reset
14///
15/// Both timers automatically reset when `index` changes. This eliminates a
16/// common source of bugs in state machines where timers are not properly reset.
17///
18/// The pattern is:
19/// 1. Set `timer_preset` (and optionally `timeout_preset`) in state N
20/// 2. Change `index` to state N+1 (timers reset and start counting)
21/// 3. In state N+1, check `timer_done()` or `timed_out()`
22///
23/// # Example
24///
25/// ```
26/// use autocore_std::fb::StateMachine;
27/// use std::time::Duration;
28///
29/// let mut state = StateMachine::new();
30///
31/// // Simulate a control loop
32/// loop {
33///     match state.index {
34///         0 => { // Reset
35///             state.clear_error();
36///             state.index = 10;
37///         }
38///         10 => { // Idle - wait for start signal
39///             // For demo, just proceed
40///             state.timer_preset = Duration::from_millis(100);
41///             state.index = 20;
42///         }
43///         20 => { // Debounce
44///             if state.timer_done() {
45///                 state.timeout_preset = Duration::from_secs(10);
46///                 state.index = 30;
47///             }
48///         }
49///         30 => { // Wait for operation (simulated)
50///             // In real code: check operation_complete
51///             // For demo, check timeout
52///             if state.timed_out() {
53///                 state.set_error(30, "Operation timeout");
54///                 state.index = 0;
55///             }
56///             // Exit demo loop
57///             break;
58///         }
59///         _ => { state.index = 0; }
60///     }
61///
62///     state.call(); // Call at end of each scan cycle
63///     # break; // Exit for doctest
64/// }
65/// ```
66///
67/// # Timer Presets Persist
68///
69/// Timer presets persist until you change them. This allows setting a preset
70/// once and using it across multiple states:
71///
72/// ```ignore
73/// 100 => {
74///     state.timer_preset = Duration::from_millis(300);
75///     state.index = 110;
76/// }
77/// 110 => {
78///     // Uses 300ms preset set in state 100
79///     if some_condition && state.timer_done() {
80///         state.index = 120;
81///     }
82/// }
83/// 120 => {
84///     // Still uses 300ms preset (timer reset on state change)
85///     if state.timer_done() {
86///         state.index = 10;
87///     }
88/// }
89/// ```
90///
91/// # Error Handling Pattern
92///
93/// ```ignore
94/// 200 => {
95///     state.timeout_preset = Duration::from_secs(7);
96///     start_operation();
97///     state.index = 210;
98/// }
99/// 210 => {
100///     if operation_complete {
101///         state.index = 1000; // Success
102///     } else if state.timed_out() {
103///         state.set_error(210, "Operation timed out");
104///         state.index = 5000; // Error handler
105///     }
106/// }
107/// 5000 => {
108///     // Error recovery
109///     state.index = 0;
110/// }
111/// ```
112#[derive(Debug, Clone)]
113pub struct StateMachine {
114    /// Current state index.
115    pub index: i32,
116
117    /// Timer preset. `timer_done()` returns true when time in current state >= this value.
118    /// Defaults to `Duration::MAX` (timer never triggers unless you set a preset).
119    pub timer_preset: Duration,
120
121    /// Timeout preset. `timed_out()` returns true when time in current state >= this value.
122    /// Defaults to `Duration::MAX` (timeout never triggers unless you set a preset).
123    pub timeout_preset: Duration,
124
125    /// Error code. A value of 0 indicates no error.
126    /// When non-zero, `is_error()` returns true.
127    pub error_code: i32,
128
129    /// Status message for UI display. Content does not indicate an error.
130    pub message: String,
131
132    /// Error message for UI display. Should only have content when `error_code != 0`.
133    pub error_message: String,
134
135    // Internal state
136    last_index: Option<i32>,
137    state_entered_at: Option<Instant>,
138}
139
140impl StateMachine {
141    /// Creates a new state machine starting at state 0.
142    ///
143    /// Timer presets default to `Duration::MAX`, meaning timers won't trigger
144    /// until you explicitly set a preset.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use autocore_std::fb::StateMachine;
150    ///
151    /// let state = StateMachine::new();
152    /// assert_eq!(state.index, 0);
153    /// assert_eq!(state.error_code, 0);
154    /// assert!(!state.is_error());
155    /// ```
156    pub fn new() -> Self {
157        Self {
158            index: 0,
159            timer_preset: Duration::MAX,
160            timeout_preset: Duration::MAX,
161            error_code: 0,
162            message: String::new(),
163            error_message: String::new(),
164            last_index: None,
165            state_entered_at: None,
166        }
167    }
168
169    /// Call once per scan cycle at the END of your state machine logic.
170    ///
171    /// This method:
172    /// - Detects state changes (when `index` differs from the previous call)
173    /// - Resets internal timers on state change
174    /// - Updates internal tracking for `elapsed()`, `timer_done()`, and `timed_out()`
175    ///
176    /// # Example
177    ///
178    /// ```
179    /// use autocore_std::fb::StateMachine;
180    ///
181    /// let mut state = StateMachine::new();
182    ///
183    /// // Your state machine logic here...
184    /// match state.index {
185    ///     0 => { state.index = 10; }
186    ///     _ => {}
187    /// }
188    ///
189    /// state.call(); // Always call at the end
190    /// ```
191    pub fn call(&mut self) {
192        if self.last_index != Some(self.index) {
193            self.state_entered_at = Some(Instant::now());
194        }
195        self.last_index = Some(self.index);
196    }
197
198    /// Returns true when time in current state >= `timer_preset`.
199    ///
200    /// The timer automatically resets when the state index changes.
201    ///
202    /// # Example
203    ///
204    /// ```
205    /// use autocore_std::fb::StateMachine;
206    /// use std::time::Duration;
207    ///
208    /// let mut state = StateMachine::new();
209    /// state.timer_preset = Duration::from_millis(50);
210    /// state.call(); // Start tracking
211    ///
212    /// assert!(!state.timer_done()); // Not enough time elapsed
213    ///
214    /// std::thread::sleep(Duration::from_millis(60));
215    /// assert!(state.timer_done()); // Now it's done
216    /// ```
217    pub fn timer_done(&self) -> bool {
218        self.elapsed() >= self.timer_preset
219    }
220
221    /// Returns true when time in current state >= `timeout_preset`.
222    ///
223    /// Use this for detecting stuck states. The timeout automatically
224    /// resets when the state index changes.
225    ///
226    /// # Example
227    ///
228    /// ```
229    /// use autocore_std::fb::StateMachine;
230    /// use std::time::Duration;
231    ///
232    /// let mut state = StateMachine::new();
233    /// state.timeout_preset = Duration::from_millis(50);
234    /// state.call();
235    ///
236    /// assert!(!state.timed_out());
237    ///
238    /// std::thread::sleep(Duration::from_millis(60));
239    /// assert!(state.timed_out());
240    /// ```
241    pub fn timed_out(&self) -> bool {
242        self.elapsed() >= self.timeout_preset
243    }
244
245    /// Returns elapsed time since entering the current state.
246    ///
247    /// Returns `Duration::ZERO` if `call()` has never been called.
248    ///
249    /// # Example
250    ///
251    /// ```
252    /// use autocore_std::fb::StateMachine;
253    /// use std::time::Duration;
254    ///
255    /// let mut state = StateMachine::new();
256    /// state.call();
257    ///
258    /// std::thread::sleep(Duration::from_millis(10));
259    /// assert!(state.elapsed() >= Duration::from_millis(10));
260    /// ```
261    pub fn elapsed(&self) -> Duration {
262        self.state_entered_at
263            .map(|t| t.elapsed())
264            .unwrap_or(Duration::ZERO)
265    }
266
267    /// Returns true if `error_code != 0`.
268    ///
269    /// # Example
270    ///
271    /// ```
272    /// use autocore_std::fb::StateMachine;
273    ///
274    /// let mut state = StateMachine::new();
275    /// assert!(!state.is_error());
276    ///
277    /// state.error_code = 100;
278    /// assert!(state.is_error());
279    /// ```
280    pub fn is_error(&self) -> bool {
281        self.error_code != 0
282    }
283
284    /// Set error state with code and message.
285    ///
286    /// This is a convenience method equivalent to setting `error_code`
287    /// and `error_message` directly.
288    ///
289    /// # Example
290    ///
291    /// ```
292    /// use autocore_std::fb::StateMachine;
293    ///
294    /// let mut state = StateMachine::new();
295    /// state.set_error(110, "Failed to home X axis");
296    ///
297    /// assert_eq!(state.error_code, 110);
298    /// assert_eq!(state.error_message, "Failed to home X axis");
299    /// assert!(state.is_error());
300    /// ```
301    pub fn set_error(&mut self, code: i32, message: impl Into<String>) {
302        self.error_code = code;
303        self.error_message = message.into();
304    }
305
306    /// Clear error state.
307    ///
308    /// Sets `error_code` to 0 and clears `error_message`.
309    ///
310    /// # Example
311    ///
312    /// ```
313    /// use autocore_std::fb::StateMachine;
314    ///
315    /// let mut state = StateMachine::new();
316    /// state.set_error(100, "Some error");
317    /// assert!(state.is_error());
318    ///
319    /// state.clear_error();
320    /// assert!(!state.is_error());
321    /// assert_eq!(state.error_code, 0);
322    /// assert!(state.error_message.is_empty());
323    /// ```
324    pub fn clear_error(&mut self) {
325        self.error_code = 0;
326        self.error_message.clear();
327    }
328
329    /// Returns the current state index.
330    ///
331    /// This is equivalent to reading `state.index` directly but provided
332    /// for API consistency.
333    pub fn state(&self) -> i32 {
334        self.index
335    }
336}
337
338impl Default for StateMachine {
339    fn default() -> Self {
340        Self::new()
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_state_machine_basic() {
350        let state = StateMachine::new();
351
352        assert_eq!(state.index, 0);
353        assert_eq!(state.error_code, 0);
354        assert!(!state.is_error());
355        assert_eq!(state.timer_preset, Duration::MAX);
356        assert_eq!(state.timeout_preset, Duration::MAX);
357    }
358
359    #[test]
360    fn test_state_machine_timer() {
361        let mut state = StateMachine::new();
362        state.timer_preset = Duration::from_millis(50);
363        state.call();
364
365        // Timer shouldn't be done yet
366        assert!(!state.timer_done());
367
368        // Wait for timer
369        std::thread::sleep(Duration::from_millis(60));
370        assert!(state.timer_done());
371        assert!(state.elapsed() >= Duration::from_millis(50));
372    }
373
374    #[test]
375    fn test_state_machine_timeout() {
376        let mut state = StateMachine::new();
377        state.timeout_preset = Duration::from_millis(50);
378        state.call();
379
380        assert!(!state.timed_out());
381
382        std::thread::sleep(Duration::from_millis(60));
383        assert!(state.timed_out());
384    }
385
386    #[test]
387    fn test_state_machine_timer_reset_on_state_change() {
388        let mut state = StateMachine::new();
389        state.timer_preset = Duration::from_millis(50);
390        state.call();
391
392        // Wait a bit
393        std::thread::sleep(Duration::from_millis(30));
394        let elapsed_before = state.elapsed();
395        assert!(elapsed_before >= Duration::from_millis(30));
396
397        // Change state
398        state.index = 10;
399        state.call();
400
401        // Timer should have reset
402        assert!(state.elapsed() < Duration::from_millis(20));
403        assert!(!state.timer_done());
404    }
405
406    #[test]
407    fn test_state_machine_error_handling() {
408        let mut state = StateMachine::new();
409
410        assert!(!state.is_error());
411
412        state.set_error(110, "Failed to home axis");
413        assert!(state.is_error());
414        assert_eq!(state.error_code, 110);
415        assert_eq!(state.error_message, "Failed to home axis");
416
417        state.clear_error();
418        assert!(!state.is_error());
419        assert_eq!(state.error_code, 0);
420        assert!(state.error_message.is_empty());
421    }
422
423    #[test]
424    fn test_state_machine_preset_persists() {
425        let mut state = StateMachine::new();
426
427        // Set preset in state 0
428        state.timer_preset = Duration::from_millis(50);
429        state.index = 10;
430        state.call();
431
432        // Preset should still be 50ms
433        assert_eq!(state.timer_preset, Duration::from_millis(50));
434
435        // Change to state 20 without changing preset
436        state.index = 20;
437        state.call();
438
439        // Preset still 50ms
440        assert_eq!(state.timer_preset, Duration::from_millis(50));
441    }
442
443    #[test]
444    fn test_state_machine_default_presets_never_trigger() {
445        let mut state = StateMachine::new();
446        state.call();
447
448        // Default presets are Duration::MAX, so timers should never trigger
449        std::thread::sleep(Duration::from_millis(10));
450        assert!(!state.timer_done());
451        assert!(!state.timed_out());
452    }
453}