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