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}