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