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 scan_stream(
60 timeout: Duration,
61) -> Result<tokio::sync::mpsc::Receiver<DiscoveredBleAccessory>> {
62 let adapter = Adapter::default()
63 .await
64 .ok_or(BleError::AccessoryNotFound)?;
65 adapter.wait_available().await.map_err(be)?;
66
67 let (tx, rx) = tokio::sync::mpsc::channel::<DiscoveredBleAccessory>(16);
68 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<Result<()>>();
72 tokio::spawn(async move {
73 let mut stream = match adapter.scan(&[]).await {
74 Ok(s) => {
75 let _ = ready_tx.send(Ok(()));
76 s
77 }
78 Err(e) => {
79 let _ = ready_tx.send(Err(be(e)));
80 return;
81 }
82 };
83 let mut seen = std::collections::HashSet::new();
84 let deadline = tokio::time::Instant::now() + timeout;
85 loop {
86 tokio::select! {
87 () = tx.closed() => break,
89 next = tokio::time::timeout_at(deadline, stream.next()) => {
90 match next {
91 Err(_) | Ok(None) => break,
92 Ok(Some(adv)) => {
93 let Some(mfg) = adv.adv_data.manufacturer_data else {
94 continue;
95 };
96 if mfg.company_id != APPLE_COMPANY_ID {
97 continue;
98 }
99 if let Some(acc) =
100 parse_hap_advert(&mfg.data, adv.device.id().to_string())
101 {
102 if seen.insert(acc.peripheral_id.clone())
103 && tx.send(acc).await.is_err()
104 {
105 break;
106 }
107 }
108 }
109 }
110 }
111 }
112 }
113 drop(stream);
116 drop(adapter);
117 });
118 match ready_rx.await {
119 Ok(Ok(())) => Ok(rx),
120 Ok(Err(e)) => Err(e),
121 Err(_recv) => Err(BleError::Backend("scan task exited early".into())),
123 }
124}
125
126pub async fn connect_gatt(accessory: &DiscoveredBleAccessory) -> Result<Arc<BluestConnection>> {
131 let adapter = Adapter::default()
132 .await
133 .ok_or(BleError::AccessoryNotFound)?;
134 adapter.wait_available().await.map_err(be)?;
135 let mut stream = adapter.scan(&[]).await.map_err(be)?;
138 let mut device = None;
139 let deadline = tokio::time::Instant::now() + Duration::from_secs(40);
140 while let Ok(Some(adv)) = tokio::time::timeout_at(deadline, stream.next()).await {
141 if adv.device.id().to_string() == accessory.peripheral_id {
142 device = Some(adv.device);
143 break;
144 }
145 }
146 drop(stream);
147 let device = device.ok_or(BleError::AccessoryNotFound)?;
148 adapter.connect_device(&device).await.map_err(be)?;
149 Ok(Arc::new(BluestConnection::new(adapter, device).await?))
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct DiscoveredBleAccessory {
155 pub peripheral_id: String,
158 pub device_id: String,
160 pub category: u16,
162 pub global_state_number: u16,
164 pub config_number: u8,
166 pub paired: bool,
168 pub setup_hash: Option<[u8; 4]>,
172}
173
174pub(crate) fn parse_hap_advert(
177 mfg: &[u8],
178 peripheral_id: String,
179) -> Option<DiscoveredBleAccessory> {
180 if mfg.len() < 15 {
182 return None;
183 }
184 let parsed = crate::advert::HapAdvert::parse(mfg)?;
185 let crate::advert::HapAdvert::Regular {
186 device_id,
187 gsn,
188 paired,
189 } = parsed
190 else {
191 return None;
192 };
193 let device_id_str = {
194 use std::fmt::Write as _;
195 device_id.iter().fold(String::new(), |mut s, b| {
196 if !s.is_empty() {
197 s.push(':');
198 }
199 let _ = write!(s, "{b:02x}");
200 s
201 })
202 };
203 let category = u16::from_le_bytes([mfg[9], mfg[10]]);
204 let config_number = mfg[13];
205 let setup_hash = if mfg.len() >= 19 {
206 Some([mfg[15], mfg[16], mfg[17], mfg[18]])
207 } else {
208 None
209 };
210 Some(DiscoveredBleAccessory {
211 peripheral_id,
212 device_id: device_id_str,
213 category,
214 global_state_number: gsn,
215 config_number,
216 paired,
217 setup_hash,
218 })
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn sample_mfg() -> Vec<u8> {
231 let mut v = vec![0x06, (1 << 5) | 0x11, 0x01];
232 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
239 }
240
241 #[test]
242 #[allow(clippy::unwrap_used)]
243 fn parses_hap_manufacturer_data() {
244 let d = parse_hap_advert(&sample_mfg(), "11:22:33:44:55:66".into()).unwrap();
245 assert_eq!(d.device_id, "aa:bb:cc:dd:ee:ff");
246 assert_eq!(d.category, 5);
247 assert_eq!(d.global_state_number, 7);
248 assert_eq!(d.config_number, 2);
249 assert_eq!(d.peripheral_id, "11:22:33:44:55:66");
250 assert!(!d.paired);
252 assert_eq!(d.setup_hash, None);
254 }
255
256 #[test]
257 fn rejects_non_hap_advert() {
258 assert!(parse_hap_advert(&[0x01, 0x02], "x".into()).is_none());
259 }
260
261 #[test]
262 #[allow(clippy::unwrap_used)]
263 fn parses_setup_hash_when_present() {
264 let mfg = [
267 0x06, 0x31, 0x01, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x0A, 0x00, 0x05, 0x00, 0x02, 0x02, 0x5c, 0x8a, 0x27, 0x40, ];
274 let d = parse_hap_advert(&mfg, "periph-1".into()).unwrap();
275 assert_eq!(d.setup_hash, Some([0x5c, 0x8a, 0x27, 0x40]));
276 assert_eq!(d.device_id, "aa:bb:cc:dd:ee:ff"); }
278
279 #[test]
280 #[allow(clippy::unwrap_used)]
281 fn setup_hash_absent_on_short_advert() {
282 let mfg = [
284 0x06, 0x31, 0x01, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x0A, 0x00, 0x05, 0x00, 0x02,
285 0x02, 0x12, 0x34,
286 ];
287 let d = parse_hap_advert(&mfg, "periph-1".into()).unwrap();
288 assert_eq!(d.setup_hash, None);
289 }
290}