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