Skip to main content

motor_codec/
codec.rs

1//! The [`MotorCodec`] trait — the seam between `can-motor-control` and vendor-specific
2//! motor protocols.
3
4use thiserror::Error;
5
6use crate::{
7    BusCapabilities, CanFrame, Command, CommandKind, Event, Limits, MotorRef, MotorTypeId,
8};
9
10/// The vendor-agnostic codec contract.
11///
12/// Every vendor codec (Damiao, Robostride, MyActuator, ...) implements this
13/// trait. `can-motor-control` uses `Box<dyn MotorCodec>` exclusively — it never
14/// depends on a specific vendor's crate.
15///
16/// The trait is intentionally object-safe (no generics, no `Self: Sized`
17/// constraints on the methods) so a `Box<dyn MotorCodec>` is a valid field
18/// type.
19pub trait MotorCodec: Send + Sync {
20    /// Vendor short name used in error messages and the TOML registry
21    /// (e.g. `"damiao"`).
22    fn vendor_name(&self) -> &'static str;
23
24    /// True iff this codec can encode commands for and decode events from the
25    /// supplied motor type.
26    fn supports(&self, motor_type: MotorTypeId) -> bool;
27
28    /// Per-motor-type physical limits.
29    ///
30    /// Returns [`CodecError::UnknownMotorType`] when the motor type is not in
31    /// this codec's vendor space (or is an unknown SKU within the vendor space).
32    fn limits(&self, motor_type: MotorTypeId) -> Result<Limits, CodecError>;
33
34    /// Called exactly once when this codec is bound to a [`crate::caps::BusCapabilities`].
35    ///
36    /// The codec MAY remember the capabilities for later use (e.g. to decide
37    /// whether to emit CAN-FD frames). Codecs may assume `bind_to_bus` has
38    /// been called by the time any encode method is invoked.
39    fn bind_to_bus(&mut self, caps: BusCapabilities);
40
41    /// Encode the lifecycle "enable motor" command.
42    fn encode_enable(&self, motor: MotorRef<'_>) -> Result<CanFrame, CodecError>;
43
44    /// Encode the lifecycle "disable motor" command.
45    fn encode_disable(&self, motor: MotorRef<'_>) -> Result<CanFrame, CodecError>;
46
47    /// Encode the lifecycle "set this position as zero" command.
48    fn encode_set_zero(&self, motor: MotorRef<'_>) -> Result<CanFrame, CodecError>;
49
50    /// Encode a control-mode command.
51    fn encode_command(&self, motor: MotorRef<'_>, cmd: &Command) -> Result<CanFrame, CodecError>;
52
53    /// Encode a "request current motor state" frame, if the vendor protocol has
54    /// one.
55    ///
56    /// The returned frame MUST request a state-feedback reply **without
57    /// commanding any motion**. Returns `Ok(None)` (the default) when the codec
58    /// has no such query; callers skip those motors. This lets a read loop poll
59    /// state via `refresh` then [`MotorCodec::decode`] without applying torque.
60    fn encode_refresh(&self, motor: MotorRef<'_>) -> Result<Option<CanFrame>, CodecError> {
61        let _ = motor;
62        Ok(None)
63    }
64
65    /// Encode a "set the motor's persistent control mode" frame, if the vendor
66    /// protocol supports it.
67    ///
68    /// `mode` selects which control law the motor will accept
69    /// (MIT / PosVel / Vel / PosForce). Returns `Ok(None)` (the default) when
70    /// the codec has no such command; callers skip those motors. This commands
71    /// no motion — call it once at startup, before the matching
72    /// [`MotorCodec::encode_command`] mode.
73    fn encode_set_mode(
74        &self,
75        motor: MotorRef<'_>,
76        mode: CommandKind,
77    ) -> Result<Option<CanFrame>, CodecError> {
78        let _ = (motor, mode);
79        Ok(None)
80    }
81
82    /// Encode a private control-mode read-back query, when supported.
83    fn encode_control_mode_readback(
84        &self,
85        motor: MotorRef<'_>,
86    ) -> Result<Option<CanFrame>, CodecError> {
87        let _ = motor;
88        Ok(None)
89    }
90
91    /// Decode a private control-mode read-back response for `motor`.
92    fn decode_control_mode_readback(
93        &self,
94        frame: &CanFrame,
95        motor: MotorRef<'_>,
96    ) -> Result<Option<u32>, CodecError> {
97        let _ = (frame, motor);
98        Ok(None)
99    }
100
101    /// Decode an inbound frame.
102    ///
103    /// Returns `Ok(Some(event))` for a recognized inbound message,
104    /// `Ok(None)` for frames the codec does not recognize (foreign vendor, or
105    /// a CAN ID outside the codec's address range), and
106    /// `Err(CodecError::DecodeFailed { .. })` for frames that look like the
107    /// codec's vendor but fail to parse.
108    fn decode(&self, frame: &CanFrame) -> Result<Option<Event>, CodecError>;
109}
110
111/// Errors returned by [`MotorCodec`] implementations.
112#[derive(Debug, Clone, PartialEq, Eq, Error)]
113#[non_exhaustive]
114pub enum CodecError {
115    /// The motor type is unknown to this vendor codec.
116    #[error("{vendor} codec does not know motor type id {type_id:#06x}")]
117    UnknownMotorType {
118        /// Vendor short name.
119        vendor: &'static str,
120        /// Opaque discriminant the codec did not recognize.
121        type_id: u16,
122    },
123
124    /// The requested command mode is not implemented for the (codec, motor type)
125    /// pair.
126    #[error("{vendor} codec does not support command mode {mode:?}")]
127    CommandNotSupported {
128        /// Vendor short name.
129        vendor: &'static str,
130        /// The command discriminant.
131        mode: CommandKind,
132    },
133
134    /// A recognized vendor frame could not be parsed (truncated payload,
135    /// unsupported sub-protocol revision, etc.).
136    #[error("frame decode failed: {reason}")]
137    DecodeFailed {
138        /// Human-readable explanation.
139        reason: &'static str,
140    },
141
142    /// A command field exceeds the motor's published limit.
143    #[error("value out of range: field {field}")]
144    OutOfRange {
145        /// Field name (`"q"`, `"dq"`, `"tau"`, `"kp"`, ...).
146        field: &'static str,
147    },
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use alloc::boxed::Box;
154
155    /// Dummy codec used only for object-safety assertions.
156    struct Dummy;
157    impl MotorCodec for Dummy {
158        fn vendor_name(&self) -> &'static str {
159            "dummy"
160        }
161        fn supports(&self, _: MotorTypeId) -> bool {
162            false
163        }
164        fn limits(&self, _: MotorTypeId) -> Result<Limits, CodecError> {
165            Err(CodecError::UnknownMotorType {
166                vendor: "dummy",
167                type_id: 0,
168            })
169        }
170        fn bind_to_bus(&mut self, _: BusCapabilities) {}
171        fn encode_enable(&self, _: MotorRef<'_>) -> Result<CanFrame, CodecError> {
172            Err(CodecError::DecodeFailed { reason: "stub" })
173        }
174        fn encode_disable(&self, _: MotorRef<'_>) -> Result<CanFrame, CodecError> {
175            Err(CodecError::DecodeFailed { reason: "stub" })
176        }
177        fn encode_set_zero(&self, _: MotorRef<'_>) -> Result<CanFrame, CodecError> {
178            Err(CodecError::DecodeFailed { reason: "stub" })
179        }
180        fn encode_command(&self, _: MotorRef<'_>, _: &Command) -> Result<CanFrame, CodecError> {
181            Err(CodecError::DecodeFailed { reason: "stub" })
182        }
183        fn decode(&self, _: &CanFrame) -> Result<Option<Event>, CodecError> {
184            Ok(None)
185        }
186    }
187
188    #[test]
189    fn trait_object_safe() {
190        let _: Box<dyn MotorCodec> = Box::new(Dummy);
191    }
192
193    #[test]
194    fn default_encode_refresh_is_none() {
195        let m = MotorRef {
196            motor_type: MotorTypeId::Damiao(3),
197            send_id: 1,
198            recv_id: 0x11,
199            name: "j",
200        };
201        assert!(matches!(Dummy.encode_refresh(m), Ok(None)));
202    }
203
204    #[test]
205    fn default_encode_set_mode_is_none() {
206        let m = MotorRef {
207            motor_type: MotorTypeId::Damiao(3),
208            send_id: 1,
209            recv_id: 0x11,
210            name: "j",
211        };
212        assert!(matches!(
213            Dummy.encode_set_mode(m, CommandKind::Mit),
214            Ok(None)
215        ));
216    }
217
218    #[test]
219    fn out_of_range_variant_constructible() {
220        let e = CodecError::OutOfRange { field: "tau" };
221        let s = alloc::format!("{e}");
222        assert!(s.contains("tau"));
223    }
224
225    #[test]
226    fn unknown_motor_type_display() {
227        let e = CodecError::UnknownMotorType {
228            vendor: "damiao",
229            type_id: 0xFFFF,
230        };
231        let s = alloc::format!("{e}");
232        assert!(s.contains("damiao"));
233        assert!(s.contains("0xffff"));
234    }
235}