Swift – Bluetooth Low Energy communication using RxSwift and RxBluetoothKit

Posted on Posted in iOS

Overview

In Bluetooth Low Energy communication using Flow Controllers I described how to separate logic like pairing or synchronization from BLE events by splitting code into BluetoothService and FlowController.

In this article I will show you another approach using RxSwift and RxBluetoothKit library which seems to be very promising. It provides a nice API to interact with CoreBluetooth in Rx manner.

If you haven’t tried RxSwift yet, you should first get familiar with basics before trying RxBluetoothKit. It’s easy to get stuck with tons of bugs without that. There is a lot of articles about RxSwift, but you can try to start with this one: Learn & Master ⚔️ the Basics of RxSwift in 10 Minutes.

Why Rx?

  • Bluetooth is in its nature asynchronous and that’s what RxSwift is designed for.
  • Easy to add timeout and retry mechanism.
  • Easy to debug Bluetooth events. By simply adding debug("ble") you can track all events of your flow in output window without adding logs in each BLE delegate method.
  • Easy to cancel and clean up by disposing subscription.
  • Using take and distinctUntilChanged operators you can limit BLE notifications without adding extra flags.
  • Code becomes shorter, because you don’t need to implement all BLE delegate methods and worry about passing data from them to other places in code.
  • You can define whole flow in a declarative manner without maintaining tons of flags in your code.
  • Provides auto-clean up in case of error or completed sequence.

Installation

RxSwift and RxBluetoothKit are both available through CocoaPods, so you can simply install all dependencies by updating your Podfile and running pod install.

RxBluetoothKit Quick Overview

RxBluetoothKit provides wrappers for standard CoreBluetooth classes. The main class to communicate with peripherals is CentralManager (equivalent to CBCentralManager from CoreBluetooth).

CentralManager provides Observables to track Bluetooth state, establish connection, track disconnects and scan for peripherals.

There are also two protocols which can be used to implement enums defining services and characteristics: ServiceIdentifier and CharacteristicIdentifier. Using them we can pass enums instead of UUID directly each time.

You will find also classes like Service, Characteristic, ScannedPeripheral and Peripheral which are basically similar to standard CoreBluetooth classes but with Rx interface.

For more information I recommend you to check the documentation.

Concept

In this article I will show how to implement Pairing Flow which requires the following steps:

  1. Wait for user to turn on Bluetooth.
  2. Scan for peripheral.
  3. Establish connection.
  4. Receive data from encrypted characteristic to ensure that pairing is successful (for more details about pairing read this article).

To make this implementation better, we will implement 30 seconds timeout for steps 2 and 3. Our flow will also notify about current progress.

Implementation

First, we need to import necessary modules in our new file PairingFlow.swift:

Now let’s define enum which will be used to inform about the progress:

Next we need to define Service and Characteristic which will be used to verify pairing.

In this example I use standard GATT Battery Service, however it doesn’t require pairing to access its data, therefore you won’t see iOS pairing alert. You need to replace it with Characteristic which requires encryption in order to trigger pairing on iOS.

We also need an extension to implement timeoutIfNoEvent Rx operator:

When basic definitions are ready we can proceed with implementation of pairing flow:

  1. CentralManager initialization – it will work on main thread.
  2. In this example we don’t want to track Bluetooth state changes during synchronization. We only want to detect when Bluetooth is turned on to trigger further actions. Turning off Bluetooth during synchronization will cause error anyway.
  3. Start scan – in this example we don’t define any required service. However, in production code you should scan only for peripherals containing Service which you expect to avoid connecting with unknown peripherals.
  4. Additional step to filter devices with a name that we expect.
  5. take(1) disposes scan when first peripheral is discovered.
  6. Set timeout. When first event with peripheral is emitted, timeout Observable is disposed and will not emit error.
  7. Notify Observer about the progress.
  8. Sometimes peripherals transfer large data through Characteristic by updating its value multiple times. That’s why you may need to subscribe for notifications. However, it depends on your specific device. If you expect only a single value, you can skip observing value updates.
  9. Skip the first notification, because it should be the same as the value from readValue.
  10. Pairing is finished when the first chunk of data is received. In your case you may need to collect whole data package here. When Observerable is disposed, it automatically disconnects from peripheral.

Usage

Having our PairingFlow implemented we can now try it in ViewController. Using RxCocoa we can easily bind pairing progress to UILabel:

Full source code

You can play with this example by checking out my sample project from GitHub: RxBluetoothDemo.