Skip to main content

atecc608b/
packet.rs

1// Copyright (c) 2026 Tuloup Simon
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! Encoding and decoding of ATECC608B command and response frames.
17//!
18//! # Command frame layout (sent host to chip)
19//!
20//! ```text
21//! +---------+--------+---------+---------+-----------+---------+
22//! |  Count  | Opcode | Param1  | Param2  |   Data... |  CRC16  |
23//! |  (1 B)  | (1 B)  |  (1 B)  | (2 B,LE)|  (0..155) | (2 B,LE)|
24//! +---------+--------+---------+---------+-----------+---------+
25//!
26//!  ^                                                  ^
27//!  |                                                  |
28//!  +-- Count includes itself, the CRC, and ---------- +
29//!      everything in between.
30//! ```
31//!
32//! The byte sent before this frame on I2C is the "word address"
33//! [`crate::opcodes::WORD_ADDRESS_COMMAND`] (`0x03`). It is not part of the
34//! frame proper and is not covered by the CRC.
35//!
36//! # Response frame layout (sent chip to host)
37//!
38//! ```text
39//! +---------+----------------+---------+
40//! |  Count  |    Payload     |  CRC16  |
41//! |  (1 B)  |   (Count-3 B)  | (2 B,LE)|
42//! +---------+----------------+---------+
43//! ```
44//!
45//! When the chip reports an error, the response is exactly 4 bytes:
46//! `04 <status> <crc_lo> <crc_hi>`. The 1-byte status is one of the values
47//! mapped by [`crate::error::ChipError::from_status_byte`].
48
49use crate::crc::{crc16, crc16_to_bytes, verify_trailing_crc};
50use crate::opcodes::{COMMAND_FRAME_OVERHEAD, MAX_COMMAND_DATA_LEN};
51
52/// Errors that can arise while parsing a response frame.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[cfg_attr(feature = "defmt", derive(defmt::Format))]
55pub(crate) enum PacketParseError
56{
57    /// The received slice is shorter than the minimum 4-byte response.
58    TooShort,
59    /// The count byte does not match the actual slice length.
60    LengthMismatch
61    {
62        /// Value of the count byte advertised by the chip.
63        declared: u8,
64        /// Number of bytes actually present in the slice.
65        actual:   usize,
66    },
67    /// The trailing CRC does not match a CRC computed over the rest of the
68    /// frame.
69    BadCrc,
70}
71
72/// Errors that can arise while serializing a command frame.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74#[cfg_attr(feature = "defmt", derive(defmt::Format))]
75pub(crate) enum PacketBuildError
76{
77    /// The provided data buffer would make the frame exceed
78    /// [`crate::opcodes::MAX_PACKET_SIZE`].
79    DataTooLong
80    {
81        /// Length the caller attempted to write.
82        attempted: usize,
83        /// Maximum length the protocol allows.
84        max:       usize,
85    },
86    /// The output buffer cannot hold the resulting frame.
87    OutputBufferTooSmall
88    {
89        /// Required output buffer size in bytes.
90        required: usize,
91        /// Size of the buffer the caller provided.
92        provided: usize,
93    },
94}
95
96/// Build a command frame into `out` and return the number of bytes written.
97///
98/// The frame is laid out exactly as the chip expects to receive it, starting
99/// with the count byte. The caller is responsible for prepending the word
100/// address byte at the I2C transmission layer.
101///
102/// # Arguments
103/// - `opcode`: One of the `OP_*` constants from [`crate::opcodes`].
104/// - `param1`: First single-byte parameter (command-specific meaning).
105/// - `param2`: Two-byte parameter, written in little-endian.
106/// - `data`  : Optional command-specific payload, up to
107///   [`MAX_COMMAND_DATA_LEN`] bytes.
108/// - `out`   : Buffer that receives the encoded frame. Must hold at least
109///   `COMMAND_FRAME_OVERHEAD + data.len()` bytes.
110///
111/// # Errors
112/// Returns [`PacketBuildError::DataTooLong`] if `data` exceeds the protocol
113/// maximum, or [`PacketBuildError::OutputBufferTooSmall`] if `out` is too
114/// small to hold the result.
115pub(crate) fn build_command_frame(
116    opcode: u8,
117    param1: u8,
118    param2: u16,
119    data: &[u8],
120    out: &mut [u8],
121) -> Result<usize, PacketBuildError>
122{
123    if data.len() > MAX_COMMAND_DATA_LEN
124    {
125        return Err(PacketBuildError::DataTooLong
126        {
127            attempted: data.len(),
128            max:       MAX_COMMAND_DATA_LEN,
129        });
130    }
131
132    let total_len = COMMAND_FRAME_OVERHEAD + data.len();
133
134    if out.len() < total_len
135    {
136        return Err(PacketBuildError::OutputBufferTooSmall
137        {
138            required: total_len,
139            provided: out.len(),
140        });
141    }
142
143    // The count byte counts itself and everything that follows including the
144    // CRC. total_len already accounts for all of that.
145    out[0] = u8::try_from(total_len).map_err(|_| PacketBuildError::DataTooLong 
146    {
147        attempted: data.len(),
148        max:       MAX_COMMAND_DATA_LEN,
149    })?;
150    out[1] = opcode;
151    out[2] = param1;
152    let param2_bytes = param2.to_le_bytes();
153    out[3] = param2_bytes[0];
154    out[4] = param2_bytes[1];
155
156    out[5..5 + data.len()].copy_from_slice(data);
157
158    // CRC is computed over the entire frame except the two trailing CRC
159    // bytes themselves.
160    let crc = crc16(&out[..5 + data.len()]);
161    let crc_bytes = crc16_to_bytes(crc);
162    out[5 + data.len()] = crc_bytes[0];
163    out[6 + data.len()] = crc_bytes[1];
164
165    Ok(total_len)
166}
167
168/// A parsed and CRC-verified response frame.
169///
170/// Borrows from the receive buffer. The lifetime ties the parsed structure to
171/// the buffer so the caller cannot reuse it while still reading the payload.
172#[derive(Debug, PartialEq, Eq)]
173pub(crate) enum ResponseFrame<'a>
174{
175    /// Standard payload response. The slice contains the bytes between the
176    /// count byte and the CRC.
177    Payload(&'a [u8]),
178    /// 4-byte status response. The chip reports a one-byte status code that
179    /// is non-zero. Callers should pass this byte to
180    /// [`crate::error::ChipError::from_status_byte`].
181    Status(u8),
182}
183
184/// Parse a response frame.
185///
186/// `frame` is the exact slice read from the chip, starting with the count
187/// byte and ending with the two CRC bytes.
188///
189/// # Errors
190/// Returns [`PacketParseError`] if the frame is too short, has an inconsistent
191/// count byte, or fails CRC verification.
192pub(crate) fn parse_response_frame(frame: &[u8]) -> Result<ResponseFrame<'_>, PacketParseError>
193{
194    // Minimum response is 4 bytes: count, status, crc_lo, crc_hi.
195    if frame.len() < 4
196    {
197        return Err(PacketParseError::TooShort);
198    }
199
200    let declared = frame[0];
201
202    if declared as usize != frame.len()
203    {
204        return Err(PacketParseError::LengthMismatch
205        {
206            declared,
207            actual: frame.len(),
208        });
209    }
210
211    if !verify_trailing_crc(frame)
212    {
213        return Err(PacketParseError::BadCrc);
214    }
215
216    // A 4-byte frame carries a single status byte rather than a payload.
217    if frame.len() == 4
218    {
219        return Ok(ResponseFrame::Status(frame[1]));
220    }
221
222    // Payload is everything between the count byte and the two CRC bytes.
223    Ok(ResponseFrame::Payload(&frame[1..frame.len() - 2]))
224}
225
226#[cfg(test)]
227mod tests
228{
229    use super::*;
230    use crate::opcodes::{OP_INFO, OP_RANDOM};
231
232    /// Verify that an Info command with no data is serialized to the exact
233    /// reference bytes seen on the wire. The reference CRC bytes (`0x03,
234    /// 0x5D`) come from the unit tests of the CRC module.
235    #[test]
236    fn build_info_command()
237    {
238        let mut buf = [0u8; 32];
239        let written = build_command_frame(OP_INFO, 0x00, 0x0000, &[], &mut buf).unwrap();
240
241        assert_eq!(written, 7);
242        assert_eq!(
243            &buf[..7],
244            &[0x07, 0x30, 0x00, 0x00, 0x00, 0x03, 0x5D],
245        );
246    }
247
248    /// Random command frame, similar shape as Info but a different opcode.
249    #[test]
250    fn build_random_command()
251    {
252        let mut buf = [0u8; 32];
253        let written = build_command_frame(OP_RANDOM, 0x00, 0x0000, &[], &mut buf).unwrap();
254
255        assert_eq!(written, 7);
256        assert_eq!(buf[0], 0x07);
257        assert_eq!(buf[1], OP_RANDOM);
258        // CRC bytes were computed independently in the crc tests as 0xCD24.
259        assert_eq!(buf[5], 0x24);
260        assert_eq!(buf[6], 0xCD);
261    }
262
263    #[test]
264    fn build_with_data_payload()
265    {
266        let mut buf = [0u8; 32];
267        let data: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF];
268        let written = build_command_frame(OP_INFO, 0xAA, 0x1234, &data, &mut buf).unwrap();
269
270        // 7 overhead + 4 data = 11
271        assert_eq!(written, 11);
272        assert_eq!(buf[0], 11);
273        assert_eq!(buf[1], OP_INFO);
274        assert_eq!(buf[2], 0xAA);
275        // Param2 little-endian.
276        assert_eq!(buf[3], 0x34);
277        assert_eq!(buf[4], 0x12);
278        // Data block.
279        assert_eq!(&buf[5..9], &data);
280        // CRC is verifiable.
281        assert!(verify_trailing_crc(&buf[..11]));
282    }
283
284    #[test]
285    fn build_rejects_oversized_data()
286    {
287        let mut buf = [0u8; 256];
288        let huge = [0xFFu8; MAX_COMMAND_DATA_LEN + 1];
289        let err = build_command_frame(OP_INFO, 0, 0, &huge, &mut buf).unwrap_err();
290        assert_eq!(
291            err,
292            PacketBuildError::DataTooLong
293            {
294                attempted: MAX_COMMAND_DATA_LEN + 1,
295                max:       MAX_COMMAND_DATA_LEN,
296            },
297        );
298    }
299
300    #[test]
301    fn build_rejects_small_output_buffer()
302    {
303        let mut buf = [0u8; 5];
304        let err = build_command_frame(OP_INFO, 0, 0, &[], &mut buf).unwrap_err();
305        assert_eq!(
306            err,
307            PacketBuildError::OutputBufferTooSmall
308            {
309                required: 7,
310                provided: 5,
311            },
312        );
313    }
314
315    /// Round-trip: build a frame, then verify its CRC parses correctly.
316    #[test]
317    fn build_then_parse_payload()
318    {
319        // Build a frame that simulates a chip response.
320        // We use build_command_frame as a CRC-generating helper here, even
321        // though responses do not have opcode/param1/param2 fields. The CRC
322        // mechanism is identical.
323        let mut buf = [0u8; 32];
324        let count = 5u8; // 1 count + 2 payload + 2 crc
325        buf[0] = count;
326        buf[1] = 0x12;
327        buf[2] = 0x34;
328        let crc = crc16(&buf[..3]);
329        let crc_bytes = crc16_to_bytes(crc);
330        buf[3] = crc_bytes[0];
331        buf[4] = crc_bytes[1];
332
333        let parsed = parse_response_frame(&buf[..5]).unwrap();
334        match parsed
335        {
336            ResponseFrame::Payload(p) => assert_eq!(p, &[0x12, 0x34]),
337            ResponseFrame::Status(_) => panic!("expected payload, got status"),
338        }
339    }
340
341    /// 4-byte status response, ie `04 <status> <crc_lo> <crc_hi>`.
342    #[test]
343    fn parse_status_response()
344    {
345        let mut buf = [0u8; 4];
346        buf[0] = 0x04;
347        buf[1] = 0x03; // ParseError chip code
348        let crc = crc16(&buf[..2]);
349        let crc_bytes = crc16_to_bytes(crc);
350        buf[2] = crc_bytes[0];
351        buf[3] = crc_bytes[1];
352
353        let parsed = parse_response_frame(&buf).unwrap();
354        match parsed
355        {
356            ResponseFrame::Status(s) => assert_eq!(s, 0x03),
357            ResponseFrame::Payload(_) => panic!("expected status, got payload"),
358        }
359    }
360
361    /// The known wake response `04 11 33 43` parses as a `Status(0x11)`.
362    /// Note that 0x11 is the wake "success" sentinel.
363    #[test]
364    fn parse_wake_response()
365    {
366        let wake = [0x04, 0x11, 0x33, 0x43];
367        let parsed = parse_response_frame(&wake).unwrap();
368        assert_eq!(parsed, ResponseFrame::Status(0x11));
369    }
370
371    #[test]
372    fn parse_rejects_too_short()
373    {
374        assert_eq!(
375            parse_response_frame(&[0x04, 0x11, 0x33]).unwrap_err(),
376            PacketParseError::TooShort,
377        );
378    }
379
380    #[test]
381    fn parse_rejects_length_mismatch()
382    {
383        // Count says 6 but we only give 4 bytes.
384        let bad = [0x06, 0x11, 0x33, 0x43];
385        let err = parse_response_frame(&bad).unwrap_err();
386        assert_eq!(
387            err,
388            PacketParseError::LengthMismatch
389            {
390                declared: 6,
391                actual:   4,
392            },
393        );
394    }
395
396    #[test]
397    fn parse_rejects_bad_crc()
398    {
399        // Length matches but CRC is wrong.
400        let bad = [0x04, 0x11, 0xFF, 0xFF];
401        assert_eq!(
402            parse_response_frame(&bad).unwrap_err(),
403            PacketParseError::BadCrc,
404        );
405    }
406}