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 if self.config.invert_direction {
821
822 let tmp_positive = commanded_positive;
823 commanded_positive = commanded_negative;
824 commanded_negative = tmp_positive;
825 }
826
827 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
829 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
830
831 let effective_hw_pos = hw_pos && !suppress_pos;
832 let effective_hw_neg = hw_neg && !suppress_neg;
833
834 let effective_sw_max = sw_max && !is_soft_homing;
836 let effective_sw_min = sw_min && !is_soft_homing;
837
838 let positive_blocked = (effective_sw_max || effective_hw_pos) && commanded_positive;
839 let negative_blocked = (effective_sw_min || effective_hw_neg) && commanded_negative;
840
841
842 if positive_blocked || negative_blocked {
843 let mut cw = RawControlWord(view.control_word());
844 cw.set_bit(8, true); view.set_control_word(cw.raw());
846
847 let msg = if effective_hw_pos && commanded_positive {
848 "Positive limit switch active".to_string()
849 } else if effective_hw_neg && commanded_negative {
850 "Negative limit switch active".to_string()
851 } else if effective_sw_max && commanded_positive {
852 format!(
853 "Software position limit: position {:.3} >= max {:.3}",
854 self.position, eff_max.unwrap_or(self.position)
855 )
856 } else {
857 format!(
858 "Software position limit: position {:.3} <= min {:.3}",
859 self.position, eff_min.unwrap_or(self.position)
860 )
861 };
862 self.set_op_error(&msg);
863 }
864 }
865
866 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
871 match self.op.clone() {
872 AxisOp::Idle => {}
873 AxisOp::Enabling(step) => self.tick_enabling(view, step),
874 AxisOp::Disabling(step) => self.tick_disabling(view, step),
875 AxisOp::Moving(kind, step, pos, neg) => self.tick_moving(view, kind, step, pos, neg),
876 AxisOp::Homing(step) => self.tick_homing(view, client, step),
877 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
878 AxisOp::Halting(step) => self.tick_halting(view, step),
879 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
880 }
881 }
882
883 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
888 match step {
889 1 => {
890 let sw = RawStatusWord(view.status_word());
891 if sw.state() == Cia402State::ReadyToSwitchOn {
892 let mut cw = RawControlWord(view.control_word());
893 cw.cmd_enable_operation();
894 view.set_control_word(cw.raw());
895 self.op = AxisOp::Enabling(2);
896 } else if self.op_timed_out() {
897 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
898 }
899 }
900 2 => {
901 let sw = RawStatusWord(view.status_word());
902 if sw.state() == Cia402State::OperationEnabled {
903 self.complete_op();
910 } else if self.op_timed_out() {
911 self.set_op_error("Enable timeout: waiting for OperationEnabled");
912 }
913 }
914 _ => self.complete_op(),
915 }
916 }
917
918 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
922 match step {
923 1 => {
924 let sw = RawStatusWord(view.status_word());
925 if sw.state() != Cia402State::OperationEnabled {
926 self.complete_op();
927 } else if self.op_timed_out() {
928 self.set_op_error("Disable timeout: drive still in OperationEnabled");
929 }
930 }
931 _ => self.complete_op(),
932 }
933 }
934
935 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8, pos: bool, neg: bool) {
941 match step {
942 1 => {
943 let sw = RawStatusWord(view.status_word());
945 if sw.raw() & (1 << 12) != 0 {
946 let mut cw = RawControlWord(view.control_word());
948 cw.set_bit(4, false);
949 view.set_control_word(cw.raw());
950 self.op = AxisOp::Moving(kind, 2, pos, neg);
951 } else if self.move_start_timed_out() {
952 self.set_op_error("Move timeout: set-point not acknowledged");
953 }
954 },
955 2 => {
956 let sw = RawStatusWord(view.status_word());
958 if sw.raw() & (1 << 12) == 0 {
959 self.op = AxisOp::Moving(kind, 3, pos, neg);
961 }
962 },
963 3 => {
964 let sw = RawStatusWord(view.status_word());
966 if sw.target_reached() {
967 self.complete_op();
968 }
969 },
970 _ => self.complete_op(),
971 }
972 }
973
974 fn tick_homing(
992 &mut self,
993 view: &mut impl AxisView,
994 client: &mut CommandClient,
995 step: u8,
996 ) {
997 match step {
998 0 => {
999 self.homing_sdo_tid = self.sdo.write(
1001 client,
1002 0x6098,
1003 0,
1004 json!(self.homing_method),
1005 );
1006 self.op = AxisOp::Homing(1);
1007 }
1008 1 => {
1009 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1011 SdoResult::Ok(_) => {
1012 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
1014 self.op = AxisOp::Homing(8);
1015 } else {
1016 self.op = AxisOp::Homing(2);
1017 }
1018 }
1019 SdoResult::Pending => {
1020 if self.homing_timed_out() {
1021 self.set_op_error("Homing timeout: SDO write for homing method");
1022 }
1023 }
1024 SdoResult::Err(e) => {
1025 self.set_op_error(&format!("Homing SDO error: {}", e));
1026 }
1027 SdoResult::Timeout => {
1028 self.set_op_error("Homing timeout: SDO write timed out");
1029 }
1030 }
1031 }
1032 2 => {
1033 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1035 self.homing_sdo_tid = self.sdo.write(
1036 client,
1037 0x6099,
1038 1,
1039 json!(speed_counts),
1040 );
1041 self.op = AxisOp::Homing(3);
1042 }
1043 3 => {
1044 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1045 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
1046 SdoResult::Pending => {
1047 if self.homing_timed_out() {
1048 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
1049 }
1050 }
1051 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1052 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1053 }
1054 }
1055 4 => {
1056 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1058 self.homing_sdo_tid = self.sdo.write(
1059 client,
1060 0x6099,
1061 2,
1062 json!(speed_counts),
1063 );
1064 self.op = AxisOp::Homing(5);
1065 }
1066 5 => {
1067 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1068 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
1069 SdoResult::Pending => {
1070 if self.homing_timed_out() {
1071 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1072 }
1073 }
1074 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1075 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1076 }
1077 }
1078 6 => {
1079 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1081 self.homing_sdo_tid = self.sdo.write(
1082 client,
1083 0x609A,
1084 0,
1085 json!(accel_counts),
1086 );
1087 self.op = AxisOp::Homing(7);
1088 }
1089 7 => {
1090 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1091 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
1092 SdoResult::Pending => {
1093 if self.homing_timed_out() {
1094 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1095 }
1096 }
1097 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1098 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1099 }
1100 }
1101 8 => {
1102 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1105 let mut cw = RawControlWord(view.control_word());
1106 cw.set_bit(4, false);
1107 view.set_control_word(cw.raw());
1108 self.op = AxisOp::Homing(9);
1109 }
1110 9 => {
1111 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
1113 self.op = AxisOp::Homing(10);
1114 } else if self.homing_timed_out() {
1115 self.set_op_error("Homing timeout: mode not confirmed");
1116 }
1117 }
1118 10 => {
1119 let mut cw = RawControlWord(view.control_word());
1121 cw.set_bit(4, true);
1122 view.set_control_word(cw.raw());
1123 self.op = AxisOp::Homing(11);
1124 }
1125 11 => {
1126 let sw = view.status_word();
1131 let error = sw & (1 << 13) != 0;
1132 if error {
1133 self.set_op_error("Homing error: drive reported homing failure");
1134 } else if sw & (1 << 12) == 0 {
1135 self.op = AxisOp::Homing(12);
1136 } else if self.homing_timed_out() {
1137 self.set_op_error(&format!("Homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1138 }
1139 }
1140 12 => {
1141 let sw = view.status_word();
1144 let error = sw & (1 << 13) != 0;
1145 let attained = sw & (1 << 12) != 0;
1146 let reached = sw & (1 << 10) != 0;
1147
1148 if error {
1149 self.set_op_error("Homing error: drive reported homing failure");
1150 } else if attained && reached {
1151 self.op = AxisOp::Homing(13);
1152 } else if self.homing_timed_out() {
1153 self.set_op_error("Homing timeout: procedure did not complete");
1154 }
1155 }
1156 13 => {
1157 self.home_offset = view.position_actual()
1160 - self.config.to_counts(self.config.home_position).round() as i32;
1161 let mut cw = RawControlWord(view.control_word());
1163 cw.set_bit(4, false);
1164 view.set_control_word(cw.raw());
1165 self.op = AxisOp::Homing(14);
1166 }
1167 14 => {
1168 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1171 log::info!("Homing complete — home offset: {}", self.home_offset);
1172 self.complete_op();
1173 }
1174 _ => self.complete_op(),
1175 }
1176 }
1177
1178 fn configure_soft_homing(&mut self, method: HomingMethod) {
1181 match method {
1182 HomingMethod::LimitSwitchPosPnp => {
1183 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1184 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1185 self.soft_home_direction = 1.0;
1186 }
1187 HomingMethod::LimitSwitchNegPnp => {
1188 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1189 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1190 self.soft_home_direction = -1.0;
1191 }
1192 HomingMethod::LimitSwitchPosNpn => {
1193 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1194 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1195 self.soft_home_direction = 1.0;
1196 }
1197 HomingMethod::LimitSwitchNegNpn => {
1198 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1199 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1200 self.soft_home_direction = -1.0;
1201 }
1202 HomingMethod::HomeSensorPosPnp => {
1203 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1204 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1205 self.soft_home_direction = 1.0;
1206 }
1207 HomingMethod::HomeSensorNegPnp => {
1208 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1209 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1210 self.soft_home_direction = -1.0;
1211 }
1212 HomingMethod::HomeSensorPosNpn => {
1213 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1214 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1215 self.soft_home_direction = 1.0;
1216 }
1217 HomingMethod::HomeSensorNegNpn => {
1218 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1219 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1220 self.soft_home_direction = -1.0;
1221 }
1222 _ => {} }
1224 }
1225
1226 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1227 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1228 self.op_started = Some(Instant::now());
1229 }
1230
1231 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1232 let raw = match self.soft_home_sensor {
1233 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1234 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1235 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1236 };
1237 match self.soft_home_sensor_type {
1238 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1241 }
1242
1243
1244 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1247 let dir = if !self.config.invert_direction {
1248 direction
1249 }
1250 else {
1251 -direction
1252 };
1253
1254 let target = if dir > 0.0 {
1255 i32::MAX
1256 }
1257 else {
1258 i32::MIN
1259 };
1260
1261 return target;
1262 }
1263
1264
1265 pub fn command_halt(&self, view: &mut impl AxisView) {
1270 let mut cw = RawControlWord(view.control_word());
1271 cw.set_bit(8, true); cw.set_bit(4, false); cw.set_bit(5, true); cw.set_bit(6, false); view.set_control_word(cw.raw());
1276 }
1277
1278 pub fn command_clear_halt(&self, view: &mut impl AxisView) {
1281 let mut cw = RawControlWord(view.control_word());
1282 cw.set_bit(8, false); view.set_control_word(cw.raw());
1284 }
1285
1286
1287 pub fn command_cancel_move(&self, view: &mut impl AxisView) {
1295
1296 let mut cw = RawControlWord(view.control_word());
1297
1298 cw.set_bit(4, true); cw.set_bit(5, true); cw.set_bit(6, false); view.set_control_word(cw.raw());
1306
1307 let current_pos = view.position_actual();
1308 view.set_target_position(current_pos);
1309 view.set_profile_velocity(0);
1310 }
1311
1312
1313 fn command_homing_speed(&self, view: &mut impl AxisView) {
1315 let cpu = self.config.counts_per_user();
1316 let vel = (self.config.homing_speed * cpu).round() as u32;
1317 let accel = (self.config.homing_accel * cpu).round() as u32;
1318 let decel = (self.config.homing_decel * cpu).round() as u32;
1319 view.set_profile_velocity(vel);
1320 view.set_profile_acceleration(accel);
1321 view.set_profile_deceleration(decel);
1322 }
1323
1324 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1340 match HomeState::from_repr(step) {
1341
1342 Some(HomeState::EnsurePpMode) => {
1343 log::info!("SoftHome: Ensuring PP mode..");
1348 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1349 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1350 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1351 },
1352 Some(HomeState::WaitPpMode) => {
1353
1354 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1355 if !self.fb_mode_of_operation.is_busy() {
1356 if self.fb_mode_of_operation.is_error() {
1357 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1358 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1359 ));
1360 }
1361 else {
1362 log::info!("SoftHome: Drive is in PP mode!");
1363
1364 if !self.check_soft_home_trigger(view) {
1368 log::info!("SoftHome: Not on home switch; seek out.");
1369 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1370 } else {
1371 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1372 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1373 }
1374 }
1375 }
1376
1377
1378 },
1379
1380 Some(HomeState::Search) => {
1382 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1383
1384 let target = self.calculate_max_relative_target(self.soft_home_direction);
1394 view.set_target_position(target);
1395
1396 self.command_homing_speed(view);
1405
1406 let mut cw = RawControlWord(view.control_word());
1407 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());
1412
1413 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1414 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1415 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1416 }
1417 Some(HomeState::WaitSearching) => {
1418 if self.check_soft_home_trigger(view) {
1419 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1420 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1421 return;
1422 }
1423 let sw = RawStatusWord(view.status_word());
1424 if sw.raw() & (1 << 12) != 0 {
1425 let mut cw = RawControlWord(view.control_word());
1426 cw.set_bit(4, false);
1427 view.set_control_word(cw.raw());
1428 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1429 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1430 } else if self.homing_timed_out() {
1431 self.set_op_error("Software homing timeout: set-point not acknowledged");
1432 }
1433 }
1434 Some(HomeState::WaitFoundSensor) => {
1444 if self.check_soft_home_trigger(view) {
1445 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1446 log::info!("ControlWord is : {} ", view.control_word());
1447
1448 let mut cw = RawControlWord(view.control_word());
1449 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1452
1453
1454 self.halt_stable_count = 0;
1455 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1456 } else if self.homing_timed_out() {
1457 self.set_op_error("Software homing timeout: sensor not detected");
1458 }
1459 }
1460
1461
1462 Some(HomeState::WaitStoppedFoundSensor) => {
1463 const STABLE_WINDOW: i32 = 1;
1464 const STABLE_TICKS_REQUIRED: u8 = 10;
1465
1466 let pos = view.position_actual();
1471 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1472 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1473 } else {
1474 self.halt_stable_count = 0;
1475 }
1476
1477 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1478
1479 log::info!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1480 self.command_cancel_move(view);
1481 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1482
1483 } else if self.homing_timed_out() {
1484 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1485 }
1486 }
1487 Some(HomeState::WaitFoundSensorAck) => {
1488 let sw = RawStatusWord(view.status_word());
1489 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1490
1491 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1492
1493 let mut cw = RawControlWord(view.control_word());
1495 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1498
1499 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1500
1501 } else if self.homing_timed_out() {
1502 self.set_op_error("Software homing timeout: cancel not acknowledged");
1503 }
1504 },
1505 Some(HomeState::WaitFoundSensorAckClear) => {
1506 let sw = RawStatusWord(view.status_word());
1507 if sw.raw() & (1 << 12) == 0 {
1509
1510 self.command_clear_halt(view);
1512
1513 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1514 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1515 self.ton.call(false, Duration::from_secs(3));
1516 }
1517 },
1518 Some(HomeState::DebounceFoundSensor) => {
1520 self.ton.call(true, Duration::from_secs(3));
1521
1522 let sw = RawStatusWord(view.status_word());
1523 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1524 self.ton.call(false, Duration::from_secs(3));
1525 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1526 view.position_actual(), view.control_word(), view.status_word());
1527 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1528 }
1529 }
1530
1531 Some(HomeState::BackOff) => {
1533
1534 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1535 view.set_target_position(target);
1536
1537
1538 self.command_homing_speed(view);
1539
1540 let mut cw = RawControlWord(view.control_word());
1541 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1545 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1546 target, self.config.homing_speed, view.position_actual(), cw.raw());
1547 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1548 }
1549 Some(HomeState::WaitBackingOff) => {
1550 let sw = RawStatusWord(view.status_word());
1551 if sw.raw() & (1 << 12) != 0 {
1552 let mut cw = RawControlWord(view.control_word());
1553 cw.set_bit(4, false);
1554 view.set_control_word(cw.raw());
1555 log::info!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1556 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1557 } else if self.homing_timed_out() {
1558 self.set_op_error("Software homing timeout: back-off not acknowledged");
1559 }
1560 }
1561 Some(HomeState::WaitLostSensor) => {
1562 if !self.check_soft_home_trigger(view) {
1563 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1564
1565 self.command_halt(view);
1566 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1567 } else if self.homing_timed_out() {
1568 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1569 }
1570 }
1571 Some(HomeState::WaitStoppedLostSensor) => {
1572 const STABLE_WINDOW: i32 = 1;
1573 const STABLE_TICKS_REQUIRED: u8 = 10;
1574
1575 let pos = view.position_actual();
1580 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1581 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1582 } else {
1583 self.halt_stable_count = 0;
1584 }
1585
1586 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1587 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1588 self.command_cancel_move(view);
1589 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1590 } else if self.homing_timed_out() {
1591 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1592 }
1593 }
1594 Some(HomeState::WaitLostSensorAck) => {
1595 let sw = RawStatusWord(view.status_word());
1596 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1597
1598 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1599
1600 let mut cw = RawControlWord(view.control_word());
1602 cw.set_bit(4, false); view.set_control_word(cw.raw());
1604
1605 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1606
1607
1608 } else if self.homing_timed_out() {
1609 self.set_op_error("Software homing timeout: cancel not acknowledged");
1610 }
1611 }
1612 Some(HomeState::WaitLostSensorAckClear) => {
1613 let sw = RawStatusWord(view.status_word());
1615 if sw.raw() & (1 << 12) == 0 {
1616
1617 self.command_clear_halt(view);
1618
1619 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1620 self.homing_sdo_tid = self.sdo.write(
1623 client, 0x607C, 0, json!(desired_counts),
1624 );
1625
1626 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1627 self.config.home_position, desired_counts
1628 );
1629
1630 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1631
1632 }
1633 },
1634
1635 Some(HomeState::WaitHomeOffsetDone) => {
1636 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1638 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1639 SdoResult::Pending => {
1640 if self.homing_timed_out() {
1641 self.set_op_error("Software homing timeout: home offset SDO write");
1642 }
1643 }
1644 SdoResult::Err(e) => {
1645 self.set_op_error(&format!("Software homing SDO error: {}", e));
1646 }
1647 SdoResult::Timeout => {
1648 self.set_op_error("Software homing: home offset SDO timed out");
1649 }
1650 }
1651 },
1652 Some(HomeState::WriteHomingModeOp) => {
1653
1654 self.fb_mode_of_operation.reset();
1658 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1659 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1660 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1661
1662
1663 },
1664 Some(HomeState::WaitWriteHomingModeOp) => {
1665 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1667
1668 if !self.fb_mode_of_operation.is_busy() {
1669 if self.fb_mode_of_operation.is_error() {
1670 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1671 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1672 ));
1673 }
1674 else {
1675 log::info!("SoftHome: Drive is now in Homing Mode.");
1676 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1677 }
1678 }
1679 },
1680 Some(HomeState::WriteHomingMethod) => {
1681 self.homing_sdo_tid = self.sdo.write(
1683 client, 0x6098, 0, json!(37i8),
1684 );
1685 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1686 }
1687 Some(HomeState::WaitWriteHomingMethodDone) => {
1688 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1690 SdoResult::Ok(_) => {
1691 log::info!("SoftHome: Successfully wrote homing method.");
1692 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1693 }
1694 SdoResult::Pending => {
1695 if self.homing_timed_out() {
1696 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1697 }
1698 }
1699 SdoResult::Err(e) => {
1700 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1701 }
1702 SdoResult::Timeout => {
1703 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1704 }
1705 }
1706 }
1707 Some(HomeState::ClearHomingTrigger) => {
1708 let mut cw = RawControlWord(view.control_word());
1711 cw.set_bit(4, false);
1712 view.set_control_word(cw.raw());
1713 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1714 }
1715 Some(HomeState::TriggerHoming) => {
1716 let mut cw = RawControlWord(view.control_word());
1718 cw.set_bit(4, true);
1719 view.set_control_word(cw.raw());
1720 log::info!("SoftHome[TriggerHoming]: start homing");
1721 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1722 }
1723 Some(HomeState::WaitHomingStarted) => {
1724 let sw = view.status_word();
1730 let error = sw & (1 << 13) != 0;
1731 if error {
1732 self.restore_pp_after_error("Software homing: drive reported homing error");
1733 } else if sw & (1 << 12) == 0 {
1734 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1735 } else if self.homing_timed_out() {
1736 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1737 }
1738 }
1739 Some(HomeState::WaitHomingDone) => {
1740 let sw = view.status_word();
1742 let error = sw & (1 << 13) != 0;
1743 let attained = sw & (1 << 12) != 0;
1744 let reached = sw & (1 << 10) != 0;
1745
1746 if error {
1747 self.restore_pp_after_error("Software homing: drive reported homing error");
1748 } else if attained && reached {
1749 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
1750 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
1751 } else if self.homing_timed_out() {
1752 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
1753 }
1754 }
1755 Some(HomeState::ResetHomingTrigger) => {
1756 let mut cw = RawControlWord(view.control_word());
1761 cw.set_bit(4, false);
1762 view.set_control_word(cw.raw());
1763 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
1764 }
1765 Some(HomeState::WaitHomingTriggerCleared) => {
1766 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
1770 }
1771
1772
1773 Some(HomeState::WriteMotionModeOfOperation) => {
1774
1775 self.fb_mode_of_operation.reset();
1778 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1779 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1780 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
1781
1782 },
1783 Some(HomeState::WaitWriteMotionModeOfOperation) => {
1784 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1786
1787 if !self.fb_mode_of_operation.is_busy() {
1788 if self.fb_mode_of_operation.is_error() {
1789 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1790 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1791 ));
1792 }
1793 else {
1794 if self.is_error {
1795 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
1796 self.finish_op_error();
1797 }
1798 else {
1799 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1802 }
1803
1804 }
1805 }
1806 },
1807
1808 Some(HomeState::SendCurrentPositionTarget) => {
1809 let current_pos = view.position_actual();
1811 view.set_target_position(current_pos);
1812 view.set_profile_velocity(0);
1813 let mut cw = RawControlWord(view.control_word());
1814 cw.set_bit(4, true);
1815 cw.set_bit(5, true);
1816 cw.set_bit(6, false); view.set_control_word(cw.raw());
1818 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1819 }
1820 Some(HomeState::WaitCurrentPositionTargetSent) => {
1821 let sw = RawStatusWord(view.status_word());
1823 if sw.raw() & (1 << 12) != 0 {
1824 let mut cw = RawControlWord(view.control_word());
1825 cw.set_bit(4, false);
1826 view.set_control_word(cw.raw());
1827 log::info!("Software homing complete — position set to {} user units",
1828 self.config.home_position);
1829 self.complete_op();
1830 } else if self.homing_timed_out() {
1831 self.set_op_error("Software homing timeout: hold position not acknowledged");
1832 }
1833 }
1834 _ => self.complete_op(),
1835 }
1836 }
1837
1838 fn tick_halting(&mut self, view: &mut impl AxisView, step: u8) {
1851 match HaltState::from_repr(step) {
1852 Some(HaltState::WaitStopped) => {
1853 let pos = view.position_actual();
1857 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
1858
1859 let vel = view.velocity_actual().abs();
1860 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
1861
1862 if pos_stable || vel_stopped {
1867 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1868 } else {
1869 self.halt_stable_count = 0;
1870 }
1871
1872 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
1873 self.command_cancel_move(view);
1874 self.op_started = Some(Instant::now());
1875 self.op = AxisOp::Halting(HaltState::WaitCancelAck as u8);
1876 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1877 self.set_op_error("Halt timeout: motor did not stop");
1878 }
1879 }
1880 Some(HaltState::WaitCancelAck) => {
1881 let sw = RawStatusWord(view.status_word());
1882 let setpoint_ack = sw.raw() & (1 << 12) != 0;
1883 if setpoint_ack {
1885 let mut cw = RawControlWord(view.control_word());
1888 cw.set_bit(4, false);
1889 cw.set_bit(5, true);
1890 view.set_control_word(cw.raw());
1891 self.op_started = Some(Instant::now());
1892 self.op = AxisOp::Halting(HaltState::WaitCancelAckClear as u8);
1893 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1894 self.set_op_error("Halt timeout: cancel not acknowledged");
1895 }
1896 }
1897 Some(HaltState::WaitCancelAckClear) => {
1898 let sw = RawStatusWord(view.status_word());
1899 if sw.raw() & (1 << 12) == 0 {
1900 self.command_clear_halt(view);
1902 self.complete_op();
1903 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
1904 self.set_op_error("Halt timeout: ack did not clear");
1905 }
1906 }
1907 None => {
1908 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
1909 self.complete_op();
1910 }
1911 }
1912 }
1913
1914 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1919 match step {
1920 1 => {
1921 let mut cw = RawControlWord(view.control_word());
1923 cw.cmd_fault_reset();
1924 view.set_control_word(cw.raw());
1925 self.op = AxisOp::FaultRecovery(2);
1926 }
1927 2 => {
1928 let sw = RawStatusWord(view.status_word());
1930 let state = sw.state();
1931 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1932 log::info!("Fault cleared (drive state: {})", state);
1933 self.complete_op();
1934 } else if self.op_timed_out() {
1935 self.set_op_error("Fault reset timeout: drive still faulted");
1936 }
1937 }
1938 _ => self.complete_op(),
1939 }
1940 }
1941}
1942
1943#[cfg(test)]
1948mod tests {
1949 use super::*;
1950
1951 struct MockView {
1953 control_word: u16,
1954 status_word: u16,
1955 target_position: i32,
1956 profile_velocity: u32,
1957 profile_acceleration: u32,
1958 profile_deceleration: u32,
1959 modes_of_operation: i8,
1960 modes_of_operation_display: i8,
1961 position_actual: i32,
1962 velocity_actual: i32,
1963 error_code: u16,
1964 positive_limit: bool,
1965 negative_limit: bool,
1966 home_sensor: bool,
1967 }
1968
1969 impl MockView {
1970 fn new() -> Self {
1971 Self {
1972 control_word: 0,
1973 status_word: 0x0040, target_position: 0,
1975 profile_velocity: 0,
1976 profile_acceleration: 0,
1977 profile_deceleration: 0,
1978 modes_of_operation: 0,
1979 modes_of_operation_display: 1, position_actual: 0,
1981 velocity_actual: 0,
1982 error_code: 0,
1983 positive_limit: false,
1984 negative_limit: false,
1985 home_sensor: false,
1986 }
1987 }
1988
1989 fn set_state(&mut self, state: u16) {
1990 self.status_word = state;
1991 }
1992 }
1993
1994 impl AxisView for MockView {
1995 fn control_word(&self) -> u16 { self.control_word }
1996 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1997 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1998 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1999 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
2000 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
2001 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
2002 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
2003 fn status_word(&self) -> u16 { self.status_word }
2004 fn position_actual(&self) -> i32 { self.position_actual }
2005 fn velocity_actual(&self) -> i32 { self.velocity_actual }
2006 fn error_code(&self) -> u16 { self.error_code }
2007 fn positive_limit_active(&self) -> bool { self.positive_limit }
2008 fn negative_limit_active(&self) -> bool { self.negative_limit }
2009 fn home_sensor_active(&self) -> bool { self.home_sensor }
2010 }
2011
2012 fn test_config() -> AxisConfig {
2013 AxisConfig::new(12_800).with_user_scale(360.0)
2014 }
2015
2016 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2018 use tokio::sync::mpsc;
2019 let (write_tx, write_rx) = mpsc::unbounded_channel();
2020 let (response_tx, response_rx) = mpsc::unbounded_channel();
2021 let client = CommandClient::new(write_tx, response_rx);
2022 let axis = Axis::new(test_config(), "TestDrive");
2023 (axis, client, response_tx, write_rx)
2024 }
2025
2026 #[test]
2027 fn axis_config_conversion() {
2028 let cfg = test_config();
2029 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2031 }
2032
2033 #[test]
2034 fn enable_sequence_sets_pp_mode_and_shutdown() {
2035 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2036 let mut view = MockView::new();
2037
2038 axis.enable(&mut view);
2039
2040 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
2042 assert_eq!(view.control_word & 0x008F, 0x0006);
2044 assert_eq!(axis.op, AxisOp::Enabling(1));
2046
2047 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2050
2051 assert_eq!(view.control_word & 0x008F, 0x000F);
2053 assert_eq!(axis.op, AxisOp::Enabling(2));
2054
2055 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2058
2059 assert_eq!(axis.op, AxisOp::Idle);
2061 assert!(axis.motor_on);
2062 }
2063
2064 #[test]
2065 fn move_absolute_sets_target() {
2066 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2067 let mut view = MockView::new();
2068 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2073
2074 assert_eq!(view.target_position, 1600);
2076 assert_eq!(view.profile_velocity, 3200);
2078 assert_eq!(view.profile_acceleration, 6400);
2080 assert_eq!(view.profile_deceleration, 6400);
2081 assert!(view.control_word & (1 << 4) != 0);
2083 assert!(view.control_word & (1 << 6) == 0);
2085 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2087 }
2088
2089 #[test]
2090 fn move_relative_sets_relative_bit() {
2091 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2092 let mut view = MockView::new();
2093 view.set_state(0x0027);
2094 axis.tick(&mut view, &mut client);
2095
2096 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2097
2098 assert!(view.control_word & (1 << 6) != 0);
2100 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1, _, _)));
2101 }
2102
2103 #[test]
2104 fn move_completes_on_target_reached() {
2105 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2106 let mut view = MockView::new();
2107 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2109
2110 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2111
2112 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2115 assert!(view.control_word & (1 << 4) == 0);
2117
2118 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2121 assert_eq!(axis.op, AxisOp::Idle);
2123 assert!(!axis.in_motion);
2124 }
2125
2126 #[test]
2127 fn fault_detected_sets_error() {
2128 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2129 let mut view = MockView::new();
2130 view.set_state(0x0008); view.error_code = 0x1234;
2132
2133 axis.tick(&mut view, &mut client);
2134
2135 assert!(axis.is_error);
2136 assert_eq!(axis.error_code, 0x1234);
2137 assert!(axis.error_message.contains("fault"));
2138 }
2139
2140 #[test]
2141 fn fault_recovery_sequence() {
2142 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2143 let mut view = MockView::new();
2144 view.set_state(0x0008); axis.reset_faults(&mut view);
2147 assert!(view.control_word & 0x0080 == 0);
2149
2150 axis.tick(&mut view, &mut client);
2152 assert!(view.control_word & 0x0080 != 0);
2153
2154 view.set_state(0x0040);
2156 axis.tick(&mut view, &mut client);
2157 assert_eq!(axis.op, AxisOp::Idle);
2158 assert!(!axis.is_error);
2159 }
2160
2161 #[test]
2162 fn disable_sequence() {
2163 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2164 let mut view = MockView::new();
2165 view.set_state(0x0027); axis.disable(&mut view);
2168 assert_eq!(view.control_word & 0x008F, 0x0007);
2170
2171 view.set_state(0x0023); axis.tick(&mut view, &mut client);
2174 assert_eq!(axis.op, AxisOp::Idle);
2175 }
2176
2177 #[test]
2178 fn position_tracks_with_home_offset() {
2179 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2180 let mut view = MockView::new();
2181 view.set_state(0x0027);
2182 view.position_actual = 5000;
2183
2184 axis.enable(&mut view);
2186 view.set_state(0x0021);
2187 axis.tick(&mut view, &mut client);
2188 view.set_state(0x0027);
2189 axis.tick(&mut view, &mut client);
2190
2191 assert_eq!(axis.home_offset, 5000);
2193
2194 assert!((axis.position - 0.0).abs() < 0.01);
2196
2197 view.position_actual = 6600;
2199 axis.tick(&mut view, &mut client);
2200
2201 assert!((axis.position - 45.0).abs() < 0.1);
2203 }
2204
2205 #[test]
2206 fn set_position_adjusts_home_offset() {
2207 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2208 let mut view = MockView::new();
2209 view.position_actual = 3200;
2210
2211 axis.set_position(&view, 90.0);
2212 axis.tick(&mut view, &mut client);
2213
2214 assert_eq!(axis.home_offset, 0);
2216 assert!((axis.position - 90.0).abs() < 0.01);
2217 }
2218
2219 #[test]
2220 fn halt_runs_multi_stage_close_out() {
2221 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2222 let mut view = MockView::new();
2223 view.set_state(0x0027);
2224
2225 axis.halt(&mut view);
2226
2227 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2229 assert!(view.control_word & (1 << 4) == 0, "new_setpoint must be cleared");
2230
2231 assert!(matches!(axis.op, AxisOp::Halting(_)),
2233 "halt should enter Halting state, not Idle");
2234 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2235 assert_eq!(step, HaltState::WaitStopped as u8);
2236
2237 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2244 axis.tick(&mut view, &mut client);
2245 }
2246 assert!(matches!(axis.op, AxisOp::Halting(_)));
2248 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2249 assert_eq!(step, HaltState::WaitCancelAck as u8,
2250 "should advance past WaitStopped once position/velocity is stable");
2251
2252 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2256 }
2257
2258 #[test]
2259 fn is_busy_tracks_operations() {
2260 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2261 let mut view = MockView::new();
2262
2263 axis.tick(&mut view, &mut client);
2265 assert!(!axis.is_busy);
2266
2267 axis.enable(&mut view);
2269 axis.tick(&mut view, &mut client);
2270 assert!(axis.is_busy);
2271
2272 view.set_state(0x0021);
2274 axis.tick(&mut view, &mut client);
2275 view.set_state(0x0027);
2276 axis.tick(&mut view, &mut client);
2277 assert!(!axis.is_busy);
2278
2279 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2281 axis.tick(&mut view, &mut client);
2282 assert!(axis.is_busy);
2283 assert!(axis.in_motion);
2284 }
2285
2286 #[test]
2287 fn fault_during_move_cancels_op() {
2288 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2289 let mut view = MockView::new();
2290 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2292
2293 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2295 axis.tick(&mut view, &mut client);
2296 assert!(axis.is_busy);
2297 assert!(!axis.is_error);
2298
2299 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2302
2303 assert!(!axis.is_busy);
2305 assert!(axis.is_error);
2306 assert_eq!(axis.op, AxisOp::Idle);
2307 }
2308
2309 #[test]
2310 fn move_absolute_rejected_by_max_limit() {
2311 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2312 let mut view = MockView::new();
2313 view.set_state(0x0027);
2314 axis.tick(&mut view, &mut client);
2315
2316 axis.set_software_max_limit(90.0);
2317 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2318
2319 assert!(axis.is_error);
2321 assert_eq!(axis.op, AxisOp::Idle);
2322 assert!(axis.error_message.contains("max software limit"));
2323 }
2324
2325 #[test]
2326 fn move_absolute_rejected_by_min_limit() {
2327 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2328 let mut view = MockView::new();
2329 view.set_state(0x0027);
2330 axis.tick(&mut view, &mut client);
2331
2332 axis.set_software_min_limit(-10.0);
2333 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2334
2335 assert!(axis.is_error);
2336 assert_eq!(axis.op, AxisOp::Idle);
2337 assert!(axis.error_message.contains("min software limit"));
2338 }
2339
2340 #[test]
2341 fn move_relative_rejected_by_max_limit() {
2342 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2343 let mut view = MockView::new();
2344 view.set_state(0x0027);
2345 axis.tick(&mut view, &mut client);
2346
2347 axis.set_software_max_limit(50.0);
2349 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2350
2351 assert!(axis.is_error);
2352 assert_eq!(axis.op, AxisOp::Idle);
2353 assert!(axis.error_message.contains("max software limit"));
2354 }
2355
2356 #[test]
2357 fn move_within_limits_allowed() {
2358 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2359 let mut view = MockView::new();
2360 view.set_state(0x0027);
2361 axis.tick(&mut view, &mut client);
2362
2363 axis.set_software_max_limit(90.0);
2364 axis.set_software_min_limit(-90.0);
2365 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2366
2367 assert!(!axis.is_error);
2369 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2370 }
2371
2372 #[test]
2373 fn runtime_limit_halts_move_in_violated_direction() {
2374 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2375 let mut view = MockView::new();
2376 view.set_state(0x0027);
2377 axis.tick(&mut view, &mut client);
2378
2379 axis.set_software_max_limit(45.0);
2380 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2382
2383 view.position_actual = 1650;
2386 view.velocity_actual = 100; view.status_word = 0x1027;
2390 axis.tick(&mut view, &mut client);
2391 view.status_word = 0x0027;
2392 axis.tick(&mut view, &mut client);
2393
2394 assert!(axis.is_error);
2396 assert!(axis.at_max_limit);
2397 assert_eq!(axis.op, AxisOp::Idle);
2398 assert!(axis.error_message.contains("Software position limit"));
2399 assert!(view.control_word & (1 << 8) != 0);
2401 }
2402
2403 #[test]
2404 fn runtime_limit_allows_move_in_opposite_direction() {
2405 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2406 let mut view = MockView::new();
2407 view.set_state(0x0027);
2408 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2411 axis.tick(&mut view, &mut client);
2412 assert!(axis.at_max_limit);
2413
2414 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2416 assert!(!axis.is_error);
2417 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2418
2419 view.velocity_actual = -100;
2421 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2423 assert!(!axis.is_error);
2425 }
2426
2427 #[test]
2428 fn positive_limit_switch_halts_positive_move() {
2429 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2430 let mut view = MockView::new();
2431 view.set_state(0x0027);
2432 axis.tick(&mut view, &mut client);
2433
2434 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2436 view.velocity_actual = 100; view.status_word = 0x1027;
2439 axis.tick(&mut view, &mut client);
2440 view.status_word = 0x0027;
2441
2442 view.positive_limit = true;
2444 axis.tick(&mut view, &mut client);
2445
2446 assert!(axis.is_error);
2447 assert!(axis.at_positive_limit_switch);
2448 assert!(!axis.is_busy);
2449 assert!(axis.error_message.contains("Positive limit switch"));
2450 assert!(view.control_word & (1 << 8) != 0);
2452 }
2453
2454 #[test]
2455 fn negative_limit_switch_halts_negative_move() {
2456 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2457 let mut view = MockView::new();
2458 view.set_state(0x0027);
2459 axis.tick(&mut view, &mut client);
2460
2461 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2463 view.velocity_actual = -100; view.status_word = 0x1027;
2465 axis.tick(&mut view, &mut client);
2466 view.status_word = 0x0027;
2467
2468 view.negative_limit = true;
2470 axis.tick(&mut view, &mut client);
2471
2472 assert!(axis.is_error);
2473 assert!(axis.at_negative_limit_switch);
2474 assert!(axis.error_message.contains("Negative limit switch"));
2475 }
2476
2477 #[test]
2478 fn limit_switch_allows_move_in_opposite_direction() {
2479 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2480 let mut view = MockView::new();
2481 view.set_state(0x0027);
2482 view.positive_limit = true;
2484 view.velocity_actual = -100;
2485 axis.tick(&mut view, &mut client);
2486 assert!(axis.at_positive_limit_switch);
2487
2488 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2490 view.status_word = 0x1027;
2491 axis.tick(&mut view, &mut client);
2492
2493 assert!(!axis.is_error);
2495 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
2496 }
2497
2498 #[test]
2499 fn limit_switch_ignored_when_not_moving() {
2500 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2501 let mut view = MockView::new();
2502 view.set_state(0x0027);
2503 view.positive_limit = true;
2504
2505 axis.tick(&mut view, &mut client);
2506
2507 assert!(axis.at_positive_limit_switch);
2509 assert!(!axis.is_error);
2510 }
2511
2512 #[test]
2513 fn home_sensor_output_tracks_view() {
2514 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2515 let mut view = MockView::new();
2516 view.set_state(0x0027);
2517
2518 axis.tick(&mut view, &mut client);
2519 assert!(!axis.home_sensor);
2520
2521 view.home_sensor = true;
2522 axis.tick(&mut view, &mut client);
2523 assert!(axis.home_sensor);
2524
2525 view.home_sensor = false;
2526 axis.tick(&mut view, &mut client);
2527 assert!(!axis.home_sensor);
2528 }
2529
2530 #[test]
2531 fn velocity_output_converted() {
2532 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2533 let mut view = MockView::new();
2534 view.set_state(0x0027);
2535 view.velocity_actual = 3200;
2537
2538 axis.tick(&mut view, &mut client);
2539
2540 assert!((axis.speed - 90.0).abs() < 0.1);
2541 assert!(axis.moving_positive);
2542 assert!(!axis.moving_negative);
2543 }
2544
2545 fn soft_homing_config() -> AxisConfig {
2548 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2549 cfg.homing_speed = 10.0;
2550 cfg.homing_accel = 20.0;
2551 cfg.homing_decel = 20.0;
2552 cfg
2553 }
2554
2555 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2556 use tokio::sync::mpsc;
2557 let (write_tx, write_rx) = mpsc::unbounded_channel();
2558 let (response_tx, response_rx) = mpsc::unbounded_channel();
2559 let client = CommandClient::new(write_tx, response_rx);
2560 let axis = Axis::new(soft_homing_config(), "TestDrive");
2561 (axis, client, response_tx, write_rx)
2562 }
2563
2564 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2566 view.set_state(0x0027); axis.tick(view, client);
2568 }
2569
2570 fn complete_soft_homing(
2575 axis: &mut Axis,
2576 view: &mut MockView,
2577 client: &mut CommandClient,
2578 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2579 trigger_pos: i32,
2580 clear_sensor: impl FnOnce(&mut MockView),
2581 ) {
2582 use mechutil::ipc::CommandMessage as IpcMsg;
2583
2584 axis.tick(view, client);
2587 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2588
2589 view.position_actual = trigger_pos + 100;
2591 axis.tick(view, client);
2592 view.position_actual = trigger_pos + 120;
2593 axis.tick(view, client);
2594 for _ in 0..10 { axis.tick(view, client); }
2596 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2597
2598 view.status_word = 0x1027;
2600 axis.tick(view, client);
2601 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2602 view.status_word = 0x0027;
2603
2604 for _ in 0..100 { axis.tick(view, client); }
2606 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2607
2608 axis.tick(view, client);
2611 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2612
2613 view.status_word = 0x1027;
2615 axis.tick(view, client);
2616 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2617 view.status_word = 0x0027;
2618
2619 axis.tick(view, client);
2621 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2622 clear_sensor(view);
2623 view.position_actual = trigger_pos - 200;
2624 axis.tick(view, client);
2625 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2626
2627 axis.tick(view, client);
2629 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2630 for _ in 0..10 { axis.tick(view, client); }
2631 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2632
2633 view.status_word = 0x1027;
2636 axis.tick(view, client);
2637 view.status_word = 0x0027;
2638 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2639
2640 let tid = axis.homing_sdo_tid;
2642 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2643 client.poll();
2644 axis.tick(view, client);
2645 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2646
2647 axis.tick(view, client);
2649 let tid = axis.homing_sdo_tid;
2650 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2651 client.poll();
2652 axis.tick(view, client);
2653 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2654
2655 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2657 axis.tick(view, client);
2658 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2659
2660 view.status_word = 0x1427; axis.tick(view, client);
2663 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2664 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2665 view.status_word = 0x0027;
2666
2667 axis.tick(view, client);
2669 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2670
2671 view.status_word = 0x1027;
2673 axis.tick(view, client);
2674 view.status_word = 0x0027;
2675
2676 assert_eq!(axis.op, AxisOp::Idle);
2677 assert!(!axis.is_busy);
2678 assert!(!axis.is_error);
2679 assert_eq!(axis.home_offset, 0); }
2681
2682 #[test]
2683 fn soft_homing_pnp_home_sensor_full_sequence() {
2684 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2685 let mut view = MockView::new();
2686 enable_axis(&mut axis, &mut view, &mut client);
2687
2688 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2689
2690 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2693 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2695 axis.tick(&mut view, &mut client); view.home_sensor = true;
2699 view.position_actual = 5000;
2700 axis.tick(&mut view, &mut client);
2701 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2702
2703 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2704 |v| { v.home_sensor = false; });
2705 }
2706
2707 #[test]
2708 fn soft_homing_npn_home_sensor_full_sequence() {
2709 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2710 let mut view = MockView::new();
2711 view.home_sensor = true;
2713 enable_axis(&mut axis, &mut view, &mut client);
2714
2715 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2716
2717 axis.tick(&mut view, &mut client);
2719 view.status_word = 0x1027;
2720 axis.tick(&mut view, &mut client);
2721 view.status_word = 0x0027;
2722 axis.tick(&mut view, &mut client);
2723
2724 view.home_sensor = false;
2726 view.position_actual = 3000;
2727 axis.tick(&mut view, &mut client);
2728 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2729
2730 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2731 |v| { v.home_sensor = true; }); }
2733
2734 #[test]
2735 fn soft_homing_limit_switch_suppresses_halt() {
2736 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2737 let mut view = MockView::new();
2738 enable_axis(&mut axis, &mut view, &mut client);
2739
2740 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
2742
2743 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2748 axis.tick(&mut view, &mut client); view.positive_limit = true;
2752 view.velocity_actual = 100; view.position_actual = 8000;
2754 axis.tick(&mut view, &mut client);
2755
2756 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2758 assert!(!axis.is_error);
2759 }
2760
2761 #[test]
2762 fn soft_homing_opposite_limit_still_protects() {
2763 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2764 let mut view = MockView::new();
2765 enable_axis(&mut axis, &mut view, &mut client);
2766
2767 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2769
2770 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2775 axis.tick(&mut view, &mut client); view.negative_limit = true;
2780 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
2782
2783 assert!(axis.is_error);
2785 assert!(axis.error_message.contains("Negative limit switch"));
2786 }
2787
2788 #[test]
2789 #[test]
2807 fn soft_homing_negative_direction_sets_negative_target() {
2808 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2809 let mut view = MockView::new();
2810 enable_axis(&mut axis, &mut view, &mut client);
2811
2812 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
2813 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
2817 }
2818
2819 #[test]
2820 fn home_integrated_method_starts_hardware_homing() {
2821 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2822 let mut view = MockView::new();
2823 enable_axis(&mut axis, &mut view, &mut client);
2824
2825 axis.home(&mut view, HomingMethod::CurrentPosition);
2826 assert!(matches!(axis.op, AxisOp::Homing(0)));
2827 assert_eq!(axis.homing_method, 37);
2828 }
2829
2830 #[test]
2831 fn home_integrated_arbitrary_code() {
2832 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2833 let mut view = MockView::new();
2834 enable_axis(&mut axis, &mut view, &mut client);
2835
2836 axis.home(&mut view, HomingMethod::Integrated(35));
2837 assert!(matches!(axis.op, AxisOp::Homing(0)));
2838 assert_eq!(axis.homing_method, 35);
2839 }
2840
2841 #[test]
2842 fn hardware_homing_skips_speed_sdos_when_zero() {
2843 use mechutil::ipc::CommandMessage;
2844
2845 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
2846 let mut view = MockView::new();
2847 enable_axis(&mut axis, &mut view, &mut client);
2848
2849 axis.home(&mut view, HomingMethod::Integrated(37));
2851
2852 axis.tick(&mut view, &mut client);
2854 assert!(matches!(axis.op, AxisOp::Homing(1)));
2855
2856 let _ = write_rx.try_recv();
2858
2859 let tid = axis.homing_sdo_tid;
2861 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2862 client.poll();
2863 axis.tick(&mut view, &mut client);
2864
2865 assert!(matches!(axis.op, AxisOp::Homing(8)));
2867 }
2868
2869 #[test]
2870 fn hardware_homing_writes_speed_sdos_when_nonzero() {
2871 use mechutil::ipc::CommandMessage;
2872
2873 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
2874 let mut view = MockView::new();
2875 enable_axis(&mut axis, &mut view, &mut client);
2876
2877 axis.home(&mut view, HomingMethod::Integrated(37));
2879
2880 axis.tick(&mut view, &mut client);
2882 assert!(matches!(axis.op, AxisOp::Homing(1)));
2883 let _ = write_rx.try_recv();
2884
2885 let tid = axis.homing_sdo_tid;
2887 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2888 client.poll();
2889 axis.tick(&mut view, &mut client);
2890 assert!(matches!(axis.op, AxisOp::Homing(2)));
2892 }
2893
2894 #[test]
2895 fn soft_homing_edge_during_ack_step() {
2896 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2897 let mut view = MockView::new();
2898 enable_axis(&mut axis, &mut view, &mut client);
2899
2900 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2901 axis.tick(&mut view, &mut client); view.home_sensor = true;
2905 view.position_actual = 2000;
2906 axis.tick(&mut view, &mut client);
2907
2908 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2910 }
2911
2912 #[test]
2913 fn soft_homing_applies_home_position() {
2914 let mut cfg = soft_homing_config();
2915 cfg.home_position = 90.0;
2916
2917 use tokio::sync::mpsc;
2918 let (write_tx, _write_rx) = mpsc::unbounded_channel();
2919 let (resp_tx, response_rx) = mpsc::unbounded_channel();
2920 let mut client = CommandClient::new(write_tx, response_rx);
2921 let mut axis = Axis::new(cfg, "TestDrive");
2922
2923 let mut view = MockView::new();
2924 enable_axis(&mut axis, &mut view, &mut client);
2925
2926 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2927
2928 axis.tick(&mut view, &mut client);
2930 view.status_word = 0x1027;
2931 axis.tick(&mut view, &mut client);
2932 view.status_word = 0x0027;
2933 axis.tick(&mut view, &mut client);
2934
2935 view.home_sensor = true;
2937 view.position_actual = 5000;
2938 axis.tick(&mut view, &mut client);
2939 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2940
2941 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2943 |v| { v.home_sensor = false; });
2944
2945 assert_eq!(axis.home_offset, 0);
2947 }
2948
2949 #[test]
2950 fn soft_homing_default_home_position_zero() {
2951 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2952 let mut view = MockView::new();
2953 enable_axis(&mut axis, &mut view, &mut client);
2954
2955 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2956
2957 axis.tick(&mut view, &mut client);
2959 view.status_word = 0x1027;
2960 axis.tick(&mut view, &mut client);
2961 view.status_word = 0x0027;
2962 axis.tick(&mut view, &mut client);
2963
2964 view.home_sensor = true;
2966 view.position_actual = 5000;
2967 axis.tick(&mut view, &mut client);
2968
2969 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2970 |v| { v.home_sensor = false; });
2971
2972 assert_eq!(axis.home_offset, 0);
2973 }
2974}