1use std::time::{Duration, Instant};
25
26use serde_json::json;
27
28use crate::command_client::CommandClient;
29use crate::ethercat::{SdoClient, SdoResult};
30use super::axis_config::AxisConfig;
31use super::axis_view::AxisView;
32use super::homing::HomingMethod;
33use super::cia402::{
34 Cia402Control, Cia402Status, Cia402State,
35 ModesOfOperation, RawControlWord, RawStatusWord,
36};
37
38#[derive(Debug, Clone, PartialEq)]
43enum AxisOp {
44 Idle,
45 Enabling(u8),
46 Disabling(u8),
47 Moving(MoveKind, u8),
48 Homing(u8),
49 SoftHoming(u8),
50 Halting,
51 FaultRecovery(u8),
52}
53
54#[derive(Debug, Clone, PartialEq)]
55enum MoveKind {
56 Absolute,
57 Relative,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq)]
61enum SoftHomeSensor {
62 PositiveLimit,
63 NegativeLimit,
64 HomeSensor,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
68enum SoftHomeSensorType {
69 Pnp,
71 Npn,
73}
74
75pub struct Axis {
85 config: AxisConfig,
86 sdo: SdoClient,
87
88 op: AxisOp,
90 home_offset: i32,
91 last_raw_position: i32,
92 op_started: Option<Instant>,
93 op_timeout: Duration,
94 homing_timeout: Duration,
95 move_start_timeout: Duration,
96 pending_move_target: i32,
97 pending_move_vel: u32,
98 pending_move_accel: u32,
99 pending_move_decel: u32,
100 homing_method: i8,
101 homing_sdo_tid: u32,
102 soft_home_sensor: SoftHomeSensor,
103 soft_home_sensor_type: SoftHomeSensorType,
104 soft_home_direction: f64,
105 prev_positive_limit: bool,
106 prev_negative_limit: bool,
107 prev_home_sensor: bool,
108
109 pub is_error: bool,
113 pub error_code: u32,
115 pub error_message: String,
117 pub motor_on: bool,
119 pub is_busy: bool,
125 pub in_motion: bool,
127 pub moving_positive: bool,
129 pub moving_negative: bool,
131 pub position: f64,
133 pub raw_position: i64,
135 pub speed: f64,
137 pub at_max_limit: bool,
139 pub at_min_limit: bool,
141 pub at_positive_limit_switch: bool,
143 pub at_negative_limit_switch: bool,
145 pub home_sensor: bool,
147}
148
149impl Axis {
150 pub fn new(config: AxisConfig, device_name: &str) -> Self {
155 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
156 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
157 let move_start_timeout = op_timeout; Self {
159 config,
160 sdo: SdoClient::new(device_name),
161 op: AxisOp::Idle,
162 home_offset: 0,
163 last_raw_position: 0,
164 op_started: None,
165 op_timeout,
166 homing_timeout,
167 move_start_timeout,
168 pending_move_target: 0,
169 pending_move_vel: 0,
170 pending_move_accel: 0,
171 pending_move_decel: 0,
172 homing_method: 37,
173 homing_sdo_tid: 0,
174 soft_home_sensor: SoftHomeSensor::HomeSensor,
175 soft_home_sensor_type: SoftHomeSensorType::Pnp,
176 soft_home_direction: 1.0,
177 prev_positive_limit: false,
178 prev_negative_limit: false,
179 prev_home_sensor: false,
180 is_error: false,
181 error_code: 0,
182 error_message: String::new(),
183 motor_on: false,
184 is_busy: false,
185 in_motion: false,
186 moving_positive: false,
187 moving_negative: false,
188 position: 0.0,
189 raw_position: 0,
190 speed: 0.0,
191 at_max_limit: false,
192 at_min_limit: false,
193 at_positive_limit_switch: false,
194 at_negative_limit_switch: false,
195 home_sensor: false,
196 }
197 }
198
199 pub fn config(&self) -> &AxisConfig {
201 &self.config
202 }
203
204 pub fn move_absolute(
214 &mut self,
215 view: &mut impl AxisView,
216 target: f64,
217 vel: f64,
218 accel: f64,
219 decel: f64,
220 ) {
221 if let Some(msg) = self.check_target_limit(target) {
222 self.set_op_error(&msg);
223 return;
224 }
225
226 let cpu = self.config.counts_per_user();
227 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
228 let raw_vel = (vel * cpu).round() as u32;
229 let raw_accel = (accel * cpu).round() as u32;
230 let raw_decel = (decel * cpu).round() as u32;
231
232 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute);
233 }
234
235 pub fn move_relative(
241 &mut self,
242 view: &mut impl AxisView,
243 distance: f64,
244 vel: f64,
245 accel: f64,
246 decel: f64,
247 ) {
248 if let Some(msg) = self.check_target_limit(self.position + distance) {
249 self.set_op_error(&msg);
250 return;
251 }
252
253 let cpu = self.config.counts_per_user();
254 let raw_distance = self.config.to_counts(distance).round() as i32;
255 let raw_vel = (vel * cpu).round() as u32;
256 let raw_accel = (accel * cpu).round() as u32;
257 let raw_decel = (decel * cpu).round() as u32;
258
259 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative);
260 }
261
262 fn start_move(
263 &mut self,
264 view: &mut impl AxisView,
265 raw_target: i32,
266 raw_vel: u32,
267 raw_accel: u32,
268 raw_decel: u32,
269 kind: MoveKind,
270 ) {
271 self.pending_move_target = raw_target;
272 self.pending_move_vel = raw_vel;
273 self.pending_move_accel = raw_accel;
274 self.pending_move_decel = raw_decel;
275
276 view.set_target_position(raw_target);
278 view.set_profile_velocity(raw_vel);
279 view.set_profile_acceleration(raw_accel);
280 view.set_profile_deceleration(raw_decel);
281
282 let mut cw = RawControlWord(view.control_word());
284 cw.set_bit(6, kind == MoveKind::Relative);
285 cw.set_bit(4, true); view.set_control_word(cw.raw());
287
288 self.op = AxisOp::Moving(kind, 1);
289 self.op_started = Some(Instant::now());
290 }
291
292 pub fn halt(&mut self, view: &mut impl AxisView) {
294 let mut cw = RawControlWord(view.control_word());
295 cw.set_bit(8, true); view.set_control_word(cw.raw());
297 self.op = AxisOp::Halting;
298 }
299
300 pub fn enable(&mut self, view: &mut impl AxisView) {
308 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
310 let mut cw = RawControlWord(view.control_word());
311 cw.cmd_shutdown();
312 view.set_control_word(cw.raw());
313
314 self.op = AxisOp::Enabling(1);
315 self.op_started = Some(Instant::now());
316 }
317
318 pub fn disable(&mut self, view: &mut impl AxisView) {
320 let mut cw = RawControlWord(view.control_word());
321 cw.cmd_disable_operation();
322 view.set_control_word(cw.raw());
323
324 self.op = AxisOp::Disabling(1);
325 self.op_started = Some(Instant::now());
326 }
327
328 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
332 let mut cw = RawControlWord(view.control_word());
334 cw.cmd_clear_fault_reset();
335 view.set_control_word(cw.raw());
336
337 self.is_error = false;
338 self.error_code = 0;
339 self.error_message.clear();
340 self.op = AxisOp::FaultRecovery(1);
341 self.op_started = Some(Instant::now());
342 }
343
344 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
352 if method.is_integrated() {
353 self.homing_method = method.cia402_code();
354 self.op = AxisOp::Homing(0);
355 self.op_started = Some(Instant::now());
356 let _ = view;
357 } else {
358 self.configure_soft_homing(method);
359 self.start_soft_homing(view);
360 }
361 }
362
363 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
372 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
373 }
374
375 pub fn set_software_max_limit(&mut self, user_units: f64) {
377 self.config.max_position_limit = user_units;
378 self.config.enable_max_position_limit = true;
379 }
380
381 pub fn set_software_min_limit(&mut self, user_units: f64) {
383 self.config.min_position_limit = user_units;
384 self.config.enable_min_position_limit = true;
385 }
386
387 pub fn sdo_write(
393 &mut self,
394 client: &mut CommandClient,
395 index: u16,
396 sub_index: u8,
397 value: serde_json::Value,
398 ) {
399 self.sdo.write(client, index, sub_index, value);
400 }
401
402 pub fn sdo_read(
404 &mut self,
405 client: &mut CommandClient,
406 index: u16,
407 sub_index: u8,
408 ) -> u32 {
409 self.sdo.read(client, index, sub_index)
410 }
411
412 pub fn sdo_result(
414 &mut self,
415 client: &mut CommandClient,
416 tid: u32,
417 ) -> SdoResult {
418 self.sdo.result(client, tid, Duration::from_secs(5))
419 }
420
421 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
435 self.check_faults(view);
436 self.progress_op(view, client);
437 self.update_outputs(view);
438 self.check_limits(view);
439 }
440
441 fn update_outputs(&mut self, view: &impl AxisView) {
446 let raw = view.position_actual();
447 self.raw_position = raw as i64;
448 self.position = self.config.to_user((raw - self.home_offset) as f64);
449
450 let vel = view.velocity_actual();
451 let user_vel = self.config.to_user(vel as f64);
452 self.speed = user_vel.abs();
453 self.moving_positive = user_vel > 0.0;
454 self.moving_negative = user_vel < 0.0;
455 self.is_busy = self.op != AxisOp::Idle;
456 self.in_motion = matches!(self.op, AxisOp::Moving(_, _) | AxisOp::SoftHoming(_));
457
458 let sw = RawStatusWord(view.status_word());
459 self.motor_on = sw.state() == Cia402State::OperationEnabled;
460
461 self.last_raw_position = raw;
462 }
463
464 fn check_faults(&mut self, view: &impl AxisView) {
469 let sw = RawStatusWord(view.status_word());
470 let state = sw.state();
471
472 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
473 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
474 self.is_error = true;
475 let ec = view.error_code();
476 if ec != 0 {
477 self.error_code = ec as u32;
478 }
479 self.error_message = format!("Drive fault (state: {})", state);
480 self.op = AxisOp::Idle;
482 self.op_started = None;
483 }
484 }
485 }
486
487 fn op_timed_out(&self) -> bool {
492 self.op_started
493 .map_or(false, |t| t.elapsed() > self.op_timeout)
494 }
495
496 fn homing_timed_out(&self) -> bool {
497 self.op_started
498 .map_or(false, |t| t.elapsed() > self.homing_timeout)
499 }
500
501 fn move_start_timed_out(&self) -> bool {
502 self.op_started
503 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
504 }
505
506 fn set_op_error(&mut self, msg: &str) {
507 self.is_error = true;
508 self.error_message = msg.to_string();
509 self.op = AxisOp::Idle;
510 self.op_started = None;
511 self.is_busy = false;
512 self.in_motion = false;
513 log::error!("Axis error: {}", msg);
514 }
515
516 fn complete_op(&mut self) {
517 self.op = AxisOp::Idle;
518 self.op_started = None;
519 }
520
521 fn check_target_limit(&self, target: f64) -> Option<String> {
528 if self.config.enable_max_position_limit && target > self.config.max_position_limit {
529 Some(format!(
530 "Target {:.3} exceeds max software limit {:.3}",
531 target, self.config.max_position_limit
532 ))
533 } else if self.config.enable_min_position_limit && target < self.config.min_position_limit {
534 Some(format!(
535 "Target {:.3} exceeds min software limit {:.3}",
536 target, self.config.min_position_limit
537 ))
538 } else {
539 None
540 }
541 }
542
543 fn check_limits(&mut self, view: &mut impl AxisView) {
552 let sw_max = self.config.enable_max_position_limit
554 && self.position >= self.config.max_position_limit;
555 let sw_min = self.config.enable_min_position_limit
556 && self.position <= self.config.min_position_limit;
557
558 self.at_max_limit = sw_max;
559 self.at_min_limit = sw_min;
560
561 let hw_pos = view.positive_limit_active();
563 let hw_neg = view.negative_limit_active();
564
565 self.at_positive_limit_switch = hw_pos;
566 self.at_negative_limit_switch = hw_neg;
567
568 self.home_sensor = view.home_sensor_active();
570
571 self.prev_positive_limit = hw_pos;
573 self.prev_negative_limit = hw_neg;
574 self.prev_home_sensor = view.home_sensor_active();
575
576 let is_moving = matches!(self.op, AxisOp::Moving(_, _));
578 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
579
580 if !is_moving && !is_soft_homing {
581 return;
582 }
583
584 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
586 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
587
588 let effective_hw_pos = hw_pos && !suppress_pos;
589 let effective_hw_neg = hw_neg && !suppress_neg;
590
591 let effective_sw_max = sw_max && !is_soft_homing;
593 let effective_sw_min = sw_min && !is_soft_homing;
594
595 let positive_blocked = (effective_sw_max || effective_hw_pos) && self.moving_positive;
596 let negative_blocked = (effective_sw_min || effective_hw_neg) && self.moving_negative;
597
598 if positive_blocked || negative_blocked {
599 let mut cw = RawControlWord(view.control_word());
600 cw.set_bit(8, true); view.set_control_word(cw.raw());
602
603 let msg = if effective_hw_pos && self.moving_positive {
604 "Positive limit switch active".to_string()
605 } else if effective_hw_neg && self.moving_negative {
606 "Negative limit switch active".to_string()
607 } else if effective_sw_max && self.moving_positive {
608 format!(
609 "Software position limit: position {:.3} >= max {:.3}",
610 self.position, self.config.max_position_limit
611 )
612 } else {
613 format!(
614 "Software position limit: position {:.3} <= min {:.3}",
615 self.position, self.config.min_position_limit
616 )
617 };
618 self.set_op_error(&msg);
619 }
620 }
621
622 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
627 match self.op.clone() {
628 AxisOp::Idle => {}
629 AxisOp::Enabling(step) => self.tick_enabling(view, step),
630 AxisOp::Disabling(step) => self.tick_disabling(view, step),
631 AxisOp::Moving(kind, step) => self.tick_moving(view, kind, step),
632 AxisOp::Homing(step) => self.tick_homing(view, client, step),
633 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, step),
634 AxisOp::Halting => self.tick_halting(view),
635 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
636 }
637 }
638
639 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
644 match step {
645 1 => {
646 let sw = RawStatusWord(view.status_word());
647 if sw.state() == Cia402State::ReadyToSwitchOn {
648 let mut cw = RawControlWord(view.control_word());
649 cw.cmd_enable_operation();
650 view.set_control_word(cw.raw());
651 self.op = AxisOp::Enabling(2);
652 } else if self.op_timed_out() {
653 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
654 }
655 }
656 2 => {
657 let sw = RawStatusWord(view.status_word());
658 if sw.state() == Cia402State::OperationEnabled {
659 self.home_offset = view.position_actual();
660 log::info!("Axis enabled — home captured at {}", self.home_offset);
661 self.complete_op();
662 } else if self.op_timed_out() {
663 self.set_op_error("Enable timeout: waiting for OperationEnabled");
664 }
665 }
666 _ => self.complete_op(),
667 }
668 }
669
670 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
674 match step {
675 1 => {
676 let sw = RawStatusWord(view.status_word());
677 if sw.state() != Cia402State::OperationEnabled {
678 self.complete_op();
679 } else if self.op_timed_out() {
680 self.set_op_error("Disable timeout: drive still in OperationEnabled");
681 }
682 }
683 _ => self.complete_op(),
684 }
685 }
686
687 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8) {
693 match step {
694 1 => {
695 let sw = RawStatusWord(view.status_word());
697 if sw.raw() & (1 << 12) != 0 {
698 let mut cw = RawControlWord(view.control_word());
700 cw.set_bit(4, false);
701 view.set_control_word(cw.raw());
702 self.op = AxisOp::Moving(kind, 2);
703 } else if self.move_start_timed_out() {
704 self.set_op_error("Move timeout: set-point not acknowledged");
705 }
706 }
707 2 => {
708 let sw = RawStatusWord(view.status_word());
710 if sw.target_reached() {
711 self.complete_op();
712 }
713 }
714 _ => self.complete_op(),
715 }
716 }
717
718 fn tick_homing(
736 &mut self,
737 view: &mut impl AxisView,
738 client: &mut CommandClient,
739 step: u8,
740 ) {
741 match step {
742 0 => {
743 self.homing_sdo_tid = self.sdo.write(
745 client,
746 0x6098,
747 0,
748 json!(self.homing_method),
749 );
750 self.op = AxisOp::Homing(1);
751 }
752 1 => {
753 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
755 SdoResult::Ok(_) => {
756 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
758 self.op = AxisOp::Homing(8);
759 } else {
760 self.op = AxisOp::Homing(2);
761 }
762 }
763 SdoResult::Pending => {
764 if self.homing_timed_out() {
765 self.set_op_error("Homing timeout: SDO write for homing method");
766 }
767 }
768 SdoResult::Err(e) => {
769 self.set_op_error(&format!("Homing SDO error: {}", e));
770 }
771 SdoResult::Timeout => {
772 self.set_op_error("Homing timeout: SDO write timed out");
773 }
774 }
775 }
776 2 => {
777 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
779 self.homing_sdo_tid = self.sdo.write(
780 client,
781 0x6099,
782 1,
783 json!(speed_counts),
784 );
785 self.op = AxisOp::Homing(3);
786 }
787 3 => {
788 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
789 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
790 SdoResult::Pending => {
791 if self.homing_timed_out() {
792 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
793 }
794 }
795 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
796 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
797 }
798 }
799 4 => {
800 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
802 self.homing_sdo_tid = self.sdo.write(
803 client,
804 0x6099,
805 2,
806 json!(speed_counts),
807 );
808 self.op = AxisOp::Homing(5);
809 }
810 5 => {
811 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
812 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
813 SdoResult::Pending => {
814 if self.homing_timed_out() {
815 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
816 }
817 }
818 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
819 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
820 }
821 }
822 6 => {
823 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
825 self.homing_sdo_tid = self.sdo.write(
826 client,
827 0x609A,
828 0,
829 json!(accel_counts),
830 );
831 self.op = AxisOp::Homing(7);
832 }
833 7 => {
834 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
835 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
836 SdoResult::Pending => {
837 if self.homing_timed_out() {
838 self.set_op_error("Homing timeout: SDO write for homing acceleration");
839 }
840 }
841 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
842 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
843 }
844 }
845 8 => {
846 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
848 self.op = AxisOp::Homing(9);
849 }
850 9 => {
851 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
853 self.op = AxisOp::Homing(10);
854 } else if self.homing_timed_out() {
855 self.set_op_error("Homing timeout: mode not confirmed");
856 }
857 }
858 10 => {
859 let mut cw = RawControlWord(view.control_word());
861 cw.set_bit(4, true);
862 view.set_control_word(cw.raw());
863 self.op = AxisOp::Homing(11);
864 }
865 11 => {
866 let sw = view.status_word();
869 let error = sw & (1 << 13) != 0;
870 let attained = sw & (1 << 12) != 0;
871 let reached = sw & (1 << 10) != 0;
872
873 if error {
874 self.set_op_error("Homing error: drive reported homing failure");
875 } else if attained && reached {
876 self.op = AxisOp::Homing(12);
878 } else if self.homing_timed_out() {
879 self.set_op_error("Homing timeout: procedure did not complete");
880 }
881 }
882 12 => {
883 self.home_offset = view.position_actual()
886 - self.config.to_counts(self.config.home_position).round() as i32;
887 let mut cw = RawControlWord(view.control_word());
889 cw.set_bit(4, false);
890 view.set_control_word(cw.raw());
891 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
893 log::info!("Homing complete — home offset: {}", self.home_offset);
894 self.complete_op();
895 }
896 _ => self.complete_op(),
897 }
898 }
899
900 fn configure_soft_homing(&mut self, method: HomingMethod) {
903 match method {
904 HomingMethod::LimitSwitchPosPnp => {
905 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
906 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
907 self.soft_home_direction = 1.0;
908 }
909 HomingMethod::LimitSwitchNegPnp => {
910 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
911 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
912 self.soft_home_direction = -1.0;
913 }
914 HomingMethod::LimitSwitchPosNpn => {
915 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
916 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
917 self.soft_home_direction = 1.0;
918 }
919 HomingMethod::LimitSwitchNegNpn => {
920 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
921 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
922 self.soft_home_direction = -1.0;
923 }
924 HomingMethod::HomeSensorPosPnp => {
925 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
926 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
927 self.soft_home_direction = 1.0;
928 }
929 HomingMethod::HomeSensorNegPnp => {
930 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
931 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
932 self.soft_home_direction = -1.0;
933 }
934 HomingMethod::HomeSensorPosNpn => {
935 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
936 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
937 self.soft_home_direction = 1.0;
938 }
939 HomingMethod::HomeSensorNegNpn => {
940 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
941 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
942 self.soft_home_direction = -1.0;
943 }
944 _ => {} }
946 }
947
948 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
949 if self.check_soft_home_trigger(view) {
951 self.set_op_error("Software homing: sensor already in trigger state");
952 return;
953 }
954
955 self.op = AxisOp::SoftHoming(0);
956 self.op_started = Some(Instant::now());
957 }
958
959 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
960 let raw = match self.soft_home_sensor {
961 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
962 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
963 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
964 };
965 match self.soft_home_sensor_type {
966 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
969 }
970
971 fn tick_soft_homing(&mut self, view: &mut impl AxisView, step: u8) {
981 match step {
982 0 => {
983 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
985
986 let target = self.config.to_counts(self.soft_home_direction * 999_999.0).round() as i32 + self.home_offset;
989 view.set_target_position(target);
990
991 let cpu = self.config.counts_per_user();
993 let vel = (self.config.homing_speed * cpu).round() as u32;
994 let accel = (self.config.homing_accel * cpu).round() as u32;
995 let decel = (self.config.homing_decel * cpu).round() as u32;
996 view.set_profile_velocity(vel);
997 view.set_profile_acceleration(accel);
998 view.set_profile_deceleration(decel);
999
1000 let mut cw = RawControlWord(view.control_word());
1002 cw.set_bit(4, true);
1003 cw.set_bit(6, false); cw.set_bit(8, false); view.set_control_word(cw.raw());
1006
1007 self.op = AxisOp::SoftHoming(1);
1008 }
1009 1 => {
1010 if self.check_soft_home_trigger(view) {
1012 self.op = AxisOp::SoftHoming(4);
1013 return;
1014 }
1015 let sw = RawStatusWord(view.status_word());
1016 if sw.raw() & (1 << 12) != 0 {
1017 let mut cw = RawControlWord(view.control_word());
1019 cw.set_bit(4, false);
1020 view.set_control_word(cw.raw());
1021 self.op = AxisOp::SoftHoming(2);
1022 } else if self.homing_timed_out() {
1023 self.set_op_error("Software homing timeout: set-point not acknowledged");
1024 }
1025 }
1026 2 => {
1027 if self.check_soft_home_trigger(view) {
1029 self.op = AxisOp::SoftHoming(4);
1030 return;
1031 }
1032 self.op = AxisOp::SoftHoming(3);
1033 }
1034 3 => {
1035 if self.check_soft_home_trigger(view) {
1037 self.op = AxisOp::SoftHoming(4);
1038 } else if self.homing_timed_out() {
1039 self.set_op_error("Software homing timeout: sensor edge not detected");
1040 }
1041 }
1042 4 => {
1043 let mut cw = RawControlWord(view.control_word());
1045 cw.set_bit(8, true); view.set_control_word(cw.raw());
1047 view.set_profile_velocity(0);
1048 self.home_offset = view.position_actual()
1049 - self.config.to_counts(self.config.home_position).round() as i32;
1050 log::info!("Software homing: sensor triggered, home offset: {}", self.home_offset);
1051 self.op = AxisOp::SoftHoming(5);
1052 }
1053 5 => {
1054 let mut cw = RawControlWord(view.control_word());
1058 cw.set_bit(8, true); view.set_control_word(cw.raw());
1060
1061 if view.velocity_actual() == 0 {
1062 cw.set_bit(8, false);
1064 view.set_control_word(cw.raw());
1065 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1066 log::info!("Software homing complete — motor stopped, home offset: {}", self.home_offset);
1067 self.complete_op();
1068 } else if self.homing_timed_out() {
1069 self.set_op_error("Software homing timeout: motor did not stop after halt");
1070 }
1071 }
1072 _ => self.complete_op(),
1073 }
1074 }
1075
1076 fn tick_halting(&mut self, _view: &mut impl AxisView) {
1078 self.complete_op();
1081 }
1082
1083 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1088 match step {
1089 1 => {
1090 let mut cw = RawControlWord(view.control_word());
1092 cw.cmd_fault_reset();
1093 view.set_control_word(cw.raw());
1094 self.op = AxisOp::FaultRecovery(2);
1095 }
1096 2 => {
1097 let sw = RawStatusWord(view.status_word());
1099 let state = sw.state();
1100 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1101 log::info!("Fault cleared (drive state: {})", state);
1102 self.complete_op();
1103 } else if self.op_timed_out() {
1104 self.set_op_error("Fault reset timeout: drive still faulted");
1105 }
1106 }
1107 _ => self.complete_op(),
1108 }
1109 }
1110}
1111
1112#[cfg(test)]
1117mod tests {
1118 use super::*;
1119
1120 struct MockView {
1122 control_word: u16,
1123 status_word: u16,
1124 target_position: i32,
1125 profile_velocity: u32,
1126 profile_acceleration: u32,
1127 profile_deceleration: u32,
1128 modes_of_operation: i8,
1129 modes_of_operation_display: i8,
1130 position_actual: i32,
1131 velocity_actual: i32,
1132 error_code: u16,
1133 positive_limit: bool,
1134 negative_limit: bool,
1135 home_sensor: bool,
1136 }
1137
1138 impl MockView {
1139 fn new() -> Self {
1140 Self {
1141 control_word: 0,
1142 status_word: 0x0040, target_position: 0,
1144 profile_velocity: 0,
1145 profile_acceleration: 0,
1146 profile_deceleration: 0,
1147 modes_of_operation: 0,
1148 modes_of_operation_display: 1, position_actual: 0,
1150 velocity_actual: 0,
1151 error_code: 0,
1152 positive_limit: false,
1153 negative_limit: false,
1154 home_sensor: false,
1155 }
1156 }
1157
1158 fn set_state(&mut self, state: u16) {
1159 self.status_word = state;
1160 }
1161 }
1162
1163 impl AxisView for MockView {
1164 fn control_word(&self) -> u16 { self.control_word }
1165 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1166 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1167 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1168 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1169 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1170 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1171 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1172 fn status_word(&self) -> u16 { self.status_word }
1173 fn position_actual(&self) -> i32 { self.position_actual }
1174 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1175 fn error_code(&self) -> u16 { self.error_code }
1176 fn positive_limit_active(&self) -> bool { self.positive_limit }
1177 fn negative_limit_active(&self) -> bool { self.negative_limit }
1178 fn home_sensor_active(&self) -> bool { self.home_sensor }
1179 }
1180
1181 fn test_config() -> AxisConfig {
1182 AxisConfig::new(12_800).with_user_scale(360.0)
1183 }
1184
1185 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1187 use tokio::sync::mpsc;
1188 let (write_tx, write_rx) = mpsc::unbounded_channel();
1189 let (response_tx, response_rx) = mpsc::unbounded_channel();
1190 let client = CommandClient::new(write_tx, response_rx);
1191 let axis = Axis::new(test_config(), "TestDrive");
1192 (axis, client, response_tx, write_rx)
1193 }
1194
1195 #[test]
1196 fn axis_config_conversion() {
1197 let cfg = test_config();
1198 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
1200 }
1201
1202 #[test]
1203 fn enable_sequence_sets_pp_mode_and_shutdown() {
1204 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1205 let mut view = MockView::new();
1206
1207 axis.enable(&mut view);
1208
1209 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
1211 assert_eq!(view.control_word & 0x008F, 0x0006);
1213 assert_eq!(axis.op, AxisOp::Enabling(1));
1215
1216 view.set_state(0x0021); axis.tick(&mut view, &mut client);
1219
1220 assert_eq!(view.control_word & 0x008F, 0x000F);
1222 assert_eq!(axis.op, AxisOp::Enabling(2));
1223
1224 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1227
1228 assert_eq!(axis.op, AxisOp::Idle);
1230 assert!(axis.motor_on);
1231 }
1232
1233 #[test]
1234 fn move_absolute_sets_target() {
1235 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1236 let mut view = MockView::new();
1237 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1242
1243 assert_eq!(view.target_position, 1600);
1245 assert_eq!(view.profile_velocity, 3200);
1247 assert_eq!(view.profile_acceleration, 6400);
1249 assert_eq!(view.profile_deceleration, 6400);
1250 assert!(view.control_word & (1 << 4) != 0);
1252 assert!(view.control_word & (1 << 6) == 0);
1254 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1256 }
1257
1258 #[test]
1259 fn move_relative_sets_relative_bit() {
1260 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1261 let mut view = MockView::new();
1262 view.set_state(0x0027);
1263 axis.tick(&mut view, &mut client);
1264
1265 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
1266
1267 assert!(view.control_word & (1 << 6) != 0);
1269 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1)));
1270 }
1271
1272 #[test]
1273 fn move_completes_on_target_reached() {
1274 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1275 let mut view = MockView::new();
1276 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1278
1279 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1280
1281 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1284 assert!(view.control_word & (1 << 4) == 0);
1286
1287 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1290 assert_eq!(axis.op, AxisOp::Idle);
1292 assert!(!axis.in_motion);
1293 }
1294
1295 #[test]
1296 fn fault_detected_sets_error() {
1297 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1298 let mut view = MockView::new();
1299 view.set_state(0x0008); view.error_code = 0x1234;
1301
1302 axis.tick(&mut view, &mut client);
1303
1304 assert!(axis.is_error);
1305 assert_eq!(axis.error_code, 0x1234);
1306 assert!(axis.error_message.contains("fault"));
1307 }
1308
1309 #[test]
1310 fn fault_recovery_sequence() {
1311 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1312 let mut view = MockView::new();
1313 view.set_state(0x0008); axis.reset_faults(&mut view);
1316 assert!(view.control_word & 0x0080 == 0);
1318
1319 axis.tick(&mut view, &mut client);
1321 assert!(view.control_word & 0x0080 != 0);
1322
1323 view.set_state(0x0040);
1325 axis.tick(&mut view, &mut client);
1326 assert_eq!(axis.op, AxisOp::Idle);
1327 assert!(!axis.is_error);
1328 }
1329
1330 #[test]
1331 fn disable_sequence() {
1332 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1333 let mut view = MockView::new();
1334 view.set_state(0x0027); axis.disable(&mut view);
1337 assert_eq!(view.control_word & 0x008F, 0x0007);
1339
1340 view.set_state(0x0023); axis.tick(&mut view, &mut client);
1343 assert_eq!(axis.op, AxisOp::Idle);
1344 }
1345
1346 #[test]
1347 fn position_tracks_with_home_offset() {
1348 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1349 let mut view = MockView::new();
1350 view.set_state(0x0027);
1351 view.position_actual = 5000;
1352
1353 axis.enable(&mut view);
1355 view.set_state(0x0021);
1356 axis.tick(&mut view, &mut client);
1357 view.set_state(0x0027);
1358 axis.tick(&mut view, &mut client);
1359
1360 assert_eq!(axis.home_offset, 5000);
1362
1363 assert!((axis.position - 0.0).abs() < 0.01);
1365
1366 view.position_actual = 6600;
1368 axis.tick(&mut view, &mut client);
1369
1370 assert!((axis.position - 45.0).abs() < 0.1);
1372 }
1373
1374 #[test]
1375 fn set_position_adjusts_home_offset() {
1376 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1377 let mut view = MockView::new();
1378 view.position_actual = 3200;
1379
1380 axis.set_position(&view, 90.0);
1381 axis.tick(&mut view, &mut client);
1382
1383 assert_eq!(axis.home_offset, 0);
1385 assert!((axis.position - 90.0).abs() < 0.01);
1386 }
1387
1388 #[test]
1389 fn halt_sets_bit_and_goes_idle() {
1390 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1391 let mut view = MockView::new();
1392 view.set_state(0x0027);
1393
1394 axis.halt(&mut view);
1395 assert!(view.control_word & (1 << 8) != 0);
1397
1398 axis.tick(&mut view, &mut client);
1400 assert_eq!(axis.op, AxisOp::Idle);
1401 }
1402
1403 #[test]
1404 fn is_busy_tracks_operations() {
1405 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1406 let mut view = MockView::new();
1407
1408 axis.tick(&mut view, &mut client);
1410 assert!(!axis.is_busy);
1411
1412 axis.enable(&mut view);
1414 axis.tick(&mut view, &mut client);
1415 assert!(axis.is_busy);
1416
1417 view.set_state(0x0021);
1419 axis.tick(&mut view, &mut client);
1420 view.set_state(0x0027);
1421 axis.tick(&mut view, &mut client);
1422 assert!(!axis.is_busy);
1423
1424 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1426 axis.tick(&mut view, &mut client);
1427 assert!(axis.is_busy);
1428 assert!(axis.in_motion);
1429 }
1430
1431 #[test]
1432 fn fault_during_move_cancels_op() {
1433 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1434 let mut view = MockView::new();
1435 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1437
1438 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1440 axis.tick(&mut view, &mut client);
1441 assert!(axis.is_busy);
1442 assert!(!axis.is_error);
1443
1444 view.set_state(0x0008); axis.tick(&mut view, &mut client);
1447
1448 assert!(!axis.is_busy);
1450 assert!(axis.is_error);
1451 assert_eq!(axis.op, AxisOp::Idle);
1452 }
1453
1454 #[test]
1455 fn move_absolute_rejected_by_max_limit() {
1456 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1457 let mut view = MockView::new();
1458 view.set_state(0x0027);
1459 axis.tick(&mut view, &mut client);
1460
1461 axis.set_software_max_limit(90.0);
1462 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
1463
1464 assert!(axis.is_error);
1466 assert_eq!(axis.op, AxisOp::Idle);
1467 assert!(axis.error_message.contains("max software limit"));
1468 }
1469
1470 #[test]
1471 fn move_absolute_rejected_by_min_limit() {
1472 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1473 let mut view = MockView::new();
1474 view.set_state(0x0027);
1475 axis.tick(&mut view, &mut client);
1476
1477 axis.set_software_min_limit(-10.0);
1478 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
1479
1480 assert!(axis.is_error);
1481 assert_eq!(axis.op, AxisOp::Idle);
1482 assert!(axis.error_message.contains("min software limit"));
1483 }
1484
1485 #[test]
1486 fn move_relative_rejected_by_max_limit() {
1487 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1488 let mut view = MockView::new();
1489 view.set_state(0x0027);
1490 axis.tick(&mut view, &mut client);
1491
1492 axis.set_software_max_limit(50.0);
1494 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
1495
1496 assert!(axis.is_error);
1497 assert_eq!(axis.op, AxisOp::Idle);
1498 assert!(axis.error_message.contains("max software limit"));
1499 }
1500
1501 #[test]
1502 fn move_within_limits_allowed() {
1503 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1504 let mut view = MockView::new();
1505 view.set_state(0x0027);
1506 axis.tick(&mut view, &mut client);
1507
1508 axis.set_software_max_limit(90.0);
1509 axis.set_software_min_limit(-90.0);
1510 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1511
1512 assert!(!axis.is_error);
1514 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1515 }
1516
1517 #[test]
1518 fn runtime_limit_halts_move_in_violated_direction() {
1519 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1520 let mut view = MockView::new();
1521 view.set_state(0x0027);
1522 axis.tick(&mut view, &mut client);
1523
1524 axis.set_software_max_limit(45.0);
1525 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1527
1528 view.position_actual = 1650;
1531 view.velocity_actual = 100; view.status_word = 0x1027;
1535 axis.tick(&mut view, &mut client);
1536 view.status_word = 0x0027;
1537 axis.tick(&mut view, &mut client);
1538
1539 assert!(axis.is_error);
1541 assert!(axis.at_max_limit);
1542 assert_eq!(axis.op, AxisOp::Idle);
1543 assert!(axis.error_message.contains("Software position limit"));
1544 assert!(view.control_word & (1 << 8) != 0);
1546 }
1547
1548 #[test]
1549 fn runtime_limit_allows_move_in_opposite_direction() {
1550 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1551 let mut view = MockView::new();
1552 view.set_state(0x0027);
1553 view.position_actual = 1778; axis.set_software_max_limit(45.0);
1556 axis.tick(&mut view, &mut client);
1557 assert!(axis.at_max_limit);
1558
1559 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
1561 assert!(!axis.is_error);
1562 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1563
1564 view.velocity_actual = -100;
1566 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1568 assert!(!axis.is_error);
1570 }
1571
1572 #[test]
1573 fn positive_limit_switch_halts_positive_move() {
1574 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1575 let mut view = MockView::new();
1576 view.set_state(0x0027);
1577 axis.tick(&mut view, &mut client);
1578
1579 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1581 view.velocity_actual = 100; view.status_word = 0x1027;
1584 axis.tick(&mut view, &mut client);
1585 view.status_word = 0x0027;
1586
1587 view.positive_limit = true;
1589 axis.tick(&mut view, &mut client);
1590
1591 assert!(axis.is_error);
1592 assert!(axis.at_positive_limit_switch);
1593 assert!(!axis.is_busy);
1594 assert!(axis.error_message.contains("Positive limit switch"));
1595 assert!(view.control_word & (1 << 8) != 0);
1597 }
1598
1599 #[test]
1600 fn negative_limit_switch_halts_negative_move() {
1601 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1602 let mut view = MockView::new();
1603 view.set_state(0x0027);
1604 axis.tick(&mut view, &mut client);
1605
1606 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
1608 view.velocity_actual = -100; view.status_word = 0x1027;
1610 axis.tick(&mut view, &mut client);
1611 view.status_word = 0x0027;
1612
1613 view.negative_limit = true;
1615 axis.tick(&mut view, &mut client);
1616
1617 assert!(axis.is_error);
1618 assert!(axis.at_negative_limit_switch);
1619 assert!(axis.error_message.contains("Negative limit switch"));
1620 }
1621
1622 #[test]
1623 fn limit_switch_allows_move_in_opposite_direction() {
1624 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1625 let mut view = MockView::new();
1626 view.set_state(0x0027);
1627 view.positive_limit = true;
1629 view.velocity_actual = -100;
1630 axis.tick(&mut view, &mut client);
1631 assert!(axis.at_positive_limit_switch);
1632
1633 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
1635 view.status_word = 0x1027;
1636 axis.tick(&mut view, &mut client);
1637
1638 assert!(!axis.is_error);
1640 assert!(matches!(axis.op, AxisOp::Moving(_, _)));
1641 }
1642
1643 #[test]
1644 fn limit_switch_ignored_when_not_moving() {
1645 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1646 let mut view = MockView::new();
1647 view.set_state(0x0027);
1648 view.positive_limit = true;
1649
1650 axis.tick(&mut view, &mut client);
1651
1652 assert!(axis.at_positive_limit_switch);
1654 assert!(!axis.is_error);
1655 }
1656
1657 #[test]
1658 fn home_sensor_output_tracks_view() {
1659 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1660 let mut view = MockView::new();
1661 view.set_state(0x0027);
1662
1663 axis.tick(&mut view, &mut client);
1664 assert!(!axis.home_sensor);
1665
1666 view.home_sensor = true;
1667 axis.tick(&mut view, &mut client);
1668 assert!(axis.home_sensor);
1669
1670 view.home_sensor = false;
1671 axis.tick(&mut view, &mut client);
1672 assert!(!axis.home_sensor);
1673 }
1674
1675 #[test]
1676 fn velocity_output_converted() {
1677 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1678 let mut view = MockView::new();
1679 view.set_state(0x0027);
1680 view.velocity_actual = 3200;
1682
1683 axis.tick(&mut view, &mut client);
1684
1685 assert!((axis.speed - 90.0).abs() < 0.1);
1686 assert!(axis.moving_positive);
1687 assert!(!axis.moving_negative);
1688 }
1689
1690 fn soft_homing_config() -> AxisConfig {
1693 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
1694 cfg.homing_speed = 10.0;
1695 cfg.homing_accel = 20.0;
1696 cfg.homing_decel = 20.0;
1697 cfg
1698 }
1699
1700 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1701 use tokio::sync::mpsc;
1702 let (write_tx, write_rx) = mpsc::unbounded_channel();
1703 let (response_tx, response_rx) = mpsc::unbounded_channel();
1704 let client = CommandClient::new(write_tx, response_rx);
1705 let axis = Axis::new(soft_homing_config(), "TestDrive");
1706 (axis, client, response_tx, write_rx)
1707 }
1708
1709 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
1711 view.set_state(0x0027); axis.tick(view, client);
1713 }
1714
1715 #[test]
1716 fn soft_homing_rising_edge_home_sensor_triggers_home() {
1717 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1718 let mut view = MockView::new();
1719 enable_axis(&mut axis, &mut view, &mut client);
1720
1721 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
1723 assert!(matches!(axis.op, AxisOp::SoftHoming(0)));
1724
1725 axis.tick(&mut view, &mut client);
1727 assert!(matches!(axis.op, AxisOp::SoftHoming(1)));
1728 assert!(view.control_word & (1 << 4) != 0);
1730
1731 view.status_word = 0x1027;
1733 axis.tick(&mut view, &mut client);
1734 assert!(view.control_word & (1 << 4) == 0);
1736 assert!(matches!(axis.op, AxisOp::SoftHoming(2)));
1737
1738 view.status_word = 0x0027;
1740 axis.tick(&mut view, &mut client);
1741 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1742
1743 axis.tick(&mut view, &mut client);
1745 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1746
1747 view.home_sensor = true;
1749 view.position_actual = 5000;
1750 axis.tick(&mut view, &mut client);
1751 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1753
1754 axis.tick(&mut view, &mut client);
1756 assert!(view.control_word & (1 << 8) != 0); assert_eq!(axis.home_offset, 5000);
1758 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
1759
1760 view.velocity_actual = 100;
1762 axis.tick(&mut view, &mut client);
1763 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
1764 assert!(view.control_word & (1 << 8) != 0); view.velocity_actual = 0;
1768 axis.tick(&mut view, &mut client);
1769 assert_eq!(axis.op, AxisOp::Idle);
1770 assert!(!axis.is_busy);
1771 assert!(!axis.is_error);
1772 assert!(view.control_word & (1 << 8) == 0); }
1774
1775 #[test]
1776 fn soft_homing_falling_edge_home_sensor_triggers_home() {
1777 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1778 let mut view = MockView::new();
1779 view.home_sensor = true;
1781 enable_axis(&mut axis, &mut view, &mut client);
1782
1783 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
1785 assert!(matches!(axis.op, AxisOp::SoftHoming(0)));
1786
1787 axis.tick(&mut view, &mut client);
1789 view.status_word = 0x1027;
1791 axis.tick(&mut view, &mut client);
1792 view.status_word = 0x0027;
1794 axis.tick(&mut view, &mut client);
1795
1796 axis.tick(&mut view, &mut client);
1798 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1799
1800 view.home_sensor = false;
1802 view.position_actual = 3000;
1803 axis.tick(&mut view, &mut client);
1804 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1805
1806 axis.tick(&mut view, &mut client);
1808 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
1809 assert_eq!(axis.home_offset, 3000);
1810
1811 view.velocity_actual = 0;
1813 axis.tick(&mut view, &mut client);
1814 assert_eq!(axis.op, AxisOp::Idle);
1815 }
1816
1817 #[test]
1818 fn soft_homing_limit_switch_suppresses_halt() {
1819 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1820 let mut view = MockView::new();
1821 enable_axis(&mut axis, &mut view, &mut client);
1822
1823 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
1825
1826 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1831 axis.tick(&mut view, &mut client); view.positive_limit = true;
1835 view.velocity_actual = 100; view.position_actual = 8000;
1837 axis.tick(&mut view, &mut client);
1838
1839 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1841 assert!(!axis.is_error);
1842 }
1843
1844 #[test]
1845 fn soft_homing_opposite_limit_still_protects() {
1846 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1847 let mut view = MockView::new();
1848 enable_axis(&mut axis, &mut view, &mut client);
1849
1850 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
1852
1853 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1858 axis.tick(&mut view, &mut client); view.negative_limit = true;
1863 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
1865
1866 assert!(axis.is_error);
1868 assert!(axis.error_message.contains("Negative limit switch"));
1869 }
1870
1871 #[test]
1872 fn soft_homing_sensor_already_active_rejects() {
1873 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1874 let mut view = MockView::new();
1875 enable_axis(&mut axis, &mut view, &mut client);
1876
1877 view.home_sensor = true;
1879 axis.tick(&mut view, &mut client); axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
1882
1883 assert!(axis.is_error);
1885 assert!(axis.error_message.contains("already in trigger state"));
1886 assert_eq!(axis.op, AxisOp::Idle);
1887 }
1888
1889 #[test]
1890 fn soft_homing_negative_direction_sets_negative_target() {
1891 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1892 let mut view = MockView::new();
1893 enable_axis(&mut axis, &mut view, &mut client);
1894
1895 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
1896 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
1900 }
1901
1902 #[test]
1903 fn home_integrated_method_starts_hardware_homing() {
1904 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1905 let mut view = MockView::new();
1906 enable_axis(&mut axis, &mut view, &mut client);
1907
1908 axis.home(&mut view, HomingMethod::CurrentPosition);
1909 assert!(matches!(axis.op, AxisOp::Homing(0)));
1910 assert_eq!(axis.homing_method, 37);
1911 }
1912
1913 #[test]
1914 fn home_integrated_arbitrary_code() {
1915 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1916 let mut view = MockView::new();
1917 enable_axis(&mut axis, &mut view, &mut client);
1918
1919 axis.home(&mut view, HomingMethod::Integrated(35));
1920 assert!(matches!(axis.op, AxisOp::Homing(0)));
1921 assert_eq!(axis.homing_method, 35);
1922 }
1923
1924 #[test]
1925 fn hardware_homing_skips_speed_sdos_when_zero() {
1926 use mechutil::ipc::CommandMessage;
1927
1928 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
1929 let mut view = MockView::new();
1930 enable_axis(&mut axis, &mut view, &mut client);
1931
1932 axis.home(&mut view, HomingMethod::Integrated(37));
1934
1935 axis.tick(&mut view, &mut client);
1937 assert!(matches!(axis.op, AxisOp::Homing(1)));
1938
1939 let _ = write_rx.try_recv();
1941
1942 let tid = axis.homing_sdo_tid;
1944 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
1945 client.poll();
1946 axis.tick(&mut view, &mut client);
1947
1948 assert!(matches!(axis.op, AxisOp::Homing(8)));
1950 }
1951
1952 #[test]
1953 fn hardware_homing_writes_speed_sdos_when_nonzero() {
1954 use mechutil::ipc::CommandMessage;
1955
1956 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
1957 let mut view = MockView::new();
1958 enable_axis(&mut axis, &mut view, &mut client);
1959
1960 axis.home(&mut view, HomingMethod::Integrated(37));
1962
1963 axis.tick(&mut view, &mut client);
1965 assert!(matches!(axis.op, AxisOp::Homing(1)));
1966 let _ = write_rx.try_recv();
1967
1968 let tid = axis.homing_sdo_tid;
1970 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
1971 client.poll();
1972 axis.tick(&mut view, &mut client);
1973 assert!(matches!(axis.op, AxisOp::Homing(2)));
1975 }
1976
1977 #[test]
1978 fn soft_homing_edge_during_ack_step() {
1979 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1980 let mut view = MockView::new();
1981 enable_axis(&mut axis, &mut view, &mut client);
1982
1983 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
1984 axis.tick(&mut view, &mut client); view.home_sensor = true;
1988 view.position_actual = 2000;
1989 axis.tick(&mut view, &mut client);
1990
1991 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1993 }
1994
1995 #[test]
1996 fn soft_homing_applies_home_position() {
1997 let mut cfg = soft_homing_config();
2000 cfg.home_position = 90.0;
2001
2002 use tokio::sync::mpsc;
2003 let (write_tx, write_rx) = mpsc::unbounded_channel();
2004 let (response_tx, response_rx) = mpsc::unbounded_channel();
2005 let mut client = CommandClient::new(write_tx, response_rx);
2006 let mut axis = Axis::new(cfg, "TestDrive");
2007 let _ = (response_tx, write_rx);
2008
2009 let mut view = MockView::new();
2010 enable_axis(&mut axis, &mut view, &mut client);
2011
2012 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2013
2014 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2019 axis.tick(&mut view, &mut client); view.home_sensor = true;
2023 view.position_actual = 5000;
2024 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); assert_eq!(axis.home_offset, 1800);
2032
2033 view.velocity_actual = 0;
2035 axis.tick(&mut view, &mut client);
2036 assert_eq!(axis.op, AxisOp::Idle);
2037
2038 assert!((axis.position - 90.0).abs() < 0.1);
2041 }
2042
2043 #[test]
2044 fn soft_homing_default_home_position_zero() {
2045 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2047 let mut view = MockView::new();
2048 enable_axis(&mut axis, &mut view, &mut client);
2049
2050 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2051
2052 axis.tick(&mut view, &mut client);
2054 view.status_word = 0x1027;
2055 axis.tick(&mut view, &mut client);
2056 view.status_word = 0x0027;
2057 axis.tick(&mut view, &mut client);
2058
2059 view.home_sensor = true;
2061 view.position_actual = 5000;
2062 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client);
2066
2067 assert_eq!(axis.home_offset, 5000);
2069
2070 view.status_word = 0x0427;
2072 axis.tick(&mut view, &mut client);
2073
2074 assert!((axis.position - 0.0).abs() < 0.01);
2076 }
2077}