stepper_motion/config/
validation.rs

1//! Configuration validation.
2
3use crate::error::{ConfigError, Error, Result, TrajectoryError};
4
5use super::SystemConfig;
6
7/// Validate a system configuration.
8///
9/// Checks:
10/// - Motor configurations are valid
11/// - Trajectory references existing motors
12/// - Velocity/acceleration percentages are in range
13/// - Soft limits are valid (min < max)
14pub fn validate_config(config: &SystemConfig) -> Result<()> {
15    // Validate motors
16    for (name, motor) in config.motors.iter() {
17        validate_motor(name.as_str(), motor)?;
18    }
19
20    // Validate trajectories
21    for (name, traj) in config.trajectories.iter() {
22        validate_trajectory(name.as_str(), traj, config)?;
23    }
24
25    // Validate sequences
26    for (name, seq) in config.sequences.iter() {
27        validate_sequence(name.as_str(), seq, config)?;
28    }
29
30    Ok(())
31}
32
33fn validate_motor(_name: &str, config: &super::MotorConfig) -> Result<()> {
34    // Gear ratio must be positive
35    if config.gear_ratio <= 0.0 {
36        return Err(Error::Config(ConfigError::InvalidGearRatio(config.gear_ratio)));
37    }
38
39    // Max velocity must be positive
40    if config.max_velocity.0 <= 0.0 {
41        return Err(Error::Config(ConfigError::InvalidMaxVelocity(
42            config.max_velocity.0,
43        )));
44    }
45
46    // Max acceleration must be positive
47    if config.max_acceleration.0 <= 0.0 {
48        return Err(Error::Config(ConfigError::InvalidMaxAcceleration(
49            config.max_acceleration.0,
50        )));
51    }
52
53    // Soft limits: min must be < max
54    if let Some(ref limits) = config.limits {
55        if !limits.is_valid() {
56            return Err(Error::Config(ConfigError::InvalidSoftLimits {
57                min: limits.min.0,
58                max: limits.max.0,
59            }));
60        }
61    }
62
63    Ok(())
64}
65
66fn validate_trajectory(
67    name: &str,
68    traj: &super::TrajectoryConfig,
69    config: &SystemConfig,
70) -> Result<()> {
71    // Motor must exist
72    if config.motor(traj.motor.as_str()).is_none() {
73        return Err(Error::Trajectory(TrajectoryError::MotorNotFound {
74            trajectory: heapless::String::try_from(name).unwrap_or_default(),
75            motor: traj.motor.clone(),
76        }));
77    }
78
79    // Velocity percent must be 1-200
80    if traj.velocity_percent == 0 || traj.velocity_percent > 200 {
81        return Err(Error::Config(ConfigError::InvalidVelocityPercent(
82            traj.velocity_percent,
83        )));
84    }
85
86    // Acceleration percent must be 1-200
87    if traj.acceleration_percent == 0 || traj.acceleration_percent > 200 {
88        return Err(Error::Config(ConfigError::InvalidAccelerationPercent(
89            traj.acceleration_percent,
90        )));
91    }
92
93    // Check target against limits if motor has them
94    if let Some(motor) = config.motor(traj.motor.as_str()) {
95        if let Some(ref limits) = motor.limits {
96            if !limits.contains(traj.target_degrees) {
97                // Note: This is a warning, not an error, if policy is Clamp
98                // For now, we only error on Reject policy
99                if limits.policy == super::LimitPolicy::Reject {
100                    return Err(Error::Trajectory(TrajectoryError::TargetExceedsLimits {
101                        target: traj.target_degrees.0,
102                        min: limits.min.0,
103                        max: limits.max.0,
104                    }));
105                }
106            }
107        }
108    }
109
110    Ok(())
111}
112
113fn validate_sequence(
114    name: &str,
115    seq: &super::WaypointTrajectory,
116    config: &SystemConfig,
117) -> Result<()> {
118    // Motor must exist
119    if config.motor(seq.motor.as_str()).is_none() {
120        return Err(Error::Trajectory(TrajectoryError::MotorNotFound {
121            trajectory: heapless::String::try_from(name).unwrap_or_default(),
122            motor: seq.motor.clone(),
123        }));
124    }
125
126    // Must have at least one waypoint
127    if seq.waypoints.is_empty() {
128        return Err(Error::Trajectory(TrajectoryError::EmptyWaypoints));
129    }
130
131    // Velocity percent must be 1-200
132    if seq.velocity_percent == 0 || seq.velocity_percent > 200 {
133        return Err(Error::Config(ConfigError::InvalidVelocityPercent(
134            seq.velocity_percent,
135        )));
136    }
137
138    Ok(())
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_invalid_gear_ratio() {
147        use crate::config::units::{DegreesPerSec, DegreesPerSecSquared, Microsteps};
148        use crate::config::MotorConfig;
149
150        let config = MotorConfig {
151            name: heapless::String::try_from("test").unwrap(),
152            steps_per_revolution: 200,
153            microsteps: Microsteps::SIXTEENTH,
154            gear_ratio: -1.0, // Invalid!
155            max_velocity: DegreesPerSec(360.0),
156            max_acceleration: DegreesPerSecSquared(720.0),
157            invert_direction: false,
158            limits: None,
159            backlash_compensation: None,
160        };
161
162        let result = validate_motor("test", &config);
163        assert!(matches!(
164            result,
165            Err(Error::Config(ConfigError::InvalidGearRatio(_)))
166        ));
167    }
168}