1use crate::mdns::{self, DnsMessage};
7use anyhow::{Context, Result};
8use byteorder::ReadBytesExt;
9use std::{
10 collections::{BTreeMap, HashMap},
11 io::{Cursor, Read},
12 net::{IpAddr, Ipv4Addr, Ipv6Addr},
13 time::Duration,
14};
15use tokio_util::bytes::Buf;
16
17#[derive(Debug)]
18pub enum CommissioningMode {
19 No,
20 Yes,
21 WithPasscode,
22}
23
24#[derive(Debug)]
25pub struct MatterDeviceInfo {
26 pub service: String,
27 pub device: String,
28 pub ips: Vec<IpAddr>,
29 pub name: Option<String>,
30 pub vendor_id: Option<String>,
31 pub product_id: Option<String>,
32 pub discriminator: Option<String>,
33 pub commissioning_mode: Option<CommissioningMode>,
34 pub pairing_hint: Option<String>,
35 pub source_ip: String,
36 pub port: Option<u16>,
37}
38
39fn parse_txt_records(data: &[u8]) -> Result<HashMap<String, String>> {
40 let mut cursor = Cursor::new(data);
41 let mut out = HashMap::new();
42 while cursor.remaining() > 0 {
43 let len = cursor.read_u8()?;
44 let mut buf = vec![0; len as usize];
45 cursor.read_exact(buf.as_mut_slice())?;
46 let splitstr = std::str::from_utf8(&buf)?.split("=");
47 let x: Vec<&str> = splitstr.collect();
48 if x.len() == 2 {
49 out.insert(x[0].to_owned(), x[1].to_owned());
50 }
51 }
52 Ok(out)
53}
54
55fn remove_string_suffix(string: &str, suffix: &str) -> String {
56 if let Some(s) = string.strip_suffix(suffix) {
57 s.to_owned()
58 } else {
59 string.to_owned()
60 }
61}
62
63fn to_matter_info(msg: &DnsMessage, svc: &str) -> Result<MatterDeviceInfo> {
64 let mut device = None;
65 let mut service = None;
66 let mut ips = BTreeMap::new();
67 let mut name = None;
68 let mut discriminator = None;
69 let mut cm = None;
70 let mut pairing_hint = None;
71 let mut vendor_id = None;
72 let mut product_id = None;
73 let mut port: Option<u16> = None;
74
75 let mut matter_service = false;
76 let svcname = ".".to_owned() + svc + ".";
77 for answer in &msg.answers {
78 if answer.name == svcname[1..] {
79 matter_service = true
80 }
81 }
82 for additional in &msg.additional {
83 if additional.typ == mdns::TYPE_A {
84 let arr: [u8; 4] = match additional.rdata.clone().try_into() {
85 Ok(v) => v,
86 Err(_e) => return Err(anyhow::anyhow!("A record is not correct")),
87 };
88 let val = IpAddr::V4(Ipv4Addr::from_bits(u32::from_be_bytes(arr)));
89 ips.insert(val, true);
90 device = Some(remove_string_suffix(&additional.name, ".local."));
91 }
92 if additional.typ == mdns::TYPE_AAAA {
93 let arr: [u8; 16] = match additional.rdata.clone().try_into() {
94 Ok(v) => v,
95 Err(_e) => return Err(anyhow::anyhow!("AAAA record is not correct")),
96 };
97 let val = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes(arr)));
98 ips.insert(val, true);
99 device = Some(remove_string_suffix(&additional.name, ".local."));
100 }
101 if additional.typ == mdns::TYPE_SRV {
102 service = Some(remove_string_suffix(&additional.name, &svcname));
103 if additional.rdata.len() >= 6 {
104 port = Some(((additional.rdata[4] as u16) << 8) | (additional.rdata[5] as u16))
105 }
106 }
107 if additional.typ == mdns::TYPE_TXT {
108 let rec = parse_txt_records(&additional.rdata)?;
109 name = rec.get("DN").cloned();
110 discriminator = rec.get("D").cloned();
111 pairing_hint = rec.get("PH").cloned();
112 if let Some(vp) = rec.get("VP") {
113 let mut split = vp.split("+");
114 vendor_id = split.next().map(str::to_owned);
115 product_id = split.next().map(str::to_owned);
116 }
117 cm = match rec.get("CM") {
118 Some(v) => match v.as_str() {
119 "0" => Some(CommissioningMode::No),
120 "1" => Some(CommissioningMode::Yes),
121 "2" => Some(CommissioningMode::WithPasscode),
122 _ => None,
123 },
124 None => None,
125 };
126 }
127 }
128
129 if !matter_service {
130 return Err(anyhow::anyhow!("not matter service"));
131 }
132
133 Ok(MatterDeviceInfo {
134 service: service.context("service name not detected")?,
135 device: device.context("device name not detected")?,
136 ips: ips.into_keys().collect(),
137 name,
138 discriminator,
139 commissioning_mode: cm,
140 pairing_hint,
141 source_ip: msg.source.to_string(),
142 vendor_id,
143 product_id,
144 port,
145 })
146}
147
148async fn discover_common(timeout: Duration, svc_type: &str) -> Result<Vec<MatterDeviceInfo>> {
149 let stop = tokio_util::sync::CancellationToken::new();
150 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<DnsMessage>();
151
152 mdns::discover(svc_type, mdns::QTYPE_ANY, sender, stop.child_token()).await?;
153
154 tokio::spawn(async move {
155 tokio::time::sleep(timeout).await;
156 stop.cancel();
157 });
158 let mut cache = HashMap::new();
159 let mut out = Vec::new();
160 while let Some(dns) = receiver.recv().await {
161 if cache.contains_key(&dns) {
162 continue;
163 }
164 let info = match to_matter_info(&dns, svc_type) {
165 Ok(info) => info,
166 Err(_) => continue,
167 };
168 out.push(info);
169 cache.insert(dns, true);
170 }
171 Ok(out)
172}
173
174pub async fn discover_commissionable(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
176 discover_common(timeout, "_matterc._udp.local").await
177}
178
179pub async fn discover_commissioned(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
181 discover_common(timeout, "_matter._tcp.local").await
182}