stepper_motion/config/
trajectory.rs

1//! Trajectory configuration from TOML.
2
3use heapless::{String, Vec};
4use serde::Deserialize;
5
6use super::mechanical::MechanicalConstraints;
7use super::units::{Degrees, DegreesPerSecSquared};
8
9/// A named trajectory from configuration.
10#[derive(Debug, Clone, Deserialize)]
11pub struct TrajectoryConfig {
12    /// Target motor name (must match a motor in config).
13    pub motor: String<32>,
14
15    /// Target position in degrees (absolute from origin).
16    pub target_degrees: Degrees,
17
18    /// Velocity as percentage of motor's max (1-200).
19    #[serde(default = "default_velocity_percent")]
20    pub velocity_percent: u8,
21
22    /// Acceleration as percentage of motor's max (1-200).
23    /// Used when absolute rates are not specified.
24    #[serde(default = "default_acceleration_percent")]
25    pub acceleration_percent: u8,
26
27    /// Absolute acceleration rate in degrees/sec² (optional).
28    /// Overrides acceleration_percent for the acceleration phase.
29    #[serde(default, rename = "acceleration_deg_per_sec2")]
30    pub acceleration: Option<DegreesPerSecSquared>,
31
32    /// Absolute deceleration rate in degrees/sec² (optional).
33    /// If not set, uses acceleration value (symmetric profile).
34    #[serde(default, rename = "deceleration_deg_per_sec2")]
35    pub deceleration: Option<DegreesPerSecSquared>,
36
37    /// Optional dwell time at target (milliseconds).
38    #[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    /// Get effective acceleration rate for this trajectory.
52    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    /// Get effective deceleration rate for this trajectory.
59    /// Falls back to acceleration if not specified (symmetric profile).
60    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    /// Get effective velocity for this trajectory.
70    pub fn effective_velocity(&self, constraints: &MechanicalConstraints) -> f32 {
71        constraints.max_velocity.0 * (self.velocity_percent as f32 / 100.0)
72    }
73
74    /// Check if this trajectory uses asymmetric acceleration.
75    pub fn is_asymmetric(&self) -> bool {
76        self.deceleration.is_some()
77            && self.acceleration.is_some()
78            && self.acceleration != self.deceleration
79    }
80
81    /// Check if this trajectory is feasible given the motor constraints.
82    ///
83    /// Returns `Ok(())` if the trajectory can be executed, or an error describing
84    /// why it cannot.
85    ///
86    /// # Checks performed:
87    /// - Velocity percent is valid (1-200)
88    /// - Acceleration percent is valid (1-200)
89    /// - Target position is within soft limits (if configured)
90    /// - Effective velocity doesn't exceed motor max
91    /// - Effective acceleration doesn't exceed motor max
92    pub fn check_feasibility(
93        &self,
94        constraints: &MechanicalConstraints,
95    ) -> crate::error::Result<()> {
96        use crate::error::{Error, MotionError};
97
98        // Check velocity percent
99        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        // Check acceleration percent
106        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        // Check if target is within limits
113        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        // Check effective velocity against max
125        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        // Check effective acceleration against max
134        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/// Trajectory with multiple waypoints.
155#[derive(Debug, Clone, Deserialize)]
156pub struct WaypointTrajectory {
157    /// Target motor name.
158    pub motor: String<32>,
159
160    /// Ordered list of waypoint positions in degrees (max 32).
161    pub waypoints: Vec<Degrees, 32>,
162
163    /// Dwell time at each waypoint (milliseconds).
164    #[serde(default)]
165    pub dwell_ms: u32,
166
167    /// Velocity percent for all moves.
168    #[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); // 720 * 50% = 360
210        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}