1use std::time::{Duration, Instant};
25
26use serde_json::json;
27use strum_macros::FromRepr;
28
29use crate::command_client::CommandClient;
30use crate::ethercat::{SdoClient, SdoResult};
31use crate::fb::Ton;
32use crate::motion::FbSetModeOfOperation;
33use super::axis_config::AxisConfig;
34use super::axis_view::AxisView;
35use super::homing::HomingMethod;
36use super::cia402::{
37 Cia402Control, Cia402Status, Cia402State,
38 ModesOfOperation, RawControlWord, RawStatusWord,
39};
40
41#[derive(Debug, Clone, PartialEq)]
46enum AxisOp {
47 Idle,
48 Enabling(u8),
49 Disabling(u8),
50 Moving(MoveKind, u8, bool, bool),
51 Homing(u8),
52 SoftHoming(u8),
53 Halting(u8),
54 FaultRecovery(u8),
55}
56
57#[repr(u8)]
63#[derive(Debug, Clone, PartialEq, FromRepr)]
64enum HaltState {
65 WaitStopped = 0,
68 WaitCancelAck = 10,
71 WaitCancelAckClear = 20,
75}
76
77const HALT_STAGE_TIMEOUT: Duration = Duration::from_secs(3);
79
80const HALT_STABLE_WINDOW: i32 = 50;
85
86const HALT_STOPPED_VELOCITY: i32 = 100;
92
93const HALT_STABLE_TICKS_REQUIRED: u8 = 5;
97
98#[repr(u8)]
99#[derive(Debug, Clone, PartialEq, FromRepr)]
100enum HomeState {
101 EnsurePpMode = 0,
102 WaitPpMode = 1,
103 Search = 5,
104 WaitSearching = 10,
105 WaitFoundSensor = 20,
106 WaitStoppedFoundSensor = 30,
107 WaitFoundSensorAck = 40,
108 WaitFoundSensorAckClear = 45,
109 DebounceFoundSensor = 50,
110 BackOff = 60,
111 WaitBackingOff = 70,
112 WaitLostSensor = 80,
113 WaitStoppedLostSensor = 90,
114 WaitLostSensorAck = 100,
115 WaitLostSensorAckClear = 120,
116 WaitHomeOffsetDone = 125,
117
118 WriteHomingModeOp = 160,
119 WaitWriteHomingModeOp = 165,
120
121 WriteHomingMethod = 205,
122 WaitWriteHomingMethodDone = 210,
123 ClearHomingTrigger = 215,
124 TriggerHoming = 217,
125 WaitHomingStarted = 218,
126 WaitHomingDone = 220,
127 ResetHomingTrigger = 222,
128 WaitHomingTriggerCleared = 223,
129 WriteMotionModeOfOperation = 230,
130 WaitWriteMotionModeOfOperation = 235,
131 SendCurrentPositionTarget = 240,
132 WaitCurrentPositionTargetSent = 245
133
134}
135
136#[derive(Debug, Clone, PartialEq)]
137enum MoveKind {
138 Absolute,
139 Relative,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq)]
143enum SoftHomeSensor {
144 PositiveLimit,
145 NegativeLimit,
146 HomeSensor,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq)]
150enum SoftHomeSensorType {
151 Pnp,
153 Npn,
155}
156
157pub struct Axis {
167 config: AxisConfig,
168 sdo: SdoClient,
169
170 op: AxisOp,
172 home_offset: i32,
173 last_raw_position: i32,
174 op_started: Option<Instant>,
175 op_timeout: Duration,
176 homing_timeout: Duration,
177 move_start_timeout: Duration,
178 pending_move_target: i32,
179 pending_move_vel: u32,
180 pending_move_accel: u32,
181 pending_move_decel: u32,
182 homing_method: i8,
183 homing_sdo_tid: u32,
184 soft_home_sensor: SoftHomeSensor,
185 soft_home_sensor_type: SoftHomeSensorType,
186 soft_home_direction: f64,
187 halt_stable_count: u8,
188 prev_positive_limit: bool,
189 prev_negative_limit: bool,
190 prev_home_sensor: bool,
191
192
193
194 fb_mode_of_operation : FbSetModeOfOperation,
195
196 pub is_error: bool,
200 pub error_code: u32,
202 pub error_message: String,
204 pub motor_on: bool,
206 pub is_busy: bool,
212 pub in_motion: bool,
214 pub moving_positive: bool,
216 pub moving_negative: bool,
218 pub position: f64,
220 pub raw_position: i64,
222 pub speed: f64,
224 pub at_max_limit: bool,
226 pub at_min_limit: bool,
228 pub at_positive_limit_switch: bool,
230 pub at_negative_limit_switch: bool,
232 pub home_sensor: bool,
234
235
236 ton : Ton
238}
239
240impl Axis {
241 pub fn new(config: AxisConfig, device_name: &str) -> Self {
246 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
247 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
248 let move_start_timeout = op_timeout; Self {
250 config,
251 sdo: SdoClient::new(device_name),
252 op: AxisOp::Idle,
253 home_offset: 0,
254 last_raw_position: 0,
255 op_started: None,
256 op_timeout,
257 homing_timeout,
258 move_start_timeout,
259 pending_move_target: 0,
260 pending_move_vel: 0,
261 pending_move_accel: 0,
262 pending_move_decel: 0,
263 homing_method: 37,
264 homing_sdo_tid: 0,
265 soft_home_sensor: SoftHomeSensor::HomeSensor,
266 soft_home_sensor_type: SoftHomeSensorType::Pnp,
267 soft_home_direction: 1.0,
268 halt_stable_count: 0,
269 prev_positive_limit: false,
270 prev_negative_limit: false,
271 prev_home_sensor: false,
272 is_error: false,
273 error_code: 0,
274 error_message: String::new(),
275 motor_on: false,
276 is_busy: false,
277 in_motion: false,
278 moving_positive: false,
279 moving_negative: false,
280 position: 0.0,
281 raw_position: 0,
282 speed: 0.0,
283 at_max_limit: false,
284 at_min_limit: false,
285 at_positive_limit_switch: false,
286 at_negative_limit_switch: false,
287 home_sensor: false,
288 ton: Ton::new(),
289 fb_mode_of_operation : FbSetModeOfOperation::new()
290 }
291 }
292
293 pub fn config(&self) -> &AxisConfig {
295 &self.config
296 }
297
298 pub fn move_absolute(
308 &mut self,
309 view: &mut impl AxisView,
310 target: f64,
311 vel: f64,
312 accel: f64,
313 decel: f64,
314 ) {
315 if let Some(msg) = self.check_target_limit(target, view) {
316 self.set_op_error(&msg);
317 return;
318 }
319
320 let cpu = self.config.counts_per_user();
321 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
322 let raw_vel = (vel * cpu).round() as u32;
323 let raw_accel = (accel * cpu).round() as u32;
324 let raw_decel = (decel * cpu).round() as u32;
325
326 let user_pos = target > self.position;
328 let user_neg = target < self.position;
329
330 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute, user_pos, user_neg);
331 }
332
333 pub fn move_relative(
339 &mut self,
340 view: &mut impl AxisView,
341 distance: f64,
342 vel: f64,
343 accel: f64,
344 decel: f64,
345 ) {
346 log::info!("Axis: request to move relative dist {} vel {} accel {} decel {}",
347 distance, vel, accel, decel
348 );
349
350 if let Some(msg) = self.check_target_limit(self.position + distance, view) {
351 self.set_op_error(&msg);
352 return;
353 }
354
355 let cpu = self.config.counts_per_user();
356 let raw_distance = self.config.to_counts(distance).round() as i32;
357 let raw_vel = (vel * cpu).round() as u32;
358 let raw_accel = (accel * cpu).round() as u32;
359 let raw_decel = (decel * cpu).round() as u32;
360
361 log::info!("Axis starting relative move: request to move relative raw dist {} raw vel {} raw accel {} raw decel {}",
362 raw_distance, raw_vel, raw_accel, raw_decel
363 );
364
365 let mut cw = RawControlWord(view.control_word());
367 cw.set_bit(4, false); view.set_control_word(cw.raw());
369
370 let user_pos = distance > 0.0;
372 let user_neg = distance < 0.0;
373
374 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative, user_pos, user_neg);
375 }
376
377 fn start_move(
378 &mut self,
379 view: &mut impl AxisView,
380 raw_target: i32,
381 raw_vel: u32,
382 raw_accel: u32,
383 raw_decel: u32,
384 kind: MoveKind,
385 user_pos: bool,
386 user_neg: bool,
387 ) {
388 self.pending_move_target = raw_target;
389 self.pending_move_vel = raw_vel;
390 self.pending_move_accel = raw_accel;
391 self.pending_move_decel = raw_decel;
392
393 view.set_target_position(raw_target);
395 view.set_profile_velocity(raw_vel);
396 view.set_profile_acceleration(raw_accel);
397 view.set_profile_deceleration(raw_decel);
398
399 let mut cw = RawControlWord(view.control_word());
404 cw.set_bit(6, kind == MoveKind::Relative);
405 cw.set_bit(8, false); cw.set_bit(4, true); view.set_control_word(cw.raw());
408
409 self.op = AxisOp::Moving(kind, 1, user_pos, user_neg);
414 self.op_started = Some(Instant::now());
415 }
416
417 pub fn halt(&mut self, view: &mut impl AxisView) {
435 self.command_halt(view);
436 self.halt_stable_count = 0;
437 self.last_raw_position = view.position_actual();
438 self.op_started = Some(Instant::now());
439 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
440 }
441
442 pub fn enable(&mut self, view: &mut impl AxisView) {
450 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
452 let mut cw = RawControlWord(view.control_word());
453 cw.cmd_shutdown();
454 view.set_control_word(cw.raw());
455
456 self.op = AxisOp::Enabling(1);
457 self.op_started = Some(Instant::now());
458 }
459
460 pub fn disable(&mut self, view: &mut impl AxisView) {
462 let mut cw = RawControlWord(view.control_word());
463 cw.cmd_disable_operation();
464 cw.set_bit(4, false);
465 cw.set_bit(8, false);
466 cw.set_bit(7, false);
467 cw.set_bit(2, true);
468 view.set_control_word(cw.raw());
469
470 self.op = AxisOp::Disabling(1);
471 self.op_started = Some(Instant::now());
472 }
473
474 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
490 let sw = RawStatusWord(view.status_word());
491 let drive_in_fault = matches!(
492 sw.state(),
493 Cia402State::Fault | Cia402State::FaultReactionActive
494 );
495
496 self.is_error = false;
497 self.error_code = 0;
498 self.error_message.clear();
499
500 if drive_in_fault {
501 let mut cw = RawControlWord(view.control_word());
503 cw.cmd_clear_fault_reset();
504 view.set_control_word(cw.raw());
505 self.op = AxisOp::FaultRecovery(1);
506 self.op_started = Some(Instant::now());
507 } else {
508 self.op = AxisOp::Idle;
510 self.op_started = None;
511 self.is_busy = false;
512 self.in_motion = false;
513 }
514 }
515
516 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
524 if method.is_integrated() {
525 self.homing_method = match method {
526 HomingMethod::CurrentPosition => self.config.soft_home_method,
527 _ => method.cia402_code(),
528 };
529 self.op = AxisOp::Homing(0);
530 self.op_started = Some(Instant::now());
531 let _ = view;
532 } else {
533 self.configure_soft_homing(method);
534 self.start_soft_homing(view);
535 }
536 }
537
538 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
547 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
548 }
549
550 pub fn set_position_persistent(
567 &mut self,
568 client: &mut CommandClient,
569 user_units: f64,
570 ) {
571 self.config.home_position = user_units;
578 let desired_counts = self.config.to_counts(user_units).round() as i32;
579 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
580 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
581 self.op_started = Some(Instant::now());
582 log::info!(
583 "set_position_persistent: writing 0x607C = {} counts ({} user units)",
584 desired_counts, user_units
585 );
586 }
587
588 pub fn set_home_position(&mut self, user_units: f64) {
592 self.config.home_position = user_units;
593 }
594
595 pub fn set_software_max_limit(&mut self, user_units: f64) {
597 self.config.max_position_limit = user_units;
598 self.config.enable_max_position_limit = true;
599 }
600
601 pub fn set_software_min_limit(&mut self, user_units: f64) {
603 self.config.min_position_limit = user_units;
604 self.config.enable_min_position_limit = true;
605 }
606
607 pub fn sdo_write(
613 &mut self,
614 client: &mut CommandClient,
615 index: u16,
616 sub_index: u8,
617 value: serde_json::Value,
618 ) {
619 self.sdo.write(client, index, sub_index, value);
620 }
621
622 pub fn sdo_read(
624 &mut self,
625 client: &mut CommandClient,
626 index: u16,
627 sub_index: u8,
628 ) -> u32 {
629 self.sdo.read(client, index, sub_index)
630 }
631
632 pub fn sdo_result(
634 &mut self,
635 client: &mut CommandClient,
636 tid: u32,
637 ) -> SdoResult {
638 self.sdo.result(client, tid, Duration::from_secs(5))
639 }
640
641 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
655 self.check_faults(view);
656 self.progress_op(view, client);
657 self.update_outputs(view);
658 self.check_limits(view);
659 }
660
661 fn update_outputs(&mut self, view: &impl AxisView) {
666 let raw = view.position_actual();
667 self.raw_position = raw as i64;
668 self.position = self.config.to_user((raw - self.home_offset) as f64);
669
670 let vel = view.velocity_actual();
671 let user_vel = self.config.to_user(vel as f64);
672 self.speed = user_vel.abs();
673 self.moving_positive = user_vel > 0.0;
674 self.moving_negative = user_vel < 0.0;
675 self.is_busy = self.op != AxisOp::Idle;
676 self.in_motion = matches!(self.op, AxisOp::Moving(_, _, _, _) | AxisOp::SoftHoming(_));
677
678 let sw = RawStatusWord(view.status_word());
679 self.motor_on = sw.state() == Cia402State::OperationEnabled;
680
681 self.last_raw_position = raw;
682 }
683
684 fn check_faults(&mut self, view: &impl AxisView) {
689 let sw = RawStatusWord(view.status_word());
690 let state = sw.state();
691
692 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
693 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
694 self.is_error = true;
695 let ec = view.error_code();
696 if ec != 0 {
697 self.error_code = ec as u32;
698 }
699 self.error_message = format!("Drive fault (state: {})", state);
700 self.op = AxisOp::Idle;
702 self.op_started = None;
703 }
704 }
705 }
706
707 fn op_timed_out(&self) -> bool {
712 self.op_started
713 .map_or(false, |t| t.elapsed() > self.op_timeout)
714 }
715
716 fn homing_timed_out(&self) -> bool {
717 self.op_started
718 .map_or(false, |t| t.elapsed() > self.homing_timeout)
719 }
720
721 fn move_start_timed_out(&self) -> bool {
722 self.op_started
723 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
724 }
725
726 fn op_stage_timed_out(&self, limit: Duration) -> bool {
730 self.op_started
731 .map_or(false, |t| t.elapsed() > limit)
732 }
733
734 fn set_op_error(&mut self, msg: &str) {
735 self.is_error = true;
736 self.error_message = msg.to_string();
737 self.op = AxisOp::Idle;
738 self.op_started = None;
739 self.is_busy = false;
740 self.in_motion = false;
741 log::error!("Axis error: {}", msg);
742 }
743
744 fn restore_pp_after_error(&mut self, msg: &str) {
745 self.is_error = true;
746 self.error_message = msg.to_string();
747 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);;
748 log::error!("Axis error: {}", msg);
749 }
750
751 fn finish_op_error(&mut self) {
752 self.op = AxisOp::Idle;
753 self.op_started = None;
754 self.is_busy = false;
755 self.in_motion = false;
756 }
757
758 fn complete_op(&mut self) {
759 self.op = AxisOp::Idle;
760 self.op_started = None;
761 self.is_busy = false;
766 self.in_motion = false;
767 }
768
769 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
778 let static_limit = if self.config.enable_max_position_limit {
779 Some(self.config.max_position_limit)
780 } else {
781 None
782 };
783 match (static_limit, view.dynamic_max_position_limit()) {
784 (Some(s), Some(d)) => Some(s.min(d)),
785 (Some(v), None) | (None, Some(v)) => Some(v),
786 (None, None) => None,
787 }
788 }
789
790 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
794 let static_limit = if self.config.enable_min_position_limit {
795 Some(self.config.min_position_limit)
796 } else {
797 None
798 };
799 match (static_limit, view.dynamic_min_position_limit()) {
800 (Some(s), Some(d)) => Some(s.max(d)),
801 (Some(v), None) | (None, Some(v)) => Some(v),
802 (None, None) => None,
803 }
804 }
805
806 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
811 if let Some(max) = self.effective_max_limit(view) {
812 if target > max {
813 return Some(format!(
814 "Target {:.3} exceeds max software limit {:.3}",
815 target, max
816 ));
817 }
818 }
819 if let Some(min) = self.effective_min_limit(view) {
820 if target < min {
821 return Some(format!(
822 "Target {:.3} exceeds min software limit {:.3}",
823 target, min
824 ));
825 }
826 }
827 None
828 }
829
830 fn check_limits(&mut self, view: &mut impl AxisView) {
839 let eff_max = self.effective_max_limit(view);
841 let eff_min = self.effective_min_limit(view);
842 let sw_max = eff_max.map_or(false, |m| self.position >= m);
843 let sw_min = eff_min.map_or(false, |m| self.position <= m);
844
845 self.at_max_limit = sw_max;
846 self.at_min_limit = sw_min;
847
848 let hw_pos = view.positive_limit_active();
850 let hw_neg = view.negative_limit_active();
851
852 self.at_positive_limit_switch = hw_pos;
853 self.at_negative_limit_switch = hw_neg;
854
855 self.home_sensor = view.home_sensor_active();
857
858 self.prev_positive_limit = hw_pos;
860 self.prev_negative_limit = hw_neg;
861 self.prev_home_sensor = view.home_sensor_active();
862
863 let mut commanded_positive = false;
865 let mut commanded_negative = false;
866
867 let is_moving = matches!(self.op, AxisOp::Moving(_, _, _, _));
868 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
869
870 if !is_moving && !is_soft_homing {
871 return; }
873
874 match &self.op {
875 AxisOp::Moving(_, _, pos, neg) => {
876 commanded_positive = *pos;
878 commanded_negative = *neg;
879 }
880 AxisOp::SoftHoming(_) => {
881 match self.soft_home_sensor {
882 SoftHomeSensor::PositiveLimit => commanded_positive = true,
883 SoftHomeSensor::NegativeLimit => commanded_negative = true,
884 SoftHomeSensor::HomeSensor => {
885 commanded_positive = self.moving_positive;
886 commanded_negative = self.moving_negative;
887 }
888 }
889 }
890 _ => {}
891 }
892
893 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
895 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
896
897 let effective_hw_pos = hw_pos && !suppress_pos;
898 let effective_hw_neg = hw_neg && !suppress_neg;
899
900 let effective_sw_max = sw_max && !is_soft_homing;
902 let effective_sw_min = sw_min && !is_soft_homing;
903
904 let positive_blocked = (effective_sw_max || effective_hw_pos) && commanded_positive;
905 let negative_blocked = (effective_sw_min || effective_hw_neg) && commanded_negative;
906
907
908 if positive_blocked || negative_blocked {
909 let mut cw = RawControlWord(view.control_word());
910 cw.set_bit(8, true); view.set_control_word(cw.raw());
912
913 let msg = if effective_hw_pos && commanded_positive {
914 "Positive limit switch active".to_string()
915 } else if effective_hw_neg && commanded_negative {
916 "Negative limit switch active".to_string()
917 } else if effective_sw_max && commanded_positive {
918 format!(
919 "Software position limit: position {:.3} >= max {:.3}",
920 self.position, eff_max.unwrap_or(self.position)
921 )
922 } else {
923 format!(
924 "Software position limit: position {:.3} <= min {:.3}",
925 self.position, eff_min.unwrap_or(self.position)
926 )
927 };
928
929 if is_soft_homing {
930 self.set_op_error(&msg);
933 } else {
934 log::info!("Axis move halted by limit: {}", msg);
947 self.command_halt(view);
948 self.halt_stable_count = 0;
949 self.last_raw_position = view.position_actual();
950 self.op_started = Some(Instant::now());
951 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
952 }
953 }
954 }
955
956 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
961 match self.op.clone() {
962 AxisOp::Idle => {}
963 AxisOp::Enabling(step) => self.tick_enabling(view, step),
964 AxisOp::Disabling(step) => self.tick_disabling(view, step),
965 AxisOp::Moving(kind, step, pos, neg) => self.tick_moving(view, kind, step, pos, neg),
966 AxisOp::Homing(step) => self.tick_homing(view, client, step),
967 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
968 AxisOp::Halting(step) => self.tick_halting(view, step),
969 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
970 }
971 }
972
973 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
978 match step {
979 1 => {
980 let sw = RawStatusWord(view.status_word());
981 if sw.state() == Cia402State::ReadyToSwitchOn {
982 let mut cw = RawControlWord(view.control_word());
983 cw.cmd_enable_operation();
984 view.set_control_word(cw.raw());
985 self.op = AxisOp::Enabling(2);
986 } else if self.op_timed_out() {
987 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
988 }
989 }
990 2 => {
991 let sw = RawStatusWord(view.status_word());
992 if sw.state() == Cia402State::OperationEnabled {
993 self.complete_op();
1000 } else if self.op_timed_out() {
1001 self.set_op_error("Enable timeout: waiting for OperationEnabled");
1002 }
1003 }
1004 _ => self.complete_op(),
1005 }
1006 }
1007
1008 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
1012 match step {
1013 1 => {
1014 let sw = RawStatusWord(view.status_word());
1015 if sw.state() != Cia402State::OperationEnabled {
1016 self.complete_op();
1017 } else if self.op_timed_out() {
1018 self.set_op_error("Disable timeout: drive still in OperationEnabled");
1019 }
1020 }
1021 _ => self.complete_op(),
1022 }
1023 }
1024
1025 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8, pos: bool, neg: bool) {
1031 match step {
1032 1 => {
1033 let sw = RawStatusWord(view.status_word());
1035 if sw.raw() & (1 << 12) != 0 {
1036 let mut cw = RawControlWord(view.control_word());
1038 cw.set_bit(4, false);
1039 view.set_control_word(cw.raw());
1040 self.op = AxisOp::Moving(kind, 2, pos, neg);
1041 } else if self.move_start_timed_out() {
1042 self.set_op_error("Move timeout: set-point not acknowledged");
1043 }
1044 },
1045 2 => {
1046 let sw = RawStatusWord(view.status_word());
1048 if sw.raw() & (1 << 12) == 0 {
1049 self.op = AxisOp::Moving(kind, 3, pos, neg);
1051 }
1052 },
1053 3 => {
1054 let sw = RawStatusWord(view.status_word());
1056 if sw.target_reached() {
1057 self.complete_op();
1058 }
1059 },
1060 _ => self.complete_op(),
1061 }
1062 }
1063
1064 fn tick_homing(
1090 &mut self,
1091 view: &mut impl AxisView,
1092 client: &mut CommandClient,
1093 step: u8,
1094 ) {
1095 match step {
1096 0 => {
1097 self.homing_sdo_tid = self.sdo.write(
1099 client,
1100 0x6098,
1101 0,
1102 json!(self.homing_method),
1103 );
1104 self.op = AxisOp::Homing(1);
1105 }
1106 1 => {
1107 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1109 SdoResult::Ok(_) => {
1110 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
1112 self.op = AxisOp::Homing(8);
1113 } else {
1114 self.op = AxisOp::Homing(2);
1115 }
1116 }
1117 SdoResult::Pending => {
1118 if self.homing_timed_out() {
1119 self.set_op_error("Homing timeout: SDO write for homing method");
1120 }
1121 }
1122 SdoResult::Err(e) => {
1123 self.set_op_error(&format!("Homing SDO error: {}", e));
1124 }
1125 SdoResult::Timeout => {
1126 self.set_op_error("Homing timeout: SDO write timed out");
1127 }
1128 }
1129 }
1130 2 => {
1131 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1133 self.homing_sdo_tid = self.sdo.write(
1134 client,
1135 0x6099,
1136 1,
1137 json!(speed_counts),
1138 );
1139 self.op = AxisOp::Homing(3);
1140 }
1141 3 => {
1142 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1143 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
1144 SdoResult::Pending => {
1145 if self.homing_timed_out() {
1146 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
1147 }
1148 }
1149 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1150 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1151 }
1152 }
1153 4 => {
1154 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1156 self.homing_sdo_tid = self.sdo.write(
1157 client,
1158 0x6099,
1159 2,
1160 json!(speed_counts),
1161 );
1162 self.op = AxisOp::Homing(5);
1163 }
1164 5 => {
1165 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1166 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
1167 SdoResult::Pending => {
1168 if self.homing_timed_out() {
1169 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1170 }
1171 }
1172 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1173 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1174 }
1175 }
1176 6 => {
1177 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1179 self.homing_sdo_tid = self.sdo.write(
1180 client,
1181 0x609A,
1182 0,
1183 json!(accel_counts),
1184 );
1185 self.op = AxisOp::Homing(7);
1186 }
1187 7 => {
1188 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1189 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
1190 SdoResult::Pending => {
1191 if self.homing_timed_out() {
1192 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1193 }
1194 }
1195 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1196 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1197 }
1198 }
1199 8 => {
1200 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1210 let mut cw = RawControlWord(view.control_word());
1212 cw.set_bit(4, false);
1213 view.set_control_word(cw.raw());
1214 self.fb_mode_of_operation.reset();
1215 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1216 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1217 self.op = AxisOp::Homing(9);
1218 }
1219 9 => {
1220 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1222 if !self.fb_mode_of_operation.is_busy() {
1223 if self.fb_mode_of_operation.is_error() {
1224 self.set_op_error(&format!(
1225 "Homing: failed to enter Homing mode: {} {}",
1226 self.fb_mode_of_operation.error_code(),
1227 self.fb_mode_of_operation.error_message()
1228 ));
1229 } else {
1230 self.op = AxisOp::Homing(10);
1231 }
1232 }
1233 }
1234 10 => {
1235 let mut cw = RawControlWord(view.control_word());
1237 cw.set_bit(4, true);
1238 view.set_control_word(cw.raw());
1239 self.op = AxisOp::Homing(11);
1240 }
1241 11 => {
1242 let sw = view.status_word();
1247 let error = sw & (1 << 13) != 0;
1248 if error {
1249 self.set_op_error("Homing error: drive reported homing failure");
1250 } else if sw & (1 << 12) == 0 {
1251 self.op = AxisOp::Homing(12);
1252 } else if self.homing_timed_out() {
1253 self.set_op_error(&format!("Homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1254 }
1255 }
1256 12 => {
1257 let sw = view.status_word();
1260 let error = sw & (1 << 13) != 0;
1261 let attained = sw & (1 << 12) != 0;
1262 let reached = sw & (1 << 10) != 0;
1263
1264 if error {
1265 self.set_op_error("Homing error: drive reported homing failure");
1266 } else if attained && reached {
1267 self.op = AxisOp::Homing(13);
1268 } else if self.homing_timed_out() {
1269 self.set_op_error("Homing timeout: procedure did not complete");
1270 }
1271 }
1272 13 => {
1273 self.home_offset = view.position_actual()
1276 - self.config.to_counts(self.config.home_position).round() as i32;
1277 let mut cw = RawControlWord(view.control_word());
1279 cw.set_bit(4, false);
1280 view.set_control_word(cw.raw());
1281 self.op = AxisOp::Homing(14);
1282 }
1283 14 => {
1284 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1290 self.fb_mode_of_operation.reset();
1291 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1292 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1293 self.op = AxisOp::Homing(15);
1294 }
1295 15 => {
1296 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1298 if !self.fb_mode_of_operation.is_busy() {
1299 if self.fb_mode_of_operation.is_error() {
1300 self.set_op_error(&format!(
1301 "Homing: failed to restore PP mode: {} {}",
1302 self.fb_mode_of_operation.error_code(),
1303 self.fb_mode_of_operation.error_message()
1304 ));
1305 } else {
1306 log::info!("Homing complete — home offset: {}", self.home_offset);
1307 self.complete_op();
1308 }
1309 }
1310 }
1311 _ => self.complete_op(),
1312 }
1313 }
1314
1315 fn configure_soft_homing(&mut self, method: HomingMethod) {
1318 match method {
1319 HomingMethod::LimitSwitchPosPnp => {
1320 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1321 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1322 self.soft_home_direction = 1.0;
1323 }
1324 HomingMethod::LimitSwitchNegPnp => {
1325 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1326 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1327 self.soft_home_direction = -1.0;
1328 }
1329 HomingMethod::LimitSwitchPosNpn => {
1330 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1331 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1332 self.soft_home_direction = 1.0;
1333 }
1334 HomingMethod::LimitSwitchNegNpn => {
1335 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1336 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1337 self.soft_home_direction = -1.0;
1338 }
1339 HomingMethod::HomeSensorPosPnp => {
1340 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1341 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1342 self.soft_home_direction = 1.0;
1343 }
1344 HomingMethod::HomeSensorNegPnp => {
1345 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1346 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1347 self.soft_home_direction = -1.0;
1348 }
1349 HomingMethod::HomeSensorPosNpn => {
1350 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1351 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1352 self.soft_home_direction = 1.0;
1353 }
1354 HomingMethod::HomeSensorNegNpn => {
1355 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1356 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1357 self.soft_home_direction = -1.0;
1358 }
1359 _ => {} }
1361 }
1362
1363 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1364 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1365 self.op_started = Some(Instant::now());
1366 }
1367
1368 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1369 let raw = match self.soft_home_sensor {
1370 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1371 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1372 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1373 };
1374 match self.soft_home_sensor_type {
1375 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1378 }
1379
1380
1381 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1384 let dir = if !self.config.invert_direction {
1385 direction
1386 }
1387 else {
1388 -direction
1389 };
1390
1391 let target = if dir > 0.0 {
1392 i32::MAX
1393 }
1394 else {
1395 i32::MIN
1396 };
1397
1398 return target;
1399 }
1400
1401
1402 pub fn command_halt(&self, view: &mut impl AxisView) {
1407 let mut cw = RawControlWord(view.control_word());
1408 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());
1413 }
1414
1415 pub fn command_clear_halt(&self, view: &mut impl AxisView) {
1418 let mut cw = RawControlWord(view.control_word());
1419 cw.set_bit(8, false); view.set_control_word(cw.raw());
1421 }
1422
1423
1424 pub fn command_cancel_move(&self, view: &mut impl AxisView) {
1432
1433 let mut cw = RawControlWord(view.control_word());
1434
1435 cw.set_bit(4, true); cw.set_bit(5, true); cw.set_bit(6, false); if self.config().clear_halt_during_cancel {
1440 cw.set_bit(8, false); }
1443
1444 view.set_control_word(cw.raw());
1445
1446 let current_pos = view.position_actual();
1447 view.set_target_position(current_pos);
1448 view.set_profile_velocity(0);
1449 }
1450
1451
1452 fn command_homing_speed(&self, view: &mut impl AxisView) {
1454 let cpu = self.config.counts_per_user();
1455 let vel = (self.config.homing_speed * cpu).round() as u32;
1456 let accel = (self.config.homing_accel * cpu).round() as u32;
1457 let decel = (self.config.homing_decel * cpu).round() as u32;
1458 view.set_profile_velocity(vel);
1459 view.set_profile_acceleration(accel);
1460 view.set_profile_deceleration(decel);
1461 }
1462
1463 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1479 match HomeState::from_repr(step) {
1480
1481 Some(HomeState::EnsurePpMode) => {
1482 log::info!("SoftHome: Ensuring PP mode..");
1487 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1488 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1489 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1490 },
1491 Some(HomeState::WaitPpMode) => {
1492
1493 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1494 if !self.fb_mode_of_operation.is_busy() {
1495 if self.fb_mode_of_operation.is_error() {
1496 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1497 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1498 ));
1499 }
1500 else {
1501 log::info!("SoftHome: Drive is in PP mode!");
1502
1503 if !self.check_soft_home_trigger(view) {
1507 log::info!("SoftHome: Not on home switch; seek out.");
1508 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1509 } else {
1510 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1511 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1512 }
1513 }
1514 }
1515
1516
1517 },
1518
1519 Some(HomeState::Search) => {
1521 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1522
1523 let target = self.calculate_max_relative_target(self.soft_home_direction);
1533 view.set_target_position(target);
1534
1535 self.command_homing_speed(view);
1544
1545 let mut cw = RawControlWord(view.control_word());
1546 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());
1551
1552 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1553 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1554 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1555 }
1556 Some(HomeState::WaitSearching) => {
1557 if self.check_soft_home_trigger(view) {
1558 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1559 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1560 return;
1561 }
1562 let sw = RawStatusWord(view.status_word());
1563 if sw.raw() & (1 << 12) != 0 {
1564 let mut cw = RawControlWord(view.control_word());
1565 cw.set_bit(4, false);
1566 view.set_control_word(cw.raw());
1567 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1568 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1569 } else if self.homing_timed_out() {
1570 self.set_op_error("Software homing timeout: set-point not acknowledged");
1571 }
1572 }
1573 Some(HomeState::WaitFoundSensor) => {
1583 if self.check_soft_home_trigger(view) {
1584 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1585 log::info!("ControlWord is : {} ", view.control_word());
1586
1587 let mut cw = RawControlWord(view.control_word());
1588 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1591
1592
1593 self.halt_stable_count = 0;
1594 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1595 } else if self.homing_timed_out() {
1596 self.set_op_error("Software homing timeout: sensor not detected");
1597 }
1598 }
1599
1600
1601 Some(HomeState::WaitStoppedFoundSensor) => {
1602 const STABLE_WINDOW: i32 = 1;
1603 const STABLE_TICKS_REQUIRED: u8 = 10;
1604
1605 let pos = view.position_actual();
1610 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1611 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1612 } else {
1613 self.halt_stable_count = 0;
1614 }
1615
1616 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1617
1618 log::info!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1619 self.command_cancel_move(view);
1620 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1621
1622 } else if self.homing_timed_out() {
1623 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1624 }
1625 }
1626 Some(HomeState::WaitFoundSensorAck) => {
1627 let sw = RawStatusWord(view.status_word());
1628 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1629
1630 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1631
1632 let mut cw = RawControlWord(view.control_word());
1634 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1637
1638 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1639
1640 } else if self.homing_timed_out() {
1641 self.set_op_error("Software homing timeout: cancel not acknowledged");
1642 }
1643 },
1644 Some(HomeState::WaitFoundSensorAckClear) => {
1645 let sw = RawStatusWord(view.status_word());
1646 if sw.raw() & (1 << 12) == 0 {
1648
1649 self.command_clear_halt(view);
1651
1652 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1653 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1654 self.ton.call(false, Duration::from_secs(3));
1655 }
1656 },
1657 Some(HomeState::DebounceFoundSensor) => {
1659 self.ton.call(true, Duration::from_secs(3));
1660
1661 let sw = RawStatusWord(view.status_word());
1662 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1663 self.ton.call(false, Duration::from_secs(3));
1664 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1665 view.position_actual(), view.control_word(), view.status_word());
1666 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1667 }
1668 }
1669
1670 Some(HomeState::BackOff) => {
1672
1673 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1674 view.set_target_position(target);
1675
1676
1677 self.command_homing_speed(view);
1678
1679 let mut cw = RawControlWord(view.control_word());
1680 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1684 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1685 target, self.config.homing_speed, view.position_actual(), cw.raw());
1686 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1687 }
1688 Some(HomeState::WaitBackingOff) => {
1689 let sw = RawStatusWord(view.status_word());
1690 if sw.raw() & (1 << 12) != 0 {
1691 let mut cw = RawControlWord(view.control_word());
1692 cw.set_bit(4, false);
1693 view.set_control_word(cw.raw());
1694 log::info!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1695 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1696 } else if self.homing_timed_out() {
1697 self.set_op_error("Software homing timeout: back-off not acknowledged");
1698 }
1699 }
1700 Some(HomeState::WaitLostSensor) => {
1701 if !self.check_soft_home_trigger(view) {
1702 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1703
1704 self.command_halt(view);
1705 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1706 } else if self.homing_timed_out() {
1707 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1708 }
1709 }
1710 Some(HomeState::WaitStoppedLostSensor) => {
1711 const STABLE_WINDOW: i32 = 1;
1712 const STABLE_TICKS_REQUIRED: u8 = 10;
1713
1714 let pos = view.position_actual();
1719 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1720 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1721 } else {
1722 self.halt_stable_count = 0;
1723 }
1724
1725 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1726 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1727 self.command_cancel_move(view);
1728 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1729 } else if self.homing_timed_out() {
1730 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1731 }
1732 }
1733 Some(HomeState::WaitLostSensorAck) => {
1734 let sw = RawStatusWord(view.status_word());
1735 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1736
1737 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1738
1739 let mut cw = RawControlWord(view.control_word());
1741 cw.set_bit(4, false); view.set_control_word(cw.raw());
1743
1744 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1745
1746
1747 } else if self.homing_timed_out() {
1748 self.set_op_error("Software homing timeout: cancel not acknowledged");
1749 }
1750 }
1751 Some(HomeState::WaitLostSensorAckClear) => {
1752 let sw = RawStatusWord(view.status_word());
1754 if sw.raw() & (1 << 12) == 0 {
1755
1756 self.command_clear_halt(view);
1757
1758 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1759 self.homing_sdo_tid = self.sdo.write(
1762 client, 0x607C, 0, json!(desired_counts),
1763 );
1764
1765 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1766 self.config.home_position, desired_counts
1767 );
1768
1769 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1770
1771 }
1772 },
1773
1774 Some(HomeState::WaitHomeOffsetDone) => {
1775 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1777 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1778 SdoResult::Pending => {
1779 if self.homing_timed_out() {
1780 self.set_op_error("Software homing timeout: home offset SDO write");
1781 }
1782 }
1783 SdoResult::Err(e) => {
1784 self.set_op_error(&format!("Software homing SDO error: {}", e));
1785 }
1786 SdoResult::Timeout => {
1787 self.set_op_error("Software homing: home offset SDO timed out");
1788 }
1789 }
1790 },
1791 Some(HomeState::WriteHomingModeOp) => {
1792
1793 self.fb_mode_of_operation.reset();
1797 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1798 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1799 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1800
1801
1802 },
1803 Some(HomeState::WaitWriteHomingModeOp) => {
1804 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1806
1807 if !self.fb_mode_of_operation.is_busy() {
1808 if self.fb_mode_of_operation.is_error() {
1809 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1810 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1811 ));
1812 }
1813 else {
1814 log::info!("SoftHome: Drive is now in Homing Mode.");
1815 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1816 }
1817 }
1818 },
1819 Some(HomeState::WriteHomingMethod) => {
1820 self.homing_sdo_tid = self.sdo.write(
1823 client, 0x6098, 0, json!(self.config.soft_home_method),
1824 );
1825 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1826 }
1827 Some(HomeState::WaitWriteHomingMethodDone) => {
1828 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1830 SdoResult::Ok(_) => {
1831 log::info!("SoftHome: Successfully wrote homing method.");
1832 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1833 }
1834 SdoResult::Pending => {
1835 if self.homing_timed_out() {
1836 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1837 }
1838 }
1839 SdoResult::Err(e) => {
1840 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1841 }
1842 SdoResult::Timeout => {
1843 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1844 }
1845 }
1846 }
1847 Some(HomeState::ClearHomingTrigger) => {
1848 let mut cw = RawControlWord(view.control_word());
1851 cw.set_bit(4, false);
1852 view.set_control_word(cw.raw());
1853 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1854 }
1855 Some(HomeState::TriggerHoming) => {
1856 let mut cw = RawControlWord(view.control_word());
1858 cw.set_bit(4, true);
1859 view.set_control_word(cw.raw());
1860 log::info!("SoftHome[TriggerHoming]: start homing");
1861 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1862 }
1863 Some(HomeState::WaitHomingStarted) => {
1864 let sw = view.status_word();
1870 let error = sw & (1 << 13) != 0;
1871 if error {
1872 self.restore_pp_after_error("Software homing: drive reported homing error");
1873 } else if sw & (1 << 12) == 0 {
1874 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1875 } else if self.homing_timed_out() {
1876 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1877 }
1878 }
1879 Some(HomeState::WaitHomingDone) => {
1880 let sw = view.status_word();
1882 let error = sw & (1 << 13) != 0;
1883 let attained = sw & (1 << 12) != 0;
1884 let reached = sw & (1 << 10) != 0;
1885
1886 if error {
1887 self.restore_pp_after_error("Software homing: drive reported homing error");
1888 } else if attained && reached {
1889 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
1890 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
1891 } else if self.homing_timed_out() {
1892 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
1893 }
1894 }
1895 Some(HomeState::ResetHomingTrigger) => {
1896 let mut cw = RawControlWord(view.control_word());
1901 cw.set_bit(4, false);
1902 view.set_control_word(cw.raw());
1903 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
1904 }
1905 Some(HomeState::WaitHomingTriggerCleared) => {
1906 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
1910 }
1911
1912
1913 Some(HomeState::WriteMotionModeOfOperation) => {
1914
1915 self.fb_mode_of_operation.reset();
1918 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1919 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1920 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
1921
1922 },
1923 Some(HomeState::WaitWriteMotionModeOfOperation) => {
1924 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1926
1927 if !self.fb_mode_of_operation.is_busy() {
1928 if self.fb_mode_of_operation.is_error() {
1929 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1930 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1931 ));
1932 }
1933 else {
1934 if self.is_error {
1935 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
1936 self.finish_op_error();
1937 }
1938 else {
1939 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1942 }
1943
1944 }
1945 }
1946 },
1947
1948 Some(HomeState::SendCurrentPositionTarget) => {
1949 let current_pos = view.position_actual();
1951 view.set_target_position(current_pos);
1952 view.set_profile_velocity(0);
1953 let mut cw = RawControlWord(view.control_word());
1954 cw.set_bit(4, true);
1955 cw.set_bit(5, true);
1956 cw.set_bit(6, false); view.set_control_word(cw.raw());
1958 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1959 }
1960 Some(HomeState::WaitCurrentPositionTargetSent) => {
1961 let sw = RawStatusWord(view.status_word());
1963 if sw.raw() & (1 << 12) != 0 {
1964 let mut cw = RawControlWord(view.control_word());
1965 cw.set_bit(4, false);
1966 view.set_control_word(cw.raw());
1967 log::info!("Software homing complete — position set to {} user units",
1968 self.config.home_position);
1969 self.complete_op();
1970 } else if self.homing_timed_out() {
1971 self.set_op_error("Software homing timeout: hold position not acknowledged");
1972 }
1973 }
1974 _ => self.complete_op(),
1975 }
1976 }
1977
1978 fn tick_halting(&mut self, view: &mut impl AxisView, step: u8) {
1991 match HaltState::from_repr(step) {
1992 Some(HaltState::WaitStopped) => {
1993 let pos = view.position_actual();
1997 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
1998
1999 let vel = view.velocity_actual().abs();
2000 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
2001
2002 if pos_stable || vel_stopped {
2007 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2008 } else {
2009 self.halt_stable_count = 0;
2010 }
2011
2012 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
2013 self.command_cancel_move(view);
2014 self.op_started = Some(Instant::now());
2015 self.op = AxisOp::Halting(HaltState::WaitCancelAck as u8);
2016 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2017 self.set_op_error("Halt timeout: motor did not stop");
2018 }
2019 }
2020 Some(HaltState::WaitCancelAck) => {
2021 let sw = RawStatusWord(view.status_word());
2022 let setpoint_ack = sw.raw() & (1 << 12) != 0;
2023 if setpoint_ack {
2025 let mut cw = RawControlWord(view.control_word());
2028 cw.set_bit(4, false);
2029 cw.set_bit(5, true);
2030 view.set_control_word(cw.raw());
2031 self.op_started = Some(Instant::now());
2032 self.op = AxisOp::Halting(HaltState::WaitCancelAckClear as u8);
2033 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2034 self.set_op_error("Halt timeout: cancel not acknowledged");
2035 }
2036 }
2037 Some(HaltState::WaitCancelAckClear) => {
2038 let sw = RawStatusWord(view.status_word());
2039 if sw.raw() & (1 << 12) == 0 {
2040 self.command_clear_halt(view);
2042 self.complete_op();
2043 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2044 self.set_op_error("Halt timeout: ack did not clear");
2045 }
2046 }
2047 None => {
2048 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
2049 self.complete_op();
2050 }
2051 }
2052 }
2053
2054 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
2059 match step {
2060 1 => {
2061 let mut cw = RawControlWord(view.control_word());
2063 cw.cmd_fault_reset();
2064 view.set_control_word(cw.raw());
2065 self.op = AxisOp::FaultRecovery(2);
2066 }
2067 2 => {
2068 let sw = RawStatusWord(view.status_word());
2070 let state = sw.state();
2071 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
2072 log::info!("Fault cleared (drive state: {})", state);
2073 self.complete_op();
2074 } else if self.op_timed_out() {
2075 self.set_op_error("Fault reset timeout: drive still faulted");
2076 }
2077 }
2078 _ => self.complete_op(),
2079 }
2080 }
2081}
2082
2083#[cfg(test)]
2088mod tests {
2089 use super::*;
2090
2091 struct MockView {
2093 control_word: u16,
2094 status_word: u16,
2095 target_position: i32,
2096 profile_velocity: u32,
2097 profile_acceleration: u32,
2098 profile_deceleration: u32,
2099 modes_of_operation: i8,
2100 modes_of_operation_display: i8,
2101 position_actual: i32,
2102 velocity_actual: i32,
2103 error_code: u16,
2104 positive_limit: bool,
2105 negative_limit: bool,
2106 home_sensor: bool,
2107 }
2108
2109 impl MockView {
2110 fn new() -> Self {
2111 Self {
2112 control_word: 0,
2113 status_word: 0x0040, target_position: 0,
2115 profile_velocity: 0,
2116 profile_acceleration: 0,
2117 profile_deceleration: 0,
2118 modes_of_operation: 0,
2119 modes_of_operation_display: 1, position_actual: 0,
2121 velocity_actual: 0,
2122 error_code: 0,
2123 positive_limit: false,
2124 negative_limit: false,
2125 home_sensor: false,
2126 }
2127 }
2128
2129 fn set_state(&mut self, state: u16) {
2130 self.status_word = state;
2131 }
2132 }
2133
2134 impl AxisView for MockView {
2135 fn control_word(&self) -> u16 { self.control_word }
2136 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
2137 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
2138 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
2139 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
2140 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
2141 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
2142 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
2143 fn status_word(&self) -> u16 { self.status_word }
2144 fn position_actual(&self) -> i32 { self.position_actual }
2145 fn velocity_actual(&self) -> i32 { self.velocity_actual }
2146 fn error_code(&self) -> u16 { self.error_code }
2147 fn positive_limit_active(&self) -> bool { self.positive_limit }
2148 fn negative_limit_active(&self) -> bool { self.negative_limit }
2149 fn home_sensor_active(&self) -> bool { self.home_sensor }
2150 }
2151
2152 fn test_config() -> AxisConfig {
2153 AxisConfig::new(12_800).with_user_scale(360.0)
2154 }
2155
2156 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2158 use tokio::sync::mpsc;
2159 let (write_tx, write_rx) = mpsc::unbounded_channel();
2160 let (response_tx, response_rx) = mpsc::unbounded_channel();
2161 let client = CommandClient::new(write_tx, response_rx);
2162 let axis = Axis::new(test_config(), "TestDrive");
2163 (axis, client, response_tx, write_rx)
2164 }
2165
2166 #[test]
2167 fn axis_config_conversion() {
2168 let cfg = test_config();
2169 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2171 }
2172
2173 #[test]
2174 fn enable_sequence_sets_pp_mode_and_shutdown() {
2175 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2176 let mut view = MockView::new();
2177
2178 axis.enable(&mut view);
2179
2180 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
2182 assert_eq!(view.control_word & 0x008F, 0x0006);
2184 assert_eq!(axis.op, AxisOp::Enabling(1));
2186
2187 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2190
2191 assert_eq!(view.control_word & 0x008F, 0x000F);
2193 assert_eq!(axis.op, AxisOp::Enabling(2));
2194
2195 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2198
2199 assert_eq!(axis.op, AxisOp::Idle);
2201 assert!(axis.motor_on);
2202 }
2203
2204 #[test]
2205 fn move_absolute_sets_target() {
2206 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2207 let mut view = MockView::new();
2208 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2213
2214 assert_eq!(view.target_position, 1600);
2216 assert_eq!(view.profile_velocity, 3200);
2218 assert_eq!(view.profile_acceleration, 6400);
2220 assert_eq!(view.profile_deceleration, 6400);
2221 assert!(view.control_word & (1 << 4) != 0);
2223 assert!(view.control_word & (1 << 6) == 0);
2225 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2227 }
2228
2229 #[test]
2230 fn move_relative_sets_relative_bit() {
2231 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2232 let mut view = MockView::new();
2233 view.set_state(0x0027);
2234 axis.tick(&mut view, &mut client);
2235
2236 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2237
2238 assert!(view.control_word & (1 << 6) != 0);
2240 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1, _, _)));
2241 }
2242
2243 #[test]
2244 fn move_completes_on_target_reached() {
2245 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2246 let mut view = MockView::new();
2247 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2249
2250 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2251
2252 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2255 assert!(view.control_word & (1 << 4) == 0);
2257
2258 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2261 assert_eq!(axis.op, AxisOp::Idle);
2263 assert!(!axis.in_motion);
2264 }
2265
2266 #[test]
2267 fn fault_detected_sets_error() {
2268 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2269 let mut view = MockView::new();
2270 view.set_state(0x0008); view.error_code = 0x1234;
2272
2273 axis.tick(&mut view, &mut client);
2274
2275 assert!(axis.is_error);
2276 assert_eq!(axis.error_code, 0x1234);
2277 assert!(axis.error_message.contains("fault"));
2278 }
2279
2280 #[test]
2281 fn fault_recovery_sequence() {
2282 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2283 let mut view = MockView::new();
2284 view.set_state(0x0008); axis.reset_faults(&mut view);
2287 assert!(view.control_word & 0x0080 == 0);
2289
2290 axis.tick(&mut view, &mut client);
2292 assert!(view.control_word & 0x0080 != 0);
2293
2294 view.set_state(0x0040);
2296 axis.tick(&mut view, &mut client);
2297 assert_eq!(axis.op, AxisOp::Idle);
2298 assert!(!axis.is_error);
2299 }
2300
2301 #[test]
2302 fn reset_faults_software_only_is_synchronous() {
2303 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2309 let mut view = MockView::new();
2310 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2312
2313 axis.set_software_min_limit(207.0);
2315 axis.move_absolute(&mut view, 200.0, 90.0, 180.0, 180.0);
2316 assert!(axis.is_error, "move past software limit must set is_error");
2317 assert!(!axis.is_busy);
2318
2319 let cw_before = view.control_word;
2322
2323 axis.reset_faults(&mut view);
2324
2325 assert!(!axis.is_error);
2327 assert_eq!(axis.error_code, 0);
2328 assert!(axis.error_message.is_empty());
2329 assert!(!axis.is_busy);
2330 assert_eq!(axis.op, AxisOp::Idle);
2331 assert_eq!(view.control_word, cw_before);
2333 }
2334
2335 #[test]
2336 fn disable_sequence() {
2337 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2338 let mut view = MockView::new();
2339 view.set_state(0x0027); axis.disable(&mut view);
2342 assert_eq!(view.control_word & 0x008F, 0x0007);
2344
2345 view.set_state(0x0023); axis.tick(&mut view, &mut client);
2348 assert_eq!(axis.op, AxisOp::Idle);
2349 }
2350
2351 #[test]
2352 fn position_tracks_with_home_offset() {
2353 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2354 let mut view = MockView::new();
2355 view.set_state(0x0027);
2356 view.position_actual = 5000;
2357
2358 axis.enable(&mut view);
2360 view.set_state(0x0021);
2361 axis.tick(&mut view, &mut client);
2362 view.set_state(0x0027);
2363 axis.tick(&mut view, &mut client);
2364
2365 assert_eq!(axis.home_offset, 5000);
2367
2368 assert!((axis.position - 0.0).abs() < 0.01);
2370
2371 view.position_actual = 6600;
2373 axis.tick(&mut view, &mut client);
2374
2375 assert!((axis.position - 45.0).abs() < 0.1);
2377 }
2378
2379 #[test]
2380 fn set_position_adjusts_home_offset() {
2381 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2382 let mut view = MockView::new();
2383 view.position_actual = 3200;
2384
2385 axis.set_position(&view, 90.0);
2386 axis.tick(&mut view, &mut client);
2387
2388 assert_eq!(axis.home_offset, 0);
2390 assert!((axis.position - 90.0).abs() < 0.01);
2391 }
2392
2393 #[test]
2394 fn halt_runs_multi_stage_close_out() {
2395 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2396 let mut view = MockView::new();
2397 view.set_state(0x0027);
2398
2399 axis.halt(&mut view);
2400
2401 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2403 assert!(view.control_word & (1 << 4) == 0, "new_setpoint must be cleared");
2404
2405 assert!(matches!(axis.op, AxisOp::Halting(_)),
2407 "halt should enter Halting state, not Idle");
2408 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2409 assert_eq!(step, HaltState::WaitStopped as u8);
2410
2411 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2418 axis.tick(&mut view, &mut client);
2419 }
2420 assert!(matches!(axis.op, AxisOp::Halting(_)));
2422 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2423 assert_eq!(step, HaltState::WaitCancelAck as u8,
2424 "should advance past WaitStopped once position/velocity is stable");
2425
2426 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2430 }
2431
2432 #[test]
2433 fn is_busy_tracks_operations() {
2434 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2435 let mut view = MockView::new();
2436
2437 axis.tick(&mut view, &mut client);
2439 assert!(!axis.is_busy);
2440
2441 axis.enable(&mut view);
2443 axis.tick(&mut view, &mut client);
2444 assert!(axis.is_busy);
2445
2446 view.set_state(0x0021);
2448 axis.tick(&mut view, &mut client);
2449 view.set_state(0x0027);
2450 axis.tick(&mut view, &mut client);
2451 assert!(!axis.is_busy);
2452
2453 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2455 axis.tick(&mut view, &mut client);
2456 assert!(axis.is_busy);
2457 assert!(axis.in_motion);
2458 }
2459
2460 #[test]
2461 fn fault_during_move_cancels_op() {
2462 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2463 let mut view = MockView::new();
2464 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2466
2467 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2469 axis.tick(&mut view, &mut client);
2470 assert!(axis.is_busy);
2471 assert!(!axis.is_error);
2472
2473 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2476
2477 assert!(!axis.is_busy);
2479 assert!(axis.is_error);
2480 assert_eq!(axis.op, AxisOp::Idle);
2481 }
2482
2483 #[test]
2484 fn move_absolute_rejected_by_max_limit() {
2485 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2486 let mut view = MockView::new();
2487 view.set_state(0x0027);
2488 axis.tick(&mut view, &mut client);
2489
2490 axis.set_software_max_limit(90.0);
2491 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2492
2493 assert!(axis.is_error);
2495 assert_eq!(axis.op, AxisOp::Idle);
2496 assert!(axis.error_message.contains("max software limit"));
2497 }
2498
2499 #[test]
2500 fn move_absolute_rejected_by_min_limit() {
2501 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2502 let mut view = MockView::new();
2503 view.set_state(0x0027);
2504 axis.tick(&mut view, &mut client);
2505
2506 axis.set_software_min_limit(-10.0);
2507 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2508
2509 assert!(axis.is_error);
2510 assert_eq!(axis.op, AxisOp::Idle);
2511 assert!(axis.error_message.contains("min software limit"));
2512 }
2513
2514 #[test]
2515 fn move_relative_rejected_by_max_limit() {
2516 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2517 let mut view = MockView::new();
2518 view.set_state(0x0027);
2519 axis.tick(&mut view, &mut client);
2520
2521 axis.set_software_max_limit(50.0);
2523 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2524
2525 assert!(axis.is_error);
2526 assert_eq!(axis.op, AxisOp::Idle);
2527 assert!(axis.error_message.contains("max software limit"));
2528 }
2529
2530 #[test]
2531 fn move_within_limits_allowed() {
2532 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2533 let mut view = MockView::new();
2534 view.set_state(0x0027);
2535 axis.tick(&mut view, &mut client);
2536
2537 axis.set_software_max_limit(90.0);
2538 axis.set_software_min_limit(-90.0);
2539 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2540
2541 assert!(!axis.is_error);
2543 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2544 }
2545
2546 #[test]
2547 fn runtime_limit_halts_move_in_violated_direction() {
2548 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2549 let mut view = MockView::new();
2550 view.set_state(0x0027);
2551 axis.tick(&mut view, &mut client);
2552
2553 axis.set_software_max_limit(45.0);
2554 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2556
2557 view.position_actual = 1650;
2560 view.velocity_actual = 100; view.status_word = 0x1027;
2564 axis.tick(&mut view, &mut client);
2565 view.status_word = 0x0027;
2566 axis.tick(&mut view, &mut client);
2567
2568 assert!(!axis.is_error);
2572 assert!(axis.at_max_limit);
2573 assert!(axis.is_busy);
2574 assert!(matches!(axis.op, AxisOp::Halting(_)));
2575 assert!(view.control_word & (1 << 8) != 0);
2577 }
2578
2579 #[test]
2580 fn runtime_limit_allows_move_in_opposite_direction() {
2581 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2582 let mut view = MockView::new();
2583 view.set_state(0x0027);
2584 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2587 axis.tick(&mut view, &mut client);
2588 assert!(axis.at_max_limit);
2589
2590 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2592 assert!(!axis.is_error);
2593 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2594
2595 view.velocity_actual = -100;
2597 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2599 assert!(!axis.is_error);
2601 }
2602
2603 #[test]
2604 fn positive_limit_switch_halts_positive_move() {
2605 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2606 let mut view = MockView::new();
2607 view.set_state(0x0027);
2608 axis.tick(&mut view, &mut client);
2609
2610 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2612 view.velocity_actual = 100; view.status_word = 0x1027;
2615 axis.tick(&mut view, &mut client);
2616 view.status_word = 0x0027;
2617
2618 view.positive_limit = true;
2620 axis.tick(&mut view, &mut client);
2621
2622 assert!(!axis.is_error);
2627 assert!(axis.at_positive_limit_switch);
2628 assert!(axis.is_busy);
2629 assert!(matches!(axis.op, AxisOp::Halting(_)));
2630 assert!(view.control_word & (1 << 8) != 0);
2632 }
2633
2634 #[test]
2635 fn negative_limit_switch_halts_negative_move() {
2636 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2637 let mut view = MockView::new();
2638 view.set_state(0x0027);
2639 axis.tick(&mut view, &mut client);
2640
2641 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2643 view.velocity_actual = -100; view.status_word = 0x1027;
2645 axis.tick(&mut view, &mut client);
2646 view.status_word = 0x0027;
2647
2648 view.negative_limit = true;
2650 axis.tick(&mut view, &mut client);
2651
2652 assert!(!axis.is_error);
2657 assert!(axis.at_negative_limit_switch);
2658 assert!(axis.is_busy);
2659 assert!(matches!(axis.op, AxisOp::Halting(_)));
2660 assert!(view.control_word & (1 << 8) != 0);
2661 }
2662
2663 #[test]
2664 fn limit_halt_cleanup_then_next_move_starts_cleanly() {
2665 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2670 let mut view = MockView::new();
2671 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2673
2674 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2676 view.velocity_actual = 100;
2677 view.status_word = 0x1027;
2678 axis.tick(&mut view, &mut client);
2679 view.status_word = 0x0027;
2680 view.positive_limit = true;
2681 axis.tick(&mut view, &mut client);
2682
2683 assert!(!axis.is_error);
2685 assert!(axis.is_busy);
2686 assert!(matches!(axis.op, AxisOp::Halting(_)));
2687 assert!(view.control_word & (1 << 8) != 0);
2688
2689 view.velocity_actual = 0;
2694 view.positive_limit = false;
2695 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2696 axis.tick(&mut view, &mut client);
2697 }
2698 assert!(matches!(axis.op, AxisOp::Halting(_)));
2699 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2700 assert_eq!(step, HaltState::WaitCancelAck as u8);
2701
2702 view.status_word = 0x1027;
2704 axis.tick(&mut view, &mut client);
2705 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2706 assert_eq!(step, HaltState::WaitCancelAckClear as u8);
2707
2708 view.status_word = 0x0027;
2711 axis.tick(&mut view, &mut client);
2712 assert_eq!(axis.op, AxisOp::Idle);
2713 assert!(!axis.is_busy);
2714 assert!(view.control_word & (1 << 8) == 0, "halt should be cleared after close-out");
2715
2716 axis.move_absolute(&mut view, 30.0, 90.0, 180.0, 180.0);
2719 assert!(view.control_word & (1 << 4) != 0, "new-setpoint asserted on start_move tick");
2720 assert!(view.control_word & (1 << 8) == 0, "halt stays cleared");
2721 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2722 }
2723
2724 #[test]
2725 fn limit_switch_allows_move_in_opposite_direction() {
2726 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2727 let mut view = MockView::new();
2728 view.set_state(0x0027);
2729 view.positive_limit = true;
2731 view.velocity_actual = -100;
2732 axis.tick(&mut view, &mut client);
2733 assert!(axis.at_positive_limit_switch);
2734
2735 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2737 view.status_word = 0x1027;
2738 axis.tick(&mut view, &mut client);
2739
2740 assert!(!axis.is_error);
2742 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
2743 }
2744
2745 #[test]
2746 fn limit_switch_ignored_when_not_moving() {
2747 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2748 let mut view = MockView::new();
2749 view.set_state(0x0027);
2750 view.positive_limit = true;
2751
2752 axis.tick(&mut view, &mut client);
2753
2754 assert!(axis.at_positive_limit_switch);
2756 assert!(!axis.is_error);
2757 }
2758
2759 #[test]
2760 fn home_sensor_output_tracks_view() {
2761 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2762 let mut view = MockView::new();
2763 view.set_state(0x0027);
2764
2765 axis.tick(&mut view, &mut client);
2766 assert!(!axis.home_sensor);
2767
2768 view.home_sensor = true;
2769 axis.tick(&mut view, &mut client);
2770 assert!(axis.home_sensor);
2771
2772 view.home_sensor = false;
2773 axis.tick(&mut view, &mut client);
2774 assert!(!axis.home_sensor);
2775 }
2776
2777 #[test]
2778 fn velocity_output_converted() {
2779 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2780 let mut view = MockView::new();
2781 view.set_state(0x0027);
2782 view.velocity_actual = 3200;
2784
2785 axis.tick(&mut view, &mut client);
2786
2787 assert!((axis.speed - 90.0).abs() < 0.1);
2788 assert!(axis.moving_positive);
2789 assert!(!axis.moving_negative);
2790 }
2791
2792 fn soft_homing_config() -> AxisConfig {
2795 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2796 cfg.homing_speed = 10.0;
2797 cfg.homing_accel = 20.0;
2798 cfg.homing_decel = 20.0;
2799 cfg
2800 }
2801
2802 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2803 use tokio::sync::mpsc;
2804 let (write_tx, write_rx) = mpsc::unbounded_channel();
2805 let (response_tx, response_rx) = mpsc::unbounded_channel();
2806 let client = CommandClient::new(write_tx, response_rx);
2807 let axis = Axis::new(soft_homing_config(), "TestDrive");
2808 (axis, client, response_tx, write_rx)
2809 }
2810
2811 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2813 view.set_state(0x0027); axis.tick(view, client);
2815 }
2816
2817 fn complete_soft_homing(
2822 axis: &mut Axis,
2823 view: &mut MockView,
2824 client: &mut CommandClient,
2825 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2826 trigger_pos: i32,
2827 clear_sensor: impl FnOnce(&mut MockView),
2828 ) {
2829 use mechutil::ipc::CommandMessage as IpcMsg;
2830
2831 axis.tick(view, client);
2834 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2835
2836 view.position_actual = trigger_pos + 100;
2838 axis.tick(view, client);
2839 view.position_actual = trigger_pos + 120;
2840 axis.tick(view, client);
2841 for _ in 0..10 { axis.tick(view, client); }
2843 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2844
2845 view.status_word = 0x1027;
2847 axis.tick(view, client);
2848 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2849 view.status_word = 0x0027;
2850
2851 for _ in 0..100 { axis.tick(view, client); }
2853 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2854
2855 axis.tick(view, client);
2858 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2859
2860 view.status_word = 0x1027;
2862 axis.tick(view, client);
2863 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2864 view.status_word = 0x0027;
2865
2866 axis.tick(view, client);
2868 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2869 clear_sensor(view);
2870 view.position_actual = trigger_pos - 200;
2871 axis.tick(view, client);
2872 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2873
2874 axis.tick(view, client);
2876 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2877 for _ in 0..10 { axis.tick(view, client); }
2878 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2879
2880 view.status_word = 0x1027;
2883 axis.tick(view, client);
2884 view.status_word = 0x0027;
2885 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2886
2887 let tid = axis.homing_sdo_tid;
2889 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2890 client.poll();
2891 axis.tick(view, client);
2892 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2893
2894 axis.tick(view, client);
2896 let tid = axis.homing_sdo_tid;
2897 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2898 client.poll();
2899 axis.tick(view, client);
2900 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2901
2902 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2904 axis.tick(view, client);
2905 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2906
2907 view.status_word = 0x1427; axis.tick(view, client);
2910 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2911 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2912 view.status_word = 0x0027;
2913
2914 axis.tick(view, client);
2916 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2917
2918 view.status_word = 0x1027;
2920 axis.tick(view, client);
2921 view.status_word = 0x0027;
2922
2923 assert_eq!(axis.op, AxisOp::Idle);
2924 assert!(!axis.is_busy);
2925 assert!(!axis.is_error);
2926 assert_eq!(axis.home_offset, 0); }
2928
2929 #[test]
2930 fn soft_homing_pnp_home_sensor_full_sequence() {
2931 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2932 let mut view = MockView::new();
2933 enable_axis(&mut axis, &mut view, &mut client);
2934
2935 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2936
2937 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2940 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2942 axis.tick(&mut view, &mut client); view.home_sensor = true;
2946 view.position_actual = 5000;
2947 axis.tick(&mut view, &mut client);
2948 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2949
2950 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2951 |v| { v.home_sensor = false; });
2952 }
2953
2954 #[test]
2955 fn soft_homing_npn_home_sensor_full_sequence() {
2956 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2957 let mut view = MockView::new();
2958 view.home_sensor = true;
2960 enable_axis(&mut axis, &mut view, &mut client);
2961
2962 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2963
2964 axis.tick(&mut view, &mut client);
2966 view.status_word = 0x1027;
2967 axis.tick(&mut view, &mut client);
2968 view.status_word = 0x0027;
2969 axis.tick(&mut view, &mut client);
2970
2971 view.home_sensor = false;
2973 view.position_actual = 3000;
2974 axis.tick(&mut view, &mut client);
2975 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2976
2977 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2978 |v| { v.home_sensor = true; }); }
2980
2981 #[test]
2982 fn soft_homing_limit_switch_suppresses_halt() {
2983 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2984 let mut view = MockView::new();
2985 enable_axis(&mut axis, &mut view, &mut client);
2986
2987 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
2989
2990 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2995 axis.tick(&mut view, &mut client); view.positive_limit = true;
2999 view.velocity_actual = 100; view.position_actual = 8000;
3001 axis.tick(&mut view, &mut client);
3002
3003 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3005 assert!(!axis.is_error);
3006 }
3007
3008 #[test]
3009 fn soft_homing_opposite_limit_still_protects() {
3010 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3011 let mut view = MockView::new();
3012 enable_axis(&mut axis, &mut view, &mut client);
3013
3014 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3016
3017 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3022 axis.tick(&mut view, &mut client); view.negative_limit = true;
3027 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
3029
3030 assert!(axis.is_error);
3032 assert!(axis.error_message.contains("Negative limit switch"));
3033 }
3034
3035 #[test]
3036 #[test]
3054 fn soft_homing_negative_direction_sets_negative_target() {
3055 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3056 let mut view = MockView::new();
3057 enable_axis(&mut axis, &mut view, &mut client);
3058
3059 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
3060 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
3064 }
3065
3066 #[test]
3067 fn home_integrated_method_starts_hardware_homing() {
3068 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3069 let mut view = MockView::new();
3070 enable_axis(&mut axis, &mut view, &mut client);
3071
3072 axis.home(&mut view, HomingMethod::CurrentPosition);
3073 assert!(matches!(axis.op, AxisOp::Homing(0)));
3074 assert_eq!(axis.homing_method, 37);
3075 }
3076
3077 #[test]
3078 fn home_integrated_arbitrary_code() {
3079 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3080 let mut view = MockView::new();
3081 enable_axis(&mut axis, &mut view, &mut client);
3082
3083 axis.home(&mut view, HomingMethod::Integrated(35));
3084 assert!(matches!(axis.op, AxisOp::Homing(0)));
3085 assert_eq!(axis.homing_method, 35);
3086 }
3087
3088 #[test]
3089 fn hardware_homing_skips_speed_sdos_when_zero() {
3090 use mechutil::ipc::CommandMessage;
3091
3092 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
3093 let mut view = MockView::new();
3094 enable_axis(&mut axis, &mut view, &mut client);
3095
3096 axis.home(&mut view, HomingMethod::Integrated(37));
3098
3099 axis.tick(&mut view, &mut client);
3101 assert!(matches!(axis.op, AxisOp::Homing(1)));
3102
3103 let _ = write_rx.try_recv();
3105
3106 let tid = axis.homing_sdo_tid;
3108 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3109 client.poll();
3110 axis.tick(&mut view, &mut client);
3111
3112 assert!(matches!(axis.op, AxisOp::Homing(8)));
3114 }
3115
3116 #[test]
3117 fn hardware_homing_writes_speed_sdos_when_nonzero() {
3118 use mechutil::ipc::CommandMessage;
3119
3120 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3121 let mut view = MockView::new();
3122 enable_axis(&mut axis, &mut view, &mut client);
3123
3124 axis.home(&mut view, HomingMethod::Integrated(37));
3126
3127 axis.tick(&mut view, &mut client);
3129 assert!(matches!(axis.op, AxisOp::Homing(1)));
3130 let _ = write_rx.try_recv();
3131
3132 let tid = axis.homing_sdo_tid;
3134 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3135 client.poll();
3136 axis.tick(&mut view, &mut client);
3137 assert!(matches!(axis.op, AxisOp::Homing(2)));
3139 }
3140
3141 #[test]
3142 fn soft_homing_edge_during_ack_step() {
3143 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3144 let mut view = MockView::new();
3145 enable_axis(&mut axis, &mut view, &mut client);
3146
3147 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3148 axis.tick(&mut view, &mut client); view.home_sensor = true;
3152 view.position_actual = 2000;
3153 axis.tick(&mut view, &mut client);
3154
3155 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3157 }
3158
3159 #[test]
3160 fn soft_homing_applies_home_position() {
3161 let mut cfg = soft_homing_config();
3162 cfg.home_position = 90.0;
3163
3164 use tokio::sync::mpsc;
3165 let (write_tx, _write_rx) = mpsc::unbounded_channel();
3166 let (resp_tx, response_rx) = mpsc::unbounded_channel();
3167 let mut client = CommandClient::new(write_tx, response_rx);
3168 let mut axis = Axis::new(cfg, "TestDrive");
3169
3170 let mut view = MockView::new();
3171 enable_axis(&mut axis, &mut view, &mut client);
3172
3173 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3174
3175 axis.tick(&mut view, &mut client);
3177 view.status_word = 0x1027;
3178 axis.tick(&mut view, &mut client);
3179 view.status_word = 0x0027;
3180 axis.tick(&mut view, &mut client);
3181
3182 view.home_sensor = true;
3184 view.position_actual = 5000;
3185 axis.tick(&mut view, &mut client);
3186 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3187
3188 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3190 |v| { v.home_sensor = false; });
3191
3192 assert_eq!(axis.home_offset, 0);
3194 }
3195
3196 #[test]
3197 fn soft_homing_default_home_position_zero() {
3198 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3199 let mut view = MockView::new();
3200 enable_axis(&mut axis, &mut view, &mut client);
3201
3202 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3203
3204 axis.tick(&mut view, &mut client);
3206 view.status_word = 0x1027;
3207 axis.tick(&mut view, &mut client);
3208 view.status_word = 0x0027;
3209 axis.tick(&mut view, &mut client);
3210
3211 view.home_sensor = true;
3213 view.position_actual = 5000;
3214 axis.tick(&mut view, &mut client);
3215
3216 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3217 |v| { v.home_sensor = false; });
3218
3219 assert_eq!(axis.home_offset, 0);
3220 }
3221}