1use std::time::{Duration, Instant};
25use std::collections::VecDeque;
26
27use serde_json::json;
28use strum_macros::FromRepr;
29
30use crate::command_client::CommandClient;
31use crate::ethercat::{SdoClient, SdoResult};
32use crate::fb::Ton;
33use crate::motion::FbSetModeOfOperation;
34use super::axis_config::AxisConfig;
35use super::axis_view::AxisView;
36use super::homing::HomingMethod;
37use super::cia402::{
38 Cia402Control, Cia402Status, Cia402State,
39 ModesOfOperation, RawControlWord, RawStatusWord,
40};
41
42#[derive(Debug, Clone, PartialEq)]
47enum AxisOp {
48 Idle,
49 Enabling(u8),
50 Disabling(u8),
51 Moving(MoveKind, u8, bool, bool),
52 Homing(u8),
53 SoftHoming(u8),
54 Halting(u8),
55 FaultRecovery(u8),
56}
57
58#[repr(u8)]
64#[derive(Debug, Clone, PartialEq, FromRepr)]
65enum HaltState {
66 WaitStopped = 0,
69 WaitCancelAck = 10,
72 WaitCancelAckClear = 20,
76}
77
78const HALT_STAGE_TIMEOUT: Duration = Duration::from_secs(3);
80
81const HALT_STABLE_WINDOW: i32 = 50;
86
87const HALT_STOPPED_VELOCITY: i32 = 100;
93
94const HALT_STABLE_TICKS_REQUIRED: u8 = 5;
98
99#[repr(u8)]
100#[derive(Debug, Clone, PartialEq, FromRepr)]
101enum HomeState {
102 EnsurePpMode = 0,
103 WaitPpMode = 1,
104 Search = 5,
105 WaitSearching = 10,
106 WaitFoundSensor = 20,
107 WaitStoppedFoundSensor = 30,
108 WaitFoundSensorAck = 40,
109 WaitFoundSensorAckClear = 45,
110 DebounceFoundSensor = 50,
111 BackOff = 60,
112 WaitBackingOff = 70,
113 WaitLostSensor = 80,
114 WaitStoppedLostSensor = 90,
115 WaitLostSensorAck = 100,
116 WaitLostSensorAckClear = 120,
117 WaitHomeOffsetDone = 125,
118
119 WriteHomingModeOp = 160,
120 WaitWriteHomingModeOp = 165,
121
122 WriteHomingMethod = 205,
123 WaitWriteHomingMethodDone = 210,
124 ClearHomingTrigger = 215,
125 TriggerHoming = 217,
126 WaitHomingStarted = 218,
127 WaitHomingDone = 220,
128 ResetHomingTrigger = 222,
129 WaitHomingTriggerCleared = 223,
130 WriteMotionModeOfOperation = 230,
131 WaitWriteMotionModeOfOperation = 235,
132 SendCurrentPositionTarget = 240,
133 WaitCurrentPositionTargetSent = 245
134
135}
136
137#[derive(Debug, Clone, PartialEq)]
138enum MoveKind {
139 Absolute,
140 Relative,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq)]
144enum SoftHomeSensor {
145 PositiveLimit,
146 NegativeLimit,
147 HomeSensor,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq)]
151enum SoftHomeSensorType {
152 Pnp,
154 Npn,
156}
157
158const CW_SW_TRACE_CAP: usize = 48;
171
172#[derive(Clone)]
175struct TraceEntry {
176 seq: u32,
177 op: AxisOp,
178 cw: u16,
179 sw: u16,
180}
181
182fn decode_cw(cw: u16) -> String {
184 const BITS: [(u16, &str); 9] = [
185 (0, "SwOn"), (1, "EnVolt"), (2, "QStop"), (3, "EnOp"),
186 (4, "NewSP"), (5, "ChgSet"), (6, "Rel"), (7, "FltRst"), (8, "HALT"),
187 ];
188 let f: Vec<&str> = BITS.iter().filter(|(b, _)| cw & (1 << b) != 0).map(|(_, n)| *n).collect();
189 if f.is_empty() { "-".to_string() } else { f.join("|") }
190}
191
192fn decode_sw(sw: u16) -> String {
194 let mut f = vec![format!("{}", RawStatusWord(sw).state())];
195 for (bit, name) in [(10, "TgtReached"), (11, "IntLim"), (12, "SP_ACK"), (13, "FollowErr")] {
196 if sw & (1 << bit) != 0 { f.push(name.to_string()); }
197 }
198 f.join("|")
199}
200
201pub struct Axis {
202 config: AxisConfig,
203 sdo: SdoClient,
204
205 op: AxisOp,
207 home_offset: i32,
208 last_raw_position: i32,
209 op_started: Option<Instant>,
210 op_timeout: Duration,
211 homing_timeout: Duration,
212 move_start_timeout: Duration,
213 pending_move_target: i32,
214 pending_move_vel: u32,
215 pending_move_accel: u32,
216 pending_move_decel: u32,
217 homing_method: i8,
218 homing_sdo_tid: u32,
219 soft_home_sensor: SoftHomeSensor,
220 soft_home_sensor_type: SoftHomeSensorType,
221 soft_home_direction: f64,
222 halt_stable_count: u8,
223 prev_positive_limit: bool,
224 prev_negative_limit: bool,
225 prev_home_sensor: bool,
226
227 cw_sw_trace: VecDeque<TraceEntry>,
229 trace_seq: u32,
230
231
232
233 fb_mode_of_operation : FbSetModeOfOperation,
234
235 pub is_error: bool,
239 pub error_code: u32,
241 pub error_message: String,
243 pub motor_on: bool,
245 pub is_busy: bool,
251 pub in_motion: bool,
253 pub moving_positive: bool,
255 pub moving_negative: bool,
257 pub position: f64,
259 pub raw_position: i64,
261 pub speed: f64,
263 pub at_max_limit: bool,
265 pub at_min_limit: bool,
267 pub at_positive_limit_switch: bool,
269 pub at_negative_limit_switch: bool,
271 pub home_sensor: bool,
273
274
275 ton : Ton
277}
278
279impl Axis {
280 pub fn new(config: AxisConfig, device_name: &str) -> Self {
285 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
286 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
287 let move_start_timeout = op_timeout; Self {
289 config,
290 sdo: SdoClient::new(device_name),
291 op: AxisOp::Idle,
292 home_offset: 0,
293 last_raw_position: 0,
294 op_started: None,
295 op_timeout,
296 homing_timeout,
297 move_start_timeout,
298 pending_move_target: 0,
299 pending_move_vel: 0,
300 pending_move_accel: 0,
301 pending_move_decel: 0,
302 homing_method: 37,
303 homing_sdo_tid: 0,
304 soft_home_sensor: SoftHomeSensor::HomeSensor,
305 soft_home_sensor_type: SoftHomeSensorType::Pnp,
306 soft_home_direction: 1.0,
307 halt_stable_count: 0,
308 prev_positive_limit: false,
309 prev_negative_limit: false,
310 prev_home_sensor: false,
311 cw_sw_trace: VecDeque::new(),
312 trace_seq: 0,
313 is_error: false,
314 error_code: 0,
315 error_message: String::new(),
316 motor_on: false,
317 is_busy: false,
318 in_motion: false,
319 moving_positive: false,
320 moving_negative: false,
321 position: 0.0,
322 raw_position: 0,
323 speed: 0.0,
324 at_max_limit: false,
325 at_min_limit: false,
326 at_positive_limit_switch: false,
327 at_negative_limit_switch: false,
328 home_sensor: false,
329 ton: Ton::new(),
330 fb_mode_of_operation : FbSetModeOfOperation::new()
331 }
332 }
333
334 pub fn config(&self) -> &AxisConfig {
336 &self.config
337 }
338
339 pub fn move_absolute(
349 &mut self,
350 view: &mut impl AxisView,
351 target: f64,
352 vel: f64,
353 accel: f64,
354 decel: f64,
355 ) {
356 if let Some(msg) = self.check_target_limit(target, view) {
357 self.set_op_error(&msg);
358 return;
359 }
360
361 let cpu = self.config.counts_per_user();
362 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
363 let raw_vel = (vel * cpu).round() as u32;
364 let raw_accel = (accel * cpu).round() as u32;
365 let raw_decel = (decel * cpu).round() as u32;
366
367 let user_pos = target > self.position;
369 let user_neg = target < self.position;
370
371 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute, user_pos, user_neg);
372 }
373
374 pub fn move_relative(
380 &mut self,
381 view: &mut impl AxisView,
382 distance: f64,
383 vel: f64,
384 accel: f64,
385 decel: f64,
386 ) {
387 log::info!("Axis: request to move relative dist {} vel {} accel {} decel {}",
388 distance, vel, accel, decel
389 );
390
391 if let Some(msg) = self.check_target_limit(self.position + distance, view) {
392 self.set_op_error(&msg);
393 return;
394 }
395
396 let cpu = self.config.counts_per_user();
397 let raw_distance = self.config.to_counts(distance).round() as i32;
398 let raw_vel = (vel * cpu).round() as u32;
399 let raw_accel = (accel * cpu).round() as u32;
400 let raw_decel = (decel * cpu).round() as u32;
401
402 log::info!("Axis starting relative move: request to move relative raw dist {} raw vel {} raw accel {} raw decel {}",
403 raw_distance, raw_vel, raw_accel, raw_decel
404 );
405
406 let mut cw = RawControlWord(view.control_word());
408 cw.set_bit(4, false); view.set_control_word(cw.raw());
410
411 let user_pos = distance > 0.0;
413 let user_neg = distance < 0.0;
414
415 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative, user_pos, user_neg);
416 }
417
418 fn start_move(
419 &mut self,
420 view: &mut impl AxisView,
421 raw_target: i32,
422 raw_vel: u32,
423 raw_accel: u32,
424 raw_decel: u32,
425 kind: MoveKind,
426 user_pos: bool,
427 user_neg: bool,
428 ) {
429 self.pending_move_target = raw_target;
430 self.pending_move_vel = raw_vel;
431 self.pending_move_accel = raw_accel;
432 self.pending_move_decel = raw_decel;
433
434 view.set_target_position(raw_target);
436 view.set_profile_velocity(raw_vel);
437 view.set_profile_acceleration(raw_accel);
438 view.set_profile_deceleration(raw_decel);
439
440 let mut cw = RawControlWord(view.control_word());
445 cw.set_bit(6, kind == MoveKind::Relative);
446 cw.set_bit(8, false); cw.set_bit(4, true); view.set_control_word(cw.raw());
449
450 self.op = AxisOp::Moving(kind, 1, user_pos, user_neg);
455 self.op_started = Some(Instant::now());
456 }
457
458 pub fn halt(&mut self, view: &mut impl AxisView) {
476 self.command_halt(view);
477 self.halt_stable_count = 0;
478 self.last_raw_position = view.position_actual();
479 self.op_started = Some(Instant::now());
480 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
481 }
482
483 pub fn enable(&mut self, view: &mut impl AxisView) {
491 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
493 let mut cw = RawControlWord(view.control_word());
494 cw.cmd_shutdown();
495 view.set_control_word(cw.raw());
496
497 self.op = AxisOp::Enabling(1);
498 self.op_started = Some(Instant::now());
499 }
500
501 pub fn disable(&mut self, view: &mut impl AxisView) {
510 let mut cw = RawControlWord(view.control_word());
511 cw.cmd_shutdown();
512 cw.set_bit(4, false);
513 cw.set_bit(8, false);
514 cw.set_bit(7, false);
515 cw.set_bit(2, true);
516 view.set_control_word(cw.raw());
517
518 self.op = AxisOp::Disabling(1);
519 self.op_started = Some(Instant::now());
520 }
521
522 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
538 let sw = RawStatusWord(view.status_word());
539 let drive_in_fault = matches!(
540 sw.state(),
541 Cia402State::Fault | Cia402State::FaultReactionActive
542 );
543
544 self.is_error = false;
545 self.error_code = 0;
546 self.error_message.clear();
547
548 if drive_in_fault {
549 let mut cw = RawControlWord(view.control_word());
551 cw.cmd_clear_fault_reset();
552 view.set_control_word(cw.raw());
553 self.op = AxisOp::FaultRecovery(1);
554 self.op_started = Some(Instant::now());
555 } else {
556 self.op = AxisOp::Idle;
558 self.op_started = None;
559 self.is_busy = false;
560 self.in_motion = false;
561 }
562 }
563
564 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
572 if method.is_integrated() {
573 self.homing_method = match method {
574 HomingMethod::CurrentPosition => self.config.soft_home_method,
575 _ => method.cia402_code(),
576 };
577 self.op = AxisOp::Homing(0);
578 self.op_started = Some(Instant::now());
579 let _ = view;
580 } else {
581 self.configure_soft_homing(method);
582 self.start_soft_homing(view);
583 }
584 }
585
586 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
595 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
596 }
597
598 pub fn set_position_persistent(
615 &mut self,
616 client: &mut CommandClient,
617 user_units: f64,
618 ) {
619 self.config.home_position = user_units;
626 let desired_counts = self.config.to_counts(user_units).round() as i32;
627 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
628 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
629 self.op_started = Some(Instant::now());
630 log::info!(
631 "set_position_persistent: writing 0x607C = {} counts ({} user units)",
632 desired_counts, user_units
633 );
634 }
635
636 pub fn set_home_position(&mut self, user_units: f64) {
640 self.config.home_position = user_units;
641 }
642
643 pub fn set_software_max_limit(&mut self, user_units: f64) {
645 self.config.max_position_limit = user_units;
646 self.config.enable_max_position_limit = true;
647 }
648
649 pub fn set_software_min_limit(&mut self, user_units: f64) {
651 self.config.min_position_limit = user_units;
652 self.config.enable_min_position_limit = true;
653 }
654
655 pub fn sdo_write(
661 &mut self,
662 client: &mut CommandClient,
663 index: u16,
664 sub_index: u8,
665 value: serde_json::Value,
666 ) {
667 self.sdo.write(client, index, sub_index, value);
668 }
669
670 pub fn sdo_read(
672 &mut self,
673 client: &mut CommandClient,
674 index: u16,
675 sub_index: u8,
676 ) -> u32 {
677 self.sdo.read(client, index, sub_index)
678 }
679
680 pub fn sdo_result(
682 &mut self,
683 client: &mut CommandClient,
684 tid: u32,
685 ) -> SdoResult {
686 self.sdo.result(client, tid, Duration::from_secs(5))
687 }
688
689 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
703 self.check_faults(view);
704 self.progress_op(view, client);
705 self.update_outputs(view);
706 self.check_limits(view);
707 self.record_trace(view);
708 }
709
710 fn record_trace(&mut self, view: &impl AxisView) {
718 let cw = view.control_word();
719 let sw = view.status_word();
720 let changed = self
721 .cw_sw_trace
722 .back()
723 .map_or(true, |e| e.cw != cw || e.sw != sw || e.op != self.op);
724 if !changed {
725 return;
726 }
727 if self.cw_sw_trace.len() >= CW_SW_TRACE_CAP {
728 self.cw_sw_trace.pop_front();
729 }
730 self.cw_sw_trace.push_back(TraceEntry {
731 seq: self.trace_seq,
732 op: self.op.clone(),
733 cw,
734 sw,
735 });
736 self.trace_seq = self.trace_seq.wrapping_add(1);
737 }
738
739 pub fn trace(&self) -> String {
750 let mut out = String::new();
751 for e in &self.cw_sw_trace {
752 out.push_str(&format!(
753 " [{:>4}] {:<20} CW={:#06x} [{}] SW={:#06x} [{}]\n",
754 e.seq,
755 format!("{:?}", e.op),
756 e.cw,
757 decode_cw(e.cw),
758 e.sw,
759 decode_sw(e.sw),
760 ));
761 }
762 out
763 }
764
765 fn update_outputs(&mut self, view: &impl AxisView) {
770 let raw = view.position_actual();
771 self.raw_position = raw as i64;
772 self.position = self.config.to_user((raw - self.home_offset) as f64);
773
774 let vel = view.velocity_actual();
775 let user_vel = self.config.to_user(vel as f64);
776 self.speed = user_vel.abs();
777 self.moving_positive = user_vel > 0.0;
778 self.moving_negative = user_vel < 0.0;
779 self.is_busy = self.op != AxisOp::Idle;
780 self.in_motion = matches!(self.op, AxisOp::Moving(_, _, _, _) | AxisOp::SoftHoming(_));
781
782 let sw = RawStatusWord(view.status_word());
783 self.motor_on = sw.state() == Cia402State::OperationEnabled;
784
785 self.last_raw_position = raw;
786 }
787
788 fn check_faults(&mut self, view: &impl AxisView) {
793 let sw = RawStatusWord(view.status_word());
794 let state = sw.state();
795
796 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
797 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
798 self.is_error = true;
799 let ec = view.error_code();
800 if ec != 0 {
801 self.error_code = ec as u32;
802 }
803 self.error_message = format!("Drive fault (state: {})", state);
804 self.op = AxisOp::Idle;
806 self.op_started = None;
807 }
808 }
809 }
810
811 fn op_timed_out(&self) -> bool {
816 self.op_started
817 .map_or(false, |t| t.elapsed() > self.op_timeout)
818 }
819
820 fn homing_timed_out(&self) -> bool {
821 self.op_started
822 .map_or(false, |t| t.elapsed() > self.homing_timeout)
823 }
824
825 fn move_start_timed_out(&self) -> bool {
826 self.op_started
827 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
828 }
829
830 fn op_stage_timed_out(&self, limit: Duration) -> bool {
834 self.op_started
835 .map_or(false, |t| t.elapsed() > limit)
836 }
837
838 fn set_op_error(&mut self, msg: &str) {
839 self.is_error = true;
840 self.error_message = msg.to_string();
841 log::error!("Axis error: {}", msg);
842 if !self.cw_sw_trace.is_empty() {
846 log::warn!(
847 "Axis CiA-402 trace ({} recent transitions, oldest first):\n{}",
848 self.cw_sw_trace.len(),
849 self.trace(),
850 );
851 }
852 self.op = AxisOp::Idle;
853 self.op_started = None;
854 self.is_busy = false;
855 self.in_motion = false;
856 }
857
858 fn restore_pp_after_error(&mut self, msg: &str) {
859 self.is_error = true;
860 self.error_message = msg.to_string();
861 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);;
862 log::error!("Axis error: {}", msg);
863 }
864
865 fn finish_op_error(&mut self) {
866 self.op = AxisOp::Idle;
867 self.op_started = None;
868 self.is_busy = false;
869 self.in_motion = false;
870 }
871
872 fn complete_op(&mut self) {
873 self.op = AxisOp::Idle;
874 self.op_started = None;
875 self.is_busy = false;
880 self.in_motion = false;
881 }
882
883 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
892 let static_limit = if self.config.enable_max_position_limit {
893 Some(self.config.max_position_limit)
894 } else {
895 None
896 };
897 match (static_limit, view.dynamic_max_position_limit()) {
898 (Some(s), Some(d)) => Some(s.min(d)),
899 (Some(v), None) | (None, Some(v)) => Some(v),
900 (None, None) => None,
901 }
902 }
903
904 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
908 let static_limit = if self.config.enable_min_position_limit {
909 Some(self.config.min_position_limit)
910 } else {
911 None
912 };
913 match (static_limit, view.dynamic_min_position_limit()) {
914 (Some(s), Some(d)) => Some(s.max(d)),
915 (Some(v), None) | (None, Some(v)) => Some(v),
916 (None, None) => None,
917 }
918 }
919
920 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
925 if let Some(max) = self.effective_max_limit(view) {
926 if target > max {
927 return Some(format!(
928 "Target {:.3} exceeds max software limit {:.3}",
929 target, max
930 ));
931 }
932 }
933 if let Some(min) = self.effective_min_limit(view) {
934 if target < min {
935 return Some(format!(
936 "Target {:.3} exceeds min software limit {:.3}",
937 target, min
938 ));
939 }
940 }
941 None
942 }
943
944 fn check_limits(&mut self, view: &mut impl AxisView) {
953 let eff_max = self.effective_max_limit(view);
955 let eff_min = self.effective_min_limit(view);
956 let sw_max = eff_max.map_or(false, |m| self.position >= m);
957 let sw_min = eff_min.map_or(false, |m| self.position <= m);
958
959 self.at_max_limit = sw_max;
960 self.at_min_limit = sw_min;
961
962 let hw_pos = view.positive_limit_active();
964 let hw_neg = view.negative_limit_active();
965
966 self.at_positive_limit_switch = hw_pos;
967 self.at_negative_limit_switch = hw_neg;
968
969 self.home_sensor = view.home_sensor_active();
971
972 self.prev_positive_limit = hw_pos;
974 self.prev_negative_limit = hw_neg;
975 self.prev_home_sensor = view.home_sensor_active();
976
977 let mut commanded_positive = false;
979 let mut commanded_negative = false;
980
981 let is_moving = matches!(self.op, AxisOp::Moving(_, _, _, _));
982 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
983
984 if !is_moving && !is_soft_homing {
985 return; }
987
988 match &self.op {
989 AxisOp::Moving(_, _, pos, neg) => {
990 commanded_positive = *pos;
992 commanded_negative = *neg;
993 }
994 AxisOp::SoftHoming(_) => {
995 match self.soft_home_sensor {
996 SoftHomeSensor::PositiveLimit => commanded_positive = true,
997 SoftHomeSensor::NegativeLimit => commanded_negative = true,
998 SoftHomeSensor::HomeSensor => {
999 commanded_positive = self.moving_positive;
1000 commanded_negative = self.moving_negative;
1001 }
1002 }
1003 }
1004 _ => {}
1005 }
1006
1007 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
1009 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
1010
1011 let effective_hw_pos = hw_pos && !suppress_pos;
1012 let effective_hw_neg = hw_neg && !suppress_neg;
1013
1014 let effective_sw_max = sw_max && !is_soft_homing;
1016 let effective_sw_min = sw_min && !is_soft_homing;
1017
1018 let positive_blocked = (effective_sw_max || effective_hw_pos) && commanded_positive;
1019 let negative_blocked = (effective_sw_min || effective_hw_neg) && commanded_negative;
1020
1021
1022 if positive_blocked || negative_blocked {
1023 let mut cw = RawControlWord(view.control_word());
1024 cw.set_bit(8, true); view.set_control_word(cw.raw());
1026
1027 let msg = if effective_hw_pos && commanded_positive {
1028 "Positive limit switch active".to_string()
1029 } else if effective_hw_neg && commanded_negative {
1030 "Negative limit switch active".to_string()
1031 } else if effective_sw_max && commanded_positive {
1032 format!(
1033 "Software position limit: position {:.3} >= max {:.3}",
1034 self.position, eff_max.unwrap_or(self.position)
1035 )
1036 } else {
1037 format!(
1038 "Software position limit: position {:.3} <= min {:.3}",
1039 self.position, eff_min.unwrap_or(self.position)
1040 )
1041 };
1042
1043 if is_soft_homing {
1044 self.set_op_error(&msg);
1047 } else {
1048 log::info!("Axis move halted by limit: {}", msg);
1061 self.command_halt(view);
1062 self.halt_stable_count = 0;
1063 self.last_raw_position = view.position_actual();
1064 self.op_started = Some(Instant::now());
1065 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
1066 }
1067 }
1068 }
1069
1070 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
1075 match self.op.clone() {
1076 AxisOp::Idle => {}
1077 AxisOp::Enabling(step) => self.tick_enabling(view, step),
1078 AxisOp::Disabling(step) => self.tick_disabling(view, step),
1079 AxisOp::Moving(kind, step, pos, neg) => self.tick_moving(view, kind, step, pos, neg),
1080 AxisOp::Homing(step) => self.tick_homing(view, client, step),
1081 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
1082 AxisOp::Halting(step) => self.tick_halting(view, step),
1083 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
1084 }
1085 }
1086
1087 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
1092 match step {
1093 1 => {
1094 let sw = RawStatusWord(view.status_word());
1095 if sw.state() == Cia402State::ReadyToSwitchOn {
1096 let mut cw = RawControlWord(view.control_word());
1097 cw.cmd_enable_operation();
1098 view.set_control_word(cw.raw());
1099 self.op = AxisOp::Enabling(2);
1100 } else if self.op_timed_out() {
1101 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
1102 }
1103 }
1104 2 => {
1105 let sw = RawStatusWord(view.status_word());
1106 if sw.state() == Cia402State::OperationEnabled {
1107 self.complete_op();
1114 } else if self.op_timed_out() {
1115 self.set_op_error("Enable timeout: waiting for OperationEnabled");
1116 }
1117 }
1118 _ => self.complete_op(),
1119 }
1120 }
1121
1122 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
1126 match step {
1127 1 => {
1128 let sw = RawStatusWord(view.status_word());
1129 if sw.state() != Cia402State::OperationEnabled {
1130 self.complete_op();
1131 } else if self.op_timed_out() {
1132 self.set_op_error("Disable timeout: drive still in OperationEnabled");
1133 }
1134 }
1135 _ => self.complete_op(),
1136 }
1137 }
1138
1139 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8, pos: bool, neg: bool) {
1145 match step {
1146 1 => {
1147 let sw = RawStatusWord(view.status_word());
1149 if sw.raw() & (1 << 12) != 0 {
1150 let mut cw = RawControlWord(view.control_word());
1152 cw.set_bit(4, false);
1153 view.set_control_word(cw.raw());
1154 self.op = AxisOp::Moving(kind, 2, pos, neg);
1155 } else if self.move_start_timed_out() {
1156 self.set_op_error("Move timeout: set-point not acknowledged");
1157 }
1158 },
1159 2 => {
1160 let sw = RawStatusWord(view.status_word());
1162 if sw.raw() & (1 << 12) == 0 {
1163 self.op = AxisOp::Moving(kind, 3, pos, neg);
1165 }
1166 },
1167 3 => {
1168 let sw = RawStatusWord(view.status_word());
1170 if sw.target_reached() {
1171 self.complete_op();
1172 }
1173 },
1174 _ => self.complete_op(),
1175 }
1176 }
1177
1178 fn tick_homing(
1204 &mut self,
1205 view: &mut impl AxisView,
1206 client: &mut CommandClient,
1207 step: u8,
1208 ) {
1209 match step {
1210 0 => {
1211 self.homing_sdo_tid = self.sdo.write(
1213 client,
1214 0x6098,
1215 0,
1216 json!(self.homing_method),
1217 );
1218 self.op = AxisOp::Homing(1);
1219 }
1220 1 => {
1221 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1223 SdoResult::Ok(_) => {
1224 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
1226 self.op = AxisOp::Homing(8);
1227 } else {
1228 self.op = AxisOp::Homing(2);
1229 }
1230 }
1231 SdoResult::Pending => {
1232 if self.homing_timed_out() {
1233 self.set_op_error("Homing timeout: SDO write for homing method");
1234 }
1235 }
1236 SdoResult::Err(e) => {
1237 self.set_op_error(&format!("Homing SDO error: {}", e));
1238 }
1239 SdoResult::Timeout => {
1240 self.set_op_error("Homing timeout: SDO write timed out");
1241 }
1242 }
1243 }
1244 2 => {
1245 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1247 self.homing_sdo_tid = self.sdo.write(
1248 client,
1249 0x6099,
1250 1,
1251 json!(speed_counts),
1252 );
1253 self.op = AxisOp::Homing(3);
1254 }
1255 3 => {
1256 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1257 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
1258 SdoResult::Pending => {
1259 if self.homing_timed_out() {
1260 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
1261 }
1262 }
1263 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1264 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1265 }
1266 }
1267 4 => {
1268 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1270 self.homing_sdo_tid = self.sdo.write(
1271 client,
1272 0x6099,
1273 2,
1274 json!(speed_counts),
1275 );
1276 self.op = AxisOp::Homing(5);
1277 }
1278 5 => {
1279 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1280 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
1281 SdoResult::Pending => {
1282 if self.homing_timed_out() {
1283 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1284 }
1285 }
1286 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1287 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1288 }
1289 }
1290 6 => {
1291 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1293 self.homing_sdo_tid = self.sdo.write(
1294 client,
1295 0x609A,
1296 0,
1297 json!(accel_counts),
1298 );
1299 self.op = AxisOp::Homing(7);
1300 }
1301 7 => {
1302 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1303 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
1304 SdoResult::Pending => {
1305 if self.homing_timed_out() {
1306 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1307 }
1308 }
1309 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1310 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1311 }
1312 }
1313 8 => {
1314 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1324 let mut cw = RawControlWord(view.control_word());
1326 cw.set_bit(4, false);
1327 view.set_control_word(cw.raw());
1328 self.fb_mode_of_operation.reset();
1329 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1330 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1331 self.op = AxisOp::Homing(9);
1332 }
1333 9 => {
1334 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1336 if !self.fb_mode_of_operation.is_busy() {
1337 if self.fb_mode_of_operation.is_error() {
1338 self.set_op_error(&format!(
1339 "Homing: failed to enter Homing mode: {} {}",
1340 self.fb_mode_of_operation.error_code(),
1341 self.fb_mode_of_operation.error_message()
1342 ));
1343 } else {
1344 self.op = AxisOp::Homing(10);
1345 }
1346 }
1347 }
1348 10 => {
1349 let mut cw = RawControlWord(view.control_word());
1351 cw.set_bit(4, true);
1352 view.set_control_word(cw.raw());
1353 self.op = AxisOp::Homing(11);
1354 }
1355 11 => {
1356 let sw = view.status_word();
1368 if sw & (1 << 13) != 0 {
1369 self.set_op_error("Homing error: drive reported homing failure");
1370 } else {
1371 self.op = AxisOp::Homing(12);
1372 }
1373 }
1374 12 => {
1375 let sw = view.status_word();
1383 let error = sw & (1 << 13) != 0;
1384 let attained = sw & (1 << 12) != 0;
1385
1386 if error {
1387 self.set_op_error("Homing error: drive reported homing failure");
1388 } else if attained {
1389 self.op = AxisOp::Homing(13);
1390 } else if self.homing_timed_out() {
1391 self.set_op_error(&format!("Homing timeout: procedure did not complete (sw=0x{:04X})", sw));
1392 }
1393 }
1394 13 => {
1395 self.home_offset = view.position_actual()
1398 - self.config.to_counts(self.config.home_position).round() as i32;
1399 let mut cw = RawControlWord(view.control_word());
1401 cw.set_bit(4, false);
1402 view.set_control_word(cw.raw());
1403 self.op = AxisOp::Homing(14);
1404 }
1405 14 => {
1406 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1412 self.fb_mode_of_operation.reset();
1413 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1414 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1415 self.op = AxisOp::Homing(15);
1416 }
1417 15 => {
1418 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1420 if !self.fb_mode_of_operation.is_busy() {
1421 if self.fb_mode_of_operation.is_error() {
1422 self.set_op_error(&format!(
1423 "Homing: failed to restore PP mode: {} {}",
1424 self.fb_mode_of_operation.error_code(),
1425 self.fb_mode_of_operation.error_message()
1426 ));
1427 } else {
1428 log::info!("Homing complete — home offset: {}", self.home_offset);
1429 self.complete_op();
1430 }
1431 }
1432 }
1433 _ => self.complete_op(),
1434 }
1435 }
1436
1437 fn configure_soft_homing(&mut self, method: HomingMethod) {
1440 match method {
1441 HomingMethod::LimitSwitchPosPnp => {
1442 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1443 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1444 self.soft_home_direction = 1.0;
1445 }
1446 HomingMethod::LimitSwitchNegPnp => {
1447 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1448 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1449 self.soft_home_direction = -1.0;
1450 }
1451 HomingMethod::LimitSwitchPosNpn => {
1452 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1453 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1454 self.soft_home_direction = 1.0;
1455 }
1456 HomingMethod::LimitSwitchNegNpn => {
1457 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1458 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1459 self.soft_home_direction = -1.0;
1460 }
1461 HomingMethod::HomeSensorPosPnp => {
1462 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1463 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1464 self.soft_home_direction = 1.0;
1465 }
1466 HomingMethod::HomeSensorNegPnp => {
1467 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1468 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1469 self.soft_home_direction = -1.0;
1470 }
1471 HomingMethod::HomeSensorPosNpn => {
1472 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1473 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1474 self.soft_home_direction = 1.0;
1475 }
1476 HomingMethod::HomeSensorNegNpn => {
1477 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1478 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1479 self.soft_home_direction = -1.0;
1480 }
1481 _ => {} }
1483 }
1484
1485 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1486 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1487 self.op_started = Some(Instant::now());
1488 }
1489
1490 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1491 let raw = match self.soft_home_sensor {
1492 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1493 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1494 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1495 };
1496 match self.soft_home_sensor_type {
1497 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1500 }
1501
1502
1503 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1506 let dir = if !self.config.invert_direction {
1507 direction
1508 }
1509 else {
1510 -direction
1511 };
1512
1513 let target = if dir > 0.0 {
1514 i32::MAX
1515 }
1516 else {
1517 i32::MIN
1518 };
1519
1520 return target;
1521 }
1522
1523
1524 pub fn command_halt(&self, view: &mut impl AxisView) {
1529 let mut cw = RawControlWord(view.control_word());
1530 cw.set_bit(8, true); cw.set_bit(4, false); cw.set_bit(5, true); cw.set_bit(6, false); view.set_control_word(cw.raw());
1535 }
1536
1537 pub fn command_clear_halt(&self, view: &mut impl AxisView) {
1540 let mut cw = RawControlWord(view.control_word());
1541 cw.set_bit(8, false); view.set_control_word(cw.raw());
1543 }
1544
1545
1546 pub fn command_cancel_move(&self, view: &mut impl AxisView) {
1554
1555 let mut cw = RawControlWord(view.control_word());
1556
1557 cw.set_bit(4, true); cw.set_bit(5, true); cw.set_bit(6, false); if self.config().halt_blocks_setpoint_ack {
1562 cw.set_bit(8, false); }
1569
1570 view.set_control_word(cw.raw());
1571
1572 let current_pos = view.position_actual();
1573 view.set_target_position(current_pos);
1574 view.set_profile_velocity(0);
1575 }
1576
1577
1578 fn command_homing_speed(&self, view: &mut impl AxisView) {
1580 let cpu = self.config.counts_per_user();
1581 let vel = (self.config.homing_speed * cpu).round() as u32;
1582 let accel = (self.config.homing_accel * cpu).round() as u32;
1583 let decel = (self.config.homing_decel * cpu).round() as u32;
1584 view.set_profile_velocity(vel);
1585 view.set_profile_acceleration(accel);
1586 view.set_profile_deceleration(decel);
1587 }
1588
1589 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1605 match HomeState::from_repr(step) {
1606
1607 Some(HomeState::EnsurePpMode) => {
1608 log::info!("SoftHome: Ensuring PP mode..");
1613 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1614 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1615 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1616 },
1617 Some(HomeState::WaitPpMode) => {
1618
1619 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1620 if !self.fb_mode_of_operation.is_busy() {
1621 if self.fb_mode_of_operation.is_error() {
1622 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1623 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1624 ));
1625 }
1626 else {
1627 log::info!("SoftHome: Drive is in PP mode!");
1628
1629 if !self.check_soft_home_trigger(view) {
1633 log::info!("SoftHome: Not on home switch; seek out.");
1634 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1635 } else {
1636 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1637 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1638 }
1639 }
1640 }
1641
1642
1643 },
1644
1645 Some(HomeState::Search) => {
1647 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1648
1649 let target = self.calculate_max_relative_target(self.soft_home_direction);
1659 view.set_target_position(target);
1660
1661 self.command_homing_speed(view);
1670
1671 let mut cw = RawControlWord(view.control_word());
1672 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());
1677
1678 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1679 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1680 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1681 }
1682 Some(HomeState::WaitSearching) => {
1683 if self.check_soft_home_trigger(view) {
1684 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1685 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1686 return;
1687 }
1688 let sw = RawStatusWord(view.status_word());
1689 if sw.raw() & (1 << 12) != 0 {
1690 let mut cw = RawControlWord(view.control_word());
1691 cw.set_bit(4, false);
1692 view.set_control_word(cw.raw());
1693 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1694 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1695 } else if self.homing_timed_out() {
1696 self.set_op_error("Software homing timeout: set-point not acknowledged");
1697 }
1698 }
1699 Some(HomeState::WaitFoundSensor) => {
1709 if self.check_soft_home_trigger(view) {
1710 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1711 log::info!("ControlWord is : {} ", view.control_word());
1712
1713 let mut cw = RawControlWord(view.control_word());
1714 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1717
1718
1719 self.halt_stable_count = 0;
1720 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1721 } else if self.homing_timed_out() {
1722 self.set_op_error("Software homing timeout: sensor not detected");
1723 }
1724 }
1725
1726
1727 Some(HomeState::WaitStoppedFoundSensor) => {
1728 const STABLE_WINDOW: i32 = 1;
1729 const STABLE_TICKS_REQUIRED: u8 = 10;
1730
1731 let pos = view.position_actual();
1736 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1737 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1738 } else {
1739 self.halt_stable_count = 0;
1740 }
1741
1742 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1743
1744 log::info!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1745 self.command_cancel_move(view);
1746 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1747
1748 } else if self.homing_timed_out() {
1749 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1750 }
1751 }
1752 Some(HomeState::WaitFoundSensorAck) => {
1753 let sw = RawStatusWord(view.status_word());
1754 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1755
1756 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1757
1758 let mut cw = RawControlWord(view.control_word());
1760 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1763
1764 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1765
1766 } else if self.homing_timed_out() {
1767 self.set_op_error("Software homing timeout: cancel not acknowledged");
1768 }
1769 },
1770 Some(HomeState::WaitFoundSensorAckClear) => {
1771 let sw = RawStatusWord(view.status_word());
1772 if sw.raw() & (1 << 12) == 0 {
1774
1775 self.command_clear_halt(view);
1777
1778 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1779 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1780 self.ton.call(false, Duration::from_secs(3));
1781 }
1782 },
1783 Some(HomeState::DebounceFoundSensor) => {
1785 self.ton.call(true, Duration::from_secs(3));
1786
1787 let sw = RawStatusWord(view.status_word());
1788 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1789 self.ton.call(false, Duration::from_secs(3));
1790 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1791 view.position_actual(), view.control_word(), view.status_word());
1792 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1793 }
1794 }
1795
1796 Some(HomeState::BackOff) => {
1798
1799 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1800 view.set_target_position(target);
1801
1802
1803 self.command_homing_speed(view);
1804
1805 let mut cw = RawControlWord(view.control_word());
1806 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1810 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1811 target, self.config.homing_speed, view.position_actual(), cw.raw());
1812 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1813 }
1814 Some(HomeState::WaitBackingOff) => {
1815 let sw = RawStatusWord(view.status_word());
1816 if sw.raw() & (1 << 12) != 0 {
1817 let mut cw = RawControlWord(view.control_word());
1818 cw.set_bit(4, false);
1819 view.set_control_word(cw.raw());
1820 log::info!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1821 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1822 } else if self.homing_timed_out() {
1823 self.set_op_error("Software homing timeout: back-off not acknowledged");
1824 }
1825 }
1826 Some(HomeState::WaitLostSensor) => {
1827 if !self.check_soft_home_trigger(view) {
1828 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1829
1830 self.command_halt(view);
1831 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1832 } else if self.homing_timed_out() {
1833 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1834 }
1835 }
1836 Some(HomeState::WaitStoppedLostSensor) => {
1837 const STABLE_WINDOW: i32 = 1;
1838 const STABLE_TICKS_REQUIRED: u8 = 10;
1839
1840 let pos = view.position_actual();
1845 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1846 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1847 } else {
1848 self.halt_stable_count = 0;
1849 }
1850
1851 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1852 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1853 self.command_cancel_move(view);
1854 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1855 } else if self.homing_timed_out() {
1856 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1857 }
1858 }
1859 Some(HomeState::WaitLostSensorAck) => {
1860 let sw = RawStatusWord(view.status_word());
1861 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1862
1863 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1864
1865 let mut cw = RawControlWord(view.control_word());
1867 cw.set_bit(4, false); view.set_control_word(cw.raw());
1869
1870 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1871
1872
1873 } else if self.homing_timed_out() {
1874 self.set_op_error("Software homing timeout: cancel not acknowledged");
1875 }
1876 }
1877 Some(HomeState::WaitLostSensorAckClear) => {
1878 let sw = RawStatusWord(view.status_word());
1880 if sw.raw() & (1 << 12) == 0 {
1881
1882 self.command_clear_halt(view);
1883
1884 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1885 self.homing_sdo_tid = self.sdo.write(
1888 client, 0x607C, 0, json!(desired_counts),
1889 );
1890
1891 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1892 self.config.home_position, desired_counts
1893 );
1894
1895 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1896
1897 }
1898 },
1899
1900 Some(HomeState::WaitHomeOffsetDone) => {
1901 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1903 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1904 SdoResult::Pending => {
1905 if self.homing_timed_out() {
1906 self.set_op_error("Software homing timeout: home offset SDO write");
1907 }
1908 }
1909 SdoResult::Err(e) => {
1910 self.set_op_error(&format!("Software homing SDO error: {}", e));
1911 }
1912 SdoResult::Timeout => {
1913 self.set_op_error("Software homing: home offset SDO timed out");
1914 }
1915 }
1916 },
1917 Some(HomeState::WriteHomingModeOp) => {
1918
1919 self.fb_mode_of_operation.reset();
1923 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1924 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1925 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1926
1927
1928 },
1929 Some(HomeState::WaitWriteHomingModeOp) => {
1930 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1932
1933 if !self.fb_mode_of_operation.is_busy() {
1934 if self.fb_mode_of_operation.is_error() {
1935 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1936 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1937 ));
1938 }
1939 else {
1940 log::info!("SoftHome: Drive is now in Homing Mode.");
1941 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1942 }
1943 }
1944 },
1945 Some(HomeState::WriteHomingMethod) => {
1946 self.homing_sdo_tid = self.sdo.write(
1949 client, 0x6098, 0, json!(self.config.soft_home_method),
1950 );
1951 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1952 }
1953 Some(HomeState::WaitWriteHomingMethodDone) => {
1954 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1956 SdoResult::Ok(_) => {
1957 log::info!("SoftHome: Successfully wrote homing method.");
1958 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1959 }
1960 SdoResult::Pending => {
1961 if self.homing_timed_out() {
1962 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1963 }
1964 }
1965 SdoResult::Err(e) => {
1966 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1967 }
1968 SdoResult::Timeout => {
1969 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1970 }
1971 }
1972 }
1973 Some(HomeState::ClearHomingTrigger) => {
1974 let mut cw = RawControlWord(view.control_word());
1977 cw.set_bit(4, false);
1978 view.set_control_word(cw.raw());
1979 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1980 }
1981 Some(HomeState::TriggerHoming) => {
1982 let mut cw = RawControlWord(view.control_word());
1984 cw.set_bit(4, true);
1985 view.set_control_word(cw.raw());
1986 log::info!("SoftHome[TriggerHoming]: start homing");
1987 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1988 }
1989 Some(HomeState::WaitHomingStarted) => {
1990 let sw = view.status_word();
1996 let error = sw & (1 << 13) != 0;
1997 if error {
1998 self.restore_pp_after_error("Software homing: drive reported homing error");
1999 } else if sw & (1 << 12) == 0 {
2000 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
2001 } else if self.homing_timed_out() {
2002 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
2003 }
2004 }
2005 Some(HomeState::WaitHomingDone) => {
2006 let sw = view.status_word();
2008 let error = sw & (1 << 13) != 0;
2009 let attained = sw & (1 << 12) != 0;
2010 let reached = sw & (1 << 10) != 0;
2011
2012 if error {
2013 self.restore_pp_after_error("Software homing: drive reported homing error");
2014 } else if attained && reached {
2015 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
2016 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
2017 } else if self.homing_timed_out() {
2018 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
2019 }
2020 }
2021 Some(HomeState::ResetHomingTrigger) => {
2022 let mut cw = RawControlWord(view.control_word());
2027 cw.set_bit(4, false);
2028 view.set_control_word(cw.raw());
2029 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
2030 }
2031 Some(HomeState::WaitHomingTriggerCleared) => {
2032 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
2036 }
2037
2038
2039 Some(HomeState::WriteMotionModeOfOperation) => {
2040
2041 self.fb_mode_of_operation.reset();
2044 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
2045 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2046 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
2047
2048 },
2049 Some(HomeState::WaitWriteMotionModeOfOperation) => {
2050 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2052
2053 if !self.fb_mode_of_operation.is_busy() {
2054 if self.fb_mode_of_operation.is_error() {
2055 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
2056 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
2057 ));
2058 }
2059 else {
2060 if self.is_error {
2061 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
2062 self.finish_op_error();
2063 }
2064 else {
2065 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
2068 }
2069
2070 }
2071 }
2072 },
2073
2074 Some(HomeState::SendCurrentPositionTarget) => {
2075 let current_pos = view.position_actual();
2077 view.set_target_position(current_pos);
2078 view.set_profile_velocity(0);
2079 let mut cw = RawControlWord(view.control_word());
2080 cw.set_bit(4, true);
2081 cw.set_bit(5, true);
2082 cw.set_bit(6, false); view.set_control_word(cw.raw());
2084 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
2085 }
2086 Some(HomeState::WaitCurrentPositionTargetSent) => {
2087 let sw = RawStatusWord(view.status_word());
2089 if sw.raw() & (1 << 12) != 0 {
2090 let mut cw = RawControlWord(view.control_word());
2091 cw.set_bit(4, false);
2092 view.set_control_word(cw.raw());
2093 log::info!("Software homing complete — position set to {} user units",
2094 self.config.home_position);
2095 self.complete_op();
2096 } else if self.homing_timed_out() {
2097 self.set_op_error("Software homing timeout: hold position not acknowledged");
2098 }
2099 }
2100 _ => self.complete_op(),
2101 }
2102 }
2103
2104 fn tick_halting(&mut self, view: &mut impl AxisView, step: u8) {
2117 match HaltState::from_repr(step) {
2118 Some(HaltState::WaitStopped) => {
2119 let pos = view.position_actual();
2123 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
2124
2125 let vel = view.velocity_actual().abs();
2126 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
2127
2128 if pos_stable || vel_stopped {
2133 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2134 } else {
2135 self.halt_stable_count = 0;
2136 }
2137
2138 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
2139 self.command_cancel_move(view);
2140 self.op_started = Some(Instant::now());
2141 self.op = AxisOp::Halting(HaltState::WaitCancelAck as u8);
2142 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2143 self.set_op_error("Halt timeout: motor did not stop");
2144 }
2145 }
2146 Some(HaltState::WaitCancelAck) => {
2147 let sw = RawStatusWord(view.status_word());
2148 let setpoint_ack = sw.raw() & (1 << 12) != 0;
2149 if setpoint_ack {
2151 let mut cw = RawControlWord(view.control_word());
2154 cw.set_bit(4, false);
2155 cw.set_bit(5, true);
2156 view.set_control_word(cw.raw());
2157 self.op_started = Some(Instant::now());
2158 self.op = AxisOp::Halting(HaltState::WaitCancelAckClear as u8);
2159 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2160 self.set_op_error("Halt timeout: cancel not acknowledged");
2161 }
2162 }
2163 Some(HaltState::WaitCancelAckClear) => {
2164 let sw = RawStatusWord(view.status_word());
2165 if sw.raw() & (1 << 12) == 0 {
2166 self.command_clear_halt(view);
2168 self.complete_op();
2169 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2170 self.set_op_error("Halt timeout: ack did not clear");
2171 }
2172 }
2173 None => {
2174 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
2175 self.complete_op();
2176 }
2177 }
2178 }
2179
2180 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
2185 match step {
2186 1 => {
2187 let mut cw = RawControlWord(view.control_word());
2189 cw.cmd_fault_reset();
2190 view.set_control_word(cw.raw());
2191 self.op = AxisOp::FaultRecovery(2);
2192 }
2193 2 => {
2194 let sw = RawStatusWord(view.status_word());
2196 let state = sw.state();
2197 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
2198 log::info!("Fault cleared (drive state: {})", state);
2199 self.complete_op();
2200 } else if self.op_timed_out() {
2201 self.set_op_error("Fault reset timeout: drive still faulted");
2202 }
2203 }
2204 _ => self.complete_op(),
2205 }
2206 }
2207}
2208
2209#[cfg(test)]
2214mod tests {
2215 use super::*;
2216
2217 struct MockView {
2219 control_word: u16,
2220 status_word: u16,
2221 target_position: i32,
2222 profile_velocity: u32,
2223 profile_acceleration: u32,
2224 profile_deceleration: u32,
2225 modes_of_operation: i8,
2226 modes_of_operation_display: i8,
2227 position_actual: i32,
2228 velocity_actual: i32,
2229 error_code: u16,
2230 positive_limit: bool,
2231 negative_limit: bool,
2232 home_sensor: bool,
2233 }
2234
2235 impl MockView {
2236 fn new() -> Self {
2237 Self {
2238 control_word: 0,
2239 status_word: 0x0040, target_position: 0,
2241 profile_velocity: 0,
2242 profile_acceleration: 0,
2243 profile_deceleration: 0,
2244 modes_of_operation: 0,
2245 modes_of_operation_display: 1, position_actual: 0,
2247 velocity_actual: 0,
2248 error_code: 0,
2249 positive_limit: false,
2250 negative_limit: false,
2251 home_sensor: false,
2252 }
2253 }
2254
2255 fn set_state(&mut self, state: u16) {
2256 self.status_word = state;
2257 }
2258 }
2259
2260 impl AxisView for MockView {
2261 fn control_word(&self) -> u16 { self.control_word }
2262 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
2263 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
2264 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
2265 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
2266 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
2267 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
2268 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
2269 fn status_word(&self) -> u16 { self.status_word }
2270 fn position_actual(&self) -> i32 { self.position_actual }
2271 fn velocity_actual(&self) -> i32 { self.velocity_actual }
2272 fn error_code(&self) -> u16 { self.error_code }
2273 fn positive_limit_active(&self) -> bool { self.positive_limit }
2274 fn negative_limit_active(&self) -> bool { self.negative_limit }
2275 fn home_sensor_active(&self) -> bool { self.home_sensor }
2276 }
2277
2278 fn test_config() -> AxisConfig {
2279 AxisConfig::new(12_800).with_user_scale(360.0)
2280 }
2281
2282 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2284 use tokio::sync::mpsc;
2285 let (write_tx, write_rx) = mpsc::unbounded_channel();
2286 let (response_tx, response_rx) = mpsc::unbounded_channel();
2287 let client = CommandClient::new(write_tx, response_rx);
2288 let axis = Axis::new(test_config(), "TestDrive");
2289 (axis, client, response_tx, write_rx)
2290 }
2291
2292 #[test]
2293 fn axis_config_conversion() {
2294 let cfg = test_config();
2295 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2297 }
2298
2299 #[test]
2300 fn trace_records_on_change_not_every_tick() {
2301 let (mut axis, mut client, _r, _w) = test_axis();
2302 let mut view = MockView::new();
2303 view.set_state(0x0040); for _ in 0..10 {
2308 axis.tick(&mut view, &mut client);
2309 }
2310 let after_idle = axis.trace().lines().count();
2311 assert!(after_idle <= 1, "constant state must record ≤1 line, got {}", after_idle);
2312
2313 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2316 assert!(axis.trace().lines().count() > after_idle, "a SW change adds a line");
2317 assert!(axis.trace().contains("Operation Enabled"), "trace decodes the SW state");
2318 }
2319
2320 #[test]
2321 fn trace_decoders_render_handshake_bits() {
2322 assert_eq!(decode_cw(0x003F), "SwOn|EnVolt|QStop|EnOp|NewSP|ChgSet");
2324 assert!(decode_cw(0x013F).contains("HALT"), "0x13F still asserts HALT");
2325 let s = decode_sw(0x1027);
2327 assert!(s.contains("Operation Enabled") && s.contains("SP_ACK"), "got {s}");
2328 }
2329
2330 #[test]
2331 fn enable_sequence_sets_pp_mode_and_shutdown() {
2332 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2333 let mut view = MockView::new();
2334
2335 axis.enable(&mut view);
2336
2337 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
2339 assert_eq!(view.control_word & 0x008F, 0x0006);
2341 assert_eq!(axis.op, AxisOp::Enabling(1));
2343
2344 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2347
2348 assert_eq!(view.control_word & 0x008F, 0x000F);
2350 assert_eq!(axis.op, AxisOp::Enabling(2));
2351
2352 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2355
2356 assert_eq!(axis.op, AxisOp::Idle);
2358 assert!(axis.motor_on);
2359 }
2360
2361 #[test]
2362 fn move_absolute_sets_target() {
2363 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2364 let mut view = MockView::new();
2365 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2370
2371 assert_eq!(view.target_position, 1600);
2373 assert_eq!(view.profile_velocity, 3200);
2375 assert_eq!(view.profile_acceleration, 6400);
2377 assert_eq!(view.profile_deceleration, 6400);
2378 assert!(view.control_word & (1 << 4) != 0);
2380 assert!(view.control_word & (1 << 6) == 0);
2382 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2384 }
2385
2386 #[test]
2387 fn move_relative_sets_relative_bit() {
2388 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2389 let mut view = MockView::new();
2390 view.set_state(0x0027);
2391 axis.tick(&mut view, &mut client);
2392
2393 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2394
2395 assert!(view.control_word & (1 << 6) != 0);
2397 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1, _, _)));
2398 }
2399
2400 #[test]
2401 fn move_completes_on_target_reached() {
2402 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2403 let mut view = MockView::new();
2404 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2406
2407 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2408
2409 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2412 assert!(view.control_word & (1 << 4) == 0);
2414
2415 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2418 assert_eq!(axis.op, AxisOp::Idle);
2420 assert!(!axis.in_motion);
2421 }
2422
2423 #[test]
2424 fn fault_detected_sets_error() {
2425 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2426 let mut view = MockView::new();
2427 view.set_state(0x0008); view.error_code = 0x1234;
2429
2430 axis.tick(&mut view, &mut client);
2431
2432 assert!(axis.is_error);
2433 assert_eq!(axis.error_code, 0x1234);
2434 assert!(axis.error_message.contains("fault"));
2435 }
2436
2437 #[test]
2438 fn fault_recovery_sequence() {
2439 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2440 let mut view = MockView::new();
2441 view.set_state(0x0008); axis.reset_faults(&mut view);
2444 assert!(view.control_word & 0x0080 == 0);
2446
2447 axis.tick(&mut view, &mut client);
2449 assert!(view.control_word & 0x0080 != 0);
2450
2451 view.set_state(0x0040);
2453 axis.tick(&mut view, &mut client);
2454 assert_eq!(axis.op, AxisOp::Idle);
2455 assert!(!axis.is_error);
2456 }
2457
2458 #[test]
2459 fn reset_faults_software_only_is_synchronous() {
2460 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2466 let mut view = MockView::new();
2467 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2469
2470 axis.set_software_min_limit(207.0);
2472 axis.move_absolute(&mut view, 200.0, 90.0, 180.0, 180.0);
2473 assert!(axis.is_error, "move past software limit must set is_error");
2474 assert!(!axis.is_busy);
2475
2476 let cw_before = view.control_word;
2479
2480 axis.reset_faults(&mut view);
2481
2482 assert!(!axis.is_error);
2484 assert_eq!(axis.error_code, 0);
2485 assert!(axis.error_message.is_empty());
2486 assert!(!axis.is_busy);
2487 assert_eq!(axis.op, AxisOp::Idle);
2488 assert_eq!(view.control_word, cw_before);
2490 }
2491
2492 #[test]
2493 fn disable_sequence() {
2494 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2495 let mut view = MockView::new();
2496 view.set_state(0x0027); axis.disable(&mut view);
2499 assert_eq!(view.control_word & 0x008F, 0x0006);
2501
2502 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2505 assert_eq!(axis.op, AxisOp::Idle);
2506 }
2507
2508 #[test]
2509 fn position_tracks_with_home_offset() {
2510 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2511 let mut view = MockView::new();
2512 view.set_state(0x0027);
2513 view.position_actual = 5000;
2514
2515 axis.enable(&mut view);
2517 view.set_state(0x0021);
2518 axis.tick(&mut view, &mut client);
2519 view.set_state(0x0027);
2520 axis.tick(&mut view, &mut client);
2521
2522 assert_eq!(axis.home_offset, 5000);
2524
2525 assert!((axis.position - 0.0).abs() < 0.01);
2527
2528 view.position_actual = 6600;
2530 axis.tick(&mut view, &mut client);
2531
2532 assert!((axis.position - 45.0).abs() < 0.1);
2534 }
2535
2536 #[test]
2537 fn set_position_adjusts_home_offset() {
2538 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2539 let mut view = MockView::new();
2540 view.position_actual = 3200;
2541
2542 axis.set_position(&view, 90.0);
2543 axis.tick(&mut view, &mut client);
2544
2545 assert_eq!(axis.home_offset, 0);
2547 assert!((axis.position - 90.0).abs() < 0.01);
2548 }
2549
2550 #[test]
2551 fn halt_runs_multi_stage_close_out() {
2552 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2553 let mut view = MockView::new();
2554 view.set_state(0x0027);
2555
2556 axis.halt(&mut view);
2557
2558 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2560 assert!(view.control_word & (1 << 4) == 0, "new_setpoint must be cleared");
2561
2562 assert!(matches!(axis.op, AxisOp::Halting(_)),
2564 "halt should enter Halting state, not Idle");
2565 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2566 assert_eq!(step, HaltState::WaitStopped as u8);
2567
2568 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2575 axis.tick(&mut view, &mut client);
2576 }
2577 assert!(matches!(axis.op, AxisOp::Halting(_)));
2579 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2580 assert_eq!(step, HaltState::WaitCancelAck as u8,
2581 "should advance past WaitStopped once position/velocity is stable");
2582
2583 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2587 }
2588
2589 #[test]
2590 fn is_busy_tracks_operations() {
2591 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2592 let mut view = MockView::new();
2593
2594 axis.tick(&mut view, &mut client);
2596 assert!(!axis.is_busy);
2597
2598 axis.enable(&mut view);
2600 axis.tick(&mut view, &mut client);
2601 assert!(axis.is_busy);
2602
2603 view.set_state(0x0021);
2605 axis.tick(&mut view, &mut client);
2606 view.set_state(0x0027);
2607 axis.tick(&mut view, &mut client);
2608 assert!(!axis.is_busy);
2609
2610 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2612 axis.tick(&mut view, &mut client);
2613 assert!(axis.is_busy);
2614 assert!(axis.in_motion);
2615 }
2616
2617 #[test]
2618 fn fault_during_move_cancels_op() {
2619 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2620 let mut view = MockView::new();
2621 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2623
2624 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2626 axis.tick(&mut view, &mut client);
2627 assert!(axis.is_busy);
2628 assert!(!axis.is_error);
2629
2630 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2633
2634 assert!(!axis.is_busy);
2636 assert!(axis.is_error);
2637 assert_eq!(axis.op, AxisOp::Idle);
2638 }
2639
2640 #[test]
2641 fn move_absolute_rejected_by_max_limit() {
2642 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2643 let mut view = MockView::new();
2644 view.set_state(0x0027);
2645 axis.tick(&mut view, &mut client);
2646
2647 axis.set_software_max_limit(90.0);
2648 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2649
2650 assert!(axis.is_error);
2652 assert_eq!(axis.op, AxisOp::Idle);
2653 assert!(axis.error_message.contains("max software limit"));
2654 }
2655
2656 #[test]
2657 fn move_absolute_rejected_by_min_limit() {
2658 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2659 let mut view = MockView::new();
2660 view.set_state(0x0027);
2661 axis.tick(&mut view, &mut client);
2662
2663 axis.set_software_min_limit(-10.0);
2664 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2665
2666 assert!(axis.is_error);
2667 assert_eq!(axis.op, AxisOp::Idle);
2668 assert!(axis.error_message.contains("min software limit"));
2669 }
2670
2671 #[test]
2672 fn move_relative_rejected_by_max_limit() {
2673 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2674 let mut view = MockView::new();
2675 view.set_state(0x0027);
2676 axis.tick(&mut view, &mut client);
2677
2678 axis.set_software_max_limit(50.0);
2680 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2681
2682 assert!(axis.is_error);
2683 assert_eq!(axis.op, AxisOp::Idle);
2684 assert!(axis.error_message.contains("max software limit"));
2685 }
2686
2687 #[test]
2688 fn move_within_limits_allowed() {
2689 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2690 let mut view = MockView::new();
2691 view.set_state(0x0027);
2692 axis.tick(&mut view, &mut client);
2693
2694 axis.set_software_max_limit(90.0);
2695 axis.set_software_min_limit(-90.0);
2696 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2697
2698 assert!(!axis.is_error);
2700 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2701 }
2702
2703 #[test]
2704 fn runtime_limit_halts_move_in_violated_direction() {
2705 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2706 let mut view = MockView::new();
2707 view.set_state(0x0027);
2708 axis.tick(&mut view, &mut client);
2709
2710 axis.set_software_max_limit(45.0);
2711 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2713
2714 view.position_actual = 1650;
2717 view.velocity_actual = 100; view.status_word = 0x1027;
2721 axis.tick(&mut view, &mut client);
2722 view.status_word = 0x0027;
2723 axis.tick(&mut view, &mut client);
2724
2725 assert!(!axis.is_error);
2729 assert!(axis.at_max_limit);
2730 assert!(axis.is_busy);
2731 assert!(matches!(axis.op, AxisOp::Halting(_)));
2732 assert!(view.control_word & (1 << 8) != 0);
2734 }
2735
2736 #[test]
2737 fn runtime_limit_allows_move_in_opposite_direction() {
2738 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2739 let mut view = MockView::new();
2740 view.set_state(0x0027);
2741 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2744 axis.tick(&mut view, &mut client);
2745 assert!(axis.at_max_limit);
2746
2747 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2749 assert!(!axis.is_error);
2750 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2751
2752 view.velocity_actual = -100;
2754 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2756 assert!(!axis.is_error);
2758 }
2759
2760 #[test]
2761 fn positive_limit_switch_halts_positive_move() {
2762 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2763 let mut view = MockView::new();
2764 view.set_state(0x0027);
2765 axis.tick(&mut view, &mut client);
2766
2767 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2769 view.velocity_actual = 100; view.status_word = 0x1027;
2772 axis.tick(&mut view, &mut client);
2773 view.status_word = 0x0027;
2774
2775 view.positive_limit = true;
2777 axis.tick(&mut view, &mut client);
2778
2779 assert!(!axis.is_error);
2784 assert!(axis.at_positive_limit_switch);
2785 assert!(axis.is_busy);
2786 assert!(matches!(axis.op, AxisOp::Halting(_)));
2787 assert!(view.control_word & (1 << 8) != 0);
2789 }
2790
2791 #[test]
2792 fn negative_limit_switch_halts_negative_move() {
2793 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2794 let mut view = MockView::new();
2795 view.set_state(0x0027);
2796 axis.tick(&mut view, &mut client);
2797
2798 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2800 view.velocity_actual = -100; view.status_word = 0x1027;
2802 axis.tick(&mut view, &mut client);
2803 view.status_word = 0x0027;
2804
2805 view.negative_limit = true;
2807 axis.tick(&mut view, &mut client);
2808
2809 assert!(!axis.is_error);
2814 assert!(axis.at_negative_limit_switch);
2815 assert!(axis.is_busy);
2816 assert!(matches!(axis.op, AxisOp::Halting(_)));
2817 assert!(view.control_word & (1 << 8) != 0);
2818 }
2819
2820 #[test]
2821 fn limit_halt_cleanup_then_next_move_starts_cleanly() {
2822 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2827 let mut view = MockView::new();
2828 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2830
2831 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2833 view.velocity_actual = 100;
2834 view.status_word = 0x1027;
2835 axis.tick(&mut view, &mut client);
2836 view.status_word = 0x0027;
2837 view.positive_limit = true;
2838 axis.tick(&mut view, &mut client);
2839
2840 assert!(!axis.is_error);
2842 assert!(axis.is_busy);
2843 assert!(matches!(axis.op, AxisOp::Halting(_)));
2844 assert!(view.control_word & (1 << 8) != 0);
2845
2846 view.velocity_actual = 0;
2851 view.positive_limit = false;
2852 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2853 axis.tick(&mut view, &mut client);
2854 }
2855 assert!(matches!(axis.op, AxisOp::Halting(_)));
2856 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2857 assert_eq!(step, HaltState::WaitCancelAck as u8);
2858
2859 view.status_word = 0x1027;
2861 axis.tick(&mut view, &mut client);
2862 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2863 assert_eq!(step, HaltState::WaitCancelAckClear as u8);
2864
2865 view.status_word = 0x0027;
2868 axis.tick(&mut view, &mut client);
2869 assert_eq!(axis.op, AxisOp::Idle);
2870 assert!(!axis.is_busy);
2871 assert!(view.control_word & (1 << 8) == 0, "halt should be cleared after close-out");
2872
2873 axis.move_absolute(&mut view, 30.0, 90.0, 180.0, 180.0);
2876 assert!(view.control_word & (1 << 4) != 0, "new-setpoint asserted on start_move tick");
2877 assert!(view.control_word & (1 << 8) == 0, "halt stays cleared");
2878 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2879 }
2880
2881 #[test]
2882 fn limit_switch_allows_move_in_opposite_direction() {
2883 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2884 let mut view = MockView::new();
2885 view.set_state(0x0027);
2886 view.positive_limit = true;
2888 view.velocity_actual = -100;
2889 axis.tick(&mut view, &mut client);
2890 assert!(axis.at_positive_limit_switch);
2891
2892 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2894 view.status_word = 0x1027;
2895 axis.tick(&mut view, &mut client);
2896
2897 assert!(!axis.is_error);
2899 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
2900 }
2901
2902 #[test]
2903 fn limit_switch_ignored_when_not_moving() {
2904 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2905 let mut view = MockView::new();
2906 view.set_state(0x0027);
2907 view.positive_limit = true;
2908
2909 axis.tick(&mut view, &mut client);
2910
2911 assert!(axis.at_positive_limit_switch);
2913 assert!(!axis.is_error);
2914 }
2915
2916 #[test]
2917 fn home_sensor_output_tracks_view() {
2918 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2919 let mut view = MockView::new();
2920 view.set_state(0x0027);
2921
2922 axis.tick(&mut view, &mut client);
2923 assert!(!axis.home_sensor);
2924
2925 view.home_sensor = true;
2926 axis.tick(&mut view, &mut client);
2927 assert!(axis.home_sensor);
2928
2929 view.home_sensor = false;
2930 axis.tick(&mut view, &mut client);
2931 assert!(!axis.home_sensor);
2932 }
2933
2934 #[test]
2935 fn velocity_output_converted() {
2936 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2937 let mut view = MockView::new();
2938 view.set_state(0x0027);
2939 view.velocity_actual = 3200;
2941
2942 axis.tick(&mut view, &mut client);
2943
2944 assert!((axis.speed - 90.0).abs() < 0.1);
2945 assert!(axis.moving_positive);
2946 assert!(!axis.moving_negative);
2947 }
2948
2949 fn soft_homing_config() -> AxisConfig {
2952 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2953 cfg.homing_speed = 10.0;
2954 cfg.homing_accel = 20.0;
2955 cfg.homing_decel = 20.0;
2956 cfg
2957 }
2958
2959 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2960 use tokio::sync::mpsc;
2961 let (write_tx, write_rx) = mpsc::unbounded_channel();
2962 let (response_tx, response_rx) = mpsc::unbounded_channel();
2963 let client = CommandClient::new(write_tx, response_rx);
2964 let axis = Axis::new(soft_homing_config(), "TestDrive");
2965 (axis, client, response_tx, write_rx)
2966 }
2967
2968 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2970 view.set_state(0x0027); axis.tick(view, client);
2972 }
2973
2974 fn complete_soft_homing(
2979 axis: &mut Axis,
2980 view: &mut MockView,
2981 client: &mut CommandClient,
2982 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2983 trigger_pos: i32,
2984 clear_sensor: impl FnOnce(&mut MockView),
2985 ) {
2986 use mechutil::ipc::CommandMessage as IpcMsg;
2987
2988 axis.tick(view, client);
2991 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2992
2993 view.position_actual = trigger_pos + 100;
2995 axis.tick(view, client);
2996 view.position_actual = trigger_pos + 120;
2997 axis.tick(view, client);
2998 for _ in 0..10 { axis.tick(view, client); }
3000 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
3001
3002 view.status_word = 0x1027;
3004 axis.tick(view, client);
3005 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
3006 view.status_word = 0x0027;
3007
3008 for _ in 0..100 { axis.tick(view, client); }
3010 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
3011
3012 axis.tick(view, client);
3015 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
3016
3017 view.status_word = 0x1027;
3019 axis.tick(view, client);
3020 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
3021 view.status_word = 0x0027;
3022
3023 axis.tick(view, client);
3025 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
3026 clear_sensor(view);
3027 view.position_actual = trigger_pos - 200;
3028 axis.tick(view, client);
3029 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
3030
3031 axis.tick(view, client);
3033 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
3034 for _ in 0..10 { axis.tick(view, client); }
3035 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
3036
3037 view.status_word = 0x1027;
3040 axis.tick(view, client);
3041 view.status_word = 0x0027;
3042 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
3043
3044 let tid = axis.homing_sdo_tid;
3046 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
3047 client.poll();
3048 axis.tick(view, client);
3049 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
3050
3051 axis.tick(view, client);
3053 let tid = axis.homing_sdo_tid;
3054 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
3055 client.poll();
3056 axis.tick(view, client);
3057 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
3058
3059 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
3061 axis.tick(view, client);
3062 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
3063
3064 view.status_word = 0x1427; axis.tick(view, client);
3067 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
3068 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
3069 view.status_word = 0x0027;
3070
3071 axis.tick(view, client);
3073 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
3074
3075 view.status_word = 0x1027;
3077 axis.tick(view, client);
3078 view.status_word = 0x0027;
3079
3080 assert_eq!(axis.op, AxisOp::Idle);
3081 assert!(!axis.is_busy);
3082 assert!(!axis.is_error);
3083 assert_eq!(axis.home_offset, 0); }
3085
3086 #[test]
3087 fn soft_homing_pnp_home_sensor_full_sequence() {
3088 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3089 let mut view = MockView::new();
3090 enable_axis(&mut axis, &mut view, &mut client);
3091
3092 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3093
3094 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
3097 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3099 axis.tick(&mut view, &mut client); view.home_sensor = true;
3103 view.position_actual = 5000;
3104 axis.tick(&mut view, &mut client);
3105 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3106
3107 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3108 |v| { v.home_sensor = false; });
3109 }
3110
3111 #[test]
3112 fn soft_homing_npn_home_sensor_full_sequence() {
3113 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3114 let mut view = MockView::new();
3115 view.home_sensor = true;
3117 enable_axis(&mut axis, &mut view, &mut client);
3118
3119 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
3120
3121 axis.tick(&mut view, &mut client);
3123 view.status_word = 0x1027;
3124 axis.tick(&mut view, &mut client);
3125 view.status_word = 0x0027;
3126 axis.tick(&mut view, &mut client);
3127
3128 view.home_sensor = false;
3130 view.position_actual = 3000;
3131 axis.tick(&mut view, &mut client);
3132 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3133
3134 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
3135 |v| { v.home_sensor = true; }); }
3137
3138 #[test]
3139 fn soft_homing_limit_switch_suppresses_halt() {
3140 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3141 let mut view = MockView::new();
3142 enable_axis(&mut axis, &mut view, &mut client);
3143
3144 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
3146
3147 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3152 axis.tick(&mut view, &mut client); view.positive_limit = true;
3156 view.velocity_actual = 100; view.position_actual = 8000;
3158 axis.tick(&mut view, &mut client);
3159
3160 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3162 assert!(!axis.is_error);
3163 }
3164
3165 #[test]
3166 fn soft_homing_opposite_limit_still_protects() {
3167 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3168 let mut view = MockView::new();
3169 enable_axis(&mut axis, &mut view, &mut client);
3170
3171 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3173
3174 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3179 axis.tick(&mut view, &mut client); view.negative_limit = true;
3184 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
3186
3187 assert!(axis.is_error);
3189 assert!(axis.error_message.contains("Negative limit switch"));
3190 }
3191
3192 #[test]
3193 #[test]
3211 fn soft_homing_negative_direction_sets_negative_target() {
3212 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3213 let mut view = MockView::new();
3214 enable_axis(&mut axis, &mut view, &mut client);
3215
3216 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
3217 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
3221 }
3222
3223 #[test]
3224 fn home_integrated_method_starts_hardware_homing() {
3225 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3226 let mut view = MockView::new();
3227 enable_axis(&mut axis, &mut view, &mut client);
3228
3229 axis.home(&mut view, HomingMethod::CurrentPosition);
3230 assert!(matches!(axis.op, AxisOp::Homing(0)));
3231 assert_eq!(axis.homing_method, 37);
3232 }
3233
3234 #[test]
3235 fn home_integrated_arbitrary_code() {
3236 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3237 let mut view = MockView::new();
3238 enable_axis(&mut axis, &mut view, &mut client);
3239
3240 axis.home(&mut view, HomingMethod::Integrated(35));
3241 assert!(matches!(axis.op, AxisOp::Homing(0)));
3242 assert_eq!(axis.homing_method, 35);
3243 }
3244
3245 #[test]
3246 fn hardware_homing_skips_speed_sdos_when_zero() {
3247 use mechutil::ipc::CommandMessage;
3248
3249 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
3250 let mut view = MockView::new();
3251 enable_axis(&mut axis, &mut view, &mut client);
3252
3253 axis.home(&mut view, HomingMethod::Integrated(37));
3255
3256 axis.tick(&mut view, &mut client);
3258 assert!(matches!(axis.op, AxisOp::Homing(1)));
3259
3260 let _ = write_rx.try_recv();
3262
3263 let tid = axis.homing_sdo_tid;
3265 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3266 client.poll();
3267 axis.tick(&mut view, &mut client);
3268
3269 assert!(matches!(axis.op, AxisOp::Homing(8)));
3271 }
3272
3273 #[test]
3274 fn hardware_homing_writes_speed_sdos_when_nonzero() {
3275 use mechutil::ipc::CommandMessage;
3276
3277 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3278 let mut view = MockView::new();
3279 enable_axis(&mut axis, &mut view, &mut client);
3280
3281 axis.home(&mut view, HomingMethod::Integrated(37));
3283
3284 axis.tick(&mut view, &mut client);
3286 assert!(matches!(axis.op, AxisOp::Homing(1)));
3287 let _ = write_rx.try_recv();
3288
3289 let tid = axis.homing_sdo_tid;
3291 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3292 client.poll();
3293 axis.tick(&mut view, &mut client);
3294 assert!(matches!(axis.op, AxisOp::Homing(2)));
3296 }
3297
3298 #[test]
3299 fn soft_homing_edge_during_ack_step() {
3300 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3301 let mut view = MockView::new();
3302 enable_axis(&mut axis, &mut view, &mut client);
3303
3304 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3305 axis.tick(&mut view, &mut client); view.home_sensor = true;
3309 view.position_actual = 2000;
3310 axis.tick(&mut view, &mut client);
3311
3312 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3314 }
3315
3316 #[test]
3317 fn soft_homing_applies_home_position() {
3318 let mut cfg = soft_homing_config();
3319 cfg.home_position = 90.0;
3320
3321 use tokio::sync::mpsc;
3322 let (write_tx, _write_rx) = mpsc::unbounded_channel();
3323 let (resp_tx, response_rx) = mpsc::unbounded_channel();
3324 let mut client = CommandClient::new(write_tx, response_rx);
3325 let mut axis = Axis::new(cfg, "TestDrive");
3326
3327 let mut view = MockView::new();
3328 enable_axis(&mut axis, &mut view, &mut client);
3329
3330 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3331
3332 axis.tick(&mut view, &mut client);
3334 view.status_word = 0x1027;
3335 axis.tick(&mut view, &mut client);
3336 view.status_word = 0x0027;
3337 axis.tick(&mut view, &mut client);
3338
3339 view.home_sensor = true;
3341 view.position_actual = 5000;
3342 axis.tick(&mut view, &mut client);
3343 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3344
3345 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3347 |v| { v.home_sensor = false; });
3348
3349 assert_eq!(axis.home_offset, 0);
3351 }
3352
3353 #[test]
3354 fn soft_homing_default_home_position_zero() {
3355 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3356 let mut view = MockView::new();
3357 enable_axis(&mut axis, &mut view, &mut client);
3358
3359 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3360
3361 axis.tick(&mut view, &mut client);
3363 view.status_word = 0x1027;
3364 axis.tick(&mut view, &mut client);
3365 view.status_word = 0x0027;
3366 axis.tick(&mut view, &mut client);
3367
3368 view.home_sensor = true;
3370 view.position_actual = 5000;
3371 axis.tick(&mut view, &mut client);
3372
3373 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3374 |v| { v.home_sensor = false; });
3375
3376 assert_eq!(axis.home_offset, 0);
3377 }
3378}