Skip to main content

autocore_std/motion/
cia402.rs

1//! CiA 402 (CANopen drive profile) base types and traits.
2//!
3//! This module provides the standard CiA 402 state machine definitions,
4//! mode-of-operation enum, and trait-based abstractions for control word
5//! and status word bit manipulation.
6//!
7//! Layers:
8//! 1. **Raw newtypes** — [`RawControlWord`], [`RawStatusWord`]
9//! 2. **Base traits** — [`Cia402Control`], [`Cia402Status`], [`Cia402State`]
10//! 3. **Mode-specific traits** — [`PpControl`]/[`PpStatus`],
11//!    [`PvControl`]/[`PvStatus`], [`HomingControl`]/[`HomingStatus`]
12//!
13//! Vendor-specific extensions (e.g. Teknic, Yaskawa) build on top of these
14//! traits in their own crates.
15
16use strum_macros::FromRepr;
17use std::time::{Duration, Instant};
18
19use serde_json::json;
20
21use crate::fb::StateMachine;
22use super::axis_view::AxisView;
23use crate::command_client::CommandClient;
24use crate::ethercat::{SdoClient, SdoResult};
25
26
27/// Raw u16 newtype — this IS the PDO data, no conversion needed
28#[derive(Clone, Copy, Debug, Default)]
29pub struct RawControlWord(pub u16);
30
31/// Raw u16 newtype for the status word.
32#[derive(Clone, Copy, Debug, Default)]
33pub struct RawStatusWord(pub u16);
34
35// ──────────────────────────────────────────────
36// Layer 1: CiA 402 State Machine (all modes)
37// ──────────────────────────────────────────────
38
39/// State machine commands common to every CiA 402 mode.
40///
41/// Bits 0–3 and 7 are the state-machine control bits.
42/// Bits 4–6, 8 are mode-specific (see `PpControl`, `PvControl`, etc.).
43/// Bits 9–15 are manufacturer-specific.
44pub trait Cia402Control {
45    /// Read the raw control word.
46    fn raw(&self) -> u16;
47    /// Get a mutable reference to the raw control word.
48    fn raw_mut(&mut self) -> &mut u16;
49
50    // ── Individual bit setters ──
51
52    /// Set bit 0 — Switch On.
53    fn set_switch_on(&mut self, v: bool) {
54        self.set_bit(0, v);
55    }
56    /// Set bit 1 — Enable Voltage.
57    fn set_enable_voltage(&mut self, v: bool) {
58        self.set_bit(1, v);
59    }
60    /// Set bit 2 — Quick Stop.
61    fn set_quick_stop(&mut self, v: bool) {
62        self.set_bit(2, v);
63    }
64    /// Set bit 3 — Enable Operation.
65    fn set_enable_operation(&mut self, v: bool) {
66        self.set_bit(3, v);
67    }
68    /// Set bit 7 — Fault Reset.
69    fn set_fault_reset(&mut self, v: bool) {
70        self.set_bit(7, v);
71    }
72
73    // ── State-machine transition commands ──
74    //
75    // Each command sets bits 0–3,7 to the pattern required by CiA 402
76    // while preserving mode-specific and vendor bits (4–6, 8–15).
77    //
78    // The mask 0x008F covers bits 0,1,2,3,7.
79
80    /// Shutdown command (transitions 2, 6, 8).
81    /// Target state: Ready to Switch On.
82    fn cmd_shutdown(&mut self) {
83        let w = self.raw_mut();
84        *w = (*w & !0x008F) | 0x0006; // bits 1,2 set; 0,3,7 clear
85    }
86
87    /// Switch On command (transition 3).
88    /// Target state: Switched On.
89    fn cmd_switch_on(&mut self) {
90        let w = self.raw_mut();
91        *w = (*w & !0x008F) | 0x0007; // bits 0,1,2 set; 3,7 clear
92    }
93
94    /// Enable Operation command (transition 4, or combined 2+3+4).
95    /// Target state: Operation Enabled.
96    fn cmd_enable_operation(&mut self) {
97        let w = self.raw_mut();
98        *w = (*w & !0x008F) | 0x000F; // bits 0-3 set; 7 clear
99    }
100
101    /// Disable Operation command (transition 5).
102    /// Target state: Switched On.
103    fn cmd_disable_operation(&mut self) {
104        let w = self.raw_mut();
105        *w = (*w & !0x008F) | 0x0007; // bits 0,1,2 set; 3,7 clear
106    }
107
108    /// Disable Voltage command (transitions 7, 9, 10, 12).
109    /// Target state: Switch On Disabled.
110    fn cmd_disable_voltage(&mut self) {
111        let w = self.raw_mut();
112        *w &= !0x0082; // clear bits 1 and 7
113    }
114
115    /// Quick Stop command (transition 11).
116    /// Target state: Quick Stop Active.
117    fn cmd_quick_stop(&mut self) {
118        let w = self.raw_mut();
119        *w = (*w & !0x0086) | 0x0002; // bit 1 set; bits 2,7 clear
120    }
121
122    /// Fault Reset command (transition 15, rising edge on bit 7).
123    /// Drive must be in Fault state. Transitions to Switch On Disabled.
124    fn cmd_fault_reset(&mut self) {
125        let w = self.raw_mut();
126        *w |= 0x0080; // set bit 7
127    }
128
129
130    /// Clear the Fault Reset command (transition 15, rising edge on bit 7).
131    fn cmd_clear_fault_reset(&mut self) {
132        self.set_bit(7, false);
133    }
134
135    /// Set or clear a single bit in the control word.
136    fn set_bit(&mut self, bit: u8, v: bool) {
137        let w = self.raw_mut();
138        if v {
139            *w |= 1 << bit;
140        } else {
141            *w &= !(1 << bit);
142        }
143    }
144}
145
146/// CiA 402 status word decoding, common to every mode.
147///
148/// Bits 0–6: state machine state.
149/// Bit 7:    warning.
150/// Bit 9:    remote (drive is controlled via fieldbus).
151/// Bit 10:   target reached (mode-dependent meaning).
152/// Bits 8, 11–15: mode-specific or manufacturer-specific.
153pub trait Cia402Status {
154    /// Read the raw status word.
155    fn raw(&self) -> u16;
156
157    /// Bit 0 — Ready to Switch On.
158    fn ready_to_switch_on(&self) -> bool {
159        self.raw() & (1 << 0) != 0
160    }
161    /// Bit 1 — Switched On.
162    fn switched_on(&self) -> bool {
163        self.raw() & (1 << 1) != 0
164    }
165    /// Bit 2 — Operation Enabled.
166    fn operation_enabled(&self) -> bool {
167        self.raw() & (1 << 2) != 0
168    }
169    /// Bit 3 — Fault.
170    fn fault(&self) -> bool {
171        self.raw() & (1 << 3) != 0
172    }
173    /// Bit 4 — Voltage Enabled.
174    fn voltage_enabled(&self) -> bool {
175        self.raw() & (1 << 4) != 0
176    }
177    /// Bit 5 — Quick Stop Active.
178    fn quick_stop_active(&self) -> bool {
179        self.raw() & (1 << 5) != 0
180    }
181    /// Bit 6 — Switch On Disabled.
182    fn switch_on_disabled(&self) -> bool {
183        self.raw() & (1 << 6) != 0
184    }
185    /// Bit 7 — Warning.
186    fn warning(&self) -> bool {
187        self.raw() & (1 << 7) != 0
188    }
189    /// Bit 9 — Remote.
190    fn remote(&self) -> bool {
191        self.raw() & (1 << 9) != 0
192    }
193    /// Bit 10 — Target Reached.
194    fn target_reached(&self) -> bool {
195        self.raw() & (1 << 10) != 0
196    }
197
198    /// Decode the CiA 402 state machine state from status word bits.
199    ///
200    /// The state is encoded in bits 0–3, 5, 6. Some states have bit 5
201    /// as "don't care", so we use different masks:
202    ///  - 0x006F (bits 0,1,2,3,5,6) for states where bit 5 is defined
203    ///  - 0x004F (bits 0,1,2,3,6)   for states where bit 5 is "x"
204    fn state(&self) -> Cia402State {
205        let w = self.raw();
206        // Check most-specific patterns first (bit 5 defined → mask 0x006F)
207        if w & 0x006F == 0x0027 { return Cia402State::OperationEnabled; }
208        if w & 0x006F == 0x0023 { return Cia402State::SwitchedOn; }
209        if w & 0x006F == 0x0021 { return Cia402State::ReadyToSwitchOn; }
210        if w & 0x006F == 0x0007 { return Cia402State::QuickStopActive; }
211        // Less-specific patterns (bit 5 is don't-care → mask 0x004F)
212        if w & 0x004F == 0x000F { return Cia402State::FaultReactionActive; }
213        if w & 0x004F == 0x0008 { return Cia402State::Fault; }
214        if w & 0x004F == 0x0040 { return Cia402State::SwitchOnDisabled; }
215        if w & 0x004F == 0x0000 { return Cia402State::NotReadyToSwitchOn; }
216        Cia402State::Unknown
217    }
218}
219
220/// CiA 402 drive state machine states.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum Cia402State {
223    /// Not Ready to Switch On — drive is initializing.
224    NotReadyToSwitchOn,
225    /// Switch On Disabled — drive power stage is disabled.
226    SwitchOnDisabled,
227    /// Ready to Switch On — waiting for Switch On command.
228    ReadyToSwitchOn,
229    /// Switched On — power stage is energized but not enabled.
230    SwitchedOn,
231    /// Operation Enabled — drive is active and accepting motion commands.
232    OperationEnabled,
233    /// Quick Stop Active — drive is decelerating to stop.
234    QuickStopActive,
235    /// Fault Reaction Active — drive is executing fault reaction.
236    FaultReactionActive,
237    /// Fault — drive has faulted and requires a fault reset.
238    Fault,
239    /// Unknown — status word pattern did not match any defined state.
240    Unknown,
241}
242
243impl std::fmt::Display for Cia402State {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match self {
246            Self::NotReadyToSwitchOn => write!(f, "Not Ready to Switch On"),
247            Self::SwitchOnDisabled   => write!(f, "Switch On Disabled"),
248            Self::ReadyToSwitchOn    => write!(f, "Ready to Switch On"),
249            Self::SwitchedOn         => write!(f, "Switched On"),
250            Self::OperationEnabled   => write!(f, "Operation Enabled"),
251            Self::QuickStopActive    => write!(f, "Quick Stop Active"),
252            Self::FaultReactionActive => write!(f, "Fault Reaction Active"),
253            Self::Fault              => write!(f, "Fault"),
254            Self::Unknown            => write!(f, "Unknown"),
255        }
256    }
257}
258
259// ── Modes of Operation (0x6060 / 0x6061) ──
260
261/// CiA 402 Modes of Operation.
262///
263/// Written to RxPDO object 0x6060; read back from TxPDO object 0x6061.
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265#[repr(i8)]
266pub enum ModesOfOperation {
267    /// Profile Position mode (PP).
268    ProfilePosition          =  1,
269    /// Profile Velocity mode (PV).
270    ProfileVelocity          =  3,
271    /// Homing mode.
272    Homing                   =  6,
273    /// Interpolated Position mode (IP).
274    InterpolatedPosition     =  7,
275    /// Cyclic Synchronous Position mode (CSP).
276    CyclicSynchronousPosition = 8,
277    /// Cyclic Synchronous Velocity mode (CSV).
278    CyclicSynchronousVelocity = 9,
279    /// Cyclic Synchronous Torque mode (CST).
280    CyclicSynchronousTorque  = 10,
281}
282
283impl ModesOfOperation {
284    /// Convert an i8 to a `ModesOfOperation` variant, if valid.
285    pub fn from_i8(v: i8) -> Option<Self> {
286        match v {
287            1  => Some(Self::ProfilePosition),
288            3  => Some(Self::ProfileVelocity),
289            6  => Some(Self::Homing),
290            7  => Some(Self::InterpolatedPosition),
291            8  => Some(Self::CyclicSynchronousPosition),
292            9  => Some(Self::CyclicSynchronousVelocity),
293            10 => Some(Self::CyclicSynchronousTorque),
294            _  => None,
295        }
296    }
297
298    /// Convert to the underlying i8 value.
299    pub fn as_i8(self) -> i8 {
300        self as i8
301    }
302}
303
304impl std::fmt::Display for ModesOfOperation {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        match self {
307            Self::ProfilePosition           => write!(f, "Profile Position (PP)"),
308            Self::ProfileVelocity           => write!(f, "Profile Velocity (PV)"),
309            Self::Homing                    => write!(f, "Homing"),
310            Self::InterpolatedPosition      => write!(f, "Interpolated Position (IP)"),
311            Self::CyclicSynchronousPosition => write!(f, "Cyclic Synchronous Position (CSP)"),
312            Self::CyclicSynchronousVelocity => write!(f, "Cyclic Synchronous Velocity (CSV)"),
313            Self::CyclicSynchronousTorque   => write!(f, "Cyclic Synchronous Torque (CST)"),
314        }
315    }
316}
317
318// Implement base traits for the raw newtypes
319impl Cia402Control for RawControlWord {
320    fn raw(&self) -> u16 {
321        self.0
322    }
323    fn raw_mut(&mut self) -> &mut u16 {
324        &mut self.0
325    }
326}
327impl Cia402Status for RawStatusWord {
328    fn raw(&self) -> u16 {
329        self.0
330    }
331}
332
333// ──────────────────────────────────────────────
334// Layer 2: Profile Position mode bits (PP)
335// ──────────────────────────────────────────────
336
337/// Profile Position (PP) mode control word bits.
338pub trait PpControl: Cia402Control {
339    /// Bit 4 — New Set-Point: rising edge starts a new positioning move.
340    fn set_new_set_point(&mut self, v: bool) {
341        self.set_bit(4, v);
342    }
343    /// Bit 5 — Change Set Immediately: if true, interrupt current move.
344    fn set_change_set_immediately(&mut self, v: bool) {
345        self.set_bit(5, v);
346    }
347    /// Bit 6 — Relative: target position is relative to current.
348    fn set_relative(&mut self, v: bool) {
349        self.set_bit(6, v);
350    }
351    /// Bit 8 — Halt: decelerate to stop.
352    fn set_halt(&mut self, v: bool) {
353        self.set_bit(8, v);
354    }
355}
356
357/// Profile Position (PP) mode status word bits.
358pub trait PpStatus: Cia402Status {
359    /// Bit 10 — Target Reached: positioning move completed.
360    fn pp_target_reached(&self) -> bool {
361        self.raw() & (1 << 10) != 0
362    }
363    /// Bit 11 — Internal Limit Active.
364    fn internal_limit(&self) -> bool {
365        self.raw() & (1 << 11) != 0
366    }
367    /// Bit 12 — Set-Point Acknowledge: drive accepted the new set-point.
368    fn set_point_acknowledge(&self) -> bool {
369        self.raw() & (1 << 12) != 0
370    }
371    /// Bit 13 — Following Error: position tracking error exceeded limit.
372    fn following_error(&self) -> bool {
373        self.raw() & (1 << 13) != 0
374    }
375}
376
377// ──────────────────────────────────────────────
378// Layer 2: Profile Velocity mode bits (PV)
379// ──────────────────────────────────────────────
380
381/// Profile Velocity (PV) mode control word bits.
382pub trait PvControl: Cia402Control {
383    /// Bit 8 — Halt: decelerate to zero velocity.
384    fn set_halt(&mut self, v: bool) {
385        self.set_bit(8, v);
386    }
387}
388
389/// Profile Velocity (PV) mode status word bits.
390pub trait PvStatus: Cia402Status {
391    /// Bit 10 — Target Reached: actual velocity equals target velocity.
392    fn pv_target_reached(&self) -> bool {
393        self.raw() & (1 << 10) != 0
394    }
395    /// Bit 11 — Internal Limit Active.
396    fn pv_internal_limit(&self) -> bool {
397        self.raw() & (1 << 11) != 0
398    }
399    /// Bit 12 — Speed: 0 = velocity != 0, 1 = velocity = 0.
400    fn speed_is_zero(&self) -> bool {
401        self.raw() & (1 << 12) != 0
402    }
403    /// Bit 13 — Max Slippage Error (AC motors; not used by ClearPath-EC).
404    fn max_slippage_error(&self) -> bool {
405        self.raw() & (1 << 13) != 0
406    }
407}
408
409// ──────────────────────────────────────────────
410// Layer 2: Homing mode bits
411// ──────────────────────────────────────────────
412
413/// Homing mode control word bits.
414pub trait HomingControl: Cia402Control {
415    /// Bit 4 — Homing Operation Start: rising edge starts homing.
416    fn set_homing_start(&mut self, v: bool) {
417        self.set_bit(4, v);
418    }
419    /// Bit 8 — Halt: interrupt homing and decelerate.
420    fn set_halt(&mut self, v: bool) {
421        self.set_bit(8, v);
422    }
423}
424
425/// Homing mode status word bits.
426pub trait HomingStatus: Cia402Status {
427    /// Bit 10 — Target Reached: homing target position reached.
428    fn homing_target_reached(&self) -> bool {
429        self.raw() & (1 << 10) != 0
430    }
431    /// Bit 12 — Homing Attained: homing procedure completed successfully.
432    fn homing_attained(&self) -> bool {
433        self.raw() & (1 << 12) != 0
434    }
435    /// Bit 13 — Homing Error: homing procedure failed.
436    fn homing_error(&self) -> bool {
437        self.raw() & (1 << 13) != 0
438    }
439}
440
441// Implement PP traits for RawControlWord / RawStatusWord
442impl PpControl for RawControlWord {}
443impl PpStatus for RawStatusWord {}
444impl PvControl for RawControlWord {}
445impl PvStatus for RawStatusWord {}
446impl HomingControl for RawControlWord {}
447impl HomingStatus for RawStatusWord {}
448
449// ──────────────────────────────────────────────
450// Tests
451// ──────────────────────────────────────────────
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn test_state_decoding() {
459        // Standard patterns
460        assert_eq!(RawStatusWord(0x0000).state(), Cia402State::NotReadyToSwitchOn);
461        assert_eq!(RawStatusWord(0x0040).state(), Cia402State::SwitchOnDisabled);
462        assert_eq!(RawStatusWord(0x0021).state(), Cia402State::ReadyToSwitchOn);
463        assert_eq!(RawStatusWord(0x0023).state(), Cia402State::SwitchedOn);
464        assert_eq!(RawStatusWord(0x0027).state(), Cia402State::OperationEnabled);
465        assert_eq!(RawStatusWord(0x0007).state(), Cia402State::QuickStopActive);
466        assert_eq!(RawStatusWord(0x000F).state(), Cia402State::FaultReactionActive);
467        assert_eq!(RawStatusWord(0x0008).state(), Cia402State::Fault);
468    }
469
470    #[test]
471    fn test_state_decoding_bit5_dont_care() {
472        // States where bit 5 is "don't care" must decode correctly
473        // regardless of whether bit 5 is 0 or 1.
474
475        // Not Ready to Switch On: bit 5 = 1 → still NRTSO
476        assert_eq!(RawStatusWord(0x0020).state(), Cia402State::NotReadyToSwitchOn);
477        // Switch On Disabled: bit 5 = 1
478        assert_eq!(RawStatusWord(0x0060).state(), Cia402State::SwitchOnDisabled);
479        // Fault Reaction Active: bit 5 = 1
480        assert_eq!(RawStatusWord(0x002F).state(), Cia402State::FaultReactionActive);
481        // Fault: bit 5 = 1
482        assert_eq!(RawStatusWord(0x0028).state(), Cia402State::Fault);
483    }
484
485    #[test]
486    fn test_state_decoding_ignores_high_bits() {
487        // Bits 8+ should not affect state decoding
488        assert_eq!(RawStatusWord(0xFF27).state(), Cia402State::OperationEnabled);
489        assert_eq!(RawStatusWord(0x8040).state(), Cia402State::SwitchOnDisabled);
490    }
491
492    #[test]
493    fn test_cmd_shutdown() {
494        let mut cw = RawControlWord(0xFF00);
495        cw.cmd_shutdown();
496        // Bits 1,2 set; bits 0,3,7 clear; bits 4-6,8-15 preserved
497        assert_eq!(cw.0, 0xFF06);
498    }
499
500    #[test]
501    fn test_cmd_enable_operation() {
502        let mut cw = RawControlWord(0x0000);
503        cw.cmd_enable_operation();
504        assert_eq!(cw.0, 0x000F);
505    }
506
507    #[test]
508    fn test_cmd_fault_reset() {
509        let mut cw = RawControlWord(0x0000);
510        cw.cmd_fault_reset();
511        assert!(cw.0 & 0x0080 != 0); // bit 7 set
512    }
513
514    #[test]
515    fn test_modes_of_operation_roundtrip() {
516        for mode in [
517            ModesOfOperation::ProfilePosition,
518            ModesOfOperation::ProfileVelocity,
519            ModesOfOperation::Homing,
520        ] {
521            assert_eq!(ModesOfOperation::from_i8(mode.as_i8()), Some(mode));
522        }
523        assert_eq!(ModesOfOperation::from_i8(99), None);
524    }
525}
526
527
528
529#[repr(i32)]
530#[derive(Copy, Clone, Debug, FromRepr)]
531enum ModeOfOperationStates {
532    Reset = 0,
533    WriteModeOp,
534    WaitWriteModeOp,
535    ReadModeOp,
536    WaitReadModeOp
537}
538
539/// Used to set the mode of operation 0x6060 of a Ci402 drive using
540/// SDO writes. The mode is confirmed using 0x6061 mode of operation
541/// display.
542#[derive(Clone, Debug)]
543pub struct FbSetModeOfOperation {
544    /// state machine fb
545    state : StateMachine,
546    /// target mode to write to the drive
547    target_mode : i8,
548    tid : u32,
549    retry_count : u16
550}
551
552impl FbSetModeOfOperation {
553    /// Constructor
554    pub fn new() -> Self {
555        Self {
556            state : StateMachine::new(),
557            target_mode : 0,
558            tid : 0,
559            retry_count : 0
560        }
561    }
562
563    pub fn reset(&mut self) {
564        self.state.error_code = 0;
565        self.state.error_message.clear();
566        self.state.index = ModeOfOperationStates::Reset as i32;
567    }
568
569    pub fn start(&mut self, target_mode : i8) {
570        self.target_mode = target_mode;
571        self.retry_count = 0;
572        self.state.error_code = 0;
573        self.state.error_message.clear();
574        self.state.index = ModeOfOperationStates::WriteModeOp as i32;
575    }
576
577    pub fn is_busy(&self) -> bool {
578        return self.state.index > ModeOfOperationStates::Reset as i32;
579    }
580
581    pub fn is_error(&self) -> bool {
582        return self.state.is_error();
583    }
584
585    pub fn error_code(&self) -> i32 {
586        return self.state.error_code;
587    }
588
589    pub fn error_message(&self) -> String {
590        return self.state.error_message.clone();
591    }
592
593    pub fn tick(&mut self, client: &mut CommandClient, sdo: &mut SdoClient) {
594        match ModeOfOperationStates::from_repr(self.state.index) {
595            Some(ModeOfOperationStates::Reset) => {
596                // do nothing
597            },
598            Some(ModeOfOperationStates::WriteModeOp) => {                
599                // Switch the mode of operation into Homing Mode so that we can execute
600                // the homing command.
601                self.tid = sdo.write(
602                    client, 0x6060, 0, json!(self.target_mode),
603                );
604                self.state.index = ModeOfOperationStates::WaitWriteModeOp as i32;
605                self.state.timeout_preset = Duration::from_secs(7);
606                log::info!("FbSetModeOfOperation: Waiting write complete target_mode {}", self.target_mode);
607            },       
608            Some(ModeOfOperationStates::WaitWriteModeOp) => {
609                // Wait for method SDO ack
610                match sdo.result(client, self.tid, Duration::from_secs(5)) {
611                    SdoResult::Ok(_) => { 
612                        log::info!("FbSetModeOfOperation: write complete.");
613                        self.state.index = ModeOfOperationStates::ReadModeOp as i32;
614                        self.state.timer_preset = Duration::from_millis(100);
615                    }
616                    SdoResult::Pending => {
617                        if self.state.timed_out() {
618                            self.state.error_code = ModeOfOperationStates::WaitWriteModeOp as i32 * 10;
619                            self.state.error_message = "Timeout waiting for SDO write to complete".to_string();
620                            self.state.index = ModeOfOperationStates::Reset as i32;
621                        }
622                    }
623                    SdoResult::Err(e) => {
624                        self.state.error_code = ModeOfOperationStates::WaitWriteModeOp as i32 * 10 + 1;
625                        self.state.error_message = format!("SDO write error {}", e);
626                        self.state.index = ModeOfOperationStates::Reset as i32;
627                    }
628                    SdoResult::Timeout => {
629                        self.state.error_code = ModeOfOperationStates::WaitWriteModeOp as i32 * 10 + 2;
630                        self.state.error_message = "SDO write resulted in timeout".to_string();
631                        self.state.index = ModeOfOperationStates::Reset as i32;
632                    }
633                }
634            },
635            Some(ModeOfOperationStates::ReadModeOp) => {
636                if self.state.timer_done() {
637                    // Setting a requested operating mode doesn't mean we're in it.
638                    // Let's read back the mode of operation to ensure
639                    log::info!("FbSetModeOfOperation: Starting read to validate operation mode.");
640                    self.tid = sdo.read(
641                        client, 0x6061, 0
642                    );
643                    self.state.index = ModeOfOperationStates::WaitReadModeOp as i32;
644                    self.state.timeout_preset = Duration::from_secs(7);
645                }
646            },            
647            Some(ModeOfOperationStates::WaitReadModeOp) => {
648                // Wait for method SDO ack
649                match sdo.result(client, self.tid, Duration::from_secs(5)) {
650                    SdoResult::Ok(val) => { 
651                        log::info!("FbSetModeOfOperation: read complete.");
652                        if let Some(val_num) = val["value"].as_i64() {
653                            if val_num as i8 == self.target_mode {
654                                log::info!("Successfully changed motor mode of operation to {}", self.target_mode);
655                                self.state.index = ModeOfOperationStates::Reset as i32;
656                            }
657                            else {
658                                self.retry_count += 1;
659                                if self.retry_count > 3 {                                    
660                                    self.state.error_code = ModeOfOperationStates::WaitReadModeOp as i32 * 10 + 3;
661                                    self.state.error_message = format!("Drive did not transition to operation mode {} from {}", 
662                                        self.target_mode, val_num as i8
663                                    );
664                                    self.state.index = ModeOfOperationStates::Reset as i32;
665                                }
666                                else {
667                                    log::info!("FbSetModeOfOperation: Operation mode does not match. Retrying...");
668                                    self.state.timer_preset = Duration::from_millis(50);
669                                    self.state.index = ModeOfOperationStates::ReadModeOp as i32;
670                                }
671                            }
672                        }
673                        else {
674                            self.state.error_code = ModeOfOperationStates::WaitReadModeOp as i32 * 10 + 4;
675                            self.state.error_message = format!("Invalid value received from drive SDO read 0x6061:01  [{}]", val );
676                            self.state.index = ModeOfOperationStates::Reset as i32;                            
677                        }
678                    }
679                    SdoResult::Pending => {
680                        if self.state.timed_out() {
681                            self.state.error_code = ModeOfOperationStates::WaitReadModeOp as i32 * 10;
682                            self.state.error_message = "Timeout waiting for SDO write to complete".to_string();
683                            self.state.index = ModeOfOperationStates::Reset as i32;
684                        }
685                    }
686                    SdoResult::Err(e) => {
687                        self.state.error_code = ModeOfOperationStates::WaitReadModeOp as i32 * 10 + 1;
688                        self.state.error_message = format!("SDO write error {}", e);
689                        self.state.index = ModeOfOperationStates::Reset as i32;
690                    }
691                    SdoResult::Timeout => {
692                        self.state.error_code = ModeOfOperationStates::WaitReadModeOp as i32 * 10 + 2;
693                        self.state.error_message = "SDO write resulted in timeout".to_string();
694                        self.state.index = ModeOfOperationStates::Reset as i32;
695                    }
696                }
697            },
698            None => {
699                self.state.index = ModeOfOperationStates::Reset as i32;
700            }
701        }
702
703
704        // tick this state machine
705        self.state.call();
706    }
707
708
709}
710
711