Skip to main content

autocore_std/motion/
axis_wait.rs

1//! Common wait-for-axis-done function block.
2//!
3//! Most state machines that command a motion axis end up writing the same
4//! 15-line wait handler over and over: poll `is_busy`, then check `is_error`,
5//! then handle `state.timed_out`, then route to the next state. [`AxisWait`]
6//! collapses that into a single call returning a [`WaitStatus`] the caller
7//! matches on.
8//!
9//! It is stateless — all state lives in the [`AxisHandle`] (the drive) and
10//! the caller's [`StateMachine`] (the timeout). Because it takes any
11//! `&impl AxisHandle`, the same call works for `AxisX` / `AxisY` / `AxisZ`
12//! / `AxisC` or any custom handle.
13//!
14//! # Example
15//!
16//! ```ignore
17//! use autocore_std::motion::axis_wait::{AxisWait, WaitStatus};
18//!
19//! Some(MyState::WaitMoveDone) => {
20//!     match AxisWait::poll(axis_z, &self.state) {
21//!         WaitStatus::Pending => {}
22//!         WaitStatus::Done => {
23//!             self.state.index = MyState::NextStep as i32;
24//!         }
25//!         WaitStatus::Error(msg) => {
26//!             log::error!("Z axis error: {}", msg);
27//!             self.state.index = MyState::Reset as i32;
28//!         }
29//!         WaitStatus::Timeout => {
30//!             log::error!("Timeout waiting for Z motion");
31//!             self.state.index = MyState::Reset as i32;
32//!         }
33//!     }
34//! }
35//! ```
36//!
37//! # Pairing with a `follow_up_state` enum
38//!
39//! State machines that re-use one wait state across many transitions
40//! typically pair this with an `Option<MyState>` field set by the
41//! command-issuing state. The `Done` arm reads + clears it:
42//!
43//! ```ignore
44//! WaitStatus::Done => {
45//!     self.state.index = self.follow_up_state
46//!         .take()
47//!         .map(|s| s as i32)
48//!         .unwrap_or(MyState::Idle as i32);
49//! }
50//! ```
51
52use crate::fb::StateMachine;
53use super::axis_view::AxisHandle;
54
55/// Result of a single [`AxisWait::poll`] call.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum WaitStatus {
58    /// Axis is still executing the command and the timeout has not
59    /// fired. Caller should stay in the wait state.
60    Pending,
61    /// Axis finished cleanly. Caller should advance to its follow-up state.
62    Done,
63    /// Axis reported an error before completion. Payload is the drive's
64    /// human-readable error message (empty if the handle's `error_message`
65    /// override hasn't been wired). Caller should log and recover.
66    Error(String),
67    /// `StateMachine.timed_out()` fired while the axis was still busy.
68    /// Caller should log and recover.
69    Timeout,
70}
71
72/// Wait-for-axis-done function block. Stateless — all state lives in
73/// the drive and the caller's [`StateMachine`].
74pub struct AxisWait;
75
76impl AxisWait {
77    /// Poll the axis exactly once. Call this from inside your wait state.
78    ///
79    /// Decision order:
80    ///
81    /// 1. **`!is_busy && is_error`** → [`WaitStatus::Error`] with the
82    ///    drive's `error_message()`. Checked first because the busy bit
83    ///    typically clears before the error bit on a faulted move.
84    /// 2. **`!is_busy`** → [`WaitStatus::Done`].
85    /// 3. **`state.timed_out()`** → [`WaitStatus::Timeout`].
86    /// 4. Otherwise → [`WaitStatus::Pending`].
87    ///
88    /// `error_message` defaults to `""` if the handle hasn't overridden
89    /// it (see the [`AxisHandle`] trait); regenerated `Axis*` handles
90    /// override it to return the drive's actual message.
91    pub fn poll<A: AxisHandle + ?Sized>(axis: &A, state: &StateMachine) -> WaitStatus {
92        if !axis.is_busy() {
93            if axis.is_error() {
94                WaitStatus::Error(axis.error_message())
95            } else {
96                WaitStatus::Done
97            }
98        } else if state.timed_out() {
99            WaitStatus::Timeout
100        } else {
101            WaitStatus::Pending
102        }
103    }
104
105    /// Convenience wrapper for the common pattern: on `Done`, jump to
106    /// the follow-up state (or `default_next` if no follow-up was set);
107    /// on `Error` or `Timeout`, log and jump to `error_next`. Returns
108    /// the new state index, or `None` if the caller should stay put
109    /// (the `Pending` case).
110    ///
111    /// # Example
112    ///
113    /// ```ignore
114    /// Some(MyState::WaitMoveDone) => {
115    ///     if let Some(next) = AxisWait::resolve(
116    ///         axis_z,
117    ///         &self.state,
118    ///         &mut self.follow_up_state,
119    ///         MyState::Idle as i32,
120    ///         MyState::Reset as i32,
121    ///         "Z axis",
122    ///     ) {
123    ///         self.state.index = next;
124    ///     }
125    /// }
126    /// ```
127    pub fn resolve<A: AxisHandle + ?Sized>(
128        axis:         &A,
129        state:        &StateMachine,
130        follow_up:    &mut Option<i32>,
131        default_next: i32,
132        error_next:   i32,
133        label:        &str,
134    ) -> Option<i32> {
135        match Self::poll(axis, state) {
136            WaitStatus::Pending => None,
137            WaitStatus::Done => {
138                let next = follow_up.take().unwrap_or(default_next);
139                log::info!("{label} command complete → state {next}");
140                Some(next)
141            }
142            WaitStatus::Error(msg) => {
143                log::error!("{label} error: {msg}");
144                Some(error_next)
145            }
146            WaitStatus::Timeout => {
147                log::error!("Timeout waiting for {label}");
148                Some(error_next)
149            }
150        }
151    }
152}
153
154// -------------------------------------------------------------------------
155// Tests
156// -------------------------------------------------------------------------
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::motion::axis_config::AxisConfig;
162
163    /// Mock implementation of AxisHandle for unit tests.
164    struct MockAxis {
165        busy:    bool,
166        error:   bool,
167        message: String,
168        config:  AxisConfig,
169    }
170
171    impl Default for MockAxis {
172        fn default() -> Self {
173            Self {
174                busy:    false,
175                error:   false,
176                message: String::new(),
177                config:  AxisConfig::new(1000),
178            }
179        }
180    }
181
182    impl AxisHandle for MockAxis {
183        fn position(&self) -> f64 { 0.0 }
184        fn config(&self) -> &AxisConfig { &self.config }
185        fn move_relative(&mut self, _: f64, _: f64, _: f64, _: f64) {}
186        fn move_absolute(&mut self, _: f64, _: f64, _: f64, _: f64) {}
187        fn halt(&mut self) {}
188        fn is_busy(&self) -> bool { self.busy }
189        fn is_error(&self) -> bool { self.error }
190        fn error_message(&self) -> String { self.message.clone() }
191        fn motor_on(&self) -> bool { true }
192    }
193
194    #[test]
195    fn poll_pending_when_busy_and_not_timed_out() {
196        let axis = MockAxis { busy: true, ..Default::default() };
197        let state = StateMachine::new();
198        assert_eq!(AxisWait::poll(&axis, &state), WaitStatus::Pending);
199    }
200
201    #[test]
202    fn poll_done_when_idle_and_no_error() {
203        let axis = MockAxis { busy: false, error: false, ..Default::default() };
204        let state = StateMachine::new();
205        assert_eq!(AxisWait::poll(&axis, &state), WaitStatus::Done);
206    }
207
208    #[test]
209    fn poll_error_takes_priority_over_done_when_idle() {
210        let axis = MockAxis {
211            busy: false, error: true,
212            message: "fault: overcurrent".into(),
213            ..Default::default()
214        };
215        let state = StateMachine::new();
216        assert_eq!(
217            AxisWait::poll(&axis, &state),
218            WaitStatus::Error("fault: overcurrent".to_string()),
219        );
220    }
221
222    #[test]
223    fn poll_timeout_only_when_still_busy() {
224        // Build a state that is timed out.
225        let mut state = StateMachine::new();
226        state.timeout_preset = std::time::Duration::from_millis(0);
227        // timed_out() compares against an internal timer — sleeping a
228        // millisecond is enough to ensure the elapsed > preset.
229        std::thread::sleep(std::time::Duration::from_millis(2));
230
231        let busy_axis = MockAxis { busy: true, ..Default::default() };
232        assert_eq!(AxisWait::poll(&busy_axis, &state), WaitStatus::Timeout);
233
234        // Same state, but not busy → Done wins over Timeout.
235        let idle_axis = MockAxis { busy: false, ..Default::default() };
236        assert_eq!(AxisWait::poll(&idle_axis, &state), WaitStatus::Done);
237    }
238
239    #[test]
240    fn resolve_returns_none_when_pending() {
241        let axis = MockAxis { busy: true, ..Default::default() };
242        let state = StateMachine::new();
243        let mut follow_up: Option<i32> = Some(42);
244        let next = AxisWait::resolve(&axis, &state, &mut follow_up, 0, 99, "test");
245        assert_eq!(next, None);
246        assert_eq!(follow_up, Some(42), "follow_up must not be consumed on Pending");
247    }
248
249    #[test]
250    fn resolve_consumes_follow_up_on_done() {
251        let axis = MockAxis { busy: false, ..Default::default() };
252        let state = StateMachine::new();
253        let mut follow_up: Option<i32> = Some(42);
254        let next = AxisWait::resolve(&axis, &state, &mut follow_up, 0, 99, "test");
255        assert_eq!(next, Some(42));
256        assert_eq!(follow_up, None);
257    }
258
259    #[test]
260    fn resolve_uses_default_when_no_follow_up_on_done() {
261        let axis = MockAxis { busy: false, ..Default::default() };
262        let state = StateMachine::new();
263        let mut follow_up: Option<i32> = None;
264        let next = AxisWait::resolve(&axis, &state, &mut follow_up, 7, 99, "test");
265        assert_eq!(next, Some(7));
266    }
267
268    #[test]
269    fn resolve_routes_to_error_next_on_error() {
270        let axis = MockAxis {
271            busy: false, error: true,
272            message: "x".into(), ..Default::default()
273        };
274        let state = StateMachine::new();
275        let mut follow_up: Option<i32> = Some(42);
276        let next = AxisWait::resolve(&axis, &state, &mut follow_up, 0, 99, "test");
277        assert_eq!(next, Some(99));
278        assert_eq!(follow_up, Some(42), "follow_up retained on Error so caller can re-attempt");
279    }
280}