Skip to main content

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}