I have an iOS app that uses Core Bluetooth to connect to a V.BTTN smart button. Everything worked perfectly fine in iOS 10, but since iOS 11 has come out the restoration process seems to break.
When I launch my app, with a previously paired button, the OS does in fact call the centralManager:willRestoreState, and the accompanying dictionary includes a pointer to a CBPeripheral object that has a status of connected. Just as it did back in iOS 10. However, the problem I am running into is when I call the discoverServices on the peripheral I am never returned any services in the peripheral:didDiscoverServices method. In fact that method, nor any other method, is called at all.
I have been searching all of the internet and have found people having similar issues with button pairing an connections, but those have generally been issues with improper lifecycle management of the peripheral object. I believe I have everything setup correct and at a loss. Does anyone have any idea what could be going on here?
import Foundation
import CoreBluetooth
class SmartButtonManager: NSObject {
//MARK: - Singleton
static let sharedManager = SmartButtonManager()
//MARK: - Properties
var discoveredDevices: [CBPeripheral] {
get {
return perfs
}
}
fileprivate(set) var isBluetoothOn = false
//MARK: - Private Constants & Variables
fileprivate var connectedButtonUUID: String?
fileprivate let queue = DispatchQueue(label: "V.BTTN", qos: .background, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
fileprivate var manager: CBCentralManager?
fileprivate var perfs = [CBPeripheral]()
fileprivate var connectedPerf: CBPeripheral?
fileprivate var isButtonReady = false
//MARK: - Initialization
override init() {
super.init()
}
//MARK: - Configure
func configure(withLaunchOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) {
// create new central manager
manager = CBCentralManager(delegate: self, queue: queue, options: [CBCentralManagerOptionRestoreIdentifierKey: managerIdentifier])
}
}
//MARK: - V.BTTN
extension SmartButtonManager: CBCentralManagerDelegate, CBPeripheralDelegate {
func scanForVbttn() {
perfs.removeAll()
manager?.scanForPeripherals(withServices: services, options: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOff:
print("[centralManagerDidUpdateState] CB BLE hardware is powered off")
perfs.removeAll()
isBluetoothOn = false
case .poweredOn:
print("[centralManagerDidUpdateState] CB BLE hardware is powered on. Start scanning for peripherals")
isBluetoothOn = true
case .unauthorized:
print("[centralManagerDidUpdateState] CB BLE hardware is not authorized")
case .unsupported:
print("[centralManagerDidUpdateState] CB BLE hardware is not supported")
isBluetoothOn = false
default:
print("[centralManagerDidUpdateState] CB BLE hardware state is unknown")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("[centralManager:didDiscover peripheral:advertisementData] CB BLE did discover peripheral with advertisement data: \(advertisementData)")
guard let perfName = advertisementData[CBAdvertisementDataLocalNameKey] as? String else {
print("[centralManager:didDiscover peripheral:advertisementData] peripheral name is unknown")
return
}
if perfName.contains("V.ALRT") {
peripheral.delegate = self
perfs.append(peripheral)
if connectedButtonUUID == perfName {
connect(peripheral: peripheral)
}
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("[central:didConnect peripheral:] CB BLE hardware did connect")
handleDidConnect(toPeripheral: peripheral)
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
print("[central:didFailToConnect error:] CB BLE peripheral did failed to connect with error: \(String(describing: error))")
connectedPerf = nil
isButtonReady = false
connectedButtonUUID = nil
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
print("[central:didDisconnectPeripheral peripheral:] CB BLE peripheral did disconnect")
connectedPerf = nil
isButtonReady = false
connectedButtonUUID = nil
}
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
print("[central:willRestoreState dict:] CB BLE hardware will restore state")
print("\(dict)")
guard let ps = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else {
print("[central:willRestoreState dict:] No perfs to restore")
return
}
print("[central:willRestoreState dict:] Will restore perfs")
perfs = ps
print("[central:willRestoreState dict:] Attempt to reconnect to V.BTTN")
print("[central:willRestoreState dict:] perfs \(perfs)")
for p in perfs {
if p.name == connectedButtonUUID {
print("[central:willRestoreState dict:] Connect to perf \(p)")
handleDidConnect(toPeripheral: p)
break
}
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let services = peripheral.services else {
print("[peripheral:didDiscoverServices error:] CB BLE peripheral unable to discover services")
return
}
print("[peripheral:didDiscoverServices error:] BLE peripheral did discover services")
for s in services {
print("[peripheral:didDiscoverServices error:] CB BLE Service \(s.description)")
peripheral.discoverCharacteristics(nil, for: s)
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
print("[peripheral:didDiscoverCharacteristicsFor service:] CB BLE did discover characteristics for service \(service.uuid.description)")
if compareCBUUID(uuid1: service.uuid, uuid2: CBUUID(string: BLE_VSN_GATT_SERVICE_UUID)) {
guard let characteristics = service.characteristics else {
return
}
for aChar in characteristics {
// write the verification key
if aChar.uuid.isEqual(CBUUID(string: BLE_VERIFICATION_SERVICE_UUID)) {
self.writeVerificationKey(forPeripheral: peripheral, characteristic: aChar)
self.enable(peripheral: peripheral)
break
}
}
}
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
//NOTE: This code has been omitted as there is little need to see this
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
print("[peripheral:didWriteValueFor characteristic:] characteristic: \(characteristic.uuid)")
if let e = error {
print("[peripheral:didWriteValueFor characteristic:] error: \(e)")
}
}
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if !characteristic.isNotifying {
print("[peripheral:didUpdateNotificationStateFor:][Characteristic is not notifiying so cancel the connection]")
manager?.cancelPeripheralConnection(peripheral)
}
}
//MARK: - Helpers
fileprivate func writeVerificationKey(forPeripheral peripheral: CBPeripheral, characteristic: CBCharacteristic) {
print("[peripheral:didDiscoverCharacteristicsFor service:] Write verification key")
let data = NSData(bytes: [0x80,0xBE,0xF5,0xAC,0xFF] as [UInt8], length: 5)
peripheral.writeValue(data as Data, for: characteristic, type: CBCharacteristicWriteType.withResponse)
}
fileprivate func handleDidConnect(toPeripheral peripheral: CBPeripheral) {
if let n = peripheral.name {
connectedButtonUUID = n
}
connectedPerf = peripheral
peripheral.delegate = self
peripheral.discoverServices(nil)
isButtonReady = true
}
fileprivate func enable(peripheral: CBPeripheral) {
guard let services = peripheral.services else {
return
}
for service: CBService in services {
if compareCBUUID(uuid1: service.uuid, uuid2: CBUUID(string: BLE_VSN_GATT_SERVICE_UUID)) {
guard let characteristics = service.characteristics else {
return
}
for aChar in characteristics {
if aChar.uuid.isEqual(CBUUID(string: BLE_KEYPRESS_DETECTION_UUID)) {
// enable button
print("[peripheral:didDiscoverCharacteristicsFor service:] Enable short press")
let data = NSData(bytes: [0x02] as [UInt8], length: 1)
peripheral.writeValue(data as Data, for: aChar, type: CBCharacteristicWriteType.withResponse);
} else if aChar.uuid.isEqual(CBUUID(string: BLE_SILENT_NORMAL_MODE)) {
// enable button
print("[peripheral:didDiscoverCharacteristicsFor service:] Enable normal mode")
let data = NSData(bytes: [0x00] as [UInt8], length: 1)
peripheral.writeValue(data as Data, for: aChar, type: CBCharacteristicWriteType.withResponse);
} else if aChar.uuid.isEqual(CBUUID(string: BLE_FALL_KEYPRESS_DETECTION_UUID)) {
// enable fall detection
print("[peripheral:didDiscoverCharacteristicsFor service:] Enable fall detection")
peripheral.setNotifyValue(true, for: aChar)
}
}
}
}
checkBatteryLevel()
}
func disconnectVbttn() {
guard let peripheral = connectedPerf else {
return
}
setVbttnToStealthMode()
manager?.cancelPeripheralConnection(peripheral)
isButtonReady = false
}
fileprivate func compareCBUUID(uuid1: CBUUID, uuid2: CBUUID) -> Bool {
if (uuid1.data as NSData).isEqual(to: uuid2.data) {
return true
}
return false
}
func stopScanning() {
manager?.stopScan()
}
}