Skip to main content

iot_core/
error.rs

1//! Unified error type returned across the SDK.
2//!
3//! Each protocol crate wraps its own internal error in the matching
4//! variant of [`IotError`] via `From` impls — those impls are gated by
5//! workspace features so the umbrella crate only compiles in the variants
6//! the user actually opted into.
7//!
8//! The transport-layer and codec-layer errors are protocol-agnostic and so
9//! they live here unconditionally.
10
11use core::fmt;
12use smol_str::SmolStr;
13
14use crate::path::PropertyPath;
15
16/// Convenience alias for SDK results.
17pub type IotResult<T> = core::result::Result<T, IotError>;
18
19/// Errors raised by transports (connect / read / write / close).
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum TransportError {
22    /// Underlying socket / serial port not connected.
23    NotConnected,
24    /// Address parse / DNS resolution failure. Carries a human-readable
25    /// reason; we do not embed `std::io::Error` here so the type stays
26    /// `Eq + Clone + no_std`.
27    AddressInvalid(SmolStr),
28    /// Failed during connect.
29    ConnectFailed(SmolStr),
30    /// I/O failure — generic. The protocol layer maps this to
31    /// `IotError::Transport`.
32    Io(SmolStr),
33    /// Operation timed out.
34    Timeout,
35    /// TLS / DTLS handshake failure.
36    Tls(SmolStr),
37    /// Serial-port specific: framing / parity error.
38    SerialFraming(SmolStr),
39    /// Peer closed the connection unexpectedly.
40    Closed,
41}
42
43impl fmt::Display for TransportError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        use TransportError::*;
46        match self {
47            NotConnected => write!(f, "transport not connected"),
48            AddressInvalid(s) => write!(f, "invalid address: {s}"),
49            ConnectFailed(s) => write!(f, "connect failed: {s}"),
50            Io(s) => write!(f, "io error: {s}"),
51            Timeout => write!(f, "operation timed out"),
52            Tls(s) => write!(f, "tls error: {s}"),
53            SerialFraming(s) => write!(f, "serial framing error: {s}"),
54            Closed => write!(f, "connection closed"),
55        }
56    }
57}
58
59impl core::error::Error for TransportError {}
60
61/// Errors raised by protocol codecs (encode / decode of a frame).
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum CodecError {
64    /// Buffer truncated mid-frame.
65    UnexpectedEof,
66    /// Frame larger than the configured max.
67    FrameTooLarge {
68        /// Maximum frame size the codec was configured to accept.
69        max: u32,
70        /// Actual size declared in the wire header.
71        actual: u32,
72    },
73    /// Reserved field had a non-zero value, or a flag combination is
74    /// disallowed by the spec.
75    ProtocolViolation(SmolStr),
76    /// CRC / LRC / checksum mismatch.
77    ChecksumMismatch,
78    /// Wire-format string was not valid utf-8 / ascii.
79    InvalidString(SmolStr),
80    /// A value the codec produced fits the wire shape but cannot be
81    /// represented in the Rust target type (e.g. negative length).
82    ValueOutOfRange(SmolStr),
83}
84
85impl fmt::Display for CodecError {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        use CodecError::*;
88        match self {
89            UnexpectedEof => write!(f, "unexpected end of buffer while decoding"),
90            FrameTooLarge { max, actual } => {
91                write!(f, "frame too large: {actual} bytes, max {max}")
92            }
93            ProtocolViolation(s) => write!(f, "protocol violation: {s}"),
94            ChecksumMismatch => write!(f, "checksum mismatch"),
95            InvalidString(s) => write!(f, "invalid string: {s}"),
96            ValueOutOfRange(s) => write!(f, "value out of range: {s}"),
97        }
98    }
99}
100
101impl core::error::Error for CodecError {}
102
103/// Top-level error every public API in the SDK returns.
104///
105/// Protocol-specific variants are intentionally *not* enumerated here — they
106/// live in their own crates and are wrapped via the protocol crate's own
107/// error type (which itself wraps `CodecError` / `TransportError` for the
108/// shared sub-failures). The conversion from a protocol-specific error to
109/// `IotError` happens at the boundary between the protocol crate and the
110/// gateway / IotClient layer.
111#[derive(Debug)]
112#[non_exhaustive]
113pub enum IotError {
114    /// Operation requested while the client is not connected.
115    NotConnected,
116    /// Connection establishment failed.
117    ConnectionFailed(SmolStr),
118    /// Authentication failed (bad credentials, expired cert, ...).
119    AuthFailed(SmolStr),
120    /// Operation timed out (any layer).
121    Timeout,
122    /// The target [`PropertyPath`] is not in the active mapping.
123    PathNotBound(PropertyPath),
124    /// The path is bound but the binding kind does not match the protocol
125    /// (e.g. asking the MQTT client to handle a Modbus binding).
126    PathKindMismatch {
127        /// The path that triggered the mismatch.
128        path: PropertyPath,
129        /// Human-readable description of the expected binding variant.
130        expected: &'static str,
131    },
132    /// Codec error shared with the protocol layer.
133    Codec(CodecError),
134    /// Transport error shared with the protocol layer.
135    Transport(TransportError),
136    /// Catch-all for protocol-specific failures.
137    Protocol(SmolStr),
138}
139
140impl fmt::Display for IotError {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        use IotError::*;
143        match self {
144            NotConnected => write!(f, "client not connected"),
145            ConnectionFailed(s) => write!(f, "connection failed: {s}"),
146            AuthFailed(s) => write!(f, "authentication failed: {s}"),
147            Timeout => write!(f, "operation timed out"),
148            PathNotBound(p) => write!(f, "path not bound: {p}"),
149            PathKindMismatch { path, expected } => {
150                write!(f, "path {path} is not a {expected} binding")
151            }
152            Codec(e) => write!(f, "codec error: {e}"),
153            Transport(e) => write!(f, "transport error: {e}"),
154            Protocol(s) => write!(f, "protocol error: {s}"),
155        }
156    }
157}
158
159impl core::error::Error for IotError {
160    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
161        match self {
162            IotError::Codec(e) => Some(e),
163            IotError::Transport(e) => Some(e),
164            _ => None,
165        }
166    }
167}
168
169impl From<CodecError> for IotError {
170    fn from(e: CodecError) -> Self {
171        Self::Codec(e)
172    }
173}
174impl From<TransportError> for IotError {
175    fn from(e: TransportError) -> Self {
176        Self::Transport(e)
177    }
178}
179
180// ---- bridge to `embedded_io_async::Error` --------------------------------
181//
182// The `iot-transport` crate's byte-level traits are aliases for
183// `embedded_io_async::{Read,Write}`, which require the associated `Error`
184// type to implement `embedded_io_async::Error`. We map TransportError onto
185// the closest ErrorKind variant — protocol crates that need finer detail
186// can downcast back to TransportError via the AsyncSocket adapter.
187impl embedded_io_async::Error for TransportError {
188    fn kind(&self) -> embedded_io_async::ErrorKind {
189        use embedded_io_async::ErrorKind as K;
190        match self {
191            TransportError::NotConnected => K::NotConnected,
192            TransportError::AddressInvalid(_) => K::AddrNotAvailable,
193            TransportError::ConnectFailed(_) => K::ConnectionRefused,
194            TransportError::Io(_) => K::Other,
195            TransportError::Timeout => K::TimedOut,
196            TransportError::Tls(_) => K::PermissionDenied,
197            TransportError::SerialFraming(_) => K::InvalidData,
198            TransportError::Closed => K::ConnectionAborted,
199        }
200    }
201}