stepper_motion/config/
trajectory.rs1use heapless::{String, Vec};
4use serde::Deserialize;
5
6use super::mechanical::MechanicalConstraints;
7use super::units::{Degrees, DegreesPerSecSquared};
8
9#[derive(Debug, Clone, Deserialize)]
11pub struct TrajectoryConfig {
12 pub motor: String<32>,
14
15 pub target_degrees: Degrees,
17
18 #[serde(default = "default_velocity_percent")]
20 pub velocity_percent: u8,
21
22 #[serde(default = "default_acceleration_percent")]
25 pub acceleration_percent: u8,
26
27 #[serde(default, rename = "acceleration_deg_per_sec2")]
30 pub acceleration: Option<DegreesPerSecSquared>,
31
32 #[serde(default, rename = "deceleration_deg_per_sec2")]
35 pub deceleration: Option<DegreesPerSecSquared>,
36
37 #[serde(default)]
39 pub dwell_ms: Option<u32>,
40}
41
42fn default_velocity_percent() -> u8 {
43 100
44}
45
46fn default_acceleration_percent() -> u8 {
47 100
48}
49
50impl TrajectoryConfig {
51 pub fn effective_acceleration(&self, constraints: &MechanicalConstraints) -> f32 {
53 self.acceleration.map(|a| a.0).unwrap_or_else(|| {
54 constraints.max_acceleration.0 * (self.acceleration_percent as f32 / 100.0)
55 })
56 }
57
58 pub fn effective_deceleration(&self, constraints: &MechanicalConstraints) -> f32 {
61 self.deceleration
62 .map(|d| d.0)
63 .or_else(|| self.acceleration.map(|a| a.0))
64 .unwrap_or_else(|| {
65 constraints.max_acceleration.0 * (self.acceleration_percent as f32 / 100.0)
66 })
67 }
68
69 pub fn effective_velocity(&self, constraints: &MechanicalConstraints) -> f32 {
71 constraints.max_velocity.0 * (self.velocity_percent as f32 / 100.0)
72 }
73
74 pub fn is_asymmetric(&self) -> bool {
76 self.deceleration.is_some()
77 && self.acceleration.is_some()
78 && self.acceleration != self.deceleration
79 }
80
81 pub fn check_feasibility(
93 &self,
94 constraints: &MechanicalConstraints,
95 ) -> crate::error::Result<()> {
96 use crate::error::{Error, MotionError};
97
98 if self.velocity_percent == 0 || self.velocity_percent > 200 {
100 return Err(Error::Config(crate::error::ConfigError::InvalidVelocityPercent(
101 self.velocity_percent,
102 )));
103 }
104
105 if self.acceleration_percent == 0 || self.acceleration_percent > 200 {
107 return Err(Error::Config(crate::error::ConfigError::InvalidAccelerationPercent(
108 self.acceleration_percent,
109 )));
110 }
111
112 if let Some(ref limits) = constraints.limits {
114 let target_steps = constraints.degrees_to_steps(self.target_degrees.0);
115 if limits.apply(target_steps).is_none() {
116 return Err(Error::Trajectory(crate::error::TrajectoryError::TargetExceedsLimits {
117 target: self.target_degrees.0,
118 min: constraints.limits.as_ref().map(|l| l.min_steps as f32 / constraints.steps_per_degree).unwrap_or(f32::MIN),
119 max: constraints.limits.as_ref().map(|l| l.max_steps as f32 / constraints.steps_per_degree).unwrap_or(f32::MAX),
120 }));
121 }
122 }
123
124 let effective_velocity = self.effective_velocity(constraints);
126 if effective_velocity > constraints.max_velocity.0 * 2.0 {
127 return Err(Error::Motion(MotionError::VelocityExceedsLimit {
128 requested: effective_velocity,
129 max: constraints.max_velocity.0,
130 }));
131 }
132
133 let effective_accel = self.effective_acceleration(constraints);
135 if effective_accel > constraints.max_acceleration.0 * 2.0 {
136 return Err(Error::Motion(MotionError::AccelerationExceedsLimit {
137 requested: effective_accel,
138 max: constraints.max_acceleration.0,
139 }));
140 }
141
142 let effective_decel = self.effective_deceleration(constraints);
143 if effective_decel > constraints.max_acceleration.0 * 2.0 {
144 return Err(Error::Motion(MotionError::AccelerationExceedsLimit {
145 requested: effective_decel,
146 max: constraints.max_acceleration.0,
147 }));
148 }
149
150 Ok(())
151 }
152}
153
154#[derive(Debug, Clone, Deserialize)]
156pub struct WaypointTrajectory {
157 pub motor: String<32>,
159
160 pub waypoints: Vec<Degrees, 32>,
162
163 #[serde(default)]
165 pub dwell_ms: u32,
166
167 #[serde(default = "default_velocity_percent")]
169 pub velocity_percent: u8,
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::config::units::{DegreesPerSec, Microsteps};
176 use crate::config::MotorConfig;
177
178 fn make_test_constraints() -> MechanicalConstraints {
179 let config = MotorConfig {
180 name: String::try_from("test").unwrap(),
181 steps_per_revolution: 200,
182 microsteps: Microsteps::SIXTEENTH,
183 gear_ratio: 1.0,
184 max_velocity: DegreesPerSec(360.0),
185 max_acceleration: DegreesPerSecSquared(720.0),
186 invert_direction: false,
187 limits: None,
188 backlash_compensation: None,
189 };
190 MechanicalConstraints::from_config(&config)
191 }
192
193 #[test]
194 fn test_symmetric_profile() {
195 let traj = TrajectoryConfig {
196 motor: String::try_from("test").unwrap(),
197 target_degrees: Degrees(90.0),
198 velocity_percent: 100,
199 acceleration_percent: 50,
200 acceleration: None,
201 deceleration: None,
202 dwell_ms: None,
203 };
204
205 let constraints = make_test_constraints();
206 let accel = traj.effective_acceleration(&constraints);
207 let decel = traj.effective_deceleration(&constraints);
208
209 assert!((accel - 360.0).abs() < 0.1); assert!((decel - 360.0).abs() < 0.1);
211 assert!(!traj.is_asymmetric());
212 }
213
214 #[test]
215 fn test_asymmetric_profile() {
216 let traj = TrajectoryConfig {
217 motor: String::try_from("test").unwrap(),
218 target_degrees: Degrees(90.0),
219 velocity_percent: 100,
220 acceleration_percent: 100,
221 acceleration: Some(DegreesPerSecSquared(500.0)),
222 deceleration: Some(DegreesPerSecSquared(200.0)),
223 dwell_ms: None,
224 };
225
226 let constraints = make_test_constraints();
227 let accel = traj.effective_acceleration(&constraints);
228 let decel = traj.effective_deceleration(&constraints);
229
230 assert!((accel - 500.0).abs() < 0.1);
231 assert!((decel - 200.0).abs() < 0.1);
232 assert!(traj.is_asymmetric());
233 }
234}