Im working on ios application that works with BLE device. The device uses BLE indications to provide data to the app. The goal is to achieve 100% data retrieval.
According to the hardware team device behaves like this:
- CCCD Persistence: Device maintains Client Characteristic Configuration Descriptor (CCCD) with indication-enabled state across reconnections
- Resume Point: Device resends indications starting from the last unacknowledged indication before disconnection
- No Custom Logic: Follows standard BLE specification for indication reliability
So it is expected that the device restores the indication streams from the last acknowledged one.
My connection routine is:
- Discover services
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
connectedPeripherals[peripheral.identifier] = peripheral
peripheral.delegate = self
updatePeripheralState(peripheral.identifier, to: .connected)
print("Starting service discovery...")
peripheral.discoverServices(nil)
}
- Discover characteristics:
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error {
print("Characteristic discovery failed for service \(service.uuid): \(error.localizedDescription)")
return
}
guard let characteristics = service.characteristics else {
return
}
for characteristic in characteristics {
if service.uuid == targetServiceUUID && characteristic.uuid == targetCharacteristicUUID {
print("Found target characteristic! Enabling indications...")
peripheral.setNotifyValue(true, for: characteristic)
print(characteristic.properties.description)
}
}
}
Then the data retrieval:
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
print("Error reading characteristic value: \(error.localizedDescription)")
return
}
if characteristic.service?.uuid == targetServiceUUID && characteristic.uuid == targetCharacteristicUUID {
if let data = characteristic.value {
let formatter = DateFormatter()
formatter.timeStyle = .medium
formatter.dateStyle = .none
// data filtering since device is sending some other events sometims
if data.count >= 15 {
let event = decodeBytes(bytes: data)
let now = Date()
let timestamp = timestampFormatter.string(from: now)
print("[\(timestamp)] Auto Increment: \(event.autoIncrement) Type: \(event.type)")
}
} else {
print("Received indication with no data")
}
}
}
Using PacketLogger from xcode toolbox i have confirmed that:
- The device starts sending indications right after didConnect finishes
- The phone is sending ACKS for those indications
- Indications are not reaching didUpdateValueFor until peripheral.setNotifyValue(true, for: characteristic) properly executes
This mekes me drop some data data on each reconnect.
I already know I can do better in terms of service and characteristics discovery: I should discover only that one which is giving me the indications.
But my intuition is: discover only the service and characteristic i care about will minimize the impact, but not guarantee 100% data retrieval
Is this expected and confirmed CoreBluetooth behavior?
It would be expected that didUpdateValueFor()
would not be called until peripheral.setNotifyValue()
is called and executed. So, the problem is with the data that arrives before all the bits are connected together between the app - the iOS stack - the chipset - and the peripheral.
Even if you were to minimize the delay between the connection being established and peripheral.setNotifyValue()
is called, this amount will not be zero, and always carry a risk of dropping a packet.
Do you have enough control on the peripheral firmware to add a "let's go" command to start sending again? Would that work?
Also, if the rediscovery is happening on the same process as before (as in, the app has not terminated), and you have not released, reinstantiated, etc. the CoreBluetooth objects of the peripheral and the characteristic, you may be able to directly call peripheral.setNotifyValue()
on the existing characteristic to start notifying immediately.
But, depending on the frequency of the data transfer, missing data could be inevitable, as there could always be a delay between async interprocess calls between your app, the stack, etc.. Depending on the criticalness of missing any data, you may want to implement a stop/go protocol for when your app is launched and ready.
Argun Tekant / DTS Engineer / Core Technologies