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().clear_halt_during_cancel {
1455 cw.set_bit(8, false); }
1458
1459 view.set_control_word(cw.raw());
1460
1461 let current_pos = view.position_actual();
1462 view.set_target_position(current_pos);
1463 view.set_profile_velocity(0);
1464 }
1465
1466
1467 fn command_homing_speed(&self, view: &mut impl AxisView) {
1469 let cpu = self.config.counts_per_user();
1470 let vel = (self.config.homing_speed * cpu).round() as u32;
1471 let accel = (self.config.homing_accel * cpu).round() as u32;
1472 let decel = (self.config.homing_decel * cpu).round() as u32;
1473 view.set_profile_velocity(vel);
1474 view.set_profile_acceleration(accel);
1475 view.set_profile_deceleration(decel);
1476 }
1477
1478 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1494 match HomeState::from_repr(step) {
1495
1496 Some(HomeState::EnsurePpMode) => {
1497 log::info!("SoftHome: Ensuring PP mode..");
1502 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1503 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1504 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1505 },
1506 Some(HomeState::WaitPpMode) => {
1507
1508 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1509 if !self.fb_mode_of_operation.is_busy() {
1510 if self.fb_mode_of_operation.is_error() {
1511 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1512 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1513 ));
1514 }
1515 else {
1516 log::info!("SoftHome: Drive is in PP mode!");
1517
1518 if !self.check_soft_home_trigger(view) {
1522 log::info!("SoftHome: Not on home switch; seek out.");
1523 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1524 } else {
1525 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1526 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1527 }
1528 }
1529 }
1530
1531
1532 },
1533
1534 Some(HomeState::Search) => {
1536 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1537
1538 let target = self.calculate_max_relative_target(self.soft_home_direction);
1548 view.set_target_position(target);
1549
1550 self.command_homing_speed(view);
1559
1560 let mut cw = RawControlWord(view.control_word());
1561 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());
1566
1567 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1568 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1569 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1570 }
1571 Some(HomeState::WaitSearching) => {
1572 if self.check_soft_home_trigger(view) {
1573 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1574 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1575 return;
1576 }
1577 let sw = RawStatusWord(view.status_word());
1578 if sw.raw() & (1 << 12) != 0 {
1579 let mut cw = RawControlWord(view.control_word());
1580 cw.set_bit(4, false);
1581 view.set_control_word(cw.raw());
1582 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1583 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1584 } else if self.homing_timed_out() {
1585 self.set_op_error("Software homing timeout: set-point not acknowledged");
1586 }
1587 }
1588 Some(HomeState::WaitFoundSensor) => {
1598 if self.check_soft_home_trigger(view) {
1599 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1600 log::info!("ControlWord is : {} ", view.control_word());
1601
1602 let mut cw = RawControlWord(view.control_word());
1603 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1606
1607
1608 self.halt_stable_count = 0;
1609 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1610 } else if self.homing_timed_out() {
1611 self.set_op_error("Software homing timeout: sensor not detected");
1612 }
1613 }
1614
1615
1616 Some(HomeState::WaitStoppedFoundSensor) => {
1617 const STABLE_WINDOW: i32 = 1;
1618 const STABLE_TICKS_REQUIRED: u8 = 10;
1619
1620 let pos = view.position_actual();
1625 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1626 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1627 } else {
1628 self.halt_stable_count = 0;
1629 }
1630
1631 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1632
1633 log::info!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1634 self.command_cancel_move(view);
1635 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1636
1637 } else if self.homing_timed_out() {
1638 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1639 }
1640 }
1641 Some(HomeState::WaitFoundSensorAck) => {
1642 let sw = RawStatusWord(view.status_word());
1643 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1644
1645 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1646
1647 let mut cw = RawControlWord(view.control_word());
1649 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1652
1653 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1654
1655 } else if self.homing_timed_out() {
1656 self.set_op_error("Software homing timeout: cancel not acknowledged");
1657 }
1658 },
1659 Some(HomeState::WaitFoundSensorAckClear) => {
1660 let sw = RawStatusWord(view.status_word());
1661 if sw.raw() & (1 << 12) == 0 {
1663
1664 self.command_clear_halt(view);
1666
1667 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1668 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1669 self.ton.call(false, Duration::from_secs(3));
1670 }
1671 },
1672 Some(HomeState::DebounceFoundSensor) => {
1674 self.ton.call(true, Duration::from_secs(3));
1675
1676 let sw = RawStatusWord(view.status_word());
1677 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1678 self.ton.call(false, Duration::from_secs(3));
1679 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1680 view.position_actual(), view.control_word(), view.status_word());
1681 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1682 }
1683 }
1684
1685 Some(HomeState::BackOff) => {
1687
1688 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1689 view.set_target_position(target);
1690
1691
1692 self.command_homing_speed(view);
1693
1694 let mut cw = RawControlWord(view.control_word());
1695 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1699 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1700 target, self.config.homing_speed, view.position_actual(), cw.raw());
1701 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1702 }
1703 Some(HomeState::WaitBackingOff) => {
1704 let sw = RawStatusWord(view.status_word());
1705 if sw.raw() & (1 << 12) != 0 {
1706 let mut cw = RawControlWord(view.control_word());
1707 cw.set_bit(4, false);
1708 view.set_control_word(cw.raw());
1709 log::info!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1710 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1711 } else if self.homing_timed_out() {
1712 self.set_op_error("Software homing timeout: back-off not acknowledged");
1713 }
1714 }
1715 Some(HomeState::WaitLostSensor) => {
1716 if !self.check_soft_home_trigger(view) {
1717 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1718
1719 self.command_halt(view);
1720 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1721 } else if self.homing_timed_out() {
1722 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1723 }
1724 }
1725 Some(HomeState::WaitStoppedLostSensor) => {
1726 const STABLE_WINDOW: i32 = 1;
1727 const STABLE_TICKS_REQUIRED: u8 = 10;
1728
1729 let pos = view.position_actual();
1734 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1735 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1736 } else {
1737 self.halt_stable_count = 0;
1738 }
1739
1740 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1741 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1742 self.command_cancel_move(view);
1743 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1744 } else if self.homing_timed_out() {
1745 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1746 }
1747 }
1748 Some(HomeState::WaitLostSensorAck) => {
1749 let sw = RawStatusWord(view.status_word());
1750 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1751
1752 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1753
1754 let mut cw = RawControlWord(view.control_word());
1756 cw.set_bit(4, false); view.set_control_word(cw.raw());
1758
1759 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1760
1761
1762 } else if self.homing_timed_out() {
1763 self.set_op_error("Software homing timeout: cancel not acknowledged");
1764 }
1765 }
1766 Some(HomeState::WaitLostSensorAckClear) => {
1767 let sw = RawStatusWord(view.status_word());
1769 if sw.raw() & (1 << 12) == 0 {
1770
1771 self.command_clear_halt(view);
1772
1773 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1774 self.homing_sdo_tid = self.sdo.write(
1777 client, 0x607C, 0, json!(desired_counts),
1778 );
1779
1780 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {} [{} counts].",
1781 self.config.home_position, desired_counts
1782 );
1783
1784 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1785
1786 }
1787 },
1788
1789 Some(HomeState::WaitHomeOffsetDone) => {
1790 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1792 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8); }
1793 SdoResult::Pending => {
1794 if self.homing_timed_out() {
1795 self.set_op_error("Software homing timeout: home offset SDO write");
1796 }
1797 }
1798 SdoResult::Err(e) => {
1799 self.set_op_error(&format!("Software homing SDO error: {}", e));
1800 }
1801 SdoResult::Timeout => {
1802 self.set_op_error("Software homing: home offset SDO timed out");
1803 }
1804 }
1805 },
1806 Some(HomeState::WriteHomingModeOp) => {
1807
1808 self.fb_mode_of_operation.reset();
1812 self.fb_mode_of_operation.start(ModesOfOperation::Homing as i8);
1813 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1814 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
1815
1816
1817 },
1818 Some(HomeState::WaitWriteHomingModeOp) => {
1819 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1821
1822 if !self.fb_mode_of_operation.is_busy() {
1823 if self.fb_mode_of_operation.is_error() {
1824 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1825 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1826 ));
1827 }
1828 else {
1829 log::info!("SoftHome: Drive is now in Homing Mode.");
1830 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
1831 }
1832 }
1833 },
1834 Some(HomeState::WriteHomingMethod) => {
1835 self.homing_sdo_tid = self.sdo.write(
1838 client, 0x6098, 0, json!(self.config.soft_home_method),
1839 );
1840 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1841 }
1842 Some(HomeState::WaitWriteHomingMethodDone) => {
1843 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1845 SdoResult::Ok(_) => {
1846 log::info!("SoftHome: Successfully wrote homing method.");
1847 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
1848 }
1849 SdoResult::Pending => {
1850 if self.homing_timed_out() {
1851 self.restore_pp_after_error("Software homing timeout: homing method SDO write");
1852 }
1853 }
1854 SdoResult::Err(e) => {
1855 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
1856 }
1857 SdoResult::Timeout => {
1858 self.restore_pp_after_error("Software homing: homing method SDO timed out");
1859 }
1860 }
1861 }
1862 Some(HomeState::ClearHomingTrigger) => {
1863 let mut cw = RawControlWord(view.control_word());
1866 cw.set_bit(4, false);
1867 view.set_control_word(cw.raw());
1868 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
1869 }
1870 Some(HomeState::TriggerHoming) => {
1871 let mut cw = RawControlWord(view.control_word());
1873 cw.set_bit(4, true);
1874 view.set_control_word(cw.raw());
1875 log::info!("SoftHome[TriggerHoming]: start homing");
1876 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
1877 }
1878 Some(HomeState::WaitHomingStarted) => {
1879 let sw = view.status_word();
1885 let error = sw & (1 << 13) != 0;
1886 if error {
1887 self.restore_pp_after_error("Software homing: drive reported homing error");
1888 } else if sw & (1 << 12) == 0 {
1889 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1890 } else if self.homing_timed_out() {
1891 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
1892 }
1893 }
1894 Some(HomeState::WaitHomingDone) => {
1895 let sw = view.status_word();
1897 let error = sw & (1 << 13) != 0;
1898 let attained = sw & (1 << 12) != 0;
1899 let reached = sw & (1 << 10) != 0;
1900
1901 if error {
1902 self.restore_pp_after_error("Software homing: drive reported homing error");
1903 } else if attained && reached {
1904 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X})", sw);
1905 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
1906 } else if self.homing_timed_out() {
1907 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
1908 }
1909 }
1910 Some(HomeState::ResetHomingTrigger) => {
1911 let mut cw = RawControlWord(view.control_word());
1916 cw.set_bit(4, false);
1917 view.set_control_word(cw.raw());
1918 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
1919 }
1920 Some(HomeState::WaitHomingTriggerCleared) => {
1921 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
1925 }
1926
1927
1928 Some(HomeState::WriteMotionModeOfOperation) => {
1929
1930 self.fb_mode_of_operation.reset();
1933 self.fb_mode_of_operation.start(ModesOfOperation::ProfilePosition as i8);
1934 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1935 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
1936
1937 },
1938 Some(HomeState::WaitWriteMotionModeOfOperation) => {
1939 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1941
1942 if !self.fb_mode_of_operation.is_busy() {
1943 if self.fb_mode_of_operation.is_error() {
1944 self.set_op_error(&format!("Software homing SDO error writing homing mode of operation: {} {}",
1945 self.fb_mode_of_operation.error_code(), self.fb_mode_of_operation.error_message()
1946 ));
1947 }
1948 else {
1949 if self.is_error {
1950 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
1951 self.finish_op_error();
1952 }
1953 else {
1954 self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1957 }
1958
1959 }
1960 }
1961 },
1962
1963 Some(HomeState::SendCurrentPositionTarget) => {
1964 let current_pos = view.position_actual();
1966 view.set_target_position(current_pos);
1967 view.set_profile_velocity(0);
1968 let mut cw = RawControlWord(view.control_word());
1969 cw.set_bit(4, true);
1970 cw.set_bit(5, true);
1971 cw.set_bit(6, false); view.set_control_word(cw.raw());
1973 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1974 }
1975 Some(HomeState::WaitCurrentPositionTargetSent) => {
1976 let sw = RawStatusWord(view.status_word());
1978 if sw.raw() & (1 << 12) != 0 {
1979 let mut cw = RawControlWord(view.control_word());
1980 cw.set_bit(4, false);
1981 view.set_control_word(cw.raw());
1982 log::info!("Software homing complete — position set to {} user units",
1983 self.config.home_position);
1984 self.complete_op();
1985 } else if self.homing_timed_out() {
1986 self.set_op_error("Software homing timeout: hold position not acknowledged");
1987 }
1988 }
1989 _ => self.complete_op(),
1990 }
1991 }
1992
1993 fn tick_halting(&mut self, view: &mut impl AxisView, step: u8) {
2006 match HaltState::from_repr(step) {
2007 Some(HaltState::WaitStopped) => {
2008 let pos = view.position_actual();
2012 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
2013
2014 let vel = view.velocity_actual().abs();
2015 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
2016
2017 if pos_stable || vel_stopped {
2022 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2023 } else {
2024 self.halt_stable_count = 0;
2025 }
2026
2027 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
2028 self.command_cancel_move(view);
2029 self.op_started = Some(Instant::now());
2030 self.op = AxisOp::Halting(HaltState::WaitCancelAck as u8);
2031 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2032 self.set_op_error("Halt timeout: motor did not stop");
2033 }
2034 }
2035 Some(HaltState::WaitCancelAck) => {
2036 let sw = RawStatusWord(view.status_word());
2037 let setpoint_ack = sw.raw() & (1 << 12) != 0;
2038 if setpoint_ack {
2040 let mut cw = RawControlWord(view.control_word());
2043 cw.set_bit(4, false);
2044 cw.set_bit(5, true);
2045 view.set_control_word(cw.raw());
2046 self.op_started = Some(Instant::now());
2047 self.op = AxisOp::Halting(HaltState::WaitCancelAckClear as u8);
2048 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2049 self.set_op_error("Halt timeout: cancel not acknowledged");
2050 }
2051 }
2052 Some(HaltState::WaitCancelAckClear) => {
2053 let sw = RawStatusWord(view.status_word());
2054 if sw.raw() & (1 << 12) == 0 {
2055 self.command_clear_halt(view);
2057 self.complete_op();
2058 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2059 self.set_op_error("Halt timeout: ack did not clear");
2060 }
2061 }
2062 None => {
2063 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
2064 self.complete_op();
2065 }
2066 }
2067 }
2068
2069 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
2074 match step {
2075 1 => {
2076 let mut cw = RawControlWord(view.control_word());
2078 cw.cmd_fault_reset();
2079 view.set_control_word(cw.raw());
2080 self.op = AxisOp::FaultRecovery(2);
2081 }
2082 2 => {
2083 let sw = RawStatusWord(view.status_word());
2085 let state = sw.state();
2086 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
2087 log::info!("Fault cleared (drive state: {})", state);
2088 self.complete_op();
2089 } else if self.op_timed_out() {
2090 self.set_op_error("Fault reset timeout: drive still faulted");
2091 }
2092 }
2093 _ => self.complete_op(),
2094 }
2095 }
2096}
2097
2098#[cfg(test)]
2103mod tests {
2104 use super::*;
2105
2106 struct MockView {
2108 control_word: u16,
2109 status_word: u16,
2110 target_position: i32,
2111 profile_velocity: u32,
2112 profile_acceleration: u32,
2113 profile_deceleration: u32,
2114 modes_of_operation: i8,
2115 modes_of_operation_display: i8,
2116 position_actual: i32,
2117 velocity_actual: i32,
2118 error_code: u16,
2119 positive_limit: bool,
2120 negative_limit: bool,
2121 home_sensor: bool,
2122 }
2123
2124 impl MockView {
2125 fn new() -> Self {
2126 Self {
2127 control_word: 0,
2128 status_word: 0x0040, target_position: 0,
2130 profile_velocity: 0,
2131 profile_acceleration: 0,
2132 profile_deceleration: 0,
2133 modes_of_operation: 0,
2134 modes_of_operation_display: 1, position_actual: 0,
2136 velocity_actual: 0,
2137 error_code: 0,
2138 positive_limit: false,
2139 negative_limit: false,
2140 home_sensor: false,
2141 }
2142 }
2143
2144 fn set_state(&mut self, state: u16) {
2145 self.status_word = state;
2146 }
2147 }
2148
2149 impl AxisView for MockView {
2150 fn control_word(&self) -> u16 { self.control_word }
2151 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
2152 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
2153 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
2154 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
2155 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
2156 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
2157 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
2158 fn status_word(&self) -> u16 { self.status_word }
2159 fn position_actual(&self) -> i32 { self.position_actual }
2160 fn velocity_actual(&self) -> i32 { self.velocity_actual }
2161 fn error_code(&self) -> u16 { self.error_code }
2162 fn positive_limit_active(&self) -> bool { self.positive_limit }
2163 fn negative_limit_active(&self) -> bool { self.negative_limit }
2164 fn home_sensor_active(&self) -> bool { self.home_sensor }
2165 }
2166
2167 fn test_config() -> AxisConfig {
2168 AxisConfig::new(12_800).with_user_scale(360.0)
2169 }
2170
2171 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2173 use tokio::sync::mpsc;
2174 let (write_tx, write_rx) = mpsc::unbounded_channel();
2175 let (response_tx, response_rx) = mpsc::unbounded_channel();
2176 let client = CommandClient::new(write_tx, response_rx);
2177 let axis = Axis::new(test_config(), "TestDrive");
2178 (axis, client, response_tx, write_rx)
2179 }
2180
2181 #[test]
2182 fn axis_config_conversion() {
2183 let cfg = test_config();
2184 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2186 }
2187
2188 #[test]
2189 fn enable_sequence_sets_pp_mode_and_shutdown() {
2190 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2191 let mut view = MockView::new();
2192
2193 axis.enable(&mut view);
2194
2195 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
2197 assert_eq!(view.control_word & 0x008F, 0x0006);
2199 assert_eq!(axis.op, AxisOp::Enabling(1));
2201
2202 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2205
2206 assert_eq!(view.control_word & 0x008F, 0x000F);
2208 assert_eq!(axis.op, AxisOp::Enabling(2));
2209
2210 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2213
2214 assert_eq!(axis.op, AxisOp::Idle);
2216 assert!(axis.motor_on);
2217 }
2218
2219 #[test]
2220 fn move_absolute_sets_target() {
2221 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2222 let mut view = MockView::new();
2223 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2228
2229 assert_eq!(view.target_position, 1600);
2231 assert_eq!(view.profile_velocity, 3200);
2233 assert_eq!(view.profile_acceleration, 6400);
2235 assert_eq!(view.profile_deceleration, 6400);
2236 assert!(view.control_word & (1 << 4) != 0);
2238 assert!(view.control_word & (1 << 6) == 0);
2240 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2242 }
2243
2244 #[test]
2245 fn move_relative_sets_relative_bit() {
2246 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2247 let mut view = MockView::new();
2248 view.set_state(0x0027);
2249 axis.tick(&mut view, &mut client);
2250
2251 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2252
2253 assert!(view.control_word & (1 << 6) != 0);
2255 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1, _, _)));
2256 }
2257
2258 #[test]
2259 fn move_completes_on_target_reached() {
2260 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2261 let mut view = MockView::new();
2262 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2264
2265 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2266
2267 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2270 assert!(view.control_word & (1 << 4) == 0);
2272
2273 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2276 assert_eq!(axis.op, AxisOp::Idle);
2278 assert!(!axis.in_motion);
2279 }
2280
2281 #[test]
2282 fn fault_detected_sets_error() {
2283 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2284 let mut view = MockView::new();
2285 view.set_state(0x0008); view.error_code = 0x1234;
2287
2288 axis.tick(&mut view, &mut client);
2289
2290 assert!(axis.is_error);
2291 assert_eq!(axis.error_code, 0x1234);
2292 assert!(axis.error_message.contains("fault"));
2293 }
2294
2295 #[test]
2296 fn fault_recovery_sequence() {
2297 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2298 let mut view = MockView::new();
2299 view.set_state(0x0008); axis.reset_faults(&mut view);
2302 assert!(view.control_word & 0x0080 == 0);
2304
2305 axis.tick(&mut view, &mut client);
2307 assert!(view.control_word & 0x0080 != 0);
2308
2309 view.set_state(0x0040);
2311 axis.tick(&mut view, &mut client);
2312 assert_eq!(axis.op, AxisOp::Idle);
2313 assert!(!axis.is_error);
2314 }
2315
2316 #[test]
2317 fn reset_faults_software_only_is_synchronous() {
2318 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2324 let mut view = MockView::new();
2325 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2327
2328 axis.set_software_min_limit(207.0);
2330 axis.move_absolute(&mut view, 200.0, 90.0, 180.0, 180.0);
2331 assert!(axis.is_error, "move past software limit must set is_error");
2332 assert!(!axis.is_busy);
2333
2334 let cw_before = view.control_word;
2337
2338 axis.reset_faults(&mut view);
2339
2340 assert!(!axis.is_error);
2342 assert_eq!(axis.error_code, 0);
2343 assert!(axis.error_message.is_empty());
2344 assert!(!axis.is_busy);
2345 assert_eq!(axis.op, AxisOp::Idle);
2346 assert_eq!(view.control_word, cw_before);
2348 }
2349
2350 #[test]
2351 fn disable_sequence() {
2352 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2353 let mut view = MockView::new();
2354 view.set_state(0x0027); axis.disable(&mut view);
2357 assert_eq!(view.control_word & 0x008F, 0x0006);
2359
2360 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2363 assert_eq!(axis.op, AxisOp::Idle);
2364 }
2365
2366 #[test]
2367 fn position_tracks_with_home_offset() {
2368 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2369 let mut view = MockView::new();
2370 view.set_state(0x0027);
2371 view.position_actual = 5000;
2372
2373 axis.enable(&mut view);
2375 view.set_state(0x0021);
2376 axis.tick(&mut view, &mut client);
2377 view.set_state(0x0027);
2378 axis.tick(&mut view, &mut client);
2379
2380 assert_eq!(axis.home_offset, 5000);
2382
2383 assert!((axis.position - 0.0).abs() < 0.01);
2385
2386 view.position_actual = 6600;
2388 axis.tick(&mut view, &mut client);
2389
2390 assert!((axis.position - 45.0).abs() < 0.1);
2392 }
2393
2394 #[test]
2395 fn set_position_adjusts_home_offset() {
2396 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2397 let mut view = MockView::new();
2398 view.position_actual = 3200;
2399
2400 axis.set_position(&view, 90.0);
2401 axis.tick(&mut view, &mut client);
2402
2403 assert_eq!(axis.home_offset, 0);
2405 assert!((axis.position - 90.0).abs() < 0.01);
2406 }
2407
2408 #[test]
2409 fn halt_runs_multi_stage_close_out() {
2410 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2411 let mut view = MockView::new();
2412 view.set_state(0x0027);
2413
2414 axis.halt(&mut view);
2415
2416 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2418 assert!(view.control_word & (1 << 4) == 0, "new_setpoint must be cleared");
2419
2420 assert!(matches!(axis.op, AxisOp::Halting(_)),
2422 "halt should enter Halting state, not Idle");
2423 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2424 assert_eq!(step, HaltState::WaitStopped as u8);
2425
2426 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2433 axis.tick(&mut view, &mut client);
2434 }
2435 assert!(matches!(axis.op, AxisOp::Halting(_)));
2437 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2438 assert_eq!(step, HaltState::WaitCancelAck as u8,
2439 "should advance past WaitStopped once position/velocity is stable");
2440
2441 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2445 }
2446
2447 #[test]
2448 fn is_busy_tracks_operations() {
2449 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2450 let mut view = MockView::new();
2451
2452 axis.tick(&mut view, &mut client);
2454 assert!(!axis.is_busy);
2455
2456 axis.enable(&mut view);
2458 axis.tick(&mut view, &mut client);
2459 assert!(axis.is_busy);
2460
2461 view.set_state(0x0021);
2463 axis.tick(&mut view, &mut client);
2464 view.set_state(0x0027);
2465 axis.tick(&mut view, &mut client);
2466 assert!(!axis.is_busy);
2467
2468 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2470 axis.tick(&mut view, &mut client);
2471 assert!(axis.is_busy);
2472 assert!(axis.in_motion);
2473 }
2474
2475 #[test]
2476 fn fault_during_move_cancels_op() {
2477 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2478 let mut view = MockView::new();
2479 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2481
2482 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2484 axis.tick(&mut view, &mut client);
2485 assert!(axis.is_busy);
2486 assert!(!axis.is_error);
2487
2488 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2491
2492 assert!(!axis.is_busy);
2494 assert!(axis.is_error);
2495 assert_eq!(axis.op, AxisOp::Idle);
2496 }
2497
2498 #[test]
2499 fn move_absolute_rejected_by_max_limit() {
2500 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2501 let mut view = MockView::new();
2502 view.set_state(0x0027);
2503 axis.tick(&mut view, &mut client);
2504
2505 axis.set_software_max_limit(90.0);
2506 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2507
2508 assert!(axis.is_error);
2510 assert_eq!(axis.op, AxisOp::Idle);
2511 assert!(axis.error_message.contains("max software limit"));
2512 }
2513
2514 #[test]
2515 fn move_absolute_rejected_by_min_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_min_limit(-10.0);
2522 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2523
2524 assert!(axis.is_error);
2525 assert_eq!(axis.op, AxisOp::Idle);
2526 assert!(axis.error_message.contains("min software limit"));
2527 }
2528
2529 #[test]
2530 fn move_relative_rejected_by_max_limit() {
2531 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2532 let mut view = MockView::new();
2533 view.set_state(0x0027);
2534 axis.tick(&mut view, &mut client);
2535
2536 axis.set_software_max_limit(50.0);
2538 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2539
2540 assert!(axis.is_error);
2541 assert_eq!(axis.op, AxisOp::Idle);
2542 assert!(axis.error_message.contains("max software limit"));
2543 }
2544
2545 #[test]
2546 fn move_within_limits_allowed() {
2547 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2548 let mut view = MockView::new();
2549 view.set_state(0x0027);
2550 axis.tick(&mut view, &mut client);
2551
2552 axis.set_software_max_limit(90.0);
2553 axis.set_software_min_limit(-90.0);
2554 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2555
2556 assert!(!axis.is_error);
2558 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2559 }
2560
2561 #[test]
2562 fn runtime_limit_halts_move_in_violated_direction() {
2563 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2564 let mut view = MockView::new();
2565 view.set_state(0x0027);
2566 axis.tick(&mut view, &mut client);
2567
2568 axis.set_software_max_limit(45.0);
2569 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2571
2572 view.position_actual = 1650;
2575 view.velocity_actual = 100; view.status_word = 0x1027;
2579 axis.tick(&mut view, &mut client);
2580 view.status_word = 0x0027;
2581 axis.tick(&mut view, &mut client);
2582
2583 assert!(!axis.is_error);
2587 assert!(axis.at_max_limit);
2588 assert!(axis.is_busy);
2589 assert!(matches!(axis.op, AxisOp::Halting(_)));
2590 assert!(view.control_word & (1 << 8) != 0);
2592 }
2593
2594 #[test]
2595 fn runtime_limit_allows_move_in_opposite_direction() {
2596 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2597 let mut view = MockView::new();
2598 view.set_state(0x0027);
2599 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2602 axis.tick(&mut view, &mut client);
2603 assert!(axis.at_max_limit);
2604
2605 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2607 assert!(!axis.is_error);
2608 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2609
2610 view.velocity_actual = -100;
2612 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2614 assert!(!axis.is_error);
2616 }
2617
2618 #[test]
2619 fn positive_limit_switch_halts_positive_move() {
2620 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2621 let mut view = MockView::new();
2622 view.set_state(0x0027);
2623 axis.tick(&mut view, &mut client);
2624
2625 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2627 view.velocity_actual = 100; view.status_word = 0x1027;
2630 axis.tick(&mut view, &mut client);
2631 view.status_word = 0x0027;
2632
2633 view.positive_limit = true;
2635 axis.tick(&mut view, &mut client);
2636
2637 assert!(!axis.is_error);
2642 assert!(axis.at_positive_limit_switch);
2643 assert!(axis.is_busy);
2644 assert!(matches!(axis.op, AxisOp::Halting(_)));
2645 assert!(view.control_word & (1 << 8) != 0);
2647 }
2648
2649 #[test]
2650 fn negative_limit_switch_halts_negative_move() {
2651 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2652 let mut view = MockView::new();
2653 view.set_state(0x0027);
2654 axis.tick(&mut view, &mut client);
2655
2656 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
2658 view.velocity_actual = -100; view.status_word = 0x1027;
2660 axis.tick(&mut view, &mut client);
2661 view.status_word = 0x0027;
2662
2663 view.negative_limit = true;
2665 axis.tick(&mut view, &mut client);
2666
2667 assert!(!axis.is_error);
2672 assert!(axis.at_negative_limit_switch);
2673 assert!(axis.is_busy);
2674 assert!(matches!(axis.op, AxisOp::Halting(_)));
2675 assert!(view.control_word & (1 << 8) != 0);
2676 }
2677
2678 #[test]
2679 fn limit_halt_cleanup_then_next_move_starts_cleanly() {
2680 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2685 let mut view = MockView::new();
2686 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2688
2689 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2691 view.velocity_actual = 100;
2692 view.status_word = 0x1027;
2693 axis.tick(&mut view, &mut client);
2694 view.status_word = 0x0027;
2695 view.positive_limit = true;
2696 axis.tick(&mut view, &mut client);
2697
2698 assert!(!axis.is_error);
2700 assert!(axis.is_busy);
2701 assert!(matches!(axis.op, AxisOp::Halting(_)));
2702 assert!(view.control_word & (1 << 8) != 0);
2703
2704 view.velocity_actual = 0;
2709 view.positive_limit = false;
2710 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2711 axis.tick(&mut view, &mut client);
2712 }
2713 assert!(matches!(axis.op, AxisOp::Halting(_)));
2714 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2715 assert_eq!(step, HaltState::WaitCancelAck as u8);
2716
2717 view.status_word = 0x1027;
2719 axis.tick(&mut view, &mut client);
2720 let AxisOp::Halting(step) = axis.op.clone() else { unreachable!() };
2721 assert_eq!(step, HaltState::WaitCancelAckClear as u8);
2722
2723 view.status_word = 0x0027;
2726 axis.tick(&mut view, &mut client);
2727 assert_eq!(axis.op, AxisOp::Idle);
2728 assert!(!axis.is_busy);
2729 assert!(view.control_word & (1 << 8) == 0, "halt should be cleared after close-out");
2730
2731 axis.move_absolute(&mut view, 30.0, 90.0, 180.0, 180.0);
2734 assert!(view.control_word & (1 << 4) != 0, "new-setpoint asserted on start_move tick");
2735 assert!(view.control_word & (1 << 8) == 0, "halt stays cleared");
2736 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1, _, _)));
2737 }
2738
2739 #[test]
2740 fn limit_switch_allows_move_in_opposite_direction() {
2741 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2742 let mut view = MockView::new();
2743 view.set_state(0x0027);
2744 view.positive_limit = true;
2746 view.velocity_actual = -100;
2747 axis.tick(&mut view, &mut client);
2748 assert!(axis.at_positive_limit_switch);
2749
2750 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2752 view.status_word = 0x1027;
2753 axis.tick(&mut view, &mut client);
2754
2755 assert!(!axis.is_error);
2757 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
2758 }
2759
2760 #[test]
2761 fn limit_switch_ignored_when_not_moving() {
2762 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2763 let mut view = MockView::new();
2764 view.set_state(0x0027);
2765 view.positive_limit = true;
2766
2767 axis.tick(&mut view, &mut client);
2768
2769 assert!(axis.at_positive_limit_switch);
2771 assert!(!axis.is_error);
2772 }
2773
2774 #[test]
2775 fn home_sensor_output_tracks_view() {
2776 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2777 let mut view = MockView::new();
2778 view.set_state(0x0027);
2779
2780 axis.tick(&mut view, &mut client);
2781 assert!(!axis.home_sensor);
2782
2783 view.home_sensor = true;
2784 axis.tick(&mut view, &mut client);
2785 assert!(axis.home_sensor);
2786
2787 view.home_sensor = false;
2788 axis.tick(&mut view, &mut client);
2789 assert!(!axis.home_sensor);
2790 }
2791
2792 #[test]
2793 fn velocity_output_converted() {
2794 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2795 let mut view = MockView::new();
2796 view.set_state(0x0027);
2797 view.velocity_actual = 3200;
2799
2800 axis.tick(&mut view, &mut client);
2801
2802 assert!((axis.speed - 90.0).abs() < 0.1);
2803 assert!(axis.moving_positive);
2804 assert!(!axis.moving_negative);
2805 }
2806
2807 fn soft_homing_config() -> AxisConfig {
2810 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2811 cfg.homing_speed = 10.0;
2812 cfg.homing_accel = 20.0;
2813 cfg.homing_decel = 20.0;
2814 cfg
2815 }
2816
2817 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2818 use tokio::sync::mpsc;
2819 let (write_tx, write_rx) = mpsc::unbounded_channel();
2820 let (response_tx, response_rx) = mpsc::unbounded_channel();
2821 let client = CommandClient::new(write_tx, response_rx);
2822 let axis = Axis::new(soft_homing_config(), "TestDrive");
2823 (axis, client, response_tx, write_rx)
2824 }
2825
2826 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2828 view.set_state(0x0027); axis.tick(view, client);
2830 }
2831
2832 fn complete_soft_homing(
2837 axis: &mut Axis,
2838 view: &mut MockView,
2839 client: &mut CommandClient,
2840 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2841 trigger_pos: i32,
2842 clear_sensor: impl FnOnce(&mut MockView),
2843 ) {
2844 use mechutil::ipc::CommandMessage as IpcMsg;
2845
2846 axis.tick(view, client);
2849 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2850
2851 view.position_actual = trigger_pos + 100;
2853 axis.tick(view, client);
2854 view.position_actual = trigger_pos + 120;
2855 axis.tick(view, client);
2856 for _ in 0..10 { axis.tick(view, client); }
2858 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2859
2860 view.status_word = 0x1027;
2862 axis.tick(view, client);
2863 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2864 view.status_word = 0x0027;
2865
2866 for _ in 0..100 { axis.tick(view, client); }
2868 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2869
2870 axis.tick(view, client);
2873 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2874
2875 view.status_word = 0x1027;
2877 axis.tick(view, client);
2878 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2879 view.status_word = 0x0027;
2880
2881 axis.tick(view, client);
2883 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2884 clear_sensor(view);
2885 view.position_actual = trigger_pos - 200;
2886 axis.tick(view, client);
2887 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2888
2889 axis.tick(view, client);
2891 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2892 for _ in 0..10 { axis.tick(view, client); }
2893 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2894
2895 view.status_word = 0x1027;
2898 axis.tick(view, client);
2899 view.status_word = 0x0027;
2900 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2901
2902 let tid = axis.homing_sdo_tid;
2904 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2905 client.poll();
2906 axis.tick(view, client);
2907 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2908
2909 axis.tick(view, client);
2911 let tid = axis.homing_sdo_tid;
2912 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2913 client.poll();
2914 axis.tick(view, client);
2915 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2916
2917 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2919 axis.tick(view, client);
2920 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2921
2922 view.status_word = 0x1427; axis.tick(view, client);
2925 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2926 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2927 view.status_word = 0x0027;
2928
2929 axis.tick(view, client);
2931 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2932
2933 view.status_word = 0x1027;
2935 axis.tick(view, client);
2936 view.status_word = 0x0027;
2937
2938 assert_eq!(axis.op, AxisOp::Idle);
2939 assert!(!axis.is_busy);
2940 assert!(!axis.is_error);
2941 assert_eq!(axis.home_offset, 0); }
2943
2944 #[test]
2945 fn soft_homing_pnp_home_sensor_full_sequence() {
2946 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2947 let mut view = MockView::new();
2948 enable_axis(&mut axis, &mut view, &mut client);
2949
2950 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2951
2952 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2955 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2957 axis.tick(&mut view, &mut client); view.home_sensor = true;
2961 view.position_actual = 5000;
2962 axis.tick(&mut view, &mut client);
2963 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2964
2965 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2966 |v| { v.home_sensor = false; });
2967 }
2968
2969 #[test]
2970 fn soft_homing_npn_home_sensor_full_sequence() {
2971 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2972 let mut view = MockView::new();
2973 view.home_sensor = true;
2975 enable_axis(&mut axis, &mut view, &mut client);
2976
2977 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2978
2979 axis.tick(&mut view, &mut client);
2981 view.status_word = 0x1027;
2982 axis.tick(&mut view, &mut client);
2983 view.status_word = 0x0027;
2984 axis.tick(&mut view, &mut client);
2985
2986 view.home_sensor = false;
2988 view.position_actual = 3000;
2989 axis.tick(&mut view, &mut client);
2990 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2991
2992 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2993 |v| { v.home_sensor = true; }); }
2995
2996 #[test]
2997 fn soft_homing_limit_switch_suppresses_halt() {
2998 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2999 let mut view = MockView::new();
3000 enable_axis(&mut axis, &mut view, &mut client);
3001
3002 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
3004
3005 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3010 axis.tick(&mut view, &mut client); view.positive_limit = true;
3014 view.velocity_actual = 100; view.position_actual = 8000;
3016 axis.tick(&mut view, &mut client);
3017
3018 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3020 assert!(!axis.is_error);
3021 }
3022
3023 #[test]
3024 fn soft_homing_opposite_limit_still_protects() {
3025 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3026 let mut view = MockView::new();
3027 enable_axis(&mut axis, &mut view, &mut client);
3028
3029 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3031
3032 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3037 axis.tick(&mut view, &mut client); view.negative_limit = true;
3042 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
3044
3045 assert!(axis.is_error);
3047 assert!(axis.error_message.contains("Negative limit switch"));
3048 }
3049
3050 #[test]
3051 #[test]
3069 fn soft_homing_negative_direction_sets_negative_target() {
3070 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3071 let mut view = MockView::new();
3072 enable_axis(&mut axis, &mut view, &mut client);
3073
3074 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
3075 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
3079 }
3080
3081 #[test]
3082 fn home_integrated_method_starts_hardware_homing() {
3083 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3084 let mut view = MockView::new();
3085 enable_axis(&mut axis, &mut view, &mut client);
3086
3087 axis.home(&mut view, HomingMethod::CurrentPosition);
3088 assert!(matches!(axis.op, AxisOp::Homing(0)));
3089 assert_eq!(axis.homing_method, 37);
3090 }
3091
3092 #[test]
3093 fn home_integrated_arbitrary_code() {
3094 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3095 let mut view = MockView::new();
3096 enable_axis(&mut axis, &mut view, &mut client);
3097
3098 axis.home(&mut view, HomingMethod::Integrated(35));
3099 assert!(matches!(axis.op, AxisOp::Homing(0)));
3100 assert_eq!(axis.homing_method, 35);
3101 }
3102
3103 #[test]
3104 fn hardware_homing_skips_speed_sdos_when_zero() {
3105 use mechutil::ipc::CommandMessage;
3106
3107 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
3108 let mut view = MockView::new();
3109 enable_axis(&mut axis, &mut view, &mut client);
3110
3111 axis.home(&mut view, HomingMethod::Integrated(37));
3113
3114 axis.tick(&mut view, &mut client);
3116 assert!(matches!(axis.op, AxisOp::Homing(1)));
3117
3118 let _ = write_rx.try_recv();
3120
3121 let tid = axis.homing_sdo_tid;
3123 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3124 client.poll();
3125 axis.tick(&mut view, &mut client);
3126
3127 assert!(matches!(axis.op, AxisOp::Homing(8)));
3129 }
3130
3131 #[test]
3132 fn hardware_homing_writes_speed_sdos_when_nonzero() {
3133 use mechutil::ipc::CommandMessage;
3134
3135 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3136 let mut view = MockView::new();
3137 enable_axis(&mut axis, &mut view, &mut client);
3138
3139 axis.home(&mut view, HomingMethod::Integrated(37));
3141
3142 axis.tick(&mut view, &mut client);
3144 assert!(matches!(axis.op, AxisOp::Homing(1)));
3145 let _ = write_rx.try_recv();
3146
3147 let tid = axis.homing_sdo_tid;
3149 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
3150 client.poll();
3151 axis.tick(&mut view, &mut client);
3152 assert!(matches!(axis.op, AxisOp::Homing(2)));
3154 }
3155
3156 #[test]
3157 fn soft_homing_edge_during_ack_step() {
3158 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3159 let mut view = MockView::new();
3160 enable_axis(&mut axis, &mut view, &mut client);
3161
3162 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3163 axis.tick(&mut view, &mut client); view.home_sensor = true;
3167 view.position_actual = 2000;
3168 axis.tick(&mut view, &mut client);
3169
3170 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3172 }
3173
3174 #[test]
3175 fn soft_homing_applies_home_position() {
3176 let mut cfg = soft_homing_config();
3177 cfg.home_position = 90.0;
3178
3179 use tokio::sync::mpsc;
3180 let (write_tx, _write_rx) = mpsc::unbounded_channel();
3181 let (resp_tx, response_rx) = mpsc::unbounded_channel();
3182 let mut client = CommandClient::new(write_tx, response_rx);
3183 let mut axis = Axis::new(cfg, "TestDrive");
3184
3185 let mut view = MockView::new();
3186 enable_axis(&mut axis, &mut view, &mut client);
3187
3188 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3189
3190 axis.tick(&mut view, &mut client);
3192 view.status_word = 0x1027;
3193 axis.tick(&mut view, &mut client);
3194 view.status_word = 0x0027;
3195 axis.tick(&mut view, &mut client);
3196
3197 view.home_sensor = true;
3199 view.position_actual = 5000;
3200 axis.tick(&mut view, &mut client);
3201 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3202
3203 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3205 |v| { v.home_sensor = false; });
3206
3207 assert_eq!(axis.home_offset, 0);
3209 }
3210
3211 #[test]
3212 fn soft_homing_default_home_position_zero() {
3213 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3214 let mut view = MockView::new();
3215 enable_axis(&mut axis, &mut view, &mut client);
3216
3217 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3218
3219 axis.tick(&mut view, &mut client);
3221 view.status_word = 0x1027;
3222 axis.tick(&mut view, &mut client);
3223 view.status_word = 0x0027;
3224 axis.tick(&mut view, &mut client);
3225
3226 view.home_sensor = true;
3228 view.position_actual = 5000;
3229 axis.tick(&mut view, &mut client);
3230
3231 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
3232 |v| { v.home_sensor = false; });
3233
3234 assert_eq!(axis.home_offset, 0);
3235 }
3236}