1use std::collections::VecDeque;
25use std::time::{Duration, Instant};
26
27use serde_json::json;
28use strum_macros::FromRepr;
29
30use super::axis_config::AxisConfig;
31use super::axis_view::AxisView;
32use super::cia402::{
33 Cia402Control, Cia402State, Cia402Status, ModesOfOperation, RawControlWord, RawStatusWord,
34};
35use super::homing::HomingMethod;
36use crate::command_client::CommandClient;
37use crate::ethercat::{SdoClient, SdoResult};
38use crate::fb::Ton;
39use crate::motion::FbSetModeOfOperation;
40
41#[derive(Debug, Clone, PartialEq)]
46enum AxisOp {
47 Idle,
48 Enabling(u8),
49 Disabling(u8),
50 Moving(MoveKind, u8, bool, bool),
51 Homing(u8),
52 SoftHoming(u8),
53 Halting(u8),
54 FaultRecovery(u8),
55}
56
57#[repr(u8)]
68#[derive(Debug, Clone, PartialEq, FromRepr)]
69enum HaltState {
70 WaitStopped = 0,
73 Flushing = 10,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq)]
84enum FlushPhase {
85 Idle,
86 Away,
88 Back,
90 Done,
91 Error,
92}
93
94#[derive(Debug, Clone)]
106struct FbFlushQueue {
107 phase: FlushPhase,
108 mode_fb: FbSetModeOfOperation,
109}
110
111impl FbFlushQueue {
112 fn new() -> Self {
113 Self { phase: FlushPhase::Idle, mode_fb: FbSetModeOfOperation::new() }
114 }
115
116 fn start(&mut self, view: &mut impl AxisView, client: &mut CommandClient, sdo: &mut SdoClient) {
118 self.begin_switch(view, client, sdo, ModesOfOperation::Homing);
119 self.phase = FlushPhase::Away;
120 }
121
122 fn begin_switch(
123 &mut self,
124 view: &mut impl AxisView,
125 client: &mut CommandClient,
126 sdo: &mut SdoClient,
127 mode: ModesOfOperation,
128 ) {
129 view.set_modes_of_operation(mode.as_i8());
130 self.mode_fb.reset();
131 self.mode_fb.start(mode.as_i8());
132 self.mode_fb.tick(client, sdo);
133 }
134
135 fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient, sdo: &mut SdoClient) {
136 match self.phase {
137 FlushPhase::Away => {
138 self.mode_fb.tick(client, sdo);
139 if !self.mode_fb.is_busy() {
140 if self.mode_fb.is_error() {
141 self.phase = FlushPhase::Error;
142 } else {
143 self.begin_switch(view, client, sdo, ModesOfOperation::ProfilePosition);
145 self.phase = FlushPhase::Back;
146 }
147 }
148 }
149 FlushPhase::Back => {
150 self.mode_fb.tick(client, sdo);
151 if !self.mode_fb.is_busy() {
152 self.phase = if self.mode_fb.is_error() {
153 FlushPhase::Error
154 } else {
155 FlushPhase::Done
156 };
157 }
158 }
159 _ => {}
160 }
161 }
162
163 fn is_busy(&self) -> bool {
164 matches!(self.phase, FlushPhase::Away | FlushPhase::Back)
165 }
166 fn is_error(&self) -> bool {
167 self.phase == FlushPhase::Error
168 }
169 fn error_code(&self) -> i32 {
170 self.mode_fb.error_code()
171 }
172 fn error_message(&self) -> String {
173 self.mode_fb.error_message()
174 }
175}
176
177const HALT_STAGE_TIMEOUT: Duration = Duration::from_secs(3);
179
180const HALT_STABLE_WINDOW: i32 = 50;
185
186const HALT_STOPPED_VELOCITY: i32 = 100;
192
193const HALT_STABLE_TICKS_REQUIRED: u8 = 5;
197
198#[repr(u8)]
199#[derive(Debug, Clone, PartialEq, FromRepr)]
200enum HomeState {
201 EnsurePpMode = 0,
202 WaitPpMode = 1,
203 Search = 5,
204 WaitSearching = 10,
205 WaitFoundSensor = 20,
206 WaitStoppedFoundSensor = 30,
207 FlushingFound = 40,
209 DebounceFoundSensor = 50,
210 BackOff = 60,
211 WaitBackingOff = 70,
212 WaitLostSensor = 80,
213 WaitStoppedLostSensor = 90,
214 FlushingLost = 100,
216 WaitHomeOffsetDone = 125,
217
218 WriteHomingModeOp = 160,
219 WaitWriteHomingModeOp = 165,
220
221 WriteHomingMethod = 205,
222 WaitWriteHomingMethodDone = 210,
223 ClearHomingTrigger = 215,
224 TriggerHoming = 217,
225 WaitHomingStarted = 218,
226 WaitHomingDone = 220,
227 ResetHomingTrigger = 222,
228 WaitHomingTriggerCleared = 223,
229 WriteMotionModeOfOperation = 230,
230 WaitWriteMotionModeOfOperation = 235,
231 SendCurrentPositionTarget = 240,
232 WaitCurrentPositionTargetSent = 245,
233}
234
235#[derive(Debug, Clone, PartialEq)]
236enum MoveKind {
237 Absolute,
238 Relative,
239}
240
241#[derive(Debug, Clone, Copy, PartialEq)]
242enum SoftHomeSensor {
243 PositiveLimit,
244 NegativeLimit,
245 HomeSensor,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq)]
249enum SoftHomeSensorType {
250 Pnp,
252 Npn,
254}
255
256const CW_SW_TRACE_CAP: usize = 48;
269
270#[derive(Clone)]
273struct TraceEntry {
274 seq: u32,
275 op: AxisOp,
276 cw: u16,
277 sw: u16,
278}
279
280fn decode_cw(cw: u16) -> String {
282 const BITS: [(u16, &str); 9] = [
283 (0, "SwOn"),
284 (1, "EnVolt"),
285 (2, "QStop"),
286 (3, "EnOp"),
287 (4, "NewSP"),
288 (5, "ChgSet"),
289 (6, "Rel"),
290 (7, "FltRst"),
291 (8, "HALT"),
292 ];
293 let f: Vec<&str> = BITS
294 .iter()
295 .filter(|(b, _)| cw & (1 << b) != 0)
296 .map(|(_, n)| *n)
297 .collect();
298 if f.is_empty() {
299 "-".to_string()
300 } else {
301 f.join("|")
302 }
303}
304
305fn decode_sw(sw: u16) -> String {
307 let mut f = vec![format!("{}", RawStatusWord(sw).state())];
308 for (bit, name) in [
309 (10, "TgtReached"),
310 (11, "IntLim"),
311 (12, "SP_ACK"),
312 (13, "FollowErr"),
313 ] {
314 if sw & (1 << bit) != 0 {
315 f.push(name.to_string());
316 }
317 }
318 f.join("|")
319}
320
321pub struct Axis {
322 config: AxisConfig,
323 sdo: SdoClient,
324
325 op: AxisOp,
327 home_offset: i32,
328 last_raw_position: i32,
329 op_started: Option<Instant>,
330 op_timeout: Duration,
331 homing_timeout: Duration,
332 move_start_timeout: Duration,
333 pending_move_target: i32,
334 pending_move_vel: u32,
335 pending_move_accel: u32,
336 pending_move_decel: u32,
337 homing_method: i8,
338 homing_sdo_tid: u32,
339 soft_home_sensor: SoftHomeSensor,
340 soft_home_sensor_type: SoftHomeSensorType,
341 soft_home_direction: f64,
342 halt_stable_count: u8,
343 prev_positive_limit: bool,
344 prev_negative_limit: bool,
345 prev_home_sensor: bool,
346
347 cw_sw_trace: VecDeque<TraceEntry>,
349 trace_seq: u32,
350
351 fb_mode_of_operation: FbSetModeOfOperation,
352 flush: FbFlushQueue,
355
356 pub is_error: bool,
359 pub error_code: u32,
361 pub error_message: String,
363 pub motor_on: bool,
365 pub is_busy: bool,
371 pub in_motion: bool,
373 pub moving_positive: bool,
375 pub moving_negative: bool,
377 pub position: f64,
379 pub raw_position: i64,
381 pub speed: f64,
383 pub at_max_limit: bool,
385 pub at_min_limit: bool,
387 pub at_positive_limit_switch: bool,
389 pub at_negative_limit_switch: bool,
391 pub home_sensor: bool,
393
394 ton: Ton,
396}
397
398impl Axis {
399 pub fn new(config: AxisConfig, device_name: &str) -> Self {
404 let op_timeout = Duration::from_secs_f64(config.operation_timeout_secs);
405 let homing_timeout = Duration::from_secs_f64(config.homing_timeout_secs);
406 let move_start_timeout = op_timeout; Self {
408 config,
409 sdo: SdoClient::new(device_name),
410 op: AxisOp::Idle,
411 home_offset: 0,
412 last_raw_position: 0,
413 op_started: None,
414 op_timeout,
415 homing_timeout,
416 move_start_timeout,
417 pending_move_target: 0,
418 pending_move_vel: 0,
419 pending_move_accel: 0,
420 pending_move_decel: 0,
421 homing_method: 37,
422 homing_sdo_tid: 0,
423 soft_home_sensor: SoftHomeSensor::HomeSensor,
424 soft_home_sensor_type: SoftHomeSensorType::Pnp,
425 soft_home_direction: 1.0,
426 halt_stable_count: 0,
427 prev_positive_limit: false,
428 prev_negative_limit: false,
429 prev_home_sensor: false,
430 cw_sw_trace: VecDeque::new(),
431 trace_seq: 0,
432 is_error: false,
433 error_code: 0,
434 error_message: String::new(),
435 motor_on: false,
436 is_busy: false,
437 in_motion: false,
438 moving_positive: false,
439 moving_negative: false,
440 position: 0.0,
441 raw_position: 0,
442 speed: 0.0,
443 at_max_limit: false,
444 at_min_limit: false,
445 at_positive_limit_switch: false,
446 at_negative_limit_switch: false,
447 home_sensor: false,
448 ton: Ton::new(),
449 fb_mode_of_operation: FbSetModeOfOperation::new(),
450 flush: FbFlushQueue::new(),
451 }
452 }
453
454 pub fn config(&self) -> &AxisConfig {
456 &self.config
457 }
458
459 pub fn move_absolute(
469 &mut self,
470 view: &mut impl AxisView,
471 target: f64,
472 vel: f64,
473 accel: f64,
474 decel: f64,
475 ) {
476 if let Some(msg) = self.check_target_limit(target, view) {
477 self.set_op_error(&msg);
478 return;
479 }
480
481 let cpu = self.config.counts_per_user();
482 let raw_target = self.config.to_counts(target).round() as i32 + self.home_offset;
483 let raw_vel = (vel * cpu).round() as u32;
484 let raw_accel = (accel * cpu).round() as u32;
485 let raw_decel = (decel * cpu).round() as u32;
486
487 let user_pos = target > self.position;
489 let user_neg = target < self.position;
490
491 self.start_move(
492 view,
493 raw_target,
494 raw_vel,
495 raw_accel,
496 raw_decel,
497 MoveKind::Absolute,
498 user_pos,
499 user_neg,
500 );
501 }
502
503 pub fn move_relative(
509 &mut self,
510 view: &mut impl AxisView,
511 distance: f64,
512 vel: f64,
513 accel: f64,
514 decel: f64,
515 ) {
516 log::info!(
517 "Axis: request to move relative dist {} vel {} accel {} decel {}",
518 distance,
519 vel,
520 accel,
521 decel
522 );
523
524 if let Some(msg) = self.check_target_limit(self.position + distance, view) {
525 self.set_op_error(&msg);
526 return;
527 }
528
529 let cpu = self.config.counts_per_user();
530 let raw_distance = self.config.to_counts(distance).round() as i32;
531 let raw_vel = (vel * cpu).round() as u32;
532 let raw_accel = (accel * cpu).round() as u32;
533 let raw_decel = (decel * cpu).round() as u32;
534
535 log::info!("Axis starting relative move: request to move relative raw dist {} raw vel {} raw accel {} raw decel {}",
536 raw_distance, raw_vel, raw_accel, raw_decel
537 );
538
539 let mut cw = RawControlWord(view.control_word());
541 cw.set_bit(4, false); view.set_control_word(cw.raw());
543
544 let user_pos = distance > 0.0;
546 let user_neg = distance < 0.0;
547
548 self.start_move(
549 view,
550 raw_distance,
551 raw_vel,
552 raw_accel,
553 raw_decel,
554 MoveKind::Relative,
555 user_pos,
556 user_neg,
557 );
558 }
559
560 fn start_move(
561 &mut self,
562 view: &mut impl AxisView,
563 raw_target: i32,
564 raw_vel: u32,
565 raw_accel: u32,
566 raw_decel: u32,
567 kind: MoveKind,
568 user_pos: bool,
569 user_neg: bool,
570 ) {
571 self.pending_move_target = raw_target;
572 self.pending_move_vel = raw_vel;
573 self.pending_move_accel = raw_accel;
574 self.pending_move_decel = raw_decel;
575
576 view.set_target_position(raw_target);
578 view.set_profile_velocity(raw_vel);
579 view.set_profile_acceleration(raw_accel);
580 view.set_profile_deceleration(raw_decel);
581
582 let mut cw = RawControlWord(view.control_word());
587 cw.set_bit(6, kind == MoveKind::Relative);
588 cw.set_bit(8, false); cw.set_bit(4, true); view.set_control_word(cw.raw());
591
592 self.op = AxisOp::Moving(kind, 1, user_pos, user_neg);
597 self.op_started = Some(Instant::now());
598 }
599
600 pub fn halt(&mut self, view: &mut impl AxisView) {
618 self.command_halt(view);
619 self.halt_stable_count = 0;
620 self.last_raw_position = view.position_actual();
621 self.op_started = Some(Instant::now());
622 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
623 }
624
625 pub fn enable(&mut self, view: &mut impl AxisView) {
633 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
635 let mut cw = RawControlWord(view.control_word());
636 cw.cmd_shutdown();
637 view.set_control_word(cw.raw());
638
639 self.op = AxisOp::Enabling(1);
640 self.op_started = Some(Instant::now());
641 }
642
643 pub fn disable(&mut self, view: &mut impl AxisView) {
652 let mut cw = RawControlWord(view.control_word());
653 cw.cmd_shutdown();
654 cw.set_bit(4, false);
655 cw.set_bit(8, false);
656 cw.set_bit(7, false);
657 cw.set_bit(2, true);
658 view.set_control_word(cw.raw());
659
660 self.op = AxisOp::Disabling(1);
661 self.op_started = Some(Instant::now());
662 }
663
664 pub fn reset_faults(&mut self, view: &mut impl AxisView) {
680 let sw = RawStatusWord(view.status_word());
681 let drive_in_fault = matches!(
682 sw.state(),
683 Cia402State::Fault | Cia402State::FaultReactionActive
684 );
685
686 self.is_error = false;
687 self.error_code = 0;
688 self.error_message.clear();
689
690 if drive_in_fault {
691 let mut cw = RawControlWord(view.control_word());
693 cw.cmd_clear_fault_reset();
694 view.set_control_word(cw.raw());
695 self.op = AxisOp::FaultRecovery(1);
696 self.op_started = Some(Instant::now());
697 } else {
698 self.op = AxisOp::Idle;
700 self.op_started = None;
701 self.is_busy = false;
702 self.in_motion = false;
703 }
704 }
705
706 pub fn home(&mut self, view: &mut impl AxisView, method: HomingMethod) {
714 if method.is_integrated() {
715 self.homing_method = match method {
716 HomingMethod::CurrentPosition => self.config.soft_home_method,
717 _ => method.cia402_code(),
718 };
719 self.op = AxisOp::Homing(0);
720 self.op_started = Some(Instant::now());
721 let _ = view;
722 } else {
723 self.configure_soft_homing(method);
724 self.start_soft_homing(view);
725 }
726 }
727
728 pub fn set_position(&mut self, view: &impl AxisView, user_units: f64) {
737 self.home_offset =
738 view.position_actual() - self.config.to_counts(user_units).round() as i32;
739 }
740
741 pub fn set_position_persistent(&mut self, client: &mut CommandClient, user_units: f64) {
758 self.config.home_position = user_units;
765 let desired_counts = self.config.to_counts(user_units).round() as i32;
766 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
767 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
768 self.op_started = Some(Instant::now());
769 log::info!(
770 "set_position_persistent: writing 0x607C = {} counts ({} user units)",
771 desired_counts,
772 user_units
773 );
774 }
775
776 pub fn set_home_position(&mut self, user_units: f64) {
780 self.config.home_position = user_units;
781 }
782
783 pub fn set_software_max_limit(&mut self, user_units: f64) {
785 self.config.max_position_limit = user_units;
786 self.config.enable_max_position_limit = true;
787 }
788
789 pub fn set_software_min_limit(&mut self, user_units: f64) {
791 self.config.min_position_limit = user_units;
792 self.config.enable_min_position_limit = true;
793 }
794
795 pub fn sdo_write(
801 &mut self,
802 client: &mut CommandClient,
803 index: u16,
804 sub_index: u8,
805 value: serde_json::Value,
806 ) {
807 self.sdo.write(client, index, sub_index, value);
808 }
809
810 pub fn sdo_read(&mut self, client: &mut CommandClient, index: u16, sub_index: u8) -> u32 {
812 self.sdo.read(client, index, sub_index)
813 }
814
815 pub fn sdo_result(&mut self, client: &mut CommandClient, tid: u32) -> SdoResult {
817 self.sdo.result(client, tid, Duration::from_secs(5))
818 }
819
820 pub fn tick(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
834 self.check_faults(view);
835 self.progress_op(view, client);
836 self.update_outputs(view);
837 self.check_limits(view);
838 self.record_trace(view);
839 }
840
841 fn record_trace(&mut self, view: &impl AxisView) {
849 let cw = view.control_word();
850 let sw = view.status_word();
851 let changed = self
852 .cw_sw_trace
853 .back()
854 .map_or(true, |e| e.cw != cw || e.sw != sw || e.op != self.op);
855 if !changed {
856 return;
857 }
858 if self.cw_sw_trace.len() >= CW_SW_TRACE_CAP {
859 self.cw_sw_trace.pop_front();
860 }
861 self.cw_sw_trace.push_back(TraceEntry {
862 seq: self.trace_seq,
863 op: self.op.clone(),
864 cw,
865 sw,
866 });
867 self.trace_seq = self.trace_seq.wrapping_add(1);
868 }
869
870 pub fn trace(&self) -> String {
881 let mut out = String::new();
882 for e in &self.cw_sw_trace {
883 out.push_str(&format!(
884 " [{:>4}] {:<20} CW={:#06x} [{}] SW={:#06x} [{}]\n",
885 e.seq,
886 format!("{:?}", e.op),
887 e.cw,
888 decode_cw(e.cw),
889 e.sw,
890 decode_sw(e.sw),
891 ));
892 }
893 out
894 }
895
896 fn update_outputs(&mut self, view: &impl AxisView) {
901 let raw = view.position_actual();
902 self.raw_position = raw as i64;
903 self.position = self.config.to_user((raw - self.home_offset) as f64);
904
905 let vel = view.velocity_actual();
906 let user_vel = self.config.to_user(vel as f64);
907 self.speed = user_vel.abs();
908 self.moving_positive = user_vel > 0.0;
909 self.moving_negative = user_vel < 0.0;
910 self.is_busy = self.op != AxisOp::Idle;
911 self.in_motion = matches!(self.op, AxisOp::Moving(_, _, _, _) | AxisOp::SoftHoming(_));
912
913 let sw = RawStatusWord(view.status_word());
914 self.motor_on = sw.state() == Cia402State::OperationEnabled;
915
916 self.last_raw_position = raw;
917 }
918
919 fn check_faults(&mut self, view: &impl AxisView) {
924 let sw = RawStatusWord(view.status_word());
925 let state = sw.state();
926
927 if matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
928 if !matches!(self.op, AxisOp::FaultRecovery(_)) {
929 self.is_error = true;
930 let ec = view.error_code();
931 if ec != 0 {
932 self.error_code = ec as u32;
933 }
934 self.error_message = format!("Drive fault (state: {})", state);
935 self.op = AxisOp::Idle;
937 self.op_started = None;
938 }
939 }
940 }
941
942 fn op_timed_out(&self) -> bool {
947 self.op_started
948 .map_or(false, |t| t.elapsed() > self.op_timeout)
949 }
950
951 fn homing_timed_out(&self) -> bool {
952 self.op_started
953 .map_or(false, |t| t.elapsed() > self.homing_timeout)
954 }
955
956 fn move_start_timed_out(&self) -> bool {
957 self.op_started
958 .map_or(false, |t| t.elapsed() > self.move_start_timeout)
959 }
960
961 fn op_stage_timed_out(&self, limit: Duration) -> bool {
965 self.op_started.map_or(false, |t| t.elapsed() > limit)
966 }
967
968 fn set_op_error(&mut self, msg: &str) {
969 self.is_error = true;
970 self.error_message = msg.to_string();
971 log::error!("Axis error: {}", msg);
972 if !self.cw_sw_trace.is_empty() {
976 log::warn!(
977 "Axis CiA-402 trace ({} recent transitions, oldest first):\n{}",
978 self.cw_sw_trace.len(),
979 self.trace(),
980 );
981 }
982 self.op = AxisOp::Idle;
983 self.op_started = None;
984 self.is_busy = false;
985 self.in_motion = false;
986 }
987
988 fn restore_pp_after_error(&mut self, msg: &str) {
989 self.is_error = true;
990 self.error_message = msg.to_string();
991 self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
992 log::error!("Axis error: {}", msg);
993 }
994
995 fn finish_op_error(&mut self) {
996 self.op = AxisOp::Idle;
997 self.op_started = None;
998 self.is_busy = false;
999 self.in_motion = false;
1000 }
1001
1002 fn complete_op(&mut self) {
1003 self.op = AxisOp::Idle;
1004 self.op_started = None;
1005 self.is_busy = false;
1010 self.in_motion = false;
1011 }
1012
1013 fn effective_max_limit(&self, view: &impl AxisView) -> Option<f64> {
1022 let static_limit = if self.config.enable_max_position_limit {
1023 Some(self.config.max_position_limit)
1024 } else {
1025 None
1026 };
1027 match (static_limit, view.dynamic_max_position_limit()) {
1028 (Some(s), Some(d)) => Some(s.min(d)),
1029 (Some(v), None) | (None, Some(v)) => Some(v),
1030 (None, None) => None,
1031 }
1032 }
1033
1034 fn effective_min_limit(&self, view: &impl AxisView) -> Option<f64> {
1038 let static_limit = if self.config.enable_min_position_limit {
1039 Some(self.config.min_position_limit)
1040 } else {
1041 None
1042 };
1043 match (static_limit, view.dynamic_min_position_limit()) {
1044 (Some(s), Some(d)) => Some(s.max(d)),
1045 (Some(v), None) | (None, Some(v)) => Some(v),
1046 (None, None) => None,
1047 }
1048 }
1049
1050 fn check_target_limit(&self, target: f64, view: &impl AxisView) -> Option<String> {
1055 if let Some(max) = self.effective_max_limit(view) {
1056 if target > max {
1057 return Some(format!(
1058 "Target {:.3} exceeds max software limit {:.3}",
1059 target, max
1060 ));
1061 }
1062 }
1063 if let Some(min) = self.effective_min_limit(view) {
1064 if target < min {
1065 return Some(format!(
1066 "Target {:.3} exceeds min software limit {:.3}",
1067 target, min
1068 ));
1069 }
1070 }
1071 None
1072 }
1073
1074 fn check_limits(&mut self, view: &mut impl AxisView) {
1083 let eff_max = self.effective_max_limit(view);
1085 let eff_min = self.effective_min_limit(view);
1086 let sw_max = eff_max.map_or(false, |m| self.position >= m);
1087 let sw_min = eff_min.map_or(false, |m| self.position <= m);
1088
1089 self.at_max_limit = sw_max;
1090 self.at_min_limit = sw_min;
1091
1092 let hw_pos = view.positive_limit_active();
1094 let hw_neg = view.negative_limit_active();
1095
1096 self.at_positive_limit_switch = hw_pos;
1097 self.at_negative_limit_switch = hw_neg;
1098
1099 self.home_sensor = view.home_sensor_active();
1101
1102 self.prev_positive_limit = hw_pos;
1104 self.prev_negative_limit = hw_neg;
1105 self.prev_home_sensor = view.home_sensor_active();
1106
1107 let mut commanded_positive = false;
1109 let mut commanded_negative = false;
1110
1111 let is_moving = matches!(self.op, AxisOp::Moving(_, _, _, _));
1112 let is_soft_homing = matches!(self.op, AxisOp::SoftHoming(_));
1113
1114 if !is_moving && !is_soft_homing {
1115 return; }
1117
1118 match &self.op {
1119 AxisOp::Moving(_, _, pos, neg) => {
1120 commanded_positive = *pos;
1122 commanded_negative = *neg;
1123 }
1124 AxisOp::SoftHoming(_) => match self.soft_home_sensor {
1125 SoftHomeSensor::PositiveLimit => commanded_positive = true,
1126 SoftHomeSensor::NegativeLimit => commanded_negative = true,
1127 SoftHomeSensor::HomeSensor => {
1128 commanded_positive = self.moving_positive;
1129 commanded_negative = self.moving_negative;
1130 }
1131 },
1132 _ => {}
1133 }
1134
1135 let suppress_pos = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::PositiveLimit;
1137 let suppress_neg = is_soft_homing && self.soft_home_sensor == SoftHomeSensor::NegativeLimit;
1138
1139 let effective_hw_pos = hw_pos && !suppress_pos;
1140 let effective_hw_neg = hw_neg && !suppress_neg;
1141
1142 let effective_sw_max = sw_max && !is_soft_homing;
1144 let effective_sw_min = sw_min && !is_soft_homing;
1145
1146 let positive_blocked = (effective_sw_max || effective_hw_pos) && commanded_positive;
1147 let negative_blocked = (effective_sw_min || effective_hw_neg) && commanded_negative;
1148
1149 if positive_blocked || negative_blocked {
1150 let mut cw = RawControlWord(view.control_word());
1151 cw.set_bit(8, true); view.set_control_word(cw.raw());
1153
1154 let msg = if effective_hw_pos && commanded_positive {
1155 "Positive limit switch active".to_string()
1156 } else if effective_hw_neg && commanded_negative {
1157 "Negative limit switch active".to_string()
1158 } else if effective_sw_max && commanded_positive {
1159 format!(
1160 "Software position limit: position {:.3} >= max {:.3}",
1161 self.position,
1162 eff_max.unwrap_or(self.position)
1163 )
1164 } else {
1165 format!(
1166 "Software position limit: position {:.3} <= min {:.3}",
1167 self.position,
1168 eff_min.unwrap_or(self.position)
1169 )
1170 };
1171
1172 if is_soft_homing {
1173 self.set_op_error(&msg);
1176 } else {
1177 log::info!("Axis move halted by limit: {}", msg);
1190 self.command_halt(view);
1191 self.halt_stable_count = 0;
1192 self.last_raw_position = view.position_actual();
1193 self.op_started = Some(Instant::now());
1194 self.op = AxisOp::Halting(HaltState::WaitStopped as u8);
1195 }
1196 }
1197 }
1198
1199 fn progress_op(&mut self, view: &mut impl AxisView, client: &mut CommandClient) {
1204 match self.op.clone() {
1205 AxisOp::Idle => {}
1206 AxisOp::Enabling(step) => self.tick_enabling(view, step),
1207 AxisOp::Disabling(step) => self.tick_disabling(view, step),
1208 AxisOp::Moving(kind, step, pos, neg) => self.tick_moving(view, kind, step, pos, neg),
1209 AxisOp::Homing(step) => self.tick_homing(view, client, step),
1210 AxisOp::SoftHoming(step) => self.tick_soft_homing(view, client, step),
1211 AxisOp::Halting(step) => self.tick_halting(view, client, step),
1212 AxisOp::FaultRecovery(step) => self.tick_fault_recovery(view, step),
1213 }
1214 }
1215
1216 fn tick_enabling(&mut self, view: &mut impl AxisView, step: u8) {
1221 match step {
1222 1 => {
1223 let sw = RawStatusWord(view.status_word());
1224 if sw.state() == Cia402State::ReadyToSwitchOn {
1225 let mut cw = RawControlWord(view.control_word());
1226 cw.cmd_enable_operation();
1227 view.set_control_word(cw.raw());
1228 self.op = AxisOp::Enabling(2);
1229 } else if self.op_timed_out() {
1230 self.set_op_error("Enable timeout: waiting for ReadyToSwitchOn");
1231 }
1232 }
1233 2 => {
1234 let sw = RawStatusWord(view.status_word());
1235 if sw.state() == Cia402State::OperationEnabled {
1236 self.complete_op();
1243 } else if self.op_timed_out() {
1244 self.set_op_error("Enable timeout: waiting for OperationEnabled");
1245 }
1246 }
1247 _ => self.complete_op(),
1248 }
1249 }
1250
1251 fn tick_disabling(&mut self, view: &mut impl AxisView, step: u8) {
1255 match step {
1256 1 => {
1257 let sw = RawStatusWord(view.status_word());
1258 if sw.state() != Cia402State::OperationEnabled {
1259 self.complete_op();
1260 } else if self.op_timed_out() {
1261 self.set_op_error("Disable timeout: drive still in OperationEnabled");
1262 }
1263 }
1264 _ => self.complete_op(),
1265 }
1266 }
1267
1268 fn tick_moving(
1274 &mut self,
1275 view: &mut impl AxisView,
1276 kind: MoveKind,
1277 step: u8,
1278 pos: bool,
1279 neg: bool,
1280 ) {
1281 match step {
1282 1 => {
1283 let sw = RawStatusWord(view.status_word());
1285 if sw.raw() & (1 << 12) != 0 {
1286 let mut cw = RawControlWord(view.control_word());
1288 cw.set_bit(4, false);
1289 view.set_control_word(cw.raw());
1290 self.op = AxisOp::Moving(kind, 2, pos, neg);
1291 } else if self.move_start_timed_out() {
1292 self.set_op_error("Move timeout: set-point not acknowledged");
1293 }
1294 }
1295 2 => {
1296 let sw = RawStatusWord(view.status_word());
1298 if sw.raw() & (1 << 12) == 0 {
1299 self.op = AxisOp::Moving(kind, 3, pos, neg);
1301 }
1302 }
1303 3 => {
1304 let sw = RawStatusWord(view.status_word());
1306 if sw.target_reached() {
1307 self.complete_op();
1308 }
1309 }
1310 _ => self.complete_op(),
1311 }
1312 }
1313
1314 fn tick_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1340 match step {
1341 0 => {
1342 self.homing_sdo_tid = self.sdo.write(client, 0x6098, 0, json!(self.homing_method));
1344 self.op = AxisOp::Homing(1);
1345 }
1346 1 => {
1347 match self
1349 .sdo
1350 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1351 {
1352 SdoResult::Ok(_) => {
1353 if self.config.homing_speed == 0.0 && self.config.homing_accel == 0.0 {
1355 self.op = AxisOp::Homing(8);
1356 } else {
1357 self.op = AxisOp::Homing(2);
1358 }
1359 }
1360 SdoResult::Pending => {
1361 if self.homing_timed_out() {
1362 self.set_op_error("Homing timeout: SDO write for homing method");
1363 }
1364 }
1365 SdoResult::Err(e) => {
1366 self.set_op_error(&format!("Homing SDO error: {}", e));
1367 }
1368 SdoResult::Timeout => {
1369 self.set_op_error("Homing timeout: SDO write timed out");
1370 }
1371 }
1372 }
1373 2 => {
1374 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1376 self.homing_sdo_tid = self.sdo.write(client, 0x6099, 1, json!(speed_counts));
1377 self.op = AxisOp::Homing(3);
1378 }
1379 3 => {
1380 match self
1381 .sdo
1382 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1383 {
1384 SdoResult::Ok(_) => {
1385 self.op = AxisOp::Homing(4);
1386 }
1387 SdoResult::Pending => {
1388 if self.homing_timed_out() {
1389 self.set_op_error(
1390 "Homing timeout: SDO write for homing speed (switch)",
1391 );
1392 }
1393 }
1394 SdoResult::Err(e) => {
1395 self.set_op_error(&format!("Homing SDO error: {}", e));
1396 }
1397 SdoResult::Timeout => {
1398 self.set_op_error("Homing timeout: SDO write timed out");
1399 }
1400 }
1401 }
1402 4 => {
1403 let speed_counts = self.config.to_counts(self.config.homing_speed).round() as u32;
1405 self.homing_sdo_tid = self.sdo.write(client, 0x6099, 2, json!(speed_counts));
1406 self.op = AxisOp::Homing(5);
1407 }
1408 5 => {
1409 match self
1410 .sdo
1411 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1412 {
1413 SdoResult::Ok(_) => {
1414 self.op = AxisOp::Homing(6);
1415 }
1416 SdoResult::Pending => {
1417 if self.homing_timed_out() {
1418 self.set_op_error("Homing timeout: SDO write for homing speed (zero)");
1419 }
1420 }
1421 SdoResult::Err(e) => {
1422 self.set_op_error(&format!("Homing SDO error: {}", e));
1423 }
1424 SdoResult::Timeout => {
1425 self.set_op_error("Homing timeout: SDO write timed out");
1426 }
1427 }
1428 }
1429 6 => {
1430 let accel_counts = self.config.to_counts(self.config.homing_accel).round() as u32;
1432 self.homing_sdo_tid = self.sdo.write(client, 0x609A, 0, json!(accel_counts));
1433 self.op = AxisOp::Homing(7);
1434 }
1435 7 => {
1436 match self
1437 .sdo
1438 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
1439 {
1440 SdoResult::Ok(_) => {
1441 self.op = AxisOp::Homing(8);
1442 }
1443 SdoResult::Pending => {
1444 if self.homing_timed_out() {
1445 self.set_op_error("Homing timeout: SDO write for homing acceleration");
1446 }
1447 }
1448 SdoResult::Err(e) => {
1449 self.set_op_error(&format!("Homing SDO error: {}", e));
1450 }
1451 SdoResult::Timeout => {
1452 self.set_op_error("Homing timeout: SDO write timed out");
1453 }
1454 }
1455 }
1456 8 => {
1457 view.set_modes_of_operation(ModesOfOperation::Homing.as_i8());
1467 let mut cw = RawControlWord(view.control_word());
1469 cw.set_bit(4, false);
1470 view.set_control_word(cw.raw());
1471 self.fb_mode_of_operation.reset();
1472 self.fb_mode_of_operation
1473 .start(ModesOfOperation::Homing as i8);
1474 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1475 self.op = AxisOp::Homing(9);
1476 }
1477 9 => {
1478 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1480 if !self.fb_mode_of_operation.is_busy() {
1481 if self.fb_mode_of_operation.is_error() {
1482 self.set_op_error(&format!(
1483 "Homing: failed to enter Homing mode: {} {}",
1484 self.fb_mode_of_operation.error_code(),
1485 self.fb_mode_of_operation.error_message()
1486 ));
1487 } else {
1488 self.op = AxisOp::Homing(10);
1489 }
1490 }
1491 }
1492 10 => {
1493 log::info!(
1498 "Homing[trigger]: method={} position_actual(0x6064) BEFORE = {}",
1499 self.homing_method,
1500 view.position_actual(),
1501 );
1502 let mut cw = RawControlWord(view.control_word());
1503 cw.set_bit(4, true);
1504 view.set_control_word(cw.raw());
1505 self.op = AxisOp::Homing(11);
1506 }
1507 11 => {
1508 let sw = view.status_word();
1520 if sw & (1 << 13) != 0 {
1521 self.set_op_error("Homing error: drive reported homing failure");
1522 } else {
1523 self.op = AxisOp::Homing(12);
1524 }
1525 }
1526 12 => {
1527 let sw = view.status_word();
1535 let error = sw & (1 << 13) != 0;
1536 let attained = sw & (1 << 12) != 0;
1537
1538 if error {
1539 self.set_op_error("Homing error: drive reported homing failure");
1540 } else if attained {
1541 self.op = AxisOp::Homing(13);
1542 } else if self.homing_timed_out() {
1543 self.set_op_error(&format!(
1544 "Homing timeout: procedure did not complete (sw=0x{:04X})",
1545 sw
1546 ));
1547 }
1548 }
1549 13 => {
1550 self.home_offset = view.position_actual()
1553 - self.config.to_counts(self.config.home_position).round() as i32;
1554 log::info!(
1560 "Homing[done]: position_actual(0x6064) AFTER = {}, home_position = {} counts, software home_offset = {}",
1561 view.position_actual(),
1562 self.config.to_counts(self.config.home_position).round() as i32,
1563 self.home_offset,
1564 );
1565 let mut cw = RawControlWord(view.control_word());
1567 cw.set_bit(4, false);
1568 view.set_control_word(cw.raw());
1569 self.op = AxisOp::Homing(14);
1570 }
1571 14 => {
1572 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1578 self.fb_mode_of_operation.reset();
1579 self.fb_mode_of_operation
1580 .start(ModesOfOperation::ProfilePosition as i8);
1581 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1582 self.op = AxisOp::Homing(15);
1583 }
1584 15 => {
1585 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1587 if !self.fb_mode_of_operation.is_busy() {
1588 if self.fb_mode_of_operation.is_error() {
1589 self.set_op_error(&format!(
1590 "Homing: failed to restore PP mode: {} {}",
1591 self.fb_mode_of_operation.error_code(),
1592 self.fb_mode_of_operation.error_message()
1593 ));
1594 } else {
1595 log::info!("Homing complete — home offset: {}", self.home_offset);
1596 self.complete_op();
1597 }
1598 }
1599 }
1600 _ => self.complete_op(),
1601 }
1602 }
1603
1604 fn configure_soft_homing(&mut self, method: HomingMethod) {
1607 match method {
1608 HomingMethod::LimitSwitchPosPnp => {
1609 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1610 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1611 self.soft_home_direction = 1.0;
1612 }
1613 HomingMethod::LimitSwitchNegPnp => {
1614 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1615 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1616 self.soft_home_direction = -1.0;
1617 }
1618 HomingMethod::LimitSwitchPosNpn => {
1619 self.soft_home_sensor = SoftHomeSensor::PositiveLimit;
1620 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1621 self.soft_home_direction = 1.0;
1622 }
1623 HomingMethod::LimitSwitchNegNpn => {
1624 self.soft_home_sensor = SoftHomeSensor::NegativeLimit;
1625 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1626 self.soft_home_direction = -1.0;
1627 }
1628 HomingMethod::HomeSensorPosPnp => {
1629 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1630 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1631 self.soft_home_direction = 1.0;
1632 }
1633 HomingMethod::HomeSensorNegPnp => {
1634 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1635 self.soft_home_sensor_type = SoftHomeSensorType::Pnp;
1636 self.soft_home_direction = -1.0;
1637 }
1638 HomingMethod::HomeSensorPosNpn => {
1639 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1640 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1641 self.soft_home_direction = 1.0;
1642 }
1643 HomingMethod::HomeSensorNegNpn => {
1644 self.soft_home_sensor = SoftHomeSensor::HomeSensor;
1645 self.soft_home_sensor_type = SoftHomeSensorType::Npn;
1646 self.soft_home_direction = -1.0;
1647 }
1648 _ => {} }
1650 }
1651
1652 fn start_soft_homing(&mut self, view: &mut impl AxisView) {
1653 self.op = AxisOp::SoftHoming(HomeState::EnsurePpMode as u8);
1654 self.op_started = Some(Instant::now());
1655 }
1656
1657 fn check_soft_home_trigger(&self, view: &impl AxisView) -> bool {
1658 let raw = match self.soft_home_sensor {
1659 SoftHomeSensor::PositiveLimit => view.positive_limit_active(),
1660 SoftHomeSensor::NegativeLimit => view.negative_limit_active(),
1661 SoftHomeSensor::HomeSensor => view.home_sensor_active(),
1662 };
1663 match self.soft_home_sensor_type {
1664 SoftHomeSensorType::Pnp => raw, SoftHomeSensorType::Npn => !raw, }
1667 }
1668
1669 fn calculate_max_relative_target(&self, direction: f64) -> i32 {
1672 let dir = if !self.config.invert_direction {
1673 direction
1674 } else {
1675 -direction
1676 };
1677
1678 let target = if dir > 0.0 { i32::MAX } else { i32::MIN };
1679
1680 return target;
1681 }
1682
1683 pub fn command_halt(&self, view: &mut impl AxisView) {
1687 let mut cw = RawControlWord(view.control_word());
1688 cw.set_bit(8, true); cw.set_bit(4, false); cw.set_bit(5, false);
1691 cw.set_bit(6, false); view.set_control_word(cw.raw());
1693 }
1694
1695 fn command_homing_speed(&self, view: &mut impl AxisView) {
1697 let cpu = self.config.counts_per_user();
1698 let vel = (self.config.homing_speed * cpu).round() as u32;
1699 let accel = (self.config.homing_accel * cpu).round() as u32;
1700 let decel = (self.config.homing_decel * cpu).round() as u32;
1701 view.set_profile_velocity(vel);
1702 view.set_profile_acceleration(accel);
1703 view.set_profile_deceleration(decel);
1704 }
1705
1706 fn tick_soft_homing(&mut self, view: &mut impl AxisView, client: &mut CommandClient, step: u8) {
1722 match HomeState::from_repr(step) {
1723 Some(HomeState::EnsurePpMode) => {
1724 log::info!("SoftHome: Ensuring PP mode..");
1729 self.fb_mode_of_operation
1730 .start(ModesOfOperation::ProfilePosition as i8);
1731 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1732 self.op = AxisOp::SoftHoming(HomeState::WaitPpMode as u8);
1733 }
1734 Some(HomeState::WaitPpMode) => {
1735 self.fb_mode_of_operation.tick(client, &mut self.sdo);
1736 if !self.fb_mode_of_operation.is_busy() {
1737 if self.fb_mode_of_operation.is_error() {
1738 self.set_op_error(&format!(
1739 "Software homing SDO error writing homing mode of operation: {} {}",
1740 self.fb_mode_of_operation.error_code(),
1741 self.fb_mode_of_operation.error_message()
1742 ));
1743 } else {
1744 log::info!("SoftHome: Drive is in PP mode!");
1745
1746 if !self.check_soft_home_trigger(view) {
1750 log::info!("SoftHome: Not on home switch; seek out.");
1751 self.op = AxisOp::SoftHoming(HomeState::Search as u8);
1752 } else {
1753 log::info!("SoftHome: Already on home switch, skipping ahead to back-off stage.");
1754 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1755 }
1756 }
1757 }
1758 }
1759
1760 Some(HomeState::Search) => {
1762 view.set_modes_of_operation(ModesOfOperation::ProfilePosition.as_i8());
1763
1764 let target = self.calculate_max_relative_target(self.soft_home_direction);
1773 view.set_target_position(target);
1774
1775 self.command_homing_speed(view);
1784
1785 let mut cw = RawControlWord(view.control_word());
1786 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());
1791
1792 log::info!(
1793 "SoftHome[0]: SEARCH relative target={} vel={} dir={} pos={}",
1794 target,
1795 self.config.homing_speed,
1796 self.soft_home_direction,
1797 view.position_actual()
1798 );
1799 self.op = AxisOp::SoftHoming(HomeState::WaitSearching as u8);
1800 }
1801 Some(HomeState::WaitSearching) => {
1802 if self.check_soft_home_trigger(view) {
1803 log::debug!("SoftHome[1]: sensor triggered during ack wait");
1804 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1805 return;
1806 }
1807 let sw = RawStatusWord(view.status_word());
1808 if sw.raw() & (1 << 12) != 0 {
1809 let mut cw = RawControlWord(view.control_word());
1810 cw.set_bit(4, false);
1811 view.set_control_word(cw.raw());
1812 log::debug!("SoftHome[1]: set-point ack received, clearing bit 4");
1813 self.op = AxisOp::SoftHoming(HomeState::WaitFoundSensor as u8);
1814 } else if self.homing_timed_out() {
1815 self.set_op_error("Software homing timeout: set-point not acknowledged");
1816 }
1817 }
1818 Some(HomeState::WaitFoundSensor) => {
1828 if self.check_soft_home_trigger(view) {
1829 log::info!(
1830 "SoftHome[3]: sensor triggered at pos={}. HALTING",
1831 view.position_actual()
1832 );
1833 log::info!("ControlWord is : {} ", view.control_word());
1834
1835 let mut cw = RawControlWord(view.control_word());
1836 cw.set_bit(8, true); cw.set_bit(4, false); view.set_control_word(cw.raw());
1839
1840 self.halt_stable_count = 0;
1841 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedFoundSensor as u8);
1842 } else if self.homing_timed_out() {
1843 self.set_op_error("Software homing timeout: sensor not detected");
1844 }
1845 }
1846
1847 Some(HomeState::WaitStoppedFoundSensor) => {
1848 const STABLE_WINDOW: i32 = 1;
1849 const STABLE_TICKS_REQUIRED: u8 = 10;
1850
1851 let pos = view.position_actual();
1856 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1857 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1858 } else {
1859 self.halt_stable_count = 0;
1860 }
1861
1862 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1863 log::info!(
1864 "SoftHome[5] motor is stopped. Cancel move and wait for bit 12 go true."
1865 );
1866 self.flush.start(view, client, &mut self.sdo);
1867 self.op = AxisOp::SoftHoming(HomeState::FlushingFound as u8);
1868 } else if self.homing_timed_out() {
1869 self.set_op_error(
1870 "Software homing timeout: motor did not stop after sensor trigger",
1871 );
1872 }
1873 }
1874 Some(HomeState::FlushingFound) => {
1875 self.flush.tick(view, client, &mut self.sdo);
1876 if self.flush.is_busy() {
1877 if self.homing_timed_out() {
1878 self.set_op_error("Software homing timeout: queue flush did not complete");
1879 }
1880 return;
1881 }
1882 if self.flush.is_error() {
1883 self.set_op_error(&format!(
1884 "Software homing: queue flush failed: {} {}",
1885 self.flush.error_code(),
1886 self.flush.error_message()
1887 ));
1888 return;
1889 }
1890 let mut cw = RawControlWord(view.control_word());
1893 cw.set_bit(8, false);
1894 cw.set_bit(4, false);
1895 cw.set_bit(5, false);
1896 view.set_control_word(cw.raw());
1897 view.set_target_position(view.position_actual());
1898 log::info!("SoftHome[6]: queue flushed; proceeding to debounce.");
1899 self.op = AxisOp::SoftHoming(HomeState::DebounceFoundSensor as u8);
1900 self.ton.call(false, Duration::from_secs(3));
1901 }
1902 Some(HomeState::DebounceFoundSensor) => {
1904 self.ton.call(true, Duration::from_secs(3));
1905
1906 let sw = RawStatusWord(view.status_word());
1907 if self.ton.q && sw.raw() & (1 << 12) == 0 {
1908 self.ton.call(false, Duration::from_secs(3));
1909 log::info!("SoftHome[6.a.]: delay complete, starting back-off from pos={} cw=0x{:04X} sw={:04x}",
1910 view.position_actual(), view.control_word(), view.status_word());
1911 self.op = AxisOp::SoftHoming(HomeState::BackOff as u8);
1912 }
1913 }
1914
1915 Some(HomeState::BackOff) => {
1917 let target = (self.calculate_max_relative_target(-self.soft_home_direction)) / 2;
1918 view.set_target_position(target);
1919
1920 self.command_homing_speed(view);
1921
1922 let mut cw = RawControlWord(view.control_word());
1923 cw.set_bit(4, true); cw.set_bit(6, true); cw.set_bit(13, true); view.set_control_word(cw.raw());
1927 log::info!(
1928 "SoftHome[7]: BACK-OFF absolute target={} vel={} pos={} cw=0x{:04X}",
1929 target,
1930 self.config.homing_speed,
1931 view.position_actual(),
1932 cw.raw()
1933 );
1934 self.op = AxisOp::SoftHoming(HomeState::WaitBackingOff as u8);
1935 }
1936 Some(HomeState::WaitBackingOff) => {
1937 let sw = RawStatusWord(view.status_word());
1938 if sw.raw() & (1 << 12) != 0 {
1939 let mut cw = RawControlWord(view.control_word());
1940 cw.set_bit(4, false);
1941 view.set_control_word(cw.raw());
1942 log::info!(
1943 "SoftHome[WaitBackingOff]: back-off ack received, pos={}",
1944 view.position_actual()
1945 );
1946 self.op = AxisOp::SoftHoming(HomeState::WaitLostSensor as u8);
1947 } else if self.homing_timed_out() {
1948 self.set_op_error("Software homing timeout: back-off not acknowledged");
1949 }
1950 }
1951 Some(HomeState::WaitLostSensor) => {
1952 if !self.check_soft_home_trigger(view) {
1953 log::info!(
1954 "SoftHome[WaitLostSensor]: sensor lost at pos={}. Halting...",
1955 view.position_actual()
1956 );
1957
1958 self.command_halt(view);
1959 self.op = AxisOp::SoftHoming(HomeState::WaitStoppedLostSensor as u8);
1960 } else if self.homing_timed_out() {
1961 self.set_op_error(
1962 "Software homing timeout: sensor did not clear during back-off",
1963 );
1964 }
1965 }
1966 Some(HomeState::WaitStoppedLostSensor) => {
1967 const STABLE_WINDOW: i32 = 1;
1968 const STABLE_TICKS_REQUIRED: u8 = 10;
1969
1970 let pos = view.position_actual();
1975 if (pos - self.last_raw_position).abs() <= STABLE_WINDOW {
1976 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
1977 } else {
1978 self.halt_stable_count = 0;
1979 }
1980
1981 if self.halt_stable_count >= STABLE_TICKS_REQUIRED {
1982 log::info!("SoftHome[WaitStoppedLostSensor] motor is stopped. Cancel move and wait for bit 12 go true.");
1983 self.flush.start(view, client, &mut self.sdo);
1984 self.op = AxisOp::SoftHoming(HomeState::FlushingLost as u8);
1985 } else if self.homing_timed_out() {
1986 self.set_op_error("Software homing timeout: motor did not stop after back-off");
1987 }
1988 }
1989 Some(HomeState::FlushingLost) => {
1990 self.flush.tick(view, client, &mut self.sdo);
1991 if self.flush.is_busy() {
1992 if self.homing_timed_out() {
1993 self.set_op_error("Software homing timeout: queue flush did not complete");
1994 }
1995 return;
1996 }
1997 if self.flush.is_error() {
1998 self.set_op_error(&format!(
1999 "Software homing: queue flush failed: {} {}",
2000 self.flush.error_code(),
2001 self.flush.error_message()
2002 ));
2003 return;
2004 }
2005 let mut cw = RawControlWord(view.control_word());
2008 cw.set_bit(8, false);
2009 cw.set_bit(4, false);
2010 cw.set_bit(5, false);
2011 view.set_control_word(cw.raw());
2012
2013 let desired_counts =
2014 self.config.to_counts(self.config.home_position).round() as i32;
2015 self.homing_sdo_tid = self.sdo.write(client, 0x607C, 0, json!(desired_counts));
2016 log::info!(
2017 "SoftHome[FlushingLost]: queue flushed; writing home offset {} [{} counts].",
2018 self.config.home_position, desired_counts
2019 );
2020 self.op = AxisOp::SoftHoming(HomeState::WaitHomeOffsetDone as u8);
2021 }
2022
2023 Some(HomeState::WaitHomeOffsetDone) => {
2024 match self
2026 .sdo
2027 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
2028 {
2029 SdoResult::Ok(_) => {
2030 self.op = AxisOp::SoftHoming(HomeState::WriteHomingModeOp as u8);
2031 }
2032 SdoResult::Pending => {
2033 if self.homing_timed_out() {
2034 self.set_op_error("Software homing timeout: home offset SDO write");
2035 }
2036 }
2037 SdoResult::Err(e) => {
2038 self.set_op_error(&format!("Software homing SDO error: {}", e));
2039 }
2040 SdoResult::Timeout => {
2041 self.set_op_error("Software homing: home offset SDO timed out");
2042 }
2043 }
2044 }
2045 Some(HomeState::WriteHomingModeOp) => {
2046 self.fb_mode_of_operation.reset();
2050 self.fb_mode_of_operation
2051 .start(ModesOfOperation::Homing as i8);
2052 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2053 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingModeOp as u8);
2054 }
2055 Some(HomeState::WaitWriteHomingModeOp) => {
2056 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2058
2059 if !self.fb_mode_of_operation.is_busy() {
2060 if self.fb_mode_of_operation.is_error() {
2061 self.set_op_error(&format!(
2062 "Software homing SDO error writing homing mode of operation: {} {}",
2063 self.fb_mode_of_operation.error_code(),
2064 self.fb_mode_of_operation.error_message()
2065 ));
2066 } else {
2067 log::info!("SoftHome: Drive is now in Homing Mode.");
2068 self.op = AxisOp::SoftHoming(HomeState::WriteHomingMethod as u8);
2069 }
2070 }
2071 }
2072 Some(HomeState::WriteHomingMethod) => {
2073 self.homing_sdo_tid =
2076 self.sdo
2077 .write(client, 0x6098, 0, json!(self.config.soft_home_method));
2078 self.op = AxisOp::SoftHoming(HomeState::WaitWriteHomingMethodDone as u8);
2079 }
2080 Some(HomeState::WaitWriteHomingMethodDone) => {
2081 match self
2083 .sdo
2084 .result(client, self.homing_sdo_tid, Duration::from_secs(5))
2085 {
2086 SdoResult::Ok(_) => {
2087 log::info!("SoftHome: Successfully wrote homing method.");
2088 self.op = AxisOp::SoftHoming(HomeState::ClearHomingTrigger as u8);
2089 }
2090 SdoResult::Pending => {
2091 if self.homing_timed_out() {
2092 self.restore_pp_after_error(
2093 "Software homing timeout: homing method SDO write",
2094 );
2095 }
2096 }
2097 SdoResult::Err(e) => {
2098 self.restore_pp_after_error(&format!("Software homing SDO error: {}", e));
2099 }
2100 SdoResult::Timeout => {
2101 self.restore_pp_after_error("Software homing: homing method SDO timed out");
2102 }
2103 }
2104 }
2105 Some(HomeState::ClearHomingTrigger) => {
2106 let mut cw = RawControlWord(view.control_word());
2109 cw.set_bit(4, false);
2110 view.set_control_word(cw.raw());
2111 self.op = AxisOp::SoftHoming(HomeState::TriggerHoming as u8);
2112 }
2113 Some(HomeState::TriggerHoming) => {
2114 let mut cw = RawControlWord(view.control_word());
2116 cw.set_bit(4, true);
2117 view.set_control_word(cw.raw());
2118 log::info!("SoftHome[TriggerHoming]: start homing");
2119 self.op = AxisOp::SoftHoming(HomeState::WaitHomingStarted as u8);
2120 }
2121 Some(HomeState::WaitHomingStarted) => {
2122 let sw = view.status_word();
2128 let error = sw & (1 << 13) != 0;
2129 if error {
2130 self.restore_pp_after_error("Software homing: drive reported homing error");
2131 } else if sw & (1 << 12) == 0 {
2132 self.op = AxisOp::SoftHoming(HomeState::WaitHomingDone as u8);
2133 } else if self.homing_timed_out() {
2134 self.restore_pp_after_error(&format!("Software homing timeout: drive did not acknowledge homing start (sw=0x{:04X})", sw));
2135 }
2136 }
2137 Some(HomeState::WaitHomingDone) => {
2138 let sw = view.status_word();
2140 let error = sw & (1 << 13) != 0;
2141 let attained = sw & (1 << 12) != 0;
2142 let reached = sw & (1 << 10) != 0;
2143
2144 if error {
2145 self.restore_pp_after_error("Software homing: drive reported homing error");
2146 } else if attained && reached {
2147 log::info!(
2154 "SoftHome[WaitHomingDone]: homing complete sw=0x{:04X} position_actual(0x6064)={} target(0x607C)={} counts",
2155 sw,
2156 view.position_actual(),
2157 self.config.to_counts(self.config.home_position).round() as i32,
2158 );
2159 self.op = AxisOp::SoftHoming(HomeState::ResetHomingTrigger as u8);
2160 } else if self.homing_timed_out() {
2161 self.restore_pp_after_error(&format!("Software homing timeout: drive homing did not complete (sw=0x{:04X} attained={} reached={})", sw, attained, reached));
2162 }
2163 }
2164 Some(HomeState::ResetHomingTrigger) => {
2165 let mut cw = RawControlWord(view.control_word());
2170 cw.set_bit(4, false);
2171 view.set_control_word(cw.raw());
2172 self.op = AxisOp::SoftHoming(HomeState::WaitHomingTriggerCleared as u8);
2173 }
2174 Some(HomeState::WaitHomingTriggerCleared) => {
2175 self.home_offset = 0; self.op = AxisOp::SoftHoming(HomeState::WriteMotionModeOfOperation as u8);
2179 }
2180
2181 Some(HomeState::WriteMotionModeOfOperation) => {
2182 self.fb_mode_of_operation.reset();
2185 self.fb_mode_of_operation
2186 .start(ModesOfOperation::ProfilePosition as i8);
2187 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2188 self.op = AxisOp::SoftHoming(HomeState::WaitWriteMotionModeOfOperation as u8);
2189 }
2190 Some(HomeState::WaitWriteMotionModeOfOperation) => {
2191 self.fb_mode_of_operation.tick(client, &mut self.sdo);
2193
2194 if !self.fb_mode_of_operation.is_busy() {
2195 if self.fb_mode_of_operation.is_error() {
2196 self.set_op_error(&format!(
2197 "Software homing SDO error writing homing mode of operation: {} {}",
2198 self.fb_mode_of_operation.error_code(),
2199 self.fb_mode_of_operation.error_message()
2200 ));
2201 } else {
2202 if self.is_error {
2203 log::error!("Drive back in PP mode after error. Homing sequence did not complete!");
2204 self.finish_op_error();
2205 } else {
2206 self.op =
2209 AxisOp::SoftHoming(HomeState::SendCurrentPositionTarget as u8);
2210 }
2211 }
2212 }
2213 }
2214
2215 Some(HomeState::SendCurrentPositionTarget) => {
2216 let current_pos = view.position_actual();
2218 view.set_target_position(current_pos);
2219 view.set_profile_velocity(0);
2220 let mut cw = RawControlWord(view.control_word());
2221 cw.set_bit(4, true);
2222 cw.set_bit(5, true);
2223 cw.set_bit(6, false); view.set_control_word(cw.raw());
2225 self.op = AxisOp::SoftHoming(HomeState::WaitCurrentPositionTargetSent as u8);
2226 }
2227 Some(HomeState::WaitCurrentPositionTargetSent) => {
2228 let sw = RawStatusWord(view.status_word());
2230 if sw.raw() & (1 << 12) != 0 {
2231 let mut cw = RawControlWord(view.control_word());
2232 cw.set_bit(4, false);
2233 view.set_control_word(cw.raw());
2234 log::info!(
2235 "Software homing complete — position set to {} user units",
2236 self.config.home_position
2237 );
2238 self.complete_op();
2239 } else if self.homing_timed_out() {
2240 self.set_op_error("Software homing timeout: hold position not acknowledged");
2241 }
2242 }
2243 _ => self.complete_op(),
2244 }
2245 }
2246
2247 fn tick_halting(
2260 &mut self,
2261 view: &mut impl AxisView,
2262 client: &mut CommandClient,
2263 step: u8,
2264 ) {
2265 match HaltState::from_repr(step) {
2266 Some(HaltState::WaitStopped) => {
2267 let pos = view.position_actual();
2268 let pos_stable = (pos - self.last_raw_position).abs() <= HALT_STABLE_WINDOW;
2269
2270 let vel = view.velocity_actual().abs();
2271 let vel_stopped = vel <= HALT_STOPPED_VELOCITY;
2272
2273 if pos_stable || vel_stopped {
2274 self.halt_stable_count = self.halt_stable_count.saturating_add(1);
2275 } else {
2276 self.halt_stable_count = 0;
2277 }
2278
2279 if self.halt_stable_count >= HALT_STABLE_TICKS_REQUIRED {
2280 self.flush.start(view, client, &mut self.sdo);
2283 self.op_started = Some(Instant::now());
2284 self.op = AxisOp::Halting(HaltState::Flushing as u8);
2285 } else if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2286 self.set_op_error("Halt timeout: motor did not stop");
2287 }
2288 }
2289 Some(HaltState::Flushing) => {
2290 self.flush.tick(view, client, &mut self.sdo);
2291 if self.flush.is_busy() {
2292 if self.op_stage_timed_out(HALT_STAGE_TIMEOUT) {
2293 self.set_op_error("Halt timeout: set-point queue flush did not complete");
2294 }
2295 return;
2296 }
2297 if self.flush.is_error() {
2298 self.set_op_error(&format!(
2299 "Halt: set-point queue flush failed: {} {}",
2300 self.flush.error_code(),
2301 self.flush.error_message()
2302 ));
2303 return;
2304 }
2305 let mut cw = RawControlWord(view.control_word());
2308 cw.set_bit(8, false); cw.set_bit(4, false);
2310 cw.set_bit(5, false);
2311 view.set_control_word(cw.raw());
2312 view.set_target_position(view.position_actual());
2313 self.complete_op();
2314 }
2315 None => {
2316 log::warn!("Axis halt: unknown sub-step {}, forcing idle", step);
2317 self.complete_op();
2318 }
2319 }
2320 }
2321
2322 fn tick_fault_recovery(&mut self, view: &mut impl AxisView, step: u8) {
2327 match step {
2328 1 => {
2329 let mut cw = RawControlWord(view.control_word());
2331 cw.cmd_fault_reset();
2332 view.set_control_word(cw.raw());
2333 self.op = AxisOp::FaultRecovery(2);
2334 }
2335 2 => {
2336 let sw = RawStatusWord(view.status_word());
2338 let state = sw.state();
2339 if !matches!(state, Cia402State::Fault | Cia402State::FaultReactionActive) {
2340 log::info!("Fault cleared (drive state: {})", state);
2341 self.complete_op();
2342 } else if self.op_timed_out() {
2343 self.set_op_error("Fault reset timeout: drive still faulted");
2344 }
2345 }
2346 _ => self.complete_op(),
2347 }
2348 }
2349}
2350
2351#[cfg(test)]
2356mod tests {
2357 use super::*;
2358
2359 struct MockView {
2361 control_word: u16,
2362 status_word: u16,
2363 target_position: i32,
2364 profile_velocity: u32,
2365 profile_acceleration: u32,
2366 profile_deceleration: u32,
2367 modes_of_operation: i8,
2368 modes_of_operation_display: i8,
2369 position_actual: i32,
2370 velocity_actual: i32,
2371 error_code: u16,
2372 positive_limit: bool,
2373 negative_limit: bool,
2374 home_sensor: bool,
2375 }
2376
2377 impl MockView {
2378 fn new() -> Self {
2379 Self {
2380 control_word: 0,
2381 status_word: 0x0040, target_position: 0,
2383 profile_velocity: 0,
2384 profile_acceleration: 0,
2385 profile_deceleration: 0,
2386 modes_of_operation: 0,
2387 modes_of_operation_display: 1, position_actual: 0,
2389 velocity_actual: 0,
2390 error_code: 0,
2391 positive_limit: false,
2392 negative_limit: false,
2393 home_sensor: false,
2394 }
2395 }
2396
2397 fn set_state(&mut self, state: u16) {
2398 self.status_word = state;
2399 }
2400 }
2401
2402 impl AxisView for MockView {
2403 fn control_word(&self) -> u16 {
2404 self.control_word
2405 }
2406 fn set_control_word(&mut self, word: u16) {
2407 self.control_word = word;
2408 }
2409 fn set_target_position(&mut self, pos: i32) {
2410 self.target_position = pos;
2411 }
2412 fn set_profile_velocity(&mut self, vel: u32) {
2413 self.profile_velocity = vel;
2414 }
2415 fn set_profile_acceleration(&mut self, accel: u32) {
2416 self.profile_acceleration = accel;
2417 }
2418 fn set_profile_deceleration(&mut self, decel: u32) {
2419 self.profile_deceleration = decel;
2420 }
2421 fn set_modes_of_operation(&mut self, mode: i8) {
2422 self.modes_of_operation = mode;
2423 }
2424 fn modes_of_operation_display(&self) -> i8 {
2425 self.modes_of_operation_display
2426 }
2427 fn status_word(&self) -> u16 {
2428 self.status_word
2429 }
2430 fn position_actual(&self) -> i32 {
2431 self.position_actual
2432 }
2433 fn velocity_actual(&self) -> i32 {
2434 self.velocity_actual
2435 }
2436 fn error_code(&self) -> u16 {
2437 self.error_code
2438 }
2439 fn positive_limit_active(&self) -> bool {
2440 self.positive_limit
2441 }
2442 fn negative_limit_active(&self) -> bool {
2443 self.negative_limit
2444 }
2445 fn home_sensor_active(&self) -> bool {
2446 self.home_sensor
2447 }
2448 }
2449
2450 fn test_config() -> AxisConfig {
2451 AxisConfig::new(12_800).with_user_scale(360.0)
2452 }
2453
2454 fn test_axis() -> (
2456 Axis,
2457 CommandClient,
2458 tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
2459 tokio::sync::mpsc::UnboundedReceiver<String>,
2460 ) {
2461 use tokio::sync::mpsc;
2462 let (write_tx, write_rx) = mpsc::unbounded_channel();
2463 let (response_tx, response_rx) = mpsc::unbounded_channel();
2464 let client = CommandClient::new(write_tx, response_rx);
2465 let axis = Axis::new(test_config(), "TestDrive");
2466 (axis, client, response_tx, write_rx)
2467 }
2468
2469 #[test]
2470 fn axis_config_conversion() {
2471 let cfg = test_config();
2472 assert!((cfg.to_counts(45.0) - 1600.0).abs() < 0.01);
2474 }
2475
2476 #[test]
2477 fn trace_records_on_change_not_every_tick() {
2478 let (mut axis, mut client, _r, _w) = test_axis();
2479 let mut view = MockView::new();
2480 view.set_state(0x0040); for _ in 0..10 {
2485 axis.tick(&mut view, &mut client);
2486 }
2487 let after_idle = axis.trace().lines().count();
2488 assert!(
2489 after_idle <= 1,
2490 "constant state must record ≤1 line, got {}",
2491 after_idle
2492 );
2493
2494 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2497 assert!(
2498 axis.trace().lines().count() > after_idle,
2499 "a SW change adds a line"
2500 );
2501 assert!(
2502 axis.trace().contains("Operation Enabled"),
2503 "trace decodes the SW state"
2504 );
2505 }
2506
2507 #[test]
2508 fn trace_decoders_render_handshake_bits() {
2509 assert_eq!(decode_cw(0x003F), "SwOn|EnVolt|QStop|EnOp|NewSP|ChgSet");
2511 assert!(
2512 decode_cw(0x013F).contains("HALT"),
2513 "0x13F still asserts HALT"
2514 );
2515 let s = decode_sw(0x1027);
2517 assert!(
2518 s.contains("Operation Enabled") && s.contains("SP_ACK"),
2519 "got {s}"
2520 );
2521 }
2522
2523 #[test]
2524 fn enable_sequence_sets_pp_mode_and_shutdown() {
2525 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2526 let mut view = MockView::new();
2527
2528 axis.enable(&mut view);
2529
2530 assert_eq!(
2532 view.modes_of_operation,
2533 ModesOfOperation::ProfilePosition.as_i8()
2534 );
2535 assert_eq!(view.control_word & 0x008F, 0x0006);
2537 assert_eq!(axis.op, AxisOp::Enabling(1));
2539
2540 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2543
2544 assert_eq!(view.control_word & 0x008F, 0x000F);
2546 assert_eq!(axis.op, AxisOp::Enabling(2));
2547
2548 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2551
2552 assert_eq!(axis.op, AxisOp::Idle);
2554 assert!(axis.motor_on);
2555 }
2556
2557 #[test]
2558 fn move_absolute_sets_target() {
2559 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2560 let mut view = MockView::new();
2561 view.set_state(0x0027); axis.tick(&mut view, &mut client); axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2566
2567 assert_eq!(view.target_position, 1600);
2569 assert_eq!(view.profile_velocity, 3200);
2571 assert_eq!(view.profile_acceleration, 6400);
2573 assert_eq!(view.profile_deceleration, 6400);
2574 assert!(view.control_word & (1 << 4) != 0);
2576 assert!(view.control_word & (1 << 6) == 0);
2578 assert!(matches!(
2580 axis.op,
2581 AxisOp::Moving(MoveKind::Absolute, 1, _, _)
2582 ));
2583 }
2584
2585 #[test]
2586 fn move_relative_sets_relative_bit() {
2587 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2588 let mut view = MockView::new();
2589 view.set_state(0x0027);
2590 axis.tick(&mut view, &mut client);
2591
2592 axis.move_relative(&mut view, 10.0, 90.0, 180.0, 180.0);
2593
2594 assert!(view.control_word & (1 << 6) != 0);
2596 assert!(matches!(
2597 axis.op,
2598 AxisOp::Moving(MoveKind::Relative, 1, _, _)
2599 ));
2600 }
2601
2602 #[test]
2603 fn move_completes_on_target_reached() {
2604 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2605 let mut view = MockView::new();
2606 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2608
2609 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2610
2611 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2614 assert!(view.control_word & (1 << 4) == 0);
2616
2617 view.status_word = 0x0427; axis.tick(&mut view, &mut client);
2620 assert_eq!(axis.op, AxisOp::Idle);
2622 assert!(!axis.in_motion);
2623 }
2624
2625 #[test]
2626 fn fault_detected_sets_error() {
2627 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2628 let mut view = MockView::new();
2629 view.set_state(0x0008); view.error_code = 0x1234;
2631
2632 axis.tick(&mut view, &mut client);
2633
2634 assert!(axis.is_error);
2635 assert_eq!(axis.error_code, 0x1234);
2636 assert!(axis.error_message.contains("fault"));
2637 }
2638
2639 #[test]
2640 fn fault_recovery_sequence() {
2641 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2642 let mut view = MockView::new();
2643 view.set_state(0x0008); axis.reset_faults(&mut view);
2646 assert!(view.control_word & 0x0080 == 0);
2648
2649 axis.tick(&mut view, &mut client);
2651 assert!(view.control_word & 0x0080 != 0);
2652
2653 view.set_state(0x0040);
2655 axis.tick(&mut view, &mut client);
2656 assert_eq!(axis.op, AxisOp::Idle);
2657 assert!(!axis.is_error);
2658 }
2659
2660 #[test]
2661 fn reset_faults_software_only_is_synchronous() {
2662 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2668 let mut view = MockView::new();
2669 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2671
2672 axis.set_software_min_limit(207.0);
2674 axis.move_absolute(&mut view, 200.0, 90.0, 180.0, 180.0);
2675 assert!(axis.is_error, "move past software limit must set is_error");
2676 assert!(!axis.is_busy);
2677
2678 let cw_before = view.control_word;
2681
2682 axis.reset_faults(&mut view);
2683
2684 assert!(!axis.is_error);
2686 assert_eq!(axis.error_code, 0);
2687 assert!(axis.error_message.is_empty());
2688 assert!(!axis.is_busy);
2689 assert_eq!(axis.op, AxisOp::Idle);
2690 assert_eq!(view.control_word, cw_before);
2692 }
2693
2694 #[test]
2695 fn disable_sequence() {
2696 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2697 let mut view = MockView::new();
2698 view.set_state(0x0027); axis.disable(&mut view);
2701 assert_eq!(view.control_word & 0x008F, 0x0006);
2703
2704 view.set_state(0x0021); axis.tick(&mut view, &mut client);
2707 assert_eq!(axis.op, AxisOp::Idle);
2708 }
2709
2710 #[test]
2711 fn position_tracks_with_home_offset() {
2712 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2713 let mut view = MockView::new();
2714 view.set_state(0x0027);
2715 view.position_actual = 5000;
2716
2717 axis.enable(&mut view);
2719 view.set_state(0x0021);
2720 axis.tick(&mut view, &mut client);
2721 view.set_state(0x0027);
2722 axis.tick(&mut view, &mut client);
2723
2724 assert_eq!(axis.home_offset, 5000);
2726
2727 assert!((axis.position - 0.0).abs() < 0.01);
2729
2730 view.position_actual = 6600;
2732 axis.tick(&mut view, &mut client);
2733
2734 assert!((axis.position - 45.0).abs() < 0.1);
2736 }
2737
2738 #[test]
2739 fn set_position_adjusts_home_offset() {
2740 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2741 let mut view = MockView::new();
2742 view.position_actual = 3200;
2743
2744 axis.set_position(&view, 90.0);
2745 axis.tick(&mut view, &mut client);
2746
2747 assert_eq!(axis.home_offset, 0);
2749 assert!((axis.position - 90.0).abs() < 0.01);
2750 }
2751
2752 #[test]
2753 fn halt_runs_multi_stage_close_out() {
2754 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2755 let mut view = MockView::new();
2756 view.set_state(0x0027);
2757
2758 axis.halt(&mut view);
2759
2760 assert!(view.control_word & (1 << 8) != 0, "halt bit must be set");
2762 assert!(
2763 view.control_word & (1 << 4) == 0,
2764 "new_setpoint must be cleared"
2765 );
2766
2767 assert!(
2769 matches!(axis.op, AxisOp::Halting(_)),
2770 "halt should enter Halting state, not Idle"
2771 );
2772 let AxisOp::Halting(step) = axis.op.clone() else {
2773 unreachable!()
2774 };
2775 assert_eq!(step, HaltState::WaitStopped as u8);
2776
2777 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
2784 axis.tick(&mut view, &mut client);
2785 }
2786 assert!(matches!(axis.op, AxisOp::Halting(_)));
2789 let AxisOp::Halting(step) = axis.op.clone() else {
2790 unreachable!()
2791 };
2792 assert_eq!(
2793 step,
2794 HaltState::Flushing as u8,
2795 "should advance past WaitStopped once position/velocity is stable"
2796 );
2797 assert!(view.control_word & (1 << 8) != 0, "halt held through the away mode switch");
2800
2801 assert!(axis.is_busy, "is_busy must stay true across Halting stages");
2805 }
2806
2807 #[test]
2808 fn is_busy_tracks_operations() {
2809 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2810 let mut view = MockView::new();
2811
2812 axis.tick(&mut view, &mut client);
2814 assert!(!axis.is_busy);
2815
2816 axis.enable(&mut view);
2818 axis.tick(&mut view, &mut client);
2819 assert!(axis.is_busy);
2820
2821 view.set_state(0x0021);
2823 axis.tick(&mut view, &mut client);
2824 view.set_state(0x0027);
2825 axis.tick(&mut view, &mut client);
2826 assert!(!axis.is_busy);
2827
2828 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2830 axis.tick(&mut view, &mut client);
2831 assert!(axis.is_busy);
2832 assert!(axis.in_motion);
2833 }
2834
2835 #[test]
2836 fn fault_during_move_cancels_op() {
2837 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2838 let mut view = MockView::new();
2839 view.set_state(0x0027); axis.tick(&mut view, &mut client);
2841
2842 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2844 axis.tick(&mut view, &mut client);
2845 assert!(axis.is_busy);
2846 assert!(!axis.is_error);
2847
2848 view.set_state(0x0008); axis.tick(&mut view, &mut client);
2851
2852 assert!(!axis.is_busy);
2854 assert!(axis.is_error);
2855 assert_eq!(axis.op, AxisOp::Idle);
2856 }
2857
2858 #[test]
2859 fn move_absolute_rejected_by_max_limit() {
2860 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2861 let mut view = MockView::new();
2862 view.set_state(0x0027);
2863 axis.tick(&mut view, &mut client);
2864
2865 axis.set_software_max_limit(90.0);
2866 axis.move_absolute(&mut view, 100.0, 90.0, 180.0, 180.0);
2867
2868 assert!(axis.is_error);
2870 assert_eq!(axis.op, AxisOp::Idle);
2871 assert!(axis.error_message.contains("max software limit"));
2872 }
2873
2874 #[test]
2875 fn move_absolute_rejected_by_min_limit() {
2876 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2877 let mut view = MockView::new();
2878 view.set_state(0x0027);
2879 axis.tick(&mut view, &mut client);
2880
2881 axis.set_software_min_limit(-10.0);
2882 axis.move_absolute(&mut view, -20.0, 90.0, 180.0, 180.0);
2883
2884 assert!(axis.is_error);
2885 assert_eq!(axis.op, AxisOp::Idle);
2886 assert!(axis.error_message.contains("min software limit"));
2887 }
2888
2889 #[test]
2890 fn move_relative_rejected_by_max_limit() {
2891 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2892 let mut view = MockView::new();
2893 view.set_state(0x0027);
2894 axis.tick(&mut view, &mut client);
2895
2896 axis.set_software_max_limit(50.0);
2898 axis.move_relative(&mut view, 60.0, 90.0, 180.0, 180.0);
2899
2900 assert!(axis.is_error);
2901 assert_eq!(axis.op, AxisOp::Idle);
2902 assert!(axis.error_message.contains("max software limit"));
2903 }
2904
2905 #[test]
2906 fn move_within_limits_allowed() {
2907 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2908 let mut view = MockView::new();
2909 view.set_state(0x0027);
2910 axis.tick(&mut view, &mut client);
2911
2912 axis.set_software_max_limit(90.0);
2913 axis.set_software_min_limit(-90.0);
2914 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2915
2916 assert!(!axis.is_error);
2918 assert!(matches!(
2919 axis.op,
2920 AxisOp::Moving(MoveKind::Absolute, 1, _, _)
2921 ));
2922 }
2923
2924 #[test]
2925 fn runtime_limit_halts_move_in_violated_direction() {
2926 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2927 let mut view = MockView::new();
2928 view.set_state(0x0027);
2929 axis.tick(&mut view, &mut client);
2930
2931 axis.set_software_max_limit(45.0);
2932 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2934
2935 view.position_actual = 1650;
2938 view.velocity_actual = 100; view.status_word = 0x1027;
2942 axis.tick(&mut view, &mut client);
2943 view.status_word = 0x0027;
2944 axis.tick(&mut view, &mut client);
2945
2946 assert!(!axis.is_error);
2950 assert!(axis.at_max_limit);
2951 assert!(axis.is_busy);
2952 assert!(matches!(axis.op, AxisOp::Halting(_)));
2953 assert!(view.control_word & (1 << 8) != 0);
2955 }
2956
2957 #[test]
2958 fn runtime_limit_allows_move_in_opposite_direction() {
2959 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2960 let mut view = MockView::new();
2961 view.set_state(0x0027);
2962 view.position_actual = 1778; axis.set_software_max_limit(45.0);
2965 axis.tick(&mut view, &mut client);
2966 assert!(axis.at_max_limit);
2967
2968 axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
2970 assert!(!axis.is_error);
2971 assert!(matches!(
2972 axis.op,
2973 AxisOp::Moving(MoveKind::Absolute, 1, _, _)
2974 ));
2975
2976 view.velocity_actual = -100;
2978 view.status_word = 0x1027; axis.tick(&mut view, &mut client);
2980 assert!(!axis.is_error);
2982 }
2983
2984 #[test]
2985 fn positive_limit_switch_halts_positive_move() {
2986 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
2987 let mut view = MockView::new();
2988 view.set_state(0x0027);
2989 axis.tick(&mut view, &mut client);
2990
2991 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
2993 view.velocity_actual = 100; view.status_word = 0x1027;
2996 axis.tick(&mut view, &mut client);
2997 view.status_word = 0x0027;
2998
2999 view.positive_limit = true;
3001 axis.tick(&mut view, &mut client);
3002
3003 assert!(!axis.is_error);
3008 assert!(axis.at_positive_limit_switch);
3009 assert!(axis.is_busy);
3010 assert!(matches!(axis.op, AxisOp::Halting(_)));
3011 assert!(view.control_word & (1 << 8) != 0);
3013 }
3014
3015 #[test]
3016 fn negative_limit_switch_halts_negative_move() {
3017 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3018 let mut view = MockView::new();
3019 view.set_state(0x0027);
3020 axis.tick(&mut view, &mut client);
3021
3022 axis.move_absolute(&mut view, -45.0, 90.0, 180.0, 180.0);
3024 view.velocity_actual = -100; view.status_word = 0x1027;
3026 axis.tick(&mut view, &mut client);
3027 view.status_word = 0x0027;
3028
3029 view.negative_limit = true;
3031 axis.tick(&mut view, &mut client);
3032
3033 assert!(!axis.is_error);
3038 assert!(axis.at_negative_limit_switch);
3039 assert!(axis.is_busy);
3040 assert!(matches!(axis.op, AxisOp::Halting(_)));
3041 assert!(view.control_word & (1 << 8) != 0);
3042 }
3043
3044 #[test]
3045 fn limit_halt_flushes_queue_via_mode_toggle() {
3046 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3051 let mut view = MockView::new();
3052 view.set_state(0x0027); axis.tick(&mut view, &mut client);
3054
3055 axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
3057 view.velocity_actual = 100;
3058 view.status_word = 0x1027;
3059 axis.tick(&mut view, &mut client);
3060 view.status_word = 0x0027;
3061 view.positive_limit = true;
3062 axis.tick(&mut view, &mut client);
3063
3064 assert!(!axis.is_error);
3066 assert!(axis.is_busy);
3067 assert!(matches!(axis.op, AxisOp::Halting(_)));
3068 assert!(view.control_word & (1 << 8) != 0);
3069
3070 view.velocity_actual = 0;
3077 view.positive_limit = false;
3078 for _ in 0..HALT_STABLE_TICKS_REQUIRED {
3079 axis.tick(&mut view, &mut client);
3080 }
3081 let AxisOp::Halting(step) = axis.op.clone() else {
3082 unreachable!()
3083 };
3084 assert_eq!(step, HaltState::Flushing as u8, "halt flushes the queue via a mode toggle");
3085 assert!(view.control_word & (1 << 8) != 0, "halt held through the queue flush");
3086 assert!(axis.is_busy, "axis stays busy through the close-out");
3087 assert_ne!(
3088 view.modes_of_operation,
3089 ModesOfOperation::ProfilePosition.as_i8(),
3090 "mode switched away from PP to flush the queue",
3091 );
3092 }
3093
3094 #[test]
3095 fn limit_switch_allows_move_in_opposite_direction() {
3096 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3097 let mut view = MockView::new();
3098 view.set_state(0x0027);
3099 view.positive_limit = true;
3101 view.velocity_actual = -100;
3102 axis.tick(&mut view, &mut client);
3103 assert!(axis.at_positive_limit_switch);
3104
3105 axis.move_absolute(&mut view, -10.0, 90.0, 180.0, 180.0);
3107 view.status_word = 0x1027;
3108 axis.tick(&mut view, &mut client);
3109
3110 assert!(!axis.is_error);
3112 assert!(matches!(axis.op, AxisOp::Moving(_, _, _, _)));
3113 }
3114
3115 #[test]
3116 fn limit_switch_ignored_when_not_moving() {
3117 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3118 let mut view = MockView::new();
3119 view.set_state(0x0027);
3120 view.positive_limit = true;
3121
3122 axis.tick(&mut view, &mut client);
3123
3124 assert!(axis.at_positive_limit_switch);
3126 assert!(!axis.is_error);
3127 }
3128
3129 #[test]
3130 fn home_sensor_output_tracks_view() {
3131 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3132 let mut view = MockView::new();
3133 view.set_state(0x0027);
3134
3135 axis.tick(&mut view, &mut client);
3136 assert!(!axis.home_sensor);
3137
3138 view.home_sensor = true;
3139 axis.tick(&mut view, &mut client);
3140 assert!(axis.home_sensor);
3141
3142 view.home_sensor = false;
3143 axis.tick(&mut view, &mut client);
3144 assert!(!axis.home_sensor);
3145 }
3146
3147 #[test]
3148 fn velocity_output_converted() {
3149 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3150 let mut view = MockView::new();
3151 view.set_state(0x0027);
3152 view.velocity_actual = 3200;
3154
3155 axis.tick(&mut view, &mut client);
3156
3157 assert!((axis.speed - 90.0).abs() < 0.1);
3158 assert!(axis.moving_positive);
3159 assert!(!axis.moving_negative);
3160 }
3161
3162 fn soft_homing_config() -> AxisConfig {
3165 let mut cfg = AxisConfig::new(12_800).with_user_scale(360.0);
3166 cfg.homing_speed = 10.0;
3167 cfg.homing_accel = 20.0;
3168 cfg.homing_decel = 20.0;
3169 cfg
3170 }
3171
3172 fn soft_homing_axis() -> (
3173 Axis,
3174 CommandClient,
3175 tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
3176 tokio::sync::mpsc::UnboundedReceiver<String>,
3177 ) {
3178 use tokio::sync::mpsc;
3179 let (write_tx, write_rx) = mpsc::unbounded_channel();
3180 let (response_tx, response_rx) = mpsc::unbounded_channel();
3181 let client = CommandClient::new(write_tx, response_rx);
3182 let axis = Axis::new(soft_homing_config(), "TestDrive");
3183 (axis, client, response_tx, write_rx)
3184 }
3185
3186 fn enable_axis(axis: &mut Axis, view: &mut MockView, client: &mut CommandClient) {
3188 view.set_state(0x0027); axis.tick(view, client);
3190 }
3191
3192 fn complete_soft_homing(
3197 axis: &mut Axis,
3198 view: &mut MockView,
3199 client: &mut CommandClient,
3200 resp_tx: &tokio::sync::mpsc::UnboundedSender<mechutil::ipc::CommandMessage>,
3201 trigger_pos: i32,
3202 clear_sensor: impl FnOnce(&mut MockView),
3203 ) {
3204 use mechutil::ipc::CommandMessage as IpcMsg;
3205
3206 axis.tick(view, client);
3209 assert!(matches!(axis.op, AxisOp::SoftHoming(5)));
3210
3211 view.position_actual = trigger_pos + 100;
3213 axis.tick(view, client);
3214 view.position_actual = trigger_pos + 120;
3215 axis.tick(view, client);
3216 for _ in 0..10 {
3218 axis.tick(view, client);
3219 }
3220 assert!(matches!(axis.op, AxisOp::SoftHoming(6)));
3221
3222 view.status_word = 0x1027;
3224 axis.tick(view, client);
3225 assert!(matches!(axis.op, AxisOp::SoftHoming(60)));
3226 view.status_word = 0x0027;
3227
3228 for _ in 0..100 {
3230 axis.tick(view, client);
3231 }
3232 assert!(matches!(axis.op, AxisOp::SoftHoming(7)));
3233
3234 axis.tick(view, client);
3237 assert!(matches!(axis.op, AxisOp::SoftHoming(8)));
3238
3239 view.status_word = 0x1027;
3241 axis.tick(view, client);
3242 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
3243 view.status_word = 0x0027;
3244
3245 axis.tick(view, client);
3247 assert!(matches!(axis.op, AxisOp::SoftHoming(9)));
3248 clear_sensor(view);
3249 view.position_actual = trigger_pos - 200;
3250 axis.tick(view, client);
3251 assert!(matches!(axis.op, AxisOp::SoftHoming(10)));
3252
3253 axis.tick(view, client);
3255 assert!(matches!(axis.op, AxisOp::SoftHoming(11)));
3256 for _ in 0..10 {
3257 axis.tick(view, client);
3258 }
3259 assert!(matches!(axis.op, AxisOp::SoftHoming(12)));
3260
3261 view.status_word = 0x1027;
3264 axis.tick(view, client);
3265 view.status_word = 0x0027;
3266 assert!(matches!(axis.op, AxisOp::SoftHoming(13)));
3267
3268 let tid = axis.homing_sdo_tid;
3270 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
3271 client.poll();
3272 axis.tick(view, client);
3273 assert!(matches!(axis.op, AxisOp::SoftHoming(14)));
3274
3275 axis.tick(view, client);
3277 let tid = axis.homing_sdo_tid;
3278 resp_tx.send(IpcMsg::response(tid, json!(null))).unwrap();
3279 client.poll();
3280 axis.tick(view, client);
3281 assert!(matches!(axis.op, AxisOp::SoftHoming(16)));
3282
3283 view.modes_of_operation_display = ModesOfOperation::Homing.as_i8();
3285 axis.tick(view, client);
3286 assert!(matches!(axis.op, AxisOp::SoftHoming(17)));
3287
3288 view.status_word = 0x1427; axis.tick(view, client);
3291 assert!(matches!(axis.op, AxisOp::SoftHoming(18)));
3292 view.modes_of_operation_display = ModesOfOperation::ProfilePosition.as_i8();
3293 view.status_word = 0x0027;
3294
3295 axis.tick(view, client);
3297 assert!(matches!(axis.op, AxisOp::SoftHoming(19)));
3298
3299 view.status_word = 0x1027;
3301 axis.tick(view, client);
3302 view.status_word = 0x0027;
3303
3304 assert_eq!(axis.op, AxisOp::Idle);
3305 assert!(!axis.is_busy);
3306 assert!(!axis.is_error);
3307 assert_eq!(axis.home_offset, 0); }
3309
3310 #[test]
3311 fn soft_homing_pnp_home_sensor_full_sequence() {
3312 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3313 let mut view = MockView::new();
3314 enable_axis(&mut axis, &mut view, &mut client);
3315
3316 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3317
3318 axis.tick(&mut view, &mut client); view.status_word = 0x1027;
3321 axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3323 axis.tick(&mut view, &mut client); view.home_sensor = true;
3327 view.position_actual = 5000;
3328 axis.tick(&mut view, &mut client);
3329 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3330
3331 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000, |v| {
3332 v.home_sensor = false;
3333 });
3334 }
3335
3336 #[test]
3337 fn soft_homing_npn_home_sensor_full_sequence() {
3338 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3339 let mut view = MockView::new();
3340 view.home_sensor = true;
3342 enable_axis(&mut axis, &mut view, &mut client);
3343
3344 axis.home(&mut view, HomingMethod::HomeSensorPosNpn);
3345
3346 axis.tick(&mut view, &mut client);
3348 view.status_word = 0x1027;
3349 axis.tick(&mut view, &mut client);
3350 view.status_word = 0x0027;
3351 axis.tick(&mut view, &mut client);
3352
3353 view.home_sensor = false;
3355 view.position_actual = 3000;
3356 axis.tick(&mut view, &mut client);
3357 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3358
3359 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 3000, |v| {
3360 v.home_sensor = true;
3361 }); }
3363
3364 #[test]
3365 fn soft_homing_limit_switch_suppresses_halt() {
3366 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3367 let mut view = MockView::new();
3368 enable_axis(&mut axis, &mut view, &mut client);
3369
3370 axis.home(&mut view, HomingMethod::LimitSwitchPosPnp);
3372
3373 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3378 axis.tick(&mut view, &mut client); view.positive_limit = true;
3382 view.velocity_actual = 100; view.position_actual = 8000;
3384 axis.tick(&mut view, &mut client);
3385
3386 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3388 assert!(!axis.is_error);
3389 }
3390
3391 #[test]
3392 fn soft_homing_opposite_limit_still_protects() {
3393 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3394 let mut view = MockView::new();
3395 enable_axis(&mut axis, &mut view, &mut client);
3396
3397 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3399
3400 axis.tick(&mut view, &mut client); view.status_word = 0x1027; axis.tick(&mut view, &mut client); view.status_word = 0x0027;
3405 axis.tick(&mut view, &mut client); view.negative_limit = true;
3410 view.velocity_actual = -100; axis.tick(&mut view, &mut client);
3412
3413 assert!(axis.is_error);
3415 assert!(axis.error_message.contains("Negative limit switch"));
3416 }
3417
3418 #[test]
3419 #[test]
3436 fn soft_homing_negative_direction_sets_negative_target() {
3437 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3438 let mut view = MockView::new();
3439 enable_axis(&mut axis, &mut view, &mut client);
3440
3441 axis.home(&mut view, HomingMethod::HomeSensorNegPnp);
3442 axis.tick(&mut view, &mut client); assert!(view.target_position < 0);
3446 }
3447
3448 #[test]
3449 fn home_integrated_method_starts_hardware_homing() {
3450 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3451 let mut view = MockView::new();
3452 enable_axis(&mut axis, &mut view, &mut client);
3453
3454 axis.home(&mut view, HomingMethod::CurrentPosition);
3455 assert!(matches!(axis.op, AxisOp::Homing(0)));
3456 assert_eq!(axis.homing_method, 37);
3457 }
3458
3459 #[test]
3460 fn home_integrated_arbitrary_code() {
3461 let (mut axis, mut client, _resp_tx, _write_rx) = test_axis();
3462 let mut view = MockView::new();
3463 enable_axis(&mut axis, &mut view, &mut client);
3464
3465 axis.home(&mut view, HomingMethod::Integrated(35));
3466 assert!(matches!(axis.op, AxisOp::Homing(0)));
3467 assert_eq!(axis.homing_method, 35);
3468 }
3469
3470 #[test]
3471 fn hardware_homing_skips_speed_sdos_when_zero() {
3472 use mechutil::ipc::CommandMessage;
3473
3474 let (mut axis, mut client, resp_tx, mut write_rx) = test_axis();
3475 let mut view = MockView::new();
3476 enable_axis(&mut axis, &mut view, &mut client);
3477
3478 axis.home(&mut view, HomingMethod::Integrated(37));
3480
3481 axis.tick(&mut view, &mut client);
3483 assert!(matches!(axis.op, AxisOp::Homing(1)));
3484
3485 let _ = write_rx.try_recv();
3487
3488 let tid = axis.homing_sdo_tid;
3490 resp_tx
3491 .send(CommandMessage::response(tid, serde_json::json!(null)))
3492 .unwrap();
3493 client.poll();
3494 axis.tick(&mut view, &mut client);
3495
3496 assert!(matches!(axis.op, AxisOp::Homing(8)));
3498 }
3499
3500 #[test]
3501 fn hardware_homing_writes_speed_sdos_when_nonzero() {
3502 use mechutil::ipc::CommandMessage;
3503
3504 let (mut axis, mut client, resp_tx, mut write_rx) = soft_homing_axis();
3505 let mut view = MockView::new();
3506 enable_axis(&mut axis, &mut view, &mut client);
3507
3508 axis.home(&mut view, HomingMethod::Integrated(37));
3510
3511 axis.tick(&mut view, &mut client);
3513 assert!(matches!(axis.op, AxisOp::Homing(1)));
3514 let _ = write_rx.try_recv();
3515
3516 let tid = axis.homing_sdo_tid;
3518 resp_tx
3519 .send(CommandMessage::response(tid, serde_json::json!(null)))
3520 .unwrap();
3521 client.poll();
3522 axis.tick(&mut view, &mut client);
3523 assert!(matches!(axis.op, AxisOp::Homing(2)));
3525 }
3526
3527 #[test]
3528 fn soft_homing_edge_during_ack_step() {
3529 let (mut axis, mut client, _resp_tx, _write_rx) = soft_homing_axis();
3530 let mut view = MockView::new();
3531 enable_axis(&mut axis, &mut view, &mut client);
3532
3533 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3534 axis.tick(&mut view, &mut client); view.home_sensor = true;
3538 view.position_actual = 2000;
3539 axis.tick(&mut view, &mut client);
3540
3541 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3543 }
3544
3545 #[test]
3546 fn soft_homing_applies_home_position() {
3547 let mut cfg = soft_homing_config();
3548 cfg.home_position = 90.0;
3549
3550 use tokio::sync::mpsc;
3551 let (write_tx, _write_rx) = mpsc::unbounded_channel();
3552 let (resp_tx, response_rx) = mpsc::unbounded_channel();
3553 let mut client = CommandClient::new(write_tx, response_rx);
3554 let mut axis = Axis::new(cfg, "TestDrive");
3555
3556 let mut view = MockView::new();
3557 enable_axis(&mut axis, &mut view, &mut client);
3558
3559 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3560
3561 axis.tick(&mut view, &mut client);
3563 view.status_word = 0x1027;
3564 axis.tick(&mut view, &mut client);
3565 view.status_word = 0x0027;
3566 axis.tick(&mut view, &mut client);
3567
3568 view.home_sensor = true;
3570 view.position_actual = 5000;
3571 axis.tick(&mut view, &mut client);
3572 assert!(matches!(axis.op, AxisOp::SoftHoming(4)));
3573
3574 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000, |v| {
3576 v.home_sensor = false;
3577 });
3578
3579 assert_eq!(axis.home_offset, 0);
3581 }
3582
3583 #[test]
3584 fn soft_homing_default_home_position_zero() {
3585 let (mut axis, mut client, resp_tx, _write_rx) = soft_homing_axis();
3586 let mut view = MockView::new();
3587 enable_axis(&mut axis, &mut view, &mut client);
3588
3589 axis.home(&mut view, HomingMethod::HomeSensorPosPnp);
3590
3591 axis.tick(&mut view, &mut client);
3593 view.status_word = 0x1027;
3594 axis.tick(&mut view, &mut client);
3595 view.status_word = 0x0027;
3596 axis.tick(&mut view, &mut client);
3597
3598 view.home_sensor = true;
3600 view.position_actual = 5000;
3601 axis.tick(&mut view, &mut client);
3602
3603 complete_soft_homing(&mut axis, &mut view, &mut client, &resp_tx, 5000, |v| {
3604 v.home_sensor = false;
3605 });
3606
3607 assert_eq!(axis.home_offset, 0);
3608 }
3609}