1use std::time::{Duration, Instant};
25
26use serde_json::json;
27use strum_macros::FromRepr;
28
29use crate::command_client::CommandClient;
30use crate::ethercat::{SdoClient, SdoResult};
31use crate::fb::Ton;
32use super::axis_config::AxisConfig;
33use super::axis_view::AxisView;
34use super::homing::HomingMethod;
35use super::cia402::{
36 Cia402Control, Cia402Status, Cia402State,
37 ModesOfOperation, RawControlWord, RawStatusWord,
38};
39
40#[derive(Debug, Clone, PartialEq)]
45enum AxisOp {
46 Idle,
47 Enabling(u8),
48 Disabling(u8),
49 Moving(MoveKind, u8),
50 Homing(u8),
51 SoftHoming(u8),
52 Halting,
53 FaultRecovery(u8),
54}
55
56#[repr(u8)]
57#[derive(Debug, Clone, PartialEq, FromRepr)]
58enum HomeState {
59 Search = 0,
60 WaitSearching = 10,
61 WaitFoundSensor = 20,
62 WaitStoppedFoundSensor = 30,
63 WaitFoundSensorAck = 40,
64 WaitFoundSensorAckClear = 45,
65 DebounceFoundSensor = 50,
66 BackOff = 60,
67 WaitBackingOff = 70,
68 WaitLostSensor = 80,
69 WaitStoppedLostSensor = 90,
70 WaitLostSensorAck = 100,
71 WaitLostSensorAckClear = 120,
72 WaitHomeOffsetDone = 200,
73 WriteHomingMethod = 205,
74 WaitWriteHomingMethodDone = 210,
75 ExecHomingMode = 215,
76 WaitHomingDone = 220,
77 SendCurrentPositionTarget = 225,
78 WaitCurrentPositionTargetSent = 230
79
80}
81
82#[derive(Debug, Clone, PartialEq)]
83enum MoveKind {
84 Absolute,
85 Relative,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq)]
89enum SoftHomeSensor {
90 PositiveLimit,
91 NegativeLimit,
92 HomeSensor,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq)]
96enum SoftHomeSensorType {
97 Pnp,
99 Npn,
101}
102
103pub struct Axis {
113 config: AxisConfig,
114 sdo: SdoClient,
115
116 op: AxisOp,
118 home_offset: i32,
119 last_raw_position: i32,
120 op_started: Option<Instant>,
121 op_timeout: Duration,
122 homing_timeout: Duration,
123 move_start_timeout: Duration,
124 pending_move_target: i32,
125 pending_move_vel: u32,
126 pending_move_accel: u32,
127 pending_move_decel: u32,
128 homing_method: i8,
129 homing_sdo_tid: u32,
130 soft_home_sensor: SoftHomeSensor,
131 soft_home_sensor_type: SoftHomeSensorType,
132 soft_home_direction: f64,
133 halt_stable_count: u8,
134 prev_positive_limit: bool,
135 prev_negative_limit: bool,
136 prev_home_sensor: bool,
137
138 pub is_error: bool,
142 pub error_code: u32,
144 pub error_message: String,
146 pub motor_on: bool,
148 pub is_busy: bool,
154 pub in_motion: bool,
156 pub moving_positive: bool,
158 pub moving_negative: bool,
160 pub position: f64,
162 pub raw_position: i64,
164 pub speed: f64,
166 pub at_max_limit: bool,
168 pub at_min_limit: bool,
170 pub at_positive_limit_switch: bool,
172 pub at_negative_limit_switch: bool,
174 pub home_sensor: bool,
176
177
178 ton : Ton
180}
181
182impl Axis {
183 pub fn new(config: AxisConfig, device_name: &str) -> Self {
188 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
189 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
190 let move_start_timeout = op_timeout; Self {
192 config,
193 sdo: SdoClient::new(device_name),
194 op: AxisOp::Idle,
195 home_offset: 0,
196 last_raw_position: 0,
197 op_started: None,
198 op_timeout,
199 homing_timeout,
200 move_start_timeout,
201 pending_move_target: 0,
202 pending_move_vel: 0,
203 pending_move_accel: 0,
204 pending_move_decel: 0,
205 homing_method: 37,
206 homing_sdo_tid: 0,
207 soft_home_sensor: SoftHomeSensor::HomeSensor,
208 soft_home_sensor_type: SoftHomeSensorType::Pnp,
209 soft_home_direction: 1.0,
210 halt_stable_count: 0,
211 prev_positive_limit: false,
212 prev_negative_limit: false,
213 prev_home_sensor: false,
214 is_error: false,
215 error_code: 0,
216 error_message: String::new(),
217 motor_on: false,
218 is_busy: false,
219 in_motion: false,
220 moving_positive: false,
221 moving_negative: false,
222 position: 0.0,
223 raw_position: 0,
224 speed: 0.0,
225 at_max_limit: false,
226 at_min_limit: false,
227 at_positive_limit_switch: false,
228 at_negative_limit_switch: false,
229 home_sensor: false,
230 ton: Ton::new()
231 }
232 }
233
234 pub fn config(&self) -> &AxisConfig {
236 &self.config
237 }
238
239 pub fn move_absolute(
249 &mut self,
250 view: &mut impl AxisView,
251 target: f64,
252 vel: f64,
253 accel: f64,
254 decel: f64,
255 ) {
256 if let Some(msg) = self.check_target_limit(target) {
257 self.set_op_error(&msg);
258 return;
259 }
260
261 let cpu = self.config.counts_per_user();
262 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
263 let raw_vel = (vel * cpu).round() as u32;
264 let raw_accel = (accel * cpu).round() as u32;
265 let raw_decel = (decel * cpu).round() as u32;
266
267 self.start_move(view, raw_target, raw_vel, raw_accel, raw_decel, MoveKind::Absolute);
268 }
269
270 pub fn move_relative(
276 &mut self,
277 view: &mut impl AxisView,
278 distance: f64,
279 vel: f64,
280 accel: f64,
281 decel: f64,
282 ) {
283 if let Some(msg) = self.check_target_limit(self.position + distance) {
284 self.set_op_error(&msg);
285 return;
286 }
287
288 let cpu = self.config.counts_per_user();
289 let raw_distance = self.config.to_counts(distance).round() as i32;
290 let raw_vel = (vel * cpu).round() as u32;
291 let raw_accel = (accel * cpu).round() as u32;
292 let raw_decel = (decel * cpu).round() as u32;
293
294 self.start_move(view, raw_distance, raw_vel, raw_accel, raw_decel, MoveKind::Relative);
295 }
296
297 fn start_move(
298 &mut self,
299 view: &mut impl AxisView,
300 raw_target: i32,
301 raw_vel: u32,
302 raw_accel: u32,
303 raw_decel: u32,
304 kind: MoveKind,
305 ) {
306 self.pending_move_target = raw_target;
307 self.pending_move_vel = raw_vel;
308 self.pending_move_accel = raw_accel;
309 self.pending_move_decel = raw_decel;
310
311 view.set_target_position(raw_target);
313 view.set_profile_velocity(raw_vel);
314 view.set_profile_acceleration(raw_accel);
315 view.set_profile_deceleration(raw_decel);
316
317 let mut cw = RawControlWord(view.control_word());
319 cw.set_bit(6, kind == MoveKind::Relative);
320 cw.set_bit(4, true); view.set_control_word(cw.raw());
322
323 self.op = AxisOp::Moving(kind, 1);
324 self.op_started = Some(Instant::now());
325 }
326
327 pub fn halt(&mut self, view: &mut impl AxisView) {
329 let mut cw = RawControlWord(view.control_word());
330 cw.set_bit(8, true); view.set_control_word(cw.raw());
332 self.op = AxisOp::Halting;
333 }
334
335 pub fn enable(&mut self, view: &mut impl AxisView) {
343 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
345 let mut cw = RawControlWord(view.control_word());
346 cw.cmd_shutdown();
347 view.set_control_word(cw.raw());
348
349 self.op = AxisOp::Enabling(1);
350 self.op_started = Some(Instant::now());
351 }
352
353 pub fn disable(&mut self, view: &mut impl AxisView) {
355 let mut cw = RawControlWord(view.control_word());
356 cw.cmd_disable_operation();
357 view.set_control_word(cw.raw());
358
359 self.op = AxisOp::Disabling(1);
360 self.op_started = Some(Instant::now());
361 }
362
363 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
367 let mut cw = RawControlWord(view.control_word());
369 cw.cmd_clear_fault_reset();
370 view.set_control_word(cw.raw());
371
372 self.is_error = false;
373 self.error_code = 0;
374 self.error_message.clear();
375 self.op = AxisOp::FaultRecovery(1);
376 self.op_started = Some(Instant::now());
377 }
378
379 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
387 if method.is_integrated() {
388 self.homing_method = method.cia402_code();
389 self.op = AxisOp::Homing(0);
390 self.op_started = Some(Instant::now());
391 let _ = view;
392 } else {
393 self.configure_soft_homing(method);
394 self.start_soft_homing(view);
395 }
396 }
397
398 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
407 self.home_offset = view.position_actual() - self.config.to_counts(user_units).round() as i32;
408 }
409
410 pub fn set_home_position(&mut self, user_units: f64) {
414 self.config.home_position = user_units;
415 }
416
417 pub fn set_software_max_limit(&mut self, user_units: f64) {
419 self.config.max_position_limit = user_units;
420 self.config.enable_max_position_limit = true;
421 }
422
423 pub fn set_software_min_limit(&mut self, user_units: f64) {
425 self.config.min_position_limit = user_units;
426 self.config.enable_min_position_limit = true;
427 }
428
429 pub fn sdo_write(
435 &mut self,
436 client: &mut CommandClient,
437 index: u16,
438 sub_index: u8,
439 value: serde_json::Value,
440 ) {
441 self.sdo.write(client, index, sub_index, value);
442 }
443
444 pub fn sdo_read(
446 &mut self,
447 client: &mut CommandClient,
448 index: u16,
449 sub_index: u8,
450 ) -> u32 {
451 self.sdo.read(client, index, sub_index)
452 }
453
454 pub fn sdo_result(
456 &mut self,
457 client: &mut CommandClient,
458 tid: u32,
459 ) -> SdoResult {
460 self.sdo.result(client, tid, Duration::from_secs(5))
461 }
462
463 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
477 self.check_faults(view);
478 self.progress_op(view, client);
479 self.update_outputs(view);
480 self.check_limits(view);
481 }
482
483 fn update_outputs(&mut self, view: &impl AxisView) {
488 let raw = view.position_actual();
489 self.raw_position = raw as i64;
490 self.position = self.config.to_user((raw - self.home_offset) as f64);
491
492 let vel = view.velocity_actual();
493 let user_vel = self.config.to_user(vel as f64);
494 self.speed = user_vel.abs();
495 self.moving_positive = user_vel > 0.0;
496 self.moving_negative = user_vel < 0.0;
497 self.is_busy = self.op != AxisOp::Idle;
498 self.in_motion = matches!(self.op, AxisOp::Moving(_, _) | AxisOp::SoftHoming(_));
499
500 let sw = RawStatusWord(view.status_word());
501 self.motor_on = sw.state() == Cia402State::OperationEnabled;
502
503 self.last_raw_position = raw;
504 }
505
506 fn check_faults(&mut self, view: &impl AxisView) {
511 let sw = RawStatusWord(view.status_word());
512 let state = sw.state();
513
514 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
515 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
516 self.is_error = true;
517 let ec = view.error_code();
518 if ec != 0 {
519 self.error_code = ec as u32;
520 }
521 self.error_message = format!("Drive fault (state: {})", state);
522 self.op = AxisOp::Idle;
524 self.op_started = None;
525 }
526 }
527 }
528
529 fn op_timed_out(&self) -> bool {
534 self.op_started
535 .map_or(false, |t| t.elapsed() > self.op_timeout)
536 }
537
538 fn homing_timed_out(&self) -> bool {
539 self.op_started
540 .map_or(false, |t| t.elapsed() > self.homing_timeout)
541 }
542
543 fn move_start_timed_out(&self) -> bool {
544 self.op_started
545 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
546 }
547
548 fn set_op_error(&mut self, msg: &str) {
549 self.is_error = true;
550 self.error_message = msg.to_string();
551 self.op = AxisOp::Idle;
552 self.op_started = None;
553 self.is_busy = false;
554 self.in_motion = false;
555 log::error!("Axis error: {}", msg);
556 }
557
558 fn complete_op(&mut self) {
559 self.op = AxisOp::Idle;
560 self.op_started = None;
561 }
562
563 fn check_target_limit(&self, target: f64) -> Option<String> {
570 if self.config.enable_max_position_limit && target > self.config.max_position_limit {
571 Some(format!(
572 "Target {:.3} exceeds max software limit {:.3}",
573 target, self.config.max_position_limit
574 ))
575 } else if self.config.enable_min_position_limit && target < self.config.min_position_limit {
576 Some(format!(
577 "Target {:.3} exceeds min software limit {:.3}",
578 target, self.config.min_position_limit
579 ))
580 } else {
581 None
582 }
583 }
584
585 fn check_limits(&mut self, view: &mut impl AxisView) {
594 let sw_max = self.config.enable_max_position_limit
596 && self.position >= self.config.max_position_limit;
597 let sw_min = self.config.enable_min_position_limit
598 && self.position <= self.config.min_position_limit;
599
600 self.at_max_limit = sw_max;
601 self.at_min_limit = sw_min;
602
603 let hw_pos = view.positive_limit_active();
605 let hw_neg = view.negative_limit_active();
606
607 self.at_positive_limit_switch = hw_pos;
608 self.at_negative_limit_switch = hw_neg;
609
610 self.home_sensor = view.home_sensor_active();
612
613 self.prev_positive_limit = hw_pos;
615 self.prev_negative_limit = hw_neg;
616 self.prev_home_sensor = view.home_sensor_active();
617
618 let is_moving = matches!(self.op, AxisOp::Moving(_, _));
620 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
621
622 if !is_moving && !is_soft_homing {
623 return;
624 }
625
626 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
628 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
629
630 let effective_hw_pos = hw_pos && !suppress_pos;
631 let effective_hw_neg = hw_neg && !suppress_neg;
632
633 let effective_sw_max = sw_max && !is_soft_homing;
635 let effective_sw_min = sw_min && !is_soft_homing;
636
637 let positive_blocked = (effective_sw_max || effective_hw_pos) && self.moving_positive;
638 let negative_blocked = (effective_sw_min || effective_hw_neg) && self.moving_negative;
639
640 if positive_blocked || negative_blocked {
641 let mut cw = RawControlWord(view.control_word());
642 cw.set_bit(8, true); view.set_control_word(cw.raw());
644
645 let msg = if effective_hw_pos && self.moving_positive {
646 "Positive limit switch active".to_string()
647 } else if effective_hw_neg && self.moving_negative {
648 "Negative limit switch active".to_string()
649 } else if effective_sw_max && self.moving_positive {
650 format!(
651 "Software position limit: position {:.3} >= max {:.3}",
652 self.position, self.config.max_position_limit
653 )
654 } else {
655 format!(
656 "Software position limit: position {:.3} <= min {:.3}",
657 self.position, self.config.min_position_limit
658 )
659 };
660 self.set_op_error(&msg);
661 }
662 }
663
664 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
669 match self.op.clone() {
670 AxisOp::Idle => {}
671 AxisOp::Enabling(step) => self.tick_enabling(view, step),
672 AxisOp::Disabling(step) => self.tick_disabling(view, step),
673 AxisOp::Moving(kind, step) => self.tick_moving(view, kind, step),
674 AxisOp::Homing(step) => self.tick_homing(view, client, step),
675 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
676 AxisOp::Halting => self.tick_halting(view),
677 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
678 }
679 }
680
681 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
686 match step {
687 1 => {
688 let sw = RawStatusWord(view.status_word());
689 if sw.state() == Cia402State::ReadyToSwitchOn {
690 let mut cw = RawControlWord(view.control_word());
691 cw.cmd_enable_operation();
692 view.set_control_word(cw.raw());
693 self.op = AxisOp::Enabling(2);
694 } else if self.op_timed_out() {
695 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
696 }
697 }
698 2 => {
699 let sw = RawStatusWord(view.status_word());
700 if sw.state() == Cia402State::OperationEnabled {
701 self.home_offset = view.position_actual();
702 log::info!("Axis enabled — home captured at {}", self.home_offset);
703 self.complete_op();
704 } else if self.op_timed_out() {
705 self.set_op_error("Enable timeout: waiting for OperationEnabled");
706 }
707 }
708 _ => self.complete_op(),
709 }
710 }
711
712 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
716 match step {
717 1 => {
718 let sw = RawStatusWord(view.status_word());
719 if sw.state() != Cia402State::OperationEnabled {
720 self.complete_op();
721 } else if self.op_timed_out() {
722 self.set_op_error("Disable timeout: drive still in OperationEnabled");
723 }
724 }
725 _ => self.complete_op(),
726 }
727 }
728
729 fn tick_moving(&mut self, view: &mut impl AxisView, kind: MoveKind, step: u8) {
735 match step {
736 1 => {
737 let sw = RawStatusWord(view.status_word());
739 if sw.raw() & (1 << 12) != 0 {
740 let mut cw = RawControlWord(view.control_word());
742 cw.set_bit(4, false);
743 view.set_control_word(cw.raw());
744 self.op = AxisOp::Moving(kind, 2);
745 } else if self.move_start_timed_out() {
746 self.set_op_error("Move timeout: set-point not acknowledged");
747 }
748 }
749 2 => {
750 let sw = RawStatusWord(view.status_word());
752 if sw.target_reached() {
753 self.complete_op();
754 }
755 }
756 _ => self.complete_op(),
757 }
758 }
759
760 fn tick_homing(
778 &mut self,
779 view: &mut impl AxisView,
780 client: &mut CommandClient,
781 step: u8,
782 ) {
783 match step {
784 0 => {
785 self.homing_sdo_tid = self.sdo.write(
787 client,
788 0x6098,
789 0,
790 json!(self.homing_method),
791 );
792 self.op = AxisOp::Homing(1);
793 }
794 1 => {
795 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
797 SdoResult::Ok(_) => {
798 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
800 self.op = AxisOp::Homing(8);
801 } else {
802 self.op = AxisOp::Homing(2);
803 }
804 }
805 SdoResult::Pending => {
806 if self.homing_timed_out() {
807 self.set_op_error("Homing timeout: SDO write for homing method");
808 }
809 }
810 SdoResult::Err(e) => {
811 self.set_op_error(&format!("Homing SDO error: {}", e));
812 }
813 SdoResult::Timeout => {
814 self.set_op_error("Homing timeout: SDO write timed out");
815 }
816 }
817 }
818 2 => {
819 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
821 self.homing_sdo_tid = self.sdo.write(
822 client,
823 0x6099,
824 1,
825 json!(speed_counts),
826 );
827 self.op = AxisOp::Homing(3);
828 }
829 3 => {
830 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
831 SdoResult::Ok(_) => { self.op = AxisOp::Homing(4); }
832 SdoResult::Pending => {
833 if self.homing_timed_out() {
834 self.set_op_error("Homing timeout: SDO write for homing speed (switch)");
835 }
836 }
837 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
838 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
839 }
840 }
841 4 => {
842 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
844 self.homing_sdo_tid = self.sdo.write(
845 client,
846 0x6099,
847 2,
848 json!(speed_counts),
849 );
850 self.op = AxisOp::Homing(5);
851 }
852 5 => {
853 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
854 SdoResult::Ok(_) => { self.op = AxisOp::Homing(6); }
855 SdoResult::Pending => {
856 if self.homing_timed_out() {
857 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
858 }
859 }
860 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
861 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
862 }
863 }
864 6 => {
865 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
867 self.homing_sdo_tid = self.sdo.write(
868 client,
869 0x609A,
870 0,
871 json!(accel_counts),
872 );
873 self.op = AxisOp::Homing(7);
874 }
875 7 => {
876 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
877 SdoResult::Ok(_) => { self.op = AxisOp::Homing(8); }
878 SdoResult::Pending => {
879 if self.homing_timed_out() {
880 self.set_op_error("Homing timeout: SDO write for homing acceleration");
881 }
882 }
883 SdoResult::Err(e) => { self.set_op_error(&format!("Homing SDO error: {}", e)); }
884 SdoResult::Timeout => { self.set_op_error("Homing timeout: SDO write timed out"); }
885 }
886 }
887 8 => {
888 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
890 self.op = AxisOp::Homing(9);
891 }
892 9 => {
893 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
895 self.op = AxisOp::Homing(10);
896 } else if self.homing_timed_out() {
897 self.set_op_error("Homing timeout: mode not confirmed");
898 }
899 }
900 10 => {
901 let mut cw = RawControlWord(view.control_word());
903 cw.set_bit(4, true);
904 view.set_control_word(cw.raw());
905 self.op = AxisOp::Homing(11);
906 }
907 11 => {
908 let sw = view.status_word();
911 let error = sw & (1 << 13) != 0;
912 let attained = sw & (1 << 12) != 0;
913 let reached = sw & (1 << 10) != 0;
914
915 if error {
916 self.set_op_error("Homing error: drive reported homing failure");
917 } else if attained && reached {
918 self.op = AxisOp::Homing(12);
920 } else if self.homing_timed_out() {
921 self.set_op_error("Homing timeout: procedure did not complete");
922 }
923 }
924 12 => {
925 self.home_offset = view.position_actual()
928 - self.config.to_counts(self.config.home_position).round() as i32;
929 let mut cw = RawControlWord(view.control_word());
931 cw.set_bit(4, false);
932 view.set_control_word(cw.raw());
933 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
935 log::info!("Homing complete — home offset: {}", self.home_offset);
936 self.complete_op();
937 }
938 _ => self.complete_op(),
939 }
940 }
941
942 fn configure_soft_homing(&mut self, method: HomingMethod) {
945 match method {
946 HomingMethod::LimitSwitchPosPnp => {
947 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
948 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
949 self.soft_home_direction = 1.0;
950 }
951 HomingMethod::LimitSwitchNegPnp => {
952 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
953 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
954 self.soft_home_direction = -1.0;
955 }
956 HomingMethod::LimitSwitchPosNpn => {
957 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
958 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
959 self.soft_home_direction = 1.0;
960 }
961 HomingMethod::LimitSwitchNegNpn => {
962 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
963 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
964 self.soft_home_direction = -1.0;
965 }
966 HomingMethod::HomeSensorPosPnp => {
967 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
968 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
969 self.soft_home_direction = 1.0;
970 }
971 HomingMethod::HomeSensorNegPnp => {
972 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
973 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
974 self.soft_home_direction = -1.0;
975 }
976 HomingMethod::HomeSensorPosNpn => {
977 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
978 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
979 self.soft_home_direction = 1.0;
980 }
981 HomingMethod::HomeSensorNegNpn => {
982 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
983 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
984 self.soft_home_direction = -1.0;
985 }
986 _ => {} }
988 }
989
990 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
991 if !self.check_soft_home_trigger(view) {
995 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
996 } else {
997 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
998 }
999
1000
1001 self.op_started = Some(Instant::now());
1002 }
1003
1004 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1005 let raw = match self.soft_home_sensor {
1006 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1007 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1008 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1009 };
1010 match self.soft_home_sensor_type {
1011 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1014 }
1015
1016
1017 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1020 let dir = if !self.config.invert_direction {
1021 direction
1022 }
1023 else {
1024 -direction
1025 };
1026
1027 let target = if dir > 0.0 {
1028 i32::MAX
1029 }
1030 else {
1031 i32::MIN
1032 };
1033
1034 return target;
1035 }
1036
1037
1038 fn command_halt(&self, view: &mut impl AxisView) {
1043 let mut cw = RawControlWord(view.control_word());
1044 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1047 }
1048
1049
1050 fn command_cancel_move(&self, view: &mut impl AxisView) {
1058
1059 let mut cw = RawControlWord(view.control_word());
1060 cw.set_bit(4, true); cw.set_bit(5, true); cw.set_bit(6, false); cw.set_bit(8, true); view.set_control_word(cw.raw());
1065
1066 let current_pos = view.position_actual();
1067 view.set_target_position(current_pos);
1068 view.set_profile_velocity(0);
1069 }
1070
1071
1072 fn command_homing_speed(&self, view: &mut impl AxisView) {
1074 let cpu = self.config.counts_per_user();
1075 let vel = (self.config.homing_speed * cpu).round() as u32;
1076 let accel = (self.config.homing_accel * cpu).round() as u32;
1077 let decel = (self.config.homing_decel * cpu).round() as u32;
1078 view.set_profile_velocity(vel);
1079 view.set_profile_acceleration(accel);
1080 view.set_profile_deceleration(decel);
1081 }
1082
1083 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1099 match HomeState::from_repr(step) {
1100 Some(HomeState::Search) => {
1102 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1103
1104 let target = self.calculate_max_relative_target(self.soft_home_direction);
1114 view.set_target_position(target);
1115
1116 self.command_homing_speed(view);
1125
1126 let mut cw = RawControlWord(view.control_word());
1127 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(8, false); cw.set_bit(13, true); view.set_control_word(cw.raw());
1132
1133 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1134 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1135 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1136 }
1137 Some(HomeState::WaitSearching) => {
1138 if self.check_soft_home_trigger(view) {
1139 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1140 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1141 return;
1142 }
1143 let sw = RawStatusWord(view.status_word());
1144 if sw.raw() & (1 << 12) != 0 {
1145 let mut cw = RawControlWord(view.control_word());
1146 cw.set_bit(4, false);
1147 view.set_control_word(cw.raw());
1148 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1149 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1150 } else if self.homing_timed_out() {
1151 self.set_op_error("Software homing timeout: set-point not acknowledged");
1152 }
1153 }
1154 Some(HomeState::WaitFoundSensor) => {
1164 if self.check_soft_home_trigger(view) {
1165 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1166 log::info!("ControlWord is : {} ", view.control_word());
1167
1168 let mut cw = RawControlWord(view.control_word());
1169 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1172
1173
1174 self.halt_stable_count = 0;
1175 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1176 } else if self.homing_timed_out() {
1177 self.set_op_error("Software homing timeout: sensor not detected");
1178 }
1179 }
1180
1181
1182 Some(HomeState::WaitStoppedFoundSensor) => {
1183 const STABLE_WINDOW: i32 = 1;
1184 const STABLE_TICKS_REQUIRED: u8 = 10;
1185
1186 let pos = view.position_actual();
1191 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1192 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1193 } else {
1194 self.halt_stable_count = 0;
1195 }
1196
1197 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1198
1199 log::debug!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1200 self.command_cancel_move(view);
1201 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1202
1203 } else if self.homing_timed_out() {
1204 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1205 }
1206 }
1207 Some(HomeState::WaitFoundSensorAck) => {
1208 let sw = RawStatusWord(view.status_word());
1209 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1210
1211 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1212
1213 let mut cw = RawControlWord(view.control_word());
1215 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1218
1219 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1220
1221 } else if self.homing_timed_out() {
1222 self.set_op_error("Software homing timeout: cancel not acknowledged");
1223 }
1224 },
1225 Some(HomeState::WaitFoundSensorAckClear) => {
1226 let sw = RawStatusWord(view.status_word());
1227 if sw.raw() & (1 << 12) == 0 {
1229
1230 let mut cw = RawControlWord(view.control_word());
1232 cw.set_bit(8, false);
1233 view.set_control_word(cw.raw());
1234
1235 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1236 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1237 self.ton.call(false, Duration::from_secs(3));
1238 }
1239 },
1240 Some(HomeState::DebounceFoundSensor) => {
1242 self.ton.call(true, Duration::from_secs(3));
1243
1244 let sw = RawStatusWord(view.status_word());
1245 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1246 self.ton.call(false, Duration::from_secs(3));
1247 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1248 view.position_actual(), view.control_word(), view.status_word());
1249 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1250 }
1251 }
1252
1253 Some(HomeState::BackOff) => {
1255
1256 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1257 view.set_target_position(target);
1258
1259
1260 self.command_homing_speed(view);
1261
1262 let mut cw = RawControlWord(view.control_word());
1263 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1267 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1268 target, self.config.homing_speed, view.position_actual(), cw.raw());
1269 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1270 }
1271 Some(HomeState::WaitBackingOff) => {
1272 let sw = RawStatusWord(view.status_word());
1273 if sw.raw() & (1 << 12) != 0 {
1274 let mut cw = RawControlWord(view.control_word());
1275 cw.set_bit(4, false);
1276 view.set_control_word(cw.raw());
1277 log::debug!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1278 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1279 } else if self.homing_timed_out() {
1280 self.set_op_error("Software homing timeout: back-off not acknowledged");
1281 }
1282 }
1283 Some(HomeState::WaitLostSensor) => {
1284 if !self.check_soft_home_trigger(view) {
1285 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1286
1287 self.command_halt(view);
1288 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1289 } else if self.homing_timed_out() {
1290 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1291 }
1292 }
1293 Some(HomeState::WaitStoppedLostSensor) => {
1294 const STABLE_WINDOW: i32 = 1;
1295 const STABLE_TICKS_REQUIRED: u8 = 10;
1296
1297 let mut cw = RawControlWord(view.control_word());
1298 cw.set_bit(8, true);
1299 view.set_control_word(cw.raw());
1300
1301 let pos = view.position_actual();
1302 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1303 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1304 } else {
1305 self.halt_stable_count = 0;
1306 }
1307
1308 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1309 log::debug!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1310 self.command_cancel_move(view);
1311 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1312 } else if self.homing_timed_out() {
1313 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1314 }
1315 }
1316 Some(HomeState::WaitLostSensorAck) => {
1317 let sw = RawStatusWord(view.status_word());
1318 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1319
1320 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1321
1322 let mut cw = RawControlWord(view.control_word());
1324 cw.set_bit(4, false); view.set_control_word(cw.raw());
1326
1327 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1328
1329
1330 } else if self.homing_timed_out() {
1331 self.set_op_error("Software homing timeout: cancel not acknowledged");
1332 }
1333 }
1334 Some(HomeState::WaitLostSensorAckClear) => {
1335 let sw = RawStatusWord(view.status_word());
1337 if sw.raw() & (1 << 12) == 0 {
1338
1339 let mut cw = RawControlWord(view.control_word());
1341 cw.set_bit(8, false);
1342 view.set_control_word(cw.raw());
1343
1344
1345 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1346 self.homing_sdo_tid = self.sdo.write(
1349 client, 0x607C, 0, json!(desired_counts),
1350 );
1351
1352 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {}.",
1353 desired_counts
1354 );
1355
1356 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1357
1358 }
1359 },
1360 Some(HomeState::WaitHomeOffsetDone) => {
1361 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1363 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8); }
1364 SdoResult::Pending => {
1365 if self.homing_timed_out() {
1366 self.set_op_error("Software homing timeout: home offset SDO write");
1367 }
1368 }
1369 SdoResult::Err(e) => {
1370 self.set_op_error(&format!("Software homing SDO error: {}", e));
1371 }
1372 SdoResult::Timeout => {
1373 self.set_op_error("Software homing: home offset SDO timed out");
1374 }
1375 }
1376 }
1377 Some(HomeState::WriteHomingMethod) => {
1378 self.homing_sdo_tid = self.sdo.write(
1380 client, 0x6098, 0, json!(37i8),
1381 );
1382 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1383 }
1384 Some(HomeState::WaitWriteHomingMethodDone) => {
1385 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1387 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::ExecHomingMode as u8); }
1388 SdoResult::Pending => {
1389 if self.homing_timed_out() {
1390 self.set_op_error("Software homing timeout: homing method SDO write");
1391 }
1392 }
1393 SdoResult::Err(e) => {
1394 self.set_op_error(&format!("Software homing SDO error: {}", e));
1395 }
1396 SdoResult::Timeout => {
1397 self.set_op_error("Software homing: homing method SDO timed out");
1398 }
1399 }
1400 }
1401 Some(HomeState::ExecHomingMode) => {
1402 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1404 let display = view.modes_of_operation_display();
1405 if display == ModesOfOperation::Homing.as_i8() {
1406 log::info!("SoftHome[ExecHomingMode]: homing mode confirmed, triggering homing");
1407 let mut cw = RawControlWord(view.control_word());
1408 cw.set_bit(4, true); view.set_control_word(cw.raw());
1410 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1411 } else if self.homing_timed_out() {
1412 self.set_op_error(&format!("Software homing timeout: homing mode not confirmed (display={})", display));
1413 }
1414 }
1415 Some(HomeState::WaitHomingDone) => {
1416 let sw = view.status_word();
1418 let error = sw & (1 << 13) != 0;
1419 let attained = sw & (1 << 12) != 0;
1420 let reached = sw & (1 << 10) != 0;
1421
1422 if error {
1423 self.set_op_error("Software homing: drive reported homing error");
1424 } else if attained && reached {
1425 log::info!("SoftHome[WaitHomingDone]: homing complete (sw=0x{:04X}), switching to PP", sw);
1426 let mut cw = RawControlWord(view.control_word());
1428 cw.set_bit(4, false);
1429 view.set_control_word(cw.raw());
1430 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1431 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1433 } else if self.homing_timed_out() {
1434 self.set_op_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
1435 }
1436 }
1437 Some(HomeState::SendCurrentPositionTarget) => {
1438 let current_pos = view.position_actual();
1440 view.set_target_position(current_pos);
1441 view.set_profile_velocity(0);
1442 let mut cw = RawControlWord(view.control_word());
1443 cw.set_bit(4, true);
1444 cw.set_bit(5, true);
1445 cw.set_bit(6, false); view.set_control_word(cw.raw());
1447 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1448 }
1449 Some(HomeState::WaitCurrentPositionTargetSent) => {
1450 let sw = RawStatusWord(view.status_word());
1452 if sw.raw() & (1 << 12) != 0 {
1453 let mut cw = RawControlWord(view.control_word());
1454 cw.set_bit(4, false);
1455 view.set_control_word(cw.raw());
1456 log::info!("Software homing complete — position set to {} user units",
1457 self.config.home_position);
1458 self.complete_op();
1459 } else if self.homing_timed_out() {
1460 self.set_op_error("Software homing timeout: hold position not acknowledged");
1461 }
1462 }
1463 _ => self.complete_op(),
1464 }
1465 }
1466
1467 fn tick_halting(&mut self, _view: &mut impl AxisView) {
1469 self.complete_op();
1472 }
1473
1474 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1479 match step {
1480 1 => {
1481 let mut cw = RawControlWord(view.control_word());
1483 cw.cmd_fault_reset();
1484 view.set_control_word(cw.raw());
1485 self.op = AxisOp::FaultRecovery(2);
1486 }
1487 2 => {
1488 let sw = RawStatusWord(view.status_word());
1490 let state = sw.state();
1491 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1492 log::info!("Fault cleared (drive state: {})", state);
1493 self.complete_op();
1494 } else if self.op_timed_out() {
1495 self.set_op_error("Fault reset timeout: drive still faulted");
1496 }
1497 }
1498 _ => self.complete_op(),
1499 }
1500 }
1501}
1502
1503#[cfg(test)]
1508mod tests {
1509 use super::*;
1510
1511 struct MockView {
1513 control_word: u16,
1514 status_word: u16,
1515 target_position: i32,
1516 profile_velocity: u32,
1517 profile_acceleration: u32,
1518 profile_deceleration: u32,
1519 modes_of_operation: i8,
1520 modes_of_operation_display: i8,
1521 position_actual: i32,
1522 velocity_actual: i32,
1523 error_code: u16,
1524 positive_limit: bool,
1525 negative_limit: bool,
1526 home_sensor: bool,
1527 }
1528
1529 impl MockView {
1530 fn new() -> Self {
1531 Self {
1532 control_word: 0,
1533 status_word: 0x0040, target_position: 0,
1535 profile_velocity: 0,
1536 profile_acceleration: 0,
1537 profile_deceleration: 0,
1538 modes_of_operation: 0,
1539 modes_of_operation_display: 1, position_actual: 0,
1541 velocity_actual: 0,
1542 error_code: 0,
1543 positive_limit: false,
1544 negative_limit: false,
1545 home_sensor: false,
1546 }
1547 }
1548
1549 fn set_state(&mut self, state: u16) {
1550 self.status_word = state;
1551 }
1552 }
1553
1554 impl AxisView for MockView {
1555 fn control_word(&self) -> u16 { self.control_word }
1556 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1557 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1558 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1559 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1560 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1561 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1562 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1563 fn status_word(&self) -> u16 { self.status_word }
1564 fn position_actual(&self) -> i32 { self.position_actual }
1565 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1566 fn error_code(&self) -> u16 { self.error_code }
1567 fn positive_limit_active(&self) -> bool { self.positive_limit }
1568 fn negative_limit_active(&self) -> bool { self.negative_limit }
1569 fn home_sensor_active(&self) -> bool { self.home_sensor }
1570 }
1571
1572 fn test_config() -> AxisConfig {
1573 AxisConfig::new(12_800).with_user_scale(360.0)
1574 }
1575
1576 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1578 use tokio::sync::mpsc;
1579 let (write_tx, write_rx) = mpsc::unbounded_channel();
1580 let (response_tx, response_rx) = mpsc::unbounded_channel();
1581 let client = CommandClient::new(write_tx, response_rx);
1582 let axis = Axis::new(test_config(), "TestDrive");
1583 (axis, client, response_tx, write_rx)
1584 }
1585
1586 #[test]
1587 fn axis_config_conversion() {
1588 let cfg = test_config();
1589 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
1591 }
1592
1593 #[test]
1594 fn enable_sequence_sets_pp_mode_and_shutdown() {
1595 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1596 let mut view = MockView::new();
1597
1598 axis.enable(&mut view);
1599
1600 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
1602 assert_eq!(view.control_word & 0x008F, 0x0006);
1604 assert_eq!(axis.op, AxisOp::Enabling(1));
1606
1607 view.set_state(0x0021); axis.tick(&mut view, &mut client);
1610
1611 assert_eq!(view.control_word & 0x008F, 0x000F);
1613 assert_eq!(axis.op, AxisOp::Enabling(2));
1614
1615 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1618
1619 assert_eq!(axis.op, AxisOp::Idle);
1621 assert!(axis.motor_on);
1622 }
1623
1624 #[test]
1625 fn move_absolute_sets_target() {
1626 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1627 let mut view = MockView::new();
1628 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1633
1634 assert_eq!(view.target_position, 1600);
1636 assert_eq!(view.profile_velocity, 3200);
1638 assert_eq!(view.profile_acceleration, 6400);
1640 assert_eq!(view.profile_deceleration, 6400);
1641 assert!(view.control_word & (1 << 4) != 0);
1643 assert!(view.control_word & (1 << 6) == 0);
1645 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1647 }
1648
1649 #[test]
1650 fn move_relative_sets_relative_bit() {
1651 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1652 let mut view = MockView::new();
1653 view.set_state(0x0027);
1654 axis.tick(&mut view, &mut client);
1655
1656 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
1657
1658 assert!(view.control_word & (1 << 6) != 0);
1660 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1)));
1661 }
1662
1663 #[test]
1664 fn move_completes_on_target_reached() {
1665 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1666 let mut view = MockView::new();
1667 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1669
1670 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1671
1672 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1675 assert!(view.control_word & (1 << 4) == 0);
1677
1678 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1681 assert_eq!(axis.op, AxisOp::Idle);
1683 assert!(!axis.in_motion);
1684 }
1685
1686 #[test]
1687 fn fault_detected_sets_error() {
1688 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1689 let mut view = MockView::new();
1690 view.set_state(0x0008); view.error_code = 0x1234;
1692
1693 axis.tick(&mut view, &mut client);
1694
1695 assert!(axis.is_error);
1696 assert_eq!(axis.error_code, 0x1234);
1697 assert!(axis.error_message.contains("fault"));
1698 }
1699
1700 #[test]
1701 fn fault_recovery_sequence() {
1702 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1703 let mut view = MockView::new();
1704 view.set_state(0x0008); axis.reset_faults(&mut view);
1707 assert!(view.control_word & 0x0080 == 0);
1709
1710 axis.tick(&mut view, &mut client);
1712 assert!(view.control_word & 0x0080 != 0);
1713
1714 view.set_state(0x0040);
1716 axis.tick(&mut view, &mut client);
1717 assert_eq!(axis.op, AxisOp::Idle);
1718 assert!(!axis.is_error);
1719 }
1720
1721 #[test]
1722 fn disable_sequence() {
1723 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1724 let mut view = MockView::new();
1725 view.set_state(0x0027); axis.disable(&mut view);
1728 assert_eq!(view.control_word & 0x008F, 0x0007);
1730
1731 view.set_state(0x0023); axis.tick(&mut view, &mut client);
1734 assert_eq!(axis.op, AxisOp::Idle);
1735 }
1736
1737 #[test]
1738 fn position_tracks_with_home_offset() {
1739 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1740 let mut view = MockView::new();
1741 view.set_state(0x0027);
1742 view.position_actual = 5000;
1743
1744 axis.enable(&mut view);
1746 view.set_state(0x0021);
1747 axis.tick(&mut view, &mut client);
1748 view.set_state(0x0027);
1749 axis.tick(&mut view, &mut client);
1750
1751 assert_eq!(axis.home_offset, 5000);
1753
1754 assert!((axis.position - 0.0).abs() < 0.01);
1756
1757 view.position_actual = 6600;
1759 axis.tick(&mut view, &mut client);
1760
1761 assert!((axis.position - 45.0).abs() < 0.1);
1763 }
1764
1765 #[test]
1766 fn set_position_adjusts_home_offset() {
1767 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1768 let mut view = MockView::new();
1769 view.position_actual = 3200;
1770
1771 axis.set_position(&view, 90.0);
1772 axis.tick(&mut view, &mut client);
1773
1774 assert_eq!(axis.home_offset, 0);
1776 assert!((axis.position - 90.0).abs() < 0.01);
1777 }
1778
1779 #[test]
1780 fn halt_sets_bit_and_goes_idle() {
1781 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1782 let mut view = MockView::new();
1783 view.set_state(0x0027);
1784
1785 axis.halt(&mut view);
1786 assert!(view.control_word & (1 << 8) != 0);
1788
1789 axis.tick(&mut view, &mut client);
1791 assert_eq!(axis.op, AxisOp::Idle);
1792 }
1793
1794 #[test]
1795 fn is_busy_tracks_operations() {
1796 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1797 let mut view = MockView::new();
1798
1799 axis.tick(&mut view, &mut client);
1801 assert!(!axis.is_busy);
1802
1803 axis.enable(&mut view);
1805 axis.tick(&mut view, &mut client);
1806 assert!(axis.is_busy);
1807
1808 view.set_state(0x0021);
1810 axis.tick(&mut view, &mut client);
1811 view.set_state(0x0027);
1812 axis.tick(&mut view, &mut client);
1813 assert!(!axis.is_busy);
1814
1815 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1817 axis.tick(&mut view, &mut client);
1818 assert!(axis.is_busy);
1819 assert!(axis.in_motion);
1820 }
1821
1822 #[test]
1823 fn fault_during_move_cancels_op() {
1824 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1825 let mut view = MockView::new();
1826 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1828
1829 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1831 axis.tick(&mut view, &mut client);
1832 assert!(axis.is_busy);
1833 assert!(!axis.is_error);
1834
1835 view.set_state(0x0008); axis.tick(&mut view, &mut client);
1838
1839 assert!(!axis.is_busy);
1841 assert!(axis.is_error);
1842 assert_eq!(axis.op, AxisOp::Idle);
1843 }
1844
1845 #[test]
1846 fn move_absolute_rejected_by_max_limit() {
1847 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1848 let mut view = MockView::new();
1849 view.set_state(0x0027);
1850 axis.tick(&mut view, &mut client);
1851
1852 axis.set_software_max_limit(90.0);
1853 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
1854
1855 assert!(axis.is_error);
1857 assert_eq!(axis.op, AxisOp::Idle);
1858 assert!(axis.error_message.contains("max software limit"));
1859 }
1860
1861 #[test]
1862 fn move_absolute_rejected_by_min_limit() {
1863 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1864 let mut view = MockView::new();
1865 view.set_state(0x0027);
1866 axis.tick(&mut view, &mut client);
1867
1868 axis.set_software_min_limit(-10.0);
1869 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
1870
1871 assert!(axis.is_error);
1872 assert_eq!(axis.op, AxisOp::Idle);
1873 assert!(axis.error_message.contains("min software limit"));
1874 }
1875
1876 #[test]
1877 fn move_relative_rejected_by_max_limit() {
1878 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1879 let mut view = MockView::new();
1880 view.set_state(0x0027);
1881 axis.tick(&mut view, &mut client);
1882
1883 axis.set_software_max_limit(50.0);
1885 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
1886
1887 assert!(axis.is_error);
1888 assert_eq!(axis.op, AxisOp::Idle);
1889 assert!(axis.error_message.contains("max software limit"));
1890 }
1891
1892 #[test]
1893 fn move_within_limits_allowed() {
1894 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1895 let mut view = MockView::new();
1896 view.set_state(0x0027);
1897 axis.tick(&mut view, &mut client);
1898
1899 axis.set_software_max_limit(90.0);
1900 axis.set_software_min_limit(-90.0);
1901 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1902
1903 assert!(!axis.is_error);
1905 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1906 }
1907
1908 #[test]
1909 fn runtime_limit_halts_move_in_violated_direction() {
1910 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1911 let mut view = MockView::new();
1912 view.set_state(0x0027);
1913 axis.tick(&mut view, &mut client);
1914
1915 axis.set_software_max_limit(45.0);
1916 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1918
1919 view.position_actual = 1650;
1922 view.velocity_actual = 100; view.status_word = 0x1027;
1926 axis.tick(&mut view, &mut client);
1927 view.status_word = 0x0027;
1928 axis.tick(&mut view, &mut client);
1929
1930 assert!(axis.is_error);
1932 assert!(axis.at_max_limit);
1933 assert_eq!(axis.op, AxisOp::Idle);
1934 assert!(axis.error_message.contains("Software position limit"));
1935 assert!(view.control_word & (1 << 8) != 0);
1937 }
1938
1939 #[test]
1940 fn runtime_limit_allows_move_in_opposite_direction() {
1941 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1942 let mut view = MockView::new();
1943 view.set_state(0x0027);
1944 view.position_actual = 1778; axis.set_software_max_limit(45.0);
1947 axis.tick(&mut view, &mut client);
1948 assert!(axis.at_max_limit);
1949
1950 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
1952 assert!(!axis.is_error);
1953 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1954
1955 view.velocity_actual = -100;
1957 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1959 assert!(!axis.is_error);
1961 }
1962
1963 #[test]
1964 fn positive_limit_switch_halts_positive_move() {
1965 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1966 let mut view = MockView::new();
1967 view.set_state(0x0027);
1968 axis.tick(&mut view, &mut client);
1969
1970 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1972 view.velocity_actual = 100; view.status_word = 0x1027;
1975 axis.tick(&mut view, &mut client);
1976 view.status_word = 0x0027;
1977
1978 view.positive_limit = true;
1980 axis.tick(&mut view, &mut client);
1981
1982 assert!(axis.is_error);
1983 assert!(axis.at_positive_limit_switch);
1984 assert!(!axis.is_busy);
1985 assert!(axis.error_message.contains("Positive limit switch"));
1986 assert!(view.control_word & (1 << 8) != 0);
1988 }
1989
1990 #[test]
1991 fn negative_limit_switch_halts_negative_move() {
1992 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1993 let mut view = MockView::new();
1994 view.set_state(0x0027);
1995 axis.tick(&mut view, &mut client);
1996
1997 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
1999 view.velocity_actual = -100; view.status_word = 0x1027;
2001 axis.tick(&mut view, &mut client);
2002 view.status_word = 0x0027;
2003
2004 view.negative_limit = true;
2006 axis.tick(&mut view, &mut client);
2007
2008 assert!(axis.is_error);
2009 assert!(axis.at_negative_limit_switch);
2010 assert!(axis.error_message.contains("Negative limit switch"));
2011 }
2012
2013 #[test]
2014 fn limit_switch_allows_move_in_opposite_direction() {
2015 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2016 let mut view = MockView::new();
2017 view.set_state(0x0027);
2018 view.positive_limit = true;
2020 view.velocity_actual = -100;
2021 axis.tick(&mut view, &mut client);
2022 assert!(axis.at_positive_limit_switch);
2023
2024 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2026 view.status_word = 0x1027;
2027 axis.tick(&mut view, &mut client);
2028
2029 assert!(!axis.is_error);
2031 assert!(matches!(axis.op, AxisOp::Moving(_, _)));
2032 }
2033
2034 #[test]
2035 fn limit_switch_ignored_when_not_moving() {
2036 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2037 let mut view = MockView::new();
2038 view.set_state(0x0027);
2039 view.positive_limit = true;
2040
2041 axis.tick(&mut view, &mut client);
2042
2043 assert!(axis.at_positive_limit_switch);
2045 assert!(!axis.is_error);
2046 }
2047
2048 #[test]
2049 fn home_sensor_output_tracks_view() {
2050 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2051 let mut view = MockView::new();
2052 view.set_state(0x0027);
2053
2054 axis.tick(&mut view, &mut client);
2055 assert!(!axis.home_sensor);
2056
2057 view.home_sensor = true;
2058 axis.tick(&mut view, &mut client);
2059 assert!(axis.home_sensor);
2060
2061 view.home_sensor = false;
2062 axis.tick(&mut view, &mut client);
2063 assert!(!axis.home_sensor);
2064 }
2065
2066 #[test]
2067 fn velocity_output_converted() {
2068 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2069 let mut view = MockView::new();
2070 view.set_state(0x0027);
2071 view.velocity_actual = 3200;
2073
2074 axis.tick(&mut view, &mut client);
2075
2076 assert!((axis.speed - 90.0).abs() < 0.1);
2077 assert!(axis.moving_positive);
2078 assert!(!axis.moving_negative);
2079 }
2080
2081 fn soft_homing_config() -> AxisConfig {
2084 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2085 cfg.homing_speed = 10.0;
2086 cfg.homing_accel = 20.0;
2087 cfg.homing_decel = 20.0;
2088 cfg
2089 }
2090
2091 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2092 use tokio::sync::mpsc;
2093 let (write_tx, write_rx) = mpsc::unbounded_channel();
2094 let (response_tx, response_rx) = mpsc::unbounded_channel();
2095 let client = CommandClient::new(write_tx, response_rx);
2096 let axis = Axis::new(soft_homing_config(), "TestDrive");
2097 (axis, client, response_tx, write_rx)
2098 }
2099
2100 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2102 view.set_state(0x0027); axis.tick(view, client);
2104 }
2105
2106 fn complete_soft_homing(
2111 axis: &mut Axis,
2112 view: &mut MockView,
2113 client: &mut CommandClient,
2114 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2115 trigger_pos: i32,
2116 clear_sensor: impl FnOnce(&mut MockView),
2117 ) {
2118 use mechutil::ipc::CommandMessage as IpcMsg;
2119
2120 axis.tick(view, client);
2123 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2124
2125 view.position_actual = trigger_pos + 100;
2127 axis.tick(view, client);
2128 view.position_actual = trigger_pos + 120;
2129 axis.tick(view, client);
2130 for _ in 0..10 { axis.tick(view, client); }
2132 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2133
2134 view.status_word = 0x1027;
2136 axis.tick(view, client);
2137 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2138 view.status_word = 0x0027;
2139
2140 for _ in 0..100 { axis.tick(view, client); }
2142 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2143
2144 axis.tick(view, client);
2147 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2148
2149 view.status_word = 0x1027;
2151 axis.tick(view, client);
2152 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2153 view.status_word = 0x0027;
2154
2155 axis.tick(view, client);
2157 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2158 clear_sensor(view);
2159 view.position_actual = trigger_pos - 200;
2160 axis.tick(view, client);
2161 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2162
2163 axis.tick(view, client);
2165 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2166 for _ in 0..10 { axis.tick(view, client); }
2167 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2168
2169 view.status_word = 0x1027;
2172 axis.tick(view, client);
2173 view.status_word = 0x0027;
2174 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2175
2176 let tid = axis.homing_sdo_tid;
2178 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2179 client.poll();
2180 axis.tick(view, client);
2181 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2182
2183 axis.tick(view, client);
2185 let tid = axis.homing_sdo_tid;
2186 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2187 client.poll();
2188 axis.tick(view, client);
2189 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2190
2191 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2193 axis.tick(view, client);
2194 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2195
2196 view.status_word = 0x1427; axis.tick(view, client);
2199 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2200 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2201 view.status_word = 0x0027;
2202
2203 axis.tick(view, client);
2205 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2206
2207 view.status_word = 0x1027;
2209 axis.tick(view, client);
2210 view.status_word = 0x0027;
2211
2212 assert_eq!(axis.op, AxisOp::Idle);
2213 assert!(!axis.is_busy);
2214 assert!(!axis.is_error);
2215 assert_eq!(axis.home_offset, 0); }
2217
2218 #[test]
2219 fn soft_homing_pnp_home_sensor_full_sequence() {
2220 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2221 let mut view = MockView::new();
2222 enable_axis(&mut axis, &mut view, &mut client);
2223
2224 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2225
2226 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2229 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2231 axis.tick(&mut view, &mut client); view.home_sensor = true;
2235 view.position_actual = 5000;
2236 axis.tick(&mut view, &mut client);
2237 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2238
2239 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2240 |v| { v.home_sensor = false; });
2241 }
2242
2243 #[test]
2244 fn soft_homing_npn_home_sensor_full_sequence() {
2245 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2246 let mut view = MockView::new();
2247 view.home_sensor = true;
2249 enable_axis(&mut axis, &mut view, &mut client);
2250
2251 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2252
2253 axis.tick(&mut view, &mut client);
2255 view.status_word = 0x1027;
2256 axis.tick(&mut view, &mut client);
2257 view.status_word = 0x0027;
2258 axis.tick(&mut view, &mut client);
2259
2260 view.home_sensor = false;
2262 view.position_actual = 3000;
2263 axis.tick(&mut view, &mut client);
2264 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2265
2266 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2267 |v| { v.home_sensor = true; }); }
2269
2270 #[test]
2271 fn soft_homing_limit_switch_suppresses_halt() {
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::LimitSwitchPosPnp);
2278
2279 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2284 axis.tick(&mut view, &mut client); view.positive_limit = true;
2288 view.velocity_actual = 100; view.position_actual = 8000;
2290 axis.tick(&mut view, &mut client);
2291
2292 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2294 assert!(!axis.is_error);
2295 }
2296
2297 #[test]
2298 fn soft_homing_opposite_limit_still_protects() {
2299 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2300 let mut view = MockView::new();
2301 enable_axis(&mut axis, &mut view, &mut client);
2302
2303 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2305
2306 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2311 axis.tick(&mut view, &mut client); view.negative_limit = true;
2316 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
2318
2319 assert!(axis.is_error);
2321 assert!(axis.error_message.contains("Negative limit switch"));
2322 }
2323
2324 #[test]
2325 #[test]
2343 fn soft_homing_negative_direction_sets_negative_target() {
2344 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2345 let mut view = MockView::new();
2346 enable_axis(&mut axis, &mut view, &mut client);
2347
2348 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
2349 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
2353 }
2354
2355 #[test]
2356 fn home_integrated_method_starts_hardware_homing() {
2357 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2358 let mut view = MockView::new();
2359 enable_axis(&mut axis, &mut view, &mut client);
2360
2361 axis.home(&mut view, HomingMethod::CurrentPosition);
2362 assert!(matches!(axis.op, AxisOp::Homing(0)));
2363 assert_eq!(axis.homing_method, 37);
2364 }
2365
2366 #[test]
2367 fn home_integrated_arbitrary_code() {
2368 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2369 let mut view = MockView::new();
2370 enable_axis(&mut axis, &mut view, &mut client);
2371
2372 axis.home(&mut view, HomingMethod::Integrated(35));
2373 assert!(matches!(axis.op, AxisOp::Homing(0)));
2374 assert_eq!(axis.homing_method, 35);
2375 }
2376
2377 #[test]
2378 fn hardware_homing_skips_speed_sdos_when_zero() {
2379 use mechutil::ipc::CommandMessage;
2380
2381 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
2382 let mut view = MockView::new();
2383 enable_axis(&mut axis, &mut view, &mut client);
2384
2385 axis.home(&mut view, HomingMethod::Integrated(37));
2387
2388 axis.tick(&mut view, &mut client);
2390 assert!(matches!(axis.op, AxisOp::Homing(1)));
2391
2392 let _ = write_rx.try_recv();
2394
2395 let tid = axis.homing_sdo_tid;
2397 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2398 client.poll();
2399 axis.tick(&mut view, &mut client);
2400
2401 assert!(matches!(axis.op, AxisOp::Homing(8)));
2403 }
2404
2405 #[test]
2406 fn hardware_homing_writes_speed_sdos_when_nonzero() {
2407 use mechutil::ipc::CommandMessage;
2408
2409 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
2410 let mut view = MockView::new();
2411 enable_axis(&mut axis, &mut view, &mut client);
2412
2413 axis.home(&mut view, HomingMethod::Integrated(37));
2415
2416 axis.tick(&mut view, &mut client);
2418 assert!(matches!(axis.op, AxisOp::Homing(1)));
2419 let _ = write_rx.try_recv();
2420
2421 let tid = axis.homing_sdo_tid;
2423 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2424 client.poll();
2425 axis.tick(&mut view, &mut client);
2426 assert!(matches!(axis.op, AxisOp::Homing(2)));
2428 }
2429
2430 #[test]
2431 fn soft_homing_edge_during_ack_step() {
2432 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2433 let mut view = MockView::new();
2434 enable_axis(&mut axis, &mut view, &mut client);
2435
2436 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2437 axis.tick(&mut view, &mut client); view.home_sensor = true;
2441 view.position_actual = 2000;
2442 axis.tick(&mut view, &mut client);
2443
2444 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2446 }
2447
2448 #[test]
2449 fn soft_homing_applies_home_position() {
2450 let mut cfg = soft_homing_config();
2451 cfg.home_position = 90.0;
2452
2453 use tokio::sync::mpsc;
2454 let (write_tx, _write_rx) = mpsc::unbounded_channel();
2455 let (resp_tx, response_rx) = mpsc::unbounded_channel();
2456 let mut client = CommandClient::new(write_tx, response_rx);
2457 let mut axis = Axis::new(cfg, "TestDrive");
2458
2459 let mut view = MockView::new();
2460 enable_axis(&mut axis, &mut view, &mut client);
2461
2462 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2463
2464 axis.tick(&mut view, &mut client);
2466 view.status_word = 0x1027;
2467 axis.tick(&mut view, &mut client);
2468 view.status_word = 0x0027;
2469 axis.tick(&mut view, &mut client);
2470
2471 view.home_sensor = true;
2473 view.position_actual = 5000;
2474 axis.tick(&mut view, &mut client);
2475 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2476
2477 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2479 |v| { v.home_sensor = false; });
2480
2481 assert_eq!(axis.home_offset, 0);
2483 }
2484
2485 #[test]
2486 fn soft_homing_default_home_position_zero() {
2487 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2488 let mut view = MockView::new();
2489 enable_axis(&mut axis, &mut view, &mut client);
2490
2491 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2492
2493 axis.tick(&mut view, &mut client);
2495 view.status_word = 0x1027;
2496 axis.tick(&mut view, &mut client);
2497 view.status_word = 0x0027;
2498 axis.tick(&mut view, &mut client);
2499
2500 view.home_sensor = true;
2502 view.position_actual = 5000;
2503 axis.tick(&mut view, &mut client);
2504
2505 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2506 |v| { v.home_sensor = false; });
2507
2508 assert_eq!(axis.home_offset, 0);
2509 }
2510}