Skip to main content

autocore_std/ethercat/
axis_config.rs

1/// Unit conversion for a rotary/linear axis with a known encoder resolution.
2///
3/// Stores the encoder's counts-per-revolution and an optional user-unit scale
4/// (e.g. 360.0 for degrees, 5.0 for mm-per-rev on a ballscrew). All conversion
5/// methods are pure arithmetic — no state, no side effects.
6///
7/// # Examples
8///
9/// ```
10/// use autocore_std::ethercat::AxisConfig;
11///
12/// // Revolutions (default)
13/// let cfg = AxisConfig::new(12_800);
14/// assert_eq!(cfg.rev(0.125), 1_600);
15/// assert_eq!(cfg.rev(1.0), 12_800);
16///
17/// // Degrees
18/// let deg = AxisConfig::new(12_800).with_user_scale(360.0);
19/// assert_eq!(deg.user(45.0), 1_600);
20/// assert_eq!(deg.user(360.0), 12_800);
21///
22/// // mm on a 5 mm/rev ballscrew
23/// let mm = AxisConfig::new(12_800).with_user_scale(5.0);
24/// assert_eq!(mm.user(5.0), 12_800);
25/// assert_eq!(mm.user(2.5), 6_400);
26/// ```
27#[derive(Debug, Clone, Copy)]
28pub struct AxisConfig {
29    counts_per_rev: f64,
30    /// User-units per revolution (e.g. 360.0 for degrees, 1.0 for revolutions).
31    user_per_rev: f64,
32}
33
34impl AxisConfig {
35    /// Create from encoder resolution (e.g. 12_800 counts/rev).
36    /// Default user units = revolutions (1.0 user unit = 1 revolution).
37    pub const fn new(counts_per_rev: u32) -> Self {
38        Self {
39            counts_per_rev: counts_per_rev as f64,
40            user_per_rev: 1.0,
41        }
42    }
43
44    /// Set user-units-per-revolution for application-specific units.
45    ///
46    /// - Degrees: `.with_user_scale(360.0)`
47    /// - mm on 5 mm/rev ballscrew: `.with_user_scale(5.0)`
48    pub const fn with_user_scale(mut self, user_per_rev: f64) -> Self {
49        self.user_per_rev = user_per_rev;
50        self
51    }
52
53    // ── Revolution-based conversions (user → counts) ──
54
55    /// Convert revolutions to encoder counts (position).
56    pub fn rev(&self, revolutions: f64) -> i32 {
57        (revolutions * self.counts_per_rev).round() as i32
58    }
59
60    /// Convert rev/s to counts/s (velocity).
61    pub fn rps(&self, rev_per_sec: f64) -> u32 {
62        (rev_per_sec * self.counts_per_rev).round() as u32
63    }
64
65    /// Convert rev/s² to counts/s² (acceleration).
66    pub fn rps2(&self, rev_per_sec2: f64) -> u32 {
67        (rev_per_sec2 * self.counts_per_rev).round() as u32
68    }
69
70    // ── User-unit-based conversions (user → counts) ──
71
72    /// Convert user units to encoder counts (position).
73    pub fn user(&self, units: f64) -> i32 {
74        (units / self.user_per_rev * self.counts_per_rev).round() as i32
75    }
76
77    /// Convert user-units/s to counts/s (velocity).
78    pub fn user_per_s(&self, units_per_sec: f64) -> u32 {
79        (units_per_sec / self.user_per_rev * self.counts_per_rev).round() as u32
80    }
81
82    /// Convert user-units/s² to counts/s² (acceleration).
83    pub fn user_per_s2(&self, units_per_sec2: f64) -> u32 {
84        (units_per_sec2 / self.user_per_rev * self.counts_per_rev).round() as u32
85    }
86
87    // ── From-counts conversions (counts → rev) ──
88
89    /// Convert encoder counts to revolutions.
90    pub fn to_rev(&self, counts: i32) -> f64 {
91        counts as f64 / self.counts_per_rev
92    }
93
94    /// Convert counts/s to rev/s.
95    pub fn to_rps(&self, counts_per_sec: u32) -> f64 {
96        counts_per_sec as f64 / self.counts_per_rev
97    }
98
99    // ── From-counts conversions (counts → user) ──
100
101    /// Convert encoder counts to user units.
102    pub fn to_user(&self, counts: i32) -> f64 {
103        counts as f64 / self.counts_per_rev * self.user_per_rev
104    }
105
106    /// Convert counts/s to user-units/s.
107    pub fn to_user_per_s(&self, counts_per_sec: u32) -> f64 {
108        counts_per_sec as f64 / self.counts_per_rev * self.user_per_rev
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    const CPR: u32 = 12_800;
117
118    #[test]
119    fn rev_position() {
120        let cfg = AxisConfig::new(CPR);
121        assert_eq!(cfg.rev(0.125), 1_600);
122        assert_eq!(cfg.rev(1.0), 12_800);
123        assert_eq!(cfg.rev(-0.5), -6_400);
124    }
125
126    #[test]
127    fn rev_velocity_accel() {
128        let cfg = AxisConfig::new(CPR);
129        assert_eq!(cfg.rps(1.0), 12_800);
130        assert_eq!(cfg.rps2(1.0), 12_800);
131    }
132
133    #[test]
134    fn user_degrees() {
135        let deg = AxisConfig::new(CPR).with_user_scale(360.0);
136        assert_eq!(deg.user(45.0), 1_600);
137        assert_eq!(deg.user(360.0), 12_800);
138        assert_eq!(deg.user(90.0), 3_200);
139    }
140
141    #[test]
142    fn user_mm_ballscrew() {
143        let mm = AxisConfig::new(CPR).with_user_scale(5.0);
144        assert_eq!(mm.user(5.0), 12_800);
145        assert_eq!(mm.user(2.5), 6_400);
146    }
147
148    #[test]
149    fn user_velocity_accel() {
150        let deg = AxisConfig::new(CPR).with_user_scale(360.0);
151        // 90 deg/s = 0.25 rev/s = 3200 counts/s
152        assert_eq!(deg.user_per_s(90.0), 3_200);
153        // 180 deg/s² = 0.5 rev/s² = 6400 counts/s²
154        assert_eq!(deg.user_per_s2(180.0), 6_400);
155    }
156
157    #[test]
158    fn round_trip_rev() {
159        let cfg = AxisConfig::new(CPR);
160        let original = 0.125;
161        let counts = cfg.rev(original);
162        let back = cfg.to_rev(counts);
163        assert!((back - original).abs() < 1e-9);
164    }
165
166    #[test]
167    fn round_trip_user() {
168        let deg = AxisConfig::new(CPR).with_user_scale(360.0);
169        let original = 45.0;
170        let counts = deg.user(original);
171        let back = deg.to_user(counts);
172        assert!((back - original).abs() < 0.1);
173    }
174
175    #[test]
176    fn round_trip_velocity() {
177        let cfg = AxisConfig::new(CPR);
178        let original = 7.8125;
179        let counts = cfg.rps(original);
180        let back = cfg.to_rps(counts);
181        assert!((back - original).abs() < 1e-9);
182    }
183
184    #[test]
185    fn to_user_per_s() {
186        let deg = AxisConfig::new(CPR).with_user_scale(360.0);
187        // 3200 counts/s = 90 deg/s
188        assert!((deg.to_user_per_s(3_200) - 90.0).abs() < 1e-9);
189    }
190
191    #[test]
192    fn const_construction() {
193        // Verify const fn works at compile time
194        const CFG: AxisConfig = AxisConfig::new(12_800).with_user_scale(360.0);
195        assert_eq!(CFG.user(45.0), 1_600);
196    }
197}