atecc608b/error.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//! Error types for the ATECC608B driver.
17//!
18//! Each layer maps its specific failure modes to a variant of [`AteccError`].
19//! The error type is generic over the HAL error so callers can match on the
20//! exact reason without losing typing information.
21//!
22//! Upper layers (the firmware HID dispatcher, the host CLI) often need to
23//! distinguish *categories* of failure (chip-side vs. communication-side)
24//! without naming the concrete `HalError` type. [`AteccErrorKind`] is the
25//! non-generic projection of [`AteccError`] intended for that use: it carries
26//! the same shape information (which variant fired) but no HAL payload, so it
27//! can travel through generic-erased layers and be serialized as a single
28//! `u8` for transport over the USB-HID protocol.
29
30use core::fmt::Debug;
31
32/// Error returned by every public method of [`crate::Atecc`].
33#[derive(Debug)]
34#[cfg_attr(feature = "defmt", derive(defmt::Format))]
35pub enum AteccError<HalError>
36where
37 HalError: Debug,
38{
39 /// The HAL itself reported an error (I2C bus, GPIO, etc).
40 Hal(HalError),
41
42 /// The wake response did not match `04 11 33 43`. The chip is either
43 /// unpowered, miswired, or in self-test failure mode.
44 WakeFailed,
45
46 /// The chip returned the self-test-failure pattern `04 07 C4 40` after
47 /// wake. Hardware is unusable until power-cycle.
48 SelfTestFailure,
49
50 /// CRC-16 of a received frame did not match. The frame is discarded. The
51 /// caller may retry.
52 BadCrc,
53
54 /// The chip returned a 4-byte status frame indicating an error.
55 Chip(ChipError),
56
57 /// Polling for a command response exceeded the watchdog window (~1.3 s
58 /// nominal, 2.5 s upper bound).
59 Timeout,
60
61 /// A received frame has an inconsistent length byte.
62 MalformedResponse,
63
64 /// A caller-supplied buffer is too small for the response.
65 BufferTooSmall,
66}
67
68impl<HalError> AteccError<HalError>
69where
70 HalError: Debug,
71{
72 /// Project this error onto its non-generic [`AteccErrorKind`].
73 ///
74 /// Useful for layers that must report the error over a wire format
75 /// without leaking the concrete `HalError` type. The returned value
76 /// preserves the variant identity and, for [`AteccError::Chip`], the
77 /// inner [`ChipError`].
78 #[must_use]
79 pub fn kind(&self) -> AteccErrorKind
80 {
81 match self
82 {
83 AteccError::Hal(_) => AteccErrorKind::Hal,
84 AteccError::WakeFailed => AteccErrorKind::WakeFailed,
85 AteccError::SelfTestFailure => AteccErrorKind::SelfTestFailure,
86 AteccError::BadCrc => AteccErrorKind::BadCrc,
87 AteccError::Chip(err) => AteccErrorKind::Chip(*err),
88 AteccError::Timeout => AteccErrorKind::Timeout,
89 AteccError::MalformedResponse => AteccErrorKind::MalformedResponse,
90 AteccError::BufferTooSmall => AteccErrorKind::BufferTooSmall,
91 }
92 }
93}
94
95impl<HalError> From<HalError> for AteccError<HalError>
96where
97 HalError: Debug,
98{
99 fn from(err: HalError) -> Self
100 {
101 AteccError::Hal(err)
102 }
103}
104
105/// Non-generic projection of [`AteccError`].
106///
107/// Carries the same variant identity (and, for [`AteccErrorKind::Chip`], the
108/// inner [`ChipError`]) but drops the HAL payload. Cheap to copy and safe to
109/// pass across layers that don't want to be generic over `HalError`.
110///
111/// The non-`Chip` variants are encoded as stable `u8` sub-codes by
112/// [`AteccErrorKind::as_sub_code`], used by the USB-HID protocol to expose
113/// the failure category to the host CLI.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115#[cfg_attr(feature = "defmt", derive(defmt::Format))]
116pub enum AteccErrorKind
117{
118 /// HAL error (I2C nack, GPIO failure, etc).
119 Hal,
120 /// Chip did not return the expected wake-response pattern.
121 WakeFailed,
122 /// Chip returned the self-test-failure pattern after wake.
123 SelfTestFailure,
124 /// CRC-16 of a received frame did not match.
125 BadCrc,
126 /// Chip-side error status. The inner [`ChipError`] carries the raw byte.
127 Chip(ChipError),
128 /// Polling exceeded the watchdog window.
129 Timeout,
130 /// A received frame has an inconsistent length byte.
131 MalformedResponse,
132 /// A caller-supplied buffer was too small for the response.
133 BufferTooSmall,
134}
135
136/// Stable wire-format sub-code for the HAL-error variant.
137pub const ATECC_ERR_SUB_HAL: u8 = 0x01;
138/// Stable wire-format sub-code for the wake-failed variant.
139pub const ATECC_ERR_SUB_WAKE_FAILED: u8 = 0x02;
140/// Stable wire-format sub-code for the self-test-failure variant.
141pub const ATECC_ERR_SUB_SELF_TEST_FAILURE: u8 = 0x03;
142/// Stable wire-format sub-code for the bad-CRC variant.
143pub const ATECC_ERR_SUB_BAD_CRC: u8 = 0x04;
144/// Stable wire-format sub-code for the timeout variant.
145pub const ATECC_ERR_SUB_TIMEOUT: u8 = 0x05;
146/// Stable wire-format sub-code for the malformed-response variant.
147pub const ATECC_ERR_SUB_MALFORMED_RESPONSE: u8 = 0x06;
148/// Stable wire-format sub-code for the buffer-too-small variant.
149pub const ATECC_ERR_SUB_BUFFER_TOO_SMALL: u8 = 0x07;
150
151impl AteccErrorKind
152{
153 /// Return the stable `u8` sub-code identifying this variant.
154 ///
155 /// `Chip(_)` is not assigned a sub-code here because chip errors carry
156 /// their own dedicated status byte (the chip's raw response byte,
157 /// available via [`ChipError::as_status_byte`]) and are routed to a
158 /// distinct top-level HID status. The fallback value `0x00` is reserved
159 /// for that case and indicates "see the dedicated chip-error path".
160 #[must_use]
161 pub const fn as_sub_code(self) -> u8
162 {
163 match self
164 {
165 AteccErrorKind::Hal => ATECC_ERR_SUB_HAL,
166 AteccErrorKind::WakeFailed => ATECC_ERR_SUB_WAKE_FAILED,
167 AteccErrorKind::SelfTestFailure => ATECC_ERR_SUB_SELF_TEST_FAILURE,
168 AteccErrorKind::BadCrc => ATECC_ERR_SUB_BAD_CRC,
169 AteccErrorKind::Timeout => ATECC_ERR_SUB_TIMEOUT,
170 AteccErrorKind::MalformedResponse => ATECC_ERR_SUB_MALFORMED_RESPONSE,
171 AteccErrorKind::BufferTooSmall => ATECC_ERR_SUB_BUFFER_TOO_SMALL,
172 AteccErrorKind::Chip(_) => 0x00,
173 }
174 }
175
176 /// Map a wire-format sub-code back to a non-`Chip` variant.
177 ///
178 /// Returns `None` for `0x00` (reserved for the chip-error path) and for
179 /// any byte outside the assigned range. Callers reconstructing an error
180 /// from the wire should handle the chip path through
181 /// [`ChipError::from_status_byte`] separately.
182 #[must_use]
183 pub const fn from_sub_code(byte: u8) -> Option<Self>
184 {
185 match byte
186 {
187 ATECC_ERR_SUB_HAL => Some(Self::Hal),
188 ATECC_ERR_SUB_WAKE_FAILED => Some(Self::WakeFailed),
189 ATECC_ERR_SUB_SELF_TEST_FAILURE => Some(Self::SelfTestFailure),
190 ATECC_ERR_SUB_BAD_CRC => Some(Self::BadCrc),
191 ATECC_ERR_SUB_TIMEOUT => Some(Self::Timeout),
192 ATECC_ERR_SUB_MALFORMED_RESPONSE => Some(Self::MalformedResponse),
193 ATECC_ERR_SUB_BUFFER_TOO_SMALL => Some(Self::BufferTooSmall),
194 _ => None,
195 }
196 }
197}
198
199/// Errors reported by the chip itself via a 4-byte response frame.
200///
201/// Status byte values are taken from the Microchip `CryptoAuthLib`
202/// `isATCAError()` function.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204#[cfg_attr(feature = "defmt", derive(defmt::Format))]
205pub enum ChipError
206{
207 /// `0x01` - `CheckMac` or Verify failed.
208 CheckMacOrVerifyFailed,
209
210 /// `0x03` - Command byte length, opcode or parameter was illegal.
211 ParseError,
212
213 /// `0x05` - Computation error during ECC processing causing invalid
214 /// results.
215 EccFault,
216
217 /// `0x07` - Chip is in self test failure mode.
218 SelfTestFailed,
219
220 /// `0x08` - RNG health test error.
221 HealthTestFailed,
222
223 /// `0x0F` - Unspecified execution error.
224 ExecutionError,
225
226 /// `0xEE` - Watchdog about to expire - command not executed.
227 WatchdogAboutToExpire,
228
229 /// `0xFF` - CRC or other communication error on the command sent to the
230 /// chip.
231 CommandCrcError,
232
233 /// An unknown status byte. The raw value is preserved for diagnosis.
234 Unknown(u8),
235}
236
237impl ChipError
238{
239 /// Map a raw status byte returned by the chip to a [`ChipError`].
240 ///
241 /// Returns `None` for `0x00` which means success. Exposed as `pub` so
242 /// host-side decoders (e.g. the CLI) can reconstruct the variant from
243 /// the wire byte received in the USB-HID payload.
244 #[must_use]
245 pub const fn from_status_byte(byte: u8) -> Option<Self>
246 {
247 match byte
248 {
249 0x00 => None,
250 0x01 => Some(Self::CheckMacOrVerifyFailed),
251 0x03 => Some(Self::ParseError),
252 0x05 => Some(Self::EccFault),
253 0x07 => Some(Self::SelfTestFailed),
254 0x08 => Some(Self::HealthTestFailed),
255 0x0F => Some(Self::ExecutionError),
256 0xEE => Some(Self::WatchdogAboutToExpire),
257 0xFF => Some(Self::CommandCrcError),
258 other => Some(Self::Unknown(other)),
259 }
260 }
261
262 /// Return the raw status byte this variant came from.
263 ///
264 /// Inverse of [`Self::from_status_byte`]. For
265 /// [`ChipError::Unknown`] returns the preserved raw byte.
266 #[must_use]
267 pub const fn as_status_byte(self) -> u8
268 {
269 match self
270 {
271 Self::CheckMacOrVerifyFailed => 0x01,
272 Self::ParseError => 0x03,
273 Self::EccFault => 0x05,
274 Self::SelfTestFailed => 0x07,
275 Self::HealthTestFailed => 0x08,
276 Self::ExecutionError => 0x0F,
277 Self::WatchdogAboutToExpire => 0xEE,
278 Self::CommandCrcError => 0xFF,
279 Self::Unknown(byte) => byte,
280 }
281 }
282}
283
284#[cfg(test)]
285mod tests
286{
287 use super::*;
288
289 #[test]
290 fn kind_strips_hal_payload()
291 {
292 let err: AteccError<&'static str> = AteccError::Hal("nack");
293 assert_eq!(err.kind(), AteccErrorKind::Hal);
294 }
295
296 #[test]
297 fn kind_preserves_chip_variant()
298 {
299 let err: AteccError<()> = AteccError::Chip(ChipError::ParseError);
300 assert_eq!(err.kind(), AteccErrorKind::Chip(ChipError::ParseError));
301 }
302
303 #[test]
304 fn sub_codes_are_distinct_and_stable()
305 {
306 let codes =
307 [
308 AteccErrorKind::Hal.as_sub_code(),
309 AteccErrorKind::WakeFailed.as_sub_code(),
310 AteccErrorKind::SelfTestFailure.as_sub_code(),
311 AteccErrorKind::BadCrc.as_sub_code(),
312 AteccErrorKind::Timeout.as_sub_code(),
313 AteccErrorKind::MalformedResponse.as_sub_code(),
314 AteccErrorKind::BufferTooSmall.as_sub_code(),
315 ];
316 // None of them clash with the reserved Chip sentinel `0x00`.
317 for c in codes
318 {
319 assert_ne!(c, 0x00);
320 }
321 // All distinct.
322 for (i, a) in codes.iter().enumerate()
323 {
324 for b in &codes[i + 1..]
325 {
326 assert_ne!(a, b);
327 }
328 }
329 }
330
331 #[test]
332 fn sub_code_round_trip()
333 {
334 for kind in
335 [
336 AteccErrorKind::Hal,
337 AteccErrorKind::WakeFailed,
338 AteccErrorKind::SelfTestFailure,
339 AteccErrorKind::BadCrc,
340 AteccErrorKind::Timeout,
341 AteccErrorKind::MalformedResponse,
342 AteccErrorKind::BufferTooSmall,
343 ]
344 {
345 assert_eq!(AteccErrorKind::from_sub_code(kind.as_sub_code()), Some(kind));
346 }
347 }
348
349 #[test]
350 fn sub_code_zero_is_reserved_for_chip()
351 {
352 assert_eq!(AteccErrorKind::from_sub_code(0x00), None);
353 assert_eq!(AteccErrorKind::Chip(ChipError::ParseError).as_sub_code(), 0x00);
354 }
355
356 #[test]
357 fn chip_error_status_byte_round_trip()
358 {
359 for byte in [0x01u8, 0x03, 0x05, 0x07, 0x08, 0x0F, 0xEE, 0xFF, 0x42]
360 {
361 let err = ChipError::from_status_byte(byte).unwrap();
362 assert_eq!(err.as_status_byte(), byte);
363 }
364 assert!(ChipError::from_status_byte(0x00).is_none());
365 }
366}