Alec Graves
Alec Graves

Reputation: 152

How to stop ios core-bluetooth CBCentralManagerDelegate from disconnecting in ios13

The Issue

I'm using core-bluetooth to transmit sensor data (an array of 64-128 bytes) to an iPad (6th gen) at a rate of 6 Hz. It was working fine in ios 12. After I updated to ios 13, the bluetooth connection became incredibly unreliable. The connection would only transmit 20 bytes at a time, and it would frequently disconnect (centralManager(_ central: CBCentralManager, didDisconnectPeripheral... is called) with Error Code=0 "Unknown error." UserInfo={NSLocalizedDescription=Unknown error.}.

Looking at the iPad screen during debugging, I noticed every time the central connects and my service is discovered, a security prompt saying "Bluetooth Pairing Request: "<device-name>" would like to pair with your iPad. [Cancel] [Pair]" pops up for half a second before the disconnect described above.

So it would seem that for some reason, core bluetooth is triggering a security prompt and (i'm guessing) the delay causes the connection to fail? The strange thing is this prompt happens like 19/20 times, but occasionally the connection goes through without triggering the prompt and a couple buffers are received before disconnecting.

Related Problems

There is another post about something similar: how to prevent the permission request in ios 13, but it looks like this is more due to the peripheral based on the error message.

There is a post detailing a missing value in the CBCentralManager didDiscover peripheral dictionary, but I am not using that value anyway.

I am definitely storing a hard-reference to the peripheral, so that is not the issue here. I have also opened other apps that allow you to browse bluetooth4.0 peripheral gatt servers. LightBlue, BLE Scanner, and BLE Hero all exhibit the bluetooth connection prompt followed by a disconnect from the peripheral on my iPad 6th gen.

My Implementation

This project is part of a school class. I have all of the code on gitlab. The critical ios bluetooth code is below, and the project is on gitlab.


import Foundation
import CoreBluetooth

struct Peripheral {
    var uuid: String
    var name: String
    var rssi: Double
    var peripheral: CBPeripheral
}

class SensorsComputer: BLECentral {
    // just a rename of below...
}

class BLECentral: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
    var manager: CBCentralManager!
    var availablePeripherals: [String: Peripheral]
    var sensorsConnected: Bool
    var ref: CBPeripheral?
    var sensorServiceUUIDs: [String]
    var bleSensorComputerUUID: String
    var data: [String:[UInt8]]
    var dataNames: [String:[String:String]]

    override init() {
        data = [:]
        availablePeripherals = [:]
        sensorsConnected = false
        ref = nil
        sensorServiceUUIDs = ["5CA4"]
        //bleSensorComputerUUID = "096A1CFA-C6BC-A00C-5A68-3227F2C58C06" // builtin is shit
        bleSensorComputerUUID = "33548EF4-EDDE-6E6D-002F-DEEFC0A7AF99" // usb dongle

        dataNames = [
            // service uuids
            "5CA4": [
                // characteristic uuids
                "0000": "LidarRanges",
            ],
        ]

        super.init()
        manager = CBCentralManager(delegate: self, queue: nil)
        self.connect(uuid:bleSensorComputerUUID)
    }

    func scan(){
        let services = dataNames.map { CBUUID(string:$0.key) }
        manager.scanForPeripherals(withServices: services, options: nil)
    }

    func connect_internal_(uuid: String) -> Bool{ // TODO known bt device uuids.
        if self.sensorsConnected {
            // do not try to connect if already connected
            return true
        } else {
            print(self.availablePeripherals.count)
            if let found = self.availablePeripherals[uuid] {
                manager.stopScan()
                manager.connect(found.peripheral, options: nil)
                return true
            } else {
                if availablePeripherals.count == 0 {
                    scan()
                }
                print("Error! no peripheral with \(uuid) found!")
                return false
            }
        }
    }

    func connect(uuid: String){
        let queue = OperationQueue()
        queue.addOperation() {
            // do something in the background
            while true {
                //usleep(100000)
                sleep(1)
                OperationQueue.main.addOperation {
                    self.connect_internal_(uuid: self.bleSensorComputerUUID)
                }
            }
        }
    }

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn:
            print("The central is powered on!")
            scan() // automatically start scanning for BLE devices
        default:
            print("The centraol is NOT powered on (state is \(central.state))")
        }
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        var name = ""
        if let name_ = peripheral.name {
            name = name_
        }

        let uuid = peripheral.identifier.uuidString
        let rssi = Double(truncating: RSSI)
        availablePeripherals[peripheral.identifier.uuidString] = Peripheral(uuid: uuid, name: name, rssi: rssi, peripheral: peripheral)
        print(uuid, rssi)
    }

    func getSorted(uuids:Bool = false) -> [Peripheral] {
        let peripherals = Array(availablePeripherals.values)
        return peripherals.sorted(by:) {$0.rssi >= $1.rssi}
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("Central connected!")
        sensorsConnected = true
        peripheral.delegate = self
        var cbuuids:[CBUUID] = []
        for id in sensorServiceUUIDs {
            cbuuids.append(CBUUID(string: id))
        }
        peripheral.discoverServices(cbuuids) // TODO store service uuids somewhere nicer
    }

    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        if let e = error {
            print("Central disconnected because \(e)")
        } else {
            print("Central disconnected! (no error)")
        }
        sensorsConnected = false
        availablePeripherals = [:]
    }

    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        print("Central failed to connect...")
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let error = error {
            print("Peripheral could not discover services! Because: \(error.localizedDescription)")
        } else {
            peripheral.services?.forEach({(service) in
                print("Service discovered \(service)")
                // TODO characteristics UUIDs known
                peripheral.discoverCharacteristics(nil, for: service)
            })
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if let error = error {
            print("Could not discover characteristic because error: \(error.localizedDescription)")
        } else {
            service.characteristics?.forEach({ (characteristic) in
                print("Characteristic: \(characteristic)")
                if characteristic.properties.contains(.notify){
                    peripheral.setNotifyValue(true, for: characteristic)
                }
                if characteristic.properties.contains(.read){
                    peripheral.readValue(for: characteristic)
                }
                if characteristic.properties.contains(.write){
                    peripheral.writeValue(Data([1]), for: characteristic, type: .withResponse)
                }
            })

        }
    }


    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error {
            print("error after didUpdateValueFor characteristic \(characteristic): \(error.localizedDescription)")
        } else {
            let sname = characteristic.service.uuid.uuidString
            let cname = characteristic.uuid.uuidString
            guard let _dname = dataNames[sname], let dataName = _dname[cname] else {
                return
            }
            //print("value of characteristic \(cname) in service \(sname) was updated.")
            if let value = characteristic.value {
                let value_arr = Array(value)
                //print("    The new data value is \(value_arr.count) bytes long")
                //print("dataname is \(dataName)")
                self.data[dataName] = value_arr
            }
        }
    }
}

