1use std::collections::VecDeque;
25use std::time::{Duration, Instant};
26
27use serde_json::json;
28use strum_macros::FromRepr;
29
30use super::axis_config::AxisConfig;
31use super::axis_view::AxisView;
32use super::cia402::{
33 Cia402Control, Cia402State, Cia402Status, ModesOfOperation, RawControlWord, RawStatusWord,
34};
35use super::homing::HomingMethod;
36use crate::command_client::CommandClient;
37use crate::ethercat::{SdoClient, SdoResult};
38use crate::fb::Ton;
39use crate::motion::FbSetModeOfOperation;
40
41#[derive(Debug, Clone, PartialEq)]
46enum AxisOp {
47 Idle,
48 Enabling(u8),
49 Disabling(u8),
50 Moving(MoveKind, u8, bool, bool),
51 Homing(u8),
52 SoftHoming(u8),
53 Halting(u8),
54 FaultRecovery(u8),
55}
56
57#[repr(u8)]
68#[derive(Debug, Clone, PartialEq, FromRepr)]
69enum HaltState {
70 WaitStopped = 0,
73 Flushing = 10,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq)]
84enum FlushPhase {
85 Idle,
86 Away,
88 Back,
90 Done,
91 Error,
92}
93
94#[derive(Debug, Clone)]
106struct FbFlushQueue {
107 phase: FlushPhase,
108 mode_fb: FbSetModeOfOperation,
109}
110
111impl FbFlushQueue {
112 fn new() -> Self {
113 Self { phase: FlushPhase::Idle, mode_fb: FbSetModeOfOperation::new() }
114 }
115
116 fn start(&mut self, view: &mut impl AxisView, client: &mut CommandClient, sdo: &mut SdoClient) {
118 self.begin_switch(view, client, sdo, ModesOfOperation::Homing);
119 self.phase = FlushPhase::Away;
120 }
121
122 fn begin_switch(
123 &mut self,
124 view: &mut impl AxisView,
125 client: &mut CommandClient,
126 sdo: &mut SdoClient,
127 mode: ModesOfOperation,
128 ) {
129 view.set_modes_of_operation(mode.as_i8());
130 self.mode_fb.reset();
131 self.mode_fb.start(mode.as_i8());
132 self.mode_fb.tick(client, sdo);
133 }
134
135 fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient, sdo: &mut SdoClient) {
136 match self.phase {
137 FlushPhase::Away => {
138 self.mode_fb.tick(client, sdo);
139 if !self.mode_fb.is_busy() {
140 if self.mode_fb.is_error() {
141 self.phase = FlushPhase::Error;
142 } else {
143 self.begin_switch(view, client, sdo, ModesOfOperation::ProfilePosition);
145 self.phase = FlushPhase::Back;
146 }
147 }
148 }
149 FlushPhase::Back => {
150 self.mode_fb.tick(client, sdo);
151 if !self.mode_fb.is_busy() {
152 self.phase = if self.mode_fb.is_error() {
153 FlushPhase::Error
154 } else {
155 FlushPhase::Done
156 };
157 }
158 }
159 _ => {}
160 }
161 }
162
163 fn is_busy(&self) -> bool {
164 matches!(self.phase, FlushPhase::Away | FlushPhase::Back)
165 }
166 fn is_error(&self) -> bool {
167 self.phase == FlushPhase::Error
168 }
169 fn error_code(&self) -> i32 {
170 self.mode_fb.error_code()
171 }
172 fn error_message(&self) -> String {
173 self.mode_fb.error_message()
174 }
175}
176
177const HALT_STAGE_TIMEOUT: Duration = Duration::from_secs(3);
179
180const HALT_STABLE_WINDOW: i32 = 50;
185
186const HALT_STOPPED_VELOCITY: i32 = 100;
192
193const HALT_STABLE_TICKS_REQUIRED: u8 = 5;
197
198#[repr(u8)]
199#[derive(Debug, Clone, PartialEq, FromRepr)]
200enum HomeState {
201 EnsurePpMode = 0,
202 WaitPpMode = 1,
203 Search = 5,
204 WaitSearching = 10,
205 WaitFoundSensor = 20,
206 WaitStoppedFoundSensor = 30,
207 FlushingFound = 40,
209 DebounceFoundSensor = 50,
210 BackOff = 60,
211 WaitBackingOff = 70,
212 WaitLostSensor = 80,
213 WaitStoppedLostSensor = 90,
214 FlushingLost = 100,
216 WriteHomeOffset = 120,
219 WaitHomeOffsetDone = 125,
220 WaitHomeOffsetReadback = 130,
222
223 WriteHomingModeOp = 160,
224 WaitWriteHomingModeOp = 165,
225
226 WriteHomingMethod = 205,
227 WaitWriteHomingMethodDone = 210,
228 ClearHomingTrigger = 215,
229 TriggerHoming = 217,
230 WaitHomingStarted = 218,
231 WaitHomingDone = 220,
232 ResetHomingTrigger = 222,
233 WaitHomingTriggerCleared = 223,
234 AlignTargetBeforePP = 225,
238 WriteMotionModeOfOperation = 230,
239 WaitWriteMotionModeOfOperation = 235,
240 SendCurrentPositionTarget = 240,
241 WaitCurrentPositionTargetSent = 245,
242}
243
244#[derive(Debug, Clone, PartialEq)]
245enum MoveKind {
246 Absolute,
247 Relative,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq)]
251enum SoftHomeSensor {
252 PositiveLimit,
253 NegativeLimit,
254 HomeSensor,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq)]
258enum SoftHomeSensorType {
259 Pnp,
261 Npn,
263}
264
265const CW_SW_TRACE_CAP: usize = 48;
278
279#[derive(Clone)]
282struct TraceEntry {
283 seq: u32,
284 op: AxisOp,
285 cw: u16,
286 sw: u16,
287}
288
289fn decode_cw(cw: u16) -> String {
291 const BITS: [(u16, &str); 9] = [
292 (0, "SwOn"),
293 (1, "EnVolt"),
294 (2, "QStop"),
295 (3, "EnOp"),
296 (4, "NewSP"),
297 (5, "ChgSet"),
298 (6, "Rel"),
299 (7, "FltRst"),
300 (8, "HALT"),
301 ];
302 let f: Vec<&str> = BITS
303 .iter()
304 .filter(|(b, _)| cw & (1 << b) != 0)
305 .map(|(_, n)| *n)
306 .collect();
307 if f.is_empty() {
308 "-".to_string()
309 } else {
310 f.join("|")
311 }
312}
313
314fn decode_sw(sw: u16) -> String {
316 let mut f = vec![format!("{}", RawStatusWord(sw).state())];
317 for (bit, name) in [
318 (10, "TgtReached"),
319 (11, "IntLim"),
320 (12, "SP_ACK"),
321 (13, "FollowErr"),
322 ] {
323 if sw & (1 << bit) != 0 {
324 f.push(name.to_string());
325 }
326 }
327 f.join("|")
328}
329
330pub struct Axis {
331 config: AxisConfig,
332 sdo: SdoClient,
333
334 op: AxisOp,
336 home_offset: i32,
337 last_raw_position: i32,
338 op_started: Option<Instant>,
339 op_timeout: Duration,
340 homing_timeout: Duration,
341 move_start_timeout: Duration,
342 pending_move_target: i32,
343 pending_move_vel: u32,
344 pending_move_accel: u32,
345 pending_move_decel: u32,
346 homing_method: i8,
347 homing_sdo_tid: u32,
348 soft_home_sensor: SoftHomeSensor,
349 soft_home_sensor_type: SoftHomeSensorType,
350 soft_home_direction: f64,
351 halt_stable_count: u8,
352 prev_positive_limit: bool,
353 prev_negative_limit: bool,
354 prev_home_sensor: bool,
355
356 cw_sw_trace: VecDeque<TraceEntry>,
358 trace_seq: u32,
359
360 fb_mode_of_operation: FbSetModeOfOperation,
361 flush: FbFlushQueue,
364
365 pub is_error: bool,
368 pub error_code: u32,
370 pub error_message: String,
372 pub motor_on: bool,
374 pub is_busy: bool,
380 pub in_motion: bool,
382 pub moving_positive: bool,
384 pub moving_negative: bool,
386 pub position: f64,
388 pub raw_position: i64,
390 pub speed: f64,
392 pub at_max_limit: bool,
394 pub at_min_limit: bool,
396 pub at_positive_limit_switch: bool,
398 pub at_negative_limit_switch: bool,
400 pub home_sensor: bool,
402
403 ton: Ton,
405}
406
407impl Axis {
408 pub fn new(config: AxisConfig, device_name: &str) -> Self {
413 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
414 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
415 let move_start_timeout = op_timeout; Self {
417 config,
418 sdo: SdoClient::new(device_name),
419 op: AxisOp::Idle,
420 home_offset: 0,
421 last_raw_position: 0,
422 op_started: None,
423 op_timeout,
424 homing_timeout,
425 move_start_timeout,
426 pending_move_target: 0,
427 pending_move_vel: 0,
428 pending_move_accel: 0,
429 pending_move_decel: 0,
430 homing_method: 37,
431 homing_sdo_tid: 0,
432 soft_home_sensor: SoftHomeSensor::HomeSensor,
433 soft_home_sensor_type: SoftHomeSensorType::Pnp,
434 soft_home_direction: 1.0,
435 halt_stable_count: 0,
436 prev_positive_limit: false,
437 prev_negative_limit: false,
438 prev_home_sensor: false,
439 cw_sw_trace: VecDeque::new(),
440 trace_seq: 0,
441 is_error: false,
442 error_code: 0,
443 error_message: String::new(),
444 motor_on: false,
445 is_busy: false,
446 in_motion: false,
447 moving_positive: false,
448 moving_negative: false,
449 position: 0.0,
450 raw_position: 0,
451 speed: 0.0,
452 at_max_limit: false,
453 at_min_limit: false,
454 at_positive_limit_switch: false,
455 at_negative_limit_switch: false,
456 home_sensor: false,
457 ton: Ton::new(),
458 fb_mode_of_operation: FbSetModeOfOperation::new(),
459 flush: FbFlushQueue::new(),
460 }
461 }
462
463 pub fn config(&self) -> &AxisConfig {
465 &self.config
466 }
467
468 pub fn move_absolute(
478 &mut self,
479 view: &mut impl AxisView,
480 target: f64,
481 vel: f64,
482 accel: f64,
483 decel: f64,
484 ) {
485 if let Some(msg) = self.check_target_limit(target, view) {
486 self.set_op_error(&msg);
487 return;
488 }
489
490 let cpu = self.config.counts_per_user();
491 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
492 let raw_vel = (vel * cpu).round() as u32;
493 let raw_accel = (accel * cpu).round() as u32;
494 let raw_decel = (decel * cpu).round() as u32;
495
496 let user_pos = target > self.position;
498 let user_neg = target < self.position;
499
500 self.start_move(
501 view,
502 raw_target,
503 raw_vel,
504 raw_accel,
505 raw_decel,
506 MoveKind::Absolute,
507 user_pos,
508 user_neg,
509 );
510 }
511
512 pub fn move_relative(
518 &mut self,
519 view: &mut impl AxisView,
520 distance: f64,
521 vel: f64,
522 accel: f64,
523 decel: f64,
524 ) {
525 log::info!(
526 "Axis: request to move relative dist {} vel {} accel {} decel {}",
527 distance,
528 vel,
529 accel,
530 decel
531 );
532
533 if let Some(msg) = self.check_target_limit(self.position + distance, view) {
534 self.set_op_error(&msg);
535 return;
536 }
537
538 let cpu = self.config.counts_per_user();
539 let raw_distance = self.config.to_counts(distance).round() as i32;
540 let raw_vel = (vel * cpu).round() as u32;
541 let raw_accel = (accel * cpu).round() as u32;
542 let raw_decel = (decel * cpu).round() as u32;
543
544 log::info!("Axis starting relative move: request to move relative raw dist {} raw vel {} raw accel {} raw decel {}",
545 raw_distance, raw_vel, raw_accel, raw_decel
546 );
547
548 let mut cw = RawControlWord(view.control_word());
550 cw.set_bit(4, false); view.set_control_word(cw.raw());
552
553 let user_pos = distance > 0.0;
555 let user_neg = distance < 0.0;
556
557 self.start_move(
558 view,
559 raw_distance,
560 raw_vel,
561 raw_accel,
562 raw_decel,
563 MoveKind::Relative,
564 user_pos,
565 user_neg,
566 );
567 }
568
569 fn start_move(
570 &mut self,
571 view: &mut impl AxisView,
572 raw_target: i32,
573 raw_vel: u32,
574 raw_accel: u32,
575 raw_decel: u32,
576 kind: MoveKind,
577 user_pos: bool,
578 user_neg: bool,
579 ) {
580 self.pending_move_target = raw_target;
581 self.pending_move_vel = raw_vel;
582 self.pending_move_accel = raw_accel;
583 self.pending_move_decel = raw_decel;
584
585 view.set_target_position(raw_target);
587 view.set_profile_velocity(raw_vel);
588 view.set_profile_acceleration(raw_accel);
589 view.set_profile_deceleration(raw_decel);
590
591 let mut cw = RawControlWord(view.control_word());
596 cw.set_bit(6, kind == MoveKind::Relative);
597 cw.set_bit(8, false); cw.set_bit(4, true); view.set_control_word(cw.raw());
600
601 self.op = AxisOp::Moving(kind, 1, user_pos, user_neg);
606 self.op_started = Some(Instant::now());
607 }
608
609 pub fn halt(&mut self, view: &mut impl AxisView) {
627 self.command_halt(view);
628 self.halt_stable_count = 0;
629 self.last_raw_position = view.position_actual();
630 self.op_started = Some(Instant::now());
631 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
632 }
633
634 pub fn enable(&mut self, view: &mut impl AxisView) {
642 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
644 let mut cw = RawControlWord(view.control_word());
645 cw.cmd_shutdown();
646 view.set_control_word(cw.raw());
647
648 self.op = AxisOp::Enabling(1);
649 self.op_started = Some(Instant::now());
650 }
651
652 pub fn disable(&mut self, view: &mut impl AxisView) {
661 let here = view.position_actual();
668 view.set_target_position(here);
669 self.pending_move_target = here;
670
671 let mut cw = RawControlWord(view.control_word());
672 cw.cmd_shutdown();
673 cw.set_bit(4, false);
674 cw.set_bit(8, false);
675 cw.set_bit(7, false);
676 cw.set_bit(2, true);
677 view.set_control_word(cw.raw());
678
679 self.op = AxisOp::Disabling(1);
680 self.op_started = Some(Instant::now());
681 }
682
683 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
699 let sw = RawStatusWord(view.status_word());
700 let drive_in_fault = matches!(
701 sw.state(),
702 Cia402State::Fault | Cia402State::FaultReactionActive
703 );
704
705 self.is_error = false;
706 self.error_code = 0;
707 self.error_message.clear();
708
709 if drive_in_fault {
710 let mut cw = RawControlWord(view.control_word());
712 cw.cmd_clear_fault_reset();
713 view.set_control_word(cw.raw());
714 self.op = AxisOp::FaultRecovery(1);
715 self.op_started = Some(Instant::now());
716 } else {
717 self.op = AxisOp::Idle;
719 self.op_started = None;
720 self.is_busy = false;
721 self.in_motion = false;
722 }
723 }
724
725 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
733 match method {
734 HomingMethod::CurrentPosition35 | HomingMethod::CurrentPosition37 => {
741 self.homing_method = method.cia402_code();
742 self.op = AxisOp::SoftHoming(HomeState::WriteHomeOffset as u8);
743 self.op_started = Some(Instant::now());
744 let _ = view;
745 }
746 m if m.is_integrated() => {
751 self.homing_method = m.cia402_code();
752 self.op = AxisOp::Homing(16);
753 self.op_started = Some(Instant::now());
754 let _ = view;
755 }
756 m => {
760 self.homing_method = self.config.soft_home_method;
761 self.configure_soft_homing(m);
762 self.start_soft_homing(view);
763 }
764 }
765 }
766
767 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
776 self.home_offset =
777 view.position_actual() - self.config.to_counts(user_units).round() as i32;
778 }
779
780 pub fn set_home_position(&mut self, user_units: f64) {
784 self.config.home_position = user_units;
785 }
786
787 pub fn set_software_max_limit(&mut self, user_units: f64) {
789 self.config.max_position_limit = user_units;
790 self.config.enable_max_position_limit = true;
791 }
792
793 pub fn set_software_min_limit(&mut self, user_units: f64) {
795 self.config.min_position_limit = user_units;
796 self.config.enable_min_position_limit = true;
797 }
798
799 pub fn sdo_write(
805 &mut self,
806 client: &mut CommandClient,
807 index: u16,
808 sub_index: u8,
809 value: serde_json::Value,
810 ) {
811 self.sdo.write(client, index, sub_index, value);
812 }
813
814 pub fn sdo_read(&mut self, client: &mut CommandClient, index: u16, sub_index: u8) -> u32 {
816 self.sdo.read(client, index, sub_index)
817 }
818
819 pub fn sdo_result(&mut self, client: &mut CommandClient, tid: u32) -> SdoResult {
821 self.sdo.result(client, tid, Duration::from_secs(5))
822 }
823
824 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
838 self.check_faults(view);
839 self.progress_op(view, client);
840 self.update_outputs(view);
841 self.check_limits(view);
842 self.record_trace(view);
843 }
844
845 fn record_trace(&mut self, view: &impl AxisView) {
853 let cw = view.control_word();
854 let sw = view.status_word();
855 let changed = self
856 .cw_sw_trace
857 .back()
858 .map_or(true, |e| e.cw != cw || e.sw != sw || e.op != self.op);
859 if !changed {
860 return;
861 }
862 if self.cw_sw_trace.len() >= CW_SW_TRACE_CAP {
863 self.cw_sw_trace.pop_front();
864 }
865 self.cw_sw_trace.push_back(TraceEntry {
866 seq: self.trace_seq,
867 op: self.op.clone(),
868 cw,
869 sw,
870 });
871 self.trace_seq = self.trace_seq.wrapping_add(1);
872 }
873
874 pub fn trace(&self) -> String {
885 let mut out = String::new();
886 for e in &self.cw_sw_trace {
887 out.push_str(&format!(
888 " [{:>4}] {:<20} CW={:#06x} [{}] SW={:#06x} [{}]\n",
889 e.seq,
890 format!("{:?}", e.op),
891 e.cw,
892 decode_cw(e.cw),
893 e.sw,
894 decode_sw(e.sw),
895 ));
896 }
897 out
898 }
899
900 fn update_outputs(&mut self, view: &impl AxisView) {
905 let raw = view.position_actual();
906 self.raw_position = raw as i64;
907 self.position = self.config.to_user((raw - self.home_offset) as f64);
908
909 let vel = view.velocity_actual();
910 let user_vel = self.config.to_user(vel as f64);
911 self.speed = user_vel.abs();
912 self.moving_positive = user_vel > 0.0;
913 self.moving_negative = user_vel < 0.0;
914 self.is_busy = self.op != AxisOp::Idle;
915 self.in_motion = matches!(self.op, AxisOp::Moving(_, _, _, _) | AxisOp::SoftHoming(_));
916
917 let sw = RawStatusWord(view.status_word());
918 self.motor_on = sw.state() == Cia402State::OperationEnabled;
919
920 self.last_raw_position = raw;
921 }
922
923 fn check_faults(&mut self, view: &impl AxisView) {
928 let sw = RawStatusWord(view.status_word());
929 let state = sw.state();
930
931 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
932 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
933 self.is_error = true;
934 let ec = view.error_code();
935 if ec != 0 {
936 self.error_code = ec as u32;
937 }
938 self.error_message = format!("Drive fault (state: {})", state);
939 self.op = AxisOp::Idle;
941 self.op_started = None;
942 }
943 }
944 }
945
946 fn op_timed_out(&self) -> bool {
951 self.op_started
952 .map_or(false, |t| t.elapsed() > self.op_timeout)
953 }
954
955 fn homing_timed_out(&self) -> bool {
956 self.op_started
957 .map_or(false, |t| t.elapsed() > self.homing_timeout)
958 }
959
960 fn move_start_timed_out(&self) -> bool {
961 self.op_started
962 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
963 }
964
965 fn op_stage_timed_out(&self, limit: Duration) -> bool {
969 self.op_started.map_or(false, |t| t.elapsed() > limit)
970 }
971
972 fn set_op_error(&mut self, msg: &str) {
973 self.is_error = true;
974 self.error_message = msg.to_string();
975 log::error!("Axis error: {}", msg);
976 if !self.cw_sw_trace.is_empty() {
980 log::warn!(
981 "Axis CiA-402 trace ({} recent transitions, oldest first):\n{}",
982 self.cw_sw_trace.len(),
983 self.trace(),
984 );
985 }
986 self.op = AxisOp::Idle;
987 self.op_started = None;
988 self.is_busy = false;
989 self.in_motion = false;
990 }
991
992 fn restore_pp_after_error(&mut self, msg: &str) {
993 self.is_error = true;
994 self.error_message = msg.to_string();
995 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
996 log::error!("Axis error: {}", msg);
997 }
998
999 fn finish_op_error(&mut self) {
1000 self.op = AxisOp::Idle;
1001 self.op_started = None;
1002 self.is_busy = false;
1003 self.in_motion = false;
1004 }
1005
1006 fn complete_op(&mut self) {
1007 self.op = AxisOp::Idle;
1008 self.op_started = None;
1009 self.is_busy = false;
1014 self.in_motion = false;
1015 }
1016
1017 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
1026 let static_limit = if self.config.enable_max_position_limit {
1027 Some(self.config.max_position_limit)
1028 } else {
1029 None
1030 };
1031 match (static_limit, view.dynamic_max_position_limit()) {
1032 (Some(s), Some(d)) => Some(s.min(d)),
1033 (Some(v), None) | (None, Some(v)) => Some(v),
1034 (None, None) => None,
1035 }
1036 }
1037
1038 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
1042 let static_limit = if self.config.enable_min_position_limit {
1043 Some(self.config.min_position_limit)
1044 } else {
1045 None
1046 };
1047 match (static_limit, view.dynamic_min_position_limit()) {
1048 (Some(s), Some(d)) => Some(s.max(d)),
1049 (Some(v), None) | (None, Some(v)) => Some(v),
1050 (None, None) => None,
1051 }
1052 }
1053
1054 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
1059 if let Some(max) = self.effective_max_limit(view) {
1060 if target > max {
1061 return Some(format!(
1062 "Target {:.3} exceeds max software limit {:.3}",
1063 target, max
1064 ));
1065 }
1066 }
1067 if let Some(min) = self.effective_min_limit(view) {
1068 if target < min {
1069 return Some(format!(
1070 "Target {:.3} exceeds min software limit {:.3}",
1071 target, min
1072 ));
1073 }
1074 }
1075 None
1076 }
1077
1078 fn check_limits(&mut self, view: &mut impl AxisView) {
1087 let eff_max = self.effective_max_limit(view);
1089 let eff_min = self.effective_min_limit(view);
1090 let sw_max = eff_max.map_or(false, |m| self.position >= m);
1091 let sw_min = eff_min.map_or(false, |m| self.position <= m);
1092
1093 self.at_max_limit = sw_max;
1094 self.at_min_limit = sw_min;
1095
1096 let hw_pos = view.positive_limit_active();
1098 let hw_neg = view.negative_limit_active();
1099
1100 self.at_positive_limit_switch = hw_pos;
1101 self.at_negative_limit_switch = hw_neg;
1102
1103 self.home_sensor = view.home_sensor_active();
1105
1106 self.prev_positive_limit = hw_pos;
1108 self.prev_negative_limit = hw_neg;
1109 self.prev_home_sensor = view.home_sensor_active();
1110
1111 let mut commanded_positive = false;
1113 let mut commanded_negative = false;
1114
1115 let is_moving = matches!(self.op, AxisOp::Moving(_, _, _, _));
1116 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
1117
1118 if !is_moving && !is_soft_homing {
1119 return; }
1121
1122 match &self.op {
1123 AxisOp::Moving(_, _, pos, neg) => {
1124 commanded_positive = *pos;
1126 commanded_negative = *neg;
1127 }
1128 AxisOp::SoftHoming(_) => match self.soft_home_sensor {
1129 SoftHomeSensor::PositiveLimit => commanded_positive = true,
1130 SoftHomeSensor::NegativeLimit => commanded_negative = true,
1131 SoftHomeSensor::HomeSensor => {
1132 commanded_positive = self.moving_positive;
1133 commanded_negative = self.moving_negative;
1134 }
1135 },
1136 _ => {}
1137 }
1138
1139 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
1141 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
1142
1143 let effective_hw_pos = hw_pos && !suppress_pos;
1144 let effective_hw_neg = hw_neg && !suppress_neg;
1145
1146 let effective_sw_max = sw_max && !is_soft_homing;
1148 let effective_sw_min = sw_min && !is_soft_homing;
1149
1150 let positive_blocked = (effective_sw_max || effective_hw_pos) && commanded_positive;
1151 let negative_blocked = (effective_sw_min || effective_hw_neg) && commanded_negative;
1152
1153 if positive_blocked || negative_blocked {
1154 let mut cw = RawControlWord(view.control_word());
1155 cw.set_bit(8, true); view.set_control_word(cw.raw());
1157
1158 let msg = if effective_hw_pos && commanded_positive {
1159 "Positive limit switch active".to_string()
1160 } else if effective_hw_neg && commanded_negative {
1161 "Negative limit switch active".to_string()
1162 } else if effective_sw_max && commanded_positive {
1163 format!(
1164 "Software position limit: position {:.3} >= max {:.3}",
1165 self.position,
1166 eff_max.unwrap_or(self.position)
1167 )
1168 } else {
1169 format!(
1170 "Software position limit: position {:.3} <= min {:.3}",
1171 self.position,
1172 eff_min.unwrap_or(self.position)
1173 )
1174 };
1175
1176 if is_soft_homing {
1177 self.set_op_error(&msg);
1180 } else {
1181 log::info!("Axis move halted by limit: {}", msg);
1194 self.command_halt(view);
1195 self.halt_stable_count = 0;
1196 self.last_raw_position = view.position_actual();
1197 self.op_started = Some(Instant::now());
1198 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
1199 }
1200 }
1201 }
1202
1203 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
1208 match self.op.clone() {
1209 AxisOp::Idle => {}
1210 AxisOp::Enabling(step) => self.tick_enabling(view, step),
1211 AxisOp::Disabling(step) => self.tick_disabling(view, step),
1212 AxisOp::Moving(kind, step, pos, neg) => self.tick_moving(view, kind, step, pos, neg),
1213 AxisOp::Homing(step) => self.tick_homing(view, client, step),
1214 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
1215 AxisOp::Halting(step) => self.tick_halting(view, client, step),
1216 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
1217 }
1218 }
1219
1220 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
1225 match step {
1226 1 => {
1227 let sw = RawStatusWord(view.status_word());
1228 if sw.state() == Cia402State::ReadyToSwitchOn {
1229 let mut cw = RawControlWord(view.control_word());
1230 cw.cmd_enable_operation();
1231 view.set_control_word(cw.raw());
1232 self.op = AxisOp::Enabling(2);
1233 } else if self.op_timed_out() {
1234 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
1235 }
1236 }
1237 2 => {
1238 let sw = RawStatusWord(view.status_word());
1239 if sw.state() == Cia402State::OperationEnabled {
1240 self.complete_op();
1247 } else if self.op_timed_out() {
1248 self.set_op_error("Enable timeout: waiting for OperationEnabled");
1249 }
1250 }
1251 _ => self.complete_op(),
1252 }
1253 }
1254
1255 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
1259 match step {
1260 1 => {
1261 let sw = RawStatusWord(view.status_word());
1262 if sw.state() != Cia402State::OperationEnabled {
1263 self.complete_op();
1264 } else if self.op_timed_out() {
1265 self.set_op_error("Disable timeout: drive still in OperationEnabled");
1266 }
1267 }
1268 _ => self.complete_op(),
1269 }
1270 }
1271
1272 fn tick_moving(
1278 &mut self,
1279 view: &mut impl AxisView,
1280 kind: MoveKind,
1281 step: u8,
1282 pos: bool,
1283 neg: bool,
1284 ) {
1285 match step {
1286 1 => {
1287 let sw = RawStatusWord(view.status_word());
1289 if sw.raw() & (1 << 12) != 0 {
1290 let mut cw = RawControlWord(view.control_word());
1292 cw.set_bit(4, false);
1293 view.set_control_word(cw.raw());
1294 self.op = AxisOp::Moving(kind, 2, pos, neg);
1295 } else if self.move_start_timed_out() {
1296 self.set_op_error("Move timeout: set-point not acknowledged");
1297 }
1298 }
1299 2 => {
1300 let sw = RawStatusWord(view.status_word());
1302 if sw.raw() & (1 << 12) == 0 {
1303 self.op = AxisOp::Moving(kind, 3, pos, neg);
1305 }
1306 }
1307 3 => {
1308 let sw = RawStatusWord(view.status_word());
1310 if sw.target_reached() {
1311 self.complete_op();
1312 }
1313 }
1314 _ => self.complete_op(),
1315 }
1316 }
1317
1318 fn tick_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1347 match step {
1348 0 => {
1349 self.homing_sdo_tid = self.sdo.write(client, 0x6098, 0, json!(self.homing_method));
1351 self.op = AxisOp::Homing(1);
1352 }
1353 1 => {
1354 match self
1356 .sdo
1357 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1358 {
1359 SdoResult::Ok(_) => {
1360 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
1362 self.op = AxisOp::Homing(8);
1363 } else {
1364 self.op = AxisOp::Homing(2);
1365 }
1366 }
1367 SdoResult::Pending => {
1368 if self.homing_timed_out() {
1369 self.set_op_error("Homing timeout: SDO write for homing method");
1370 }
1371 }
1372 SdoResult::Err(e) => {
1373 self.set_op_error(&format!("Homing SDO error: {}", e));
1374 }
1375 SdoResult::Timeout => {
1376 self.set_op_error("Homing timeout: SDO write timed out");
1377 }
1378 }
1379 }
1380 2 => {
1381 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1383 self.homing_sdo_tid = self.sdo.write(client, 0x6099, 1, json!(speed_counts));
1384 self.op = AxisOp::Homing(3);
1385 }
1386 3 => {
1387 match self
1388 .sdo
1389 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1390 {
1391 SdoResult::Ok(_) => {
1392 self.op = AxisOp::Homing(4);
1393 }
1394 SdoResult::Pending => {
1395 if self.homing_timed_out() {
1396 self.set_op_error(
1397 "Homing timeout: SDO write for homing speed (switch)",
1398 );
1399 }
1400 }
1401 SdoResult::Err(e) => {
1402 self.set_op_error(&format!("Homing SDO error: {}", e));
1403 }
1404 SdoResult::Timeout => {
1405 self.set_op_error("Homing timeout: SDO write timed out");
1406 }
1407 }
1408 }
1409 4 => {
1410 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1412 self.homing_sdo_tid = self.sdo.write(client, 0x6099, 2, json!(speed_counts));
1413 self.op = AxisOp::Homing(5);
1414 }
1415 5 => {
1416 match self
1417 .sdo
1418 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1419 {
1420 SdoResult::Ok(_) => {
1421 self.op = AxisOp::Homing(6);
1422 }
1423 SdoResult::Pending => {
1424 if self.homing_timed_out() {
1425 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1426 }
1427 }
1428 SdoResult::Err(e) => {
1429 self.set_op_error(&format!("Homing SDO error: {}", e));
1430 }
1431 SdoResult::Timeout => {
1432 self.set_op_error("Homing timeout: SDO write timed out");
1433 }
1434 }
1435 }
1436 6 => {
1437 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1439 self.homing_sdo_tid = self.sdo.write(client, 0x609A, 0, json!(accel_counts));
1440 self.op = AxisOp::Homing(7);
1441 }
1442 7 => {
1443 match self
1444 .sdo
1445 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1446 {
1447 SdoResult::Ok(_) => {
1448 self.op = AxisOp::Homing(8);
1449 }
1450 SdoResult::Pending => {
1451 if self.homing_timed_out() {
1452 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1453 }
1454 }
1455 SdoResult::Err(e) => {
1456 self.set_op_error(&format!("Homing SDO error: {}", e));
1457 }
1458 SdoResult::Timeout => {
1459 self.set_op_error("Homing timeout: SDO write timed out");
1460 }
1461 }
1462 }
1463 8 => {
1464 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1474 let mut cw = RawControlWord(view.control_word());
1476 cw.set_bit(4, false);
1477 view.set_control_word(cw.raw());
1478 self.fb_mode_of_operation.reset();
1479 self.fb_mode_of_operation
1480 .start(ModesOfOperation::Homing as i8);
1481 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1482 self.op = AxisOp::Homing(9);
1483 }
1484 9 => {
1485 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1487 if !self.fb_mode_of_operation.is_busy() {
1488 if self.fb_mode_of_operation.is_error() {
1489 self.set_op_error(&format!(
1490 "Homing: failed to enter Homing mode: {} {}",
1491 self.fb_mode_of_operation.error_code(),
1492 self.fb_mode_of_operation.error_message()
1493 ));
1494 } else {
1495 self.op = AxisOp::Homing(10);
1496 }
1497 }
1498 }
1499 10 => {
1500 log::info!(
1505 "Homing[trigger]: method={} position_actual(0x6064) BEFORE = {}",
1506 self.homing_method,
1507 view.position_actual(),
1508 );
1509 let mut cw = RawControlWord(view.control_word());
1510 cw.set_bit(4, true);
1511 view.set_control_word(cw.raw());
1512 self.op = AxisOp::Homing(11);
1513 }
1514 11 => {
1515 let sw = view.status_word();
1527 if sw & (1 << 13) != 0 {
1528 self.set_op_error("Homing error: drive reported homing failure");
1529 } else {
1530 self.op = AxisOp::Homing(12);
1531 }
1532 }
1533 12 => {
1534 let sw = view.status_word();
1542 let error = sw & (1 << 13) != 0;
1543 let attained = sw & (1 << 12) != 0;
1544
1545 if error {
1546 self.set_op_error("Homing error: drive reported homing failure");
1547 } else if attained {
1548 self.op = AxisOp::Homing(13);
1549 } else if self.homing_timed_out() {
1550 self.set_op_error(&format!(
1551 "Homing timeout: procedure did not complete (sw=0x{:04X})",
1552 sw
1553 ));
1554 }
1555 }
1556 13 => {
1557 self.home_offset = 0;
1563 log::info!(
1564 "Homing[done]: position_actual(0x6064) AFTER = {} (drive owns the reference)",
1565 view.position_actual(),
1566 );
1567 let mut cw = RawControlWord(view.control_word());
1569 cw.set_bit(4, false);
1570 view.set_control_word(cw.raw());
1571 self.op = AxisOp::Homing(14);
1572 }
1573 14 => {
1574 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1580 self.fb_mode_of_operation.reset();
1581 self.fb_mode_of_operation
1582 .start(ModesOfOperation::ProfilePosition as i8);
1583 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1584 self.op = AxisOp::Homing(15);
1585 }
1586 15 => {
1587 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1589 if !self.fb_mode_of_operation.is_busy() {
1590 if self.fb_mode_of_operation.is_error() {
1591 self.set_op_error(&format!(
1592 "Homing: failed to restore PP mode: {} {}",
1593 self.fb_mode_of_operation.error_code(),
1594 self.fb_mode_of_operation.error_message()
1595 ));
1596 } else {
1597 log::info!("Homing complete — home offset: {}", self.home_offset);
1598 self.complete_op();
1599 }
1600 }
1601 }
1602 16 => {
1603 let desired_counts =
1608 self.config.to_counts(self.config.home_position).round() as i32;
1609 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
1610 log::info!(
1611 "Homing[offset]: writing 0x607C = {} counts ({} user units)",
1612 desired_counts, self.config.home_position
1613 );
1614 self.op = AxisOp::Homing(17);
1615 }
1616 17 => {
1617 match self
1618 .sdo
1619 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1620 {
1621 SdoResult::Ok(_) => {
1622 self.op = AxisOp::Homing(0);
1623 }
1624 SdoResult::Pending => {
1625 if self.homing_timed_out() {
1626 self.set_op_error("Homing timeout: SDO write for home offset");
1627 }
1628 }
1629 SdoResult::Err(e) => {
1630 self.set_op_error(&format!("Homing SDO error (home offset): {}", e));
1631 }
1632 SdoResult::Timeout => {
1633 self.set_op_error("Homing timeout: home offset SDO write timed out");
1634 }
1635 }
1636 }
1637 _ => self.complete_op(),
1638 }
1639 }
1640
1641 fn configure_soft_homing(&mut self, method: HomingMethod) {
1644 match method {
1645 HomingMethod::LimitSwitchPosPnp => {
1646 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1647 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1648 self.soft_home_direction = 1.0;
1649 }
1650 HomingMethod::LimitSwitchNegPnp => {
1651 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1652 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1653 self.soft_home_direction = -1.0;
1654 }
1655 HomingMethod::LimitSwitchPosNpn => {
1656 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1657 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1658 self.soft_home_direction = 1.0;
1659 }
1660 HomingMethod::LimitSwitchNegNpn => {
1661 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1662 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1663 self.soft_home_direction = -1.0;
1664 }
1665 HomingMethod::HomeSensorPosPnp => {
1666 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1667 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1668 self.soft_home_direction = 1.0;
1669 }
1670 HomingMethod::HomeSensorNegPnp => {
1671 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1672 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1673 self.soft_home_direction = -1.0;
1674 }
1675 HomingMethod::HomeSensorPosNpn => {
1676 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1677 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1678 self.soft_home_direction = 1.0;
1679 }
1680 HomingMethod::HomeSensorNegNpn => {
1681 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1682 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1683 self.soft_home_direction = -1.0;
1684 }
1685 _ => {} }
1687 }
1688
1689 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1690 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1691 self.op_started = Some(Instant::now());
1692 }
1693
1694 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1695 let raw = match self.soft_home_sensor {
1696 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1697 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1698 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1699 };
1700 match self.soft_home_sensor_type {
1701 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1704 }
1705
1706 fn calculate_max_relative_target(&self, direction: f64) -> i32 {
1709 let dir = if !self.config.invert_direction {
1710 direction
1711 } else {
1712 -direction
1713 };
1714
1715 let target = if dir > 0.0 { i32::MAX } else { i32::MIN };
1716
1717 return target;
1718 }
1719
1720 pub fn command_halt(&self, view: &mut impl AxisView) {
1724 let mut cw = RawControlWord(view.control_word());
1725 cw.set_bit(8, true); cw.set_bit(4, false); cw.set_bit(5, false);
1728 cw.set_bit(6, false); view.set_control_word(cw.raw());
1730 }
1731
1732 fn command_homing_speed(&self, view: &mut impl AxisView) {
1734 let cpu = self.config.counts_per_user();
1735 let vel = (self.config.homing_speed * cpu).round() as u32;
1736 let accel = (self.config.homing_accel * cpu).round() as u32;
1737 let decel = (self.config.homing_decel * cpu).round() as u32;
1738 view.set_profile_velocity(vel);
1739 view.set_profile_acceleration(accel);
1740 view.set_profile_deceleration(decel);
1741 }
1742
1743 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1759 match HomeState::from_repr(step) {
1760 Some(HomeState::EnsurePpMode) => {
1761 log::info!("SoftHome: Ensuring PP mode..");
1766 self.fb_mode_of_operation
1767 .start(ModesOfOperation::ProfilePosition as i8);
1768 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1769 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1770 }
1771 Some(HomeState::WaitPpMode) => {
1772 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1773 if !self.fb_mode_of_operation.is_busy() {
1774 if self.fb_mode_of_operation.is_error() {
1775 self.set_op_error(&format!(
1776 "Software homing SDO error writing homing mode of operation: {} {}",
1777 self.fb_mode_of_operation.error_code(),
1778 self.fb_mode_of_operation.error_message()
1779 ));
1780 } else {
1781 log::info!("SoftHome: Drive is in PP mode!");
1782
1783 if !self.check_soft_home_trigger(view) {
1787 log::info!("SoftHome: Not on home switch; seek out.");
1788 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1789 } else {
1790 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1791 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1792 }
1793 }
1794 }
1795 }
1796
1797 Some(HomeState::Search) => {
1799 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1800
1801 let target = self.calculate_max_relative_target(self.soft_home_direction);
1810 view.set_target_position(target);
1811
1812 self.command_homing_speed(view);
1821
1822 let mut cw = RawControlWord(view.control_word());
1823 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(8, false); cw.set_bit(13, true); view.set_control_word(cw.raw());
1828
1829 log::info!(
1830 "SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1831 target,
1832 self.config.homing_speed,
1833 self.soft_home_direction,
1834 view.position_actual()
1835 );
1836 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1837 }
1838 Some(HomeState::WaitSearching) => {
1839 if self.check_soft_home_trigger(view) {
1840 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1841 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1842 return;
1843 }
1844 let sw = RawStatusWord(view.status_word());
1845 if sw.raw() & (1 << 12) != 0 {
1846 let mut cw = RawControlWord(view.control_word());
1847 cw.set_bit(4, false);
1848 view.set_control_word(cw.raw());
1849 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1850 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1851 } else if self.homing_timed_out() {
1852 self.set_op_error("Software homing timeout: set-point not acknowledged");
1853 }
1854 }
1855 Some(HomeState::WaitFoundSensor) => {
1865 if self.check_soft_home_trigger(view) {
1866 log::info!(
1867 "SoftHome[3]: sensor triggered at pos={}. HALTING",
1868 view.position_actual()
1869 );
1870 log::info!("ControlWord is : {} ", view.control_word());
1871
1872 let mut cw = RawControlWord(view.control_word());
1873 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1876
1877 self.halt_stable_count = 0;
1878 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1879 } else if self.homing_timed_out() {
1880 self.set_op_error("Software homing timeout: sensor not detected");
1881 }
1882 }
1883
1884 Some(HomeState::WaitStoppedFoundSensor) => {
1885 const STABLE_WINDOW: i32 = 1;
1886 const STABLE_TICKS_REQUIRED: u8 = 10;
1887
1888 let pos = view.position_actual();
1893 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1894 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1895 } else {
1896 self.halt_stable_count = 0;
1897 }
1898
1899 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1900 log::info!(
1901 "SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true."
1902 );
1903 self.flush.start(view, client, &mut self.sdo);
1904 self.op = AxisOp::SoftHoming(HomeState::FlushingFound as u8);
1905 } else if self.homing_timed_out() {
1906 self.set_op_error(
1907 "Software homing timeout: motor did not stop after sensor trigger",
1908 );
1909 }
1910 }
1911 Some(HomeState::FlushingFound) => {
1912 self.flush.tick(view, client, &mut self.sdo);
1913 if self.flush.is_busy() {
1914 if self.homing_timed_out() {
1915 self.set_op_error("Software homing timeout: queue flush did not complete");
1916 }
1917 return;
1918 }
1919 if self.flush.is_error() {
1920 self.set_op_error(&format!(
1921 "Software homing: queue flush failed: {} {}",
1922 self.flush.error_code(),
1923 self.flush.error_message()
1924 ));
1925 return;
1926 }
1927 let mut cw = RawControlWord(view.control_word());
1930 cw.set_bit(8, false);
1931 cw.set_bit(4, false);
1932 cw.set_bit(5, false);
1933 view.set_control_word(cw.raw());
1934 view.set_target_position(view.position_actual());
1935 log::info!("SoftHome[6]: queue flushed; proceeding to debounce.");
1936 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1937 self.ton.call(false, Duration::from_secs(3));
1938 }
1939 Some(HomeState::DebounceFoundSensor) => {
1941 self.ton.call(true, Duration::from_secs(3));
1942
1943 let sw = RawStatusWord(view.status_word());
1944 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1945 self.ton.call(false, Duration::from_secs(3));
1946 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1947 view.position_actual(), view.control_word(), view.status_word());
1948 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1949 }
1950 }
1951
1952 Some(HomeState::BackOff) => {
1954 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1955 view.set_target_position(target);
1956
1957 self.command_homing_speed(view);
1958
1959 let mut cw = RawControlWord(view.control_word());
1960 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1964 log::info!(
1965 "SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1966 target,
1967 self.config.homing_speed,
1968 view.position_actual(),
1969 cw.raw()
1970 );
1971 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1972 }
1973 Some(HomeState::WaitBackingOff) => {
1974 let sw = RawStatusWord(view.status_word());
1975 if sw.raw() & (1 << 12) != 0 {
1976 let mut cw = RawControlWord(view.control_word());
1977 cw.set_bit(4, false);
1978 view.set_control_word(cw.raw());
1979 log::info!(
1980 "SoftHome[WaitBackingOff]: back-off ack received, pos={}",
1981 view.position_actual()
1982 );
1983 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1984 } else if self.homing_timed_out() {
1985 self.set_op_error("Software homing timeout: back-off not acknowledged");
1986 }
1987 }
1988 Some(HomeState::WaitLostSensor) => {
1989 if !self.check_soft_home_trigger(view) {
1990 log::info!(
1991 "SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...",
1992 view.position_actual()
1993 );
1994
1995 self.command_halt(view);
1996 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1997 } else if self.homing_timed_out() {
1998 self.set_op_error(
1999 "Software homing timeout: sensor did not clear during back-off",
2000 );
2001 }
2002 }
2003 Some(HomeState::WaitStoppedLostSensor) => {
2004 const STABLE_WINDOW: i32 = 1;
2005 const STABLE_TICKS_REQUIRED: u8 = 10;
2006
2007 let pos = view.position_actual();
2012 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
2013 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2014 } else {
2015 self.halt_stable_count = 0;
2016 }
2017
2018 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
2019 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
2020 self.flush.start(view, client, &mut self.sdo);
2021 self.op = AxisOp::SoftHoming(HomeState::FlushingLost as u8);
2022 } else if self.homing_timed_out() {
2023 self.set_op_error("Software homing timeout: motor did not stop after back-off");
2024 }
2025 }
2026 Some(HomeState::FlushingLost) => {
2027 self.flush.tick(view, client, &mut self.sdo);
2028 if self.flush.is_busy() {
2029 if self.homing_timed_out() {
2030 self.set_op_error("Software homing timeout: queue flush did not complete");
2031 }
2032 return;
2033 }
2034 if self.flush.is_error() {
2035 self.set_op_error(&format!(
2036 "Software homing: queue flush failed: {} {}",
2037 self.flush.error_code(),
2038 self.flush.error_message()
2039 ));
2040 return;
2041 }
2042 let mut cw = RawControlWord(view.control_word());
2045 cw.set_bit(8, false);
2046 cw.set_bit(4, false);
2047 cw.set_bit(5, false);
2048 view.set_control_word(cw.raw());
2049
2050 let desired_counts =
2051 self.config.to_counts(self.config.home_position).round() as i32;
2052 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
2053 log::info!(
2054 "SoftHome[FlushingLost]: queue flushed; writing home offset {} [{} counts].",
2055 self.config.home_position, desired_counts
2056 );
2057 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
2058 }
2059
2060 Some(HomeState::WriteHomeOffset) => {
2061 let desired_counts =
2067 self.config.to_counts(self.config.home_position).round() as i32;
2068 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
2069 log::info!(
2070 "Home[CurrentPosition]: writing 0x607C = {} counts ({} user units)",
2071 desired_counts, self.config.home_position
2072 );
2073 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
2074 }
2075 Some(HomeState::WaitHomeOffsetDone) => {
2076 match self
2078 .sdo
2079 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
2080 {
2081 SdoResult::Ok(_) => {
2082 self.homing_sdo_tid = self.sdo.read(client, 0x607C, 0);
2086 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetReadback as u8);
2087 }
2088 SdoResult::Pending => {
2089 if self.homing_timed_out() {
2090 self.set_op_error("Software homing timeout: home offset SDO write");
2091 }
2092 }
2093 SdoResult::Err(e) => {
2094 self.set_op_error(&format!("Software homing SDO error: {}", e));
2095 }
2096 SdoResult::Timeout => {
2097 self.set_op_error("Software homing: home offset SDO timed out");
2098 }
2099 }
2100 }
2101 Some(HomeState::WaitHomeOffsetReadback) => {
2102 let target = self.config.to_counts(self.config.home_position).round() as i32;
2105 match self
2106 .sdo
2107 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
2108 {
2109 SdoResult::Ok(val) => {
2110 log::info!(
2111 "SoftHome[diag]: 0x607C (home offset) read-back = {} (wrote {} counts)",
2112 val,
2113 target,
2114 );
2115 self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8);
2116 }
2117 SdoResult::Pending => {
2118 if self.homing_timed_out() {
2119 log::warn!("SoftHome[diag]: 0x607C read-back timed out; continuing");
2120 self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8);
2121 }
2122 }
2123 SdoResult::Err(e) => {
2124 log::warn!("SoftHome[diag]: 0x607C read-back error: {}; continuing", e);
2125 self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8);
2126 }
2127 SdoResult::Timeout => {
2128 log::warn!("SoftHome[diag]: 0x607C read-back timed out; continuing");
2129 self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8);
2130 }
2131 }
2132 }
2133 Some(HomeState::WriteHomingModeOp) => {
2134 self.fb_mode_of_operation.reset();
2138 self.fb_mode_of_operation
2139 .start(ModesOfOperation::Homing as i8);
2140 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2141 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
2142 }
2143 Some(HomeState::WaitWriteHomingModeOp) => {
2144 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2146
2147 if !self.fb_mode_of_operation.is_busy() {
2148 if self.fb_mode_of_operation.is_error() {
2149 self.set_op_error(&format!(
2150 "Software homing SDO error writing homing mode of operation: {} {}",
2151 self.fb_mode_of_operation.error_code(),
2152 self.fb_mode_of_operation.error_message()
2153 ));
2154 } else {
2155 log::info!("SoftHome: Drive is now in Homing Mode.");
2156 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
2157 }
2158 }
2159 }
2160 Some(HomeState::WriteHomingMethod) => {
2161 log::info!(
2167 "SoftHome[diag]: writing 0x6098 (homing method) = {}",
2168 self.homing_method
2169 );
2170 self.homing_sdo_tid =
2171 self.sdo.write(client, 0x6098, 0, json!(self.homing_method));
2172 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
2173 }
2174 Some(HomeState::WaitWriteHomingMethodDone) => {
2175 match self
2177 .sdo
2178 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
2179 {
2180 SdoResult::Ok(_) => {
2181 log::info!("SoftHome: Successfully wrote homing method.");
2182 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
2183 }
2184 SdoResult::Pending => {
2185 if self.homing_timed_out() {
2186 self.restore_pp_after_error(
2187 "Software homing timeout: homing method SDO write",
2188 );
2189 }
2190 }
2191 SdoResult::Err(e) => {
2192 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
2193 }
2194 SdoResult::Timeout => {
2195 self.restore_pp_after_error("Software homing: homing method SDO timed out");
2196 }
2197 }
2198 }
2199 Some(HomeState::ClearHomingTrigger) => {
2200 let mut cw = RawControlWord(view.control_word());
2203 cw.set_bit(4, false);
2204 view.set_control_word(cw.raw());
2205 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
2206 }
2207 Some(HomeState::TriggerHoming) => {
2208 let mut cw = RawControlWord(view.control_word());
2210 cw.set_bit(4, true);
2211 view.set_control_word(cw.raw());
2212 log::info!("SoftHome[TriggerHoming]: start homing");
2213 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
2214 }
2215 Some(HomeState::WaitHomingStarted) => {
2216 let sw = view.status_word();
2230 if sw & (1 << 13) != 0 {
2231 self.restore_pp_after_error("Software homing: drive reported homing error");
2232 } else {
2233 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
2234 }
2235 }
2236 Some(HomeState::WaitHomingDone) => {
2237 let sw = view.status_word();
2246 let error = sw & (1 << 13) != 0;
2247 let attained = sw & (1 << 12) != 0;
2248 let reached = sw & (1 << 10) != 0;
2249
2250 if error {
2251 self.restore_pp_after_error("Software homing: drive reported homing error");
2252 } else if attained {
2253 log::info!(
2260 "SoftHome[WaitHomingDone]: homing complete sw=0x{:04X} position_actual(0x6064)={} target(0x607C)={} counts",
2261 sw,
2262 view.position_actual(),
2263 self.config.to_counts(self.config.home_position).round() as i32,
2264 );
2265 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
2266 } else if self.homing_timed_out() {
2267 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
2268 }
2269 }
2270 Some(HomeState::ResetHomingTrigger) => {
2271 let mut cw = RawControlWord(view.control_word());
2276 cw.set_bit(4, false);
2277 view.set_control_word(cw.raw());
2278 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
2279 }
2280 Some(HomeState::WaitHomingTriggerCleared) => {
2281 let pos = view.position_actual();
2290 view.set_target_position(pos);
2291 log::info!(
2292 "SoftHome[diag]: post-home, pre-PP-switch position_actual(0x6064) = {}; \
2293 set target_position(0x607A) = {} so PP re-entry has nothing to chase",
2294 pos, pos
2295 );
2296 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::AlignTargetBeforePP as u8);
2298 }
2299 Some(HomeState::AlignTargetBeforePP) => {
2300 view.set_target_position(view.position_actual());
2304 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
2305 }
2306
2307 Some(HomeState::WriteMotionModeOfOperation) => {
2308 self.fb_mode_of_operation.reset();
2311 self.fb_mode_of_operation
2312 .start(ModesOfOperation::ProfilePosition as i8);
2313 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2314 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
2315 }
2316 Some(HomeState::WaitWriteMotionModeOfOperation) => {
2317 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2319
2320 if !self.fb_mode_of_operation.is_busy() {
2321 if self.fb_mode_of_operation.is_error() {
2322 self.set_op_error(&format!(
2323 "Software homing SDO error writing homing mode of operation: {} {}",
2324 self.fb_mode_of_operation.error_code(),
2325 self.fb_mode_of_operation.error_message()
2326 ));
2327 } else {
2328 if self.is_error {
2329 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
2330 self.finish_op_error();
2331 } else {
2332 log::info!(
2333 "SoftHome[diag]: back in PP mode, position_actual(0x6064) = {}; \
2334 homing complete, holding (no new set-point latched)",
2335 view.position_actual()
2336 );
2337 self.complete_op();
2357 }
2358 }
2359 }
2360 }
2361
2362 Some(HomeState::SendCurrentPositionTarget) => {
2363 let current_pos = view.position_actual();
2365 log::info!(
2366 "SoftHome[diag]: holding at position_actual(0x6064) = {}",
2367 current_pos
2368 );
2369 view.set_target_position(current_pos);
2370 view.set_profile_velocity(0);
2371 let mut cw = RawControlWord(view.control_word());
2372 cw.set_bit(4, true);
2373 cw.set_bit(5, true);
2374 cw.set_bit(6, false); view.set_control_word(cw.raw());
2376 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
2377 }
2378 Some(HomeState::WaitCurrentPositionTargetSent) => {
2379 let sw = RawStatusWord(view.status_word());
2381 if sw.raw() & (1 << 12) != 0 {
2382 let mut cw = RawControlWord(view.control_word());
2383 cw.set_bit(4, false);
2384 view.set_control_word(cw.raw());
2385 log::info!(
2386 "Software homing complete — commanded {} user units; final position_actual(0x6064) = {} counts",
2387 self.config.home_position,
2388 view.position_actual()
2389 );
2390 self.complete_op();
2391 } else if self.homing_timed_out() {
2392 self.set_op_error("Software homing timeout: hold position not acknowledged");
2393 }
2394 }
2395 _ => self.complete_op(),
2396 }
2397 }
2398
2399 fn tick_halting(
2412 &mut self,
2413 view: &mut impl AxisView,
2414 client: &mut CommandClient,
2415 step: u8,
2416 ) {
2417 match HaltState::from_repr(step) {
2418 Some(HaltState::WaitStopped) => {
2419 let pos = view.position_actual();
2420 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
2421
2422 let vel = view.velocity_actual().abs();
2423 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
2424
2425 if pos_stable || vel_stopped {
2426 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2427 } else {
2428 self.halt_stable_count = 0;
2429 }
2430
2431 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
2432 self.flush.start(view, client, &mut self.sdo);
2435 self.op_started = Some(Instant::now());
2436 self.op = AxisOp::Halting(HaltState::Flushing as u8);
2437 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2438 self.set_op_error("Halt timeout: motor did not stop");
2439 }
2440 }
2441 Some(HaltState::Flushing) => {
2442 self.flush.tick(view, client, &mut self.sdo);
2443 if self.flush.is_busy() {
2444 if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2445 self.set_op_error("Halt timeout: set-point queue flush did not complete");
2446 }
2447 return;
2448 }
2449 if self.flush.is_error() {
2450 self.set_op_error(&format!(
2451 "Halt: set-point queue flush failed: {} {}",
2452 self.flush.error_code(),
2453 self.flush.error_message()
2454 ));
2455 return;
2456 }
2457 let mut cw = RawControlWord(view.control_word());
2460 cw.set_bit(8, false); cw.set_bit(4, false);
2462 cw.set_bit(5, false);
2463 view.set_control_word(cw.raw());
2464 view.set_target_position(view.position_actual());
2465 self.complete_op();
2466 }
2467 None => {
2468 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
2469 self.complete_op();
2470 }
2471 }
2472 }
2473
2474 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
2479 match step {
2480 1 => {
2481 let mut cw = RawControlWord(view.control_word());
2483 cw.cmd_fault_reset();
2484 view.set_control_word(cw.raw());
2485 self.op = AxisOp::FaultRecovery(2);
2486 }
2487 2 => {
2488 let sw = RawStatusWord(view.status_word());
2490 let state = sw.state();
2491 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
2492 log::info!("Fault cleared (drive state: {})", state);
2493 self.complete_op();
2494 } else if self.op_timed_out() {
2495 self.set_op_error("Fault reset timeout: drive still faulted");
2496 }
2497 }
2498 _ => self.complete_op(),
2499 }
2500 }
2501}
2502
2503#[cfg(test)]
2508mod tests {
2509 use super::*;
2510
2511 struct MockView {
2513 control_word: u16,
2514 status_word: u16,
2515 target_position: i32,
2516 profile_velocity: u32,
2517 profile_acceleration: u32,
2518 profile_deceleration: u32,
2519 modes_of_operation: i8,
2520 modes_of_operation_display: i8,
2521 position_actual: i32,
2522 velocity_actual: i32,
2523 error_code: u16,
2524 positive_limit: bool,
2525 negative_limit: bool,
2526 home_sensor: bool,
2527 }
2528
2529 impl MockView {
2530 fn new() -> Self {
2531 Self {
2532 control_word: 0,
2533 status_word: 0x0040, target_position: 0,
2535 profile_velocity: 0,
2536 profile_acceleration: 0,
2537 profile_deceleration: 0,
2538 modes_of_operation: 0,
2539 modes_of_operation_display: 1, position_actual: 0,
2541 velocity_actual: 0,
2542 error_code: 0,
2543 positive_limit: false,
2544 negative_limit: false,
2545 home_sensor: false,
2546 }
2547 }
2548
2549 fn set_state(&mut self, state: u16) {
2550 self.status_word = state;
2551 }
2552 }
2553
2554 impl AxisView for MockView {
2555 fn control_word(&self) -> u16 {
2556 self.control_word
2557 }
2558 fn set_control_word(&mut self, word: u16) {
2559 self.control_word = word;
2560 }
2561 fn set_target_position(&mut self, pos: i32) {
2562 self.target_position = pos;
2563 }
2564 fn set_profile_velocity(&mut self, vel: u32) {
2565 self.profile_velocity = vel;
2566 }
2567 fn set_profile_acceleration(&mut self, accel: u32) {
2568 self.profile_acceleration = accel;
2569 }
2570 fn set_profile_deceleration(&mut self, decel: u32) {
2571 self.profile_deceleration = decel;
2572 }
2573 fn set_modes_of_operation(&mut self, mode: i8) {
2574 self.modes_of_operation = mode;
2575 }
2576 fn modes_of_operation_display(&self) -> i8 {
2577 self.modes_of_operation_display
2578 }
2579 fn status_word(&self) -> u16 {
2580 self.status_word
2581 }
2582 fn position_actual(&self) -> i32 {
2583 self.position_actual
2584 }
2585 fn velocity_actual(&self) -> i32 {
2586 self.velocity_actual
2587 }
2588 fn error_code(&self) -> u16 {
2589 self.error_code
2590 }
2591 fn positive_limit_active(&self) -> bool {
2592 self.positive_limit
2593 }
2594 fn negative_limit_active(&self) -> bool {
2595 self.negative_limit
2596 }
2597 fn home_sensor_active(&self) -> bool {
2598 self.home_sensor
2599 }
2600 }
2601
2602 fn test_config() -> AxisConfig {
2603 AxisConfig::new(12_800).with_user_scale(360.0)
2604 }
2605
2606 fn test_axis() -> (
2608 Axis,
2609 CommandClient,
2610 tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2611 tokio::sync::mpsc::UnboundedReceiver<String>,
2612 ) {
2613 use tokio::sync::mpsc;
2614 let (write_tx, write_rx) = mpsc::unbounded_channel();
2615 let (response_tx, response_rx) = mpsc::unbounded_channel();
2616 let client = CommandClient::new(write_tx, response_rx);
2617 let axis = Axis::new(test_config(), "TestDrive");
2618 (axis, client, response_tx, write_rx)
2619 }
2620
2621 #[test]
2622 fn axis_config_conversion() {
2623 let cfg = test_config();
2624 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2626 }
2627
2628 #[test]
2629 fn trace_records_on_change_not_every_tick() {
2630 let (mut axis, mut client, _r, _w) = test_axis();
2631 let mut view = MockView::new();
2632 view.set_state(0x0040); for _ in 0..10 {
2637 axis.tick(&mut view, &mut client);
2638 }
2639 let after_idle = axis.trace().lines().count();
2640 assert!(
2641 after_idle <= 1,
2642 "constant state must record ≤1 line, got {}",
2643 after_idle
2644 );
2645
2646 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2649 assert!(
2650 axis.trace().lines().count() > after_idle,
2651 "a SW change adds a line"
2652 );
2653 assert!(
2654 axis.trace().contains("Operation Enabled"),
2655 "trace decodes the SW state"
2656 );
2657 }
2658
2659 #[test]
2660 fn trace_decoders_render_handshake_bits() {
2661 assert_eq!(decode_cw(0x003F), "SwOn|EnVolt|QStop|EnOp|NewSP|ChgSet");
2663 assert!(
2664 decode_cw(0x013F).contains("HALT"),
2665 "0x13F still asserts HALT"
2666 );
2667 let s = decode_sw(0x1027);
2669 assert!(
2670 s.contains("Operation Enabled") && s.contains("SP_ACK"),
2671 "got {s}"
2672 );
2673 }
2674
2675 #[test]
2676 fn enable_sequence_sets_pp_mode_and_shutdown() {
2677 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2678 let mut view = MockView::new();
2679
2680 axis.enable(&mut view);
2681
2682 assert_eq!(
2684 view.modes_of_operation,
2685 ModesOfOperation::ProfilePosition.as_i8()
2686 );
2687 assert_eq!(view.control_word & 0x008F, 0x0006);
2689 assert_eq!(axis.op, AxisOp::Enabling(1));
2691
2692 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2695
2696 assert_eq!(view.control_word & 0x008F, 0x000F);
2698 assert_eq!(axis.op, AxisOp::Enabling(2));
2699
2700 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2703
2704 assert_eq!(axis.op, AxisOp::Idle);
2706 assert!(axis.motor_on);
2707 }
2708
2709 #[test]
2710 fn move_absolute_sets_target() {
2711 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2712 let mut view = MockView::new();
2713 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2718
2719 assert_eq!(view.target_position, 1600);
2721 assert_eq!(view.profile_velocity, 3200);
2723 assert_eq!(view.profile_acceleration, 6400);
2725 assert_eq!(view.profile_deceleration, 6400);
2726 assert!(view.control_word & (1 << 4) != 0);
2728 assert!(view.control_word & (1 << 6) == 0);
2730 assert!(matches!(
2732 axis.op,
2733 AxisOp::Moving(MoveKind::Absolute, 1, _, _)
2734 ));
2735 }
2736
2737 #[test]
2738 fn move_relative_sets_relative_bit() {
2739 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2740 let mut view = MockView::new();
2741 view.set_state(0x0027);
2742 axis.tick(&mut view, &mut client);
2743
2744 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2745
2746 assert!(view.control_word & (1 << 6) != 0);
2748 assert!(matches!(
2749 axis.op,
2750 AxisOp::Moving(MoveKind::Relative, 1, _, _)
2751 ));
2752 }
2753
2754 #[test]
2755 fn move_completes_on_target_reached() {
2756 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2757 let mut view = MockView::new();
2758 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2760
2761 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2762
2763 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2766 assert!(view.control_word & (1 << 4) == 0);
2768
2769 view.status_word = 0x0027; axis.tick(&mut view, &mut client);
2773
2774 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2777 assert_eq!(axis.op, AxisOp::Idle);
2779 assert!(!axis.in_motion);
2780 }
2781
2782 #[test]
2783 fn fault_detected_sets_error() {
2784 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2785 let mut view = MockView::new();
2786 view.set_state(0x0008); view.error_code = 0x1234;
2788
2789 axis.tick(&mut view, &mut client);
2790
2791 assert!(axis.is_error);
2792 assert_eq!(axis.error_code, 0x1234);
2793 assert!(axis.error_message.contains("fault"));
2794 }
2795
2796 #[test]
2797 fn fault_recovery_sequence() {
2798 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2799 let mut view = MockView::new();
2800 view.set_state(0x0008); axis.reset_faults(&mut view);
2803 assert!(view.control_word & 0x0080 == 0);
2805
2806 axis.tick(&mut view, &mut client);
2808 assert!(view.control_word & 0x0080 != 0);
2809
2810 view.set_state(0x0040);
2812 axis.tick(&mut view, &mut client);
2813 assert_eq!(axis.op, AxisOp::Idle);
2814 assert!(!axis.is_error);
2815 }
2816
2817 #[test]
2818 fn reset_faults_software_only_is_synchronous() {
2819 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2825 let mut view = MockView::new();
2826 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2828
2829 axis.set_software_min_limit(207.0);
2831 axis.move_absolute(&mut view, 200.0, 90.0, 180.0, 180.0);
2832 assert!(axis.is_error, "move past software limit must set is_error");
2833 assert!(!axis.is_busy);
2834
2835 let cw_before = view.control_word;
2838
2839 axis.reset_faults(&mut view);
2840
2841 assert!(!axis.is_error);
2843 assert_eq!(axis.error_code, 0);
2844 assert!(axis.error_message.is_empty());
2845 assert!(!axis.is_busy);
2846 assert_eq!(axis.op, AxisOp::Idle);
2847 assert_eq!(view.control_word, cw_before);
2849 }
2850
2851 #[test]
2852 fn disable_sequence() {
2853 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2854 let mut view = MockView::new();
2855 view.set_state(0x0027); axis.disable(&mut view);
2858 assert_eq!(view.control_word & 0x008F, 0x0006);
2860
2861 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2864 assert_eq!(axis.op, AxisOp::Idle);
2865 }
2866
2867 #[test]
2868 fn position_tracks_with_home_offset() {
2869 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2870 let mut view = MockView::new();
2871 view.set_state(0x0027);
2872
2873 axis.home_offset = 5000;
2877 view.position_actual = 6600;
2878 axis.tick(&mut view, &mut client);
2879
2880 assert!((axis.position - 45.0).abs() < 0.1);
2882 }
2883
2884 #[test]
2885 fn set_position_adjusts_home_offset() {
2886 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2887 let mut view = MockView::new();
2888 view.position_actual = 3200;
2889
2890 axis.set_position(&view, 90.0);
2891 axis.tick(&mut view, &mut client);
2892
2893 assert_eq!(axis.home_offset, 0);
2895 assert!((axis.position - 90.0).abs() < 0.01);
2896 }
2897
2898 #[test]
2899 fn halt_runs_multi_stage_close_out() {
2900 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2901 let mut view = MockView::new();
2902 view.set_state(0x0027);
2903
2904 axis.halt(&mut view);
2905
2906 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2908 assert!(
2909 view.control_word & (1 << 4) == 0,
2910 "new_setpoint must be cleared"
2911 );
2912
2913 assert!(
2915 matches!(axis.op, AxisOp::Halting(_)),
2916 "halt should enter Halting state, not Idle"
2917 );
2918 let AxisOp::Halting(step) = axis.op.clone() else {
2919 unreachable!()
2920 };
2921 assert_eq!(step, HaltState::WaitStopped as u8);
2922
2923 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2930 axis.tick(&mut view, &mut client);
2931 }
2932 assert!(matches!(axis.op, AxisOp::Halting(_)));
2935 let AxisOp::Halting(step) = axis.op.clone() else {
2936 unreachable!()
2937 };
2938 assert_eq!(
2939 step,
2940 HaltState::Flushing as u8,
2941 "should advance past WaitStopped once position/velocity is stable"
2942 );
2943 assert!(view.control_word & (1 << 8) != 0, "halt held through the away mode switch");
2946
2947 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2951 }
2952
2953 #[test]
2954 fn is_busy_tracks_operations() {
2955 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2956 let mut view = MockView::new();
2957
2958 axis.tick(&mut view, &mut client);
2960 assert!(!axis.is_busy);
2961
2962 axis.enable(&mut view);
2964 axis.tick(&mut view, &mut client);
2965 assert!(axis.is_busy);
2966
2967 view.set_state(0x0021);
2969 axis.tick(&mut view, &mut client);
2970 view.set_state(0x0027);
2971 axis.tick(&mut view, &mut client);
2972 assert!(!axis.is_busy);
2973
2974 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2976 axis.tick(&mut view, &mut client);
2977 assert!(axis.is_busy);
2978 assert!(axis.in_motion);
2979 }
2980
2981 #[test]
2982 fn fault_during_move_cancels_op() {
2983 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2984 let mut view = MockView::new();
2985 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2987
2988 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2990 axis.tick(&mut view, &mut client);
2991 assert!(axis.is_busy);
2992 assert!(!axis.is_error);
2993
2994 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2997
2998 assert!(!axis.is_busy);
3000 assert!(axis.is_error);
3001 assert_eq!(axis.op, AxisOp::Idle);
3002 }
3003
3004 #[test]
3005 fn move_absolute_rejected_by_max_limit() {
3006 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3007 let mut view = MockView::new();
3008 view.set_state(0x0027);
3009 axis.tick(&mut view, &mut client);
3010
3011 axis.set_software_max_limit(90.0);
3012 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
3013
3014 assert!(axis.is_error);
3016 assert_eq!(axis.op, AxisOp::Idle);
3017 assert!(axis.error_message.contains("max software limit"));
3018 }
3019
3020 #[test]
3021 fn move_absolute_rejected_by_min_limit() {
3022 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3023 let mut view = MockView::new();
3024 view.set_state(0x0027);
3025 axis.tick(&mut view, &mut client);
3026
3027 axis.set_software_min_limit(-10.0);
3028 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
3029
3030 assert!(axis.is_error);
3031 assert_eq!(axis.op, AxisOp::Idle);
3032 assert!(axis.error_message.contains("min software limit"));
3033 }
3034
3035 #[test]
3036 fn move_relative_rejected_by_max_limit() {
3037 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3038 let mut view = MockView::new();
3039 view.set_state(0x0027);
3040 axis.tick(&mut view, &mut client);
3041
3042 axis.set_software_max_limit(50.0);
3044 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
3045
3046 assert!(axis.is_error);
3047 assert_eq!(axis.op, AxisOp::Idle);
3048 assert!(axis.error_message.contains("max software limit"));
3049 }
3050
3051 #[test]
3052 fn move_within_limits_allowed() {
3053 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3054 let mut view = MockView::new();
3055 view.set_state(0x0027);
3056 axis.tick(&mut view, &mut client);
3057
3058 axis.set_software_max_limit(90.0);
3059 axis.set_software_min_limit(-90.0);
3060 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
3061
3062 assert!(!axis.is_error);
3064 assert!(matches!(
3065 axis.op,
3066 AxisOp::Moving(MoveKind::Absolute, 1, _, _)
3067 ));
3068 }
3069
3070 #[test]
3071 fn runtime_limit_halts_move_in_violated_direction() {
3072 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3073 let mut view = MockView::new();
3074 view.set_state(0x0027);
3075 axis.tick(&mut view, &mut client);
3076
3077 axis.set_software_max_limit(45.0);
3078 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
3080
3081 view.position_actual = 1650;
3084 view.velocity_actual = 100; view.status_word = 0x1027;
3088 axis.tick(&mut view, &mut client);
3089 view.status_word = 0x0027;
3090 axis.tick(&mut view, &mut client);
3091
3092 assert!(!axis.is_error);
3096 assert!(axis.at_max_limit);
3097 assert!(axis.is_busy);
3098 assert!(matches!(axis.op, AxisOp::Halting(_)));
3099 assert!(view.control_word & (1 << 8) != 0);
3101 }
3102
3103 #[test]
3104 fn runtime_limit_allows_move_in_opposite_direction() {
3105 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3106 let mut view = MockView::new();
3107 view.set_state(0x0027);
3108 view.position_actual = 1778; axis.set_software_max_limit(45.0);
3111 axis.tick(&mut view, &mut client);
3112 assert!(axis.at_max_limit);
3113
3114 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
3116 assert!(!axis.is_error);
3117 assert!(matches!(
3118 axis.op,
3119 AxisOp::Moving(MoveKind::Absolute, 1, _, _)
3120 ));
3121
3122 view.velocity_actual = -100;
3124 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
3126 assert!(!axis.is_error);
3128 }
3129
3130 #[test]
3131 fn positive_limit_switch_halts_positive_move() {
3132 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3133 let mut view = MockView::new();
3134 view.set_state(0x0027);
3135 axis.tick(&mut view, &mut client);
3136
3137 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
3139 view.velocity_actual = 100; view.status_word = 0x1027;
3142 axis.tick(&mut view, &mut client);
3143 view.status_word = 0x0027;
3144
3145 view.positive_limit = true;
3147 axis.tick(&mut view, &mut client);
3148
3149 assert!(!axis.is_error);
3154 assert!(axis.at_positive_limit_switch);
3155 assert!(axis.is_busy);
3156 assert!(matches!(axis.op, AxisOp::Halting(_)));
3157 assert!(view.control_word & (1 << 8) != 0);
3159 }
3160
3161 #[test]
3162 fn negative_limit_switch_halts_negative_move() {
3163 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3164 let mut view = MockView::new();
3165 view.set_state(0x0027);
3166 axis.tick(&mut view, &mut client);
3167
3168 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
3170 view.velocity_actual = -100; view.status_word = 0x1027;
3172 axis.tick(&mut view, &mut client);
3173 view.status_word = 0x0027;
3174
3175 view.negative_limit = true;
3177 axis.tick(&mut view, &mut client);
3178
3179 assert!(!axis.is_error);
3184 assert!(axis.at_negative_limit_switch);
3185 assert!(axis.is_busy);
3186 assert!(matches!(axis.op, AxisOp::Halting(_)));
3187 assert!(view.control_word & (1 << 8) != 0);
3188 }
3189
3190 #[test]
3191 fn limit_halt_flushes_queue_via_mode_toggle() {
3192 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3197 let mut view = MockView::new();
3198 view.set_state(0x0027); axis.tick(&mut view, &mut client);
3200
3201 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
3203 view.velocity_actual = 100;
3204 view.status_word = 0x1027;
3205 axis.tick(&mut view, &mut client);
3206 view.status_word = 0x0027;
3207 view.positive_limit = true;
3208 axis.tick(&mut view, &mut client);
3209
3210 assert!(!axis.is_error);
3212 assert!(axis.is_busy);
3213 assert!(matches!(axis.op, AxisOp::Halting(_)));
3214 assert!(view.control_word & (1 << 8) != 0);
3215
3216 view.velocity_actual = 0;
3223 view.positive_limit = false;
3224 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
3225 axis.tick(&mut view, &mut client);
3226 }
3227 let AxisOp::Halting(step) = axis.op.clone() else {
3228 unreachable!()
3229 };
3230 assert_eq!(step, HaltState::Flushing as u8, "halt flushes the queue via a mode toggle");
3231 assert!(view.control_word & (1 << 8) != 0, "halt held through the queue flush");
3232 assert!(axis.is_busy, "axis stays busy through the close-out");
3233 assert_ne!(
3234 view.modes_of_operation,
3235 ModesOfOperation::ProfilePosition.as_i8(),
3236 "mode switched away from PP to flush the queue",
3237 );
3238 }
3239
3240 #[test]
3241 fn limit_switch_allows_move_in_opposite_direction() {
3242 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3243 let mut view = MockView::new();
3244 view.set_state(0x0027);
3245 view.positive_limit = true;
3247 view.velocity_actual = -100;
3248 axis.tick(&mut view, &mut client);
3249 assert!(axis.at_positive_limit_switch);
3250
3251 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
3253 view.status_word = 0x1027;
3254 axis.tick(&mut view, &mut client);
3255
3256 assert!(!axis.is_error);
3258 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
3259 }
3260
3261 #[test]
3262 fn limit_switch_ignored_when_not_moving() {
3263 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3264 let mut view = MockView::new();
3265 view.set_state(0x0027);
3266 view.positive_limit = true;
3267
3268 axis.tick(&mut view, &mut client);
3269
3270 assert!(axis.at_positive_limit_switch);
3272 assert!(!axis.is_error);
3273 }
3274
3275 #[test]
3276 fn home_sensor_output_tracks_view() {
3277 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3278 let mut view = MockView::new();
3279 view.set_state(0x0027);
3280
3281 axis.tick(&mut view, &mut client);
3282 assert!(!axis.home_sensor);
3283
3284 view.home_sensor = true;
3285 axis.tick(&mut view, &mut client);
3286 assert!(axis.home_sensor);
3287
3288 view.home_sensor = false;
3289 axis.tick(&mut view, &mut client);
3290 assert!(!axis.home_sensor);
3291 }
3292
3293 #[test]
3294 fn velocity_output_converted() {
3295 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3296 let mut view = MockView::new();
3297 view.set_state(0x0027);
3298 view.velocity_actual = 3200;
3300
3301 axis.tick(&mut view, &mut client);
3302
3303 assert!((axis.speed - 90.0).abs() < 0.1);
3304 assert!(axis.moving_positive);
3305 assert!(!axis.moving_negative);
3306 }
3307
3308 fn soft_homing_config() -> AxisConfig {
3311 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
3312 cfg.homing_speed = 10.0;
3313 cfg.homing_accel = 20.0;
3314 cfg.homing_decel = 20.0;
3315 cfg
3316 }
3317
3318 fn soft_homing_axis() -> (
3319 Axis,
3320 CommandClient,
3321 tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
3322 tokio::sync::mpsc::UnboundedReceiver<String>,
3323 ) {
3324 use tokio::sync::mpsc;
3325 let (write_tx, write_rx) = mpsc::unbounded_channel();
3326 let (response_tx, response_rx) = mpsc::unbounded_channel();
3327 let client = CommandClient::new(write_tx, response_rx);
3328 let axis = Axis::new(soft_homing_config(), "TestDrive");
3329 (axis, client, response_tx, write_rx)
3330 }
3331
3332 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
3334 view.set_state(0x0027); axis.tick(view, client);
3336 }
3337
3338 fn ack_next_sdo(
3344 write_rx: &mut tokio::sync::mpsc::UnboundedReceiver<String>,
3345 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
3346 client: &mut CommandClient,
3347 value: serde_json::Value,
3348 ) -> mechutil::ipc::CommandMessage {
3349 let raw = write_rx
3350 .try_recv()
3351 .expect("expected an outgoing SDO request on write_rx");
3352 let req: mechutil::ipc::CommandMessage =
3353 serde_json::from_str(&raw).expect("outgoing SDO request is valid CommandMessage JSON");
3354 resp_tx
3355 .send(mechutil::ipc::CommandMessage::response(
3356 req.transaction_id,
3357 value,
3358 ))
3359 .unwrap();
3360 client.poll();
3361 req
3362 }
3363
3364 fn drive_one_mode_switch(
3369 axis: &mut Axis,
3370 view: &mut MockView,
3371 client: &mut CommandClient,
3372 write_rx: &mut tokio::sync::mpsc::UnboundedReceiver<String>,
3373 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
3374 mode: i8,
3375 ) {
3376 ack_next_sdo(write_rx, resp_tx, client, json!(null));
3378 axis.tick(view, client);
3379 std::thread::sleep(Duration::from_millis(110));
3380 axis.tick(view, client);
3382 ack_next_sdo(write_rx, resp_tx, client, json!({ "value": mode as i64 }));
3383 axis.tick(view, client);
3384 }
3385
3386 #[test]
3387 fn soft_homing_pnp_sensor_detection_halts() {
3388 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3389 let mut view = MockView::new();
3390 enable_axis(&mut axis, &mut view, &mut client);
3391
3392 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3393 axis.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
3395
3396 view.home_sensor = true;
3398 view.position_actual = 5000;
3399 axis.tick(&mut view, &mut client);
3400
3401 assert_eq!(
3403 axis.op,
3404 AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8)
3405 );
3406 assert!(
3407 view.control_word & (1 << 8) != 0,
3408 "halt bit set on sensor detection"
3409 );
3410 assert!(!axis.is_error);
3411 }
3412
3413 #[test]
3414 fn soft_homing_npn_sensor_detection_halts() {
3415 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3416 let mut view = MockView::new();
3417 view.home_sensor = true;
3419 enable_axis(&mut axis, &mut view, &mut client);
3420
3421 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
3422 axis.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
3423
3424 view.home_sensor = false;
3426 view.position_actual = 3000;
3427 axis.tick(&mut view, &mut client);
3428
3429 assert_eq!(
3430 axis.op,
3431 AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8)
3432 );
3433 assert!(!axis.is_error);
3434 }
3435
3436 #[test]
3437 fn soft_homing_limit_switch_suppresses_halt() {
3438 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3439 let mut view = MockView::new();
3440 enable_axis(&mut axis, &mut view, &mut client);
3441
3442 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
3444 axis.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
3445
3446 view.positive_limit = true;
3450 view.velocity_actual = 100; view.position_actual = 8000;
3452 axis.tick(&mut view, &mut client);
3453
3454 assert_eq!(
3455 axis.op,
3456 AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8)
3457 );
3458 assert!(
3459 !axis.is_error,
3460 "homed-on limit switch must be suppressed, not faulted"
3461 );
3462 }
3463
3464 #[test]
3465 fn soft_homing_opposite_limit_still_protects() {
3466 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3467 let mut view = MockView::new();
3468 enable_axis(&mut axis, &mut view, &mut client);
3469
3470 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3472
3473 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3478 axis.tick(&mut view, &mut client); view.negative_limit = true;
3483 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
3485
3486 assert!(axis.is_error);
3488 assert!(axis.error_message.contains("Negative limit switch"));
3489 }
3490
3491 #[test]
3492 fn soft_homing_search_sets_directional_target() {
3493 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3494 let mut view = MockView::new();
3495 enable_axis(&mut axis, &mut view, &mut client);
3496
3497 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
3499 axis.op = AxisOp::SoftHoming(HomeState::Search as u8); axis.tick(&mut view, &mut client);
3501 assert!(view.target_position < 0, "negative home seeks a negative target");
3502
3503 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3505 axis.op = AxisOp::SoftHoming(HomeState::Search as u8);
3506 axis.tick(&mut view, &mut client);
3507 assert!(view.target_position > 0, "positive home seeks a positive target");
3508 }
3509
3510 #[test]
3511 fn home_current_position_routes_to_drive_set() {
3512 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3513 let mut view = MockView::new();
3514 enable_axis(&mut axis, &mut view, &mut client);
3515
3516 axis.home(&mut view, HomingMethod::CurrentPosition35);
3520 assert!(matches!(
3521 axis.op,
3522 AxisOp::SoftHoming(s) if s == HomeState::WriteHomeOffset as u8
3523 ));
3524 assert_eq!(axis.homing_method, 35);
3525 }
3526
3527 #[test]
3528 fn home_integrated_arbitrary_code() {
3529 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3530 let mut view = MockView::new();
3531 enable_axis(&mut axis, &mut view, &mut client);
3532
3533 axis.home(&mut view, HomingMethod::Integrated(35));
3534 assert!(matches!(axis.op, AxisOp::Homing(16)));
3536 assert_eq!(axis.homing_method, 35);
3537 }
3538
3539 #[test]
3540 fn hardware_homing_skips_speed_sdos_when_zero() {
3541 use mechutil::ipc::CommandMessage;
3542
3543 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
3544 let mut view = MockView::new();
3545 enable_axis(&mut axis, &mut view, &mut client);
3546
3547 axis.home(&mut view, HomingMethod::Integrated(37));
3549
3550 axis.tick(&mut view, &mut client);
3552 assert!(matches!(axis.op, AxisOp::Homing(17)));
3553 let _ = write_rx.try_recv();
3554 let tid = axis.homing_sdo_tid;
3555 resp_tx
3556 .send(CommandMessage::response(tid, serde_json::json!(null)))
3557 .unwrap();
3558 client.poll();
3559 axis.tick(&mut view, &mut client);
3560 assert!(matches!(axis.op, AxisOp::Homing(0)));
3561
3562 axis.tick(&mut view, &mut client);
3564 assert!(matches!(axis.op, AxisOp::Homing(1)));
3565
3566 let _ = write_rx.try_recv();
3568
3569 let tid = axis.homing_sdo_tid;
3571 resp_tx
3572 .send(CommandMessage::response(tid, serde_json::json!(null)))
3573 .unwrap();
3574 client.poll();
3575 axis.tick(&mut view, &mut client);
3576
3577 assert!(matches!(axis.op, AxisOp::Homing(8)));
3579 }
3580
3581 #[test]
3582 fn hardware_homing_writes_speed_sdos_when_nonzero() {
3583 use mechutil::ipc::CommandMessage;
3584
3585 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3586 let mut view = MockView::new();
3587 enable_axis(&mut axis, &mut view, &mut client);
3588
3589 axis.home(&mut view, HomingMethod::Integrated(37));
3591
3592 axis.tick(&mut view, &mut client);
3594 assert!(matches!(axis.op, AxisOp::Homing(17)));
3595 let _ = write_rx.try_recv();
3596 let tid = axis.homing_sdo_tid;
3597 resp_tx
3598 .send(CommandMessage::response(tid, serde_json::json!(null)))
3599 .unwrap();
3600 client.poll();
3601 axis.tick(&mut view, &mut client);
3602
3603 axis.tick(&mut view, &mut client);
3605 assert!(matches!(axis.op, AxisOp::Homing(1)));
3606 let _ = write_rx.try_recv();
3607
3608 let tid = axis.homing_sdo_tid;
3610 resp_tx
3611 .send(CommandMessage::response(tid, serde_json::json!(null)))
3612 .unwrap();
3613 client.poll();
3614 axis.tick(&mut view, &mut client);
3615 assert!(matches!(axis.op, AxisOp::Homing(2)));
3617 }
3618
3619 #[test]
3620 fn soft_homing_edge_during_search_ack() {
3621 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3622 let mut view = MockView::new();
3623 enable_axis(&mut axis, &mut view, &mut client);
3624
3625 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3626 axis.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
3628
3629 view.home_sensor = true;
3631 view.position_actual = 2000;
3632 axis.tick(&mut view, &mut client);
3633
3634 assert_eq!(
3635 axis.op,
3636 AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8)
3637 );
3638 }
3639
3640 #[test]
3645 fn soft_homing_completes_on_attained_without_reached() {
3646 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3647 let mut view = MockView::new();
3648 view.status_word = 0x1027;
3650 axis.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
3651 axis.op_started = Some(Instant::now());
3652
3653 axis.tick(&mut view, &mut client);
3654
3655 assert_eq!(
3656 axis.op,
3657 AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8),
3658 "attained-only must complete homing, not wait for target_reached"
3659 );
3660 assert!(!axis.is_error);
3661 }
3662
3663 #[test]
3668 fn soft_homing_start_gate_does_not_require_attained_clear() {
3669 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3670 let mut view = MockView::new();
3671 view.status_word = 0x1027;
3673 axis.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
3674 axis.op_started = Some(Instant::now());
3675
3676 axis.tick(&mut view, &mut client);
3677
3678 assert_eq!(
3679 axis.op,
3680 AxisOp::SoftHoming(HomeState::WaitHomingDone as u8),
3681 "start gate must advance even with attained already set"
3682 );
3683 assert!(!axis.is_error);
3684 }
3685
3686 #[test]
3690 fn flush_queue_toggles_pp_homing_pp() {
3691 use tokio::sync::mpsc;
3692 let (write_tx, mut write_rx) = mpsc::unbounded_channel();
3693 let (resp_tx, response_rx) = mpsc::unbounded_channel();
3694 let mut client = CommandClient::new(write_tx, response_rx);
3695 let mut view = MockView::new();
3696 let mut sdo = SdoClient::new("TestDrive");
3697 let mut flush = FbFlushQueue::new();
3698
3699 flush.start(&mut view, &mut client, &mut sdo);
3701 assert!(flush.is_busy());
3702 assert_eq!(view.modes_of_operation, ModesOfOperation::Homing.as_i8());
3703
3704 ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!(null)); flush.tick(&mut view, &mut client, &mut sdo);
3707 std::thread::sleep(Duration::from_millis(110));
3708 flush.tick(&mut view, &mut client, &mut sdo); ack_next_sdo(
3710 &mut write_rx,
3711 &resp_tx,
3712 &mut client,
3713 json!({ "value": ModesOfOperation::Homing.as_i8() as i64 }),
3714 );
3715 flush.tick(&mut view, &mut client, &mut sdo); assert!(flush.is_busy(), "still flushing — now switching back to PP");
3718 assert_eq!(
3719 view.modes_of_operation,
3720 ModesOfOperation::ProfilePosition.as_i8()
3721 );
3722
3723 ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!(null)); flush.tick(&mut view, &mut client, &mut sdo);
3726 std::thread::sleep(Duration::from_millis(110));
3727 flush.tick(&mut view, &mut client, &mut sdo); ack_next_sdo(
3729 &mut write_rx,
3730 &resp_tx,
3731 &mut client,
3732 json!({ "value": ModesOfOperation::ProfilePosition.as_i8() as i64 }),
3733 );
3734 flush.tick(&mut view, &mut client, &mut sdo);
3735
3736 assert!(!flush.is_busy(), "flush complete");
3737 assert!(!flush.is_error());
3738 }
3739
3740 #[test]
3747 fn home_current_position_sets_drive_and_zeros_offset() {
3748 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3749 let mut view = MockView::new();
3750 enable_axis(&mut axis, &mut view, &mut client);
3751 view.status_word = 0x1027;
3753
3754 axis.set_home_position(90.0);
3755 axis.home(&mut view, HomingMethod::CurrentPosition35);
3756 assert_eq!(axis.config.home_position, 90.0);
3757 assert!(matches!(
3758 axis.op,
3759 AxisOp::SoftHoming(s) if s == HomeState::WriteHomeOffset as u8
3760 ));
3761
3762 axis.tick(&mut view, &mut client); ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!(null));
3766 axis.tick(&mut view, &mut client); ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!({ "value": -9701 }));
3769 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); drive_one_mode_switch(
3774 &mut axis,
3775 &mut view,
3776 &mut client,
3777 &mut write_rx,
3778 &resp_tx,
3779 ModesOfOperation::Homing.as_i8(),
3780 ); axis.tick(&mut view, &mut client); ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!(null));
3785 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); assert_eq!(axis.home_offset, 0);
3796
3797 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); drive_one_mode_switch(
3808 &mut axis,
3809 &mut view,
3810 &mut client,
3811 &mut write_rx,
3812 &resp_tx,
3813 ModesOfOperation::ProfilePosition.as_i8(),
3814 ); assert_eq!(axis.op, AxisOp::Idle);
3817 assert!(!axis.is_error, "persistent home must finish clean");
3818 assert!(!axis.is_busy);
3819 assert_eq!(axis.home_offset, 0);
3820 }
3821}