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 let user_vel = self.config.to_user(vel as f64);
443 self.speed = user_vel.abs();
444 self.moving_positive = user_vel > 0.0;
445 self.moving_negative = user_vel < 0.0;
446 self.is_busy = self.op != AxisOp::Idle;
447 self.in_motion = matches!(self.op, AxisOp::Moving(_, _) | AxisOp::SoftHoming(_));
448
449 let sw = RawStatusWord(view.status_word());
450 self.motor_on = sw.state() == Cia402State::OperationEnabled;
451
452 self.last_raw_position = raw;
453 }
454
455 fn check_faults(&mut self, view: &impl AxisView) {
460 let sw = RawStatusWord(view.status_word());
461 let state = sw.state();
462
463 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
464 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
465 self.is_error = true;
466 let ec = view.error_code();
467 if ec != 0 {
468 self.error_code = ec as u32;
469 }
470 self.error_message = format!("Drive fault (state: {})", state);
471 self.op = AxisOp::Idle;
473 self.op_started = None;
474 }
475 }
476 }
477
478 fn op_timed_out(&self) -> bool {
483 self.op_started
484 .map_or(false, |t| t.elapsed() > self.op_timeout)
485 }
486
487 fn set_op_error(&mut self, msg: &str) {
488 self.is_error = true;
489 self.error_message = msg.to_string();
490 self.op = AxisOp::Idle;
491 self.op_started = None;
492 self.is_busy = false;
493 self.in_motion = false;
494 log::error!("Axis error: {}", msg);
495 }
496
497 fn complete_op(&mut self) {
498 self.op = AxisOp::Idle;
499 self.op_started = None;
500 }
501
502 fn check_target_limit(&self, target: f64) -> Option<String> {
509 if self.config.enable_max_position_limit && target > self.config.max_position_limit {
510 Some(format!(
511 "Target {:.3} exceeds max software limit {:.3}",
512 target, self.config.max_position_limit
513 ))
514 } else if self.config.enable_min_position_limit && target < self.config.min_position_limit {
515 Some(format!(
516 "Target {:.3} exceeds min software limit {:.3}",
517 target, self.config.min_position_limit
518 ))
519 } else {
520 None
521 }
522 }
523
524 fn check_limits(&mut self, view: &mut impl AxisView) {
533 let sw_max = self.config.enable_max_position_limit
535 && self.position >= self.config.max_position_limit;
536 let sw_min = self.config.enable_min_position_limit
537 && self.position <= self.config.min_position_limit;
538
539 self.at_max_limit = sw_max;
540 self.at_min_limit = sw_min;
541
542 let hw_pos = view.positive_limit_active();
544 let hw_neg = view.negative_limit_active();
545
546 self.at_positive_limit_switch = hw_pos;
547 self.at_negative_limit_switch = hw_neg;
548
549 self.home_sensor = view.home_sensor_active();
551
552 self.prev_positive_limit = hw_pos;
554 self.prev_negative_limit = hw_neg;
555 self.prev_home_sensor = view.home_sensor_active();
556
557 let is_moving = matches!(self.op, AxisOp::Moving(_, _));
559 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
560
561 if !is_moving && !is_soft_homing {
562 return;
563 }
564
565 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
567 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
568
569 let effective_hw_pos = hw_pos && !suppress_pos;
570 let effective_hw_neg = hw_neg && !suppress_neg;
571
572 let effective_sw_max = sw_max && !is_soft_homing;
574 let effective_sw_min = sw_min && !is_soft_homing;
575
576 let positive_blocked = (effective_sw_max || effective_hw_pos) && self.moving_positive;
577 let negative_blocked = (effective_sw_min || effective_hw_neg) && self.moving_negative;
578
579 if positive_blocked || negative_blocked {
580 let mut cw = RawControlWord(view.control_word());
581 cw.set_bit(8, true); view.set_control_word(cw.raw());
583
584 let msg = if effective_hw_pos && self.moving_positive {
585 "Positive limit switch active".to_string()
586 } else if effective_hw_neg && self.moving_negative {
587 "Negative limit switch active".to_string()
588 } else if effective_sw_max && self.moving_positive {
589 format!(
590 "Software position limit: position {:.3} >= max {:.3}",
591 self.position, self.config.max_position_limit
592 )
593 } else {
594 format!(
595 "Software position limit: position {:.3} <= min {:.3}",
596 self.position, self.config.min_position_limit
597 )
598 };
599 self.set_op_error(&msg);
600 }
601 }
602
603 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
608 match self.op.clone() {
609 AxisOp::Idle => {}
610 AxisOp::Enabling(step) => self.tick_enabling(view, step),
611 AxisOp::Disabling(step) => self.tick_disabling(view, step),
612 AxisOp::Moving(kind, step) => self.tick_moving(view, kind, step),
613 AxisOp::Homing(step) => self.tick_homing(view, client, step),
614 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, step),
615 AxisOp::Halting => self.tick_halting(view),
616 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
617 }
618 }
619
620 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
625 match step {
626 1 => {
627 let sw = RawStatusWord(view.status_word());
628 if sw.state() == Cia402State::ReadyToSwitchOn {
629 let mut cw = RawControlWord(view.control_word());
630 cw.cmd_enable_operation();
631 view.set_control_word(cw.raw());
632 self.op = AxisOp::Enabling(2);
633 } else if self.op_timed_out() {
634 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
635 }
636 }
637 2 => {
638 let sw = RawStatusWord(view.status_word());
639 if sw.state() == Cia402State::OperationEnabled {
640 self.home_offset = view.position_actual();
641 log::info!("Axis enabled — home captured at {}", self.home_offset);
642 self.complete_op();
643 } else if self.op_timed_out() {
644 self.set_op_error("Enable timeout: waiting for OperationEnabled");
645 }
646 }
647 _ => self.complete_op(),
648 }
649 }
650
651 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
655 match step {
656 1 => {
657 let sw = RawStatusWord(view.status_word());
658 if sw.state() != Cia402State::OperationEnabled {
659 self.complete_op();
660 } else if self.op_timed_out() {
661 self.set_op_error("Disable timeout: drive still in OperationEnabled");
662 }
663 }
664 _ => self.complete_op(),
665 }
666 }
667
668 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8) {
674 match step {
675 1 => {
676 let sw = RawStatusWord(view.status_word());
678 if sw.raw() & (1 << 12) != 0 {
679 let mut cw = RawControlWord(view.control_word());
681 cw.set_bit(4, false);
682 view.set_control_word(cw.raw());
683 self.op = AxisOp::Moving(kind, 2);
684 } else if self.op_timed_out() {
685 self.set_op_error("Move timeout: set-point not acknowledged");
686 }
687 }
688 2 => {
689 let sw = RawStatusWord(view.status_word());
691 if sw.target_reached() {
692 self.complete_op();
693 } else if self.op_timed_out() {
694 self.set_op_error("Move timeout: target not reached");
695 }
696 }
697 _ => self.complete_op(),
698 }
699 }
700
701 fn tick_homing(
719 &mut self,
720 view: &mut impl AxisView,
721 client: &mut CommandClient,
722 step: u8,
723 ) {
724 match step {
725 0 => {
726 self.homing_sdo_tid = self.sdo.write(
728 client,
729 0x6098,
730 0,
731 json!(self.homing_method),
732 );
733 self.op = AxisOp::Homing(1);
734 }
735 1 => {
736 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
738 SdoResult::Ok(_) => {
739 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
741 self.op = AxisOp::Homing(8);
742 } else {
743 self.op = AxisOp::Homing(2);
744 }
745 }
746 SdoResult::Pending => {
747 if self.op_timed_out() {
748 self.set_op_error("Homing timeout: SDO write for homing method");
749 }
750 }
751 SdoResult::Err(e) => {
752 self.set_op_error(&format!("Homing SDO error: {}", e));
753 }
754 SdoResult::Timeout => {
755 self.set_op_error("Homing timeout: SDO write timed out");
756 }
757 }
758 }
759 2 => {
760 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
762 self.homing_sdo_tid = self.sdo.write(
763 client,
764 0x6099,
765 1,
766 json!(speed_counts),
767 );
768 self.op = AxisOp::Homing(3);
769 }
770 3 => {
771 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
772 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
773 SdoResult::Pending => {
774 if self.op_timed_out() {
775 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
776 }
777 }
778 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
779 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
780 }
781 }
782 4 => {
783 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
785 self.homing_sdo_tid = self.sdo.write(
786 client,
787 0x6099,
788 2,
789 json!(speed_counts),
790 );
791 self.op = AxisOp::Homing(5);
792 }
793 5 => {
794 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
795 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
796 SdoResult::Pending => {
797 if self.op_timed_out() {
798 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
799 }
800 }
801 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
802 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
803 }
804 }
805 6 => {
806 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
808 self.homing_sdo_tid = self.sdo.write(
809 client,
810 0x609A,
811 0,
812 json!(accel_counts),
813 );
814 self.op = AxisOp::Homing(7);
815 }
816 7 => {
817 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
818 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
819 SdoResult::Pending => {
820 if self.op_timed_out() {
821 self.set_op_error("Homing timeout: SDO write for homing acceleration");
822 }
823 }
824 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
825 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
826 }
827 }
828 8 => {
829 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
831 self.op = AxisOp::Homing(9);
832 }
833 9 => {
834 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
836 self.op = AxisOp::Homing(10);
837 } else if self.op_timed_out() {
838 self.set_op_error("Homing timeout: mode not confirmed");
839 }
840 }
841 10 => {
842 let mut cw = RawControlWord(view.control_word());
844 cw.set_bit(4, true);
845 view.set_control_word(cw.raw());
846 self.op = AxisOp::Homing(11);
847 }
848 11 => {
849 let sw = view.status_word();
852 let error = sw & (1 << 13) != 0;
853 let attained = sw & (1 << 12) != 0;
854 let reached = sw & (1 << 10) != 0;
855
856 if error {
857 self.set_op_error("Homing error: drive reported homing failure");
858 } else if attained && reached {
859 self.op = AxisOp::Homing(12);
861 } else if self.op_timed_out() {
862 self.set_op_error("Homing timeout: procedure did not complete");
863 }
864 }
865 12 => {
866 self.home_offset = view.position_actual()
869 - self.config.to_counts(self.config.home_position).round() as i32;
870 let mut cw = RawControlWord(view.control_word());
872 cw.set_bit(4, false);
873 view.set_control_word(cw.raw());
874 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
876 log::info!("Homing complete — home offset: {}", self.home_offset);
877 self.complete_op();
878 }
879 _ => self.complete_op(),
880 }
881 }
882
883 fn configure_soft_homing(&mut self, method: HomingMethod) {
886 match method {
887 HomingMethod::LimitSwitchPosRt => {
888 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
889 self.soft_home_edge = SoftHomeEdge::Rising;
890 self.soft_home_direction = 1.0;
891 }
892 HomingMethod::LimitSwitchNegRt => {
893 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
894 self.soft_home_edge = SoftHomeEdge::Rising;
895 self.soft_home_direction = -1.0;
896 }
897 HomingMethod::LimitSwitchPosFt => {
898 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
899 self.soft_home_edge = SoftHomeEdge::Falling;
900 self.soft_home_direction = 1.0;
901 }
902 HomingMethod::LimitSwitchNegFt => {
903 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
904 self.soft_home_edge = SoftHomeEdge::Falling;
905 self.soft_home_direction = -1.0;
906 }
907 HomingMethod::HomeSensorPosRt => {
908 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
909 self.soft_home_edge = SoftHomeEdge::Rising;
910 self.soft_home_direction = 1.0;
911 }
912 HomingMethod::HomeSensorNegRt => {
913 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
914 self.soft_home_edge = SoftHomeEdge::Rising;
915 self.soft_home_direction = -1.0;
916 }
917 HomingMethod::HomeSensorPosFt => {
918 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
919 self.soft_home_edge = SoftHomeEdge::Falling;
920 self.soft_home_direction = 1.0;
921 }
922 HomingMethod::HomeSensorNegFt => {
923 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
924 self.soft_home_edge = SoftHomeEdge::Falling;
925 self.soft_home_direction = -1.0;
926 }
927 _ => {} }
929 }
930
931 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
932 let already_active = match (self.soft_home_sensor, self.soft_home_edge) {
934 (SoftHomeSensor::PositiveLimit, SoftHomeEdge::Rising) => view.positive_limit_active(),
935 (SoftHomeSensor::NegativeLimit, SoftHomeEdge::Rising) => view.negative_limit_active(),
936 (SoftHomeSensor::HomeSensor, SoftHomeEdge::Rising) => view.home_sensor_active(),
937 (SoftHomeSensor::PositiveLimit, SoftHomeEdge::Falling) => !view.positive_limit_active(),
938 (SoftHomeSensor::NegativeLimit, SoftHomeEdge::Falling) => !view.negative_limit_active(),
939 (SoftHomeSensor::HomeSensor, SoftHomeEdge::Falling) => !view.home_sensor_active(),
940 };
941
942 if already_active {
943 self.set_op_error("Software homing: sensor already in trigger state");
944 return;
945 }
946
947 self.op = AxisOp::SoftHoming(0);
948 self.op_started = Some(Instant::now());
949 }
950
951 fn check_soft_home_edge(&self, view: &impl AxisView) -> bool {
952 let (current, prev) = match self.soft_home_sensor {
953 SoftHomeSensor::PositiveLimit => (view.positive_limit_active(), self.prev_positive_limit),
954 SoftHomeSensor::NegativeLimit => (view.negative_limit_active(), self.prev_negative_limit),
955 SoftHomeSensor::HomeSensor => (view.home_sensor_active(), self.prev_home_sensor),
956 };
957 match self.soft_home_edge {
958 SoftHomeEdge::Rising => !prev && current, SoftHomeEdge::Falling => prev && !current, }
961 }
962
963 fn tick_soft_homing(&mut self, view: &mut impl AxisView, step: u8) {
973 match step {
974 0 => {
975 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
977
978 let target = self.config.to_counts(self.soft_home_direction * 999_999.0).round() as i32 + self.home_offset;
981 view.set_target_position(target);
982
983 let cpu = self.config.counts_per_user();
985 let vel = (self.config.homing_speed * cpu).round() as u32;
986 let accel = (self.config.homing_accel * cpu).round() as u32;
987 let decel = (self.config.homing_decel * cpu).round() as u32;
988 view.set_profile_velocity(vel);
989 view.set_profile_acceleration(accel);
990 view.set_profile_deceleration(decel);
991
992 let mut cw = RawControlWord(view.control_word());
994 cw.set_bit(4, true);
995 cw.set_bit(6, false); cw.set_bit(8, false); view.set_control_word(cw.raw());
998
999 self.op = AxisOp::SoftHoming(1);
1000 }
1001 1 => {
1002 if self.check_soft_home_edge(view) {
1004 self.op = AxisOp::SoftHoming(4);
1005 return;
1006 }
1007 let sw = RawStatusWord(view.status_word());
1008 if sw.raw() & (1 << 12) != 0 {
1009 let mut cw = RawControlWord(view.control_word());
1011 cw.set_bit(4, false);
1012 view.set_control_word(cw.raw());
1013 self.op = AxisOp::SoftHoming(2);
1014 } else if self.op_timed_out() {
1015 self.set_op_error("Software homing timeout: set-point not acknowledged");
1016 }
1017 }
1018 2 => {
1019 if self.check_soft_home_edge(view) {
1021 self.op = AxisOp::SoftHoming(4);
1022 return;
1023 }
1024 self.op = AxisOp::SoftHoming(3);
1025 }
1026 3 => {
1027 if self.check_soft_home_edge(view) {
1029 self.op = AxisOp::SoftHoming(4);
1030 } else if self.op_timed_out() {
1031 self.set_op_error("Software homing timeout: sensor edge not detected");
1032 }
1033 }
1034 4 => {
1035 let mut cw = RawControlWord(view.control_word());
1038 cw.set_bit(8, true); view.set_control_word(cw.raw());
1040 self.home_offset = view.position_actual()
1041 - self.config.to_counts(self.config.home_position).round() as i32;
1042 log::info!("Software homing: edge detected, home offset: {}", self.home_offset);
1043 self.op = AxisOp::SoftHoming(5);
1044 }
1045 5 => {
1046 let sw = RawStatusWord(view.status_word());
1048 if sw.target_reached() {
1049 let mut cw = RawControlWord(view.control_word());
1051 cw.set_bit(8, false);
1052 view.set_control_word(cw.raw());
1053 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1054 log::info!("Software homing complete — home offset: {}", self.home_offset);
1055 self.complete_op();
1056 } else if self.op_timed_out() {
1057 self.set_op_error("Software homing timeout: halt not acknowledged");
1058 }
1059 }
1060 _ => self.complete_op(),
1061 }
1062 }
1063
1064 fn tick_halting(&mut self, _view: &mut impl AxisView) {
1066 self.complete_op();
1069 }
1070
1071 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1076 match step {
1077 1 => {
1078 let mut cw = RawControlWord(view.control_word());
1080 cw.cmd_fault_reset();
1081 view.set_control_word(cw.raw());
1082 self.op = AxisOp::FaultRecovery(2);
1083 }
1084 2 => {
1085 let sw = RawStatusWord(view.status_word());
1087 let state = sw.state();
1088 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1089 log::info!("Fault cleared (drive state: {})", state);
1090 self.complete_op();
1091 } else if self.op_timed_out() {
1092 self.set_op_error("Fault reset timeout: drive still faulted");
1093 }
1094 }
1095 _ => self.complete_op(),
1096 }
1097 }
1098}
1099
1100#[cfg(test)]
1105mod tests {
1106 use super::*;
1107
1108 struct MockView {
1110 control_word: u16,
1111 status_word: u16,
1112 target_position: i32,
1113 profile_velocity: u32,
1114 profile_acceleration: u32,
1115 profile_deceleration: u32,
1116 modes_of_operation: i8,
1117 modes_of_operation_display: i8,
1118 position_actual: i32,
1119 velocity_actual: i32,
1120 error_code: u16,
1121 positive_limit: bool,
1122 negative_limit: bool,
1123 home_sensor: bool,
1124 }
1125
1126 impl MockView {
1127 fn new() -> Self {
1128 Self {
1129 control_word: 0,
1130 status_word: 0x0040, target_position: 0,
1132 profile_velocity: 0,
1133 profile_acceleration: 0,
1134 profile_deceleration: 0,
1135 modes_of_operation: 0,
1136 modes_of_operation_display: 1, position_actual: 0,
1138 velocity_actual: 0,
1139 error_code: 0,
1140 positive_limit: false,
1141 negative_limit: false,
1142 home_sensor: false,
1143 }
1144 }
1145
1146 fn set_state(&mut self, state: u16) {
1147 self.status_word = state;
1148 }
1149 }
1150
1151 impl AxisView for MockView {
1152 fn control_word(&self) -> u16 { self.control_word }
1153 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1154 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1155 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1156 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1157 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1158 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1159 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1160 fn status_word(&self) -> u16 { self.status_word }
1161 fn position_actual(&self) -> i32 { self.position_actual }
1162 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1163 fn error_code(&self) -> u16 { self.error_code }
1164 fn positive_limit_active(&self) -> bool { self.positive_limit }
1165 fn negative_limit_active(&self) -> bool { self.negative_limit }
1166 fn home_sensor_active(&self) -> bool { self.home_sensor }
1167 }
1168
1169 fn test_config() -> AxisConfig {
1170 AxisConfig::new(12_800).with_user_scale(360.0)
1171 }
1172
1173 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1175 use tokio::sync::mpsc;
1176 let (write_tx, write_rx) = mpsc::unbounded_channel();
1177 let (response_tx, response_rx) = mpsc::unbounded_channel();
1178 let client = CommandClient::new(write_tx, response_rx);
1179 let axis = Axis::new(test_config(), "TestDrive");
1180 (axis, client, response_tx, write_rx)
1181 }
1182
1183 #[test]
1184 fn axis_config_conversion() {
1185 let cfg = test_config();
1186 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
1188 }
1189
1190 #[test]
1191 fn enable_sequence_sets_pp_mode_and_shutdown() {
1192 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1193 let mut view = MockView::new();
1194
1195 axis.enable(&mut view);
1196
1197 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
1199 assert_eq!(view.control_word & 0x008F, 0x0006);
1201 assert_eq!(axis.op, AxisOp::Enabling(1));
1203
1204 view.set_state(0x0021); axis.tick(&mut view, &mut client);
1207
1208 assert_eq!(view.control_word & 0x008F, 0x000F);
1210 assert_eq!(axis.op, AxisOp::Enabling(2));
1211
1212 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1215
1216 assert_eq!(axis.op, AxisOp::Idle);
1218 assert!(axis.motor_on);
1219 }
1220
1221 #[test]
1222 fn move_absolute_sets_target() {
1223 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1224 let mut view = MockView::new();
1225 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1230
1231 assert_eq!(view.target_position, 1600);
1233 assert_eq!(view.profile_velocity, 3200);
1235 assert_eq!(view.profile_acceleration, 6400);
1237 assert_eq!(view.profile_deceleration, 6400);
1238 assert!(view.control_word & (1 << 4) != 0);
1240 assert!(view.control_word & (1 << 6) == 0);
1242 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1244 }
1245
1246 #[test]
1247 fn move_relative_sets_relative_bit() {
1248 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1249 let mut view = MockView::new();
1250 view.set_state(0x0027);
1251 axis.tick(&mut view, &mut client);
1252
1253 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
1254
1255 assert!(view.control_word & (1 << 6) != 0);
1257 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1)));
1258 }
1259
1260 #[test]
1261 fn move_completes_on_target_reached() {
1262 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1263 let mut view = MockView::new();
1264 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1266
1267 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1268
1269 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1272 assert!(view.control_word & (1 << 4) == 0);
1274
1275 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1278 assert_eq!(axis.op, AxisOp::Idle);
1280 assert!(!axis.in_motion);
1281 }
1282
1283 #[test]
1284 fn fault_detected_sets_error() {
1285 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1286 let mut view = MockView::new();
1287 view.set_state(0x0008); view.error_code = 0x1234;
1289
1290 axis.tick(&mut view, &mut client);
1291
1292 assert!(axis.is_error);
1293 assert_eq!(axis.error_code, 0x1234);
1294 assert!(axis.error_message.contains("fault"));
1295 }
1296
1297 #[test]
1298 fn fault_recovery_sequence() {
1299 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1300 let mut view = MockView::new();
1301 view.set_state(0x0008); axis.reset_faults(&mut view);
1304 assert!(view.control_word & 0x0080 == 0);
1306
1307 axis.tick(&mut view, &mut client);
1309 assert!(view.control_word & 0x0080 != 0);
1310
1311 view.set_state(0x0040);
1313 axis.tick(&mut view, &mut client);
1314 assert_eq!(axis.op, AxisOp::Idle);
1315 assert!(!axis.is_error);
1316 }
1317
1318 #[test]
1319 fn disable_sequence() {
1320 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1321 let mut view = MockView::new();
1322 view.set_state(0x0027); axis.disable(&mut view);
1325 assert_eq!(view.control_word & 0x008F, 0x0007);
1327
1328 view.set_state(0x0023); axis.tick(&mut view, &mut client);
1331 assert_eq!(axis.op, AxisOp::Idle);
1332 }
1333
1334 #[test]
1335 fn position_tracks_with_home_offset() {
1336 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1337 let mut view = MockView::new();
1338 view.set_state(0x0027);
1339 view.position_actual = 5000;
1340
1341 axis.enable(&mut view);
1343 view.set_state(0x0021);
1344 axis.tick(&mut view, &mut client);
1345 view.set_state(0x0027);
1346 axis.tick(&mut view, &mut client);
1347
1348 assert_eq!(axis.home_offset, 5000);
1350
1351 assert!((axis.position - 0.0).abs() < 0.01);
1353
1354 view.position_actual = 6600;
1356 axis.tick(&mut view, &mut client);
1357
1358 assert!((axis.position - 45.0).abs() < 0.1);
1360 }
1361
1362 #[test]
1363 fn set_position_adjusts_home_offset() {
1364 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1365 let mut view = MockView::new();
1366 view.position_actual = 3200;
1367
1368 axis.set_position(&view, 90.0);
1369 axis.tick(&mut view, &mut client);
1370
1371 assert_eq!(axis.home_offset, 0);
1373 assert!((axis.position - 90.0).abs() < 0.01);
1374 }
1375
1376 #[test]
1377 fn halt_sets_bit_and_goes_idle() {
1378 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1379 let mut view = MockView::new();
1380 view.set_state(0x0027);
1381
1382 axis.halt(&mut view);
1383 assert!(view.control_word & (1 << 8) != 0);
1385
1386 axis.tick(&mut view, &mut client);
1388 assert_eq!(axis.op, AxisOp::Idle);
1389 }
1390
1391 #[test]
1392 fn is_busy_tracks_operations() {
1393 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1394 let mut view = MockView::new();
1395
1396 axis.tick(&mut view, &mut client);
1398 assert!(!axis.is_busy);
1399
1400 axis.enable(&mut view);
1402 axis.tick(&mut view, &mut client);
1403 assert!(axis.is_busy);
1404
1405 view.set_state(0x0021);
1407 axis.tick(&mut view, &mut client);
1408 view.set_state(0x0027);
1409 axis.tick(&mut view, &mut client);
1410 assert!(!axis.is_busy);
1411
1412 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1414 axis.tick(&mut view, &mut client);
1415 assert!(axis.is_busy);
1416 assert!(axis.in_motion);
1417 }
1418
1419 #[test]
1420 fn fault_during_move_cancels_op() {
1421 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1422 let mut view = MockView::new();
1423 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1425
1426 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1428 axis.tick(&mut view, &mut client);
1429 assert!(axis.is_busy);
1430 assert!(!axis.is_error);
1431
1432 view.set_state(0x0008); axis.tick(&mut view, &mut client);
1435
1436 assert!(!axis.is_busy);
1438 assert!(axis.is_error);
1439 assert_eq!(axis.op, AxisOp::Idle);
1440 }
1441
1442 #[test]
1443 fn move_absolute_rejected_by_max_limit() {
1444 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1445 let mut view = MockView::new();
1446 view.set_state(0x0027);
1447 axis.tick(&mut view, &mut client);
1448
1449 axis.set_software_max_limit(90.0);
1450 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
1451
1452 assert!(axis.is_error);
1454 assert_eq!(axis.op, AxisOp::Idle);
1455 assert!(axis.error_message.contains("max software limit"));
1456 }
1457
1458 #[test]
1459 fn move_absolute_rejected_by_min_limit() {
1460 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1461 let mut view = MockView::new();
1462 view.set_state(0x0027);
1463 axis.tick(&mut view, &mut client);
1464
1465 axis.set_software_min_limit(-10.0);
1466 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
1467
1468 assert!(axis.is_error);
1469 assert_eq!(axis.op, AxisOp::Idle);
1470 assert!(axis.error_message.contains("min software limit"));
1471 }
1472
1473 #[test]
1474 fn move_relative_rejected_by_max_limit() {
1475 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1476 let mut view = MockView::new();
1477 view.set_state(0x0027);
1478 axis.tick(&mut view, &mut client);
1479
1480 axis.set_software_max_limit(50.0);
1482 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
1483
1484 assert!(axis.is_error);
1485 assert_eq!(axis.op, AxisOp::Idle);
1486 assert!(axis.error_message.contains("max software limit"));
1487 }
1488
1489 #[test]
1490 fn move_within_limits_allowed() {
1491 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1492 let mut view = MockView::new();
1493 view.set_state(0x0027);
1494 axis.tick(&mut view, &mut client);
1495
1496 axis.set_software_max_limit(90.0);
1497 axis.set_software_min_limit(-90.0);
1498 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1499
1500 assert!(!axis.is_error);
1502 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1503 }
1504
1505 #[test]
1506 fn runtime_limit_halts_move_in_violated_direction() {
1507 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1508 let mut view = MockView::new();
1509 view.set_state(0x0027);
1510 axis.tick(&mut view, &mut client);
1511
1512 axis.set_software_max_limit(45.0);
1513 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1515
1516 view.position_actual = 1650;
1519 view.velocity_actual = 100; view.status_word = 0x1027;
1523 axis.tick(&mut view, &mut client);
1524 view.status_word = 0x0027;
1525 axis.tick(&mut view, &mut client);
1526
1527 assert!(axis.is_error);
1529 assert!(axis.at_max_limit);
1530 assert_eq!(axis.op, AxisOp::Idle);
1531 assert!(axis.error_message.contains("Software position limit"));
1532 assert!(view.control_word & (1 << 8) != 0);
1534 }
1535
1536 #[test]
1537 fn runtime_limit_allows_move_in_opposite_direction() {
1538 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1539 let mut view = MockView::new();
1540 view.set_state(0x0027);
1541 view.position_actual = 1778; axis.set_software_max_limit(45.0);
1544 axis.tick(&mut view, &mut client);
1545 assert!(axis.at_max_limit);
1546
1547 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
1549 assert!(!axis.is_error);
1550 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1551
1552 view.velocity_actual = -100;
1554 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1556 assert!(!axis.is_error);
1558 }
1559
1560 #[test]
1561 fn positive_limit_switch_halts_positive_move() {
1562 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1563 let mut view = MockView::new();
1564 view.set_state(0x0027);
1565 axis.tick(&mut view, &mut client);
1566
1567 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1569 view.velocity_actual = 100; view.status_word = 0x1027;
1572 axis.tick(&mut view, &mut client);
1573 view.status_word = 0x0027;
1574
1575 view.positive_limit = true;
1577 axis.tick(&mut view, &mut client);
1578
1579 assert!(axis.is_error);
1580 assert!(axis.at_positive_limit_switch);
1581 assert!(!axis.is_busy);
1582 assert!(axis.error_message.contains("Positive limit switch"));
1583 assert!(view.control_word & (1 << 8) != 0);
1585 }
1586
1587 #[test]
1588 fn negative_limit_switch_halts_negative_move() {
1589 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1590 let mut view = MockView::new();
1591 view.set_state(0x0027);
1592 axis.tick(&mut view, &mut client);
1593
1594 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
1596 view.velocity_actual = -100; view.status_word = 0x1027;
1598 axis.tick(&mut view, &mut client);
1599 view.status_word = 0x0027;
1600
1601 view.negative_limit = true;
1603 axis.tick(&mut view, &mut client);
1604
1605 assert!(axis.is_error);
1606 assert!(axis.at_negative_limit_switch);
1607 assert!(axis.error_message.contains("Negative limit switch"));
1608 }
1609
1610 #[test]
1611 fn limit_switch_allows_move_in_opposite_direction() {
1612 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1613 let mut view = MockView::new();
1614 view.set_state(0x0027);
1615 view.positive_limit = true;
1617 view.velocity_actual = -100;
1618 axis.tick(&mut view, &mut client);
1619 assert!(axis.at_positive_limit_switch);
1620
1621 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
1623 view.status_word = 0x1027;
1624 axis.tick(&mut view, &mut client);
1625
1626 assert!(!axis.is_error);
1628 assert!(matches!(axis.op, AxisOp::Moving(_, _)));
1629 }
1630
1631 #[test]
1632 fn limit_switch_ignored_when_not_moving() {
1633 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1634 let mut view = MockView::new();
1635 view.set_state(0x0027);
1636 view.positive_limit = true;
1637
1638 axis.tick(&mut view, &mut client);
1639
1640 assert!(axis.at_positive_limit_switch);
1642 assert!(!axis.is_error);
1643 }
1644
1645 #[test]
1646 fn home_sensor_output_tracks_view() {
1647 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1648 let mut view = MockView::new();
1649 view.set_state(0x0027);
1650
1651 axis.tick(&mut view, &mut client);
1652 assert!(!axis.home_sensor);
1653
1654 view.home_sensor = true;
1655 axis.tick(&mut view, &mut client);
1656 assert!(axis.home_sensor);
1657
1658 view.home_sensor = false;
1659 axis.tick(&mut view, &mut client);
1660 assert!(!axis.home_sensor);
1661 }
1662
1663 #[test]
1664 fn velocity_output_converted() {
1665 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1666 let mut view = MockView::new();
1667 view.set_state(0x0027);
1668 view.velocity_actual = 3200;
1670
1671 axis.tick(&mut view, &mut client);
1672
1673 assert!((axis.speed - 90.0).abs() < 0.1);
1674 assert!(axis.moving_positive);
1675 assert!(!axis.moving_negative);
1676 }
1677
1678 fn soft_homing_config() -> AxisConfig {
1681 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
1682 cfg.homing_speed = 10.0;
1683 cfg.homing_accel = 20.0;
1684 cfg.homing_decel = 20.0;
1685 cfg
1686 }
1687
1688 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1689 use tokio::sync::mpsc;
1690 let (write_tx, write_rx) = mpsc::unbounded_channel();
1691 let (response_tx, response_rx) = mpsc::unbounded_channel();
1692 let client = CommandClient::new(write_tx, response_rx);
1693 let axis = Axis::new(soft_homing_config(), "TestDrive");
1694 (axis, client, response_tx, write_rx)
1695 }
1696
1697 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
1699 view.set_state(0x0027); axis.tick(view, client);
1701 }
1702
1703 #[test]
1704 fn soft_homing_rising_edge_home_sensor_triggers_home() {
1705 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1706 let mut view = MockView::new();
1707 enable_axis(&mut axis, &mut view, &mut client);
1708
1709 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1711 assert!(matches!(axis.op, AxisOp::SoftHoming(0)));
1712
1713 axis.tick(&mut view, &mut client);
1715 assert!(matches!(axis.op, AxisOp::SoftHoming(1)));
1716 assert!(view.control_word & (1 << 4) != 0);
1718
1719 view.status_word = 0x1027;
1721 axis.tick(&mut view, &mut client);
1722 assert!(view.control_word & (1 << 4) == 0);
1724 assert!(matches!(axis.op, AxisOp::SoftHoming(2)));
1725
1726 view.status_word = 0x0027;
1728 axis.tick(&mut view, &mut client);
1729 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1730
1731 axis.tick(&mut view, &mut client);
1733 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1734
1735 view.home_sensor = true;
1737 view.position_actual = 5000;
1738 axis.tick(&mut view, &mut client);
1739 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1741
1742 axis.tick(&mut view, &mut client);
1744 assert!(view.control_word & (1 << 8) != 0); assert_eq!(axis.home_offset, 5000);
1746 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
1747
1748 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1751 assert_eq!(axis.op, AxisOp::Idle);
1753 assert!(!axis.is_busy);
1754 assert!(!axis.is_error);
1755 }
1756
1757 #[test]
1758 fn soft_homing_falling_edge_home_sensor_triggers_home() {
1759 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1760 let mut view = MockView::new();
1761 view.home_sensor = true;
1763 enable_axis(&mut axis, &mut view, &mut client);
1764
1765 axis.home(&mut view, HomingMethod::HomeSensorPosFt);
1767 assert!(matches!(axis.op, AxisOp::SoftHoming(0)));
1768
1769 axis.tick(&mut view, &mut client);
1771 view.status_word = 0x1027;
1773 axis.tick(&mut view, &mut client);
1774 view.status_word = 0x0027;
1776 axis.tick(&mut view, &mut client);
1777
1778 axis.tick(&mut view, &mut client);
1780 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1781
1782 view.home_sensor = false;
1784 view.position_actual = 3000;
1785 axis.tick(&mut view, &mut client);
1786 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1787
1788 axis.tick(&mut view, &mut client);
1790 view.status_word = 0x0427;
1791 axis.tick(&mut view, &mut client);
1792 assert_eq!(axis.op, AxisOp::Idle);
1793 assert_eq!(axis.home_offset, 3000);
1794 }
1795
1796 #[test]
1797 fn soft_homing_limit_switch_suppresses_halt() {
1798 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1799 let mut view = MockView::new();
1800 enable_axis(&mut axis, &mut view, &mut client);
1801
1802 axis.home(&mut view, HomingMethod::LimitSwitchPosRt);
1804
1805 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1810 axis.tick(&mut view, &mut client); view.positive_limit = true;
1814 view.velocity_actual = 100; view.position_actual = 8000;
1816 axis.tick(&mut view, &mut client);
1817
1818 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1820 assert!(!axis.is_error);
1821 }
1822
1823 #[test]
1824 fn soft_homing_opposite_limit_still_protects() {
1825 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1826 let mut view = MockView::new();
1827 enable_axis(&mut axis, &mut view, &mut client);
1828
1829 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1831
1832 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1837 axis.tick(&mut view, &mut client); view.negative_limit = true;
1842 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
1844
1845 assert!(axis.is_error);
1847 assert!(axis.error_message.contains("Negative limit switch"));
1848 }
1849
1850 #[test]
1851 fn soft_homing_sensor_already_active_rejects() {
1852 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1853 let mut view = MockView::new();
1854 enable_axis(&mut axis, &mut view, &mut client);
1855
1856 view.home_sensor = true;
1858 axis.tick(&mut view, &mut client); axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1861
1862 assert!(axis.is_error);
1864 assert!(axis.error_message.contains("already in trigger state"));
1865 assert_eq!(axis.op, AxisOp::Idle);
1866 }
1867
1868 #[test]
1869 fn soft_homing_negative_direction_sets_negative_target() {
1870 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1871 let mut view = MockView::new();
1872 enable_axis(&mut axis, &mut view, &mut client);
1873
1874 axis.home(&mut view, HomingMethod::HomeSensorNegRt);
1875 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
1879 }
1880
1881 #[test]
1882 fn home_integrated_method_starts_hardware_homing() {
1883 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1884 let mut view = MockView::new();
1885 enable_axis(&mut axis, &mut view, &mut client);
1886
1887 axis.home(&mut view, HomingMethod::CurrentPosition);
1888 assert!(matches!(axis.op, AxisOp::Homing(0)));
1889 assert_eq!(axis.homing_method, 37);
1890 }
1891
1892 #[test]
1893 fn home_integrated_arbitrary_code() {
1894 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1895 let mut view = MockView::new();
1896 enable_axis(&mut axis, &mut view, &mut client);
1897
1898 axis.home(&mut view, HomingMethod::Integrated(35));
1899 assert!(matches!(axis.op, AxisOp::Homing(0)));
1900 assert_eq!(axis.homing_method, 35);
1901 }
1902
1903 #[test]
1904 fn hardware_homing_skips_speed_sdos_when_zero() {
1905 use mechutil::ipc::CommandMessage;
1906
1907 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
1908 let mut view = MockView::new();
1909 enable_axis(&mut axis, &mut view, &mut client);
1910
1911 axis.home(&mut view, HomingMethod::Integrated(37));
1913
1914 axis.tick(&mut view, &mut client);
1916 assert!(matches!(axis.op, AxisOp::Homing(1)));
1917
1918 let _ = write_rx.try_recv();
1920
1921 let tid = axis.homing_sdo_tid;
1923 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
1924 client.poll();
1925 axis.tick(&mut view, &mut client);
1926
1927 assert!(matches!(axis.op, AxisOp::Homing(8)));
1929 }
1930
1931 #[test]
1932 fn hardware_homing_writes_speed_sdos_when_nonzero() {
1933 use mechutil::ipc::CommandMessage;
1934
1935 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
1936 let mut view = MockView::new();
1937 enable_axis(&mut axis, &mut view, &mut client);
1938
1939 axis.home(&mut view, HomingMethod::Integrated(37));
1941
1942 axis.tick(&mut view, &mut client);
1944 assert!(matches!(axis.op, AxisOp::Homing(1)));
1945 let _ = write_rx.try_recv();
1946
1947 let tid = axis.homing_sdo_tid;
1949 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
1950 client.poll();
1951 axis.tick(&mut view, &mut client);
1952 assert!(matches!(axis.op, AxisOp::Homing(2)));
1954 }
1955
1956 #[test]
1957 fn soft_homing_edge_during_ack_step() {
1958 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1959 let mut view = MockView::new();
1960 enable_axis(&mut axis, &mut view, &mut client);
1961
1962 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1963 axis.tick(&mut view, &mut client); view.home_sensor = true;
1967 view.position_actual = 2000;
1968 axis.tick(&mut view, &mut client);
1969
1970 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1972 }
1973
1974 #[test]
1975 fn soft_homing_applies_home_position() {
1976 let mut cfg = soft_homing_config();
1979 cfg.home_position = 90.0;
1980
1981 use tokio::sync::mpsc;
1982 let (write_tx, write_rx) = mpsc::unbounded_channel();
1983 let (response_tx, response_rx) = mpsc::unbounded_channel();
1984 let mut client = CommandClient::new(write_tx, response_rx);
1985 let mut axis = Axis::new(cfg, "TestDrive");
1986 let _ = (response_tx, write_rx);
1987
1988 let mut view = MockView::new();
1989 enable_axis(&mut axis, &mut view, &mut client);
1990
1991 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1992
1993 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1998 axis.tick(&mut view, &mut client); view.home_sensor = true;
2002 view.position_actual = 5000;
2003 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); assert_eq!(axis.home_offset, 1800);
2011
2012 view.status_word = 0x0427;
2014 axis.tick(&mut view, &mut client);
2015 assert_eq!(axis.op, AxisOp::Idle);
2016
2017 assert!((axis.position - 90.0).abs() < 0.1);
2020 }
2021
2022 #[test]
2023 fn soft_homing_default_home_position_zero() {
2024 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2026 let mut view = MockView::new();
2027 enable_axis(&mut axis, &mut view, &mut client);
2028
2029 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
2030
2031 axis.tick(&mut view, &mut client);
2033 view.status_word = 0x1027;
2034 axis.tick(&mut view, &mut client);
2035 view.status_word = 0x0027;
2036 axis.tick(&mut view, &mut client);
2037
2038 view.home_sensor = true;
2040 view.position_actual = 5000;
2041 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client);
2045
2046 assert_eq!(axis.home_offset, 5000);
2048
2049 view.status_word = 0x0427;
2051 axis.tick(&mut view, &mut client);
2052
2053 assert!((axis.position - 0.0).abs() < 0.01);
2055 }
2056}