1use core::fmt::Write;
19use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
20
21use domain::base::name::{Label, ToLabelIter};
22
23use crate::dm::clusters::basic_info::BasicInfoConfig;
24use crate::dm::clusters::icd_mgmt::OperatingModeEnum;
25use crate::error::{Error, ErrorCode};
26use crate::tlv::EitherIter;
27use crate::utils::storage::{write_split, Vec, WriteBuf};
28use crate::Matter;
29
30use super::{MatterLocalService, MatterRemoteService};
31
32#[cfg(feature = "astro-dnssd")]
33pub mod astro;
34#[cfg(feature = "zbus")]
35pub mod avahi;
36pub mod builtin;
37#[cfg(feature = "zbus")]
38pub mod resolve;
39#[cfg(feature = "zeroconf")]
40pub mod zeroconf;
41
42pub const MDNS_IPV6_BROADCAST_ADDR: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x00fb);
44
45pub const MDNS_IPV4_BROADCAST_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
47
48pub const MDNS_PORT: u16 = 5353;
50
51pub const MDNS_SOCKET_DEFAULT_BIND_ADDR: SocketAddr =
53 SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, MDNS_PORT, 0, 0));
54
55impl MatterLocalService {
56 #[allow(clippy::type_complexity)]
63 pub fn service<'a>(
64 &self,
65 matter: &Matter<'_>,
66 buf: &'a mut [u8],
67 ) -> Result<
68 (
69 MdnsLocalService<
70 'a,
71 impl Iterator<Item = &'a str> + Clone,
72 impl Iterator<Item = (&'a str, &'a str)> + Clone,
73 >,
74 &'a mut [u8],
75 ),
76 Error,
77 > {
78 self.service_internal(matter.dev_det(), matter.port(), matter.icd_mode(), buf)
79 }
80
81 #[allow(clippy::type_complexity)]
84 fn service_internal<'a>(
85 &self,
86 dev_det: &BasicInfoConfig<'_>,
87 matter_port: u16,
88 icd_mode: Option<OperatingModeEnum>,
89 buf: &'a mut [u8],
90 ) -> Result<
91 (
92 MdnsLocalService<
93 'a,
94 impl Iterator<Item = &'a str> + Clone,
95 impl Iterator<Item = (&'a str, &'a str)> + Clone,
96 >,
97 &'a mut [u8],
98 ),
99 Error,
100 > {
101 match self {
102 Self::Commissioned {
103 compressed_fabric_id,
104 node_id,
105 } => {
106 let mut wb = WriteBuf::new(buf);
107
108 let (name, mut wb) =
109 write_split!(wb, "{:016X}-{:016X}", compressed_fabric_id, node_id)?;
110
111 let (subtype_i, mut wb) = write_split!(wb, "_I{:016X}", compressed_fabric_id)?;
116 let (txt_sai, mut wb) = if let Some(sai) = dev_det.sai {
117 write_split!(wb, "{}", sai)?
118 } else {
119 ("", wb)
120 };
121 let (txt_sii, wb) = if let Some(sii) = dev_det.sii {
122 write_split!(wb, "{}", sii)?
123 } else {
124 ("", wb)
125 };
126
127 let txt_icd = match icd_mode {
131 Some(OperatingModeEnum::LIT) => "1",
132 Some(OperatingModeEnum::SIT) => "0",
133 None => "",
134 };
135
136 let txt_kvs = [
139 ("SAI", txt_sai),
140 ("SII", txt_sii),
141 ("T", if dev_det.tcp_supported { "6" } else { "" }),
142 ("ICD", txt_icd),
143 ("DUMMY", "DUMMY"),
145 ]
146 .into_iter()
147 .filter(|(_, v)| !v.is_empty());
148
149 Ok((
150 MdnsLocalService {
151 name,
152 service: "_matter",
153 protocol: "_tcp",
154 service_protocol: "_matter._tcp",
155 port: matter_port,
156 service_subtypes: EitherIter::First(core::iter::once(subtype_i)),
157 txt_kvs: EitherIter::First(txt_kvs),
158 },
159 wb.into_buf(),
160 ))
161 }
162 Self::Commissionable {
163 id,
164 discriminator,
165 enhanced,
166 } => {
167 let mut wb = WriteBuf::new(buf);
168
169 let (name, mut wb) = write_split!(wb, "{:016X}", id)?;
170
171 let (subtype_discr, mut wb) = write_split!(wb, "_L{}", *discriminator)?;
172 let (subtype_short_discr, mut wb) = write_split!(
173 wb,
174 "_S{}",
175 Self::compute_short_discriminator(*discriminator)
176 )?;
177 let (subtype_v, mut wb) = write_split!(wb, "_V{}", dev_det.vid)?;
178 let (subtype_t, mut wb) = if let Some(dt) = dev_det.device_type {
179 write_split!(wb, "_T{}", dt)?
180 } else {
181 ("", wb)
182 };
183
184 let service_subtypes = [
185 subtype_discr,
186 subtype_short_discr,
187 subtype_v,
188 subtype_t,
189 "_CM",
190 ]
191 .into_iter()
192 .filter(|s| !s.is_empty());
193
194 let (txt_discr, mut wb) = write_split!(wb, "{}", *discriminator)?;
195 let (txt_vid_pid, mut wb) = write_split!(wb, "{}+{}", dev_det.vid, dev_det.pid)?;
196 let (txt_sai, mut wb) = if let Some(sai) = dev_det.sai {
197 write_split!(wb, "{}", sai)?
198 } else {
199 ("", wb)
200 };
201 let (txt_sii, mut wb) = if let Some(sii) = dev_det.sii {
202 write_split!(wb, "{}", sii)?
203 } else {
204 ("", wb)
205 };
206 let (txt_dn, mut wb) = write_split!(wb, "{}", dev_det.device_name)?;
207 let (txt_pi, mut wb) = write_split!(wb, "{}", dev_det.pairing_instruction)?;
208 let (txt_ph, mut wb) = write_split!(wb, "{}", dev_det.pairing_hint.bits())?;
209 let (txt_dt, mut wb) = if let Some(dt) = dev_det.device_type {
210 write_split!(wb, "{}", dt)?
211 } else {
212 ("", wb)
213 };
214 let (txt_tcp, wb) = if dev_det.tcp_supported {
215 write_split!(wb, "6")?
216 } else {
217 ("", wb)
218 };
219
220 let txt_icd = match icd_mode {
224 Some(OperatingModeEnum::LIT) => "1",
225 Some(OperatingModeEnum::SIT) => "0",
226 None => "",
227 };
228
229 let txt_kvs = [
230 ("D", txt_discr),
231 ("CM", if *enhanced { "2" } else { "1" }),
232 ("VP", txt_vid_pid),
233 ("SAI", txt_sai),
234 ("SII", txt_sii),
235 ("DN", txt_dn),
236 ("PI", txt_pi),
237 ("PH", txt_ph),
238 ("DT", txt_dt),
239 ("T", txt_tcp),
240 ("ICD", txt_icd),
241 ]
242 .into_iter()
243 .filter(|(_, v)| !v.is_empty());
244
245 Ok((
246 MdnsLocalService {
247 name,
248 service: "_matterc",
249 protocol: "_udp",
250 service_protocol: "_matterc._udp",
251 port: matter_port,
252 service_subtypes: EitherIter::Second(service_subtypes),
253 txt_kvs: EitherIter::Second(txt_kvs),
254 },
255 wb.into_buf(),
256 ))
257 }
258 }
259 }
260
261 fn compute_short_discriminator(discriminator: u16) -> u16 {
262 const SHORT_DISCRIMINATOR_MASK: u16 = 0xF00;
263 const SHORT_DISCRIMINATOR_SHIFT: u16 = 8;
264
265 (discriminator & SHORT_DISCRIMINATOR_MASK) >> SHORT_DISCRIMINATOR_SHIFT
266 }
267}
268
269impl MatterRemoteService {
270 pub fn service_type(&self) -> &'static str {
275 match self {
276 Self::Operational { .. } => "_matter._tcp",
277 Self::Commissionable { .. } => "_matterc._udp",
278 }
279 }
280
281 pub fn instance_name(&self, buf: &mut heapless::String<128>) {
285 buf.clear();
286
287 match self {
288 Self::Operational {
289 compressed_fabric_id,
290 node_id,
291 } => {
292 write_unwrap!(
293 buf,
294 "{:016X}-{:016X}._matter._tcp.local",
295 compressed_fabric_id,
296 node_id
297 );
298 }
299 Self::Commissionable { id } => {
300 write_unwrap!(buf, "{:016X}._matterc._udp.local", id);
301 }
302 }
303 }
304
305 fn suffix_labels(&self) -> &'static [&'static str] {
309 match self {
310 Self::Operational { .. } => &["_matter", "_tcp", "local"],
311 Self::Commissionable { .. } => &["_matterc", "_udp", "local"],
312 }
313 }
314
315 pub(crate) fn query_name_labels<'b>(&self, buf: &'b mut heapless::String<33>) -> [&'b str; 4] {
320 buf.clear();
321
322 match self {
323 Self::Operational {
324 compressed_fabric_id,
325 node_id,
326 } => {
327 write_unwrap!(buf, "{:016X}-{:016X}", compressed_fabric_id, node_id);
328 [buf.as_str(), "_matter", "_tcp", "local"]
329 }
330 Self::Commissionable { id } => {
331 write_unwrap!(buf, "{:016X}", id);
332 [buf.as_str(), "_matterc", "_udp", "local"]
333 }
334 }
335 }
336
337 pub fn matches_instance<I: ToLabelIter>(&self, instance: &I) -> bool {
343 let mut labels = instance.iter_labels().filter(|l| !l.is_empty());
345
346 let Some(first) = labels.next() else {
347 return false;
348 };
349
350 let mut suffix = self.suffix_labels().iter();
352 for label in labels {
353 match suffix.next() {
354 Some(expected) if label.as_slice().eq_ignore_ascii_case(expected.as_bytes()) => {}
355 _ => return false,
356 }
357 }
358 if suffix.next().is_some() {
359 return false;
360 }
361
362 let Ok(first) = core::str::from_utf8(first.as_slice()) else {
364 return false;
365 };
366
367 match self {
368 Self::Operational {
369 compressed_fabric_id,
370 node_id,
371 } => {
372 let Some((fabric, node)) = first.split_once('-') else {
373 return false;
374 };
375
376 parse_hex_u64(fabric) == Some(*compressed_fabric_id)
377 && parse_hex_u64(node) == Some(*node_id)
378 }
379 Self::Commissionable { id } => parse_hex_u64(first) == Some(*id),
380 }
381 }
382}
383
384pub struct MdnsLocalService<'a, S, T>
388where
389 S: Iterator<Item = &'a str> + Clone,
390 T: Iterator<Item = (&'a str, &'a str)> + Clone,
391{
392 pub name: &'a str,
394 pub service: &'a str,
396 pub protocol: &'a str,
398 pub service_protocol: &'a str,
400 pub port: u16,
402 pub service_subtypes: S,
404 pub txt_kvs: T,
406}
407
408#[derive(Debug, Clone, Copy)]
423pub struct MdnsRemoteService<I, A, T> {
424 pub instance_name: I,
428 pub port: Option<u16>,
430 pub addrs: A,
432 pub txt: T,
434 pub scope_id: u32,
440}
441
442impl<'a, I, A, T> MdnsRemoteService<I, A, T>
443where
444 T: Iterator<Item = (&'a str, &'a str)> + Clone,
445{
446 pub fn session_params(&self) -> (Option<u32>, Option<u32>, Option<u16>) {
450 let (mut sii, mut sai, mut sat) = (None, None, None);
451
452 for (key, value) in self.txt.clone() {
453 if key.eq_ignore_ascii_case("SII") {
454 sii = value.parse().ok();
455 } else if key.eq_ignore_ascii_case("SAI") {
456 sai = value.parse().ok();
457 } else if key.eq_ignore_ascii_case("SAT") {
458 sat = value.parse().ok();
459 }
460 }
461
462 (sii, sai, sat)
463 }
464}
465
466#[derive(Debug, Clone, Default, Eq, PartialEq)]
475#[cfg_attr(feature = "defmt", derive(defmt::Format))]
476pub struct CommissionableFilter {
477 pub discriminator: Option<u16>,
479 pub short_discriminator: Option<u8>,
481 pub vendor_id: Option<u16>,
483 pub product_id: Option<u16>,
485 pub device_type: Option<u32>,
487 pub commissioning_mode_only: bool,
489}
490
491impl CommissionableFilter {
492 pub fn service_type(&self, buf: &mut heapless::String<64>, include_local: bool) {
512 buf.clear();
513 let suffix = if include_local { ".local" } else { "" };
514
515 let mut sbuf = heapless::String::<24>::new();
516 if let Some(sub) = self.subtype(&mut sbuf) {
517 write_unwrap!(buf, "{}._sub._matterc._udp{}", sub, suffix);
518 } else {
519 write_unwrap!(buf, "_matterc._udp{}", suffix);
520 }
521 }
522
523 pub(crate) fn subtype<'b>(&self, buf: &'b mut heapless::String<24>) -> Option<&'b str> {
529 buf.clear();
530
531 if let Some(disc) = self.discriminator {
532 write_unwrap!(buf, "_L{}", disc);
533 } else if let Some(short_disc) = self.short_discriminator {
534 write_unwrap!(buf, "_S{}", short_disc);
535 } else if let Some(vid) = self.vendor_id {
536 write_unwrap!(buf, "_V{}", vid);
537 } else if let Some(dt) = self.device_type {
538 write_unwrap!(buf, "_T{}", dt);
539 } else if self.commissioning_mode_only {
540 write_unwrap!(buf, "_CM");
541 } else {
542 return None;
543 }
544
545 Some(buf.as_str())
546 }
547
548 pub fn matches<'a, I, A, T>(&self, service: &MdnsRemoteService<I, A, T>) -> bool
551 where
552 T: Iterator<Item = (&'a str, &'a str)> + Clone,
553 {
554 self.matches_txt(service.txt.clone())
555 }
556
557 fn matches_txt<'a, I>(&self, txt: I) -> bool
566 where
567 I: IntoIterator<Item = (&'a str, &'a str)>,
568 {
569 let mut discriminator: Option<u16> = None;
570 let mut vendor_id: Option<u16> = None;
571 let mut product_id: Option<u16> = None;
572 let mut device_type: Option<u32> = None;
573 let mut commissioning = CommissioningMode::Disabled;
574
575 for (key, value) in txt {
576 if key.eq_ignore_ascii_case("D") {
577 discriminator = value.parse::<u16>().ok().filter(|d| *d <= 0xFFF);
578 } else if key.eq_ignore_ascii_case("VP") {
579 if let Some(plus) = value.find('+') {
580 vendor_id = value[..plus].parse::<u16>().ok();
581 product_id = value[plus + 1..].parse::<u16>().ok();
582 } else {
583 vendor_id = value.parse::<u16>().ok();
584 }
585 } else if key.eq_ignore_ascii_case("CM") {
586 commissioning = CommissioningMode::from_txt_value(value);
587 } else if key.eq_ignore_ascii_case("DT") {
588 device_type = value.parse::<u32>().ok();
589 }
590 }
591
592 if let Some(want) = self.discriminator {
593 if discriminator != Some(want) {
594 return false;
595 }
596 }
597
598 if let Some(want) = self.short_discriminator {
599 if discriminator.map(|d| (d >> 8) as u8) != Some(want) {
601 return false;
602 }
603 }
604
605 if let Some(want) = self.vendor_id {
606 if vendor_id != Some(want) {
607 return false;
608 }
609 }
610
611 if let Some(want) = self.product_id {
612 if product_id != Some(want) {
613 return false;
614 }
615 }
616
617 if let Some(want) = self.device_type {
618 if device_type != Some(want) {
619 return false;
620 }
621 }
622
623 if self.commissioning_mode_only && !commissioning.is_commissionable() {
624 return false;
625 }
626
627 true
628 }
629}
630
631#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
636#[cfg_attr(feature = "defmt", derive(defmt::Format))]
637#[repr(u8)]
638enum CommissioningMode {
639 #[default]
641 Disabled = 0,
642 Basic = 1,
644 Enhanced = 2,
646}
647
648impl CommissioningMode {
649 fn from_txt_value(value: &str) -> Self {
651 match value {
652 "1" => Self::Basic,
653 "2" => Self::Enhanced,
654 _ => Self::Disabled,
655 }
656 }
657
658 fn is_commissionable(&self) -> bool {
660 !matches!(self, Self::Disabled)
661 }
662}
663
664#[derive(Debug, Clone, Copy)]
671pub struct DottedName<'a>(pub &'a str);
672
673impl ToLabelIter for DottedName<'_> {
674 type LabelIter<'t>
675 = core::iter::Map<core::str::Split<'t, char>, fn(&str) -> &Label>
676 where
677 Self: 't;
678
679 fn iter_labels(&self) -> Self::LabelIter<'_> {
680 fn str_to_label(s: &str) -> &Label {
683 Label::from_slice(s.as_bytes()).unwrap_or_else(|_| Label::root())
684 }
685
686 self.0
687 .trim_end_matches('.')
688 .split('.')
689 .map(str_to_label as fn(&str) -> &Label)
690 }
691}
692
693#[derive(Debug, Clone, Copy)]
700pub struct ResolvedNode {
701 pub addr: SocketAddr,
703 pub sii: Option<u32>,
705 pub sai: Option<u32>,
707 pub sat: Option<u16>,
709}
710
711#[derive(Debug, Clone)]
718pub(crate) enum MdnsResolveState {
719 Idle,
721 Requested { service: MatterRemoteService },
723 InFlight { service: MatterRemoteService },
725 Resolved {
730 ip: IpAddr,
731 port: u16,
732 scope_id: u32,
735 sii: Option<u32>,
736 sai: Option<u32>,
737 sat: Option<u16>,
738 },
739}
740
741pub(crate) const MAX_BROWSE_EXCLUDE: usize = 6;
746
747pub(crate) type BrowseExclude = Vec<u64, MAX_BROWSE_EXCLUDE>;
749
750#[derive(Debug, Clone)]
760pub(crate) enum MdnsBrowseState {
761 Idle,
763 Requested {
765 filter: CommissionableFilter,
766 exclude: BrowseExclude,
767 },
768 InFlight {
770 filter: CommissionableFilter,
771 exclude: BrowseExclude,
772 },
773 Found {
775 ip: IpAddr,
776 port: u16,
777 scope_id: u32,
780 id: u64,
781 },
782}
783
784pub fn score_ip_address(addr: &IpAddr) -> u8 {
797 match addr {
798 IpAddr::V6(ipv6) => {
799 if ipv6.is_unicast_link_local() {
800 100
802 } else if ipv6.is_unique_local() {
803 80
805 } else if is_ipv6_global_unicast(ipv6) {
806 60
808 } else {
809 40
811 }
812 }
813 IpAddr::V4(_) => {
814 20
816 }
817 }
818}
819
820fn is_ipv6_global_unicast(addr: &Ipv6Addr) -> bool {
822 let segments = addr.segments();
823 (segments[0] & 0xe000) == 0x2000
824}
825
826pub(crate) fn commissionable_instance_id<I: ToLabelIter>(instance: &I) -> Option<u64> {
830 let first = instance.iter_labels().find(|l| !l.is_empty())?;
831 parse_hex_u64(core::str::from_utf8(first.as_slice()).ok()?)
832}
833
834fn parse_hex_u64(s: &str) -> Option<u64> {
837 if s.bytes().all(|b| b.is_ascii_hexdigit()) {
840 u64::from_str_radix(s, 16).ok()
841 } else {
842 None
843 }
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849
850 #[test]
851 fn can_compute_short_discriminator() {
852 let discriminator: u16 = 0b0000_1111_0000_0000;
853 let short = MatterLocalService::compute_short_discriminator(discriminator);
854 assert_eq!(short, 0b1111);
855
856 let discriminator: u16 = 840;
857 let short = MatterLocalService::compute_short_discriminator(discriminator);
858 assert_eq!(short, 3);
859 }
860
861 #[test]
864 fn matches_txt_empty_filter_matches_all() {
865 let filter = CommissionableFilter::default();
866 assert!(filter.matches_txt([("D", "1234"), ("VP", "65521+32768"), ("CM", "1")]));
867 assert!(filter.matches_txt(core::iter::empty::<(&str, &str)>())); }
869
870 #[test]
871 fn matches_txt_discriminator_and_short() {
872 let filter = CommissionableFilter {
873 discriminator: Some(1234),
874 ..Default::default()
875 };
876 assert!(filter.matches_txt([("D", "1234")]));
877 assert!(!filter.matches_txt([("D", "5678")]));
878
879 let filter = CommissionableFilter {
881 short_discriminator: Some(3),
882 ..Default::default()
883 };
884 assert!(filter.matches_txt([("D", "840")]));
885 assert!(!filter.matches_txt([("D", "1024")])); }
887
888 #[test]
889 fn matches_txt_vendor_product_and_combined() {
890 let filter = CommissionableFilter {
891 vendor_id: Some(0xFFF1),
892 product_id: Some(0x8000),
893 ..Default::default()
894 };
895 assert!(filter.matches_txt([("VP", "65521+32768")]));
896 assert!(!filter.matches_txt([("VP", "65521+1")])); assert!(!filter.matches_txt([("VP", "1+32768")])); let filter = CommissionableFilter {
901 discriminator: Some(1234),
902 vendor_id: Some(0xFFF1),
903 ..Default::default()
904 };
905 assert!(filter.matches_txt([("D", "1234"), ("VP", "65521+1")]));
906 assert!(!filter.matches_txt([("D", "1234"), ("VP", "1+1")]));
907 assert!(!filter.matches_txt([("D", "9999"), ("VP", "65521+1")]));
908 }
909
910 #[test]
911 fn matches_txt_device_type_and_commissioning_mode() {
912 let filter = CommissionableFilter {
913 device_type: Some(257),
914 ..Default::default()
915 };
916 assert!(filter.matches_txt([("DT", "257")]));
917 assert!(!filter.matches_txt([("DT", "256")]));
918
919 let filter = CommissionableFilter {
920 commissioning_mode_only: true,
921 ..Default::default()
922 };
923 assert!(filter.matches_txt([("CM", "1")]));
924 assert!(filter.matches_txt([("CM", "2")]));
925 assert!(!filter.matches_txt([("CM", "0")]));
926 assert!(!filter.matches_txt(core::iter::empty::<(&str, &str)>())); }
928
929 #[test]
932 fn service_type_no_filter() {
933 let filter = CommissionableFilter::default();
934 let mut buf = heapless::String::<64>::new();
935
936 filter.service_type(&mut buf, false);
937 assert_eq!(buf.as_str(), "_matterc._udp");
938
939 filter.service_type(&mut buf, true);
940 assert_eq!(buf.as_str(), "_matterc._udp.local");
941 }
942
943 #[test]
944 fn service_type_with_discriminator() {
945 let filter = CommissionableFilter {
946 discriminator: Some(1234),
947 ..Default::default()
948 };
949 let mut buf = heapless::String::<64>::new();
950
951 filter.service_type(&mut buf, false);
952 assert_eq!(buf.as_str(), "_L1234._sub._matterc._udp");
953
954 filter.service_type(&mut buf, true);
955 assert_eq!(buf.as_str(), "_L1234._sub._matterc._udp.local");
956 }
957
958 #[test]
959 fn service_type_with_short_discriminator() {
960 let filter = CommissionableFilter {
961 short_discriminator: Some(3),
962 ..Default::default()
963 };
964 let mut buf = heapless::String::<64>::new();
965
966 filter.service_type(&mut buf, false);
967 assert_eq!(buf.as_str(), "_S3._sub._matterc._udp");
968 }
969
970 #[test]
971 fn service_type_with_vendor_id() {
972 let filter = CommissionableFilter {
973 vendor_id: Some(0xFFF1),
974 ..Default::default()
975 };
976 let mut buf = heapless::String::<64>::new();
977
978 filter.service_type(&mut buf, false);
979 assert_eq!(buf.as_str(), "_V65521._sub._matterc._udp");
980 }
981
982 #[test]
983 fn service_type_with_device_type() {
984 let filter = CommissionableFilter {
985 device_type: Some(257),
986 ..Default::default()
987 };
988 let mut buf = heapless::String::<64>::new();
989
990 filter.service_type(&mut buf, false);
991 assert_eq!(buf.as_str(), "_T257._sub._matterc._udp");
992 }
993
994 #[test]
995 fn service_type_with_commissioning_mode_only() {
996 let filter = CommissionableFilter {
997 commissioning_mode_only: true,
998 ..Default::default()
999 };
1000 let mut buf = heapless::String::<64>::new();
1001
1002 filter.service_type(&mut buf, false);
1003 assert_eq!(buf.as_str(), "_CM._sub._matterc._udp");
1004 }
1005
1006 #[test]
1007 fn service_type_priority_order() {
1008 let mut buf = heapless::String::<64>::new();
1010
1011 CommissionableFilter {
1012 discriminator: Some(1234),
1013 short_discriminator: Some(3),
1014 vendor_id: Some(0xFFF1),
1015 device_type: Some(257),
1016 commissioning_mode_only: true,
1017 product_id: Some(0x8000),
1018 }
1019 .service_type(&mut buf, false);
1020 assert_eq!(buf.as_str(), "_L1234._sub._matterc._udp");
1021
1022 CommissionableFilter {
1023 short_discriminator: Some(3),
1024 vendor_id: Some(0xFFF1),
1025 ..Default::default()
1026 }
1027 .service_type(&mut buf, false);
1028 assert_eq!(buf.as_str(), "_S3._sub._matterc._udp");
1029
1030 CommissionableFilter {
1031 vendor_id: Some(0xFFF1),
1032 device_type: Some(257),
1033 ..Default::default()
1034 }
1035 .service_type(&mut buf, false);
1036 assert_eq!(buf.as_str(), "_V65521._sub._matterc._udp");
1037
1038 CommissionableFilter {
1039 device_type: Some(257),
1040 commissioning_mode_only: true,
1041 ..Default::default()
1042 }
1043 .service_type(&mut buf, false);
1044 assert_eq!(buf.as_str(), "_T257._sub._matterc._udp");
1045
1046 CommissionableFilter {
1048 product_id: Some(0x8000),
1049 ..Default::default()
1050 }
1051 .service_type(&mut buf, false);
1052 assert_eq!(buf.as_str(), "_matterc._udp");
1053 }
1054
1055 #[test]
1058 fn commissioning_mode_from_txt_value() {
1059 assert_eq!(
1060 CommissioningMode::from_txt_value("0"),
1061 CommissioningMode::Disabled
1062 );
1063 assert_eq!(
1064 CommissioningMode::from_txt_value("1"),
1065 CommissioningMode::Basic
1066 );
1067 assert_eq!(
1068 CommissioningMode::from_txt_value("2"),
1069 CommissioningMode::Enhanced
1070 );
1071 assert_eq!(
1072 CommissioningMode::from_txt_value("x"),
1073 CommissioningMode::Disabled
1074 );
1075
1076 assert!(!CommissioningMode::Disabled.is_commissionable());
1077 assert!(CommissioningMode::Basic.is_commissionable());
1078 assert!(CommissioningMode::Enhanced.is_commissionable());
1079 }
1080
1081 #[test]
1082 fn score_ip_address_priority_order() {
1083 let link_local = IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1));
1084 let ula = IpAddr::V6(Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1));
1085 let global = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
1086 let ipv4 = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
1087
1088 assert!(score_ip_address(&link_local) > score_ip_address(&ula));
1089 assert!(score_ip_address(&ula) > score_ip_address(&global));
1090 assert!(score_ip_address(&global) > score_ip_address(&ipv4));
1091 }
1092
1093 #[test]
1094 fn is_ipv6_global_unicast_correct() {
1095 assert!(is_ipv6_global_unicast(&Ipv6Addr::new(
1096 0x2001, 0xdb8, 0, 0, 0, 0, 0, 1
1097 )));
1098 assert!(is_ipv6_global_unicast(&Ipv6Addr::new(
1099 0x3fff, 0xffff, 0, 0, 0, 0, 0, 1
1100 )));
1101 assert!(!is_ipv6_global_unicast(&Ipv6Addr::new(
1102 0xfe80, 0, 0, 0, 0, 0, 0, 1
1103 )));
1104 assert!(!is_ipv6_global_unicast(&Ipv6Addr::new(
1105 0xfc00, 0, 0, 0, 0, 0, 0, 1
1106 )));
1107 }
1108}