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 mut cw = RawControlWord(view.control_word());
662 cw.cmd_shutdown();
663 cw.set_bit(4, false);
664 cw.set_bit(8, false);
665 cw.set_bit(7, false);
666 cw.set_bit(2, true);
667 view.set_control_word(cw.raw());
668
669 self.op = AxisOp::Disabling(1);
670 self.op_started = Some(Instant::now());
671 }
672
673 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
689 let sw = RawStatusWord(view.status_word());
690 let drive_in_fault = matches!(
691 sw.state(),
692 Cia402State::Fault | Cia402State::FaultReactionActive
693 );
694
695 self.is_error = false;
696 self.error_code = 0;
697 self.error_message.clear();
698
699 if drive_in_fault {
700 let mut cw = RawControlWord(view.control_word());
702 cw.cmd_clear_fault_reset();
703 view.set_control_word(cw.raw());
704 self.op = AxisOp::FaultRecovery(1);
705 self.op_started = Some(Instant::now());
706 } else {
707 self.op = AxisOp::Idle;
709 self.op_started = None;
710 self.is_busy = false;
711 self.in_motion = false;
712 }
713 }
714
715 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
723 match method {
724 HomingMethod::CurrentPosition35 | HomingMethod::CurrentPosition37 => {
731 self.homing_method = method.cia402_code();
732 self.op = AxisOp::SoftHoming(HomeState::WriteHomeOffset as u8);
733 self.op_started = Some(Instant::now());
734 let _ = view;
735 }
736 m if m.is_integrated() => {
741 self.homing_method = m.cia402_code();
742 self.op = AxisOp::Homing(16);
743 self.op_started = Some(Instant::now());
744 let _ = view;
745 }
746 m => {
750 self.homing_method = self.config.soft_home_method;
751 self.configure_soft_homing(m);
752 self.start_soft_homing(view);
753 }
754 }
755 }
756
757 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
766 self.home_offset =
767 view.position_actual() - self.config.to_counts(user_units).round() as i32;
768 }
769
770 pub fn set_home_position(&mut self, user_units: f64) {
774 self.config.home_position = user_units;
775 }
776
777 pub fn set_software_max_limit(&mut self, user_units: f64) {
779 self.config.max_position_limit = user_units;
780 self.config.enable_max_position_limit = true;
781 }
782
783 pub fn set_software_min_limit(&mut self, user_units: f64) {
785 self.config.min_position_limit = user_units;
786 self.config.enable_min_position_limit = true;
787 }
788
789 pub fn sdo_write(
795 &mut self,
796 client: &mut CommandClient,
797 index: u16,
798 sub_index: u8,
799 value: serde_json::Value,
800 ) {
801 self.sdo.write(client, index, sub_index, value);
802 }
803
804 pub fn sdo_read(&mut self, client: &mut CommandClient, index: u16, sub_index: u8) -> u32 {
806 self.sdo.read(client, index, sub_index)
807 }
808
809 pub fn sdo_result(&mut self, client: &mut CommandClient, tid: u32) -> SdoResult {
811 self.sdo.result(client, tid, Duration::from_secs(5))
812 }
813
814 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
828 self.check_faults(view);
829 self.progress_op(view, client);
830 self.update_outputs(view);
831 self.check_limits(view);
832 self.record_trace(view);
833 }
834
835 fn record_trace(&mut self, view: &impl AxisView) {
843 let cw = view.control_word();
844 let sw = view.status_word();
845 let changed = self
846 .cw_sw_trace
847 .back()
848 .map_or(true, |e| e.cw != cw || e.sw != sw || e.op != self.op);
849 if !changed {
850 return;
851 }
852 if self.cw_sw_trace.len() >= CW_SW_TRACE_CAP {
853 self.cw_sw_trace.pop_front();
854 }
855 self.cw_sw_trace.push_back(TraceEntry {
856 seq: self.trace_seq,
857 op: self.op.clone(),
858 cw,
859 sw,
860 });
861 self.trace_seq = self.trace_seq.wrapping_add(1);
862 }
863
864 pub fn trace(&self) -> String {
875 let mut out = String::new();
876 for e in &self.cw_sw_trace {
877 out.push_str(&format!(
878 " [{:>4}] {:<20} CW={:#06x} [{}] SW={:#06x} [{}]\n",
879 e.seq,
880 format!("{:?}", e.op),
881 e.cw,
882 decode_cw(e.cw),
883 e.sw,
884 decode_sw(e.sw),
885 ));
886 }
887 out
888 }
889
890 fn update_outputs(&mut self, view: &impl AxisView) {
895 let raw = view.position_actual();
896 self.raw_position = raw as i64;
897 self.position = self.config.to_user((raw - self.home_offset) as f64);
898
899 let vel = view.velocity_actual();
900 let user_vel = self.config.to_user(vel as f64);
901 self.speed = user_vel.abs();
902 self.moving_positive = user_vel > 0.0;
903 self.moving_negative = user_vel < 0.0;
904 self.is_busy = self.op != AxisOp::Idle;
905 self.in_motion = matches!(self.op, AxisOp::Moving(_, _, _, _) | AxisOp::SoftHoming(_));
906
907 let sw = RawStatusWord(view.status_word());
908 self.motor_on = sw.state() == Cia402State::OperationEnabled;
909
910 self.last_raw_position = raw;
911 }
912
913 fn check_faults(&mut self, view: &impl AxisView) {
918 let sw = RawStatusWord(view.status_word());
919 let state = sw.state();
920
921 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
922 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
923 self.is_error = true;
924 let ec = view.error_code();
925 if ec != 0 {
926 self.error_code = ec as u32;
927 }
928 self.error_message = format!("Drive fault (state: {})", state);
929 self.op = AxisOp::Idle;
931 self.op_started = None;
932 }
933 }
934 }
935
936 fn op_timed_out(&self) -> bool {
941 self.op_started
942 .map_or(false, |t| t.elapsed() > self.op_timeout)
943 }
944
945 fn homing_timed_out(&self) -> bool {
946 self.op_started
947 .map_or(false, |t| t.elapsed() > self.homing_timeout)
948 }
949
950 fn move_start_timed_out(&self) -> bool {
951 self.op_started
952 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
953 }
954
955 fn op_stage_timed_out(&self, limit: Duration) -> bool {
959 self.op_started.map_or(false, |t| t.elapsed() > limit)
960 }
961
962 fn set_op_error(&mut self, msg: &str) {
963 self.is_error = true;
964 self.error_message = msg.to_string();
965 log::error!("Axis error: {}", msg);
966 if !self.cw_sw_trace.is_empty() {
970 log::warn!(
971 "Axis CiA-402 trace ({} recent transitions, oldest first):\n{}",
972 self.cw_sw_trace.len(),
973 self.trace(),
974 );
975 }
976 self.op = AxisOp::Idle;
977 self.op_started = None;
978 self.is_busy = false;
979 self.in_motion = false;
980 }
981
982 fn restore_pp_after_error(&mut self, msg: &str) {
983 self.is_error = true;
984 self.error_message = msg.to_string();
985 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
986 log::error!("Axis error: {}", msg);
987 }
988
989 fn finish_op_error(&mut self) {
990 self.op = AxisOp::Idle;
991 self.op_started = None;
992 self.is_busy = false;
993 self.in_motion = false;
994 }
995
996 fn complete_op(&mut self) {
997 self.op = AxisOp::Idle;
998 self.op_started = None;
999 self.is_busy = false;
1004 self.in_motion = false;
1005 }
1006
1007 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
1016 let static_limit = if self.config.enable_max_position_limit {
1017 Some(self.config.max_position_limit)
1018 } else {
1019 None
1020 };
1021 match (static_limit, view.dynamic_max_position_limit()) {
1022 (Some(s), Some(d)) => Some(s.min(d)),
1023 (Some(v), None) | (None, Some(v)) => Some(v),
1024 (None, None) => None,
1025 }
1026 }
1027
1028 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
1032 let static_limit = if self.config.enable_min_position_limit {
1033 Some(self.config.min_position_limit)
1034 } else {
1035 None
1036 };
1037 match (static_limit, view.dynamic_min_position_limit()) {
1038 (Some(s), Some(d)) => Some(s.max(d)),
1039 (Some(v), None) | (None, Some(v)) => Some(v),
1040 (None, None) => None,
1041 }
1042 }
1043
1044 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
1049 if let Some(max) = self.effective_max_limit(view) {
1050 if target > max {
1051 return Some(format!(
1052 "Target {:.3} exceeds max software limit {:.3}",
1053 target, max
1054 ));
1055 }
1056 }
1057 if let Some(min) = self.effective_min_limit(view) {
1058 if target < min {
1059 return Some(format!(
1060 "Target {:.3} exceeds min software limit {:.3}",
1061 target, min
1062 ));
1063 }
1064 }
1065 None
1066 }
1067
1068 fn check_limits(&mut self, view: &mut impl AxisView) {
1077 let eff_max = self.effective_max_limit(view);
1079 let eff_min = self.effective_min_limit(view);
1080 let sw_max = eff_max.map_or(false, |m| self.position >= m);
1081 let sw_min = eff_min.map_or(false, |m| self.position <= m);
1082
1083 self.at_max_limit = sw_max;
1084 self.at_min_limit = sw_min;
1085
1086 let hw_pos = view.positive_limit_active();
1088 let hw_neg = view.negative_limit_active();
1089
1090 self.at_positive_limit_switch = hw_pos;
1091 self.at_negative_limit_switch = hw_neg;
1092
1093 self.home_sensor = view.home_sensor_active();
1095
1096 self.prev_positive_limit = hw_pos;
1098 self.prev_negative_limit = hw_neg;
1099 self.prev_home_sensor = view.home_sensor_active();
1100
1101 let mut commanded_positive = false;
1103 let mut commanded_negative = false;
1104
1105 let is_moving = matches!(self.op, AxisOp::Moving(_, _, _, _));
1106 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
1107
1108 if !is_moving && !is_soft_homing {
1109 return; }
1111
1112 match &self.op {
1113 AxisOp::Moving(_, _, pos, neg) => {
1114 commanded_positive = *pos;
1116 commanded_negative = *neg;
1117 }
1118 AxisOp::SoftHoming(_) => match self.soft_home_sensor {
1119 SoftHomeSensor::PositiveLimit => commanded_positive = true,
1120 SoftHomeSensor::NegativeLimit => commanded_negative = true,
1121 SoftHomeSensor::HomeSensor => {
1122 commanded_positive = self.moving_positive;
1123 commanded_negative = self.moving_negative;
1124 }
1125 },
1126 _ => {}
1127 }
1128
1129 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
1131 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
1132
1133 let effective_hw_pos = hw_pos && !suppress_pos;
1134 let effective_hw_neg = hw_neg && !suppress_neg;
1135
1136 let effective_sw_max = sw_max && !is_soft_homing;
1138 let effective_sw_min = sw_min && !is_soft_homing;
1139
1140 let positive_blocked = (effective_sw_max || effective_hw_pos) && commanded_positive;
1141 let negative_blocked = (effective_sw_min || effective_hw_neg) && commanded_negative;
1142
1143 if positive_blocked || negative_blocked {
1144 let mut cw = RawControlWord(view.control_word());
1145 cw.set_bit(8, true); view.set_control_word(cw.raw());
1147
1148 let msg = if effective_hw_pos && commanded_positive {
1149 "Positive limit switch active".to_string()
1150 } else if effective_hw_neg && commanded_negative {
1151 "Negative limit switch active".to_string()
1152 } else if effective_sw_max && commanded_positive {
1153 format!(
1154 "Software position limit: position {:.3} >= max {:.3}",
1155 self.position,
1156 eff_max.unwrap_or(self.position)
1157 )
1158 } else {
1159 format!(
1160 "Software position limit: position {:.3} <= min {:.3}",
1161 self.position,
1162 eff_min.unwrap_or(self.position)
1163 )
1164 };
1165
1166 if is_soft_homing {
1167 self.set_op_error(&msg);
1170 } else {
1171 log::info!("Axis move halted by limit: {}", msg);
1184 self.command_halt(view);
1185 self.halt_stable_count = 0;
1186 self.last_raw_position = view.position_actual();
1187 self.op_started = Some(Instant::now());
1188 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
1189 }
1190 }
1191 }
1192
1193 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
1198 match self.op.clone() {
1199 AxisOp::Idle => {}
1200 AxisOp::Enabling(step) => self.tick_enabling(view, step),
1201 AxisOp::Disabling(step) => self.tick_disabling(view, step),
1202 AxisOp::Moving(kind, step, pos, neg) => self.tick_moving(view, kind, step, pos, neg),
1203 AxisOp::Homing(step) => self.tick_homing(view, client, step),
1204 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
1205 AxisOp::Halting(step) => self.tick_halting(view, client, step),
1206 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
1207 }
1208 }
1209
1210 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
1215 match step {
1216 1 => {
1217 let sw = RawStatusWord(view.status_word());
1218 if sw.state() == Cia402State::ReadyToSwitchOn {
1219 let mut cw = RawControlWord(view.control_word());
1220 cw.cmd_enable_operation();
1221 view.set_control_word(cw.raw());
1222 self.op = AxisOp::Enabling(2);
1223 } else if self.op_timed_out() {
1224 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
1225 }
1226 }
1227 2 => {
1228 let sw = RawStatusWord(view.status_word());
1229 if sw.state() == Cia402State::OperationEnabled {
1230 self.complete_op();
1237 } else if self.op_timed_out() {
1238 self.set_op_error("Enable timeout: waiting for OperationEnabled");
1239 }
1240 }
1241 _ => self.complete_op(),
1242 }
1243 }
1244
1245 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
1249 match step {
1250 1 => {
1251 let sw = RawStatusWord(view.status_word());
1252 if sw.state() != Cia402State::OperationEnabled {
1253 self.complete_op();
1254 } else if self.op_timed_out() {
1255 self.set_op_error("Disable timeout: drive still in OperationEnabled");
1256 }
1257 }
1258 _ => self.complete_op(),
1259 }
1260 }
1261
1262 fn tick_moving(
1268 &mut self,
1269 view: &mut impl AxisView,
1270 kind: MoveKind,
1271 step: u8,
1272 pos: bool,
1273 neg: bool,
1274 ) {
1275 match step {
1276 1 => {
1277 let sw = RawStatusWord(view.status_word());
1279 if sw.raw() & (1 << 12) != 0 {
1280 let mut cw = RawControlWord(view.control_word());
1282 cw.set_bit(4, false);
1283 view.set_control_word(cw.raw());
1284 self.op = AxisOp::Moving(kind, 2, pos, neg);
1285 } else if self.move_start_timed_out() {
1286 self.set_op_error("Move timeout: set-point not acknowledged");
1287 }
1288 }
1289 2 => {
1290 let sw = RawStatusWord(view.status_word());
1292 if sw.raw() & (1 << 12) == 0 {
1293 self.op = AxisOp::Moving(kind, 3, pos, neg);
1295 }
1296 }
1297 3 => {
1298 let sw = RawStatusWord(view.status_word());
1300 if sw.target_reached() {
1301 self.complete_op();
1302 }
1303 }
1304 _ => self.complete_op(),
1305 }
1306 }
1307
1308 fn tick_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1337 match step {
1338 0 => {
1339 self.homing_sdo_tid = self.sdo.write(client, 0x6098, 0, json!(self.homing_method));
1341 self.op = AxisOp::Homing(1);
1342 }
1343 1 => {
1344 match self
1346 .sdo
1347 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1348 {
1349 SdoResult::Ok(_) => {
1350 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
1352 self.op = AxisOp::Homing(8);
1353 } else {
1354 self.op = AxisOp::Homing(2);
1355 }
1356 }
1357 SdoResult::Pending => {
1358 if self.homing_timed_out() {
1359 self.set_op_error("Homing timeout: SDO write for homing method");
1360 }
1361 }
1362 SdoResult::Err(e) => {
1363 self.set_op_error(&format!("Homing SDO error: {}", e));
1364 }
1365 SdoResult::Timeout => {
1366 self.set_op_error("Homing timeout: SDO write timed out");
1367 }
1368 }
1369 }
1370 2 => {
1371 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1373 self.homing_sdo_tid = self.sdo.write(client, 0x6099, 1, json!(speed_counts));
1374 self.op = AxisOp::Homing(3);
1375 }
1376 3 => {
1377 match self
1378 .sdo
1379 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1380 {
1381 SdoResult::Ok(_) => {
1382 self.op = AxisOp::Homing(4);
1383 }
1384 SdoResult::Pending => {
1385 if self.homing_timed_out() {
1386 self.set_op_error(
1387 "Homing timeout: SDO write for homing speed (switch)",
1388 );
1389 }
1390 }
1391 SdoResult::Err(e) => {
1392 self.set_op_error(&format!("Homing SDO error: {}", e));
1393 }
1394 SdoResult::Timeout => {
1395 self.set_op_error("Homing timeout: SDO write timed out");
1396 }
1397 }
1398 }
1399 4 => {
1400 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1402 self.homing_sdo_tid = self.sdo.write(client, 0x6099, 2, json!(speed_counts));
1403 self.op = AxisOp::Homing(5);
1404 }
1405 5 => {
1406 match self
1407 .sdo
1408 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1409 {
1410 SdoResult::Ok(_) => {
1411 self.op = AxisOp::Homing(6);
1412 }
1413 SdoResult::Pending => {
1414 if self.homing_timed_out() {
1415 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1416 }
1417 }
1418 SdoResult::Err(e) => {
1419 self.set_op_error(&format!("Homing SDO error: {}", e));
1420 }
1421 SdoResult::Timeout => {
1422 self.set_op_error("Homing timeout: SDO write timed out");
1423 }
1424 }
1425 }
1426 6 => {
1427 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1429 self.homing_sdo_tid = self.sdo.write(client, 0x609A, 0, json!(accel_counts));
1430 self.op = AxisOp::Homing(7);
1431 }
1432 7 => {
1433 match self
1434 .sdo
1435 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1436 {
1437 SdoResult::Ok(_) => {
1438 self.op = AxisOp::Homing(8);
1439 }
1440 SdoResult::Pending => {
1441 if self.homing_timed_out() {
1442 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1443 }
1444 }
1445 SdoResult::Err(e) => {
1446 self.set_op_error(&format!("Homing SDO error: {}", e));
1447 }
1448 SdoResult::Timeout => {
1449 self.set_op_error("Homing timeout: SDO write timed out");
1450 }
1451 }
1452 }
1453 8 => {
1454 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1464 let mut cw = RawControlWord(view.control_word());
1466 cw.set_bit(4, false);
1467 view.set_control_word(cw.raw());
1468 self.fb_mode_of_operation.reset();
1469 self.fb_mode_of_operation
1470 .start(ModesOfOperation::Homing as i8);
1471 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1472 self.op = AxisOp::Homing(9);
1473 }
1474 9 => {
1475 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1477 if !self.fb_mode_of_operation.is_busy() {
1478 if self.fb_mode_of_operation.is_error() {
1479 self.set_op_error(&format!(
1480 "Homing: failed to enter Homing mode: {} {}",
1481 self.fb_mode_of_operation.error_code(),
1482 self.fb_mode_of_operation.error_message()
1483 ));
1484 } else {
1485 self.op = AxisOp::Homing(10);
1486 }
1487 }
1488 }
1489 10 => {
1490 log::info!(
1495 "Homing[trigger]: method={} position_actual(0x6064) BEFORE = {}",
1496 self.homing_method,
1497 view.position_actual(),
1498 );
1499 let mut cw = RawControlWord(view.control_word());
1500 cw.set_bit(4, true);
1501 view.set_control_word(cw.raw());
1502 self.op = AxisOp::Homing(11);
1503 }
1504 11 => {
1505 let sw = view.status_word();
1517 if sw & (1 << 13) != 0 {
1518 self.set_op_error("Homing error: drive reported homing failure");
1519 } else {
1520 self.op = AxisOp::Homing(12);
1521 }
1522 }
1523 12 => {
1524 let sw = view.status_word();
1532 let error = sw & (1 << 13) != 0;
1533 let attained = sw & (1 << 12) != 0;
1534
1535 if error {
1536 self.set_op_error("Homing error: drive reported homing failure");
1537 } else if attained {
1538 self.op = AxisOp::Homing(13);
1539 } else if self.homing_timed_out() {
1540 self.set_op_error(&format!(
1541 "Homing timeout: procedure did not complete (sw=0x{:04X})",
1542 sw
1543 ));
1544 }
1545 }
1546 13 => {
1547 self.home_offset = 0;
1553 log::info!(
1554 "Homing[done]: position_actual(0x6064) AFTER = {} (drive owns the reference)",
1555 view.position_actual(),
1556 );
1557 let mut cw = RawControlWord(view.control_word());
1559 cw.set_bit(4, false);
1560 view.set_control_word(cw.raw());
1561 self.op = AxisOp::Homing(14);
1562 }
1563 14 => {
1564 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1570 self.fb_mode_of_operation.reset();
1571 self.fb_mode_of_operation
1572 .start(ModesOfOperation::ProfilePosition as i8);
1573 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1574 self.op = AxisOp::Homing(15);
1575 }
1576 15 => {
1577 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1579 if !self.fb_mode_of_operation.is_busy() {
1580 if self.fb_mode_of_operation.is_error() {
1581 self.set_op_error(&format!(
1582 "Homing: failed to restore PP mode: {} {}",
1583 self.fb_mode_of_operation.error_code(),
1584 self.fb_mode_of_operation.error_message()
1585 ));
1586 } else {
1587 log::info!("Homing complete — home offset: {}", self.home_offset);
1588 self.complete_op();
1589 }
1590 }
1591 }
1592 16 => {
1593 let desired_counts =
1598 self.config.to_counts(self.config.home_position).round() as i32;
1599 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
1600 log::info!(
1601 "Homing[offset]: writing 0x607C = {} counts ({} user units)",
1602 desired_counts, self.config.home_position
1603 );
1604 self.op = AxisOp::Homing(17);
1605 }
1606 17 => {
1607 match self
1608 .sdo
1609 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1610 {
1611 SdoResult::Ok(_) => {
1612 self.op = AxisOp::Homing(0);
1613 }
1614 SdoResult::Pending => {
1615 if self.homing_timed_out() {
1616 self.set_op_error("Homing timeout: SDO write for home offset");
1617 }
1618 }
1619 SdoResult::Err(e) => {
1620 self.set_op_error(&format!("Homing SDO error (home offset): {}", e));
1621 }
1622 SdoResult::Timeout => {
1623 self.set_op_error("Homing timeout: home offset SDO write timed out");
1624 }
1625 }
1626 }
1627 _ => self.complete_op(),
1628 }
1629 }
1630
1631 fn configure_soft_homing(&mut self, method: HomingMethod) {
1634 match method {
1635 HomingMethod::LimitSwitchPosPnp => {
1636 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1637 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1638 self.soft_home_direction = 1.0;
1639 }
1640 HomingMethod::LimitSwitchNegPnp => {
1641 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1642 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1643 self.soft_home_direction = -1.0;
1644 }
1645 HomingMethod::LimitSwitchPosNpn => {
1646 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1647 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1648 self.soft_home_direction = 1.0;
1649 }
1650 HomingMethod::LimitSwitchNegNpn => {
1651 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1652 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1653 self.soft_home_direction = -1.0;
1654 }
1655 HomingMethod::HomeSensorPosPnp => {
1656 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1657 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1658 self.soft_home_direction = 1.0;
1659 }
1660 HomingMethod::HomeSensorNegPnp => {
1661 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1662 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1663 self.soft_home_direction = -1.0;
1664 }
1665 HomingMethod::HomeSensorPosNpn => {
1666 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1667 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1668 self.soft_home_direction = 1.0;
1669 }
1670 HomingMethod::HomeSensorNegNpn => {
1671 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1672 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1673 self.soft_home_direction = -1.0;
1674 }
1675 _ => {} }
1677 }
1678
1679 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1680 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1681 self.op_started = Some(Instant::now());
1682 }
1683
1684 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1685 let raw = match self.soft_home_sensor {
1686 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1687 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1688 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1689 };
1690 match self.soft_home_sensor_type {
1691 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1694 }
1695
1696 fn calculate_max_relative_target(&self, direction: f64) -> i32 {
1699 let dir = if !self.config.invert_direction {
1700 direction
1701 } else {
1702 -direction
1703 };
1704
1705 let target = if dir > 0.0 { i32::MAX } else { i32::MIN };
1706
1707 return target;
1708 }
1709
1710 pub fn command_halt(&self, view: &mut impl AxisView) {
1714 let mut cw = RawControlWord(view.control_word());
1715 cw.set_bit(8, true); cw.set_bit(4, false); cw.set_bit(5, false);
1718 cw.set_bit(6, false); view.set_control_word(cw.raw());
1720 }
1721
1722 fn command_homing_speed(&self, view: &mut impl AxisView) {
1724 let cpu = self.config.counts_per_user();
1725 let vel = (self.config.homing_speed * cpu).round() as u32;
1726 let accel = (self.config.homing_accel * cpu).round() as u32;
1727 let decel = (self.config.homing_decel * cpu).round() as u32;
1728 view.set_profile_velocity(vel);
1729 view.set_profile_acceleration(accel);
1730 view.set_profile_deceleration(decel);
1731 }
1732
1733 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1749 match HomeState::from_repr(step) {
1750 Some(HomeState::EnsurePpMode) => {
1751 log::info!("SoftHome: Ensuring PP mode..");
1756 self.fb_mode_of_operation
1757 .start(ModesOfOperation::ProfilePosition as i8);
1758 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1759 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1760 }
1761 Some(HomeState::WaitPpMode) => {
1762 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1763 if !self.fb_mode_of_operation.is_busy() {
1764 if self.fb_mode_of_operation.is_error() {
1765 self.set_op_error(&format!(
1766 "Software homing SDO error writing homing mode of operation: {} {}",
1767 self.fb_mode_of_operation.error_code(),
1768 self.fb_mode_of_operation.error_message()
1769 ));
1770 } else {
1771 log::info!("SoftHome: Drive is in PP mode!");
1772
1773 if !self.check_soft_home_trigger(view) {
1777 log::info!("SoftHome: Not on home switch; seek out.");
1778 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1779 } else {
1780 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1781 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1782 }
1783 }
1784 }
1785 }
1786
1787 Some(HomeState::Search) => {
1789 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1790
1791 let target = self.calculate_max_relative_target(self.soft_home_direction);
1800 view.set_target_position(target);
1801
1802 self.command_homing_speed(view);
1811
1812 let mut cw = RawControlWord(view.control_word());
1813 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());
1818
1819 log::info!(
1820 "SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1821 target,
1822 self.config.homing_speed,
1823 self.soft_home_direction,
1824 view.position_actual()
1825 );
1826 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1827 }
1828 Some(HomeState::WaitSearching) => {
1829 if self.check_soft_home_trigger(view) {
1830 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1831 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1832 return;
1833 }
1834 let sw = RawStatusWord(view.status_word());
1835 if sw.raw() & (1 << 12) != 0 {
1836 let mut cw = RawControlWord(view.control_word());
1837 cw.set_bit(4, false);
1838 view.set_control_word(cw.raw());
1839 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1840 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1841 } else if self.homing_timed_out() {
1842 self.set_op_error("Software homing timeout: set-point not acknowledged");
1843 }
1844 }
1845 Some(HomeState::WaitFoundSensor) => {
1855 if self.check_soft_home_trigger(view) {
1856 log::info!(
1857 "SoftHome[3]: sensor triggered at pos={}. HALTING",
1858 view.position_actual()
1859 );
1860 log::info!("ControlWord is : {} ", view.control_word());
1861
1862 let mut cw = RawControlWord(view.control_word());
1863 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1866
1867 self.halt_stable_count = 0;
1868 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1869 } else if self.homing_timed_out() {
1870 self.set_op_error("Software homing timeout: sensor not detected");
1871 }
1872 }
1873
1874 Some(HomeState::WaitStoppedFoundSensor) => {
1875 const STABLE_WINDOW: i32 = 1;
1876 const STABLE_TICKS_REQUIRED: u8 = 10;
1877
1878 let pos = view.position_actual();
1883 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1884 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1885 } else {
1886 self.halt_stable_count = 0;
1887 }
1888
1889 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1890 log::info!(
1891 "SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true."
1892 );
1893 self.flush.start(view, client, &mut self.sdo);
1894 self.op = AxisOp::SoftHoming(HomeState::FlushingFound as u8);
1895 } else if self.homing_timed_out() {
1896 self.set_op_error(
1897 "Software homing timeout: motor did not stop after sensor trigger",
1898 );
1899 }
1900 }
1901 Some(HomeState::FlushingFound) => {
1902 self.flush.tick(view, client, &mut self.sdo);
1903 if self.flush.is_busy() {
1904 if self.homing_timed_out() {
1905 self.set_op_error("Software homing timeout: queue flush did not complete");
1906 }
1907 return;
1908 }
1909 if self.flush.is_error() {
1910 self.set_op_error(&format!(
1911 "Software homing: queue flush failed: {} {}",
1912 self.flush.error_code(),
1913 self.flush.error_message()
1914 ));
1915 return;
1916 }
1917 let mut cw = RawControlWord(view.control_word());
1920 cw.set_bit(8, false);
1921 cw.set_bit(4, false);
1922 cw.set_bit(5, false);
1923 view.set_control_word(cw.raw());
1924 view.set_target_position(view.position_actual());
1925 log::info!("SoftHome[6]: queue flushed; proceeding to debounce.");
1926 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1927 self.ton.call(false, Duration::from_secs(3));
1928 }
1929 Some(HomeState::DebounceFoundSensor) => {
1931 self.ton.call(true, Duration::from_secs(3));
1932
1933 let sw = RawStatusWord(view.status_word());
1934 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1935 self.ton.call(false, Duration::from_secs(3));
1936 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1937 view.position_actual(), view.control_word(), view.status_word());
1938 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1939 }
1940 }
1941
1942 Some(HomeState::BackOff) => {
1944 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1945 view.set_target_position(target);
1946
1947 self.command_homing_speed(view);
1948
1949 let mut cw = RawControlWord(view.control_word());
1950 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1954 log::info!(
1955 "SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1956 target,
1957 self.config.homing_speed,
1958 view.position_actual(),
1959 cw.raw()
1960 );
1961 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1962 }
1963 Some(HomeState::WaitBackingOff) => {
1964 let sw = RawStatusWord(view.status_word());
1965 if sw.raw() & (1 << 12) != 0 {
1966 let mut cw = RawControlWord(view.control_word());
1967 cw.set_bit(4, false);
1968 view.set_control_word(cw.raw());
1969 log::info!(
1970 "SoftHome[WaitBackingOff]: back-off ack received, pos={}",
1971 view.position_actual()
1972 );
1973 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1974 } else if self.homing_timed_out() {
1975 self.set_op_error("Software homing timeout: back-off not acknowledged");
1976 }
1977 }
1978 Some(HomeState::WaitLostSensor) => {
1979 if !self.check_soft_home_trigger(view) {
1980 log::info!(
1981 "SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...",
1982 view.position_actual()
1983 );
1984
1985 self.command_halt(view);
1986 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1987 } else if self.homing_timed_out() {
1988 self.set_op_error(
1989 "Software homing timeout: sensor did not clear during back-off",
1990 );
1991 }
1992 }
1993 Some(HomeState::WaitStoppedLostSensor) => {
1994 const STABLE_WINDOW: i32 = 1;
1995 const STABLE_TICKS_REQUIRED: u8 = 10;
1996
1997 let pos = view.position_actual();
2002 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
2003 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2004 } else {
2005 self.halt_stable_count = 0;
2006 }
2007
2008 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
2009 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
2010 self.flush.start(view, client, &mut self.sdo);
2011 self.op = AxisOp::SoftHoming(HomeState::FlushingLost as u8);
2012 } else if self.homing_timed_out() {
2013 self.set_op_error("Software homing timeout: motor did not stop after back-off");
2014 }
2015 }
2016 Some(HomeState::FlushingLost) => {
2017 self.flush.tick(view, client, &mut self.sdo);
2018 if self.flush.is_busy() {
2019 if self.homing_timed_out() {
2020 self.set_op_error("Software homing timeout: queue flush did not complete");
2021 }
2022 return;
2023 }
2024 if self.flush.is_error() {
2025 self.set_op_error(&format!(
2026 "Software homing: queue flush failed: {} {}",
2027 self.flush.error_code(),
2028 self.flush.error_message()
2029 ));
2030 return;
2031 }
2032 let mut cw = RawControlWord(view.control_word());
2035 cw.set_bit(8, false);
2036 cw.set_bit(4, false);
2037 cw.set_bit(5, false);
2038 view.set_control_word(cw.raw());
2039
2040 let desired_counts =
2041 self.config.to_counts(self.config.home_position).round() as i32;
2042 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
2043 log::info!(
2044 "SoftHome[FlushingLost]: queue flushed; writing home offset {} [{} counts].",
2045 self.config.home_position, desired_counts
2046 );
2047 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
2048 }
2049
2050 Some(HomeState::WriteHomeOffset) => {
2051 let desired_counts =
2057 self.config.to_counts(self.config.home_position).round() as i32;
2058 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
2059 log::info!(
2060 "Home[CurrentPosition]: writing 0x607C = {} counts ({} user units)",
2061 desired_counts, self.config.home_position
2062 );
2063 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
2064 }
2065 Some(HomeState::WaitHomeOffsetDone) => {
2066 match self
2068 .sdo
2069 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
2070 {
2071 SdoResult::Ok(_) => {
2072 self.homing_sdo_tid = self.sdo.read(client, 0x607C, 0);
2076 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetReadback as u8);
2077 }
2078 SdoResult::Pending => {
2079 if self.homing_timed_out() {
2080 self.set_op_error("Software homing timeout: home offset SDO write");
2081 }
2082 }
2083 SdoResult::Err(e) => {
2084 self.set_op_error(&format!("Software homing SDO error: {}", e));
2085 }
2086 SdoResult::Timeout => {
2087 self.set_op_error("Software homing: home offset SDO timed out");
2088 }
2089 }
2090 }
2091 Some(HomeState::WaitHomeOffsetReadback) => {
2092 let target = self.config.to_counts(self.config.home_position).round() as i32;
2095 match self
2096 .sdo
2097 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
2098 {
2099 SdoResult::Ok(val) => {
2100 log::info!(
2101 "SoftHome[diag]: 0x607C (home offset) read-back = {} (wrote {} counts)",
2102 val,
2103 target,
2104 );
2105 self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8);
2106 }
2107 SdoResult::Pending => {
2108 if self.homing_timed_out() {
2109 log::warn!("SoftHome[diag]: 0x607C read-back timed out; continuing");
2110 self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8);
2111 }
2112 }
2113 SdoResult::Err(e) => {
2114 log::warn!("SoftHome[diag]: 0x607C read-back error: {}; continuing", e);
2115 self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8);
2116 }
2117 SdoResult::Timeout => {
2118 log::warn!("SoftHome[diag]: 0x607C read-back timed out; continuing");
2119 self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8);
2120 }
2121 }
2122 }
2123 Some(HomeState::WriteHomingModeOp) => {
2124 self.fb_mode_of_operation.reset();
2128 self.fb_mode_of_operation
2129 .start(ModesOfOperation::Homing as i8);
2130 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2131 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
2132 }
2133 Some(HomeState::WaitWriteHomingModeOp) => {
2134 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2136
2137 if !self.fb_mode_of_operation.is_busy() {
2138 if self.fb_mode_of_operation.is_error() {
2139 self.set_op_error(&format!(
2140 "Software homing SDO error writing homing mode of operation: {} {}",
2141 self.fb_mode_of_operation.error_code(),
2142 self.fb_mode_of_operation.error_message()
2143 ));
2144 } else {
2145 log::info!("SoftHome: Drive is now in Homing Mode.");
2146 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
2147 }
2148 }
2149 }
2150 Some(HomeState::WriteHomingMethod) => {
2151 log::info!(
2157 "SoftHome[diag]: writing 0x6098 (homing method) = {}",
2158 self.homing_method
2159 );
2160 self.homing_sdo_tid =
2161 self.sdo.write(client, 0x6098, 0, json!(self.homing_method));
2162 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
2163 }
2164 Some(HomeState::WaitWriteHomingMethodDone) => {
2165 match self
2167 .sdo
2168 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
2169 {
2170 SdoResult::Ok(_) => {
2171 log::info!("SoftHome: Successfully wrote homing method.");
2172 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
2173 }
2174 SdoResult::Pending => {
2175 if self.homing_timed_out() {
2176 self.restore_pp_after_error(
2177 "Software homing timeout: homing method SDO write",
2178 );
2179 }
2180 }
2181 SdoResult::Err(e) => {
2182 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
2183 }
2184 SdoResult::Timeout => {
2185 self.restore_pp_after_error("Software homing: homing method SDO timed out");
2186 }
2187 }
2188 }
2189 Some(HomeState::ClearHomingTrigger) => {
2190 let mut cw = RawControlWord(view.control_word());
2193 cw.set_bit(4, false);
2194 view.set_control_word(cw.raw());
2195 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
2196 }
2197 Some(HomeState::TriggerHoming) => {
2198 let mut cw = RawControlWord(view.control_word());
2200 cw.set_bit(4, true);
2201 view.set_control_word(cw.raw());
2202 log::info!("SoftHome[TriggerHoming]: start homing");
2203 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
2204 }
2205 Some(HomeState::WaitHomingStarted) => {
2206 let sw = view.status_word();
2220 if sw & (1 << 13) != 0 {
2221 self.restore_pp_after_error("Software homing: drive reported homing error");
2222 } else {
2223 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
2224 }
2225 }
2226 Some(HomeState::WaitHomingDone) => {
2227 let sw = view.status_word();
2236 let error = sw & (1 << 13) != 0;
2237 let attained = sw & (1 << 12) != 0;
2238 let reached = sw & (1 << 10) != 0;
2239
2240 if error {
2241 self.restore_pp_after_error("Software homing: drive reported homing error");
2242 } else if attained {
2243 log::info!(
2250 "SoftHome[WaitHomingDone]: homing complete sw=0x{:04X} position_actual(0x6064)={} target(0x607C)={} counts",
2251 sw,
2252 view.position_actual(),
2253 self.config.to_counts(self.config.home_position).round() as i32,
2254 );
2255 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
2256 } else if self.homing_timed_out() {
2257 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
2258 }
2259 }
2260 Some(HomeState::ResetHomingTrigger) => {
2261 let mut cw = RawControlWord(view.control_word());
2266 cw.set_bit(4, false);
2267 view.set_control_word(cw.raw());
2268 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
2269 }
2270 Some(HomeState::WaitHomingTriggerCleared) => {
2271 let pos = view.position_actual();
2280 view.set_target_position(pos);
2281 log::info!(
2282 "SoftHome[diag]: post-home, pre-PP-switch position_actual(0x6064) = {}; \
2283 set target_position(0x607A) = {} so PP re-entry has nothing to chase",
2284 pos, pos
2285 );
2286 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::AlignTargetBeforePP as u8);
2288 }
2289 Some(HomeState::AlignTargetBeforePP) => {
2290 view.set_target_position(view.position_actual());
2294 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
2295 }
2296
2297 Some(HomeState::WriteMotionModeOfOperation) => {
2298 self.fb_mode_of_operation.reset();
2301 self.fb_mode_of_operation
2302 .start(ModesOfOperation::ProfilePosition as i8);
2303 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2304 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
2305 }
2306 Some(HomeState::WaitWriteMotionModeOfOperation) => {
2307 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2309
2310 if !self.fb_mode_of_operation.is_busy() {
2311 if self.fb_mode_of_operation.is_error() {
2312 self.set_op_error(&format!(
2313 "Software homing SDO error writing homing mode of operation: {} {}",
2314 self.fb_mode_of_operation.error_code(),
2315 self.fb_mode_of_operation.error_message()
2316 ));
2317 } else {
2318 if self.is_error {
2319 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
2320 self.finish_op_error();
2321 } else {
2322 log::info!(
2323 "SoftHome[diag]: back in PP mode, position_actual(0x6064) = {}; \
2324 homing complete, holding (no new set-point latched)",
2325 view.position_actual()
2326 );
2327 self.complete_op();
2347 }
2348 }
2349 }
2350 }
2351
2352 Some(HomeState::SendCurrentPositionTarget) => {
2353 let current_pos = view.position_actual();
2355 log::info!(
2356 "SoftHome[diag]: holding at position_actual(0x6064) = {}",
2357 current_pos
2358 );
2359 view.set_target_position(current_pos);
2360 view.set_profile_velocity(0);
2361 let mut cw = RawControlWord(view.control_word());
2362 cw.set_bit(4, true);
2363 cw.set_bit(5, true);
2364 cw.set_bit(6, false); view.set_control_word(cw.raw());
2366 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
2367 }
2368 Some(HomeState::WaitCurrentPositionTargetSent) => {
2369 let sw = RawStatusWord(view.status_word());
2371 if sw.raw() & (1 << 12) != 0 {
2372 let mut cw = RawControlWord(view.control_word());
2373 cw.set_bit(4, false);
2374 view.set_control_word(cw.raw());
2375 log::info!(
2376 "Software homing complete — commanded {} user units; final position_actual(0x6064) = {} counts",
2377 self.config.home_position,
2378 view.position_actual()
2379 );
2380 self.complete_op();
2381 } else if self.homing_timed_out() {
2382 self.set_op_error("Software homing timeout: hold position not acknowledged");
2383 }
2384 }
2385 _ => self.complete_op(),
2386 }
2387 }
2388
2389 fn tick_halting(
2402 &mut self,
2403 view: &mut impl AxisView,
2404 client: &mut CommandClient,
2405 step: u8,
2406 ) {
2407 match HaltState::from_repr(step) {
2408 Some(HaltState::WaitStopped) => {
2409 let pos = view.position_actual();
2410 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
2411
2412 let vel = view.velocity_actual().abs();
2413 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
2414
2415 if pos_stable || vel_stopped {
2416 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2417 } else {
2418 self.halt_stable_count = 0;
2419 }
2420
2421 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
2422 self.flush.start(view, client, &mut self.sdo);
2425 self.op_started = Some(Instant::now());
2426 self.op = AxisOp::Halting(HaltState::Flushing as u8);
2427 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2428 self.set_op_error("Halt timeout: motor did not stop");
2429 }
2430 }
2431 Some(HaltState::Flushing) => {
2432 self.flush.tick(view, client, &mut self.sdo);
2433 if self.flush.is_busy() {
2434 if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2435 self.set_op_error("Halt timeout: set-point queue flush did not complete");
2436 }
2437 return;
2438 }
2439 if self.flush.is_error() {
2440 self.set_op_error(&format!(
2441 "Halt: set-point queue flush failed: {} {}",
2442 self.flush.error_code(),
2443 self.flush.error_message()
2444 ));
2445 return;
2446 }
2447 let mut cw = RawControlWord(view.control_word());
2450 cw.set_bit(8, false); cw.set_bit(4, false);
2452 cw.set_bit(5, false);
2453 view.set_control_word(cw.raw());
2454 view.set_target_position(view.position_actual());
2455 self.complete_op();
2456 }
2457 None => {
2458 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
2459 self.complete_op();
2460 }
2461 }
2462 }
2463
2464 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
2469 match step {
2470 1 => {
2471 let mut cw = RawControlWord(view.control_word());
2473 cw.cmd_fault_reset();
2474 view.set_control_word(cw.raw());
2475 self.op = AxisOp::FaultRecovery(2);
2476 }
2477 2 => {
2478 let sw = RawStatusWord(view.status_word());
2480 let state = sw.state();
2481 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
2482 log::info!("Fault cleared (drive state: {})", state);
2483 self.complete_op();
2484 } else if self.op_timed_out() {
2485 self.set_op_error("Fault reset timeout: drive still faulted");
2486 }
2487 }
2488 _ => self.complete_op(),
2489 }
2490 }
2491}
2492
2493#[cfg(test)]
2498mod tests {
2499 use super::*;
2500
2501 struct MockView {
2503 control_word: u16,
2504 status_word: u16,
2505 target_position: i32,
2506 profile_velocity: u32,
2507 profile_acceleration: u32,
2508 profile_deceleration: u32,
2509 modes_of_operation: i8,
2510 modes_of_operation_display: i8,
2511 position_actual: i32,
2512 velocity_actual: i32,
2513 error_code: u16,
2514 positive_limit: bool,
2515 negative_limit: bool,
2516 home_sensor: bool,
2517 }
2518
2519 impl MockView {
2520 fn new() -> Self {
2521 Self {
2522 control_word: 0,
2523 status_word: 0x0040, target_position: 0,
2525 profile_velocity: 0,
2526 profile_acceleration: 0,
2527 profile_deceleration: 0,
2528 modes_of_operation: 0,
2529 modes_of_operation_display: 1, position_actual: 0,
2531 velocity_actual: 0,
2532 error_code: 0,
2533 positive_limit: false,
2534 negative_limit: false,
2535 home_sensor: false,
2536 }
2537 }
2538
2539 fn set_state(&mut self, state: u16) {
2540 self.status_word = state;
2541 }
2542 }
2543
2544 impl AxisView for MockView {
2545 fn control_word(&self) -> u16 {
2546 self.control_word
2547 }
2548 fn set_control_word(&mut self, word: u16) {
2549 self.control_word = word;
2550 }
2551 fn set_target_position(&mut self, pos: i32) {
2552 self.target_position = pos;
2553 }
2554 fn set_profile_velocity(&mut self, vel: u32) {
2555 self.profile_velocity = vel;
2556 }
2557 fn set_profile_acceleration(&mut self, accel: u32) {
2558 self.profile_acceleration = accel;
2559 }
2560 fn set_profile_deceleration(&mut self, decel: u32) {
2561 self.profile_deceleration = decel;
2562 }
2563 fn set_modes_of_operation(&mut self, mode: i8) {
2564 self.modes_of_operation = mode;
2565 }
2566 fn modes_of_operation_display(&self) -> i8 {
2567 self.modes_of_operation_display
2568 }
2569 fn status_word(&self) -> u16 {
2570 self.status_word
2571 }
2572 fn position_actual(&self) -> i32 {
2573 self.position_actual
2574 }
2575 fn velocity_actual(&self) -> i32 {
2576 self.velocity_actual
2577 }
2578 fn error_code(&self) -> u16 {
2579 self.error_code
2580 }
2581 fn positive_limit_active(&self) -> bool {
2582 self.positive_limit
2583 }
2584 fn negative_limit_active(&self) -> bool {
2585 self.negative_limit
2586 }
2587 fn home_sensor_active(&self) -> bool {
2588 self.home_sensor
2589 }
2590 }
2591
2592 fn test_config() -> AxisConfig {
2593 AxisConfig::new(12_800).with_user_scale(360.0)
2594 }
2595
2596 fn test_axis() -> (
2598 Axis,
2599 CommandClient,
2600 tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2601 tokio::sync::mpsc::UnboundedReceiver<String>,
2602 ) {
2603 use tokio::sync::mpsc;
2604 let (write_tx, write_rx) = mpsc::unbounded_channel();
2605 let (response_tx, response_rx) = mpsc::unbounded_channel();
2606 let client = CommandClient::new(write_tx, response_rx);
2607 let axis = Axis::new(test_config(), "TestDrive");
2608 (axis, client, response_tx, write_rx)
2609 }
2610
2611 #[test]
2612 fn axis_config_conversion() {
2613 let cfg = test_config();
2614 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2616 }
2617
2618 #[test]
2619 fn trace_records_on_change_not_every_tick() {
2620 let (mut axis, mut client, _r, _w) = test_axis();
2621 let mut view = MockView::new();
2622 view.set_state(0x0040); for _ in 0..10 {
2627 axis.tick(&mut view, &mut client);
2628 }
2629 let after_idle = axis.trace().lines().count();
2630 assert!(
2631 after_idle <= 1,
2632 "constant state must record ≤1 line, got {}",
2633 after_idle
2634 );
2635
2636 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2639 assert!(
2640 axis.trace().lines().count() > after_idle,
2641 "a SW change adds a line"
2642 );
2643 assert!(
2644 axis.trace().contains("Operation Enabled"),
2645 "trace decodes the SW state"
2646 );
2647 }
2648
2649 #[test]
2650 fn trace_decoders_render_handshake_bits() {
2651 assert_eq!(decode_cw(0x003F), "SwOn|EnVolt|QStop|EnOp|NewSP|ChgSet");
2653 assert!(
2654 decode_cw(0x013F).contains("HALT"),
2655 "0x13F still asserts HALT"
2656 );
2657 let s = decode_sw(0x1027);
2659 assert!(
2660 s.contains("Operation Enabled") && s.contains("SP_ACK"),
2661 "got {s}"
2662 );
2663 }
2664
2665 #[test]
2666 fn enable_sequence_sets_pp_mode_and_shutdown() {
2667 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2668 let mut view = MockView::new();
2669
2670 axis.enable(&mut view);
2671
2672 assert_eq!(
2674 view.modes_of_operation,
2675 ModesOfOperation::ProfilePosition.as_i8()
2676 );
2677 assert_eq!(view.control_word & 0x008F, 0x0006);
2679 assert_eq!(axis.op, AxisOp::Enabling(1));
2681
2682 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2685
2686 assert_eq!(view.control_word & 0x008F, 0x000F);
2688 assert_eq!(axis.op, AxisOp::Enabling(2));
2689
2690 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2693
2694 assert_eq!(axis.op, AxisOp::Idle);
2696 assert!(axis.motor_on);
2697 }
2698
2699 #[test]
2700 fn move_absolute_sets_target() {
2701 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2702 let mut view = MockView::new();
2703 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2708
2709 assert_eq!(view.target_position, 1600);
2711 assert_eq!(view.profile_velocity, 3200);
2713 assert_eq!(view.profile_acceleration, 6400);
2715 assert_eq!(view.profile_deceleration, 6400);
2716 assert!(view.control_word & (1 << 4) != 0);
2718 assert!(view.control_word & (1 << 6) == 0);
2720 assert!(matches!(
2722 axis.op,
2723 AxisOp::Moving(MoveKind::Absolute, 1, _, _)
2724 ));
2725 }
2726
2727 #[test]
2728 fn move_relative_sets_relative_bit() {
2729 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2730 let mut view = MockView::new();
2731 view.set_state(0x0027);
2732 axis.tick(&mut view, &mut client);
2733
2734 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2735
2736 assert!(view.control_word & (1 << 6) != 0);
2738 assert!(matches!(
2739 axis.op,
2740 AxisOp::Moving(MoveKind::Relative, 1, _, _)
2741 ));
2742 }
2743
2744 #[test]
2745 fn move_completes_on_target_reached() {
2746 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2747 let mut view = MockView::new();
2748 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2750
2751 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2752
2753 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2756 assert!(view.control_word & (1 << 4) == 0);
2758
2759 view.status_word = 0x0027; axis.tick(&mut view, &mut client);
2763
2764 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2767 assert_eq!(axis.op, AxisOp::Idle);
2769 assert!(!axis.in_motion);
2770 }
2771
2772 #[test]
2773 fn fault_detected_sets_error() {
2774 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2775 let mut view = MockView::new();
2776 view.set_state(0x0008); view.error_code = 0x1234;
2778
2779 axis.tick(&mut view, &mut client);
2780
2781 assert!(axis.is_error);
2782 assert_eq!(axis.error_code, 0x1234);
2783 assert!(axis.error_message.contains("fault"));
2784 }
2785
2786 #[test]
2787 fn fault_recovery_sequence() {
2788 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2789 let mut view = MockView::new();
2790 view.set_state(0x0008); axis.reset_faults(&mut view);
2793 assert!(view.control_word & 0x0080 == 0);
2795
2796 axis.tick(&mut view, &mut client);
2798 assert!(view.control_word & 0x0080 != 0);
2799
2800 view.set_state(0x0040);
2802 axis.tick(&mut view, &mut client);
2803 assert_eq!(axis.op, AxisOp::Idle);
2804 assert!(!axis.is_error);
2805 }
2806
2807 #[test]
2808 fn reset_faults_software_only_is_synchronous() {
2809 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2815 let mut view = MockView::new();
2816 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2818
2819 axis.set_software_min_limit(207.0);
2821 axis.move_absolute(&mut view, 200.0, 90.0, 180.0, 180.0);
2822 assert!(axis.is_error, "move past software limit must set is_error");
2823 assert!(!axis.is_busy);
2824
2825 let cw_before = view.control_word;
2828
2829 axis.reset_faults(&mut view);
2830
2831 assert!(!axis.is_error);
2833 assert_eq!(axis.error_code, 0);
2834 assert!(axis.error_message.is_empty());
2835 assert!(!axis.is_busy);
2836 assert_eq!(axis.op, AxisOp::Idle);
2837 assert_eq!(view.control_word, cw_before);
2839 }
2840
2841 #[test]
2842 fn disable_sequence() {
2843 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2844 let mut view = MockView::new();
2845 view.set_state(0x0027); axis.disable(&mut view);
2848 assert_eq!(view.control_word & 0x008F, 0x0006);
2850
2851 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2854 assert_eq!(axis.op, AxisOp::Idle);
2855 }
2856
2857 #[test]
2858 fn position_tracks_with_home_offset() {
2859 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2860 let mut view = MockView::new();
2861 view.set_state(0x0027);
2862
2863 axis.home_offset = 5000;
2867 view.position_actual = 6600;
2868 axis.tick(&mut view, &mut client);
2869
2870 assert!((axis.position - 45.0).abs() < 0.1);
2872 }
2873
2874 #[test]
2875 fn set_position_adjusts_home_offset() {
2876 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2877 let mut view = MockView::new();
2878 view.position_actual = 3200;
2879
2880 axis.set_position(&view, 90.0);
2881 axis.tick(&mut view, &mut client);
2882
2883 assert_eq!(axis.home_offset, 0);
2885 assert!((axis.position - 90.0).abs() < 0.01);
2886 }
2887
2888 #[test]
2889 fn halt_runs_multi_stage_close_out() {
2890 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2891 let mut view = MockView::new();
2892 view.set_state(0x0027);
2893
2894 axis.halt(&mut view);
2895
2896 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2898 assert!(
2899 view.control_word & (1 << 4) == 0,
2900 "new_setpoint must be cleared"
2901 );
2902
2903 assert!(
2905 matches!(axis.op, AxisOp::Halting(_)),
2906 "halt should enter Halting state, not Idle"
2907 );
2908 let AxisOp::Halting(step) = axis.op.clone() else {
2909 unreachable!()
2910 };
2911 assert_eq!(step, HaltState::WaitStopped as u8);
2912
2913 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2920 axis.tick(&mut view, &mut client);
2921 }
2922 assert!(matches!(axis.op, AxisOp::Halting(_)));
2925 let AxisOp::Halting(step) = axis.op.clone() else {
2926 unreachable!()
2927 };
2928 assert_eq!(
2929 step,
2930 HaltState::Flushing as u8,
2931 "should advance past WaitStopped once position/velocity is stable"
2932 );
2933 assert!(view.control_word & (1 << 8) != 0, "halt held through the away mode switch");
2936
2937 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2941 }
2942
2943 #[test]
2944 fn is_busy_tracks_operations() {
2945 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2946 let mut view = MockView::new();
2947
2948 axis.tick(&mut view, &mut client);
2950 assert!(!axis.is_busy);
2951
2952 axis.enable(&mut view);
2954 axis.tick(&mut view, &mut client);
2955 assert!(axis.is_busy);
2956
2957 view.set_state(0x0021);
2959 axis.tick(&mut view, &mut client);
2960 view.set_state(0x0027);
2961 axis.tick(&mut view, &mut client);
2962 assert!(!axis.is_busy);
2963
2964 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2966 axis.tick(&mut view, &mut client);
2967 assert!(axis.is_busy);
2968 assert!(axis.in_motion);
2969 }
2970
2971 #[test]
2972 fn fault_during_move_cancels_op() {
2973 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2974 let mut view = MockView::new();
2975 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2977
2978 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2980 axis.tick(&mut view, &mut client);
2981 assert!(axis.is_busy);
2982 assert!(!axis.is_error);
2983
2984 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2987
2988 assert!(!axis.is_busy);
2990 assert!(axis.is_error);
2991 assert_eq!(axis.op, AxisOp::Idle);
2992 }
2993
2994 #[test]
2995 fn move_absolute_rejected_by_max_limit() {
2996 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2997 let mut view = MockView::new();
2998 view.set_state(0x0027);
2999 axis.tick(&mut view, &mut client);
3000
3001 axis.set_software_max_limit(90.0);
3002 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
3003
3004 assert!(axis.is_error);
3006 assert_eq!(axis.op, AxisOp::Idle);
3007 assert!(axis.error_message.contains("max software limit"));
3008 }
3009
3010 #[test]
3011 fn move_absolute_rejected_by_min_limit() {
3012 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3013 let mut view = MockView::new();
3014 view.set_state(0x0027);
3015 axis.tick(&mut view, &mut client);
3016
3017 axis.set_software_min_limit(-10.0);
3018 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
3019
3020 assert!(axis.is_error);
3021 assert_eq!(axis.op, AxisOp::Idle);
3022 assert!(axis.error_message.contains("min software limit"));
3023 }
3024
3025 #[test]
3026 fn move_relative_rejected_by_max_limit() {
3027 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3028 let mut view = MockView::new();
3029 view.set_state(0x0027);
3030 axis.tick(&mut view, &mut client);
3031
3032 axis.set_software_max_limit(50.0);
3034 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
3035
3036 assert!(axis.is_error);
3037 assert_eq!(axis.op, AxisOp::Idle);
3038 assert!(axis.error_message.contains("max software limit"));
3039 }
3040
3041 #[test]
3042 fn move_within_limits_allowed() {
3043 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3044 let mut view = MockView::new();
3045 view.set_state(0x0027);
3046 axis.tick(&mut view, &mut client);
3047
3048 axis.set_software_max_limit(90.0);
3049 axis.set_software_min_limit(-90.0);
3050 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
3051
3052 assert!(!axis.is_error);
3054 assert!(matches!(
3055 axis.op,
3056 AxisOp::Moving(MoveKind::Absolute, 1, _, _)
3057 ));
3058 }
3059
3060 #[test]
3061 fn runtime_limit_halts_move_in_violated_direction() {
3062 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3063 let mut view = MockView::new();
3064 view.set_state(0x0027);
3065 axis.tick(&mut view, &mut client);
3066
3067 axis.set_software_max_limit(45.0);
3068 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
3070
3071 view.position_actual = 1650;
3074 view.velocity_actual = 100; view.status_word = 0x1027;
3078 axis.tick(&mut view, &mut client);
3079 view.status_word = 0x0027;
3080 axis.tick(&mut view, &mut client);
3081
3082 assert!(!axis.is_error);
3086 assert!(axis.at_max_limit);
3087 assert!(axis.is_busy);
3088 assert!(matches!(axis.op, AxisOp::Halting(_)));
3089 assert!(view.control_word & (1 << 8) != 0);
3091 }
3092
3093 #[test]
3094 fn runtime_limit_allows_move_in_opposite_direction() {
3095 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3096 let mut view = MockView::new();
3097 view.set_state(0x0027);
3098 view.position_actual = 1778; axis.set_software_max_limit(45.0);
3101 axis.tick(&mut view, &mut client);
3102 assert!(axis.at_max_limit);
3103
3104 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
3106 assert!(!axis.is_error);
3107 assert!(matches!(
3108 axis.op,
3109 AxisOp::Moving(MoveKind::Absolute, 1, _, _)
3110 ));
3111
3112 view.velocity_actual = -100;
3114 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
3116 assert!(!axis.is_error);
3118 }
3119
3120 #[test]
3121 fn positive_limit_switch_halts_positive_move() {
3122 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3123 let mut view = MockView::new();
3124 view.set_state(0x0027);
3125 axis.tick(&mut view, &mut client);
3126
3127 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
3129 view.velocity_actual = 100; view.status_word = 0x1027;
3132 axis.tick(&mut view, &mut client);
3133 view.status_word = 0x0027;
3134
3135 view.positive_limit = true;
3137 axis.tick(&mut view, &mut client);
3138
3139 assert!(!axis.is_error);
3144 assert!(axis.at_positive_limit_switch);
3145 assert!(axis.is_busy);
3146 assert!(matches!(axis.op, AxisOp::Halting(_)));
3147 assert!(view.control_word & (1 << 8) != 0);
3149 }
3150
3151 #[test]
3152 fn negative_limit_switch_halts_negative_move() {
3153 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3154 let mut view = MockView::new();
3155 view.set_state(0x0027);
3156 axis.tick(&mut view, &mut client);
3157
3158 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
3160 view.velocity_actual = -100; view.status_word = 0x1027;
3162 axis.tick(&mut view, &mut client);
3163 view.status_word = 0x0027;
3164
3165 view.negative_limit = true;
3167 axis.tick(&mut view, &mut client);
3168
3169 assert!(!axis.is_error);
3174 assert!(axis.at_negative_limit_switch);
3175 assert!(axis.is_busy);
3176 assert!(matches!(axis.op, AxisOp::Halting(_)));
3177 assert!(view.control_word & (1 << 8) != 0);
3178 }
3179
3180 #[test]
3181 fn limit_halt_flushes_queue_via_mode_toggle() {
3182 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3187 let mut view = MockView::new();
3188 view.set_state(0x0027); axis.tick(&mut view, &mut client);
3190
3191 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
3193 view.velocity_actual = 100;
3194 view.status_word = 0x1027;
3195 axis.tick(&mut view, &mut client);
3196 view.status_word = 0x0027;
3197 view.positive_limit = true;
3198 axis.tick(&mut view, &mut client);
3199
3200 assert!(!axis.is_error);
3202 assert!(axis.is_busy);
3203 assert!(matches!(axis.op, AxisOp::Halting(_)));
3204 assert!(view.control_word & (1 << 8) != 0);
3205
3206 view.velocity_actual = 0;
3213 view.positive_limit = false;
3214 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
3215 axis.tick(&mut view, &mut client);
3216 }
3217 let AxisOp::Halting(step) = axis.op.clone() else {
3218 unreachable!()
3219 };
3220 assert_eq!(step, HaltState::Flushing as u8, "halt flushes the queue via a mode toggle");
3221 assert!(view.control_word & (1 << 8) != 0, "halt held through the queue flush");
3222 assert!(axis.is_busy, "axis stays busy through the close-out");
3223 assert_ne!(
3224 view.modes_of_operation,
3225 ModesOfOperation::ProfilePosition.as_i8(),
3226 "mode switched away from PP to flush the queue",
3227 );
3228 }
3229
3230 #[test]
3231 fn limit_switch_allows_move_in_opposite_direction() {
3232 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3233 let mut view = MockView::new();
3234 view.set_state(0x0027);
3235 view.positive_limit = true;
3237 view.velocity_actual = -100;
3238 axis.tick(&mut view, &mut client);
3239 assert!(axis.at_positive_limit_switch);
3240
3241 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
3243 view.status_word = 0x1027;
3244 axis.tick(&mut view, &mut client);
3245
3246 assert!(!axis.is_error);
3248 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
3249 }
3250
3251 #[test]
3252 fn limit_switch_ignored_when_not_moving() {
3253 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3254 let mut view = MockView::new();
3255 view.set_state(0x0027);
3256 view.positive_limit = true;
3257
3258 axis.tick(&mut view, &mut client);
3259
3260 assert!(axis.at_positive_limit_switch);
3262 assert!(!axis.is_error);
3263 }
3264
3265 #[test]
3266 fn home_sensor_output_tracks_view() {
3267 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3268 let mut view = MockView::new();
3269 view.set_state(0x0027);
3270
3271 axis.tick(&mut view, &mut client);
3272 assert!(!axis.home_sensor);
3273
3274 view.home_sensor = true;
3275 axis.tick(&mut view, &mut client);
3276 assert!(axis.home_sensor);
3277
3278 view.home_sensor = false;
3279 axis.tick(&mut view, &mut client);
3280 assert!(!axis.home_sensor);
3281 }
3282
3283 #[test]
3284 fn velocity_output_converted() {
3285 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3286 let mut view = MockView::new();
3287 view.set_state(0x0027);
3288 view.velocity_actual = 3200;
3290
3291 axis.tick(&mut view, &mut client);
3292
3293 assert!((axis.speed - 90.0).abs() < 0.1);
3294 assert!(axis.moving_positive);
3295 assert!(!axis.moving_negative);
3296 }
3297
3298 fn soft_homing_config() -> AxisConfig {
3301 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
3302 cfg.homing_speed = 10.0;
3303 cfg.homing_accel = 20.0;
3304 cfg.homing_decel = 20.0;
3305 cfg
3306 }
3307
3308 fn soft_homing_axis() -> (
3309 Axis,
3310 CommandClient,
3311 tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
3312 tokio::sync::mpsc::UnboundedReceiver<String>,
3313 ) {
3314 use tokio::sync::mpsc;
3315 let (write_tx, write_rx) = mpsc::unbounded_channel();
3316 let (response_tx, response_rx) = mpsc::unbounded_channel();
3317 let client = CommandClient::new(write_tx, response_rx);
3318 let axis = Axis::new(soft_homing_config(), "TestDrive");
3319 (axis, client, response_tx, write_rx)
3320 }
3321
3322 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
3324 view.set_state(0x0027); axis.tick(view, client);
3326 }
3327
3328 fn ack_next_sdo(
3334 write_rx: &mut tokio::sync::mpsc::UnboundedReceiver<String>,
3335 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
3336 client: &mut CommandClient,
3337 value: serde_json::Value,
3338 ) -> mechutil::ipc::CommandMessage {
3339 let raw = write_rx
3340 .try_recv()
3341 .expect("expected an outgoing SDO request on write_rx");
3342 let req: mechutil::ipc::CommandMessage =
3343 serde_json::from_str(&raw).expect("outgoing SDO request is valid CommandMessage JSON");
3344 resp_tx
3345 .send(mechutil::ipc::CommandMessage::response(
3346 req.transaction_id,
3347 value,
3348 ))
3349 .unwrap();
3350 client.poll();
3351 req
3352 }
3353
3354 fn drive_one_mode_switch(
3359 axis: &mut Axis,
3360 view: &mut MockView,
3361 client: &mut CommandClient,
3362 write_rx: &mut tokio::sync::mpsc::UnboundedReceiver<String>,
3363 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
3364 mode: i8,
3365 ) {
3366 ack_next_sdo(write_rx, resp_tx, client, json!(null));
3368 axis.tick(view, client);
3369 std::thread::sleep(Duration::from_millis(110));
3370 axis.tick(view, client);
3372 ack_next_sdo(write_rx, resp_tx, client, json!({ "value": mode as i64 }));
3373 axis.tick(view, client);
3374 }
3375
3376 #[test]
3377 fn soft_homing_pnp_sensor_detection_halts() {
3378 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3379 let mut view = MockView::new();
3380 enable_axis(&mut axis, &mut view, &mut client);
3381
3382 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3383 axis.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
3385
3386 view.home_sensor = true;
3388 view.position_actual = 5000;
3389 axis.tick(&mut view, &mut client);
3390
3391 assert_eq!(
3393 axis.op,
3394 AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8)
3395 );
3396 assert!(
3397 view.control_word & (1 << 8) != 0,
3398 "halt bit set on sensor detection"
3399 );
3400 assert!(!axis.is_error);
3401 }
3402
3403 #[test]
3404 fn soft_homing_npn_sensor_detection_halts() {
3405 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3406 let mut view = MockView::new();
3407 view.home_sensor = true;
3409 enable_axis(&mut axis, &mut view, &mut client);
3410
3411 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
3412 axis.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
3413
3414 view.home_sensor = false;
3416 view.position_actual = 3000;
3417 axis.tick(&mut view, &mut client);
3418
3419 assert_eq!(
3420 axis.op,
3421 AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8)
3422 );
3423 assert!(!axis.is_error);
3424 }
3425
3426 #[test]
3427 fn soft_homing_limit_switch_suppresses_halt() {
3428 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3429 let mut view = MockView::new();
3430 enable_axis(&mut axis, &mut view, &mut client);
3431
3432 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
3434 axis.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
3435
3436 view.positive_limit = true;
3440 view.velocity_actual = 100; view.position_actual = 8000;
3442 axis.tick(&mut view, &mut client);
3443
3444 assert_eq!(
3445 axis.op,
3446 AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8)
3447 );
3448 assert!(
3449 !axis.is_error,
3450 "homed-on limit switch must be suppressed, not faulted"
3451 );
3452 }
3453
3454 #[test]
3455 fn soft_homing_opposite_limit_still_protects() {
3456 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3457 let mut view = MockView::new();
3458 enable_axis(&mut axis, &mut view, &mut client);
3459
3460 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3462
3463 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3468 axis.tick(&mut view, &mut client); view.negative_limit = true;
3473 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
3475
3476 assert!(axis.is_error);
3478 assert!(axis.error_message.contains("Negative limit switch"));
3479 }
3480
3481 #[test]
3482 fn soft_homing_search_sets_directional_target() {
3483 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3484 let mut view = MockView::new();
3485 enable_axis(&mut axis, &mut view, &mut client);
3486
3487 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
3489 axis.op = AxisOp::SoftHoming(HomeState::Search as u8); axis.tick(&mut view, &mut client);
3491 assert!(view.target_position < 0, "negative home seeks a negative target");
3492
3493 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3495 axis.op = AxisOp::SoftHoming(HomeState::Search as u8);
3496 axis.tick(&mut view, &mut client);
3497 assert!(view.target_position > 0, "positive home seeks a positive target");
3498 }
3499
3500 #[test]
3501 fn home_current_position_routes_to_drive_set() {
3502 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3503 let mut view = MockView::new();
3504 enable_axis(&mut axis, &mut view, &mut client);
3505
3506 axis.home(&mut view, HomingMethod::CurrentPosition35);
3510 assert!(matches!(
3511 axis.op,
3512 AxisOp::SoftHoming(s) if s == HomeState::WriteHomeOffset as u8
3513 ));
3514 assert_eq!(axis.homing_method, 35);
3515 }
3516
3517 #[test]
3518 fn home_integrated_arbitrary_code() {
3519 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3520 let mut view = MockView::new();
3521 enable_axis(&mut axis, &mut view, &mut client);
3522
3523 axis.home(&mut view, HomingMethod::Integrated(35));
3524 assert!(matches!(axis.op, AxisOp::Homing(16)));
3526 assert_eq!(axis.homing_method, 35);
3527 }
3528
3529 #[test]
3530 fn hardware_homing_skips_speed_sdos_when_zero() {
3531 use mechutil::ipc::CommandMessage;
3532
3533 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
3534 let mut view = MockView::new();
3535 enable_axis(&mut axis, &mut view, &mut client);
3536
3537 axis.home(&mut view, HomingMethod::Integrated(37));
3539
3540 axis.tick(&mut view, &mut client);
3542 assert!(matches!(axis.op, AxisOp::Homing(17)));
3543 let _ = write_rx.try_recv();
3544 let tid = axis.homing_sdo_tid;
3545 resp_tx
3546 .send(CommandMessage::response(tid, serde_json::json!(null)))
3547 .unwrap();
3548 client.poll();
3549 axis.tick(&mut view, &mut client);
3550 assert!(matches!(axis.op, AxisOp::Homing(0)));
3551
3552 axis.tick(&mut view, &mut client);
3554 assert!(matches!(axis.op, AxisOp::Homing(1)));
3555
3556 let _ = write_rx.try_recv();
3558
3559 let tid = axis.homing_sdo_tid;
3561 resp_tx
3562 .send(CommandMessage::response(tid, serde_json::json!(null)))
3563 .unwrap();
3564 client.poll();
3565 axis.tick(&mut view, &mut client);
3566
3567 assert!(matches!(axis.op, AxisOp::Homing(8)));
3569 }
3570
3571 #[test]
3572 fn hardware_homing_writes_speed_sdos_when_nonzero() {
3573 use mechutil::ipc::CommandMessage;
3574
3575 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3576 let mut view = MockView::new();
3577 enable_axis(&mut axis, &mut view, &mut client);
3578
3579 axis.home(&mut view, HomingMethod::Integrated(37));
3581
3582 axis.tick(&mut view, &mut client);
3584 assert!(matches!(axis.op, AxisOp::Homing(17)));
3585 let _ = write_rx.try_recv();
3586 let tid = axis.homing_sdo_tid;
3587 resp_tx
3588 .send(CommandMessage::response(tid, serde_json::json!(null)))
3589 .unwrap();
3590 client.poll();
3591 axis.tick(&mut view, &mut client);
3592
3593 axis.tick(&mut view, &mut client);
3595 assert!(matches!(axis.op, AxisOp::Homing(1)));
3596 let _ = write_rx.try_recv();
3597
3598 let tid = axis.homing_sdo_tid;
3600 resp_tx
3601 .send(CommandMessage::response(tid, serde_json::json!(null)))
3602 .unwrap();
3603 client.poll();
3604 axis.tick(&mut view, &mut client);
3605 assert!(matches!(axis.op, AxisOp::Homing(2)));
3607 }
3608
3609 #[test]
3610 fn soft_homing_edge_during_search_ack() {
3611 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3612 let mut view = MockView::new();
3613 enable_axis(&mut axis, &mut view, &mut client);
3614
3615 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3616 axis.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
3618
3619 view.home_sensor = true;
3621 view.position_actual = 2000;
3622 axis.tick(&mut view, &mut client);
3623
3624 assert_eq!(
3625 axis.op,
3626 AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8)
3627 );
3628 }
3629
3630 #[test]
3635 fn soft_homing_completes_on_attained_without_reached() {
3636 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3637 let mut view = MockView::new();
3638 view.status_word = 0x1027;
3640 axis.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
3641 axis.op_started = Some(Instant::now());
3642
3643 axis.tick(&mut view, &mut client);
3644
3645 assert_eq!(
3646 axis.op,
3647 AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8),
3648 "attained-only must complete homing, not wait for target_reached"
3649 );
3650 assert!(!axis.is_error);
3651 }
3652
3653 #[test]
3658 fn soft_homing_start_gate_does_not_require_attained_clear() {
3659 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3660 let mut view = MockView::new();
3661 view.status_word = 0x1027;
3663 axis.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
3664 axis.op_started = Some(Instant::now());
3665
3666 axis.tick(&mut view, &mut client);
3667
3668 assert_eq!(
3669 axis.op,
3670 AxisOp::SoftHoming(HomeState::WaitHomingDone as u8),
3671 "start gate must advance even with attained already set"
3672 );
3673 assert!(!axis.is_error);
3674 }
3675
3676 #[test]
3680 fn flush_queue_toggles_pp_homing_pp() {
3681 use tokio::sync::mpsc;
3682 let (write_tx, mut write_rx) = mpsc::unbounded_channel();
3683 let (resp_tx, response_rx) = mpsc::unbounded_channel();
3684 let mut client = CommandClient::new(write_tx, response_rx);
3685 let mut view = MockView::new();
3686 let mut sdo = SdoClient::new("TestDrive");
3687 let mut flush = FbFlushQueue::new();
3688
3689 flush.start(&mut view, &mut client, &mut sdo);
3691 assert!(flush.is_busy());
3692 assert_eq!(view.modes_of_operation, ModesOfOperation::Homing.as_i8());
3693
3694 ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!(null)); flush.tick(&mut view, &mut client, &mut sdo);
3697 std::thread::sleep(Duration::from_millis(110));
3698 flush.tick(&mut view, &mut client, &mut sdo); ack_next_sdo(
3700 &mut write_rx,
3701 &resp_tx,
3702 &mut client,
3703 json!({ "value": ModesOfOperation::Homing.as_i8() as i64 }),
3704 );
3705 flush.tick(&mut view, &mut client, &mut sdo); assert!(flush.is_busy(), "still flushing — now switching back to PP");
3708 assert_eq!(
3709 view.modes_of_operation,
3710 ModesOfOperation::ProfilePosition.as_i8()
3711 );
3712
3713 ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!(null)); flush.tick(&mut view, &mut client, &mut sdo);
3716 std::thread::sleep(Duration::from_millis(110));
3717 flush.tick(&mut view, &mut client, &mut sdo); ack_next_sdo(
3719 &mut write_rx,
3720 &resp_tx,
3721 &mut client,
3722 json!({ "value": ModesOfOperation::ProfilePosition.as_i8() as i64 }),
3723 );
3724 flush.tick(&mut view, &mut client, &mut sdo);
3725
3726 assert!(!flush.is_busy(), "flush complete");
3727 assert!(!flush.is_error());
3728 }
3729
3730 #[test]
3737 fn home_current_position_sets_drive_and_zeros_offset() {
3738 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3739 let mut view = MockView::new();
3740 enable_axis(&mut axis, &mut view, &mut client);
3741 view.status_word = 0x1027;
3743
3744 axis.set_home_position(90.0);
3745 axis.home(&mut view, HomingMethod::CurrentPosition35);
3746 assert_eq!(axis.config.home_position, 90.0);
3747 assert!(matches!(
3748 axis.op,
3749 AxisOp::SoftHoming(s) if s == HomeState::WriteHomeOffset as u8
3750 ));
3751
3752 axis.tick(&mut view, &mut client); ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!(null));
3756 axis.tick(&mut view, &mut client); ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!({ "value": -9701 }));
3759 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); drive_one_mode_switch(
3764 &mut axis,
3765 &mut view,
3766 &mut client,
3767 &mut write_rx,
3768 &resp_tx,
3769 ModesOfOperation::Homing.as_i8(),
3770 ); axis.tick(&mut view, &mut client); ack_next_sdo(&mut write_rx, &resp_tx, &mut client, json!(null));
3775 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);
3786
3787 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); drive_one_mode_switch(
3798 &mut axis,
3799 &mut view,
3800 &mut client,
3801 &mut write_rx,
3802 &resp_tx,
3803 ModesOfOperation::ProfilePosition.as_i8(),
3804 ); assert_eq!(axis.op, AxisOp::Idle);
3807 assert!(!axis.is_error, "persistent home must finish clean");
3808 assert!(!axis.is_busy);
3809 assert_eq!(axis.home_offset, 0);
3810 }
3811}