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}