matter_commissioning/state_machine/stage.rs
1//! `Stage` — every cursor position the state machine can occupy.
2//!
3//! Stages match `project-chip/connectedhomeip`'s `CommissioningStage`
4//! enum (translated to Rust style and trimmed to the M6.5 subset).
5//! Stages we defer past M6.5 are noted inline as `// deferred: kFoo`
6//! so future expansion is mechanical.
7
8#![forbid(unsafe_code)]
9
10/// Cursor position inside the commissioning sequence.
11///
12/// Variants are ordered top-to-bottom in transition order. The transition
13/// function lives in `next_stage` (crate-internal).
14#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum Stage {
17 /// Entry state — caller has just constructed the [`super::Commissioner`]
18 /// with a valid PASE session. No action emitted; advances on first
19 /// `poll()` to [`Self::ReadCommissioningInfo`].
20 SecurePairing,
21 /// Read `BasicCommissioningInfo`, `RegulatoryConfig`, etc. from the
22 /// `GeneralCommissioning` cluster (id `0x0030`) so the commissioner
23 /// knows `failsafe_expiry_length_seconds` before arming the failsafe.
24 ReadCommissioningInfo,
25 /// `GeneralCommissioning::ArmFailSafe` (command id `0x00`).
26 ArmFailsafe,
27 /// `GeneralCommissioning::SetRegulatoryConfig` (command id `0x02`).
28 ConfigRegulatory,
29 /// `OperationalCredentials::CertificateChainRequest` with `type=PAI`
30 /// (cluster `0x003E`, command `0x02`, type enum `0x01`).
31 SendPaiCertRequest,
32 /// `OperationalCredentials::CertificateChainRequest` with `type=DAC`
33 /// (type enum `0x02`).
34 SendDacCertRequest,
35 /// `OperationalCredentials::AttestationRequest` (command `0x00`).
36 SendAttestationRequest,
37 /// Off-wire: chain + signature + CD verification.
38 AttestationVerification,
39 /// `OperationalCredentials::CSRRequest` (command `0x04`).
40 SendOpCertSigningRequest,
41 /// Off-wire: PKCS#10 self-signature + DAC attestation + nonce echo.
42 ValidateCsr,
43 /// Off-wire: build + sign the NOC under the commissioner's RCAC.
44 GenerateNocChain,
45 /// `OperationalCredentials::AddTrustedRootCertificate` (command `0x0B`).
46 SendTrustedRootCert,
47 /// `OperationalCredentials::AddNOC` (command `0x06`).
48 SendNoc,
49 /// Read `NetworkCommissioning::FeatureMap` (attribute `0xFFFC`) and
50 /// `ConnectMaxTimeSeconds` (attribute `0x0003`) on endpoint 0.
51 /// `FeatureMap` determines whether the device supports Wi-Fi,
52 /// Ethernet, or Thread (or some combination). Branching at this
53 /// stage's response routes by the *supplied*
54 /// [`super::NetworkCredentials`] variant, cross-checked against the
55 /// device `FeatureMap`: matching credentials advance to
56 /// `NetworkSetup`, `AlreadyOnNetwork` skips to
57 /// `EvictPreviousCaseSessions`, and a credential type absent from the
58 /// `FeatureMap` fails with `NetworkFeatureUnsupported`.
59 ReadNetworkCommissioningInfo,
60 /// Network provisioning: `NetworkCommissioning::AddOrUpdateWiFiNetwork`
61 /// (cluster `0x0031` command `0x02`) for Wi-Fi credentials, or
62 /// `AddOrUpdateThreadNetwork` (command `0x03`) for a Thread dataset.
63 /// The stage is generic; the command is selected by the supplied
64 /// [`super::NetworkCredentials`] variant. Skipped for `AlreadyOnNetwork`
65 /// / Ethernet-only devices.
66 NetworkSetup,
67 /// Second `GeneralCommissioning::ArmFailSafe` (cluster `0x0030`
68 /// command `0x00`). Extends the failsafe window before
69 /// `ConnectNetwork` so the device has room to associate with the
70 /// operational network and re-discover the commissioner via mDNS.
71 /// The extension is sized from the device's `ConnectMaxTimeSeconds`
72 /// (Thread attach is slower than Wi-Fi association), falling back to
73 /// a generous default. Re-uses the existing
74 /// `Expectation::ArmFailsafeResponse`.
75 FailsafeBeforeNetworkEnable,
76 /// `NetworkCommissioning::ConnectNetwork` (cluster `0x0031`
77 /// command `0x06`). The device associates with the operational
78 /// network and (typically) returns `ConnectNetworkResponse` over
79 /// PASE before switching networks. The `network_id` is the SSID for
80 /// Wi-Fi and the Extended PAN ID for Thread.
81 NetworkEnable,
82 /// Evict any prior CASE session for this fabric/peer pair. M6.4
83 /// only supports new-fabric commissioning — no eviction needed —
84 /// so the stage advances immediately. Slot reserved for M8
85 /// multi-fabric work.
86 EvictPreviousCaseSessions,
87 /// Caller establishes a CASE session via mDNS find-operational +
88 /// SIGMA handshake (M6.6 mechanics). State machine emits
89 /// `Action::EstablishCase` and waits for `on_case_established()`.
90 FindOperationalForComplete,
91 /// `GeneralCommissioning::CommissioningComplete` (command `0x04`),
92 /// sent over the freshly-established CASE session.
93 SendComplete,
94 /// Terminal success. Emits `Action::Done(CommissionedFabric)`.
95 Cleanup,
96 /// Terminal failure. Emits `Action::Abort`.
97 Failed,
98 // deferred: kReadCommissioningInfo2 (post-NOC capability re-read)
99 // deferred: kConfigureUTCTime, kConfigureTimeZone, kConfigureDSTOffset, kConfigureDefaultNTP
100 // deferred: kAttestationRevocationCheck
101 // deferred: kJCMTrustVerification
102 // deferred: kICDGetRegistrationInfo, kICDRegistration
103 // deferred: kConfigureTCAcknowledgments
104 // deferred: kPrimaryOperationalNetworkFailed, kRemoveWiFiNetworkConfig, kRemoveThreadNetworkConfig
105}
106
107/// Happy-path successor of `current`. Returns `None` for terminal
108/// stages (`Cleanup`, `Failed`).
109///
110/// Used by [`super::Commissioner`] to advance the cursor after a stage
111/// completes successfully. Errors at any stage transition the cursor
112/// directly to [`Stage::Failed`] rather than calling this function.
113// Used by Commissioner::advance from M6.4.1 T6 onward.
114#[allow(dead_code)]
115#[allow(unreachable_pub)]
116#[must_use]
117pub fn next_stage(current: Stage) -> Option<Stage> {
118 Some(match current {
119 Stage::SecurePairing => Stage::ReadCommissioningInfo,
120 Stage::ReadCommissioningInfo => Stage::ArmFailsafe,
121 Stage::ArmFailsafe => Stage::ConfigRegulatory,
122 Stage::ConfigRegulatory => Stage::SendPaiCertRequest,
123 Stage::SendPaiCertRequest => Stage::SendDacCertRequest,
124 Stage::SendDacCertRequest => Stage::SendAttestationRequest,
125 Stage::SendAttestationRequest => Stage::AttestationVerification,
126 Stage::AttestationVerification => Stage::SendOpCertSigningRequest,
127 Stage::SendOpCertSigningRequest => Stage::ValidateCsr,
128 Stage::ValidateCsr => Stage::GenerateNocChain,
129 Stage::GenerateNocChain => Stage::SendTrustedRootCert,
130 Stage::SendTrustedRootCert => Stage::SendNoc,
131 Stage::SendNoc => Stage::ReadNetworkCommissioningInfo,
132 Stage::ReadNetworkCommissioningInfo => Stage::NetworkSetup,
133 Stage::NetworkSetup => Stage::FailsafeBeforeNetworkEnable,
134 Stage::FailsafeBeforeNetworkEnable => Stage::NetworkEnable,
135 Stage::NetworkEnable => Stage::EvictPreviousCaseSessions,
136 Stage::EvictPreviousCaseSessions => Stage::FindOperationalForComplete,
137 Stage::FindOperationalForComplete => Stage::SendComplete,
138 Stage::SendComplete => Stage::Cleanup,
139 Stage::Cleanup | Stage::Failed => return None,
140 })
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn happy_path_advances_through_all_stages() {
149 // Test-code carve-out: see CLAUDE.md.
150 #![allow(clippy::unwrap_used)]
151 let expected = [
152 Stage::SecurePairing,
153 Stage::ReadCommissioningInfo,
154 Stage::ArmFailsafe,
155 Stage::ConfigRegulatory,
156 Stage::SendPaiCertRequest,
157 Stage::SendDacCertRequest,
158 Stage::SendAttestationRequest,
159 Stage::AttestationVerification,
160 Stage::SendOpCertSigningRequest,
161 Stage::ValidateCsr,
162 Stage::GenerateNocChain,
163 Stage::SendTrustedRootCert,
164 Stage::SendNoc,
165 Stage::ReadNetworkCommissioningInfo,
166 Stage::NetworkSetup,
167 Stage::FailsafeBeforeNetworkEnable,
168 Stage::NetworkEnable,
169 Stage::EvictPreviousCaseSessions,
170 Stage::FindOperationalForComplete,
171 Stage::SendComplete,
172 Stage::Cleanup,
173 ];
174 for pair in expected.windows(2) {
175 assert_eq!(
176 next_stage(pair[0]),
177 Some(pair[1]),
178 "next_stage({:?}) should be Some({:?})",
179 pair[0],
180 pair[1],
181 );
182 }
183 assert_eq!(next_stage(Stage::Cleanup), None, "Cleanup is terminal");
184 assert_eq!(next_stage(Stage::Failed), None, "Failed is terminal");
185 }
186}