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) {
469 let mut cw = RawControlWord(view.control_word());
470 cw.cmd_shutdown();
471 cw.set_bit(4, false);
472 cw.set_bit(8, false);
473 cw.set_bit(7, false);
474 cw.set_bit(2, true);
475 view.set_control_word(cw.raw());
476
477 self.op = AxisOp::Disabling(1);
478 self.op_started = Some(Instant::now());
479 }
480
481 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
497 let sw = RawStatusWord(view.status_word());
498 let drive_in_fault = matches!(
499 sw.state(),
500 Cia402State::Fault | Cia402State::FaultReactionActive
501 );
502
503 self.is_error = false;
504 self.error_code = 0;
505 self.error_message.clear();
506
507 if drive_in_fault {
508 let mut cw = RawControlWord(view.control_word());
510 cw.cmd_clear_fault_reset();
511 view.set_control_word(cw.raw());
512 self.op = AxisOp::FaultRecovery(1);
513 self.op_started = Some(Instant::now());
514 } else {
515 self.op = AxisOp::Idle;
517 self.op_started = None;
518 self.is_busy = false;
519 self.in_motion = false;
520 }
521 }
522
523 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
531 if method.is_integrated() {
532 self.homing_method = match method {
533 HomingMethod::CurrentPosition => self.config.soft_home_method,
534 _ => method.cia402_code(),
535 };
536 self.op = AxisOp::Homing(0);
537 self.op_started = Some(Instant::now());
538 let _ = view;
539 } else {
540 self.configure_soft_homing(method);
541 self.start_soft_homing(view);
542 }
543 }
544
545 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
554 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
555 }
556
557 pub fn set_position_persistent(
574 &mut self,
575 client: &mut CommandClient,
576 user_units: f64,
577 ) {
578 self.config.home_position = user_units;
585 let desired_counts = self.config.to_counts(user_units).round() as i32;
586 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
587 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
588 self.op_started = Some(Instant::now());
589 log::info!(
590 "set_position_persistent: writing 0x607C = {} counts ({} user units)",
591 desired_counts, user_units
592 );
593 }
594
595 pub fn set_home_position(&mut self, user_units: f64) {
599 self.config.home_position = user_units;
600 }
601
602 pub fn set_software_max_limit(&mut self, user_units: f64) {
604 self.config.max_position_limit = user_units;
605 self.config.enable_max_position_limit = true;
606 }
607
608 pub fn set_software_min_limit(&mut self, user_units: f64) {
610 self.config.min_position_limit = user_units;
611 self.config.enable_min_position_limit = true;
612 }
613
614 pub fn sdo_write(
620 &mut self,
621 client: &mut CommandClient,
622 index: u16,
623 sub_index: u8,
624 value: serde_json::Value,
625 ) {
626 self.sdo.write(client, index, sub_index, value);
627 }
628
629 pub fn sdo_read(
631 &mut self,
632 client: &mut CommandClient,
633 index: u16,
634 sub_index: u8,
635 ) -> u32 {
636 self.sdo.read(client, index, sub_index)
637 }
638
639 pub fn sdo_result(
641 &mut self,
642 client: &mut CommandClient,
643 tid: u32,
644 ) -> SdoResult {
645 self.sdo.result(client, tid, Duration::from_secs(5))
646 }
647
648 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
662 self.check_faults(view);
663 self.progress_op(view, client);
664 self.update_outputs(view);
665 self.check_limits(view);
666 }
667
668 fn update_outputs(&mut self, view: &impl AxisView) {
673 let raw = view.position_actual();
674 self.raw_position = raw as i64;
675 self.position = self.config.to_user((raw - self.home_offset) as f64);
676
677 let vel = view.velocity_actual();
678 let user_vel = self.config.to_user(vel as f64);
679 self.speed = user_vel.abs();
680 self.moving_positive = user_vel > 0.0;
681 self.moving_negative = user_vel < 0.0;
682 self.is_busy = self.op != AxisOp::Idle;
683 self.in_motion = matches!(self.op, AxisOp::Moving(_, _, _, _) | AxisOp::SoftHoming(_));
684
685 let sw = RawStatusWord(view.status_word());
686 self.motor_on = sw.state() == Cia402State::OperationEnabled;
687
688 self.last_raw_position = raw;
689 }
690
691 fn check_faults(&mut self, view: &impl AxisView) {
696 let sw = RawStatusWord(view.status_word());
697 let state = sw.state();
698
699 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
700 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
701 self.is_error = true;
702 let ec = view.error_code();
703 if ec != 0 {
704 self.error_code = ec as u32;
705 }
706 self.error_message = format!("Drive fault (state: {})", state);
707 self.op = AxisOp::Idle;
709 self.op_started = None;
710 }
711 }
712 }
713
714 fn op_timed_out(&self) -> bool {
719 self.op_started
720 .map_or(false, |t| t.elapsed() > self.op_timeout)
721 }
722
723 fn homing_timed_out(&self) -> bool {
724 self.op_started
725 .map_or(false, |t| t.elapsed() > self.homing_timeout)
726 }
727
728 fn move_start_timed_out(&self) -> bool {
729 self.op_started
730 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
731 }
732
733 fn op_stage_timed_out(&self, limit: Duration) -> bool {
737 self.op_started
738 .map_or(false, |t| t.elapsed() > limit)
739 }
740
741 fn set_op_error(&mut self, msg: &str) {
742 self.is_error = true;
743 self.error_message = msg.to_string();
744 self.op = AxisOp::Idle;
745 self.op_started = None;
746 self.is_busy = false;
747 self.in_motion = false;
748 log::error!("Axis error: {}", msg);
749 }
750
751 fn restore_pp_after_error(&mut self, msg: &str) {
752 self.is_error = true;
753 self.error_message = msg.to_string();
754 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);;
755 log::error!("Axis error: {}", msg);
756 }
757
758 fn finish_op_error(&mut self) {
759 self.op = AxisOp::Idle;
760 self.op_started = None;
761 self.is_busy = false;
762 self.in_motion = false;
763 }
764
765 fn complete_op(&mut self) {
766 self.op = AxisOp::Idle;
767 self.op_started = None;
768 self.is_busy = false;
773 self.in_motion = false;
774 }
775
776 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
785 let static_limit = if self.config.enable_max_position_limit {
786 Some(self.config.max_position_limit)
787 } else {
788 None
789 };
790 match (static_limit, view.dynamic_max_position_limit()) {
791 (Some(s), Some(d)) => Some(s.min(d)),
792 (Some(v), None) | (None, Some(v)) => Some(v),
793 (None, None) => None,
794 }
795 }
796
797 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
801 let static_limit = if self.config.enable_min_position_limit {
802 Some(self.config.min_position_limit)
803 } else {
804 None
805 };
806 match (static_limit, view.dynamic_min_position_limit()) {
807 (Some(s), Some(d)) => Some(s.max(d)),
808 (Some(v), None) | (None, Some(v)) => Some(v),
809 (None, None) => None,
810 }
811 }
812
813 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
818 if let Some(max) = self.effective_max_limit(view) {
819 if target > max {
820 return Some(format!(
821 "Target {:.3} exceeds max software limit {:.3}",
822 target, max
823 ));
824 }
825 }
826 if let Some(min) = self.effective_min_limit(view) {
827 if target < min {
828 return Some(format!(
829 "Target {:.3} exceeds min software limit {:.3}",
830 target, min
831 ));
832 }
833 }
834 None
835 }
836
837 fn check_limits(&mut self, view: &mut impl AxisView) {
846 let eff_max = self.effective_max_limit(view);
848 let eff_min = self.effective_min_limit(view);
849 let sw_max = eff_max.map_or(false, |m| self.position >= m);
850 let sw_min = eff_min.map_or(false, |m| self.position <= m);
851
852 self.at_max_limit = sw_max;
853 self.at_min_limit = sw_min;
854
855 let hw_pos = view.positive_limit_active();
857 let hw_neg = view.negative_limit_active();
858
859 self.at_positive_limit_switch = hw_pos;
860 self.at_negative_limit_switch = hw_neg;
861
862 self.home_sensor = view.home_sensor_active();
864
865 self.prev_positive_limit = hw_pos;
867 self.prev_negative_limit = hw_neg;
868 self.prev_home_sensor = view.home_sensor_active();
869
870 let mut commanded_positive = false;
872 let mut commanded_negative = false;
873
874 let is_moving = matches!(self.op, AxisOp::Moving(_, _, _, _));
875 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
876
877 if !is_moving && !is_soft_homing {
878 return; }
880
881 match &self.op {
882 AxisOp::Moving(_, _, pos, neg) => {
883 commanded_positive = *pos;
885 commanded_negative = *neg;
886 }
887 AxisOp::SoftHoming(_) => {
888 match self.soft_home_sensor {
889 SoftHomeSensor::PositiveLimit => commanded_positive = true,
890 SoftHomeSensor::NegativeLimit => commanded_negative = true,
891 SoftHomeSensor::HomeSensor => {
892 commanded_positive = self.moving_positive;
893 commanded_negative = self.moving_negative;
894 }
895 }
896 }
897 _ => {}
898 }
899
900 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
902 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
903
904 let effective_hw_pos = hw_pos && !suppress_pos;
905 let effective_hw_neg = hw_neg && !suppress_neg;
906
907 let effective_sw_max = sw_max && !is_soft_homing;
909 let effective_sw_min = sw_min && !is_soft_homing;
910
911 let positive_blocked = (effective_sw_max || effective_hw_pos) && commanded_positive;
912 let negative_blocked = (effective_sw_min || effective_hw_neg) && commanded_negative;
913
914
915 if positive_blocked || negative_blocked {
916 let mut cw = RawControlWord(view.control_word());
917 cw.set_bit(8, true); view.set_control_word(cw.raw());
919
920 let msg = if effective_hw_pos && commanded_positive {
921 "Positive limit switch active".to_string()
922 } else if effective_hw_neg && commanded_negative {
923 "Negative limit switch active".to_string()
924 } else if effective_sw_max && commanded_positive {
925 format!(
926 "Software position limit: position {:.3} >= max {:.3}",
927 self.position, eff_max.unwrap_or(self.position)
928 )
929 } else {
930 format!(
931 "Software position limit: position {:.3} <= min {:.3}",
932 self.position, eff_min.unwrap_or(self.position)
933 )
934 };
935
936 if is_soft_homing {
937 self.set_op_error(&msg);
940 } else {
941 log::info!("Axis move halted by limit: {}", msg);
954 self.command_halt(view);
955 self.halt_stable_count = 0;
956 self.last_raw_position = view.position_actual();
957 self.op_started = Some(Instant::now());
958 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
959 }
960 }
961 }
962
963 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
968 match self.op.clone() {
969 AxisOp::Idle => {}
970 AxisOp::Enabling(step) => self.tick_enabling(view, step),
971 AxisOp::Disabling(step) => self.tick_disabling(view, step),
972 AxisOp::Moving(kind, step, pos, neg) => self.tick_moving(view, kind, step, pos, neg),
973 AxisOp::Homing(step) => self.tick_homing(view, client, step),
974 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
975 AxisOp::Halting(step) => self.tick_halting(view, step),
976 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
977 }
978 }
979
980 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
985 match step {
986 1 => {
987 let sw = RawStatusWord(view.status_word());
988 if sw.state() == Cia402State::ReadyToSwitchOn {
989 let mut cw = RawControlWord(view.control_word());
990 cw.cmd_enable_operation();
991 view.set_control_word(cw.raw());
992 self.op = AxisOp::Enabling(2);
993 } else if self.op_timed_out() {
994 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
995 }
996 }
997 2 => {
998 let sw = RawStatusWord(view.status_word());
999 if sw.state() == Cia402State::OperationEnabled {
1000 self.complete_op();
1007 } else if self.op_timed_out() {
1008 self.set_op_error("Enable timeout: waiting for OperationEnabled");
1009 }
1010 }
1011 _ => self.complete_op(),
1012 }
1013 }
1014
1015 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
1019 match step {
1020 1 => {
1021 let sw = RawStatusWord(view.status_word());
1022 if sw.state() != Cia402State::OperationEnabled {
1023 self.complete_op();
1024 } else if self.op_timed_out() {
1025 self.set_op_error("Disable timeout: drive still in OperationEnabled");
1026 }
1027 }
1028 _ => self.complete_op(),
1029 }
1030 }
1031
1032 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8, pos: bool, neg: bool) {
1038 match step {
1039 1 => {
1040 let sw = RawStatusWord(view.status_word());
1042 if sw.raw() & (1 << 12) != 0 {
1043 let mut cw = RawControlWord(view.control_word());
1045 cw.set_bit(4, false);
1046 view.set_control_word(cw.raw());
1047 self.op = AxisOp::Moving(kind, 2, pos, neg);
1048 } else if self.move_start_timed_out() {
1049 self.set_op_error("Move timeout: set-point not acknowledged");
1050 }
1051 },
1052 2 => {
1053 let sw = RawStatusWord(view.status_word());
1055 if sw.raw() & (1 << 12) == 0 {
1056 self.op = AxisOp::Moving(kind, 3, pos, neg);
1058 }
1059 },
1060 3 => {
1061 let sw = RawStatusWord(view.status_word());
1063 if sw.target_reached() {
1064 self.complete_op();
1065 }
1066 },
1067 _ => self.complete_op(),
1068 }
1069 }
1070
1071 fn tick_homing(
1097 &mut self,
1098 view: &mut impl AxisView,
1099 client: &mut CommandClient,
1100 step: u8,
1101 ) {
1102 match step {
1103 0 => {
1104 self.homing_sdo_tid = self.sdo.write(
1106 client,
1107 0x6098,
1108 0,
1109 json!(self.homing_method),
1110 );
1111 self.op = AxisOp::Homing(1);
1112 }
1113 1 => {
1114 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1116 SdoResult::Ok(_) => {
1117 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
1119 self.op = AxisOp::Homing(8);
1120 } else {
1121 self.op = AxisOp::Homing(2);
1122 }
1123 }
1124 SdoResult::Pending => {
1125 if self.homing_timed_out() {
1126 self.set_op_error("Homing timeout: SDO write for homing method");
1127 }
1128 }
1129 SdoResult::Err(e) => {
1130 self.set_op_error(&format!("Homing SDO error: {}", e));
1131 }
1132 SdoResult::Timeout => {
1133 self.set_op_error("Homing timeout: SDO write timed out");
1134 }
1135 }
1136 }
1137 2 => {
1138 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1140 self.homing_sdo_tid = self.sdo.write(
1141 client,
1142 0x6099,
1143 1,
1144 json!(speed_counts),
1145 );
1146 self.op = AxisOp::Homing(3);
1147 }
1148 3 => {
1149 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1150 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
1151 SdoResult::Pending => {
1152 if self.homing_timed_out() {
1153 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
1154 }
1155 }
1156 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1157 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1158 }
1159 }
1160 4 => {
1161 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1163 self.homing_sdo_tid = self.sdo.write(
1164 client,
1165 0x6099,
1166 2,
1167 json!(speed_counts),
1168 );
1169 self.op = AxisOp::Homing(5);
1170 }
1171 5 => {
1172 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1173 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
1174 SdoResult::Pending => {
1175 if self.homing_timed_out() {
1176 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1177 }
1178 }
1179 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1180 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1181 }
1182 }
1183 6 => {
1184 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1186 self.homing_sdo_tid = self.sdo.write(
1187 client,
1188 0x609A,
1189 0,
1190 json!(accel_counts),
1191 );
1192 self.op = AxisOp::Homing(7);
1193 }
1194 7 => {
1195 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1196 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
1197 SdoResult::Pending => {
1198 if self.homing_timed_out() {
1199 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1200 }
1201 }
1202 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
1203 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
1204 }
1205 }
1206 8 => {
1207 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1217 let mut cw = RawControlWord(view.control_word());
1219 cw.set_bit(4, false);
1220 view.set_control_word(cw.raw());
1221 self.fb_mode_of_operation.reset();
1222 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1223 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1224 self.op = AxisOp::Homing(9);
1225 }
1226 9 => {
1227 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1229 if !self.fb_mode_of_operation.is_busy() {
1230 if self.fb_mode_of_operation.is_error() {
1231 self.set_op_error(&format!(
1232 "Homing: failed to enter Homing mode: {} {}",
1233 self.fb_mode_of_operation.error_code(),
1234 self.fb_mode_of_operation.error_message()
1235 ));
1236 } else {
1237 self.op = AxisOp::Homing(10);
1238 }
1239 }
1240 }
1241 10 => {
1242 let mut cw = RawControlWord(view.control_word());
1244 cw.set_bit(4, true);
1245 view.set_control_word(cw.raw());
1246 self.op = AxisOp::Homing(11);
1247 }
1248 11 => {
1249 let sw = view.status_word();
1261 if sw & (1 << 13) != 0 {
1262 self.set_op_error("Homing error: drive reported homing failure");
1263 } else {
1264 self.op = AxisOp::Homing(12);
1265 }
1266 }
1267 12 => {
1268 let sw = view.status_word();
1276 let error = sw & (1 << 13) != 0;
1277 let attained = sw & (1 << 12) != 0;
1278
1279 if error {
1280 self.set_op_error("Homing error: drive reported homing failure");
1281 } else if attained {
1282 self.op = AxisOp::Homing(13);
1283 } else if self.homing_timed_out() {
1284 self.set_op_error(&format!("Homing timeout: procedure did not complete (sw=0x{:04X})", sw));
1285 }
1286 }
1287 13 => {
1288 self.home_offset = view.position_actual()
1291 - self.config.to_counts(self.config.home_position).round() as i32;
1292 let mut cw = RawControlWord(view.control_word());
1294 cw.set_bit(4, false);
1295 view.set_control_word(cw.raw());
1296 self.op = AxisOp::Homing(14);
1297 }
1298 14 => {
1299 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1305 self.fb_mode_of_operation.reset();
1306 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1307 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1308 self.op = AxisOp::Homing(15);
1309 }
1310 15 => {
1311 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1313 if !self.fb_mode_of_operation.is_busy() {
1314 if self.fb_mode_of_operation.is_error() {
1315 self.set_op_error(&format!(
1316 "Homing: failed to restore PP mode: {} {}",
1317 self.fb_mode_of_operation.error_code(),
1318 self.fb_mode_of_operation.error_message()
1319 ));
1320 } else {
1321 log::info!("Homing complete — home offset: {}", self.home_offset);
1322 self.complete_op();
1323 }
1324 }
1325 }
1326 _ => self.complete_op(),
1327 }
1328 }
1329
1330 fn configure_soft_homing(&mut self, method: HomingMethod) {
1333 match method {
1334 HomingMethod::LimitSwitchPosPnp => {
1335 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1336 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1337 self.soft_home_direction = 1.0;
1338 }
1339 HomingMethod::LimitSwitchNegPnp => {
1340 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1341 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1342 self.soft_home_direction = -1.0;
1343 }
1344 HomingMethod::LimitSwitchPosNpn => {
1345 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1346 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1347 self.soft_home_direction = 1.0;
1348 }
1349 HomingMethod::LimitSwitchNegNpn => {
1350 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1351 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1352 self.soft_home_direction = -1.0;
1353 }
1354 HomingMethod::HomeSensorPosPnp => {
1355 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1356 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1357 self.soft_home_direction = 1.0;
1358 }
1359 HomingMethod::HomeSensorNegPnp => {
1360 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1361 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1362 self.soft_home_direction = -1.0;
1363 }
1364 HomingMethod::HomeSensorPosNpn => {
1365 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1366 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1367 self.soft_home_direction = 1.0;
1368 }
1369 HomingMethod::HomeSensorNegNpn => {
1370 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1371 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1372 self.soft_home_direction = -1.0;
1373 }
1374 _ => {} }
1376 }
1377
1378 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1379 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1380 self.op_started = Some(Instant::now());
1381 }
1382
1383 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1384 let raw = match self.soft_home_sensor {
1385 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1386 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1387 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1388 };
1389 match self.soft_home_sensor_type {
1390 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1393 }
1394
1395
1396 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1399 let dir = if !self.config.invert_direction {
1400 direction
1401 }
1402 else {
1403 -direction
1404 };
1405
1406 let target = if dir > 0.0 {
1407 i32::MAX
1408 }
1409 else {
1410 i32::MIN
1411 };
1412
1413 return target;
1414 }
1415
1416
1417 pub fn command_halt(&self, view: &mut impl AxisView) {
1422 let mut cw = RawControlWord(view.control_word());
1423 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());
1428 }
1429
1430 pub fn command_clear_halt(&self, view: &mut impl AxisView) {
1433 let mut cw = RawControlWord(view.control_word());
1434 cw.set_bit(8, false); view.set_control_word(cw.raw());
1436 }
1437
1438
1439 pub fn command_cancel_move(&self, view: &mut impl AxisView) {
1447
1448 let mut cw = RawControlWord(view.control_word());
1449
1450 cw.set_bit(4, true); cw.set_bit(5, true); cw.set_bit(6, false); if self.config().halt_blocks_setpoint_ack {
1455 cw.set_bit(8, false); }
1462
1463 view.set_control_word(cw.raw());
1464
1465 let current_pos = view.position_actual();
1466 view.set_target_position(current_pos);
1467 view.set_profile_velocity(0);
1468 }
1469
1470
1471 fn command_homing_speed(&self, view: &mut impl AxisView) {
1473 let cpu = self.config.counts_per_user();
1474 let vel = (self.config.homing_speed * cpu).round() as u32;
1475 let accel = (self.config.homing_accel * cpu).round() as u32;
1476 let decel = (self.config.homing_decel * cpu).round() as u32;
1477 view.set_profile_velocity(vel);
1478 view.set_profile_acceleration(accel);
1479 view.set_profile_deceleration(decel);
1480 }
1481
1482 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1498 match HomeState::from_repr(step) {
1499
1500 Some(HomeState::EnsurePpMode) => {
1501 log::info!("SoftHome: Ensuring PP mode..");
1506 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1507 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1508 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1509 },
1510 Some(HomeState::WaitPpMode) => {
1511
1512 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1513 if !self.fb_mode_of_operation.is_busy() {
1514 if self.fb_mode_of_operation.is_error() {
1515 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1516 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1517 ));
1518 }
1519 else {
1520 log::info!("SoftHome: Drive is in PP mode!");
1521
1522 if !self.check_soft_home_trigger(view) {
1526 log::info!("SoftHome: Not on home switch; seek out.");
1527 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1528 } else {
1529 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1530 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1531 }
1532 }
1533 }
1534
1535
1536 },
1537
1538 Some(HomeState::Search) => {
1540 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1541
1542 let target = self.calculate_max_relative_target(self.soft_home_direction);
1552 view.set_target_position(target);
1553
1554 self.command_homing_speed(view);
1563
1564 let mut cw = RawControlWord(view.control_word());
1565 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());
1570
1571 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1572 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1573 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1574 }
1575 Some(HomeState::WaitSearching) => {
1576 if self.check_soft_home_trigger(view) {
1577 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1578 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1579 return;
1580 }
1581 let sw = RawStatusWord(view.status_word());
1582 if sw.raw() & (1 << 12) != 0 {
1583 let mut cw = RawControlWord(view.control_word());
1584 cw.set_bit(4, false);
1585 view.set_control_word(cw.raw());
1586 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1587 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1588 } else if self.homing_timed_out() {
1589 self.set_op_error("Software homing timeout: set-point not acknowledged");
1590 }
1591 }
1592 Some(HomeState::WaitFoundSensor) => {
1602 if self.check_soft_home_trigger(view) {
1603 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1604 log::info!("ControlWord is : {} ", view.control_word());
1605
1606 let mut cw = RawControlWord(view.control_word());
1607 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1610
1611
1612 self.halt_stable_count = 0;
1613 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1614 } else if self.homing_timed_out() {
1615 self.set_op_error("Software homing timeout: sensor not detected");
1616 }
1617 }
1618
1619
1620 Some(HomeState::WaitStoppedFoundSensor) => {
1621 const STABLE_WINDOW: i32 = 1;
1622 const STABLE_TICKS_REQUIRED: u8 = 10;
1623
1624 let pos = view.position_actual();
1629 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1630 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1631 } else {
1632 self.halt_stable_count = 0;
1633 }
1634
1635 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1636
1637 log::info!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1638 self.command_cancel_move(view);
1639 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1640
1641 } else if self.homing_timed_out() {
1642 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1643 }
1644 }
1645 Some(HomeState::WaitFoundSensorAck) => {
1646 let sw = RawStatusWord(view.status_word());
1647 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1648
1649 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1650
1651 let mut cw = RawControlWord(view.control_word());
1653 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1656
1657 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1658
1659 } else if self.homing_timed_out() {
1660 self.set_op_error("Software homing timeout: cancel not acknowledged");
1661 }
1662 },
1663 Some(HomeState::WaitFoundSensorAckClear) => {
1664 let sw = RawStatusWord(view.status_word());
1665 if sw.raw() & (1 << 12) == 0 {
1667
1668 self.command_clear_halt(view);
1670
1671 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1672 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1673 self.ton.call(false, Duration::from_secs(3));
1674 }
1675 },
1676 Some(HomeState::DebounceFoundSensor) => {
1678 self.ton.call(true, Duration::from_secs(3));
1679
1680 let sw = RawStatusWord(view.status_word());
1681 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1682 self.ton.call(false, Duration::from_secs(3));
1683 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1684 view.position_actual(), view.control_word(), view.status_word());
1685 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1686 }
1687 }
1688
1689 Some(HomeState::BackOff) => {
1691
1692 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1693 view.set_target_position(target);
1694
1695
1696 self.command_homing_speed(view);
1697
1698 let mut cw = RawControlWord(view.control_word());
1699 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1703 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1704 target, self.config.homing_speed, view.position_actual(), cw.raw());
1705 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1706 }
1707 Some(HomeState::WaitBackingOff) => {
1708 let sw = RawStatusWord(view.status_word());
1709 if sw.raw() & (1 << 12) != 0 {
1710 let mut cw = RawControlWord(view.control_word());
1711 cw.set_bit(4, false);
1712 view.set_control_word(cw.raw());
1713 log::info!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1714 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1715 } else if self.homing_timed_out() {
1716 self.set_op_error("Software homing timeout: back-off not acknowledged");
1717 }
1718 }
1719 Some(HomeState::WaitLostSensor) => {
1720 if !self.check_soft_home_trigger(view) {
1721 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1722
1723 self.command_halt(view);
1724 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1725 } else if self.homing_timed_out() {
1726 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1727 }
1728 }
1729 Some(HomeState::WaitStoppedLostSensor) => {
1730 const STABLE_WINDOW: i32 = 1;
1731 const STABLE_TICKS_REQUIRED: u8 = 10;
1732
1733 let pos = view.position_actual();
1738 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1739 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1740 } else {
1741 self.halt_stable_count = 0;
1742 }
1743
1744 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1745 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1746 self.command_cancel_move(view);
1747 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1748 } else if self.homing_timed_out() {
1749 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1750 }
1751 }
1752 Some(HomeState::WaitLostSensorAck) => {
1753 let sw = RawStatusWord(view.status_word());
1754 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1755
1756 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1757
1758 let mut cw = RawControlWord(view.control_word());
1760 cw.set_bit(4, false); view.set_control_word(cw.raw());
1762
1763 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1764
1765
1766 } else if self.homing_timed_out() {
1767 self.set_op_error("Software homing timeout: cancel not acknowledged");
1768 }
1769 }
1770 Some(HomeState::WaitLostSensorAckClear) => {
1771 let sw = RawStatusWord(view.status_word());
1773 if sw.raw() & (1 << 12) == 0 {
1774
1775 self.command_clear_halt(view);
1776
1777 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1778 self.homing_sdo_tid = self.sdo.write(
1781 client, 0x607C, 0, json!(desired_counts),
1782 );
1783
1784 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1785 self.config.home_position, desired_counts
1786 );
1787
1788 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1789
1790 }
1791 },
1792
1793 Some(HomeState::WaitHomeOffsetDone) => {
1794 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1796 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1797 SdoResult::Pending => {
1798 if self.homing_timed_out() {
1799 self.set_op_error("Software homing timeout: home offset SDO write");
1800 }
1801 }
1802 SdoResult::Err(e) => {
1803 self.set_op_error(&format!("Software homing SDO error: {}", e));
1804 }
1805 SdoResult::Timeout => {
1806 self.set_op_error("Software homing: home offset SDO timed out");
1807 }
1808 }
1809 },
1810 Some(HomeState::WriteHomingModeOp) => {
1811
1812 self.fb_mode_of_operation.reset();
1816 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1817 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1818 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1819
1820
1821 },
1822 Some(HomeState::WaitWriteHomingModeOp) => {
1823 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1825
1826 if !self.fb_mode_of_operation.is_busy() {
1827 if self.fb_mode_of_operation.is_error() {
1828 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1829 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1830 ));
1831 }
1832 else {
1833 log::info!("SoftHome: Drive is now in Homing Mode.");
1834 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1835 }
1836 }
1837 },
1838 Some(HomeState::WriteHomingMethod) => {
1839 self.homing_sdo_tid = self.sdo.write(
1842 client, 0x6098, 0, json!(self.config.soft_home_method),
1843 );
1844 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1845 }
1846 Some(HomeState::WaitWriteHomingMethodDone) => {
1847 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1849 SdoResult::Ok(_) => {
1850 log::info!("SoftHome: Successfully wrote homing method.");
1851 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1852 }
1853 SdoResult::Pending => {
1854 if self.homing_timed_out() {
1855 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1856 }
1857 }
1858 SdoResult::Err(e) => {
1859 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1860 }
1861 SdoResult::Timeout => {
1862 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1863 }
1864 }
1865 }
1866 Some(HomeState::ClearHomingTrigger) => {
1867 let mut cw = RawControlWord(view.control_word());
1870 cw.set_bit(4, false);
1871 view.set_control_word(cw.raw());
1872 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1873 }
1874 Some(HomeState::TriggerHoming) => {
1875 let mut cw = RawControlWord(view.control_word());
1877 cw.set_bit(4, true);
1878 view.set_control_word(cw.raw());
1879 log::info!("SoftHome[TriggerHoming]: start homing");
1880 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1881 }
1882 Some(HomeState::WaitHomingStarted) => {
1883 let sw = view.status_word();
1889 let error = sw & (1 << 13) != 0;
1890 if error {
1891 self.restore_pp_after_error("Software homing: drive reported homing error");
1892 } else if sw & (1 << 12) == 0 {
1893 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1894 } else if self.homing_timed_out() {
1895 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1896 }
1897 }
1898 Some(HomeState::WaitHomingDone) => {
1899 let sw = view.status_word();
1901 let error = sw & (1 << 13) != 0;
1902 let attained = sw & (1 << 12) != 0;
1903 let reached = sw & (1 << 10) != 0;
1904
1905 if error {
1906 self.restore_pp_after_error("Software homing: drive reported homing error");
1907 } else if attained && reached {
1908 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
1909 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
1910 } else if self.homing_timed_out() {
1911 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
1912 }
1913 }
1914 Some(HomeState::ResetHomingTrigger) => {
1915 let mut cw = RawControlWord(view.control_word());
1920 cw.set_bit(4, false);
1921 view.set_control_word(cw.raw());
1922 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
1923 }
1924 Some(HomeState::WaitHomingTriggerCleared) => {
1925 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
1929 }
1930
1931
1932 Some(HomeState::WriteMotionModeOfOperation) => {
1933
1934 self.fb_mode_of_operation.reset();
1937 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1938 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1939 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
1940
1941 },
1942 Some(HomeState::WaitWriteMotionModeOfOperation) => {
1943 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1945
1946 if !self.fb_mode_of_operation.is_busy() {
1947 if self.fb_mode_of_operation.is_error() {
1948 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1949 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1950 ));
1951 }
1952 else {
1953 if self.is_error {
1954 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
1955 self.finish_op_error();
1956 }
1957 else {
1958 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1961 }
1962
1963 }
1964 }
1965 },
1966
1967 Some(HomeState::SendCurrentPositionTarget) => {
1968 let current_pos = view.position_actual();
1970 view.set_target_position(current_pos);
1971 view.set_profile_velocity(0);
1972 let mut cw = RawControlWord(view.control_word());
1973 cw.set_bit(4, true);
1974 cw.set_bit(5, true);
1975 cw.set_bit(6, false); view.set_control_word(cw.raw());
1977 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1978 }
1979 Some(HomeState::WaitCurrentPositionTargetSent) => {
1980 let sw = RawStatusWord(view.status_word());
1982 if sw.raw() & (1 << 12) != 0 {
1983 let mut cw = RawControlWord(view.control_word());
1984 cw.set_bit(4, false);
1985 view.set_control_word(cw.raw());
1986 log::info!("Software homing complete — position set to {} user units",
1987 self.config.home_position);
1988 self.complete_op();
1989 } else if self.homing_timed_out() {
1990 self.set_op_error("Software homing timeout: hold position not acknowledged");
1991 }
1992 }
1993 _ => self.complete_op(),
1994 }
1995 }
1996
1997 fn tick_halting(&mut self, view: &mut impl AxisView, step: u8) {
2010 match HaltState::from_repr(step) {
2011 Some(HaltState::WaitStopped) => {
2012 let pos = view.position_actual();
2016 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
2017
2018 let vel = view.velocity_actual().abs();
2019 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
2020
2021 if pos_stable || vel_stopped {
2026 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2027 } else {
2028 self.halt_stable_count = 0;
2029 }
2030
2031 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
2032 self.command_cancel_move(view);
2033 self.op_started = Some(Instant::now());
2034 self.op = AxisOp::Halting(HaltState::WaitCancelAck as u8);
2035 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2036 self.set_op_error("Halt timeout: motor did not stop");
2037 }
2038 }
2039 Some(HaltState::WaitCancelAck) => {
2040 let sw = RawStatusWord(view.status_word());
2041 let setpoint_ack = sw.raw() & (1 << 12) != 0;
2042 if setpoint_ack {
2044 let mut cw = RawControlWord(view.control_word());
2047 cw.set_bit(4, false);
2048 cw.set_bit(5, true);
2049 view.set_control_word(cw.raw());
2050 self.op_started = Some(Instant::now());
2051 self.op = AxisOp::Halting(HaltState::WaitCancelAckClear as u8);
2052 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2053 self.set_op_error("Halt timeout: cancel not acknowledged");
2054 }
2055 }
2056 Some(HaltState::WaitCancelAckClear) => {
2057 let sw = RawStatusWord(view.status_word());
2058 if sw.raw() & (1 << 12) == 0 {
2059 self.command_clear_halt(view);
2061 self.complete_op();
2062 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2063 self.set_op_error("Halt timeout: ack did not clear");
2064 }
2065 }
2066 None => {
2067 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
2068 self.complete_op();
2069 }
2070 }
2071 }
2072
2073 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
2078 match step {
2079 1 => {
2080 let mut cw = RawControlWord(view.control_word());
2082 cw.cmd_fault_reset();
2083 view.set_control_word(cw.raw());
2084 self.op = AxisOp::FaultRecovery(2);
2085 }
2086 2 => {
2087 let sw = RawStatusWord(view.status_word());
2089 let state = sw.state();
2090 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
2091 log::info!("Fault cleared (drive state: {})", state);
2092 self.complete_op();
2093 } else if self.op_timed_out() {
2094 self.set_op_error("Fault reset timeout: drive still faulted");
2095 }
2096 }
2097 _ => self.complete_op(),
2098 }
2099 }
2100}
2101
2102#[cfg(test)]
2107mod tests {
2108 use super::*;
2109
2110 struct MockView {
2112 control_word: u16,
2113 status_word: u16,
2114 target_position: i32,
2115 profile_velocity: u32,
2116 profile_acceleration: u32,
2117 profile_deceleration: u32,
2118 modes_of_operation: i8,
2119 modes_of_operation_display: i8,
2120 position_actual: i32,
2121 velocity_actual: i32,
2122 error_code: u16,
2123 positive_limit: bool,
2124 negative_limit: bool,
2125 home_sensor: bool,
2126 }
2127
2128 impl MockView {
2129 fn new() -> Self {
2130 Self {
2131 control_word: 0,
2132 status_word: 0x0040, target_position: 0,
2134 profile_velocity: 0,
2135 profile_acceleration: 0,
2136 profile_deceleration: 0,
2137 modes_of_operation: 0,
2138 modes_of_operation_display: 1, position_actual: 0,
2140 velocity_actual: 0,
2141 error_code: 0,
2142 positive_limit: false,
2143 negative_limit: false,
2144 home_sensor: false,
2145 }
2146 }
2147
2148 fn set_state(&mut self, state: u16) {
2149 self.status_word = state;
2150 }
2151 }
2152
2153 impl AxisView for MockView {
2154 fn control_word(&self) -> u16 { self.control_word }
2155 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
2156 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
2157 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
2158 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
2159 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
2160 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
2161 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
2162 fn status_word(&self) -> u16 { self.status_word }
2163 fn position_actual(&self) -> i32 { self.position_actual }
2164 fn velocity_actual(&self) -> i32 { self.velocity_actual }
2165 fn error_code(&self) -> u16 { self.error_code }
2166 fn positive_limit_active(&self) -> bool { self.positive_limit }
2167 fn negative_limit_active(&self) -> bool { self.negative_limit }
2168 fn home_sensor_active(&self) -> bool { self.home_sensor }
2169 }
2170
2171 fn test_config() -> AxisConfig {
2172 AxisConfig::new(12_800).with_user_scale(360.0)
2173 }
2174
2175 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2177 use tokio::sync::mpsc;
2178 let (write_tx, write_rx) = mpsc::unbounded_channel();
2179 let (response_tx, response_rx) = mpsc::unbounded_channel();
2180 let client = CommandClient::new(write_tx, response_rx);
2181 let axis = Axis::new(test_config(), "TestDrive");
2182 (axis, client, response_tx, write_rx)
2183 }
2184
2185 #[test]
2186 fn axis_config_conversion() {
2187 let cfg = test_config();
2188 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2190 }
2191
2192 #[test]
2193 fn enable_sequence_sets_pp_mode_and_shutdown() {
2194 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2195 let mut view = MockView::new();
2196
2197 axis.enable(&mut view);
2198
2199 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
2201 assert_eq!(view.control_word & 0x008F, 0x0006);
2203 assert_eq!(axis.op, AxisOp::Enabling(1));
2205
2206 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2209
2210 assert_eq!(view.control_word & 0x008F, 0x000F);
2212 assert_eq!(axis.op, AxisOp::Enabling(2));
2213
2214 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2217
2218 assert_eq!(axis.op, AxisOp::Idle);
2220 assert!(axis.motor_on);
2221 }
2222
2223 #[test]
2224 fn move_absolute_sets_target() {
2225 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2226 let mut view = MockView::new();
2227 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2232
2233 assert_eq!(view.target_position, 1600);
2235 assert_eq!(view.profile_velocity, 3200);
2237 assert_eq!(view.profile_acceleration, 6400);
2239 assert_eq!(view.profile_deceleration, 6400);
2240 assert!(view.control_word & (1 << 4) != 0);
2242 assert!(view.control_word & (1 << 6) == 0);
2244 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2246 }
2247
2248 #[test]
2249 fn move_relative_sets_relative_bit() {
2250 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2251 let mut view = MockView::new();
2252 view.set_state(0x0027);
2253 axis.tick(&mut view, &mut client);
2254
2255 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2256
2257 assert!(view.control_word & (1 << 6) != 0);
2259 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1, _, _)));
2260 }
2261
2262 #[test]
2263 fn move_completes_on_target_reached() {
2264 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2265 let mut view = MockView::new();
2266 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2268
2269 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2270
2271 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2274 assert!(view.control_word & (1 << 4) == 0);
2276
2277 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2280 assert_eq!(axis.op, AxisOp::Idle);
2282 assert!(!axis.in_motion);
2283 }
2284
2285 #[test]
2286 fn fault_detected_sets_error() {
2287 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2288 let mut view = MockView::new();
2289 view.set_state(0x0008); view.error_code = 0x1234;
2291
2292 axis.tick(&mut view, &mut client);
2293
2294 assert!(axis.is_error);
2295 assert_eq!(axis.error_code, 0x1234);
2296 assert!(axis.error_message.contains("fault"));
2297 }
2298
2299 #[test]
2300 fn fault_recovery_sequence() {
2301 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2302 let mut view = MockView::new();
2303 view.set_state(0x0008); axis.reset_faults(&mut view);
2306 assert!(view.control_word & 0x0080 == 0);
2308
2309 axis.tick(&mut view, &mut client);
2311 assert!(view.control_word & 0x0080 != 0);
2312
2313 view.set_state(0x0040);
2315 axis.tick(&mut view, &mut client);
2316 assert_eq!(axis.op, AxisOp::Idle);
2317 assert!(!axis.is_error);
2318 }
2319
2320 #[test]
2321 fn reset_faults_software_only_is_synchronous() {
2322 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2328 let mut view = MockView::new();
2329 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2331
2332 axis.set_software_min_limit(207.0);
2334 axis.move_absolute(&mut view, 200.0, 90.0, 180.0, 180.0);
2335 assert!(axis.is_error, "move past software limit must set is_error");
2336 assert!(!axis.is_busy);
2337
2338 let cw_before = view.control_word;
2341
2342 axis.reset_faults(&mut view);
2343
2344 assert!(!axis.is_error);
2346 assert_eq!(axis.error_code, 0);
2347 assert!(axis.error_message.is_empty());
2348 assert!(!axis.is_busy);
2349 assert_eq!(axis.op, AxisOp::Idle);
2350 assert_eq!(view.control_word, cw_before);
2352 }
2353
2354 #[test]
2355 fn disable_sequence() {
2356 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2357 let mut view = MockView::new();
2358 view.set_state(0x0027); axis.disable(&mut view);
2361 assert_eq!(view.control_word & 0x008F, 0x0006);
2363
2364 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2367 assert_eq!(axis.op, AxisOp::Idle);
2368 }
2369
2370 #[test]
2371 fn position_tracks_with_home_offset() {
2372 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2373 let mut view = MockView::new();
2374 view.set_state(0x0027);
2375 view.position_actual = 5000;
2376
2377 axis.enable(&mut view);
2379 view.set_state(0x0021);
2380 axis.tick(&mut view, &mut client);
2381 view.set_state(0x0027);
2382 axis.tick(&mut view, &mut client);
2383
2384 assert_eq!(axis.home_offset, 5000);
2386
2387 assert!((axis.position - 0.0).abs() < 0.01);
2389
2390 view.position_actual = 6600;
2392 axis.tick(&mut view, &mut client);
2393
2394 assert!((axis.position - 45.0).abs() < 0.1);
2396 }
2397
2398 #[test]
2399 fn set_position_adjusts_home_offset() {
2400 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2401 let mut view = MockView::new();
2402 view.position_actual = 3200;
2403
2404 axis.set_position(&view, 90.0);
2405 axis.tick(&mut view, &mut client);
2406
2407 assert_eq!(axis.home_offset, 0);
2409 assert!((axis.position - 90.0).abs() < 0.01);
2410 }
2411
2412 #[test]
2413 fn halt_runs_multi_stage_close_out() {
2414 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2415 let mut view = MockView::new();
2416 view.set_state(0x0027);
2417
2418 axis.halt(&mut view);
2419
2420 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2422 assert!(view.control_word & (1 << 4) == 0, "new_setpoint must be cleared");
2423
2424 assert!(matches!(axis.op, AxisOp::Halting(_)),
2426 "halt should enter Halting state, not Idle");
2427 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2428 assert_eq!(step, HaltState::WaitStopped as u8);
2429
2430 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2437 axis.tick(&mut view, &mut client);
2438 }
2439 assert!(matches!(axis.op, AxisOp::Halting(_)));
2441 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2442 assert_eq!(step, HaltState::WaitCancelAck as u8,
2443 "should advance past WaitStopped once position/velocity is stable");
2444
2445 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2449 }
2450
2451 #[test]
2452 fn is_busy_tracks_operations() {
2453 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2454 let mut view = MockView::new();
2455
2456 axis.tick(&mut view, &mut client);
2458 assert!(!axis.is_busy);
2459
2460 axis.enable(&mut view);
2462 axis.tick(&mut view, &mut client);
2463 assert!(axis.is_busy);
2464
2465 view.set_state(0x0021);
2467 axis.tick(&mut view, &mut client);
2468 view.set_state(0x0027);
2469 axis.tick(&mut view, &mut client);
2470 assert!(!axis.is_busy);
2471
2472 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2474 axis.tick(&mut view, &mut client);
2475 assert!(axis.is_busy);
2476 assert!(axis.in_motion);
2477 }
2478
2479 #[test]
2480 fn fault_during_move_cancels_op() {
2481 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2482 let mut view = MockView::new();
2483 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2485
2486 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2488 axis.tick(&mut view, &mut client);
2489 assert!(axis.is_busy);
2490 assert!(!axis.is_error);
2491
2492 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2495
2496 assert!(!axis.is_busy);
2498 assert!(axis.is_error);
2499 assert_eq!(axis.op, AxisOp::Idle);
2500 }
2501
2502 #[test]
2503 fn move_absolute_rejected_by_max_limit() {
2504 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2505 let mut view = MockView::new();
2506 view.set_state(0x0027);
2507 axis.tick(&mut view, &mut client);
2508
2509 axis.set_software_max_limit(90.0);
2510 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2511
2512 assert!(axis.is_error);
2514 assert_eq!(axis.op, AxisOp::Idle);
2515 assert!(axis.error_message.contains("max software limit"));
2516 }
2517
2518 #[test]
2519 fn move_absolute_rejected_by_min_limit() {
2520 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2521 let mut view = MockView::new();
2522 view.set_state(0x0027);
2523 axis.tick(&mut view, &mut client);
2524
2525 axis.set_software_min_limit(-10.0);
2526 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2527
2528 assert!(axis.is_error);
2529 assert_eq!(axis.op, AxisOp::Idle);
2530 assert!(axis.error_message.contains("min software limit"));
2531 }
2532
2533 #[test]
2534 fn move_relative_rejected_by_max_limit() {
2535 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2536 let mut view = MockView::new();
2537 view.set_state(0x0027);
2538 axis.tick(&mut view, &mut client);
2539
2540 axis.set_software_max_limit(50.0);
2542 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2543
2544 assert!(axis.is_error);
2545 assert_eq!(axis.op, AxisOp::Idle);
2546 assert!(axis.error_message.contains("max software limit"));
2547 }
2548
2549 #[test]
2550 fn move_within_limits_allowed() {
2551 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2552 let mut view = MockView::new();
2553 view.set_state(0x0027);
2554 axis.tick(&mut view, &mut client);
2555
2556 axis.set_software_max_limit(90.0);
2557 axis.set_software_min_limit(-90.0);
2558 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2559
2560 assert!(!axis.is_error);
2562 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2563 }
2564
2565 #[test]
2566 fn runtime_limit_halts_move_in_violated_direction() {
2567 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2568 let mut view = MockView::new();
2569 view.set_state(0x0027);
2570 axis.tick(&mut view, &mut client);
2571
2572 axis.set_software_max_limit(45.0);
2573 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2575
2576 view.position_actual = 1650;
2579 view.velocity_actual = 100; view.status_word = 0x1027;
2583 axis.tick(&mut view, &mut client);
2584 view.status_word = 0x0027;
2585 axis.tick(&mut view, &mut client);
2586
2587 assert!(!axis.is_error);
2591 assert!(axis.at_max_limit);
2592 assert!(axis.is_busy);
2593 assert!(matches!(axis.op, AxisOp::Halting(_)));
2594 assert!(view.control_word & (1 << 8) != 0);
2596 }
2597
2598 #[test]
2599 fn runtime_limit_allows_move_in_opposite_direction() {
2600 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2601 let mut view = MockView::new();
2602 view.set_state(0x0027);
2603 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2606 axis.tick(&mut view, &mut client);
2607 assert!(axis.at_max_limit);
2608
2609 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2611 assert!(!axis.is_error);
2612 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2613
2614 view.velocity_actual = -100;
2616 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2618 assert!(!axis.is_error);
2620 }
2621
2622 #[test]
2623 fn positive_limit_switch_halts_positive_move() {
2624 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2625 let mut view = MockView::new();
2626 view.set_state(0x0027);
2627 axis.tick(&mut view, &mut client);
2628
2629 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2631 view.velocity_actual = 100; view.status_word = 0x1027;
2634 axis.tick(&mut view, &mut client);
2635 view.status_word = 0x0027;
2636
2637 view.positive_limit = true;
2639 axis.tick(&mut view, &mut client);
2640
2641 assert!(!axis.is_error);
2646 assert!(axis.at_positive_limit_switch);
2647 assert!(axis.is_busy);
2648 assert!(matches!(axis.op, AxisOp::Halting(_)));
2649 assert!(view.control_word & (1 << 8) != 0);
2651 }
2652
2653 #[test]
2654 fn negative_limit_switch_halts_negative_move() {
2655 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2656 let mut view = MockView::new();
2657 view.set_state(0x0027);
2658 axis.tick(&mut view, &mut client);
2659
2660 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2662 view.velocity_actual = -100; view.status_word = 0x1027;
2664 axis.tick(&mut view, &mut client);
2665 view.status_word = 0x0027;
2666
2667 view.negative_limit = true;
2669 axis.tick(&mut view, &mut client);
2670
2671 assert!(!axis.is_error);
2676 assert!(axis.at_negative_limit_switch);
2677 assert!(axis.is_busy);
2678 assert!(matches!(axis.op, AxisOp::Halting(_)));
2679 assert!(view.control_word & (1 << 8) != 0);
2680 }
2681
2682 #[test]
2683 fn limit_halt_cleanup_then_next_move_starts_cleanly() {
2684 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2689 let mut view = MockView::new();
2690 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2692
2693 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2695 view.velocity_actual = 100;
2696 view.status_word = 0x1027;
2697 axis.tick(&mut view, &mut client);
2698 view.status_word = 0x0027;
2699 view.positive_limit = true;
2700 axis.tick(&mut view, &mut client);
2701
2702 assert!(!axis.is_error);
2704 assert!(axis.is_busy);
2705 assert!(matches!(axis.op, AxisOp::Halting(_)));
2706 assert!(view.control_word & (1 << 8) != 0);
2707
2708 view.velocity_actual = 0;
2713 view.positive_limit = false;
2714 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2715 axis.tick(&mut view, &mut client);
2716 }
2717 assert!(matches!(axis.op, AxisOp::Halting(_)));
2718 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2719 assert_eq!(step, HaltState::WaitCancelAck as u8);
2720
2721 view.status_word = 0x1027;
2723 axis.tick(&mut view, &mut client);
2724 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2725 assert_eq!(step, HaltState::WaitCancelAckClear as u8);
2726
2727 view.status_word = 0x0027;
2730 axis.tick(&mut view, &mut client);
2731 assert_eq!(axis.op, AxisOp::Idle);
2732 assert!(!axis.is_busy);
2733 assert!(view.control_word & (1 << 8) == 0, "halt should be cleared after close-out");
2734
2735 axis.move_absolute(&mut view, 30.0, 90.0, 180.0, 180.0);
2738 assert!(view.control_word & (1 << 4) != 0, "new-setpoint asserted on start_move tick");
2739 assert!(view.control_word & (1 << 8) == 0, "halt stays cleared");
2740 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2741 }
2742
2743 #[test]
2744 fn limit_switch_allows_move_in_opposite_direction() {
2745 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2746 let mut view = MockView::new();
2747 view.set_state(0x0027);
2748 view.positive_limit = true;
2750 view.velocity_actual = -100;
2751 axis.tick(&mut view, &mut client);
2752 assert!(axis.at_positive_limit_switch);
2753
2754 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2756 view.status_word = 0x1027;
2757 axis.tick(&mut view, &mut client);
2758
2759 assert!(!axis.is_error);
2761 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
2762 }
2763
2764 #[test]
2765 fn limit_switch_ignored_when_not_moving() {
2766 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2767 let mut view = MockView::new();
2768 view.set_state(0x0027);
2769 view.positive_limit = true;
2770
2771 axis.tick(&mut view, &mut client);
2772
2773 assert!(axis.at_positive_limit_switch);
2775 assert!(!axis.is_error);
2776 }
2777
2778 #[test]
2779 fn home_sensor_output_tracks_view() {
2780 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2781 let mut view = MockView::new();
2782 view.set_state(0x0027);
2783
2784 axis.tick(&mut view, &mut client);
2785 assert!(!axis.home_sensor);
2786
2787 view.home_sensor = true;
2788 axis.tick(&mut view, &mut client);
2789 assert!(axis.home_sensor);
2790
2791 view.home_sensor = false;
2792 axis.tick(&mut view, &mut client);
2793 assert!(!axis.home_sensor);
2794 }
2795
2796 #[test]
2797 fn velocity_output_converted() {
2798 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2799 let mut view = MockView::new();
2800 view.set_state(0x0027);
2801 view.velocity_actual = 3200;
2803
2804 axis.tick(&mut view, &mut client);
2805
2806 assert!((axis.speed - 90.0).abs() < 0.1);
2807 assert!(axis.moving_positive);
2808 assert!(!axis.moving_negative);
2809 }
2810
2811 fn soft_homing_config() -> AxisConfig {
2814 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2815 cfg.homing_speed = 10.0;
2816 cfg.homing_accel = 20.0;
2817 cfg.homing_decel = 20.0;
2818 cfg
2819 }
2820
2821 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2822 use tokio::sync::mpsc;
2823 let (write_tx, write_rx) = mpsc::unbounded_channel();
2824 let (response_tx, response_rx) = mpsc::unbounded_channel();
2825 let client = CommandClient::new(write_tx, response_rx);
2826 let axis = Axis::new(soft_homing_config(), "TestDrive");
2827 (axis, client, response_tx, write_rx)
2828 }
2829
2830 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2832 view.set_state(0x0027); axis.tick(view, client);
2834 }
2835
2836 fn complete_soft_homing(
2841 axis: &mut Axis,
2842 view: &mut MockView,
2843 client: &mut CommandClient,
2844 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2845 trigger_pos: i32,
2846 clear_sensor: impl FnOnce(&mut MockView),
2847 ) {
2848 use mechutil::ipc::CommandMessage as IpcMsg;
2849
2850 axis.tick(view, client);
2853 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2854
2855 view.position_actual = trigger_pos + 100;
2857 axis.tick(view, client);
2858 view.position_actual = trigger_pos + 120;
2859 axis.tick(view, client);
2860 for _ in 0..10 { axis.tick(view, client); }
2862 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2863
2864 view.status_word = 0x1027;
2866 axis.tick(view, client);
2867 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2868 view.status_word = 0x0027;
2869
2870 for _ in 0..100 { axis.tick(view, client); }
2872 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2873
2874 axis.tick(view, client);
2877 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2878
2879 view.status_word = 0x1027;
2881 axis.tick(view, client);
2882 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2883 view.status_word = 0x0027;
2884
2885 axis.tick(view, client);
2887 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2888 clear_sensor(view);
2889 view.position_actual = trigger_pos - 200;
2890 axis.tick(view, client);
2891 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2892
2893 axis.tick(view, client);
2895 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2896 for _ in 0..10 { axis.tick(view, client); }
2897 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2898
2899 view.status_word = 0x1027;
2902 axis.tick(view, client);
2903 view.status_word = 0x0027;
2904 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2905
2906 let tid = axis.homing_sdo_tid;
2908 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2909 client.poll();
2910 axis.tick(view, client);
2911 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2912
2913 axis.tick(view, client);
2915 let tid = axis.homing_sdo_tid;
2916 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2917 client.poll();
2918 axis.tick(view, client);
2919 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2920
2921 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2923 axis.tick(view, client);
2924 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2925
2926 view.status_word = 0x1427; axis.tick(view, client);
2929 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2930 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2931 view.status_word = 0x0027;
2932
2933 axis.tick(view, client);
2935 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2936
2937 view.status_word = 0x1027;
2939 axis.tick(view, client);
2940 view.status_word = 0x0027;
2941
2942 assert_eq!(axis.op, AxisOp::Idle);
2943 assert!(!axis.is_busy);
2944 assert!(!axis.is_error);
2945 assert_eq!(axis.home_offset, 0); }
2947
2948 #[test]
2949 fn soft_homing_pnp_home_sensor_full_sequence() {
2950 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2951 let mut view = MockView::new();
2952 enable_axis(&mut axis, &mut view, &mut client);
2953
2954 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2955
2956 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2959 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2961 axis.tick(&mut view, &mut client); view.home_sensor = true;
2965 view.position_actual = 5000;
2966 axis.tick(&mut view, &mut client);
2967 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2968
2969 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2970 |v| { v.home_sensor = false; });
2971 }
2972
2973 #[test]
2974 fn soft_homing_npn_home_sensor_full_sequence() {
2975 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2976 let mut view = MockView::new();
2977 view.home_sensor = true;
2979 enable_axis(&mut axis, &mut view, &mut client);
2980
2981 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2982
2983 axis.tick(&mut view, &mut client);
2985 view.status_word = 0x1027;
2986 axis.tick(&mut view, &mut client);
2987 view.status_word = 0x0027;
2988 axis.tick(&mut view, &mut client);
2989
2990 view.home_sensor = false;
2992 view.position_actual = 3000;
2993 axis.tick(&mut view, &mut client);
2994 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2995
2996 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2997 |v| { v.home_sensor = true; }); }
2999
3000 #[test]
3001 fn soft_homing_limit_switch_suppresses_halt() {
3002 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3003 let mut view = MockView::new();
3004 enable_axis(&mut axis, &mut view, &mut client);
3005
3006 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
3008
3009 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3014 axis.tick(&mut view, &mut client); view.positive_limit = true;
3018 view.velocity_actual = 100; view.position_actual = 8000;
3020 axis.tick(&mut view, &mut client);
3021
3022 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3024 assert!(!axis.is_error);
3025 }
3026
3027 #[test]
3028 fn soft_homing_opposite_limit_still_protects() {
3029 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3030 let mut view = MockView::new();
3031 enable_axis(&mut axis, &mut view, &mut client);
3032
3033 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3035
3036 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3041 axis.tick(&mut view, &mut client); view.negative_limit = true;
3046 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
3048
3049 assert!(axis.is_error);
3051 assert!(axis.error_message.contains("Negative limit switch"));
3052 }
3053
3054 #[test]
3055 #[test]
3073 fn soft_homing_negative_direction_sets_negative_target() {
3074 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3075 let mut view = MockView::new();
3076 enable_axis(&mut axis, &mut view, &mut client);
3077
3078 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
3079 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
3083 }
3084
3085 #[test]
3086 fn home_integrated_method_starts_hardware_homing() {
3087 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3088 let mut view = MockView::new();
3089 enable_axis(&mut axis, &mut view, &mut client);
3090
3091 axis.home(&mut view, HomingMethod::CurrentPosition);
3092 assert!(matches!(axis.op, AxisOp::Homing(0)));
3093 assert_eq!(axis.homing_method, 37);
3094 }
3095
3096 #[test]
3097 fn home_integrated_arbitrary_code() {
3098 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3099 let mut view = MockView::new();
3100 enable_axis(&mut axis, &mut view, &mut client);
3101
3102 axis.home(&mut view, HomingMethod::Integrated(35));
3103 assert!(matches!(axis.op, AxisOp::Homing(0)));
3104 assert_eq!(axis.homing_method, 35);
3105 }
3106
3107 #[test]
3108 fn hardware_homing_skips_speed_sdos_when_zero() {
3109 use mechutil::ipc::CommandMessage;
3110
3111 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
3112 let mut view = MockView::new();
3113 enable_axis(&mut axis, &mut view, &mut client);
3114
3115 axis.home(&mut view, HomingMethod::Integrated(37));
3117
3118 axis.tick(&mut view, &mut client);
3120 assert!(matches!(axis.op, AxisOp::Homing(1)));
3121
3122 let _ = write_rx.try_recv();
3124
3125 let tid = axis.homing_sdo_tid;
3127 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3128 client.poll();
3129 axis.tick(&mut view, &mut client);
3130
3131 assert!(matches!(axis.op, AxisOp::Homing(8)));
3133 }
3134
3135 #[test]
3136 fn hardware_homing_writes_speed_sdos_when_nonzero() {
3137 use mechutil::ipc::CommandMessage;
3138
3139 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3140 let mut view = MockView::new();
3141 enable_axis(&mut axis, &mut view, &mut client);
3142
3143 axis.home(&mut view, HomingMethod::Integrated(37));
3145
3146 axis.tick(&mut view, &mut client);
3148 assert!(matches!(axis.op, AxisOp::Homing(1)));
3149 let _ = write_rx.try_recv();
3150
3151 let tid = axis.homing_sdo_tid;
3153 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3154 client.poll();
3155 axis.tick(&mut view, &mut client);
3156 assert!(matches!(axis.op, AxisOp::Homing(2)));
3158 }
3159
3160 #[test]
3161 fn soft_homing_edge_during_ack_step() {
3162 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3163 let mut view = MockView::new();
3164 enable_axis(&mut axis, &mut view, &mut client);
3165
3166 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3167 axis.tick(&mut view, &mut client); view.home_sensor = true;
3171 view.position_actual = 2000;
3172 axis.tick(&mut view, &mut client);
3173
3174 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3176 }
3177
3178 #[test]
3179 fn soft_homing_applies_home_position() {
3180 let mut cfg = soft_homing_config();
3181 cfg.home_position = 90.0;
3182
3183 use tokio::sync::mpsc;
3184 let (write_tx, _write_rx) = mpsc::unbounded_channel();
3185 let (resp_tx, response_rx) = mpsc::unbounded_channel();
3186 let mut client = CommandClient::new(write_tx, response_rx);
3187 let mut axis = Axis::new(cfg, "TestDrive");
3188
3189 let mut view = MockView::new();
3190 enable_axis(&mut axis, &mut view, &mut client);
3191
3192 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3193
3194 axis.tick(&mut view, &mut client);
3196 view.status_word = 0x1027;
3197 axis.tick(&mut view, &mut client);
3198 view.status_word = 0x0027;
3199 axis.tick(&mut view, &mut client);
3200
3201 view.home_sensor = true;
3203 view.position_actual = 5000;
3204 axis.tick(&mut view, &mut client);
3205 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3206
3207 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3209 |v| { v.home_sensor = false; });
3210
3211 assert_eq!(axis.home_offset, 0);
3213 }
3214
3215 #[test]
3216 fn soft_homing_default_home_position_zero() {
3217 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3218 let mut view = MockView::new();
3219 enable_axis(&mut axis, &mut view, &mut client);
3220
3221 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3222
3223 axis.tick(&mut view, &mut client);
3225 view.status_word = 0x1027;
3226 axis.tick(&mut view, &mut client);
3227 view.status_word = 0x0027;
3228 axis.tick(&mut view, &mut client);
3229
3230 view.home_sensor = true;
3232 view.position_actual = 5000;
3233 axis.tick(&mut view, &mut client);
3234
3235 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3236 |v| { v.home_sensor = false; });
3237
3238 assert_eq!(axis.home_offset, 0);
3239 }
3240}