Subscribing for notifications from a CBCharacteristic does not work
Asked Answered
P

3

9

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!

Prognostic answered 7/7, 2015 at 17:4 Comment(5)
It looks like you might not be retaining a reference to the peripheral. Paulw11 has a great answer for that #26377970Amentia
Nope, I'm doing that; I left that out for brevity: I've got Arrays for Peripherals in the discovered, connecting and connected State and make sure to keep the Peripheral in the appropriate one during the setup.Prognostic
Your code looks all right and it may be a big on either your device or on iOS. My go-to tool for testing is the LightBlue app. You can use this to discover your peripheral and services and see if it can set a notify on your characteristic.Memorabilia
Great hint. Funnily enough, LightBlue can connect to another characteristic for notifications, but for the characteristic in question, tapping on Listen for notifications simply does not change the text. Looks like the problem is not with my code, hm? I'll try to check with the manufacturer.Prognostic
For me it was problem with the peripheral which was reseting the notify flag, so I had to set notify to true every time.Carefree
A
5

For me the issue was that I was using another Android device as the peripheral and needed to implement configuration descriptor. See here: https://mcmap.net/q/1315778/-any-way-to-implement-ble-notifications-in-android-l-preview

Antecede answered 12/4, 2016 at 13:47 Comment(0)
P
3

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.

Prognostic answered 22/7, 2015 at 12:26 Comment(0)
P
3

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

  • Check with Peripheral Manufacturer about characteristic implementations and if it supports necessary properties.
  • Check your implementation and make sure you are following all the necessary steps.

I have tried to answer as much as possible in all aspects. Hope it helps. Happy coding :)

Puzzlement answered 10/4, 2020 at 5:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.