stepper_motion/config/
limits.rs

1//! Soft limit configuration and types.
2
3use serde::Deserialize;
4
5use super::units::Degrees;
6
7/// Policy for handling limit violations.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum LimitPolicy {
11    /// Reject moves that would exceed limits.
12    #[default]
13    Reject,
14    /// Clamp target to nearest limit.
15    Clamp,
16}
17
18/// Soft limits in degrees (from configuration).
19#[derive(Debug, Clone, Deserialize)]
20pub struct SoftLimits {
21    /// Minimum allowed position in degrees.
22    #[serde(rename = "min_degrees")]
23    pub min: Degrees,
24
25    /// Maximum allowed position in degrees.
26    #[serde(rename = "max_degrees")]
27    pub max: Degrees,
28
29    /// What to do when limit is exceeded.
30    #[serde(default)]
31    pub policy: LimitPolicy,
32}
33
34impl SoftLimits {
35    /// Create new soft limits.
36    pub fn new(min: Degrees, max: Degrees, policy: LimitPolicy) -> Self {
37        Self { min, max, policy }
38    }
39
40    /// Check if limits are valid (min < max).
41    pub fn is_valid(&self) -> bool {
42        self.min.0 < self.max.0
43    }
44
45    /// Check if a position is within limits.
46    pub fn contains(&self, position: Degrees) -> bool {
47        position.0 >= self.min.0 && position.0 <= self.max.0
48    }
49
50    /// Apply limit policy to a target position.
51    ///
52    /// Returns `Some(position)` if valid or clamped, `None` if rejected.
53    pub fn apply(&self, target: Degrees) -> Option<Degrees> {
54        if self.contains(target) {
55            Some(target)
56        } else {
57            match self.policy {
58                LimitPolicy::Reject => None,
59                LimitPolicy::Clamp => {
60                    if target.0 < self.min.0 {
61                        Some(self.min)
62                    } else {
63                        Some(self.max)
64                    }
65                }
66            }
67        }
68    }
69}
70
71/// Soft limits converted to steps (for runtime use).
72#[derive(Debug, Clone)]
73pub struct StepLimits {
74    /// Minimum position in steps.
75    pub min_steps: i64,
76    /// Maximum position in steps.
77    pub max_steps: i64,
78    /// Limit policy.
79    pub policy: LimitPolicy,
80}
81
82impl StepLimits {
83    /// Create step limits from soft limits and steps per degree.
84    pub fn from_soft_limits(soft: &SoftLimits, steps_per_degree: f32) -> Self {
85        Self {
86            min_steps: (soft.min.0 * steps_per_degree) as i64,
87            max_steps: (soft.max.0 * steps_per_degree) as i64,
88            policy: soft.policy,
89        }
90    }
91
92    /// Check if a position is within limits.
93    pub fn contains(&self, steps: i64) -> bool {
94        steps >= self.min_steps && steps <= self.max_steps
95    }
96
97    /// Apply limit policy to a target position.
98    ///
99    /// Returns `Some(steps)` if valid or clamped, `None` if rejected.
100    pub fn apply(&self, target: i64) -> Option<i64> {
101        if self.contains(target) {
102            Some(target)
103        } else {
104            match self.policy {
105                LimitPolicy::Reject => None,
106                LimitPolicy::Clamp => {
107                    if target < self.min_steps {
108                        Some(self.min_steps)
109                    } else {
110                        Some(self.max_steps)
111                    }
112                }
113            }
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_soft_limits_reject() {
124        let limits = SoftLimits::new(Degrees(-180.0), Degrees(180.0), LimitPolicy::Reject);
125
126        assert!(limits.apply(Degrees(0.0)).is_some());
127        assert!(limits.apply(Degrees(180.0)).is_some());
128        assert!(limits.apply(Degrees(-180.0)).is_some());
129        assert!(limits.apply(Degrees(181.0)).is_none());
130        assert!(limits.apply(Degrees(-181.0)).is_none());
131    }
132
133    #[test]
134    fn test_soft_limits_clamp() {
135        let limits = SoftLimits::new(Degrees(-180.0), Degrees(180.0), LimitPolicy::Clamp);
136
137        assert_eq!(limits.apply(Degrees(0.0)).unwrap().0, 0.0);
138        assert_eq!(limits.apply(Degrees(360.0)).unwrap().0, 180.0);
139        assert_eq!(limits.apply(Degrees(-360.0)).unwrap().0, -180.0);
140    }
141}