First things first: running OSX 10.10.4, iOS 4, Xcode 6.3.2, iPhone 6, Swift
Short story: I have a certain Bluetooth LE device here from which I want to receive notifications when values of a Characteristic change, e.g. by user input. Trying to subscribe to it does not succeed, but rather yields an error Error Domain=CBATTErrorDomain Code=10 "The attribute could not be found."
Long story: So, I have a BluetoothManager class in which I start scanning for Peripherals as soon as my $CBCentralManager.state
is .PoweredOn
. That's easy, I'm even a good citizen and scan specifically for those with the Service I want
centralManager.scanForPeripheralsWithServices([ServiceUUID], options: nil)
Hoping this will succeed, I implemented the following delegate method:
func centralManager(central: CBCentralManager!, didDiscoverPeripheral peripheral: CBPeripheral!, advertisementData: [NSObject : AnyObject]!, RSSI: NSNumber!) {
if *this is a known device* {
connectToPeripheral(peripheral)
return
}
[...] // various stuff to make something a known device, this works
}
So moving along, we get to:
func connectToPeripheral(peripheral: CBPeripheral) {
println("connecting to \(peripheral.identifier)")
[...] // saving the peripheral in an array along the way so it is being retained
centralManager.connectPeripheral(peripheral, options: nil)
}
Yupp, this succeeds, so I get the confirmation and start to discover the Service:
func centralManager(central: CBCentralManager!, didConnectPeripheral peripheral: CBPeripheral!) {
println("Connected \(peripheral.name)")
peripheral.delegate = self
println("connected to \(peripheral)")
peripheral.discoverServices([BluetoothConstants.MY_SERVICE_UUID])
}
Which also works, since that delegate method gets called as well:
func peripheral(peripheral: CBPeripheral!, didDiscoverServices error: NSError!) {
if peripheral.services != nil {
for service in peripheral.services {
println("discovered service \(service)")
let serviceObject = service as! CBService
[...] // Discover the Characteristic to send controls to, this works
peripheral.discoverCharacteristics([BluetoothConstants.MY_CHARACTERISTIC_NOTIFICATION_UUID], forService: serviceObject)
[...] // Some unneccessary stuff about command caches
}
}
}
And what do you know: the characteristic gets discovered!
func peripheral(peripheral: CBPeripheral!, didDiscoverCharacteristicsForService service: CBService!, error: NSError!) {
for characteristic in service.characteristics {
let castCharacteristic = characteristic as! CBCharacteristic
characteristics.append(castCharacteristic) // Retaining the characteristic in an Array as well, not sure if I need to do this
println("discovered characteristic \(castCharacteristic)")
if *this is the control characteristic* {
println("control")
} else if castCharacteristic.UUID.UUIDString == BluetoothConstants.MY_CHARACTERISTIC_NOTIFICATION_UUID.UUIDString {
println("notification")
peripheral.setNotifyValue(true, forCharacteristic: castCharacteristic)
} else {
println(castCharacteristic.UUID.UUIDString) // Just in case
}
println("following properties:")
// Just to see what we are dealing with
if (castCharacteristic.properties & CBCharacteristicProperties.Broadcast) != nil {
println("broadcast")
}
if (castCharacteristic.properties & CBCharacteristicProperties.Read) != nil {
println("read")
}
if (castCharacteristic.properties & CBCharacteristicProperties.WriteWithoutResponse) != nil {
println("write without response")
}
if (castCharacteristic.properties & CBCharacteristicProperties.Write) != nil {
println("write")
}
if (castCharacteristic.properties & CBCharacteristicProperties.Notify) != nil {
println("notify")
}
if (castCharacteristic.properties & CBCharacteristicProperties.Indicate) != nil {
println("indicate")
}
if (castCharacteristic.properties & CBCharacteristicProperties.AuthenticatedSignedWrites) != nil {
println("authenticated signed writes ")
}
if (castCharacteristic.properties & CBCharacteristicProperties.ExtendedProperties) != nil {
println("indicate")
}
if (castCharacteristic.properties & CBCharacteristicProperties.NotifyEncryptionRequired) != nil {
println("notify encryption required")
}
if (castCharacteristic.properties & CBCharacteristicProperties.IndicateEncryptionRequired) != nil {
println("indicate encryption required")
}
peripheral.discoverDescriptorsForCharacteristic(castCharacteristic) // Do I need this?
}
}
Now the console output up until here looks like this:
connected to <CBPeripheral: 0x1740fc780, identifier = $FOO, name = $SomeName, state = connected>
discovered service <CBService: 0x170272c80, isPrimary = YES, UUID = $BAR>
[...]
discovered characteristic <CBCharacteristic: 0x17009f220, UUID = $BARBAR properties = 0xA, value = (null), notifying = NO>
control
following properties:
read
write
[...]
discovered characteristic <CBCharacteristic: 0x17409d0b0, UUID = $BAZBAZ, properties = 0x1A, value = (null), notifying = NO>
notification
following properties:
read
write
notify
[...]
discovered DescriptorsForCharacteristic
[]
updateNotification: false
Hey! It says updateNotification
is false
. Where does that come from? Why, it's my callback for setNotify...
:
func peripheral(peripheral: CBPeripheral!, didUpdateNotificationStateForCharacteristic characteristic: CBCharacteristic!, error: NSError!) {
println("updateNotification: \(characteristic.isNotifying)")
}
What gives? I told it to be notifying! Why isn't it notifying? Let's set a breakpoint in the line with the println and check out the error object:
(lldb) po error
Error Domain=CBATTErrorDomain Code=10 "The attribute could not be found." UserInfo=0x17026eac0 {NSLocalizedDescription=The attribute could not be found.}
OK, so this leaves me out of ideas. I wasn't able to finde relevant clues regarding that error code. The description itself I cannot fathom since I tried to set up the notification for a Characteristic that I discovered earlier, hence it must exist, right? Also, on Android it seems possible to subscribe for notifications, so I guess I can rule out problems with the device... or can I? Any clues regarding this are truly appreciated!
For me the issue was that I was using another Android device as the peripheral and needed to implement configuration descriptor. See here: https://stackoverflow.com/a/25508053/599743
Having received more information from the manufacturer, I have gathered that the device is connected and sending out notifications regardless of the communicated notification state of the characteristic. Which means that even though peripheral(_, didUpdateNotificationStateForCharacteristic, error)
is telling me the Characteristic is not notifying, peripheral(_, didUpdateValueForCharacteristic, error)
is getting called and delivering data when the device sends it. I had not implemented that callback previously, as I did not expect data to be sent in this state.
So basically, my problem seems to have solved itself. Nevertheless, I'm still interested if the device's behaviour is according to the Bluetooth LE specifications or not; I have no insight into those lower levels of implementation but suppose that Apple wrote their docs for some reason to at least strongly imply that a change in the Characteristic's state will preceed the reception of any data.
Though it's an old question and solved still I would like to provide my inputs and comments.
First thing first, BLE Peripheral is responsible for defining behaviour of it's Characteristics. Peripheral has option to define Characteristics property. Various supported properties for Characteristic are listed in Apple documentation here.
Now let's focus on your question, You are trying to listen to Characteristic value change but subscription fails.
The problem which I see in your code is, a subscription is made regardless of Characteristics support it or not. Subscribe to characteristic only if it's supported. Refer following code block which shows how to identify supported property for characteristic and perform the operation based on it.
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
// Check if characteristics found for service.
guard let characteristics = service.characteristics, error == nil else {
print("error: \(String(describing: error))")
return
}
// Loop over all the characteristics found.
for characteristic in characteristics {
if characteristic.properties.contains([.notify, .notifyEncryptionRequired]) {
peripheral.setNotifyValue(true, for: characteristic)
} else if characteristic.properties.contains([.read]) {
peripheral.readValue(for: characteristic)
} else if characteristic.properties.contains([.write]) {
// Perform write operation
}
}
}
If Characteristic supports notify
property and you subscribe to it. On successful subscription following delegate is invoked where you can verify subscription status.
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
// Check subscription here
print("isNotifying: \(characteristic.isNotifying)")
}
If a subscription is successful, whenever characteristic is updated following delegate method will be invoked with a new value.
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
// read updated value for characteristic.
}
Few things to check if it's not working
I have tried to answer as much as possible in all aspects. Hope it helps. Happy coding :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With