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