diff --git a/Cargo.toml b/Cargo.toml index ed77ba06..4ec4a882 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ objc2-core-bluetooth = { version = "0.2.2", default-features = false, features = ] } [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.61", features = ["Devices_Bluetooth", "Devices_Bluetooth_GenericAttributeProfile", "Devices_Bluetooth_Advertisement", "Devices_Radios", "Foundation_Collections", "Foundation", "Storage_Streams"] } +windows = { version = "0.61", features = ["Devices_Bluetooth", "Devices_Bluetooth_GenericAttributeProfile", "Devices_Bluetooth_Advertisement", "Devices_Enumeration", "Devices_Radios", "Foundation_Collections", "Foundation", "Storage_Streams"] } windows-future = "0.2.0" [dev-dependencies] diff --git a/src/api/mod.rs b/src/api/mod.rs index bb3acf68..c364320d 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -359,6 +359,10 @@ pub trait Central: Send + Sync + Clone { /// Stops scanning for BLE devices. async fn stop_scan(&self) -> Result<()>; + /// Retrieve connected peripherals matching the given filter. Same filter and discovery rules + /// apply as for start_scan. + async fn connected_peripherals(&self, filter: ScanFilter) -> Result<()>; + /// Returns the list of [`Peripheral`]s that have been discovered so far. Note that this list /// may contain peripherals that are no longer available. async fn peripherals(&self) -> Result>; diff --git a/src/common/adapter_manager.rs b/src/common/adapter_manager.rs index d50c660e..1058861a 100644 --- a/src/common/adapter_manager.rs +++ b/src/common/adapter_manager.rs @@ -73,6 +73,10 @@ where .collect() } + pub fn clear_peripherals(&self) { + self.peripherals.clear(); + } + // Only used on windows and macOS/iOS, so turn off deadcode so we don't get warnings on android/linux. #[allow(dead_code)] pub fn peripheral_mut( diff --git a/src/corebluetooth/adapter.rs b/src/corebluetooth/adapter.rs index b8626bd1..f3220e00 100644 --- a/src/corebluetooth/adapter.rs +++ b/src/corebluetooth/adapter.rs @@ -92,6 +92,10 @@ impl Adapter { sender: adapter_sender, }) } + + pub fn clear_cache(&self) { + self.manager.clear_peripherals(); + } } #[async_trait] @@ -118,6 +122,14 @@ impl Central for Adapter { Ok(()) } + async fn connected_peripherals(&self, filter: ScanFilter) -> Result<()> { + self.sender + .to_owned() + .send(CoreBluetoothMessage::RetrieveConnectedPeripherals { filter }) + .await?; + Ok(()) + } + async fn peripherals(&self) -> Result> { Ok(self.manager.peripherals()) } diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 93597bc7..9ac28439 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -14,7 +14,7 @@ use super::{ future::{BtlePlugFuture, BtlePlugFutureStateShared}, utils::{ core_bluetooth::{cbuuid_to_uuid, uuid_to_cbuuid}, - nsuuid_to_uuid, + nsstring_to_string, nsuuid_to_uuid, }, }; use crate::api::{CharPropFlags, Characteristic, Descriptor, ScanFilter, Service, WriteType}; @@ -399,6 +399,9 @@ pub enum CoreBluetoothMessage { filter: ScanFilter, }, StopScanning, + RetrieveConnectedPeripherals { + filter: ScanFilter, + }, ConnectDevice { peripheral_uuid: Uuid, future: CoreBluetoothReplyStateShared, @@ -1169,6 +1172,9 @@ impl CoreBluetoothInternal { }, CoreBluetoothMessage::StartScanning{filter} => self.start_discovery(filter), CoreBluetoothMessage::StopScanning => self.stop_discovery(), + CoreBluetoothMessage::RetrieveConnectedPeripherals{filter} => { + self.retrieve_connected_peripherals(filter); + }, CoreBluetoothMessage::ConnectDevice{peripheral_uuid, future} => { trace!("got connectdevice msg!"); self.connect_peripheral(peripheral_uuid, future); @@ -1239,6 +1245,49 @@ impl CoreBluetoothInternal { trace!("BluetoothAdapter::stop_discovery"); unsafe { self.manager.stopScan() }; } + + fn retrieve_connected_peripherals(&mut self, filter: ScanFilter) { + trace!("BluetoothAdapter::retrieve_connected_peripherals"); + let service_uuids = scan_filter_to_service_uuids(filter); + if service_uuids.is_none() { + warn!("MacOS requires a filter of services to be provided, so we cannot continue."); + return; + } + let peripherals = unsafe { + self.manager + .retrieveConnectedPeripheralsWithServices(service_uuids.as_deref().unwrap()) + }; + + for peripheral in peripherals { + let uuid = nsuuid_to_uuid(unsafe { &peripheral.identifier() }); + trace!("Discovered connected peripheral: {}", uuid); + let (event_sender, event_receiver) = mpsc::channel(256); + let name: Option = unsafe { + match peripheral.name() { + Some(ns_name) => nsstring_to_string(ns_name.as_ref()), + None => None, + } + }; + if !self.peripherals.contains_key(&uuid) { + self.peripherals.insert( + uuid, + PeripheralInternal::new(Retained::from(peripheral), event_sender), + ); + } + let discovered_device = CoreBluetoothEvent::DeviceDiscovered { + uuid, + name, + event_receiver, + }; + // Must use a synchronous sender. + match self.event_sender.try_send(discovered_device) { + Ok(_) => (), + Err(e) => { + error!("Error sending discovered device event: {}", e); + } + } + } + } } /// Convert a `ScanFilter` to the appropriate `NSArray *` to use for discovery. If the diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index d840c403..465d06cf 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -19,11 +19,15 @@ use crate::{ }; use async_trait::async_trait; use futures::stream::Stream; +use log::trace; use std::convert::TryInto; use std::fmt::{self, Debug, Formatter}; use std::pin::Pin; use std::sync::{Arc, Mutex}; +use uuid::Uuid; use windows::{ + Devices::Bluetooth::BluetoothLEDevice, + Devices::Enumeration::DeviceInformation, Devices::Radios::{Radio, RadioState}, Foundation::TypedEventHandler, }; @@ -68,6 +72,10 @@ impl Adapter { radio, }) } + + pub fn clear_cache(&self) { + self.manager.clear_peripherals(); + } } impl Debug for Adapter { @@ -114,6 +122,150 @@ impl Central for Adapter { Ok(()) } + async fn connected_peripherals(&self, filter: ScanFilter) -> Result<()> { + let base_selector = BluetoothLEDevice::GetDeviceSelector() + .map_err(|e| Error::Other(format!("GetDeviceSelector failed: {:?}", e).into()))?; + let aqs = format!( + "{} AND System.Devices.Aep.IsConnected:=System.StructuredQueryType.Boolean#True", + base_selector.to_string() + ); + + // Query all BLE devices that are currently connected to the system + let devices = DeviceInformation::FindAllAsyncAqsFilter(&windows::core::HSTRING::from(aqs)) + .map_err(|e| Error::Other(format!("FindAllAsyncAqsFilter failed: {:?}", e).into()))? + .get() + .map_err(|e| Error::Other(format!("FindAllAsync().get() failed: {:?}", e).into()))?; + + let manager = self.manager.clone(); + let required_services: Vec = filter.services.clone(); + + trace!( + "Scanning for connected peripherals with {} service filters", + required_services.len() + ); + + // Iterate through each connected device + for device in devices { + let device_id = match device.Id() { + Ok(id) => id, + Err(e) => { + trace!("Failed to get device ID: {:?}", e); + continue; + } + }; + trace!("Checking connected device: {:?}", device_id); + + // BluetoothLEDevice from the device ID + let ble_device = match BluetoothLEDevice::FromIdAsync(&device_id) { + Ok(async_op) => match async_op.get() { + Ok(dev) => dev, + Err(e) => { + trace!("FromIdAsync.get() failed for {:?}: {:?}", device_id, e); + continue; + } + }, + Err(e) => { + trace!("FromIdAsync failed for {:?}: {:?}", device_id, e); + continue; + } + }; + + // Double-check the connection status + match ble_device.ConnectionStatus() { + Ok(status) + if status + == windows::Devices::Bluetooth::BluetoothConnectionStatus::Connected => {} + Ok(_) => { + trace!("Device {:?} not connected, skipping", device_id); + continue; + } + Err(e) => { + trace!("Failed to get connection status: {:?}", e); + continue; + } + } + + // Service filtering logic: + // - If no services specified in filter, accept all connected devices + // - Otherwise, accept only if the device has at least one matching service + let mut accept_device = required_services.is_empty(); + + if !accept_device { + // Query the device's GATT services to check for matches + let services_result = match ble_device.GetGattServicesAsync() { + Ok(async_op) => async_op.get(), + Err(e) => { + trace!("GetGattServicesAsync failed: {:?}", e); + continue; + } + }; + + let services = match services_result { + Ok(gatt_services) => match gatt_services.Services() { + Ok(service_list) => service_list, + Err(e) => { + trace!("Failed to get Services list: {:?}", e); + continue; + } + }, + Err(e) => { + trace!("GetGattServicesAsync.get() failed: {:?}", e); + continue; + } + }; + + // Check if any of the device's services match the filter + for service in &services { + if let Ok(guid) = service.Uuid() { + let service_uuid = Uuid::from_u128(guid.to_u128()); + if required_services.contains(&service_uuid) { + trace!("Found matching service: {:?}", service_uuid); + accept_device = true; + break; + } + } + } + } + + if !accept_device { + trace!("Device does not match service filter, skipping"); + continue; + } + + // Convert Bluetooth address to BDAddr + let address: BDAddr = match ble_device.BluetoothAddress() { + Ok(addr) => match (addr as u64).try_into() { + Ok(bd_addr) => bd_addr, + Err(_) => { + trace!("Failed to convert Bluetooth address: {}", addr); + continue; + } + }, + Err(e) => { + trace!("BluetoothAddress() failed: {:?}", e); + continue; + } + }; + + // Update the peripheral in the manager + match manager.peripheral_mut(&address.into()) { + Some(_) => { + trace!("Peripheral already exists in manager: {:?}", address); + manager.emit(CentralEvent::DeviceDiscovered(address.into())); + } + None => { + trace!("Adding new peripheral: {:?}", address); + let peripheral = Peripheral::new(Arc::downgrade(&manager), address); + manager.add_peripheral(peripheral); + manager.emit(CentralEvent::DeviceDiscovered(address.into())); + } + } + } + + trace!("Finished scanning for connected peripherals"); + Ok(()) + } + async fn peripherals(&self) -> Result> { Ok(self.manager.peripherals()) }