matter_commissioning/clusters/
general_commissioning.rs1#![forbid(unsafe_code)]
8
9use crate::state_machine::{CommissioningError, Stage};
10
11pub const CLUSTER_ID: u32 = 0x0030;
13
14pub mod command_id {
16 pub const ARM_FAIL_SAFE: u32 = 0x00;
18 pub const SET_REGULATORY_CONFIG: u32 = 0x02;
20 pub const COMMISSIONING_COMPLETE: u32 = 0x04;
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ArmFailSafeResponse {
27 pub error_code: u8,
29 pub debug_text: Option<String>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct SetRegulatoryConfigResponse {
36 pub error_code: u8,
38 pub debug_text: Option<String>,
40}
41
42#[derive(Copy, Clone, Debug, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum RegulatoryLocation {
46 Indoor = 0,
48 Outdoor = 1,
50 IndoorOutdoor = 2,
52}
53
54#[must_use]
58#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_arm_fail_safe(expiry_length_seconds: u16, breadcrumb: u64) -> Vec<u8> {
60 use matter_codec::{Tag, TlvWriter};
61 let mut buf = Vec::new();
62 let mut w = TlvWriter::new(&mut buf);
63 w.start_structure(Tag::Anonymous)
64 .expect("infallible: vec writer");
65 w.put_uint(Tag::Context(0), u64::from(expiry_length_seconds))
66 .expect("infallible: vec writer");
67 w.put_uint(Tag::Context(1), breadcrumb)
68 .expect("infallible: vec writer");
69 w.end_container().expect("infallible: vec writer");
70 buf
71}
72
73#[must_use]
75#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_set_regulatory_config(
77 new_regulatory_config: RegulatoryLocation,
78 country_code: &str,
79 breadcrumb: u64,
80) -> Vec<u8> {
81 use matter_codec::{Tag, TlvWriter};
82 let mut buf = Vec::new();
83 let mut w = TlvWriter::new(&mut buf);
84 w.start_structure(Tag::Anonymous)
85 .expect("infallible: vec writer");
86 w.put_uint(Tag::Context(0), u64::from(new_regulatory_config as u8))
87 .expect("infallible: vec writer");
88 w.put_utf8(Tag::Context(1), country_code)
89 .expect("infallible: vec writer");
90 w.put_uint(Tag::Context(2), breadcrumb)
91 .expect("infallible: vec writer");
92 w.end_container().expect("infallible: vec writer");
93 buf
94}
95
96#[must_use]
102#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_commissioning_complete() -> Vec<u8> {
104 use matter_codec::{Tag, TlvWriter};
105 let mut buf = Vec::new();
106 let mut w = TlvWriter::new(&mut buf);
107 w.start_structure(Tag::Anonymous)
108 .expect("infallible: vec writer");
109 w.end_container().expect("infallible: vec writer");
110 buf
111}
112
113#[allow(clippy::match_same_arms)] pub(crate) fn decode_commissioning_error_response(
121 stage: Stage,
122 tlv: &[u8],
123) -> Result<(u8, Option<String>), CommissioningError> {
124 use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
125 let mut reader = TlvReader::new(tlv);
126 match reader
127 .next()
128 .map_err(|_| CommissioningError::MalformedResponse(stage))?
129 {
130 Some(Element::ContainerStart {
131 tag: Tag::Anonymous,
132 kind: ContainerKind::Structure,
133 }) => {}
134 _ => return Err(CommissioningError::MalformedResponse(stage)),
135 }
136 let mut error_code: Option<u8> = None;
137 let mut debug_text: Option<String> = None;
138 loop {
139 match reader
140 .next()
141 .map_err(|_| CommissioningError::MalformedResponse(stage))?
142 {
143 None => return Err(CommissioningError::MalformedResponse(stage)),
144 Some(Element::ContainerEnd) => break,
145 Some(Element::Scalar {
146 tag: Tag::Context(0),
147 value: Value::Uint(v),
148 }) => {
149 if error_code.is_some() {
150 return Err(CommissioningError::MalformedResponse(stage));
151 }
152 let n =
153 u8::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?;
154 error_code = Some(n);
155 }
156 Some(Element::Scalar {
157 tag: Tag::Context(1),
158 value: Value::Utf8(s),
159 }) => {
160 if debug_text.is_some() {
161 return Err(CommissioningError::MalformedResponse(stage));
162 }
163 debug_text = Some(s);
164 }
165 Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
167 Some(_) => return Err(CommissioningError::MalformedResponse(stage)),
168 }
169 }
170 let error_code = error_code.ok_or(CommissioningError::MalformedResponse(stage))?;
171 Ok((error_code, debug_text))
172}
173
174pub fn decode_arm_fail_safe_response(
181 tlv: &[u8],
182) -> Result<ArmFailSafeResponse, CommissioningError> {
183 let (error_code, debug_text) = decode_commissioning_error_response(Stage::ArmFailsafe, tlv)?;
184 Ok(ArmFailSafeResponse {
185 error_code,
186 debug_text,
187 })
188}
189
190pub fn decode_set_regulatory_config_response(
197 tlv: &[u8],
198) -> Result<SetRegulatoryConfigResponse, CommissioningError> {
199 let (error_code, debug_text) =
200 decode_commissioning_error_response(Stage::ConfigRegulatory, tlv)?;
201 Ok(SetRegulatoryConfigResponse {
202 error_code,
203 debug_text,
204 })
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct BasicCommissioningInfo {
215 pub failsafe_expiry_length_seconds: u16,
218 pub max_cumulative_failsafe_seconds: u16,
221}
222
223#[must_use]
230pub fn decode_basic_commissioning_info(tlv: &[u8]) -> Option<BasicCommissioningInfo> {
231 use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
232 let mut reader = TlvReader::new(tlv);
233 match reader.next().ok().flatten()? {
234 Element::ContainerStart {
235 tag: Tag::Anonymous,
236 kind: ContainerKind::Structure,
237 } => {}
238 _ => return None,
239 }
240 let mut failsafe: Option<u16> = None;
241 let mut max_cumulative: u16 = 0;
242 loop {
243 match reader.next().ok().flatten() {
244 None => return None,
245 Some(Element::ContainerEnd) => break,
246 Some(Element::Scalar {
247 tag: Tag::Context(0),
248 value: Value::Uint(v),
249 }) => {
250 failsafe = u16::try_from(v).ok();
251 }
252 Some(Element::Scalar {
253 tag: Tag::Context(1),
254 value: Value::Uint(v),
255 }) => {
256 if let Ok(n) = u16::try_from(v) {
257 max_cumulative = n;
258 }
259 }
260 Some(_) => {}
262 }
263 }
264 failsafe.map(|fs| BasicCommissioningInfo {
265 failsafe_expiry_length_seconds: fs,
266 max_cumulative_failsafe_seconds: max_cumulative,
267 })
268}
269
270#[cfg(test)]
271#[allow(
272 clippy::unwrap_used,
273 clippy::expect_used,
274 clippy::items_after_statements
275)] mod tests {
277 use super::*;
278
279 #[test]
280 fn arm_fail_safe_60_0_matches_spec_bytes() {
281 let bytes = encode_arm_fail_safe(60, 0);
282 assert_eq!(
283 bytes,
284 vec![0x15, 0x24, 0x00, 0x3C, 0x24, 0x01, 0x00, 0x18],
285 "encoded bytes: {bytes:02x?}"
286 );
287 }
288
289 #[test]
290 fn commissioning_complete_is_empty_anonymous_struct() {
291 let bytes = encode_commissioning_complete();
292 assert_eq!(bytes, vec![0x15, 0x18]);
293 }
294
295 #[test]
296 fn arm_fail_safe_response_ok_round_trips() {
297 let tlv = vec![0x15, 0x24, 0x00, 0x00, 0x18];
299 let decoded = decode_arm_fail_safe_response(&tlv).expect("happy path decodes");
300 assert_eq!(decoded.error_code, 0);
301 assert_eq!(decoded.debug_text, None);
302 }
303
304 #[test]
305 fn arm_fail_safe_response_with_debug_text_round_trips() {
306 let tlv = vec![
308 0x15, 0x24, 0x00, 0x01, 0x2C, 0x01, 0x04, b'b', b'u', b's', b'y', 0x18, ];
312 let decoded = decode_arm_fail_safe_response(&tlv).expect("happy path decodes");
313 assert_eq!(decoded.error_code, 1);
314 assert_eq!(decoded.debug_text.as_deref(), Some("busy"));
315 }
316
317 #[test]
318 fn malformed_response_returns_error() {
319 let err = decode_arm_fail_safe_response(&[0xFF]).expect_err("should fail");
321 assert!(
322 matches!(
323 err,
324 crate::state_machine::CommissioningError::MalformedResponse(
325 crate::state_machine::Stage::ArmFailsafe
326 )
327 ),
328 "got {err:?}"
329 );
330 }
331
332 #[test]
333 fn basic_commissioning_info_120_decodes() {
334 let tlv = vec![
336 0x15, 0x25, 0x00, 0x78, 0x00, 0x25, 0x01, 0x84, 0x03, 0x18,
339 ];
340 let info = decode_basic_commissioning_info(&tlv).expect("decodes");
341 assert_eq!(info.failsafe_expiry_length_seconds, 120);
342 assert_eq!(info.max_cumulative_failsafe_seconds, 900);
343 }
344
345 #[test]
346 fn basic_commissioning_info_malformed_returns_none() {
347 assert!(decode_basic_commissioning_info(&[0xFF]).is_none());
348 assert!(decode_basic_commissioning_info(&[]).is_none());
349 }
350}