matter_commissioning/clusters/
general_commissioning.rs1#![forbid(unsafe_code)]
8
9use crate::clusters::network_commissioning::truncate_utf8;
10use crate::state_machine::{CommissioningError, Stage};
11
12pub const CLUSTER_ID: u32 = 0x0030;
14
15pub mod command_id {
17 pub const ARM_FAIL_SAFE: u32 = 0x00;
19 pub const SET_REGULATORY_CONFIG: u32 = 0x02;
21 pub const COMMISSIONING_COMPLETE: u32 = 0x04;
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ArmFailSafeResponse {
28 pub error_code: u8,
30 pub debug_text: Option<String>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct SetRegulatoryConfigResponse {
39 pub error_code: u8,
41 pub debug_text: Option<String>,
44}
45
46#[derive(Copy, Clone, Debug, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum RegulatoryLocation {
50 Indoor = 0,
52 Outdoor = 1,
54 IndoorOutdoor = 2,
56}
57
58#[must_use]
62#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_arm_fail_safe(expiry_length_seconds: u16, breadcrumb: u64) -> Vec<u8> {
64 use matter_codec::{Tag, TlvWriter};
65 let mut buf = Vec::new();
66 let mut w = TlvWriter::new(&mut buf);
67 w.start_structure(Tag::Anonymous)
68 .expect("infallible: vec writer");
69 w.put_uint(Tag::Context(0), u64::from(expiry_length_seconds))
70 .expect("infallible: vec writer");
71 w.put_uint(Tag::Context(1), breadcrumb)
72 .expect("infallible: vec writer");
73 w.end_container().expect("infallible: vec writer");
74 buf
75}
76
77#[must_use]
79#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_set_regulatory_config(
81 new_regulatory_config: RegulatoryLocation,
82 country_code: &str,
83 breadcrumb: u64,
84) -> Vec<u8> {
85 use matter_codec::{Tag, TlvWriter};
86 let mut buf = Vec::new();
87 let mut w = TlvWriter::new(&mut buf);
88 w.start_structure(Tag::Anonymous)
89 .expect("infallible: vec writer");
90 w.put_uint(Tag::Context(0), u64::from(new_regulatory_config as u8))
91 .expect("infallible: vec writer");
92 w.put_utf8(Tag::Context(1), country_code)
93 .expect("infallible: vec writer");
94 w.put_uint(Tag::Context(2), breadcrumb)
95 .expect("infallible: vec writer");
96 w.end_container().expect("infallible: vec writer");
97 buf
98}
99
100#[must_use]
106#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_commissioning_complete() -> Vec<u8> {
108 use matter_codec::{Tag, TlvWriter};
109 let mut buf = Vec::new();
110 let mut w = TlvWriter::new(&mut buf);
111 w.start_structure(Tag::Anonymous)
112 .expect("infallible: vec writer");
113 w.end_container().expect("infallible: vec writer");
114 buf
115}
116
117#[allow(clippy::match_same_arms)] pub(crate) fn decode_commissioning_error_response(
125 stage: Stage,
126 tlv: &[u8],
127) -> Result<(u8, Option<String>), CommissioningError> {
128 use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
129 let mut reader = TlvReader::new(tlv);
130 match reader
131 .next()
132 .map_err(|_| CommissioningError::MalformedResponse(stage))?
133 {
134 Some(Element::ContainerStart {
135 tag: Tag::Anonymous,
136 kind: ContainerKind::Structure,
137 }) => {}
138 _ => return Err(CommissioningError::MalformedResponse(stage)),
139 }
140 let mut error_code: Option<u8> = None;
141 let mut debug_text: Option<String> = None;
142 loop {
143 match reader
144 .next()
145 .map_err(|_| CommissioningError::MalformedResponse(stage))?
146 {
147 None => return Err(CommissioningError::MalformedResponse(stage)),
148 Some(Element::ContainerEnd) => break,
149 Some(Element::Scalar {
150 tag: Tag::Context(0),
151 value: Value::Uint(v),
152 }) => {
153 if error_code.is_some() {
154 return Err(CommissioningError::MalformedResponse(stage));
155 }
156 let n =
157 u8::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?;
158 error_code = Some(n);
159 }
160 Some(Element::Scalar {
161 tag: Tag::Context(1),
162 value: Value::Utf8(s),
163 }) => {
164 if debug_text.is_some() {
165 return Err(CommissioningError::MalformedResponse(stage));
166 }
167 debug_text = Some(truncate_utf8(s, 512));
169 }
170 Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
172 Some(_) => return Err(CommissioningError::MalformedResponse(stage)),
173 }
174 }
175 let error_code = error_code.ok_or(CommissioningError::MalformedResponse(stage))?;
176 Ok((error_code, debug_text))
177}
178
179pub fn decode_arm_fail_safe_response(
186 tlv: &[u8],
187) -> Result<ArmFailSafeResponse, CommissioningError> {
188 let (error_code, debug_text) = decode_commissioning_error_response(Stage::ArmFailsafe, tlv)?;
189 Ok(ArmFailSafeResponse {
190 error_code,
191 debug_text,
192 })
193}
194
195pub fn decode_set_regulatory_config_response(
202 tlv: &[u8],
203) -> Result<SetRegulatoryConfigResponse, CommissioningError> {
204 let (error_code, debug_text) =
205 decode_commissioning_error_response(Stage::ConfigRegulatory, tlv)?;
206 Ok(SetRegulatoryConfigResponse {
207 error_code,
208 debug_text,
209 })
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub struct BasicCommissioningInfo {
220 pub failsafe_expiry_length_seconds: u16,
223 pub max_cumulative_failsafe_seconds: u16,
226}
227
228#[must_use]
235pub fn decode_basic_commissioning_info(tlv: &[u8]) -> Option<BasicCommissioningInfo> {
236 use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
237 let mut reader = TlvReader::new(tlv);
238 match reader.next().ok().flatten()? {
239 Element::ContainerStart {
240 tag: Tag::Anonymous,
241 kind: ContainerKind::Structure,
242 } => {}
243 _ => return None,
244 }
245 let mut failsafe: Option<u16> = None;
246 let mut max_cumulative: u16 = 0;
247 loop {
248 match reader.next().ok().flatten() {
249 None => return None,
250 Some(Element::ContainerEnd) => break,
251 Some(Element::Scalar {
252 tag: Tag::Context(0),
253 value: Value::Uint(v),
254 }) => {
255 failsafe = u16::try_from(v).ok();
256 }
257 Some(Element::Scalar {
258 tag: Tag::Context(1),
259 value: Value::Uint(v),
260 }) => {
261 if let Ok(n) = u16::try_from(v) {
262 max_cumulative = n;
263 }
264 }
265 Some(_) => {}
267 }
268 }
269 failsafe.map(|fs| BasicCommissioningInfo {
270 failsafe_expiry_length_seconds: fs,
271 max_cumulative_failsafe_seconds: max_cumulative,
272 })
273}
274
275#[cfg(test)]
276#[allow(
277 clippy::unwrap_used,
278 clippy::expect_used,
279 clippy::items_after_statements
280)] mod tests {
282 use super::*;
283
284 #[test]
285 fn arm_fail_safe_60_0_matches_spec_bytes() {
286 let bytes = encode_arm_fail_safe(60, 0);
287 assert_eq!(
288 bytes,
289 vec![0x15, 0x24, 0x00, 0x3C, 0x24, 0x01, 0x00, 0x18],
290 "encoded bytes: {bytes:02x?}"
291 );
292 }
293
294 #[test]
295 fn commissioning_complete_is_empty_anonymous_struct() {
296 let bytes = encode_commissioning_complete();
297 assert_eq!(bytes, vec![0x15, 0x18]);
298 }
299
300 #[test]
301 fn arm_fail_safe_response_ok_round_trips() {
302 let tlv = vec![0x15, 0x24, 0x00, 0x00, 0x18];
304 let decoded = decode_arm_fail_safe_response(&tlv).expect("happy path decodes");
305 assert_eq!(decoded.error_code, 0);
306 assert_eq!(decoded.debug_text, None);
307 }
308
309 #[test]
310 fn arm_fail_safe_response_with_debug_text_round_trips() {
311 let tlv = vec![
313 0x15, 0x24, 0x00, 0x01, 0x2C, 0x01, 0x04, b'b', b'u', b's', b'y', 0x18, ];
317 let decoded = decode_arm_fail_safe_response(&tlv).expect("happy path decodes");
318 assert_eq!(decoded.error_code, 1);
319 assert_eq!(decoded.debug_text.as_deref(), Some("busy"));
320 }
321
322 #[test]
323 fn malformed_response_returns_error() {
324 let err = decode_arm_fail_safe_response(&[0xFF]).expect_err("should fail");
326 assert!(
327 matches!(
328 err,
329 crate::state_machine::CommissioningError::MalformedResponse(
330 crate::state_machine::Stage::ArmFailsafe
331 )
332 ),
333 "got {err:?}"
334 );
335 }
336
337 #[test]
338 fn basic_commissioning_info_120_decodes() {
339 let tlv = vec![
341 0x15, 0x25, 0x00, 0x78, 0x00, 0x25, 0x01, 0x84, 0x03, 0x18,
344 ];
345 let info = decode_basic_commissioning_info(&tlv).expect("decodes");
346 assert_eq!(info.failsafe_expiry_length_seconds, 120);
347 assert_eq!(info.max_cumulative_failsafe_seconds, 900);
348 }
349
350 #[test]
351 fn basic_commissioning_info_malformed_returns_none() {
352 assert!(decode_basic_commissioning_info(&[0xFF]).is_none());
353 assert!(decode_basic_commissioning_info(&[]).is_none());
354 }
355
356 #[test]
357 fn arm_fail_safe_response_caps_debug_text_at_512_bytes() {
358 use matter_codec::{Tag, TlvWriter};
359 let long_text = "x".repeat(600);
361 let mut buf = Vec::new();
362 let mut w = TlvWriter::new(&mut buf);
363 w.start_structure(Tag::Anonymous).unwrap();
364 w.put_uint(Tag::Context(0), 1).unwrap();
365 w.put_utf8(Tag::Context(1), &long_text).unwrap();
366 w.end_container().unwrap();
367
368 let decoded = decode_arm_fail_safe_response(&buf).expect("happy path decodes");
369 assert_eq!(decoded.error_code, 1);
370 assert_eq!(
371 decoded.debug_text.unwrap().len(),
372 512,
373 "capped at spec bound"
374 );
375 }
376}