matter_commissioning/state_machine/error.rs
1//! `CommissioningError` — error variants surfaced by the state machine.
2
3#![forbid(unsafe_code)]
4
5use crate::attestation::AttestationError;
6use crate::noc::NocError;
7use crate::state_machine::action::Expectation;
8use crate::state_machine::stage::Stage;
9
10/// Errors emitted by the commissioning state machine.
11///
12/// All variants are `#[non_exhaustive]` — future sub-phases or future
13/// milestones (M6.5 network commissioning, etc.) can add variants
14/// without breaking `SemVer`.
15///
16/// `CommissioningError` is intentionally **not** `Clone`. The summary
17/// emitted in [`super::Action::Abort`] is a pre-rendered `String`, so
18/// callers never need to clone the full error.
19#[derive(Debug, thiserror::Error)]
20#[non_exhaustive]
21pub enum CommissioningError {
22 /// `CommissionerConfig` failed validation in `Commissioner::new`.
23 /// Carries a `&'static str` describing which field is bad — no
24 /// alloc on the error path.
25 #[error("invalid commissioner config: {0}")]
26 InvalidConfig(&'static str),
27
28 /// Caller invoked `on_response` with an `Expectation` that does
29 /// not match the last `poll()`'s emitted `Expectation`.
30 #[error("unexpected response kind: expected {expected:?}, got {got:?}")]
31 UnexpectedResponseKind {
32 /// The Expectation the state machine emitted with the last
33 /// Action.
34 expected: Expectation,
35 /// The Expectation the caller passed in.
36 got: Expectation,
37 },
38
39 /// Caller invoked `on_response` or `on_case_established` in a
40 /// stage where the state machine is not waiting for input (e.g.
41 /// `Stage::Cleanup` or `Stage::SecurePairing`).
42 #[error("response delivered out of order at stage {0:?}")]
43 OutOfOrderResponse(Stage),
44
45 /// Device returned a non-OK Interaction Model status for a cluster
46 /// command at `stage`. The 16-bit `im_status` is the canonical
47 /// Matter status code from the response envelope.
48 #[error("device rejected stage {stage:?}: IM status {im_status:#x}")]
49 DeviceImStatus {
50 /// Where the rejection happened.
51 stage: Stage,
52 /// IM status code (Matter Core Spec §8.10).
53 im_status: u16,
54 },
55
56 /// Response TLV failed to decode at the cluster command level.
57 #[error("malformed response at stage {0:?}")]
58 MalformedResponse(Stage),
59
60 /// Attestation verification failed (chain / signature / CD).
61 #[error("attestation verification failed: {0}")]
62 Attestation(#[from] AttestationError),
63
64 /// CSR verification or NOC issuance failed.
65 #[error("NOC issuance failed: {0}")]
66 Noc(#[from] NocError),
67
68 /// CASE establishment failed (caller called
69 /// `on_response(Expectation::CaseFailed, &[])`).
70 #[error("CASE session establishment failed")]
71 CaseEstablishmentFailed,
72
73 /// The device's `NetworkCommissioning::FeatureMap` does not declare
74 /// the network type the caller supplied credentials for (e.g.
75 /// `NetworkCredentials::Thread` was supplied but the device's
76 /// `FeatureMap` lacks the Thread bit). Both Wi-Fi and Thread are
77 /// supported network types as of M9-C2 — this variant signals a
78 /// device/credential *mismatch*, not an unsupported network type.
79 #[error("device does not support {needed:?} network type (credential/device mismatch)")]
80 NetworkFeatureUnsupported {
81 /// Which network type the supplied credentials required.
82 needed: NetworkKind,
83 },
84
85 /// Device rejected `AddOrUpdateWiFiNetwork` or `ConnectNetwork`
86 /// with a non-OK `NetworkCommissioningStatusEnum` value
87 /// (spec §11.9.5.1).
88 #[error(
89 "network commissioning rejected at stage {stage:?}: \
90 networking_status {networking_status:#x}, \
91 debug_text={debug_text:?}, hint={remediation_hint:?}"
92 )]
93 NetworkRejected {
94 /// Which stage the device rejected.
95 stage: Stage,
96 /// Raw `NetworkCommissioningStatusEnum` value from the
97 /// response.
98 networking_status: u8,
99 /// Optional human-readable debug text echoed by the device.
100 debug_text: Option<String>,
101 /// Mapped remediation category for downstream UI rendering.
102 remediation_hint: RemediationHint,
103 },
104
105 /// `network` was not `NetworkCredentials::WiFi` but the device's
106 /// `FeatureMap` declared Wi-Fi at a stage that requires the SSID/PSK.
107 /// Distinct from `InvalidConfig` because it surfaces at a later
108 /// stage once the device's network shape is known.
109 #[error("device is Wi-Fi but no wifi credentials supplied")]
110 WifiCredentialsRequired,
111}
112
113/// Which Matter network-commissioning type a device declared in its
114/// `NetworkCommissioning::FeatureMap`.
115///
116/// `#[non_exhaustive]` — future Matter-spec network interfaces
117/// (e.g. Thread Border Router relay) can be added without a breaking
118/// change.
119#[non_exhaustive]
120#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
121pub enum NetworkKind {
122 /// Wi-Fi network interface (`FeatureMap` bit 0).
123 WiFi,
124 /// Thread network interface (`FeatureMap` bit 1).
125 Thread,
126 /// Ethernet network interface (`FeatureMap` bit 2).
127 Ethernet,
128}
129
130/// Hint describing what a downstream UI could suggest to remediate a
131/// `CommissioningError::NetworkRejected` (lands in M6.5.2).
132///
133/// Maps from a Matter `NetworkCommissioningStatusEnum` value (spec
134/// §11.9.5.1) into a category callers can render meaningfully without
135/// parsing the raw status code. The mapping table lives in
136/// `crate::clusters::network_commissioning::remediation_for`.
137///
138/// # Stability
139///
140/// `#[non_exhaustive]` from inception. New variants may be added in any
141/// release. Existing variants will never be renamed or reordered.
142/// Changes to the `status_code` → variant mapping are documented in the
143/// CHANGELOG as semi-public behavioural changes.
144#[non_exhaustive]
145#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
146pub enum RemediationHint {
147 /// Password/passphrase likely wrong. From `AuthFailure` (7).
148 CheckPassphrase,
149 /// SSID not found. From `NetworkNotFound` (5), `NetworkIDNotFound` (3).
150 CheckSsid,
151 /// Country code / regulatory location mismatch. From
152 /// `RegulatoryError` (6).
153 CheckRegulatoryRegion,
154 /// Wi-Fi security cipher unsupported (e.g. WEP-only device). From
155 /// `UnsupportedSecurity` (8).
156 UpgradeSecurityMode,
157 /// Device reached its `MaxNetworks` limit. From `BoundsExceeded` (2).
158 DeviceNetworkSlotsFull,
159 /// IP-stack-layer failure on the device side. From `IPV6Failed` (10),
160 /// `IPBindFailed` (11).
161 DeviceIpStackFailure,
162 /// No specific guidance available. From `OtherConnectionFailure` (9),
163 /// `UnknownError` (12), or any status code not yet mapped.
164 None,
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::state_machine::action::Expectation;
171 use crate::state_machine::stage::Stage;
172
173 #[test]
174 fn invalid_config_carries_message() {
175 let e = CommissioningError::InvalidConfig("missing IPK epoch key");
176 let msg = e.to_string();
177 assert!(msg.contains("missing IPK"), "{msg}");
178 }
179
180 #[test]
181 fn unexpected_response_kind_shows_both_sides() {
182 let e = CommissioningError::UnexpectedResponseKind {
183 expected: Expectation::ArmFailsafeResponse,
184 got: Expectation::AttestationResponse,
185 };
186 let msg = e.to_string();
187 assert!(msg.contains("ArmFailsafeResponse"), "{msg}");
188 assert!(msg.contains("AttestationResponse"), "{msg}");
189 }
190
191 #[test]
192 fn out_of_order_response_names_the_stage() {
193 let e = CommissioningError::OutOfOrderResponse(Stage::ArmFailsafe);
194 let msg = e.to_string();
195 assert!(msg.contains("ArmFailsafe"), "{msg}");
196 }
197
198 #[test]
199 fn device_im_status_includes_stage_and_status_code() {
200 let e = CommissioningError::DeviceImStatus {
201 stage: Stage::ArmFailsafe,
202 im_status: 0x0098,
203 };
204 let msg = e.to_string();
205 assert!(msg.contains("ArmFailsafe"), "{msg}");
206 assert!(msg.contains("0x98"), "{msg}");
207 }
208
209 #[test]
210 fn remediation_hint_is_copy_eq_hash() {
211 fn assert_copy<T: Copy + Eq + std::hash::Hash>() {}
212 assert_copy::<RemediationHint>();
213 assert_eq!(RemediationHint::None, RemediationHint::None);
214 assert_ne!(RemediationHint::None, RemediationHint::CheckPassphrase);
215 }
216}