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 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 let (pos, neg) = match kind {
402 MoveKind::Absolute => {
403 let actual = view.position_actual();
404 (raw_target > actual, raw_target < actual)
405 }
406 MoveKind::Relative => {
407 (raw_target > 0, raw_target < 0)
408 }
409 };
410
411 self.op = AxisOp::Moving(kind, 1, pos, neg);
412 self.op_started = Some(Instant::now());
413 }
414
415 pub fn halt(&mut self, view: &mut impl AxisView) {
433 self.command_halt(view);
434 self.halt_stable_count = 0;
435 self.last_raw_position = view.position_actual();
436 self.op_started = Some(Instant::now());
437 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
438 }
439
440 pub fn enable(&mut self, view: &mut impl AxisView) {
448 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
450 let mut cw = RawControlWord(view.control_word());
451 cw.cmd_shutdown();
452 view.set_control_word(cw.raw());
453
454 self.op = AxisOp::Enabling(1);
455 self.op_started = Some(Instant::now());
456 }
457
458 pub fn disable(&mut self, view: &mut impl AxisView) {
460 let mut cw = RawControlWord(view.control_word());
461 cw.cmd_disable_operation();
462 cw.set_bit(4, false);
463 cw.set_bit(8, false);
464 cw.set_bit(7, false);
465 cw.set_bit(2, true);
466 view.set_control_word(cw.raw());
467
468 self.op = AxisOp::Disabling(1);
469 self.op_started = Some(Instant::now());
470 }
471
472 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
476 let mut cw = RawControlWord(view.control_word());
478 cw.cmd_clear_fault_reset();
479 view.set_control_word(cw.raw());
480
481 self.is_error = false;
482 self.error_code = 0;
483 self.error_message.clear();
484 self.op = AxisOp::FaultRecovery(1);
485 self.op_started = Some(Instant::now());
486 }
487
488 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
496 if method.is_integrated() {
497 self.homing_method = method.cia402_code();
498 self.op = AxisOp::Homing(0);
499 self.op_started = Some(Instant::now());
500 let _ = view;
501 } else {
502 self.configure_soft_homing(method);
503 self.start_soft_homing(view);
504 }
505 }
506
507 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
516 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
517 }
518
519 pub fn set_home_position(&mut self, user_units: f64) {
523 self.config.home_position = user_units;
524 }
525
526 pub fn set_software_max_limit(&mut self, user_units: f64) {
528 self.config.max_position_limit = user_units;
529 self.config.enable_max_position_limit = true;
530 }
531
532 pub fn set_software_min_limit(&mut self, user_units: f64) {
534 self.config.min_position_limit = user_units;
535 self.config.enable_min_position_limit = true;
536 }
537
538 pub fn sdo_write(
544 &mut self,
545 client: &mut CommandClient,
546 index: u16,
547 sub_index: u8,
548 value: serde_json::Value,
549 ) {
550 self.sdo.write(client, index, sub_index, value);
551 }
552
553 pub fn sdo_read(
555 &mut self,
556 client: &mut CommandClient,
557 index: u16,
558 sub_index: u8,
559 ) -> u32 {
560 self.sdo.read(client, index, sub_index)
561 }
562
563 pub fn sdo_result(
565 &mut self,
566 client: &mut CommandClient,
567 tid: u32,
568 ) -> SdoResult {
569 self.sdo.result(client, tid, Duration::from_secs(5))
570 }
571
572 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
586 self.check_faults(view);
587 self.progress_op(view, client);
588 self.update_outputs(view);
589 self.check_limits(view);
590 }
591
592 fn update_outputs(&mut self, view: &impl AxisView) {
597 let raw = view.position_actual();
598 self.raw_position = raw as i64;
599 self.position = self.config.to_user((raw - self.home_offset) as f64);
600
601 let vel = view.velocity_actual();
602 let user_vel = self.config.to_user(vel as f64);
603 self.speed = user_vel.abs();
604 self.moving_positive = user_vel > 0.0;
605 self.moving_negative = user_vel < 0.0;
606 self.is_busy = self.op != AxisOp::Idle;
607 self.in_motion = matches!(self.op, AxisOp::Moving(_, _, _, _) | AxisOp::SoftHoming(_));
608
609 let sw = RawStatusWord(view.status_word());
610 self.motor_on = sw.state() == Cia402State::OperationEnabled;
611
612 self.last_raw_position = raw;
613 }
614
615 fn check_faults(&mut self, view: &impl AxisView) {
620 let sw = RawStatusWord(view.status_word());
621 let state = sw.state();
622
623 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
624 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
625 self.is_error = true;
626 let ec = view.error_code();
627 if ec != 0 {
628 self.error_code = ec as u32;
629 }
630 self.error_message = format!("Drive fault (state: {})", state);
631 self.op = AxisOp::Idle;
633 self.op_started = None;
634 }
635 }
636 }
637
638 fn op_timed_out(&self) -> bool {
643 self.op_started
644 .map_or(false, |t| t.elapsed() > self.op_timeout)
645 }
646
647 fn homing_timed_out(&self) -> bool {
648 self.op_started
649 .map_or(false, |t| t.elapsed() > self.homing_timeout)
650 }
651
652 fn move_start_timed_out(&self) -> bool {
653 self.op_started
654 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
655 }
656
657 fn op_stage_timed_out(&self, limit: Duration) -> bool {
661 self.op_started
662 .map_or(false, |t| t.elapsed() > limit)
663 }
664
665 fn set_op_error(&mut self, msg: &str) {
666 self.is_error = true;
667 self.error_message = msg.to_string();
668 self.op = AxisOp::Idle;
669 self.op_started = None;
670 self.is_busy = false;
671 self.in_motion = false;
672 log::error!("Axis error: {}", msg);
673 }
674
675 fn restore_pp_after_error(&mut self, msg: &str) {
676 self.is_error = true;
677 self.error_message = msg.to_string();
678 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);;
679 log::error!("Axis error: {}", msg);
680 }
681
682 fn finish_op_error(&mut self) {
683 self.op = AxisOp::Idle;
684 self.op_started = None;
685 self.is_busy = false;
686 self.in_motion = false;
687 }
688
689 fn complete_op(&mut self) {
690 self.op = AxisOp::Idle;
691 self.op_started = None;
692 }
693
694 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
703 let static_limit = if self.config.enable_max_position_limit {
704 Some(self.config.max_position_limit)
705 } else {
706 None
707 };
708 match (static_limit, view.dynamic_max_position_limit()) {
709 (Some(s), Some(d)) => Some(s.min(d)),
710 (Some(v), None) | (None, Some(v)) => Some(v),
711 (None, None) => None,
712 }
713 }
714
715 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
719 let static_limit = if self.config.enable_min_position_limit {
720 Some(self.config.min_position_limit)
721 } else {
722 None
723 };
724 match (static_limit, view.dynamic_min_position_limit()) {
725 (Some(s), Some(d)) => Some(s.max(d)),
726 (Some(v), None) | (None, Some(v)) => Some(v),
727 (None, None) => None,
728 }
729 }
730
731 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
736 if let Some(max) = self.effective_max_limit(view) {
737 if target > max {
738 return Some(format!(
739 "Target {:.3} exceeds max software limit {:.3}",
740 target, max
741 ));
742 }
743 }
744 if let Some(min) = self.effective_min_limit(view) {
745 if target < min {
746 return Some(format!(
747 "Target {:.3} exceeds min software limit {:.3}",
748 target, min
749 ));
750 }
751 }
752 None
753 }
754
755 fn check_limits(&mut self, view: &mut impl AxisView) {
764 let eff_max = self.effective_max_limit(view);
766 let eff_min = self.effective_min_limit(view);
767 let sw_max = eff_max.map_or(false, |m| self.position >= m);
768 let sw_min = eff_min.map_or(false, |m| self.position <= m);
769
770 self.at_max_limit = sw_max;
771 self.at_min_limit = sw_min;
772
773 let hw_pos = view.positive_limit_active();
775 let hw_neg = view.negative_limit_active();
776
777 self.at_positive_limit_switch = hw_pos;
778 self.at_negative_limit_switch = hw_neg;
779
780 self.home_sensor = view.home_sensor_active();
782
783 self.prev_positive_limit = hw_pos;
785 self.prev_negative_limit = hw_neg;
786 self.prev_home_sensor = view.home_sensor_active();
787
788 let mut commanded_positive = false;
790 let mut commanded_negative = false;
791
792 let is_moving = matches!(self.op, AxisOp::Moving(_, _, _, _));
793 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
794
795 if !is_moving && !is_soft_homing {
796 return; }
798
799 match &self.op {
800 AxisOp::Moving(_, _, pos, neg) => {
801 commanded_positive = *pos;
802 commanded_negative = *neg;
803 }
804 AxisOp::SoftHoming(_) => {
805 match self.soft_home_sensor {
806 SoftHomeSensor::PositiveLimit => commanded_positive = true,
807 SoftHomeSensor::NegativeLimit => commanded_negative = true,
808 SoftHomeSensor::HomeSensor => {
809 commanded_positive = self.moving_positive;
810 commanded_negative = self.moving_negative;
811 }
812 }
813 }
814 _ => {}
815 }
816
817 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
819 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
820
821 let effective_hw_pos = hw_pos && !suppress_pos;
822 let effective_hw_neg = hw_neg && !suppress_neg;
823
824 let effective_sw_max = sw_max && !is_soft_homing;
826 let effective_sw_min = sw_min && !is_soft_homing;
827
828 let positive_blocked = (effective_sw_max || effective_hw_pos) && commanded_positive;
829 let negative_blocked = (effective_sw_min || effective_hw_neg) && commanded_negative;
830
831 if positive_blocked || negative_blocked {
832 let mut cw = RawControlWord(view.control_word());
833 cw.set_bit(8, true); view.set_control_word(cw.raw());
835
836 let msg = if effective_hw_pos && commanded_positive {
837 "Positive limit switch active".to_string()
838 } else if effective_hw_neg && commanded_negative {
839 "Negative limit switch active".to_string()
840 } else if effective_sw_max && commanded_positive {
841 format!(
842 "Software position limit: position {:.3} >= max {:.3}",
843 self.position, eff_max.unwrap_or(self.position)
844 )
845 } else {
846 format!(
847 "Software position limit: position {:.3} <= min {:.3}",
848 self.position, eff_min.unwrap_or(self.position)
849 )
850 };
851 self.set_op_error(&msg);
852 }
853 }
854
855 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
860 match self.op.clone() {
861 AxisOp::Idle => {}
862 AxisOp::Enabling(step) => self.tick_enabling(view, step),
863 AxisOp::Disabling(step) => self.tick_disabling(view, step),
864 AxisOp::Moving(kind, step, pos, neg) => self.tick_moving(view, kind, step, pos, neg),
865 AxisOp::Homing(step) => self.tick_homing(view, client, step),
866 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
867 AxisOp::Halting(step) => self.tick_halting(view, step),
868 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
869 }
870 }
871
872 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
877 match step {
878 1 => {
879 let sw = RawStatusWord(view.status_word());
880 if sw.state() == Cia402State::ReadyToSwitchOn {
881 let mut cw = RawControlWord(view.control_word());
882 cw.cmd_enable_operation();
883 view.set_control_word(cw.raw());
884 self.op = AxisOp::Enabling(2);
885 } else if self.op_timed_out() {
886 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
887 }
888 }
889 2 => {
890 let sw = RawStatusWord(view.status_word());
891 if sw.state() == Cia402State::OperationEnabled {
892 self.complete_op();
899 } else if self.op_timed_out() {
900 self.set_op_error("Enable timeout: waiting for OperationEnabled");
901 }
902 }
903 _ => self.complete_op(),
904 }
905 }
906
907 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
911 match step {
912 1 => {
913 let sw = RawStatusWord(view.status_word());
914 if sw.state() != Cia402State::OperationEnabled {
915 self.complete_op();
916 } else if self.op_timed_out() {
917 self.set_op_error("Disable timeout: drive still in OperationEnabled");
918 }
919 }
920 _ => self.complete_op(),
921 }
922 }
923
924 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8, pos: bool, neg: bool) {
930 match step {
931 1 => {
932 let sw = RawStatusWord(view.status_word());
934 if sw.raw() & (1 << 12) != 0 {
935 let mut cw = RawControlWord(view.control_word());
937 cw.set_bit(4, false);
938 view.set_control_word(cw.raw());
939 self.op = AxisOp::Moving(kind, 2, pos, neg);
940 } else if self.move_start_timed_out() {
941 self.set_op_error("Move timeout: set-point not acknowledged");
942 }
943 },
944 2 => {
945 let sw = RawStatusWord(view.status_word());
947 if sw.raw() & (1 << 12) == 0 {
948 self.op = AxisOp::Moving(kind, 3, pos, neg);
950 }
951 },
952 3 => {
953 let sw = RawStatusWord(view.status_word());
955 if sw.target_reached() {
956 self.complete_op();
957 }
958 },
959 _ => self.complete_op(),
960 }
961 }
962
963 fn tick_homing(
981 &mut self,
982 view: &mut impl AxisView,
983 client: &mut CommandClient,
984 step: u8,
985 ) {
986 match step {
987 0 => {
988 self.homing_sdo_tid = self.sdo.write(
990 client,
991 0x6098,
992 0,
993 json!(self.homing_method),
994 );
995 self.op = AxisOp::Homing(1);
996 }
997 1 => {
998 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1000 SdoResult::Ok(_) => {
1001 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
1003 self.op = AxisOp::Homing(8);
1004 } else {
1005 self.op = AxisOp::Homing(2);
1006 }
1007 }
1008 SdoResult::Pending => {
1009 if self.homing_timed_out() {
1010 self.set_op_error("Homing timeout: SDO write for homing method");
1011 }
1012 }
1013 SdoResult::Err(e) => {
1014 self.set_op_error(&format!("Homing SDO error: {}", e));
1015 }
1016 SdoResult::Timeout => {
1017 self.set_op_error("Homing timeout: SDO write timed out");
1018 }
1019 }
1020 }
1021 2 => {
1022 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1024 self.homing_sdo_tid = self.sdo.write(
1025 client,
1026 0x6099,
1027 1,
1028 json!(speed_counts),
1029 );
1030 self.op = AxisOp::Homing(3);
1031 }
1032 3 => {
1033 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1034 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
1035 SdoResult::Pending => {
1036 if self.homing_timed_out() {
1037 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
1038 }
1039 }
1040 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1041 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1042 }
1043 }
1044 4 => {
1045 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1047 self.homing_sdo_tid = self.sdo.write(
1048 client,
1049 0x6099,
1050 2,
1051 json!(speed_counts),
1052 );
1053 self.op = AxisOp::Homing(5);
1054 }
1055 5 => {
1056 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1057 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
1058 SdoResult::Pending => {
1059 if self.homing_timed_out() {
1060 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1061 }
1062 }
1063 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1064 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1065 }
1066 }
1067 6 => {
1068 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1070 self.homing_sdo_tid = self.sdo.write(
1071 client,
1072 0x609A,
1073 0,
1074 json!(accel_counts),
1075 );
1076 self.op = AxisOp::Homing(7);
1077 }
1078 7 => {
1079 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1080 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
1081 SdoResult::Pending => {
1082 if self.homing_timed_out() {
1083 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1084 }
1085 }
1086 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1087 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1088 }
1089 }
1090 8 => {
1091 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1094 let mut cw = RawControlWord(view.control_word());
1095 cw.set_bit(4, false);
1096 view.set_control_word(cw.raw());
1097 self.op = AxisOp::Homing(9);
1098 }
1099 9 => {
1100 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
1102 self.op = AxisOp::Homing(10);
1103 } else if self.homing_timed_out() {
1104 self.set_op_error("Homing timeout: mode not confirmed");
1105 }
1106 }
1107 10 => {
1108 let mut cw = RawControlWord(view.control_word());
1110 cw.set_bit(4, true);
1111 view.set_control_word(cw.raw());
1112 self.op = AxisOp::Homing(11);
1113 }
1114 11 => {
1115 let sw = view.status_word();
1120 let error = sw & (1 << 13) != 0;
1121 if error {
1122 self.set_op_error("Homing error: drive reported homing failure");
1123 } else if sw & (1 << 12) == 0 {
1124 self.op = AxisOp::Homing(12);
1125 } else if self.homing_timed_out() {
1126 self.set_op_error(&format!("Homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1127 }
1128 }
1129 12 => {
1130 let sw = view.status_word();
1133 let error = sw & (1 << 13) != 0;
1134 let attained = sw & (1 << 12) != 0;
1135 let reached = sw & (1 << 10) != 0;
1136
1137 if error {
1138 self.set_op_error("Homing error: drive reported homing failure");
1139 } else if attained && reached {
1140 self.op = AxisOp::Homing(13);
1141 } else if self.homing_timed_out() {
1142 self.set_op_error("Homing timeout: procedure did not complete");
1143 }
1144 }
1145 13 => {
1146 self.home_offset = view.position_actual()
1149 - self.config.to_counts(self.config.home_position).round() as i32;
1150 let mut cw = RawControlWord(view.control_word());
1152 cw.set_bit(4, false);
1153 view.set_control_word(cw.raw());
1154 self.op = AxisOp::Homing(14);
1155 }
1156 14 => {
1157 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1160 log::info!("Homing complete — home offset: {}", self.home_offset);
1161 self.complete_op();
1162 }
1163 _ => self.complete_op(),
1164 }
1165 }
1166
1167 fn configure_soft_homing(&mut self, method: HomingMethod) {
1170 match method {
1171 HomingMethod::LimitSwitchPosPnp => {
1172 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1173 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1174 self.soft_home_direction = 1.0;
1175 }
1176 HomingMethod::LimitSwitchNegPnp => {
1177 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1178 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1179 self.soft_home_direction = -1.0;
1180 }
1181 HomingMethod::LimitSwitchPosNpn => {
1182 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1183 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1184 self.soft_home_direction = 1.0;
1185 }
1186 HomingMethod::LimitSwitchNegNpn => {
1187 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1188 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1189 self.soft_home_direction = -1.0;
1190 }
1191 HomingMethod::HomeSensorPosPnp => {
1192 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1193 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1194 self.soft_home_direction = 1.0;
1195 }
1196 HomingMethod::HomeSensorNegPnp => {
1197 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1198 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1199 self.soft_home_direction = -1.0;
1200 }
1201 HomingMethod::HomeSensorPosNpn => {
1202 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1203 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1204 self.soft_home_direction = 1.0;
1205 }
1206 HomingMethod::HomeSensorNegNpn => {
1207 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1208 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1209 self.soft_home_direction = -1.0;
1210 }
1211 _ => {} }
1213 }
1214
1215 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1216 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1217 self.op_started = Some(Instant::now());
1218 }
1219
1220 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1221 let raw = match self.soft_home_sensor {
1222 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1223 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1224 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1225 };
1226 match self.soft_home_sensor_type {
1227 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1230 }
1231
1232
1233 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1236 let dir = if !self.config.invert_direction {
1237 direction
1238 }
1239 else {
1240 -direction
1241 };
1242
1243 let target = if dir > 0.0 {
1244 i32::MAX
1245 }
1246 else {
1247 i32::MIN
1248 };
1249
1250 return target;
1251 }
1252
1253
1254 pub fn command_halt(&self, view: &mut impl AxisView) {
1259 let mut cw = RawControlWord(view.control_word());
1260 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1263 }
1264
1265
1266 pub fn command_cancel_move(&self, view: &mut impl AxisView) {
1274
1275 let mut cw = RawControlWord(view.control_word());
1276 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());
1281
1282 let current_pos = view.position_actual();
1283 view.set_target_position(current_pos);
1284 view.set_profile_velocity(0);
1285 }
1286
1287
1288 fn command_homing_speed(&self, view: &mut impl AxisView) {
1290 let cpu = self.config.counts_per_user();
1291 let vel = (self.config.homing_speed * cpu).round() as u32;
1292 let accel = (self.config.homing_accel * cpu).round() as u32;
1293 let decel = (self.config.homing_decel * cpu).round() as u32;
1294 view.set_profile_velocity(vel);
1295 view.set_profile_acceleration(accel);
1296 view.set_profile_deceleration(decel);
1297 }
1298
1299 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1315 match HomeState::from_repr(step) {
1316
1317 Some(HomeState::EnsurePpMode) => {
1318 log::info!("SoftHome: Ensuring PP mode..");
1323 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1324 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1325 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1326 },
1327 Some(HomeState::WaitPpMode) => {
1328
1329 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1330 if !self.fb_mode_of_operation.is_busy() {
1331 if self.fb_mode_of_operation.is_error() {
1332 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1333 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1334 ));
1335 }
1336 else {
1337 log::info!("SoftHome: Drive is in PP mode!");
1338
1339 if !self.check_soft_home_trigger(view) {
1343 log::info!("SoftHome: Not on home switch; seek out.");
1344 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1345 } else {
1346 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1347 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1348 }
1349 }
1350 }
1351
1352
1353 },
1354
1355 Some(HomeState::Search) => {
1357 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1358
1359 let target = self.calculate_max_relative_target(self.soft_home_direction);
1369 view.set_target_position(target);
1370
1371 self.command_homing_speed(view);
1380
1381 let mut cw = RawControlWord(view.control_word());
1382 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());
1387
1388 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1389 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1390 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1391 }
1392 Some(HomeState::WaitSearching) => {
1393 if self.check_soft_home_trigger(view) {
1394 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1395 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1396 return;
1397 }
1398 let sw = RawStatusWord(view.status_word());
1399 if sw.raw() & (1 << 12) != 0 {
1400 let mut cw = RawControlWord(view.control_word());
1401 cw.set_bit(4, false);
1402 view.set_control_word(cw.raw());
1403 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1404 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1405 } else if self.homing_timed_out() {
1406 self.set_op_error("Software homing timeout: set-point not acknowledged");
1407 }
1408 }
1409 Some(HomeState::WaitFoundSensor) => {
1419 if self.check_soft_home_trigger(view) {
1420 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1421 log::info!("ControlWord is : {} ", view.control_word());
1422
1423 let mut cw = RawControlWord(view.control_word());
1424 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1427
1428
1429 self.halt_stable_count = 0;
1430 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1431 } else if self.homing_timed_out() {
1432 self.set_op_error("Software homing timeout: sensor not detected");
1433 }
1434 }
1435
1436
1437 Some(HomeState::WaitStoppedFoundSensor) => {
1438 const STABLE_WINDOW: i32 = 1;
1439 const STABLE_TICKS_REQUIRED: u8 = 10;
1440
1441 let pos = view.position_actual();
1446 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1447 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1448 } else {
1449 self.halt_stable_count = 0;
1450 }
1451
1452 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1453
1454 log::debug!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1455 self.command_cancel_move(view);
1456 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1457
1458 } else if self.homing_timed_out() {
1459 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1460 }
1461 }
1462 Some(HomeState::WaitFoundSensorAck) => {
1463 let sw = RawStatusWord(view.status_word());
1464 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1465
1466 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1467
1468 let mut cw = RawControlWord(view.control_word());
1470 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1473
1474 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1475
1476 } else if self.homing_timed_out() {
1477 self.set_op_error("Software homing timeout: cancel not acknowledged");
1478 }
1479 },
1480 Some(HomeState::WaitFoundSensorAckClear) => {
1481 let sw = RawStatusWord(view.status_word());
1482 if sw.raw() & (1 << 12) == 0 {
1484
1485 let mut cw = RawControlWord(view.control_word());
1487 cw.set_bit(8, false);
1488 view.set_control_word(cw.raw());
1489
1490 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1491 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1492 self.ton.call(false, Duration::from_secs(3));
1493 }
1494 },
1495 Some(HomeState::DebounceFoundSensor) => {
1497 self.ton.call(true, Duration::from_secs(3));
1498
1499 let sw = RawStatusWord(view.status_word());
1500 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1501 self.ton.call(false, Duration::from_secs(3));
1502 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1503 view.position_actual(), view.control_word(), view.status_word());
1504 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1505 }
1506 }
1507
1508 Some(HomeState::BackOff) => {
1510
1511 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1512 view.set_target_position(target);
1513
1514
1515 self.command_homing_speed(view);
1516
1517 let mut cw = RawControlWord(view.control_word());
1518 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1522 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1523 target, self.config.homing_speed, view.position_actual(), cw.raw());
1524 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1525 }
1526 Some(HomeState::WaitBackingOff) => {
1527 let sw = RawStatusWord(view.status_word());
1528 if sw.raw() & (1 << 12) != 0 {
1529 let mut cw = RawControlWord(view.control_word());
1530 cw.set_bit(4, false);
1531 view.set_control_word(cw.raw());
1532 log::debug!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1533 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1534 } else if self.homing_timed_out() {
1535 self.set_op_error("Software homing timeout: back-off not acknowledged");
1536 }
1537 }
1538 Some(HomeState::WaitLostSensor) => {
1539 if !self.check_soft_home_trigger(view) {
1540 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1541
1542 self.command_halt(view);
1543 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1544 } else if self.homing_timed_out() {
1545 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1546 }
1547 }
1548 Some(HomeState::WaitStoppedLostSensor) => {
1549 const STABLE_WINDOW: i32 = 1;
1550 const STABLE_TICKS_REQUIRED: u8 = 10;
1551
1552 let mut cw = RawControlWord(view.control_word());
1553 cw.set_bit(8, true);
1554 view.set_control_word(cw.raw());
1555
1556 let pos = view.position_actual();
1557 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1558 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1559 } else {
1560 self.halt_stable_count = 0;
1561 }
1562
1563 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1564 log::debug!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1565 self.command_cancel_move(view);
1566 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1567 } else if self.homing_timed_out() {
1568 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1569 }
1570 }
1571 Some(HomeState::WaitLostSensorAck) => {
1572 let sw = RawStatusWord(view.status_word());
1573 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1574
1575 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1576
1577 let mut cw = RawControlWord(view.control_word());
1579 cw.set_bit(4, false); view.set_control_word(cw.raw());
1581
1582 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1583
1584
1585 } else if self.homing_timed_out() {
1586 self.set_op_error("Software homing timeout: cancel not acknowledged");
1587 }
1588 }
1589 Some(HomeState::WaitLostSensorAckClear) => {
1590 let sw = RawStatusWord(view.status_word());
1592 if sw.raw() & (1 << 12) == 0 {
1593
1594 let mut cw = RawControlWord(view.control_word());
1596 cw.set_bit(8, false);
1597 view.set_control_word(cw.raw());
1598
1599
1600 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1601 self.homing_sdo_tid = self.sdo.write(
1604 client, 0x607C, 0, json!(desired_counts),
1605 );
1606
1607 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1608 self.config.home_position, desired_counts
1609 );
1610
1611 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1612
1613 }
1614 },
1615
1616 Some(HomeState::WaitHomeOffsetDone) => {
1617 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1619 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1620 SdoResult::Pending => {
1621 if self.homing_timed_out() {
1622 self.set_op_error("Software homing timeout: home offset SDO write");
1623 }
1624 }
1625 SdoResult::Err(e) => {
1626 self.set_op_error(&format!("Software homing SDO error: {}", e));
1627 }
1628 SdoResult::Timeout => {
1629 self.set_op_error("Software homing: home offset SDO timed out");
1630 }
1631 }
1632 },
1633 Some(HomeState::WriteHomingModeOp) => {
1634
1635 self.fb_mode_of_operation.reset();
1639 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1640 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1641 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1642
1643
1644 },
1645 Some(HomeState::WaitWriteHomingModeOp) => {
1646 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1648
1649 if !self.fb_mode_of_operation.is_busy() {
1650 if self.fb_mode_of_operation.is_error() {
1651 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1652 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1653 ));
1654 }
1655 else {
1656 log::info!("SoftHome: Drive is now in Homing Mode.");
1657 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1658 }
1659 }
1660 },
1661 Some(HomeState::WriteHomingMethod) => {
1662 self.homing_sdo_tid = self.sdo.write(
1664 client, 0x6098, 0, json!(37i8),
1665 );
1666 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1667 }
1668 Some(HomeState::WaitWriteHomingMethodDone) => {
1669 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1671 SdoResult::Ok(_) => {
1672 log::info!("SoftHome: Successfully wrote homing method.");
1673 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1674 }
1675 SdoResult::Pending => {
1676 if self.homing_timed_out() {
1677 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1678 }
1679 }
1680 SdoResult::Err(e) => {
1681 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1682 }
1683 SdoResult::Timeout => {
1684 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1685 }
1686 }
1687 }
1688 Some(HomeState::ClearHomingTrigger) => {
1689 let mut cw = RawControlWord(view.control_word());
1692 cw.set_bit(4, false);
1693 view.set_control_word(cw.raw());
1694 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1695 }
1696 Some(HomeState::TriggerHoming) => {
1697 let mut cw = RawControlWord(view.control_word());
1699 cw.set_bit(4, true);
1700 view.set_control_word(cw.raw());
1701 log::info!("SoftHome[TriggerHoming]: start homing");
1702 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1703 }
1704 Some(HomeState::WaitHomingStarted) => {
1705 let sw = view.status_word();
1711 let error = sw & (1 << 13) != 0;
1712 if error {
1713 self.restore_pp_after_error("Software homing: drive reported homing error");
1714 } else if sw & (1 << 12) == 0 {
1715 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1716 } else if self.homing_timed_out() {
1717 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1718 }
1719 }
1720 Some(HomeState::WaitHomingDone) => {
1721 let sw = view.status_word();
1723 let error = sw & (1 << 13) != 0;
1724 let attained = sw & (1 << 12) != 0;
1725 let reached = sw & (1 << 10) != 0;
1726
1727 if error {
1728 self.restore_pp_after_error("Software homing: drive reported homing error");
1729 } else if attained && reached {
1730 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
1731 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
1732 } else if self.homing_timed_out() {
1733 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
1734 }
1735 }
1736 Some(HomeState::ResetHomingTrigger) => {
1737 let mut cw = RawControlWord(view.control_word());
1742 cw.set_bit(4, false);
1743 view.set_control_word(cw.raw());
1744 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
1745 }
1746 Some(HomeState::WaitHomingTriggerCleared) => {
1747 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
1751 }
1752
1753
1754 Some(HomeState::WriteMotionModeOfOperation) => {
1755
1756 self.fb_mode_of_operation.reset();
1759 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1760 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1761 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
1762
1763 },
1764 Some(HomeState::WaitWriteMotionModeOfOperation) => {
1765 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1767
1768 if !self.fb_mode_of_operation.is_busy() {
1769 if self.fb_mode_of_operation.is_error() {
1770 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1771 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1772 ));
1773 }
1774 else {
1775 if self.is_error {
1776 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
1777 self.finish_op_error();
1778 }
1779 else {
1780 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1783 }
1784
1785 }
1786 }
1787 },
1788
1789 Some(HomeState::SendCurrentPositionTarget) => {
1790 let current_pos = view.position_actual();
1792 view.set_target_position(current_pos);
1793 view.set_profile_velocity(0);
1794 let mut cw = RawControlWord(view.control_word());
1795 cw.set_bit(4, true);
1796 cw.set_bit(5, true);
1797 cw.set_bit(6, false); view.set_control_word(cw.raw());
1799 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1800 }
1801 Some(HomeState::WaitCurrentPositionTargetSent) => {
1802 let sw = RawStatusWord(view.status_word());
1804 if sw.raw() & (1 << 12) != 0 {
1805 let mut cw = RawControlWord(view.control_word());
1806 cw.set_bit(4, false);
1807 view.set_control_word(cw.raw());
1808 log::info!("Software homing complete — position set to {} user units",
1809 self.config.home_position);
1810 self.complete_op();
1811 } else if self.homing_timed_out() {
1812 self.set_op_error("Software homing timeout: hold position not acknowledged");
1813 }
1814 }
1815 _ => self.complete_op(),
1816 }
1817 }
1818
1819 fn tick_halting(&mut self, view: &mut impl AxisView, step: u8) {
1832 match HaltState::from_repr(step) {
1833 Some(HaltState::WaitStopped) => {
1834 let pos = view.position_actual();
1838 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
1839
1840 let vel = view.velocity_actual().abs();
1841 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
1842
1843 if pos_stable || vel_stopped {
1848 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1849 } else {
1850 self.halt_stable_count = 0;
1851 }
1852
1853 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
1854 self.command_cancel_move(view);
1855 self.op_started = Some(Instant::now());
1856 self.op = AxisOp::Halting(HaltState::WaitCancelAck as u8);
1857 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1858 self.set_op_error("Halt timeout: motor did not stop");
1859 }
1860 }
1861 Some(HaltState::WaitCancelAck) => {
1862 let sw = RawStatusWord(view.status_word());
1863 let setpoint_ack = sw.raw() & (1 << 12) != 0;
1864 if setpoint_ack {
1866 let mut cw = RawControlWord(view.control_word());
1869 cw.set_bit(4, false);
1870 cw.set_bit(5, true);
1871 view.set_control_word(cw.raw());
1872 self.op_started = Some(Instant::now());
1873 self.op = AxisOp::Halting(HaltState::WaitCancelAckClear as u8);
1874 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1875 self.set_op_error("Halt timeout: cancel not acknowledged");
1876 }
1877 }
1878 Some(HaltState::WaitCancelAckClear) => {
1879 let sw = RawStatusWord(view.status_word());
1880 if sw.raw() & (1 << 12) == 0 {
1881 self.complete_op();
1883 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1884 self.set_op_error("Halt timeout: ack did not clear");
1885 }
1886 }
1887 None => {
1888 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
1889 self.complete_op();
1890 }
1891 }
1892 }
1893
1894 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1899 match step {
1900 1 => {
1901 let mut cw = RawControlWord(view.control_word());
1903 cw.cmd_fault_reset();
1904 view.set_control_word(cw.raw());
1905 self.op = AxisOp::FaultRecovery(2);
1906 }
1907 2 => {
1908 let sw = RawStatusWord(view.status_word());
1910 let state = sw.state();
1911 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1912 log::info!("Fault cleared (drive state: {})", state);
1913 self.complete_op();
1914 } else if self.op_timed_out() {
1915 self.set_op_error("Fault reset timeout: drive still faulted");
1916 }
1917 }
1918 _ => self.complete_op(),
1919 }
1920 }
1921}
1922
1923#[cfg(test)]
1928mod tests {
1929 use super::*;
1930
1931 struct MockView {
1933 control_word: u16,
1934 status_word: u16,
1935 target_position: i32,
1936 profile_velocity: u32,
1937 profile_acceleration: u32,
1938 profile_deceleration: u32,
1939 modes_of_operation: i8,
1940 modes_of_operation_display: i8,
1941 position_actual: i32,
1942 velocity_actual: i32,
1943 error_code: u16,
1944 positive_limit: bool,
1945 negative_limit: bool,
1946 home_sensor: bool,
1947 }
1948
1949 impl MockView {
1950 fn new() -> Self {
1951 Self {
1952 control_word: 0,
1953 status_word: 0x0040, target_position: 0,
1955 profile_velocity: 0,
1956 profile_acceleration: 0,
1957 profile_deceleration: 0,
1958 modes_of_operation: 0,
1959 modes_of_operation_display: 1, position_actual: 0,
1961 velocity_actual: 0,
1962 error_code: 0,
1963 positive_limit: false,
1964 negative_limit: false,
1965 home_sensor: false,
1966 }
1967 }
1968
1969 fn set_state(&mut self, state: u16) {
1970 self.status_word = state;
1971 }
1972 }
1973
1974 impl AxisView for MockView {
1975 fn control_word(&self) -> u16 { self.control_word }
1976 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1977 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1978 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1979 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1980 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1981 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1982 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1983 fn status_word(&self) -> u16 { self.status_word }
1984 fn position_actual(&self) -> i32 { self.position_actual }
1985 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1986 fn error_code(&self) -> u16 { self.error_code }
1987 fn positive_limit_active(&self) -> bool { self.positive_limit }
1988 fn negative_limit_active(&self) -> bool { self.negative_limit }
1989 fn home_sensor_active(&self) -> bool { self.home_sensor }
1990 }
1991
1992 fn test_config() -> AxisConfig {
1993 AxisConfig::new(12_800).with_user_scale(360.0)
1994 }
1995
1996 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1998 use tokio::sync::mpsc;
1999 let (write_tx, write_rx) = mpsc::unbounded_channel();
2000 let (response_tx, response_rx) = mpsc::unbounded_channel();
2001 let client = CommandClient::new(write_tx, response_rx);
2002 let axis = Axis::new(test_config(), "TestDrive");
2003 (axis, client, response_tx, write_rx)
2004 }
2005
2006 #[test]
2007 fn axis_config_conversion() {
2008 let cfg = test_config();
2009 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2011 }
2012
2013 #[test]
2014 fn enable_sequence_sets_pp_mode_and_shutdown() {
2015 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2016 let mut view = MockView::new();
2017
2018 axis.enable(&mut view);
2019
2020 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
2022 assert_eq!(view.control_word & 0x008F, 0x0006);
2024 assert_eq!(axis.op, AxisOp::Enabling(1));
2026
2027 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2030
2031 assert_eq!(view.control_word & 0x008F, 0x000F);
2033 assert_eq!(axis.op, AxisOp::Enabling(2));
2034
2035 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2038
2039 assert_eq!(axis.op, AxisOp::Idle);
2041 assert!(axis.motor_on);
2042 }
2043
2044 #[test]
2045 fn move_absolute_sets_target() {
2046 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2047 let mut view = MockView::new();
2048 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2053
2054 assert_eq!(view.target_position, 1600);
2056 assert_eq!(view.profile_velocity, 3200);
2058 assert_eq!(view.profile_acceleration, 6400);
2060 assert_eq!(view.profile_deceleration, 6400);
2061 assert!(view.control_word & (1 << 4) != 0);
2063 assert!(view.control_word & (1 << 6) == 0);
2065 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2067 }
2068
2069 #[test]
2070 fn move_relative_sets_relative_bit() {
2071 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2072 let mut view = MockView::new();
2073 view.set_state(0x0027);
2074 axis.tick(&mut view, &mut client);
2075
2076 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2077
2078 assert!(view.control_word & (1 << 6) != 0);
2080 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1, _, _)));
2081 }
2082
2083 #[test]
2084 fn move_completes_on_target_reached() {
2085 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2086 let mut view = MockView::new();
2087 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2089
2090 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2091
2092 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2095 assert!(view.control_word & (1 << 4) == 0);
2097
2098 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2101 assert_eq!(axis.op, AxisOp::Idle);
2103 assert!(!axis.in_motion);
2104 }
2105
2106 #[test]
2107 fn fault_detected_sets_error() {
2108 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2109 let mut view = MockView::new();
2110 view.set_state(0x0008); view.error_code = 0x1234;
2112
2113 axis.tick(&mut view, &mut client);
2114
2115 assert!(axis.is_error);
2116 assert_eq!(axis.error_code, 0x1234);
2117 assert!(axis.error_message.contains("fault"));
2118 }
2119
2120 #[test]
2121 fn fault_recovery_sequence() {
2122 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2123 let mut view = MockView::new();
2124 view.set_state(0x0008); axis.reset_faults(&mut view);
2127 assert!(view.control_word & 0x0080 == 0);
2129
2130 axis.tick(&mut view, &mut client);
2132 assert!(view.control_word & 0x0080 != 0);
2133
2134 view.set_state(0x0040);
2136 axis.tick(&mut view, &mut client);
2137 assert_eq!(axis.op, AxisOp::Idle);
2138 assert!(!axis.is_error);
2139 }
2140
2141 #[test]
2142 fn disable_sequence() {
2143 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2144 let mut view = MockView::new();
2145 view.set_state(0x0027); axis.disable(&mut view);
2148 assert_eq!(view.control_word & 0x008F, 0x0007);
2150
2151 view.set_state(0x0023); axis.tick(&mut view, &mut client);
2154 assert_eq!(axis.op, AxisOp::Idle);
2155 }
2156
2157 #[test]
2158 fn position_tracks_with_home_offset() {
2159 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2160 let mut view = MockView::new();
2161 view.set_state(0x0027);
2162 view.position_actual = 5000;
2163
2164 axis.enable(&mut view);
2166 view.set_state(0x0021);
2167 axis.tick(&mut view, &mut client);
2168 view.set_state(0x0027);
2169 axis.tick(&mut view, &mut client);
2170
2171 assert_eq!(axis.home_offset, 5000);
2173
2174 assert!((axis.position - 0.0).abs() < 0.01);
2176
2177 view.position_actual = 6600;
2179 axis.tick(&mut view, &mut client);
2180
2181 assert!((axis.position - 45.0).abs() < 0.1);
2183 }
2184
2185 #[test]
2186 fn set_position_adjusts_home_offset() {
2187 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2188 let mut view = MockView::new();
2189 view.position_actual = 3200;
2190
2191 axis.set_position(&view, 90.0);
2192 axis.tick(&mut view, &mut client);
2193
2194 assert_eq!(axis.home_offset, 0);
2196 assert!((axis.position - 90.0).abs() < 0.01);
2197 }
2198
2199 #[test]
2200 fn halt_runs_multi_stage_close_out() {
2201 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2202 let mut view = MockView::new();
2203 view.set_state(0x0027);
2204
2205 axis.halt(&mut view);
2206
2207 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2209 assert!(view.control_word & (1 << 4) == 0, "new_setpoint must be cleared");
2210
2211 assert!(matches!(axis.op, AxisOp::Halting(_)),
2213 "halt should enter Halting state, not Idle");
2214 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2215 assert_eq!(step, HaltState::WaitStopped as u8);
2216
2217 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2224 axis.tick(&mut view, &mut client);
2225 }
2226 assert!(matches!(axis.op, AxisOp::Halting(_)));
2228 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2229 assert_eq!(step, HaltState::WaitCancelAck as u8,
2230 "should advance past WaitStopped once position/velocity is stable");
2231
2232 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2236 }
2237
2238 #[test]
2239 fn is_busy_tracks_operations() {
2240 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2241 let mut view = MockView::new();
2242
2243 axis.tick(&mut view, &mut client);
2245 assert!(!axis.is_busy);
2246
2247 axis.enable(&mut view);
2249 axis.tick(&mut view, &mut client);
2250 assert!(axis.is_busy);
2251
2252 view.set_state(0x0021);
2254 axis.tick(&mut view, &mut client);
2255 view.set_state(0x0027);
2256 axis.tick(&mut view, &mut client);
2257 assert!(!axis.is_busy);
2258
2259 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2261 axis.tick(&mut view, &mut client);
2262 assert!(axis.is_busy);
2263 assert!(axis.in_motion);
2264 }
2265
2266 #[test]
2267 fn fault_during_move_cancels_op() {
2268 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2269 let mut view = MockView::new();
2270 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2272
2273 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2275 axis.tick(&mut view, &mut client);
2276 assert!(axis.is_busy);
2277 assert!(!axis.is_error);
2278
2279 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2282
2283 assert!(!axis.is_busy);
2285 assert!(axis.is_error);
2286 assert_eq!(axis.op, AxisOp::Idle);
2287 }
2288
2289 #[test]
2290 fn move_absolute_rejected_by_max_limit() {
2291 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2292 let mut view = MockView::new();
2293 view.set_state(0x0027);
2294 axis.tick(&mut view, &mut client);
2295
2296 axis.set_software_max_limit(90.0);
2297 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2298
2299 assert!(axis.is_error);
2301 assert_eq!(axis.op, AxisOp::Idle);
2302 assert!(axis.error_message.contains("max software limit"));
2303 }
2304
2305 #[test]
2306 fn move_absolute_rejected_by_min_limit() {
2307 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2308 let mut view = MockView::new();
2309 view.set_state(0x0027);
2310 axis.tick(&mut view, &mut client);
2311
2312 axis.set_software_min_limit(-10.0);
2313 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2314
2315 assert!(axis.is_error);
2316 assert_eq!(axis.op, AxisOp::Idle);
2317 assert!(axis.error_message.contains("min software limit"));
2318 }
2319
2320 #[test]
2321 fn move_relative_rejected_by_max_limit() {
2322 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2323 let mut view = MockView::new();
2324 view.set_state(0x0027);
2325 axis.tick(&mut view, &mut client);
2326
2327 axis.set_software_max_limit(50.0);
2329 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2330
2331 assert!(axis.is_error);
2332 assert_eq!(axis.op, AxisOp::Idle);
2333 assert!(axis.error_message.contains("max software limit"));
2334 }
2335
2336 #[test]
2337 fn move_within_limits_allowed() {
2338 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2339 let mut view = MockView::new();
2340 view.set_state(0x0027);
2341 axis.tick(&mut view, &mut client);
2342
2343 axis.set_software_max_limit(90.0);
2344 axis.set_software_min_limit(-90.0);
2345 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2346
2347 assert!(!axis.is_error);
2349 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2350 }
2351
2352 #[test]
2353 fn runtime_limit_halts_move_in_violated_direction() {
2354 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2355 let mut view = MockView::new();
2356 view.set_state(0x0027);
2357 axis.tick(&mut view, &mut client);
2358
2359 axis.set_software_max_limit(45.0);
2360 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2362
2363 view.position_actual = 1650;
2366 view.velocity_actual = 100; view.status_word = 0x1027;
2370 axis.tick(&mut view, &mut client);
2371 view.status_word = 0x0027;
2372 axis.tick(&mut view, &mut client);
2373
2374 assert!(axis.is_error);
2376 assert!(axis.at_max_limit);
2377 assert_eq!(axis.op, AxisOp::Idle);
2378 assert!(axis.error_message.contains("Software position limit"));
2379 assert!(view.control_word & (1 << 8) != 0);
2381 }
2382
2383 #[test]
2384 fn runtime_limit_allows_move_in_opposite_direction() {
2385 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2386 let mut view = MockView::new();
2387 view.set_state(0x0027);
2388 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2391 axis.tick(&mut view, &mut client);
2392 assert!(axis.at_max_limit);
2393
2394 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2396 assert!(!axis.is_error);
2397 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2398
2399 view.velocity_actual = -100;
2401 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2403 assert!(!axis.is_error);
2405 }
2406
2407 #[test]
2408 fn positive_limit_switch_halts_positive_move() {
2409 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2410 let mut view = MockView::new();
2411 view.set_state(0x0027);
2412 axis.tick(&mut view, &mut client);
2413
2414 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2416 view.velocity_actual = 100; view.status_word = 0x1027;
2419 axis.tick(&mut view, &mut client);
2420 view.status_word = 0x0027;
2421
2422 view.positive_limit = true;
2424 axis.tick(&mut view, &mut client);
2425
2426 assert!(axis.is_error);
2427 assert!(axis.at_positive_limit_switch);
2428 assert!(!axis.is_busy);
2429 assert!(axis.error_message.contains("Positive limit switch"));
2430 assert!(view.control_word & (1 << 8) != 0);
2432 }
2433
2434 #[test]
2435 fn negative_limit_switch_halts_negative_move() {
2436 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2437 let mut view = MockView::new();
2438 view.set_state(0x0027);
2439 axis.tick(&mut view, &mut client);
2440
2441 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2443 view.velocity_actual = -100; view.status_word = 0x1027;
2445 axis.tick(&mut view, &mut client);
2446 view.status_word = 0x0027;
2447
2448 view.negative_limit = true;
2450 axis.tick(&mut view, &mut client);
2451
2452 assert!(axis.is_error);
2453 assert!(axis.at_negative_limit_switch);
2454 assert!(axis.error_message.contains("Negative limit switch"));
2455 }
2456
2457 #[test]
2458 fn limit_switch_allows_move_in_opposite_direction() {
2459 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2460 let mut view = MockView::new();
2461 view.set_state(0x0027);
2462 view.positive_limit = true;
2464 view.velocity_actual = -100;
2465 axis.tick(&mut view, &mut client);
2466 assert!(axis.at_positive_limit_switch);
2467
2468 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2470 view.status_word = 0x1027;
2471 axis.tick(&mut view, &mut client);
2472
2473 assert!(!axis.is_error);
2475 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
2476 }
2477
2478 #[test]
2479 fn limit_switch_ignored_when_not_moving() {
2480 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2481 let mut view = MockView::new();
2482 view.set_state(0x0027);
2483 view.positive_limit = true;
2484
2485 axis.tick(&mut view, &mut client);
2486
2487 assert!(axis.at_positive_limit_switch);
2489 assert!(!axis.is_error);
2490 }
2491
2492 #[test]
2493 fn home_sensor_output_tracks_view() {
2494 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2495 let mut view = MockView::new();
2496 view.set_state(0x0027);
2497
2498 axis.tick(&mut view, &mut client);
2499 assert!(!axis.home_sensor);
2500
2501 view.home_sensor = true;
2502 axis.tick(&mut view, &mut client);
2503 assert!(axis.home_sensor);
2504
2505 view.home_sensor = false;
2506 axis.tick(&mut view, &mut client);
2507 assert!(!axis.home_sensor);
2508 }
2509
2510 #[test]
2511 fn velocity_output_converted() {
2512 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2513 let mut view = MockView::new();
2514 view.set_state(0x0027);
2515 view.velocity_actual = 3200;
2517
2518 axis.tick(&mut view, &mut client);
2519
2520 assert!((axis.speed - 90.0).abs() < 0.1);
2521 assert!(axis.moving_positive);
2522 assert!(!axis.moving_negative);
2523 }
2524
2525 fn soft_homing_config() -> AxisConfig {
2528 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2529 cfg.homing_speed = 10.0;
2530 cfg.homing_accel = 20.0;
2531 cfg.homing_decel = 20.0;
2532 cfg
2533 }
2534
2535 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2536 use tokio::sync::mpsc;
2537 let (write_tx, write_rx) = mpsc::unbounded_channel();
2538 let (response_tx, response_rx) = mpsc::unbounded_channel();
2539 let client = CommandClient::new(write_tx, response_rx);
2540 let axis = Axis::new(soft_homing_config(), "TestDrive");
2541 (axis, client, response_tx, write_rx)
2542 }
2543
2544 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2546 view.set_state(0x0027); axis.tick(view, client);
2548 }
2549
2550 fn complete_soft_homing(
2555 axis: &mut Axis,
2556 view: &mut MockView,
2557 client: &mut CommandClient,
2558 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2559 trigger_pos: i32,
2560 clear_sensor: impl FnOnce(&mut MockView),
2561 ) {
2562 use mechutil::ipc::CommandMessage as IpcMsg;
2563
2564 axis.tick(view, client);
2567 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2568
2569 view.position_actual = trigger_pos + 100;
2571 axis.tick(view, client);
2572 view.position_actual = trigger_pos + 120;
2573 axis.tick(view, client);
2574 for _ in 0..10 { axis.tick(view, client); }
2576 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2577
2578 view.status_word = 0x1027;
2580 axis.tick(view, client);
2581 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2582 view.status_word = 0x0027;
2583
2584 for _ in 0..100 { axis.tick(view, client); }
2586 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2587
2588 axis.tick(view, client);
2591 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2592
2593 view.status_word = 0x1027;
2595 axis.tick(view, client);
2596 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2597 view.status_word = 0x0027;
2598
2599 axis.tick(view, client);
2601 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2602 clear_sensor(view);
2603 view.position_actual = trigger_pos - 200;
2604 axis.tick(view, client);
2605 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2606
2607 axis.tick(view, client);
2609 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2610 for _ in 0..10 { axis.tick(view, client); }
2611 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2612
2613 view.status_word = 0x1027;
2616 axis.tick(view, client);
2617 view.status_word = 0x0027;
2618 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2619
2620 let tid = axis.homing_sdo_tid;
2622 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2623 client.poll();
2624 axis.tick(view, client);
2625 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2626
2627 axis.tick(view, client);
2629 let tid = axis.homing_sdo_tid;
2630 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2631 client.poll();
2632 axis.tick(view, client);
2633 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2634
2635 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2637 axis.tick(view, client);
2638 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2639
2640 view.status_word = 0x1427; axis.tick(view, client);
2643 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2644 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2645 view.status_word = 0x0027;
2646
2647 axis.tick(view, client);
2649 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2650
2651 view.status_word = 0x1027;
2653 axis.tick(view, client);
2654 view.status_word = 0x0027;
2655
2656 assert_eq!(axis.op, AxisOp::Idle);
2657 assert!(!axis.is_busy);
2658 assert!(!axis.is_error);
2659 assert_eq!(axis.home_offset, 0); }
2661
2662 #[test]
2663 fn soft_homing_pnp_home_sensor_full_sequence() {
2664 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2665 let mut view = MockView::new();
2666 enable_axis(&mut axis, &mut view, &mut client);
2667
2668 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2669
2670 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2673 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2675 axis.tick(&mut view, &mut client); view.home_sensor = true;
2679 view.position_actual = 5000;
2680 axis.tick(&mut view, &mut client);
2681 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2682
2683 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2684 |v| { v.home_sensor = false; });
2685 }
2686
2687 #[test]
2688 fn soft_homing_npn_home_sensor_full_sequence() {
2689 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2690 let mut view = MockView::new();
2691 view.home_sensor = true;
2693 enable_axis(&mut axis, &mut view, &mut client);
2694
2695 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2696
2697 axis.tick(&mut view, &mut client);
2699 view.status_word = 0x1027;
2700 axis.tick(&mut view, &mut client);
2701 view.status_word = 0x0027;
2702 axis.tick(&mut view, &mut client);
2703
2704 view.home_sensor = false;
2706 view.position_actual = 3000;
2707 axis.tick(&mut view, &mut client);
2708 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2709
2710 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2711 |v| { v.home_sensor = true; }); }
2713
2714 #[test]
2715 fn soft_homing_limit_switch_suppresses_halt() {
2716 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2717 let mut view = MockView::new();
2718 enable_axis(&mut axis, &mut view, &mut client);
2719
2720 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
2722
2723 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2728 axis.tick(&mut view, &mut client); view.positive_limit = true;
2732 view.velocity_actual = 100; view.position_actual = 8000;
2734 axis.tick(&mut view, &mut client);
2735
2736 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2738 assert!(!axis.is_error);
2739 }
2740
2741 #[test]
2742 fn soft_homing_opposite_limit_still_protects() {
2743 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2744 let mut view = MockView::new();
2745 enable_axis(&mut axis, &mut view, &mut client);
2746
2747 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2749
2750 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2755 axis.tick(&mut view, &mut client); view.negative_limit = true;
2760 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
2762
2763 assert!(axis.is_error);
2765 assert!(axis.error_message.contains("Negative limit switch"));
2766 }
2767
2768 #[test]
2769 #[test]
2787 fn soft_homing_negative_direction_sets_negative_target() {
2788 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2789 let mut view = MockView::new();
2790 enable_axis(&mut axis, &mut view, &mut client);
2791
2792 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
2793 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
2797 }
2798
2799 #[test]
2800 fn home_integrated_method_starts_hardware_homing() {
2801 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2802 let mut view = MockView::new();
2803 enable_axis(&mut axis, &mut view, &mut client);
2804
2805 axis.home(&mut view, HomingMethod::CurrentPosition);
2806 assert!(matches!(axis.op, AxisOp::Homing(0)));
2807 assert_eq!(axis.homing_method, 37);
2808 }
2809
2810 #[test]
2811 fn home_integrated_arbitrary_code() {
2812 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2813 let mut view = MockView::new();
2814 enable_axis(&mut axis, &mut view, &mut client);
2815
2816 axis.home(&mut view, HomingMethod::Integrated(35));
2817 assert!(matches!(axis.op, AxisOp::Homing(0)));
2818 assert_eq!(axis.homing_method, 35);
2819 }
2820
2821 #[test]
2822 fn hardware_homing_skips_speed_sdos_when_zero() {
2823 use mechutil::ipc::CommandMessage;
2824
2825 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
2826 let mut view = MockView::new();
2827 enable_axis(&mut axis, &mut view, &mut client);
2828
2829 axis.home(&mut view, HomingMethod::Integrated(37));
2831
2832 axis.tick(&mut view, &mut client);
2834 assert!(matches!(axis.op, AxisOp::Homing(1)));
2835
2836 let _ = write_rx.try_recv();
2838
2839 let tid = axis.homing_sdo_tid;
2841 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2842 client.poll();
2843 axis.tick(&mut view, &mut client);
2844
2845 assert!(matches!(axis.op, AxisOp::Homing(8)));
2847 }
2848
2849 #[test]
2850 fn hardware_homing_writes_speed_sdos_when_nonzero() {
2851 use mechutil::ipc::CommandMessage;
2852
2853 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
2854 let mut view = MockView::new();
2855 enable_axis(&mut axis, &mut view, &mut client);
2856
2857 axis.home(&mut view, HomingMethod::Integrated(37));
2859
2860 axis.tick(&mut view, &mut client);
2862 assert!(matches!(axis.op, AxisOp::Homing(1)));
2863 let _ = write_rx.try_recv();
2864
2865 let tid = axis.homing_sdo_tid;
2867 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2868 client.poll();
2869 axis.tick(&mut view, &mut client);
2870 assert!(matches!(axis.op, AxisOp::Homing(2)));
2872 }
2873
2874 #[test]
2875 fn soft_homing_edge_during_ack_step() {
2876 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2877 let mut view = MockView::new();
2878 enable_axis(&mut axis, &mut view, &mut client);
2879
2880 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2881 axis.tick(&mut view, &mut client); view.home_sensor = true;
2885 view.position_actual = 2000;
2886 axis.tick(&mut view, &mut client);
2887
2888 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2890 }
2891
2892 #[test]
2893 fn soft_homing_applies_home_position() {
2894 let mut cfg = soft_homing_config();
2895 cfg.home_position = 90.0;
2896
2897 use tokio::sync::mpsc;
2898 let (write_tx, _write_rx) = mpsc::unbounded_channel();
2899 let (resp_tx, response_rx) = mpsc::unbounded_channel();
2900 let mut client = CommandClient::new(write_tx, response_rx);
2901 let mut axis = Axis::new(cfg, "TestDrive");
2902
2903 let mut view = MockView::new();
2904 enable_axis(&mut axis, &mut view, &mut client);
2905
2906 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2907
2908 axis.tick(&mut view, &mut client);
2910 view.status_word = 0x1027;
2911 axis.tick(&mut view, &mut client);
2912 view.status_word = 0x0027;
2913 axis.tick(&mut view, &mut client);
2914
2915 view.home_sensor = true;
2917 view.position_actual = 5000;
2918 axis.tick(&mut view, &mut client);
2919 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2920
2921 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2923 |v| { v.home_sensor = false; });
2924
2925 assert_eq!(axis.home_offset, 0);
2927 }
2928
2929 #[test]
2930 fn soft_homing_default_home_position_zero() {
2931 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2932 let mut view = MockView::new();
2933 enable_axis(&mut axis, &mut view, &mut client);
2934
2935 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2936
2937 axis.tick(&mut view, &mut client);
2939 view.status_word = 0x1027;
2940 axis.tick(&mut view, &mut client);
2941 view.status_word = 0x0027;
2942 axis.tick(&mut view, &mut client);
2943
2944 view.home_sensor = true;
2946 view.position_actual = 5000;
2947 axis.tick(&mut view, &mut client);
2948
2949 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2950 |v| { v.home_sensor = false; });
2951
2952 assert_eq!(axis.home_offset, 0);
2953 }
2954}