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
106/// Which Matter network-commissioning type a device declared in its
107/// `NetworkCommissioning::FeatureMap`.
108///
109/// `#[non_exhaustive]` — future Matter-spec network interfaces
110/// (e.g. Thread Border Router relay) can be added without a breaking
111/// change.
112#[non_exhaustive]
113#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
114pub enum NetworkKind {
115 /// Wi-Fi network interface (`FeatureMap` bit 0).
116 WiFi,
117 /// Thread network interface (`FeatureMap` bit 1).
118 Thread,
119 /// Ethernet network interface (`FeatureMap` bit 2).
120 Ethernet,
121}
122
123/// Hint describing what a downstream UI could suggest to remediate a
124/// `CommissioningError::NetworkRejected` (lands in M6.5.2).
125///
126/// Maps from a Matter `NetworkCommissioningStatusEnum` value (spec
127/// §11.9.5.1) into a category callers can render meaningfully without
128/// parsing the raw status code. The mapping table lives in
129/// `crate::clusters::network_commissioning::remediation_for`.
130///
131/// # Stability
132///
133/// `#[non_exhaustive]` from inception. New variants may be added in any
134/// release. Existing variants will never be renamed or reordered.
135/// Changes to the `status_code` → variant mapping are documented in the
136/// CHANGELOG as semi-public behavioural changes.
137#[non_exhaustive]
138#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
139pub enum RemediationHint {
140 /// Password/passphrase likely wrong. From `AuthFailure` (7).
141 CheckPassphrase,
142 /// SSID not found. From `NetworkNotFound` (5), `NetworkIDNotFound` (3).
143 CheckSsid,
144 /// Country code / regulatory location mismatch. From
145 /// `RegulatoryError` (6).
146 CheckRegulatoryRegion,
147 /// Wi-Fi security cipher unsupported (e.g. WEP-only device). From
148 /// `UnsupportedSecurity` (8).
149 UpgradeSecurityMode,
150 /// Device reached its `MaxNetworks` limit. From `BoundsExceeded` (2).
151 DeviceNetworkSlotsFull,
152 /// IP-stack-layer failure on the device side. From `IPV6Failed` (10),
153 /// `IPBindFailed` (11).
154 DeviceIpStackFailure,
155 /// No specific guidance available. From `OtherConnectionFailure` (9),
156 /// `UnknownError` (12), or any status code not yet mapped.
157 None,
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::state_machine::action::Expectation;
164 use crate::state_machine::stage::Stage;
165
166 #[test]
167 fn invalid_config_carries_message() {
168 let e = CommissioningError::InvalidConfig("missing IPK epoch key");
169 let msg = e.to_string();
170 assert!(msg.contains("missing IPK"), "{msg}");
171 }
172
173 #[test]
174 fn unexpected_response_kind_shows_both_sides() {
175 let e = CommissioningError::UnexpectedResponseKind {
176 expected: Expectation::ArmFailsafeResponse,
177 got: Expectation::AttestationResponse,
178 };
179 let msg = e.to_string();
180 assert!(msg.contains("ArmFailsafeResponse"), "{msg}");
181 assert!(msg.contains("AttestationResponse"), "{msg}");
182 }
183
184 #[test]
185 fn out_of_order_response_names_the_stage() {
186 let e = CommissioningError::OutOfOrderResponse(Stage::ArmFailsafe);
187 let msg = e.to_string();
188 assert!(msg.contains("ArmFailsafe"), "{msg}");
189 }
190
191 #[test]
192 fn device_im_status_includes_stage_and_status_code() {
193 let e = CommissioningError::DeviceImStatus {
194 stage: Stage::ArmFailsafe,
195 im_status: 0x0098,
196 };
197 let msg = e.to_string();
198 assert!(msg.contains("ArmFailsafe"), "{msg}");
199 assert!(msg.contains("0x98"), "{msg}");
200 }
201
202 #[test]
203 fn remediation_hint_is_copy_eq_hash() {
204 fn assert_copy<T: Copy + Eq + std::hash::Hash>() {}
205 assert_copy::<RemediationHint>();
206 assert_eq!(RemediationHint::None, RemediationHint::None);
207 assert_ne!(RemediationHint::None, RemediationHint::CheckPassphrase);
208 }
209}