From 912cc15e1e1f82fbdde926f99ab3b96a30839ab3 Mon Sep 17 00:00:00 2001 From: Corbin Halliwill Date: Fri, 30 May 2025 13:11:15 -0700 Subject: [PATCH 1/5] Retrieve connected devices on macOS --- src/api/mod.rs | 4 +++ src/corebluetooth/adapter.rs | 8 ++++++ src/corebluetooth/internal.rs | 53 ++++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) 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/corebluetooth/adapter.rs b/src/corebluetooth/adapter.rs index b8626bd1..ab10a0ed 100644 --- a/src/corebluetooth/adapter.rs +++ b/src/corebluetooth/adapter.rs @@ -118,6 +118,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..a6778c16 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,51 @@ 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() }); + if self.peripherals.contains_key(&uuid) { + trace!("Peripheral {} already exists, skipping.", uuid); + continue; + } + 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, + } + }; + 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 From f13f19ac219953c5344c7516db71d8c2c3063f56 Mon Sep 17 00:00:00 2001 From: Corbin Halliwill Date: Mon, 2 Jun 2025 13:43:43 -0700 Subject: [PATCH 2/5] Retrieve connected peripherals on Windows --- Cargo.toml | 2 +- src/winrtble/adapter.rs | 74 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) 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/winrtble/adapter.rs b/src/winrtble/adapter.rs index d840c403..a984418a 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, }; @@ -114,6 +118,76 @@ impl Central for Adapter { Ok(()) } + async fn connected_peripherals(&self, filter: ScanFilter) -> Result<()> { + let device_selector = BluetoothLEDevice::GetDeviceSelector()?; + let get_devices = match DeviceInformation::FindAllAsyncAqsFilter(&device_selector) { + Ok(devices) => devices, + Err(e) => { + return Err(Error::Other(format!("{:?}", e).into())); + } + }; + let devices = match get_devices.get() { + Ok(devices) => devices, + Err(e) => { + return Err(Error::Other(format!("{:?}", e).into())); + } + }; + let manager = self.manager.clone(); + + for device in devices { + let Ok(device_id) = device.Id() else { + continue; + }; + let Ok(ble_device) = BluetoothLEDevice::FromIdAsync(&device_id) else { + continue; + }; + let Ok(ble_device) = ble_device.get() else { + continue; + }; + let Ok(services_result_op) = ble_device.GetGattServicesAsync() else { + continue; + }; + let Ok(services_result) = services_result_op.get() else { + continue; + }; + let Ok(services) = services_result.Services() else { + continue; + }; + + for service in services { + let Ok(s_uuid) = service.Uuid() else { + continue; + }; + let service_uuid = Uuid::from_u128(s_uuid.to_u128()); + if !filter.services.is_empty() && !filter.services.contains(&service_uuid) { + // Skip if services filter is provided, but it does not contain service_uuid. + continue; + } + let Ok(bluetooth_address) = ble_device.BluetoothAddress() else { + continue; + }; + let address: BDAddr = match bluetooth_address.try_into() { + Ok(address) => address, + Err(_) => { + continue; + } + }; + match manager.peripheral_mut(&address.into()) { + Some(_) => { + trace!("Skipping over existing peripheral: {:?}", address); + } + None => { + let peripheral = Peripheral::new(Arc::downgrade(&manager), address); + manager.add_peripheral(peripheral); + manager.emit(CentralEvent::DeviceDiscovered(address.into())); + } + } + return Ok(()); + } + } + Ok(()) + } + async fn peripherals(&self) -> Result> { Ok(self.manager.peripherals()) } From e417dd942ccff7ceb87d8509db6e93a2a7b0905d Mon Sep 17 00:00:00 2001 From: Corbin Halliwill Date: Mon, 2 Jun 2025 15:06:59 -0700 Subject: [PATCH 3/5] Make sure service filter is satisfied on Windows --- src/winrtble/adapter.rs | 63 ++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index a984418a..d2b8e39d 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -133,7 +133,8 @@ impl Central for Adapter { } }; let manager = self.manager.clone(); - + let mut required_services = filter.clone().services; + required_services.sort(); for device in devices { let Ok(device_id) = device.Id() else { continue; @@ -153,37 +154,53 @@ impl Central for Adapter { let Ok(services) = services_result.Services() else { continue; }; - + // Verify all services in filter exist on the device. + let mut found_services: Vec = Vec::new(); for service in services { let Ok(s_uuid) = service.Uuid() else { continue; }; let service_uuid = Uuid::from_u128(s_uuid.to_u128()); - if !filter.services.is_empty() && !filter.services.contains(&service_uuid) { - // Skip if services filter is provided, but it does not contain service_uuid. - continue; + if filter.services.contains(&service_uuid) { + found_services.push(service_uuid); } - let Ok(bluetooth_address) = ble_device.BluetoothAddress() else { + } + found_services.sort(); + trace!( + "Found required services: {:?} of {:?}", + found_services.len(), + required_services.len() + ); + if (required_services.len() != found_services.len()) + || !(required_services + .iter() + .zip(found_services.iter()) + .all(|(l, r)| l == r)) + { + trace!("Not all required services are accounted for, continuing..."); + continue; + } + + let Ok(bluetooth_address) = ble_device.BluetoothAddress() else { + continue; + }; + let address: BDAddr = match bluetooth_address.try_into() { + Ok(address) => address, + Err(_) => { continue; - }; - let address: BDAddr = match bluetooth_address.try_into() { - Ok(address) => address, - Err(_) => { - continue; - } - }; - match manager.peripheral_mut(&address.into()) { - Some(_) => { - trace!("Skipping over existing peripheral: {:?}", address); - } - None => { - let peripheral = Peripheral::new(Arc::downgrade(&manager), address); - manager.add_peripheral(peripheral); - manager.emit(CentralEvent::DeviceDiscovered(address.into())); - } } - return Ok(()); + }; + match manager.peripheral_mut(&address.into()) { + Some(_) => { + trace!("Skipping over existing peripheral: {:?}", address); + } + None => { + let peripheral = Peripheral::new(Arc::downgrade(&manager), address); + manager.add_peripheral(peripheral); + manager.emit(CentralEvent::DeviceDiscovered(address.into())); + } } + return Ok(()); } Ok(()) } From 4a12ed6a617c0104396d3448610dd6d60c23a76f Mon Sep 17 00:00:00 2001 From: Corbin Halliwill Date: Mon, 2 Jun 2025 16:29:51 -0700 Subject: [PATCH 4/5] Re-emit already discovered devices when asking for connected devices --- src/winrtble/adapter.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index d2b8e39d..4f77a634 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -193,6 +193,7 @@ impl Central for Adapter { match manager.peripheral_mut(&address.into()) { Some(_) => { trace!("Skipping over existing peripheral: {:?}", address); + manager.emit(CentralEvent::DeviceDiscovered(address.into())); } None => { let peripheral = Peripheral::new(Arc::downgrade(&manager), address); From 8fb582d8bf7b8d6b41796ca4fe539945b343a280 Mon Sep 17 00:00:00 2001 From: Corbin Halliwill Date: Mon, 2 Jun 2025 16:32:43 -0700 Subject: [PATCH 5/5] Also on Mac, emit already discovered peripherals. This feels more expected to me, otherwise if you ask for connected peripherals a second time you will not get alerts for the previously found peripherals. --- src/corebluetooth/internal.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index a6778c16..9ac28439 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -1260,10 +1260,6 @@ impl CoreBluetoothInternal { for peripheral in peripherals { let uuid = nsuuid_to_uuid(unsafe { &peripheral.identifier() }); - if self.peripherals.contains_key(&uuid) { - trace!("Peripheral {} already exists, skipping.", uuid); - continue; - } trace!("Discovered connected peripheral: {}", uuid); let (event_sender, event_receiver) = mpsc::channel(256); let name: Option = unsafe { @@ -1272,10 +1268,12 @@ impl CoreBluetoothInternal { None => None, } }; - self.peripherals.insert( - uuid, - PeripheralInternal::new(Retained::from(peripheral), event_sender), - ); + if !self.peripherals.contains_key(&uuid) { + self.peripherals.insert( + uuid, + PeripheralInternal::new(Retained::from(peripheral), event_sender), + ); + } let discovered_device = CoreBluetoothEvent::DeviceDiscovered { uuid, name,