CoreBluetooth peripheral service becomes empty while reconnecting/restoring - ios

In my iOS app I am able to successfully connect to BLE peripherals, discover service and subscribe to characteristics so that app gets notified whenever there is a change in characteristic value. All these happens with no issues as long as app runs in background.
But issue occurs when state restoration happens. In centralManager:willRestoreState: method, I am able to retrieve previously connected peripherals using the method retrieveConnectedPeripheralsWithServices:. But nothing happens when I call discoverServices: method on the retrieved peripheral. peripheral:didDiscoverServices: method never gets called. The value of retrieved peripheral's services property is also null. Does iOS not cache the services and characteristics ?
Note: Our BLE peripheral advertises service initially. When the app launches for first time, it reads value from peripheral and writes user specific data to a characteristic. Post writing, peripheral stops advertising services. But firmware engineer claims that even though service is stopped by peripheral, peripheral retrieved from state restoration should have the cached service. Is it true ?

Have you checked the connection state of the peripherals that you have retrieved via retrieveConnectedPeripheralsWithServices:? The CoreBluetooth framework has some quirks that you need to be aware of, one being the behaviour of the retrieveConnectedPeripheralsWithServices: method:
When you obtain the peripherals via the aforementioned method they might only be connected on the system level (iOS) but not within your app (see the Discussion section in the API documentation). Therefore, you still have to call connect on the peripherals before you can use them properly. Here is the corresponding part from the API documentation:
The list of connected peripherals can include those that are connected by other apps and that will need to be connected locally using the connectPeripheral:options: method before they can be used.
Regarding the caching of services: iOS caches any discovered service and corresponding characteristic. There are only to ways to force iOS to update the cache:
a BLE power cycle, e.g., turning off and on BLE in the Settings App or restarting your iOS device
send a Services Changed notification via the GAP service from the peripheral side (requires an active connection)
Note: You can also activate log messages from the Bluetooth Stack via the Bluetooth Configuration Profile. They can be quite helpful when debugging BLE related issues especially with custom hardware involved (even though the logs are a bit cumbersome to use).

Related

No connectionEventDidOccur for GATT over BR/EDR (aka Classic) using CoreBluetooth

I'm trying to use the iOS 13 introduced capability in CoreBluetooth to do GATT over BR/EDR.
The accessory I'm using obviously implements GATT over BR/EDR (it even publishes its services using SDP).
According to the session WWDC 2019 What's New in Core Bluetooth session, we have to use the registerForConnectionEvents API on a CBCentralManager instance, providing services UUIDs (CBUUIDs) that will be available to use on the BR/EDR accessory.
In the session, they explicitly say:
Your app will have instantiated a CBCentralManager, passed us a known
service UID, and in the case of a BR/EDR or classic device, your user
will go to the Bluetooth settings and search for the device, in this
case let's say it's a headset running heart rate.
They'll discover the device, find it, and attempt to connect.
Pairing will be triggered, and then afterwards when we're connected,
we'll run a service discovery of the GATT services.
If we find a service that you want, then you'll get the delegate
callback.
So when doing a manual connection in the Settings app to the accessory, iOS does a GATT service discovery and if it matches, we receive an associated connectionEventDidOccur delegate callback with the event (peerConnected) and the associated CBPeripheral.
However, this delegate callback never happens.
I'm using the UsingCoreBluetoothClassic sample from Apple, with custom service CBUUIDs implemented by my accessory.
I did multiple attempts: providing only one service CBUUID, severals CBUUIDs, none, generic ones, very specific ones... It never fires.
Note that on Android, it seems to work: connection is possible using the MAC address directly, and a service discovery using SDP lists the accessory services.
Also note that the event fires when I'm scanning using scanForPeripherals and connect to my accessory (because my accessory also does GATT over BLE, but for specific reasons we explicitly want to perform GATT over BR/EDR).
Is there something I have to do on the accessory that I probably missed ?

central unsubscribes from characteristic when restart app

IOS app is a peripheral. After pairing, peripheral can successfully update characteristic value to and indicate to subscribed central via updateValue:forCharacteristic:onSubscribedCentrals:
When I call this method successfully (after pairing), logs show that characteristic had 1 subscribed central, via characteristic.subscribedCentrals.
However, If I use Xcode to stop and start the app again and try to update characteristic value, logs show that characteristic now has 0 subscribed centrals, and central device doesnt do expected behavior so I'm assuming it wasnt received.
How I make sure the central remains subscribed to my services characteristic after reboot/reconnect?
Thanks
I believe that the only way you can do this is to create a bond between the devices. Usually, once the app is killed/restarted, the value of the CCCD (Client Characteristic Configuration Descriptor) is reset and therefore the remote central app needs to re-enable it every time upon connection. However, when the two devices bond, the GATT server stores the information on which CCCDs are enabled, and then upon each subsequent connection those CCCD values are loaded. You can find more information here:-
What does CCCD mean
How to know if the notification/indication of a characteristic is enabled
GATT (Services and Characteristics) - check attribute caching
The ultimate guide to iOS's CoreBluetooth - check Pairing and Bonding

