CentralManager won't connect to device for watchOS, but will for iOS?

Hi there, I'm having an issue hoping someone could help. We have an iOS app that uses CoreBluetooth to connect to peripherals using the central manager. The app works great - However, when using the same exact central manager for our watchos app, it will attempt to connect, but I never get a callback for either didConnect or didFailToConnect.

The watch can connect successfully to other BLE devices, so the watch itself is capable of BLE connectivity.

Here's a list of thing's I've tried (unsuccessfully):

1) Added every bluetooth-related entitlement to info.plist

  • Privacy - Bluetooth Always Usage Description
  • Privacy - Bluetooth Peripheral Usage Description
  • Background Modes: App communicates using CoreBluetooth, App shares data using CoreBluetooth

2) Checked for Single-Connection Limits

  • Verified that the iPhone was fully disconnected from the peripheral to ensure the device wasn’t limited to one connection.
  • Attempted to connect on watchOS alone (with iPhone turned off)

3) Tried various options for CBCentralManager, scanForPeripherals, and connect

  • I went through all the keys for various options and tried just setting them, they had no effect
  • CBCentralManagerOptionShowPowerAlertKey, CBConnectPeripheralOptionEnableTransportBridgingKey
  • Item 2

4) Tried .registerForConnectionEvents()

5) Set peripheral's delegate to the central in the didDiscover, stored it in a variable to ensure a strong reference to it

I get no warnings either. The last time I ran into something like this, I found out the watchOS blocks TCP sockets. If I print out the CBPeripheralState a few seconds after trying to connect, it shows its stuck on CBPeripheralStateConnecting.

Any advice or direction is greatly appreciated

Below is the code and various print outs (day 2 into debugging, so it's not pretty)

class WatchBLEManager:NSObject,CBCentralManagerDelegate, ObservableObject{
    var centralManager: CBCentralManager?

    @Published var devices : [String:AtomBLEDevice] = [:]
    private var scanningDevice:AtomBLEDevice?
    var bleStatus:WatchBLEStatus = .blePoweredOff
    
    func startBLE() {
        centralManager = CBCentralManager(delegate: self, queue: nil,options: [CBCentralManagerOptionShowPowerAlertKey: true])
        self.centralManager?.delegate = self
    }
  
    func startScan() {
        self.centralManager?.scanForPeripherals(withServices: [],options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])
        self.centralManager?.delegate = self
    }
    
    func stopScan() {
        print("stopping scan")
        self.centralManager?.stopScan()
        filterName = ""
        scanningDevice = nil
    }
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch (central.state) {
//... other states omitted
        case .poweredOff:
            bleStatus = .blePoweredOff
          //  bleStateDelegate?.didBlePoweredOff()
            for device in devices.values{
                device.isConnected = false
            }
          print("BLE is Powered Off")
        case .poweredOn:
            bleStatus = .blePoweredOn
           // bleStateDelegate?.didBlePoweredOn()
            startScan()
            centralManager?.registerForConnectionEvents()
            
            print("Central supports extended scan and connect: ", CBCentralManager.supports(.extendedScanAndConnect))
           print("powered on")
        @unknown default:
          print("BLE is Unknown")
        }
    }

    private let connectionQueue = DispatchQueue(label: "com.atom.connectionQueue")
    var connectingTo: String? = nil
    
    var peripheral: CBPeripheral? = nil
    
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        guard let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String else { return}

        if localName.contains("Atom") {
                print("\nConnecting to \(localName)")
                print("\tAdvertising data: \(advertisementData)")
                print("\tANCS Authorized: ",peripheral.ancsAuthorized)
                print("\tServices", peripheral.services, "\n")
                self.peripheral = peripheral
                self.peripheral?.delegate = self
            
               // central.registerForConnectionEvents()
              //  central.delegate = self
                peripheral.delegate = self
                DispatchQueue.main.async {
                   // central.connect(peripheral)
                    self.centralManager?.connect(peripheral, options: [                                                                     CBConnectPeripheralOptionEnableTransportBridgingKey: true])
                }
            DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
                print("\tState", String(describing: peripheral.state))
                    print("Connected Peripherals: \(self.centralManager?.retrieveConnectedPeripherals(withServices: []))")
                }
        }


    }
    
    // Never gets called for watchos
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        
        print("Connected to peripheral: \(peripheral.identifier)")
        if let atomDevice = getAtomBLEDevice(peripheral: peripheral) {
            //atomDevice.setPeripheral(perpipheral: <#T##CBPeripheral?#>)
            atomDevice.isConnected = true
            atomDevice.isConnecting = false
            //delegate?.didConnected(atomBLE: atomDevice!)
            atomDevice.startDiscoveringService()
            //atomDevice?.delegate?.didConnected(atomBLE: atomDevice!)
            print("Connected: \(peripheral.name)")
        } else {
            print("no matching atom device found for didConnect")
            print("connected peripheral :",peripheral.identifier.uuidString)
            
        }

    }
    
    func centralManager(_ central: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral) {
        print("Connection event: \(event)")
    }

    
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: (any Error)?) {
        print("Failed to connect: \(error?.localizedDescription)")
    }
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        let atomDevice = getAtomBLEDevice(peripheral: peripheral)
        atomDevice?.isConnected = false
        print("Peripheral disconnected:\(peripheral.name)")
    }
    
    
    func clearData() {
        filterName = ""
        for device in devices.values{
            disconnect(atomBLEDevice: device)
            device.perpipheral?.delegate = nil
        }
        devices  = [:]
        scanningDevice = nil
      //  delegate  = nil
        centralManager = nil
    }
}
extension WatchBLEManager: CBPeripheralDelegate {
    
}```

CoreBluetooth on watchOS does not work the same way it does on iOS. There are more limitations, and things work differently. So, the code you have that works on iOS will not work on watchOS as-is.

The best would be to watch WWDC sessions Connect Bluetooth devices to Apple Watch and the superceding Get timely alerts from Bluetooth devices on watchOS (the second one changes things a lot, but the first one is still good for the basics), and the sample project Interacting with Bluetooth peripherals during background app refresh


Argun Tekant /  DTS Engineer / Core Technologies

CentralManager won't connect to device for watchOS, but will for iOS?
 
 
Q