Skip to main content

autocore_std/ethercat/teknic/
view.rs

1use crate::motion::{
2    AxisView,
3    cia402::{
4        Cia402Control, Cia402Status, Cia402State, ModesOfOperation,
5        PpControl, PpStatus,
6        HomingControl, HomingStatus,
7    },
8};
9use crate::ethercat::teknic::types::{
10    TeknicPpControlWord, TeknicPpStatusWord,
11    TeknicPpControl, TeknicPpStatus,
12    TeknicHomingControlWord, TeknicHomingStatusWord,
13};
14
15// ──────────────────────────────────────────────
16// HomingProgress
17// ──────────────────────────────────────────────
18
19/// Homing procedure progress, decoded from status word bits 10, 12, 13.
20///
21/// | Error (13) | Attained (12) | Reached (10) | Progress     |
22/// |------------|---------------|--------------|--------------|
23/// | 0          | 0             | 1            | `Idle`       |
24/// | 0          | 0             | 0            | `InProgress` |
25/// | 0          | 1             | 0            | `Attained`   |
26/// | 0          | 1             | 1            | `Complete`   |
27/// | 1          | x             | x            | `Error`      |
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum HomingProgress {
30    /// Homing not started or interrupted (motor stationary).
31    Idle,
32    /// Homing actively in progress (motor searching).
33    InProgress,
34    /// Reference found; offset move still in progress.
35    Attained,
36    /// Homing completed successfully — position is now referenced.
37    Complete,
38    /// A homing error occurred (motor may still be moving).
39    Error,
40}
41
42impl std::fmt::Display for HomingProgress {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::Idle       => write!(f, "Idle"),
46            Self::InProgress => write!(f, "In Progress"),
47            Self::Attained   => write!(f, "Attained"),
48            Self::Complete   => write!(f, "Complete"),
49            Self::Error      => write!(f, "Error"),
50        }
51    }
52}
53
54// ──────────────────────────────────────────────
55// TeknicPpView — borrowed view for one axis
56// ──────────────────────────────────────────────
57
58/// A borrowed view into GlobalMemory for one Teknic ClearPath axis.
59///
60/// Supports both **Profile Position (PP)** moves and the **Homing**
61/// phase that typically precedes them. Holds mutable references to
62/// RxPDO (output) fields and shared references to TxPDO (input) fields.
63/// Reuse the same struct for multiple axes — just point the references
64/// at different GlobalMemory fields via [`teknic_pp_view!`].
65///
66/// # Homing Example
67///
68/// Homing is performed once at startup. Configure homing parameters
69/// (method, speeds, acceleration) via SDO before entering the control
70/// loop.
71///
72/// ```ignore
73/// use crate::teknic::view::{TeknicPpView, HomingProgress};
74/// use crate::motion::types::{Cia402State, ModesOfOperation};
75///
76/// #[derive(Debug, Clone, Copy, PartialEq)]
77/// enum HomingState {
78///     Init,
79///     WaitMode,
80///     WaitComplete,
81///     Done,
82///     Failed,
83/// }
84///
85/// fn tick_homing(view: &mut TeknicPpView, state: &mut HomingState) {
86///     if view.is_faulted() && *state != HomingState::Failed {
87///         log::error!("Drive fault during homing");
88///         *state = HomingState::Failed;
89///     }
90///
91///     match state {
92///         HomingState::Init => {
93///             view.ensure_homing_mode();
94///             view.cmd_enable_operation();
95///             *state = HomingState::WaitMode;
96///         }
97///         HomingState::WaitMode => {
98///             if view.state() == Cia402State::OperationEnabled
99///                 && view.current_mode() == Some(ModesOfOperation::Homing)
100///             {
101///                 view.trigger_homing();
102///                 *state = HomingState::WaitComplete;
103///             }
104///         }
105///         HomingState::WaitComplete => {
106///             match view.homing_progress() {
107///                 HomingProgress::Complete => {
108///                     view.clear_homing_start();
109///                     log::info!("Homing complete at pos {}", view.position());
110///                     *state = HomingState::Done;
111///                 }
112///                 HomingProgress::Error => {
113///                     log::error!("Homing error!");
114///                     *state = HomingState::Failed;
115///                 }
116///                 _ => {} // still in progress
117///             }
118///         }
119///         HomingState::Done => {
120///             // Switch to PP mode for positioning.
121///             view.ensure_pp_mode();
122///         }
123///         HomingState::Failed => {
124///             view.cmd_fault_reset();
125///             if view.state() == Cia402State::SwitchOnDisabled {
126///                 *state = HomingState::Init;
127///             }
128///         }
129///     }
130/// }
131/// ```
132///
133/// # Profile Position Example
134///
135/// After homing, use PP mode to make absolute or relative moves.
136///
137/// ```ignore
138/// use crate::teknic::view::TeknicPpView;
139/// use crate::motion::types::Cia402State;
140///
141/// #[derive(Debug, Clone, Copy, PartialEq)]
142/// enum MoveState {
143///     Init,
144///     WaitReady,
145///     WaitEnabled,
146///     StartMove,
147///     WaitAck,
148///     WaitComplete,
149///     Done,
150///     Faulted,
151/// }
152///
153/// fn tick_move(view: &mut TeknicPpView, state: &mut MoveState) {
154///     if view.is_faulted() && *state != MoveState::Faulted {
155///         log::error!("Drive fault detected in state {:?}", state);
156///         *state = MoveState::Faulted;
157///     }
158///
159///     match state {
160///         MoveState::Init => {
161///             view.ensure_pp_mode();
162///             view.cmd_shutdown();           // → Ready to Switch On
163///             *state = MoveState::WaitReady;
164///         }
165///         MoveState::WaitReady => {
166///             if view.state() == Cia402State::ReadyToSwitchOn {
167///                 view.cmd_enable_operation(); // → Operation Enabled
168///                 *state = MoveState::WaitEnabled;
169///             }
170///         }
171///         MoveState::WaitEnabled => {
172///             if view.state() == Cia402State::OperationEnabled {
173///                 *state = MoveState::StartMove;
174///             }
175///         }
176///         MoveState::StartMove => {
177///             // Absolute move to 100 000 counts at 50 000 cts/s
178///             view.set_target(100_000, 50_000, 10_000, 10_000);
179///             view.set_relative(false);      // absolute move
180///             view.trigger_move();
181///             *state = MoveState::WaitAck;
182///         }
183///         MoveState::WaitAck => {
184///             if view.set_point_acknowledged() {
185///                 view.ack_set_point();       // complete handshake
186///                 *state = MoveState::WaitComplete;
187///             }
188///         }
189///         MoveState::WaitComplete => {
190///             if view.following_error() {
191///                 log::error!("Following error at pos {}", view.position());
192///                 *state = MoveState::Faulted;
193///             } else if view.target_reached() && view.in_range() {
194///                 *state = MoveState::Done;
195///             }
196///         }
197///         MoveState::Done => {
198///             // Move complete — start another or disable the drive.
199///         }
200///         MoveState::Faulted => {
201///             view.cmd_fault_reset();
202///             if view.state() == Cia402State::SwitchOnDisabled {
203///                 *state = MoveState::Init;   // recovered, restart
204///             }
205///         }
206///     }
207/// }
208/// ```
209pub struct TeknicPpView<'a> {
210    // ── RxPDO — master → drive ──
211    pub control_word: &'a mut u16,
212    pub target_position: &'a mut i32,
213    pub profile_velocity: &'a mut u32,
214    pub profile_acceleration: &'a mut u32,
215    pub profile_deceleration: &'a mut u32,
216    pub modes_of_operation: &'a mut i8,
217
218    // ── TxPDO — drive → master ──
219    pub status_word: &'a u16,
220    pub position_actual: &'a i32,
221    pub velocity_actual: &'a i32,
222    pub torque_actual: &'a i16,
223    pub modes_of_operation_display: &'a i8,
224}
225
226impl<'a> TeknicPpView<'a> {
227    // ── Typed access ──
228
229    /// Read the control word as a typed Teknic PP control word.
230    pub fn pp_control(&self) -> TeknicPpControlWord {
231        TeknicPpControlWord(*self.control_word)
232    }
233
234    /// Read the status word as a typed Teknic PP status word.
235    pub fn pp_status(&self) -> TeknicPpStatusWord {
236        TeknicPpStatusWord(*self.status_word)
237    }
238
239    /// Write back a modified PP control word.
240    pub fn set_pp_control(&mut self, cw: TeknicPpControlWord) {
241        *self.control_word = cw.raw();
242    }
243
244    // ── State machine ──
245
246    /// Current CiA 402 state.
247    pub fn state(&self) -> Cia402State {
248        self.pp_status().state()
249    }
250
251    /// Shutdown → Ready to Switch On.
252    pub fn cmd_shutdown(&mut self) {
253        let mut cw = self.pp_control();
254        cw.cmd_shutdown();
255        self.set_pp_control(cw);
256    }
257
258    /// Switch On → Switched On.
259    pub fn cmd_switch_on(&mut self) {
260        let mut cw = self.pp_control();
261        cw.cmd_switch_on();
262        self.set_pp_control(cw);
263    }
264
265    /// Enable Operation → Operation Enabled.
266    pub fn cmd_enable_operation(&mut self) {
267        let mut cw = self.pp_control();
268        cw.cmd_enable_operation();
269        self.set_pp_control(cw);
270    }
271
272    /// Disable Operation → Switched On.
273    pub fn cmd_disable_operation(&mut self) {
274        let mut cw = self.pp_control();
275        cw.cmd_disable_operation();
276        self.set_pp_control(cw);
277    }
278
279    /// Disable Voltage → Switch On Disabled.
280    pub fn cmd_disable_voltage(&mut self) {
281        let mut cw = self.pp_control();
282        cw.cmd_disable_voltage();
283        self.set_pp_control(cw);
284    }
285
286    /// Quick Stop → Quick Stop Active.
287    pub fn cmd_quick_stop(&mut self) {
288        let mut cw = self.pp_control();
289        cw.cmd_quick_stop();
290        self.set_pp_control(cw);
291    }
292
293
294    /// Fault Reset (rising edge on bit 7).
295    pub fn cmd_fault_reset(&mut self) {
296        let mut cw = self.pp_control();
297        cw.cmd_fault_reset();
298        self.set_pp_control(cw);
299    }
300
301    /// Clear/reset Fault Reset bit (bit 7).
302    pub fn cmd_clear_fault_reset(&mut self) {
303        let mut cw = self.pp_control();
304        cw.cmd_clear_fault_reset();
305        self.set_pp_control(cw);
306    }
307
308    // ── PP motion ──
309
310    /// Set mode to Profile Position.
311    pub fn ensure_pp_mode(&mut self) {
312        *self.modes_of_operation = ModesOfOperation::ProfilePosition.as_i8();
313    }
314
315    /// Configure move parameters (does not trigger the move).
316    pub fn set_target(&mut self, position: i32, velocity: u32, accel: u32, decel: u32) {
317        *self.target_position = position;
318        *self.profile_velocity = velocity;
319        *self.profile_acceleration = accel;
320        *self.profile_deceleration = decel;
321    }
322
323    /// Assert New Set-Point (bit 4). Call after set_target().
324    pub fn trigger_move(&mut self) {
325        let mut cw = self.pp_control();
326        cw.set_new_set_point(true);
327        self.set_pp_control(cw);
328    }
329
330    /// Clear New Set-Point. Call when set_point_acknowledged() is true.
331    pub fn ack_set_point(&mut self) {
332        let mut cw = self.pp_control();
333        cw.set_new_set_point(false);
334        self.set_pp_control(cw);
335    }
336
337    /// Bit 8 — Halt: decelerate to stop.
338    pub fn set_halt(&mut self, v: bool) {
339        let mut cw = self.pp_control();
340        cw.set_halt(v);
341        self.set_pp_control(cw);
342    }
343
344    /// Bit 6 — Relative: when true, target_position is relative to the
345    /// current *command* position. When false, target_position is absolute.
346    pub fn set_relative(&mut self, v: bool) {
347        let mut cw = self.pp_control();
348        cw.set_relative(v);
349        self.set_pp_control(cw);
350    }
351
352    /// Bit 13 (Teknic) — Relative to Actual: when true *and* bit 6 is set,
353    /// target_position is relative to the *actual* position rather than the
354    /// command position. Only meaningful when `set_relative(true)`.
355    pub fn set_relative_to_actual(&mut self, v: bool) {
356        let mut cw = self.pp_control();
357        cw.set_relative_to_actual(v);
358        self.set_pp_control(cw);
359    }
360
361    // ── PP status queries ──
362
363    /// Drive has reached the target position.
364    pub fn target_reached(&self) -> bool {
365        self.pp_status().pp_target_reached()
366    }
367
368    /// Set-point was acknowledged by the drive.
369    pub fn set_point_acknowledged(&self) -> bool {
370        self.pp_status().set_point_acknowledge()
371    }
372
373    /// Teknic: position within in-range window.
374    pub fn in_range(&self) -> bool {
375        self.pp_status().in_range()
376    }
377
378    /// Teknic bit 8 — Has Homed: persistent flag that remains set after any
379    /// successful homing procedure, even after switching back to PP or PV mode.
380    pub fn has_homed(&self) -> bool {
381        self.pp_status().has_homed()
382    }
383
384    /// Teknic: actual velocity has reached target.
385    pub fn at_velocity(&self) -> bool {
386        self.pp_status().at_velocity()
387    }
388
389    // ── Error / fault status ──
390
391    /// Bit 13 — Following Error: position tracking error exceeded the
392    /// configured limit. Typically leads to a fault transition.
393    pub fn following_error(&self) -> bool {
394        self.pp_status().following_error()
395    }
396
397    /// Bit 11 — Internal Limit Active: a hardware or software limit
398    /// is currently active (e.g. position or velocity limit).
399    pub fn internal_limit(&self) -> bool {
400        self.pp_status().internal_limit()
401    }
402
403    /// Bit 7 — Warning: a non-fatal warning condition exists.
404    pub fn warning(&self) -> bool {
405        self.pp_status().warning()
406    }
407
408    /// True if the drive is in Fault or Fault Reaction Active state.
409    pub fn is_faulted(&self) -> bool {
410        matches!(self.state(), Cia402State::Fault | Cia402State::FaultReactionActive)
411    }
412
413    // ── Homing ──
414
415    /// Set mode to Homing.
416    pub fn ensure_homing_mode(&mut self) {
417        *self.modes_of_operation = ModesOfOperation::Homing.as_i8();
418    }
419
420    /// Start the homing procedure (rising edge on bit 4).
421    ///
422    /// The drive must be in Operation Enabled state and homing mode.
423    /// Configure homing parameters (method, speeds, acceleration) via
424    /// SDO before calling this.
425    pub fn trigger_homing(&mut self) {
426        let mut cw = TeknicHomingControlWord(*self.control_word);
427        cw.set_homing_start(true);
428        *self.control_word = cw.raw();
429    }
430
431    /// Clear the homing start bit. Call after homing completes.
432    pub fn clear_homing_start(&mut self) {
433        let mut cw = TeknicHomingControlWord(*self.control_word);
434        cw.set_homing_start(false);
435        *self.control_word = cw.raw();
436    }
437
438    /// Decode the current homing progress from status word bits 10, 12, 13.
439    ///
440    /// Only meaningful when the drive is in Homing mode.
441    pub fn homing_progress(&self) -> HomingProgress {
442        let sw = TeknicHomingStatusWord(*self.status_word);
443        let attained = sw.homing_attained();
444        let reached  = sw.homing_target_reached();
445        let error    = sw.homing_error();
446
447        if error {
448            HomingProgress::Error
449        } else if attained && reached {
450            HomingProgress::Complete
451        } else if attained {
452            HomingProgress::Attained
453        } else if reached {
454            HomingProgress::Idle
455        } else {
456            HomingProgress::InProgress
457        }
458    }
459
460    // ── Feedback ──
461
462    /// Actual position in encoder counts.
463    pub fn position(&self) -> i32 {
464        *self.position_actual
465    }
466
467    /// Actual velocity in encoder counts/s.
468    pub fn velocity(&self) -> i32 {
469        *self.velocity_actual
470    }
471
472    /// Actual torque in per-mille of rated torque.
473    pub fn torque(&self) -> i16 {
474        *self.torque_actual
475    }
476
477    /// Mode the drive is currently operating in.
478    pub fn current_mode(&self) -> Option<ModesOfOperation> {
479        ModesOfOperation::from_i8(*self.modes_of_operation_display)
480    }
481}
482
483// ──────────────────────────────────────────────
484// AxisView implementation
485// ──────────────────────────────────────────────
486
487impl AxisView for TeknicPpView<'_> {
488    fn control_word(&self) -> u16 { *self.control_word }
489    fn set_control_word(&mut self, word: u16) { *self.control_word = word; }
490    fn status_word(&self) -> u16 { *self.status_word }
491    fn set_target_position(&mut self, pos: i32) { *self.target_position = pos; }
492    fn set_profile_velocity(&mut self, vel: u32) { *self.profile_velocity = vel; }
493    fn set_profile_acceleration(&mut self, accel: u32) { *self.profile_acceleration = accel; }
494    fn set_profile_deceleration(&mut self, decel: u32) { *self.profile_deceleration = decel; }
495    fn set_modes_of_operation(&mut self, mode: i8) { *self.modes_of_operation = mode; }
496    fn modes_of_operation_display(&self) -> i8 { *self.modes_of_operation_display }
497    fn position_actual(&self) -> i32 { *self.position_actual }
498    fn velocity_actual(&self) -> i32 { *self.velocity_actual }
499    // error_code: uses default (0) — Teknic error code not in standard view fields
500}
501
502// ──────────────────────────────────────────────
503// View factory macro
504// ──────────────────────────────────────────────
505
506/// Create a [`TeknicPpView`] by projecting GlobalMemory fields with a common prefix.
507///
508/// Fields must follow the naming convention `{prefix}_control_word`,
509/// `{prefix}_target_position`, etc.
510///
511/// # Example
512///
513/// ```ignore
514/// let mut view = teknic_pp_view!(ctx.gm, axis1);
515/// view.ensure_pp_mode();
516/// view.cmd_enable_operation();
517/// view.set_target(10_000, 5_000, 1_000, 1_000);
518/// view.trigger_move();
519/// ```
520#[macro_export]
521macro_rules! teknic_pp_view {
522    ($gm:expr, $prefix:ident) => {
523        paste::paste! {
524            $crate::teknic::view::TeknicPpView {
525                control_word:              &mut $gm.[<$prefix _control_word>],
526                target_position:           &mut $gm.[<$prefix _target_position>],
527                profile_velocity:          &mut $gm.[<$prefix _profile_velocity>],
528                profile_acceleration:      &mut $gm.[<$prefix _profile_acceleration>],
529                profile_deceleration:      &mut $gm.[<$prefix _profile_deceleration>],
530                modes_of_operation:        &mut $gm.[<$prefix _modes_of_operation>],
531                status_word:               & $gm.[<$prefix _status_word>],
532                position_actual:           & $gm.[<$prefix _position_actual>],
533                velocity_actual:           & $gm.[<$prefix _velocity_actual>],
534                torque_actual:             & $gm.[<$prefix _torque_actual>],
535                modes_of_operation_display: & $gm.[<$prefix _modes_of_operation_display>],
536            }
537        }
538    };
539}
540
541// ──────────────────────────────────────────────
542// Tests
543// ──────────────────────────────────────────────
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548
549    /// Local storage for PDO fields used in tests. Avoids depending on
550    /// auto-generated GlobalMemory field names.
551    #[derive(Default)]
552    struct TestPdo {
553        control_word: u16,
554        target_position: i32,
555        profile_velocity: u32,
556        profile_acceleration: u32,
557        profile_deceleration: u32,
558        modes_of_operation: i8,
559        status_word: u16,
560        position_actual: i32,
561        velocity_actual: i32,
562        torque_actual: i16,
563        modes_of_operation_display: i8,
564    }
565
566    impl TestPdo {
567        fn view(&mut self) -> TeknicPpView<'_> {
568            TeknicPpView {
569                control_word:              &mut self.control_word,
570                target_position:           &mut self.target_position,
571                profile_velocity:          &mut self.profile_velocity,
572                profile_acceleration:      &mut self.profile_acceleration,
573                profile_deceleration:      &mut self.profile_deceleration,
574                modes_of_operation:        &mut self.modes_of_operation,
575                status_word:               &self.status_word,
576                position_actual:           &self.position_actual,
577                velocity_actual:           &self.velocity_actual,
578                torque_actual:             &self.torque_actual,
579                modes_of_operation_display: &self.modes_of_operation_display,
580            }
581        }
582    }
583
584    // ── State machine ──
585
586    #[test]
587    fn test_view_reads_state() {
588        let mut pdo = TestPdo { status_word: 0x0040, ..Default::default() };
589        let view = pdo.view();
590        assert_eq!(view.state(), Cia402State::SwitchOnDisabled);
591    }
592
593    #[test]
594    fn test_view_cmd_shutdown() {
595        let mut pdo = TestPdo { status_word: 0x0040, ..Default::default() };
596        let mut view = pdo.view();
597        view.cmd_shutdown();
598        assert_eq!(*view.control_word & 0x008F, 0x0006);
599    }
600
601    #[test]
602    fn test_view_cmd_enable_operation() {
603        let mut pdo = TestPdo::default();
604        let mut view = pdo.view();
605        view.cmd_enable_operation();
606        assert_eq!(*view.control_word & 0x008F, 0x000F);
607    }
608
609    // ── PP motion ──
610
611    #[test]
612    fn test_view_set_target_and_trigger() {
613        let mut pdo = TestPdo::default();
614        let mut view = pdo.view();
615        view.cmd_enable_operation();
616        view.set_target(50_000, 10_000, 2_000, 2_000);
617        view.trigger_move();
618
619        assert_eq!(*view.target_position, 50_000);
620        assert_eq!(*view.profile_velocity, 10_000);
621        assert_eq!(*view.profile_acceleration, 2_000);
622        assert_eq!(*view.profile_deceleration, 2_000);
623        assert!(*view.control_word & (1 << 4) != 0);
624    }
625
626    #[test]
627    fn test_view_ack_set_point() {
628        let mut pdo = TestPdo::default();
629        let mut view = pdo.view();
630        view.cmd_enable_operation();
631        view.trigger_move();
632        assert!(*view.control_word & (1 << 4) != 0);
633
634        view.ack_set_point();
635        assert!(*view.control_word & (1 << 4) == 0);
636        assert_eq!(*view.control_word & 0x000F, 0x000F);
637    }
638
639    #[test]
640    fn test_view_absolute_move() {
641        let mut pdo = TestPdo::default();
642        let mut view = pdo.view();
643        view.cmd_enable_operation();
644        view.set_relative(false);
645        view.set_target(100_000, 50_000, 10_000, 10_000);
646        view.trigger_move();
647
648        assert_eq!(*view.control_word & (1 << 6), 0);
649        assert!(*view.control_word & (1 << 4) != 0);
650    }
651
652    #[test]
653    fn test_view_relative_move() {
654        let mut pdo = TestPdo::default();
655        let mut view = pdo.view();
656        view.cmd_enable_operation();
657        view.set_relative(true);
658        view.set_target(5_000, 10_000, 2_000, 2_000);
659        view.trigger_move();
660
661        assert!(*view.control_word & (1 << 6) != 0);
662    }
663
664    #[test]
665    fn test_view_relative_to_actual() {
666        let mut pdo = TestPdo::default();
667        let mut view = pdo.view();
668        view.cmd_enable_operation();
669        view.set_relative(true);
670        view.set_relative_to_actual(true);
671        view.set_target(1_000, 5_000, 1_000, 1_000);
672        view.trigger_move();
673
674        assert!(*view.control_word & (1 << 6) != 0);
675        assert!(*view.control_word & (1 << 13) != 0);
676    }
677
678    // ── Feedback ──
679
680    #[test]
681    fn test_view_feedback() {
682        let mut pdo = TestPdo {
683            position_actual: 12345,
684            velocity_actual: -500,
685            torque_actual: 100,
686            modes_of_operation_display: 1,
687            ..Default::default()
688        };
689        let view = pdo.view();
690
691        assert_eq!(view.position(), 12345);
692        assert_eq!(view.velocity(), -500);
693        assert_eq!(view.torque(), 100);
694        assert_eq!(view.current_mode(), Some(ModesOfOperation::ProfilePosition));
695    }
696
697    #[test]
698    fn test_view_teknic_status_bits() {
699        let mut pdo = TestPdo {
700            // Operation Enabled (0x27) + has_homed (bit 8) + in_range (bit 15)
701            status_word: 0x8127,
702            ..Default::default()
703        };
704        let view = pdo.view();
705
706        assert_eq!(view.state(), Cia402State::OperationEnabled);
707        assert!(view.has_homed());
708        assert!(view.in_range());
709        assert!(!view.at_velocity());
710    }
711
712    #[test]
713    fn test_view_ensure_pp_mode() {
714        let mut pdo = TestPdo::default();
715        let mut view = pdo.view();
716        view.ensure_pp_mode();
717        assert_eq!(*view.modes_of_operation, 1);
718    }
719
720    // ── Error / fault ──
721
722    #[test]
723    fn test_view_following_error() {
724        let mut pdo = TestPdo {
725            // Operation Enabled (0x27) + following error (bit 13)
726            status_word: 0x2027,
727            ..Default::default()
728        };
729        let view = pdo.view();
730
731        assert_eq!(view.state(), Cia402State::OperationEnabled);
732        assert!(view.following_error());
733        assert!(!view.is_faulted());
734    }
735
736    #[test]
737    fn test_view_internal_limit() {
738        let mut pdo = TestPdo { status_word: 0x0827, ..Default::default() };
739        let view = pdo.view();
740        assert!(view.internal_limit());
741    }
742
743    #[test]
744    fn test_view_warning() {
745        let mut pdo = TestPdo { status_word: 0x00A7, ..Default::default() };
746        let view = pdo.view();
747        assert_eq!(view.state(), Cia402State::OperationEnabled);
748        assert!(view.warning());
749    }
750
751    #[test]
752    fn test_view_is_faulted() {
753        let mut pdo = TestPdo::default();
754
755        pdo.status_word = 0x0008; // Fault
756        let view = pdo.view();
757        assert!(view.is_faulted());
758        assert_eq!(view.state(), Cia402State::Fault);
759
760        pdo.status_word = 0x000F; // Fault Reaction Active
761        let view = pdo.view();
762        assert!(view.is_faulted());
763
764        pdo.status_word = 0x0027; // Operation Enabled — not faulted
765        let view = pdo.view();
766        assert!(!view.is_faulted());
767    }
768
769    // ── Homing ──
770
771    #[test]
772    fn test_view_ensure_homing_mode() {
773        let mut pdo = TestPdo::default();
774        let mut view = pdo.view();
775        view.ensure_homing_mode();
776        assert_eq!(*view.modes_of_operation, 6);
777    }
778
779    #[test]
780    fn test_view_trigger_and_clear_homing() {
781        let mut pdo = TestPdo::default();
782        let mut view = pdo.view();
783        view.cmd_enable_operation();
784
785        // Bit 4 should be clear initially
786        assert_eq!(*view.control_word & (1 << 4), 0);
787
788        // Trigger homing — bit 4 set
789        view.trigger_homing();
790        assert!(*view.control_word & (1 << 4) != 0);
791        // State machine bits preserved
792        assert_eq!(*view.control_word & 0x000F, 0x000F);
793
794        // Clear homing start — bit 4 clear
795        view.clear_homing_start();
796        assert_eq!(*view.control_word & (1 << 4), 0);
797        assert_eq!(*view.control_word & 0x000F, 0x000F);
798    }
799
800    #[test]
801    fn test_homing_progress_idle() {
802        let mut pdo = TestPdo {
803            // Bit 10 = 1, bit 12 = 0, bit 13 = 0 → Idle (not started)
804            status_word: 0x0427, // OpEnabled + bit10
805            ..Default::default()
806        };
807        let view = pdo.view();
808        assert_eq!(view.homing_progress(), HomingProgress::Idle);
809    }
810
811    #[test]
812    fn test_homing_progress_in_progress() {
813        let mut pdo = TestPdo {
814            // Bit 10 = 0, bit 12 = 0, bit 13 = 0 → In Progress
815            status_word: 0x0027, // OpEnabled, bits 10/12/13 clear
816            ..Default::default()
817        };
818        let view = pdo.view();
819        assert_eq!(view.homing_progress(), HomingProgress::InProgress);
820    }
821
822    #[test]
823    fn test_homing_progress_attained() {
824        let mut pdo = TestPdo {
825            // Bit 12 = 1, bit 10 = 0, bit 13 = 0 → Attained
826            status_word: 0x1027,
827            ..Default::default()
828        };
829        let view = pdo.view();
830        assert_eq!(view.homing_progress(), HomingProgress::Attained);
831    }
832
833    #[test]
834    fn test_homing_progress_complete() {
835        let mut pdo = TestPdo {
836            // Bit 12 = 1, bit 10 = 1, bit 13 = 0 → Complete
837            status_word: 0x1427,
838            ..Default::default()
839        };
840        let view = pdo.view();
841        assert_eq!(view.homing_progress(), HomingProgress::Complete);
842    }
843
844    #[test]
845    fn test_homing_progress_error() {
846        let mut pdo = TestPdo {
847            // Bit 13 = 1 → Error (regardless of other bits)
848            status_word: 0x2027,
849            ..Default::default()
850        };
851        let view = pdo.view();
852        assert_eq!(view.homing_progress(), HomingProgress::Error);
853
854        // Error + attained + reached → still Error
855        pdo.status_word = 0x3427;
856        let view = pdo.view();
857        assert_eq!(view.homing_progress(), HomingProgress::Error);
858    }
859
860    #[test]
861    fn test_homing_has_homed_persists_across_modes() {
862        let mut pdo = TestPdo {
863            // OpEnabled + bit8 (has_homed)
864            status_word: 0x0127,
865            modes_of_operation_display: 1, // PP mode
866            ..Default::default()
867        };
868        let view = pdo.view();
869
870        assert!(view.has_homed());
871        assert_eq!(view.current_mode(), Some(ModesOfOperation::ProfilePosition));
872    }
873
874    // ── AxisView ──
875
876    #[test]
877    fn test_axis_view_impl() {
878        let mut pdo = TestPdo {
879            status_word: 0x0027,
880            position_actual: 5000,
881            velocity_actual: 1000,
882            modes_of_operation_display: 1,
883            ..Default::default()
884        };
885        let mut view = pdo.view();
886
887        // AxisView reads
888        assert_eq!(AxisView::status_word(&view), 0x0027);
889        assert_eq!(AxisView::position_actual(&view), 5000);
890        assert_eq!(AxisView::velocity_actual(&view), 1000);
891        assert_eq!(AxisView::modes_of_operation_display(&view), 1);
892
893        // AxisView writes
894        AxisView::set_control_word(&mut view, 0x000F);
895        assert_eq!(*view.control_word, 0x000F);
896        AxisView::set_target_position(&mut view, 10_000);
897        assert_eq!(*view.target_position, 10_000);
898    }
899}