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 halt_stable_count: u8,
106 prev_positive_limit: bool,
107 prev_negative_limit: bool,
108 prev_home_sensor: bool,
109
110 pub is_error: bool,
114 pub error_code: u32,
116 pub error_message: String,
118 pub motor_on: bool,
120 pub is_busy: bool,
126 pub in_motion: bool,
128 pub moving_positive: bool,
130 pub moving_negative: bool,
132 pub position: f64,
134 pub raw_position: i64,
136 pub speed: f64,
138 pub at_max_limit: bool,
140 pub at_min_limit: bool,
142 pub at_positive_limit_switch: bool,
144 pub at_negative_limit_switch: bool,
146 pub home_sensor: bool,
148}
149
150impl Axis {
151 pub fn new(config: AxisConfig, device_name: &str) -> Self {
156 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
157 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
158 let move_start_timeout = op_timeout; Self {
160 config,
161 sdo: SdoClient::new(device_name),
162 op: AxisOp::Idle,
163 home_offset: 0,
164 last_raw_position: 0,
165 op_started: None,
166 op_timeout,
167 homing_timeout,
168 move_start_timeout,
169 pending_move_target: 0,
170 pending_move_vel: 0,
171 pending_move_accel: 0,
172 pending_move_decel: 0,
173 homing_method: 37,
174 homing_sdo_tid: 0,
175 soft_home_sensor: SoftHomeSensor::HomeSensor,
176 soft_home_sensor_type: SoftHomeSensorType::Pnp,
177 soft_home_direction: 1.0,
178 halt_stable_count: 0,
179 prev_positive_limit: false,
180 prev_negative_limit: false,
181 prev_home_sensor: false,
182 is_error: false,
183 error_code: 0,
184 error_message: String::new(),
185 motor_on: false,
186 is_busy: false,
187 in_motion: false,
188 moving_positive: false,
189 moving_negative: false,
190 position: 0.0,
191 raw_position: 0,
192 speed: 0.0,
193 at_max_limit: false,
194 at_min_limit: false,
195 at_positive_limit_switch: false,
196 at_negative_limit_switch: false,
197 home_sensor: false,
198 }
199 }
200
201 pub fn config(&self) -> &AxisConfig {
203 &self.config
204 }
205
206 pub fn move_absolute(
216 &mut self,
217 view: &mut impl AxisView,
218 target: f64,
219 vel: f64,
220 accel: f64,
221 decel: f64,
222 ) {
223 if let Some(msg) = self.check_target_limit(target) {
224 self.set_op_error(&msg);
225 return;
226 }
227
228 let cpu = self.config.counts_per_user();
229 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
230 let raw_vel = (vel * cpu).round() as u32;
231 let raw_accel = (accel * cpu).round() as u32;
232 let raw_decel = (decel * cpu).round() as u32;
233
234 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute);
235 }
236
237 pub fn move_relative(
243 &mut self,
244 view: &mut impl AxisView,
245 distance: f64,
246 vel: f64,
247 accel: f64,
248 decel: f64,
249 ) {
250 if let Some(msg) = self.check_target_limit(self.position + distance) {
251 self.set_op_error(&msg);
252 return;
253 }
254
255 let cpu = self.config.counts_per_user();
256 let raw_distance = self.config.to_counts(distance).round() as i32;
257 let raw_vel = (vel * cpu).round() as u32;
258 let raw_accel = (accel * cpu).round() as u32;
259 let raw_decel = (decel * cpu).round() as u32;
260
261 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative);
262 }
263
264 fn start_move(
265 &mut self,
266 view: &mut impl AxisView,
267 raw_target: i32,
268 raw_vel: u32,
269 raw_accel: u32,
270 raw_decel: u32,
271 kind: MoveKind,
272 ) {
273 self.pending_move_target = raw_target;
274 self.pending_move_vel = raw_vel;
275 self.pending_move_accel = raw_accel;
276 self.pending_move_decel = raw_decel;
277
278 view.set_target_position(raw_target);
280 view.set_profile_velocity(raw_vel);
281 view.set_profile_acceleration(raw_accel);
282 view.set_profile_deceleration(raw_decel);
283
284 let mut cw = RawControlWord(view.control_word());
286 cw.set_bit(6, kind == MoveKind::Relative);
287 cw.set_bit(4, true); view.set_control_word(cw.raw());
289
290 self.op = AxisOp::Moving(kind, 1);
291 self.op_started = Some(Instant::now());
292 }
293
294 pub fn halt(&mut self, view: &mut impl AxisView) {
296 let mut cw = RawControlWord(view.control_word());
297 cw.set_bit(8, true); view.set_control_word(cw.raw());
299 self.op = AxisOp::Halting;
300 }
301
302 pub fn enable(&mut self, view: &mut impl AxisView) {
310 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
312 let mut cw = RawControlWord(view.control_word());
313 cw.cmd_shutdown();
314 view.set_control_word(cw.raw());
315
316 self.op = AxisOp::Enabling(1);
317 self.op_started = Some(Instant::now());
318 }
319
320 pub fn disable(&mut self, view: &mut impl AxisView) {
322 let mut cw = RawControlWord(view.control_word());
323 cw.cmd_disable_operation();
324 view.set_control_word(cw.raw());
325
326 self.op = AxisOp::Disabling(1);
327 self.op_started = Some(Instant::now());
328 }
329
330 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
334 let mut cw = RawControlWord(view.control_word());
336 cw.cmd_clear_fault_reset();
337 view.set_control_word(cw.raw());
338
339 self.is_error = false;
340 self.error_code = 0;
341 self.error_message.clear();
342 self.op = AxisOp::FaultRecovery(1);
343 self.op_started = Some(Instant::now());
344 }
345
346 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
354 if method.is_integrated() {
355 self.homing_method = method.cia402_code();
356 self.op = AxisOp::Homing(0);
357 self.op_started = Some(Instant::now());
358 let _ = view;
359 } else {
360 self.configure_soft_homing(method);
361 self.start_soft_homing(view);
362 }
363 }
364
365 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
374 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
375 }
376
377 pub fn set_home_position(&mut self, user_units: f64) {
381 self.config.home_position = user_units;
382 }
383
384 pub fn set_software_max_limit(&mut self, user_units: f64) {
386 self.config.max_position_limit = user_units;
387 self.config.enable_max_position_limit = true;
388 }
389
390 pub fn set_software_min_limit(&mut self, user_units: f64) {
392 self.config.min_position_limit = user_units;
393 self.config.enable_min_position_limit = true;
394 }
395
396 pub fn sdo_write(
402 &mut self,
403 client: &mut CommandClient,
404 index: u16,
405 sub_index: u8,
406 value: serde_json::Value,
407 ) {
408 self.sdo.write(client, index, sub_index, value);
409 }
410
411 pub fn sdo_read(
413 &mut self,
414 client: &mut CommandClient,
415 index: u16,
416 sub_index: u8,
417 ) -> u32 {
418 self.sdo.read(client, index, sub_index)
419 }
420
421 pub fn sdo_result(
423 &mut self,
424 client: &mut CommandClient,
425 tid: u32,
426 ) -> SdoResult {
427 self.sdo.result(client, tid, Duration::from_secs(5))
428 }
429
430 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
444 self.check_faults(view);
445 self.progress_op(view, client);
446 self.update_outputs(view);
447 self.check_limits(view);
448 }
449
450 fn update_outputs(&mut self, view: &impl AxisView) {
455 let raw = view.position_actual();
456 self.raw_position = raw as i64;
457 self.position = self.config.to_user((raw - self.home_offset) as f64);
458
459 let vel = view.velocity_actual();
460 let user_vel = self.config.to_user(vel as f64);
461 self.speed = user_vel.abs();
462 self.moving_positive = user_vel > 0.0;
463 self.moving_negative = user_vel < 0.0;
464 self.is_busy = self.op != AxisOp::Idle;
465 self.in_motion = matches!(self.op, AxisOp::Moving(_, _) | AxisOp::SoftHoming(_));
466
467 let sw = RawStatusWord(view.status_word());
468 self.motor_on = sw.state() == Cia402State::OperationEnabled;
469
470 self.last_raw_position = raw;
471 }
472
473 fn check_faults(&mut self, view: &impl AxisView) {
478 let sw = RawStatusWord(view.status_word());
479 let state = sw.state();
480
481 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
482 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
483 self.is_error = true;
484 let ec = view.error_code();
485 if ec != 0 {
486 self.error_code = ec as u32;
487 }
488 self.error_message = format!("Drive fault (state: {})", state);
489 self.op = AxisOp::Idle;
491 self.op_started = None;
492 }
493 }
494 }
495
496 fn op_timed_out(&self) -> bool {
501 self.op_started
502 .map_or(false, |t| t.elapsed() > self.op_timeout)
503 }
504
505 fn homing_timed_out(&self) -> bool {
506 self.op_started
507 .map_or(false, |t| t.elapsed() > self.homing_timeout)
508 }
509
510 fn move_start_timed_out(&self) -> bool {
511 self.op_started
512 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
513 }
514
515 fn set_op_error(&mut self, msg: &str) {
516 self.is_error = true;
517 self.error_message = msg.to_string();
518 self.op = AxisOp::Idle;
519 self.op_started = None;
520 self.is_busy = false;
521 self.in_motion = false;
522 log::error!("Axis error: {}", msg);
523 }
524
525 fn complete_op(&mut self) {
526 self.op = AxisOp::Idle;
527 self.op_started = None;
528 }
529
530 fn check_target_limit(&self, target: f64) -> Option<String> {
537 if self.config.enable_max_position_limit && target > self.config.max_position_limit {
538 Some(format!(
539 "Target {:.3} exceeds max software limit {:.3}",
540 target, self.config.max_position_limit
541 ))
542 } else if self.config.enable_min_position_limit && target < self.config.min_position_limit {
543 Some(format!(
544 "Target {:.3} exceeds min software limit {:.3}",
545 target, self.config.min_position_limit
546 ))
547 } else {
548 None
549 }
550 }
551
552 fn check_limits(&mut self, view: &mut impl AxisView) {
561 let sw_max = self.config.enable_max_position_limit
563 && self.position >= self.config.max_position_limit;
564 let sw_min = self.config.enable_min_position_limit
565 && self.position <= self.config.min_position_limit;
566
567 self.at_max_limit = sw_max;
568 self.at_min_limit = sw_min;
569
570 let hw_pos = view.positive_limit_active();
572 let hw_neg = view.negative_limit_active();
573
574 self.at_positive_limit_switch = hw_pos;
575 self.at_negative_limit_switch = hw_neg;
576
577 self.home_sensor = view.home_sensor_active();
579
580 self.prev_positive_limit = hw_pos;
582 self.prev_negative_limit = hw_neg;
583 self.prev_home_sensor = view.home_sensor_active();
584
585 let is_moving = matches!(self.op, AxisOp::Moving(_, _));
587 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
588
589 if !is_moving && !is_soft_homing {
590 return;
591 }
592
593 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
595 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
596
597 let effective_hw_pos = hw_pos && !suppress_pos;
598 let effective_hw_neg = hw_neg && !suppress_neg;
599
600 let effective_sw_max = sw_max && !is_soft_homing;
602 let effective_sw_min = sw_min && !is_soft_homing;
603
604 let positive_blocked = (effective_sw_max || effective_hw_pos) && self.moving_positive;
605 let negative_blocked = (effective_sw_min || effective_hw_neg) && self.moving_negative;
606
607 if positive_blocked || negative_blocked {
608 let mut cw = RawControlWord(view.control_word());
609 cw.set_bit(8, true); view.set_control_word(cw.raw());
611
612 let msg = if effective_hw_pos && self.moving_positive {
613 "Positive limit switch active".to_string()
614 } else if effective_hw_neg && self.moving_negative {
615 "Negative limit switch active".to_string()
616 } else if effective_sw_max && self.moving_positive {
617 format!(
618 "Software position limit: position {:.3} >= max {:.3}",
619 self.position, self.config.max_position_limit
620 )
621 } else {
622 format!(
623 "Software position limit: position {:.3} <= min {:.3}",
624 self.position, self.config.min_position_limit
625 )
626 };
627 self.set_op_error(&msg);
628 }
629 }
630
631 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
636 match self.op.clone() {
637 AxisOp::Idle => {}
638 AxisOp::Enabling(step) => self.tick_enabling(view, step),
639 AxisOp::Disabling(step) => self.tick_disabling(view, step),
640 AxisOp::Moving(kind, step) => self.tick_moving(view, kind, step),
641 AxisOp::Homing(step) => self.tick_homing(view, client, step),
642 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
643 AxisOp::Halting => self.tick_halting(view),
644 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
645 }
646 }
647
648 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
653 match step {
654 1 => {
655 let sw = RawStatusWord(view.status_word());
656 if sw.state() == Cia402State::ReadyToSwitchOn {
657 let mut cw = RawControlWord(view.control_word());
658 cw.cmd_enable_operation();
659 view.set_control_word(cw.raw());
660 self.op = AxisOp::Enabling(2);
661 } else if self.op_timed_out() {
662 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
663 }
664 }
665 2 => {
666 let sw = RawStatusWord(view.status_word());
667 if sw.state() == Cia402State::OperationEnabled {
668 self.home_offset = view.position_actual();
669 log::info!("Axis enabled — home captured at {}", self.home_offset);
670 self.complete_op();
671 } else if self.op_timed_out() {
672 self.set_op_error("Enable timeout: waiting for OperationEnabled");
673 }
674 }
675 _ => self.complete_op(),
676 }
677 }
678
679 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
683 match step {
684 1 => {
685 let sw = RawStatusWord(view.status_word());
686 if sw.state() != Cia402State::OperationEnabled {
687 self.complete_op();
688 } else if self.op_timed_out() {
689 self.set_op_error("Disable timeout: drive still in OperationEnabled");
690 }
691 }
692 _ => self.complete_op(),
693 }
694 }
695
696 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8) {
702 match step {
703 1 => {
704 let sw = RawStatusWord(view.status_word());
706 if sw.raw() & (1 << 12) != 0 {
707 let mut cw = RawControlWord(view.control_word());
709 cw.set_bit(4, false);
710 view.set_control_word(cw.raw());
711 self.op = AxisOp::Moving(kind, 2);
712 } else if self.move_start_timed_out() {
713 self.set_op_error("Move timeout: set-point not acknowledged");
714 }
715 }
716 2 => {
717 let sw = RawStatusWord(view.status_word());
719 if sw.target_reached() {
720 self.complete_op();
721 }
722 }
723 _ => self.complete_op(),
724 }
725 }
726
727 fn tick_homing(
745 &mut self,
746 view: &mut impl AxisView,
747 client: &mut CommandClient,
748 step: u8,
749 ) {
750 match step {
751 0 => {
752 self.homing_sdo_tid = self.sdo.write(
754 client,
755 0x6098,
756 0,
757 json!(self.homing_method),
758 );
759 self.op = AxisOp::Homing(1);
760 }
761 1 => {
762 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
764 SdoResult::Ok(_) => {
765 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
767 self.op = AxisOp::Homing(8);
768 } else {
769 self.op = AxisOp::Homing(2);
770 }
771 }
772 SdoResult::Pending => {
773 if self.homing_timed_out() {
774 self.set_op_error("Homing timeout: SDO write for homing method");
775 }
776 }
777 SdoResult::Err(e) => {
778 self.set_op_error(&format!("Homing SDO error: {}", e));
779 }
780 SdoResult::Timeout => {
781 self.set_op_error("Homing timeout: SDO write timed out");
782 }
783 }
784 }
785 2 => {
786 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
788 self.homing_sdo_tid = self.sdo.write(
789 client,
790 0x6099,
791 1,
792 json!(speed_counts),
793 );
794 self.op = AxisOp::Homing(3);
795 }
796 3 => {
797 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
798 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
799 SdoResult::Pending => {
800 if self.homing_timed_out() {
801 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
802 }
803 }
804 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
805 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
806 }
807 }
808 4 => {
809 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
811 self.homing_sdo_tid = self.sdo.write(
812 client,
813 0x6099,
814 2,
815 json!(speed_counts),
816 );
817 self.op = AxisOp::Homing(5);
818 }
819 5 => {
820 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
821 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
822 SdoResult::Pending => {
823 if self.homing_timed_out() {
824 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
825 }
826 }
827 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
828 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
829 }
830 }
831 6 => {
832 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
834 self.homing_sdo_tid = self.sdo.write(
835 client,
836 0x609A,
837 0,
838 json!(accel_counts),
839 );
840 self.op = AxisOp::Homing(7);
841 }
842 7 => {
843 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
844 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
845 SdoResult::Pending => {
846 if self.homing_timed_out() {
847 self.set_op_error("Homing timeout: SDO write for homing acceleration");
848 }
849 }
850 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
851 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
852 }
853 }
854 8 => {
855 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
857 self.op = AxisOp::Homing(9);
858 }
859 9 => {
860 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
862 self.op = AxisOp::Homing(10);
863 } else if self.homing_timed_out() {
864 self.set_op_error("Homing timeout: mode not confirmed");
865 }
866 }
867 10 => {
868 let mut cw = RawControlWord(view.control_word());
870 cw.set_bit(4, true);
871 view.set_control_word(cw.raw());
872 self.op = AxisOp::Homing(11);
873 }
874 11 => {
875 let sw = view.status_word();
878 let error = sw & (1 << 13) != 0;
879 let attained = sw & (1 << 12) != 0;
880 let reached = sw & (1 << 10) != 0;
881
882 if error {
883 self.set_op_error("Homing error: drive reported homing failure");
884 } else if attained && reached {
885 self.op = AxisOp::Homing(12);
887 } else if self.homing_timed_out() {
888 self.set_op_error("Homing timeout: procedure did not complete");
889 }
890 }
891 12 => {
892 self.home_offset = view.position_actual()
895 - self.config.to_counts(self.config.home_position).round() as i32;
896 let mut cw = RawControlWord(view.control_word());
898 cw.set_bit(4, false);
899 view.set_control_word(cw.raw());
900 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
902 log::info!("Homing complete — home offset: {}", self.home_offset);
903 self.complete_op();
904 }
905 _ => self.complete_op(),
906 }
907 }
908
909 fn configure_soft_homing(&mut self, method: HomingMethod) {
912 match method {
913 HomingMethod::LimitSwitchPosPnp => {
914 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
915 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
916 self.soft_home_direction = 1.0;
917 }
918 HomingMethod::LimitSwitchNegPnp => {
919 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
920 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
921 self.soft_home_direction = -1.0;
922 }
923 HomingMethod::LimitSwitchPosNpn => {
924 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
925 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
926 self.soft_home_direction = 1.0;
927 }
928 HomingMethod::LimitSwitchNegNpn => {
929 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
930 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
931 self.soft_home_direction = -1.0;
932 }
933 HomingMethod::HomeSensorPosPnp => {
934 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
935 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
936 self.soft_home_direction = 1.0;
937 }
938 HomingMethod::HomeSensorNegPnp => {
939 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
940 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
941 self.soft_home_direction = -1.0;
942 }
943 HomingMethod::HomeSensorPosNpn => {
944 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
945 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
946 self.soft_home_direction = 1.0;
947 }
948 HomingMethod::HomeSensorNegNpn => {
949 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
950 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
951 self.soft_home_direction = -1.0;
952 }
953 _ => {} }
955 }
956
957 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
958 if self.check_soft_home_trigger(view) {
960 self.set_op_error("Software homing: sensor already in trigger state");
961 return;
962 }
963
964 self.op = AxisOp::SoftHoming(0);
965 self.op_started = Some(Instant::now());
966 }
967
968 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
969 let raw = match self.soft_home_sensor {
970 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
971 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
972 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
973 };
974 match self.soft_home_sensor_type {
975 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
978 }
979
980 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
996 match step {
997 0 => {
999 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1001
1002 let target = self.config.to_counts(self.soft_home_direction * 999_999.0).round() as i32;
1003 view.set_target_position(target);
1004
1005 let cpu = self.config.counts_per_user();
1006 let vel = (self.config.homing_speed * cpu).round() as u32;
1007 let accel = (self.config.homing_accel * cpu).round() as u32;
1008 let decel = (self.config.homing_decel * cpu).round() as u32;
1009 view.set_profile_velocity(vel);
1010 view.set_profile_acceleration(accel);
1011 view.set_profile_deceleration(decel);
1012
1013 let mut cw = RawControlWord(view.control_word());
1014 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(8, false); view.set_control_word(cw.raw());
1018
1019 self.op = AxisOp::SoftHoming(1);
1020 }
1021 1 => {
1022 if self.check_soft_home_trigger(view) {
1024 self.op = AxisOp::SoftHoming(4);
1025 return;
1026 }
1027 let sw = RawStatusWord(view.status_word());
1028 if sw.raw() & (1 << 12) != 0 {
1029 let mut cw = RawControlWord(view.control_word());
1030 cw.set_bit(4, false);
1031 view.set_control_word(cw.raw());
1032 self.op = AxisOp::SoftHoming(2);
1033 } else if self.homing_timed_out() {
1034 self.set_op_error("Software homing timeout: set-point not acknowledged");
1035 }
1036 }
1037 2 => {
1038 if self.check_soft_home_trigger(view) {
1040 self.op = AxisOp::SoftHoming(4);
1041 return;
1042 }
1043 self.op = AxisOp::SoftHoming(3);
1044 }
1045 3 => {
1046 if self.check_soft_home_trigger(view) {
1048 self.op = AxisOp::SoftHoming(4);
1049 } else if self.homing_timed_out() {
1050 self.set_op_error("Software homing timeout: sensor not detected");
1051 }
1052 }
1053
1054 4 => {
1056 let mut cw = RawControlWord(view.control_word());
1058 cw.set_bit(8, true);
1059 view.set_control_word(cw.raw());
1060 view.set_profile_velocity(0);
1061 self.halt_stable_count = 0;
1062 log::info!("Software homing: sensor triggered at position {}", view.position_actual());
1063 self.op = AxisOp::SoftHoming(5);
1064 }
1065 5 => {
1066 const STABLE_WINDOW: i32 = 1;
1068 const STABLE_TICKS_REQUIRED: u8 = 10;
1069
1070 let mut cw = RawControlWord(view.control_word());
1071 cw.set_bit(8, true); view.set_control_word(cw.raw());
1073
1074 let pos = view.position_actual();
1075 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1076 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1077 } else {
1078 self.halt_stable_count = 0;
1079 }
1080
1081 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1082 let current_pos = view.position_actual();
1084 view.set_target_position(current_pos);
1085 view.set_profile_velocity(0);
1086 cw.set_bit(8, false);
1087 cw.set_bit(4, true);
1088 cw.set_bit(6, false); view.set_control_word(cw.raw());
1090 self.op = AxisOp::SoftHoming(6);
1091 } else if self.homing_timed_out() {
1092 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1093 }
1094 }
1095 6 => {
1096 let sw = RawStatusWord(view.status_word());
1098 if sw.raw() & (1 << 12) != 0 {
1099 let mut cw = RawControlWord(view.control_word());
1100 cw.set_bit(4, false);
1101 view.set_control_word(cw.raw());
1102 log::info!("Software homing: motor stopped, backing off sensor");
1103 self.op = AxisOp::SoftHoming(7);
1104 } else if self.homing_timed_out() {
1105 self.set_op_error("Software homing timeout: cancel not acknowledged");
1106 }
1107 }
1108
1109 7 => {
1111 let target = self.config.to_counts(-self.soft_home_direction * 999_999.0).round() as i32;
1113 view.set_target_position(target);
1114
1115 let cpu = self.config.counts_per_user();
1116 let vel = (self.config.homing_speed * cpu).round() as u32;
1117 view.set_profile_velocity(vel);
1118
1119 let mut cw = RawControlWord(view.control_word());
1120 cw.set_bit(4, true);
1121 cw.set_bit(6, true); cw.set_bit(8, false);
1123 view.set_control_word(cw.raw());
1124 self.op = AxisOp::SoftHoming(8);
1125 }
1126 8 => {
1127 let sw = RawStatusWord(view.status_word());
1129 if sw.raw() & (1 << 12) != 0 {
1130 let mut cw = RawControlWord(view.control_word());
1131 cw.set_bit(4, false);
1132 view.set_control_word(cw.raw());
1133 self.op = AxisOp::SoftHoming(9);
1134 } else if self.homing_timed_out() {
1135 self.set_op_error("Software homing timeout: back-off not acknowledged");
1136 }
1137 }
1138 9 => {
1139 if !self.check_soft_home_trigger(view) {
1141 log::info!("Software homing: sensor cleared at position {}", view.position_actual());
1142 self.op = AxisOp::SoftHoming(10);
1143 } else if self.homing_timed_out() {
1144 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1145 }
1146 }
1147 10 => {
1148 let mut cw = RawControlWord(view.control_word());
1150 cw.set_bit(8, true);
1151 view.set_control_word(cw.raw());
1152 view.set_profile_velocity(0);
1153 self.halt_stable_count = 0;
1154 self.op = AxisOp::SoftHoming(11);
1155 }
1156 11 => {
1157 const STABLE_WINDOW: i32 = 1;
1159 const STABLE_TICKS_REQUIRED: u8 = 10;
1160
1161 let mut cw = RawControlWord(view.control_word());
1162 cw.set_bit(8, true);
1163 view.set_control_word(cw.raw());
1164
1165 let pos = view.position_actual();
1166 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1167 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1168 } else {
1169 self.halt_stable_count = 0;
1170 }
1171
1172 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1173 let current_pos = view.position_actual();
1174 view.set_target_position(current_pos);
1175 view.set_profile_velocity(0);
1176 cw.set_bit(8, false);
1177 cw.set_bit(4, true);
1178 cw.set_bit(6, false);
1179 view.set_control_word(cw.raw());
1180 self.op = AxisOp::SoftHoming(12);
1181 } else if self.homing_timed_out() {
1182 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1183 }
1184 }
1185
1186 12 => {
1188 let sw = RawStatusWord(view.status_word());
1190 if sw.raw() & (1 << 12) != 0 {
1191 let mut cw = RawControlWord(view.control_word());
1192 cw.set_bit(4, false);
1193 view.set_control_word(cw.raw());
1194
1195 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1197 let current_pos = view.position_actual();
1198 let offset = desired_counts - current_pos;
1199 self.homing_sdo_tid = self.sdo.write(
1200 client, 0x607C, 0, json!(offset),
1201 );
1202 log::info!("Software homing: writing home offset SDO 0x607C = {}", offset);
1203 self.op = AxisOp::SoftHoming(13);
1204 } else if self.homing_timed_out() {
1205 self.set_op_error("Software homing timeout: back-off cancel not acknowledged");
1206 }
1207 }
1208 13 => {
1209 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1211 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(14); }
1212 SdoResult::Pending => {
1213 if self.homing_timed_out() {
1214 self.set_op_error("Software homing timeout: home offset SDO write");
1215 }
1216 }
1217 SdoResult::Err(e) => {
1218 self.set_op_error(&format!("Software homing SDO error: {}", e));
1219 }
1220 SdoResult::Timeout => {
1221 self.set_op_error("Software homing: home offset SDO timed out");
1222 }
1223 }
1224 }
1225 14 => {
1226 self.homing_sdo_tid = self.sdo.write(
1228 client, 0x6098, 0, json!(37i8),
1229 );
1230 self.op = AxisOp::SoftHoming(15);
1231 }
1232 15 => {
1233 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1235 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(16); }
1236 SdoResult::Pending => {
1237 if self.homing_timed_out() {
1238 self.set_op_error("Software homing timeout: homing method SDO write");
1239 }
1240 }
1241 SdoResult::Err(e) => {
1242 self.set_op_error(&format!("Software homing SDO error: {}", e));
1243 }
1244 SdoResult::Timeout => {
1245 self.set_op_error("Software homing: homing method SDO timed out");
1246 }
1247 }
1248 }
1249 16 => {
1250 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1252 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
1253 let mut cw = RawControlWord(view.control_word());
1254 cw.set_bit(4, true); view.set_control_word(cw.raw());
1256 self.op = AxisOp::SoftHoming(17);
1257 } else if self.homing_timed_out() {
1258 self.set_op_error("Software homing timeout: homing mode not confirmed");
1259 }
1260 }
1261 17 => {
1262 let sw = view.status_word();
1264 let error = sw & (1 << 13) != 0;
1265 let attained = sw & (1 << 12) != 0;
1266 let reached = sw & (1 << 10) != 0;
1267
1268 if error {
1269 self.set_op_error("Software homing: drive reported homing error");
1270 } else if attained && reached {
1271 let mut cw = RawControlWord(view.control_word());
1273 cw.set_bit(4, false);
1274 view.set_control_word(cw.raw());
1275 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1276 self.home_offset = 0; self.op = AxisOp::SoftHoming(18);
1278 } else if self.homing_timed_out() {
1279 self.set_op_error("Software homing timeout: drive homing did not complete");
1280 }
1281 }
1282 18 => {
1283 let current_pos = view.position_actual();
1285 view.set_target_position(current_pos);
1286 view.set_profile_velocity(0);
1287 let mut cw = RawControlWord(view.control_word());
1288 cw.set_bit(4, true);
1289 cw.set_bit(6, false); view.set_control_word(cw.raw());
1291 self.op = AxisOp::SoftHoming(19);
1292 }
1293 19 => {
1294 let sw = RawStatusWord(view.status_word());
1296 if sw.raw() & (1 << 12) != 0 {
1297 let mut cw = RawControlWord(view.control_word());
1298 cw.set_bit(4, false);
1299 view.set_control_word(cw.raw());
1300 log::info!("Software homing complete — position set to {} user units",
1301 self.config.home_position);
1302 self.complete_op();
1303 } else if self.homing_timed_out() {
1304 self.set_op_error("Software homing timeout: hold position not acknowledged");
1305 }
1306 }
1307 _ => self.complete_op(),
1308 }
1309 }
1310
1311 fn tick_halting(&mut self, _view: &mut impl AxisView) {
1313 self.complete_op();
1316 }
1317
1318 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1323 match step {
1324 1 => {
1325 let mut cw = RawControlWord(view.control_word());
1327 cw.cmd_fault_reset();
1328 view.set_control_word(cw.raw());
1329 self.op = AxisOp::FaultRecovery(2);
1330 }
1331 2 => {
1332 let sw = RawStatusWord(view.status_word());
1334 let state = sw.state();
1335 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1336 log::info!("Fault cleared (drive state: {})", state);
1337 self.complete_op();
1338 } else if self.op_timed_out() {
1339 self.set_op_error("Fault reset timeout: drive still faulted");
1340 }
1341 }
1342 _ => self.complete_op(),
1343 }
1344 }
1345}
1346
1347#[cfg(test)]
1352mod tests {
1353 use super::*;
1354
1355 struct MockView {
1357 control_word: u16,
1358 status_word: u16,
1359 target_position: i32,
1360 profile_velocity: u32,
1361 profile_acceleration: u32,
1362 profile_deceleration: u32,
1363 modes_of_operation: i8,
1364 modes_of_operation_display: i8,
1365 position_actual: i32,
1366 velocity_actual: i32,
1367 error_code: u16,
1368 positive_limit: bool,
1369 negative_limit: bool,
1370 home_sensor: bool,
1371 }
1372
1373 impl MockView {
1374 fn new() -> Self {
1375 Self {
1376 control_word: 0,
1377 status_word: 0x0040, target_position: 0,
1379 profile_velocity: 0,
1380 profile_acceleration: 0,
1381 profile_deceleration: 0,
1382 modes_of_operation: 0,
1383 modes_of_operation_display: 1, position_actual: 0,
1385 velocity_actual: 0,
1386 error_code: 0,
1387 positive_limit: false,
1388 negative_limit: false,
1389 home_sensor: false,
1390 }
1391 }
1392
1393 fn set_state(&mut self, state: u16) {
1394 self.status_word = state;
1395 }
1396 }
1397
1398 impl AxisView for MockView {
1399 fn control_word(&self) -> u16 { self.control_word }
1400 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1401 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1402 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1403 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1404 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1405 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1406 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1407 fn status_word(&self) -> u16 { self.status_word }
1408 fn position_actual(&self) -> i32 { self.position_actual }
1409 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1410 fn error_code(&self) -> u16 { self.error_code }
1411 fn positive_limit_active(&self) -> bool { self.positive_limit }
1412 fn negative_limit_active(&self) -> bool { self.negative_limit }
1413 fn home_sensor_active(&self) -> bool { self.home_sensor }
1414 }
1415
1416 fn test_config() -> AxisConfig {
1417 AxisConfig::new(12_800).with_user_scale(360.0)
1418 }
1419
1420 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1422 use tokio::sync::mpsc;
1423 let (write_tx, write_rx) = mpsc::unbounded_channel();
1424 let (response_tx, response_rx) = mpsc::unbounded_channel();
1425 let client = CommandClient::new(write_tx, response_rx);
1426 let axis = Axis::new(test_config(), "TestDrive");
1427 (axis, client, response_tx, write_rx)
1428 }
1429
1430 #[test]
1431 fn axis_config_conversion() {
1432 let cfg = test_config();
1433 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
1435 }
1436
1437 #[test]
1438 fn enable_sequence_sets_pp_mode_and_shutdown() {
1439 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1440 let mut view = MockView::new();
1441
1442 axis.enable(&mut view);
1443
1444 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
1446 assert_eq!(view.control_word & 0x008F, 0x0006);
1448 assert_eq!(axis.op, AxisOp::Enabling(1));
1450
1451 view.set_state(0x0021); axis.tick(&mut view, &mut client);
1454
1455 assert_eq!(view.control_word & 0x008F, 0x000F);
1457 assert_eq!(axis.op, AxisOp::Enabling(2));
1458
1459 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1462
1463 assert_eq!(axis.op, AxisOp::Idle);
1465 assert!(axis.motor_on);
1466 }
1467
1468 #[test]
1469 fn move_absolute_sets_target() {
1470 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1471 let mut view = MockView::new();
1472 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1477
1478 assert_eq!(view.target_position, 1600);
1480 assert_eq!(view.profile_velocity, 3200);
1482 assert_eq!(view.profile_acceleration, 6400);
1484 assert_eq!(view.profile_deceleration, 6400);
1485 assert!(view.control_word & (1 << 4) != 0);
1487 assert!(view.control_word & (1 << 6) == 0);
1489 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1491 }
1492
1493 #[test]
1494 fn move_relative_sets_relative_bit() {
1495 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1496 let mut view = MockView::new();
1497 view.set_state(0x0027);
1498 axis.tick(&mut view, &mut client);
1499
1500 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
1501
1502 assert!(view.control_word & (1 << 6) != 0);
1504 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1)));
1505 }
1506
1507 #[test]
1508 fn move_completes_on_target_reached() {
1509 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1510 let mut view = MockView::new();
1511 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1513
1514 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1515
1516 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1519 assert!(view.control_word & (1 << 4) == 0);
1521
1522 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1525 assert_eq!(axis.op, AxisOp::Idle);
1527 assert!(!axis.in_motion);
1528 }
1529
1530 #[test]
1531 fn fault_detected_sets_error() {
1532 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1533 let mut view = MockView::new();
1534 view.set_state(0x0008); view.error_code = 0x1234;
1536
1537 axis.tick(&mut view, &mut client);
1538
1539 assert!(axis.is_error);
1540 assert_eq!(axis.error_code, 0x1234);
1541 assert!(axis.error_message.contains("fault"));
1542 }
1543
1544 #[test]
1545 fn fault_recovery_sequence() {
1546 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1547 let mut view = MockView::new();
1548 view.set_state(0x0008); axis.reset_faults(&mut view);
1551 assert!(view.control_word & 0x0080 == 0);
1553
1554 axis.tick(&mut view, &mut client);
1556 assert!(view.control_word & 0x0080 != 0);
1557
1558 view.set_state(0x0040);
1560 axis.tick(&mut view, &mut client);
1561 assert_eq!(axis.op, AxisOp::Idle);
1562 assert!(!axis.is_error);
1563 }
1564
1565 #[test]
1566 fn disable_sequence() {
1567 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1568 let mut view = MockView::new();
1569 view.set_state(0x0027); axis.disable(&mut view);
1572 assert_eq!(view.control_word & 0x008F, 0x0007);
1574
1575 view.set_state(0x0023); axis.tick(&mut view, &mut client);
1578 assert_eq!(axis.op, AxisOp::Idle);
1579 }
1580
1581 #[test]
1582 fn position_tracks_with_home_offset() {
1583 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1584 let mut view = MockView::new();
1585 view.set_state(0x0027);
1586 view.position_actual = 5000;
1587
1588 axis.enable(&mut view);
1590 view.set_state(0x0021);
1591 axis.tick(&mut view, &mut client);
1592 view.set_state(0x0027);
1593 axis.tick(&mut view, &mut client);
1594
1595 assert_eq!(axis.home_offset, 5000);
1597
1598 assert!((axis.position - 0.0).abs() < 0.01);
1600
1601 view.position_actual = 6600;
1603 axis.tick(&mut view, &mut client);
1604
1605 assert!((axis.position - 45.0).abs() < 0.1);
1607 }
1608
1609 #[test]
1610 fn set_position_adjusts_home_offset() {
1611 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1612 let mut view = MockView::new();
1613 view.position_actual = 3200;
1614
1615 axis.set_position(&view, 90.0);
1616 axis.tick(&mut view, &mut client);
1617
1618 assert_eq!(axis.home_offset, 0);
1620 assert!((axis.position - 90.0).abs() < 0.01);
1621 }
1622
1623 #[test]
1624 fn halt_sets_bit_and_goes_idle() {
1625 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1626 let mut view = MockView::new();
1627 view.set_state(0x0027);
1628
1629 axis.halt(&mut view);
1630 assert!(view.control_word & (1 << 8) != 0);
1632
1633 axis.tick(&mut view, &mut client);
1635 assert_eq!(axis.op, AxisOp::Idle);
1636 }
1637
1638 #[test]
1639 fn is_busy_tracks_operations() {
1640 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1641 let mut view = MockView::new();
1642
1643 axis.tick(&mut view, &mut client);
1645 assert!(!axis.is_busy);
1646
1647 axis.enable(&mut view);
1649 axis.tick(&mut view, &mut client);
1650 assert!(axis.is_busy);
1651
1652 view.set_state(0x0021);
1654 axis.tick(&mut view, &mut client);
1655 view.set_state(0x0027);
1656 axis.tick(&mut view, &mut client);
1657 assert!(!axis.is_busy);
1658
1659 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1661 axis.tick(&mut view, &mut client);
1662 assert!(axis.is_busy);
1663 assert!(axis.in_motion);
1664 }
1665
1666 #[test]
1667 fn fault_during_move_cancels_op() {
1668 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1669 let mut view = MockView::new();
1670 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1672
1673 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1675 axis.tick(&mut view, &mut client);
1676 assert!(axis.is_busy);
1677 assert!(!axis.is_error);
1678
1679 view.set_state(0x0008); axis.tick(&mut view, &mut client);
1682
1683 assert!(!axis.is_busy);
1685 assert!(axis.is_error);
1686 assert_eq!(axis.op, AxisOp::Idle);
1687 }
1688
1689 #[test]
1690 fn move_absolute_rejected_by_max_limit() {
1691 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1692 let mut view = MockView::new();
1693 view.set_state(0x0027);
1694 axis.tick(&mut view, &mut client);
1695
1696 axis.set_software_max_limit(90.0);
1697 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
1698
1699 assert!(axis.is_error);
1701 assert_eq!(axis.op, AxisOp::Idle);
1702 assert!(axis.error_message.contains("max software limit"));
1703 }
1704
1705 #[test]
1706 fn move_absolute_rejected_by_min_limit() {
1707 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1708 let mut view = MockView::new();
1709 view.set_state(0x0027);
1710 axis.tick(&mut view, &mut client);
1711
1712 axis.set_software_min_limit(-10.0);
1713 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
1714
1715 assert!(axis.is_error);
1716 assert_eq!(axis.op, AxisOp::Idle);
1717 assert!(axis.error_message.contains("min software limit"));
1718 }
1719
1720 #[test]
1721 fn move_relative_rejected_by_max_limit() {
1722 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1723 let mut view = MockView::new();
1724 view.set_state(0x0027);
1725 axis.tick(&mut view, &mut client);
1726
1727 axis.set_software_max_limit(50.0);
1729 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
1730
1731 assert!(axis.is_error);
1732 assert_eq!(axis.op, AxisOp::Idle);
1733 assert!(axis.error_message.contains("max software limit"));
1734 }
1735
1736 #[test]
1737 fn move_within_limits_allowed() {
1738 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1739 let mut view = MockView::new();
1740 view.set_state(0x0027);
1741 axis.tick(&mut view, &mut client);
1742
1743 axis.set_software_max_limit(90.0);
1744 axis.set_software_min_limit(-90.0);
1745 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1746
1747 assert!(!axis.is_error);
1749 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1750 }
1751
1752 #[test]
1753 fn runtime_limit_halts_move_in_violated_direction() {
1754 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1755 let mut view = MockView::new();
1756 view.set_state(0x0027);
1757 axis.tick(&mut view, &mut client);
1758
1759 axis.set_software_max_limit(45.0);
1760 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1762
1763 view.position_actual = 1650;
1766 view.velocity_actual = 100; view.status_word = 0x1027;
1770 axis.tick(&mut view, &mut client);
1771 view.status_word = 0x0027;
1772 axis.tick(&mut view, &mut client);
1773
1774 assert!(axis.is_error);
1776 assert!(axis.at_max_limit);
1777 assert_eq!(axis.op, AxisOp::Idle);
1778 assert!(axis.error_message.contains("Software position limit"));
1779 assert!(view.control_word & (1 << 8) != 0);
1781 }
1782
1783 #[test]
1784 fn runtime_limit_allows_move_in_opposite_direction() {
1785 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1786 let mut view = MockView::new();
1787 view.set_state(0x0027);
1788 view.position_actual = 1778; axis.set_software_max_limit(45.0);
1791 axis.tick(&mut view, &mut client);
1792 assert!(axis.at_max_limit);
1793
1794 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
1796 assert!(!axis.is_error);
1797 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1798
1799 view.velocity_actual = -100;
1801 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1803 assert!(!axis.is_error);
1805 }
1806
1807 #[test]
1808 fn positive_limit_switch_halts_positive_move() {
1809 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1810 let mut view = MockView::new();
1811 view.set_state(0x0027);
1812 axis.tick(&mut view, &mut client);
1813
1814 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1816 view.velocity_actual = 100; view.status_word = 0x1027;
1819 axis.tick(&mut view, &mut client);
1820 view.status_word = 0x0027;
1821
1822 view.positive_limit = true;
1824 axis.tick(&mut view, &mut client);
1825
1826 assert!(axis.is_error);
1827 assert!(axis.at_positive_limit_switch);
1828 assert!(!axis.is_busy);
1829 assert!(axis.error_message.contains("Positive limit switch"));
1830 assert!(view.control_word & (1 << 8) != 0);
1832 }
1833
1834 #[test]
1835 fn negative_limit_switch_halts_negative_move() {
1836 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1837 let mut view = MockView::new();
1838 view.set_state(0x0027);
1839 axis.tick(&mut view, &mut client);
1840
1841 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
1843 view.velocity_actual = -100; view.status_word = 0x1027;
1845 axis.tick(&mut view, &mut client);
1846 view.status_word = 0x0027;
1847
1848 view.negative_limit = true;
1850 axis.tick(&mut view, &mut client);
1851
1852 assert!(axis.is_error);
1853 assert!(axis.at_negative_limit_switch);
1854 assert!(axis.error_message.contains("Negative limit switch"));
1855 }
1856
1857 #[test]
1858 fn limit_switch_allows_move_in_opposite_direction() {
1859 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1860 let mut view = MockView::new();
1861 view.set_state(0x0027);
1862 view.positive_limit = true;
1864 view.velocity_actual = -100;
1865 axis.tick(&mut view, &mut client);
1866 assert!(axis.at_positive_limit_switch);
1867
1868 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
1870 view.status_word = 0x1027;
1871 axis.tick(&mut view, &mut client);
1872
1873 assert!(!axis.is_error);
1875 assert!(matches!(axis.op, AxisOp::Moving(_, _)));
1876 }
1877
1878 #[test]
1879 fn limit_switch_ignored_when_not_moving() {
1880 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1881 let mut view = MockView::new();
1882 view.set_state(0x0027);
1883 view.positive_limit = true;
1884
1885 axis.tick(&mut view, &mut client);
1886
1887 assert!(axis.at_positive_limit_switch);
1889 assert!(!axis.is_error);
1890 }
1891
1892 #[test]
1893 fn home_sensor_output_tracks_view() {
1894 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1895 let mut view = MockView::new();
1896 view.set_state(0x0027);
1897
1898 axis.tick(&mut view, &mut client);
1899 assert!(!axis.home_sensor);
1900
1901 view.home_sensor = true;
1902 axis.tick(&mut view, &mut client);
1903 assert!(axis.home_sensor);
1904
1905 view.home_sensor = false;
1906 axis.tick(&mut view, &mut client);
1907 assert!(!axis.home_sensor);
1908 }
1909
1910 #[test]
1911 fn velocity_output_converted() {
1912 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1913 let mut view = MockView::new();
1914 view.set_state(0x0027);
1915 view.velocity_actual = 3200;
1917
1918 axis.tick(&mut view, &mut client);
1919
1920 assert!((axis.speed - 90.0).abs() < 0.1);
1921 assert!(axis.moving_positive);
1922 assert!(!axis.moving_negative);
1923 }
1924
1925 fn soft_homing_config() -> AxisConfig {
1928 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
1929 cfg.homing_speed = 10.0;
1930 cfg.homing_accel = 20.0;
1931 cfg.homing_decel = 20.0;
1932 cfg
1933 }
1934
1935 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1936 use tokio::sync::mpsc;
1937 let (write_tx, write_rx) = mpsc::unbounded_channel();
1938 let (response_tx, response_rx) = mpsc::unbounded_channel();
1939 let client = CommandClient::new(write_tx, response_rx);
1940 let axis = Axis::new(soft_homing_config(), "TestDrive");
1941 (axis, client, response_tx, write_rx)
1942 }
1943
1944 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
1946 view.set_state(0x0027); axis.tick(view, client);
1948 }
1949
1950 fn complete_soft_homing(
1955 axis: &mut Axis,
1956 view: &mut MockView,
1957 client: &mut CommandClient,
1958 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
1959 trigger_pos: i32,
1960 clear_sensor: impl FnOnce(&mut MockView),
1961 ) {
1962 use mechutil::ipc::CommandMessage as IpcMsg;
1963
1964 axis.tick(view, client);
1967 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
1968
1969 view.position_actual = trigger_pos + 100;
1971 axis.tick(view, client);
1972 view.position_actual = trigger_pos + 120;
1973 axis.tick(view, client);
1974 for _ in 0..10 { axis.tick(view, client); }
1976 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
1977
1978 view.status_word = 0x1027;
1980 axis.tick(view, client);
1981 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
1982 view.status_word = 0x0027;
1983
1984 axis.tick(view, client);
1987 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
1988
1989 view.status_word = 0x1027;
1991 axis.tick(view, client);
1992 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
1993 view.status_word = 0x0027;
1994
1995 axis.tick(view, client);
1997 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
1998 clear_sensor(view);
1999 view.position_actual = trigger_pos - 200;
2000 axis.tick(view, client);
2001 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2002
2003 axis.tick(view, client);
2005 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2006 for _ in 0..10 { axis.tick(view, client); }
2007 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2008
2009 view.status_word = 0x1027;
2012 axis.tick(view, client);
2013 view.status_word = 0x0027;
2014 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2015
2016 let tid = axis.homing_sdo_tid;
2018 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2019 client.poll();
2020 axis.tick(view, client);
2021 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2022
2023 axis.tick(view, client);
2025 let tid = axis.homing_sdo_tid;
2026 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2027 client.poll();
2028 axis.tick(view, client);
2029 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2030
2031 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2033 axis.tick(view, client);
2034 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2035
2036 view.status_word = 0x1427; axis.tick(view, client);
2039 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2040 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2041 view.status_word = 0x0027;
2042
2043 axis.tick(view, client);
2045 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2046
2047 view.status_word = 0x1027;
2049 axis.tick(view, client);
2050 view.status_word = 0x0027;
2051
2052 assert_eq!(axis.op, AxisOp::Idle);
2053 assert!(!axis.is_busy);
2054 assert!(!axis.is_error);
2055 assert_eq!(axis.home_offset, 0); }
2057
2058 #[test]
2059 fn soft_homing_pnp_home_sensor_full_sequence() {
2060 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2061 let mut view = MockView::new();
2062 enable_axis(&mut axis, &mut view, &mut client);
2063
2064 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2065
2066 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2069 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2071 axis.tick(&mut view, &mut client); view.home_sensor = true;
2075 view.position_actual = 5000;
2076 axis.tick(&mut view, &mut client);
2077 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2078
2079 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2080 |v| { v.home_sensor = false; });
2081 }
2082
2083 #[test]
2084 fn soft_homing_npn_home_sensor_full_sequence() {
2085 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2086 let mut view = MockView::new();
2087 view.home_sensor = true;
2089 enable_axis(&mut axis, &mut view, &mut client);
2090
2091 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2092
2093 axis.tick(&mut view, &mut client);
2095 view.status_word = 0x1027;
2096 axis.tick(&mut view, &mut client);
2097 view.status_word = 0x0027;
2098 axis.tick(&mut view, &mut client);
2099
2100 view.home_sensor = false;
2102 view.position_actual = 3000;
2103 axis.tick(&mut view, &mut client);
2104 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2105
2106 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2107 |v| { v.home_sensor = true; }); }
2109
2110 #[test]
2111 fn soft_homing_limit_switch_suppresses_halt() {
2112 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2113 let mut view = MockView::new();
2114 enable_axis(&mut axis, &mut view, &mut client);
2115
2116 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
2118
2119 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2124 axis.tick(&mut view, &mut client); view.positive_limit = true;
2128 view.velocity_actual = 100; view.position_actual = 8000;
2130 axis.tick(&mut view, &mut client);
2131
2132 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2134 assert!(!axis.is_error);
2135 }
2136
2137 #[test]
2138 fn soft_homing_opposite_limit_still_protects() {
2139 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2140 let mut view = MockView::new();
2141 enable_axis(&mut axis, &mut view, &mut client);
2142
2143 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2145
2146 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2151 axis.tick(&mut view, &mut client); view.negative_limit = true;
2156 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
2158
2159 assert!(axis.is_error);
2161 assert!(axis.error_message.contains("Negative limit switch"));
2162 }
2163
2164 #[test]
2165 fn soft_homing_sensor_already_active_rejects() {
2166 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2167 let mut view = MockView::new();
2168 enable_axis(&mut axis, &mut view, &mut client);
2169
2170 view.home_sensor = true;
2172 axis.tick(&mut view, &mut client); axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2175
2176 assert!(axis.is_error);
2178 assert!(axis.error_message.contains("already in trigger state"));
2179 assert_eq!(axis.op, AxisOp::Idle);
2180 }
2181
2182 #[test]
2183 fn soft_homing_negative_direction_sets_negative_target() {
2184 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2185 let mut view = MockView::new();
2186 enable_axis(&mut axis, &mut view, &mut client);
2187
2188 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
2189 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
2193 }
2194
2195 #[test]
2196 fn home_integrated_method_starts_hardware_homing() {
2197 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2198 let mut view = MockView::new();
2199 enable_axis(&mut axis, &mut view, &mut client);
2200
2201 axis.home(&mut view, HomingMethod::CurrentPosition);
2202 assert!(matches!(axis.op, AxisOp::Homing(0)));
2203 assert_eq!(axis.homing_method, 37);
2204 }
2205
2206 #[test]
2207 fn home_integrated_arbitrary_code() {
2208 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2209 let mut view = MockView::new();
2210 enable_axis(&mut axis, &mut view, &mut client);
2211
2212 axis.home(&mut view, HomingMethod::Integrated(35));
2213 assert!(matches!(axis.op, AxisOp::Homing(0)));
2214 assert_eq!(axis.homing_method, 35);
2215 }
2216
2217 #[test]
2218 fn hardware_homing_skips_speed_sdos_when_zero() {
2219 use mechutil::ipc::CommandMessage;
2220
2221 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
2222 let mut view = MockView::new();
2223 enable_axis(&mut axis, &mut view, &mut client);
2224
2225 axis.home(&mut view, HomingMethod::Integrated(37));
2227
2228 axis.tick(&mut view, &mut client);
2230 assert!(matches!(axis.op, AxisOp::Homing(1)));
2231
2232 let _ = write_rx.try_recv();
2234
2235 let tid = axis.homing_sdo_tid;
2237 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2238 client.poll();
2239 axis.tick(&mut view, &mut client);
2240
2241 assert!(matches!(axis.op, AxisOp::Homing(8)));
2243 }
2244
2245 #[test]
2246 fn hardware_homing_writes_speed_sdos_when_nonzero() {
2247 use mechutil::ipc::CommandMessage;
2248
2249 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
2250 let mut view = MockView::new();
2251 enable_axis(&mut axis, &mut view, &mut client);
2252
2253 axis.home(&mut view, HomingMethod::Integrated(37));
2255
2256 axis.tick(&mut view, &mut client);
2258 assert!(matches!(axis.op, AxisOp::Homing(1)));
2259 let _ = write_rx.try_recv();
2260
2261 let tid = axis.homing_sdo_tid;
2263 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2264 client.poll();
2265 axis.tick(&mut view, &mut client);
2266 assert!(matches!(axis.op, AxisOp::Homing(2)));
2268 }
2269
2270 #[test]
2271 fn soft_homing_edge_during_ack_step() {
2272 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2273 let mut view = MockView::new();
2274 enable_axis(&mut axis, &mut view, &mut client);
2275
2276 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2277 axis.tick(&mut view, &mut client); view.home_sensor = true;
2281 view.position_actual = 2000;
2282 axis.tick(&mut view, &mut client);
2283
2284 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2286 }
2287
2288 #[test]
2289 fn soft_homing_applies_home_position() {
2290 let mut cfg = soft_homing_config();
2291 cfg.home_position = 90.0;
2292
2293 use tokio::sync::mpsc;
2294 let (write_tx, _write_rx) = mpsc::unbounded_channel();
2295 let (resp_tx, response_rx) = mpsc::unbounded_channel();
2296 let mut client = CommandClient::new(write_tx, response_rx);
2297 let mut axis = Axis::new(cfg, "TestDrive");
2298
2299 let mut view = MockView::new();
2300 enable_axis(&mut axis, &mut view, &mut client);
2301
2302 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2303
2304 axis.tick(&mut view, &mut client);
2306 view.status_word = 0x1027;
2307 axis.tick(&mut view, &mut client);
2308 view.status_word = 0x0027;
2309 axis.tick(&mut view, &mut client);
2310
2311 view.home_sensor = true;
2313 view.position_actual = 5000;
2314 axis.tick(&mut view, &mut client);
2315 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2316
2317 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2319 |v| { v.home_sensor = false; });
2320
2321 assert_eq!(axis.home_offset, 0);
2323 }
2324
2325 #[test]
2326 fn soft_homing_default_home_position_zero() {
2327 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2328 let mut view = MockView::new();
2329 enable_axis(&mut axis, &mut view, &mut client);
2330
2331 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2332
2333 axis.tick(&mut view, &mut client);
2335 view.status_word = 0x1027;
2336 axis.tick(&mut view, &mut client);
2337 view.status_word = 0x0027;
2338 axis.tick(&mut view, &mut client);
2339
2340 view.home_sensor = true;
2342 view.position_actual = 5000;
2343 axis.tick(&mut view, &mut client);
2344
2345 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2346 |v| { v.home_sensor = false; });
2347
2348 assert_eq!(axis.home_offset, 0);
2349 }
2350}