From ce08f42e8f6a07ef3a86c17a86e6babee8f7b5a6 Mon Sep 17 00:00:00 2001 From: Andrew Fischer Date: Thu, 8 Jan 2026 16:19:17 -0600 Subject: [PATCH 1/2] Split service discovery out from connection in corebluetooth and handle didModifyServices --- src/api/mod.rs | 2 + src/corebluetooth/central_delegate.rs | 25 +++++++++- src/corebluetooth/internal.rs | 71 ++++++++++++++++++++++++--- src/corebluetooth/peripheral.rs | 26 ++++++++-- 4 files changed, 111 insertions(+), 13 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index bb3acf68..785cba34 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -319,6 +319,8 @@ pub enum CentralEvent { DeviceUpdated(PeripheralId), DeviceConnected(PeripheralId), DeviceDisconnected(PeripheralId), + /// Only emitted on the corebluetooth subsystem + DeviceServicesModified(PeripheralId), /// Emitted when a Manufacturer Data advertisement has been received from a device ManufacturerDataAdvertisement { id: PeripheralId, diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index 9dacd721..599e12cb 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -68,6 +68,9 @@ pub enum CentralDelegateEvent { service_uuids: Vec, rssi: i16, }, + ServicesModified { + peripheral_uuid: Uuid, + }, // DiscoveredIncludedServices(Uuid, HashMap>), DiscoveredCharacteristics { peripheral_uuid: Uuid, @@ -265,6 +268,10 @@ impl Debug for CentralDelegateEvent { .field("service_uuids", service_uuids) .field("rssi", rssi) .finish(), + CentralDelegateEvent::ServicesModified { peripheral_uuid } => f + .debug_struct("ServicesModified") + .field("peripheral_uuid", peripheral_uuid) + .finish(), CentralDelegateEvent::DescriptorNotified { peripheral_uuid, service_uuid, @@ -335,7 +342,6 @@ declare_class!( peripheral_debug(peripheral) ); unsafe { peripheral.setDelegate(Some(ProtocolObject::from_ref(self))) }; - unsafe { peripheral.discoverServices(None) } let peripheral_uuid = nsuuid_to_uuid(unsafe { &peripheral.identifier() }); self.send_event(CentralDelegateEvent::ConnectedDevice { peripheral_uuid }); } @@ -721,6 +727,23 @@ declare_class!( }); } } + + #[method(peripheral:didModifyServices:)] + fn delegate_peripheral_didmodifyservices( + &self, + peripheral: &CBPeripheral, + _invalidated_services: &NSArray, + ) { + trace!( + "delegate_peripheral_didmodifyservices {}", + peripheral_debug(peripheral), + ); + // NOTE: This is a corebluetooth-only even that makes the peripheral unusable until service discovery has been performed again. + // https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate/peripheral(_:didmodifyservices:)?language=objc + self.send_event(CentralDelegateEvent::ServicesModified { + peripheral_uuid: nsuuid_to_uuid(unsafe { &peripheral.identifier() }), + }); + } } ); diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 93597bc7..48629f00 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -148,7 +148,8 @@ impl CharacteristicInternal { pub enum CoreBluetoothReply { AdapterState(CBManagerState), ReadResult(Vec), - Connected(BTreeSet), + Connected, + ServicesDiscovered(BTreeSet), State(CBPeripheralState), Ok, Err(String), @@ -161,6 +162,7 @@ pub enum PeripheralEventInternal { ManufacturerData(u16, Vec, i16), ServiceData(HashMap>, i16), Services(Vec, i16), + ServicesModified, } pub type CoreBluetoothReplyStateShared = BtlePlugFutureStateShared; @@ -178,6 +180,7 @@ struct PeripheralInternal { pub event_sender: Sender, pub disconnected_future_state: Option, pub connected_future_state: Option, + pub services_discovered_future_state: Option, } impl Debug for PeripheralInternal { @@ -194,6 +197,10 @@ impl Debug for PeripheralInternal { ) .field("event_sender", &self.event_sender) .field("connected_future_state", &self.connected_future_state) + .field( + "services_discovered_future_state", + &self.services_discovered_future_state, + ) .finish() } } @@ -209,6 +216,7 @@ impl PeripheralInternal { event_sender, connected_future_state: None, disconnected_future_state: None, + services_discovered_future_state: None, } } @@ -285,7 +293,7 @@ impl PeripheralInternal { // back a Connected reply to the waiting future with all of the // characteristic info in it. if !self.services.values().any(|service| !service.discovered) { - if self.connected_future_state.is_none() { + if self.services_discovered_future_state.is_none() { panic!("We should still have a future at this point!"); } let services = self @@ -317,12 +325,12 @@ impl PeripheralInternal { .collect(), }) .collect(); - self.connected_future_state + self.services_discovered_future_state .take() .unwrap() .lock() .unwrap() - .set_reply(CoreBluetoothReply::Connected(services)); + .set_reply(CoreBluetoothReply::ServicesDiscovered(services)); } } @@ -452,6 +460,10 @@ pub enum CoreBluetoothMessage { data: Vec, future: CoreBluetoothReplyStateShared, }, + DiscoverServices { + peripheral_uuid: Uuid, + future: CoreBluetoothReplyStateShared, + }, } #[derive(Debug)] @@ -566,6 +578,23 @@ impl CoreBluetoothInternal { } } + async fn on_services_modified(&mut self, peripheral_uuid: Uuid) { + trace!( + "Peripheral modified services and must be rediscovered! {:?}", + peripheral_uuid + ); + if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { + p.services.clear(); + if let Err(e) = p + .event_sender + .send(PeripheralEventInternal::ServicesModified) + .await + { + error!("Error sending notification event: {}", e); + } + } + } + async fn on_discovered_peripheral( &mut self, peripheral: Retained, @@ -673,9 +702,20 @@ impl CoreBluetoothInternal { } } - fn on_peripheral_connect(&mut self, _peripheral_uuid: Uuid) { - // Don't actually do anything here. The peripheral will fire the future - // itself when it receives all of its service/characteristic info. + fn on_peripheral_connect(&mut self, peripheral_uuid: Uuid) { + if self.peripherals.contains_key(&peripheral_uuid) { + let peripheral = self + .peripherals + .get_mut(&peripheral_uuid) + .expect("If we're here we should have an ID"); + peripheral + .connected_future_state + .take() + .unwrap() + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Connected); + } } fn on_peripheral_connection_failed( @@ -1085,6 +1125,15 @@ impl CoreBluetoothInternal { } } + fn discover_services(&mut self, peripheral_uuid: Uuid, fut: CoreBluetoothReplyStateShared) { + if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { + trace!("Discovering services!"); + p.services_discovered_future_state = Some(fut); + // This will trigger the delegate_peripheral_diddiscoverservices in central_delegate.rs + unsafe { p.peripheral.discoverServices(None) }; + } + } + async fn wait_for_message(&mut self) { select! { delegate_msg = self.delegate_receiver.select_next_some() => { @@ -1108,7 +1157,7 @@ impl CoreBluetoothInternal { self.on_discovered_characteristic_descriptors(peripheral_uuid, service_uuid, characteristic_uuid, descriptors) } CentralDelegateEvent::ConnectedDevice{peripheral_uuid} => { - self.on_peripheral_connect(peripheral_uuid) + self.on_peripheral_connect(peripheral_uuid) }, CentralDelegateEvent::ConnectionFailed{peripheral_uuid, error_description} => { self.on_peripheral_connection_failed(peripheral_uuid, error_description) @@ -1146,6 +1195,9 @@ impl CoreBluetoothInternal { CentralDelegateEvent::Services{peripheral_uuid, service_uuids, rssi} => { self.on_services(peripheral_uuid, service_uuids, rssi).await }, + CentralDelegateEvent::ServicesModified{peripheral_uuid} => { + self.on_services_modified(peripheral_uuid).await + }, CentralDelegateEvent::DescriptorNotified{ peripheral_uuid, service_uuid, @@ -1205,6 +1257,9 @@ impl CoreBluetoothInternal { data, future, } => self.write_descriptor_value(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, data, future), + CoreBluetoothMessage::DiscoverServices{peripheral_uuid, future} => { + self.discover_services(peripheral_uuid, future); + } }; } } diff --git a/src/corebluetooth/peripheral.rs b/src/corebluetooth/peripheral.rs index 5ef24288..f66e3fca 100644 --- a/src/corebluetooth/peripheral.rs +++ b/src/corebluetooth/peripheral.rs @@ -160,6 +160,10 @@ impl Peripheral { services, }); } + Some(PeripheralEventInternal::ServicesModified) => { + shared.services.lock().unwrap().clear(); + shared.emit_event(CentralEvent::DeviceServicesModified(shared.uuid.into())); + } Some(PeripheralEventInternal::Disconnected) => (), None => { info!("Event receiver died, breaking out of corebluetooth device loop."); @@ -251,8 +255,7 @@ impl api::Peripheral for Peripheral { }) .await?; match fut.await { - CoreBluetoothReply::Connected(services) => { - *(self.shared.services.lock().map_err(Into::::into)?) = services; + CoreBluetoothReply::Connected => { self.shared .emit_event(CentralEvent::DeviceConnected(self.shared.uuid.into())); } @@ -285,8 +288,23 @@ impl api::Peripheral for Peripheral { } async fn discover_services(&self) -> Result<()> { - // TODO: Actually discover on this, rather than on connection - Ok(()) + let fut = CoreBluetoothReplyFuture::default(); + self.shared + .message_sender + .to_owned() + .send(CoreBluetoothMessage::DiscoverServices { + peripheral_uuid: self.shared.uuid, + future: fut.get_state_clone(), + }) + .await?; + match fut.await { + CoreBluetoothReply::ServicesDiscovered(services) => { + *(self.shared.services.lock().unwrap()) = services.clone(); + return Ok(()); + } + CoreBluetoothReply::Err(msg) => return Err(Error::RuntimeError(msg)), + _ => panic!("Shouldn't get anything but discovered or err!"), + } } async fn write( From 910fe9a00ac848546c607465d09cf54d8f2bddf6 Mon Sep 17 00:00:00 2001 From: Andrew Fischer Date: Fri, 9 Jan 2026 11:36:32 -0600 Subject: [PATCH 2/2] Improve the comments around corebluetooth didmodifyservices --- src/corebluetooth/central_delegate.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index 599e12cb..fd719321 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -738,8 +738,11 @@ declare_class!( "delegate_peripheral_didmodifyservices {}", peripheral_debug(peripheral), ); - // NOTE: This is a corebluetooth-only even that makes the peripheral unusable until service discovery has been performed again. + // This is a corebluetooth-only event that makes peripheral services unusable until discovery has been performed again. // https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate/peripheral(_:didmodifyservices:)?language=objc + // Trigger the removal of internal corebluetooth peripheral discovered services. It is also expected that + // discover_services() will be performed again on the peripheral at the API level as soon as is practical. + // NOTE: the list of modified services does not appear to be particularly useful; a full service rediscovery is needed. self.send_event(CentralDelegateEvent::ServicesModified { peripheral_uuid: nsuuid_to_uuid(unsafe { &peripheral.identifier() }), });