Skip to main content

autocore_std/motion/
pp_snapshot.rs

1//! Owned-value snapshot of CiA 402 Profile Position (PP) fields.
2//!
3//! [`Cia402PpSnapshot`] holds all standard CiA 402 PP fields by value
4//! (no references, no lifetimes). Used by generated DriveHandle structs
5//! to enable multiple axes without borrow conflicts.
6//!
7//! This is vendor-neutral — it works with any CiA 402 drive (Teknic,
8//! Yaskawa, Beckhoff, etc.) as long as the standard PP objects are used.
9
10use super::axis_view::AxisView;
11use super::cia402::{
12    Cia402Control, Cia402Status, Cia402State,
13    PpControl, PpStatus,
14    HomingControl, HomingStatus,
15    RawControlWord, RawStatusWord,
16    ModesOfOperation,
17};
18
19// Re-export HomingProgress so generated DriveHandle code can use it
20// if needed via autocore_std::motion::HomingProgress
21pub use crate::ethercat::teknic::view::HomingProgress;
22
23/// Owned copy of all CiA 402 PP fields for one axis.
24///
25/// Unlike borrowed views (which hold references into GlobalMemory),
26/// this struct owns its data by value. Used by generated DriveHandle
27/// structs to enable multiple axes without borrow conflicts.
28///
29/// Typical flow:
30/// 1. `sync()` — copy TxPDO feedback from shared memory into snapshot
31/// 2. Issue commands (they modify the snapshot)
32/// 3. `tick()` — advance axis state machine, then flush RxPDO back
33#[derive(Debug, Clone, Default)]
34pub struct Cia402PpSnapshot {
35    // ── RxPDO — master → drive ──
36    pub control_word: u16,
37    pub target_position: i32,
38    pub profile_velocity: u32,
39    pub profile_acceleration: u32,
40    pub profile_deceleration: u32,
41    pub modes_of_operation: i8,
42
43    // ── TxPDO — drive → master ──
44    pub status_word: u16,
45    pub position_actual: i32,
46    pub velocity_actual: i32,
47    pub torque_actual: i16,
48    pub modes_of_operation_display: i8,
49}
50
51impl Cia402PpSnapshot {
52    // ── Typed access ──
53
54    /// Read the control word as a typed CiA 402 control word.
55    pub fn pp_control(&self) -> RawControlWord {
56        RawControlWord(self.control_word)
57    }
58
59    /// Read the status word as a typed CiA 402 status word.
60    pub fn pp_status(&self) -> RawStatusWord {
61        RawStatusWord(self.status_word)
62    }
63
64    /// Write back a modified control word.
65    pub fn set_pp_control(&mut self, cw: RawControlWord) {
66        self.control_word = cw.raw();
67    }
68
69    // ── State machine ──
70
71    /// Current CiA 402 state.
72    pub fn state(&self) -> Cia402State {
73        self.pp_status().state()
74    }
75
76    /// Shutdown → Ready to Switch On.
77    pub fn cmd_shutdown(&mut self) {
78        let mut cw = self.pp_control();
79        cw.cmd_shutdown();
80        self.set_pp_control(cw);
81    }
82
83    /// Switch On → Switched On.
84    pub fn cmd_switch_on(&mut self) {
85        let mut cw = self.pp_control();
86        cw.cmd_switch_on();
87        self.set_pp_control(cw);
88    }
89
90    /// Enable Operation → Operation Enabled.
91    pub fn cmd_enable_operation(&mut self) {
92        let mut cw = self.pp_control();
93        cw.cmd_enable_operation();
94        self.set_pp_control(cw);
95    }
96
97    /// Disable Operation → Switched On.
98    pub fn cmd_disable_operation(&mut self) {
99        let mut cw = self.pp_control();
100        cw.cmd_disable_operation();
101        self.set_pp_control(cw);
102    }
103
104    /// Disable Voltage → Switch On Disabled.
105    pub fn cmd_disable_voltage(&mut self) {
106        let mut cw = self.pp_control();
107        cw.cmd_disable_voltage();
108        self.set_pp_control(cw);
109    }
110
111    /// Quick Stop → Quick Stop Active.
112    pub fn cmd_quick_stop(&mut self) {
113        let mut cw = self.pp_control();
114        cw.cmd_quick_stop();
115        self.set_pp_control(cw);
116    }
117
118    /// Fault Reset (rising edge on bit 7).
119    pub fn cmd_fault_reset(&mut self) {
120        let mut cw = self.pp_control();
121        cw.cmd_fault_reset();
122        self.set_pp_control(cw);
123    }
124
125    /// Clear/reset Fault Reset bit (bit 7).
126    pub fn cmd_clear_fault_reset(&mut self) {
127        let mut cw = self.pp_control();
128        cw.cmd_clear_fault_reset();
129        self.set_pp_control(cw);
130    }
131
132    // ── PP motion ──
133
134    /// Set mode to Profile Position.
135    pub fn ensure_pp_mode(&mut self) {
136        self.modes_of_operation = ModesOfOperation::ProfilePosition.as_i8();
137    }
138
139    /// Configure move parameters (does not trigger the move).
140    pub fn set_target(&mut self, position: i32, velocity: u32, accel: u32, decel: u32) {
141        self.target_position = position;
142        self.profile_velocity = velocity;
143        self.profile_acceleration = accel;
144        self.profile_deceleration = decel;
145    }
146
147    /// Assert New Set-Point (bit 4). Call after set_target().
148    pub fn trigger_move(&mut self) {
149        let mut cw = self.pp_control();
150        cw.set_new_set_point(true);
151        self.set_pp_control(cw);
152    }
153
154    /// Clear New Set-Point. Call when set_point_acknowledged() is true.
155    pub fn ack_set_point(&mut self) {
156        let mut cw = self.pp_control();
157        cw.set_new_set_point(false);
158        self.set_pp_control(cw);
159    }
160
161    /// Bit 8 — Halt: decelerate to stop.
162    pub fn set_halt(&mut self, v: bool) {
163        let mut cw = self.pp_control();
164        PpControl::set_halt(&mut cw, v);
165        self.set_pp_control(cw);
166    }
167
168    /// Bit 6 — Relative: when true, target_position is relative to the
169    /// current *command* position. When false, target_position is absolute.
170    pub fn set_relative(&mut self, v: bool) {
171        let mut cw = self.pp_control();
172        cw.set_relative(v);
173        self.set_pp_control(cw);
174    }
175
176    // ── PP status queries ──
177
178    /// Drive has reached the target position (bit 10).
179    pub fn target_reached(&self) -> bool {
180        self.pp_status().pp_target_reached()
181    }
182
183    /// Set-point was acknowledged by the drive (bit 12).
184    pub fn set_point_acknowledged(&self) -> bool {
185        self.pp_status().set_point_acknowledge()
186    }
187
188    // ── Error / fault status ──
189
190    /// Bit 13 — Following Error: position tracking error exceeded the
191    /// configured limit. Typically leads to a fault transition.
192    pub fn following_error(&self) -> bool {
193        self.pp_status().following_error()
194    }
195
196    /// Bit 11 — Internal Limit Active: a hardware or software limit
197    /// is currently active (e.g. position or velocity limit).
198    pub fn internal_limit(&self) -> bool {
199        self.pp_status().internal_limit()
200    }
201
202    /// Bit 7 — Warning: a non-fatal warning condition exists.
203    pub fn warning(&self) -> bool {
204        self.pp_status().warning()
205    }
206
207    /// True if the drive is in Fault or Fault Reaction Active state.
208    pub fn is_faulted(&self) -> bool {
209        matches!(self.state(), Cia402State::Fault | Cia402State::FaultReactionActive)
210    }
211
212    // ── Homing ──
213
214    /// Set mode to Homing.
215    pub fn ensure_homing_mode(&mut self) {
216        self.modes_of_operation = ModesOfOperation::Homing.as_i8();
217    }
218
219    /// Start the homing procedure (rising edge on bit 4).
220    pub fn trigger_homing(&mut self) {
221        let mut cw = RawControlWord(self.control_word);
222        cw.set_homing_start(true);
223        self.control_word = cw.raw();
224    }
225
226    /// Clear the homing start bit. Call after homing completes.
227    pub fn clear_homing_start(&mut self) {
228        let mut cw = RawControlWord(self.control_word);
229        cw.set_homing_start(false);
230        self.control_word = cw.raw();
231    }
232
233    /// Decode the current homing progress from status word bits 10, 12, 13.
234    ///
235    /// Only meaningful when the drive is in Homing mode.
236    pub fn homing_progress(&self) -> HomingProgress {
237        let sw = RawStatusWord(self.status_word);
238        let attained = sw.homing_attained();
239        let reached  = sw.homing_target_reached();
240        let error    = sw.homing_error();
241
242        if error {
243            HomingProgress::Error
244        } else if attained && reached {
245            HomingProgress::Complete
246        } else if attained {
247            HomingProgress::Attained
248        } else if reached {
249            HomingProgress::Idle
250        } else {
251            HomingProgress::InProgress
252        }
253    }
254
255    // ── Feedback ──
256
257    /// Actual position in encoder counts.
258    pub fn position(&self) -> i32 {
259        self.position_actual
260    }
261
262    /// Actual velocity in encoder counts/s.
263    pub fn velocity(&self) -> i32 {
264        self.velocity_actual
265    }
266
267    /// Actual torque in per-mille of rated torque.
268    pub fn torque(&self) -> i16 {
269        self.torque_actual
270    }
271
272    /// Mode the drive is currently operating in.
273    pub fn current_mode(&self) -> Option<ModesOfOperation> {
274        ModesOfOperation::from_i8(self.modes_of_operation_display)
275    }
276}
277
278// ──────────────────────────────────────────────
279// AxisView implementation
280// ──────────────────────────────────────────────
281
282impl AxisView for Cia402PpSnapshot {
283    fn control_word(&self) -> u16 { self.control_word }
284    fn set_control_word(&mut self, word: u16) { self.control_word = word; }
285    fn status_word(&self) -> u16 { self.status_word }
286    fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
287    fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
288    fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
289    fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
290    fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
291    fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
292    fn position_actual(&self) -> i32 { self.position_actual }
293    fn velocity_actual(&self) -> i32 { self.velocity_actual }
294}
295
296// ──────────────────────────────────────────────
297// Tests
298// ──────────────────────────────────────────────
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_snapshot_default() {
306        let s = Cia402PpSnapshot::default();
307        assert_eq!(s.control_word, 0);
308        assert_eq!(s.status_word, 0);
309        assert_eq!(s.target_position, 0);
310    }
311
312    #[test]
313    fn test_snapshot_state_machine() {
314        let mut s = Cia402PpSnapshot { status_word: 0x0040, ..Default::default() };
315        assert_eq!(s.state(), Cia402State::SwitchOnDisabled);
316
317        s.cmd_shutdown();
318        assert_eq!(s.control_word & 0x008F, 0x0006);
319
320        s.cmd_enable_operation();
321        assert_eq!(s.control_word & 0x008F, 0x000F);
322    }
323
324    #[test]
325    fn test_snapshot_pp_motion() {
326        let mut s = Cia402PpSnapshot::default();
327        s.cmd_enable_operation();
328        s.set_target(50_000, 10_000, 2_000, 2_000);
329        s.trigger_move();
330
331        assert_eq!(s.target_position, 50_000);
332        assert_eq!(s.profile_velocity, 10_000);
333        assert!(s.control_word & (1 << 4) != 0);
334
335        s.ack_set_point();
336        assert!(s.control_word & (1 << 4) == 0);
337    }
338
339    #[test]
340    fn test_snapshot_homing() {
341        let mut s = Cia402PpSnapshot::default();
342        s.ensure_homing_mode();
343        assert_eq!(s.modes_of_operation, 6);
344
345        s.cmd_enable_operation();
346        s.trigger_homing();
347        assert!(s.control_word & (1 << 4) != 0);
348
349        s.clear_homing_start();
350        assert!(s.control_word & (1 << 4) == 0);
351    }
352
353    #[test]
354    fn test_snapshot_homing_progress() {
355        // Complete: bit 12 + bit 10
356        let s = Cia402PpSnapshot { status_word: 0x1427, ..Default::default() };
357        assert_eq!(s.homing_progress(), HomingProgress::Complete);
358
359        // Error: bit 13
360        let s = Cia402PpSnapshot { status_word: 0x2027, ..Default::default() };
361        assert_eq!(s.homing_progress(), HomingProgress::Error);
362
363        // In progress: no bits
364        let s = Cia402PpSnapshot { status_word: 0x0027, ..Default::default() };
365        assert_eq!(s.homing_progress(), HomingProgress::InProgress);
366    }
367
368    #[test]
369    fn test_snapshot_axis_view() {
370        let mut s = Cia402PpSnapshot {
371            status_word: 0x0027,
372            position_actual: 5000,
373            velocity_actual: 1000,
374            modes_of_operation_display: 1,
375            ..Default::default()
376        };
377
378        assert_eq!(AxisView::status_word(&s), 0x0027);
379        assert_eq!(AxisView::position_actual(&s), 5000);
380        assert_eq!(AxisView::velocity_actual(&s), 1000);
381
382        AxisView::set_control_word(&mut s, 0x000F);
383        assert_eq!(s.control_word, 0x000F);
384        AxisView::set_target_position(&mut s, 10_000);
385        assert_eq!(s.target_position, 10_000);
386    }
387
388    #[test]
389    fn test_snapshot_error_status() {
390        let s = Cia402PpSnapshot { status_word: 0x0008, ..Default::default() };
391        assert!(s.is_faulted());
392
393        let s = Cia402PpSnapshot { status_word: 0x2027, ..Default::default() };
394        assert!(s.following_error());
395
396        let s = Cia402PpSnapshot { status_word: 0x0827, ..Default::default() };
397        assert!(s.internal_limit());
398    }
399}