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}