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) {
993 self.set_op_error("Software homing: sensor already in trigger state");
994 return;
995 }
996
997 self.op = AxisOp::SoftHoming(0);
998 self.op_started = Some(Instant::now());
999 }
1000
1001 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1002 let raw = match self.soft_home_sensor {
1003 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1004 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1005 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1006 };
1007 match self.soft_home_sensor_type {
1008 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1011 }
1012
1013
1014 fn calculate_max_relative_target(&self, direction : f64) -> i32 {
1017 let dir = if !self.config.invert_direction {
1018 direction
1019 }
1020 else {
1021 -direction
1022 };
1023
1024 let target = if dir > 0.0 {
1025 i32::MAX
1026 }
1027 else {
1028 i32::MIN
1029 };
1030
1031 return target;
1032 }
1033
1034
1035 fn command_halt(&self, view: &mut impl AxisView) {
1040 let mut cw = RawControlWord(view.control_word());
1041 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1044 }
1045
1046
1047 fn command_cancel_move(&self, view: &mut impl AxisView) {
1055
1056 let mut cw = RawControlWord(view.control_word());
1057 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());
1062
1063 let current_pos = view.position_actual();
1064 view.set_target_position(current_pos);
1065 view.set_profile_velocity(0);
1066 }
1067
1068
1069 fn command_homing_speed(&self, view: &mut impl AxisView) {
1071 let cpu = self.config.counts_per_user();
1072 let vel = (self.config.homing_speed * cpu).round() as u32;
1073 let accel = (self.config.homing_accel * cpu).round() as u32;
1074 let decel = (self.config.homing_decel * cpu).round() as u32;
1075 view.set_profile_velocity(vel);
1076 view.set_profile_acceleration(accel);
1077 view.set_profile_deceleration(decel);
1078 }
1079
1080 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1096 match HomeState::from_repr(step) {
1097 Some(HomeState::Search) => {
1099 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1100
1101 let target = self.calculate_max_relative_target(self.soft_home_direction);
1111 view.set_target_position(target);
1112
1113 self.command_homing_speed(view);
1122
1123 let mut cw = RawControlWord(view.control_word());
1124 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());
1129
1130 log::info!("SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1131 target, self.config.homing_speed, self.soft_home_direction, view.position_actual());
1132 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1133 }
1134 Some(HomeState::WaitSearching) => {
1135 if self.check_soft_home_trigger(view) {
1136 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1137 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1138 return;
1139 }
1140 let sw = RawStatusWord(view.status_word());
1141 if sw.raw() & (1 << 12) != 0 {
1142 let mut cw = RawControlWord(view.control_word());
1143 cw.set_bit(4, false);
1144 view.set_control_word(cw.raw());
1145 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1146 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1147 } else if self.homing_timed_out() {
1148 self.set_op_error("Software homing timeout: set-point not acknowledged");
1149 }
1150 }
1151 Some(HomeState::WaitFoundSensor) => {
1161 if self.check_soft_home_trigger(view) {
1162 log::info!("SoftHome[3]: sensor triggered at pos={}. HALTING", view.position_actual());
1163 log::info!("ControlWord is : {} ", view.control_word());
1164
1165 let mut cw = RawControlWord(view.control_word());
1166 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1169
1170
1171 self.halt_stable_count = 0;
1172 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1173 } else if self.homing_timed_out() {
1174 self.set_op_error("Software homing timeout: sensor not detected");
1175 }
1176 }
1177
1178
1179 Some(HomeState::WaitStoppedFoundSensor) => {
1180 const STABLE_WINDOW: i32 = 1;
1181 const STABLE_TICKS_REQUIRED: u8 = 10;
1182
1183 let pos = view.position_actual();
1188 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1189 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1190 } else {
1191 self.halt_stable_count = 0;
1192 }
1193
1194 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1195
1196 log::debug!("SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true.");
1197 self.command_cancel_move(view);
1198 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAck as u8);
1199
1200 } else if self.homing_timed_out() {
1201 self.set_op_error("Software homing timeout: motor did not stop after sensor trigger");
1202 }
1203 }
1204 Some(HomeState::WaitFoundSensorAck) => {
1205 let sw = RawStatusWord(view.status_word());
1206 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1207
1208 log::info!("SoftHome[6]: relative move cancel ack received. Waiting before back-off...");
1209
1210 let mut cw = RawControlWord(view.control_word());
1212 cw.set_bit(4, false); cw.set_bit(5, true); view.set_control_word(cw.raw());
1215
1216 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensorAckClear as u8);
1217
1218 } else if self.homing_timed_out() {
1219 self.set_op_error("Software homing timeout: cancel not acknowledged");
1220 }
1221 },
1222 Some(HomeState::WaitFoundSensorAckClear) => {
1223 let sw = RawStatusWord(view.status_word());
1224 if sw.raw() & (1 << 12) == 0 {
1226
1227 let mut cw = RawControlWord(view.control_word());
1229 cw.set_bit(8, false);
1230 view.set_control_word(cw.raw());
1231
1232 log::info!("SoftHome[6]: Handshake cleared (Bit 12 is LOW). Proceeding to delay.");
1233 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1234 self.ton.call(false, Duration::from_secs(3));
1235 }
1236 },
1237 Some(HomeState::DebounceFoundSensor) => {
1239 self.ton.call(true, Duration::from_secs(3));
1240
1241 let sw = RawStatusWord(view.status_word());
1242 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1243 self.ton.call(false, Duration::from_secs(3));
1244 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1245 view.position_actual(), view.control_word(), view.status_word());
1246 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1247 }
1248 }
1249
1250 Some(HomeState::BackOff) => {
1252
1253 let target = self.calculate_max_relative_target(-self.soft_home_direction);
1254 view.set_target_position(target);
1255
1256
1257 self.command_homing_speed(view);
1258
1259 let mut cw = RawControlWord(view.control_word());
1260 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1264 log::info!("SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1265 target, self.config.homing_speed, view.position_actual(), cw.raw());
1266 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1267 }
1268 Some(HomeState::WaitBackingOff) => {
1269 let sw = RawStatusWord(view.status_word());
1270 if sw.raw() & (1 << 12) != 0 {
1271 let mut cw = RawControlWord(view.control_word());
1272 cw.set_bit(4, false);
1273 view.set_control_word(cw.raw());
1274 log::debug!("SoftHome[WaitBackingOff]: back-off ack received, pos={}", view.position_actual());
1275 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1276 } else if self.homing_timed_out() {
1277 self.set_op_error("Software homing timeout: back-off not acknowledged");
1278 }
1279 }
1280 Some(HomeState::WaitLostSensor) => {
1281 if !self.check_soft_home_trigger(view) {
1282 log::info!("SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...", view.position_actual());
1283
1284 self.command_halt(view);
1285 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1286 } else if self.homing_timed_out() {
1287 self.set_op_error("Software homing timeout: sensor did not clear during back-off");
1288 }
1289 }
1290 Some(HomeState::WaitStoppedLostSensor) => {
1291 const STABLE_WINDOW: i32 = 1;
1292 const STABLE_TICKS_REQUIRED: u8 = 10;
1293
1294 let mut cw = RawControlWord(view.control_word());
1295 cw.set_bit(8, true);
1296 view.set_control_word(cw.raw());
1297
1298 let pos = view.position_actual();
1299 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1300 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1301 } else {
1302 self.halt_stable_count = 0;
1303 }
1304
1305 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1306 log::debug!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1307 self.command_cancel_move(view);
1308 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAck as u8);
1309 } else if self.homing_timed_out() {
1310 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1311 }
1312 }
1313 Some(HomeState::WaitLostSensorAck) => {
1314 let sw = RawStatusWord(view.status_word());
1315 if sw.raw() & (1 << 12) != 0 && sw.raw() & (1 << 10) != 0 {
1316
1317 log::info!("SoftHome[WaitLostSensorAck]: relative move cancel ack received. Waiting before back-off...");
1318
1319 let mut cw = RawControlWord(view.control_word());
1321 cw.set_bit(4, false); view.set_control_word(cw.raw());
1323
1324 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensorAckClear as u8);
1325
1326
1327 } else if self.homing_timed_out() {
1328 self.set_op_error("Software homing timeout: cancel not acknowledged");
1329 }
1330 }
1331 Some(HomeState::WaitLostSensorAckClear) => {
1332 let sw = RawStatusWord(view.status_word());
1334 if sw.raw() & (1 << 12) == 0 {
1335
1336 let mut cw = RawControlWord(view.control_word());
1338 cw.set_bit(8, false);
1339 view.set_control_word(cw.raw());
1340
1341
1342 let desired_counts = self.config.to_counts(self.config.home_position).round() as i32;
1343 let current_pos = view.position_actual();
1344 let offset = desired_counts - current_pos;
1345 self.homing_sdo_tid = self.sdo.write(
1346 client, 0x607C, 0, json!(offset),
1347 );
1348
1349 log::info!("SoftHome[WaitLostSensorAckClear]: Handshake cleared (Bit 12 is LOW). Writing home offset {}.",
1350 offset
1351 );
1352
1353 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
1354
1355 }
1356 },
1357 Some(HomeState::WaitHomeOffsetDone) => {
1358 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1360 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8); }
1361 SdoResult::Pending => {
1362 if self.homing_timed_out() {
1363 self.set_op_error("Software homing timeout: home offset SDO write");
1364 }
1365 }
1366 SdoResult::Err(e) => {
1367 self.set_op_error(&format!("Software homing SDO error: {}", e));
1368 }
1369 SdoResult::Timeout => {
1370 self.set_op_error("Software homing: home offset SDO timed out");
1371 }
1372 }
1373 }
1374 Some(HomeState::WriteHomingMethod) => {
1375 self.homing_sdo_tid = self.sdo.write(
1377 client, 0x6098, 0, json!(37i8),
1378 );
1379 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
1380 }
1381 Some(HomeState::WaitWriteHomingMethodDone) => {
1382 match self.sdo.result(client, self.homing_sdo_tid, Duration::from_secs(5)) {
1384 SdoResult::Ok(_) => { self.op = AxisOp::SoftHoming(HomeState::ExecHomingMode as u8); }
1385 SdoResult::Pending => {
1386 if self.homing_timed_out() {
1387 self.set_op_error("Software homing timeout: homing method SDO write");
1388 }
1389 }
1390 SdoResult::Err(e) => {
1391 self.set_op_error(&format!("Software homing SDO error: {}", e));
1392 }
1393 SdoResult::Timeout => {
1394 self.set_op_error("Software homing: homing method SDO timed out");
1395 }
1396 }
1397 }
1398 Some(HomeState::ExecHomingMode) => {
1399 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1401 if view.modes_of_operation_display() == ModesOfOperation::Homing.as_i8() {
1402 let mut cw = RawControlWord(view.control_word());
1403 cw.set_bit(4, true); view.set_control_word(cw.raw());
1405 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
1406 } else if self.homing_timed_out() {
1407 self.set_op_error("Software homing timeout: homing mode not confirmed");
1408 }
1409 }
1410 Some(HomeState::WaitHomingDone) => {
1411 let sw = view.status_word();
1413 let error = sw & (1 << 13) != 0;
1414 let attained = sw & (1 << 12) != 0;
1415 let reached = sw & (1 << 10) != 0;
1416
1417 if error {
1418 self.set_op_error("Software homing: drive reported homing error");
1419 } else if attained && reached {
1420 let mut cw = RawControlWord(view.control_word());
1422 cw.set_bit(4, false);
1423 view.set_control_word(cw.raw());
1424 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1425 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
1427 } else if self.homing_timed_out() {
1428 self.set_op_error("Software homing timeout: drive homing did not complete");
1429 }
1430 }
1431 Some(HomeState::SendCurrentPositionTarget) => {
1432 let current_pos = view.position_actual();
1434 view.set_target_position(current_pos);
1435 view.set_profile_velocity(0);
1436 let mut cw = RawControlWord(view.control_word());
1437 cw.set_bit(4, true);
1438 cw.set_bit(5, true);
1439 cw.set_bit(6, false); view.set_control_word(cw.raw());
1441 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
1442 }
1443 Some(HomeState::WaitCurrentPositionTargetSent) => {
1444 let sw = RawStatusWord(view.status_word());
1446 if sw.raw() & (1 << 12) != 0 {
1447 let mut cw = RawControlWord(view.control_word());
1448 cw.set_bit(4, false);
1449 view.set_control_word(cw.raw());
1450 log::info!("Software homing complete — position set to {} user units",
1451 self.config.home_position);
1452 self.complete_op();
1453 } else if self.homing_timed_out() {
1454 self.set_op_error("Software homing timeout: hold position not acknowledged");
1455 }
1456 }
1457 _ => self.complete_op(),
1458 }
1459 }
1460
1461 fn tick_halting(&mut self, _view: &mut impl AxisView) {
1463 self.complete_op();
1466 }
1467
1468 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
1473 match step {
1474 1 => {
1475 let mut cw = RawControlWord(view.control_word());
1477 cw.cmd_fault_reset();
1478 view.set_control_word(cw.raw());
1479 self.op = AxisOp::FaultRecovery(2);
1480 }
1481 2 => {
1482 let sw = RawStatusWord(view.status_word());
1484 let state = sw.state();
1485 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
1486 log::info!("Fault cleared (drive state: {})", state);
1487 self.complete_op();
1488 } else if self.op_timed_out() {
1489 self.set_op_error("Fault reset timeout: drive still faulted");
1490 }
1491 }
1492 _ => self.complete_op(),
1493 }
1494 }
1495}
1496
1497#[cfg(test)]
1502mod tests {
1503 use super::*;
1504
1505 struct MockView {
1507 control_word: u16,
1508 status_word: u16,
1509 target_position: i32,
1510 profile_velocity: u32,
1511 profile_acceleration: u32,
1512 profile_deceleration: u32,
1513 modes_of_operation: i8,
1514 modes_of_operation_display: i8,
1515 position_actual: i32,
1516 velocity_actual: i32,
1517 error_code: u16,
1518 positive_limit: bool,
1519 negative_limit: bool,
1520 home_sensor: bool,
1521 }
1522
1523 impl MockView {
1524 fn new() -> Self {
1525 Self {
1526 control_word: 0,
1527 status_word: 0x0040, target_position: 0,
1529 profile_velocity: 0,
1530 profile_acceleration: 0,
1531 profile_deceleration: 0,
1532 modes_of_operation: 0,
1533 modes_of_operation_display: 1, position_actual: 0,
1535 velocity_actual: 0,
1536 error_code: 0,
1537 positive_limit: false,
1538 negative_limit: false,
1539 home_sensor: false,
1540 }
1541 }
1542
1543 fn set_state(&mut self, state: u16) {
1544 self.status_word = state;
1545 }
1546 }
1547
1548 impl AxisView for MockView {
1549 fn control_word(&self) -> u16 { self.control_word }
1550 fn set_control_word(&mut self, word: u16) { self.control_word = word; }
1551 fn set_target_position(&mut self, pos: i32) { self.target_position = pos; }
1552 fn set_profile_velocity(&mut self, vel: u32) { self.profile_velocity = vel; }
1553 fn set_profile_acceleration(&mut self, accel: u32) { self.profile_acceleration = accel; }
1554 fn set_profile_deceleration(&mut self, decel: u32) { self.profile_deceleration = decel; }
1555 fn set_modes_of_operation(&mut self, mode: i8) { self.modes_of_operation = mode; }
1556 fn modes_of_operation_display(&self) -> i8 { self.modes_of_operation_display }
1557 fn status_word(&self) -> u16 { self.status_word }
1558 fn position_actual(&self) -> i32 { self.position_actual }
1559 fn velocity_actual(&self) -> i32 { self.velocity_actual }
1560 fn error_code(&self) -> u16 { self.error_code }
1561 fn positive_limit_active(&self) -> bool { self.positive_limit }
1562 fn negative_limit_active(&self) -> bool { self.negative_limit }
1563 fn home_sensor_active(&self) -> bool { self.home_sensor }
1564 }
1565
1566 fn test_config() -> AxisConfig {
1567 AxisConfig::new(12_800).with_user_scale(360.0)
1568 }
1569
1570 fn test_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
1572 use tokio::sync::mpsc;
1573 let (write_tx, write_rx) = mpsc::unbounded_channel();
1574 let (response_tx, response_rx) = mpsc::unbounded_channel();
1575 let client = CommandClient::new(write_tx, response_rx);
1576 let axis = Axis::new(test_config(), "TestDrive");
1577 (axis, client, response_tx, write_rx)
1578 }
1579
1580 #[test]
1581 fn axis_config_conversion() {
1582 let cfg = test_config();
1583 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
1585 }
1586
1587 #[test]
1588 fn enable_sequence_sets_pp_mode_and_shutdown() {
1589 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1590 let mut view = MockView::new();
1591
1592 axis.enable(&mut view);
1593
1594 assert_eq!(view.modes_of_operation, ModesOfOperation::ProfilePosition.as_i8());
1596 assert_eq!(view.control_word & 0x008F, 0x0006);
1598 assert_eq!(axis.op, AxisOp::Enabling(1));
1600
1601 view.set_state(0x0021); axis.tick(&mut view, &mut client);
1604
1605 assert_eq!(view.control_word & 0x008F, 0x000F);
1607 assert_eq!(axis.op, AxisOp::Enabling(2));
1608
1609 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1612
1613 assert_eq!(axis.op, AxisOp::Idle);
1615 assert!(axis.motor_on);
1616 }
1617
1618 #[test]
1619 fn move_absolute_sets_target() {
1620 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1621 let mut view = MockView::new();
1622 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1627
1628 assert_eq!(view.target_position, 1600);
1630 assert_eq!(view.profile_velocity, 3200);
1632 assert_eq!(view.profile_acceleration, 6400);
1634 assert_eq!(view.profile_deceleration, 6400);
1635 assert!(view.control_word & (1 << 4) != 0);
1637 assert!(view.control_word & (1 << 6) == 0);
1639 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1641 }
1642
1643 #[test]
1644 fn move_relative_sets_relative_bit() {
1645 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1646 let mut view = MockView::new();
1647 view.set_state(0x0027);
1648 axis.tick(&mut view, &mut client);
1649
1650 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
1651
1652 assert!(view.control_word & (1 << 6) != 0);
1654 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Relative, 1)));
1655 }
1656
1657 #[test]
1658 fn move_completes_on_target_reached() {
1659 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1660 let mut view = MockView::new();
1661 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1663
1664 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1665
1666 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1669 assert!(view.control_word & (1 << 4) == 0);
1671
1672 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
1675 assert_eq!(axis.op, AxisOp::Idle);
1677 assert!(!axis.in_motion);
1678 }
1679
1680 #[test]
1681 fn fault_detected_sets_error() {
1682 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1683 let mut view = MockView::new();
1684 view.set_state(0x0008); view.error_code = 0x1234;
1686
1687 axis.tick(&mut view, &mut client);
1688
1689 assert!(axis.is_error);
1690 assert_eq!(axis.error_code, 0x1234);
1691 assert!(axis.error_message.contains("fault"));
1692 }
1693
1694 #[test]
1695 fn fault_recovery_sequence() {
1696 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1697 let mut view = MockView::new();
1698 view.set_state(0x0008); axis.reset_faults(&mut view);
1701 assert!(view.control_word & 0x0080 == 0);
1703
1704 axis.tick(&mut view, &mut client);
1706 assert!(view.control_word & 0x0080 != 0);
1707
1708 view.set_state(0x0040);
1710 axis.tick(&mut view, &mut client);
1711 assert_eq!(axis.op, AxisOp::Idle);
1712 assert!(!axis.is_error);
1713 }
1714
1715 #[test]
1716 fn disable_sequence() {
1717 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1718 let mut view = MockView::new();
1719 view.set_state(0x0027); axis.disable(&mut view);
1722 assert_eq!(view.control_word & 0x008F, 0x0007);
1724
1725 view.set_state(0x0023); axis.tick(&mut view, &mut client);
1728 assert_eq!(axis.op, AxisOp::Idle);
1729 }
1730
1731 #[test]
1732 fn position_tracks_with_home_offset() {
1733 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1734 let mut view = MockView::new();
1735 view.set_state(0x0027);
1736 view.position_actual = 5000;
1737
1738 axis.enable(&mut view);
1740 view.set_state(0x0021);
1741 axis.tick(&mut view, &mut client);
1742 view.set_state(0x0027);
1743 axis.tick(&mut view, &mut client);
1744
1745 assert_eq!(axis.home_offset, 5000);
1747
1748 assert!((axis.position - 0.0).abs() < 0.01);
1750
1751 view.position_actual = 6600;
1753 axis.tick(&mut view, &mut client);
1754
1755 assert!((axis.position - 45.0).abs() < 0.1);
1757 }
1758
1759 #[test]
1760 fn set_position_adjusts_home_offset() {
1761 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1762 let mut view = MockView::new();
1763 view.position_actual = 3200;
1764
1765 axis.set_position(&view, 90.0);
1766 axis.tick(&mut view, &mut client);
1767
1768 assert_eq!(axis.home_offset, 0);
1770 assert!((axis.position - 90.0).abs() < 0.01);
1771 }
1772
1773 #[test]
1774 fn halt_sets_bit_and_goes_idle() {
1775 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1776 let mut view = MockView::new();
1777 view.set_state(0x0027);
1778
1779 axis.halt(&mut view);
1780 assert!(view.control_word & (1 << 8) != 0);
1782
1783 axis.tick(&mut view, &mut client);
1785 assert_eq!(axis.op, AxisOp::Idle);
1786 }
1787
1788 #[test]
1789 fn is_busy_tracks_operations() {
1790 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1791 let mut view = MockView::new();
1792
1793 axis.tick(&mut view, &mut client);
1795 assert!(!axis.is_busy);
1796
1797 axis.enable(&mut view);
1799 axis.tick(&mut view, &mut client);
1800 assert!(axis.is_busy);
1801
1802 view.set_state(0x0021);
1804 axis.tick(&mut view, &mut client);
1805 view.set_state(0x0027);
1806 axis.tick(&mut view, &mut client);
1807 assert!(!axis.is_busy);
1808
1809 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1811 axis.tick(&mut view, &mut client);
1812 assert!(axis.is_busy);
1813 assert!(axis.in_motion);
1814 }
1815
1816 #[test]
1817 fn fault_during_move_cancels_op() {
1818 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1819 let mut view = MockView::new();
1820 view.set_state(0x0027); axis.tick(&mut view, &mut client);
1822
1823 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1825 axis.tick(&mut view, &mut client);
1826 assert!(axis.is_busy);
1827 assert!(!axis.is_error);
1828
1829 view.set_state(0x0008); axis.tick(&mut view, &mut client);
1832
1833 assert!(!axis.is_busy);
1835 assert!(axis.is_error);
1836 assert_eq!(axis.op, AxisOp::Idle);
1837 }
1838
1839 #[test]
1840 fn move_absolute_rejected_by_max_limit() {
1841 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1842 let mut view = MockView::new();
1843 view.set_state(0x0027);
1844 axis.tick(&mut view, &mut client);
1845
1846 axis.set_software_max_limit(90.0);
1847 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
1848
1849 assert!(axis.is_error);
1851 assert_eq!(axis.op, AxisOp::Idle);
1852 assert!(axis.error_message.contains("max software limit"));
1853 }
1854
1855 #[test]
1856 fn move_absolute_rejected_by_min_limit() {
1857 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1858 let mut view = MockView::new();
1859 view.set_state(0x0027);
1860 axis.tick(&mut view, &mut client);
1861
1862 axis.set_software_min_limit(-10.0);
1863 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
1864
1865 assert!(axis.is_error);
1866 assert_eq!(axis.op, AxisOp::Idle);
1867 assert!(axis.error_message.contains("min software limit"));
1868 }
1869
1870 #[test]
1871 fn move_relative_rejected_by_max_limit() {
1872 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1873 let mut view = MockView::new();
1874 view.set_state(0x0027);
1875 axis.tick(&mut view, &mut client);
1876
1877 axis.set_software_max_limit(50.0);
1879 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
1880
1881 assert!(axis.is_error);
1882 assert_eq!(axis.op, AxisOp::Idle);
1883 assert!(axis.error_message.contains("max software limit"));
1884 }
1885
1886 #[test]
1887 fn move_within_limits_allowed() {
1888 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1889 let mut view = MockView::new();
1890 view.set_state(0x0027);
1891 axis.tick(&mut view, &mut client);
1892
1893 axis.set_software_max_limit(90.0);
1894 axis.set_software_min_limit(-90.0);
1895 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1896
1897 assert!(!axis.is_error);
1899 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1900 }
1901
1902 #[test]
1903 fn runtime_limit_halts_move_in_violated_direction() {
1904 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1905 let mut view = MockView::new();
1906 view.set_state(0x0027);
1907 axis.tick(&mut view, &mut client);
1908
1909 axis.set_software_max_limit(45.0);
1910 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1912
1913 view.position_actual = 1650;
1916 view.velocity_actual = 100; view.status_word = 0x1027;
1920 axis.tick(&mut view, &mut client);
1921 view.status_word = 0x0027;
1922 axis.tick(&mut view, &mut client);
1923
1924 assert!(axis.is_error);
1926 assert!(axis.at_max_limit);
1927 assert_eq!(axis.op, AxisOp::Idle);
1928 assert!(axis.error_message.contains("Software position limit"));
1929 assert!(view.control_word & (1 << 8) != 0);
1931 }
1932
1933 #[test]
1934 fn runtime_limit_allows_move_in_opposite_direction() {
1935 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1936 let mut view = MockView::new();
1937 view.set_state(0x0027);
1938 view.position_actual = 1778; axis.set_software_max_limit(45.0);
1941 axis.tick(&mut view, &mut client);
1942 assert!(axis.at_max_limit);
1943
1944 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
1946 assert!(!axis.is_error);
1947 assert!(matches!(axis.op, AxisOp::Moving(MoveKind::Absolute, 1)));
1948
1949 view.velocity_actual = -100;
1951 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
1953 assert!(!axis.is_error);
1955 }
1956
1957 #[test]
1958 fn positive_limit_switch_halts_positive_move() {
1959 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1960 let mut view = MockView::new();
1961 view.set_state(0x0027);
1962 axis.tick(&mut view, &mut client);
1963
1964 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
1966 view.velocity_actual = 100; view.status_word = 0x1027;
1969 axis.tick(&mut view, &mut client);
1970 view.status_word = 0x0027;
1971
1972 view.positive_limit = true;
1974 axis.tick(&mut view, &mut client);
1975
1976 assert!(axis.is_error);
1977 assert!(axis.at_positive_limit_switch);
1978 assert!(!axis.is_busy);
1979 assert!(axis.error_message.contains("Positive limit switch"));
1980 assert!(view.control_word & (1 << 8) != 0);
1982 }
1983
1984 #[test]
1985 fn negative_limit_switch_halts_negative_move() {
1986 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
1987 let mut view = MockView::new();
1988 view.set_state(0x0027);
1989 axis.tick(&mut view, &mut client);
1990
1991 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
1993 view.velocity_actual = -100; view.status_word = 0x1027;
1995 axis.tick(&mut view, &mut client);
1996 view.status_word = 0x0027;
1997
1998 view.negative_limit = true;
2000 axis.tick(&mut view, &mut client);
2001
2002 assert!(axis.is_error);
2003 assert!(axis.at_negative_limit_switch);
2004 assert!(axis.error_message.contains("Negative limit switch"));
2005 }
2006
2007 #[test]
2008 fn limit_switch_allows_move_in_opposite_direction() {
2009 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2010 let mut view = MockView::new();
2011 view.set_state(0x0027);
2012 view.positive_limit = true;
2014 view.velocity_actual = -100;
2015 axis.tick(&mut view, &mut client);
2016 assert!(axis.at_positive_limit_switch);
2017
2018 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
2020 view.status_word = 0x1027;
2021 axis.tick(&mut view, &mut client);
2022
2023 assert!(!axis.is_error);
2025 assert!(matches!(axis.op, AxisOp::Moving(_, _)));
2026 }
2027
2028 #[test]
2029 fn limit_switch_ignored_when_not_moving() {
2030 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2031 let mut view = MockView::new();
2032 view.set_state(0x0027);
2033 view.positive_limit = true;
2034
2035 axis.tick(&mut view, &mut client);
2036
2037 assert!(axis.at_positive_limit_switch);
2039 assert!(!axis.is_error);
2040 }
2041
2042 #[test]
2043 fn home_sensor_output_tracks_view() {
2044 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2045 let mut view = MockView::new();
2046 view.set_state(0x0027);
2047
2048 axis.tick(&mut view, &mut client);
2049 assert!(!axis.home_sensor);
2050
2051 view.home_sensor = true;
2052 axis.tick(&mut view, &mut client);
2053 assert!(axis.home_sensor);
2054
2055 view.home_sensor = false;
2056 axis.tick(&mut view, &mut client);
2057 assert!(!axis.home_sensor);
2058 }
2059
2060 #[test]
2061 fn velocity_output_converted() {
2062 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2063 let mut view = MockView::new();
2064 view.set_state(0x0027);
2065 view.velocity_actual = 3200;
2067
2068 axis.tick(&mut view, &mut client);
2069
2070 assert!((axis.speed - 90.0).abs() < 0.1);
2071 assert!(axis.moving_positive);
2072 assert!(!axis.moving_negative);
2073 }
2074
2075 fn soft_homing_config() -> AxisConfig {
2078 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
2079 cfg.homing_speed = 10.0;
2080 cfg.homing_accel = 20.0;
2081 cfg.homing_decel = 20.0;
2082 cfg
2083 }
2084
2085 fn soft_homing_axis() -> (Axis, CommandClient, tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>, tokio::sync::mpsc::UnboundedReceiver<String>) {
2086 use tokio::sync::mpsc;
2087 let (write_tx, write_rx) = mpsc::unbounded_channel();
2088 let (response_tx, response_rx) = mpsc::unbounded_channel();
2089 let client = CommandClient::new(write_tx, response_rx);
2090 let axis = Axis::new(soft_homing_config(), "TestDrive");
2091 (axis, client, response_tx, write_rx)
2092 }
2093
2094 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
2096 view.set_state(0x0027); axis.tick(view, client);
2098 }
2099
2100 fn complete_soft_homing(
2105 axis: &mut Axis,
2106 view: &mut MockView,
2107 client: &mut CommandClient,
2108 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2109 trigger_pos: i32,
2110 clear_sensor: impl FnOnce(&mut MockView),
2111 ) {
2112 use mechutil::ipc::CommandMessage as IpcMsg;
2113
2114 axis.tick(view, client);
2117 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
2118
2119 view.position_actual = trigger_pos + 100;
2121 axis.tick(view, client);
2122 view.position_actual = trigger_pos + 120;
2123 axis.tick(view, client);
2124 for _ in 0..10 { axis.tick(view, client); }
2126 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
2127
2128 view.status_word = 0x1027;
2130 axis.tick(view, client);
2131 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
2132 view.status_word = 0x0027;
2133
2134 for _ in 0..100 { axis.tick(view, client); }
2136 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
2137
2138 axis.tick(view, client);
2141 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
2142
2143 view.status_word = 0x1027;
2145 axis.tick(view, client);
2146 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2147 view.status_word = 0x0027;
2148
2149 axis.tick(view, client);
2151 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
2152 clear_sensor(view);
2153 view.position_actual = trigger_pos - 200;
2154 axis.tick(view, client);
2155 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
2156
2157 axis.tick(view, client);
2159 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
2160 for _ in 0..10 { axis.tick(view, client); }
2161 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
2162
2163 view.status_word = 0x1027;
2166 axis.tick(view, client);
2167 view.status_word = 0x0027;
2168 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
2169
2170 let tid = axis.homing_sdo_tid;
2172 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2173 client.poll();
2174 axis.tick(view, client);
2175 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
2176
2177 axis.tick(view, client);
2179 let tid = axis.homing_sdo_tid;
2180 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
2181 client.poll();
2182 axis.tick(view, client);
2183 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
2184
2185 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
2187 axis.tick(view, client);
2188 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
2189
2190 view.status_word = 0x1427; axis.tick(view, client);
2193 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
2194 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
2195 view.status_word = 0x0027;
2196
2197 axis.tick(view, client);
2199 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
2200
2201 view.status_word = 0x1027;
2203 axis.tick(view, client);
2204 view.status_word = 0x0027;
2205
2206 assert_eq!(axis.op, AxisOp::Idle);
2207 assert!(!axis.is_busy);
2208 assert!(!axis.is_error);
2209 assert_eq!(axis.home_offset, 0); }
2211
2212 #[test]
2213 fn soft_homing_pnp_home_sensor_full_sequence() {
2214 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2215 let mut view = MockView::new();
2216 enable_axis(&mut axis, &mut view, &mut client);
2217
2218 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2219
2220 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
2223 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2225 axis.tick(&mut view, &mut client); view.home_sensor = true;
2229 view.position_actual = 5000;
2230 axis.tick(&mut view, &mut client);
2231 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2232
2233 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2234 |v| { v.home_sensor = false; });
2235 }
2236
2237 #[test]
2238 fn soft_homing_npn_home_sensor_full_sequence() {
2239 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2240 let mut view = MockView::new();
2241 view.home_sensor = true;
2243 enable_axis(&mut axis, &mut view, &mut client);
2244
2245 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
2246
2247 axis.tick(&mut view, &mut client);
2249 view.status_word = 0x1027;
2250 axis.tick(&mut view, &mut client);
2251 view.status_word = 0x0027;
2252 axis.tick(&mut view, &mut client);
2253
2254 view.home_sensor = false;
2256 view.position_actual = 3000;
2257 axis.tick(&mut view, &mut client);
2258 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2259
2260 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000,
2261 |v| { v.home_sensor = true; }); }
2263
2264 #[test]
2265 fn soft_homing_limit_switch_suppresses_halt() {
2266 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2267 let mut view = MockView::new();
2268 enable_axis(&mut axis, &mut view, &mut client);
2269
2270 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
2272
2273 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2278 axis.tick(&mut view, &mut client); view.positive_limit = true;
2282 view.velocity_actual = 100; view.position_actual = 8000;
2284 axis.tick(&mut view, &mut client);
2285
2286 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2288 assert!(!axis.is_error);
2289 }
2290
2291 #[test]
2292 fn soft_homing_opposite_limit_still_protects() {
2293 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2294 let mut view = MockView::new();
2295 enable_axis(&mut axis, &mut view, &mut client);
2296
2297 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2299
2300 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
2305 axis.tick(&mut view, &mut client); view.negative_limit = true;
2310 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
2312
2313 assert!(axis.is_error);
2315 assert!(axis.error_message.contains("Negative limit switch"));
2316 }
2317
2318 #[test]
2319 fn soft_homing_sensor_already_active_rejects() {
2320 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2321 let mut view = MockView::new();
2322 enable_axis(&mut axis, &mut view, &mut client);
2323
2324 view.home_sensor = true;
2326 axis.tick(&mut view, &mut client); axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2329
2330 assert!(axis.is_error);
2332 assert!(axis.error_message.contains("already in trigger state"));
2333 assert_eq!(axis.op, AxisOp::Idle);
2334 }
2335
2336 #[test]
2337 fn soft_homing_negative_direction_sets_negative_target() {
2338 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2339 let mut view = MockView::new();
2340 enable_axis(&mut axis, &mut view, &mut client);
2341
2342 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
2343 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
2347 }
2348
2349 #[test]
2350 fn home_integrated_method_starts_hardware_homing() {
2351 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2352 let mut view = MockView::new();
2353 enable_axis(&mut axis, &mut view, &mut client);
2354
2355 axis.home(&mut view, HomingMethod::CurrentPosition);
2356 assert!(matches!(axis.op, AxisOp::Homing(0)));
2357 assert_eq!(axis.homing_method, 37);
2358 }
2359
2360 #[test]
2361 fn home_integrated_arbitrary_code() {
2362 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2363 let mut view = MockView::new();
2364 enable_axis(&mut axis, &mut view, &mut client);
2365
2366 axis.home(&mut view, HomingMethod::Integrated(35));
2367 assert!(matches!(axis.op, AxisOp::Homing(0)));
2368 assert_eq!(axis.homing_method, 35);
2369 }
2370
2371 #[test]
2372 fn hardware_homing_skips_speed_sdos_when_zero() {
2373 use mechutil::ipc::CommandMessage;
2374
2375 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
2376 let mut view = MockView::new();
2377 enable_axis(&mut axis, &mut view, &mut client);
2378
2379 axis.home(&mut view, HomingMethod::Integrated(37));
2381
2382 axis.tick(&mut view, &mut client);
2384 assert!(matches!(axis.op, AxisOp::Homing(1)));
2385
2386 let _ = write_rx.try_recv();
2388
2389 let tid = axis.homing_sdo_tid;
2391 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2392 client.poll();
2393 axis.tick(&mut view, &mut client);
2394
2395 assert!(matches!(axis.op, AxisOp::Homing(8)));
2397 }
2398
2399 #[test]
2400 fn hardware_homing_writes_speed_sdos_when_nonzero() {
2401 use mechutil::ipc::CommandMessage;
2402
2403 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
2404 let mut view = MockView::new();
2405 enable_axis(&mut axis, &mut view, &mut client);
2406
2407 axis.home(&mut view, HomingMethod::Integrated(37));
2409
2410 axis.tick(&mut view, &mut client);
2412 assert!(matches!(axis.op, AxisOp::Homing(1)));
2413 let _ = write_rx.try_recv();
2414
2415 let tid = axis.homing_sdo_tid;
2417 resp_tx.send(CommandMessage::response(tid, serde_json::json!(null))).unwrap();
2418 client.poll();
2419 axis.tick(&mut view, &mut client);
2420 assert!(matches!(axis.op, AxisOp::Homing(2)));
2422 }
2423
2424 #[test]
2425 fn soft_homing_edge_during_ack_step() {
2426 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
2427 let mut view = MockView::new();
2428 enable_axis(&mut axis, &mut view, &mut client);
2429
2430 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2431 axis.tick(&mut view, &mut client); view.home_sensor = true;
2435 view.position_actual = 2000;
2436 axis.tick(&mut view, &mut client);
2437
2438 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2440 }
2441
2442 #[test]
2443 fn soft_homing_applies_home_position() {
2444 let mut cfg = soft_homing_config();
2445 cfg.home_position = 90.0;
2446
2447 use tokio::sync::mpsc;
2448 let (write_tx, _write_rx) = mpsc::unbounded_channel();
2449 let (resp_tx, response_rx) = mpsc::unbounded_channel();
2450 let mut client = CommandClient::new(write_tx, response_rx);
2451 let mut axis = Axis::new(cfg, "TestDrive");
2452
2453 let mut view = MockView::new();
2454 enable_axis(&mut axis, &mut view, &mut client);
2455
2456 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2457
2458 axis.tick(&mut view, &mut client);
2460 view.status_word = 0x1027;
2461 axis.tick(&mut view, &mut client);
2462 view.status_word = 0x0027;
2463 axis.tick(&mut view, &mut client);
2464
2465 view.home_sensor = true;
2467 view.position_actual = 5000;
2468 axis.tick(&mut view, &mut client);
2469 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
2470
2471 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2473 |v| { v.home_sensor = false; });
2474
2475 assert_eq!(axis.home_offset, 0);
2477 }
2478
2479 #[test]
2480 fn soft_homing_default_home_position_zero() {
2481 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
2482 let mut view = MockView::new();
2483 enable_axis(&mut axis, &mut view, &mut client);
2484
2485 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
2486
2487 axis.tick(&mut view, &mut client);
2489 view.status_word = 0x1027;
2490 axis.tick(&mut view, &mut client);
2491 view.status_word = 0x0027;
2492 axis.tick(&mut view, &mut client);
2493
2494 view.home_sensor = true;
2496 view.position_actual = 5000;
2497 axis.tick(&mut view, &mut client);
2498
2499 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000,
2500 |v| { v.home_sensor = false; });
2501
2502 assert_eq!(axis.home_offset, 0);
2503 }
2504}