Skip to main content

retch_sysinfo/
bluetooth.rs

1// SPDX-FileCopyrightText: 2026 Ken Tobias
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4//! Bluetooth controller state and connected device detection.
5
6/// Detects Bluetooth power state, adapter hardware, and connected devices.
7pub fn detect_bluetooth() -> Option<String> {
8    #[cfg(target_os = "linux")]
9    {
10        if let Ok(entries) = std::fs::read_dir("/sys/class/bluetooth") {
11            let mut hcis = Vec::new();
12            for entry in entries.filter_map(|e| e.ok()) {
13                let name = entry.file_name().to_string_lossy().to_string();
14                if name.starts_with("hci") {
15                    hcis.push(name);
16                }
17            }
18            hcis.sort();
19
20            if !hcis.is_empty() {
21                let hci = &hcis[0];
22                let mut state = "Off";
23                if let Ok(subdirs) = std::fs::read_dir(format!("/sys/class/bluetooth/{}", hci)) {
24                    for sub in subdirs.filter_map(|e| e.ok()) {
25                        let sub_name = sub.file_name().to_string_lossy().to_string();
26                        if sub_name.starts_with("rfkill") {
27                            if let Ok(st) = std::fs::read_to_string(sub.path().join("state")) {
28                                if st.trim() == "1" || st.trim() == "3" {
29                                    state = "On";
30                                }
31                            }
32                        }
33                    }
34                }
35
36                let mut hw_info = None;
37                if let Ok(canonical_device) =
38                    std::fs::canonicalize(format!("/sys/class/bluetooth/{}/device", hci))
39                {
40                    let mut current = Some(canonical_device);
41                    while let Some(path) = current {
42                        let id_vendor = path.join("idVendor");
43                        let id_product = path.join("idProduct");
44                        let pci_vendor = path.join("vendor");
45                        let pci_device = path.join("device");
46
47                        if id_vendor.exists() && id_product.exists() {
48                            if let (Ok(v), Ok(p)) = (
49                                std::fs::read_to_string(id_vendor),
50                                std::fs::read_to_string(id_product),
51                            ) {
52                                let v_clean = v.trim();
53                                let p_clean = p.trim();
54                                let vendor_name = lookup_usb_vendor(v_clean);
55                                let product_name = lookup_usb_device(v_clean, p_clean);
56                                match (vendor_name, product_name) {
57                                    (Some(v_name), Some(p_name)) => {
58                                        let v_disp = v_name
59                                            .replace(", Inc.", "")
60                                            .replace(" Corporation", "")
61                                            .replace(" Co., Ltd.", "")
62                                            .replace(" Co., Ltd", "");
63                                        hw_info = Some(format!("{} {}", v_disp, p_name));
64                                    }
65                                    (Some(v_name), None) => {
66                                        let v_disp = v_name
67                                            .replace(", Inc.", "")
68                                            .replace(" Corporation", "")
69                                            .replace(" Co., Ltd.", "")
70                                            .replace(" Co., Ltd", "");
71                                        hw_info = Some(v_disp);
72                                    }
73                                    _ => {}
74                                }
75                                break;
76                            }
77                        } else if pci_vendor.exists()
78                            && pci_device.exists()
79                            && !pci_vendor.is_dir()
80                            && !pci_device.is_dir()
81                        {
82                            if let (Ok(v), Ok(d)) = (
83                                std::fs::read_to_string(pci_vendor),
84                                std::fs::read_to_string(pci_device),
85                            ) {
86                                let v_clean = v.trim().trim_start_matches("0x").to_lowercase();
87                                let d_clean = d.trim().trim_start_matches("0x").to_lowercase();
88                                let vendor_name = crate::network::lookup_pci_vendor(&v_clean);
89                                let product_name =
90                                    crate::gpu::lookup_pci_device(&v_clean, &d_clean);
91                                match (vendor_name, product_name) {
92                                    (Some(v_name), Some(p_name)) => {
93                                        let v_disp = v_name
94                                            .replace(", Inc.", "")
95                                            .replace(" Corporation", "")
96                                            .replace(" Co., Ltd.", "")
97                                            .replace(" Co., Ltd", "");
98                                        hw_info = Some(format!("{} {}", v_disp, p_name));
99                                    }
100                                    (Some(v_name), None) => {
101                                        let v_disp = v_name
102                                            .replace(", Inc.", "")
103                                            .replace(" Corporation", "")
104                                            .replace(" Co., Ltd.", "")
105                                            .replace(" Co., Ltd", "");
106                                        hw_info = Some(v_disp);
107                                    }
108                                    _ => {}
109                                }
110                                break;
111                            }
112                        }
113                        current = path.parent().map(|p| p.to_path_buf());
114                    }
115                }
116
117                let mut connected_names = Vec::new();
118                if let Ok(output) = std::process::Command::new("bluetoothctl")
119                    .args(["devices", "Connected"])
120                    .output()
121                {
122                    if let Ok(stdout) = String::from_utf8(output.stdout) {
123                        for line in stdout.lines() {
124                            let trimmed = line.trim();
125                            if trimmed.starts_with("Device ") {
126                                let parts: Vec<&str> = trimmed.split_whitespace().collect();
127                                if parts.len() >= 3 {
128                                    let name = parts[2..].join(" ");
129                                    connected_names.push(name);
130                                }
131                            }
132                        }
133                    }
134                }
135                let mut info_str = state.to_string();
136                info_str.push_str(&format!(" [{}]", hci));
137                if let Some(hw) = hw_info {
138                    info_str.push_str(&format!(" ({})", hw));
139                }
140
141                if state == "On" {
142                    info_str.push_str(&format!(" - {} connected", connected_names.len()));
143                    if !connected_names.is_empty() {
144                        info_str.push_str(&format!(" ({})", connected_names.join(", ")));
145                    }
146                }
147
148                return Some(info_str);
149            }
150        }
151        None
152    }
153
154    #[cfg(target_os = "macos")]
155    {
156        if let Some((power_on, chipset)) = crate::macos_ffi::get_bluetooth_state() {
157            let state = if power_on { "On" } else { "Off" };
158            let mut info_str = state.to_string();
159            if let Some(ch) = chipset {
160                info_str.push_str(&format!(" (Apple {})", ch));
161            } else {
162                info_str.push_str(" (Apple Bluetooth)");
163            }
164            // Connected device names require Obj-C IOBluetooth; not available via C IOKit.
165            if power_on {
166                info_str.push_str(" - connected devices unknown");
167            }
168            Some(info_str)
169        } else {
170            None
171        }
172    }
173
174    #[cfg(target_os = "windows")]
175    {
176        windows_impl::detect()
177    }
178
179    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
180    {
181        None
182    }
183}
184
185#[cfg(target_os = "linux")]
186fn lookup_usb_vendor(vendor_id: &str) -> Option<String> {
187    let vendor_id = vendor_id.trim_start_matches("0x").to_lowercase();
188    let paths = ["/usr/share/hwdata/usb.ids", "/usr/share/misc/usb.ids"];
189    for path in &paths {
190        if let Ok(content) = std::fs::read_to_string(path) {
191            for line in content.lines() {
192                if line.starts_with('#') || line.is_empty() {
193                    continue;
194                }
195                if !line.starts_with('\t') {
196                    let parts: Vec<&str> = line.split_whitespace().collect();
197                    if parts.len() >= 2 && parts[0].to_lowercase() == vendor_id {
198                        let name = line.strip_prefix(parts[0]).unwrap().trim();
199                        return Some(name.to_string());
200                    }
201                }
202            }
203        }
204    }
205    None
206}
207
208#[cfg(target_os = "linux")]
209fn lookup_usb_device(vendor_id: &str, product_id: &str) -> Option<String> {
210    let vendor_id = vendor_id.trim_start_matches("0x").to_lowercase();
211    let product_id = product_id.trim_start_matches("0x").to_lowercase();
212    let paths = ["/usr/share/hwdata/usb.ids", "/usr/share/misc/usb.ids"];
213    for path in &paths {
214        if let Ok(content) = std::fs::read_to_string(path) {
215            let mut in_vendor = false;
216            for line in content.lines() {
217                if line.starts_with('#') || line.is_empty() {
218                    continue;
219                }
220                if !line.starts_with('\t') {
221                    let parts: Vec<&str> = line.split_whitespace().collect();
222                    in_vendor = parts.len() >= 2 && parts[0].to_lowercase() == vendor_id;
223                } else if in_vendor && line.starts_with('\t') && !line.starts_with("\t\t") {
224                    let trimmed = line.trim_start();
225                    if let Some(stripped) = trimmed.strip_prefix(&product_id) {
226                        let name = stripped.trim();
227                        return Some(name.to_string());
228                    }
229                }
230            }
231        }
232    }
233    None
234}
235
236#[allow(dead_code)]
237fn parse_macos_bluetooth(stdout: &str) -> Option<String> {
238    let mut state = "Off";
239    let mut connected_names = Vec::new();
240    let mut chipset = None;
241    let mut current_device = None;
242
243    for line in stdout.lines() {
244        let trimmed = line.trim();
245        if trimmed.starts_with("Bluetooth Power:") || trimmed.starts_with("State:") {
246            if trimmed.contains("On") {
247                state = "On";
248            }
249        } else if trimmed.starts_with("Chipset:") {
250            chipset = Some(trimmed.strip_prefix("Chipset:").unwrap().trim().to_string());
251        } else if line.starts_with("          ") && !trimmed.is_empty() && trimmed.ends_with(':') {
252            current_device = Some(trimmed.trim_end_matches(':').trim().to_string());
253        } else if (trimmed.starts_with("Connected:") || trimmed.starts_with("Connection:"))
254            && trimmed.contains("Yes")
255        {
256            if let Some(ref dev) = current_device {
257                connected_names.push(dev.clone());
258            }
259        }
260    }
261
262    let mut info_str = state.to_string();
263    if let Some(ch) = chipset {
264        info_str.push_str(&format!(" (Apple {})", ch));
265    } else {
266        info_str.push_str(" (Apple Bluetooth)");
267    }
268
269    if state == "On" {
270        info_str.push_str(&format!(" - {} connected", connected_names.len()));
271        if !connected_names.is_empty() {
272            info_str.push_str(&format!(" ({})", connected_names.join(", ")));
273        }
274    }
275    Some(info_str)
276}
277
278/// Formats Bluetooth state into the display string, matching the previous
279/// PowerShell-parsing output: `"On (Adapter) - N connected (name1, name2)"`,
280/// `"Off (Adapter)"`, or `"Off"`. Connected devices are only shown when powered on.
281#[cfg(target_os = "windows")]
282fn format_windows_bluetooth(on: bool, adapter: &str, devices: &[String]) -> String {
283    let mut s = if on { "On" } else { "Off" }.to_string();
284    if !adapter.is_empty() {
285        s.push_str(&format!(" ({})", adapter));
286    }
287    if on {
288        s.push_str(&format!(" - {} connected", devices.len()));
289        if !devices.is_empty() {
290            s.push_str(&format!(" ({})", devices.join(", ")));
291        }
292    }
293    s
294}
295
296/// Native Windows Bluetooth detection.
297///
298/// Replaces the previous PowerShell spawn (`Get-Service bthserv` + two
299/// `Get-PnpDevice -Class Bluetooth` queries, ~1.8 s) with native Win32:
300/// - Power state: the `bthserv` service state via the Service Control Manager
301///   (advapi32) — the same signal the old `Get-Service` check used.
302/// - Adapter name: SetupAPI enumeration of `GUID_DEVCLASS_BLUETOOTH`.
303/// - Connected devices: SetupAPI again, reading `System.Devices.Connected` per device
304///   node. This replaced the classic `bthprops` API (`BluetoothFindFirstDevice` with
305///   `fReturnConnected`), which is **BR/EDR-only** and therefore never reported a
306///   Bluetooth Low Energy peripheral at all — the field under-counted every LE mouse,
307///   keyboard and headset on the machine. Measured on a box with a classic headset and
308///   an LE mouse connected, `bthprops` returned only the headset while the device nodes
309///   reported both. LE state is not reachable from `bthprops`, and the WinRT route that
310///   does expose it (`DeviceInformation` over association endpoints) never completed in
311///   testing and cost ~1 s where it did work, against ~11 ms for this enumeration.
312///
313/// No WinRT: this is the same synchronous SetupAPI already used for the adapter name and
314/// for `camera`.
315///
316/// Hand-written `extern "system"` FFI matching the crate's style (`win_reg.rs`).
317#[cfg(target_os = "windows")]
318mod windows_impl {
319    use super::format_windows_bluetooth;
320    use std::ffi::{c_void, OsStr};
321    use std::os::windows::ffi::OsStrExt;
322    use std::ptr;
323
324    type Handle = *mut c_void;
325
326    // Service Control Manager (advapi32 — linked by std, like win_reg.rs's Reg* calls).
327    const SC_MANAGER_CONNECT: u32 = 0x0001;
328    const SERVICE_QUERY_STATUS: u32 = 0x0004;
329    const SERVICE_RUNNING: u32 = 4;
330
331    #[repr(C)]
332    struct ServiceStatus {
333        service_type: u32,
334        current_state: u32,
335        controls_accepted: u32,
336        win32_exit_code: u32,
337        service_specific_exit_code: u32,
338        check_point: u32,
339        wait_hint: u32,
340    }
341
342    extern "system" {
343        fn OpenSCManagerW(
344            machine_name: *const u16,
345            database_name: *const u16,
346            desired_access: u32,
347        ) -> Handle;
348        fn OpenServiceW(scm: Handle, service_name: *const u16, desired_access: u32) -> Handle;
349        fn QueryServiceStatus(service: Handle, status: *mut ServiceStatus) -> i32;
350        fn CloseServiceHandle(handle: Handle) -> i32;
351    }
352
353    fn wide(s: &str) -> Vec<u16> {
354        OsStr::new(s).encode_wide().chain(Some(0)).collect()
355    }
356
357    /// Whether the `bthserv` (Bluetooth Support Service) is running — the power-state
358    /// signal the old `Get-Service -Name bthserv` check used.
359    fn bthserv_running() -> bool {
360        // SAFETY: SCM handles are opened and closed in-scope; QueryServiceStatus writes
361        // into a stack-allocated ServiceStatus.
362        unsafe {
363            let scm = OpenSCManagerW(ptr::null(), ptr::null(), SC_MANAGER_CONNECT);
364            if scm.is_null() {
365                return false;
366            }
367            let name = wide("bthserv");
368            let svc = OpenServiceW(scm, name.as_ptr(), SERVICE_QUERY_STATUS);
369            let mut running = false;
370            if !svc.is_null() {
371                let mut status: ServiceStatus = std::mem::zeroed();
372                if QueryServiceStatus(svc, &mut status) != 0 {
373                    running = status.current_state == SERVICE_RUNNING;
374                }
375                CloseServiceHandle(svc);
376            }
377            CloseServiceHandle(scm);
378            running
379        }
380    }
381
382    /// Whether a device friendly name looks like the Bluetooth adapter/controller itself
383    /// (rather than a paired peripheral). Mirrors the old PowerShell name filter.
384    pub(super) fn looks_like_adapter(name: &str) -> bool {
385        let l = name.to_ascii_lowercase();
386        [
387            "adapter",
388            "controller",
389            "radio",
390            "intel",
391            "realtek",
392            "broadcom",
393        ]
394        .iter()
395        .any(|k| l.contains(k))
396    }
397
398    /// The Bluetooth adapter's hardware friendly name, via the shared SetupAPI helper over
399    /// the Bluetooth device class (what the old `Get-PnpDevice -Class Bluetooth` reported).
400    fn adapter_name() -> Option<String> {
401        crate::win_setupapi::present_device_names(&crate::win_setupapi::GUID_DEVCLASS_BLUETOOTH)
402            .into_iter()
403            .find(|name| looks_like_adapter(name))
404    }
405
406    /// True for a *remote device* instance id, as opposed to a service, an enumerator or
407    /// the local radio.
408    ///
409    /// The Bluetooth setup class holds all of them. Remote devices are `BTHENUM\DEV_…`
410    /// (classic) and `BTHLE\DEV_…` (LE); per-profile service nodes are
411    /// `BTHENUM\{guid}_…` / `BTHLEDEVICE\{guid}_…`, so the `DEV_` segment is what
412    /// separates a device from one of its services.
413    pub(super) fn is_remote_device_id(instance_id: &str) -> bool {
414        let id = instance_id.to_ascii_uppercase();
415        id.starts_with("BTHENUM\\DEV_") || id.starts_with("BTHLE\\DEV_")
416    }
417
418    /// The device address embedded in a Bluetooth instance id, uppercased.
419    ///
420    /// `BTHLE\DEV_F5183CA50C6B\9&1C053637&0&F5183CA50C6B` yields `F5183CA50C6B`. Used to
421    /// collapse a dual-mode device, which enumerates once per transport — a phone paired
422    /// for both audio and LE appears as both `BTHENUM\DEV_<addr>` and `BTHLE\DEV_<addr>`
423    /// and would otherwise be counted twice. De-duplicating on the address rather than
424    /// the name is deliberate: two distinct devices may share a name, and collapsing
425    /// those would under-count.
426    pub(super) fn address_from_instance_id(instance_id: &str) -> Option<String> {
427        let id = instance_id.to_ascii_uppercase();
428        let rest = id.split_once("\\DEV_")?.1;
429        let addr = rest.split('\\').next()?;
430        if addr.is_empty() || !addr.chars().all(|c| c.is_ascii_hexdigit()) {
431            return None;
432        }
433        Some(addr.to_string())
434    }
435
436    /// Names of currently-connected Bluetooth devices, classic and LE alike.
437    fn connected_devices() -> Vec<String> {
438        let mut seen = std::collections::HashSet::new();
439        let mut names = Vec::new();
440        for device in
441            crate::win_setupapi::present_devices(&crate::win_setupapi::GUID_DEVCLASS_BLUETOOTH)
442        {
443            if !is_remote_device_id(&device.instance_id) {
444                continue;
445            }
446            // Only a definite `true` counts: a node that does not expose the property is
447            // unknown, and reporting an unknown device as connected would overstate.
448            if device.connected != Some(true) {
449                continue;
450            }
451            if let Some(addr) = address_from_instance_id(&device.instance_id) {
452                if !seen.insert(addr) {
453                    continue;
454                }
455            }
456            if let Some(name) = device.name {
457                names.push(name);
458            }
459        }
460        names
461    }
462
463    pub fn detect() -> Option<String> {
464        let on = bthserv_running();
465        let adapter = adapter_name().unwrap_or_default();
466        let devices = if on { connected_devices() } else { Vec::new() };
467        Some(format_windows_bluetooth(on, &adapter, &devices))
468    }
469
470    #[cfg(test)]
471    mod layout {
472        use std::mem::size_of;
473
474        // `QueryServiceStatus` fills this `#[repr(C)]` buffer by offset — pin the layout
475        // so a reorder/padding change can't slip through.
476        #[test]
477        fn ffi_struct_layout() {
478            assert_eq!(size_of::<super::ServiceStatus>(), 28);
479        }
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn test_parse_macos_bluetooth() {
489        let sample = "Bluetooth:\n\n      Bluetooth Power: On\n      Chipset: BCM4350\n      Devices (Connected):\n          Sony WH-1000XM4:\n              Address: AA-BB-CC\n              Connected: Yes\n          Logitech MX Master:\n              Address: DD-EE-FF\n              Connected: Yes\n";
490        assert_eq!(
491            parse_macos_bluetooth(sample),
492            Some(
493                "On (Apple BCM4350) - 2 connected (Sony WH-1000XM4, Logitech MX Master)"
494                    .to_string()
495            )
496        );
497
498        let sample_off = "Bluetooth:\n\n      Bluetooth Power: Off\n";
499        assert_eq!(
500            parse_macos_bluetooth(sample_off),
501            Some("Off (Apple Bluetooth)".to_string())
502        );
503
504        let sample_state_on = "Bluetooth:\n\n      State: On\n      Chipset: BCM_4388\n";
505        assert_eq!(
506            parse_macos_bluetooth(sample_state_on),
507            Some("On (Apple BCM_4388) - 0 connected".to_string())
508        );
509    }
510
511    #[cfg(target_os = "windows")]
512    #[test]
513    fn test_format_windows_bluetooth() {
514        // On, adapter, two connected devices.
515        assert_eq!(
516            format_windows_bluetooth(
517                true,
518                "Intel(R) Wireless Bluetooth(R)",
519                &["Sony WH-1000XM4".to_string(), "Logitech MX Master".to_string()],
520            ),
521            "On (Intel(R) Wireless Bluetooth(R)) - 2 connected (Sony WH-1000XM4, Logitech MX Master)"
522        );
523
524        // On, adapter, nothing connected.
525        assert_eq!(
526            format_windows_bluetooth(true, "MediaTek Bluetooth Adapter", &[]),
527            "On (MediaTek Bluetooth Adapter) - 0 connected"
528        );
529
530        // Off with a known adapter — no connected suffix when powered off.
531        assert_eq!(
532            format_windows_bluetooth(false, "MediaTek Bluetooth Adapter", &[]),
533            "Off (MediaTek Bluetooth Adapter)"
534        );
535
536        // Off, no adapter detected.
537        assert_eq!(format_windows_bluetooth(false, "", &[]), "Off");
538
539        // On, no adapter name resolved.
540        assert_eq!(
541            format_windows_bluetooth(true, "", &["Pixel Buds".to_string()]),
542            "On - 1 connected (Pixel Buds)"
543        );
544    }
545
546    #[cfg(target_os = "windows")]
547    #[test]
548    fn test_looks_like_adapter() {
549        use super::windows_impl::looks_like_adapter;
550        assert!(looks_like_adapter("MediaTek Bluetooth Adapter"));
551        assert!(looks_like_adapter("Intel(R) Wireless Bluetooth(R)"));
552        assert!(looks_like_adapter("Realtek Bluetooth Controller"));
553        assert!(!looks_like_adapter("Ken's Pixel Buds Pro 2"));
554        assert!(!looks_like_adapter("MX Anywhere 3S"));
555    }
556
557    /// Instance ids below are verbatim from a live machine, so the discriminator is
558    /// pinned against real data rather than an invented shape.
559    #[test]
560    #[cfg(target_os = "windows")]
561    fn test_is_remote_device_id() {
562        use super::windows_impl::is_remote_device_id;
563        // Real remote devices — classic and LE.
564        assert!(is_remote_device_id(
565            r"BTHENUM\DEV_7CE9138B5564\9&8E04A68&0&BLUETOOTHDEVICE_7CE9138B5564"
566        ));
567        assert!(is_remote_device_id(
568            r"BTHLE\DEV_F5183CA50C6B\9&1C053637&0&F5183CA50C6B"
569        ));
570        // Per-profile service nodes sit in the same setup class and must not be counted:
571        // an Avrcp transport and a GATT service are not devices.
572        assert!(!is_remote_device_id(
573            r"BTHENUM\{0000110E-0000-1000-8000-00805F9B34FB}_VID&000100E0_PID&4115\9&8E04A68&0&7CE9138B5564_C00000000"
574        ));
575        assert!(!is_remote_device_id(
576            r"BTHLEDEVICE\{0000180F-0000-1000-8000-00805F9B34FB}_DEV_VID&02046D_PID&B037_REV&0003_D0940BA106B6\A&38AF489E&0&001B"
577        ));
578        // The radio itself and the enumerators.
579        assert!(!is_remote_device_id(
580            r"USB\VID_13D3&PID_3602&MI_00\7&2434504C&0&0000"
581        ));
582        assert!(!is_remote_device_id(r"BTH\MS_BTHLE\8&29FC6E36&0&3"));
583    }
584
585    #[test]
586    #[cfg(target_os = "windows")]
587    fn test_address_from_instance_id() {
588        use super::windows_impl::address_from_instance_id;
589        // The same physical device on both transports yields one address, which is what
590        // makes de-duplicating a dual-mode device possible.
591        assert_eq!(
592            address_from_instance_id(
593                r"BTHENUM\DEV_B0D5FBBB66EA\9&8E04A68&0&BLUETOOTHDEVICE_B0D5FBBB66EA"
594            ),
595            Some("B0D5FBBB66EA".to_string())
596        );
597        assert_eq!(
598            address_from_instance_id(r"BTHLE\DEV_B0D5FBBB66EA\9&1C053637&0&B0D5FBBB66EA"),
599            Some("B0D5FBBB66EA".to_string())
600        );
601        // Case-insensitive: the two transports disagree on case for the same device.
602        assert_eq!(
603            address_from_instance_id(r"BTHLE\Dev_f5183ca50c6b\9&1c053637&0&f5183ca50c6b"),
604            Some("F5183CA50C6B".to_string())
605        );
606        // Not an address: a service node's `_DEV_` segment is followed by VID/PID text,
607        // which must not be mistaken for one.
608        assert_eq!(
609            address_from_instance_id(
610                r"BTHLEDEVICE\{0000180F-0000-1000-8000-00805F9B34FB}_DEV_VID&02046D_PID&B037_REV&0003_D0940BA106B6\A&38AF489E&0&001B"
611            ),
612            None
613        );
614        assert_eq!(
615            address_from_instance_id(r"BTH\MS_BTHLE\8&29FC6E36&0&3"),
616            None
617        );
618        // Synthetic — no observed device produces this. It covers the hex guard, which is
619        // what stops a malformed id becoming a de-duplication key: a bogus key shared by
620        // two real devices would silently drop one of them.
621        assert_eq!(
622            address_from_instance_id(r"BTHENUM\DEV_NOTANADDRESS\9&1"),
623            None
624        );
625    }
626}