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, !self.config().change_set_immediately_blocks_ack);
1537 cw.set_bit(6, false); view.set_control_word(cw.raw());
1539 }
1540
1541 pub fn command_clear_halt(&self, view: &mut impl AxisView) {
1544 let mut cw = RawControlWord(view.control_word());
1545 cw.set_bit(8, false); view.set_control_word(cw.raw());
1547 }
1548
1549
1550 pub fn command_cancel_move(&self, view: &mut impl AxisView) {
1558
1559 let mut cw = RawControlWord(view.control_word());
1560
1561 cw.set_bit(4, true); cw.set_bit(5, !self.config().change_set_immediately_blocks_ack);
1567 cw.set_bit(6, false); if self.config().halt_blocks_setpoint_ack {
1570 cw.set_bit(8, false); }
1577
1578 view.set_control_word(cw.raw());
1579
1580 let current_pos = view.position_actual();
1581 view.set_target_position(current_pos);
1582 view.set_profile_velocity(0);
1583 }
1584
1585
1586 fn command_homing_speed(&self, view: &mut impl AxisView) {
1588 let cpu = self.config.counts_per_user();
1589 let vel = (self.config.homing_speed * cpu).round() as u32;
1590 let accel = (self.config.homing_accel * cpu).round() as u32;
1591 let decel = (self.config.homing_decel * cpu).round() as u32;
1592 view.set_profile_velocity(vel);
1593 view.set_profile_acceleration(accel);
1594 view.set_profile_deceleration(decel);
1595 }
1596
1597 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1613 match HomeState::from_repr(step) {
1614
1615 Some(HomeState::EnsurePpMode) => {
1616 log::info!("SoftHome: Ensuring PP mode..");
1621 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1622 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1623 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1624 },
1625 Some(HomeState::WaitPpMode) => {
1626
1627 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1628 if !self.fb_mode_of_operation.is_busy() {
1629 if self.fb_mode_of_operation.is_error() {
1630 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1631 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1632 ));
1633 }
1634 else {
1635 log::info!("SoftHome: Drive is in PP mode!");
1636
1637 if !self.check_soft_home_trigger(view) {
1641 log::info!("SoftHome: Not on home switch; seek out.");
1642 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1643 } else {
1644 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1645 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1646 }
1647 }
1648 }
1649
1650
1651 },
1652
1653 Some(HomeState::Search) => {
1655 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1656
1657 let target = self.calculate_max_relative_target(self.soft_home_direction);
1667 view.set_target_position(target);
1668
1669 self.command_homing_speed(view);
1678
1679 let mut cw = RawControlWord(view.control_word());
1680 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());
1685
1686 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1687 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1688 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1689 }
1690 Some(HomeState::WaitSearching) => {
1691 if self.check_soft_home_trigger(view) {
1692 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1693 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1694 return;
1695 }
1696 let sw = RawStatusWord(view.status_word());
1697 if sw.raw() & (1 << 12) != 0 {
1698 let mut cw = RawControlWord(view.control_word());
1699 cw.set_bit(4, false);
1700 view.set_control_word(cw.raw());
1701 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1702 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1703 } else if self.homing_timed_out() {
1704 self.set_op_error("Software homing timeout: set-point not acknowledged");
1705 }
1706 }
1707 Some(HomeState::WaitFoundSensor) => {
1717 if self.check_soft_home_trigger(view) {
1718 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1719 log::info!("ControlWord is : {} ", view.control_word());
1720
1721 let mut cw = RawControlWord(view.control_word());
1722 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1725
1726
1727 self.halt_stable_count = 0;
1728 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1729 } else if self.homing_timed_out() {
1730 self.set_op_error("Software homing timeout: sensor not detected");
1731 }
1732 }
1733
1734
1735 Some(HomeState::WaitStoppedFoundSensor) => {
1736 const STABLE_WINDOW: i32 = 1;
1737 const STABLE_TICKS_REQUIRED: u8 = 10;
1738
1739 let pos = view.position_actual();
1744 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1745 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1746 } else {
1747 self.halt_stable_count = 0;
1748 }
1749
1750 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1751
1752 log::info!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1753 self.command_cancel_move(view);
1754 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1755
1756 } else if self.homing_timed_out() {
1757 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1758 }
1759 }
1760 Some(HomeState::WaitFoundSensorAck) => {
1761 let sw = RawStatusWord(view.status_word());
1762 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1763
1764 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1765
1766 let mut cw = RawControlWord(view.control_word());
1768 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1771
1772 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1773
1774 } else if self.homing_timed_out() {
1775 self.set_op_error("Software homing timeout: cancel not acknowledged");
1776 }
1777 },
1778 Some(HomeState::WaitFoundSensorAckClear) => {
1779 let sw = RawStatusWord(view.status_word());
1780 if sw.raw() & (1 << 12) == 0 {
1782
1783 self.command_clear_halt(view);
1785
1786 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1787 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1788 self.ton.call(false, Duration::from_secs(3));
1789 }
1790 },
1791 Some(HomeState::DebounceFoundSensor) => {
1793 self.ton.call(true, Duration::from_secs(3));
1794
1795 let sw = RawStatusWord(view.status_word());
1796 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1797 self.ton.call(false, Duration::from_secs(3));
1798 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1799 view.position_actual(), view.control_word(), view.status_word());
1800 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1801 }
1802 }
1803
1804 Some(HomeState::BackOff) => {
1806
1807 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1808 view.set_target_position(target);
1809
1810
1811 self.command_homing_speed(view);
1812
1813 let mut cw = RawControlWord(view.control_word());
1814 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1818 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1819 target, self.config.homing_speed, view.position_actual(), cw.raw());
1820 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1821 }
1822 Some(HomeState::WaitBackingOff) => {
1823 let sw = RawStatusWord(view.status_word());
1824 if sw.raw() & (1 << 12) != 0 {
1825 let mut cw = RawControlWord(view.control_word());
1826 cw.set_bit(4, false);
1827 view.set_control_word(cw.raw());
1828 log::info!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1829 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1830 } else if self.homing_timed_out() {
1831 self.set_op_error("Software homing timeout: back-off not acknowledged");
1832 }
1833 }
1834 Some(HomeState::WaitLostSensor) => {
1835 if !self.check_soft_home_trigger(view) {
1836 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1837
1838 self.command_halt(view);
1839 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1840 } else if self.homing_timed_out() {
1841 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1842 }
1843 }
1844 Some(HomeState::WaitStoppedLostSensor) => {
1845 const STABLE_WINDOW: i32 = 1;
1846 const STABLE_TICKS_REQUIRED: u8 = 10;
1847
1848 let pos = view.position_actual();
1853 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1854 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1855 } else {
1856 self.halt_stable_count = 0;
1857 }
1858
1859 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1860 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1861 self.command_cancel_move(view);
1862 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1863 } else if self.homing_timed_out() {
1864 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1865 }
1866 }
1867 Some(HomeState::WaitLostSensorAck) => {
1868 let sw = RawStatusWord(view.status_word());
1869 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1870
1871 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1872
1873 let mut cw = RawControlWord(view.control_word());
1875 cw.set_bit(4, false); view.set_control_word(cw.raw());
1877
1878 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1879
1880
1881 } else if self.homing_timed_out() {
1882 self.set_op_error("Software homing timeout: cancel not acknowledged");
1883 }
1884 }
1885 Some(HomeState::WaitLostSensorAckClear) => {
1886 let sw = RawStatusWord(view.status_word());
1888 if sw.raw() & (1 << 12) == 0 {
1889
1890 self.command_clear_halt(view);
1891
1892 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1893 self.homing_sdo_tid = self.sdo.write(
1896 client, 0x607C, 0, json!(desired_counts),
1897 );
1898
1899 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1900 self.config.home_position, desired_counts
1901 );
1902
1903 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1904
1905 }
1906 },
1907
1908 Some(HomeState::WaitHomeOffsetDone) => {
1909 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1911 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1912 SdoResult::Pending => {
1913 if self.homing_timed_out() {
1914 self.set_op_error("Software homing timeout: home offset SDO write");
1915 }
1916 }
1917 SdoResult::Err(e) => {
1918 self.set_op_error(&format!("Software homing SDO error: {}", e));
1919 }
1920 SdoResult::Timeout => {
1921 self.set_op_error("Software homing: home offset SDO timed out");
1922 }
1923 }
1924 },
1925 Some(HomeState::WriteHomingModeOp) => {
1926
1927 self.fb_mode_of_operation.reset();
1931 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1932 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1933 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1934
1935
1936 },
1937 Some(HomeState::WaitWriteHomingModeOp) => {
1938 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1940
1941 if !self.fb_mode_of_operation.is_busy() {
1942 if self.fb_mode_of_operation.is_error() {
1943 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1944 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1945 ));
1946 }
1947 else {
1948 log::info!("SoftHome: Drive is now in Homing Mode.");
1949 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1950 }
1951 }
1952 },
1953 Some(HomeState::WriteHomingMethod) => {
1954 self.homing_sdo_tid = self.sdo.write(
1957 client, 0x6098, 0, json!(self.config.soft_home_method),
1958 );
1959 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1960 }
1961 Some(HomeState::WaitWriteHomingMethodDone) => {
1962 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1964 SdoResult::Ok(_) => {
1965 log::info!("SoftHome: Successfully wrote homing method.");
1966 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1967 }
1968 SdoResult::Pending => {
1969 if self.homing_timed_out() {
1970 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1971 }
1972 }
1973 SdoResult::Err(e) => {
1974 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1975 }
1976 SdoResult::Timeout => {
1977 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1978 }
1979 }
1980 }
1981 Some(HomeState::ClearHomingTrigger) => {
1982 let mut cw = RawControlWord(view.control_word());
1985 cw.set_bit(4, false);
1986 view.set_control_word(cw.raw());
1987 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1988 }
1989 Some(HomeState::TriggerHoming) => {
1990 let mut cw = RawControlWord(view.control_word());
1992 cw.set_bit(4, true);
1993 view.set_control_word(cw.raw());
1994 log::info!("SoftHome[TriggerHoming]: start homing");
1995 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1996 }
1997 Some(HomeState::WaitHomingStarted) => {
1998 let sw = view.status_word();
2004 let error = sw & (1 << 13) != 0;
2005 if error {
2006 self.restore_pp_after_error("Software homing: drive reported homing error");
2007 } else if sw & (1 << 12) == 0 {
2008 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
2009 } else if self.homing_timed_out() {
2010 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
2011 }
2012 }
2013 Some(HomeState::WaitHomingDone) => {
2014 let sw = view.status_word();
2016 let error = sw & (1 << 13) != 0;
2017 let attained = sw & (1 << 12) != 0;
2018 let reached = sw & (1 << 10) != 0;
2019
2020 if error {
2021 self.restore_pp_after_error("Software homing: drive reported homing error");
2022 } else if attained && reached {
2023 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
2024 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
2025 } else if self.homing_timed_out() {
2026 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
2027 }
2028 }
2029 Some(HomeState::ResetHomingTrigger) => {
2030 let mut cw = RawControlWord(view.control_word());
2035 cw.set_bit(4, false);
2036 view.set_control_word(cw.raw());
2037 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
2038 }
2039 Some(HomeState::WaitHomingTriggerCleared) => {
2040 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
2044 }
2045
2046
2047 Some(HomeState::WriteMotionModeOfOperation) => {
2048
2049 self.fb_mode_of_operation.reset();
2052 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
2053 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2054 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
2055
2056 },
2057 Some(HomeState::WaitWriteMotionModeOfOperation) => {
2058 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2060
2061 if !self.fb_mode_of_operation.is_busy() {
2062 if self.fb_mode_of_operation.is_error() {
2063 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
2064 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
2065 ));
2066 }
2067 else {
2068 if self.is_error {
2069 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
2070 self.finish_op_error();
2071 }
2072 else {
2073 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
2076 }
2077
2078 }
2079 }
2080 },
2081
2082 Some(HomeState::SendCurrentPositionTarget) => {
2083 let current_pos = view.position_actual();
2085 view.set_target_position(current_pos);
2086 view.set_profile_velocity(0);
2087 let mut cw = RawControlWord(view.control_word());
2088 cw.set_bit(4, true);
2089 cw.set_bit(5, true);
2090 cw.set_bit(6, false); view.set_control_word(cw.raw());
2092 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
2093 }
2094 Some(HomeState::WaitCurrentPositionTargetSent) => {
2095 let sw = RawStatusWord(view.status_word());
2097 if sw.raw() & (1 << 12) != 0 {
2098 let mut cw = RawControlWord(view.control_word());
2099 cw.set_bit(4, false);
2100 view.set_control_word(cw.raw());
2101 log::info!("Software homing complete — position set to {} user units",
2102 self.config.home_position);
2103 self.complete_op();
2104 } else if self.homing_timed_out() {
2105 self.set_op_error("Software homing timeout: hold position not acknowledged");
2106 }
2107 }
2108 _ => self.complete_op(),
2109 }
2110 }
2111
2112 fn tick_halting(&mut self, view: &mut impl AxisView, step: u8) {
2125 match HaltState::from_repr(step) {
2126 Some(HaltState::WaitStopped) => {
2127 let pos = view.position_actual();
2131 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
2132
2133 let vel = view.velocity_actual().abs();
2134 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
2135
2136 if pos_stable || vel_stopped {
2141 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2142 } else {
2143 self.halt_stable_count = 0;
2144 }
2145
2146 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
2147 self.command_cancel_move(view);
2148 self.op_started = Some(Instant::now());
2149 self.op = AxisOp::Halting(HaltState::WaitCancelAck as u8);
2150 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2151 self.set_op_error("Halt timeout: motor did not stop");
2152 }
2153 }
2154 Some(HaltState::WaitCancelAck) => {
2155 let sw = RawStatusWord(view.status_word());
2156 let setpoint_ack = sw.raw() & (1 << 12) != 0;
2157 if setpoint_ack {
2159 let mut cw = RawControlWord(view.control_word());
2165 cw.set_bit(4, false);
2166 cw.set_bit(5, !self.config().change_set_immediately_blocks_ack);
2167 view.set_control_word(cw.raw());
2168 self.op_started = Some(Instant::now());
2169 self.op = AxisOp::Halting(HaltState::WaitCancelAckClear as u8);
2170 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2171 self.set_op_error("Halt timeout: cancel not acknowledged");
2172 }
2173 }
2174 Some(HaltState::WaitCancelAckClear) => {
2175 let sw = RawStatusWord(view.status_word());
2176 if sw.raw() & (1 << 12) == 0 {
2177 self.command_clear_halt(view);
2179 self.complete_op();
2180 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2181 self.set_op_error("Halt timeout: ack did not clear");
2182 }
2183 }
2184 None => {
2185 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
2186 self.complete_op();
2187 }
2188 }
2189 }
2190
2191 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
2196 match step {
2197 1 => {
2198 let mut cw = RawControlWord(view.control_word());
2200 cw.cmd_fault_reset();
2201 view.set_control_word(cw.raw());
2202 self.op = AxisOp::FaultRecovery(2);
2203 }
2204 2 => {
2205 let sw = RawStatusWord(view.status_word());
2207 let state = sw.state();
2208 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
2209 log::info!("Fault cleared (drive state: {})", state);
2210 self.complete_op();
2211 } else if self.op_timed_out() {
2212 self.set_op_error("Fault reset timeout: drive still faulted");
2213 }
2214 }
2215 _ => self.complete_op(),
2216 }
2217 }
2218}
2219
2220#[cfg(test)]
2225mod tests {
2226 use super::*;
2227
2228 struct MockView {
2230 control_word: u16,
2231 status_word: u16,
2232 target_position: i32,
2233 profile_velocity: u32,
2234 profile_acceleration: u32,
2235 profile_deceleration: u32,
2236 modes_of_operation: i8,
2237 modes_of_operation_display: i8,
2238 position_actual: i32,
2239 velocity_actual: i32,
2240 error_code: u16,
2241 positive_limit: bool,
2242 negative_limit: bool,
2243 home_sensor: bool,
2244 }
2245
2246 impl MockView {
2247 fn new() -> Self {
2248 Self {
2249 control_word: 0,
2250 status_word: 0x0040, target_position: 0,
2252 profile_velocity: 0,
2253 profile_acceleration: 0,
2254 profile_deceleration: 0,
2255 modes_of_operation: 0,
2256 modes_of_operation_display: 1, position_actual: 0,
2258 velocity_actual: 0,
2259 error_code: 0,
2260 positive_limit: false,
2261 negative_limit: false,
2262 home_sensor: false,
2263 }
2264 }
2265
2266 fn set_state(&mut self, state: u16) {
2267 self.status_word = state;
2268 }
2269 }
2270
2271 impl AxisView for MockView {
2272 fn control_word(&self) -> u16 { self.control_word }
2273 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
2274 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
2275 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
2276 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
2277 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
2278 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
2279 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
2280 fn status_word(&self) -> u16 { self.status_word }
2281 fn position_actual(&self) -> i32 { self.position_actual }
2282 fn velocity_actual(&self) -> i32 { self.velocity_actual }
2283 fn error_code(&self) -> u16 { self.error_code }
2284 fn positive_limit_active(&self) -> bool { self.positive_limit }
2285 fn negative_limit_active(&self) -> bool { self.negative_limit }
2286 fn home_sensor_active(&self) -> bool { self.home_sensor }
2287 }
2288
2289 fn test_config() -> AxisConfig {
2290 AxisConfig::new(12_800).with_user_scale(360.0)
2291 }
2292
2293 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2295 use tokio::sync::mpsc;
2296 let (write_tx, write_rx) = mpsc::unbounded_channel();
2297 let (response_tx, response_rx) = mpsc::unbounded_channel();
2298 let client = CommandClient::new(write_tx, response_rx);
2299 let axis = Axis::new(test_config(), "TestDrive");
2300 (axis, client, response_tx, write_rx)
2301 }
2302
2303 #[test]
2304 fn axis_config_conversion() {
2305 let cfg = test_config();
2306 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2308 }
2309
2310 #[test]
2311 fn trace_records_on_change_not_every_tick() {
2312 let (mut axis, mut client, _r, _w) = test_axis();
2313 let mut view = MockView::new();
2314 view.set_state(0x0040); for _ in 0..10 {
2319 axis.tick(&mut view, &mut client);
2320 }
2321 let after_idle = axis.trace().lines().count();
2322 assert!(after_idle <= 1, "constant state must record ≤1 line, got {}", after_idle);
2323
2324 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2327 assert!(axis.trace().lines().count() > after_idle, "a SW change adds a line");
2328 assert!(axis.trace().contains("Operation Enabled"), "trace decodes the SW state");
2329 }
2330
2331 #[test]
2332 fn change_set_immediately_trait_shapes_cancel_control_word() {
2333 let axis_def = Axis::new(test_config(), "Teknic");
2336 let mut view = MockView::new();
2337 view.control_word = 0x012f; axis_def.command_cancel_move(&mut view);
2339 assert!(view.control_word & (1 << 5) != 0, "default keeps CSI (bit 5)");
2340 assert!(view.control_word & (1 << 4) != 0, "new set-point asserted");
2341
2342 let mut cfg = test_config();
2346 cfg.change_set_immediately_blocks_ack = true;
2347 cfg.halt_blocks_setpoint_ack = true;
2348 let axis_akd = Axis::new(cfg, "AKD");
2349 let mut v = MockView::new();
2350 v.control_word = 0x012f;
2351 axis_akd.command_cancel_move(&mut v);
2352 assert_eq!(v.control_word & 0x01ff, 0x001f, "cancel CW == normal-move 0x001f");
2353 }
2354
2355 #[test]
2356 fn trace_decoders_render_handshake_bits() {
2357 assert_eq!(decode_cw(0x003F), "SwOn|EnVolt|QStop|EnOp|NewSP|ChgSet");
2359 assert!(decode_cw(0x013F).contains("HALT"), "0x13F still asserts HALT");
2360 let s = decode_sw(0x1027);
2362 assert!(s.contains("Operation Enabled") && s.contains("SP_ACK"), "got {s}");
2363 }
2364
2365 #[test]
2366 fn enable_sequence_sets_pp_mode_and_shutdown() {
2367 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2368 let mut view = MockView::new();
2369
2370 axis.enable(&mut view);
2371
2372 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
2374 assert_eq!(view.control_word & 0x008F, 0x0006);
2376 assert_eq!(axis.op, AxisOp::Enabling(1));
2378
2379 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2382
2383 assert_eq!(view.control_word & 0x008F, 0x000F);
2385 assert_eq!(axis.op, AxisOp::Enabling(2));
2386
2387 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2390
2391 assert_eq!(axis.op, AxisOp::Idle);
2393 assert!(axis.motor_on);
2394 }
2395
2396 #[test]
2397 fn move_absolute_sets_target() {
2398 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2399 let mut view = MockView::new();
2400 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2405
2406 assert_eq!(view.target_position, 1600);
2408 assert_eq!(view.profile_velocity, 3200);
2410 assert_eq!(view.profile_acceleration, 6400);
2412 assert_eq!(view.profile_deceleration, 6400);
2413 assert!(view.control_word & (1 << 4) != 0);
2415 assert!(view.control_word & (1 << 6) == 0);
2417 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2419 }
2420
2421 #[test]
2422 fn move_relative_sets_relative_bit() {
2423 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2424 let mut view = MockView::new();
2425 view.set_state(0x0027);
2426 axis.tick(&mut view, &mut client);
2427
2428 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2429
2430 assert!(view.control_word & (1 << 6) != 0);
2432 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1, _, _)));
2433 }
2434
2435 #[test]
2436 fn move_completes_on_target_reached() {
2437 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2438 let mut view = MockView::new();
2439 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2441
2442 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2443
2444 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2447 assert!(view.control_word & (1 << 4) == 0);
2449
2450 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2453 assert_eq!(axis.op, AxisOp::Idle);
2455 assert!(!axis.in_motion);
2456 }
2457
2458 #[test]
2459 fn fault_detected_sets_error() {
2460 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2461 let mut view = MockView::new();
2462 view.set_state(0x0008); view.error_code = 0x1234;
2464
2465 axis.tick(&mut view, &mut client);
2466
2467 assert!(axis.is_error);
2468 assert_eq!(axis.error_code, 0x1234);
2469 assert!(axis.error_message.contains("fault"));
2470 }
2471
2472 #[test]
2473 fn fault_recovery_sequence() {
2474 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2475 let mut view = MockView::new();
2476 view.set_state(0x0008); axis.reset_faults(&mut view);
2479 assert!(view.control_word & 0x0080 == 0);
2481
2482 axis.tick(&mut view, &mut client);
2484 assert!(view.control_word & 0x0080 != 0);
2485
2486 view.set_state(0x0040);
2488 axis.tick(&mut view, &mut client);
2489 assert_eq!(axis.op, AxisOp::Idle);
2490 assert!(!axis.is_error);
2491 }
2492
2493 #[test]
2494 fn reset_faults_software_only_is_synchronous() {
2495 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2501 let mut view = MockView::new();
2502 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2504
2505 axis.set_software_min_limit(207.0);
2507 axis.move_absolute(&mut view, 200.0, 90.0, 180.0, 180.0);
2508 assert!(axis.is_error, "move past software limit must set is_error");
2509 assert!(!axis.is_busy);
2510
2511 let cw_before = view.control_word;
2514
2515 axis.reset_faults(&mut view);
2516
2517 assert!(!axis.is_error);
2519 assert_eq!(axis.error_code, 0);
2520 assert!(axis.error_message.is_empty());
2521 assert!(!axis.is_busy);
2522 assert_eq!(axis.op, AxisOp::Idle);
2523 assert_eq!(view.control_word, cw_before);
2525 }
2526
2527 #[test]
2528 fn disable_sequence() {
2529 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2530 let mut view = MockView::new();
2531 view.set_state(0x0027); axis.disable(&mut view);
2534 assert_eq!(view.control_word & 0x008F, 0x0006);
2536
2537 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2540 assert_eq!(axis.op, AxisOp::Idle);
2541 }
2542
2543 #[test]
2544 fn position_tracks_with_home_offset() {
2545 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2546 let mut view = MockView::new();
2547 view.set_state(0x0027);
2548 view.position_actual = 5000;
2549
2550 axis.enable(&mut view);
2552 view.set_state(0x0021);
2553 axis.tick(&mut view, &mut client);
2554 view.set_state(0x0027);
2555 axis.tick(&mut view, &mut client);
2556
2557 assert_eq!(axis.home_offset, 5000);
2559
2560 assert!((axis.position - 0.0).abs() < 0.01);
2562
2563 view.position_actual = 6600;
2565 axis.tick(&mut view, &mut client);
2566
2567 assert!((axis.position - 45.0).abs() < 0.1);
2569 }
2570
2571 #[test]
2572 fn set_position_adjusts_home_offset() {
2573 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2574 let mut view = MockView::new();
2575 view.position_actual = 3200;
2576
2577 axis.set_position(&view, 90.0);
2578 axis.tick(&mut view, &mut client);
2579
2580 assert_eq!(axis.home_offset, 0);
2582 assert!((axis.position - 90.0).abs() < 0.01);
2583 }
2584
2585 #[test]
2586 fn halt_runs_multi_stage_close_out() {
2587 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2588 let mut view = MockView::new();
2589 view.set_state(0x0027);
2590
2591 axis.halt(&mut view);
2592
2593 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2595 assert!(view.control_word & (1 << 4) == 0, "new_setpoint must be cleared");
2596
2597 assert!(matches!(axis.op, AxisOp::Halting(_)),
2599 "halt should enter Halting state, not Idle");
2600 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2601 assert_eq!(step, HaltState::WaitStopped as u8);
2602
2603 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2610 axis.tick(&mut view, &mut client);
2611 }
2612 assert!(matches!(axis.op, AxisOp::Halting(_)));
2614 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2615 assert_eq!(step, HaltState::WaitCancelAck as u8,
2616 "should advance past WaitStopped once position/velocity is stable");
2617
2618 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2622 }
2623
2624 #[test]
2625 fn is_busy_tracks_operations() {
2626 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2627 let mut view = MockView::new();
2628
2629 axis.tick(&mut view, &mut client);
2631 assert!(!axis.is_busy);
2632
2633 axis.enable(&mut view);
2635 axis.tick(&mut view, &mut client);
2636 assert!(axis.is_busy);
2637
2638 view.set_state(0x0021);
2640 axis.tick(&mut view, &mut client);
2641 view.set_state(0x0027);
2642 axis.tick(&mut view, &mut client);
2643 assert!(!axis.is_busy);
2644
2645 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2647 axis.tick(&mut view, &mut client);
2648 assert!(axis.is_busy);
2649 assert!(axis.in_motion);
2650 }
2651
2652 #[test]
2653 fn fault_during_move_cancels_op() {
2654 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2655 let mut view = MockView::new();
2656 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2658
2659 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2661 axis.tick(&mut view, &mut client);
2662 assert!(axis.is_busy);
2663 assert!(!axis.is_error);
2664
2665 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2668
2669 assert!(!axis.is_busy);
2671 assert!(axis.is_error);
2672 assert_eq!(axis.op, AxisOp::Idle);
2673 }
2674
2675 #[test]
2676 fn move_absolute_rejected_by_max_limit() {
2677 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2678 let mut view = MockView::new();
2679 view.set_state(0x0027);
2680 axis.tick(&mut view, &mut client);
2681
2682 axis.set_software_max_limit(90.0);
2683 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2684
2685 assert!(axis.is_error);
2687 assert_eq!(axis.op, AxisOp::Idle);
2688 assert!(axis.error_message.contains("max software limit"));
2689 }
2690
2691 #[test]
2692 fn move_absolute_rejected_by_min_limit() {
2693 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2694 let mut view = MockView::new();
2695 view.set_state(0x0027);
2696 axis.tick(&mut view, &mut client);
2697
2698 axis.set_software_min_limit(-10.0);
2699 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2700
2701 assert!(axis.is_error);
2702 assert_eq!(axis.op, AxisOp::Idle);
2703 assert!(axis.error_message.contains("min software limit"));
2704 }
2705
2706 #[test]
2707 fn move_relative_rejected_by_max_limit() {
2708 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2709 let mut view = MockView::new();
2710 view.set_state(0x0027);
2711 axis.tick(&mut view, &mut client);
2712
2713 axis.set_software_max_limit(50.0);
2715 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2716
2717 assert!(axis.is_error);
2718 assert_eq!(axis.op, AxisOp::Idle);
2719 assert!(axis.error_message.contains("max software limit"));
2720 }
2721
2722 #[test]
2723 fn move_within_limits_allowed() {
2724 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2725 let mut view = MockView::new();
2726 view.set_state(0x0027);
2727 axis.tick(&mut view, &mut client);
2728
2729 axis.set_software_max_limit(90.0);
2730 axis.set_software_min_limit(-90.0);
2731 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2732
2733 assert!(!axis.is_error);
2735 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2736 }
2737
2738 #[test]
2739 fn runtime_limit_halts_move_in_violated_direction() {
2740 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2741 let mut view = MockView::new();
2742 view.set_state(0x0027);
2743 axis.tick(&mut view, &mut client);
2744
2745 axis.set_software_max_limit(45.0);
2746 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2748
2749 view.position_actual = 1650;
2752 view.velocity_actual = 100; view.status_word = 0x1027;
2756 axis.tick(&mut view, &mut client);
2757 view.status_word = 0x0027;
2758 axis.tick(&mut view, &mut client);
2759
2760 assert!(!axis.is_error);
2764 assert!(axis.at_max_limit);
2765 assert!(axis.is_busy);
2766 assert!(matches!(axis.op, AxisOp::Halting(_)));
2767 assert!(view.control_word & (1 << 8) != 0);
2769 }
2770
2771 #[test]
2772 fn runtime_limit_allows_move_in_opposite_direction() {
2773 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2774 let mut view = MockView::new();
2775 view.set_state(0x0027);
2776 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2779 axis.tick(&mut view, &mut client);
2780 assert!(axis.at_max_limit);
2781
2782 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2784 assert!(!axis.is_error);
2785 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2786
2787 view.velocity_actual = -100;
2789 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2791 assert!(!axis.is_error);
2793 }
2794
2795 #[test]
2796 fn positive_limit_switch_halts_positive_move() {
2797 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2798 let mut view = MockView::new();
2799 view.set_state(0x0027);
2800 axis.tick(&mut view, &mut client);
2801
2802 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2804 view.velocity_actual = 100; view.status_word = 0x1027;
2807 axis.tick(&mut view, &mut client);
2808 view.status_word = 0x0027;
2809
2810 view.positive_limit = true;
2812 axis.tick(&mut view, &mut client);
2813
2814 assert!(!axis.is_error);
2819 assert!(axis.at_positive_limit_switch);
2820 assert!(axis.is_busy);
2821 assert!(matches!(axis.op, AxisOp::Halting(_)));
2822 assert!(view.control_word & (1 << 8) != 0);
2824 }
2825
2826 #[test]
2827 fn negative_limit_switch_halts_negative_move() {
2828 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2829 let mut view = MockView::new();
2830 view.set_state(0x0027);
2831 axis.tick(&mut view, &mut client);
2832
2833 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2835 view.velocity_actual = -100; view.status_word = 0x1027;
2837 axis.tick(&mut view, &mut client);
2838 view.status_word = 0x0027;
2839
2840 view.negative_limit = true;
2842 axis.tick(&mut view, &mut client);
2843
2844 assert!(!axis.is_error);
2849 assert!(axis.at_negative_limit_switch);
2850 assert!(axis.is_busy);
2851 assert!(matches!(axis.op, AxisOp::Halting(_)));
2852 assert!(view.control_word & (1 << 8) != 0);
2853 }
2854
2855 #[test]
2856 fn limit_halt_cleanup_then_next_move_starts_cleanly() {
2857 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2862 let mut view = MockView::new();
2863 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2865
2866 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2868 view.velocity_actual = 100;
2869 view.status_word = 0x1027;
2870 axis.tick(&mut view, &mut client);
2871 view.status_word = 0x0027;
2872 view.positive_limit = true;
2873 axis.tick(&mut view, &mut client);
2874
2875 assert!(!axis.is_error);
2877 assert!(axis.is_busy);
2878 assert!(matches!(axis.op, AxisOp::Halting(_)));
2879 assert!(view.control_word & (1 << 8) != 0);
2880
2881 view.velocity_actual = 0;
2886 view.positive_limit = false;
2887 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2888 axis.tick(&mut view, &mut client);
2889 }
2890 assert!(matches!(axis.op, AxisOp::Halting(_)));
2891 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2892 assert_eq!(step, HaltState::WaitCancelAck as u8);
2893
2894 view.status_word = 0x1027;
2896 axis.tick(&mut view, &mut client);
2897 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2898 assert_eq!(step, HaltState::WaitCancelAckClear as u8);
2899
2900 view.status_word = 0x0027;
2903 axis.tick(&mut view, &mut client);
2904 assert_eq!(axis.op, AxisOp::Idle);
2905 assert!(!axis.is_busy);
2906 assert!(view.control_word & (1 << 8) == 0, "halt should be cleared after close-out");
2907
2908 axis.move_absolute(&mut view, 30.0, 90.0, 180.0, 180.0);
2911 assert!(view.control_word & (1 << 4) != 0, "new-setpoint asserted on start_move tick");
2912 assert!(view.control_word & (1 << 8) == 0, "halt stays cleared");
2913 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2914 }
2915
2916 #[test]
2917 fn limit_switch_allows_move_in_opposite_direction() {
2918 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2919 let mut view = MockView::new();
2920 view.set_state(0x0027);
2921 view.positive_limit = true;
2923 view.velocity_actual = -100;
2924 axis.tick(&mut view, &mut client);
2925 assert!(axis.at_positive_limit_switch);
2926
2927 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2929 view.status_word = 0x1027;
2930 axis.tick(&mut view, &mut client);
2931
2932 assert!(!axis.is_error);
2934 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
2935 }
2936
2937 #[test]
2938 fn limit_switch_ignored_when_not_moving() {
2939 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2940 let mut view = MockView::new();
2941 view.set_state(0x0027);
2942 view.positive_limit = true;
2943
2944 axis.tick(&mut view, &mut client);
2945
2946 assert!(axis.at_positive_limit_switch);
2948 assert!(!axis.is_error);
2949 }
2950
2951 #[test]
2952 fn home_sensor_output_tracks_view() {
2953 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2954 let mut view = MockView::new();
2955 view.set_state(0x0027);
2956
2957 axis.tick(&mut view, &mut client);
2958 assert!(!axis.home_sensor);
2959
2960 view.home_sensor = true;
2961 axis.tick(&mut view, &mut client);
2962 assert!(axis.home_sensor);
2963
2964 view.home_sensor = false;
2965 axis.tick(&mut view, &mut client);
2966 assert!(!axis.home_sensor);
2967 }
2968
2969 #[test]
2970 fn velocity_output_converted() {
2971 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2972 let mut view = MockView::new();
2973 view.set_state(0x0027);
2974 view.velocity_actual = 3200;
2976
2977 axis.tick(&mut view, &mut client);
2978
2979 assert!((axis.speed - 90.0).abs() < 0.1);
2980 assert!(axis.moving_positive);
2981 assert!(!axis.moving_negative);
2982 }
2983
2984 fn soft_homing_config() -> AxisConfig {
2987 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2988 cfg.homing_speed = 10.0;
2989 cfg.homing_accel = 20.0;
2990 cfg.homing_decel = 20.0;
2991 cfg
2992 }
2993
2994 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2995 use tokio::sync::mpsc;
2996 let (write_tx, write_rx) = mpsc::unbounded_channel();
2997 let (response_tx, response_rx) = mpsc::unbounded_channel();
2998 let client = CommandClient::new(write_tx, response_rx);
2999 let axis = Axis::new(soft_homing_config(), "TestDrive");
3000 (axis, client, response_tx, write_rx)
3001 }
3002
3003 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
3005 view.set_state(0x0027); axis.tick(view, client);
3007 }
3008
3009 fn complete_soft_homing(
3014 axis: &mut Axis,
3015 view: &mut MockView,
3016 client: &mut CommandClient,
3017 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
3018 trigger_pos: i32,
3019 clear_sensor: impl FnOnce(&mut MockView),
3020 ) {
3021 use mechutil::ipc::CommandMessage as IpcMsg;
3022
3023 axis.tick(view, client);
3026 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
3027
3028 view.position_actual = trigger_pos + 100;
3030 axis.tick(view, client);
3031 view.position_actual = trigger_pos + 120;
3032 axis.tick(view, client);
3033 for _ in 0..10 { axis.tick(view, client); }
3035 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
3036
3037 view.status_word = 0x1027;
3039 axis.tick(view, client);
3040 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
3041 view.status_word = 0x0027;
3042
3043 for _ in 0..100 { axis.tick(view, client); }
3045 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
3046
3047 axis.tick(view, client);
3050 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
3051
3052 view.status_word = 0x1027;
3054 axis.tick(view, client);
3055 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
3056 view.status_word = 0x0027;
3057
3058 axis.tick(view, client);
3060 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
3061 clear_sensor(view);
3062 view.position_actual = trigger_pos - 200;
3063 axis.tick(view, client);
3064 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
3065
3066 axis.tick(view, client);
3068 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
3069 for _ in 0..10 { axis.tick(view, client); }
3070 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
3071
3072 view.status_word = 0x1027;
3075 axis.tick(view, client);
3076 view.status_word = 0x0027;
3077 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
3078
3079 let tid = axis.homing_sdo_tid;
3081 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
3082 client.poll();
3083 axis.tick(view, client);
3084 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
3085
3086 axis.tick(view, client);
3088 let tid = axis.homing_sdo_tid;
3089 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
3090 client.poll();
3091 axis.tick(view, client);
3092 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
3093
3094 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
3096 axis.tick(view, client);
3097 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
3098
3099 view.status_word = 0x1427; axis.tick(view, client);
3102 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
3103 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
3104 view.status_word = 0x0027;
3105
3106 axis.tick(view, client);
3108 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
3109
3110 view.status_word = 0x1027;
3112 axis.tick(view, client);
3113 view.status_word = 0x0027;
3114
3115 assert_eq!(axis.op, AxisOp::Idle);
3116 assert!(!axis.is_busy);
3117 assert!(!axis.is_error);
3118 assert_eq!(axis.home_offset, 0); }
3120
3121 #[test]
3122 fn soft_homing_pnp_home_sensor_full_sequence() {
3123 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3124 let mut view = MockView::new();
3125 enable_axis(&mut axis, &mut view, &mut client);
3126
3127 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3128
3129 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
3132 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3134 axis.tick(&mut view, &mut client); view.home_sensor = true;
3138 view.position_actual = 5000;
3139 axis.tick(&mut view, &mut client);
3140 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3141
3142 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3143 |v| { v.home_sensor = false; });
3144 }
3145
3146 #[test]
3147 fn soft_homing_npn_home_sensor_full_sequence() {
3148 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3149 let mut view = MockView::new();
3150 view.home_sensor = true;
3152 enable_axis(&mut axis, &mut view, &mut client);
3153
3154 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
3155
3156 axis.tick(&mut view, &mut client);
3158 view.status_word = 0x1027;
3159 axis.tick(&mut view, &mut client);
3160 view.status_word = 0x0027;
3161 axis.tick(&mut view, &mut client);
3162
3163 view.home_sensor = false;
3165 view.position_actual = 3000;
3166 axis.tick(&mut view, &mut client);
3167 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3168
3169 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
3170 |v| { v.home_sensor = true; }); }
3172
3173 #[test]
3174 fn soft_homing_limit_switch_suppresses_halt() {
3175 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3176 let mut view = MockView::new();
3177 enable_axis(&mut axis, &mut view, &mut client);
3178
3179 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
3181
3182 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3187 axis.tick(&mut view, &mut client); view.positive_limit = true;
3191 view.velocity_actual = 100; view.position_actual = 8000;
3193 axis.tick(&mut view, &mut client);
3194
3195 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3197 assert!(!axis.is_error);
3198 }
3199
3200 #[test]
3201 fn soft_homing_opposite_limit_still_protects() {
3202 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3203 let mut view = MockView::new();
3204 enable_axis(&mut axis, &mut view, &mut client);
3205
3206 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3208
3209 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3214 axis.tick(&mut view, &mut client); view.negative_limit = true;
3219 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
3221
3222 assert!(axis.is_error);
3224 assert!(axis.error_message.contains("Negative limit switch"));
3225 }
3226
3227 #[test]
3228 #[test]
3246 fn soft_homing_negative_direction_sets_negative_target() {
3247 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3248 let mut view = MockView::new();
3249 enable_axis(&mut axis, &mut view, &mut client);
3250
3251 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
3252 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
3256 }
3257
3258 #[test]
3259 fn home_integrated_method_starts_hardware_homing() {
3260 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3261 let mut view = MockView::new();
3262 enable_axis(&mut axis, &mut view, &mut client);
3263
3264 axis.home(&mut view, HomingMethod::CurrentPosition);
3265 assert!(matches!(axis.op, AxisOp::Homing(0)));
3266 assert_eq!(axis.homing_method, 37);
3267 }
3268
3269 #[test]
3270 fn home_integrated_arbitrary_code() {
3271 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3272 let mut view = MockView::new();
3273 enable_axis(&mut axis, &mut view, &mut client);
3274
3275 axis.home(&mut view, HomingMethod::Integrated(35));
3276 assert!(matches!(axis.op, AxisOp::Homing(0)));
3277 assert_eq!(axis.homing_method, 35);
3278 }
3279
3280 #[test]
3281 fn hardware_homing_skips_speed_sdos_when_zero() {
3282 use mechutil::ipc::CommandMessage;
3283
3284 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
3285 let mut view = MockView::new();
3286 enable_axis(&mut axis, &mut view, &mut client);
3287
3288 axis.home(&mut view, HomingMethod::Integrated(37));
3290
3291 axis.tick(&mut view, &mut client);
3293 assert!(matches!(axis.op, AxisOp::Homing(1)));
3294
3295 let _ = write_rx.try_recv();
3297
3298 let tid = axis.homing_sdo_tid;
3300 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3301 client.poll();
3302 axis.tick(&mut view, &mut client);
3303
3304 assert!(matches!(axis.op, AxisOp::Homing(8)));
3306 }
3307
3308 #[test]
3309 fn hardware_homing_writes_speed_sdos_when_nonzero() {
3310 use mechutil::ipc::CommandMessage;
3311
3312 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3313 let mut view = MockView::new();
3314 enable_axis(&mut axis, &mut view, &mut client);
3315
3316 axis.home(&mut view, HomingMethod::Integrated(37));
3318
3319 axis.tick(&mut view, &mut client);
3321 assert!(matches!(axis.op, AxisOp::Homing(1)));
3322 let _ = write_rx.try_recv();
3323
3324 let tid = axis.homing_sdo_tid;
3326 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3327 client.poll();
3328 axis.tick(&mut view, &mut client);
3329 assert!(matches!(axis.op, AxisOp::Homing(2)));
3331 }
3332
3333 #[test]
3334 fn soft_homing_edge_during_ack_step() {
3335 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3336 let mut view = MockView::new();
3337 enable_axis(&mut axis, &mut view, &mut client);
3338
3339 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3340 axis.tick(&mut view, &mut client); view.home_sensor = true;
3344 view.position_actual = 2000;
3345 axis.tick(&mut view, &mut client);
3346
3347 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3349 }
3350
3351 #[test]
3352 fn soft_homing_applies_home_position() {
3353 let mut cfg = soft_homing_config();
3354 cfg.home_position = 90.0;
3355
3356 use tokio::sync::mpsc;
3357 let (write_tx, _write_rx) = mpsc::unbounded_channel();
3358 let (resp_tx, response_rx) = mpsc::unbounded_channel();
3359 let mut client = CommandClient::new(write_tx, response_rx);
3360 let mut axis = Axis::new(cfg, "TestDrive");
3361
3362 let mut view = MockView::new();
3363 enable_axis(&mut axis, &mut view, &mut client);
3364
3365 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3366
3367 axis.tick(&mut view, &mut client);
3369 view.status_word = 0x1027;
3370 axis.tick(&mut view, &mut client);
3371 view.status_word = 0x0027;
3372 axis.tick(&mut view, &mut client);
3373
3374 view.home_sensor = true;
3376 view.position_actual = 5000;
3377 axis.tick(&mut view, &mut client);
3378 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3379
3380 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3382 |v| { v.home_sensor = false; });
3383
3384 assert_eq!(axis.home_offset, 0);
3386 }
3387
3388 #[test]
3389 fn soft_homing_default_home_position_zero() {
3390 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3391 let mut view = MockView::new();
3392 enable_axis(&mut axis, &mut view, &mut client);
3393
3394 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3395
3396 axis.tick(&mut view, &mut client);
3398 view.status_word = 0x1027;
3399 axis.tick(&mut view, &mut client);
3400 view.status_word = 0x0027;
3401 axis.tick(&mut view, &mut client);
3402
3403 view.home_sensor = true;
3405 view.position_actual = 5000;
3406 axis.tick(&mut view, &mut client);
3407
3408 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3409 |v| { v.home_sensor = false; });
3410
3411 assert_eq!(axis.home_offset, 0);
3412 }
3413}