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),
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 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute);
327 }
328
329 pub fn move_relative(
335 &mut self,
336 view: &mut impl AxisView,
337 distance: f64,
338 vel: f64,
339 accel: f64,
340 decel: f64,
341 ) {
342 log::info!("Axis: request to move relative dist {} vel {} accel {} decel {}",
343 distance, vel, accel, decel
344 );
345
346 if let Some(msg) = self.check_target_limit(self.position + distance, view) {
347 self.set_op_error(&msg);
348 return;
349 }
350
351 let cpu = self.config.counts_per_user();
352 let raw_distance = self.config.to_counts(distance).round() as i32;
353 let raw_vel = (vel * cpu).round() as u32;
354 let raw_accel = (accel * cpu).round() as u32;
355 let raw_decel = (decel * cpu).round() as u32;
356
357 log::info!("Axis starting relative move: request to move relative raw dist {} raw vel {} raw accel {} raw decel {}",
358 raw_distance, raw_vel, raw_accel, raw_decel
359 );
360
361 let mut cw = RawControlWord(view.control_word());
363 cw.set_bit(4, false); view.set_control_word(cw.raw());
365
366 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative);
367 }
368
369 fn start_move(
370 &mut self,
371 view: &mut impl AxisView,
372 raw_target: i32,
373 raw_vel: u32,
374 raw_accel: u32,
375 raw_decel: u32,
376 kind: MoveKind,
377 ) {
378 self.pending_move_target = raw_target;
379 self.pending_move_vel = raw_vel;
380 self.pending_move_accel = raw_accel;
381 self.pending_move_decel = raw_decel;
382
383 view.set_target_position(raw_target);
385 view.set_profile_velocity(raw_vel);
386 view.set_profile_acceleration(raw_accel);
387 view.set_profile_deceleration(raw_decel);
388
389 let mut cw = RawControlWord(view.control_word());
396 cw.set_bit(6, kind == MoveKind::Relative);
397 cw.set_bit(8, false); cw.set_bit(4, true); view.set_control_word(cw.raw());
400
401 self.op = AxisOp::Moving(kind, 1);
402 self.op_started = Some(Instant::now());
403 }
404
405 pub fn halt(&mut self, view: &mut impl AxisView) {
423 self.command_halt(view);
424 self.halt_stable_count = 0;
425 self.last_raw_position = view.position_actual();
426 self.op_started = Some(Instant::now());
427 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
428 }
429
430 pub fn enable(&mut self, view: &mut impl AxisView) {
438 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
440 let mut cw = RawControlWord(view.control_word());
441 cw.cmd_shutdown();
442 view.set_control_word(cw.raw());
443
444 self.op = AxisOp::Enabling(1);
445 self.op_started = Some(Instant::now());
446 }
447
448 pub fn disable(&mut self, view: &mut impl AxisView) {
450 let mut cw = RawControlWord(view.control_word());
451 cw.cmd_disable_operation();
452 view.set_control_word(cw.raw());
453
454 self.op = AxisOp::Disabling(1);
455 self.op_started = Some(Instant::now());
456 }
457
458 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
462 let mut cw = RawControlWord(view.control_word());
464 cw.cmd_clear_fault_reset();
465 view.set_control_word(cw.raw());
466
467 self.is_error = false;
468 self.error_code = 0;
469 self.error_message.clear();
470 self.op = AxisOp::FaultRecovery(1);
471 self.op_started = Some(Instant::now());
472 }
473
474 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
482 if method.is_integrated() {
483 self.homing_method = method.cia402_code();
484 self.op = AxisOp::Homing(0);
485 self.op_started = Some(Instant::now());
486 let _ = view;
487 } else {
488 self.configure_soft_homing(method);
489 self.start_soft_homing(view);
490 }
491 }
492
493 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
502 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
503 }
504
505 pub fn set_home_position(&mut self, user_units: f64) {
509 self.config.home_position = user_units;
510 }
511
512 pub fn set_software_max_limit(&mut self, user_units: f64) {
514 self.config.max_position_limit = user_units;
515 self.config.enable_max_position_limit = true;
516 }
517
518 pub fn set_software_min_limit(&mut self, user_units: f64) {
520 self.config.min_position_limit = user_units;
521 self.config.enable_min_position_limit = true;
522 }
523
524 pub fn sdo_write(
530 &mut self,
531 client: &mut CommandClient,
532 index: u16,
533 sub_index: u8,
534 value: serde_json::Value,
535 ) {
536 self.sdo.write(client, index, sub_index, value);
537 }
538
539 pub fn sdo_read(
541 &mut self,
542 client: &mut CommandClient,
543 index: u16,
544 sub_index: u8,
545 ) -> u32 {
546 self.sdo.read(client, index, sub_index)
547 }
548
549 pub fn sdo_result(
551 &mut self,
552 client: &mut CommandClient,
553 tid: u32,
554 ) -> SdoResult {
555 self.sdo.result(client, tid, Duration::from_secs(5))
556 }
557
558 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
572 self.check_faults(view);
573 self.progress_op(view, client);
574 self.update_outputs(view);
575 self.check_limits(view);
576 }
577
578 fn update_outputs(&mut self, view: &impl AxisView) {
583 let raw = view.position_actual();
584 self.raw_position = raw as i64;
585 self.position = self.config.to_user((raw - self.home_offset) as f64);
586
587 let vel = view.velocity_actual();
588 let user_vel = self.config.to_user(vel as f64);
589 self.speed = user_vel.abs();
590 self.moving_positive = user_vel > 0.0;
591 self.moving_negative = user_vel < 0.0;
592 self.is_busy = self.op != AxisOp::Idle;
593 self.in_motion = matches!(self.op, AxisOp::Moving(_, _) | AxisOp::SoftHoming(_));
594
595 let sw = RawStatusWord(view.status_word());
596 self.motor_on = sw.state() == Cia402State::OperationEnabled;
597
598 self.last_raw_position = raw;
599 }
600
601 fn check_faults(&mut self, view: &impl AxisView) {
606 let sw = RawStatusWord(view.status_word());
607 let state = sw.state();
608
609 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
610 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
611 self.is_error = true;
612 let ec = view.error_code();
613 if ec != 0 {
614 self.error_code = ec as u32;
615 }
616 self.error_message = format!("Drive fault (state: {})", state);
617 self.op = AxisOp::Idle;
619 self.op_started = None;
620 }
621 }
622 }
623
624 fn op_timed_out(&self) -> bool {
629 self.op_started
630 .map_or(false, |t| t.elapsed() > self.op_timeout)
631 }
632
633 fn homing_timed_out(&self) -> bool {
634 self.op_started
635 .map_or(false, |t| t.elapsed() > self.homing_timeout)
636 }
637
638 fn move_start_timed_out(&self) -> bool {
639 self.op_started
640 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
641 }
642
643 fn op_stage_timed_out(&self, limit: Duration) -> bool {
647 self.op_started
648 .map_or(false, |t| t.elapsed() > limit)
649 }
650
651 fn set_op_error(&mut self, msg: &str) {
652 self.is_error = true;
653 self.error_message = msg.to_string();
654 self.op = AxisOp::Idle;
655 self.op_started = None;
656 self.is_busy = false;
657 self.in_motion = false;
658 log::error!("Axis error: {}", msg);
659 }
660
661 fn restore_pp_after_error(&mut self, msg: &str) {
662 self.is_error = true;
663 self.error_message = msg.to_string();
664 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);;
665 log::error!("Axis error: {}", msg);
666 }
667
668 fn finish_op_error(&mut self) {
669 self.op = AxisOp::Idle;
670 self.op_started = None;
671 self.is_busy = false;
672 self.in_motion = false;
673 }
674
675 fn complete_op(&mut self) {
676 self.op = AxisOp::Idle;
677 self.op_started = None;
678 }
679
680 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
689 let static_limit = if self.config.enable_max_position_limit {
690 Some(self.config.max_position_limit)
691 } else {
692 None
693 };
694 match (static_limit, view.dynamic_max_position_limit()) {
695 (Some(s), Some(d)) => Some(s.min(d)),
696 (Some(v), None) | (None, Some(v)) => Some(v),
697 (None, None) => None,
698 }
699 }
700
701 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
705 let static_limit = if self.config.enable_min_position_limit {
706 Some(self.config.min_position_limit)
707 } else {
708 None
709 };
710 match (static_limit, view.dynamic_min_position_limit()) {
711 (Some(s), Some(d)) => Some(s.max(d)),
712 (Some(v), None) | (None, Some(v)) => Some(v),
713 (None, None) => None,
714 }
715 }
716
717 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
722 if let Some(max) = self.effective_max_limit(view) {
723 if target > max {
724 return Some(format!(
725 "Target {:.3} exceeds max software limit {:.3}",
726 target, max
727 ));
728 }
729 }
730 if let Some(min) = self.effective_min_limit(view) {
731 if target < min {
732 return Some(format!(
733 "Target {:.3} exceeds min software limit {:.3}",
734 target, min
735 ));
736 }
737 }
738 None
739 }
740
741 fn check_limits(&mut self, view: &mut impl AxisView) {
750 let eff_max = self.effective_max_limit(view);
752 let eff_min = self.effective_min_limit(view);
753 let sw_max = eff_max.map_or(false, |m| self.position >= m);
754 let sw_min = eff_min.map_or(false, |m| self.position <= m);
755
756 self.at_max_limit = sw_max;
757 self.at_min_limit = sw_min;
758
759 let hw_pos = view.positive_limit_active();
761 let hw_neg = view.negative_limit_active();
762
763 self.at_positive_limit_switch = hw_pos;
764 self.at_negative_limit_switch = hw_neg;
765
766 self.home_sensor = view.home_sensor_active();
768
769 self.prev_positive_limit = hw_pos;
771 self.prev_negative_limit = hw_neg;
772 self.prev_home_sensor = view.home_sensor_active();
773
774 let is_moving = matches!(self.op, AxisOp::Moving(_, _));
776 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
777
778 if !is_moving && !is_soft_homing {
779 return;
780 }
781
782 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
784 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
785
786 let effective_hw_pos = hw_pos && !suppress_pos;
787 let effective_hw_neg = hw_neg && !suppress_neg;
788
789 let effective_sw_max = sw_max && !is_soft_homing;
791 let effective_sw_min = sw_min && !is_soft_homing;
792
793 let positive_blocked = (effective_sw_max || effective_hw_pos) && self.moving_positive;
794 let negative_blocked = (effective_sw_min || effective_hw_neg) && self.moving_negative;
795
796 if positive_blocked || negative_blocked {
797 let mut cw = RawControlWord(view.control_word());
798 cw.set_bit(8, true); view.set_control_word(cw.raw());
800
801 let msg = if effective_hw_pos && self.moving_positive {
802 "Positive limit switch active".to_string()
803 } else if effective_hw_neg && self.moving_negative {
804 "Negative limit switch active".to_string()
805 } else if effective_sw_max && self.moving_positive {
806 format!(
807 "Software position limit: position {:.3} >= max {:.3}",
808 self.position, eff_max.unwrap_or(self.position)
809 )
810 } else {
811 format!(
812 "Software position limit: position {:.3} <= min {:.3}",
813 self.position, eff_min.unwrap_or(self.position)
814 )
815 };
816 self.set_op_error(&msg);
817 }
818 }
819
820 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
825 match self.op.clone() {
826 AxisOp::Idle => {}
827 AxisOp::Enabling(step) => self.tick_enabling(view, step),
828 AxisOp::Disabling(step) => self.tick_disabling(view, step),
829 AxisOp::Moving(kind, step) => self.tick_moving(view, kind, step),
830 AxisOp::Homing(step) => self.tick_homing(view, client, step),
831 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
832 AxisOp::Halting(step) => self.tick_halting(view, step),
833 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
834 }
835 }
836
837 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
842 match step {
843 1 => {
844 let sw = RawStatusWord(view.status_word());
845 if sw.state() == Cia402State::ReadyToSwitchOn {
846 let mut cw = RawControlWord(view.control_word());
847 cw.cmd_enable_operation();
848 view.set_control_word(cw.raw());
849 self.op = AxisOp::Enabling(2);
850 } else if self.op_timed_out() {
851 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
852 }
853 }
854 2 => {
855 let sw = RawStatusWord(view.status_word());
856 if sw.state() == Cia402State::OperationEnabled {
857 self.complete_op();
864 } else if self.op_timed_out() {
865 self.set_op_error("Enable timeout: waiting for OperationEnabled");
866 }
867 }
868 _ => self.complete_op(),
869 }
870 }
871
872 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
876 match step {
877 1 => {
878 let sw = RawStatusWord(view.status_word());
879 if sw.state() != Cia402State::OperationEnabled {
880 self.complete_op();
881 } else if self.op_timed_out() {
882 self.set_op_error("Disable timeout: drive still in OperationEnabled");
883 }
884 }
885 _ => self.complete_op(),
886 }
887 }
888
889 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8) {
895 match step {
896 1 => {
897 let sw = RawStatusWord(view.status_word());
899 if sw.raw() & (1 << 12) != 0 {
900 let mut cw = RawControlWord(view.control_word());
902 cw.set_bit(4, false);
903 view.set_control_word(cw.raw());
904 self.op = AxisOp::Moving(kind, 2);
905 } else if self.move_start_timed_out() {
906 self.set_op_error("Move timeout: set-point not acknowledged");
907 }
908 },
909 2 => {
910 let sw = RawStatusWord(view.status_word());
912 if sw.raw() & (1 << 12) == 0 {
913 self.op = AxisOp::Moving(kind, 3);
915 }
916 },
917 3 => {
918 let sw = RawStatusWord(view.status_word());
920 if sw.target_reached() {
921 self.complete_op();
922 }
923 },
924 _ => self.complete_op(),
925 }
926 }
927
928 fn tick_homing(
946 &mut self,
947 view: &mut impl AxisView,
948 client: &mut CommandClient,
949 step: u8,
950 ) {
951 match step {
952 0 => {
953 self.homing_sdo_tid = self.sdo.write(
955 client,
956 0x6098,
957 0,
958 json!(self.homing_method),
959 );
960 self.op = AxisOp::Homing(1);
961 }
962 1 => {
963 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
965 SdoResult::Ok(_) => {
966 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
968 self.op = AxisOp::Homing(8);
969 } else {
970 self.op = AxisOp::Homing(2);
971 }
972 }
973 SdoResult::Pending => {
974 if self.homing_timed_out() {
975 self.set_op_error("Homing timeout: SDO write for homing method");
976 }
977 }
978 SdoResult::Err(e) => {
979 self.set_op_error(&format!("Homing SDO error: {}", e));
980 }
981 SdoResult::Timeout => {
982 self.set_op_error("Homing timeout: SDO write timed out");
983 }
984 }
985 }
986 2 => {
987 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
989 self.homing_sdo_tid = self.sdo.write(
990 client,
991 0x6099,
992 1,
993 json!(speed_counts),
994 );
995 self.op = AxisOp::Homing(3);
996 }
997 3 => {
998 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
999 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
1000 SdoResult::Pending => {
1001 if self.homing_timed_out() {
1002 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
1003 }
1004 }
1005 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1006 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1007 }
1008 }
1009 4 => {
1010 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1012 self.homing_sdo_tid = self.sdo.write(
1013 client,
1014 0x6099,
1015 2,
1016 json!(speed_counts),
1017 );
1018 self.op = AxisOp::Homing(5);
1019 }
1020 5 => {
1021 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1022 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
1023 SdoResult::Pending => {
1024 if self.homing_timed_out() {
1025 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1026 }
1027 }
1028 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1029 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1030 }
1031 }
1032 6 => {
1033 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1035 self.homing_sdo_tid = self.sdo.write(
1036 client,
1037 0x609A,
1038 0,
1039 json!(accel_counts),
1040 );
1041 self.op = AxisOp::Homing(7);
1042 }
1043 7 => {
1044 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1045 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
1046 SdoResult::Pending => {
1047 if self.homing_timed_out() {
1048 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1049 }
1050 }
1051 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1052 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1053 }
1054 }
1055 8 => {
1056 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1059 let mut cw = RawControlWord(view.control_word());
1060 cw.set_bit(4, false);
1061 view.set_control_word(cw.raw());
1062 self.op = AxisOp::Homing(9);
1063 }
1064 9 => {
1065 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
1067 self.op = AxisOp::Homing(10);
1068 } else if self.homing_timed_out() {
1069 self.set_op_error("Homing timeout: mode not confirmed");
1070 }
1071 }
1072 10 => {
1073 let mut cw = RawControlWord(view.control_word());
1075 cw.set_bit(4, true);
1076 view.set_control_word(cw.raw());
1077 self.op = AxisOp::Homing(11);
1078 }
1079 11 => {
1080 let sw = view.status_word();
1085 let error = sw & (1 << 13) != 0;
1086 if error {
1087 self.set_op_error("Homing error: drive reported homing failure");
1088 } else if sw & (1 << 12) == 0 {
1089 self.op = AxisOp::Homing(12);
1090 } else if self.homing_timed_out() {
1091 self.set_op_error(&format!("Homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1092 }
1093 }
1094 12 => {
1095 let sw = view.status_word();
1098 let error = sw & (1 << 13) != 0;
1099 let attained = sw & (1 << 12) != 0;
1100 let reached = sw & (1 << 10) != 0;
1101
1102 if error {
1103 self.set_op_error("Homing error: drive reported homing failure");
1104 } else if attained && reached {
1105 self.op = AxisOp::Homing(13);
1106 } else if self.homing_timed_out() {
1107 self.set_op_error("Homing timeout: procedure did not complete");
1108 }
1109 }
1110 13 => {
1111 self.home_offset = view.position_actual()
1114 - self.config.to_counts(self.config.home_position).round() as i32;
1115 let mut cw = RawControlWord(view.control_word());
1117 cw.set_bit(4, false);
1118 view.set_control_word(cw.raw());
1119 self.op = AxisOp::Homing(14);
1120 }
1121 14 => {
1122 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1125 log::info!("Homing complete — home offset: {}", self.home_offset);
1126 self.complete_op();
1127 }
1128 _ => self.complete_op(),
1129 }
1130 }
1131
1132 fn configure_soft_homing(&mut self, method: HomingMethod) {
1135 match method {
1136 HomingMethod::LimitSwitchPosPnp => {
1137 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1138 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1139 self.soft_home_direction = 1.0;
1140 }
1141 HomingMethod::LimitSwitchNegPnp => {
1142 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1143 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1144 self.soft_home_direction = -1.0;
1145 }
1146 HomingMethod::LimitSwitchPosNpn => {
1147 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1148 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1149 self.soft_home_direction = 1.0;
1150 }
1151 HomingMethod::LimitSwitchNegNpn => {
1152 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1153 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1154 self.soft_home_direction = -1.0;
1155 }
1156 HomingMethod::HomeSensorPosPnp => {
1157 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1158 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1159 self.soft_home_direction = 1.0;
1160 }
1161 HomingMethod::HomeSensorNegPnp => {
1162 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1163 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1164 self.soft_home_direction = -1.0;
1165 }
1166 HomingMethod::HomeSensorPosNpn => {
1167 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1168 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1169 self.soft_home_direction = 1.0;
1170 }
1171 HomingMethod::HomeSensorNegNpn => {
1172 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1173 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1174 self.soft_home_direction = -1.0;
1175 }
1176 _ => {} }
1178 }
1179
1180 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1181 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1182 self.op_started = Some(Instant::now());
1183 }
1184
1185 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1186 let raw = match self.soft_home_sensor {
1187 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1188 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1189 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1190 };
1191 match self.soft_home_sensor_type {
1192 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1195 }
1196
1197
1198 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1201 let dir = if !self.config.invert_direction {
1202 direction
1203 }
1204 else {
1205 -direction
1206 };
1207
1208 let target = if dir > 0.0 {
1209 i32::MAX
1210 }
1211 else {
1212 i32::MIN
1213 };
1214
1215 return target;
1216 }
1217
1218
1219 pub fn command_halt(&self, view: &mut impl AxisView) {
1224 let mut cw = RawControlWord(view.control_word());
1225 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1228 }
1229
1230
1231 pub fn command_cancel_move(&self, view: &mut impl AxisView) {
1239
1240 let mut cw = RawControlWord(view.control_word());
1241 cw.set_bit(4, true); cw.set_bit(5, true); cw.set_bit(6, false); cw.set_bit(8, false); view.set_control_word(cw.raw());
1246
1247 let current_pos = view.position_actual();
1248 view.set_target_position(current_pos);
1249 view.set_profile_velocity(0);
1250 }
1251
1252
1253 fn command_homing_speed(&self, view: &mut impl AxisView) {
1255 let cpu = self.config.counts_per_user();
1256 let vel = (self.config.homing_speed * cpu).round() as u32;
1257 let accel = (self.config.homing_accel * cpu).round() as u32;
1258 let decel = (self.config.homing_decel * cpu).round() as u32;
1259 view.set_profile_velocity(vel);
1260 view.set_profile_acceleration(accel);
1261 view.set_profile_deceleration(decel);
1262 }
1263
1264 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1280 match HomeState::from_repr(step) {
1281
1282 Some(HomeState::EnsurePpMode) => {
1283 log::info!("SoftHome: Ensuring PP mode..");
1288 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1289 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1290 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1291 },
1292 Some(HomeState::WaitPpMode) => {
1293
1294 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1295 if !self.fb_mode_of_operation.is_busy() {
1296 if self.fb_mode_of_operation.is_error() {
1297 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1298 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1299 ));
1300 }
1301 else {
1302 log::info!("SoftHome: Drive is in PP mode!");
1303
1304 if !self.check_soft_home_trigger(view) {
1308 log::info!("SoftHome: Not on home switch; seek out.");
1309 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1310 } else {
1311 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1312 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1313 }
1314 }
1315 }
1316
1317
1318 },
1319
1320 Some(HomeState::Search) => {
1322 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1323
1324 let target = self.calculate_max_relative_target(self.soft_home_direction);
1334 view.set_target_position(target);
1335
1336 self.command_homing_speed(view);
1345
1346 let mut cw = RawControlWord(view.control_word());
1347 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());
1352
1353 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1354 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1355 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1356 }
1357 Some(HomeState::WaitSearching) => {
1358 if self.check_soft_home_trigger(view) {
1359 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1360 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1361 return;
1362 }
1363 let sw = RawStatusWord(view.status_word());
1364 if sw.raw() & (1 << 12) != 0 {
1365 let mut cw = RawControlWord(view.control_word());
1366 cw.set_bit(4, false);
1367 view.set_control_word(cw.raw());
1368 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1369 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1370 } else if self.homing_timed_out() {
1371 self.set_op_error("Software homing timeout: set-point not acknowledged");
1372 }
1373 }
1374 Some(HomeState::WaitFoundSensor) => {
1384 if self.check_soft_home_trigger(view) {
1385 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1386 log::info!("ControlWord is : {} ", view.control_word());
1387
1388 let mut cw = RawControlWord(view.control_word());
1389 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1392
1393
1394 self.halt_stable_count = 0;
1395 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1396 } else if self.homing_timed_out() {
1397 self.set_op_error("Software homing timeout: sensor not detected");
1398 }
1399 }
1400
1401
1402 Some(HomeState::WaitStoppedFoundSensor) => {
1403 const STABLE_WINDOW: i32 = 1;
1404 const STABLE_TICKS_REQUIRED: u8 = 10;
1405
1406 let pos = view.position_actual();
1411 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1412 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1413 } else {
1414 self.halt_stable_count = 0;
1415 }
1416
1417 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1418
1419 log::debug!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1420 self.command_cancel_move(view);
1421 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1422
1423 } else if self.homing_timed_out() {
1424 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1425 }
1426 }
1427 Some(HomeState::WaitFoundSensorAck) => {
1428 let sw = RawStatusWord(view.status_word());
1429 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1430
1431 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1432
1433 let mut cw = RawControlWord(view.control_word());
1435 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1438
1439 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1440
1441 } else if self.homing_timed_out() {
1442 self.set_op_error("Software homing timeout: cancel not acknowledged");
1443 }
1444 },
1445 Some(HomeState::WaitFoundSensorAckClear) => {
1446 let sw = RawStatusWord(view.status_word());
1447 if sw.raw() & (1 << 12) == 0 {
1449
1450 let mut cw = RawControlWord(view.control_word());
1452 cw.set_bit(8, false);
1453 view.set_control_word(cw.raw());
1454
1455 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1456 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1457 self.ton.call(false, Duration::from_secs(3));
1458 }
1459 },
1460 Some(HomeState::DebounceFoundSensor) => {
1462 self.ton.call(true, Duration::from_secs(3));
1463
1464 let sw = RawStatusWord(view.status_word());
1465 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1466 self.ton.call(false, Duration::from_secs(3));
1467 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1468 view.position_actual(), view.control_word(), view.status_word());
1469 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1470 }
1471 }
1472
1473 Some(HomeState::BackOff) => {
1475
1476 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1477 view.set_target_position(target);
1478
1479
1480 self.command_homing_speed(view);
1481
1482 let mut cw = RawControlWord(view.control_word());
1483 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1487 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1488 target, self.config.homing_speed, view.position_actual(), cw.raw());
1489 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1490 }
1491 Some(HomeState::WaitBackingOff) => {
1492 let sw = RawStatusWord(view.status_word());
1493 if sw.raw() & (1 << 12) != 0 {
1494 let mut cw = RawControlWord(view.control_word());
1495 cw.set_bit(4, false);
1496 view.set_control_word(cw.raw());
1497 log::debug!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1498 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1499 } else if self.homing_timed_out() {
1500 self.set_op_error("Software homing timeout: back-off not acknowledged");
1501 }
1502 }
1503 Some(HomeState::WaitLostSensor) => {
1504 if !self.check_soft_home_trigger(view) {
1505 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1506
1507 self.command_halt(view);
1508 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1509 } else if self.homing_timed_out() {
1510 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1511 }
1512 }
1513 Some(HomeState::WaitStoppedLostSensor) => {
1514 const STABLE_WINDOW: i32 = 1;
1515 const STABLE_TICKS_REQUIRED: u8 = 10;
1516
1517 let mut cw = RawControlWord(view.control_word());
1518 cw.set_bit(8, true);
1519 view.set_control_word(cw.raw());
1520
1521 let pos = view.position_actual();
1522 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1523 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1524 } else {
1525 self.halt_stable_count = 0;
1526 }
1527
1528 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1529 log::debug!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1530 self.command_cancel_move(view);
1531 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1532 } else if self.homing_timed_out() {
1533 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1534 }
1535 }
1536 Some(HomeState::WaitLostSensorAck) => {
1537 let sw = RawStatusWord(view.status_word());
1538 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1539
1540 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1541
1542 let mut cw = RawControlWord(view.control_word());
1544 cw.set_bit(4, false); view.set_control_word(cw.raw());
1546
1547 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1548
1549
1550 } else if self.homing_timed_out() {
1551 self.set_op_error("Software homing timeout: cancel not acknowledged");
1552 }
1553 }
1554 Some(HomeState::WaitLostSensorAckClear) => {
1555 let sw = RawStatusWord(view.status_word());
1557 if sw.raw() & (1 << 12) == 0 {
1558
1559 let mut cw = RawControlWord(view.control_word());
1561 cw.set_bit(8, false);
1562 view.set_control_word(cw.raw());
1563
1564
1565 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1566 self.homing_sdo_tid = self.sdo.write(
1569 client, 0x607C, 0, json!(desired_counts),
1570 );
1571
1572 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1573 self.config.home_position, desired_counts
1574 );
1575
1576 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1577
1578 }
1579 },
1580
1581 Some(HomeState::WaitHomeOffsetDone) => {
1582 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1584 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1585 SdoResult::Pending => {
1586 if self.homing_timed_out() {
1587 self.set_op_error("Software homing timeout: home offset SDO write");
1588 }
1589 }
1590 SdoResult::Err(e) => {
1591 self.set_op_error(&format!("Software homing SDO error: {}", e));
1592 }
1593 SdoResult::Timeout => {
1594 self.set_op_error("Software homing: home offset SDO timed out");
1595 }
1596 }
1597 },
1598 Some(HomeState::WriteHomingModeOp) => {
1599
1600 self.fb_mode_of_operation.reset();
1604 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1605 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1606 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1607
1608
1609 },
1610 Some(HomeState::WaitWriteHomingModeOp) => {
1611 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1613
1614 if !self.fb_mode_of_operation.is_busy() {
1615 if self.fb_mode_of_operation.is_error() {
1616 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1617 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1618 ));
1619 }
1620 else {
1621 log::info!("SoftHome: Drive is now in Homing Mode.");
1622 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1623 }
1624 }
1625 },
1626 Some(HomeState::WriteHomingMethod) => {
1627 self.homing_sdo_tid = self.sdo.write(
1629 client, 0x6098, 0, json!(37i8),
1630 );
1631 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1632 }
1633 Some(HomeState::WaitWriteHomingMethodDone) => {
1634 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1636 SdoResult::Ok(_) => {
1637 log::info!("SoftHome: Successfully wrote homing method.");
1638 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1639 }
1640 SdoResult::Pending => {
1641 if self.homing_timed_out() {
1642 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1643 }
1644 }
1645 SdoResult::Err(e) => {
1646 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1647 }
1648 SdoResult::Timeout => {
1649 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1650 }
1651 }
1652 }
1653 Some(HomeState::ClearHomingTrigger) => {
1654 let mut cw = RawControlWord(view.control_word());
1657 cw.set_bit(4, false);
1658 view.set_control_word(cw.raw());
1659 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1660 }
1661 Some(HomeState::TriggerHoming) => {
1662 let mut cw = RawControlWord(view.control_word());
1664 cw.set_bit(4, true);
1665 view.set_control_word(cw.raw());
1666 log::info!("SoftHome[TriggerHoming]: start homing");
1667 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1668 }
1669 Some(HomeState::WaitHomingStarted) => {
1670 let sw = view.status_word();
1676 let error = sw & (1 << 13) != 0;
1677 if error {
1678 self.restore_pp_after_error("Software homing: drive reported homing error");
1679 } else if sw & (1 << 12) == 0 {
1680 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1681 } else if self.homing_timed_out() {
1682 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1683 }
1684 }
1685 Some(HomeState::WaitHomingDone) => {
1686 let sw = view.status_word();
1688 let error = sw & (1 << 13) != 0;
1689 let attained = sw & (1 << 12) != 0;
1690 let reached = sw & (1 << 10) != 0;
1691
1692 if error {
1693 self.restore_pp_after_error("Software homing: drive reported homing error");
1694 } else if attained && reached {
1695 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
1696 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
1697 } else if self.homing_timed_out() {
1698 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
1699 }
1700 }
1701 Some(HomeState::ResetHomingTrigger) => {
1702 let mut cw = RawControlWord(view.control_word());
1707 cw.set_bit(4, false);
1708 view.set_control_word(cw.raw());
1709 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
1710 }
1711 Some(HomeState::WaitHomingTriggerCleared) => {
1712 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
1716 }
1717
1718
1719 Some(HomeState::WriteMotionModeOfOperation) => {
1720
1721 self.fb_mode_of_operation.reset();
1724 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1725 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1726 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
1727
1728 },
1729 Some(HomeState::WaitWriteMotionModeOfOperation) => {
1730 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1732
1733 if !self.fb_mode_of_operation.is_busy() {
1734 if self.fb_mode_of_operation.is_error() {
1735 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1736 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1737 ));
1738 }
1739 else {
1740 if self.is_error {
1741 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
1742 self.finish_op_error();
1743 }
1744 else {
1745 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1748 }
1749
1750 }
1751 }
1752 },
1753
1754 Some(HomeState::SendCurrentPositionTarget) => {
1755 let current_pos = view.position_actual();
1757 view.set_target_position(current_pos);
1758 view.set_profile_velocity(0);
1759 let mut cw = RawControlWord(view.control_word());
1760 cw.set_bit(4, true);
1761 cw.set_bit(5, true);
1762 cw.set_bit(6, false); view.set_control_word(cw.raw());
1764 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1765 }
1766 Some(HomeState::WaitCurrentPositionTargetSent) => {
1767 let sw = RawStatusWord(view.status_word());
1769 if sw.raw() & (1 << 12) != 0 {
1770 let mut cw = RawControlWord(view.control_word());
1771 cw.set_bit(4, false);
1772 view.set_control_word(cw.raw());
1773 log::info!("Software homing complete — position set to {} user units",
1774 self.config.home_position);
1775 self.complete_op();
1776 } else if self.homing_timed_out() {
1777 self.set_op_error("Software homing timeout: hold position not acknowledged");
1778 }
1779 }
1780 _ => self.complete_op(),
1781 }
1782 }
1783
1784 fn tick_halting(&mut self, view: &mut impl AxisView, step: u8) {
1797 match HaltState::from_repr(step) {
1798 Some(HaltState::WaitStopped) => {
1799 let pos = view.position_actual();
1803 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
1804
1805 let vel = view.velocity_actual().abs();
1806 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
1807
1808 if pos_stable || vel_stopped {
1813 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1814 } else {
1815 self.halt_stable_count = 0;
1816 }
1817
1818 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
1819 self.command_cancel_move(view);
1820 self.op_started = Some(Instant::now());
1821 self.op = AxisOp::Halting(HaltState::WaitCancelAck as u8);
1822 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1823 self.set_op_error("Halt timeout: motor did not stop");
1824 }
1825 }
1826 Some(HaltState::WaitCancelAck) => {
1827 let sw = RawStatusWord(view.status_word());
1828 let setpoint_ack = sw.raw() & (1 << 12) != 0;
1829 if setpoint_ack {
1831 let mut cw = RawControlWord(view.control_word());
1834 cw.set_bit(4, false);
1835 cw.set_bit(5, true);
1836 view.set_control_word(cw.raw());
1837 self.op_started = Some(Instant::now());
1838 self.op = AxisOp::Halting(HaltState::WaitCancelAckClear as u8);
1839 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1840 self.set_op_error("Halt timeout: cancel not acknowledged");
1841 }
1842 }
1843 Some(HaltState::WaitCancelAckClear) => {
1844 let sw = RawStatusWord(view.status_word());
1845 if sw.raw() & (1 << 12) == 0 {
1846 self.complete_op();
1848 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1849 self.set_op_error("Halt timeout: ack did not clear");
1850 }
1851 }
1852 None => {
1853 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
1854 self.complete_op();
1855 }
1856 }
1857 }
1858
1859 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1864 match step {
1865 1 => {
1866 let mut cw = RawControlWord(view.control_word());
1868 cw.cmd_fault_reset();
1869 view.set_control_word(cw.raw());
1870 self.op = AxisOp::FaultRecovery(2);
1871 }
1872 2 => {
1873 let sw = RawStatusWord(view.status_word());
1875 let state = sw.state();
1876 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1877 log::info!("Fault cleared (drive state: {})", state);
1878 self.complete_op();
1879 } else if self.op_timed_out() {
1880 self.set_op_error("Fault reset timeout: drive still faulted");
1881 }
1882 }
1883 _ => self.complete_op(),
1884 }
1885 }
1886}
1887
1888#[cfg(test)]
1893mod tests {
1894 use super::*;
1895
1896 struct MockView {
1898 control_word: u16,
1899 status_word: u16,
1900 target_position: i32,
1901 profile_velocity: u32,
1902 profile_acceleration: u32,
1903 profile_deceleration: u32,
1904 modes_of_operation: i8,
1905 modes_of_operation_display: i8,
1906 position_actual: i32,
1907 velocity_actual: i32,
1908 error_code: u16,
1909 positive_limit: bool,
1910 negative_limit: bool,
1911 home_sensor: bool,
1912 }
1913
1914 impl MockView {
1915 fn new() -> Self {
1916 Self {
1917 control_word: 0,
1918 status_word: 0x0040, target_position: 0,
1920 profile_velocity: 0,
1921 profile_acceleration: 0,
1922 profile_deceleration: 0,
1923 modes_of_operation: 0,
1924 modes_of_operation_display: 1, position_actual: 0,
1926 velocity_actual: 0,
1927 error_code: 0,
1928 positive_limit: false,
1929 negative_limit: false,
1930 home_sensor: false,
1931 }
1932 }
1933
1934 fn set_state(&mut self, state: u16) {
1935 self.status_word = state;
1936 }
1937 }
1938
1939 impl AxisView for MockView {
1940 fn control_word(&self) -> u16 { self.control_word }
1941 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1942 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1943 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1944 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1945 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1946 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1947 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1948 fn status_word(&self) -> u16 { self.status_word }
1949 fn position_actual(&self) -> i32 { self.position_actual }
1950 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1951 fn error_code(&self) -> u16 { self.error_code }
1952 fn positive_limit_active(&self) -> bool { self.positive_limit }
1953 fn negative_limit_active(&self) -> bool { self.negative_limit }
1954 fn home_sensor_active(&self) -> bool { self.home_sensor }
1955 }
1956
1957 fn test_config() -> AxisConfig {
1958 AxisConfig::new(12_800).with_user_scale(360.0)
1959 }
1960
1961 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1963 use tokio::sync::mpsc;
1964 let (write_tx, write_rx) = mpsc::unbounded_channel();
1965 let (response_tx, response_rx) = mpsc::unbounded_channel();
1966 let client = CommandClient::new(write_tx, response_rx);
1967 let axis = Axis::new(test_config(), "TestDrive");
1968 (axis, client, response_tx, write_rx)
1969 }
1970
1971 #[test]
1972 fn axis_config_conversion() {
1973 let cfg = test_config();
1974 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
1976 }
1977
1978 #[test]
1979 fn enable_sequence_sets_pp_mode_and_shutdown() {
1980 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1981 let mut view = MockView::new();
1982
1983 axis.enable(&mut view);
1984
1985 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
1987 assert_eq!(view.control_word & 0x008F, 0x0006);
1989 assert_eq!(axis.op, AxisOp::Enabling(1));
1991
1992 view.set_state(0x0021); axis.tick(&mut view, &mut client);
1995
1996 assert_eq!(view.control_word & 0x008F, 0x000F);
1998 assert_eq!(axis.op, AxisOp::Enabling(2));
1999
2000 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2003
2004 assert_eq!(axis.op, AxisOp::Idle);
2006 assert!(axis.motor_on);
2007 }
2008
2009 #[test]
2010 fn move_absolute_sets_target() {
2011 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2012 let mut view = MockView::new();
2013 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2018
2019 assert_eq!(view.target_position, 1600);
2021 assert_eq!(view.profile_velocity, 3200);
2023 assert_eq!(view.profile_acceleration, 6400);
2025 assert_eq!(view.profile_deceleration, 6400);
2026 assert!(view.control_word & (1 << 4) != 0);
2028 assert!(view.control_word & (1 << 6) == 0);
2030 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
2032 }
2033
2034 #[test]
2035 fn move_relative_sets_relative_bit() {
2036 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2037 let mut view = MockView::new();
2038 view.set_state(0x0027);
2039 axis.tick(&mut view, &mut client);
2040
2041 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2042
2043 assert!(view.control_word & (1 << 6) != 0);
2045 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1)));
2046 }
2047
2048 #[test]
2049 fn move_completes_on_target_reached() {
2050 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2051 let mut view = MockView::new();
2052 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2054
2055 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2056
2057 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2060 assert!(view.control_word & (1 << 4) == 0);
2062
2063 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2066 assert_eq!(axis.op, AxisOp::Idle);
2068 assert!(!axis.in_motion);
2069 }
2070
2071 #[test]
2072 fn fault_detected_sets_error() {
2073 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2074 let mut view = MockView::new();
2075 view.set_state(0x0008); view.error_code = 0x1234;
2077
2078 axis.tick(&mut view, &mut client);
2079
2080 assert!(axis.is_error);
2081 assert_eq!(axis.error_code, 0x1234);
2082 assert!(axis.error_message.contains("fault"));
2083 }
2084
2085 #[test]
2086 fn fault_recovery_sequence() {
2087 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2088 let mut view = MockView::new();
2089 view.set_state(0x0008); axis.reset_faults(&mut view);
2092 assert!(view.control_word & 0x0080 == 0);
2094
2095 axis.tick(&mut view, &mut client);
2097 assert!(view.control_word & 0x0080 != 0);
2098
2099 view.set_state(0x0040);
2101 axis.tick(&mut view, &mut client);
2102 assert_eq!(axis.op, AxisOp::Idle);
2103 assert!(!axis.is_error);
2104 }
2105
2106 #[test]
2107 fn disable_sequence() {
2108 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2109 let mut view = MockView::new();
2110 view.set_state(0x0027); axis.disable(&mut view);
2113 assert_eq!(view.control_word & 0x008F, 0x0007);
2115
2116 view.set_state(0x0023); axis.tick(&mut view, &mut client);
2119 assert_eq!(axis.op, AxisOp::Idle);
2120 }
2121
2122 #[test]
2123 fn position_tracks_with_home_offset() {
2124 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2125 let mut view = MockView::new();
2126 view.set_state(0x0027);
2127 view.position_actual = 5000;
2128
2129 axis.enable(&mut view);
2131 view.set_state(0x0021);
2132 axis.tick(&mut view, &mut client);
2133 view.set_state(0x0027);
2134 axis.tick(&mut view, &mut client);
2135
2136 assert_eq!(axis.home_offset, 5000);
2138
2139 assert!((axis.position - 0.0).abs() < 0.01);
2141
2142 view.position_actual = 6600;
2144 axis.tick(&mut view, &mut client);
2145
2146 assert!((axis.position - 45.0).abs() < 0.1);
2148 }
2149
2150 #[test]
2151 fn set_position_adjusts_home_offset() {
2152 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2153 let mut view = MockView::new();
2154 view.position_actual = 3200;
2155
2156 axis.set_position(&view, 90.0);
2157 axis.tick(&mut view, &mut client);
2158
2159 assert_eq!(axis.home_offset, 0);
2161 assert!((axis.position - 90.0).abs() < 0.01);
2162 }
2163
2164 #[test]
2165 fn halt_runs_multi_stage_close_out() {
2166 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2167 let mut view = MockView::new();
2168 view.set_state(0x0027);
2169
2170 axis.halt(&mut view);
2171
2172 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2174 assert!(view.control_word & (1 << 4) == 0, "new_setpoint must be cleared");
2175
2176 assert!(matches!(axis.op, AxisOp::Halting(_)),
2178 "halt should enter Halting state, not Idle");
2179 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2180 assert_eq!(step, HaltState::WaitStopped as u8);
2181
2182 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2189 axis.tick(&mut view, &mut client);
2190 }
2191 assert!(matches!(axis.op, AxisOp::Halting(_)));
2193 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2194 assert_eq!(step, HaltState::WaitCancelAck as u8,
2195 "should advance past WaitStopped once position/velocity is stable");
2196
2197 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2201 }
2202
2203 #[test]
2204 fn is_busy_tracks_operations() {
2205 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2206 let mut view = MockView::new();
2207
2208 axis.tick(&mut view, &mut client);
2210 assert!(!axis.is_busy);
2211
2212 axis.enable(&mut view);
2214 axis.tick(&mut view, &mut client);
2215 assert!(axis.is_busy);
2216
2217 view.set_state(0x0021);
2219 axis.tick(&mut view, &mut client);
2220 view.set_state(0x0027);
2221 axis.tick(&mut view, &mut client);
2222 assert!(!axis.is_busy);
2223
2224 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2226 axis.tick(&mut view, &mut client);
2227 assert!(axis.is_busy);
2228 assert!(axis.in_motion);
2229 }
2230
2231 #[test]
2232 fn fault_during_move_cancels_op() {
2233 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2234 let mut view = MockView::new();
2235 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2237
2238 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2240 axis.tick(&mut view, &mut client);
2241 assert!(axis.is_busy);
2242 assert!(!axis.is_error);
2243
2244 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2247
2248 assert!(!axis.is_busy);
2250 assert!(axis.is_error);
2251 assert_eq!(axis.op, AxisOp::Idle);
2252 }
2253
2254 #[test]
2255 fn move_absolute_rejected_by_max_limit() {
2256 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2257 let mut view = MockView::new();
2258 view.set_state(0x0027);
2259 axis.tick(&mut view, &mut client);
2260
2261 axis.set_software_max_limit(90.0);
2262 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2263
2264 assert!(axis.is_error);
2266 assert_eq!(axis.op, AxisOp::Idle);
2267 assert!(axis.error_message.contains("max software limit"));
2268 }
2269
2270 #[test]
2271 fn move_absolute_rejected_by_min_limit() {
2272 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2273 let mut view = MockView::new();
2274 view.set_state(0x0027);
2275 axis.tick(&mut view, &mut client);
2276
2277 axis.set_software_min_limit(-10.0);
2278 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2279
2280 assert!(axis.is_error);
2281 assert_eq!(axis.op, AxisOp::Idle);
2282 assert!(axis.error_message.contains("min software limit"));
2283 }
2284
2285 #[test]
2286 fn move_relative_rejected_by_max_limit() {
2287 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2288 let mut view = MockView::new();
2289 view.set_state(0x0027);
2290 axis.tick(&mut view, &mut client);
2291
2292 axis.set_software_max_limit(50.0);
2294 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2295
2296 assert!(axis.is_error);
2297 assert_eq!(axis.op, AxisOp::Idle);
2298 assert!(axis.error_message.contains("max software limit"));
2299 }
2300
2301 #[test]
2302 fn move_within_limits_allowed() {
2303 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2304 let mut view = MockView::new();
2305 view.set_state(0x0027);
2306 axis.tick(&mut view, &mut client);
2307
2308 axis.set_software_max_limit(90.0);
2309 axis.set_software_min_limit(-90.0);
2310 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2311
2312 assert!(!axis.is_error);
2314 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
2315 }
2316
2317 #[test]
2318 fn runtime_limit_halts_move_in_violated_direction() {
2319 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2320 let mut view = MockView::new();
2321 view.set_state(0x0027);
2322 axis.tick(&mut view, &mut client);
2323
2324 axis.set_software_max_limit(45.0);
2325 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2327
2328 view.position_actual = 1650;
2331 view.velocity_actual = 100; view.status_word = 0x1027;
2335 axis.tick(&mut view, &mut client);
2336 view.status_word = 0x0027;
2337 axis.tick(&mut view, &mut client);
2338
2339 assert!(axis.is_error);
2341 assert!(axis.at_max_limit);
2342 assert_eq!(axis.op, AxisOp::Idle);
2343 assert!(axis.error_message.contains("Software position limit"));
2344 assert!(view.control_word & (1 << 8) != 0);
2346 }
2347
2348 #[test]
2349 fn runtime_limit_allows_move_in_opposite_direction() {
2350 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2351 let mut view = MockView::new();
2352 view.set_state(0x0027);
2353 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2356 axis.tick(&mut view, &mut client);
2357 assert!(axis.at_max_limit);
2358
2359 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2361 assert!(!axis.is_error);
2362 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
2363
2364 view.velocity_actual = -100;
2366 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2368 assert!(!axis.is_error);
2370 }
2371
2372 #[test]
2373 fn positive_limit_switch_halts_positive_move() {
2374 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2375 let mut view = MockView::new();
2376 view.set_state(0x0027);
2377 axis.tick(&mut view, &mut client);
2378
2379 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2381 view.velocity_actual = 100; view.status_word = 0x1027;
2384 axis.tick(&mut view, &mut client);
2385 view.status_word = 0x0027;
2386
2387 view.positive_limit = true;
2389 axis.tick(&mut view, &mut client);
2390
2391 assert!(axis.is_error);
2392 assert!(axis.at_positive_limit_switch);
2393 assert!(!axis.is_busy);
2394 assert!(axis.error_message.contains("Positive limit switch"));
2395 assert!(view.control_word & (1 << 8) != 0);
2397 }
2398
2399 #[test]
2400 fn negative_limit_switch_halts_negative_move() {
2401 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2402 let mut view = MockView::new();
2403 view.set_state(0x0027);
2404 axis.tick(&mut view, &mut client);
2405
2406 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2408 view.velocity_actual = -100; view.status_word = 0x1027;
2410 axis.tick(&mut view, &mut client);
2411 view.status_word = 0x0027;
2412
2413 view.negative_limit = true;
2415 axis.tick(&mut view, &mut client);
2416
2417 assert!(axis.is_error);
2418 assert!(axis.at_negative_limit_switch);
2419 assert!(axis.error_message.contains("Negative limit switch"));
2420 }
2421
2422 #[test]
2423 fn limit_switch_allows_move_in_opposite_direction() {
2424 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2425 let mut view = MockView::new();
2426 view.set_state(0x0027);
2427 view.positive_limit = true;
2429 view.velocity_actual = -100;
2430 axis.tick(&mut view, &mut client);
2431 assert!(axis.at_positive_limit_switch);
2432
2433 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2435 view.status_word = 0x1027;
2436 axis.tick(&mut view, &mut client);
2437
2438 assert!(!axis.is_error);
2440 assert!(matches!(axis.op, AxisOp::Moving(_, _)));
2441 }
2442
2443 #[test]
2444 fn limit_switch_ignored_when_not_moving() {
2445 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2446 let mut view = MockView::new();
2447 view.set_state(0x0027);
2448 view.positive_limit = true;
2449
2450 axis.tick(&mut view, &mut client);
2451
2452 assert!(axis.at_positive_limit_switch);
2454 assert!(!axis.is_error);
2455 }
2456
2457 #[test]
2458 fn home_sensor_output_tracks_view() {
2459 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2460 let mut view = MockView::new();
2461 view.set_state(0x0027);
2462
2463 axis.tick(&mut view, &mut client);
2464 assert!(!axis.home_sensor);
2465
2466 view.home_sensor = true;
2467 axis.tick(&mut view, &mut client);
2468 assert!(axis.home_sensor);
2469
2470 view.home_sensor = false;
2471 axis.tick(&mut view, &mut client);
2472 assert!(!axis.home_sensor);
2473 }
2474
2475 #[test]
2476 fn velocity_output_converted() {
2477 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2478 let mut view = MockView::new();
2479 view.set_state(0x0027);
2480 view.velocity_actual = 3200;
2482
2483 axis.tick(&mut view, &mut client);
2484
2485 assert!((axis.speed - 90.0).abs() < 0.1);
2486 assert!(axis.moving_positive);
2487 assert!(!axis.moving_negative);
2488 }
2489
2490 fn soft_homing_config() -> AxisConfig {
2493 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2494 cfg.homing_speed = 10.0;
2495 cfg.homing_accel = 20.0;
2496 cfg.homing_decel = 20.0;
2497 cfg
2498 }
2499
2500 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2501 use tokio::sync::mpsc;
2502 let (write_tx, write_rx) = mpsc::unbounded_channel();
2503 let (response_tx, response_rx) = mpsc::unbounded_channel();
2504 let client = CommandClient::new(write_tx, response_rx);
2505 let axis = Axis::new(soft_homing_config(), "TestDrive");
2506 (axis, client, response_tx, write_rx)
2507 }
2508
2509 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2511 view.set_state(0x0027); axis.tick(view, client);
2513 }
2514
2515 fn complete_soft_homing(
2520 axis: &mut Axis,
2521 view: &mut MockView,
2522 client: &mut CommandClient,
2523 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2524 trigger_pos: i32,
2525 clear_sensor: impl FnOnce(&mut MockView),
2526 ) {
2527 use mechutil::ipc::CommandMessage as IpcMsg;
2528
2529 axis.tick(view, client);
2532 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2533
2534 view.position_actual = trigger_pos + 100;
2536 axis.tick(view, client);
2537 view.position_actual = trigger_pos + 120;
2538 axis.tick(view, client);
2539 for _ in 0..10 { axis.tick(view, client); }
2541 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2542
2543 view.status_word = 0x1027;
2545 axis.tick(view, client);
2546 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2547 view.status_word = 0x0027;
2548
2549 for _ in 0..100 { axis.tick(view, client); }
2551 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2552
2553 axis.tick(view, client);
2556 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2557
2558 view.status_word = 0x1027;
2560 axis.tick(view, client);
2561 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2562 view.status_word = 0x0027;
2563
2564 axis.tick(view, client);
2566 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2567 clear_sensor(view);
2568 view.position_actual = trigger_pos - 200;
2569 axis.tick(view, client);
2570 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2571
2572 axis.tick(view, client);
2574 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2575 for _ in 0..10 { axis.tick(view, client); }
2576 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2577
2578 view.status_word = 0x1027;
2581 axis.tick(view, client);
2582 view.status_word = 0x0027;
2583 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2584
2585 let tid = axis.homing_sdo_tid;
2587 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2588 client.poll();
2589 axis.tick(view, client);
2590 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2591
2592 axis.tick(view, client);
2594 let tid = axis.homing_sdo_tid;
2595 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2596 client.poll();
2597 axis.tick(view, client);
2598 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2599
2600 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2602 axis.tick(view, client);
2603 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2604
2605 view.status_word = 0x1427; axis.tick(view, client);
2608 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2609 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2610 view.status_word = 0x0027;
2611
2612 axis.tick(view, client);
2614 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2615
2616 view.status_word = 0x1027;
2618 axis.tick(view, client);
2619 view.status_word = 0x0027;
2620
2621 assert_eq!(axis.op, AxisOp::Idle);
2622 assert!(!axis.is_busy);
2623 assert!(!axis.is_error);
2624 assert_eq!(axis.home_offset, 0); }
2626
2627 #[test]
2628 fn soft_homing_pnp_home_sensor_full_sequence() {
2629 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2630 let mut view = MockView::new();
2631 enable_axis(&mut axis, &mut view, &mut client);
2632
2633 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2634
2635 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2638 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2640 axis.tick(&mut view, &mut client); view.home_sensor = true;
2644 view.position_actual = 5000;
2645 axis.tick(&mut view, &mut client);
2646 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2647
2648 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2649 |v| { v.home_sensor = false; });
2650 }
2651
2652 #[test]
2653 fn soft_homing_npn_home_sensor_full_sequence() {
2654 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2655 let mut view = MockView::new();
2656 view.home_sensor = true;
2658 enable_axis(&mut axis, &mut view, &mut client);
2659
2660 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2661
2662 axis.tick(&mut view, &mut client);
2664 view.status_word = 0x1027;
2665 axis.tick(&mut view, &mut client);
2666 view.status_word = 0x0027;
2667 axis.tick(&mut view, &mut client);
2668
2669 view.home_sensor = false;
2671 view.position_actual = 3000;
2672 axis.tick(&mut view, &mut client);
2673 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2674
2675 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2676 |v| { v.home_sensor = true; }); }
2678
2679 #[test]
2680 fn soft_homing_limit_switch_suppresses_halt() {
2681 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2682 let mut view = MockView::new();
2683 enable_axis(&mut axis, &mut view, &mut client);
2684
2685 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
2687
2688 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2693 axis.tick(&mut view, &mut client); view.positive_limit = true;
2697 view.velocity_actual = 100; view.position_actual = 8000;
2699 axis.tick(&mut view, &mut client);
2700
2701 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2703 assert!(!axis.is_error);
2704 }
2705
2706 #[test]
2707 fn soft_homing_opposite_limit_still_protects() {
2708 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2709 let mut view = MockView::new();
2710 enable_axis(&mut axis, &mut view, &mut client);
2711
2712 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2714
2715 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2720 axis.tick(&mut view, &mut client); view.negative_limit = true;
2725 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
2727
2728 assert!(axis.is_error);
2730 assert!(axis.error_message.contains("Negative limit switch"));
2731 }
2732
2733 #[test]
2734 #[test]
2752 fn soft_homing_negative_direction_sets_negative_target() {
2753 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2754 let mut view = MockView::new();
2755 enable_axis(&mut axis, &mut view, &mut client);
2756
2757 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
2758 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
2762 }
2763
2764 #[test]
2765 fn home_integrated_method_starts_hardware_homing() {
2766 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2767 let mut view = MockView::new();
2768 enable_axis(&mut axis, &mut view, &mut client);
2769
2770 axis.home(&mut view, HomingMethod::CurrentPosition);
2771 assert!(matches!(axis.op, AxisOp::Homing(0)));
2772 assert_eq!(axis.homing_method, 37);
2773 }
2774
2775 #[test]
2776 fn home_integrated_arbitrary_code() {
2777 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2778 let mut view = MockView::new();
2779 enable_axis(&mut axis, &mut view, &mut client);
2780
2781 axis.home(&mut view, HomingMethod::Integrated(35));
2782 assert!(matches!(axis.op, AxisOp::Homing(0)));
2783 assert_eq!(axis.homing_method, 35);
2784 }
2785
2786 #[test]
2787 fn hardware_homing_skips_speed_sdos_when_zero() {
2788 use mechutil::ipc::CommandMessage;
2789
2790 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
2791 let mut view = MockView::new();
2792 enable_axis(&mut axis, &mut view, &mut client);
2793
2794 axis.home(&mut view, HomingMethod::Integrated(37));
2796
2797 axis.tick(&mut view, &mut client);
2799 assert!(matches!(axis.op, AxisOp::Homing(1)));
2800
2801 let _ = write_rx.try_recv();
2803
2804 let tid = axis.homing_sdo_tid;
2806 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2807 client.poll();
2808 axis.tick(&mut view, &mut client);
2809
2810 assert!(matches!(axis.op, AxisOp::Homing(8)));
2812 }
2813
2814 #[test]
2815 fn hardware_homing_writes_speed_sdos_when_nonzero() {
2816 use mechutil::ipc::CommandMessage;
2817
2818 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
2819 let mut view = MockView::new();
2820 enable_axis(&mut axis, &mut view, &mut client);
2821
2822 axis.home(&mut view, HomingMethod::Integrated(37));
2824
2825 axis.tick(&mut view, &mut client);
2827 assert!(matches!(axis.op, AxisOp::Homing(1)));
2828 let _ = write_rx.try_recv();
2829
2830 let tid = axis.homing_sdo_tid;
2832 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2833 client.poll();
2834 axis.tick(&mut view, &mut client);
2835 assert!(matches!(axis.op, AxisOp::Homing(2)));
2837 }
2838
2839 #[test]
2840 fn soft_homing_edge_during_ack_step() {
2841 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2842 let mut view = MockView::new();
2843 enable_axis(&mut axis, &mut view, &mut client);
2844
2845 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2846 axis.tick(&mut view, &mut client); view.home_sensor = true;
2850 view.position_actual = 2000;
2851 axis.tick(&mut view, &mut client);
2852
2853 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2855 }
2856
2857 #[test]
2858 fn soft_homing_applies_home_position() {
2859 let mut cfg = soft_homing_config();
2860 cfg.home_position = 90.0;
2861
2862 use tokio::sync::mpsc;
2863 let (write_tx, _write_rx) = mpsc::unbounded_channel();
2864 let (resp_tx, response_rx) = mpsc::unbounded_channel();
2865 let mut client = CommandClient::new(write_tx, response_rx);
2866 let mut axis = Axis::new(cfg, "TestDrive");
2867
2868 let mut view = MockView::new();
2869 enable_axis(&mut axis, &mut view, &mut client);
2870
2871 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
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 = true;
2882 view.position_actual = 5000;
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, 5000,
2888 |v| { v.home_sensor = false; });
2889
2890 assert_eq!(axis.home_offset, 0);
2892 }
2893
2894 #[test]
2895 fn soft_homing_default_home_position_zero() {
2896 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2897 let mut view = MockView::new();
2898 enable_axis(&mut axis, &mut view, &mut client);
2899
2900 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2901
2902 axis.tick(&mut view, &mut client);
2904 view.status_word = 0x1027;
2905 axis.tick(&mut view, &mut client);
2906 view.status_word = 0x0027;
2907 axis.tick(&mut view, &mut client);
2908
2909 view.home_sensor = true;
2911 view.position_actual = 5000;
2912 axis.tick(&mut view, &mut client);
2913
2914 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2915 |v| { v.home_sensor = false; });
2916
2917 assert_eq!(axis.home_offset, 0);
2918 }
2919}