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 homing_timeout: Duration,
93 move_start_timeout: Duration,
94 pending_move_target: i32,
95 pending_move_vel: u32,
96 pending_move_accel: u32,
97 pending_move_decel: u32,
98 homing_method: i8,
99 homing_sdo_tid: u32,
100 soft_home_sensor: SoftHomeSensor,
101 soft_home_edge: SoftHomeEdge,
102 soft_home_direction: f64,
103 prev_positive_limit: bool,
104 prev_negative_limit: bool,
105 prev_home_sensor: bool,
106
107 pub is_error: bool,
111 pub error_code: u32,
113 pub error_message: String,
115 pub motor_on: bool,
117 pub is_busy: bool,
123 pub in_motion: bool,
125 pub moving_positive: bool,
127 pub moving_negative: bool,
129 pub position: f64,
131 pub raw_position: i64,
133 pub speed: f64,
135 pub at_max_limit: bool,
137 pub at_min_limit: bool,
139 pub at_positive_limit_switch: bool,
141 pub at_negative_limit_switch: bool,
143 pub home_sensor: bool,
145}
146
147impl Axis {
148 pub fn new(config: AxisConfig, device_name: &str) -> Self {
153 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
154 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
155 let move_start_timeout = op_timeout; Self {
157 config,
158 sdo: SdoClient::new(device_name),
159 op: AxisOp::Idle,
160 home_offset: 0,
161 last_raw_position: 0,
162 op_started: None,
163 op_timeout,
164 homing_timeout,
165 move_start_timeout,
166 pending_move_target: 0,
167 pending_move_vel: 0,
168 pending_move_accel: 0,
169 pending_move_decel: 0,
170 homing_method: 37,
171 homing_sdo_tid: 0,
172 soft_home_sensor: SoftHomeSensor::HomeSensor,
173 soft_home_edge: SoftHomeEdge::Rising,
174 soft_home_direction: 1.0,
175 prev_positive_limit: false,
176 prev_negative_limit: false,
177 prev_home_sensor: false,
178 is_error: false,
179 error_code: 0,
180 error_message: String::new(),
181 motor_on: false,
182 is_busy: false,
183 in_motion: false,
184 moving_positive: false,
185 moving_negative: false,
186 position: 0.0,
187 raw_position: 0,
188 speed: 0.0,
189 at_max_limit: false,
190 at_min_limit: false,
191 at_positive_limit_switch: false,
192 at_negative_limit_switch: false,
193 home_sensor: false,
194 }
195 }
196
197 pub fn config(&self) -> &AxisConfig {
199 &self.config
200 }
201
202 pub fn move_absolute(
212 &mut self,
213 view: &mut impl AxisView,
214 target: f64,
215 vel: f64,
216 accel: f64,
217 decel: f64,
218 ) {
219 if let Some(msg) = self.check_target_limit(target) {
220 self.set_op_error(&msg);
221 return;
222 }
223
224 let cpu = self.config.counts_per_user();
225 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
226 let raw_vel = (vel * cpu).round() as u32;
227 let raw_accel = (accel * cpu).round() as u32;
228 let raw_decel = (decel * cpu).round() as u32;
229
230 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute);
231 }
232
233 pub fn move_relative(
239 &mut self,
240 view: &mut impl AxisView,
241 distance: f64,
242 vel: f64,
243 accel: f64,
244 decel: f64,
245 ) {
246 if let Some(msg) = self.check_target_limit(self.position + distance) {
247 self.set_op_error(&msg);
248 return;
249 }
250
251 let cpu = self.config.counts_per_user();
252 let raw_distance = self.config.to_counts(distance).round() as i32;
253 let raw_vel = (vel * cpu).round() as u32;
254 let raw_accel = (accel * cpu).round() as u32;
255 let raw_decel = (decel * cpu).round() as u32;
256
257 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative);
258 }
259
260 fn start_move(
261 &mut self,
262 view: &mut impl AxisView,
263 raw_target: i32,
264 raw_vel: u32,
265 raw_accel: u32,
266 raw_decel: u32,
267 kind: MoveKind,
268 ) {
269 self.pending_move_target = raw_target;
270 self.pending_move_vel = raw_vel;
271 self.pending_move_accel = raw_accel;
272 self.pending_move_decel = raw_decel;
273
274 view.set_target_position(raw_target);
276 view.set_profile_velocity(raw_vel);
277 view.set_profile_acceleration(raw_accel);
278 view.set_profile_deceleration(raw_decel);
279
280 let mut cw = RawControlWord(view.control_word());
282 cw.set_bit(6, kind == MoveKind::Relative);
283 cw.set_bit(4, true); view.set_control_word(cw.raw());
285
286 self.op = AxisOp::Moving(kind, 1);
287 self.op_started = Some(Instant::now());
288 }
289
290 pub fn halt(&mut self, view: &mut impl AxisView) {
292 let mut cw = RawControlWord(view.control_word());
293 cw.set_bit(8, true); view.set_control_word(cw.raw());
295 self.op = AxisOp::Halting;
296 }
297
298 pub fn enable(&mut self, view: &mut impl AxisView) {
306 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
308 let mut cw = RawControlWord(view.control_word());
309 cw.cmd_shutdown();
310 view.set_control_word(cw.raw());
311
312 self.op = AxisOp::Enabling(1);
313 self.op_started = Some(Instant::now());
314 }
315
316 pub fn disable(&mut self, view: &mut impl AxisView) {
318 let mut cw = RawControlWord(view.control_word());
319 cw.cmd_disable_operation();
320 view.set_control_word(cw.raw());
321
322 self.op = AxisOp::Disabling(1);
323 self.op_started = Some(Instant::now());
324 }
325
326 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
330 let mut cw = RawControlWord(view.control_word());
332 cw.cmd_clear_fault_reset();
333 view.set_control_word(cw.raw());
334
335 self.is_error = false;
336 self.error_code = 0;
337 self.error_message.clear();
338 self.op = AxisOp::FaultRecovery(1);
339 self.op_started = Some(Instant::now());
340 }
341
342 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
350 if method.is_integrated() {
351 self.homing_method = method.cia402_code();
352 self.op = AxisOp::Homing(0);
353 self.op_started = Some(Instant::now());
354 let _ = view;
355 } else {
356 self.configure_soft_homing(method);
357 self.start_soft_homing(view);
358 }
359 }
360
361 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
370 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
371 }
372
373 pub fn set_software_max_limit(&mut self, user_units: f64) {
375 self.config.max_position_limit = user_units;
376 self.config.enable_max_position_limit = true;
377 }
378
379 pub fn set_software_min_limit(&mut self, user_units: f64) {
381 self.config.min_position_limit = user_units;
382 self.config.enable_min_position_limit = true;
383 }
384
385 pub fn sdo_write(
391 &mut self,
392 client: &mut CommandClient,
393 index: u16,
394 sub_index: u8,
395 value: serde_json::Value,
396 ) {
397 self.sdo.write(client, index, sub_index, value);
398 }
399
400 pub fn sdo_read(
402 &mut self,
403 client: &mut CommandClient,
404 index: u16,
405 sub_index: u8,
406 ) -> u32 {
407 self.sdo.read(client, index, sub_index)
408 }
409
410 pub fn sdo_result(
412 &mut self,
413 client: &mut CommandClient,
414 tid: u32,
415 ) -> SdoResult {
416 self.sdo.result(client, tid, Duration::from_secs(5))
417 }
418
419 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
433 self.check_faults(view);
434 self.progress_op(view, client);
435 self.update_outputs(view);
436 self.check_limits(view);
437 }
438
439 fn update_outputs(&mut self, view: &impl AxisView) {
444 let raw = view.position_actual();
445 self.raw_position = raw as i64;
446 self.position = self.config.to_user((raw - self.home_offset) as f64);
447
448 let vel = view.velocity_actual();
449 let user_vel = self.config.to_user(vel as f64);
450 self.speed = user_vel.abs();
451 self.moving_positive = user_vel > 0.0;
452 self.moving_negative = user_vel < 0.0;
453 self.is_busy = self.op != AxisOp::Idle;
454 self.in_motion = matches!(self.op, AxisOp::Moving(_, _) | AxisOp::SoftHoming(_));
455
456 let sw = RawStatusWord(view.status_word());
457 self.motor_on = sw.state() == Cia402State::OperationEnabled;
458
459 self.last_raw_position = raw;
460 }
461
462 fn check_faults(&mut self, view: &impl AxisView) {
467 let sw = RawStatusWord(view.status_word());
468 let state = sw.state();
469
470 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
471 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
472 self.is_error = true;
473 let ec = view.error_code();
474 if ec != 0 {
475 self.error_code = ec as u32;
476 }
477 self.error_message = format!("Drive fault (state: {})", state);
478 self.op = AxisOp::Idle;
480 self.op_started = None;
481 }
482 }
483 }
484
485 fn op_timed_out(&self) -> bool {
490 self.op_started
491 .map_or(false, |t| t.elapsed() > self.op_timeout)
492 }
493
494 fn homing_timed_out(&self) -> bool {
495 self.op_started
496 .map_or(false, |t| t.elapsed() > self.homing_timeout)
497 }
498
499 fn move_start_timed_out(&self) -> bool {
500 self.op_started
501 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
502 }
503
504 fn set_op_error(&mut self, msg: &str) {
505 self.is_error = true;
506 self.error_message = msg.to_string();
507 self.op = AxisOp::Idle;
508 self.op_started = None;
509 self.is_busy = false;
510 self.in_motion = false;
511 log::error!("Axis error: {}", msg);
512 }
513
514 fn complete_op(&mut self) {
515 self.op = AxisOp::Idle;
516 self.op_started = None;
517 }
518
519 fn check_target_limit(&self, target: f64) -> Option<String> {
526 if self.config.enable_max_position_limit && target > self.config.max_position_limit {
527 Some(format!(
528 "Target {:.3} exceeds max software limit {:.3}",
529 target, self.config.max_position_limit
530 ))
531 } else if self.config.enable_min_position_limit && target < self.config.min_position_limit {
532 Some(format!(
533 "Target {:.3} exceeds min software limit {:.3}",
534 target, self.config.min_position_limit
535 ))
536 } else {
537 None
538 }
539 }
540
541 fn check_limits(&mut self, view: &mut impl AxisView) {
550 let sw_max = self.config.enable_max_position_limit
552 && self.position >= self.config.max_position_limit;
553 let sw_min = self.config.enable_min_position_limit
554 && self.position <= self.config.min_position_limit;
555
556 self.at_max_limit = sw_max;
557 self.at_min_limit = sw_min;
558
559 let hw_pos = view.positive_limit_active();
561 let hw_neg = view.negative_limit_active();
562
563 self.at_positive_limit_switch = hw_pos;
564 self.at_negative_limit_switch = hw_neg;
565
566 self.home_sensor = view.home_sensor_active();
568
569 self.prev_positive_limit = hw_pos;
571 self.prev_negative_limit = hw_neg;
572 self.prev_home_sensor = view.home_sensor_active();
573
574 let is_moving = matches!(self.op, AxisOp::Moving(_, _));
576 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
577
578 if !is_moving && !is_soft_homing {
579 return;
580 }
581
582 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
584 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
585
586 let effective_hw_pos = hw_pos && !suppress_pos;
587 let effective_hw_neg = hw_neg && !suppress_neg;
588
589 let effective_sw_max = sw_max && !is_soft_homing;
591 let effective_sw_min = sw_min && !is_soft_homing;
592
593 let positive_blocked = (effective_sw_max || effective_hw_pos) && self.moving_positive;
594 let negative_blocked = (effective_sw_min || effective_hw_neg) && self.moving_negative;
595
596 if positive_blocked || negative_blocked {
597 let mut cw = RawControlWord(view.control_word());
598 cw.set_bit(8, true); view.set_control_word(cw.raw());
600
601 let msg = if effective_hw_pos && self.moving_positive {
602 "Positive limit switch active".to_string()
603 } else if effective_hw_neg && self.moving_negative {
604 "Negative limit switch active".to_string()
605 } else if effective_sw_max && self.moving_positive {
606 format!(
607 "Software position limit: position {:.3} >= max {:.3}",
608 self.position, self.config.max_position_limit
609 )
610 } else {
611 format!(
612 "Software position limit: position {:.3} <= min {:.3}",
613 self.position, self.config.min_position_limit
614 )
615 };
616 self.set_op_error(&msg);
617 }
618 }
619
620 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
625 match self.op.clone() {
626 AxisOp::Idle => {}
627 AxisOp::Enabling(step) => self.tick_enabling(view, step),
628 AxisOp::Disabling(step) => self.tick_disabling(view, step),
629 AxisOp::Moving(kind, step) => self.tick_moving(view, kind, step),
630 AxisOp::Homing(step) => self.tick_homing(view, client, step),
631 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, step),
632 AxisOp::Halting => self.tick_halting(view),
633 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
634 }
635 }
636
637 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
642 match step {
643 1 => {
644 let sw = RawStatusWord(view.status_word());
645 if sw.state() == Cia402State::ReadyToSwitchOn {
646 let mut cw = RawControlWord(view.control_word());
647 cw.cmd_enable_operation();
648 view.set_control_word(cw.raw());
649 self.op = AxisOp::Enabling(2);
650 } else if self.op_timed_out() {
651 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
652 }
653 }
654 2 => {
655 let sw = RawStatusWord(view.status_word());
656 if sw.state() == Cia402State::OperationEnabled {
657 self.home_offset = view.position_actual();
658 log::info!("Axis enabled — home captured at {}", self.home_offset);
659 self.complete_op();
660 } else if self.op_timed_out() {
661 self.set_op_error("Enable timeout: waiting for OperationEnabled");
662 }
663 }
664 _ => self.complete_op(),
665 }
666 }
667
668 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
672 match step {
673 1 => {
674 let sw = RawStatusWord(view.status_word());
675 if sw.state() != Cia402State::OperationEnabled {
676 self.complete_op();
677 } else if self.op_timed_out() {
678 self.set_op_error("Disable timeout: drive still in OperationEnabled");
679 }
680 }
681 _ => self.complete_op(),
682 }
683 }
684
685 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8) {
691 match step {
692 1 => {
693 let sw = RawStatusWord(view.status_word());
695 if sw.raw() & (1 << 12) != 0 {
696 let mut cw = RawControlWord(view.control_word());
698 cw.set_bit(4, false);
699 view.set_control_word(cw.raw());
700 self.op = AxisOp::Moving(kind, 2);
701 } else if self.move_start_timed_out() {
702 self.set_op_error("Move timeout: set-point not acknowledged");
703 }
704 }
705 2 => {
706 let sw = RawStatusWord(view.status_word());
708 if sw.target_reached() {
709 self.complete_op();
710 }
711 }
712 _ => self.complete_op(),
713 }
714 }
715
716 fn tick_homing(
734 &mut self,
735 view: &mut impl AxisView,
736 client: &mut CommandClient,
737 step: u8,
738 ) {
739 match step {
740 0 => {
741 self.homing_sdo_tid = self.sdo.write(
743 client,
744 0x6098,
745 0,
746 json!(self.homing_method),
747 );
748 self.op = AxisOp::Homing(1);
749 }
750 1 => {
751 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
753 SdoResult::Ok(_) => {
754 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
756 self.op = AxisOp::Homing(8);
757 } else {
758 self.op = AxisOp::Homing(2);
759 }
760 }
761 SdoResult::Pending => {
762 if self.homing_timed_out() {
763 self.set_op_error("Homing timeout: SDO write for homing method");
764 }
765 }
766 SdoResult::Err(e) => {
767 self.set_op_error(&format!("Homing SDO error: {}", e));
768 }
769 SdoResult::Timeout => {
770 self.set_op_error("Homing timeout: SDO write timed out");
771 }
772 }
773 }
774 2 => {
775 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
777 self.homing_sdo_tid = self.sdo.write(
778 client,
779 0x6099,
780 1,
781 json!(speed_counts),
782 );
783 self.op = AxisOp::Homing(3);
784 }
785 3 => {
786 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
787 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
788 SdoResult::Pending => {
789 if self.homing_timed_out() {
790 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
791 }
792 }
793 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
794 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
795 }
796 }
797 4 => {
798 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
800 self.homing_sdo_tid = self.sdo.write(
801 client,
802 0x6099,
803 2,
804 json!(speed_counts),
805 );
806 self.op = AxisOp::Homing(5);
807 }
808 5 => {
809 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
810 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
811 SdoResult::Pending => {
812 if self.homing_timed_out() {
813 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
814 }
815 }
816 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
817 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
818 }
819 }
820 6 => {
821 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
823 self.homing_sdo_tid = self.sdo.write(
824 client,
825 0x609A,
826 0,
827 json!(accel_counts),
828 );
829 self.op = AxisOp::Homing(7);
830 }
831 7 => {
832 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
833 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
834 SdoResult::Pending => {
835 if self.homing_timed_out() {
836 self.set_op_error("Homing timeout: SDO write for homing acceleration");
837 }
838 }
839 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
840 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
841 }
842 }
843 8 => {
844 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
846 self.op = AxisOp::Homing(9);
847 }
848 9 => {
849 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
851 self.op = AxisOp::Homing(10);
852 } else if self.homing_timed_out() {
853 self.set_op_error("Homing timeout: mode not confirmed");
854 }
855 }
856 10 => {
857 let mut cw = RawControlWord(view.control_word());
859 cw.set_bit(4, true);
860 view.set_control_word(cw.raw());
861 self.op = AxisOp::Homing(11);
862 }
863 11 => {
864 let sw = view.status_word();
867 let error = sw & (1 << 13) != 0;
868 let attained = sw & (1 << 12) != 0;
869 let reached = sw & (1 << 10) != 0;
870
871 if error {
872 self.set_op_error("Homing error: drive reported homing failure");
873 } else if attained && reached {
874 self.op = AxisOp::Homing(12);
876 } else if self.homing_timed_out() {
877 self.set_op_error("Homing timeout: procedure did not complete");
878 }
879 }
880 12 => {
881 self.home_offset = view.position_actual()
884 - self.config.to_counts(self.config.home_position).round() as i32;
885 let mut cw = RawControlWord(view.control_word());
887 cw.set_bit(4, false);
888 view.set_control_word(cw.raw());
889 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
891 log::info!("Homing complete — home offset: {}", self.home_offset);
892 self.complete_op();
893 }
894 _ => self.complete_op(),
895 }
896 }
897
898 fn configure_soft_homing(&mut self, method: HomingMethod) {
901 match method {
902 HomingMethod::LimitSwitchPosRt => {
903 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
904 self.soft_home_edge = SoftHomeEdge::Rising;
905 self.soft_home_direction = 1.0;
906 }
907 HomingMethod::LimitSwitchNegRt => {
908 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
909 self.soft_home_edge = SoftHomeEdge::Rising;
910 self.soft_home_direction = -1.0;
911 }
912 HomingMethod::LimitSwitchPosFt => {
913 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
914 self.soft_home_edge = SoftHomeEdge::Falling;
915 self.soft_home_direction = 1.0;
916 }
917 HomingMethod::LimitSwitchNegFt => {
918 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
919 self.soft_home_edge = SoftHomeEdge::Falling;
920 self.soft_home_direction = -1.0;
921 }
922 HomingMethod::HomeSensorPosRt => {
923 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
924 self.soft_home_edge = SoftHomeEdge::Rising;
925 self.soft_home_direction = 1.0;
926 }
927 HomingMethod::HomeSensorNegRt => {
928 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
929 self.soft_home_edge = SoftHomeEdge::Rising;
930 self.soft_home_direction = -1.0;
931 }
932 HomingMethod::HomeSensorPosFt => {
933 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
934 self.soft_home_edge = SoftHomeEdge::Falling;
935 self.soft_home_direction = 1.0;
936 }
937 HomingMethod::HomeSensorNegFt => {
938 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
939 self.soft_home_edge = SoftHomeEdge::Falling;
940 self.soft_home_direction = -1.0;
941 }
942 _ => {} }
944 }
945
946 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
947 let already_active = match (self.soft_home_sensor, self.soft_home_edge) {
949 (SoftHomeSensor::PositiveLimit, SoftHomeEdge::Rising) => view.positive_limit_active(),
950 (SoftHomeSensor::NegativeLimit, SoftHomeEdge::Rising) => view.negative_limit_active(),
951 (SoftHomeSensor::HomeSensor, SoftHomeEdge::Rising) => view.home_sensor_active(),
952 (SoftHomeSensor::PositiveLimit, SoftHomeEdge::Falling) => !view.positive_limit_active(),
953 (SoftHomeSensor::NegativeLimit, SoftHomeEdge::Falling) => !view.negative_limit_active(),
954 (SoftHomeSensor::HomeSensor, SoftHomeEdge::Falling) => !view.home_sensor_active(),
955 };
956
957 if already_active {
958 self.set_op_error("Software homing: sensor already in trigger state");
959 return;
960 }
961
962 self.op = AxisOp::SoftHoming(0);
963 self.op_started = Some(Instant::now());
964 }
965
966 fn check_soft_home_edge(&self, view: &impl AxisView) -> bool {
967 let (current, prev) = match self.soft_home_sensor {
968 SoftHomeSensor::PositiveLimit => (view.positive_limit_active(), self.prev_positive_limit),
969 SoftHomeSensor::NegativeLimit => (view.negative_limit_active(), self.prev_negative_limit),
970 SoftHomeSensor::HomeSensor => (view.home_sensor_active(), self.prev_home_sensor),
971 };
972 match self.soft_home_edge {
973 SoftHomeEdge::Rising => !prev && current, SoftHomeEdge::Falling => prev && !current, }
976 }
977
978 fn tick_soft_homing(&mut self, view: &mut impl AxisView, step: u8) {
988 match step {
989 0 => {
990 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
992
993 let target = self.config.to_counts(self.soft_home_direction * 999_999.0).round() as i32 + self.home_offset;
996 view.set_target_position(target);
997
998 let cpu = self.config.counts_per_user();
1000 let vel = (self.config.homing_speed * cpu).round() as u32;
1001 let accel = (self.config.homing_accel * cpu).round() as u32;
1002 let decel = (self.config.homing_decel * cpu).round() as u32;
1003 view.set_profile_velocity(vel);
1004 view.set_profile_acceleration(accel);
1005 view.set_profile_deceleration(decel);
1006
1007 let mut cw = RawControlWord(view.control_word());
1009 cw.set_bit(4, true);
1010 cw.set_bit(6, false); cw.set_bit(8, false); view.set_control_word(cw.raw());
1013
1014 self.op = AxisOp::SoftHoming(1);
1015 }
1016 1 => {
1017 if self.check_soft_home_edge(view) {
1019 self.op = AxisOp::SoftHoming(4);
1020 return;
1021 }
1022 let sw = RawStatusWord(view.status_word());
1023 if sw.raw() & (1 << 12) != 0 {
1024 let mut cw = RawControlWord(view.control_word());
1026 cw.set_bit(4, false);
1027 view.set_control_word(cw.raw());
1028 self.op = AxisOp::SoftHoming(2);
1029 } else if self.homing_timed_out() {
1030 self.set_op_error("Software homing timeout: set-point not acknowledged");
1031 }
1032 }
1033 2 => {
1034 if self.check_soft_home_edge(view) {
1036 self.op = AxisOp::SoftHoming(4);
1037 return;
1038 }
1039 self.op = AxisOp::SoftHoming(3);
1040 }
1041 3 => {
1042 if self.check_soft_home_edge(view) {
1044 self.op = AxisOp::SoftHoming(4);
1045 } else if self.homing_timed_out() {
1046 self.set_op_error("Software homing timeout: sensor edge not detected");
1047 }
1048 }
1049 4 => {
1050 let mut cw = RawControlWord(view.control_word());
1053 cw.set_bit(8, true); view.set_control_word(cw.raw());
1055 self.home_offset = view.position_actual()
1056 - self.config.to_counts(self.config.home_position).round() as i32;
1057 log::info!("Software homing: edge detected, home offset: {}", self.home_offset);
1058 self.op = AxisOp::SoftHoming(5);
1059 }
1060 5 => {
1061 let sw = RawStatusWord(view.status_word());
1063 if sw.target_reached() {
1064 let mut cw = RawControlWord(view.control_word());
1066 cw.set_bit(8, false);
1067 view.set_control_word(cw.raw());
1068 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1069 log::info!("Software homing complete — home offset: {}", self.home_offset);
1070 self.complete_op();
1071 } else if self.homing_timed_out() {
1072 self.set_op_error("Software homing timeout: halt not acknowledged");
1073 }
1074 }
1075 _ => self.complete_op(),
1076 }
1077 }
1078
1079 fn tick_halting(&mut self, _view: &mut impl AxisView) {
1081 self.complete_op();
1084 }
1085
1086 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1091 match step {
1092 1 => {
1093 let mut cw = RawControlWord(view.control_word());
1095 cw.cmd_fault_reset();
1096 view.set_control_word(cw.raw());
1097 self.op = AxisOp::FaultRecovery(2);
1098 }
1099 2 => {
1100 let sw = RawStatusWord(view.status_word());
1102 let state = sw.state();
1103 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1104 log::info!("Fault cleared (drive state: {})", state);
1105 self.complete_op();
1106 } else if self.op_timed_out() {
1107 self.set_op_error("Fault reset timeout: drive still faulted");
1108 }
1109 }
1110 _ => self.complete_op(),
1111 }
1112 }
1113}
1114
1115#[cfg(test)]
1120mod tests {
1121 use super::*;
1122
1123 struct MockView {
1125 control_word: u16,
1126 status_word: u16,
1127 target_position: i32,
1128 profile_velocity: u32,
1129 profile_acceleration: u32,
1130 profile_deceleration: u32,
1131 modes_of_operation: i8,
1132 modes_of_operation_display: i8,
1133 position_actual: i32,
1134 velocity_actual: i32,
1135 error_code: u16,
1136 positive_limit: bool,
1137 negative_limit: bool,
1138 home_sensor: bool,
1139 }
1140
1141 impl MockView {
1142 fn new() -> Self {
1143 Self {
1144 control_word: 0,
1145 status_word: 0x0040, target_position: 0,
1147 profile_velocity: 0,
1148 profile_acceleration: 0,
1149 profile_deceleration: 0,
1150 modes_of_operation: 0,
1151 modes_of_operation_display: 1, position_actual: 0,
1153 velocity_actual: 0,
1154 error_code: 0,
1155 positive_limit: false,
1156 negative_limit: false,
1157 home_sensor: false,
1158 }
1159 }
1160
1161 fn set_state(&mut self, state: u16) {
1162 self.status_word = state;
1163 }
1164 }
1165
1166 impl AxisView for MockView {
1167 fn control_word(&self) -> u16 { self.control_word }
1168 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1169 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1170 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1171 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1172 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1173 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1174 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1175 fn status_word(&self) -> u16 { self.status_word }
1176 fn position_actual(&self) -> i32 { self.position_actual }
1177 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1178 fn error_code(&self) -> u16 { self.error_code }
1179 fn positive_limit_active(&self) -> bool { self.positive_limit }
1180 fn negative_limit_active(&self) -> bool { self.negative_limit }
1181 fn home_sensor_active(&self) -> bool { self.home_sensor }
1182 }
1183
1184 fn test_config() -> AxisConfig {
1185 AxisConfig::new(12_800).with_user_scale(360.0)
1186 }
1187
1188 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1190 use tokio::sync::mpsc;
1191 let (write_tx, write_rx) = mpsc::unbounded_channel();
1192 let (response_tx, response_rx) = mpsc::unbounded_channel();
1193 let client = CommandClient::new(write_tx, response_rx);
1194 let axis = Axis::new(test_config(), "TestDrive");
1195 (axis, client, response_tx, write_rx)
1196 }
1197
1198 #[test]
1199 fn axis_config_conversion() {
1200 let cfg = test_config();
1201 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
1203 }
1204
1205 #[test]
1206 fn enable_sequence_sets_pp_mode_and_shutdown() {
1207 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1208 let mut view = MockView::new();
1209
1210 axis.enable(&mut view);
1211
1212 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
1214 assert_eq!(view.control_word & 0x008F, 0x0006);
1216 assert_eq!(axis.op, AxisOp::Enabling(1));
1218
1219 view.set_state(0x0021); axis.tick(&mut view, &mut client);
1222
1223 assert_eq!(view.control_word & 0x008F, 0x000F);
1225 assert_eq!(axis.op, AxisOp::Enabling(2));
1226
1227 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1230
1231 assert_eq!(axis.op, AxisOp::Idle);
1233 assert!(axis.motor_on);
1234 }
1235
1236 #[test]
1237 fn move_absolute_sets_target() {
1238 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1239 let mut view = MockView::new();
1240 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1245
1246 assert_eq!(view.target_position, 1600);
1248 assert_eq!(view.profile_velocity, 3200);
1250 assert_eq!(view.profile_acceleration, 6400);
1252 assert_eq!(view.profile_deceleration, 6400);
1253 assert!(view.control_word & (1 << 4) != 0);
1255 assert!(view.control_word & (1 << 6) == 0);
1257 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1259 }
1260
1261 #[test]
1262 fn move_relative_sets_relative_bit() {
1263 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1264 let mut view = MockView::new();
1265 view.set_state(0x0027);
1266 axis.tick(&mut view, &mut client);
1267
1268 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
1269
1270 assert!(view.control_word & (1 << 6) != 0);
1272 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1)));
1273 }
1274
1275 #[test]
1276 fn move_completes_on_target_reached() {
1277 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1278 let mut view = MockView::new();
1279 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1281
1282 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1283
1284 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1287 assert!(view.control_word & (1 << 4) == 0);
1289
1290 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1293 assert_eq!(axis.op, AxisOp::Idle);
1295 assert!(!axis.in_motion);
1296 }
1297
1298 #[test]
1299 fn fault_detected_sets_error() {
1300 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1301 let mut view = MockView::new();
1302 view.set_state(0x0008); view.error_code = 0x1234;
1304
1305 axis.tick(&mut view, &mut client);
1306
1307 assert!(axis.is_error);
1308 assert_eq!(axis.error_code, 0x1234);
1309 assert!(axis.error_message.contains("fault"));
1310 }
1311
1312 #[test]
1313 fn fault_recovery_sequence() {
1314 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1315 let mut view = MockView::new();
1316 view.set_state(0x0008); axis.reset_faults(&mut view);
1319 assert!(view.control_word & 0x0080 == 0);
1321
1322 axis.tick(&mut view, &mut client);
1324 assert!(view.control_word & 0x0080 != 0);
1325
1326 view.set_state(0x0040);
1328 axis.tick(&mut view, &mut client);
1329 assert_eq!(axis.op, AxisOp::Idle);
1330 assert!(!axis.is_error);
1331 }
1332
1333 #[test]
1334 fn disable_sequence() {
1335 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1336 let mut view = MockView::new();
1337 view.set_state(0x0027); axis.disable(&mut view);
1340 assert_eq!(view.control_word & 0x008F, 0x0007);
1342
1343 view.set_state(0x0023); axis.tick(&mut view, &mut client);
1346 assert_eq!(axis.op, AxisOp::Idle);
1347 }
1348
1349 #[test]
1350 fn position_tracks_with_home_offset() {
1351 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1352 let mut view = MockView::new();
1353 view.set_state(0x0027);
1354 view.position_actual = 5000;
1355
1356 axis.enable(&mut view);
1358 view.set_state(0x0021);
1359 axis.tick(&mut view, &mut client);
1360 view.set_state(0x0027);
1361 axis.tick(&mut view, &mut client);
1362
1363 assert_eq!(axis.home_offset, 5000);
1365
1366 assert!((axis.position - 0.0).abs() < 0.01);
1368
1369 view.position_actual = 6600;
1371 axis.tick(&mut view, &mut client);
1372
1373 assert!((axis.position - 45.0).abs() < 0.1);
1375 }
1376
1377 #[test]
1378 fn set_position_adjusts_home_offset() {
1379 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1380 let mut view = MockView::new();
1381 view.position_actual = 3200;
1382
1383 axis.set_position(&view, 90.0);
1384 axis.tick(&mut view, &mut client);
1385
1386 assert_eq!(axis.home_offset, 0);
1388 assert!((axis.position - 90.0).abs() < 0.01);
1389 }
1390
1391 #[test]
1392 fn halt_sets_bit_and_goes_idle() {
1393 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1394 let mut view = MockView::new();
1395 view.set_state(0x0027);
1396
1397 axis.halt(&mut view);
1398 assert!(view.control_word & (1 << 8) != 0);
1400
1401 axis.tick(&mut view, &mut client);
1403 assert_eq!(axis.op, AxisOp::Idle);
1404 }
1405
1406 #[test]
1407 fn is_busy_tracks_operations() {
1408 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1409 let mut view = MockView::new();
1410
1411 axis.tick(&mut view, &mut client);
1413 assert!(!axis.is_busy);
1414
1415 axis.enable(&mut view);
1417 axis.tick(&mut view, &mut client);
1418 assert!(axis.is_busy);
1419
1420 view.set_state(0x0021);
1422 axis.tick(&mut view, &mut client);
1423 view.set_state(0x0027);
1424 axis.tick(&mut view, &mut client);
1425 assert!(!axis.is_busy);
1426
1427 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1429 axis.tick(&mut view, &mut client);
1430 assert!(axis.is_busy);
1431 assert!(axis.in_motion);
1432 }
1433
1434 #[test]
1435 fn fault_during_move_cancels_op() {
1436 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1437 let mut view = MockView::new();
1438 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1440
1441 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1443 axis.tick(&mut view, &mut client);
1444 assert!(axis.is_busy);
1445 assert!(!axis.is_error);
1446
1447 view.set_state(0x0008); axis.tick(&mut view, &mut client);
1450
1451 assert!(!axis.is_busy);
1453 assert!(axis.is_error);
1454 assert_eq!(axis.op, AxisOp::Idle);
1455 }
1456
1457 #[test]
1458 fn move_absolute_rejected_by_max_limit() {
1459 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1460 let mut view = MockView::new();
1461 view.set_state(0x0027);
1462 axis.tick(&mut view, &mut client);
1463
1464 axis.set_software_max_limit(90.0);
1465 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
1466
1467 assert!(axis.is_error);
1469 assert_eq!(axis.op, AxisOp::Idle);
1470 assert!(axis.error_message.contains("max software limit"));
1471 }
1472
1473 #[test]
1474 fn move_absolute_rejected_by_min_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_min_limit(-10.0);
1481 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
1482
1483 assert!(axis.is_error);
1484 assert_eq!(axis.op, AxisOp::Idle);
1485 assert!(axis.error_message.contains("min software limit"));
1486 }
1487
1488 #[test]
1489 fn move_relative_rejected_by_max_limit() {
1490 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1491 let mut view = MockView::new();
1492 view.set_state(0x0027);
1493 axis.tick(&mut view, &mut client);
1494
1495 axis.set_software_max_limit(50.0);
1497 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
1498
1499 assert!(axis.is_error);
1500 assert_eq!(axis.op, AxisOp::Idle);
1501 assert!(axis.error_message.contains("max software limit"));
1502 }
1503
1504 #[test]
1505 fn move_within_limits_allowed() {
1506 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1507 let mut view = MockView::new();
1508 view.set_state(0x0027);
1509 axis.tick(&mut view, &mut client);
1510
1511 axis.set_software_max_limit(90.0);
1512 axis.set_software_min_limit(-90.0);
1513 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1514
1515 assert!(!axis.is_error);
1517 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1518 }
1519
1520 #[test]
1521 fn runtime_limit_halts_move_in_violated_direction() {
1522 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1523 let mut view = MockView::new();
1524 view.set_state(0x0027);
1525 axis.tick(&mut view, &mut client);
1526
1527 axis.set_software_max_limit(45.0);
1528 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1530
1531 view.position_actual = 1650;
1534 view.velocity_actual = 100; view.status_word = 0x1027;
1538 axis.tick(&mut view, &mut client);
1539 view.status_word = 0x0027;
1540 axis.tick(&mut view, &mut client);
1541
1542 assert!(axis.is_error);
1544 assert!(axis.at_max_limit);
1545 assert_eq!(axis.op, AxisOp::Idle);
1546 assert!(axis.error_message.contains("Software position limit"));
1547 assert!(view.control_word & (1 << 8) != 0);
1549 }
1550
1551 #[test]
1552 fn runtime_limit_allows_move_in_opposite_direction() {
1553 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1554 let mut view = MockView::new();
1555 view.set_state(0x0027);
1556 view.position_actual = 1778; axis.set_software_max_limit(45.0);
1559 axis.tick(&mut view, &mut client);
1560 assert!(axis.at_max_limit);
1561
1562 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
1564 assert!(!axis.is_error);
1565 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1566
1567 view.velocity_actual = -100;
1569 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1571 assert!(!axis.is_error);
1573 }
1574
1575 #[test]
1576 fn positive_limit_switch_halts_positive_move() {
1577 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1578 let mut view = MockView::new();
1579 view.set_state(0x0027);
1580 axis.tick(&mut view, &mut client);
1581
1582 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1584 view.velocity_actual = 100; view.status_word = 0x1027;
1587 axis.tick(&mut view, &mut client);
1588 view.status_word = 0x0027;
1589
1590 view.positive_limit = true;
1592 axis.tick(&mut view, &mut client);
1593
1594 assert!(axis.is_error);
1595 assert!(axis.at_positive_limit_switch);
1596 assert!(!axis.is_busy);
1597 assert!(axis.error_message.contains("Positive limit switch"));
1598 assert!(view.control_word & (1 << 8) != 0);
1600 }
1601
1602 #[test]
1603 fn negative_limit_switch_halts_negative_move() {
1604 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1605 let mut view = MockView::new();
1606 view.set_state(0x0027);
1607 axis.tick(&mut view, &mut client);
1608
1609 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
1611 view.velocity_actual = -100; view.status_word = 0x1027;
1613 axis.tick(&mut view, &mut client);
1614 view.status_word = 0x0027;
1615
1616 view.negative_limit = true;
1618 axis.tick(&mut view, &mut client);
1619
1620 assert!(axis.is_error);
1621 assert!(axis.at_negative_limit_switch);
1622 assert!(axis.error_message.contains("Negative limit switch"));
1623 }
1624
1625 #[test]
1626 fn limit_switch_allows_move_in_opposite_direction() {
1627 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1628 let mut view = MockView::new();
1629 view.set_state(0x0027);
1630 view.positive_limit = true;
1632 view.velocity_actual = -100;
1633 axis.tick(&mut view, &mut client);
1634 assert!(axis.at_positive_limit_switch);
1635
1636 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
1638 view.status_word = 0x1027;
1639 axis.tick(&mut view, &mut client);
1640
1641 assert!(!axis.is_error);
1643 assert!(matches!(axis.op, AxisOp::Moving(_, _)));
1644 }
1645
1646 #[test]
1647 fn limit_switch_ignored_when_not_moving() {
1648 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1649 let mut view = MockView::new();
1650 view.set_state(0x0027);
1651 view.positive_limit = true;
1652
1653 axis.tick(&mut view, &mut client);
1654
1655 assert!(axis.at_positive_limit_switch);
1657 assert!(!axis.is_error);
1658 }
1659
1660 #[test]
1661 fn home_sensor_output_tracks_view() {
1662 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1663 let mut view = MockView::new();
1664 view.set_state(0x0027);
1665
1666 axis.tick(&mut view, &mut client);
1667 assert!(!axis.home_sensor);
1668
1669 view.home_sensor = true;
1670 axis.tick(&mut view, &mut client);
1671 assert!(axis.home_sensor);
1672
1673 view.home_sensor = false;
1674 axis.tick(&mut view, &mut client);
1675 assert!(!axis.home_sensor);
1676 }
1677
1678 #[test]
1679 fn velocity_output_converted() {
1680 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1681 let mut view = MockView::new();
1682 view.set_state(0x0027);
1683 view.velocity_actual = 3200;
1685
1686 axis.tick(&mut view, &mut client);
1687
1688 assert!((axis.speed - 90.0).abs() < 0.1);
1689 assert!(axis.moving_positive);
1690 assert!(!axis.moving_negative);
1691 }
1692
1693 fn soft_homing_config() -> AxisConfig {
1696 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
1697 cfg.homing_speed = 10.0;
1698 cfg.homing_accel = 20.0;
1699 cfg.homing_decel = 20.0;
1700 cfg
1701 }
1702
1703 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1704 use tokio::sync::mpsc;
1705 let (write_tx, write_rx) = mpsc::unbounded_channel();
1706 let (response_tx, response_rx) = mpsc::unbounded_channel();
1707 let client = CommandClient::new(write_tx, response_rx);
1708 let axis = Axis::new(soft_homing_config(), "TestDrive");
1709 (axis, client, response_tx, write_rx)
1710 }
1711
1712 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
1714 view.set_state(0x0027); axis.tick(view, client);
1716 }
1717
1718 #[test]
1719 fn soft_homing_rising_edge_home_sensor_triggers_home() {
1720 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1721 let mut view = MockView::new();
1722 enable_axis(&mut axis, &mut view, &mut client);
1723
1724 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1726 assert!(matches!(axis.op, AxisOp::SoftHoming(0)));
1727
1728 axis.tick(&mut view, &mut client);
1730 assert!(matches!(axis.op, AxisOp::SoftHoming(1)));
1731 assert!(view.control_word & (1 << 4) != 0);
1733
1734 view.status_word = 0x1027;
1736 axis.tick(&mut view, &mut client);
1737 assert!(view.control_word & (1 << 4) == 0);
1739 assert!(matches!(axis.op, AxisOp::SoftHoming(2)));
1740
1741 view.status_word = 0x0027;
1743 axis.tick(&mut view, &mut client);
1744 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1745
1746 axis.tick(&mut view, &mut client);
1748 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1749
1750 view.home_sensor = true;
1752 view.position_actual = 5000;
1753 axis.tick(&mut view, &mut client);
1754 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1756
1757 axis.tick(&mut view, &mut client);
1759 assert!(view.control_word & (1 << 8) != 0); assert_eq!(axis.home_offset, 5000);
1761 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
1762
1763 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1766 assert_eq!(axis.op, AxisOp::Idle);
1768 assert!(!axis.is_busy);
1769 assert!(!axis.is_error);
1770 }
1771
1772 #[test]
1773 fn soft_homing_falling_edge_home_sensor_triggers_home() {
1774 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1775 let mut view = MockView::new();
1776 view.home_sensor = true;
1778 enable_axis(&mut axis, &mut view, &mut client);
1779
1780 axis.home(&mut view, HomingMethod::HomeSensorPosFt);
1782 assert!(matches!(axis.op, AxisOp::SoftHoming(0)));
1783
1784 axis.tick(&mut view, &mut client);
1786 view.status_word = 0x1027;
1788 axis.tick(&mut view, &mut client);
1789 view.status_word = 0x0027;
1791 axis.tick(&mut view, &mut client);
1792
1793 axis.tick(&mut view, &mut client);
1795 assert!(matches!(axis.op, AxisOp::SoftHoming(3)));
1796
1797 view.home_sensor = false;
1799 view.position_actual = 3000;
1800 axis.tick(&mut view, &mut client);
1801 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1802
1803 axis.tick(&mut view, &mut client);
1805 view.status_word = 0x0427;
1806 axis.tick(&mut view, &mut client);
1807 assert_eq!(axis.op, AxisOp::Idle);
1808 assert_eq!(axis.home_offset, 3000);
1809 }
1810
1811 #[test]
1812 fn soft_homing_limit_switch_suppresses_halt() {
1813 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1814 let mut view = MockView::new();
1815 enable_axis(&mut axis, &mut view, &mut client);
1816
1817 axis.home(&mut view, HomingMethod::LimitSwitchPosRt);
1819
1820 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1825 axis.tick(&mut view, &mut client); view.positive_limit = true;
1829 view.velocity_actual = 100; view.position_actual = 8000;
1831 axis.tick(&mut view, &mut client);
1832
1833 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1835 assert!(!axis.is_error);
1836 }
1837
1838 #[test]
1839 fn soft_homing_opposite_limit_still_protects() {
1840 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1841 let mut view = MockView::new();
1842 enable_axis(&mut axis, &mut view, &mut client);
1843
1844 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1846
1847 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
1852 axis.tick(&mut view, &mut client); view.negative_limit = true;
1857 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
1859
1860 assert!(axis.is_error);
1862 assert!(axis.error_message.contains("Negative limit switch"));
1863 }
1864
1865 #[test]
1866 fn soft_homing_sensor_already_active_rejects() {
1867 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1868 let mut view = MockView::new();
1869 enable_axis(&mut axis, &mut view, &mut client);
1870
1871 view.home_sensor = true;
1873 axis.tick(&mut view, &mut client); axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1876
1877 assert!(axis.is_error);
1879 assert!(axis.error_message.contains("already in trigger state"));
1880 assert_eq!(axis.op, AxisOp::Idle);
1881 }
1882
1883 #[test]
1884 fn soft_homing_negative_direction_sets_negative_target() {
1885 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1886 let mut view = MockView::new();
1887 enable_axis(&mut axis, &mut view, &mut client);
1888
1889 axis.home(&mut view, HomingMethod::HomeSensorNegRt);
1890 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
1894 }
1895
1896 #[test]
1897 fn home_integrated_method_starts_hardware_homing() {
1898 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1899 let mut view = MockView::new();
1900 enable_axis(&mut axis, &mut view, &mut client);
1901
1902 axis.home(&mut view, HomingMethod::CurrentPosition);
1903 assert!(matches!(axis.op, AxisOp::Homing(0)));
1904 assert_eq!(axis.homing_method, 37);
1905 }
1906
1907 #[test]
1908 fn home_integrated_arbitrary_code() {
1909 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1910 let mut view = MockView::new();
1911 enable_axis(&mut axis, &mut view, &mut client);
1912
1913 axis.home(&mut view, HomingMethod::Integrated(35));
1914 assert!(matches!(axis.op, AxisOp::Homing(0)));
1915 assert_eq!(axis.homing_method, 35);
1916 }
1917
1918 #[test]
1919 fn hardware_homing_skips_speed_sdos_when_zero() {
1920 use mechutil::ipc::CommandMessage;
1921
1922 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
1923 let mut view = MockView::new();
1924 enable_axis(&mut axis, &mut view, &mut client);
1925
1926 axis.home(&mut view, HomingMethod::Integrated(37));
1928
1929 axis.tick(&mut view, &mut client);
1931 assert!(matches!(axis.op, AxisOp::Homing(1)));
1932
1933 let _ = write_rx.try_recv();
1935
1936 let tid = axis.homing_sdo_tid;
1938 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
1939 client.poll();
1940 axis.tick(&mut view, &mut client);
1941
1942 assert!(matches!(axis.op, AxisOp::Homing(8)));
1944 }
1945
1946 #[test]
1947 fn hardware_homing_writes_speed_sdos_when_nonzero() {
1948 use mechutil::ipc::CommandMessage;
1949
1950 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
1951 let mut view = MockView::new();
1952 enable_axis(&mut axis, &mut view, &mut client);
1953
1954 axis.home(&mut view, HomingMethod::Integrated(37));
1956
1957 axis.tick(&mut view, &mut client);
1959 assert!(matches!(axis.op, AxisOp::Homing(1)));
1960 let _ = write_rx.try_recv();
1961
1962 let tid = axis.homing_sdo_tid;
1964 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
1965 client.poll();
1966 axis.tick(&mut view, &mut client);
1967 assert!(matches!(axis.op, AxisOp::Homing(2)));
1969 }
1970
1971 #[test]
1972 fn soft_homing_edge_during_ack_step() {
1973 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
1974 let mut view = MockView::new();
1975 enable_axis(&mut axis, &mut view, &mut client);
1976
1977 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
1978 axis.tick(&mut view, &mut client); view.home_sensor = true;
1982 view.position_actual = 2000;
1983 axis.tick(&mut view, &mut client);
1984
1985 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
1987 }
1988
1989 #[test]
1990 fn soft_homing_applies_home_position() {
1991 let mut cfg = soft_homing_config();
1994 cfg.home_position = 90.0;
1995
1996 use tokio::sync::mpsc;
1997 let (write_tx, write_rx) = mpsc::unbounded_channel();
1998 let (response_tx, response_rx) = mpsc::unbounded_channel();
1999 let mut client = CommandClient::new(write_tx, response_rx);
2000 let mut axis = Axis::new(cfg, "TestDrive");
2001 let _ = (response_tx, write_rx);
2002
2003 let mut view = MockView::new();
2004 enable_axis(&mut axis, &mut view, &mut client);
2005
2006 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
2007
2008 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2013 axis.tick(&mut view, &mut client); view.home_sensor = true;
2017 view.position_actual = 5000;
2018 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client); assert_eq!(axis.home_offset, 1800);
2026
2027 view.status_word = 0x0427;
2029 axis.tick(&mut view, &mut client);
2030 assert_eq!(axis.op, AxisOp::Idle);
2031
2032 assert!((axis.position - 90.0).abs() < 0.1);
2035 }
2036
2037 #[test]
2038 fn soft_homing_default_home_position_zero() {
2039 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2041 let mut view = MockView::new();
2042 enable_axis(&mut axis, &mut view, &mut client);
2043
2044 axis.home(&mut view, HomingMethod::HomeSensorPosRt);
2045
2046 axis.tick(&mut view, &mut client);
2048 view.status_word = 0x1027;
2049 axis.tick(&mut view, &mut client);
2050 view.status_word = 0x0027;
2051 axis.tick(&mut view, &mut client);
2052
2053 view.home_sensor = true;
2055 view.position_actual = 5000;
2056 axis.tick(&mut view, &mut client); axis.tick(&mut view, &mut client);
2060
2061 assert_eq!(axis.home_offset, 5000);
2063
2064 view.status_word = 0x0427;
2066 axis.tick(&mut view, &mut client);
2067
2068 assert!((axis.position - 0.0).abs() < 0.01);
2070 }
2071}