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!
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.
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