Skip to main content

autocore_std/motion/
axis_config.rs

1//! Axis configuration: unit conversion, jog defaults, limits.
2//!
3//! [`AxisConfig`] stores the encoder resolution and user-unit scaling,
4//! plus jog parameters, home position, and software position limits.
5//! It is used by [`Axis`](super::Axis) for all unit conversions.
6
7/// Configuration for a single motion axis.
8///
9/// Stores encoder resolution, user-unit scaling, jog defaults,
10/// and position limits. Used by [`Axis`](super::Axis) internally
11/// and also available for direct use (e.g. logging, HMI display).
12///
13/// # Examples
14///
15/// ```
16/// use autocore_std::motion::AxisConfig;
17///
18/// // ClearPath with 12,800 counts/rev, user units = degrees
19/// let cfg = AxisConfig::new(12_800).with_user_scale(360.0);
20/// assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
21/// assert!((cfg.to_user(1600.0) - 45.0).abs() < 0.01);
22///
23/// // mm on a 5 mm/rev ballscrew
24/// let mm = AxisConfig::new(12_800).with_user_scale(5.0);
25/// assert!((mm.to_counts(5.0) - 12800.0).abs() < 0.01);
26/// ```
27#[derive(Debug, Clone)]
28pub struct AxisConfig {
29    // ── Unit conversion (private — use to_counts/to_user) ──
30    counts_per_rev: f64,
31    user_per_rev: f64,
32    /// When true, invert the sign of all position/velocity conversions.
33    /// Use when the motor counts in the opposite direction to your user-unit convention.
34    pub invert_direction: bool,
35
36    // ── Jog defaults ──
37
38    /// Jog speed in user units/s.
39    pub jog_speed: f64,
40    /// Jog acceleration in user units/s².
41    pub jog_accel: f64,
42    /// Jog deceleration in user units/s².
43    pub jog_decel: f64,
44
45    // ── Home ──
46
47    /// User-unit position assigned at the home reference point.
48    pub home_position: f64,
49    /// Homing search speed in user units/s.
50    pub homing_speed: f64,
51    /// Homing acceleration in user units/s².
52    pub homing_accel: f64,
53    /// Homing deceleration in user units/s².
54    pub homing_decel: f64,
55    /// CiA 402 current-position homing method used by the terminal "set position"
56    /// step of **software** homing (after a sensor search). Drive-side
57    /// current-position homing instead carries its code in the
58    /// [`HomingMethod::CurrentPosition35`]/[`CurrentPosition37`](HomingMethod::CurrentPosition37)
59    /// variant, so this field no longer affects it.
60    ///
61    /// Methods 37 and 35 are functionally equivalent; 37 is the newer CiA 402
62    /// addition. Default: 37 (works on Teknic ClearPath and other modern drives).
63    /// Set to 35 for drives whose 6098h range stops at 35 (Inovance SV660N, AKD).
64    pub soft_home_method: i8,
65
66    // ── Timeouts ──
67
68    /// Timeout for general operations (enable, disable, halt, fault recovery) in seconds.
69    /// Default: 7s.
70    pub operation_timeout_secs: f64,
71    /// Timeout for homing operations in seconds. Default: 30s.
72    pub homing_timeout_secs: f64,
73
74    // ── Software position limits ──
75
76    /// Enable the maximum (positive) software position limit.
77    pub enable_max_position_limit: bool,
78    /// Enable the minimum (negative) software position limit.
79    pub enable_min_position_limit: bool,
80    /// Maximum position limit in user units.
81    pub max_position_limit: f64,
82    /// Minimum position limit in user units.
83    pub min_position_limit: f64,
84}
85
86impl AxisConfig {
87    /// Create a new configuration from encoder resolution.
88    ///
89    /// Default user units are revolutions (1.0 user unit = 1 revolution).
90    /// Call [`with_user_scale`](Self::with_user_scale) to change.
91    pub fn new(counts_per_rev: u32) -> Self {
92        Self {
93            counts_per_rev: counts_per_rev as f64,
94            user_per_rev: 1.0,
95            invert_direction: false,
96            jog_speed: 0.0,
97            jog_accel: 0.0,
98            jog_decel: 0.0,
99            home_position: 0.0,
100            homing_speed: 0.0,
101            homing_accel: 0.0,
102            homing_decel: 0.0,
103            soft_home_method: 37,
104            operation_timeout_secs: 7.0,
105            homing_timeout_secs: 30.0,
106            enable_max_position_limit: false,
107            enable_min_position_limit: false,
108            max_position_limit: 0.0,
109            min_position_limit: 0.0,
110        }
111    }
112
113    /// Set user-units-per-revolution.
114    ///
115    /// - Degrees: `.with_user_scale(360.0)`
116    /// - mm on 5 mm/rev ballscrew: `.with_user_scale(5.0)`
117    pub fn with_user_scale(mut self, user_per_rev: f64) -> Self {
118        self.user_per_rev = user_per_rev;
119        self
120    }
121
122    // ── Conversion methods ──
123
124    /// Convert user units to encoder counts (f64).
125    ///
126    /// `to_counts(45.0)` on a 12800 cpr / 360° config → 1600.0
127    pub fn to_counts(&self, user_units: f64) -> f64 {
128        let sign = if self.invert_direction { -1.0 } else { 1.0 };
129        user_units * self.counts_per_user() * sign
130    }
131
132    /// Convert encoder counts to user units (f64).
133    ///
134    /// `to_user(1600.0)` on a 12800 cpr / 360° config → 45.0
135    pub fn to_user(&self, counts: f64) -> f64 {
136        let sign = if self.invert_direction { -1.0 } else { 1.0 };
137        counts * sign / self.counts_per_user()
138    }
139
140    /// Encoder counts per user unit (scale factor).
141    ///
142    /// For 12800 cpr / 360°: `counts_per_user() ≈ 35.556`
143    pub fn counts_per_user(&self) -> f64 {
144        self.counts_per_rev / self.user_per_rev
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn basic_conversion_degrees() {
154        let cfg = AxisConfig::new(12_800).with_user_scale(360.0);
155        assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
156        assert!((cfg.to_counts(360.0) - 12800.0).abs() < 0.01);
157        assert!((cfg.to_user(1600.0) - 45.0).abs() < 0.01);
158        assert!((cfg.to_user(12800.0) - 360.0).abs() < 0.01);
159    }
160
161    #[test]
162    fn basic_conversion_mm() {
163        let cfg = AxisConfig::new(12_800).with_user_scale(5.0);
164        assert!((cfg.to_counts(5.0) - 12800.0).abs() < 0.01);
165        assert!((cfg.to_counts(2.5) - 6400.0).abs() < 0.01);
166    }
167
168    #[test]
169    fn counts_per_user() {
170        let cfg = AxisConfig::new(12_800).with_user_scale(360.0);
171        let cpu = cfg.counts_per_user();
172        assert!((cpu - 12800.0 / 360.0).abs() < 1e-9);
173    }
174
175    #[test]
176    fn default_revolutions() {
177        let cfg = AxisConfig::new(12_800);
178        // 1 rev = 12800 counts
179        assert!((cfg.to_counts(1.0) - 12800.0).abs() < 0.01);
180        assert!((cfg.to_counts(0.125) - 1600.0).abs() < 0.01);
181    }
182
183    #[test]
184    fn round_trip() {
185        let cfg = AxisConfig::new(12_800).with_user_scale(360.0);
186        let original = 45.0;
187        let counts = cfg.to_counts(original);
188        let back = cfg.to_user(counts);
189        assert!((back - original).abs() < 1e-9);
190    }
191
192    #[test]
193    fn defaults() {
194        let cfg = AxisConfig::new(12_800);
195        assert_eq!(cfg.jog_speed, 0.0);
196        assert_eq!(cfg.home_position, 0.0);
197        assert!(!cfg.enable_max_position_limit);
198        assert!(!cfg.enable_min_position_limit);
199    }
200}