use core::iter::{empty, once};
use crate::dm::clusters::basic_info::BasicInfoConfig;
use crate::transport::network::mdns::CommissionableFilter;
use super::{GATT_HEADER_SIZE, MAX_BTP_SEGMENT_SIZE};
const AD_TYPE_SERVICE_DATA_UUID16: u8 = 0x16;
const MATTER_SERVICE_DATA_PAYLOAD_LEN: usize = 8;
const MATTER_ADV_OPCODE_COMMISSIONABLE: u8 = 0x00;
const MATTER_ADV_OPCODE_NETWORK_RECOVERY: u8 = 0x01;
const MATTER_RECOVERY_SERVICE_DATA_PAYLOAD_LEN: usize = 11;
pub const RECOVERY_ID_LEN: usize = 8;
#[cfg(all(feature = "os", feature = "bluer", target_os = "linux"))]
pub mod bluer;
#[cfg(all(feature = "zbus", target_os = "linux"))]
pub mod bluez;
pub const MATTER_BLE_SERVICE_UUID16: u16 = 0xFFF6;
pub const MATTER_BLE_SERVICE_UUID: u128 = 0x0000FFF600001000800000805F9B34FB;
pub const C1_CHARACTERISTIC_UUID: u128 = 0x18EE2EF5263D4559959F4F9C429F9D11;
pub const C2_CHARACTERISTIC_UUID: u128 = 0x18EE2EF5263D4559959F4F9C429F9D12;
pub const C3_CHARACTERISTIC_UUID: u128 = 0x64630238877245F2B87D748A83218F04;
pub const C1_MAX_LEN: usize = MAX_BTP_SEGMENT_SIZE + GATT_HEADER_SIZE;
pub const C2_MAX_LEN: usize = MAX_BTP_SEGMENT_SIZE + GATT_HEADER_SIZE;
pub const C3_MAX_LEN: usize = 512;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct AdvData {
vid: u16,
pid: u16,
discriminator: u16,
additional_data: bool,
}
impl AdvData {
pub const fn new(dev_det: &BasicInfoConfig, discriminator: u16) -> Self {
Self {
vid: dev_det.vid,
pid: dev_det.pid,
discriminator,
additional_data: false,
}
}
pub const fn vid(&self) -> u16 {
self.vid
}
pub const fn pid(&self) -> u16 {
self.pid
}
pub const fn discriminator(&self) -> u16 {
self.discriminator
}
pub const fn additional_data(&self) -> bool {
self.additional_data
}
pub fn iter(&self) -> impl Iterator<Item = u8> + '_ {
self.flags_iter().chain(self.service_iter())
}
pub fn flags_iter(&self) -> impl Iterator<Item = u8> + '_ {
empty()
.chain(once(self.flags_payload_iter().count() as u8 + 1)) .chain(once(self.flags_adv_type()))
.chain(self.flags_payload_iter())
}
pub const fn flags_adv_type(&self) -> u8 {
0x01
}
pub fn flags_payload_iter(&self) -> impl Iterator<Item = u8> + '_ {
once(0x06)
}
pub fn service_iter(&self) -> impl Iterator<Item = u8> + '_ {
empty()
.chain(once(self.service_payload_iter().count() as u8 + 3)) .chain(once(self.service_adv_type()))
.chain(MATTER_BLE_SERVICE_UUID16.to_le_bytes())
.chain(self.service_payload_iter())
}
pub const fn service_adv_type(&self) -> u8 {
0x16
}
pub fn service_payload_iter(&self) -> impl Iterator<Item = u8> + '_ {
[
0, self.discriminator.to_le_bytes()[0],
self.discriminator.to_le_bytes()[1],
self.vid.to_le_bytes()[0],
self.vid.to_le_bytes()[1],
self.pid.to_le_bytes()[0],
self.pid.to_le_bytes()[1],
self.additional_data as u8, ]
.into_iter()
}
pub fn parse_adv(adv: &[u8]) -> Option<Self> {
matter_service_data(adv).and_then(Self::parse_service_data)
}
pub fn parse_service_data(payload: &[u8]) -> Option<Self> {
if payload.len() < MATTER_SERVICE_DATA_PAYLOAD_LEN {
return None;
}
if payload[0] != MATTER_ADV_OPCODE_COMMISSIONABLE {
return None;
}
let discriminator = u16::from_le_bytes([payload[1], payload[2]]) & 0x0FFF;
let vid = u16::from_le_bytes([payload[3], payload[4]]);
let pid = u16::from_le_bytes([payload[5], payload[6]]);
let additional_data = payload[7] & 0x01 != 0;
Some(Self {
vid,
pid,
discriminator,
additional_data,
})
}
pub fn matches(&self, filter: &CommissionableFilter) -> bool {
if let Some(want) = filter.discriminator {
if self.discriminator != want {
return false;
}
}
if let Some(want) = filter.short_discriminator {
if (self.discriminator >> 8) as u8 != want {
return false;
}
}
if let Some(want) = filter.vendor_id {
if self.vid != want {
return false;
}
}
if let Some(want) = filter.product_id {
if self.pid != want {
return false;
}
}
if filter.device_type.is_some() {
return false;
}
true
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct RecoveryAdvData {
recovery_id: [u8; RECOVERY_ID_LEN],
additional_data: bool,
}
impl RecoveryAdvData {
pub const fn new(recovery_id: [u8; RECOVERY_ID_LEN]) -> Self {
Self {
recovery_id,
additional_data: false,
}
}
pub const fn recovery_id(&self) -> [u8; RECOVERY_ID_LEN] {
self.recovery_id
}
pub const fn additional_data(&self) -> bool {
self.additional_data
}
pub fn iter(&self) -> impl Iterator<Item = u8> + '_ {
self.flags_iter().chain(self.service_iter())
}
pub fn flags_iter(&self) -> impl Iterator<Item = u8> + '_ {
empty()
.chain(once(self.flags_payload_iter().count() as u8 + 1)) .chain(once(self.flags_adv_type()))
.chain(self.flags_payload_iter())
}
pub const fn flags_adv_type(&self) -> u8 {
0x01
}
pub fn flags_payload_iter(&self) -> impl Iterator<Item = u8> + '_ {
once(0x05)
}
pub fn service_iter(&self) -> impl Iterator<Item = u8> + '_ {
empty()
.chain(once(self.service_payload_iter().count() as u8 + 3)) .chain(once(self.service_adv_type()))
.chain(MATTER_BLE_SERVICE_UUID16.to_le_bytes())
.chain(self.service_payload_iter())
}
pub const fn service_adv_type(&self) -> u8 {
0x16
}
pub fn service_payload_iter(&self) -> impl Iterator<Item = u8> + '_ {
empty()
.chain(once(MATTER_ADV_OPCODE_NETWORK_RECOVERY))
.chain(once(0x00)) .chain(self.recovery_id) .chain(once(self.additional_data as u8)) }
pub fn parse_adv(adv: &[u8]) -> Option<Self> {
matter_service_data(adv).and_then(Self::parse_service_data)
}
pub fn parse_service_data(payload: &[u8]) -> Option<Self> {
if payload.len() < MATTER_RECOVERY_SERVICE_DATA_PAYLOAD_LEN {
return None;
}
if payload[0] != MATTER_ADV_OPCODE_NETWORK_RECOVERY {
return None;
}
let recovery_id: [u8; RECOVERY_ID_LEN] =
payload[2..2 + RECOVERY_ID_LEN].try_into().unwrap();
let additional_data = payload[2 + RECOVERY_ID_LEN] & 0x01 != 0;
Some(Self {
recovery_id,
additional_data,
})
}
pub fn matches(&self, recovery_id: &[u8; RECOVERY_ID_LEN]) -> bool {
self.recovery_id == *recovery_id
}
}
fn matter_service_data(adv: &[u8]) -> Option<&[u8]> {
for (ad_type, ad_payload) in AdStructures::new(adv) {
if ad_type != AD_TYPE_SERVICE_DATA_UUID16 {
continue;
}
let Some((uuid16, service_data)) = ad_payload.split_first_chunk::<2>() else {
continue;
};
if u16::from_le_bytes(*uuid16) == MATTER_BLE_SERVICE_UUID16 {
return Some(service_data);
}
}
None
}
struct AdStructures<'a> {
rem: &'a [u8],
}
impl<'a> AdStructures<'a> {
const fn new(adv: &'a [u8]) -> Self {
Self { rem: adv }
}
}
impl<'a> Iterator for AdStructures<'a> {
type Item = (u8, &'a [u8]);
fn next(&mut self) -> Option<Self::Item> {
let (&len, rest) = self.rem.split_first()?;
let len = len as usize;
if len == 0 || len > rest.len() {
self.rem = &[];
return None;
}
let (structure, rest) = rest.split_at(len);
self.rem = rest;
let (&ad_type, payload) = structure.split_first()?;
Some((ad_type, payload))
}
}
#[cfg(test)]
mod test {
use crate::transport::network::mdns::CommissionableFilter;
use super::{AdvData, RecoveryAdvData};
fn sample() -> AdvData {
AdvData {
vid: 0xFFF1,
pid: 0x8000,
discriminator: 0xF00,
additional_data: false,
}
}
fn sample_parsed() -> AdvData {
let payload: heapless::Vec<u8, 16> = sample().service_payload_iter().collect();
AdvData::parse_service_data(&payload).unwrap()
}
#[test]
fn parse_service_data_round_trips_adv_data() {
let adv = sample();
let parsed = sample_parsed();
assert_eq!(parsed, adv);
assert_eq!(parsed.discriminator(), adv.discriminator());
assert_eq!(parsed.vid(), adv.vid());
assert_eq!(parsed.pid(), adv.pid());
assert!(!parsed.additional_data());
}
#[test]
fn parse_service_data_round_trips_additional_data_flag() {
let adv = AdvData {
additional_data: true,
..sample()
};
let payload: heapless::Vec<u8, 16> = adv.service_payload_iter().collect();
let parsed = AdvData::parse_service_data(&payload).unwrap();
assert!(parsed.additional_data());
assert_eq!(parsed, adv);
}
#[test]
fn parse_full_adv_round_trips_adv_data() {
let adv = sample();
let blob: heapless::Vec<u8, 32> = adv.iter().collect();
let parsed = AdvData::parse_adv(&blob).unwrap();
assert_eq!(parsed, adv);
}
#[test]
fn parse_adv_ignores_unrelated_records() {
let adv = sample();
let mut blob: heapless::Vec<u8, 64> = heapless::Vec::new();
blob.extend([0x02, 0x01, 0x06]);
blob.extend([0x06, 0x16, 0x34, 0x12, 0xAA, 0xBB, 0xCC]);
blob.extend(adv.service_iter());
let parsed = AdvData::parse_adv(&blob).unwrap();
assert_eq!(parsed.discriminator(), adv.discriminator());
}
#[test]
fn parse_adv_skips_a_malformed_service_data_record() {
let adv = sample();
let mut blob: heapless::Vec<u8, 64> = heapless::Vec::new();
blob.extend([0x02, 0x16, 0x34]);
blob.extend(adv.service_iter());
let parsed = unwrap!(AdvData::parse_adv(&blob), "Failed to parse");
assert_eq!(parsed.discriminator(), adv.discriminator());
}
#[test]
fn parse_service_data_rejects_non_commissionable_opcode() {
let mut payload: heapless::Vec<u8, 16> = sample().service_payload_iter().collect();
payload[0] = 0x01;
assert!(AdvData::parse_service_data(&payload).is_none());
}
#[test]
fn parse_service_data_rejects_short_payload() {
assert!(AdvData::parse_service_data(&[0, 1, 2, 3]).is_none());
}
#[test]
fn parse_adv_returns_none_without_matter_service_data() {
assert!(AdvData::parse_adv(&[0x02, 0x01, 0x06]).is_none());
}
#[test]
fn parse_adv_tolerates_truncated_trailing_record() {
let adv = sample();
let mut blob: heapless::Vec<u8, 64> = adv.iter().collect();
blob.extend([0x05, 0x16, 0x00]);
assert!(AdvData::parse_adv(&blob).is_some());
}
const SPEC_RECOVERY_ID: [u8; 8] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
const SPEC_RECOVERY_ADV: [u8; 18] = [
0x02, 0x01, 0x05, 0x0E, 0x16, 0xF6, 0xFF, 0x01, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x00, ];
#[test]
fn recovery_serialize_matches_spec_example() {
let adv = RecoveryAdvData::new(SPEC_RECOVERY_ID);
let blob: heapless::Vec<u8, 32> = adv.iter().collect();
assert_eq!(&blob[..], &SPEC_RECOVERY_ADV[..]);
}
#[test]
fn recovery_parse_service_data_round_trips() {
let adv = RecoveryAdvData::new(SPEC_RECOVERY_ID);
let payload: heapless::Vec<u8, 16> = adv.service_payload_iter().collect();
let parsed = RecoveryAdvData::parse_service_data(&payload).unwrap();
assert_eq!(parsed, adv);
assert_eq!(parsed.recovery_id(), SPEC_RECOVERY_ID);
assert!(!parsed.additional_data());
}
#[test]
fn recovery_parse_full_spec_adv() {
let parsed = RecoveryAdvData::parse_adv(&SPEC_RECOVERY_ADV).unwrap();
assert_eq!(parsed.recovery_id(), SPEC_RECOVERY_ID);
assert!(!parsed.additional_data());
}
#[test]
fn recovery_additional_data_flag_round_trips() {
let adv = RecoveryAdvData {
additional_data: true,
..RecoveryAdvData::new(SPEC_RECOVERY_ID)
};
let payload: heapless::Vec<u8, 16> = adv.service_payload_iter().collect();
let parsed = RecoveryAdvData::parse_service_data(&payload).unwrap();
assert!(parsed.additional_data());
assert_eq!(parsed, adv);
}
#[test]
fn recovery_matches_only_its_own_id() {
let adv = RecoveryAdvData::new(SPEC_RECOVERY_ID);
assert!(adv.matches(&SPEC_RECOVERY_ID));
let mut other = SPEC_RECOVERY_ID;
other[0] ^= 0xFF;
assert!(!adv.matches(&other));
}
#[test]
fn recovery_parse_rejects_short_payload() {
assert!(RecoveryAdvData::parse_service_data(&[1, 0, 1, 2, 3, 4, 5, 6, 7, 8]).is_none());
}
#[test]
fn recovery_and_commissionable_payloads_are_disjoint() {
let comm: heapless::Vec<u8, 16> = sample().service_payload_iter().collect();
assert!(RecoveryAdvData::parse_service_data(&comm).is_none());
let recovery: heapless::Vec<u8, 16> = RecoveryAdvData::new(SPEC_RECOVERY_ID)
.service_payload_iter()
.collect();
assert!(AdvData::parse_service_data(&recovery).is_none());
}
#[test]
fn recovery_parse_adv_returns_none_without_matter_service_data() {
assert!(RecoveryAdvData::parse_adv(&[0x02, 0x01, 0x05]).is_none());
}
#[test]
fn matches_filter_by_full_and_short_discriminator() {
let parsed = sample_parsed();
let mut filter = CommissionableFilter {
discriminator: Some(0xF00),
..Default::default()
};
assert!(parsed.matches(&filter));
filter.discriminator = Some(0xF01);
assert!(!parsed.matches(&filter));
let filter = CommissionableFilter {
short_discriminator: Some(0xF),
..Default::default()
};
assert!(parsed.matches(&filter));
let filter = CommissionableFilter {
short_discriminator: Some(0x0),
..Default::default()
};
assert!(!parsed.matches(&filter));
}
#[test]
fn matches_filter_by_vid_pid() {
let parsed = sample_parsed();
let filter = CommissionableFilter {
vendor_id: Some(0xFFF1),
product_id: Some(0x8000),
..Default::default()
};
assert!(parsed.matches(&filter));
let filter = CommissionableFilter {
vendor_id: Some(0x1234),
..Default::default()
};
assert!(!parsed.matches(&filter));
}
#[test]
fn empty_filter_matches_any_commissionable_adv() {
assert!(sample_parsed().matches(&CommissionableFilter::default()));
}
#[test]
fn device_type_filter_never_matches_ble() {
let filter = CommissionableFilter {
device_type: Some(0x0100),
..Default::default()
};
assert!(!sample_parsed().matches(&filter));
}
}