Disconnect time for iOS BLE

I've noticed that when you disconnect for a bluetooth device in an application the iOS device will continue to hold that connection for around 10 seconds. I've attempted to get around this by writing to a characteristic that causes the bluetooth module to cancel the connection with the iOS device instead but that isn't working (mostly because I changed the module and the iOS doesn't see the change because I assume the device is cached somewhere). Is there a way to make it disconnect instantly in code? I am using the swift command
manager.cancelPeripheralConnection(peripheral) currently.
Unfortunately, there isn't. The only way to tell the system to disconnect a peripheral is via the cancelPeripheralConnection method that you are already using. Yet, if you call this method it doesn't necessarily mean that the peripheral will be disconnect.
Background
On iOS the whole BLE connection management is maintained by the operating system. That means that no single application "owns" a connection. All BLE functionality is multiplexed to allow more than one application to gain access to centrals and peripherals.
For example, if you have installed a fitness application that tracks data from your heart rate sensor in the background then you can also "connect" to the heart rate sensor in your app but you will be unable to trigger a real disconnect as long as the fitness app maintains a connection.
The disconnect delay you have noticed is basically an optimization of the operating system. If no application holds a connection to the peripheral anymore it will wait for some time (to avoid unnecessary connect/disconnect cycles) and then trigger the disconnect on the bluetooth chip.
Hope that helps.
In iOS when you call the CBCentralManager method cancelPeripheralConnection(CBPeripheral), it does not always immediately terminate the connection. As Apple's documentation states:
Because other apps may still have a connection to the peripheral, canceling
a local connection does not guarantee that the underlying physical link is
immediately disconnected. From the app’s perspective, however, the
peripheral is considered disconnected, and the central manager object calls
the centralManager:didDisconnectPeripheral:error: method of its delegate
object.
If you have a need to immediately terminate a connection programmatically, say to free up the peripheral to be connected from another central device or to reset one's own security layer, then you need to follow the procedure described in the following StackOverflow thread:
iOS 6 - Bluetooth LE disconnect
which is for the app to send your own proprietary command to the peripheral that tells the peripheral to disconnect through normal means (e.g. "GAPRole_TerminateConnection" or "GAP_TerminateLinkReq" or "LL_Disconnect" or "HCI_Disconnect[_*]" with reason HCI_DISCONNECT_REMOTE_USER_TERM). This always works and is not delayed by the connection supervision timeout because it is a formal disconnection notifying the central device (i.e. iOS). The supervision timeout (up to 6 seconds in iOS; on Android the default is 20 seconds) only comes into play if the disconnection is unplanned as with going out of range or if the peripheral does a disconnect without notifying the remote device as with "LL_EXT_DisconnectImmed" (only available in some BLE peripheral implementations).

CoreBluetooth `retrieveConnectedPeripheralsWithServices` returns disconnected peripherals

I wonder about the semantics of CBManager's retrieveConnectedPeripheralsWithServices. Everytime I'm using this I get CBPeripheral instances back with state CBPeripheralStateDisconnected.
It's not a problem to connect again, but isn't this method supposed to return connected peripherals?
Unfortunately, this is intended behaviour. It is a bit counterintuitive I have to admit but the peripheral state is always related to your app. That means if you have connected/paired/bonded the peripheral using another app or via the bluetooth system settings, it will still show as disconnected within your app. If you have connected some peripherals within your app they will show as connected.

Core Bluetooth iOS - retrieving false peripherals

I'm making an iOS app that uses Core Bluetooth and periodically checks to see if the peripherals are still around. My problem is that when I shut off the advertising device (even shut off the bluetooth entirely), my central device still returns the CBPeripheral object when I call retrievePeripheralsWithIdentifiers. I need it to accurately NOT return this the peripheral once it is no longer advertising. Thanks in advance!
retrievePeripheralsWithIdentifiers looks into the Core Bluetooth database to see if it can find an peripheral with the specified identifier, regardless of whether advertisements from that peripheral are currently visible. This allows you to issue a connect to a peripheral that has been seen before and may be seen again (The "lock" scenario described in the Core Bluetooth Programming Guide is one example where you could use this).
To see if a peripheral is advertising or not you need to call scanForPeripheralsWithServices, specifying YES for CBCentralManagerScanOptionAllowDuplicatesKey and keep your own table of visible peripherals, ageing them out when you haven't seen an advertisement for some period.

Resources