Dec 05, 2016

Read From A Bluetooth LE Device with CoreBluetooth

Apple's CoreBluetooth Programming Guide is pretty good, but it can be overwhelming if you're diving into Bluetooth LE (BLE) for the first time. One potential point of confusion is that some objects are needed to connect to another device, and other objects are needed to turn your phone into a BLE device. I'm am not going to cover all the things you can do with CoreBluetooth, but just follow one common use case: connecting to a device and querying it for the values of whatever properties (or characteristics) it keeps.

Here the overview: a CBCentralManager is queried to discover nearby devices. Each found device is presented as an instance of CBPeripheral. Peripherals are then queried to find available services, which are returned as instances of CBService. A service is queried to find available characataristics, presented as CBCharacteristic. Finally, with your characteristics, you can query the peripheral to read the value.

It's not that hard once you've done it. The main difficulty is that you have to make sure you're listening on the right delegate method for the object you just messaged. Just keeping the right method pairs in mind during this extended call-and-response is most of the battle. Here's how it happens in detail:

First I create a CBCentralManager. I store it in an instance variable because there will be a lot of back-and-forth delegate calls.

self.central = CBCentralManager(delegate: self, queue: nil)
// Passing nil to queue: uses the main queue. Use a background
// queue in your shipping app

There is one required delegate method to implement: centralManagerDidUpdateState(_ central: CBCentralManager). I'll implement others too, but first the required method:

func centralManagerDidUpdateState(_ central: CBCentralManager) {
    switch central.state {
    case .poweredOn:
        central.scanForPeripherals(withServices: nil, options: nil)
    default:
        print("Bluetooth couldn't be powered on and I'm too " +
              "lazy to do prooper error handling")
    }
}

Here I'm told the device is powered on. I'm ready to start scanning with central.scanForPeripherals(withServices: nil, options: nil). Notice that I provide nil here for the list of services. For a shipping app this is frowned upon becaue it is power intensive. While I'm poking around I'm going to ask for all the data I can get.

One I ask for devices, the CBCentralManager responds with a delegate callback.

    func centralManager(_ central: CBCentralManager,
           didDiscover peripheral: CBPeripheral,
                advertisementData: [String : Any],
                        rssi RSSI: NSNumber) {

        self.peripiherals.append(peripheral)
    }

I get a callback for each peripheral, containing an instance of CBPeripiheral, along with some data: the RSSI and the advertisement data. I'm going to ignore the extra information and just append the peripheral to an array.

Now I want to connect to my peripheral. I'll pick one and use the CBCentralManager to connect to the peripheral.

let peripheral = peripherals[index]
self.centralManager.connect(peripheral options: nil)

Again I await notification via a callback from the CBCentralManager. Here's my CBCentralManagerDelegate method:

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {

        // Assume I have a peripheralDelegate
        // Maybe it's my view controller
        peripheral.delegate = peripheralDelegate
        central.stopScan() // Be kind to your battery.
        peripheral.discoverServices(nil)
}

Once the peripheral connects, I discover all the service by passing nil to discoverServices(). Again, I can save power by asking only for the services I need. My current quest for knowledge outweighs the need for power efficiency.

I wait to hear back from a delegate call, this time not from the CBCentralManager, but from the CBPeripheral1.

Here's my delegate method from CBPeripheralDelegate for discovered services. Notice that I am not passed the discovered services directly, but they are in a property on the peripheral.

func peripheral(_ peripheral: CBPeripheral,
                didDiscoverServices error: Error?) {
    guard let services = peripheral.services else { return }
    // Use service
}

Now that I have a service, I query the service for characteristics with discoverCharacteristics2. Here I pick one and ask for all characteristics for that service.

service = services[idx]
peripheral.discoverCharacteristics(nil, for: service)

As before, passing nil requests everything: in this case all characteristics. I implement aother delegate function from CBPeripheralDelegate to listen for characteristics:

func peripheral(_ peripheral: CBPeripheral,
        didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    guard let characteristics = peripheral.characterists else { return }
}

Almost done! I have an array of characteristics, so I can finally ask for the value of one of these characteristics. I'll just pick one out of the array and try to read it:

let characteristic = characteristics[idx]
peripheral.readValue(for: characteristic)

Another delegate callback on my CBPeripheralDelgate receives the actual data.

func peripheral(_ peripheral: CBPeripheral,
        didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        guard let data = characteristic.value else { return }
        // Woohoo! Data!
}

Yay! I finally have my data, in the form of a Data object. Now it's up to me to interpret that data. Is it a Float? Int? Signed or unsiged? Maybe it's a String. I wonder how it's encoded...

To recap, here are the steps for all this back and forth.

  1. Create a CBCentralManager.
  2. Make sure the Bluetooth radio is on, then discover peripherals.
  3. Get an instance of a CBPeripheral from a CBCentralManagerDelegate callback.
  4. Tell the central manager to connect to the peripheral.
  5. Get notified of the connection in a CBCentralManagerDelegate callback.
  6. Query the peripheral about included services.
  7. Get notified of discovered peripherals in a CBPeripheralDelegate callback.
  8. Query the peripheral about charactieristics in a given service.
  9. Get notified of discovered characteristics in a CBPeripheralDelegate callback.
  10. Query the peripheral about the value of a give characteristic.
  11. Get notified of the value in a CBPeripheralDelgate callback.
  12. Interperet data.

  1. This little handoff can require thought. I find that I'm switching view controllers as I switch from the CBCentralManager to the CBPeripheral, and the two controllers need to touch both the CBCentralManager and the CBPeripheral. Take some time to plan out the proper ownership. I'll ignore all such complications for now. 

  2. You can also query a service for other services with discoverIncludedServices(_:for:). I won't be doing that here.