use crate::bluest_gatt::{be, BluestConnection};
use crate::error::{BleError, Result};
use bluest::Adapter;
use std::sync::Arc;
use std::time::Duration;
use tokio_stream::StreamExt as _;
const APPLE_COMPANY_ID: u16 = 0x004C;
pub async fn scan(timeout: Duration) -> Result<Vec<DiscoveredBleAccessory>> {
let adapter = Adapter::default()
.await
.ok_or(BleError::AccessoryNotFound)?;
adapter.wait_available().await.map_err(be)?;
let mut stream = adapter.scan(&[]).await.map_err(be)?;
let mut found = Vec::new();
let mut seen = std::collections::HashSet::new();
let deadline = tokio::time::Instant::now() + timeout;
while let Ok(Some(adv)) = tokio::time::timeout_at(deadline, stream.next()).await {
let Some(mfg) = adv.adv_data.manufacturer_data else {
continue;
};
if mfg.company_id != APPLE_COMPANY_ID {
continue;
}
if let Some(acc) = parse_hap_advert(&mfg.data, adv.device.id().to_string()) {
if seen.insert(acc.peripheral_id.clone()) {
found.push(acc);
}
}
}
Ok(found)
}
pub async fn connect_gatt(accessory: &DiscoveredBleAccessory) -> Result<Arc<BluestConnection>> {
let adapter = Adapter::default()
.await
.ok_or(BleError::AccessoryNotFound)?;
adapter.wait_available().await.map_err(be)?;
let mut stream = adapter.scan(&[]).await.map_err(be)?;
let mut device = None;
let deadline = tokio::time::Instant::now() + Duration::from_secs(40);
while let Ok(Some(adv)) = tokio::time::timeout_at(deadline, stream.next()).await {
if adv.device.id().to_string() == accessory.peripheral_id {
device = Some(adv.device);
break;
}
}
drop(stream);
let device = device.ok_or(BleError::AccessoryNotFound)?;
adapter.connect_device(&device).await.map_err(be)?;
Ok(Arc::new(BluestConnection::new(adapter, device).await?))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveredBleAccessory {
pub peripheral_id: String,
pub device_id: String,
pub category: u16,
pub global_state_number: u16,
pub config_number: u8,
pub paired: bool,
}
pub(crate) fn parse_hap_advert(
mfg: &[u8],
peripheral_id: String,
) -> Option<DiscoveredBleAccessory> {
if mfg.len() < 17 {
return None;
}
let parsed = crate::advert::HapAdvert::parse(mfg)?;
let crate::advert::HapAdvert::Regular {
device_id,
gsn,
paired,
} = parsed
else {
return None;
};
let device_id_str = {
use std::fmt::Write as _;
device_id.iter().fold(String::new(), |mut s, b| {
if !s.is_empty() {
s.push(':');
}
let _ = write!(s, "{b:02x}");
s
})
};
let category = u16::from_le_bytes([mfg[9], mfg[10]]);
let config_number = mfg[13];
Some(DiscoveredBleAccessory {
peripheral_id,
device_id: device_id_str,
category,
global_state_number: gsn,
config_number,
paired,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_mfg() -> Vec<u8> {
let mut v = vec![0x06, (1 << 5) | 0x11, 0x01];
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
}
#[test]
#[allow(clippy::unwrap_used)]
fn parses_hap_manufacturer_data() {
let d = parse_hap_advert(&sample_mfg(), "11:22:33:44:55:66".into()).unwrap();
assert_eq!(d.device_id, "aa:bb:cc:dd:ee:ff");
assert_eq!(d.category, 5);
assert_eq!(d.global_state_number, 7);
assert_eq!(d.config_number, 2);
assert_eq!(d.peripheral_id, "11:22:33:44:55:66");
assert!(!d.paired);
}
#[test]
fn rejects_non_hap_advert() {
assert!(parse_hap_advert(&[0x01, 0x02], "x".into()).is_none());
}
}