1use std::time::{Duration, Instant};
25
26use serde_json::json;
27use strum_macros::FromRepr;
28
29use crate::command_client::CommandClient;
30use crate::ethercat::{SdoClient, SdoResult};
31use crate::fb::Ton;
32use crate::motion::FbSetModeOfOperation;
33use super::axis_config::AxisConfig;
34use super::axis_view::AxisView;
35use super::homing::HomingMethod;
36use super::cia402::{
37 Cia402Control, Cia402Status, Cia402State,
38 ModesOfOperation, RawControlWord, RawStatusWord,
39};
40
41#[derive(Debug, Clone, PartialEq)]
46enum AxisOp {
47 Idle,
48 Enabling(u8),
49 Disabling(u8),
50 Moving(MoveKind, u8, bool, bool),
51 Homing(u8),
52 SoftHoming(u8),
53 Halting(u8),
54 FaultRecovery(u8),
55}
56
57#[repr(u8)]
63#[derive(Debug, Clone, PartialEq, FromRepr)]
64enum HaltState {
65 WaitStopped = 0,
68 WaitCancelAck = 10,
71 WaitCancelAckClear = 20,
75}
76
77const HALT_STAGE_TIMEOUT: Duration = Duration::from_secs(3);
79
80const HALT_STABLE_WINDOW: i32 = 50;
85
86const HALT_STOPPED_VELOCITY: i32 = 100;
92
93const HALT_STABLE_TICKS_REQUIRED: u8 = 5;
97
98#[repr(u8)]
99#[derive(Debug, Clone, PartialEq, FromRepr)]
100enum HomeState {
101 EnsurePpMode = 0,
102 WaitPpMode = 1,
103 Search = 5,
104 WaitSearching = 10,
105 WaitFoundSensor = 20,
106 WaitStoppedFoundSensor = 30,
107 WaitFoundSensorAck = 40,
108 WaitFoundSensorAckClear = 45,
109 DebounceFoundSensor = 50,
110 BackOff = 60,
111 WaitBackingOff = 70,
112 WaitLostSensor = 80,
113 WaitStoppedLostSensor = 90,
114 WaitLostSensorAck = 100,
115 WaitLostSensorAckClear = 120,
116 WaitHomeOffsetDone = 125,
117
118 WriteHomingModeOp = 160,
119 WaitWriteHomingModeOp = 165,
120
121 WriteHomingMethod = 205,
122 WaitWriteHomingMethodDone = 210,
123 ClearHomingTrigger = 215,
124 TriggerHoming = 217,
125 WaitHomingStarted = 218,
126 WaitHomingDone = 220,
127 ResetHomingTrigger = 222,
128 WaitHomingTriggerCleared = 223,
129 WriteMotionModeOfOperation = 230,
130 WaitWriteMotionModeOfOperation = 235,
131 SendCurrentPositionTarget = 240,
132 WaitCurrentPositionTargetSent = 245
133
134}
135
136#[derive(Debug, Clone, PartialEq)]
137enum MoveKind {
138 Absolute,
139 Relative,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq)]
143enum SoftHomeSensor {
144 PositiveLimit,
145 NegativeLimit,
146 HomeSensor,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq)]
150enum SoftHomeSensorType {
151 Pnp,
153 Npn,
155}
156
157pub struct Axis {
167 config: AxisConfig,
168 sdo: SdoClient,
169
170 op: AxisOp,
172 home_offset: i32,
173 last_raw_position: i32,
174 op_started: Option<Instant>,
175 op_timeout: Duration,
176 homing_timeout: Duration,
177 move_start_timeout: Duration,
178 pending_move_target: i32,
179 pending_move_vel: u32,
180 pending_move_accel: u32,
181 pending_move_decel: u32,
182 homing_method: i8,
183 homing_sdo_tid: u32,
184 soft_home_sensor: SoftHomeSensor,
185 soft_home_sensor_type: SoftHomeSensorType,
186 soft_home_direction: f64,
187 halt_stable_count: u8,
188 prev_positive_limit: bool,
189 prev_negative_limit: bool,
190 prev_home_sensor: bool,
191
192
193
194 fb_mode_of_operation : FbSetModeOfOperation,
195
196 pub is_error: bool,
200 pub error_code: u32,
202 pub error_message: String,
204 pub motor_on: bool,
206 pub is_busy: bool,
212 pub in_motion: bool,
214 pub moving_positive: bool,
216 pub moving_negative: bool,
218 pub position: f64,
220 pub raw_position: i64,
222 pub speed: f64,
224 pub at_max_limit: bool,
226 pub at_min_limit: bool,
228 pub at_positive_limit_switch: bool,
230 pub at_negative_limit_switch: bool,
232 pub home_sensor: bool,
234
235
236 ton : Ton
238}
239
240impl Axis {
241 pub fn new(config: AxisConfig, device_name: &str) -> Self {
246 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
247 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
248 let move_start_timeout = op_timeout; Self {
250 config,
251 sdo: SdoClient::new(device_name),
252 op: AxisOp::Idle,
253 home_offset: 0,
254 last_raw_position: 0,
255 op_started: None,
256 op_timeout,
257 homing_timeout,
258 move_start_timeout,
259 pending_move_target: 0,
260 pending_move_vel: 0,
261 pending_move_accel: 0,
262 pending_move_decel: 0,
263 homing_method: 37,
264 homing_sdo_tid: 0,
265 soft_home_sensor: SoftHomeSensor::HomeSensor,
266 soft_home_sensor_type: SoftHomeSensorType::Pnp,
267 soft_home_direction: 1.0,
268 halt_stable_count: 0,
269 prev_positive_limit: false,
270 prev_negative_limit: false,
271 prev_home_sensor: false,
272 is_error: false,
273 error_code: 0,
274 error_message: String::new(),
275 motor_on: false,
276 is_busy: false,
277 in_motion: false,
278 moving_positive: false,
279 moving_negative: false,
280 position: 0.0,
281 raw_position: 0,
282 speed: 0.0,
283 at_max_limit: false,
284 at_min_limit: false,
285 at_positive_limit_switch: false,
286 at_negative_limit_switch: false,
287 home_sensor: false,
288 ton: Ton::new(),
289 fb_mode_of_operation : FbSetModeOfOperation::new()
290 }
291 }
292
293 pub fn config(&self) -> &AxisConfig {
295 &self.config
296 }
297
298 pub fn move_absolute(
308 &mut self,
309 view: &mut impl AxisView,
310 target: f64,
311 vel: f64,
312 accel: f64,
313 decel: f64,
314 ) {
315 if let Some(msg) = self.check_target_limit(target, view) {
316 self.set_op_error(&msg);
317 return;
318 }
319
320 let cpu = self.config.counts_per_user();
321 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
322 let raw_vel = (vel * cpu).round() as u32;
323 let raw_accel = (accel * cpu).round() as u32;
324 let raw_decel = (decel * cpu).round() as u32;
325
326 let user_pos = target > self.position;
328 let user_neg = target < self.position;
329
330 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute, user_pos, user_neg);
331 }
332
333 pub fn move_relative(
339 &mut self,
340 view: &mut impl AxisView,
341 distance: f64,
342 vel: f64,
343 accel: f64,
344 decel: f64,
345 ) {
346 log::info!("Axis: request to move relative dist {} vel {} accel {} decel {}",
347 distance, vel, accel, decel
348 );
349
350 if let Some(msg) = self.check_target_limit(self.position + distance, view) {
351 self.set_op_error(&msg);
352 return;
353 }
354
355 let cpu = self.config.counts_per_user();
356 let raw_distance = self.config.to_counts(distance).round() as i32;
357 let raw_vel = (vel * cpu).round() as u32;
358 let raw_accel = (accel * cpu).round() as u32;
359 let raw_decel = (decel * cpu).round() as u32;
360
361 log::info!("Axis starting relative move: request to move relative raw dist {} raw vel {} raw accel {} raw decel {}",
362 raw_distance, raw_vel, raw_accel, raw_decel
363 );
364
365 let mut cw = RawControlWord(view.control_word());
367 cw.set_bit(4, false); view.set_control_word(cw.raw());
369
370 let user_pos = distance > 0.0;
372 let user_neg = distance < 0.0;
373
374 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative, user_pos, user_neg);
375 }
376
377 fn start_move(
378 &mut self,
379 view: &mut impl AxisView,
380 raw_target: i32,
381 raw_vel: u32,
382 raw_accel: u32,
383 raw_decel: u32,
384 kind: MoveKind,
385 user_pos: bool,
386 user_neg: bool,
387 ) {
388 self.pending_move_target = raw_target;
389 self.pending_move_vel = raw_vel;
390 self.pending_move_accel = raw_accel;
391 self.pending_move_decel = raw_decel;
392
393 view.set_target_position(raw_target);
395 view.set_profile_velocity(raw_vel);
396 view.set_profile_acceleration(raw_accel);
397 view.set_profile_deceleration(raw_decel);
398
399 let mut cw = RawControlWord(view.control_word());
404 cw.set_bit(6, kind == MoveKind::Relative);
405 cw.set_bit(8, false); cw.set_bit(4, true); view.set_control_word(cw.raw());
408
409 self.op = AxisOp::Moving(kind, 1, user_pos, user_neg);
414 self.op_started = Some(Instant::now());
415 }
416
417 pub fn halt(&mut self, view: &mut impl AxisView) {
435 self.command_halt(view);
436 self.halt_stable_count = 0;
437 self.last_raw_position = view.position_actual();
438 self.op_started = Some(Instant::now());
439 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
440 }
441
442 pub fn enable(&mut self, view: &mut impl AxisView) {
450 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
452 let mut cw = RawControlWord(view.control_word());
453 cw.cmd_shutdown();
454 view.set_control_word(cw.raw());
455
456 self.op = AxisOp::Enabling(1);
457 self.op_started = Some(Instant::now());
458 }
459
460 pub fn disable(&mut self, view: &mut impl AxisView) {
462 let mut cw = RawControlWord(view.control_word());
463 cw.cmd_disable_operation();
464 cw.set_bit(4, false);
465 cw.set_bit(8, false);
466 cw.set_bit(7, false);
467 cw.set_bit(2, true);
468 view.set_control_word(cw.raw());
469
470 self.op = AxisOp::Disabling(1);
471 self.op_started = Some(Instant::now());
472 }
473
474 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
490 let sw = RawStatusWord(view.status_word());
491 let drive_in_fault = matches!(
492 sw.state(),
493 Cia402State::Fault | Cia402State::FaultReactionActive
494 );
495
496 self.is_error = false;
497 self.error_code = 0;
498 self.error_message.clear();
499
500 if drive_in_fault {
501 let mut cw = RawControlWord(view.control_word());
503 cw.cmd_clear_fault_reset();
504 view.set_control_word(cw.raw());
505 self.op = AxisOp::FaultRecovery(1);
506 self.op_started = Some(Instant::now());
507 } else {
508 self.op = AxisOp::Idle;
510 self.op_started = None;
511 self.is_busy = false;
512 self.in_motion = false;
513 }
514 }
515
516 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
524 if method.is_integrated() {
525 self.homing_method = method.cia402_code();
526 self.op = AxisOp::Homing(0);
527 self.op_started = Some(Instant::now());
528 let _ = view;
529 } else {
530 self.configure_soft_homing(method);
531 self.start_soft_homing(view);
532 }
533 }
534
535 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
544 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
545 }
546
547 pub fn set_home_position(&mut self, user_units: f64) {
551 self.config.home_position = user_units;
552 }
553
554 pub fn set_software_max_limit(&mut self, user_units: f64) {
556 self.config.max_position_limit = user_units;
557 self.config.enable_max_position_limit = true;
558 }
559
560 pub fn set_software_min_limit(&mut self, user_units: f64) {
562 self.config.min_position_limit = user_units;
563 self.config.enable_min_position_limit = true;
564 }
565
566 pub fn sdo_write(
572 &mut self,
573 client: &mut CommandClient,
574 index: u16,
575 sub_index: u8,
576 value: serde_json::Value,
577 ) {
578 self.sdo.write(client, index, sub_index, value);
579 }
580
581 pub fn sdo_read(
583 &mut self,
584 client: &mut CommandClient,
585 index: u16,
586 sub_index: u8,
587 ) -> u32 {
588 self.sdo.read(client, index, sub_index)
589 }
590
591 pub fn sdo_result(
593 &mut self,
594 client: &mut CommandClient,
595 tid: u32,
596 ) -> SdoResult {
597 self.sdo.result(client, tid, Duration::from_secs(5))
598 }
599
600 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
614 self.check_faults(view);
615 self.progress_op(view, client);
616 self.update_outputs(view);
617 self.check_limits(view);
618 }
619
620 fn update_outputs(&mut self, view: &impl AxisView) {
625 let raw = view.position_actual();
626 self.raw_position = raw as i64;
627 self.position = self.config.to_user((raw - self.home_offset) as f64);
628
629 let vel = view.velocity_actual();
630 let user_vel = self.config.to_user(vel as f64);
631 self.speed = user_vel.abs();
632 self.moving_positive = user_vel > 0.0;
633 self.moving_negative = user_vel < 0.0;
634 self.is_busy = self.op != AxisOp::Idle;
635 self.in_motion = matches!(self.op, AxisOp::Moving(_, _, _, _) | AxisOp::SoftHoming(_));
636
637 let sw = RawStatusWord(view.status_word());
638 self.motor_on = sw.state() == Cia402State::OperationEnabled;
639
640 self.last_raw_position = raw;
641 }
642
643 fn check_faults(&mut self, view: &impl AxisView) {
648 let sw = RawStatusWord(view.status_word());
649 let state = sw.state();
650
651 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
652 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
653 self.is_error = true;
654 let ec = view.error_code();
655 if ec != 0 {
656 self.error_code = ec as u32;
657 }
658 self.error_message = format!("Drive fault (state: {})", state);
659 self.op = AxisOp::Idle;
661 self.op_started = None;
662 }
663 }
664 }
665
666 fn op_timed_out(&self) -> bool {
671 self.op_started
672 .map_or(false, |t| t.elapsed() > self.op_timeout)
673 }
674
675 fn homing_timed_out(&self) -> bool {
676 self.op_started
677 .map_or(false, |t| t.elapsed() > self.homing_timeout)
678 }
679
680 fn move_start_timed_out(&self) -> bool {
681 self.op_started
682 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
683 }
684
685 fn op_stage_timed_out(&self, limit: Duration) -> bool {
689 self.op_started
690 .map_or(false, |t| t.elapsed() > limit)
691 }
692
693 fn set_op_error(&mut self, msg: &str) {
694 self.is_error = true;
695 self.error_message = msg.to_string();
696 self.op = AxisOp::Idle;
697 self.op_started = None;
698 self.is_busy = false;
699 self.in_motion = false;
700 log::error!("Axis error: {}", msg);
701 }
702
703 fn restore_pp_after_error(&mut self, msg: &str) {
704 self.is_error = true;
705 self.error_message = msg.to_string();
706 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);;
707 log::error!("Axis error: {}", msg);
708 }
709
710 fn finish_op_error(&mut self) {
711 self.op = AxisOp::Idle;
712 self.op_started = None;
713 self.is_busy = false;
714 self.in_motion = false;
715 }
716
717 fn complete_op(&mut self) {
718 self.op = AxisOp::Idle;
719 self.op_started = None;
720 self.is_busy = false;
725 self.in_motion = false;
726 }
727
728 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
737 let static_limit = if self.config.enable_max_position_limit {
738 Some(self.config.max_position_limit)
739 } else {
740 None
741 };
742 match (static_limit, view.dynamic_max_position_limit()) {
743 (Some(s), Some(d)) => Some(s.min(d)),
744 (Some(v), None) | (None, Some(v)) => Some(v),
745 (None, None) => None,
746 }
747 }
748
749 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
753 let static_limit = if self.config.enable_min_position_limit {
754 Some(self.config.min_position_limit)
755 } else {
756 None
757 };
758 match (static_limit, view.dynamic_min_position_limit()) {
759 (Some(s), Some(d)) => Some(s.max(d)),
760 (Some(v), None) | (None, Some(v)) => Some(v),
761 (None, None) => None,
762 }
763 }
764
765 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
770 if let Some(max) = self.effective_max_limit(view) {
771 if target > max {
772 return Some(format!(
773 "Target {:.3} exceeds max software limit {:.3}",
774 target, max
775 ));
776 }
777 }
778 if let Some(min) = self.effective_min_limit(view) {
779 if target < min {
780 return Some(format!(
781 "Target {:.3} exceeds min software limit {:.3}",
782 target, min
783 ));
784 }
785 }
786 None
787 }
788
789 fn check_limits(&mut self, view: &mut impl AxisView) {
798 let eff_max = self.effective_max_limit(view);
800 let eff_min = self.effective_min_limit(view);
801 let sw_max = eff_max.map_or(false, |m| self.position >= m);
802 let sw_min = eff_min.map_or(false, |m| self.position <= m);
803
804 self.at_max_limit = sw_max;
805 self.at_min_limit = sw_min;
806
807 let hw_pos = view.positive_limit_active();
809 let hw_neg = view.negative_limit_active();
810
811 self.at_positive_limit_switch = hw_pos;
812 self.at_negative_limit_switch = hw_neg;
813
814 self.home_sensor = view.home_sensor_active();
816
817 self.prev_positive_limit = hw_pos;
819 self.prev_negative_limit = hw_neg;
820 self.prev_home_sensor = view.home_sensor_active();
821
822 let mut commanded_positive = false;
824 let mut commanded_negative = false;
825
826 let is_moving = matches!(self.op, AxisOp::Moving(_, _, _, _));
827 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
828
829 if !is_moving && !is_soft_homing {
830 return; }
832
833 match &self.op {
834 AxisOp::Moving(_, _, pos, neg) => {
835 commanded_positive = *pos;
837 commanded_negative = *neg;
838 }
839 AxisOp::SoftHoming(_) => {
840 match self.soft_home_sensor {
841 SoftHomeSensor::PositiveLimit => commanded_positive = true,
842 SoftHomeSensor::NegativeLimit => commanded_negative = true,
843 SoftHomeSensor::HomeSensor => {
844 commanded_positive = self.moving_positive;
845 commanded_negative = self.moving_negative;
846 }
847 }
848 }
849 _ => {}
850 }
851
852 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
854 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
855
856 let effective_hw_pos = hw_pos && !suppress_pos;
857 let effective_hw_neg = hw_neg && !suppress_neg;
858
859 let effective_sw_max = sw_max && !is_soft_homing;
861 let effective_sw_min = sw_min && !is_soft_homing;
862
863 let positive_blocked = (effective_sw_max || effective_hw_pos) && commanded_positive;
864 let negative_blocked = (effective_sw_min || effective_hw_neg) && commanded_negative;
865
866
867 if positive_blocked || negative_blocked {
868 let mut cw = RawControlWord(view.control_word());
869 cw.set_bit(8, true); view.set_control_word(cw.raw());
871
872 let msg = if effective_hw_pos && commanded_positive {
873 "Positive limit switch active".to_string()
874 } else if effective_hw_neg && commanded_negative {
875 "Negative limit switch active".to_string()
876 } else if effective_sw_max && commanded_positive {
877 format!(
878 "Software position limit: position {:.3} >= max {:.3}",
879 self.position, eff_max.unwrap_or(self.position)
880 )
881 } else {
882 format!(
883 "Software position limit: position {:.3} <= min {:.3}",
884 self.position, eff_min.unwrap_or(self.position)
885 )
886 };
887
888 if is_soft_homing {
889 self.set_op_error(&msg);
892 } else {
893 log::info!("Axis move halted by limit: {}", msg);
906 self.command_halt(view);
907 self.halt_stable_count = 0;
908 self.last_raw_position = view.position_actual();
909 self.op_started = Some(Instant::now());
910 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
911 }
912 }
913 }
914
915 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
920 match self.op.clone() {
921 AxisOp::Idle => {}
922 AxisOp::Enabling(step) => self.tick_enabling(view, step),
923 AxisOp::Disabling(step) => self.tick_disabling(view, step),
924 AxisOp::Moving(kind, step, pos, neg) => self.tick_moving(view, kind, step, pos, neg),
925 AxisOp::Homing(step) => self.tick_homing(view, client, step),
926 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
927 AxisOp::Halting(step) => self.tick_halting(view, step),
928 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
929 }
930 }
931
932 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
937 match step {
938 1 => {
939 let sw = RawStatusWord(view.status_word());
940 if sw.state() == Cia402State::ReadyToSwitchOn {
941 let mut cw = RawControlWord(view.control_word());
942 cw.cmd_enable_operation();
943 view.set_control_word(cw.raw());
944 self.op = AxisOp::Enabling(2);
945 } else if self.op_timed_out() {
946 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
947 }
948 }
949 2 => {
950 let sw = RawStatusWord(view.status_word());
951 if sw.state() == Cia402State::OperationEnabled {
952 self.complete_op();
959 } else if self.op_timed_out() {
960 self.set_op_error("Enable timeout: waiting for OperationEnabled");
961 }
962 }
963 _ => self.complete_op(),
964 }
965 }
966
967 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
971 match step {
972 1 => {
973 let sw = RawStatusWord(view.status_word());
974 if sw.state() != Cia402State::OperationEnabled {
975 self.complete_op();
976 } else if self.op_timed_out() {
977 self.set_op_error("Disable timeout: drive still in OperationEnabled");
978 }
979 }
980 _ => self.complete_op(),
981 }
982 }
983
984 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8, pos: bool, neg: bool) {
990 match step {
991 1 => {
992 let sw = RawStatusWord(view.status_word());
994 if sw.raw() & (1 << 12) != 0 {
995 let mut cw = RawControlWord(view.control_word());
997 cw.set_bit(4, false);
998 view.set_control_word(cw.raw());
999 self.op = AxisOp::Moving(kind, 2, pos, neg);
1000 } else if self.move_start_timed_out() {
1001 self.set_op_error("Move timeout: set-point not acknowledged");
1002 }
1003 },
1004 2 => {
1005 let sw = RawStatusWord(view.status_word());
1007 if sw.raw() & (1 << 12) == 0 {
1008 self.op = AxisOp::Moving(kind, 3, pos, neg);
1010 }
1011 },
1012 3 => {
1013 let sw = RawStatusWord(view.status_word());
1015 if sw.target_reached() {
1016 self.complete_op();
1017 }
1018 },
1019 _ => self.complete_op(),
1020 }
1021 }
1022
1023 fn tick_homing(
1041 &mut self,
1042 view: &mut impl AxisView,
1043 client: &mut CommandClient,
1044 step: u8,
1045 ) {
1046 match step {
1047 0 => {
1048 self.homing_sdo_tid = self.sdo.write(
1050 client,
1051 0x6098,
1052 0,
1053 json!(self.homing_method),
1054 );
1055 self.op = AxisOp::Homing(1);
1056 }
1057 1 => {
1058 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1060 SdoResult::Ok(_) => {
1061 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
1063 self.op = AxisOp::Homing(8);
1064 } else {
1065 self.op = AxisOp::Homing(2);
1066 }
1067 }
1068 SdoResult::Pending => {
1069 if self.homing_timed_out() {
1070 self.set_op_error("Homing timeout: SDO write for homing method");
1071 }
1072 }
1073 SdoResult::Err(e) => {
1074 self.set_op_error(&format!("Homing SDO error: {}", e));
1075 }
1076 SdoResult::Timeout => {
1077 self.set_op_error("Homing timeout: SDO write timed out");
1078 }
1079 }
1080 }
1081 2 => {
1082 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1084 self.homing_sdo_tid = self.sdo.write(
1085 client,
1086 0x6099,
1087 1,
1088 json!(speed_counts),
1089 );
1090 self.op = AxisOp::Homing(3);
1091 }
1092 3 => {
1093 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1094 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
1095 SdoResult::Pending => {
1096 if self.homing_timed_out() {
1097 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
1098 }
1099 }
1100 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1101 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1102 }
1103 }
1104 4 => {
1105 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1107 self.homing_sdo_tid = self.sdo.write(
1108 client,
1109 0x6099,
1110 2,
1111 json!(speed_counts),
1112 );
1113 self.op = AxisOp::Homing(5);
1114 }
1115 5 => {
1116 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1117 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
1118 SdoResult::Pending => {
1119 if self.homing_timed_out() {
1120 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1121 }
1122 }
1123 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1124 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1125 }
1126 }
1127 6 => {
1128 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1130 self.homing_sdo_tid = self.sdo.write(
1131 client,
1132 0x609A,
1133 0,
1134 json!(accel_counts),
1135 );
1136 self.op = AxisOp::Homing(7);
1137 }
1138 7 => {
1139 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1140 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
1141 SdoResult::Pending => {
1142 if self.homing_timed_out() {
1143 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1144 }
1145 }
1146 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1147 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1148 }
1149 }
1150 8 => {
1151 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1154 let mut cw = RawControlWord(view.control_word());
1155 cw.set_bit(4, false);
1156 view.set_control_word(cw.raw());
1157 self.op = AxisOp::Homing(9);
1158 }
1159 9 => {
1160 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
1162 self.op = AxisOp::Homing(10);
1163 } else if self.homing_timed_out() {
1164 self.set_op_error("Homing timeout: mode not confirmed");
1165 }
1166 }
1167 10 => {
1168 let mut cw = RawControlWord(view.control_word());
1170 cw.set_bit(4, true);
1171 view.set_control_word(cw.raw());
1172 self.op = AxisOp::Homing(11);
1173 }
1174 11 => {
1175 let sw = view.status_word();
1180 let error = sw & (1 << 13) != 0;
1181 if error {
1182 self.set_op_error("Homing error: drive reported homing failure");
1183 } else if sw & (1 << 12) == 0 {
1184 self.op = AxisOp::Homing(12);
1185 } else if self.homing_timed_out() {
1186 self.set_op_error(&format!("Homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1187 }
1188 }
1189 12 => {
1190 let sw = view.status_word();
1193 let error = sw & (1 << 13) != 0;
1194 let attained = sw & (1 << 12) != 0;
1195 let reached = sw & (1 << 10) != 0;
1196
1197 if error {
1198 self.set_op_error("Homing error: drive reported homing failure");
1199 } else if attained && reached {
1200 self.op = AxisOp::Homing(13);
1201 } else if self.homing_timed_out() {
1202 self.set_op_error("Homing timeout: procedure did not complete");
1203 }
1204 }
1205 13 => {
1206 self.home_offset = view.position_actual()
1209 - self.config.to_counts(self.config.home_position).round() as i32;
1210 let mut cw = RawControlWord(view.control_word());
1212 cw.set_bit(4, false);
1213 view.set_control_word(cw.raw());
1214 self.op = AxisOp::Homing(14);
1215 }
1216 14 => {
1217 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1220 log::info!("Homing complete — home offset: {}", self.home_offset);
1221 self.complete_op();
1222 }
1223 _ => self.complete_op(),
1224 }
1225 }
1226
1227 fn configure_soft_homing(&mut self, method: HomingMethod) {
1230 match method {
1231 HomingMethod::LimitSwitchPosPnp => {
1232 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1233 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1234 self.soft_home_direction = 1.0;
1235 }
1236 HomingMethod::LimitSwitchNegPnp => {
1237 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1238 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1239 self.soft_home_direction = -1.0;
1240 }
1241 HomingMethod::LimitSwitchPosNpn => {
1242 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1243 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1244 self.soft_home_direction = 1.0;
1245 }
1246 HomingMethod::LimitSwitchNegNpn => {
1247 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1248 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1249 self.soft_home_direction = -1.0;
1250 }
1251 HomingMethod::HomeSensorPosPnp => {
1252 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1253 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1254 self.soft_home_direction = 1.0;
1255 }
1256 HomingMethod::HomeSensorNegPnp => {
1257 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1258 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1259 self.soft_home_direction = -1.0;
1260 }
1261 HomingMethod::HomeSensorPosNpn => {
1262 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1263 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1264 self.soft_home_direction = 1.0;
1265 }
1266 HomingMethod::HomeSensorNegNpn => {
1267 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1268 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1269 self.soft_home_direction = -1.0;
1270 }
1271 _ => {} }
1273 }
1274
1275 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1276 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1277 self.op_started = Some(Instant::now());
1278 }
1279
1280 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1281 let raw = match self.soft_home_sensor {
1282 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1283 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1284 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1285 };
1286 match self.soft_home_sensor_type {
1287 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1290 }
1291
1292
1293 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1296 let dir = if !self.config.invert_direction {
1297 direction
1298 }
1299 else {
1300 -direction
1301 };
1302
1303 let target = if dir > 0.0 {
1304 i32::MAX
1305 }
1306 else {
1307 i32::MIN
1308 };
1309
1310 return target;
1311 }
1312
1313
1314 pub fn command_halt(&self, view: &mut impl AxisView) {
1319 let mut cw = RawControlWord(view.control_word());
1320 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());
1325 }
1326
1327 pub fn command_clear_halt(&self, view: &mut impl AxisView) {
1330 let mut cw = RawControlWord(view.control_word());
1331 cw.set_bit(8, false); view.set_control_word(cw.raw());
1333 }
1334
1335
1336 pub fn command_cancel_move(&self, view: &mut impl AxisView) {
1344
1345 let mut cw = RawControlWord(view.control_word());
1346
1347 cw.set_bit(4, true); cw.set_bit(5, true); cw.set_bit(6, false); view.set_control_word(cw.raw());
1355
1356 let current_pos = view.position_actual();
1357 view.set_target_position(current_pos);
1358 view.set_profile_velocity(0);
1359 }
1360
1361
1362 fn command_homing_speed(&self, view: &mut impl AxisView) {
1364 let cpu = self.config.counts_per_user();
1365 let vel = (self.config.homing_speed * cpu).round() as u32;
1366 let accel = (self.config.homing_accel * cpu).round() as u32;
1367 let decel = (self.config.homing_decel * cpu).round() as u32;
1368 view.set_profile_velocity(vel);
1369 view.set_profile_acceleration(accel);
1370 view.set_profile_deceleration(decel);
1371 }
1372
1373 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1389 match HomeState::from_repr(step) {
1390
1391 Some(HomeState::EnsurePpMode) => {
1392 log::info!("SoftHome: Ensuring PP mode..");
1397 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1398 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1399 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1400 },
1401 Some(HomeState::WaitPpMode) => {
1402
1403 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1404 if !self.fb_mode_of_operation.is_busy() {
1405 if self.fb_mode_of_operation.is_error() {
1406 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1407 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1408 ));
1409 }
1410 else {
1411 log::info!("SoftHome: Drive is in PP mode!");
1412
1413 if !self.check_soft_home_trigger(view) {
1417 log::info!("SoftHome: Not on home switch; seek out.");
1418 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1419 } else {
1420 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1421 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1422 }
1423 }
1424 }
1425
1426
1427 },
1428
1429 Some(HomeState::Search) => {
1431 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1432
1433 let target = self.calculate_max_relative_target(self.soft_home_direction);
1443 view.set_target_position(target);
1444
1445 self.command_homing_speed(view);
1454
1455 let mut cw = RawControlWord(view.control_word());
1456 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());
1461
1462 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1463 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1464 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1465 }
1466 Some(HomeState::WaitSearching) => {
1467 if self.check_soft_home_trigger(view) {
1468 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1469 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1470 return;
1471 }
1472 let sw = RawStatusWord(view.status_word());
1473 if sw.raw() & (1 << 12) != 0 {
1474 let mut cw = RawControlWord(view.control_word());
1475 cw.set_bit(4, false);
1476 view.set_control_word(cw.raw());
1477 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1478 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1479 } else if self.homing_timed_out() {
1480 self.set_op_error("Software homing timeout: set-point not acknowledged");
1481 }
1482 }
1483 Some(HomeState::WaitFoundSensor) => {
1493 if self.check_soft_home_trigger(view) {
1494 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1495 log::info!("ControlWord is : {} ", view.control_word());
1496
1497 let mut cw = RawControlWord(view.control_word());
1498 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1501
1502
1503 self.halt_stable_count = 0;
1504 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1505 } else if self.homing_timed_out() {
1506 self.set_op_error("Software homing timeout: sensor not detected");
1507 }
1508 }
1509
1510
1511 Some(HomeState::WaitStoppedFoundSensor) => {
1512 const STABLE_WINDOW: i32 = 1;
1513 const STABLE_TICKS_REQUIRED: u8 = 10;
1514
1515 let pos = view.position_actual();
1520 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1521 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1522 } else {
1523 self.halt_stable_count = 0;
1524 }
1525
1526 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1527
1528 log::info!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1529 self.command_cancel_move(view);
1530 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1531
1532 } else if self.homing_timed_out() {
1533 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1534 }
1535 }
1536 Some(HomeState::WaitFoundSensorAck) => {
1537 let sw = RawStatusWord(view.status_word());
1538 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1539
1540 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1541
1542 let mut cw = RawControlWord(view.control_word());
1544 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1547
1548 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1549
1550 } else if self.homing_timed_out() {
1551 self.set_op_error("Software homing timeout: cancel not acknowledged");
1552 }
1553 },
1554 Some(HomeState::WaitFoundSensorAckClear) => {
1555 let sw = RawStatusWord(view.status_word());
1556 if sw.raw() & (1 << 12) == 0 {
1558
1559 self.command_clear_halt(view);
1561
1562 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1563 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1564 self.ton.call(false, Duration::from_secs(3));
1565 }
1566 },
1567 Some(HomeState::DebounceFoundSensor) => {
1569 self.ton.call(true, Duration::from_secs(3));
1570
1571 let sw = RawStatusWord(view.status_word());
1572 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1573 self.ton.call(false, Duration::from_secs(3));
1574 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1575 view.position_actual(), view.control_word(), view.status_word());
1576 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1577 }
1578 }
1579
1580 Some(HomeState::BackOff) => {
1582
1583 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1584 view.set_target_position(target);
1585
1586
1587 self.command_homing_speed(view);
1588
1589 let mut cw = RawControlWord(view.control_word());
1590 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1594 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1595 target, self.config.homing_speed, view.position_actual(), cw.raw());
1596 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1597 }
1598 Some(HomeState::WaitBackingOff) => {
1599 let sw = RawStatusWord(view.status_word());
1600 if sw.raw() & (1 << 12) != 0 {
1601 let mut cw = RawControlWord(view.control_word());
1602 cw.set_bit(4, false);
1603 view.set_control_word(cw.raw());
1604 log::info!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1605 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1606 } else if self.homing_timed_out() {
1607 self.set_op_error("Software homing timeout: back-off not acknowledged");
1608 }
1609 }
1610 Some(HomeState::WaitLostSensor) => {
1611 if !self.check_soft_home_trigger(view) {
1612 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1613
1614 self.command_halt(view);
1615 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1616 } else if self.homing_timed_out() {
1617 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1618 }
1619 }
1620 Some(HomeState::WaitStoppedLostSensor) => {
1621 const STABLE_WINDOW: i32 = 1;
1622 const STABLE_TICKS_REQUIRED: u8 = 10;
1623
1624 let pos = view.position_actual();
1629 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1630 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1631 } else {
1632 self.halt_stable_count = 0;
1633 }
1634
1635 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1636 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1637 self.command_cancel_move(view);
1638 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1639 } else if self.homing_timed_out() {
1640 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1641 }
1642 }
1643 Some(HomeState::WaitLostSensorAck) => {
1644 let sw = RawStatusWord(view.status_word());
1645 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1646
1647 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1648
1649 let mut cw = RawControlWord(view.control_word());
1651 cw.set_bit(4, false); view.set_control_word(cw.raw());
1653
1654 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1655
1656
1657 } else if self.homing_timed_out() {
1658 self.set_op_error("Software homing timeout: cancel not acknowledged");
1659 }
1660 }
1661 Some(HomeState::WaitLostSensorAckClear) => {
1662 let sw = RawStatusWord(view.status_word());
1664 if sw.raw() & (1 << 12) == 0 {
1665
1666 self.command_clear_halt(view);
1667
1668 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1669 self.homing_sdo_tid = self.sdo.write(
1672 client, 0x607C, 0, json!(desired_counts),
1673 );
1674
1675 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1676 self.config.home_position, desired_counts
1677 );
1678
1679 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1680
1681 }
1682 },
1683
1684 Some(HomeState::WaitHomeOffsetDone) => {
1685 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1687 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1688 SdoResult::Pending => {
1689 if self.homing_timed_out() {
1690 self.set_op_error("Software homing timeout: home offset SDO write");
1691 }
1692 }
1693 SdoResult::Err(e) => {
1694 self.set_op_error(&format!("Software homing SDO error: {}", e));
1695 }
1696 SdoResult::Timeout => {
1697 self.set_op_error("Software homing: home offset SDO timed out");
1698 }
1699 }
1700 },
1701 Some(HomeState::WriteHomingModeOp) => {
1702
1703 self.fb_mode_of_operation.reset();
1707 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1708 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1709 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1710
1711
1712 },
1713 Some(HomeState::WaitWriteHomingModeOp) => {
1714 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1716
1717 if !self.fb_mode_of_operation.is_busy() {
1718 if self.fb_mode_of_operation.is_error() {
1719 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1720 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1721 ));
1722 }
1723 else {
1724 log::info!("SoftHome: Drive is now in Homing Mode.");
1725 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1726 }
1727 }
1728 },
1729 Some(HomeState::WriteHomingMethod) => {
1730 self.homing_sdo_tid = self.sdo.write(
1732 client, 0x6098, 0, json!(37i8),
1733 );
1734 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1735 }
1736 Some(HomeState::WaitWriteHomingMethodDone) => {
1737 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1739 SdoResult::Ok(_) => {
1740 log::info!("SoftHome: Successfully wrote homing method.");
1741 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1742 }
1743 SdoResult::Pending => {
1744 if self.homing_timed_out() {
1745 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1746 }
1747 }
1748 SdoResult::Err(e) => {
1749 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1750 }
1751 SdoResult::Timeout => {
1752 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1753 }
1754 }
1755 }
1756 Some(HomeState::ClearHomingTrigger) => {
1757 let mut cw = RawControlWord(view.control_word());
1760 cw.set_bit(4, false);
1761 view.set_control_word(cw.raw());
1762 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1763 }
1764 Some(HomeState::TriggerHoming) => {
1765 let mut cw = RawControlWord(view.control_word());
1767 cw.set_bit(4, true);
1768 view.set_control_word(cw.raw());
1769 log::info!("SoftHome[TriggerHoming]: start homing");
1770 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1771 }
1772 Some(HomeState::WaitHomingStarted) => {
1773 let sw = view.status_word();
1779 let error = sw & (1 << 13) != 0;
1780 if error {
1781 self.restore_pp_after_error("Software homing: drive reported homing error");
1782 } else if sw & (1 << 12) == 0 {
1783 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1784 } else if self.homing_timed_out() {
1785 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1786 }
1787 }
1788 Some(HomeState::WaitHomingDone) => {
1789 let sw = view.status_word();
1791 let error = sw & (1 << 13) != 0;
1792 let attained = sw & (1 << 12) != 0;
1793 let reached = sw & (1 << 10) != 0;
1794
1795 if error {
1796 self.restore_pp_after_error("Software homing: drive reported homing error");
1797 } else if attained && reached {
1798 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
1799 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
1800 } else if self.homing_timed_out() {
1801 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
1802 }
1803 }
1804 Some(HomeState::ResetHomingTrigger) => {
1805 let mut cw = RawControlWord(view.control_word());
1810 cw.set_bit(4, false);
1811 view.set_control_word(cw.raw());
1812 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
1813 }
1814 Some(HomeState::WaitHomingTriggerCleared) => {
1815 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
1819 }
1820
1821
1822 Some(HomeState::WriteMotionModeOfOperation) => {
1823
1824 self.fb_mode_of_operation.reset();
1827 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1828 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1829 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
1830
1831 },
1832 Some(HomeState::WaitWriteMotionModeOfOperation) => {
1833 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1835
1836 if !self.fb_mode_of_operation.is_busy() {
1837 if self.fb_mode_of_operation.is_error() {
1838 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1839 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1840 ));
1841 }
1842 else {
1843 if self.is_error {
1844 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
1845 self.finish_op_error();
1846 }
1847 else {
1848 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1851 }
1852
1853 }
1854 }
1855 },
1856
1857 Some(HomeState::SendCurrentPositionTarget) => {
1858 let current_pos = view.position_actual();
1860 view.set_target_position(current_pos);
1861 view.set_profile_velocity(0);
1862 let mut cw = RawControlWord(view.control_word());
1863 cw.set_bit(4, true);
1864 cw.set_bit(5, true);
1865 cw.set_bit(6, false); view.set_control_word(cw.raw());
1867 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1868 }
1869 Some(HomeState::WaitCurrentPositionTargetSent) => {
1870 let sw = RawStatusWord(view.status_word());
1872 if sw.raw() & (1 << 12) != 0 {
1873 let mut cw = RawControlWord(view.control_word());
1874 cw.set_bit(4, false);
1875 view.set_control_word(cw.raw());
1876 log::info!("Software homing complete — position set to {} user units",
1877 self.config.home_position);
1878 self.complete_op();
1879 } else if self.homing_timed_out() {
1880 self.set_op_error("Software homing timeout: hold position not acknowledged");
1881 }
1882 }
1883 _ => self.complete_op(),
1884 }
1885 }
1886
1887 fn tick_halting(&mut self, view: &mut impl AxisView, step: u8) {
1900 match HaltState::from_repr(step) {
1901 Some(HaltState::WaitStopped) => {
1902 let pos = view.position_actual();
1906 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
1907
1908 let vel = view.velocity_actual().abs();
1909 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
1910
1911 if pos_stable || vel_stopped {
1916 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1917 } else {
1918 self.halt_stable_count = 0;
1919 }
1920
1921 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
1922 self.command_cancel_move(view);
1923 self.op_started = Some(Instant::now());
1924 self.op = AxisOp::Halting(HaltState::WaitCancelAck as u8);
1925 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1926 self.set_op_error("Halt timeout: motor did not stop");
1927 }
1928 }
1929 Some(HaltState::WaitCancelAck) => {
1930 let sw = RawStatusWord(view.status_word());
1931 let setpoint_ack = sw.raw() & (1 << 12) != 0;
1932 if setpoint_ack {
1934 let mut cw = RawControlWord(view.control_word());
1937 cw.set_bit(4, false);
1938 cw.set_bit(5, true);
1939 view.set_control_word(cw.raw());
1940 self.op_started = Some(Instant::now());
1941 self.op = AxisOp::Halting(HaltState::WaitCancelAckClear as u8);
1942 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1943 self.set_op_error("Halt timeout: cancel not acknowledged");
1944 }
1945 }
1946 Some(HaltState::WaitCancelAckClear) => {
1947 let sw = RawStatusWord(view.status_word());
1948 if sw.raw() & (1 << 12) == 0 {
1949 self.command_clear_halt(view);
1951 self.complete_op();
1952 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1953 self.set_op_error("Halt timeout: ack did not clear");
1954 }
1955 }
1956 None => {
1957 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
1958 self.complete_op();
1959 }
1960 }
1961 }
1962
1963 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1968 match step {
1969 1 => {
1970 let mut cw = RawControlWord(view.control_word());
1972 cw.cmd_fault_reset();
1973 view.set_control_word(cw.raw());
1974 self.op = AxisOp::FaultRecovery(2);
1975 }
1976 2 => {
1977 let sw = RawStatusWord(view.status_word());
1979 let state = sw.state();
1980 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1981 log::info!("Fault cleared (drive state: {})", state);
1982 self.complete_op();
1983 } else if self.op_timed_out() {
1984 self.set_op_error("Fault reset timeout: drive still faulted");
1985 }
1986 }
1987 _ => self.complete_op(),
1988 }
1989 }
1990}
1991
1992#[cfg(test)]
1997mod tests {
1998 use super::*;
1999
2000 struct MockView {
2002 control_word: u16,
2003 status_word: u16,
2004 target_position: i32,
2005 profile_velocity: u32,
2006 profile_acceleration: u32,
2007 profile_deceleration: u32,
2008 modes_of_operation: i8,
2009 modes_of_operation_display: i8,
2010 position_actual: i32,
2011 velocity_actual: i32,
2012 error_code: u16,
2013 positive_limit: bool,
2014 negative_limit: bool,
2015 home_sensor: bool,
2016 }
2017
2018 impl MockView {
2019 fn new() -> Self {
2020 Self {
2021 control_word: 0,
2022 status_word: 0x0040, target_position: 0,
2024 profile_velocity: 0,
2025 profile_acceleration: 0,
2026 profile_deceleration: 0,
2027 modes_of_operation: 0,
2028 modes_of_operation_display: 1, position_actual: 0,
2030 velocity_actual: 0,
2031 error_code: 0,
2032 positive_limit: false,
2033 negative_limit: false,
2034 home_sensor: false,
2035 }
2036 }
2037
2038 fn set_state(&mut self, state: u16) {
2039 self.status_word = state;
2040 }
2041 }
2042
2043 impl AxisView for MockView {
2044 fn control_word(&self) -> u16 { self.control_word }
2045 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
2046 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
2047 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
2048 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
2049 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
2050 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
2051 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
2052 fn status_word(&self) -> u16 { self.status_word }
2053 fn position_actual(&self) -> i32 { self.position_actual }
2054 fn velocity_actual(&self) -> i32 { self.velocity_actual }
2055 fn error_code(&self) -> u16 { self.error_code }
2056 fn positive_limit_active(&self) -> bool { self.positive_limit }
2057 fn negative_limit_active(&self) -> bool { self.negative_limit }
2058 fn home_sensor_active(&self) -> bool { self.home_sensor }
2059 }
2060
2061 fn test_config() -> AxisConfig {
2062 AxisConfig::new(12_800).with_user_scale(360.0)
2063 }
2064
2065 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2067 use tokio::sync::mpsc;
2068 let (write_tx, write_rx) = mpsc::unbounded_channel();
2069 let (response_tx, response_rx) = mpsc::unbounded_channel();
2070 let client = CommandClient::new(write_tx, response_rx);
2071 let axis = Axis::new(test_config(), "TestDrive");
2072 (axis, client, response_tx, write_rx)
2073 }
2074
2075 #[test]
2076 fn axis_config_conversion() {
2077 let cfg = test_config();
2078 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2080 }
2081
2082 #[test]
2083 fn enable_sequence_sets_pp_mode_and_shutdown() {
2084 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2085 let mut view = MockView::new();
2086
2087 axis.enable(&mut view);
2088
2089 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
2091 assert_eq!(view.control_word & 0x008F, 0x0006);
2093 assert_eq!(axis.op, AxisOp::Enabling(1));
2095
2096 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2099
2100 assert_eq!(view.control_word & 0x008F, 0x000F);
2102 assert_eq!(axis.op, AxisOp::Enabling(2));
2103
2104 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2107
2108 assert_eq!(axis.op, AxisOp::Idle);
2110 assert!(axis.motor_on);
2111 }
2112
2113 #[test]
2114 fn move_absolute_sets_target() {
2115 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2116 let mut view = MockView::new();
2117 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2122
2123 assert_eq!(view.target_position, 1600);
2125 assert_eq!(view.profile_velocity, 3200);
2127 assert_eq!(view.profile_acceleration, 6400);
2129 assert_eq!(view.profile_deceleration, 6400);
2130 assert!(view.control_word & (1 << 4) != 0);
2132 assert!(view.control_word & (1 << 6) == 0);
2134 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2136 }
2137
2138 #[test]
2139 fn move_relative_sets_relative_bit() {
2140 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2141 let mut view = MockView::new();
2142 view.set_state(0x0027);
2143 axis.tick(&mut view, &mut client);
2144
2145 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2146
2147 assert!(view.control_word & (1 << 6) != 0);
2149 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1, _, _)));
2150 }
2151
2152 #[test]
2153 fn move_completes_on_target_reached() {
2154 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2155 let mut view = MockView::new();
2156 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2158
2159 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2160
2161 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2164 assert!(view.control_word & (1 << 4) == 0);
2166
2167 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2170 assert_eq!(axis.op, AxisOp::Idle);
2172 assert!(!axis.in_motion);
2173 }
2174
2175 #[test]
2176 fn fault_detected_sets_error() {
2177 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2178 let mut view = MockView::new();
2179 view.set_state(0x0008); view.error_code = 0x1234;
2181
2182 axis.tick(&mut view, &mut client);
2183
2184 assert!(axis.is_error);
2185 assert_eq!(axis.error_code, 0x1234);
2186 assert!(axis.error_message.contains("fault"));
2187 }
2188
2189 #[test]
2190 fn fault_recovery_sequence() {
2191 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2192 let mut view = MockView::new();
2193 view.set_state(0x0008); axis.reset_faults(&mut view);
2196 assert!(view.control_word & 0x0080 == 0);
2198
2199 axis.tick(&mut view, &mut client);
2201 assert!(view.control_word & 0x0080 != 0);
2202
2203 view.set_state(0x0040);
2205 axis.tick(&mut view, &mut client);
2206 assert_eq!(axis.op, AxisOp::Idle);
2207 assert!(!axis.is_error);
2208 }
2209
2210 #[test]
2211 fn reset_faults_software_only_is_synchronous() {
2212 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2218 let mut view = MockView::new();
2219 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2221
2222 axis.set_software_min_limit(207.0);
2224 axis.move_absolute(&mut view, 200.0, 90.0, 180.0, 180.0);
2225 assert!(axis.is_error, "move past software limit must set is_error");
2226 assert!(!axis.is_busy);
2227
2228 let cw_before = view.control_word;
2231
2232 axis.reset_faults(&mut view);
2233
2234 assert!(!axis.is_error);
2236 assert_eq!(axis.error_code, 0);
2237 assert!(axis.error_message.is_empty());
2238 assert!(!axis.is_busy);
2239 assert_eq!(axis.op, AxisOp::Idle);
2240 assert_eq!(view.control_word, cw_before);
2242 }
2243
2244 #[test]
2245 fn disable_sequence() {
2246 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2247 let mut view = MockView::new();
2248 view.set_state(0x0027); axis.disable(&mut view);
2251 assert_eq!(view.control_word & 0x008F, 0x0007);
2253
2254 view.set_state(0x0023); axis.tick(&mut view, &mut client);
2257 assert_eq!(axis.op, AxisOp::Idle);
2258 }
2259
2260 #[test]
2261 fn position_tracks_with_home_offset() {
2262 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2263 let mut view = MockView::new();
2264 view.set_state(0x0027);
2265 view.position_actual = 5000;
2266
2267 axis.enable(&mut view);
2269 view.set_state(0x0021);
2270 axis.tick(&mut view, &mut client);
2271 view.set_state(0x0027);
2272 axis.tick(&mut view, &mut client);
2273
2274 assert_eq!(axis.home_offset, 5000);
2276
2277 assert!((axis.position - 0.0).abs() < 0.01);
2279
2280 view.position_actual = 6600;
2282 axis.tick(&mut view, &mut client);
2283
2284 assert!((axis.position - 45.0).abs() < 0.1);
2286 }
2287
2288 #[test]
2289 fn set_position_adjusts_home_offset() {
2290 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2291 let mut view = MockView::new();
2292 view.position_actual = 3200;
2293
2294 axis.set_position(&view, 90.0);
2295 axis.tick(&mut view, &mut client);
2296
2297 assert_eq!(axis.home_offset, 0);
2299 assert!((axis.position - 90.0).abs() < 0.01);
2300 }
2301
2302 #[test]
2303 fn halt_runs_multi_stage_close_out() {
2304 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2305 let mut view = MockView::new();
2306 view.set_state(0x0027);
2307
2308 axis.halt(&mut view);
2309
2310 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2312 assert!(view.control_word & (1 << 4) == 0, "new_setpoint must be cleared");
2313
2314 assert!(matches!(axis.op, AxisOp::Halting(_)),
2316 "halt should enter Halting state, not Idle");
2317 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2318 assert_eq!(step, HaltState::WaitStopped as u8);
2319
2320 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2327 axis.tick(&mut view, &mut client);
2328 }
2329 assert!(matches!(axis.op, AxisOp::Halting(_)));
2331 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2332 assert_eq!(step, HaltState::WaitCancelAck as u8,
2333 "should advance past WaitStopped once position/velocity is stable");
2334
2335 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2339 }
2340
2341 #[test]
2342 fn is_busy_tracks_operations() {
2343 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2344 let mut view = MockView::new();
2345
2346 axis.tick(&mut view, &mut client);
2348 assert!(!axis.is_busy);
2349
2350 axis.enable(&mut view);
2352 axis.tick(&mut view, &mut client);
2353 assert!(axis.is_busy);
2354
2355 view.set_state(0x0021);
2357 axis.tick(&mut view, &mut client);
2358 view.set_state(0x0027);
2359 axis.tick(&mut view, &mut client);
2360 assert!(!axis.is_busy);
2361
2362 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2364 axis.tick(&mut view, &mut client);
2365 assert!(axis.is_busy);
2366 assert!(axis.in_motion);
2367 }
2368
2369 #[test]
2370 fn fault_during_move_cancels_op() {
2371 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2372 let mut view = MockView::new();
2373 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2375
2376 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2378 axis.tick(&mut view, &mut client);
2379 assert!(axis.is_busy);
2380 assert!(!axis.is_error);
2381
2382 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2385
2386 assert!(!axis.is_busy);
2388 assert!(axis.is_error);
2389 assert_eq!(axis.op, AxisOp::Idle);
2390 }
2391
2392 #[test]
2393 fn move_absolute_rejected_by_max_limit() {
2394 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2395 let mut view = MockView::new();
2396 view.set_state(0x0027);
2397 axis.tick(&mut view, &mut client);
2398
2399 axis.set_software_max_limit(90.0);
2400 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2401
2402 assert!(axis.is_error);
2404 assert_eq!(axis.op, AxisOp::Idle);
2405 assert!(axis.error_message.contains("max software limit"));
2406 }
2407
2408 #[test]
2409 fn move_absolute_rejected_by_min_limit() {
2410 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2411 let mut view = MockView::new();
2412 view.set_state(0x0027);
2413 axis.tick(&mut view, &mut client);
2414
2415 axis.set_software_min_limit(-10.0);
2416 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2417
2418 assert!(axis.is_error);
2419 assert_eq!(axis.op, AxisOp::Idle);
2420 assert!(axis.error_message.contains("min software limit"));
2421 }
2422
2423 #[test]
2424 fn move_relative_rejected_by_max_limit() {
2425 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2426 let mut view = MockView::new();
2427 view.set_state(0x0027);
2428 axis.tick(&mut view, &mut client);
2429
2430 axis.set_software_max_limit(50.0);
2432 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2433
2434 assert!(axis.is_error);
2435 assert_eq!(axis.op, AxisOp::Idle);
2436 assert!(axis.error_message.contains("max software limit"));
2437 }
2438
2439 #[test]
2440 fn move_within_limits_allowed() {
2441 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2442 let mut view = MockView::new();
2443 view.set_state(0x0027);
2444 axis.tick(&mut view, &mut client);
2445
2446 axis.set_software_max_limit(90.0);
2447 axis.set_software_min_limit(-90.0);
2448 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2449
2450 assert!(!axis.is_error);
2452 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2453 }
2454
2455 #[test]
2456 fn runtime_limit_halts_move_in_violated_direction() {
2457 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2458 let mut view = MockView::new();
2459 view.set_state(0x0027);
2460 axis.tick(&mut view, &mut client);
2461
2462 axis.set_software_max_limit(45.0);
2463 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2465
2466 view.position_actual = 1650;
2469 view.velocity_actual = 100; view.status_word = 0x1027;
2473 axis.tick(&mut view, &mut client);
2474 view.status_word = 0x0027;
2475 axis.tick(&mut view, &mut client);
2476
2477 assert!(!axis.is_error);
2481 assert!(axis.at_max_limit);
2482 assert!(axis.is_busy);
2483 assert!(matches!(axis.op, AxisOp::Halting(_)));
2484 assert!(view.control_word & (1 << 8) != 0);
2486 }
2487
2488 #[test]
2489 fn runtime_limit_allows_move_in_opposite_direction() {
2490 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2491 let mut view = MockView::new();
2492 view.set_state(0x0027);
2493 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2496 axis.tick(&mut view, &mut client);
2497 assert!(axis.at_max_limit);
2498
2499 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2501 assert!(!axis.is_error);
2502 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2503
2504 view.velocity_actual = -100;
2506 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2508 assert!(!axis.is_error);
2510 }
2511
2512 #[test]
2513 fn positive_limit_switch_halts_positive_move() {
2514 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2515 let mut view = MockView::new();
2516 view.set_state(0x0027);
2517 axis.tick(&mut view, &mut client);
2518
2519 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2521 view.velocity_actual = 100; view.status_word = 0x1027;
2524 axis.tick(&mut view, &mut client);
2525 view.status_word = 0x0027;
2526
2527 view.positive_limit = true;
2529 axis.tick(&mut view, &mut client);
2530
2531 assert!(!axis.is_error);
2536 assert!(axis.at_positive_limit_switch);
2537 assert!(axis.is_busy);
2538 assert!(matches!(axis.op, AxisOp::Halting(_)));
2539 assert!(view.control_word & (1 << 8) != 0);
2541 }
2542
2543 #[test]
2544 fn negative_limit_switch_halts_negative_move() {
2545 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2546 let mut view = MockView::new();
2547 view.set_state(0x0027);
2548 axis.tick(&mut view, &mut client);
2549
2550 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2552 view.velocity_actual = -100; view.status_word = 0x1027;
2554 axis.tick(&mut view, &mut client);
2555 view.status_word = 0x0027;
2556
2557 view.negative_limit = true;
2559 axis.tick(&mut view, &mut client);
2560
2561 assert!(!axis.is_error);
2566 assert!(axis.at_negative_limit_switch);
2567 assert!(axis.is_busy);
2568 assert!(matches!(axis.op, AxisOp::Halting(_)));
2569 assert!(view.control_word & (1 << 8) != 0);
2570 }
2571
2572 #[test]
2573 fn limit_halt_cleanup_then_next_move_starts_cleanly() {
2574 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2579 let mut view = MockView::new();
2580 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2582
2583 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2585 view.velocity_actual = 100;
2586 view.status_word = 0x1027;
2587 axis.tick(&mut view, &mut client);
2588 view.status_word = 0x0027;
2589 view.positive_limit = true;
2590 axis.tick(&mut view, &mut client);
2591
2592 assert!(!axis.is_error);
2594 assert!(axis.is_busy);
2595 assert!(matches!(axis.op, AxisOp::Halting(_)));
2596 assert!(view.control_word & (1 << 8) != 0);
2597
2598 view.velocity_actual = 0;
2603 view.positive_limit = false;
2604 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2605 axis.tick(&mut view, &mut client);
2606 }
2607 assert!(matches!(axis.op, AxisOp::Halting(_)));
2608 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2609 assert_eq!(step, HaltState::WaitCancelAck as u8);
2610
2611 view.status_word = 0x1027;
2613 axis.tick(&mut view, &mut client);
2614 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2615 assert_eq!(step, HaltState::WaitCancelAckClear as u8);
2616
2617 view.status_word = 0x0027;
2620 axis.tick(&mut view, &mut client);
2621 assert_eq!(axis.op, AxisOp::Idle);
2622 assert!(!axis.is_busy);
2623 assert!(view.control_word & (1 << 8) == 0, "halt should be cleared after close-out");
2624
2625 axis.move_absolute(&mut view, 30.0, 90.0, 180.0, 180.0);
2628 assert!(view.control_word & (1 << 4) != 0, "new-setpoint asserted on start_move tick");
2629 assert!(view.control_word & (1 << 8) == 0, "halt stays cleared");
2630 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2631 }
2632
2633 #[test]
2634 fn limit_switch_allows_move_in_opposite_direction() {
2635 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2636 let mut view = MockView::new();
2637 view.set_state(0x0027);
2638 view.positive_limit = true;
2640 view.velocity_actual = -100;
2641 axis.tick(&mut view, &mut client);
2642 assert!(axis.at_positive_limit_switch);
2643
2644 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2646 view.status_word = 0x1027;
2647 axis.tick(&mut view, &mut client);
2648
2649 assert!(!axis.is_error);
2651 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
2652 }
2653
2654 #[test]
2655 fn limit_switch_ignored_when_not_moving() {
2656 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2657 let mut view = MockView::new();
2658 view.set_state(0x0027);
2659 view.positive_limit = true;
2660
2661 axis.tick(&mut view, &mut client);
2662
2663 assert!(axis.at_positive_limit_switch);
2665 assert!(!axis.is_error);
2666 }
2667
2668 #[test]
2669 fn home_sensor_output_tracks_view() {
2670 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2671 let mut view = MockView::new();
2672 view.set_state(0x0027);
2673
2674 axis.tick(&mut view, &mut client);
2675 assert!(!axis.home_sensor);
2676
2677 view.home_sensor = true;
2678 axis.tick(&mut view, &mut client);
2679 assert!(axis.home_sensor);
2680
2681 view.home_sensor = false;
2682 axis.tick(&mut view, &mut client);
2683 assert!(!axis.home_sensor);
2684 }
2685
2686 #[test]
2687 fn velocity_output_converted() {
2688 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2689 let mut view = MockView::new();
2690 view.set_state(0x0027);
2691 view.velocity_actual = 3200;
2693
2694 axis.tick(&mut view, &mut client);
2695
2696 assert!((axis.speed - 90.0).abs() < 0.1);
2697 assert!(axis.moving_positive);
2698 assert!(!axis.moving_negative);
2699 }
2700
2701 fn soft_homing_config() -> AxisConfig {
2704 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2705 cfg.homing_speed = 10.0;
2706 cfg.homing_accel = 20.0;
2707 cfg.homing_decel = 20.0;
2708 cfg
2709 }
2710
2711 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2712 use tokio::sync::mpsc;
2713 let (write_tx, write_rx) = mpsc::unbounded_channel();
2714 let (response_tx, response_rx) = mpsc::unbounded_channel();
2715 let client = CommandClient::new(write_tx, response_rx);
2716 let axis = Axis::new(soft_homing_config(), "TestDrive");
2717 (axis, client, response_tx, write_rx)
2718 }
2719
2720 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2722 view.set_state(0x0027); axis.tick(view, client);
2724 }
2725
2726 fn complete_soft_homing(
2731 axis: &mut Axis,
2732 view: &mut MockView,
2733 client: &mut CommandClient,
2734 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2735 trigger_pos: i32,
2736 clear_sensor: impl FnOnce(&mut MockView),
2737 ) {
2738 use mechutil::ipc::CommandMessage as IpcMsg;
2739
2740 axis.tick(view, client);
2743 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2744
2745 view.position_actual = trigger_pos + 100;
2747 axis.tick(view, client);
2748 view.position_actual = trigger_pos + 120;
2749 axis.tick(view, client);
2750 for _ in 0..10 { axis.tick(view, client); }
2752 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2753
2754 view.status_word = 0x1027;
2756 axis.tick(view, client);
2757 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2758 view.status_word = 0x0027;
2759
2760 for _ in 0..100 { axis.tick(view, client); }
2762 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2763
2764 axis.tick(view, client);
2767 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2768
2769 view.status_word = 0x1027;
2771 axis.tick(view, client);
2772 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2773 view.status_word = 0x0027;
2774
2775 axis.tick(view, client);
2777 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2778 clear_sensor(view);
2779 view.position_actual = trigger_pos - 200;
2780 axis.tick(view, client);
2781 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2782
2783 axis.tick(view, client);
2785 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2786 for _ in 0..10 { axis.tick(view, client); }
2787 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2788
2789 view.status_word = 0x1027;
2792 axis.tick(view, client);
2793 view.status_word = 0x0027;
2794 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2795
2796 let tid = axis.homing_sdo_tid;
2798 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2799 client.poll();
2800 axis.tick(view, client);
2801 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2802
2803 axis.tick(view, client);
2805 let tid = axis.homing_sdo_tid;
2806 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2807 client.poll();
2808 axis.tick(view, client);
2809 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2810
2811 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2813 axis.tick(view, client);
2814 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2815
2816 view.status_word = 0x1427; axis.tick(view, client);
2819 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2820 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2821 view.status_word = 0x0027;
2822
2823 axis.tick(view, client);
2825 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2826
2827 view.status_word = 0x1027;
2829 axis.tick(view, client);
2830 view.status_word = 0x0027;
2831
2832 assert_eq!(axis.op, AxisOp::Idle);
2833 assert!(!axis.is_busy);
2834 assert!(!axis.is_error);
2835 assert_eq!(axis.home_offset, 0); }
2837
2838 #[test]
2839 fn soft_homing_pnp_home_sensor_full_sequence() {
2840 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2841 let mut view = MockView::new();
2842 enable_axis(&mut axis, &mut view, &mut client);
2843
2844 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2845
2846 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2849 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2851 axis.tick(&mut view, &mut client); view.home_sensor = true;
2855 view.position_actual = 5000;
2856 axis.tick(&mut view, &mut client);
2857 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2858
2859 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2860 |v| { v.home_sensor = false; });
2861 }
2862
2863 #[test]
2864 fn soft_homing_npn_home_sensor_full_sequence() {
2865 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2866 let mut view = MockView::new();
2867 view.home_sensor = true;
2869 enable_axis(&mut axis, &mut view, &mut client);
2870
2871 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2872
2873 axis.tick(&mut view, &mut client);
2875 view.status_word = 0x1027;
2876 axis.tick(&mut view, &mut client);
2877 view.status_word = 0x0027;
2878 axis.tick(&mut view, &mut client);
2879
2880 view.home_sensor = false;
2882 view.position_actual = 3000;
2883 axis.tick(&mut view, &mut client);
2884 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2885
2886 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2887 |v| { v.home_sensor = true; }); }
2889
2890 #[test]
2891 fn soft_homing_limit_switch_suppresses_halt() {
2892 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2893 let mut view = MockView::new();
2894 enable_axis(&mut axis, &mut view, &mut client);
2895
2896 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
2898
2899 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2904 axis.tick(&mut view, &mut client); view.positive_limit = true;
2908 view.velocity_actual = 100; view.position_actual = 8000;
2910 axis.tick(&mut view, &mut client);
2911
2912 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2914 assert!(!axis.is_error);
2915 }
2916
2917 #[test]
2918 fn soft_homing_opposite_limit_still_protects() {
2919 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2920 let mut view = MockView::new();
2921 enable_axis(&mut axis, &mut view, &mut client);
2922
2923 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2925
2926 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2931 axis.tick(&mut view, &mut client); view.negative_limit = true;
2936 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
2938
2939 assert!(axis.is_error);
2941 assert!(axis.error_message.contains("Negative limit switch"));
2942 }
2943
2944 #[test]
2945 #[test]
2963 fn soft_homing_negative_direction_sets_negative_target() {
2964 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2965 let mut view = MockView::new();
2966 enable_axis(&mut axis, &mut view, &mut client);
2967
2968 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
2969 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
2973 }
2974
2975 #[test]
2976 fn home_integrated_method_starts_hardware_homing() {
2977 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2978 let mut view = MockView::new();
2979 enable_axis(&mut axis, &mut view, &mut client);
2980
2981 axis.home(&mut view, HomingMethod::CurrentPosition);
2982 assert!(matches!(axis.op, AxisOp::Homing(0)));
2983 assert_eq!(axis.homing_method, 37);
2984 }
2985
2986 #[test]
2987 fn home_integrated_arbitrary_code() {
2988 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2989 let mut view = MockView::new();
2990 enable_axis(&mut axis, &mut view, &mut client);
2991
2992 axis.home(&mut view, HomingMethod::Integrated(35));
2993 assert!(matches!(axis.op, AxisOp::Homing(0)));
2994 assert_eq!(axis.homing_method, 35);
2995 }
2996
2997 #[test]
2998 fn hardware_homing_skips_speed_sdos_when_zero() {
2999 use mechutil::ipc::CommandMessage;
3000
3001 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
3002 let mut view = MockView::new();
3003 enable_axis(&mut axis, &mut view, &mut client);
3004
3005 axis.home(&mut view, HomingMethod::Integrated(37));
3007
3008 axis.tick(&mut view, &mut client);
3010 assert!(matches!(axis.op, AxisOp::Homing(1)));
3011
3012 let _ = write_rx.try_recv();
3014
3015 let tid = axis.homing_sdo_tid;
3017 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3018 client.poll();
3019 axis.tick(&mut view, &mut client);
3020
3021 assert!(matches!(axis.op, AxisOp::Homing(8)));
3023 }
3024
3025 #[test]
3026 fn hardware_homing_writes_speed_sdos_when_nonzero() {
3027 use mechutil::ipc::CommandMessage;
3028
3029 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3030 let mut view = MockView::new();
3031 enable_axis(&mut axis, &mut view, &mut client);
3032
3033 axis.home(&mut view, HomingMethod::Integrated(37));
3035
3036 axis.tick(&mut view, &mut client);
3038 assert!(matches!(axis.op, AxisOp::Homing(1)));
3039 let _ = write_rx.try_recv();
3040
3041 let tid = axis.homing_sdo_tid;
3043 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3044 client.poll();
3045 axis.tick(&mut view, &mut client);
3046 assert!(matches!(axis.op, AxisOp::Homing(2)));
3048 }
3049
3050 #[test]
3051 fn soft_homing_edge_during_ack_step() {
3052 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3053 let mut view = MockView::new();
3054 enable_axis(&mut axis, &mut view, &mut client);
3055
3056 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3057 axis.tick(&mut view, &mut client); view.home_sensor = true;
3061 view.position_actual = 2000;
3062 axis.tick(&mut view, &mut client);
3063
3064 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3066 }
3067
3068 #[test]
3069 fn soft_homing_applies_home_position() {
3070 let mut cfg = soft_homing_config();
3071 cfg.home_position = 90.0;
3072
3073 use tokio::sync::mpsc;
3074 let (write_tx, _write_rx) = mpsc::unbounded_channel();
3075 let (resp_tx, response_rx) = mpsc::unbounded_channel();
3076 let mut client = CommandClient::new(write_tx, response_rx);
3077 let mut axis = Axis::new(cfg, "TestDrive");
3078
3079 let mut view = MockView::new();
3080 enable_axis(&mut axis, &mut view, &mut client);
3081
3082 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3083
3084 axis.tick(&mut view, &mut client);
3086 view.status_word = 0x1027;
3087 axis.tick(&mut view, &mut client);
3088 view.status_word = 0x0027;
3089 axis.tick(&mut view, &mut client);
3090
3091 view.home_sensor = true;
3093 view.position_actual = 5000;
3094 axis.tick(&mut view, &mut client);
3095 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3096
3097 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3099 |v| { v.home_sensor = false; });
3100
3101 assert_eq!(axis.home_offset, 0);
3103 }
3104
3105 #[test]
3106 fn soft_homing_default_home_position_zero() {
3107 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3108 let mut view = MockView::new();
3109 enable_axis(&mut axis, &mut view, &mut client);
3110
3111 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3112
3113 axis.tick(&mut view, &mut client);
3115 view.status_word = 0x1027;
3116 axis.tick(&mut view, &mut client);
3117 view.status_word = 0x0027;
3118 axis.tick(&mut view, &mut client);
3119
3120 view.home_sensor = true;
3122 view.position_actual = 5000;
3123 axis.tick(&mut view, &mut client);
3124
3125 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3126 |v| { v.home_sensor = false; });
3127
3128 assert_eq!(axis.home_offset, 0);
3129 }
3130}