#![forbid(unsafe_code)]
use crate::clusters::network_commissioning::truncate_utf8;
use crate::state_machine::{CommissioningError, Stage};
pub const CLUSTER_ID: u32 = 0x0030;
pub mod command_id {
pub const ARM_FAIL_SAFE: u32 = 0x00;
pub const SET_REGULATORY_CONFIG: u32 = 0x02;
pub const COMMISSIONING_COMPLETE: u32 = 0x04;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArmFailSafeResponse {
pub error_code: u8,
pub debug_text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetRegulatoryConfigResponse {
pub error_code: u8,
pub debug_text: Option<String>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum RegulatoryLocation {
Indoor = 0,
Outdoor = 1,
IndoorOutdoor = 2,
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_arm_fail_safe(expiry_length_seconds: u16, breadcrumb: u64) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_uint(Tag::Context(0), u64::from(expiry_length_seconds))
.expect("infallible: vec writer");
w.put_uint(Tag::Context(1), breadcrumb)
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_set_regulatory_config(
new_regulatory_config: RegulatoryLocation,
country_code: &str,
breadcrumb: u64,
) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_uint(Tag::Context(0), u64::from(new_regulatory_config as u8))
.expect("infallible: vec writer");
w.put_utf8(Tag::Context(1), country_code)
.expect("infallible: vec writer");
w.put_uint(Tag::Context(2), breadcrumb)
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_commissioning_complete() -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
#[allow(clippy::match_same_arms)] pub(crate) fn decode_commissioning_error_response(
stage: Stage,
tlv: &[u8],
) -> Result<(u8, Option<String>), CommissioningError> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader
.next()
.map_err(|_| CommissioningError::MalformedResponse(stage))?
{
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
_ => return Err(CommissioningError::MalformedResponse(stage)),
}
let mut error_code: Option<u8> = None;
let mut debug_text: Option<String> = None;
loop {
match reader
.next()
.map_err(|_| CommissioningError::MalformedResponse(stage))?
{
None => return Err(CommissioningError::MalformedResponse(stage)),
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(v),
}) => {
if error_code.is_some() {
return Err(CommissioningError::MalformedResponse(stage));
}
let n =
u8::try_from(v).map_err(|_| CommissioningError::MalformedResponse(stage))?;
error_code = Some(n);
}
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Utf8(s),
}) => {
if debug_text.is_some() {
return Err(CommissioningError::MalformedResponse(stage));
}
debug_text = Some(truncate_utf8(s, 512));
}
Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
Some(_) => return Err(CommissioningError::MalformedResponse(stage)),
}
}
let error_code = error_code.ok_or(CommissioningError::MalformedResponse(stage))?;
Ok((error_code, debug_text))
}
pub fn decode_arm_fail_safe_response(
tlv: &[u8],
) -> Result<ArmFailSafeResponse, CommissioningError> {
let (error_code, debug_text) = decode_commissioning_error_response(Stage::ArmFailsafe, tlv)?;
Ok(ArmFailSafeResponse {
error_code,
debug_text,
})
}
pub fn decode_set_regulatory_config_response(
tlv: &[u8],
) -> Result<SetRegulatoryConfigResponse, CommissioningError> {
let (error_code, debug_text) =
decode_commissioning_error_response(Stage::ConfigRegulatory, tlv)?;
Ok(SetRegulatoryConfigResponse {
error_code,
debug_text,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BasicCommissioningInfo {
pub failsafe_expiry_length_seconds: u16,
pub max_cumulative_failsafe_seconds: u16,
}
#[must_use]
pub fn decode_basic_commissioning_info(tlv: &[u8]) -> Option<BasicCommissioningInfo> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader.next().ok().flatten()? {
Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
} => {}
_ => return None,
}
let mut failsafe: Option<u16> = None;
let mut max_cumulative: u16 = 0;
loop {
match reader.next().ok().flatten() {
None => return None,
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(v),
}) => {
failsafe = u16::try_from(v).ok();
}
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Uint(v),
}) => {
if let Ok(n) = u16::try_from(v) {
max_cumulative = n;
}
}
Some(_) => {}
}
}
failsafe.map(|fs| BasicCommissioningInfo {
failsafe_expiry_length_seconds: fs,
max_cumulative_failsafe_seconds: max_cumulative,
})
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::items_after_statements
)] mod tests {
use super::*;
#[test]
fn arm_fail_safe_60_0_matches_spec_bytes() {
let bytes = encode_arm_fail_safe(60, 0);
assert_eq!(
bytes,
vec![0x15, 0x24, 0x00, 0x3C, 0x24, 0x01, 0x00, 0x18],
"encoded bytes: {bytes:02x?}"
);
}
#[test]
fn commissioning_complete_is_empty_anonymous_struct() {
let bytes = encode_commissioning_complete();
assert_eq!(bytes, vec![0x15, 0x18]);
}
#[test]
fn arm_fail_safe_response_ok_round_trips() {
let tlv = vec![0x15, 0x24, 0x00, 0x00, 0x18];
let decoded = decode_arm_fail_safe_response(&tlv).expect("happy path decodes");
assert_eq!(decoded.error_code, 0);
assert_eq!(decoded.debug_text, None);
}
#[test]
fn arm_fail_safe_response_with_debug_text_round_trips() {
let tlv = vec![
0x15, 0x24, 0x00, 0x01, 0x2C, 0x01, 0x04, b'b', b'u', b's', b'y', 0x18, ];
let decoded = decode_arm_fail_safe_response(&tlv).expect("happy path decodes");
assert_eq!(decoded.error_code, 1);
assert_eq!(decoded.debug_text.as_deref(), Some("busy"));
}
#[test]
fn malformed_response_returns_error() {
let err = decode_arm_fail_safe_response(&[0xFF]).expect_err("should fail");
assert!(
matches!(
err,
crate::state_machine::CommissioningError::MalformedResponse(
crate::state_machine::Stage::ArmFailsafe
)
),
"got {err:?}"
);
}
#[test]
fn basic_commissioning_info_120_decodes() {
let tlv = vec![
0x15, 0x25, 0x00, 0x78, 0x00, 0x25, 0x01, 0x84, 0x03, 0x18,
];
let info = decode_basic_commissioning_info(&tlv).expect("decodes");
assert_eq!(info.failsafe_expiry_length_seconds, 120);
assert_eq!(info.max_cumulative_failsafe_seconds, 900);
}
#[test]
fn basic_commissioning_info_malformed_returns_none() {
assert!(decode_basic_commissioning_info(&[0xFF]).is_none());
assert!(decode_basic_commissioning_info(&[]).is_none());
}
#[test]
fn arm_fail_safe_response_caps_debug_text_at_512_bytes() {
use matter_codec::{Tag, TlvWriter};
let long_text = "x".repeat(600);
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 1).unwrap();
w.put_utf8(Tag::Context(1), &long_text).unwrap();
w.end_container().unwrap();
let decoded = decode_arm_fail_safe_response(&buf).expect("happy path decodes");
assert_eq!(decoded.error_code, 1);
assert_eq!(
decoded.debug_text.unwrap().len(),
512,
"capped at spec bound"
);
}
}