stepper_motion/motion/
executor.rs

1//! Motion execution - step pulse generation.
2
3use super::profile::{MotionPhase, MotionProfile};
4
5/// Runtime state during motion execution.
6#[derive(Debug, Clone)]
7pub struct MotionExecutor {
8    /// The computed profile being executed.
9    profile: MotionProfile,
10
11    /// Current step number (0 to total_steps - 1).
12    current_step: u32,
13
14    /// Current step interval in nanoseconds.
15    current_interval_ns: u32,
16
17    /// Current phase of motion.
18    phase: MotionPhase,
19}
20
21impl MotionExecutor {
22    /// Create a new executor for a motion profile.
23    pub fn new(profile: MotionProfile) -> Self {
24        let phase = if profile.is_zero() {
25            MotionPhase::Complete
26        } else {
27            MotionPhase::Accelerating
28        };
29
30        let interval = if profile.is_zero() {
31            u32::MAX
32        } else {
33            profile.initial_interval_ns
34        };
35
36        Self {
37            profile,
38            current_step: 0,
39            current_interval_ns: interval,
40            phase,
41        }
42    }
43
44    /// Check if motion is complete.
45    #[inline]
46    pub fn is_complete(&self) -> bool {
47        self.phase == MotionPhase::Complete
48    }
49
50    /// Get the current step number.
51    #[inline]
52    pub fn current_step(&self) -> u32 {
53        self.current_step
54    }
55
56    /// Get the total number of steps.
57    #[inline]
58    pub fn total_steps(&self) -> u32 {
59        self.profile.total_steps
60    }
61
62    /// Get steps remaining.
63    #[inline]
64    pub fn steps_remaining(&self) -> u32 {
65        self.profile.total_steps.saturating_sub(self.current_step)
66    }
67
68    /// Get the current phase.
69    #[inline]
70    pub fn phase(&self) -> MotionPhase {
71        self.phase
72    }
73
74    /// Get the current step interval in nanoseconds.
75    #[inline]
76    pub fn current_interval_ns(&self) -> u32 {
77        self.current_interval_ns
78    }
79
80    /// Get the motion profile.
81    #[inline]
82    pub fn profile(&self) -> &MotionProfile {
83        &self.profile
84    }
85
86    /// Advance to the next step.
87    ///
88    /// Returns `true` if a step should be executed, `false` if complete.
89    pub fn advance(&mut self) -> bool {
90        if self.is_complete() {
91            return false;
92        }
93
94        self.current_step += 1;
95
96        if self.current_step >= self.profile.total_steps {
97            self.phase = MotionPhase::Complete;
98            self.current_interval_ns = u32::MAX;
99            return false;
100        }
101
102        // Update phase and interval
103        self.phase = self.profile.phase_at(self.current_step);
104        self.current_interval_ns = self.profile.interval_at(self.current_step);
105
106        true
107    }
108
109    /// Reset the executor to the beginning.
110    pub fn reset(&mut self) {
111        self.current_step = 0;
112        self.phase = if self.profile.is_zero() {
113            MotionPhase::Complete
114        } else {
115            MotionPhase::Accelerating
116        };
117        self.current_interval_ns = if self.profile.is_zero() {
118            u32::MAX
119        } else {
120            self.profile.initial_interval_ns
121        };
122    }
123
124    /// Get progress as a percentage (0.0 to 1.0).
125    #[inline]
126    pub fn progress(&self) -> f32 {
127        if self.profile.total_steps == 0 {
128            1.0
129        } else {
130            self.current_step as f32 / self.profile.total_steps as f32
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_executor_complete() {
141        let profile = MotionProfile::symmetric_trapezoidal(10, 1000.0, 2000.0);
142        let mut executor = MotionExecutor::new(profile);
143
144        assert!(!executor.is_complete());
145        assert_eq!(executor.current_step(), 0);
146
147        // Advance through all steps
148        while executor.advance() {}
149
150        assert!(executor.is_complete());
151        assert_eq!(executor.current_step(), 10);
152    }
153
154    #[test]
155    fn test_zero_profile() {
156        let profile = MotionProfile::zero();
157        let executor = MotionExecutor::new(profile);
158
159        assert!(executor.is_complete());
160        assert_eq!(executor.steps_remaining(), 0);
161    }
162
163    #[test]
164    fn test_phase_transitions() {
165        let profile = MotionProfile::symmetric_trapezoidal(100, 1000.0, 2000.0);
166        let mut executor = MotionExecutor::new(profile);
167
168        let mut saw_accel = false;
169        let mut saw_decel = false;
170
171        while !executor.is_complete() {
172            match executor.phase() {
173                MotionPhase::Accelerating => saw_accel = true,
174                MotionPhase::Cruising => {} // May or may not be present
175                MotionPhase::Decelerating => saw_decel = true,
176                MotionPhase::Complete => {}
177            }
178            executor.advance();
179        }
180
181        assert!(saw_accel);
182        assert!(saw_decel);
183    }
184}