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}