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 SoftHomeEdge {
69 Rising,
70 Falling,
71}
72
73pub struct Axis {
83 config: AxisConfig,
84 sdo: SdoClient,
85
86 op: AxisOp,
88 home_offset: i32,
89 last_raw_position: i32,
90 op_started: Option<Instant>,
91 op_timeout: Duration,
92 pending_move_target: i32,
93 pending_move_vel: u32,
94 pending_move_accel: u32,
95 pending_move_decel: u32,
96 homing_method: i8,
97 homing_sdo_tid: u32,
98 soft_home_sensor: SoftHomeSensor,
99 soft_home_edge: SoftHomeEdge,
100 soft_home_direction: f64,
101 prev_positive_limit: bool,
102 prev_negative_limit: bool,
103 prev_home_sensor: bool,
104
105 pub is_error: bool,
109 pub error_code: u32,
111 pub error_message: String,
113 pub motor_on: bool,
115 pub is_busy: bool,
121 pub in_motion: bool,
123 pub moving_positive: bool,
125 pub moving_negative: bool,
127 pub position: f64,
129 pub raw_position: i64,
131 pub speed: f64,
133 pub at_max_limit: bool,
135 pub at_min_limit: bool,
137 pub at_positive_limit_switch: bool,
139 pub at_negative_limit_switch: bool,
141 pub home_sensor: bool,
143}
144
145impl Axis {
146 pub fn new(config: AxisConfig, device_name: &str) -> Self {
151 Self {
152 config,
153 sdo: SdoClient::new(device_name),
154 op: AxisOp::Idle,
155 home_offset: 0,
156 last_raw_position: 0,
157 op_started: None,
158 op_timeout: Duration::from_secs(5),
159 pending_move_target: 0,
160 pending_move_vel: 0,
161 pending_move_accel: 0,
162 pending_move_decel: 0,
163 homing_method: 37,
164 homing_sdo_tid: 0,
165 soft_home_sensor: SoftHomeSensor::HomeSensor,
166 soft_home_edge: SoftHomeEdge::Rising,
167 soft_home_direction: 1.0,
168 prev_positive_limit: false,
169 prev_negative_limit: false,
170 prev_home_sensor: false,
171 is_error: false,
172 error_code: 0,
173 error_message: String::new(),
174 motor_on: false,
175 is_busy: false,
176 in_motion: false,
177 moving_positive: false,
178 moving_negative: false,
179 position: 0.0,
180 raw_position: 0,
181 speed: 0.0,
182 at_max_limit: false,
183 at_min_limit: false,
184 at_positive_limit_switch: false,
185 at_negative_limit_switch: false,
186 home_sensor: false,
187 }
188 }
189
190 pub fn config(&self) -> &AxisConfig {
192 &self.config
193 }
194
195 pub fn move_absolute(
205 &mut self,
206 view: &mut impl AxisView,
207 target: f64,
208 vel: f64,
209 accel: f64,
210 decel: f64,
211 ) {
212 if let Some(msg) = self.check_target_limit(target) {
213 self.set_op_error(&msg);
214 return;
215 }
216
217 let cpu = self.config.counts_per_user();
218 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
219 let raw_vel = (vel * cpu).round() as u32;
220 let raw_accel = (accel * cpu).round() as u32;
221 let raw_decel = (decel * cpu).round() as u32;
222
223 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute);
224 }
225
226 pub fn move_relative(
232 &mut self,
233 view: &mut impl AxisView,
234 distance: f64,
235 vel: f64,
236 accel: f64,
237 decel: f64,
238 ) {
239 if let Some(msg) = self.check_target_limit(self.position + distance) {
240 self.set_op_error(&msg);
241 return;
242 }
243
244 let cpu = self.config.counts_per_user();
245 let raw_distance = self.config.to_counts(distance).round() as i32;
246 let raw_vel = (vel * cpu).round() as u32;
247 let raw_accel = (accel * cpu).round() as u32;
248 let raw_decel = (decel * cpu).round() as u32;
249
250 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative);
251 }
252
253 fn start_move(
254 &mut self,
255 view: &mut impl AxisView,
256 raw_target: i32,
257 raw_vel: u32,
258 raw_accel: u32,
259 raw_decel: u32,
260 kind: MoveKind,
261 ) {
262 self.pending_move_target = raw_target;
263 self.pending_move_vel = raw_vel;
264 self.pending_move_accel = raw_accel;
265 self.pending_move_decel = raw_decel;
266
267 view.set_target_position(raw_target);
269 view.set_profile_velocity(raw_vel);
270 view.set_profile_acceleration(raw_accel);
271 view.set_profile_deceleration(raw_decel);
272
273 let mut cw = RawControlWord(view.control_word());
275 cw.set_bit(6, kind == MoveKind::Relative);
276 cw.set_bit(4, true); view.set_control_word(cw.raw());
278
279 self.op = AxisOp::Moving(kind, 1);
280 self.op_started = Some(Instant::now());
281 }
282
283 pub fn halt(&mut self, view: &mut impl AxisView) {
285 let mut cw = RawControlWord(view.control_word());
286 cw.set_bit(8, true); view.set_control_word(cw.raw());
288 self.op = AxisOp::Halting;
289 }
290
291 pub fn enable(&mut self, view: &mut impl AxisView) {
299 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
301 let mut cw = RawControlWord(view.control_word());
302 cw.cmd_shutdown();
303 view.set_control_word(cw.raw());
304
305 self.op = AxisOp::Enabling(1);
306 self.op_started = Some(Instant::now());
307 }
308
309 pub fn disable(&mut self, view: &mut impl AxisView) {
311 let mut cw = RawControlWord(view.control_word());
312 cw.cmd_disable_operation();
313 view.set_control_word(cw.raw());
314
315 self.op = AxisOp::Disabling(1);
316 self.op_started = Some(Instant::now());
317 }
318
319 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
323 let mut cw = RawControlWord(view.control_word());
325 cw.cmd_clear_fault_reset();
326 view.set_control_word(cw.raw());
327
328 self.is_error = false;
329 self.error_code = 0;
330 self.error_message.clear();
331 self.op = AxisOp::FaultRecovery(1);
332 self.op_started = Some(Instant::now());
333 }
334
335 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
343 if method.is_integrated() {
344 self.homing_method = method.cia402_code();
345 self.op = AxisOp::Homing(0);
346 self.op_started = Some(Instant::now());
347 let _ = view;
348 } else {
349 self.configure_soft_homing(method);
350 self.start_soft_homing(view);
351 }
352 }
353
354 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
363 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
364 }
365
366 pub fn set_software_max_limit(&mut self, user_units: f64) {
368 self.config.max_position_limit = user_units;
369 self.config.enable_max_position_limit = true;
370 }
371
372 pub fn set_software_min_limit(&mut self, user_units: f64) {
374 self.config.min_position_limit = user_units;
375 self.config.enable_min_position_limit = true;
376 }
377
378 pub fn sdo_write(
384 &mut self,
385 client: &mut CommandClient,
386 index: u16,
387 sub_index: u8,
388 value: serde_json::Value,
389 ) {
390 self.sdo.write(client, index, sub_index, value);
391 }
392
393 pub fn sdo_read(
395 &mut self,
396 client: &mut CommandClient,
397 index: u16,
398 sub_index: u8,
399 ) -> u32 {
400 self.sdo.read(client, index, sub_index)
401 }
402
403 pub fn sdo_result(
405 &mut self,
406 client: &mut CommandClient,
407 tid: u32,
408 ) -> SdoResult {
409 self.sdo.result(client, tid, Duration::from_secs(5))
410 }
411
412 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
426 self.check_faults(view);
427 self.progress_op(view, client);
428 self.update_outputs(view);
429 self.check_limits(view);
430 }
431
432 fn update_outputs(&mut self, view: &impl AxisView) {
437 let raw = view.position_actual();
438 self.raw_position = raw as i64;
439 self.position = self.config.to_user((raw - self.home_offset) as f64);
440
441 let vel = view.velocity_actual();
442 self.speed = self.config.to_user(vel.unsigned_abs() as f64).abs();
443 self.moving_positive = vel > 0;
444 self.moving_negative = vel < 0;
445 self.is_busy = self.op != AxisOp::Idle;
446 self.in_motion = matches!(self.op, AxisOp::Moving(_, _) | AxisOp::SoftHoming(_));
447
448 let sw = RawStatusWord(view.status_word());
449 self.motor_on = sw.state() == Cia402State::OperationEnabled;
450
451 self.last_raw_position = raw;
452 }
453
454 fn check_faults(&mut self, view: &impl AxisView) {
459 let sw = RawStatusWord(view.status_word());
460 let state = sw.state();
461
462 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
463 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
464 self.is_error = true;
465 let ec = view.error_code();
466 if ec != 0 {
467 self.error_code = ec as u32;
468 }
469 self.error_message = format!("Drive fault (state: {})", state);
470 self.op = AxisOp::Idle;
472 self.op_started = None;
473 }
474 }
475 }
476
477 fn op_timed_out(&self) -> bool {
482 self.op_started
483 .map_or(false, |t| t.elapsed() > self.op_timeout)
484 }
485
486 fn set_op_error(&mut self, msg: &str) {
487 self.is_error = true;
488 self.error_message = msg.to_string();
489 self.op = AxisOp::Idle;
490 self.op_started = None;
491 self.is_busy = false;
492 self.in_motion = false;
493 log::error!("Axis error: {}", msg);
494 }
495
496 fn complete_op(&mut self) {
497 self.op = AxisOp::Idle;
498 self.op_started = None;
499 }
500
501 fn check_target_limit(&self, target: f64) -> Option<String> {
508 if self.config.enable_max_position_limit && target > self.config.max_position_limit {
509 Some(format!(
510 "Target {:.3} exceeds max software limit {:.3}",
511 target, self.config.max_position_limit
512 ))
513 } else if self.config.enable_min_position_limit && target < self.config.min_position_limit {
514 Some(format!(
515 "Target {:.3} exceeds min software limit {:.3}",
516 target, self.config.min_position_limit
517 ))
518 } else {
519 None
520 }
521 }
522
523 fn check_limits(&mut self, view: &mut impl AxisView) {
532 let sw_max = self.config.enable_max_position_limit
534 && self.position >= self.config.max_position_limit;
535 let sw_min = self.config.enable_min_position_limit
536 && self.position <= self.config.min_position_limit;
537
538 self.at_max_limit = sw_max;
539 self.at_min_limit = sw_min;
540
541 let hw_pos = view.positive_limit_active();
543 let hw_neg = view.negative_limit_active();
544
545 self.at_positive_limit_switch = hw_pos;
546 self.at_negative_limit_switch = hw_neg;
547
548 self.home_sensor = view.home_sensor_active();
550
551 self.prev_positive_limit = hw_pos;
553 self.prev_negative_limit = hw_neg;
554 self.prev_home_sensor = view.home_sensor_active();
555
556 let is_moving = matches!(self.op, AxisOp::Moving(_, _));
558 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
559
560 if !is_moving && !is_soft_homing {
561 return;
562 }
563
564 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
566 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
567
568 let effective_hw_pos = hw_pos && !suppress_pos;
569 let effective_hw_neg = hw_neg && !suppress_neg;
570
571 let effective_sw_max = sw_max && !is_soft_homing;
573 let effective_sw_min = sw_min && !is_soft_homing;
574
575 let positive_blocked = (effective_sw_max || effective_hw_pos) && self.moving_positive;
576 let negative_blocked = (effective_sw_min || effective_hw_neg) && self.moving_negative;
577
578 if positive_blocked || negative_blocked {
579 let mut cw = RawControlWord(view.control_word());
580 cw.set_bit(8, true); view.set_control_word(cw.raw());
582
583 let msg = if effective_hw_pos && self.moving_positive {
584 "Positive limit switch active".to_string()
585 } else if effective_hw_neg && self.moving_negative {
586 "Negative limit switch active".to_string()
587 } else if effective_sw_max && self.moving_positive {
588 format!(
589 "Software position limit: position {:.3} >= max {:.3}",
590 self.position, self.config.max_position_limit
591 )
592 } else {
593 format!(
594 "Software position limit: position {:.3} <= min {:.3}",
595 self.position, self.config.min_position_limit
596 )
597 };
598 self.set_op_error(&msg);
599 }
600 }
601
602 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
607 match self.op.clone() {
608 AxisOp::Idle => {}
609 AxisOp::Enabling(step) => self.tick_enabling(view, step),
610 AxisOp::Disabling(step) => self.tick_disabling(view, step),
611 AxisOp::Moving(kind, step) => self.tick_moving(view, kind, step),
612 AxisOp::Homing(step) => self.tick_homing(view, client, step),
613 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, step),
614 AxisOp::Halting => self.tick_halting(view),
615 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
616 }
617 }
618
619 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
624 match step {
625 1 => {
626 let sw = RawStatusWord(view.status_word());
627 if sw.state() == Cia402State::ReadyToSwitchOn {
628 let mut cw = RawControlWord(view.control_word());
629 cw.cmd_enable_operation();
630 view.set_control_word(cw.raw());
631 self.op = AxisOp::Enabling(2);
632 } else if self.op_timed_out() {
633 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
634 }
635 }
636 2 => {
637 let sw = RawStatusWord(view.status_word());
638 if sw.state() == Cia402State::OperationEnabled {
639 self.home_offset = view.position_actual();
640 log::info!("Axis enabled — home captured at {}", self.home_offset);
641 self.complete_op();
642 } else if self.op_timed_out() {
643 self.set_op_error("Enable timeout: waiting for OperationEnabled");
644 }
645 }
646 _ => self.complete_op(),
647 }
648 }
649
650 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
654 match step {
655 1 => {
656 let sw = RawStatusWord(view.status_word());
657 if sw.state() != Cia402State::OperationEnabled {
658 self.complete_op();
659 } else if self.op_timed_out() {
660 self.set_op_error("Disable timeout: drive still in OperationEnabled");
661 }
662 }
663 _ => self.complete_op(),
664 }
665 }
666
667 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8) {
673 match step {
674 1 => {
675 let sw = RawStatusWord(view.status_word());
677 if sw.raw() & (1 << 12) != 0 {
678 let mut cw = RawControlWord(view.control_word());
680 cw.set_bit(4, false);
681 view.set_control_word(cw.raw());
682 self.op = AxisOp::Moving(kind, 2);
683 } else if self.op_timed_out() {
684 self.set_op_error("Move timeout: set-point not acknowledged");
685 }
686 }
687 2 => {
688 let sw = RawStatusWord(view.status_word());
690 if sw.target_reached() {
691 self.complete_op();
692 } else if self.op_timed_out() {
693 self.set_op_error("Move timeout: target not reached");
694 }
695 }
696 _ => self.complete_op(),
697 }
698 }
699
700 fn tick_homing(
718 &mut self,
719 view: &mut impl AxisView,
720 client: &mut CommandClient,
721 step: u8,
722 ) {
723 match step {
724 0 => {
725 self.homing_sdo_tid = self.sdo.write(
727 client,
728 0x6098,
729 0,
730 json!(self.homing_method),
731 );
732 self.op = AxisOp::Homing(1);
733 }
734 1 => {
735 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
737 SdoResult::Ok(_) => {
738 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
740 self.op = AxisOp::Homing(8);
741 } else {
742 self.op = AxisOp::Homing(2);
743 }
744 }
745 SdoResult::Pending => {
746 if self.op_timed_out() {
747 self.set_op_error("Homing timeout: SDO write for homing method");
748 }
749 }
750 SdoResult::Err(e) => {
751 self.set_op_error(&format!("Homing SDO error: {}", e));
752 }
753 SdoResult::Timeout => {
754 self.set_op_error("Homing timeout: SDO write timed out");
755 }
756 }
757 }
758 2 => {
759 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
761 self.homing_sdo_tid = self.sdo.write(
762 client,
763 0x6099,
764 1,
765 json!(speed_counts),
766 );
767 self.op = AxisOp::Homing(3);
768 }
769 3 => {
770 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
771 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
772 SdoResult::Pending => {
773 if self.op_timed_out() {
774 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
775 }
776 }
777 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
778 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
779 }
780 }
781 4 => {
782 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
784 self.homing_sdo_tid = self.sdo.write(
785 client,
786 0x6099,
787 2,
788 json!(speed_counts),
789 );
790 self.op = AxisOp::Homing(5);
791 }
792 5 => {
793 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
794 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
795 SdoResult::Pending => {
796 if self.op_timed_out() {
797 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
798 }
799 }
800 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
801 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
802 }
803 }
804 6 => {
805 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
807 self.homing_sdo_tid = self.sdo.write(
808 client,
809 0x609A,
810 0,
811 json!(accel_counts),
812 );
813 self.op = AxisOp::Homing(7);
814 }
815 7 => {
816 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
817 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
818 SdoResult::Pending => {
819 if self.op_timed_out() {
820 self.set_op_error("Homing timeout: SDO write for homing acceleration");
821 }
822 }
823 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
824 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
825 }
826 }
827 8 => {
828 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
830 self.op = AxisOp::Homing(9);
831 }
832 9 => {
833 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
835 self.op = AxisOp::Homing(10);
836 } else if self.op_timed_out() {
837 self.set_op_error("Homing timeout: mode not confirmed");
838 }
839 }
840 10 => {
841 let mut cw = RawControlWord(view.control_word());
843 cw.set_bit(4, true);
844 view.set_control_word(cw.raw());
845 self.op = AxisOp::Homing(11);
846 }
847 11 => {
848 let sw = view.status_word();
851 let error = sw & (1 << 13) != 0;
852 let attained = sw & (1 << 12) != 0;
853 let reached = sw & (1 << 10) != 0;
854
855 if error {
856 self.set_op_error("Homing error: drive reported homing failure");
857 } else if attained && reached {
858 self.op = AxisOp::Homing(12);
860 } else if self.op_timed_out() {
861 self.set_op_error("Homing timeout: procedure did not complete");
862 }
863 }
864 12 => {
865 self.home_offset = view.position_actual()
868 - self.config.to_counts(self.config.home_position).round() as i32;
869 let mut cw = RawControlWord(view.control_word());
871 cw.set_bit(4, false);
872 view.set_control_word(cw.raw());
873 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
875 log::info!("Homing complete — home offset: {}", self.home_offset);
876 self.complete_op();
877 }
878 _ => self.complete_op(),
879 }
880 }
881
882 fn configure_soft_homing(&mut self, method: HomingMethod) {
885 match method {
886 HomingMethod::LimitSwitchPosRt => {
887 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
888 self.soft_home_edge = SoftHomeEdge::Rising;
889 self.soft_home_direction = 1.0;
890 }
891 HomingMethod::LimitSwitchNegRt => {
892 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
893 self.soft_home_edge = SoftHomeEdge::Rising;
894 self.soft_home_direction = -1.0;
895 }
896 HomingMethod::LimitSwitchPosFt => {
897 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
898 self.soft_home_edge = SoftHomeEdge::Falling;
899 self.soft_home_direction = 1.0;
900 }
901 HomingMethod::LimitSwitchNegFt => {
902 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
903 self.soft_home_edge = SoftHomeEdge::Falling;
904 self.soft_home_direction = -1.0;
905 }
906 HomingMethod::HomeSensorPosRt => {
907 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
908 self.soft_home_edge = SoftHomeEdge::Rising;
909 self.soft_home_direction = 1.0;
910 }
911 HomingMethod::HomeSensorNegRt => {
912 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
913 self.soft_home_edge = SoftHomeEdge::Rising;
914 self.soft_home_direction = -1.0;
915 }
916 HomingMethod::HomeSensorPosFt => {
917 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
918 self.soft_home_edge = SoftHomeEdge::Falling;
919 self.soft_home_direction = 1.0;
920 }
921 HomingMethod::HomeSensorNegFt => {
922 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
923 self.soft_home_edge = SoftHomeEdge::Falling;
924 self.soft_home_direction = -1.0;
925 }
926 _ => {} }
928 }
929
930 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
931 let already_active = match (self.soft_home_sensor, self.soft_home_edge) {
933 (SoftHomeSensor::PositiveLimit, SoftHomeEdge::Rising) => view.positive_limit_active(),
934 (SoftHomeSensor::NegativeLimit, SoftHomeEdge::Rising) => view.negative_limit_active(),
935 (SoftHomeSensor::HomeSensor, SoftHomeEdge::Rising) => view.home_sensor_active(),
936 (SoftHomeSensor::PositiveLimit, SoftHomeEdge::Falling) => !view.positive_limit_active(),
937 (SoftHomeSensor::NegativeLimit, SoftHomeEdge::Falling) => !view.negative_limit_active(),
938 (SoftHomeSensor::HomeSensor, SoftHomeEdge::Falling) => !view.home_sensor_active(),
939 };
940
941 if already_active {
942 self.set_op_error("Software homing: sensor already in trigger state");
943 return;
944 }
945
946 self.op = AxisOp::SoftHoming(0);
947 self.op_started = Some(Instant::now());
948 }
949
950 fn check_soft_home_edge(&self, view: &impl AxisView) -> bool {
951 let (current, prev) = match self.soft_home_sensor {
952 SoftHomeSensor::PositiveLimit => (view.positive_limit_active(), self.prev_positive_limit),
953 SoftHomeSensor::NegativeLimit => (view.negative_limit_active(), self.prev_negative_limit),
954 SoftHomeSensor::HomeSensor => (view.home_sensor_active(), self.prev_home_sensor),
955 };
956 match self.soft_home_edge {
957 SoftHomeEdge::Rising => !prev && current, SoftHomeEdge::Falling => prev && !current, }
960 }
961
962 fn tick_soft_homing(&mut self, view: &mut impl AxisView, step: u8) {
972 match step {
973 0 => {
974 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
976
977 let target = (self.soft_home_direction * 999_999.0) as i32 + self.home_offset;
979 view.set_target_position(target);
980
981 let cpu = self.config.counts_per_user();
983 let vel = (self.config.homing_speed * cpu).round() as u32;
984 let accel = (self.config.homing_accel * cpu).round() as u32;
985 let decel = (self.config.homing_decel * cpu).round() as u32;
986 view.set_profile_velocity(vel);
987 view.set_profile_acceleration(accel);
988 view.set_profile_deceleration(decel);
989
990 let mut cw = RawControlWord(view.control_word());
992 cw.set_bit(4, true);
993 cw.set_bit(6, false); cw.set_bit(8, false); view.set_control_word(cw.raw());
996
997 self.op = AxisOp::SoftHoming(1);
998 }
999 1 => {
1000 if self.check_soft_home_edge(view) {
1002 self.op = AxisOp::SoftHoming(4);
1003 return;
1004 }
1005 let sw = RawStatusWord(view.status_word());
1006 if sw.raw() & (1 << 12) != 0 {
1007 let mut cw = RawControlWord(view.control_word());
1009 cw.set_bit(4, false);
1010 view.set_control_word(cw.raw());
1011 self.op = AxisOp::SoftHoming(2);
1012 } else if self.op_timed_out() {
1013 self.set_op_error("Software homing timeout: set-point not acknowledged");
1014 }
1015 }
1016 2 => {
1017 if self.check_soft_home_edge(view) {
1019 self.op = AxisOp::SoftHoming(4);
1020 return;
1021 }
1022 self.op = AxisOp::SoftHoming(3);
1023 }
1024 3 => {
1025 if self.check_soft_home_edge(view) {
1027 self.op = AxisOp::SoftHoming(4);
1028 } else if self.op_timed_out() {
1029 self.set_op_error("Software homing timeout: sensor edge not detected");
1030 }
1031 }
1032 4 => {
1033 let mut cw = RawControlWord(view.control_word());
1036 cw.set_bit(8, true); view.set_control_word(cw.raw());
1038 self.home_offset = view.position_actual()
1039 - self.config.to_counts(self.config.home_position).round() as i32;
1040 log::info!("Software homing: edge detected, home offset: {}", self.home_offset);
1041 self.op = AxisOp::SoftHoming(5);
1042 }
1043 5 => {
1044 let sw = RawStatusWord(view.status_word());
1046 if sw.target_reached() {
1047 let mut cw = RawControlWord(view.control_word());
1049 cw.set_bit(8, false);
1050 view.set_control_word(cw.raw());
1051 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1052 log::info!("Software homing complete — home offset: {}", self.home_offset);
1053 self.complete_op();
1054 } else if self.op_timed_out() {
1055 self.set_op_error("Software homing timeout: halt not acknowledged");
1056 }
1057 }
1058 _ => self.complete_op(),
1059 }
1060 }
1061
1062 fn tick_halting(&mut self, _view: &mut impl AxisView) {
1064 self.complete_op();
1067 }
1068
1069 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1074 match step {
1075 1 => {
1076 let mut cw = RawControlWord(view.control_word());
1078 cw.cmd_fault_reset();
1079 view.set_control_word(cw.raw());
1080 self.op = AxisOp::FaultRecovery(2);
1081 }
1082 2 => {
1083 let sw = RawStatusWord(view.status_word());
1085 let state = sw.state();
1086 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1087 log::info!("Fault cleared (drive state: {})", state);
1088 self.complete_op();
1089 } else if self.op_timed_out() {
1090 self.set_op_error("Fault reset timeout: drive still faulted");
1091 }
1092 }
1093 _ => self.complete_op(),
1094 }
1095 }
1096}
1097
1098#[cfg(test)]
1103mod tests {
1104 use super::*;
1105
1106 struct MockView {
1108 control_word: u16,
1109 status_word: u16,
1110 target_position: i32,
1111 profile_velocity: u32,
1112 profile_acceleration: u32,
1113 profile_deceleration: u32,
1114 modes_of_operation: i8,
1115 modes_of_operation_display: i8,
1116 position_actual: i32,
1117 velocity_actual: i32,
1118 error_code: u16,
1119 positive_limit: bool,
1120 negative_limit: bool,
1121 home_sensor: bool,
1122 }
1123
1124 impl MockView {
1125 fn new() -> Self {
1126 Self {
1127 control_word: 0,
1128 status_word: 0x0040, target_position: 0,
1130 profile_velocity: 0,
1131 profile_acceleration: 0,
1132 profile_deceleration: 0,
1133 modes_of_operation: 0,
1134 modes_of_operation_display: 1, position_actual: 0,
1136 velocity_actual: 0,
1137 error_code: 0,
1138 positive_limit: false,
1139 negative_limit: false,
1140 home_sensor: false,
1141 }
1142 }
1143
1144 fn set_state(&mut self, state: u16) {
1145 self.status_word = state;
1146 }
1147 }
1148
1149 impl AxisView for MockView {
1150 fn control_word(&self) -> u16 { self.control_word }
1151 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1152 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1153 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1154 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1155 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1156 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1157 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1158 fn status_word(&self) -> u16 { self.status_word }
1159 fn position_actual(&self) -> i32 { self.position_actual }
1160 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1161 fn error_code(&self) -> u16 { self.error_code }
1162 fn positive_limit_active(&self) -> bool { self.positive_limit }
1163 fn negative_limit_active(&self) -> bool { self.negative_limit }
1164 fn home_sensor_active(&self) -> bool { self.home_sensor }
1165 }
1166
1167 fn test_config() -> AxisConfig {
1168 AxisConfig::new(12_800).with_user_scale(360.0)
1169 }
1170
1171 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1173 use tokio::sync::mpsc;
1174 let (write_tx, write_rx) = mpsc::unbounded_channel();
1175 let (response_tx, response_rx) = mpsc::unbounded_channel();
1176 let client = CommandClient::new(write_tx, response_rx);
1177 let axis = Axis::new(test_config(), "TestDrive");
1178 (axis, client, response_tx, write_rx)
1179 }
1180
1181 #[test]
1182 fn axis_config_conversion() {
1183 let cfg = test_config();
1184 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
1186 }
1187
1188 #[test]
1189 fn enable_sequence_sets_pp_mode_and_shutdown() {
1190 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1191 let mut view = MockView::new();
1192
1193 axis.enable(&mut view);
1194
1195 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
1197 assert_eq!(view.control_word & 0x008F, 0x0006);
1199 assert_eq!(axis.op, AxisOp::Enabling(1));
1201
1202 view.set_state(0x0021); axis.tick(&mut view, &mut client);
1205
1206 assert_eq!(view.control_word & 0x008F, 0x000F);
1208 assert_eq!(axis.op, AxisOp::Enabling(2));
1209
1210 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1213
1214 assert_eq!(axis.op, AxisOp::Idle);
1216 assert!(axis.motor_on);
1217 }
1218
1219 #[test]
1220 fn move_absolute_sets_target() {
1221 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1222 let mut view = MockView::new();
1223 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1228
1229 assert_eq!(view.target_position, 1600);
1231 assert_eq!(view.profile_velocity, 3200);
1233 assert_eq!(view.profile_acceleration, 6400);
1235 assert_eq!(view.profile_deceleration, 6400);
1236 assert!(view.control_word & (1 << 4) != 0);
1238 assert!(view.control_word & (1 << 6) == 0);
1240 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1242 }
1243
1244 #[test]
1245 fn move_relative_sets_relative_bit() {
1246 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1247 let mut view = MockView::new();
1248 view.set_state(0x0027);
1249 axis.tick(&mut view, &mut client);
1250
1251 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
1252
1253 assert!(view.control_word & (1 << 6) != 0);
1255 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1)));
1256 }
1257
1258 #[test]
1259 fn move_completes_on_target_reached() {
1260 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1261 let mut view = MockView::new();
1262 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1264
1265 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1266
1267 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1270 assert!(view.control_word & (1 << 4) == 0);
1272
1273 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1276 assert_eq!(axis.op, AxisOp::Idle);
1278 assert!(!axis.in_motion);
1279 }
1280
1281 #[test]
1282 fn fault_detected_sets_error() {
1283 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1284 let mut view = MockView::new();
1285 view.set_state(0x0008); view.error_code = 0x1234;
1287
1288 axis.tick(&mut view, &mut client);
1289
1290 assert!(axis.is_error);
1291 assert_eq!(axis.error_code, 0x1234);
1292 assert!(axis.error_message.contains("fault"));
1293 }
1294
1295 #[test]
1296 fn fault_recovery_sequence() {
1297 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1298 let mut view = MockView::new();
1299 view.set_state(0x0008); axis.reset_faults(&mut view);
1302 assert!(view.control_word & 0x0080 == 0);
1304
1305 axis.tick(&mut view, &mut client);
1307 assert!(view.control_word & 0x0080 != 0);
1308
1309 view.set_state(0x0040);
1311 axis.tick(&mut view, &mut client);
1312 assert_eq!(axis.op, AxisOp::Idle);
1313 assert!(!axis.is_error);
1314 }
1315
1316 #[test]
1317 fn disable_sequence() {
1318 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1319 let mut view = MockView::new();
1320 view.set_state(0x0027); axis.disable(&mut view);
1323 assert_eq!(view.control_word & 0x008F, 0x0007);
1325
1326 view.set_state(0x0023); axis.tick(&mut view, &mut client);
1329 assert_eq!(axis.op, AxisOp::Idle);
1330 }
1331
1332 #[test]
1333 fn position_tracks_with_home_offset() {
1334 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1335 let mut view = MockView::new();
1336 view.set_state(0x0027);
1337 view.position_actual = 5000;
1338
1339 axis.enable(&mut view);
1341 view.set_state(0x0021);
1342 axis.tick(&mut view, &mut client);
1343 view.set_state(0x0027);
1344 axis.tick(&mut view, &mut client);
1345
1346 assert_eq!(axis.home_offset, 5000);
1348
1349 assert!((axis.position - 0.0).abs() < 0.01);
1351
1352 view.position_actual = 6600;
1354 axis.tick(&mut view, &mut client);
1355
1356 assert!((axis.position - 45.0).abs() < 0.1);
1358 }
1359
1360 #[test]
1361 fn set_position_adjusts_home_offset() {
1362 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1363 let mut view = MockView::new();
1364 view.position_actual = 3200;
1365
1366 axis.set_position(&view, 90.0);
1367 axis.tick(&mut view, &mut client);
1368
1369 assert_eq!(axis.home_offset, 0);
1371 assert!((axis.position - 90.0).abs() < 0.01);
1372 }
1373
1374 #[test]
1375 fn halt_sets_bit_and_goes_idle() {
1376 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1377 let mut view = MockView::new();
1378 view.set_state(0x0027);
1379
1380 axis.halt(&mut view);
1381 assert!(view.control_word & (1 << 8) != 0);
1383
1384 axis.tick(&mut view, &mut client);
1386 assert_eq!(axis.op, AxisOp::Idle);
1387 }
1388
1389 #[test]
1390 fn is_busy_tracks_operations() {
1391 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1392 let mut view = MockView::new();
1393
1394 axis.tick(&mut view, &mut client);
1396 assert!(!axis.is_busy);
1397
1398 axis.enable(&mut view);
1400 axis.tick(&mut view, &mut client);
1401 assert!(axis.is_busy);
1402
1403 view.set_state(0x0021);
1405 axis.tick(&mut view, &mut client);
1406 view.set_state(0x0027);
1407 axis.tick(&mut view, &mut client);
1408 assert!(!axis.is_busy);
1409
1410 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1412 axis.tick(&mut view, &mut client);
1413 assert!(axis.is_busy);
1414 assert!(axis.in_motion);
1415 }
1416
1417 #[test]
1418 fn fault_during_move_cancels_op() {
1419 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1420 let mut view = MockView::new();
1421 view.set_state(0x0027); axis.tick(&mut view, &mut client);
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.is_error);
1429
1430 view.set_state(0x0008); axis.tick(&mut view, &mut client);
1433
1434 assert!(!axis.is_busy);
1436 assert!(axis.is_error);
1437 assert_eq!(axis.op, AxisOp::Idle);
1438 }
1439
1440 #[test]
1441 fn move_absolute_rejected_by_max_limit() {
1442 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1443 let mut view = MockView::new();
1444 view.set_state(0x0027);
1445 axis.tick(&mut view, &mut client);
1446
1447 axis.set_software_max_limit(90.0);
1448 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
1449
1450 assert!(axis.is_error);
1452 assert_eq!(axis.op, AxisOp::Idle);
1453 assert!(axis.error_message.contains("max software limit"));
1454 }
1455
1456 #[test]
1457 fn move_absolute_rejected_by_min_limit() {
1458 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1459 let mut view = MockView::new();
1460 view.set_state(0x0027);
1461 axis.tick(&mut view, &mut client);
1462
1463 axis.set_software_min_limit(-10.0);
1464 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
1465
1466 assert!(axis.is_error);
1467 assert_eq!(axis.op, AxisOp::Idle);
1468 assert!(axis.error_message.contains("min software limit"));
1469 }
1470
1471 #[test]
1472 fn move_relative_rejected_by_max_limit() {
1473 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1474 let mut view = MockView::new();
1475 view.set_state(0x0027);
1476 axis.tick(&mut view, &mut client);
1477
1478 axis.set_software_max_limit(50.0);
1480 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
1481
1482 assert!(axis.is_error);
1483 assert_eq!(axis.op, AxisOp::Idle);
1484 assert!(axis.error_message.contains("max software limit"));
1485 }
1486
1487 #[test]
1488 fn move_within_limits_allowed() {
1489 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1490 let mut view = MockView::new();
1491 view.set_state(0x0027);
1492 axis.tick(&mut view, &mut client);
1493
1494 axis.set_software_max_limit(90.0);
1495 axis.set_software_min_limit(-90.0);
1496 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1497
1498 assert!(!axis.is_error);
1500 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1501 }
1502
1503 #[test]
1504 fn runtime_limit_halts_move_in_violated_direction() {
1505 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1506 let mut view = MockView::new();
1507 view.set_state(0x0027);
1508 axis.tick(&mut view, &mut client);
1509
1510 axis.set_software_max_limit(45.0);
1511 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1513
1514 view.position_actual = 1650;
1517 view.velocity_actual = 100; view.status_word = 0x1027;
1521 axis.tick(&mut view, &mut client);
1522 view.status_word = 0x0027;
1523 axis.tick(&mut view, &mut client);
1524
1525 assert!(axis.is_error);
1527 assert!(axis.at_max_limit);
1528 assert_eq!(axis.op, AxisOp::Idle);
1529 assert!(axis.error_message.contains("Software position limit"));
1530 assert!(view.control_word & (1 << 8) != 0);
1532 }
1533
1534 #[test]
1535 fn runtime_limit_allows_move_in_opposite_direction() {
1536 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1537 let mut view = MockView::new();
1538 view.set_state(0x0027);
1539 view.position_actual = 1778; axis.set_software_max_limit(45.0);
1542 axis.tick(&mut view, &mut client);
1543 assert!(axis.at_max_limit);
1544
1545 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
1547 assert!(!axis.is_error);
1548 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1549
1550 view.velocity_actual = -100;
1552 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1554 assert!(!axis.is_error);
1556 }
1557
1558 #[test]
1559 fn positive_limit_switch_halts_positive_move() {
1560 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1561 let mut view = MockView::new();
1562 view.set_state(0x0027);
1563 axis.tick(&mut view, &mut client);
1564
1565 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1567 view.velocity_actual = 100; view.status_word = 0x1027;
1570 axis.tick(&mut view, &mut client);
1571 view.status_word = 0x0027;
1572
1573 view.positive_limit = true;
1575 axis.tick(&mut view, &mut client);
1576
1577 assert!(axis.is_error);
1578 assert!(axis.at_positive_limit_switch);
1579 assert!(!axis.is_busy);
1580 assert!(axis.error_message.contains("Positive limit switch"));
1581 assert!(view.control_word & (1 << 8) != 0);
1583 }
1584
1585 #[test]
1586 fn negative_limit_switch_halts_negative_move() {
1587 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1588 let mut view = MockView::new();
1589 view.set_state(0x0027);
1590 axis.tick(&mut view, &mut client);
1591
1592 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
1594 view.velocity_actual = -100; view.status_word = 0x1027;
1596 axis.tick(&mut view, &mut client);
1597 view.status_word = 0x0027;
1598
1599 view.negative_limit = true;
1601 axis.tick(&mut view, &mut client);
1602
1603 assert!(axis.is_error);
1604 assert!(axis.at_negative_limit_switch);
1605 assert!(axis.error_message.contains("Negative limit switch"));
1606 }
1607
1608 #[test]
1609 fn limit_switch_allows_move_in_opposite_direction() {
1610 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1611 let mut view = MockView::new();
1612 view.set_state(0x0027);
1613 view.positive_limit = true;
1615 view.velocity_actual = -100;
1616 axis.tick(&mut view, &mut client);
1617 assert!(axis.at_positive_limit_switch);
1618
1619 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
1621 view.status_word = 0x1027;
1622 axis.tick(&mut view, &mut client);
1623
1624 assert!(!axis.is_error);
1626 assert!(matches!(axis.op, AxisOp::Moving(_, _)));
1627 }
1628
1629 #[test]
1630 fn limit_switch_ignored_when_not_moving() {
1631 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1632 let mut view = MockView::new();
1633 view.set_state(0x0027);
1634 view.positive_limit = true;
1635
1636 axis.tick(&mut view, &mut client);
1637
1638 assert!(axis.at_positive_limit_switch);
1640 assert!(!axis.is_error);
1641 }
1642
1643 #[test]
1644 fn home_sensor_output_tracks_view() {
1645 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1646 let mut view = MockView::new();
1647 view.set_state(0x0027);
1648
1649 axis.tick(&mut view, &mut client);
1650 assert!(!axis.home_sensor);
1651
1652 view.home_sensor = true;
1653 axis.tick(&mut view, &mut client);
1654 assert!(axis.home_sensor);
1655
1656 view.home_sensor = false;
1657 axis.tick(&mut view, &mut client);
1658 assert!(!axis.home_sensor);
1659 }
1660
1661 #[test]
1662 fn velocity_output_converted() {
1663 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1664 let mut view = MockView::new();
1665 view.set_state(0x0027);
1666 view.velocity_actual = 3200;
1668
1669 axis.tick(&mut view, &mut client);
1670
1671 assert!((axis.speed - 90.0).abs() < 0.1);
1672 assert!(axis.moving_positive);
1673 assert!(!axis.moving_negative);
1674 }
1675
1676 fn soft_homing_config() -> AxisConfig {
1679 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
1680 cfg.homing_speed = 10.0;
1681 cfg.homing_accel = 20.0;
1682 cfg.homing_decel = 20.0;
1683 cfg
1684 }
1685
1686 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1687 use tokio::sync::mpsc;
1688 let (write_tx, write_rx) = mpsc::unbounded_channel();
1689 let (response_tx, response_rx) = mpsc::unbounded_channel();
1690 let client = CommandClient::new(write_tx, response_rx);
1691 let axis = Axis::new(soft_homing_config(), "TestDrive");
1692 (axis, client, response_tx, write_rx)
1693 }
1694
1695 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
1697 view.set_state(0x0027); axis.tick(view, client);
1699 }
1700
1701 #[test]
1702 fn soft_homing_rising_edge_home_sensor_triggers_home() {
1703 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1704 let mut view = MockView::new();
1705 enable_axis(&mut axis, &mut view, &mut client);
1706
1707 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1709 assert!(matches!(axis.op, AxisOp::SoftHoming(0)));
1710
1711 axis.tick(&mut view, &mut client);
1713 assert!(matches!(axis.op, AxisOp::SoftHoming(1)));
1714 assert!(view.control_word & (1 << 4) != 0);
1716
1717 view.status_word = 0x1027;
1719 axis.tick(&mut view, &mut client);
1720 assert!(view.control_word & (1 << 4) == 0);
1722 assert!(matches!(axis.op, AxisOp::SoftHoming(2)));
1723
1724 view.status_word = 0x0027;
1726 axis.tick(&mut view, &mut client);
1727 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1728
1729 axis.tick(&mut view, &mut client);
1731 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1732
1733 view.home_sensor = true;
1735 view.position_actual = 5000;
1736 axis.tick(&mut view, &mut client);
1737 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1739
1740 axis.tick(&mut view, &mut client);
1742 assert!(view.control_word & (1 << 8) != 0); assert_eq!(axis.home_offset, 5000);
1744 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
1745
1746 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1749 assert_eq!(axis.op, AxisOp::Idle);
1751 assert!(!axis.is_busy);
1752 assert!(!axis.is_error);
1753 }
1754
1755 #[test]
1756 fn soft_homing_falling_edge_home_sensor_triggers_home() {
1757 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1758 let mut view = MockView::new();
1759 view.home_sensor = true;
1761 enable_axis(&mut axis, &mut view, &mut client);
1762
1763 axis.home(&mut view, HomingMethod::HomeSensorPosFt);
1765 assert!(matches!(axis.op, AxisOp::SoftHoming(0)));
1766
1767 axis.tick(&mut view, &mut client);
1769 view.status_word = 0x1027;
1771 axis.tick(&mut view, &mut client);
1772 view.status_word = 0x0027;
1774 axis.tick(&mut view, &mut client);
1775
1776 axis.tick(&mut view, &mut client);
1778 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1779
1780 view.home_sensor = false;
1782 view.position_actual = 3000;
1783 axis.tick(&mut view, &mut client);
1784 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1785
1786 axis.tick(&mut view, &mut client);
1788 view.status_word = 0x0427;
1789 axis.tick(&mut view, &mut client);
1790 assert_eq!(axis.op, AxisOp::Idle);
1791 assert_eq!(axis.home_offset, 3000);
1792 }
1793
1794 #[test]
1795 fn soft_homing_limit_switch_suppresses_halt() {
1796 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1797 let mut view = MockView::new();
1798 enable_axis(&mut axis, &mut view, &mut client);
1799
1800 axis.home(&mut view, HomingMethod::LimitSwitchPosRt);
1802
1803 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1808 axis.tick(&mut view, &mut client); view.positive_limit = true;
1812 view.velocity_actual = 100; view.position_actual = 8000;
1814 axis.tick(&mut view, &mut client);
1815
1816 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1818 assert!(!axis.is_error);
1819 }
1820
1821 #[test]
1822 fn soft_homing_opposite_limit_still_protects() {
1823 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1824 let mut view = MockView::new();
1825 enable_axis(&mut axis, &mut view, &mut client);
1826
1827 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1829
1830 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1835 axis.tick(&mut view, &mut client); view.negative_limit = true;
1840 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
1842
1843 assert!(axis.is_error);
1845 assert!(axis.error_message.contains("Negative limit switch"));
1846 }
1847
1848 #[test]
1849 fn soft_homing_sensor_already_active_rejects() {
1850 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1851 let mut view = MockView::new();
1852 enable_axis(&mut axis, &mut view, &mut client);
1853
1854 view.home_sensor = true;
1856 axis.tick(&mut view, &mut client); axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1859
1860 assert!(axis.is_error);
1862 assert!(axis.error_message.contains("already in trigger state"));
1863 assert_eq!(axis.op, AxisOp::Idle);
1864 }
1865
1866 #[test]
1867 fn soft_homing_negative_direction_sets_negative_target() {
1868 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1869 let mut view = MockView::new();
1870 enable_axis(&mut axis, &mut view, &mut client);
1871
1872 axis.home(&mut view, HomingMethod::HomeSensorNegRt);
1873 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
1877 }
1878
1879 #[test]
1880 fn home_integrated_method_starts_hardware_homing() {
1881 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1882 let mut view = MockView::new();
1883 enable_axis(&mut axis, &mut view, &mut client);
1884
1885 axis.home(&mut view, HomingMethod::CurrentPosition);
1886 assert!(matches!(axis.op, AxisOp::Homing(0)));
1887 assert_eq!(axis.homing_method, 37);
1888 }
1889
1890 #[test]
1891 fn home_integrated_arbitrary_code() {
1892 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1893 let mut view = MockView::new();
1894 enable_axis(&mut axis, &mut view, &mut client);
1895
1896 axis.home(&mut view, HomingMethod::Integrated(35));
1897 assert!(matches!(axis.op, AxisOp::Homing(0)));
1898 assert_eq!(axis.homing_method, 35);
1899 }
1900
1901 #[test]
1902 fn hardware_homing_skips_speed_sdos_when_zero() {
1903 use mechutil::ipc::CommandMessage;
1904
1905 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
1906 let mut view = MockView::new();
1907 enable_axis(&mut axis, &mut view, &mut client);
1908
1909 axis.home(&mut view, HomingMethod::Integrated(37));
1911
1912 axis.tick(&mut view, &mut client);
1914 assert!(matches!(axis.op, AxisOp::Homing(1)));
1915
1916 let _ = write_rx.try_recv();
1918
1919 let tid = axis.homing_sdo_tid;
1921 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
1922 client.poll();
1923 axis.tick(&mut view, &mut client);
1924
1925 assert!(matches!(axis.op, AxisOp::Homing(8)));
1927 }
1928
1929 #[test]
1930 fn hardware_homing_writes_speed_sdos_when_nonzero() {
1931 use mechutil::ipc::CommandMessage;
1932
1933 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
1934 let mut view = MockView::new();
1935 enable_axis(&mut axis, &mut view, &mut client);
1936
1937 axis.home(&mut view, HomingMethod::Integrated(37));
1939
1940 axis.tick(&mut view, &mut client);
1942 assert!(matches!(axis.op, AxisOp::Homing(1)));
1943 let _ = write_rx.try_recv();
1944
1945 let tid = axis.homing_sdo_tid;
1947 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
1948 client.poll();
1949 axis.tick(&mut view, &mut client);
1950 assert!(matches!(axis.op, AxisOp::Homing(2)));
1952 }
1953
1954 #[test]
1955 fn soft_homing_edge_during_ack_step() {
1956 let (mut axis, mut client, _resp_tx, _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::HomeSensorPosRt);
1961 axis.tick(&mut view, &mut client); view.home_sensor = true;
1965 view.position_actual = 2000;
1966 axis.tick(&mut view, &mut client);
1967
1968 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1970 }
1971
1972 #[test]
1973 fn soft_homing_applies_home_position() {
1974 let mut cfg = soft_homing_config();
1977 cfg.home_position = 90.0;
1978
1979 use tokio::sync::mpsc;
1980 let (write_tx, write_rx) = mpsc::unbounded_channel();
1981 let (response_tx, response_rx) = mpsc::unbounded_channel();
1982 let mut client = CommandClient::new(write_tx, response_rx);
1983 let mut axis = Axis::new(cfg, "TestDrive");
1984 let _ = (response_tx, write_rx);
1985
1986 let mut view = MockView::new();
1987 enable_axis(&mut axis, &mut view, &mut client);
1988
1989 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1990
1991 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1996 axis.tick(&mut view, &mut client); view.home_sensor = true;
2000 view.position_actual = 5000;
2001 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); assert_eq!(axis.home_offset, 1800);
2009
2010 view.status_word = 0x0427;
2012 axis.tick(&mut view, &mut client);
2013 assert_eq!(axis.op, AxisOp::Idle);
2014
2015 assert!((axis.position - 90.0).abs() < 0.1);
2018 }
2019
2020 #[test]
2021 fn soft_homing_default_home_position_zero() {
2022 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2024 let mut view = MockView::new();
2025 enable_axis(&mut axis, &mut view, &mut client);
2026
2027 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
2028
2029 axis.tick(&mut view, &mut client);
2031 view.status_word = 0x1027;
2032 axis.tick(&mut view, &mut client);
2033 view.status_word = 0x0027;
2034 axis.tick(&mut view, &mut client);
2035
2036 view.home_sensor = true;
2038 view.position_actual = 5000;
2039 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client);
2043
2044 assert_eq!(axis.home_offset, 5000);
2046
2047 view.status_word = 0x0427;
2049 axis.tick(&mut view, &mut client);
2050
2051 assert!((axis.position - 0.0).abs() < 0.01);
2053 }
2054}