1use crate::bluest_gatt::{be, BluestConnection};
5use crate::error::{BleError, Result};
6use bluest::Adapter;
7use std::sync::Arc;
8use std::time::Duration;
9use tokio_stream::StreamExt as _;
10
11const APPLE_COMPANY_ID: u16 = 0x004C;
13
14pub async fn scan(timeout: Duration) -> Result<Vec<DiscoveredBleAccessory>> {
19 let adapter = Adapter::default()
20 .await
21 .ok_or(BleError::AccessoryNotFound)?;
22 adapter.wait_available().await.map_err(be)?;
23 let mut stream = adapter.scan(&[]).await.map_err(be)?;
24
25 let mut found = Vec::new();
26 let mut seen = std::collections::HashSet::new();
27 let deadline = tokio::time::Instant::now() + timeout;
28 while let Ok(Some(adv)) = tokio::time::timeout_at(deadline, stream.next()).await {
29 let Some(mfg) = adv.adv_data.manufacturer_data else {
30 continue;
31 };
32 if mfg.company_id != APPLE_COMPANY_ID {
33 continue;
34 }
35 if let Some(acc) = parse_hap_advert(&mfg.data, adv.device.id().to_string()) {
36 if seen.insert(acc.peripheral_id.clone()) {
37 found.push(acc);
38 }
39 }
40 }
41 Ok(found)
42}
43
44pub async fn connect_gatt(accessory: &DiscoveredBleAccessory) -> Result<Arc<BluestConnection>> {
49 let adapter = Adapter::default()
50 .await
51 .ok_or(BleError::AccessoryNotFound)?;
52 adapter.wait_available().await.map_err(be)?;
53 let mut stream = adapter.scan(&[]).await.map_err(be)?;
56 let mut device = None;
57 let deadline = tokio::time::Instant::now() + Duration::from_secs(40);
58 while let Ok(Some(adv)) = tokio::time::timeout_at(deadline, stream.next()).await {
59 if adv.device.id().to_string() == accessory.peripheral_id {
60 device = Some(adv.device);
61 break;
62 }
63 }
64 drop(stream);
65 let device = device.ok_or(BleError::AccessoryNotFound)?;
66 adapter.connect_device(&device).await.map_err(be)?;
67 Ok(Arc::new(BluestConnection::new(adapter, device).await?))
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct DiscoveredBleAccessory {
73 pub peripheral_id: String,
76 pub device_id: String,
78 pub category: u16,
80 pub global_state_number: u16,
82 pub config_number: u8,
84 pub paired: bool,
86}
87
88pub(crate) fn parse_hap_advert(
91 mfg: &[u8],
92 peripheral_id: String,
93) -> Option<DiscoveredBleAccessory> {
94 if mfg.len() < 17 {
96 return None;
97 }
98 let parsed = crate::advert::HapAdvert::parse(mfg)?;
99 let crate::advert::HapAdvert::Regular {
100 device_id,
101 gsn,
102 paired,
103 } = parsed
104 else {
105 return None;
106 };
107 let device_id_str = {
108 use std::fmt::Write as _;
109 device_id.iter().fold(String::new(), |mut s, b| {
110 if !s.is_empty() {
111 s.push(':');
112 }
113 let _ = write!(s, "{b:02x}");
114 s
115 })
116 };
117 let category = u16::from_le_bytes([mfg[9], mfg[10]]);
118 let config_number = mfg[13];
119 Some(DiscoveredBleAccessory {
120 peripheral_id,
121 device_id: device_id_str,
122 category,
123 global_state_number: gsn,
124 config_number,
125 paired,
126 })
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn sample_mfg() -> Vec<u8> {
139 let mut v = vec![0x06, (1 << 5) | 0x11, 0x01];
140 v.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]); v.extend_from_slice(&5u16.to_le_bytes()); v.extend_from_slice(&7u16.to_le_bytes()); v.push(2); v.push(2); v.extend_from_slice(&[0x12, 0x34]); v
147 }
148
149 #[test]
150 #[allow(clippy::unwrap_used)]
151 fn parses_hap_manufacturer_data() {
152 let d = parse_hap_advert(&sample_mfg(), "11:22:33:44:55:66".into()).unwrap();
153 assert_eq!(d.device_id, "aa:bb:cc:dd:ee:ff");
154 assert_eq!(d.category, 5);
155 assert_eq!(d.global_state_number, 7);
156 assert_eq!(d.config_number, 2);
157 assert_eq!(d.peripheral_id, "11:22:33:44:55:66");
158 assert!(!d.paired);
160 }
161
162 #[test]
163 fn rejects_non_hap_advert() {
164 assert!(parse_hap_advert(&[0x01, 0x02], "x".into()).is_none());
165 }
166}