Skip to main content

motor_codec/
frame.rs

1//! Unified CAN frame representation covering both classical CAN and CAN-FD.
2
3use bitflags::bitflags;
4use thiserror::Error;
5
6bitflags! {
7    /// Metadata bits attached to every [`CanFrame`].
8    ///
9    /// A frame's classical-vs-FD nature is determined entirely by the
10    /// [`FrameFlags::FD_FORMAT`] bit — there is no separate `CanFdFrame` type.
11    #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
12    pub struct FrameFlags: u8 {
13        /// 29-bit extended CAN identifier (vs 11-bit base ID).
14        const EXTENDED_ID = 0b0000_0001;
15        /// CAN-FD format (FDF).
16        const FD_FORMAT = 0b0000_0010;
17        /// CAN-FD data-phase uses the higher bit rate (BRS). FD only.
18        const BIT_RATE_SWITCH = 0b0000_0100;
19        /// CAN-FD error-state indicator (ESI). FD only.
20        const ERROR_STATE = 0b0000_1000;
21        /// Classical-CAN remote-transmission request (RTR). Mutually exclusive
22        /// with [`FrameFlags::FD_FORMAT`].
23        const REMOTE_REQUEST = 0b0001_0000;
24    }
25}
26
27/// Errors that can arise constructing a [`CanFrame`].
28#[derive(Debug, Clone, PartialEq, Eq, Error)]
29#[non_exhaustive]
30pub enum FrameError {
31    /// Classical CAN payloads cannot exceed 8 bytes.
32    #[error("classical CAN payload too long: got {got} bytes, max is 8")]
33    PayloadTooLong {
34        /// Length the caller attempted to pack.
35        got: usize,
36    },
37
38    /// CAN-FD payload length must be one of the valid DLC values
39    /// (0..=8, 12, 16, 20, 24, 32, 48, 64).
40    #[error("invalid CAN-FD payload length: got {got} bytes")]
41    InvalidFdLength {
42        /// Length the caller attempted to pack.
43        got: usize,
44    },
45
46    /// A flag combination forbidden by the CAN specification was supplied.
47    #[error("incompatible frame flags: {reason}")]
48    IncompatibleFlags {
49        /// Human-readable description of the conflict.
50        reason: &'static str,
51    },
52}
53
54/// Unified CAN frame: covers both classical CAN (8-byte payload) and CAN-FD
55/// (up to 64-byte payload).
56///
57/// The inline `[u8; 64]` payload buffer is sized for CAN-FD's maximum DLC;
58/// classical frames use only the first 8 bytes and pay ~56 bytes of latent
59/// memory in exchange for full upper-layer agnosticism between classical and
60/// FD. See `openspec/changes/walking-skeleton-single-arm/design.md` Decision 14.
61#[derive(Copy, Clone, Debug, PartialEq, Eq)]
62pub struct CanFrame {
63    /// CAN identifier (11-bit base or 29-bit extended; see [`FrameFlags::EXTENDED_ID`]).
64    pub id: u32,
65    /// Metadata bits (FD/BRS/ESI/EXTENDED/RTR).
66    pub flags: FrameFlags,
67    /// Payload length in bytes. `len <= 8` for classical, `len <= 64` for FD.
68    pub len: u8,
69    data: [u8; 64],
70}
71
72impl CanFrame {
73    /// Construct a classical CAN frame with an 11-bit base identifier.
74    pub fn classical(id: u32, payload: &[u8]) -> Result<Self, FrameError> {
75        if payload.len() > 8 {
76            return Err(FrameError::PayloadTooLong { got: payload.len() });
77        }
78        let mut data = [0u8; 64];
79        data[..payload.len()].copy_from_slice(payload);
80        Ok(Self {
81            id,
82            flags: FrameFlags::empty(),
83            len: payload.len() as u8,
84            data,
85        })
86    }
87
88    /// Construct a classical CAN frame with a 29-bit extended identifier.
89    pub fn classical_extended(id: u32, payload: &[u8]) -> Result<Self, FrameError> {
90        let mut f = Self::classical(id, payload)?;
91        f.flags |= FrameFlags::EXTENDED_ID;
92        Ok(f)
93    }
94
95    /// Construct a CAN-FD frame. Length must be a valid FD DLC; BRS is set by default.
96    pub fn fd(id: u32, payload: &[u8]) -> Result<Self, FrameError> {
97        if !is_valid_fd_dlc(payload.len()) {
98            return Err(FrameError::InvalidFdLength { got: payload.len() });
99        }
100        let mut data = [0u8; 64];
101        data[..payload.len()].copy_from_slice(payload);
102        Ok(Self {
103            id,
104            flags: FrameFlags::FD_FORMAT | FrameFlags::BIT_RATE_SWITCH,
105            len: payload.len() as u8,
106            data,
107        })
108    }
109
110    /// Construct a CAN-FD frame with a 29-bit extended identifier.
111    pub fn fd_extended(id: u32, payload: &[u8]) -> Result<Self, FrameError> {
112        let mut f = Self::fd(id, payload)?;
113        f.flags |= FrameFlags::EXTENDED_ID;
114        Ok(f)
115    }
116
117    /// Construct a frame from raw fields. Performs the flag-compatibility
118    /// validation that [`CanFrame::classical`] / [`CanFrame::fd`] enforce
119    /// automatically.
120    pub fn from_parts(id: u32, flags: FrameFlags, payload: &[u8]) -> Result<Self, FrameError> {
121        check_flag_compat(flags)?;
122        if flags.contains(FrameFlags::FD_FORMAT) {
123            if !is_valid_fd_dlc(payload.len()) {
124                return Err(FrameError::InvalidFdLength { got: payload.len() });
125            }
126        } else if payload.len() > 8 {
127            return Err(FrameError::PayloadTooLong { got: payload.len() });
128        }
129        let mut data = [0u8; 64];
130        data[..payload.len()].copy_from_slice(payload);
131        Ok(Self {
132            id,
133            flags,
134            len: payload.len() as u8,
135            data,
136        })
137    }
138
139    /// Return the active payload bytes (`&self.data[..self.len]`).
140    pub fn payload(&self) -> &[u8] {
141        &self.data[..self.len as usize]
142    }
143
144    /// True iff [`FrameFlags::FD_FORMAT`] is set.
145    pub fn is_fd(&self) -> bool {
146        self.flags.contains(FrameFlags::FD_FORMAT)
147    }
148
149    /// True iff [`FrameFlags::EXTENDED_ID`] is set.
150    pub fn is_extended(&self) -> bool {
151        self.flags.contains(FrameFlags::EXTENDED_ID)
152    }
153}
154
155/// True iff `len` is a valid CAN-FD payload length (0..=8, 12, 16, 20, 24, 32, 48, 64).
156pub fn is_valid_fd_dlc(len: usize) -> bool {
157    matches!(len, 0..=8 | 12 | 16 | 20 | 24 | 32 | 48 | 64)
158}
159
160fn check_flag_compat(flags: FrameFlags) -> Result<(), FrameError> {
161    if flags.contains(FrameFlags::FD_FORMAT) && flags.contains(FrameFlags::REMOTE_REQUEST) {
162        return Err(FrameError::IncompatibleFlags {
163            reason: "FD_FORMAT and REMOTE_REQUEST are mutually exclusive",
164        });
165    }
166    Ok(())
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn classical_8byte_ok() {
175        let f = CanFrame::classical(0x101, &[0xFF; 8]).unwrap();
176        assert_eq!(f.id, 0x101);
177        assert_eq!(f.len, 8);
178        assert!(!f.is_fd());
179        assert!(!f.is_extended());
180        assert_eq!(f.payload(), &[0xFF; 8]);
181    }
182
183    #[test]
184    fn classical_9byte_rejected() {
185        let err = CanFrame::classical(0x101, &[0; 9]).unwrap_err();
186        assert_eq!(err, FrameError::PayloadTooLong { got: 9 });
187    }
188
189    #[test]
190    fn fd_16byte_ok() {
191        let f = CanFrame::fd(0x101, &[0xAA; 16]).unwrap();
192        assert_eq!(f.id, 0x101);
193        assert_eq!(f.len, 16);
194        assert!(f.is_fd());
195        assert!(f.flags.contains(FrameFlags::BIT_RATE_SWITCH));
196        assert_eq!(f.payload(), &[0xAA; 16]);
197    }
198
199    #[test]
200    fn fd_invalid_dlc_rejected() {
201        let err = CanFrame::fd(0x101, &[0; 9]).unwrap_err();
202        assert_eq!(err, FrameError::InvalidFdLength { got: 9 });
203    }
204
205    #[test]
206    fn fd_dlc_table() {
207        for n in [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64] {
208            let payload = alloc::vec![0u8; n];
209            assert!(CanFrame::fd(0x100, &payload).is_ok(), "len={n}");
210        }
211        for n in [9, 10, 11, 13, 17, 33, 65] {
212            let payload = alloc::vec![0u8; n];
213            assert!(CanFrame::fd(0x100, &payload).is_err(), "len={n}");
214        }
215    }
216
217    #[test]
218    fn is_fd_reflects_flag() {
219        let f = CanFrame::classical(0x100, &[]).unwrap();
220        assert_eq!(f.is_fd(), f.flags.contains(FrameFlags::FD_FORMAT));
221        let f = CanFrame::fd(0x100, &[]).unwrap();
222        assert_eq!(f.is_fd(), f.flags.contains(FrameFlags::FD_FORMAT));
223    }
224
225    #[test]
226    fn fd_and_rtr_incompatible() {
227        let bad = FrameFlags::FD_FORMAT | FrameFlags::REMOTE_REQUEST;
228        let err = CanFrame::from_parts(0x100, bad, &[]).unwrap_err();
229        matches!(err, FrameError::IncompatibleFlags { .. });
230    }
231
232    #[test]
233    fn flag_bits_compose() {
234        let f = FrameFlags::FD_FORMAT | FrameFlags::BIT_RATE_SWITCH;
235        assert!(f.contains(FrameFlags::FD_FORMAT));
236        assert!(f.contains(FrameFlags::BIT_RATE_SWITCH));
237        assert!(!f.contains(FrameFlags::REMOTE_REQUEST));
238    }
239
240    #[test]
241    fn error_display_is_informative() {
242        let s = alloc::format!("{}", FrameError::PayloadTooLong { got: 9 });
243        assert!(s.contains("classical"));
244        assert!(s.contains("9"));
245        let s = alloc::format!("{}", FrameError::InvalidFdLength { got: 13 });
246        assert!(s.contains("CAN-FD"));
247    }
248}