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
226/// Teknic-specific convenience methods for `TeknicPpView` (borrowed).
227/// The macro parameter `$deref` controls whether field access dereferences
228/// a reference (`*`) or reads directly. Includes both standard CiA 402 methods
229/// and Teknic vendor-specific extensions (has_homed, in_range, at_velocity).
230macro_rules! impl_teknic_pp_methods {
231    ($T:ty, deref: $($d:tt)*) => {
232        impl $T {
233            // ── Typed access ──
234
235            /// Read the control word as a typed Teknic PP control word.
236            pub fn pp_control(&self) -> TeknicPpControlWord {
237                TeknicPpControlWord($($d)* self.control_word)
238            }
239
240            /// Read the status word as a typed Teknic PP status word.
241            pub fn pp_status(&self) -> TeknicPpStatusWord {
242                TeknicPpStatusWord($($d)* self.status_word)
243            }
244
245            /// Write back a modified PP control word.
246            pub fn set_pp_control(&mut self, cw: TeknicPpControlWord) {
247                $($d)* self.control_word = cw.raw();
248            }
249
250            // ── State machine ──
251
252            /// Current CiA 402 state.
253            pub fn state(&self) -> Cia402State {
254                self.pp_status().state()
255            }
256
257            /// Shutdown → Ready to Switch On.
258            pub fn cmd_shutdown(&mut self) {
259                let mut cw = self.pp_control();
260                cw.cmd_shutdown();
261                self.set_pp_control(cw);
262            }
263
264            /// Switch On → Switched On.
265            pub fn cmd_switch_on(&mut self) {
266                let mut cw = self.pp_control();
267                cw.cmd_switch_on();
268                self.set_pp_control(cw);
269            }
270
271            /// Enable Operation → Operation Enabled.
272            pub fn cmd_enable_operation(&mut self) {
273                let mut cw = self.pp_control();
274                cw.cmd_enable_operation();
275                self.set_pp_control(cw);
276            }
277
278            /// Disable Operation → Switched On.
279            pub fn cmd_disable_operation(&mut self) {
280                let mut cw = self.pp_control();
281                cw.cmd_disable_operation();
282                self.set_pp_control(cw);
283            }
284
285            /// Disable Voltage → Switch On Disabled.
286            pub fn cmd_disable_voltage(&mut self) {
287                let mut cw = self.pp_control();
288                cw.cmd_disable_voltage();
289                self.set_pp_control(cw);
290            }
291
292            /// Quick Stop → Quick Stop Active.
293            pub fn cmd_quick_stop(&mut self) {
294                let mut cw = self.pp_control();
295                cw.cmd_quick_stop();
296                self.set_pp_control(cw);
297            }
298
299            /// Fault Reset (rising edge on bit 7).
300            pub fn cmd_fault_reset(&mut self) {
301                let mut cw = self.pp_control();
302                cw.cmd_fault_reset();
303                self.set_pp_control(cw);
304            }
305
306            /// Clear/reset Fault Reset bit (bit 7).
307            pub fn cmd_clear_fault_reset(&mut self) {
308                let mut cw = self.pp_control();
309                cw.cmd_clear_fault_reset();
310                self.set_pp_control(cw);
311            }
312
313            // ── PP motion ──
314
315            /// Set mode to Profile Position.
316            pub fn ensure_pp_mode(&mut self) {
317                $($d)* self.modes_of_operation = ModesOfOperation::ProfilePosition.as_i8();
318            }
319
320            /// Configure move parameters (does not trigger the move).
321            pub fn set_target(&mut self, position: i32, velocity: u32, accel: u32, decel: u32) {
322                $($d)* self.target_position = position;
323                $($d)* self.profile_velocity = velocity;
324                $($d)* self.profile_acceleration = accel;
325                $($d)* self.profile_deceleration = decel;
326            }
327
328            /// Assert New Set-Point (bit 4). Call after set_target().
329            pub fn trigger_move(&mut self) {
330                let mut cw = self.pp_control();
331                cw.set_new_set_point(true);
332                self.set_pp_control(cw);
333            }
334
335            /// Clear New Set-Point. Call when set_point_acknowledged() is true.
336            pub fn ack_set_point(&mut self) {
337                let mut cw = self.pp_control();
338                cw.set_new_set_point(false);
339                self.set_pp_control(cw);
340            }
341
342            /// Bit 8 — Halt: decelerate to stop.
343            pub fn set_halt(&mut self, v: bool) {
344                let mut cw = self.pp_control();
345                cw.set_halt(v);
346                self.set_pp_control(cw);
347            }
348
349            /// Bit 6 — Relative: when true, target_position is relative to the
350            /// current *command* position. When false, target_position is absolute.
351            pub fn set_relative(&mut self, v: bool) {
352                let mut cw = self.pp_control();
353                cw.set_relative(v);
354                self.set_pp_control(cw);
355            }
356
357            /// Bit 13 (Teknic) — Relative to Actual: when true *and* bit 6 is set,
358            /// target_position is relative to the *actual* position rather than the
359            /// command position. Only meaningful when `set_relative(true)`.
360            pub fn set_relative_to_actual(&mut self, v: bool) {
361                let mut cw = self.pp_control();
362                cw.set_relative_to_actual(v);
363                self.set_pp_control(cw);
364            }
365
366            // ── PP status queries ──
367
368            /// Drive has reached the target position.
369            pub fn target_reached(&self) -> bool {
370                self.pp_status().pp_target_reached()
371            }
372
373            /// Set-point was acknowledged by the drive.
374            pub fn set_point_acknowledged(&self) -> bool {
375                self.pp_status().set_point_acknowledge()
376            }
377
378            /// Teknic: position within in-range window.
379            pub fn in_range(&self) -> bool {
380                self.pp_status().in_range()
381            }
382
383            /// Teknic bit 8 — Has Homed: persistent flag that remains set after any
384            /// successful homing procedure, even after switching back to PP or PV mode.
385            pub fn has_homed(&self) -> bool {
386                self.pp_status().has_homed()
387            }
388
389            /// Teknic: actual velocity has reached target.
390            pub fn at_velocity(&self) -> bool {
391                self.pp_status().at_velocity()
392            }
393
394            // ── Error / fault status ──
395
396            /// Bit 13 — Following Error: position tracking error exceeded the
397            /// configured limit. Typically leads to a fault transition.
398            pub fn following_error(&self) -> bool {
399                self.pp_status().following_error()
400            }
401
402            /// Bit 11 — Internal Limit Active: a hardware or software limit
403            /// is currently active (e.g. position or velocity limit).
404            pub fn internal_limit(&self) -> bool {
405                self.pp_status().internal_limit()
406            }
407
408            /// Bit 7 — Warning: a non-fatal warning condition exists.
409            pub fn warning(&self) -> bool {
410                self.pp_status().warning()
411            }
412
413            /// True if the drive is in Fault or Fault Reaction Active state.
414            pub fn is_faulted(&self) -> bool {
415                matches!(self.state(), Cia402State::Fault | Cia402State::FaultReactionActive)
416            }
417
418            // ── Homing ──
419
420            /// Set mode to Homing.
421            pub fn ensure_homing_mode(&mut self) {
422                $($d)* self.modes_of_operation = ModesOfOperation::Homing.as_i8();
423            }
424
425            /// Start the homing procedure (rising edge on bit 4).
426            pub fn trigger_homing(&mut self) {
427                let mut cw = TeknicHomingControlWord($($d)* self.control_word);
428                cw.set_homing_start(true);
429                $($d)* self.control_word = cw.raw();
430            }
431
432            /// Clear the homing start bit. Call after homing completes.
433            pub fn clear_homing_start(&mut self) {
434                let mut cw = TeknicHomingControlWord($($d)* self.control_word);
435                cw.set_homing_start(false);
436                $($d)* self.control_word = cw.raw();
437            }
438
439            /// Decode the current homing progress from status word bits 10, 12, 13.
440            pub fn homing_progress(&self) -> HomingProgress {
441                let sw = TeknicHomingStatusWord($($d)* self.status_word);
442                let attained = sw.homing_attained();
443                let reached  = sw.homing_target_reached();
444                let error    = sw.homing_error();
445
446                if error {
447                    HomingProgress::Error
448                } else if attained && reached {
449                    HomingProgress::Complete
450                } else if attained {
451                    HomingProgress::Attained
452                } else if reached {
453                    HomingProgress::Idle
454                } else {
455                    HomingProgress::InProgress
456                }
457            }
458
459            // ── Feedback ──
460
461            /// Actual position in encoder counts.
462            pub fn position(&self) -> i32 {
463                $($d)* self.position_actual
464            }
465
466            /// Actual velocity in encoder counts/s.
467            pub fn velocity(&self) -> i32 {
468                $($d)* self.velocity_actual
469            }
470
471            /// Actual torque in per-mille of rated torque.
472            pub fn torque(&self) -> i16 {
473                $($d)* self.torque_actual
474            }
475
476            /// Mode the drive is currently operating in.
477            pub fn current_mode(&self) -> Option<ModesOfOperation> {
478                ModesOfOperation::from_i8($($d)* self.modes_of_operation_display)
479            }
480        }
481    };
482}
483
484// Apply shared methods to TeknicPpView (dereferences references)
485impl_teknic_pp_methods!(TeknicPpView<'_>, deref: *);
486
487// ──────────────────────────────────────────────
488// AxisView implementation for TeknicPpView
489// ──────────────────────────────────────────────
490
491impl AxisView for TeknicPpView<'_> {
492    fn control_word(&self) -> u16 { *self.control_word }
493    fn set_control_word(&mut self, word: u16) { *self.control_word = word; }
494    fn status_word(&self) -> u16 { *self.status_word }
495    fn set_target_position(&mut self, pos: i32) { *self.target_position = pos; }
496    fn set_profile_velocity(&mut self, vel: u32) { *self.profile_velocity = vel; }
497    fn set_profile_acceleration(&mut self, accel: u32) { *self.profile_acceleration = accel; }
498    fn set_profile_deceleration(&mut self, decel: u32) { *self.profile_deceleration = decel; }
499    fn set_modes_of_operation(&mut self, mode: i8) { *self.modes_of_operation = mode; }
500    fn modes_of_operation_display(&self) -> i8 { *self.modes_of_operation_display }
501    fn position_actual(&self) -> i32 { *self.position_actual }
502    fn velocity_actual(&self) -> i32 { *self.velocity_actual }
503    // error_code: uses default (0) — Teknic error code not in standard view fields
504}
505
506// ──────────────────────────────────────────────
507// View factory macro
508// ──────────────────────────────────────────────
509
510/// Create a [`TeknicPpView`] by projecting GlobalMemory fields with a common prefix.
511///
512/// Fields must follow the naming convention `{prefix}_control_word`,
513/// `{prefix}_target_position`, etc.
514///
515/// # Example
516///
517/// ```ignore
518/// let mut view = teknic_pp_view!(ctx.gm, axis1);
519/// view.ensure_pp_mode();
520/// view.cmd_enable_operation();
521/// view.set_target(10_000, 5_000, 1_000, 1_000);
522/// view.trigger_move();
523/// ```
524#[macro_export]
525macro_rules! teknic_pp_view {
526    ($gm:expr, $prefix:ident) => {
527        paste::paste! {
528            $crate::teknic::view::TeknicPpView {
529                control_word:              &mut $gm.[<$prefix _control_word>],
530                target_position:           &mut $gm.[<$prefix _target_position>],
531                profile_velocity:          &mut $gm.[<$prefix _profile_velocity>],
532                profile_acceleration:      &mut $gm.[<$prefix _profile_acceleration>],
533                profile_deceleration:      &mut $gm.[<$prefix _profile_deceleration>],
534                modes_of_operation:        &mut $gm.[<$prefix _modes_of_operation>],
535                status_word:               & $gm.[<$prefix _status_word>],
536                position_actual:           & $gm.[<$prefix _position_actual>],
537                velocity_actual:           & $gm.[<$prefix _velocity_actual>],
538                torque_actual:             & $gm.[<$prefix _torque_actual>],
539                modes_of_operation_display: & $gm.[<$prefix _modes_of_operation_display>],
540            }
541        }
542    };
543}
544
545// ──────────────────────────────────────────────
546// Tests
547// ──────────────────────────────────────────────
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    /// Local storage for PDO fields used in tests. Avoids depending on
554    /// auto-generated GlobalMemory field names.
555    #[derive(Default)]
556    struct TestPdo {
557        control_word: u16,
558        target_position: i32,
559        profile_velocity: u32,
560        profile_acceleration: u32,
561        profile_deceleration: u32,
562        modes_of_operation: i8,
563        status_word: u16,
564        position_actual: i32,
565        velocity_actual: i32,
566        torque_actual: i16,
567        modes_of_operation_display: i8,
568    }
569
570    impl TestPdo {
571        fn view(&mut self) -> TeknicPpView<'_> {
572            TeknicPpView {
573                control_word:              &mut self.control_word,
574                target_position:           &mut self.target_position,
575                profile_velocity:          &mut self.profile_velocity,
576                profile_acceleration:      &mut self.profile_acceleration,
577                profile_deceleration:      &mut self.profile_deceleration,
578                modes_of_operation:        &mut self.modes_of_operation,
579                status_word:               &self.status_word,
580                position_actual:           &self.position_actual,
581                velocity_actual:           &self.velocity_actual,
582                torque_actual:             &self.torque_actual,
583                modes_of_operation_display: &self.modes_of_operation_display,
584            }
585        }
586    }
587
588    // ── State machine ──
589
590    #[test]
591    fn test_view_reads_state() {
592        let mut pdo = TestPdo { status_word: 0x0040, ..Default::default() };
593        let view = pdo.view();
594        assert_eq!(view.state(), Cia402State::SwitchOnDisabled);
595    }
596
597    #[test]
598    fn test_view_cmd_shutdown() {
599        let mut pdo = TestPdo { status_word: 0x0040, ..Default::default() };
600        let mut view = pdo.view();
601        view.cmd_shutdown();
602        assert_eq!(*view.control_word & 0x008F, 0x0006);
603    }
604
605    #[test]
606    fn test_view_cmd_enable_operation() {
607        let mut pdo = TestPdo::default();
608        let mut view = pdo.view();
609        view.cmd_enable_operation();
610        assert_eq!(*view.control_word & 0x008F, 0x000F);
611    }
612
613    // ── PP motion ──
614
615    #[test]
616    fn test_view_set_target_and_trigger() {
617        let mut pdo = TestPdo::default();
618        let mut view = pdo.view();
619        view.cmd_enable_operation();
620        view.set_target(50_000, 10_000, 2_000, 2_000);
621        view.trigger_move();
622
623        assert_eq!(*view.target_position, 50_000);
624        assert_eq!(*view.profile_velocity, 10_000);
625        assert_eq!(*view.profile_acceleration, 2_000);
626        assert_eq!(*view.profile_deceleration, 2_000);
627        assert!(*view.control_word & (1 << 4) != 0);
628    }
629
630    #[test]
631    fn test_view_ack_set_point() {
632        let mut pdo = TestPdo::default();
633        let mut view = pdo.view();
634        view.cmd_enable_operation();
635        view.trigger_move();
636        assert!(*view.control_word & (1 << 4) != 0);
637
638        view.ack_set_point();
639        assert!(*view.control_word & (1 << 4) == 0);
640        assert_eq!(*view.control_word & 0x000F, 0x000F);
641    }
642
643    #[test]
644    fn test_view_absolute_move() {
645        let mut pdo = TestPdo::default();
646        let mut view = pdo.view();
647        view.cmd_enable_operation();
648        view.set_relative(false);
649        view.set_target(100_000, 50_000, 10_000, 10_000);
650        view.trigger_move();
651
652        assert_eq!(*view.control_word & (1 << 6), 0);
653        assert!(*view.control_word & (1 << 4) != 0);
654    }
655
656    #[test]
657    fn test_view_relative_move() {
658        let mut pdo = TestPdo::default();
659        let mut view = pdo.view();
660        view.cmd_enable_operation();
661        view.set_relative(true);
662        view.set_target(5_000, 10_000, 2_000, 2_000);
663        view.trigger_move();
664
665        assert!(*view.control_word & (1 << 6) != 0);
666    }
667
668    #[test]
669    fn test_view_relative_to_actual() {
670        let mut pdo = TestPdo::default();
671        let mut view = pdo.view();
672        view.cmd_enable_operation();
673        view.set_relative(true);
674        view.set_relative_to_actual(true);
675        view.set_target(1_000, 5_000, 1_000, 1_000);
676        view.trigger_move();
677
678        assert!(*view.control_word & (1 << 6) != 0);
679        assert!(*view.control_word & (1 << 13) != 0);
680    }
681
682    // ── Feedback ──
683
684    #[test]
685    fn test_view_feedback() {
686        let mut pdo = TestPdo {
687            position_actual: 12345,
688            velocity_actual: -500,
689            torque_actual: 100,
690            modes_of_operation_display: 1,
691            ..Default::default()
692        };
693        let view = pdo.view();
694
695        assert_eq!(view.position(), 12345);
696        assert_eq!(view.velocity(), -500);
697        assert_eq!(view.torque(), 100);
698        assert_eq!(view.current_mode(), Some(ModesOfOperation::ProfilePosition));
699    }
700
701    #[test]
702    fn test_view_teknic_status_bits() {
703        let mut pdo = TestPdo {
704            // Operation Enabled (0x27) + has_homed (bit 8) + in_range (bit 15)
705            status_word: 0x8127,
706            ..Default::default()
707        };
708        let view = pdo.view();
709
710        assert_eq!(view.state(), Cia402State::OperationEnabled);
711        assert!(view.has_homed());
712        assert!(view.in_range());
713        assert!(!view.at_velocity());
714    }
715
716    #[test]
717    fn test_view_ensure_pp_mode() {
718        let mut pdo = TestPdo::default();
719        let mut view = pdo.view();
720        view.ensure_pp_mode();
721        assert_eq!(*view.modes_of_operation, 1);
722    }
723
724    // ── Error / fault ──
725
726    #[test]
727    fn test_view_following_error() {
728        let mut pdo = TestPdo {
729            // Operation Enabled (0x27) + following error (bit 13)
730            status_word: 0x2027,
731            ..Default::default()
732        };
733        let view = pdo.view();
734
735        assert_eq!(view.state(), Cia402State::OperationEnabled);
736        assert!(view.following_error());
737        assert!(!view.is_faulted());
738    }
739
740    #[test]
741    fn test_view_internal_limit() {
742        let mut pdo = TestPdo { status_word: 0x0827, ..Default::default() };
743        let view = pdo.view();
744        assert!(view.internal_limit());
745    }
746
747    #[test]
748    fn test_view_warning() {
749        let mut pdo = TestPdo { status_word: 0x00A7, ..Default::default() };
750        let view = pdo.view();
751        assert_eq!(view.state(), Cia402State::OperationEnabled);
752        assert!(view.warning());
753    }
754
755    #[test]
756    fn test_view_is_faulted() {
757        let mut pdo = TestPdo::default();
758
759        pdo.status_word = 0x0008; // Fault
760        let view = pdo.view();
761        assert!(view.is_faulted());
762        assert_eq!(view.state(), Cia402State::Fault);
763
764        pdo.status_word = 0x000F; // Fault Reaction Active
765        let view = pdo.view();
766        assert!(view.is_faulted());
767
768        pdo.status_word = 0x0027; // Operation Enabled — not faulted
769        let view = pdo.view();
770        assert!(!view.is_faulted());
771    }
772
773    // ── Homing ──
774
775    #[test]
776    fn test_view_ensure_homing_mode() {
777        let mut pdo = TestPdo::default();
778        let mut view = pdo.view();
779        view.ensure_homing_mode();
780        assert_eq!(*view.modes_of_operation, 6);
781    }
782
783    #[test]
784    fn test_view_trigger_and_clear_homing() {
785        let mut pdo = TestPdo::default();
786        let mut view = pdo.view();
787        view.cmd_enable_operation();
788
789        // Bit 4 should be clear initially
790        assert_eq!(*view.control_word & (1 << 4), 0);
791
792        // Trigger homing — bit 4 set
793        view.trigger_homing();
794        assert!(*view.control_word & (1 << 4) != 0);
795        // State machine bits preserved
796        assert_eq!(*view.control_word & 0x000F, 0x000F);
797
798        // Clear homing start — bit 4 clear
799        view.clear_homing_start();
800        assert_eq!(*view.control_word & (1 << 4), 0);
801        assert_eq!(*view.control_word & 0x000F, 0x000F);
802    }
803
804    #[test]
805    fn test_homing_progress_idle() {
806        let mut pdo = TestPdo {
807            // Bit 10 = 1, bit 12 = 0, bit 13 = 0 → Idle (not started)
808            status_word: 0x0427, // OpEnabled + bit10
809            ..Default::default()
810        };
811        let view = pdo.view();
812        assert_eq!(view.homing_progress(), HomingProgress::Idle);
813    }
814
815    #[test]
816    fn test_homing_progress_in_progress() {
817        let mut pdo = TestPdo {
818            // Bit 10 = 0, bit 12 = 0, bit 13 = 0 → In Progress
819            status_word: 0x0027, // OpEnabled, bits 10/12/13 clear
820            ..Default::default()
821        };
822        let view = pdo.view();
823        assert_eq!(view.homing_progress(), HomingProgress::InProgress);
824    }
825
826    #[test]
827    fn test_homing_progress_attained() {
828        let mut pdo = TestPdo {
829            // Bit 12 = 1, bit 10 = 0, bit 13 = 0 → Attained
830            status_word: 0x1027,
831            ..Default::default()
832        };
833        let view = pdo.view();
834        assert_eq!(view.homing_progress(), HomingProgress::Attained);
835    }
836
837    #[test]
838    fn test_homing_progress_complete() {
839        let mut pdo = TestPdo {
840            // Bit 12 = 1, bit 10 = 1, bit 13 = 0 → Complete
841            status_word: 0x1427,
842            ..Default::default()
843        };
844        let view = pdo.view();
845        assert_eq!(view.homing_progress(), HomingProgress::Complete);
846    }
847
848    #[test]
849    fn test_homing_progress_error() {
850        let mut pdo = TestPdo {
851            // Bit 13 = 1 → Error (regardless of other bits)
852            status_word: 0x2027,
853            ..Default::default()
854        };
855        let view = pdo.view();
856        assert_eq!(view.homing_progress(), HomingProgress::Error);
857
858        // Error + attained + reached → still Error
859        pdo.status_word = 0x3427;
860        let view = pdo.view();
861        assert_eq!(view.homing_progress(), HomingProgress::Error);
862    }
863
864    #[test]
865    fn test_homing_has_homed_persists_across_modes() {
866        let mut pdo = TestPdo {
867            // OpEnabled + bit8 (has_homed)
868            status_word: 0x0127,
869            modes_of_operation_display: 1, // PP mode
870            ..Default::default()
871        };
872        let view = pdo.view();
873
874        assert!(view.has_homed());
875        assert_eq!(view.current_mode(), Some(ModesOfOperation::ProfilePosition));
876    }
877
878    // ── AxisView ──
879
880    #[test]
881    fn test_axis_view_impl() {
882        let mut pdo = TestPdo {
883            status_word: 0x0027,
884            position_actual: 5000,
885            velocity_actual: 1000,
886            modes_of_operation_display: 1,
887            ..Default::default()
888        };
889        let mut view = pdo.view();
890
891        // AxisView reads
892        assert_eq!(AxisView::status_word(&view), 0x0027);
893        assert_eq!(AxisView::position_actual(&view), 5000);
894        assert_eq!(AxisView::velocity_actual(&view), 1000);
895        assert_eq!(AxisView::modes_of_operation_display(&view), 1);
896
897        // AxisView writes
898        AxisView::set_control_word(&mut view, 0x000F);
899        assert_eq!(*view.control_word, 0x000F);
900        AxisView::set_target_position(&mut view, 10_000);
901        assert_eq!(*view.target_position, 10_000);
902    }
903}