1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5use std::thread;
6use std::time::{Duration, Instant};
7
8use mio::Token;
9
10use crate::bus::{Bus, RouteKey};
11use crate::error::Error;
12use crate::group::{Arm, Generic, Gripper, GroupKind, MotorGroup, OPENING_CALIBRATION_TRAVEL_RAD};
13use crate::motor::Motor;
14use crate::spec::{GripperOpeningSpec, GroupSpecKind, MotorSpec};
15use crate::transport::{BusPoller, CanBus};
16use motor_codec::{CommandKind, MotorCodec};
17
18#[cfg(test)]
19const OPENING_CALIBRATION_ENDPOINT_DWELL: Duration = Duration::from_millis(100);
20#[cfg(not(test))]
21const OPENING_CALIBRATION_ENDPOINT_DWELL: Duration = Duration::from_millis(1500);
22const OPENING_CALIBRATION_TICK: Duration = Duration::from_millis(10);
23const OPENING_CALIBRATION_REFRESH_TIMEOUT: Duration = Duration::from_millis(250);
24const OPENING_MODE_VERIFY_TIMEOUT: Duration = Duration::from_millis(50);
25const OPENING_MODE_VERIFY_TICK: Duration = Duration::from_millis(5);
26const OPENING_MODE_SETTLE: Duration = Duration::from_millis(2);
27
28pub struct Robot {
30 buses: HashMap<String, Arc<Mutex<Bus>>>,
31 bus_order: Vec<String>,
32 groups: HashMap<String, GroupKind>,
33 group_order: Vec<String>,
34 connected: bool,
35 poller: Option<BusPoller>,
36 bus_tokens: HashMap<String, Token>,
38 token_to_bus: HashMap<Token, String>,
40}
41
42impl Robot {
43 pub fn builder() -> RobotBuilder {
45 RobotBuilder::new()
46 }
47
48 pub fn group(&self, name: &str) -> Option<&GroupKind> {
50 self.groups.get(name)
51 }
52
53 pub fn group_mut(&mut self, name: &str) -> Option<&mut GroupKind> {
55 self.groups.get_mut(name)
56 }
57
58 pub fn group_names(&self) -> impl Iterator<Item = &str> {
60 self.group_order.iter().map(String::as_str)
61 }
62
63 pub fn bus_names(&self) -> impl Iterator<Item = &str> {
65 self.bus_order.iter().map(String::as_str)
66 }
67
68 pub fn is_connected(&self) -> bool {
70 self.connected
71 }
72
73 pub fn bus(&self, name: &str) -> Option<&Arc<Mutex<Bus>>> {
75 self.buses.get(name)
76 }
77
78 pub fn connect(&mut self) -> Result<(), Error> {
81 if self.connected {
82 return Ok(());
83 }
84 for (bus_name, bus_arc) in &self.buses {
86 let mut bus = bus_arc.lock().map_err(|_| Error::BusPoisoned)?;
87 bus.routes.clear();
88 for group_name in &self.group_order {
89 let group = self.groups.get(group_name).expect("group order invariant");
90 if group.bus_name() != bus_name {
91 continue;
92 }
93 for (i, motor) in group.inner().motors().iter().enumerate() {
94 let key = RouteKey {
95 group_name: group_name.clone(),
96 motor_index: i,
97 };
98 if let Some(existing) = bus.routes.get(&motor.recv_id()) {
99 return Err(Error::CanIdCollision {
100 bus_name: bus_name.clone(),
101 recv_id: motor.recv_id(),
102 existing: existing.clone(),
103 attempted: key,
104 });
105 }
106 bus.routes.insert(motor.recv_id(), key);
107 }
108 }
109 }
110 let poller = BusPoller::with_capacity(self.buses.len().max(1))?;
112 let mut bus_tokens = HashMap::with_capacity(self.buses.len());
113 let mut token_to_bus = HashMap::with_capacity(self.buses.len());
114 for (idx, bus_name) in self.bus_order.iter().enumerate() {
115 let bus = self.buses[bus_name]
116 .lock()
117 .map_err(|_| Error::BusPoisoned)?;
118 if let Some(fd) = bus.transport.raw_fd() {
119 let token = Token(idx);
120 poller.register(token, fd)?;
121 bus_tokens.insert(bus_name.clone(), token);
122 token_to_bus.insert(token, bus_name.clone());
123 }
124 }
125 self.poller = Some(poller);
126 self.bus_tokens = bus_tokens;
127 self.token_to_bus = token_to_bus;
128 self.connected = true;
129 Ok(())
130 }
131
132 pub fn enable(&mut self) -> Result<(), Error> {
134 if !self.connected {
135 return Err(Error::NotConnected);
136 }
137 let opening_names: Vec<String> = self
138 .group_order
139 .iter()
140 .filter(|name| {
141 self.groups
142 .get(*name)
143 .and_then(GroupKind::as_gripper)
144 .is_some_and(Gripper::has_opening_control)
145 })
146 .cloned()
147 .collect();
148
149 for name in &opening_names {
152 let is_damiao = self
153 .groups
154 .get(name)
155 .expect("group order invariant")
156 .inner()
157 .bus_vendor()
158 .map(|vendor| vendor == "damiao")?;
159 if is_damiao {
160 self.groups
161 .get_mut(name)
162 .and_then(GroupKind::as_gripper_mut)
163 .expect("opening gripper invariant")
164 .set_mode(CommandKind::PosForce)?;
165 thread::sleep(OPENING_MODE_SETTLE);
166 self.verify_opening_mode(name)?;
167 }
168 }
169
170 for name in &self.group_order.clone() {
171 let has_opening_control = self
172 .groups
173 .get(name)
174 .and_then(GroupKind::as_gripper)
175 .is_some_and(Gripper::has_opening_control);
176 if has_opening_control
177 && self
178 .groups
179 .get(name)
180 .expect("group order invariant")
181 .inner()
182 .bus_vendor()
183 .map(|vendor| vendor != "damiao")?
184 {
185 self.groups
186 .get_mut(name)
187 .and_then(GroupKind::as_gripper_mut)
188 .expect("checked gripper kind")
189 .set_mode(CommandKind::PosForce)?;
190 }
191 if let Some(g) = self.groups.get_mut(name) {
192 g.enable_all()?;
193 }
194 if has_opening_control {
195 if let Err(err) = self.calibrate_gripper_opening(name) {
196 if let Some(g) = self.groups.get_mut(name) {
197 if let Some(gripper) = g.as_gripper_mut() {
198 gripper.clear_opening_calibration();
199 }
200 let _ = g.disable_all();
201 }
202 return Err(err);
203 }
204 }
205 }
206 Ok(())
207 }
208
209 fn verify_opening_mode(&mut self, name: &str) -> Result<(), Error> {
210 let (send_id, recv_id) = self
211 .groups
212 .get_mut(name)
213 .and_then(GroupKind::as_gripper_mut)
214 .expect("opening gripper invariant")
215 .inner_mut()
216 .send_control_mode_readback()?
217 .ok_or_else(|| Error::OpeningControlModeVerificationFailed {
218 name: name.to_string(),
219 reason: "codec does not support CTRL_MODE read-back".to_string(),
220 })?;
221 let deadline = Instant::now() + OPENING_MODE_VERIFY_TIMEOUT;
222 while Instant::now() < deadline {
223 if let Some(value) = self.tick_for_mode(name, send_id, recv_id)? {
224 if value == 4 {
225 return Ok(());
226 }
227 return Err(Error::OpeningControlModeVerificationFailed {
228 name: name.to_string(),
229 reason: format!("CTRL_MODE read-back was {value}, expected 4"),
230 });
231 }
232 self.groups
233 .get_mut(name)
234 .and_then(GroupKind::as_gripper_mut)
235 .expect("opening gripper invariant")
236 .inner_mut()
237 .send_control_mode_readback()?;
238 }
239 Err(Error::OpeningControlModeVerificationFailed {
240 name: name.to_string(),
241 reason: "CTRL_MODE read-back timed out".to_string(),
242 })
243 }
244
245 fn calibrate_gripper_opening(&mut self, name: &str) -> Result<(), Error> {
246 let Some(opening_direction_sign) = self
247 .groups
248 .get_mut(name)
249 .and_then(GroupKind::as_gripper_mut)
250 .and_then(|gripper| {
251 gripper.clear_opening_calibration();
252 gripper.opening_direction_sign()
253 })
254 else {
255 return Ok(());
256 };
257
258 self.refresh_gripper_feedback(name)?;
259
260 let start_position = self.gripper_position(name)?;
261 let closed_position = self.calibrate_gripper_endpoint(
262 name,
263 "close",
264 start_position,
265 -opening_direction_sign,
266 )?;
267 let open_position =
268 self.calibrate_gripper_endpoint(name, "open", closed_position, opening_direction_sign)?;
269
270 let Some(gripper) = self
271 .groups
272 .get_mut(name)
273 .and_then(GroupKind::as_gripper_mut)
274 else {
275 return Ok(());
276 };
277 gripper.set_opening_calibration(closed_position, open_position)
278 }
279
280 fn refresh_gripper_feedback(&mut self, name: &str) -> Result<(), Error> {
281 let (_, initial_sequence) = self.gripper_position_and_sequence(name)?;
282 let deadline = Instant::now() + OPENING_CALIBRATION_REFRESH_TIMEOUT;
283 while Instant::now() < deadline {
284 self.groups
285 .get_mut(name)
286 .and_then(GroupKind::as_gripper_mut)
287 .ok_or_else(|| Error::OpeningCalibrationFailed {
288 name: name.to_string(),
289 reason: "group is not a gripper".to_string(),
290 })?
291 .refresh()?;
292 self.tick(OPENING_CALIBRATION_TICK)?;
293 let (position, sequence) = self.gripper_position_and_sequence(name)?;
294 if sequence != initial_sequence {
295 log_calibration_debug(format_args!(
296 "{name}: fresh pre-calibration feedback position={position:.5}, sequence={sequence}"
297 ));
298 return Ok(());
299 }
300 thread::sleep(Duration::from_millis(1));
301 }
302
303 Err(Error::OpeningCalibrationFailed {
304 name: name.to_string(),
305 reason: "opening calibration did not receive fresh feedback before starting"
306 .to_string(),
307 })
308 }
309
310 fn calibrate_gripper_endpoint(
311 &mut self,
312 name: &str,
313 phase: &str,
314 start_position: f64,
315 raw_direction: f64,
316 ) -> Result<f64, Error> {
317 let target_position = start_position + raw_direction * OPENING_CALIBRATION_TRAVEL_RAD;
318 let mut measured_position = start_position;
319 let mut ticks = 0;
320 let deadline = Instant::now() + OPENING_CALIBRATION_ENDPOINT_DWELL;
321
322 log_calibration_debug(format_args!(
323 "{name}: {phase} phase start={start_position:.5}, target={target_position:.5}, raw_direction={raw_direction:+.1}"
324 ));
325
326 while Instant::now() < deadline {
327 let iteration_start = Instant::now();
328 self.groups
329 .get_mut(name)
330 .and_then(GroupKind::as_gripper_mut)
331 .ok_or_else(|| Error::OpeningCalibrationFailed {
332 name: name.to_string(),
333 reason: "group is not a gripper".to_string(),
334 })?
335 .opening_calibration_command(target_position)?;
336 self.tick(OPENING_CALIBRATION_TICK)?;
337 measured_position = self.gripper_position(name)?;
338 ticks += 1;
339 thread::sleep(OPENING_CALIBRATION_TICK.saturating_sub(iteration_start.elapsed()));
340 }
341
342 log_calibration_debug(format_args!(
343 "{name}: {phase} endpoint_measured={measured_position:.5}, command_target={target_position:.5}, ticks={ticks}, dwell_ms={}",
344 OPENING_CALIBRATION_ENDPOINT_DWELL.as_millis()
345 ));
346 Ok(measured_position)
347 }
348
349 fn gripper_position(&self, name: &str) -> Result<f64, Error> {
350 self.gripper_position_and_sequence(name)
351 .map(|(position, _)| position)
352 }
353
354 fn gripper_position_and_sequence(&self, name: &str) -> Result<(f64, u64), Error> {
355 self.groups
356 .get(name)
357 .and_then(GroupKind::as_gripper)
358 .map(|gripper| (gripper.motor().position(), gripper.motor().state_sequence()))
359 .ok_or_else(|| Error::OpeningCalibrationFailed {
360 name: name.to_string(),
361 reason: "group is not a gripper".to_string(),
362 })
363 }
364
365 pub fn disable(&mut self) -> Result<(), Error> {
368 if !self.connected {
369 return Ok(());
370 }
371 for name in self.group_order.clone().iter().rev() {
372 if let Some(g) = self.groups.get_mut(name) {
373 g.disable_all()?;
374 }
375 }
376 Ok(())
377 }
378
379 pub fn refresh(&mut self) -> Result<(), Error> {
383 if !self.connected {
384 return Err(Error::NotConnected);
385 }
386 for name in &self.group_order.clone() {
387 if let Some(g) = self.groups.get_mut(name) {
388 g.refresh_all()?;
389 }
390 }
391 Ok(())
392 }
393
394 pub fn set_mode(&mut self, mode: CommandKind) -> Result<(), Error> {
399 if !self.connected {
400 return Err(Error::NotConnected);
401 }
402 for name in &self.group_order.clone() {
403 if let Some(g) = self.groups.get_mut(name) {
404 g.set_mode(mode)?;
405 }
406 }
407 Ok(())
408 }
409
410 pub fn tick(&mut self, deadline: Duration) -> Result<(), Error> {
414 self.tick_internal(deadline, None).map(|_| ())
415 }
416
417 fn tick_for_mode(
418 &mut self,
419 name: &str,
420 send_id: u32,
421 recv_id: u32,
422 ) -> Result<Option<u32>, Error> {
423 self.tick_internal(OPENING_MODE_VERIFY_TICK, Some((name, send_id, recv_id)))
424 }
425
426 fn tick_internal(
427 &mut self,
428 deadline: Duration,
429 mode_target: Option<(&str, u32, u32)>,
430 ) -> Result<Option<u32>, Error> {
431 if !self.connected {
432 return Err(Error::NotConnected);
433 }
434 let mode_target =
435 mode_target.map(|(name, send_id, recv_id)| (name.to_string(), send_id, recv_id));
436 let ready = if let Some(p) = self.poller.as_mut() {
438 p.wait(deadline)?
439 } else {
440 Vec::new()
441 };
442 let mut dispatches: Vec<(String, usize, motor_codec::Event)> = Vec::new();
445 let mut mode_value = None;
446 let mut readable_bus_names: Vec<String> = ready
447 .into_iter()
448 .filter_map(|token| self.token_to_bus.get(&token).cloned())
449 .collect();
450 readable_bus_names.extend(
451 self.bus_order
452 .iter()
453 .filter(|bus_name| !self.bus_tokens.contains_key(*bus_name))
454 .cloned(),
455 );
456 for bus_name in readable_bus_names {
457 let bus_arc = self.buses[&bus_name].clone();
458 let mut bus = bus_arc.lock().map_err(|_| Error::BusPoisoned)?;
459 let frames = bus.transport.drain_inbound_nonblocking()?;
460 for frame in &frames {
461 if let Some((ref target_name, send_id, recv_id)) = mode_target {
462 if frame.id == recv_id {
463 let group = self.groups.get(target_name).expect("group order invariant");
464 let motor = &group.inner().motors()[0];
465 let m_ref = motor_codec::MotorRef {
466 motor_type: motor.motor_type(),
467 send_id,
468 recv_id,
469 name: motor.name(),
470 };
471 mode_value = bus
472 .codec
473 .decode_control_mode_readback(frame, m_ref)
474 .map_err(Error::Codec)?;
475 if mode_value.is_some() {
476 continue;
477 }
478 }
479 }
480 let decoded = bus.codec.decode(frame).map_err(Error::Codec)?;
481 if let Some(event) = decoded {
482 let motor_id = match event {
483 motor_codec::Event::State { motor_id, .. } => motor_id,
484 motor_codec::Event::ParamReply { motor_id, .. } => motor_id,
485 motor_codec::Event::Fault { motor_id, .. } => motor_id,
486 _ => continue,
487 };
488 if let Some(route) = bus.routes.get(&motor_id) {
489 dispatches.push((route.group_name.clone(), route.motor_index, event));
490 }
491 }
493 }
495 }
496 for (group_name, motor_index, event) in dispatches {
500 if let Some(g) = self.groups.get_mut(&group_name) {
501 g.apply_event(motor_index, &event);
502 }
503 }
504 Ok(mode_value)
505 }
506}
507
508fn log_calibration_debug(args: std::fmt::Arguments<'_>) {
509 if std::env::var_os("CAN_MOTOR_CONTROL_CALIBRATION_DEBUG").is_some() {
510 eprintln!("[gripper-calibration] {args}");
511 }
512}
513
514impl Drop for Robot {
515 fn drop(&mut self) {
516 for token in self.token_to_bus.keys() {
520 if let Some(p) = self.poller.as_ref() {
521 if let Some(name) = self.token_to_bus.get(token) {
522 if let Some(bus_arc) = self.buses.get(name) {
523 if let Ok(bus) = bus_arc.lock() {
524 if let Some(fd) = bus.transport.raw_fd() {
525 let _ = p.deregister(fd);
526 }
527 }
528 }
529 }
530 }
531 }
532 }
533}
534
535struct PendingBus {
537 name: String,
538 transport: Box<dyn CanBus>,
539 codec: Box<dyn MotorCodec>,
540}
541
542struct PendingGroup {
544 name: String,
545 bus_name: String,
546 kind: GroupSpecKind,
547 motors: Vec<MotorSpec>,
548 opening: Option<GripperOpeningSpec>,
549}
550
551pub struct RobotBuilder {
553 buses: Vec<PendingBus>,
554 groups: Vec<PendingGroup>,
555}
556
557impl Default for RobotBuilder {
558 fn default() -> Self {
559 Self::new()
560 }
561}
562
563impl RobotBuilder {
564 pub fn new() -> Self {
566 Self {
567 buses: Vec::new(),
568 groups: Vec::new(),
569 }
570 }
571
572 pub fn add_bus(
575 mut self,
576 name: impl Into<String>,
577 transport: Box<dyn CanBus>,
578 codec: Box<dyn MotorCodec>,
579 ) -> Self {
580 self.buses.push(PendingBus {
581 name: name.into(),
582 transport,
583 codec,
584 });
585 self
586 }
587
588 pub fn add_arm(
590 mut self,
591 name: impl Into<String>,
592 bus_name: impl Into<String>,
593 motors: Vec<MotorSpec>,
594 ) -> Self {
595 self.groups.push(PendingGroup {
596 name: name.into(),
597 bus_name: bus_name.into(),
598 kind: GroupSpecKind::Arm,
599 motors,
600 opening: None,
601 });
602 self
603 }
604
605 pub fn add_gripper(
608 mut self,
609 name: impl Into<String>,
610 bus_name: impl Into<String>,
611 motor: MotorSpec,
612 ) -> Self {
613 self.groups.push(PendingGroup {
614 name: name.into(),
615 bus_name: bus_name.into(),
616 kind: GroupSpecKind::Gripper,
617 motors: vec![motor],
618 opening: None,
619 });
620 self
621 }
622
623 pub fn add_gripper_with_opening(
625 mut self,
626 name: impl Into<String>,
627 bus_name: impl Into<String>,
628 motor: MotorSpec,
629 opening: GripperOpeningSpec,
630 ) -> Self {
631 self.groups.push(PendingGroup {
632 name: name.into(),
633 bus_name: bus_name.into(),
634 kind: GroupSpecKind::Gripper,
635 motors: vec![motor],
636 opening: Some(opening),
637 });
638 self
639 }
640
641 pub fn add_generic(
643 mut self,
644 name: impl Into<String>,
645 bus_name: impl Into<String>,
646 motors: Vec<MotorSpec>,
647 ) -> Self {
648 self.groups.push(PendingGroup {
649 name: name.into(),
650 bus_name: bus_name.into(),
651 kind: GroupSpecKind::Generic,
652 motors,
653 opening: None,
654 });
655 self
656 }
657
658 pub fn build(self) -> Result<Robot, Error> {
660 let mut bus_order = Vec::with_capacity(self.buses.len());
662 let mut buses: HashMap<String, Arc<Mutex<Bus>>> = HashMap::with_capacity(self.buses.len());
663 for pending in self.buses {
664 if pending.name.trim().is_empty() || pending.name != pending.name.trim() {
665 return Err(Error::ConfigSchema(format!(
666 "bus name '{}' must be non-empty and not have leading/trailing whitespace",
667 pending.name
668 )));
669 }
670 if buses.contains_key(&pending.name) {
671 return Err(Error::DuplicateBusName(pending.name));
672 }
673 let bus = Bus::new(pending.transport, pending.codec);
674 bus_order.push(pending.name.clone());
675 buses.insert(pending.name, Arc::new(Mutex::new(bus)));
676 }
677 let mut group_order = Vec::with_capacity(self.groups.len());
679 let mut groups: HashMap<String, GroupKind> = HashMap::with_capacity(self.groups.len());
680 for pending in self.groups {
681 if pending.name.trim().is_empty() || pending.name != pending.name.trim() {
682 return Err(Error::ConfigSchema(format!(
683 "group name '{}' must be non-empty and not have leading/trailing whitespace",
684 pending.name
685 )));
686 }
687 if groups.contains_key(&pending.name) {
688 return Err(Error::DuplicateGroupName(pending.name));
689 }
690 let bus_arc = buses
691 .get(&pending.bus_name)
692 .ok_or_else(|| Error::UnknownBusName(pending.bus_name.clone()))?;
693 let (vendor, supported_all) = {
694 let bus = bus_arc.lock().map_err(|_| Error::BusPoisoned)?;
695 let v = bus.vendor().to_string();
696 let supported = pending
697 .motors
698 .iter()
699 .find(|m| !bus.codec_supports(m.motor_type))
700 .cloned();
701 (v, supported)
702 };
703 if let Some(unsupported) = supported_all {
704 return Err(Error::MotorNotSupportedByCodec {
705 vendor,
706 motor_type: unsupported.motor_type,
707 bus_name: pending.bus_name,
708 });
709 }
710 if matches!(pending.kind, GroupSpecKind::Gripper) && pending.motors.len() != 1 {
711 return Err(Error::GripperRequiresOneMotor {
712 got: pending.motors.len(),
713 });
714 }
715 if let Some(opening) = pending.opening {
716 if !matches!(pending.kind, GroupSpecKind::Gripper) {
717 return Err(Error::ConfigSchema(format!(
718 "group '{}': opening configuration is only valid for grippers",
719 pending.name
720 )));
721 }
722 if let Some(current) = opening.default_current {
723 validate_opening_current(current)?;
724 }
725 }
726 let motors: Vec<Motor> = pending
728 .motors
729 .into_iter()
730 .map(|s| Motor::new(s.name, s.motor_type, s.send_id, s.recv_id))
731 .collect();
732 let mut group = MotorGroup::new(pending.name.clone(), pending.bus_name, motors);
733 group.attach_bus(bus_arc.clone());
734 let kind = match pending.kind {
735 GroupSpecKind::Arm => GroupKind::Arm(Arm(group)),
736 GroupSpecKind::Gripper => match pending.opening {
737 Some(opening) => GroupKind::Gripper(Gripper::with_opening(group, opening)),
738 None => GroupKind::Gripper(Gripper::raw(group)),
739 },
740 GroupSpecKind::Generic => GroupKind::Generic(Generic(group)),
741 };
742 group_order.push(pending.name.clone());
743 groups.insert(pending.name, kind);
744 }
745 Ok(Robot {
746 buses,
747 bus_order,
748 groups,
749 group_order,
750 connected: false,
751 poller: None,
752 bus_tokens: HashMap::new(),
753 token_to_bus: HashMap::new(),
754 })
755 }
756}
757
758fn validate_opening_current(current: f64) -> Result<(), Error> {
759 if current > 0.0 && current <= 1.0 {
760 Ok(())
761 } else {
762 Err(Error::OpeningCurrentOutOfRange { got: current })
763 }
764}
765
766#[cfg(test)]
767mod tests {
768 use motor_codec::{
769 BusCapabilities, CanFrame, CodecError, Command, Event, FrameFlags, Limits, MotorCodec,
770 MotorRef, MotorTypeId,
771 };
772
773 use super::*;
774 use crate::spec::OpeningDirection;
775 use crate::transport::MockCanBus;
776 use std::sync::atomic::{AtomicUsize, Ordering};
777
778 struct CountingCodec {
779 binds: Arc<AtomicUsize>,
780 decodes: Arc<AtomicUsize>,
781 }
782 impl CountingCodec {
783 fn new() -> (Self, Arc<AtomicUsize>, Arc<AtomicUsize>) {
784 let b = Arc::new(AtomicUsize::new(0));
785 let d = Arc::new(AtomicUsize::new(0));
786 (
787 Self {
788 binds: b.clone(),
789 decodes: d.clone(),
790 },
791 b,
792 d,
793 )
794 }
795 }
796 impl MotorCodec for CountingCodec {
797 fn vendor_name(&self) -> &'static str {
798 "stub"
799 }
800 fn supports(&self, t: MotorTypeId) -> bool {
801 matches!(t, MotorTypeId::Damiao(_))
802 }
803 fn limits(&self, _: MotorTypeId) -> Result<Limits, CodecError> {
804 Ok(Limits {
805 p_max: 1.0,
806 v_max: 1.0,
807 t_max: 1.0,
808 })
809 }
810 fn bind_to_bus(&mut self, _: BusCapabilities) {
811 self.binds.fetch_add(1, Ordering::SeqCst);
812 }
813 fn encode_enable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
814 CanFrame::classical(m.send_id, &[0xFC])
815 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
816 }
817 fn encode_disable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
818 CanFrame::classical(m.send_id, &[0xFD])
819 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
820 }
821 fn encode_set_zero(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
822 CanFrame::classical(m.send_id, &[0xFE])
823 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
824 }
825 fn encode_command(&self, m: MotorRef<'_>, _: &Command) -> Result<CanFrame, CodecError> {
826 CanFrame::classical(m.send_id, &[0x55])
827 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
828 }
829 fn decode(&self, frame: &CanFrame) -> Result<Option<Event>, CodecError> {
830 self.decodes.fetch_add(1, Ordering::SeqCst);
831 if frame.is_fd() || frame.flags.contains(FrameFlags::REMOTE_REQUEST) {
832 return Ok(None);
833 }
834 Ok(Some(Event::State {
835 motor_id: frame.id,
836 q: 1.0,
837 dq: 2.0,
838 tau: 3.0,
839 t_mos: 30,
840 t_rotor: 35,
841 }))
842 }
843 }
844
845 struct FailingCommandCodec;
846 impl MotorCodec for FailingCommandCodec {
847 fn vendor_name(&self) -> &'static str {
848 "failing"
849 }
850 fn supports(&self, t: MotorTypeId) -> bool {
851 matches!(t, MotorTypeId::Damiao(_))
852 }
853 fn limits(&self, _: MotorTypeId) -> Result<Limits, CodecError> {
854 Ok(Limits {
855 p_max: 1.0,
856 v_max: 1.0,
857 t_max: 1.0,
858 })
859 }
860 fn bind_to_bus(&mut self, _: BusCapabilities) {}
861 fn encode_enable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
862 CanFrame::classical(m.send_id, &[0xFC])
863 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
864 }
865 fn encode_disable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
866 CanFrame::classical(m.send_id, &[0xFD])
867 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
868 }
869 fn encode_set_zero(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
870 CanFrame::classical(m.send_id, &[0xFE])
871 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
872 }
873 fn encode_command(&self, _: MotorRef<'_>, _: &Command) -> Result<CanFrame, CodecError> {
874 Err(CodecError::DecodeFailed { reason: "command" })
875 }
876 fn encode_refresh(&self, m: MotorRef<'_>) -> Result<Option<CanFrame>, CodecError> {
877 CanFrame::classical(m.recv_id, &[0xCC])
878 .map(Some)
879 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
880 }
881 fn decode(&self, frame: &CanFrame) -> Result<Option<Event>, CodecError> {
882 Ok(Some(Event::State {
883 motor_id: frame.id,
884 q: 0.0,
885 dq: 0.0,
886 tau: 0.0,
887 t_mos: 30,
888 t_rotor: 35,
889 }))
890 }
891 }
892
893 struct FeedbackCodec {
894 positions: Vec<f64>,
895 decodes: AtomicUsize,
896 }
897 impl FeedbackCodec {
898 fn new(positions: Vec<f64>) -> Self {
899 Self {
900 positions,
901 decodes: AtomicUsize::new(0),
902 }
903 }
904 }
905 impl MotorCodec for FeedbackCodec {
906 fn vendor_name(&self) -> &'static str {
907 "feedback"
908 }
909 fn supports(&self, t: MotorTypeId) -> bool {
910 matches!(t, MotorTypeId::Damiao(_))
911 }
912 fn limits(&self, _: MotorTypeId) -> Result<Limits, CodecError> {
913 Ok(Limits {
914 p_max: 1.0,
915 v_max: 1.0,
916 t_max: 1.0,
917 })
918 }
919 fn bind_to_bus(&mut self, _: BusCapabilities) {}
920 fn encode_enable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
921 CanFrame::classical(m.send_id, &[0xFC])
922 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
923 }
924 fn encode_disable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
925 CanFrame::classical(m.send_id, &[0xFD])
926 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
927 }
928 fn encode_set_zero(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
929 CanFrame::classical(m.send_id, &[0xFE])
930 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
931 }
932 fn encode_command(
933 &self,
934 m: MotorRef<'_>,
935 command: &Command,
936 ) -> Result<CanFrame, CodecError> {
937 let id = if matches!(command, Command::PosForce { .. }) {
938 m.recv_id
939 } else {
940 m.send_id
941 };
942 CanFrame::classical(id, &[0x55])
943 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
944 }
945 fn encode_refresh(&self, m: MotorRef<'_>) -> Result<Option<CanFrame>, CodecError> {
946 CanFrame::classical(m.recv_id, &[0xCC])
947 .map(Some)
948 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
949 }
950 fn decode(&self, frame: &CanFrame) -> Result<Option<Event>, CodecError> {
951 let idx = self.decodes.fetch_add(1, Ordering::SeqCst);
952 let q = self.positions[idx.min(self.positions.len().saturating_sub(1))];
953 Ok(Some(Event::State {
954 motor_id: frame.id,
955 q,
956 dq: 0.0,
957 tau: 0.0,
958 t_mos: 30,
959 t_rotor: 35,
960 }))
961 }
962 }
963
964 struct EchoFeedbackCodec {
965 position: Mutex<f64>,
966 }
967
968 impl EchoFeedbackCodec {
969 fn new() -> Self {
970 Self {
971 position: Mutex::new(0.0),
972 }
973 }
974 }
975
976 impl MotorCodec for EchoFeedbackCodec {
977 fn vendor_name(&self) -> &'static str {
978 "echo-feedback"
979 }
980 fn supports(&self, t: MotorTypeId) -> bool {
981 matches!(t, MotorTypeId::Damiao(_))
982 }
983 fn limits(&self, _: MotorTypeId) -> Result<Limits, CodecError> {
984 Ok(Limits {
985 p_max: 1.0,
986 v_max: 1.0,
987 t_max: 1.0,
988 })
989 }
990 fn bind_to_bus(&mut self, _: BusCapabilities) {}
991 fn encode_enable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
992 CanFrame::classical(m.send_id, &[0xFC])
993 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
994 }
995 fn encode_disable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
996 CanFrame::classical(m.send_id, &[0xFD])
997 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
998 }
999 fn encode_set_zero(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
1000 CanFrame::classical(m.send_id, &[0xFE])
1001 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
1002 }
1003 fn encode_command(
1004 &self,
1005 m: MotorRef<'_>,
1006 command: &Command,
1007 ) -> Result<CanFrame, CodecError> {
1008 if let Command::PosForce { q, .. } = *command {
1009 *self.position.lock().unwrap() = q;
1010 CanFrame::classical(m.recv_id, &[0x55])
1011 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
1012 } else {
1013 CanFrame::classical(m.send_id, &[0x55])
1014 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
1015 }
1016 }
1017 fn encode_refresh(&self, m: MotorRef<'_>) -> Result<Option<CanFrame>, CodecError> {
1018 CanFrame::classical(m.recv_id, &[0xCC])
1019 .map(Some)
1020 .map_err(|_| CodecError::DecodeFailed { reason: "frame" })
1021 }
1022 fn decode(&self, frame: &CanFrame) -> Result<Option<Event>, CodecError> {
1023 Ok(Some(Event::State {
1024 motor_id: frame.id,
1025 q: *self.position.lock().unwrap(),
1026 dq: 0.0,
1027 tau: 0.0,
1028 t_mos: 30,
1029 t_rotor: 35,
1030 }))
1031 }
1032 }
1033
1034 struct ModeLifecycleCodec {
1037 reply: Option<u32>,
1038 position: Mutex<f64>,
1039 }
1040
1041 impl MotorCodec for ModeLifecycleCodec {
1042 fn vendor_name(&self) -> &'static str {
1043 "damiao"
1044 }
1045 fn supports(&self, t: MotorTypeId) -> bool {
1046 matches!(t, MotorTypeId::Damiao(_))
1047 }
1048 fn limits(&self, _: MotorTypeId) -> Result<Limits, CodecError> {
1049 Ok(Limits {
1050 p_max: 10.0,
1051 v_max: 10.0,
1052 t_max: 10.0,
1053 })
1054 }
1055 fn bind_to_bus(&mut self, _: BusCapabilities) {}
1056 fn encode_enable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
1057 Ok(CanFrame::classical(m.send_id, &[0xfc]).unwrap())
1058 }
1059 fn encode_disable(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
1060 Ok(CanFrame::classical(m.send_id, &[0xfd]).unwrap())
1061 }
1062 fn encode_set_zero(&self, m: MotorRef<'_>) -> Result<CanFrame, CodecError> {
1063 Ok(CanFrame::classical(m.send_id, &[0xfe]).unwrap())
1064 }
1065 fn encode_command(&self, m: MotorRef<'_>, cmd: &Command) -> Result<CanFrame, CodecError> {
1066 let marker = if let Command::PosForce { q, .. } = cmd {
1067 *self.position.lock().unwrap() = *q;
1068 0x55
1069 } else {
1070 0x01
1071 };
1072 Ok(CanFrame::classical(m.recv_id, &[marker]).unwrap())
1073 }
1074 fn encode_refresh(&self, m: MotorRef<'_>) -> Result<Option<CanFrame>, CodecError> {
1075 Ok(Some(CanFrame::classical(m.recv_id, &[0xcc]).unwrap()))
1076 }
1077 fn encode_set_mode(
1078 &self,
1079 m: MotorRef<'_>,
1080 _: CommandKind,
1081 ) -> Result<Option<CanFrame>, CodecError> {
1082 Ok(Some(CanFrame::classical(m.send_id, &[0x10]).unwrap()))
1083 }
1084 fn encode_control_mode_readback(
1085 &self,
1086 m: MotorRef<'_>,
1087 ) -> Result<Option<CanFrame>, CodecError> {
1088 let id = if self.reply.is_some() {
1089 m.recv_id
1090 } else {
1091 0x7ff
1092 };
1093 let mut payload = [0u8; 8];
1094 payload[0] = (m.send_id & 0xff) as u8;
1095 payload[2] = 0x20;
1096 payload[3] = self.reply.unwrap_or(0) as u8;
1097 Ok(Some(CanFrame::classical(id, &payload).unwrap()))
1098 }
1099 fn decode_control_mode_readback(
1100 &self,
1101 frame: &CanFrame,
1102 _: MotorRef<'_>,
1103 ) -> Result<Option<u32>, CodecError> {
1104 if frame.payload()[2] == 0x20 {
1105 Ok(Some(frame.payload()[3] as u32))
1106 } else {
1107 Ok(None)
1108 }
1109 }
1110 fn decode(&self, frame: &CanFrame) -> Result<Option<Event>, CodecError> {
1111 if frame.payload()[0] == 0x20 || frame.payload()[0] == 0x10 || frame.id == 0x7ff {
1112 return Ok(None);
1113 }
1114 Ok(Some(Event::State {
1115 motor_id: frame.id,
1116 q: *self.position.lock().unwrap(),
1117 dq: 0.0,
1118 tau: 0.0,
1119 t_mos: 20,
1120 t_rotor: 20,
1121 }))
1122 }
1123 }
1124
1125 fn motor(name: &str, send: u32, recv: u32) -> MotorSpec {
1126 MotorSpec::new(name, MotorTypeId::Damiao(3), send, recv)
1127 }
1128
1129 #[test]
1130 fn build_simple_robot() {
1131 let (codec, binds, _) = CountingCodec::new();
1132 let robot = RobotBuilder::new()
1133 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(codec))
1134 .add_arm(
1135 "arm",
1136 "main",
1137 vec![motor("j0", 0x01, 0x11), motor("j1", 0x02, 0x12)],
1138 )
1139 .build()
1140 .unwrap();
1141 assert_eq!(robot.bus_names().collect::<Vec<_>>(), vec!["main"]);
1142 assert_eq!(robot.group_names().collect::<Vec<_>>(), vec!["arm"]);
1143 assert_eq!(binds.load(Ordering::SeqCst), 1);
1144 }
1145
1146 #[test]
1147 fn duplicate_bus_name_rejected() {
1148 let (c1, _, _) = CountingCodec::new();
1149 let (c2, _, _) = CountingCodec::new();
1150 let r = RobotBuilder::new()
1151 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c1))
1152 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c2))
1153 .build();
1154 assert!(matches!(r, Err(Error::DuplicateBusName(ref s)) if s == "main"));
1155 }
1156
1157 #[test]
1158 fn duplicate_group_name_rejected() {
1159 let (c, _, _) = CountingCodec::new();
1160 let r = RobotBuilder::new()
1161 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1162 .add_arm("arm", "main", vec![motor("j0", 0x01, 0x11)])
1163 .add_arm("arm", "main", vec![motor("j1", 0x02, 0x12)])
1164 .build();
1165 assert!(matches!(r, Err(Error::DuplicateGroupName(ref s)) if s == "arm"));
1166 }
1167
1168 #[test]
1169 fn unknown_bus_name_rejected() {
1170 let r = RobotBuilder::new()
1171 .add_arm("arm", "ghost", vec![motor("j0", 0x01, 0x11)])
1172 .build();
1173 assert!(matches!(r, Err(Error::UnknownBusName(ref s)) if s == "ghost"));
1174 }
1175
1176 #[test]
1177 fn motor_not_supported_by_codec() {
1178 let (c, _, _) = CountingCodec::new();
1179 let r = RobotBuilder::new()
1180 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1181 .add_arm(
1182 "arm",
1183 "main",
1184 vec![MotorSpec::new("j0", MotorTypeId::Robostride(0), 0x01, 0x11)],
1185 )
1186 .build();
1187 assert!(matches!(r, Err(Error::MotorNotSupportedByCodec { .. })));
1188 }
1189
1190 #[test]
1191 fn gripper_requires_one_motor() {
1192 let (c, _, _) = CountingCodec::new();
1193 let r = RobotBuilder::new()
1197 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1198 .add_gripper("g", "main", motor("g0", 0x05, 0x18))
1199 .build();
1200 assert!(r.is_ok());
1201 }
1202
1203 #[test]
1204 fn build_does_not_open_sockets() {
1205 let (c, _, _) = CountingCodec::new();
1209 let robot = RobotBuilder::new()
1210 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1211 .add_arm("arm", "main", vec![motor("j0", 0x01, 0x11)])
1212 .build()
1213 .unwrap();
1214 assert!(!robot.is_connected());
1215 }
1216
1217 #[test]
1218 fn connect_populates_routes() {
1219 let (c, _, _) = CountingCodec::new();
1220 let mut robot = RobotBuilder::new()
1221 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1222 .add_arm(
1223 "arm",
1224 "main",
1225 vec![motor("j0", 0x01, 0x11), motor("j1", 0x02, 0x12)],
1226 )
1227 .build()
1228 .unwrap();
1229 robot.connect().unwrap();
1230 assert!(robot.is_connected());
1231 let bus = robot.bus("main").unwrap().lock().unwrap();
1232 assert_eq!(bus.routes.len(), 2);
1233 let r11 = bus.routes.get(&0x11).unwrap();
1234 assert_eq!(r11.group_name, "arm");
1235 assert_eq!(r11.motor_index, 0);
1236 }
1237
1238 #[test]
1239 fn can_id_collision_across_groups_on_same_bus() {
1240 let (c, _, _) = CountingCodec::new();
1241 let mut robot = RobotBuilder::new()
1242 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1243 .add_arm("arm", "main", vec![motor("j0", 0x01, 0x18)])
1244 .add_gripper("g", "main", motor("g0", 0x05, 0x18))
1245 .build()
1246 .unwrap();
1247 let r = robot.connect();
1248 assert!(matches!(
1249 r,
1250 Err(Error::CanIdCollision {
1251 ref bus_name,
1252 recv_id: 0x18,
1253 ..
1254 }) if bus_name == "main"
1255 ));
1256 }
1257
1258 #[test]
1259 fn collision_across_buses_is_not_error() {
1260 let (c1, _, _) = CountingCodec::new();
1261 let (c2, _, _) = CountingCodec::new();
1262 let mut robot = RobotBuilder::new()
1263 .add_bus("left", Box::new(MockCanBus::new("l")), Box::new(c1))
1264 .add_bus("right", Box::new(MockCanBus::new("r")), Box::new(c2))
1265 .add_arm("la", "left", vec![motor("j0", 0x01, 0x11)])
1266 .add_arm("ra", "right", vec![motor("j0", 0x01, 0x11)])
1267 .build()
1268 .unwrap();
1269 robot.connect().unwrap();
1270 }
1271
1272 #[test]
1273 fn tick_before_connect_returns_not_connected() {
1274 let (c, _, _) = CountingCodec::new();
1275 let mut robot = RobotBuilder::new()
1276 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1277 .add_arm("arm", "main", vec![motor("j0", 0x01, 0x11)])
1278 .build()
1279 .unwrap();
1280 let r = robot.tick(Duration::from_millis(1));
1281 assert!(matches!(r, Err(Error::NotConnected)));
1282 }
1283
1284 #[test]
1285 fn enable_before_connect_returns_not_connected() {
1286 let (c, _, _) = CountingCodec::new();
1287 let mut robot = RobotBuilder::new()
1288 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1289 .add_arm("arm", "main", vec![motor("j0", 0x01, 0x11)])
1290 .build()
1291 .unwrap();
1292 assert!(matches!(robot.enable(), Err(Error::NotConnected)));
1293 }
1294
1295 #[test]
1296 fn enable_calibrates_configured_gripper_opening() {
1297 let mut robot = RobotBuilder::new()
1298 .add_bus(
1299 "main",
1300 Box::new(MockCanBus::new("m")),
1301 Box::new(EchoFeedbackCodec::new()),
1302 )
1303 .add_gripper_with_opening(
1304 "g",
1305 "main",
1306 motor("g0", 0x05, 0x18),
1307 GripperOpeningSpec::new(OpeningDirection::IncreasingPosition, Some(0.2)),
1308 )
1309 .build()
1310 .unwrap();
1311 robot.connect().unwrap();
1312 robot.enable().unwrap();
1313 let gripper = robot
1314 .group_mut("g")
1315 .and_then(|group| group.as_gripper_mut())
1316 .unwrap();
1317 gripper.set_opening(0.5, None).unwrap();
1318 }
1319
1320 fn lifecycle_robot(reply: Option<u32>) -> (Robot, MockCanBus) {
1321 let mock = MockCanBus::new("m");
1322 let inspector = mock.clone();
1323 let mut robot = RobotBuilder::new()
1324 .add_bus(
1325 "main",
1326 Box::new(mock),
1327 Box::new(ModeLifecycleCodec {
1328 reply,
1329 position: Mutex::new(0.0),
1330 }),
1331 )
1332 .add_gripper_with_opening(
1333 "g",
1334 "main",
1335 motor("g0", 0x05, 0x18),
1336 GripperOpeningSpec::new(OpeningDirection::IncreasingPosition, Some(0.2)),
1337 )
1338 .build()
1339 .unwrap();
1340 robot.connect().unwrap();
1341 (robot, inspector)
1342 }
1343
1344 #[test]
1345 fn damiao_opening_lifecycle_orders_write_readback_enable_then_calibration() {
1346 let (mut robot, bus) = lifecycle_robot(Some(4));
1347 robot.enable().unwrap();
1348 let markers: Vec<u8> = bus
1349 .sent_frames()
1350 .iter()
1351 .map(|f| {
1352 if f.len >= 4 && f.payload()[2] == 0x20 {
1353 0x20
1354 } else {
1355 f.payload()[0]
1356 }
1357 })
1358 .collect();
1359 assert_eq!(&markers[..4], &[0x10, 0x20, 0xfc, 0xcc]);
1360 assert!(markers.iter().skip(3).any(|marker| *marker == 0x55));
1361 }
1362
1363 #[test]
1364 fn non_four_mode_readback_stops_before_enable_or_calibration() {
1365 let (mut robot, bus) = lifecycle_robot(Some(3));
1366 assert!(matches!(
1367 robot.enable(),
1368 Err(Error::OpeningControlModeVerificationFailed { .. })
1369 ));
1370 let markers: Vec<u8> = bus
1371 .sent_frames()
1372 .iter()
1373 .map(|f| {
1374 if f.len >= 4 && f.payload()[2] == 0x20 {
1375 0x20
1376 } else {
1377 f.payload()[0]
1378 }
1379 })
1380 .collect();
1381 assert!(markers.iter().all(|m| *m != 0xfc && *m != 0x55));
1382 }
1383
1384 #[test]
1385 fn mode_readback_timeout_stops_before_enable_or_calibration() {
1386 let (mut robot, bus) = lifecycle_robot(None);
1387 assert!(matches!(
1388 robot.enable(),
1389 Err(Error::OpeningControlModeVerificationFailed { .. })
1390 ));
1391 let markers: Vec<u8> = bus
1392 .sent_frames()
1393 .iter()
1394 .map(|f| {
1395 if f.len >= 4 && f.payload()[2] == 0x20 {
1396 0x20
1397 } else {
1398 f.payload()[0]
1399 }
1400 })
1401 .collect();
1402 assert!(markers.iter().all(|m| *m != 0xfc && *m != 0x55));
1403 }
1404
1405 #[test]
1406 fn enable_fails_when_measured_feedback_span_is_zero() {
1407 let mut robot = RobotBuilder::new()
1408 .add_bus(
1409 "main",
1410 Box::new(MockCanBus::new("m")),
1411 Box::new(FeedbackCodec::new(vec![0.0])),
1412 )
1413 .add_gripper_with_opening(
1414 "g",
1415 "main",
1416 motor("g0", 0x05, 0x18),
1417 GripperOpeningSpec::new(OpeningDirection::IncreasingPosition, Some(0.2)),
1418 )
1419 .build()
1420 .unwrap();
1421 robot.connect().unwrap();
1422 let result = robot.enable();
1423 assert!(
1424 matches!(
1425 result,
1426 Err(Error::OpeningCalibrationFailed { ref reason, .. })
1427 if reason.starts_with("calibrated opening span is too small")
1428 ),
1429 "unexpected result: {result:?}"
1430 );
1431 }
1432
1433 #[test]
1434 fn enable_fails_when_measured_feedback_span_is_too_small() {
1435 let mut robot = RobotBuilder::new()
1436 .add_bus(
1437 "main",
1438 Box::new(MockCanBus::new("m")),
1439 Box::new(FeedbackCodec::new(
1440 [0.0]
1441 .into_iter()
1442 .chain([-0.06; 24])
1443 .chain([0.02; 24])
1444 .collect(),
1445 )),
1446 )
1447 .add_gripper_with_opening(
1448 "g",
1449 "main",
1450 motor("g0", 0x05, 0x18),
1451 GripperOpeningSpec::new(OpeningDirection::IncreasingPosition, Some(0.2)),
1452 )
1453 .build()
1454 .unwrap();
1455 robot.connect().unwrap();
1456 let result = robot.enable();
1457 assert!(
1458 matches!(
1459 result,
1460 Err(Error::OpeningCalibrationFailed { ref reason, .. })
1461 if reason.starts_with("calibrated opening span is too small")
1462 ),
1463 "unexpected result: {result:?}"
1464 );
1465 }
1466
1467 #[test]
1468 fn set_opening_before_enable_calibration_requires_calibration() {
1469 let mut robot = RobotBuilder::new()
1470 .add_bus(
1471 "main",
1472 Box::new(MockCanBus::new("m")),
1473 Box::new(FeedbackCodec::new(vec![0.0])),
1474 )
1475 .add_gripper_with_opening(
1476 "g",
1477 "main",
1478 motor("g0", 0x05, 0x18),
1479 GripperOpeningSpec::new(OpeningDirection::IncreasingPosition, Some(0.2)),
1480 )
1481 .build()
1482 .unwrap();
1483 let gripper = robot
1484 .group_mut("g")
1485 .and_then(|group| group.as_gripper_mut())
1486 .unwrap();
1487 assert!(matches!(
1488 gripper.set_opening(0.5, None),
1489 Err(Error::OpeningCalibrationRequired)
1490 ));
1491 }
1492
1493 #[test]
1494 fn enable_fails_when_gripper_opening_calibration_command_fails() {
1495 let mut robot = RobotBuilder::new()
1496 .add_bus(
1497 "main",
1498 Box::new(MockCanBus::new("m")),
1499 Box::new(FailingCommandCodec),
1500 )
1501 .add_gripper_with_opening(
1502 "g",
1503 "main",
1504 motor("g0", 0x05, 0x18),
1505 GripperOpeningSpec::new(OpeningDirection::IncreasingPosition, Some(0.2)),
1506 )
1507 .build()
1508 .unwrap();
1509 robot.connect().unwrap();
1510 assert!(matches!(robot.enable(), Err(Error::Codec(_))));
1511 }
1512
1513 #[test]
1514 fn disable_without_enable_is_noop() {
1515 let (c, _, _) = CountingCodec::new();
1516 let mut robot = RobotBuilder::new()
1517 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1518 .add_arm("arm", "main", vec![motor("j0", 0x01, 0x11)])
1519 .build()
1520 .unwrap();
1521 robot.disable().unwrap();
1523 robot.connect().unwrap();
1525 robot.disable().unwrap();
1526 }
1527
1528 #[test]
1529 fn tick_quiet_buses_returns_within_deadline() {
1530 let (c, _, _) = CountingCodec::new();
1531 let mut robot = RobotBuilder::new()
1532 .add_bus("main", Box::new(MockCanBus::new("m")), Box::new(c))
1533 .add_arm("arm", "main", vec![motor("j0", 0x01, 0x11)])
1534 .build()
1535 .unwrap();
1536 robot.connect().unwrap();
1537 let t0 = std::time::Instant::now();
1538 robot.tick(Duration::from_millis(5)).unwrap();
1539 let elapsed = t0.elapsed();
1540 assert!(elapsed < Duration::from_millis(50), "tick took {elapsed:?}");
1544 }
1545
1546 #[test]
1552 fn group_source_does_not_call_drain() {
1553 let src = include_str!("group.rs");
1554 let scan = match src.find("#[cfg(test)]") {
1555 Some(idx) => &src[..idx],
1556 None => src,
1557 };
1558 for needle in ["drain_inbound_nonblocking(", ".drain_inbound_nonblocking"] {
1559 assert!(
1560 !scan.contains(needle),
1561 "group.rs must not call {needle}; only Robot::tick may"
1562 );
1563 }
1564 }
1565}