stepper_motion/motion/
profile.rs1use libm::sqrtf;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Direction {
11 Clockwise,
13 CounterClockwise,
15}
16
17impl Direction {
18 #[inline]
20 pub fn from_steps(steps: i64) -> Self {
21 if steps >= 0 {
22 Direction::Clockwise
23 } else {
24 Direction::CounterClockwise
25 }
26 }
27
28 #[inline]
30 pub fn sign(self) -> i64 {
31 match self {
32 Direction::Clockwise => 1,
33 Direction::CounterClockwise => -1,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum MotionPhase {
41 Accelerating,
43 Cruising,
45 Decelerating,
47 Complete,
49}
50
51#[derive(Debug, Clone)]
53pub struct MotionProfile {
54 pub total_steps: u32,
56
57 pub direction: Direction,
59
60 pub accel_steps: u32,
62
63 pub cruise_steps: u32,
65
66 pub decel_steps: u32,
68
69 pub initial_interval_ns: u32,
71
72 pub cruise_interval_ns: u32,
74
75 pub accel_rate: f32,
77
78 pub decel_rate: f32,
80}
81
82impl MotionProfile {
83 pub fn asymmetric_trapezoidal(
92 total_steps: i64,
93 max_velocity: f32,
94 acceleration: f32,
95 deceleration: f32,
96 ) -> Self {
97 let direction = Direction::from_steps(total_steps);
98 let steps = total_steps.unsigned_abs() as u32;
99
100 if steps == 0 || max_velocity <= 0.0 || acceleration <= 0.0 || deceleration <= 0.0 {
101 return Self::zero();
102 }
103
104 let t_accel = max_velocity / acceleration;
108 let t_decel = max_velocity / deceleration;
109
110 let accel_distance = 0.5 * acceleration * t_accel * t_accel;
111 let decel_distance = 0.5 * deceleration * t_decel * t_decel;
112
113 let (accel_steps, cruise_steps, decel_steps) =
114 if accel_distance + decel_distance >= steps as f32 {
115 let ratio = acceleration / (acceleration + deceleration);
118 let accel_steps = (steps as f32 * ratio) as u32;
119 let decel_steps = steps.saturating_sub(accel_steps);
120 (accel_steps, 0u32, decel_steps)
121 } else {
122 let accel_steps = accel_distance as u32;
124 let decel_steps = decel_distance as u32;
125 let cruise_steps = steps.saturating_sub(accel_steps + decel_steps);
126 (accel_steps, cruise_steps, decel_steps)
127 };
128
129 let initial_velocity = sqrtf(2.0 * acceleration);
133 let initial_interval_ns = (1_000_000_000.0 / initial_velocity) as u32;
134 let cruise_interval_ns = (1_000_000_000.0 / max_velocity) as u32;
135
136 Self {
137 total_steps: steps,
138 direction,
139 accel_steps,
140 cruise_steps,
141 decel_steps,
142 initial_interval_ns,
143 cruise_interval_ns,
144 accel_rate: acceleration,
145 decel_rate: deceleration,
146 }
147 }
148
149 pub fn symmetric_trapezoidal(
151 total_steps: i64,
152 max_velocity: f32,
153 acceleration: f32,
154 ) -> Self {
155 Self::asymmetric_trapezoidal(total_steps, max_velocity, acceleration, acceleration)
156 }
157
158 pub fn zero() -> Self {
160 Self {
161 total_steps: 0,
162 direction: Direction::Clockwise,
163 accel_steps: 0,
164 cruise_steps: 0,
165 decel_steps: 0,
166 initial_interval_ns: u32::MAX,
167 cruise_interval_ns: u32::MAX,
168 accel_rate: 0.0,
169 decel_rate: 0.0,
170 }
171 }
172
173 #[inline]
175 pub fn is_zero(&self) -> bool {
176 self.total_steps == 0
177 }
178
179 pub fn phase_at(&self, step: u32) -> MotionPhase {
181 if step >= self.total_steps {
182 MotionPhase::Complete
183 } else if step < self.accel_steps {
184 MotionPhase::Accelerating
185 } else if step < self.accel_steps + self.cruise_steps {
186 MotionPhase::Cruising
187 } else {
188 MotionPhase::Decelerating
189 }
190 }
191
192 pub fn interval_at(&self, step: u32) -> u32 {
196 let phase = self.phase_at(step);
197
198 match phase {
199 MotionPhase::Complete => u32::MAX,
200 MotionPhase::Cruising => self.cruise_interval_ns,
201 MotionPhase::Accelerating => {
202 let progress = step as f32 / self.accel_steps.max(1) as f32;
206 let interval = self.initial_interval_ns as f32
207 - (self.initial_interval_ns as f32 - self.cruise_interval_ns as f32) * progress;
208 interval as u32
209 }
210 MotionPhase::Decelerating => {
211 let decel_step = step - self.accel_steps - self.cruise_steps;
213 let progress = decel_step as f32 / self.decel_steps.max(1) as f32;
214 let interval = self.cruise_interval_ns as f32
215 + (self.initial_interval_ns as f32 - self.cruise_interval_ns as f32) * progress;
216 interval as u32
217 }
218 }
219 }
220
221 pub fn estimated_duration_secs(&self) -> f32 {
225 if self.total_steps == 0 {
226 return 0.0;
227 }
228
229 let cruise_velocity = 1_000_000_000.0 / self.cruise_interval_ns as f32;
230
231 let accel_time = if self.accel_rate > 0.0 {
234 cruise_velocity / self.accel_rate
235 } else {
236 0.0
237 };
238
239 let cruise_time = self.cruise_steps as f32 / cruise_velocity;
241
242 let decel_time = if self.decel_rate > 0.0 {
244 cruise_velocity / self.decel_rate
245 } else {
246 0.0
247 };
248
249 accel_time + cruise_time + decel_time
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_symmetric_profile() {
259 let profile = MotionProfile::symmetric_trapezoidal(
260 1000, 1000.0, 2000.0, );
264
265 assert_eq!(profile.total_steps, 1000);
266 assert_eq!(profile.direction, Direction::Clockwise);
267 assert!(profile.accel_steps > 0);
268 assert!(profile.cruise_steps > 0);
269 assert_eq!(profile.accel_steps, profile.decel_steps);
270 }
271
272 #[test]
273 fn test_asymmetric_profile() {
274 let profile = MotionProfile::asymmetric_trapezoidal(
275 1000, 1000.0, 2000.0, 1000.0, );
280
281 assert!(profile.decel_steps > profile.accel_steps);
282 }
283
284 #[test]
285 fn test_triangle_profile() {
286 let profile = MotionProfile::symmetric_trapezoidal(
288 100, 10000.0, 1000.0, );
292
293 assert_eq!(profile.cruise_steps, 0);
295 }
296
297 #[test]
298 fn test_direction() {
299 let cw = MotionProfile::symmetric_trapezoidal(100, 1000.0, 2000.0);
300 let ccw = MotionProfile::symmetric_trapezoidal(-100, 1000.0, 2000.0);
301
302 assert_eq!(cw.direction, Direction::Clockwise);
303 assert_eq!(ccw.direction, Direction::CounterClockwise);
304 assert_eq!(cw.total_steps, ccw.total_steps);
305 }
306}