matter_commissioning/state_machine/action.rs
1//! `Action` / `Expectation` / `SessionContext` / `CommissionedFabric` —
2//! the outbound vocabulary the [`super::Commissioner`] uses to ask the
3//! caller for work.
4
5#![forbid(unsafe_code)]
6
7use crate::noc::FabricRecord;
8use crate::state_machine::stage::Stage;
9
10/// Whether an `Action::Invoke` or `Action::ReadAttribute` should be
11/// routed over the PASE session (pre-commissioning) or the CASE session
12/// (post-AddNOC, after [`Action::EstablishCase`] is fulfilled).
13#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
14pub enum SessionContext {
15 /// Pre-commissioning session keyed off the device's passcode.
16 Pase,
17 /// Post-AddNOC operational session keyed off the new fabric.
18 Case,
19}
20
21/// The next piece of work the caller must perform.
22///
23/// Returned by [`super::Commissioner::poll`]. The state machine is
24/// idempotent: calling `poll` twice without an intervening `on_response`
25/// returns the same `Action`.
26// `Done(CommissionedFabric)` is intentionally large (~130 B) but is emitted
27// exactly once per successful commission — boxing it would force an alloc on
28// the happy path with no real benefit.
29#[allow(clippy::large_enum_variant)]
30#[derive(Clone, Debug)]
31#[non_exhaustive]
32pub enum Action {
33 /// Invoke a cluster command. The caller frames `payload` into an
34 /// Invoke envelope and routes via `matter-transport` over the
35 /// indicated session. The decoded response payload is fed back via
36 /// [`super::Commissioner::on_response`] with the matching `expect`.
37 Invoke {
38 /// Which session to route the Invoke over.
39 session: SessionContext,
40 /// Matter endpoint (always `0` for commissioning).
41 endpoint: u16,
42 /// Cluster ID (`0x0030` `GeneralCommissioning` or `0x003E`
43 /// `OperationalCredentials` for all M6.4 stages).
44 cluster: u32,
45 /// Cluster command ID.
46 command: u32,
47 /// TLV-encoded command payload.
48 payload: Vec<u8>,
49 /// The response type the state machine expects next.
50 expect: Expectation,
51 },
52
53 /// Read attributes from a cluster. Used only by
54 /// [`Stage::ReadCommissioningInfo`].
55 ReadAttribute {
56 /// Which session to route the Read over.
57 session: SessionContext,
58 /// Matter endpoint (always `0` for commissioning).
59 endpoint: u16,
60 /// Cluster ID.
61 cluster: u32,
62 /// Attribute IDs to read.
63 attributes: &'static [u32],
64 /// The response type the state machine expects next.
65 expect: Expectation,
66 },
67
68 /// Evict any prior CASE session for this fabric/peer pair.
69 ///
70 /// **Reserved for M8 multi-fabric work; never emitted by M6.4's
71 /// state machine.** Kept in the enum so M8 can wire eviction in
72 /// without a `SemVer` bump.
73 EvictCase {
74 /// Fabric ID to evict on.
75 fabric_id: u64,
76 /// Peer operational node ID to evict for.
77 peer_node_id: u64,
78 },
79
80 /// Discover the device on its operational network and establish a
81 /// CASE session. Caller calls `Commissioner::on_case_established`
82 /// (added in M6.4.5) on success or
83 /// `on_response(Expectation::CaseFailed, &[])` on failure.
84 EstablishCase {
85 /// Fabric ID to establish CASE on.
86 fabric_id: u64,
87 /// Peer operational node ID to establish with.
88 peer_node_id: u64,
89 },
90
91 /// Commissioning succeeded. Caller may persist the
92 /// [`CommissionedFabric`] long-term.
93 Done(CommissionedFabric),
94
95 /// Commissioning failed. If `send_disarm_failsafe` is true, the
96 /// caller should send `ArmFailSafe(expiry_length_seconds=0)` to the
97 /// device over PASE to roll the device back to its
98 /// pre-commissioning state.
99 ///
100 /// `reason` is a rendered, log-friendly summary of the
101 /// `CommissioningError` that caused the abort. The caller will have
102 /// also received that error directly via
103 /// [`super::Commissioner::on_response`]'s `Err` return — `reason`
104 /// here is supplementary, intended for logs.
105 Abort {
106 /// Whether the caller should send `DisarmFailsafe` before
107 /// dropping the PASE session.
108 send_disarm_failsafe: bool,
109 /// Pre-rendered description of the failure cause.
110 reason: String,
111 },
112}
113
114/// The response type the state machine expects after an
115/// [`Action::Invoke`] or [`Action::ReadAttribute`].
116///
117/// Passed back into [`super::Commissioner::on_response`] alongside the
118/// raw TLV payload so the state machine can validate that the response
119/// matches the request without parsing the entire TLV first.
120#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
121#[non_exhaustive]
122pub enum Expectation {
123 /// Response to `Action::ReadAttribute` for `BasicCommissioningInfo` +
124 /// `RegulatoryConfig` + `CapabilityMinima`.
125 CommissioningInfo,
126 /// `GeneralCommissioning::ArmFailSafeResponse` (cluster `0x0030`,
127 /// response `0x01`).
128 ArmFailsafeResponse,
129 /// `GeneralCommissioning::SetRegulatoryConfigResponse` (response `0x03`).
130 SetRegulatoryConfigResponse,
131 /// `OperationalCredentials::CertificateChainResponse` for the PAI.
132 PaiCertChainResponse,
133 /// `OperationalCredentials::CertificateChainResponse` for the DAC.
134 DacCertChainResponse,
135 /// `OperationalCredentials::AttestationResponse` (response `0x01`).
136 AttestationResponse,
137 /// `OperationalCredentials::CSRResponse` (response `0x05`).
138 CsrResponse,
139 /// Status-only ack for `OperationalCredentials::AddTrustedRootCertificate`.
140 AddTrustedRootResponse,
141 /// `OperationalCredentials::NOCResponse` (response `0x08`).
142 NocResponse,
143 /// `GeneralCommissioning::CommissioningCompleteResponse` (response `0x05`).
144 CommissioningCompleteResponse,
145 /// Response to `Action::ReadAttribute` for
146 /// `NetworkCommissioning::FeatureMap` (attribute `0xFFFC`). Caller
147 /// delivers the bare u32 attribute value's TLV bytes, not the
148 /// Interaction Model `AttributeReportIB` envelope.
149 NetworkCommissioningInfo,
150 /// `NetworkCommissioning::NetworkConfigResponse` (cluster `0x0031`
151 /// response `0x05`) — emitted by `AddOrUpdateWiFiNetwork`.
152 NetworkConfigResponse,
153 /// `NetworkCommissioning::ConnectNetworkResponse` (response `0x07`).
154 ConnectNetworkResponse,
155 /// Caller-side signal that CASE establishment failed. Fed into
156 /// `on_response(Expectation::CaseFailed, &[])` after
157 /// [`Action::EstablishCase`].
158 CaseFailed,
159}
160
161/// Output of a successful commissioning run. Returned in
162/// [`Action::Done`].
163#[derive(Debug, Clone)]
164#[non_exhaustive]
165pub struct CommissionedFabric {
166 /// The fabric record the device is now a member of (RCAC + IPK +
167 /// fabric ID).
168 pub fabric: FabricRecord,
169 /// Operational node ID the device was assigned on this fabric.
170 pub peer_node_id: u64,
171 /// Raw SEC1 uncompressed P-256 (65 bytes) — the device's NOC public
172 /// key, extracted from the issued NOC.
173 pub peer_root_public_key: [u8; 65],
174 /// Stage where the run terminated. Always [`Stage::Cleanup`] on
175 /// success; useful for logging.
176 pub terminated_at: Stage,
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn invoke_action_round_trips_through_clone() {
185 let a = Action::Invoke {
186 session: SessionContext::Pase,
187 endpoint: 0,
188 cluster: 0x0030,
189 command: 0x00,
190 payload: vec![0x15, 0x18],
191 expect: Expectation::ArmFailsafeResponse,
192 };
193 let b = a.clone();
194 match (a, b) {
195 (
196 Action::Invoke {
197 endpoint: e1,
198 cluster: c1,
199 command: cmd1,
200 ..
201 },
202 Action::Invoke {
203 endpoint: e2,
204 cluster: c2,
205 command: cmd2,
206 ..
207 },
208 ) => {
209 assert_eq!(e1, e2);
210 assert_eq!(c1, c2);
211 assert_eq!(cmd1, cmd2);
212 }
213 _ => panic!("clone produced wrong variant"),
214 }
215 }
216
217 #[test]
218 fn expectation_is_copy() {
219 fn assert_copy<T: Copy>() {}
220 assert_copy::<Expectation>();
221 }
222
223 #[test]
224 fn session_context_distinguishes_pase_and_case() {
225 assert_ne!(SessionContext::Pase, SessionContext::Case);
226 }
227
228 #[test]
229 fn abort_reason_carries_string() {
230 let a = Action::Abort {
231 send_disarm_failsafe: true,
232 reason: "synthetic failure".to_string(),
233 };
234 match a {
235 Action::Abort {
236 reason,
237 send_disarm_failsafe,
238 } => {
239 assert!(send_disarm_failsafe);
240 assert_eq!(reason, "synthetic failure");
241 }
242 _ => panic!("expected Abort"),
243 }
244 }
245}