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 homing method used when the axis needs to declare "current position
56 /// = home offset" — i.e. the terminal step of software homing (after a sensor
57 /// search) and the integrated [`HomingMethod::CurrentPosition`] variant.
58 ///
59 /// Methods 37 and 35 are functionally equivalent; 37 is the newer CiA 402
60 /// addition. Default: 37 (works on Teknic ClearPath and other modern drives).
61 /// Set to 35 for drives whose 6098h range stops at 35 (Inovance SV660N).
62 pub soft_home_method: i8,
63
64 /// Whether `command_cancel_move` should clear the halt bit (CW 8) at the
65 /// same time it issues the cancel setpoint.
66 ///
67 /// Drives differ on this:
68 /// - Teknic ClearPath: halt must stay asserted through the full cancel
69 /// handshake. Clearing it early causes the drive to resume the prior
70 /// move when halt later drops. Leave this `false`.
71 /// - Inovance SV660N: refuses to set setpoint_ack (SW 12) while halt is
72 /// asserted, so the WaitCancelAck stage stalls. Set this `true`.
73 ///
74 /// Default: `false` (Teknic-safe).
75 pub clear_halt_during_cancel: bool,
76
77 // ── Timeouts ──
78
79 /// Timeout for general operations (enable, disable, halt, fault recovery) in seconds.
80 /// Default: 7s.
81 pub operation_timeout_secs: f64,
82 /// Timeout for homing operations in seconds. Default: 30s.
83 pub homing_timeout_secs: f64,
84
85 // ── Software position limits ──
86
87 /// Enable the maximum (positive) software position limit.
88 pub enable_max_position_limit: bool,
89 /// Enable the minimum (negative) software position limit.
90 pub enable_min_position_limit: bool,
91 /// Maximum position limit in user units.
92 pub max_position_limit: f64,
93 /// Minimum position limit in user units.
94 pub min_position_limit: f64,
95}
96
97impl AxisConfig {
98 /// Create a new configuration from encoder resolution.
99 ///
100 /// Default user units are revolutions (1.0 user unit = 1 revolution).
101 /// Call [`with_user_scale`](Self::with_user_scale) to change.
102 pub fn new(counts_per_rev: u32) -> Self {
103 Self {
104 counts_per_rev: counts_per_rev as f64,
105 user_per_rev: 1.0,
106 invert_direction: false,
107 jog_speed: 0.0,
108 jog_accel: 0.0,
109 jog_decel: 0.0,
110 home_position: 0.0,
111 homing_speed: 0.0,
112 homing_accel: 0.0,
113 homing_decel: 0.0,
114 soft_home_method: 37,
115 clear_halt_during_cancel: false,
116 operation_timeout_secs: 7.0,
117 homing_timeout_secs: 30.0,
118 enable_max_position_limit: false,
119 enable_min_position_limit: false,
120 max_position_limit: 0.0,
121 min_position_limit: 0.0,
122 }
123 }
124
125 /// Set user-units-per-revolution.
126 ///
127 /// - Degrees: `.with_user_scale(360.0)`
128 /// - mm on 5 mm/rev ballscrew: `.with_user_scale(5.0)`
129 pub fn with_user_scale(mut self, user_per_rev: f64) -> Self {
130 self.user_per_rev = user_per_rev;
131 self
132 }
133
134 // ── Conversion methods ──
135
136 /// Convert user units to encoder counts (f64).
137 ///
138 /// `to_counts(45.0)` on a 12800 cpr / 360° config → 1600.0
139 pub fn to_counts(&self, user_units: f64) -> f64 {
140 let sign = if self.invert_direction { -1.0 } else { 1.0 };
141 user_units * self.counts_per_user() * sign
142 }
143
144 /// Convert encoder counts to user units (f64).
145 ///
146 /// `to_user(1600.0)` on a 12800 cpr / 360° config → 45.0
147 pub fn to_user(&self, counts: f64) -> f64 {
148 let sign = if self.invert_direction { -1.0 } else { 1.0 };
149 counts * sign / self.counts_per_user()
150 }
151
152 /// Encoder counts per user unit (scale factor).
153 ///
154 /// For 12800 cpr / 360°: `counts_per_user() ≈ 35.556`
155 pub fn counts_per_user(&self) -> f64 {
156 self.counts_per_rev / self.user_per_rev
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn basic_conversion_degrees() {
166 let cfg = AxisConfig::new(12_800).with_user_scale(360.0);
167 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
168 assert!((cfg.to_counts(360.0) - 12800.0).abs() < 0.01);
169 assert!((cfg.to_user(1600.0) - 45.0).abs() < 0.01);
170 assert!((cfg.to_user(12800.0) - 360.0).abs() < 0.01);
171 }
172
173 #[test]
174 fn basic_conversion_mm() {
175 let cfg = AxisConfig::new(12_800).with_user_scale(5.0);
176 assert!((cfg.to_counts(5.0) - 12800.0).abs() < 0.01);
177 assert!((cfg.to_counts(2.5) - 6400.0).abs() < 0.01);
178 }
179
180 #[test]
181 fn counts_per_user() {
182 let cfg = AxisConfig::new(12_800).with_user_scale(360.0);
183 let cpu = cfg.counts_per_user();
184 assert!((cpu - 12800.0 / 360.0).abs() < 1e-9);
185 }
186
187 #[test]
188 fn default_revolutions() {
189 let cfg = AxisConfig::new(12_800);
190 // 1 rev = 12800 counts
191 assert!((cfg.to_counts(1.0) - 12800.0).abs() < 0.01);
192 assert!((cfg.to_counts(0.125) - 1600.0).abs() < 0.01);
193 }
194
195 #[test]
196 fn round_trip() {
197 let cfg = AxisConfig::new(12_800).with_user_scale(360.0);
198 let original = 45.0;
199 let counts = cfg.to_counts(original);
200 let back = cfg.to_user(counts);
201 assert!((back - original).abs() < 1e-9);
202 }
203
204 #[test]
205 fn defaults() {
206 let cfg = AxisConfig::new(12_800);
207 assert_eq!(cfg.jog_speed, 0.0);
208 assert_eq!(cfg.home_position, 0.0);
209 assert!(!cfg.enable_max_position_limit);
210 assert!(!cfg.enable_min_position_limit);
211 }
212}