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}