The BLE peripheral code is a node project based on bleno and rosnodejs for sensor data. The peripheral code is also on gitlab.

I have rolled back to several known working project versions in git, and nothing appears to work after updating to ios 13. I am going to try to run the corebluetooth code on a mac project to test if it will work there.

Question

So, in conclusion, after updating to ios 13 from ios 12.4, when connecting to a BLE peripheral, a bluetooth connection prompt is opened even though the communication is bluetooth 4.0 / BLE. Along with this prompt, the connection drops as soon as it starts, sometimes giving me a few partial data buffers and sometimes giving me nothing.

The BLE connection not properly initiated, and if the prompt for connection did work correctly, it would interfere with my ability to connect to a device (robot sensors) automatically without the user input.

If anyone knows some option in info.plist to prevent this breaking connection prompt from appearing, that would be great. I am at a loss here, is there an easy way to roll-back ios versions? Or some way to get Apple INC to release an emergency patch preventing the popup for core-bluetooth low-energy? Or some way to work around the popup and allow the user to properly authenticate without breaking BLE protocol?

Thanks to anyone who takes a look at this.

Upvotes: 1

Views: 2160

Answers (1)

Matt Whitlock
Matt Whitlock

Reputation: 78

I may not have the full solution, but this should help point in the right direction.

I assume the code has NSBluetoothAlwaysUsageDescription set in Info.plist, which is now required in iOS 13. NSBluetoothPeripheralUsageDescription was required prior to iOS 13.

The prompt you're seeing before it goes away is caused by the pairing process, and could be triggered by two things:

  1. The peripheral tries to initiate the pairing process (discouraged by Apple in 25.10 of Accessory Design Guidelines)

  2. The app has tried to access a characteristic that is encrypted and has been denied. This triggers the pairing process and hence the alert that pops up.

There are a few things that could cause the connection to be disconnected before you can tap the Pair button.

  • The app is not holding on to the CBPeripheral reference (you indicate that it is though), and you would see an API misuse log message if this were the case.

  • The peripheral is disconnecting before you have a chance to respond. This is more likely, and you could sniff the BLE packets to see for sure. More details below.

  • There may be a timing issue where the app is trying to proceed with requests before the pairing process is complete and that could trigger the disconnect. To test this possibility, add a delay after service and characteristic discovery is complete before making additional read/write/notify requests.

Most likely the peripheral is disconnecting, and that could happen during the key exchange process that happens when you work with secured characteristics if there is a key mismatch. If the keys change for some reason (I typically see it with a firmware update, perhaps something on the peripheral side changed, possibly iOS update to 13), then this behavior could happen. If you go to Bluetooth settings and forget the device, the keys will be discarded so the key exchange process may then start working again. If so, then there were stale keys, most likely on the iOS side that had to be renegotiated.

The final option is to temporarily make sure services and characteristics are not secured. I did not see the secure: [...] in your code though so I'm not sure about that.

If none of the easy options fixes it, my next step would be sniffing packets.

Upvotes: 3

Related Questions