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,
54 FaultRecovery(u8),
55}
56
57#[repr(u8)]
58#[derive(Debug, Clone, PartialEq, FromRepr)]
59enum HomeState {
60 EnsurePpMode = 0,
61 WaitPpMode = 1,
62 Search = 5,
63 WaitSearching = 10,
64 WaitFoundSensor = 20,
65 WaitStoppedFoundSensor = 30,
66 WaitFoundSensorAck = 40,
67 WaitFoundSensorAckClear = 45,
68 DebounceFoundSensor = 50,
69 BackOff = 60,
70 WaitBackingOff = 70,
71 WaitLostSensor = 80,
72 WaitStoppedLostSensor = 90,
73 WaitLostSensorAck = 100,
74 WaitLostSensorAckClear = 120,
75 WaitHomeOffsetDone = 125,
76
77 WriteHomingModeOp = 160,
78 WaitWriteHomingModeOp = 165,
79
80 WriteHomingMethod = 205,
81 WaitWriteHomingMethodDone = 210,
82 ClearHomingTrigger = 215,
83 TriggerHoming = 217,
84 WaitHomingStarted = 218,
85 WaitHomingDone = 220,
86 ResetHomingTrigger = 222,
87 WaitHomingTriggerCleared = 223,
88 WriteMotionModeOfOperation = 230,
89 WaitWriteMotionModeOfOperation = 235,
90 SendCurrentPositionTarget = 240,
91 WaitCurrentPositionTargetSent = 245
92
93}
94
95#[derive(Debug, Clone, PartialEq)]
96enum MoveKind {
97 Absolute,
98 Relative,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq)]
102enum SoftHomeSensor {
103 PositiveLimit,
104 NegativeLimit,
105 HomeSensor,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq)]
109enum SoftHomeSensorType {
110 Pnp,
112 Npn,
114}
115
116pub struct Axis {
126 config: AxisConfig,
127 sdo: SdoClient,
128
129 op: AxisOp,
131 home_offset: i32,
132 last_raw_position: i32,
133 op_started: Option<Instant>,
134 op_timeout: Duration,
135 homing_timeout: Duration,
136 move_start_timeout: Duration,
137 pending_move_target: i32,
138 pending_move_vel: u32,
139 pending_move_accel: u32,
140 pending_move_decel: u32,
141 homing_method: i8,
142 homing_sdo_tid: u32,
143 soft_home_sensor: SoftHomeSensor,
144 soft_home_sensor_type: SoftHomeSensorType,
145 soft_home_direction: f64,
146 halt_stable_count: u8,
147 prev_positive_limit: bool,
148 prev_negative_limit: bool,
149 prev_home_sensor: bool,
150
151
152
153 fb_mode_of_operation : FbSetModeOfOperation,
154
155 pub is_error: bool,
159 pub error_code: u32,
161 pub error_message: String,
163 pub motor_on: bool,
165 pub is_busy: bool,
171 pub in_motion: bool,
173 pub moving_positive: bool,
175 pub moving_negative: bool,
177 pub position: f64,
179 pub raw_position: i64,
181 pub speed: f64,
183 pub at_max_limit: bool,
185 pub at_min_limit: bool,
187 pub at_positive_limit_switch: bool,
189 pub at_negative_limit_switch: bool,
191 pub home_sensor: bool,
193
194
195 ton : Ton
197}
198
199impl Axis {
200 pub fn new(config: AxisConfig, device_name: &str) -> Self {
205 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
206 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
207 let move_start_timeout = op_timeout; Self {
209 config,
210 sdo: SdoClient::new(device_name),
211 op: AxisOp::Idle,
212 home_offset: 0,
213 last_raw_position: 0,
214 op_started: None,
215 op_timeout,
216 homing_timeout,
217 move_start_timeout,
218 pending_move_target: 0,
219 pending_move_vel: 0,
220 pending_move_accel: 0,
221 pending_move_decel: 0,
222 homing_method: 37,
223 homing_sdo_tid: 0,
224 soft_home_sensor: SoftHomeSensor::HomeSensor,
225 soft_home_sensor_type: SoftHomeSensorType::Pnp,
226 soft_home_direction: 1.0,
227 halt_stable_count: 0,
228 prev_positive_limit: false,
229 prev_negative_limit: false,
230 prev_home_sensor: false,
231 is_error: false,
232 error_code: 0,
233 error_message: String::new(),
234 motor_on: false,
235 is_busy: false,
236 in_motion: false,
237 moving_positive: false,
238 moving_negative: false,
239 position: 0.0,
240 raw_position: 0,
241 speed: 0.0,
242 at_max_limit: false,
243 at_min_limit: false,
244 at_positive_limit_switch: false,
245 at_negative_limit_switch: false,
246 home_sensor: false,
247 ton: Ton::new(),
248 fb_mode_of_operation : FbSetModeOfOperation::new()
249 }
250 }
251
252 pub fn config(&self) -> &AxisConfig {
254 &self.config
255 }
256
257 pub fn move_absolute(
267 &mut self,
268 view: &mut impl AxisView,
269 target: f64,
270 vel: f64,
271 accel: f64,
272 decel: f64,
273 ) {
274 if let Some(msg) = self.check_target_limit(target, view) {
275 self.set_op_error(&msg);
276 return;
277 }
278
279 let cpu = self.config.counts_per_user();
280 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
281 let raw_vel = (vel * cpu).round() as u32;
282 let raw_accel = (accel * cpu).round() as u32;
283 let raw_decel = (decel * cpu).round() as u32;
284
285 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute);
286 }
287
288 pub fn move_relative(
294 &mut self,
295 view: &mut impl AxisView,
296 distance: f64,
297 vel: f64,
298 accel: f64,
299 decel: f64,
300 ) {
301 log::info!("Axis: request to move relative dist {} vel {} accel {} decel {}",
302 distance, vel, accel, decel
303 );
304
305 if let Some(msg) = self.check_target_limit(self.position + distance, view) {
306 self.set_op_error(&msg);
307 return;
308 }
309
310 let cpu = self.config.counts_per_user();
311 let raw_distance = self.config.to_counts(distance).round() as i32;
312 let raw_vel = (vel * cpu).round() as u32;
313 let raw_accel = (accel * cpu).round() as u32;
314 let raw_decel = (decel * cpu).round() as u32;
315
316 log::info!("Axis starting relative move: request to move relative raw dist {} raw vel {} raw accel {} raw decel {}",
317 raw_distance, raw_vel, raw_accel, raw_decel
318 );
319
320 let mut cw = RawControlWord(view.control_word());
322 cw.set_bit(4, false); view.set_control_word(cw.raw());
324
325 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative);
326 }
327
328 fn start_move(
329 &mut self,
330 view: &mut impl AxisView,
331 raw_target: i32,
332 raw_vel: u32,
333 raw_accel: u32,
334 raw_decel: u32,
335 kind: MoveKind,
336 ) {
337 self.pending_move_target = raw_target;
338 self.pending_move_vel = raw_vel;
339 self.pending_move_accel = raw_accel;
340 self.pending_move_decel = raw_decel;
341
342 view.set_target_position(raw_target);
344 view.set_profile_velocity(raw_vel);
345 view.set_profile_acceleration(raw_accel);
346 view.set_profile_deceleration(raw_decel);
347
348 let mut cw = RawControlWord(view.control_word());
350 cw.set_bit(6, kind == MoveKind::Relative);
351 cw.set_bit(4, true); view.set_control_word(cw.raw());
353
354 self.op = AxisOp::Moving(kind, 1);
355 self.op_started = Some(Instant::now());
356 }
357
358 pub fn halt(&mut self, view: &mut impl AxisView) {
360 let mut cw = RawControlWord(view.control_word());
361 cw.set_bit(8, true); view.set_control_word(cw.raw());
363 self.op = AxisOp::Halting;
364 }
365
366 pub fn enable(&mut self, view: &mut impl AxisView) {
374 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
376 let mut cw = RawControlWord(view.control_word());
377 cw.cmd_shutdown();
378 view.set_control_word(cw.raw());
379
380 self.op = AxisOp::Enabling(1);
381 self.op_started = Some(Instant::now());
382 }
383
384 pub fn disable(&mut self, view: &mut impl AxisView) {
386 let mut cw = RawControlWord(view.control_word());
387 cw.cmd_disable_operation();
388 view.set_control_word(cw.raw());
389
390 self.op = AxisOp::Disabling(1);
391 self.op_started = Some(Instant::now());
392 }
393
394 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
398 let mut cw = RawControlWord(view.control_word());
400 cw.cmd_clear_fault_reset();
401 view.set_control_word(cw.raw());
402
403 self.is_error = false;
404 self.error_code = 0;
405 self.error_message.clear();
406 self.op = AxisOp::FaultRecovery(1);
407 self.op_started = Some(Instant::now());
408 }
409
410 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
418 if method.is_integrated() {
419 self.homing_method = method.cia402_code();
420 self.op = AxisOp::Homing(0);
421 self.op_started = Some(Instant::now());
422 let _ = view;
423 } else {
424 self.configure_soft_homing(method);
425 self.start_soft_homing(view);
426 }
427 }
428
429 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
438 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
439 }
440
441 pub fn set_home_position(&mut self, user_units: f64) {
445 self.config.home_position = user_units;
446 }
447
448 pub fn set_software_max_limit(&mut self, user_units: f64) {
450 self.config.max_position_limit = user_units;
451 self.config.enable_max_position_limit = true;
452 }
453
454 pub fn set_software_min_limit(&mut self, user_units: f64) {
456 self.config.min_position_limit = user_units;
457 self.config.enable_min_position_limit = true;
458 }
459
460 pub fn sdo_write(
466 &mut self,
467 client: &mut CommandClient,
468 index: u16,
469 sub_index: u8,
470 value: serde_json::Value,
471 ) {
472 self.sdo.write(client, index, sub_index, value);
473 }
474
475 pub fn sdo_read(
477 &mut self,
478 client: &mut CommandClient,
479 index: u16,
480 sub_index: u8,
481 ) -> u32 {
482 self.sdo.read(client, index, sub_index)
483 }
484
485 pub fn sdo_result(
487 &mut self,
488 client: &mut CommandClient,
489 tid: u32,
490 ) -> SdoResult {
491 self.sdo.result(client, tid, Duration::from_secs(5))
492 }
493
494 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
508 self.check_faults(view);
509 self.progress_op(view, client);
510 self.update_outputs(view);
511 self.check_limits(view);
512 }
513
514 fn update_outputs(&mut self, view: &impl AxisView) {
519 let raw = view.position_actual();
520 self.raw_position = raw as i64;
521 self.position = self.config.to_user((raw - self.home_offset) as f64);
522
523 let vel = view.velocity_actual();
524 let user_vel = self.config.to_user(vel as f64);
525 self.speed = user_vel.abs();
526 self.moving_positive = user_vel > 0.0;
527 self.moving_negative = user_vel < 0.0;
528 self.is_busy = self.op != AxisOp::Idle;
529 self.in_motion = matches!(self.op, AxisOp::Moving(_, _) | AxisOp::SoftHoming(_));
530
531 let sw = RawStatusWord(view.status_word());
532 self.motor_on = sw.state() == Cia402State::OperationEnabled;
533
534 self.last_raw_position = raw;
535 }
536
537 fn check_faults(&mut self, view: &impl AxisView) {
542 let sw = RawStatusWord(view.status_word());
543 let state = sw.state();
544
545 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
546 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
547 self.is_error = true;
548 let ec = view.error_code();
549 if ec != 0 {
550 self.error_code = ec as u32;
551 }
552 self.error_message = format!("Drive fault (state: {})", state);
553 self.op = AxisOp::Idle;
555 self.op_started = None;
556 }
557 }
558 }
559
560 fn op_timed_out(&self) -> bool {
565 self.op_started
566 .map_or(false, |t| t.elapsed() > self.op_timeout)
567 }
568
569 fn homing_timed_out(&self) -> bool {
570 self.op_started
571 .map_or(false, |t| t.elapsed() > self.homing_timeout)
572 }
573
574 fn move_start_timed_out(&self) -> bool {
575 self.op_started
576 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
577 }
578
579 fn set_op_error(&mut self, msg: &str) {
580 self.is_error = true;
581 self.error_message = msg.to_string();
582 self.op = AxisOp::Idle;
583 self.op_started = None;
584 self.is_busy = false;
585 self.in_motion = false;
586 log::error!("Axis error: {}", msg);
587 }
588
589 fn restore_pp_after_error(&mut self, msg: &str) {
590 self.is_error = true;
591 self.error_message = msg.to_string();
592 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);;
593 log::error!("Axis error: {}", msg);
594 }
595
596 fn finish_op_error(&mut self) {
597 self.op = AxisOp::Idle;
598 self.op_started = None;
599 self.is_busy = false;
600 self.in_motion = false;
601 }
602
603 fn complete_op(&mut self) {
604 self.op = AxisOp::Idle;
605 self.op_started = None;
606 }
607
608 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
617 let static_limit = if self.config.enable_max_position_limit {
618 Some(self.config.max_position_limit)
619 } else {
620 None
621 };
622 match (static_limit, view.dynamic_max_position_limit()) {
623 (Some(s), Some(d)) => Some(s.min(d)),
624 (Some(v), None) | (None, Some(v)) => Some(v),
625 (None, None) => None,
626 }
627 }
628
629 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
633 let static_limit = if self.config.enable_min_position_limit {
634 Some(self.config.min_position_limit)
635 } else {
636 None
637 };
638 match (static_limit, view.dynamic_min_position_limit()) {
639 (Some(s), Some(d)) => Some(s.max(d)),
640 (Some(v), None) | (None, Some(v)) => Some(v),
641 (None, None) => None,
642 }
643 }
644
645 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
650 if let Some(max) = self.effective_max_limit(view) {
651 if target > max {
652 return Some(format!(
653 "Target {:.3} exceeds max software limit {:.3}",
654 target, max
655 ));
656 }
657 }
658 if let Some(min) = self.effective_min_limit(view) {
659 if target < min {
660 return Some(format!(
661 "Target {:.3} exceeds min software limit {:.3}",
662 target, min
663 ));
664 }
665 }
666 None
667 }
668
669 fn check_limits(&mut self, view: &mut impl AxisView) {
678 let eff_max = self.effective_max_limit(view);
680 let eff_min = self.effective_min_limit(view);
681 let sw_max = eff_max.map_or(false, |m| self.position >= m);
682 let sw_min = eff_min.map_or(false, |m| self.position <= m);
683
684 self.at_max_limit = sw_max;
685 self.at_min_limit = sw_min;
686
687 let hw_pos = view.positive_limit_active();
689 let hw_neg = view.negative_limit_active();
690
691 self.at_positive_limit_switch = hw_pos;
692 self.at_negative_limit_switch = hw_neg;
693
694 self.home_sensor = view.home_sensor_active();
696
697 self.prev_positive_limit = hw_pos;
699 self.prev_negative_limit = hw_neg;
700 self.prev_home_sensor = view.home_sensor_active();
701
702 let is_moving = matches!(self.op, AxisOp::Moving(_, _));
704 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
705
706 if !is_moving && !is_soft_homing {
707 return;
708 }
709
710 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
712 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
713
714 let effective_hw_pos = hw_pos && !suppress_pos;
715 let effective_hw_neg = hw_neg && !suppress_neg;
716
717 let effective_sw_max = sw_max && !is_soft_homing;
719 let effective_sw_min = sw_min && !is_soft_homing;
720
721 let positive_blocked = (effective_sw_max || effective_hw_pos) && self.moving_positive;
722 let negative_blocked = (effective_sw_min || effective_hw_neg) && self.moving_negative;
723
724 if positive_blocked || negative_blocked {
725 let mut cw = RawControlWord(view.control_word());
726 cw.set_bit(8, true); view.set_control_word(cw.raw());
728
729 let msg = if effective_hw_pos && self.moving_positive {
730 "Positive limit switch active".to_string()
731 } else if effective_hw_neg && self.moving_negative {
732 "Negative limit switch active".to_string()
733 } else if effective_sw_max && self.moving_positive {
734 format!(
735 "Software position limit: position {:.3} >= max {:.3}",
736 self.position, eff_max.unwrap_or(self.position)
737 )
738 } else {
739 format!(
740 "Software position limit: position {:.3} <= min {:.3}",
741 self.position, eff_min.unwrap_or(self.position)
742 )
743 };
744 self.set_op_error(&msg);
745 }
746 }
747
748 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
753 match self.op.clone() {
754 AxisOp::Idle => {}
755 AxisOp::Enabling(step) => self.tick_enabling(view, step),
756 AxisOp::Disabling(step) => self.tick_disabling(view, step),
757 AxisOp::Moving(kind, step) => self.tick_moving(view, kind, step),
758 AxisOp::Homing(step) => self.tick_homing(view, client, step),
759 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
760 AxisOp::Halting => self.tick_halting(view),
761 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
762 }
763 }
764
765 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
770 match step {
771 1 => {
772 let sw = RawStatusWord(view.status_word());
773 if sw.state() == Cia402State::ReadyToSwitchOn {
774 let mut cw = RawControlWord(view.control_word());
775 cw.cmd_enable_operation();
776 view.set_control_word(cw.raw());
777 self.op = AxisOp::Enabling(2);
778 } else if self.op_timed_out() {
779 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
780 }
781 }
782 2 => {
783 let sw = RawStatusWord(view.status_word());
784 if sw.state() == Cia402State::OperationEnabled {
785 self.complete_op();
792 } else if self.op_timed_out() {
793 self.set_op_error("Enable timeout: waiting for OperationEnabled");
794 }
795 }
796 _ => self.complete_op(),
797 }
798 }
799
800 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
804 match step {
805 1 => {
806 let sw = RawStatusWord(view.status_word());
807 if sw.state() != Cia402State::OperationEnabled {
808 self.complete_op();
809 } else if self.op_timed_out() {
810 self.set_op_error("Disable timeout: drive still in OperationEnabled");
811 }
812 }
813 _ => self.complete_op(),
814 }
815 }
816
817 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8) {
823 match step {
824 1 => {
825 let sw = RawStatusWord(view.status_word());
827 if sw.raw() & (1 << 12) != 0 {
828 let mut cw = RawControlWord(view.control_word());
830 cw.set_bit(4, false);
831 view.set_control_word(cw.raw());
832 self.op = AxisOp::Moving(kind, 2);
833 } else if self.move_start_timed_out() {
834 self.set_op_error("Move timeout: set-point not acknowledged");
835 }
836 },
837 2 => {
838 let sw = RawStatusWord(view.status_word());
840 if sw.raw() & (1 << 12) == 0 {
841 self.op = AxisOp::Moving(kind, 3);
843 }
844 },
845 3 => {
846 let sw = RawStatusWord(view.status_word());
848 if sw.target_reached() {
849 self.complete_op();
850 }
851 },
852 _ => self.complete_op(),
853 }
854 }
855
856 fn tick_homing(
874 &mut self,
875 view: &mut impl AxisView,
876 client: &mut CommandClient,
877 step: u8,
878 ) {
879 match step {
880 0 => {
881 self.homing_sdo_tid = self.sdo.write(
883 client,
884 0x6098,
885 0,
886 json!(self.homing_method),
887 );
888 self.op = AxisOp::Homing(1);
889 }
890 1 => {
891 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
893 SdoResult::Ok(_) => {
894 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
896 self.op = AxisOp::Homing(8);
897 } else {
898 self.op = AxisOp::Homing(2);
899 }
900 }
901 SdoResult::Pending => {
902 if self.homing_timed_out() {
903 self.set_op_error("Homing timeout: SDO write for homing method");
904 }
905 }
906 SdoResult::Err(e) => {
907 self.set_op_error(&format!("Homing SDO error: {}", e));
908 }
909 SdoResult::Timeout => {
910 self.set_op_error("Homing timeout: SDO write timed out");
911 }
912 }
913 }
914 2 => {
915 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
917 self.homing_sdo_tid = self.sdo.write(
918 client,
919 0x6099,
920 1,
921 json!(speed_counts),
922 );
923 self.op = AxisOp::Homing(3);
924 }
925 3 => {
926 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
927 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
928 SdoResult::Pending => {
929 if self.homing_timed_out() {
930 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
931 }
932 }
933 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
934 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
935 }
936 }
937 4 => {
938 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
940 self.homing_sdo_tid = self.sdo.write(
941 client,
942 0x6099,
943 2,
944 json!(speed_counts),
945 );
946 self.op = AxisOp::Homing(5);
947 }
948 5 => {
949 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
950 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
951 SdoResult::Pending => {
952 if self.homing_timed_out() {
953 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
954 }
955 }
956 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
957 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
958 }
959 }
960 6 => {
961 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
963 self.homing_sdo_tid = self.sdo.write(
964 client,
965 0x609A,
966 0,
967 json!(accel_counts),
968 );
969 self.op = AxisOp::Homing(7);
970 }
971 7 => {
972 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
973 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
974 SdoResult::Pending => {
975 if self.homing_timed_out() {
976 self.set_op_error("Homing timeout: SDO write for homing acceleration");
977 }
978 }
979 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
980 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
981 }
982 }
983 8 => {
984 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
987 let mut cw = RawControlWord(view.control_word());
988 cw.set_bit(4, false);
989 view.set_control_word(cw.raw());
990 self.op = AxisOp::Homing(9);
991 }
992 9 => {
993 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
995 self.op = AxisOp::Homing(10);
996 } else if self.homing_timed_out() {
997 self.set_op_error("Homing timeout: mode not confirmed");
998 }
999 }
1000 10 => {
1001 let mut cw = RawControlWord(view.control_word());
1003 cw.set_bit(4, true);
1004 view.set_control_word(cw.raw());
1005 self.op = AxisOp::Homing(11);
1006 }
1007 11 => {
1008 let sw = view.status_word();
1013 let error = sw & (1 << 13) != 0;
1014 if error {
1015 self.set_op_error("Homing error: drive reported homing failure");
1016 } else if sw & (1 << 12) == 0 {
1017 self.op = AxisOp::Homing(12);
1018 } else if self.homing_timed_out() {
1019 self.set_op_error(&format!("Homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1020 }
1021 }
1022 12 => {
1023 let sw = view.status_word();
1026 let error = sw & (1 << 13) != 0;
1027 let attained = sw & (1 << 12) != 0;
1028 let reached = sw & (1 << 10) != 0;
1029
1030 if error {
1031 self.set_op_error("Homing error: drive reported homing failure");
1032 } else if attained && reached {
1033 self.op = AxisOp::Homing(13);
1034 } else if self.homing_timed_out() {
1035 self.set_op_error("Homing timeout: procedure did not complete");
1036 }
1037 }
1038 13 => {
1039 self.home_offset = view.position_actual()
1042 - self.config.to_counts(self.config.home_position).round() as i32;
1043 let mut cw = RawControlWord(view.control_word());
1045 cw.set_bit(4, false);
1046 view.set_control_word(cw.raw());
1047 self.op = AxisOp::Homing(14);
1048 }
1049 14 => {
1050 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1053 log::info!("Homing complete — home offset: {}", self.home_offset);
1054 self.complete_op();
1055 }
1056 _ => self.complete_op(),
1057 }
1058 }
1059
1060 fn configure_soft_homing(&mut self, method: HomingMethod) {
1063 match method {
1064 HomingMethod::LimitSwitchPosPnp => {
1065 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1066 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1067 self.soft_home_direction = 1.0;
1068 }
1069 HomingMethod::LimitSwitchNegPnp => {
1070 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1071 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1072 self.soft_home_direction = -1.0;
1073 }
1074 HomingMethod::LimitSwitchPosNpn => {
1075 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1076 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1077 self.soft_home_direction = 1.0;
1078 }
1079 HomingMethod::LimitSwitchNegNpn => {
1080 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1081 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1082 self.soft_home_direction = -1.0;
1083 }
1084 HomingMethod::HomeSensorPosPnp => {
1085 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1086 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1087 self.soft_home_direction = 1.0;
1088 }
1089 HomingMethod::HomeSensorNegPnp => {
1090 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1091 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1092 self.soft_home_direction = -1.0;
1093 }
1094 HomingMethod::HomeSensorPosNpn => {
1095 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1096 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1097 self.soft_home_direction = 1.0;
1098 }
1099 HomingMethod::HomeSensorNegNpn => {
1100 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1101 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1102 self.soft_home_direction = -1.0;
1103 }
1104 _ => {} }
1106 }
1107
1108 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1109 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1110 self.op_started = Some(Instant::now());
1111 }
1112
1113 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1114 let raw = match self.soft_home_sensor {
1115 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1116 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1117 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1118 };
1119 match self.soft_home_sensor_type {
1120 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1123 }
1124
1125
1126 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1129 let dir = if !self.config.invert_direction {
1130 direction
1131 }
1132 else {
1133 -direction
1134 };
1135
1136 let target = if dir > 0.0 {
1137 i32::MAX
1138 }
1139 else {
1140 i32::MIN
1141 };
1142
1143 return target;
1144 }
1145
1146
1147 fn command_halt(&self, view: &mut impl AxisView) {
1152 let mut cw = RawControlWord(view.control_word());
1153 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1156 }
1157
1158
1159 fn command_cancel_move(&self, view: &mut impl AxisView) {
1167
1168 let mut cw = RawControlWord(view.control_word());
1169 cw.set_bit(4, true); cw.set_bit(5, true); cw.set_bit(6, false); cw.set_bit(8, true); view.set_control_word(cw.raw());
1174
1175 let current_pos = view.position_actual();
1176 view.set_target_position(current_pos);
1177 view.set_profile_velocity(0);
1178 }
1179
1180
1181 fn command_homing_speed(&self, view: &mut impl AxisView) {
1183 let cpu = self.config.counts_per_user();
1184 let vel = (self.config.homing_speed * cpu).round() as u32;
1185 let accel = (self.config.homing_accel * cpu).round() as u32;
1186 let decel = (self.config.homing_decel * cpu).round() as u32;
1187 view.set_profile_velocity(vel);
1188 view.set_profile_acceleration(accel);
1189 view.set_profile_deceleration(decel);
1190 }
1191
1192 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1208 match HomeState::from_repr(step) {
1209
1210 Some(HomeState::EnsurePpMode) => {
1211 log::info!("SoftHome: Ensuring PP mode..");
1216 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1217 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1218 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1219 },
1220 Some(HomeState::WaitPpMode) => {
1221
1222 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1223 if !self.fb_mode_of_operation.is_busy() {
1224 if self.fb_mode_of_operation.is_error() {
1225 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1226 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1227 ));
1228 }
1229 else {
1230 log::info!("SoftHome: Drive is in PP mode!");
1231
1232 if !self.check_soft_home_trigger(view) {
1236 log::info!("SoftHome: Not on home switch; seek out.");
1237 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1238 } else {
1239 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1240 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1241 }
1242 }
1243 }
1244
1245
1246 },
1247
1248 Some(HomeState::Search) => {
1250 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1251
1252 let target = self.calculate_max_relative_target(self.soft_home_direction);
1262 view.set_target_position(target);
1263
1264 self.command_homing_speed(view);
1273
1274 let mut cw = RawControlWord(view.control_word());
1275 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());
1280
1281 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1282 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1283 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1284 }
1285 Some(HomeState::WaitSearching) => {
1286 if self.check_soft_home_trigger(view) {
1287 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1288 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1289 return;
1290 }
1291 let sw = RawStatusWord(view.status_word());
1292 if sw.raw() & (1 << 12) != 0 {
1293 let mut cw = RawControlWord(view.control_word());
1294 cw.set_bit(4, false);
1295 view.set_control_word(cw.raw());
1296 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1297 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1298 } else if self.homing_timed_out() {
1299 self.set_op_error("Software homing timeout: set-point not acknowledged");
1300 }
1301 }
1302 Some(HomeState::WaitFoundSensor) => {
1312 if self.check_soft_home_trigger(view) {
1313 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1314 log::info!("ControlWord is : {} ", view.control_word());
1315
1316 let mut cw = RawControlWord(view.control_word());
1317 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1320
1321
1322 self.halt_stable_count = 0;
1323 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1324 } else if self.homing_timed_out() {
1325 self.set_op_error("Software homing timeout: sensor not detected");
1326 }
1327 }
1328
1329
1330 Some(HomeState::WaitStoppedFoundSensor) => {
1331 const STABLE_WINDOW: i32 = 1;
1332 const STABLE_TICKS_REQUIRED: u8 = 10;
1333
1334 let pos = view.position_actual();
1339 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1340 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1341 } else {
1342 self.halt_stable_count = 0;
1343 }
1344
1345 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1346
1347 log::debug!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1348 self.command_cancel_move(view);
1349 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1350
1351 } else if self.homing_timed_out() {
1352 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1353 }
1354 }
1355 Some(HomeState::WaitFoundSensorAck) => {
1356 let sw = RawStatusWord(view.status_word());
1357 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1358
1359 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1360
1361 let mut cw = RawControlWord(view.control_word());
1363 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1366
1367 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1368
1369 } else if self.homing_timed_out() {
1370 self.set_op_error("Software homing timeout: cancel not acknowledged");
1371 }
1372 },
1373 Some(HomeState::WaitFoundSensorAckClear) => {
1374 let sw = RawStatusWord(view.status_word());
1375 if sw.raw() & (1 << 12) == 0 {
1377
1378 let mut cw = RawControlWord(view.control_word());
1380 cw.set_bit(8, false);
1381 view.set_control_word(cw.raw());
1382
1383 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1384 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1385 self.ton.call(false, Duration::from_secs(3));
1386 }
1387 },
1388 Some(HomeState::DebounceFoundSensor) => {
1390 self.ton.call(true, Duration::from_secs(3));
1391
1392 let sw = RawStatusWord(view.status_word());
1393 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1394 self.ton.call(false, Duration::from_secs(3));
1395 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1396 view.position_actual(), view.control_word(), view.status_word());
1397 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1398 }
1399 }
1400
1401 Some(HomeState::BackOff) => {
1403
1404 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1405 view.set_target_position(target);
1406
1407
1408 self.command_homing_speed(view);
1409
1410 let mut cw = RawControlWord(view.control_word());
1411 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1415 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1416 target, self.config.homing_speed, view.position_actual(), cw.raw());
1417 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1418 }
1419 Some(HomeState::WaitBackingOff) => {
1420 let sw = RawStatusWord(view.status_word());
1421 if sw.raw() & (1 << 12) != 0 {
1422 let mut cw = RawControlWord(view.control_word());
1423 cw.set_bit(4, false);
1424 view.set_control_word(cw.raw());
1425 log::debug!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1426 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1427 } else if self.homing_timed_out() {
1428 self.set_op_error("Software homing timeout: back-off not acknowledged");
1429 }
1430 }
1431 Some(HomeState::WaitLostSensor) => {
1432 if !self.check_soft_home_trigger(view) {
1433 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1434
1435 self.command_halt(view);
1436 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1437 } else if self.homing_timed_out() {
1438 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1439 }
1440 }
1441 Some(HomeState::WaitStoppedLostSensor) => {
1442 const STABLE_WINDOW: i32 = 1;
1443 const STABLE_TICKS_REQUIRED: u8 = 10;
1444
1445 let mut cw = RawControlWord(view.control_word());
1446 cw.set_bit(8, true);
1447 view.set_control_word(cw.raw());
1448
1449 let pos = view.position_actual();
1450 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1451 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1452 } else {
1453 self.halt_stable_count = 0;
1454 }
1455
1456 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1457 log::debug!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1458 self.command_cancel_move(view);
1459 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1460 } else if self.homing_timed_out() {
1461 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1462 }
1463 }
1464 Some(HomeState::WaitLostSensorAck) => {
1465 let sw = RawStatusWord(view.status_word());
1466 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1467
1468 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1469
1470 let mut cw = RawControlWord(view.control_word());
1472 cw.set_bit(4, false); view.set_control_word(cw.raw());
1474
1475 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1476
1477
1478 } else if self.homing_timed_out() {
1479 self.set_op_error("Software homing timeout: cancel not acknowledged");
1480 }
1481 }
1482 Some(HomeState::WaitLostSensorAckClear) => {
1483 let sw = RawStatusWord(view.status_word());
1485 if sw.raw() & (1 << 12) == 0 {
1486
1487 let mut cw = RawControlWord(view.control_word());
1489 cw.set_bit(8, false);
1490 view.set_control_word(cw.raw());
1491
1492
1493 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1494 self.homing_sdo_tid = self.sdo.write(
1497 client, 0x607C, 0, json!(desired_counts),
1498 );
1499
1500 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1501 self.config.home_position, desired_counts
1502 );
1503
1504 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1505
1506 }
1507 },
1508
1509 Some(HomeState::WaitHomeOffsetDone) => {
1510 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1512 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1513 SdoResult::Pending => {
1514 if self.homing_timed_out() {
1515 self.set_op_error("Software homing timeout: home offset SDO write");
1516 }
1517 }
1518 SdoResult::Err(e) => {
1519 self.set_op_error(&format!("Software homing SDO error: {}", e));
1520 }
1521 SdoResult::Timeout => {
1522 self.set_op_error("Software homing: home offset SDO timed out");
1523 }
1524 }
1525 },
1526 Some(HomeState::WriteHomingModeOp) => {
1527
1528 self.fb_mode_of_operation.reset();
1532 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1533 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1534 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1535
1536
1537 },
1538 Some(HomeState::WaitWriteHomingModeOp) => {
1539 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1541
1542 if !self.fb_mode_of_operation.is_busy() {
1543 if self.fb_mode_of_operation.is_error() {
1544 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1545 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1546 ));
1547 }
1548 else {
1549 log::info!("SoftHome: Drive is now in Homing Mode.");
1550 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1551 }
1552 }
1553 },
1554 Some(HomeState::WriteHomingMethod) => {
1555 self.homing_sdo_tid = self.sdo.write(
1557 client, 0x6098, 0, json!(37i8),
1558 );
1559 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1560 }
1561 Some(HomeState::WaitWriteHomingMethodDone) => {
1562 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1564 SdoResult::Ok(_) => {
1565 log::info!("SoftHome: Successfully wrote homing method.");
1566 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1567 }
1568 SdoResult::Pending => {
1569 if self.homing_timed_out() {
1570 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1571 }
1572 }
1573 SdoResult::Err(e) => {
1574 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1575 }
1576 SdoResult::Timeout => {
1577 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1578 }
1579 }
1580 }
1581 Some(HomeState::ClearHomingTrigger) => {
1582 let mut cw = RawControlWord(view.control_word());
1585 cw.set_bit(4, false);
1586 view.set_control_word(cw.raw());
1587 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1588 }
1589 Some(HomeState::TriggerHoming) => {
1590 let mut cw = RawControlWord(view.control_word());
1592 cw.set_bit(4, true);
1593 view.set_control_word(cw.raw());
1594 log::info!("SoftHome[TriggerHoming]: start homing");
1595 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1596 }
1597 Some(HomeState::WaitHomingStarted) => {
1598 let sw = view.status_word();
1604 let error = sw & (1 << 13) != 0;
1605 if error {
1606 self.restore_pp_after_error("Software homing: drive reported homing error");
1607 } else if sw & (1 << 12) == 0 {
1608 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1609 } else if self.homing_timed_out() {
1610 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1611 }
1612 }
1613 Some(HomeState::WaitHomingDone) => {
1614 let sw = view.status_word();
1616 let error = sw & (1 << 13) != 0;
1617 let attained = sw & (1 << 12) != 0;
1618 let reached = sw & (1 << 10) != 0;
1619
1620 if error {
1621 self.restore_pp_after_error("Software homing: drive reported homing error");
1622 } else if attained && reached {
1623 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
1624 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
1625 } else if self.homing_timed_out() {
1626 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
1627 }
1628 }
1629 Some(HomeState::ResetHomingTrigger) => {
1630 let mut cw = RawControlWord(view.control_word());
1635 cw.set_bit(4, false);
1636 view.set_control_word(cw.raw());
1637 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
1638 }
1639 Some(HomeState::WaitHomingTriggerCleared) => {
1640 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
1644 }
1645
1646
1647 Some(HomeState::WriteMotionModeOfOperation) => {
1648
1649 self.fb_mode_of_operation.reset();
1652 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1653 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1654 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
1655
1656 },
1657 Some(HomeState::WaitWriteMotionModeOfOperation) => {
1658 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1660
1661 if !self.fb_mode_of_operation.is_busy() {
1662 if self.fb_mode_of_operation.is_error() {
1663 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1664 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1665 ));
1666 }
1667 else {
1668 if self.is_error {
1669 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
1670 self.finish_op_error();
1671 }
1672 else {
1673 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1676 }
1677
1678 }
1679 }
1680 },
1681
1682 Some(HomeState::SendCurrentPositionTarget) => {
1683 let current_pos = view.position_actual();
1685 view.set_target_position(current_pos);
1686 view.set_profile_velocity(0);
1687 let mut cw = RawControlWord(view.control_word());
1688 cw.set_bit(4, true);
1689 cw.set_bit(5, true);
1690 cw.set_bit(6, false); view.set_control_word(cw.raw());
1692 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1693 }
1694 Some(HomeState::WaitCurrentPositionTargetSent) => {
1695 let sw = RawStatusWord(view.status_word());
1697 if sw.raw() & (1 << 12) != 0 {
1698 let mut cw = RawControlWord(view.control_word());
1699 cw.set_bit(4, false);
1700 view.set_control_word(cw.raw());
1701 log::info!("Software homing complete — position set to {} user units",
1702 self.config.home_position);
1703 self.complete_op();
1704 } else if self.homing_timed_out() {
1705 self.set_op_error("Software homing timeout: hold position not acknowledged");
1706 }
1707 }
1708 _ => self.complete_op(),
1709 }
1710 }
1711
1712 fn tick_halting(&mut self, _view: &mut impl AxisView) {
1714 self.complete_op();
1717 }
1718
1719 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1724 match step {
1725 1 => {
1726 let mut cw = RawControlWord(view.control_word());
1728 cw.cmd_fault_reset();
1729 view.set_control_word(cw.raw());
1730 self.op = AxisOp::FaultRecovery(2);
1731 }
1732 2 => {
1733 let sw = RawStatusWord(view.status_word());
1735 let state = sw.state();
1736 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1737 log::info!("Fault cleared (drive state: {})", state);
1738 self.complete_op();
1739 } else if self.op_timed_out() {
1740 self.set_op_error("Fault reset timeout: drive still faulted");
1741 }
1742 }
1743 _ => self.complete_op(),
1744 }
1745 }
1746}
1747
1748#[cfg(test)]
1753mod tests {
1754 use super::*;
1755
1756 struct MockView {
1758 control_word: u16,
1759 status_word: u16,
1760 target_position: i32,
1761 profile_velocity: u32,
1762 profile_acceleration: u32,
1763 profile_deceleration: u32,
1764 modes_of_operation: i8,
1765 modes_of_operation_display: i8,
1766 position_actual: i32,
1767 velocity_actual: i32,
1768 error_code: u16,
1769 positive_limit: bool,
1770 negative_limit: bool,
1771 home_sensor: bool,
1772 }
1773
1774 impl MockView {
1775 fn new() -> Self {
1776 Self {
1777 control_word: 0,
1778 status_word: 0x0040, target_position: 0,
1780 profile_velocity: 0,
1781 profile_acceleration: 0,
1782 profile_deceleration: 0,
1783 modes_of_operation: 0,
1784 modes_of_operation_display: 1, position_actual: 0,
1786 velocity_actual: 0,
1787 error_code: 0,
1788 positive_limit: false,
1789 negative_limit: false,
1790 home_sensor: false,
1791 }
1792 }
1793
1794 fn set_state(&mut self, state: u16) {
1795 self.status_word = state;
1796 }
1797 }
1798
1799 impl AxisView for MockView {
1800 fn control_word(&self) -> u16 { self.control_word }
1801 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1802 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1803 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1804 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1805 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1806 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1807 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1808 fn status_word(&self) -> u16 { self.status_word }
1809 fn position_actual(&self) -> i32 { self.position_actual }
1810 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1811 fn error_code(&self) -> u16 { self.error_code }
1812 fn positive_limit_active(&self) -> bool { self.positive_limit }
1813 fn negative_limit_active(&self) -> bool { self.negative_limit }
1814 fn home_sensor_active(&self) -> bool { self.home_sensor }
1815 }
1816
1817 fn test_config() -> AxisConfig {
1818 AxisConfig::new(12_800).with_user_scale(360.0)
1819 }
1820
1821 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1823 use tokio::sync::mpsc;
1824 let (write_tx, write_rx) = mpsc::unbounded_channel();
1825 let (response_tx, response_rx) = mpsc::unbounded_channel();
1826 let client = CommandClient::new(write_tx, response_rx);
1827 let axis = Axis::new(test_config(), "TestDrive");
1828 (axis, client, response_tx, write_rx)
1829 }
1830
1831 #[test]
1832 fn axis_config_conversion() {
1833 let cfg = test_config();
1834 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
1836 }
1837
1838 #[test]
1839 fn enable_sequence_sets_pp_mode_and_shutdown() {
1840 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1841 let mut view = MockView::new();
1842
1843 axis.enable(&mut view);
1844
1845 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
1847 assert_eq!(view.control_word & 0x008F, 0x0006);
1849 assert_eq!(axis.op, AxisOp::Enabling(1));
1851
1852 view.set_state(0x0021); axis.tick(&mut view, &mut client);
1855
1856 assert_eq!(view.control_word & 0x008F, 0x000F);
1858 assert_eq!(axis.op, AxisOp::Enabling(2));
1859
1860 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1863
1864 assert_eq!(axis.op, AxisOp::Idle);
1866 assert!(axis.motor_on);
1867 }
1868
1869 #[test]
1870 fn move_absolute_sets_target() {
1871 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1872 let mut view = MockView::new();
1873 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1878
1879 assert_eq!(view.target_position, 1600);
1881 assert_eq!(view.profile_velocity, 3200);
1883 assert_eq!(view.profile_acceleration, 6400);
1885 assert_eq!(view.profile_deceleration, 6400);
1886 assert!(view.control_word & (1 << 4) != 0);
1888 assert!(view.control_word & (1 << 6) == 0);
1890 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1892 }
1893
1894 #[test]
1895 fn move_relative_sets_relative_bit() {
1896 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1897 let mut view = MockView::new();
1898 view.set_state(0x0027);
1899 axis.tick(&mut view, &mut client);
1900
1901 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
1902
1903 assert!(view.control_word & (1 << 6) != 0);
1905 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1)));
1906 }
1907
1908 #[test]
1909 fn move_completes_on_target_reached() {
1910 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1911 let mut view = MockView::new();
1912 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1914
1915 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1916
1917 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1920 assert!(view.control_word & (1 << 4) == 0);
1922
1923 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1926 assert_eq!(axis.op, AxisOp::Idle);
1928 assert!(!axis.in_motion);
1929 }
1930
1931 #[test]
1932 fn fault_detected_sets_error() {
1933 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1934 let mut view = MockView::new();
1935 view.set_state(0x0008); view.error_code = 0x1234;
1937
1938 axis.tick(&mut view, &mut client);
1939
1940 assert!(axis.is_error);
1941 assert_eq!(axis.error_code, 0x1234);
1942 assert!(axis.error_message.contains("fault"));
1943 }
1944
1945 #[test]
1946 fn fault_recovery_sequence() {
1947 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1948 let mut view = MockView::new();
1949 view.set_state(0x0008); axis.reset_faults(&mut view);
1952 assert!(view.control_word & 0x0080 == 0);
1954
1955 axis.tick(&mut view, &mut client);
1957 assert!(view.control_word & 0x0080 != 0);
1958
1959 view.set_state(0x0040);
1961 axis.tick(&mut view, &mut client);
1962 assert_eq!(axis.op, AxisOp::Idle);
1963 assert!(!axis.is_error);
1964 }
1965
1966 #[test]
1967 fn disable_sequence() {
1968 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1969 let mut view = MockView::new();
1970 view.set_state(0x0027); axis.disable(&mut view);
1973 assert_eq!(view.control_word & 0x008F, 0x0007);
1975
1976 view.set_state(0x0023); axis.tick(&mut view, &mut client);
1979 assert_eq!(axis.op, AxisOp::Idle);
1980 }
1981
1982 #[test]
1983 fn position_tracks_with_home_offset() {
1984 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1985 let mut view = MockView::new();
1986 view.set_state(0x0027);
1987 view.position_actual = 5000;
1988
1989 axis.enable(&mut view);
1991 view.set_state(0x0021);
1992 axis.tick(&mut view, &mut client);
1993 view.set_state(0x0027);
1994 axis.tick(&mut view, &mut client);
1995
1996 assert_eq!(axis.home_offset, 5000);
1998
1999 assert!((axis.position - 0.0).abs() < 0.01);
2001
2002 view.position_actual = 6600;
2004 axis.tick(&mut view, &mut client);
2005
2006 assert!((axis.position - 45.0).abs() < 0.1);
2008 }
2009
2010 #[test]
2011 fn set_position_adjusts_home_offset() {
2012 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2013 let mut view = MockView::new();
2014 view.position_actual = 3200;
2015
2016 axis.set_position(&view, 90.0);
2017 axis.tick(&mut view, &mut client);
2018
2019 assert_eq!(axis.home_offset, 0);
2021 assert!((axis.position - 90.0).abs() < 0.01);
2022 }
2023
2024 #[test]
2025 fn halt_sets_bit_and_goes_idle() {
2026 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2027 let mut view = MockView::new();
2028 view.set_state(0x0027);
2029
2030 axis.halt(&mut view);
2031 assert!(view.control_word & (1 << 8) != 0);
2033
2034 axis.tick(&mut view, &mut client);
2036 assert_eq!(axis.op, AxisOp::Idle);
2037 }
2038
2039 #[test]
2040 fn is_busy_tracks_operations() {
2041 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2042 let mut view = MockView::new();
2043
2044 axis.tick(&mut view, &mut client);
2046 assert!(!axis.is_busy);
2047
2048 axis.enable(&mut view);
2050 axis.tick(&mut view, &mut client);
2051 assert!(axis.is_busy);
2052
2053 view.set_state(0x0021);
2055 axis.tick(&mut view, &mut client);
2056 view.set_state(0x0027);
2057 axis.tick(&mut view, &mut client);
2058 assert!(!axis.is_busy);
2059
2060 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2062 axis.tick(&mut view, &mut client);
2063 assert!(axis.is_busy);
2064 assert!(axis.in_motion);
2065 }
2066
2067 #[test]
2068 fn fault_during_move_cancels_op() {
2069 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2070 let mut view = MockView::new();
2071 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2073
2074 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2076 axis.tick(&mut view, &mut client);
2077 assert!(axis.is_busy);
2078 assert!(!axis.is_error);
2079
2080 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2083
2084 assert!(!axis.is_busy);
2086 assert!(axis.is_error);
2087 assert_eq!(axis.op, AxisOp::Idle);
2088 }
2089
2090 #[test]
2091 fn move_absolute_rejected_by_max_limit() {
2092 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2093 let mut view = MockView::new();
2094 view.set_state(0x0027);
2095 axis.tick(&mut view, &mut client);
2096
2097 axis.set_software_max_limit(90.0);
2098 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2099
2100 assert!(axis.is_error);
2102 assert_eq!(axis.op, AxisOp::Idle);
2103 assert!(axis.error_message.contains("max software limit"));
2104 }
2105
2106 #[test]
2107 fn move_absolute_rejected_by_min_limit() {
2108 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2109 let mut view = MockView::new();
2110 view.set_state(0x0027);
2111 axis.tick(&mut view, &mut client);
2112
2113 axis.set_software_min_limit(-10.0);
2114 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2115
2116 assert!(axis.is_error);
2117 assert_eq!(axis.op, AxisOp::Idle);
2118 assert!(axis.error_message.contains("min software limit"));
2119 }
2120
2121 #[test]
2122 fn move_relative_rejected_by_max_limit() {
2123 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2124 let mut view = MockView::new();
2125 view.set_state(0x0027);
2126 axis.tick(&mut view, &mut client);
2127
2128 axis.set_software_max_limit(50.0);
2130 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2131
2132 assert!(axis.is_error);
2133 assert_eq!(axis.op, AxisOp::Idle);
2134 assert!(axis.error_message.contains("max software limit"));
2135 }
2136
2137 #[test]
2138 fn move_within_limits_allowed() {
2139 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2140 let mut view = MockView::new();
2141 view.set_state(0x0027);
2142 axis.tick(&mut view, &mut client);
2143
2144 axis.set_software_max_limit(90.0);
2145 axis.set_software_min_limit(-90.0);
2146 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2147
2148 assert!(!axis.is_error);
2150 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
2151 }
2152
2153 #[test]
2154 fn runtime_limit_halts_move_in_violated_direction() {
2155 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2156 let mut view = MockView::new();
2157 view.set_state(0x0027);
2158 axis.tick(&mut view, &mut client);
2159
2160 axis.set_software_max_limit(45.0);
2161 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2163
2164 view.position_actual = 1650;
2167 view.velocity_actual = 100; view.status_word = 0x1027;
2171 axis.tick(&mut view, &mut client);
2172 view.status_word = 0x0027;
2173 axis.tick(&mut view, &mut client);
2174
2175 assert!(axis.is_error);
2177 assert!(axis.at_max_limit);
2178 assert_eq!(axis.op, AxisOp::Idle);
2179 assert!(axis.error_message.contains("Software position limit"));
2180 assert!(view.control_word & (1 << 8) != 0);
2182 }
2183
2184 #[test]
2185 fn runtime_limit_allows_move_in_opposite_direction() {
2186 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2187 let mut view = MockView::new();
2188 view.set_state(0x0027);
2189 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2192 axis.tick(&mut view, &mut client);
2193 assert!(axis.at_max_limit);
2194
2195 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2197 assert!(!axis.is_error);
2198 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
2199
2200 view.velocity_actual = -100;
2202 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2204 assert!(!axis.is_error);
2206 }
2207
2208 #[test]
2209 fn positive_limit_switch_halts_positive_move() {
2210 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2211 let mut view = MockView::new();
2212 view.set_state(0x0027);
2213 axis.tick(&mut view, &mut client);
2214
2215 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2217 view.velocity_actual = 100; view.status_word = 0x1027;
2220 axis.tick(&mut view, &mut client);
2221 view.status_word = 0x0027;
2222
2223 view.positive_limit = true;
2225 axis.tick(&mut view, &mut client);
2226
2227 assert!(axis.is_error);
2228 assert!(axis.at_positive_limit_switch);
2229 assert!(!axis.is_busy);
2230 assert!(axis.error_message.contains("Positive limit switch"));
2231 assert!(view.control_word & (1 << 8) != 0);
2233 }
2234
2235 #[test]
2236 fn negative_limit_switch_halts_negative_move() {
2237 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2238 let mut view = MockView::new();
2239 view.set_state(0x0027);
2240 axis.tick(&mut view, &mut client);
2241
2242 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2244 view.velocity_actual = -100; view.status_word = 0x1027;
2246 axis.tick(&mut view, &mut client);
2247 view.status_word = 0x0027;
2248
2249 view.negative_limit = true;
2251 axis.tick(&mut view, &mut client);
2252
2253 assert!(axis.is_error);
2254 assert!(axis.at_negative_limit_switch);
2255 assert!(axis.error_message.contains("Negative limit switch"));
2256 }
2257
2258 #[test]
2259 fn limit_switch_allows_move_in_opposite_direction() {
2260 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2261 let mut view = MockView::new();
2262 view.set_state(0x0027);
2263 view.positive_limit = true;
2265 view.velocity_actual = -100;
2266 axis.tick(&mut view, &mut client);
2267 assert!(axis.at_positive_limit_switch);
2268
2269 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2271 view.status_word = 0x1027;
2272 axis.tick(&mut view, &mut client);
2273
2274 assert!(!axis.is_error);
2276 assert!(matches!(axis.op, AxisOp::Moving(_, _)));
2277 }
2278
2279 #[test]
2280 fn limit_switch_ignored_when_not_moving() {
2281 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2282 let mut view = MockView::new();
2283 view.set_state(0x0027);
2284 view.positive_limit = true;
2285
2286 axis.tick(&mut view, &mut client);
2287
2288 assert!(axis.at_positive_limit_switch);
2290 assert!(!axis.is_error);
2291 }
2292
2293 #[test]
2294 fn home_sensor_output_tracks_view() {
2295 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2296 let mut view = MockView::new();
2297 view.set_state(0x0027);
2298
2299 axis.tick(&mut view, &mut client);
2300 assert!(!axis.home_sensor);
2301
2302 view.home_sensor = true;
2303 axis.tick(&mut view, &mut client);
2304 assert!(axis.home_sensor);
2305
2306 view.home_sensor = false;
2307 axis.tick(&mut view, &mut client);
2308 assert!(!axis.home_sensor);
2309 }
2310
2311 #[test]
2312 fn velocity_output_converted() {
2313 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2314 let mut view = MockView::new();
2315 view.set_state(0x0027);
2316 view.velocity_actual = 3200;
2318
2319 axis.tick(&mut view, &mut client);
2320
2321 assert!((axis.speed - 90.0).abs() < 0.1);
2322 assert!(axis.moving_positive);
2323 assert!(!axis.moving_negative);
2324 }
2325
2326 fn soft_homing_config() -> AxisConfig {
2329 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2330 cfg.homing_speed = 10.0;
2331 cfg.homing_accel = 20.0;
2332 cfg.homing_decel = 20.0;
2333 cfg
2334 }
2335
2336 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2337 use tokio::sync::mpsc;
2338 let (write_tx, write_rx) = mpsc::unbounded_channel();
2339 let (response_tx, response_rx) = mpsc::unbounded_channel();
2340 let client = CommandClient::new(write_tx, response_rx);
2341 let axis = Axis::new(soft_homing_config(), "TestDrive");
2342 (axis, client, response_tx, write_rx)
2343 }
2344
2345 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2347 view.set_state(0x0027); axis.tick(view, client);
2349 }
2350
2351 fn complete_soft_homing(
2356 axis: &mut Axis,
2357 view: &mut MockView,
2358 client: &mut CommandClient,
2359 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2360 trigger_pos: i32,
2361 clear_sensor: impl FnOnce(&mut MockView),
2362 ) {
2363 use mechutil::ipc::CommandMessage as IpcMsg;
2364
2365 axis.tick(view, client);
2368 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2369
2370 view.position_actual = trigger_pos + 100;
2372 axis.tick(view, client);
2373 view.position_actual = trigger_pos + 120;
2374 axis.tick(view, client);
2375 for _ in 0..10 { axis.tick(view, client); }
2377 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2378
2379 view.status_word = 0x1027;
2381 axis.tick(view, client);
2382 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2383 view.status_word = 0x0027;
2384
2385 for _ in 0..100 { axis.tick(view, client); }
2387 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2388
2389 axis.tick(view, client);
2392 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2393
2394 view.status_word = 0x1027;
2396 axis.tick(view, client);
2397 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2398 view.status_word = 0x0027;
2399
2400 axis.tick(view, client);
2402 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2403 clear_sensor(view);
2404 view.position_actual = trigger_pos - 200;
2405 axis.tick(view, client);
2406 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2407
2408 axis.tick(view, client);
2410 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2411 for _ in 0..10 { axis.tick(view, client); }
2412 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2413
2414 view.status_word = 0x1027;
2417 axis.tick(view, client);
2418 view.status_word = 0x0027;
2419 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2420
2421 let tid = axis.homing_sdo_tid;
2423 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2424 client.poll();
2425 axis.tick(view, client);
2426 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2427
2428 axis.tick(view, client);
2430 let tid = axis.homing_sdo_tid;
2431 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2432 client.poll();
2433 axis.tick(view, client);
2434 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2435
2436 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2438 axis.tick(view, client);
2439 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2440
2441 view.status_word = 0x1427; axis.tick(view, client);
2444 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2445 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2446 view.status_word = 0x0027;
2447
2448 axis.tick(view, client);
2450 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2451
2452 view.status_word = 0x1027;
2454 axis.tick(view, client);
2455 view.status_word = 0x0027;
2456
2457 assert_eq!(axis.op, AxisOp::Idle);
2458 assert!(!axis.is_busy);
2459 assert!(!axis.is_error);
2460 assert_eq!(axis.home_offset, 0); }
2462
2463 #[test]
2464 fn soft_homing_pnp_home_sensor_full_sequence() {
2465 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2466 let mut view = MockView::new();
2467 enable_axis(&mut axis, &mut view, &mut client);
2468
2469 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2470
2471 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2474 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2476 axis.tick(&mut view, &mut client); view.home_sensor = true;
2480 view.position_actual = 5000;
2481 axis.tick(&mut view, &mut client);
2482 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2483
2484 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2485 |v| { v.home_sensor = false; });
2486 }
2487
2488 #[test]
2489 fn soft_homing_npn_home_sensor_full_sequence() {
2490 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2491 let mut view = MockView::new();
2492 view.home_sensor = true;
2494 enable_axis(&mut axis, &mut view, &mut client);
2495
2496 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2497
2498 axis.tick(&mut view, &mut client);
2500 view.status_word = 0x1027;
2501 axis.tick(&mut view, &mut client);
2502 view.status_word = 0x0027;
2503 axis.tick(&mut view, &mut client);
2504
2505 view.home_sensor = false;
2507 view.position_actual = 3000;
2508 axis.tick(&mut view, &mut client);
2509 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2510
2511 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2512 |v| { v.home_sensor = true; }); }
2514
2515 #[test]
2516 fn soft_homing_limit_switch_suppresses_halt() {
2517 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2518 let mut view = MockView::new();
2519 enable_axis(&mut axis, &mut view, &mut client);
2520
2521 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
2523
2524 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2529 axis.tick(&mut view, &mut client); view.positive_limit = true;
2533 view.velocity_actual = 100; view.position_actual = 8000;
2535 axis.tick(&mut view, &mut client);
2536
2537 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2539 assert!(!axis.is_error);
2540 }
2541
2542 #[test]
2543 fn soft_homing_opposite_limit_still_protects() {
2544 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2545 let mut view = MockView::new();
2546 enable_axis(&mut axis, &mut view, &mut client);
2547
2548 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2550
2551 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2556 axis.tick(&mut view, &mut client); view.negative_limit = true;
2561 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
2563
2564 assert!(axis.is_error);
2566 assert!(axis.error_message.contains("Negative limit switch"));
2567 }
2568
2569 #[test]
2570 #[test]
2588 fn soft_homing_negative_direction_sets_negative_target() {
2589 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2590 let mut view = MockView::new();
2591 enable_axis(&mut axis, &mut view, &mut client);
2592
2593 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
2594 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
2598 }
2599
2600 #[test]
2601 fn home_integrated_method_starts_hardware_homing() {
2602 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2603 let mut view = MockView::new();
2604 enable_axis(&mut axis, &mut view, &mut client);
2605
2606 axis.home(&mut view, HomingMethod::CurrentPosition);
2607 assert!(matches!(axis.op, AxisOp::Homing(0)));
2608 assert_eq!(axis.homing_method, 37);
2609 }
2610
2611 #[test]
2612 fn home_integrated_arbitrary_code() {
2613 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2614 let mut view = MockView::new();
2615 enable_axis(&mut axis, &mut view, &mut client);
2616
2617 axis.home(&mut view, HomingMethod::Integrated(35));
2618 assert!(matches!(axis.op, AxisOp::Homing(0)));
2619 assert_eq!(axis.homing_method, 35);
2620 }
2621
2622 #[test]
2623 fn hardware_homing_skips_speed_sdos_when_zero() {
2624 use mechutil::ipc::CommandMessage;
2625
2626 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
2627 let mut view = MockView::new();
2628 enable_axis(&mut axis, &mut view, &mut client);
2629
2630 axis.home(&mut view, HomingMethod::Integrated(37));
2632
2633 axis.tick(&mut view, &mut client);
2635 assert!(matches!(axis.op, AxisOp::Homing(1)));
2636
2637 let _ = write_rx.try_recv();
2639
2640 let tid = axis.homing_sdo_tid;
2642 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2643 client.poll();
2644 axis.tick(&mut view, &mut client);
2645
2646 assert!(matches!(axis.op, AxisOp::Homing(8)));
2648 }
2649
2650 #[test]
2651 fn hardware_homing_writes_speed_sdos_when_nonzero() {
2652 use mechutil::ipc::CommandMessage;
2653
2654 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
2655 let mut view = MockView::new();
2656 enable_axis(&mut axis, &mut view, &mut client);
2657
2658 axis.home(&mut view, HomingMethod::Integrated(37));
2660
2661 axis.tick(&mut view, &mut client);
2663 assert!(matches!(axis.op, AxisOp::Homing(1)));
2664 let _ = write_rx.try_recv();
2665
2666 let tid = axis.homing_sdo_tid;
2668 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2669 client.poll();
2670 axis.tick(&mut view, &mut client);
2671 assert!(matches!(axis.op, AxisOp::Homing(2)));
2673 }
2674
2675 #[test]
2676 fn soft_homing_edge_during_ack_step() {
2677 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2678 let mut view = MockView::new();
2679 enable_axis(&mut axis, &mut view, &mut client);
2680
2681 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2682 axis.tick(&mut view, &mut client); view.home_sensor = true;
2686 view.position_actual = 2000;
2687 axis.tick(&mut view, &mut client);
2688
2689 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2691 }
2692
2693 #[test]
2694 fn soft_homing_applies_home_position() {
2695 let mut cfg = soft_homing_config();
2696 cfg.home_position = 90.0;
2697
2698 use tokio::sync::mpsc;
2699 let (write_tx, _write_rx) = mpsc::unbounded_channel();
2700 let (resp_tx, response_rx) = mpsc::unbounded_channel();
2701 let mut client = CommandClient::new(write_tx, response_rx);
2702 let mut axis = Axis::new(cfg, "TestDrive");
2703
2704 let mut view = MockView::new();
2705 enable_axis(&mut axis, &mut view, &mut client);
2706
2707 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2708
2709 axis.tick(&mut view, &mut client);
2711 view.status_word = 0x1027;
2712 axis.tick(&mut view, &mut client);
2713 view.status_word = 0x0027;
2714 axis.tick(&mut view, &mut client);
2715
2716 view.home_sensor = true;
2718 view.position_actual = 5000;
2719 axis.tick(&mut view, &mut client);
2720 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2721
2722 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2724 |v| { v.home_sensor = false; });
2725
2726 assert_eq!(axis.home_offset, 0);
2728 }
2729
2730 #[test]
2731 fn soft_homing_default_home_position_zero() {
2732 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2733 let mut view = MockView::new();
2734 enable_axis(&mut axis, &mut view, &mut client);
2735
2736 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2737
2738 axis.tick(&mut view, &mut client);
2740 view.status_word = 0x1027;
2741 axis.tick(&mut view, &mut client);
2742 view.status_word = 0x0027;
2743 axis.tick(&mut view, &mut client);
2744
2745 view.home_sensor = true;
2747 view.position_actual = 5000;
2748 axis.tick(&mut view, &mut client);
2749
2750 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2751 |v| { v.home_sensor = false; });
2752
2753 assert_eq!(axis.home_offset, 0);
2754 }
2755}