Search code examples
iosswiftbluetooth-lowenergycore-bluetoothcbcentralmanager

iOS BluetoothLE: CBCentralManager Unsubscribes From Updates


I have a mac set up as a bluetooth accessory using CoreBluetooth as a CBPeripheralManager. The Mac is advertising on a set characteristic CBUUID, and once it has a subscriber, I click a button to stream a UTF-8-encoded time stamp every half second.

I have an iPhone set up as a CBCentralManager subscribing to the appropriate characteristic. I update the iPhone's UI with the decoded timestamp every time it receives the data and the app is active. The bluetooth-central background mode is set in the .plist file.

The iPhone continues to debug print and update the UI for the timestamps for about 25 seconds, then just stops. This happens whether the app is in the foreground or the background. EDIT: The CBPeripheralManager receives an didUnsubscribeFrom characteristic callback at this time. I don't see any reason didUnsubscribeFrom would be called, no idea why it's always after 25 seconds.

The CBPeripheralManager continues to merrily send its time stamps. The return value from the CBPeripheralManager's updateData(_:for:onSubscribedCentrals:) call is always true, indicating that the queue never is full.

There's a lot of code involved here, showing the most relevant.

From the CBCentralManager app:

 func centralManagerDidUpdateState(_ central: CBCentralManager) {
     print(central.state.rawValue)
     centralManager.scanForPeripherals(withServices: [timeUUID], options: nil)
     print("scanning")
 }

 func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
     peripheral.delegate = self
     peripheral.discoverServices(nil)

     print("connected")
 }

 func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
     print(peripheral.name as Any)
     print(advertisementData[CBAdvertisementDataServiceUUIDsKey] as! Array<CBUUID>)
     self.peripheralController = peripheral

     self.centralManager.connect(peripheral, options: nil)
 }

 func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {

    if service.uuid.uuidString == timeUUID.uuidString {
        peripheralController.setNotifyValue(true, for: service.characteristics!.first!)
        peripheralController.readValue(for: service.characteristics!.first!) // EDIT: This was the offending line
    }
 }

 func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {

    if characteristic.uuid.uuidString == timeUUID.uuidString {
        if let valueFrom = characteristic.value  {
            if let this = String(data: valueFrom, encoding: .utf8) {
                if UIApplication.shared.applicationState == .active {
                    label.text = this
                    print("ACTIVE \(this)")
                } else if UIApplication.shared.applicationState == .background {
                    print("BACKGROUND \(this)")
                } else if UIApplication.shared.applicationState == .inactive {
                    print("INACTIVE \(this)")
                }
            }
        }
    }
 }

From the CBPeripheralManager app:

func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
    myCharacteristic = CBMutableCharacteristic(type: myServiceUUID, properties: [CBCharacteristicProperties.read, CBCharacteristicProperties.notify], value: nil, permissions: CBAttributePermissions.readable)
    let myService = CBMutableService(type: myServiceUUID, primary: true)
    myService.characteristics = [myCharacteristic]
    bluetoothController.add(myService)
}

func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
    print(characteristic.uuid)
    subscriber = central
}

func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
    print(characteristic)
    print("unsubscribe")
}

func repeatAdvertisement() {
    timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [unowned self] (timerRef) in
        guard let maybeTimer = self.timer, maybeTimer.isValid else { return }
        let datum = Date()
        let stringFromDate = self.dateFormatter.string(from: datum)
        let data = stringFromDate.data(using: .utf8)
        print(data!.count)
        //        myCharacteristic.value = data
        let myService = CBMutableService(type: self.myServiceUUID, primary: true)
        myService.characteristics = [self.myCharacteristic]
        let did = self.bluetoothController.updateValue(data!, for: self.myCharacteristic as! CBMutableCharacteristic, onSubscribedCentrals: [self.subscriber])
        print("timed \(stringFromDate) \(did)")
    }


}


func advertise() {
    if timer == nil {
        repeatAdvertisement()
    } else {
        timer?.invalidate()
        timer = nil
    }
}

Let me know anything else you need.


Solution

  • Okay, for heaven's sake. The issue was the line peripheralController.readValue(for: service.characteristics!.first!) I had that line in the app based on some sample code and, well, it was unnecessary.

    Apparently the call to readValue(for:) causes some sort of timeout. I edited that line out of the app and it happily updates on and on.

    Leaving the question up and adding this answer in case anyone ends up facing the same thing someday.