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}