use crate::{Error, Result, api::ScanFilter, winrtble::utils};
use log::debug;
use std::{collections::HashSet, sync::Mutex};
use windows::{Devices::Bluetooth::Advertisement::*, Foundation::TypedEventHandler, core::Ref};
const MATCH_CACHE_CAPACITY: usize = 1024;
pub type AdvertisementEventHandler =
Box<dyn Fn(&BluetoothLEAdvertisementReceivedEventArgs) -> windows::core::Result<()> + Send>;
#[derive(Debug)]
pub struct BLEWatcher {
watcher: BluetoothLEAdvertisementWatcher,
received_token: Option<i64>,
coded_phy_supported: bool,
use_coded_phy: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CodedPhyScan {
Enabled,
NotRequested,
Unsupported,
}
impl CodedPhyScan {
fn new(supported: bool, requested: bool) -> Self {
match (requested, supported) {
(false, _) => Self::NotRequested,
(true, false) => Self::Unsupported,
(true, true) => Self::Enabled,
}
}
}
impl From<windows::core::Error> for Error {
fn from(err: windows::core::Error) -> Error {
Error::Other(format!("{:?}", err).into())
}
}
#[derive(Default)]
struct MatchCache {
addresses: HashSet<u64>,
}
impl MatchCache {
fn record(&mut self, address: u64) {
if self.addresses.len() < MATCH_CACHE_CAPACITY || self.addresses.contains(&address) {
self.addresses.insert(address);
}
}
fn contains(&self, address: u64) -> bool {
self.addresses.contains(&address)
}
}
impl BLEWatcher {
pub fn new(coded_phy_supported: bool) -> Result<Self> {
let ad = BluetoothLEAdvertisementFilter::new()?;
let watcher = BluetoothLEAdvertisementWatcher::Create(&ad)?;
Ok(BLEWatcher {
watcher,
received_token: None,
coded_phy_supported,
use_coded_phy: false,
})
}
pub fn set_use_coded_phy(&mut self, enabled: bool) {
self.use_coded_phy = enabled;
}
pub fn start(
&mut self,
filter: ScanFilter,
on_received: AdvertisementEventHandler,
) -> Result<()> {
self.remove_received_handler()?;
let ScanFilter { services } = filter;
let ad = self.watcher.AdvertisementFilter()?.Advertisement()?;
ad.ServiceUuids()?.Clear()?;
self.watcher
.SetScanningMode(BluetoothLEScanningMode::Active)?;
let _ = self.watcher.SetAllowExtendedAdvertisements(true);
let coded_phy = CodedPhyScan::new(self.coded_phy_supported, self.use_coded_phy);
let _ = self
.watcher
.SetUseCodedPhy(coded_phy == CodedPhyScan::Enabled);
debug!(
"extended scanning enabled; coded PHY {}",
match coded_phy {
CodedPhyScan::Enabled => "enabled",
CodedPhyScan::NotRequested => "not requested, disabled",
CodedPhyScan::Unsupported => "requested but not supported by adapter, disabled",
}
);
let filter_guids: Vec<windows::core::GUID> = services.iter().map(utils::to_guid).collect();
let matching_devices = Mutex::new(MatchCache::default());
let handler: TypedEventHandler<
BluetoothLEAdvertisementWatcher,
BluetoothLEAdvertisementReceivedEventArgs,
> = TypedEventHandler::new(
move |_sender, args: Ref<BluetoothLEAdvertisementReceivedEventArgs>| {
if let Ok(args) = args.ok() {
if !filter_guids.is_empty() {
let address = args.BluetoothAddress().unwrap_or(0);
let mut is_match = false;
if let Ok(ad) = args.Advertisement()
&& let Ok(ad_uuids) = ad.ServiceUuids()
{
let count = ad_uuids.Size().unwrap_or(0);
if count > 0 {
let advertised: Vec<windows::core::GUID> =
(0..count).filter_map(|i| ad_uuids.GetAt(i).ok()).collect();
is_match = filter_guids.iter().any(|g| advertised.contains(g));
}
}
let mut cache = matching_devices.lock().unwrap();
if is_match {
cache.record(address);
} else if !matches!(
args.AdvertisementType(),
Ok(BluetoothLEAdvertisementType::ScanResponse)
) || !cache.contains(address)
{
return Ok(());
}
}
on_received(args)?;
}
Ok(())
},
);
self.received_token = Some(self.watcher.Received(&handler)?);
self.watcher.Start()?;
Ok(())
}
pub fn stop(&mut self) -> Result<()> {
self.watcher.Stop()?;
self.remove_received_handler()
}
fn remove_received_handler(&mut self) -> Result<()> {
if let Some(token) = self.received_token.take() {
self.watcher.RemoveReceived(token)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{CodedPhyScan, MATCH_CACHE_CAPACITY, MatchCache};
#[test]
fn coded_phy_requires_request_and_adapter_support() {
assert_eq!(CodedPhyScan::new(true, true), CodedPhyScan::Enabled);
assert_eq!(CodedPhyScan::new(false, true), CodedPhyScan::Unsupported);
assert_eq!(CodedPhyScan::new(true, false), CodedPhyScan::NotRequested);
assert_eq!(CodedPhyScan::new(false, false), CodedPhyScan::NotRequested);
}
#[test]
fn match_cache_is_bounded() {
let mut cache = MatchCache::default();
for address in 0..MATCH_CACHE_CAPACITY as u64 {
cache.record(address);
}
cache.record(MATCH_CACHE_CAPACITY as u64);
assert_eq!(cache.addresses.len(), MATCH_CACHE_CAPACITY);
assert!(cache.contains(0));
assert!(!cache.contains(MATCH_CACHE_CAPACITY as u64));
}
}