iOS Smartwatch App using CoreBluetooth and RxSwift

Elanur Yoktan
Delivery Hero Tech Hub
9 min readMar 23, 2022

This article will help you to get a general understanding of data transfer procedures and formats in Bluetooth Low Energy (BLE) connection and how to build an application for your BLE device with CoreBluetooth and RxSwift. Also, you will find a little bit of reverse engineering thanks to the PacketLogger. I hope you enjoy it!

I have a smartwatch (Everest W311 Fit Mate) that I bought years ago and haven’t used for a long time. I have recently decided to use it again, but its application was outdated and had problems with connection and UI. I thought it would be fun to play with it and try to develop my own application that will monitor heart rate, sleep quality, and steps and allow customizing the settings.

GATT

Let me tell you a little bit about GATT(The Generic Attribute Profile). It is a profile that defines data transfer procedures and formats between a client(central) and a server(peripheral) in a BLE connection. The server contains the data to be monitored and listens to requests from clients and sends responses back. The Client can discover the services of the Server and send requests and receive responses. So basically in this project, the watch is our server and the phone is our client.

A server contains data in form of attributes. Attributes are the smallest addressable pieces of information that are actually transferred between devices. Services and Characteristics are defined to organize this data in a practical manner. You can see the illustration of the data hierarchy that is defined by GATT.

image source: https://www.oreilly.com/library/view/getting-started-with/9781491900550/ch04.html#gatt_data_hierarchy

Each service with a 16-bit UUID(Universally Unique Identifier)s contains characteristics that have related user data. Characteristic is a container for user data and may have descriptors that provide meta-information about the characteristic.

Access permission determines whether the client can read and write attribute values. Each attribute can have one of the following access permissions: none, write, read, and both. For characteristics, to understand which operations are allowed, characteristic properties were defined. Besides Read and Write properties, there are also Write without response, Notify and Indicate. Notify and Indicate properties are particularly important because these operations are initiated by the server, but the client first must activate them using the descriptors described in the Client Characteristic Configuration Descriptor.

For further information about GATT, I suggest this reading Getting Started with Bluetooth Low Energy

Sniffing BLE packets

From some previous projects on BLE, I knew an application named nRF Connect by Nordic Semiconductor which provides users to explore GATT services and characteristics and runs on both iOS and Android. It is a really handy application if you have no documentation about the GATT services of the device.

In the first step, to understand the GATT services of the device, I used nRF Connect. Below you can see the services and characteristics that I explored thanks to the application.

When I try to be notified of the characteristic that named heart rate measurement via nRF Connect, could not see any value or value updating. But after enabling the client characteristic configuration, I got all updates instantly. Client characteristic configuration acts as a switch that enables or disables server-initiated updates, but only for the feature in which it is located. After the bit is set to 1 (true), the server responds with a write response and starts sending the appropriate packets if it wants to inform the client about a value change. After I start to develop the application realized that CoreBluetooth already handles this with setNotifyValue(), so there is no need to write a value to this descriptor. If you still try to set this configuration with writeValue() you will get an error like below.

Although nRF Connect really help me to understand services and characteristics of the device, I could not figure out how can I read step count or how can I change the settings. What I need was sniffing of Bluetooth packets that the current app sending or receiving from the device to understand which characteristic stands for. Further Googling, I came across the PacketLogger which is presented at a session of WWDC19. It was exactly what I need, PacketLogger is a packet analysis application that can help you debug and trace your application. To trace the packets live, I downloaded the Bluetooth logging profile and install it on my phone. Here is a link for more detailed instructions for setup.

On the screenshot below, you can see the Bluetooth packets that are sent or received from the current outdated app that I mentioned before. To get detailed information, I filter the logs for each characteristic and tried to analyze raw data. There are also filtering options in PacketLogger that helped me to filter the logs.

image: Screenshot from PacketLogger that shows all Bluetooth packets that are sent to the device or received from the device

The next step was filtering the logs and trying to understand the characteristics. The challenging part is understanding which byte represents what. That was easy for heart rate but for others, I had to analyze logs from different situations and try to understand the differences.

You can see the characteristic which gives step count below. First I had to convert hex values to decimals and then after a couple of trials, I figure out which byte represents what, still there are bytes that I couldn’t understand what they represent.

Next characteristic is used to write settings. For this device, each setting can be customized via same characteristic. 3rd byte refers to the type of the setting. For example, if 3rd byte is equal to 9 it means it will set the alarm clock, if it is equal to 3 it means it will set display options. I found the pattern by setting different alarms and different repeat times for each. Below you can see the details to set clock.

Develop MySmartWatch App

After figure out how to read heart rate, step count and how to customize settings, the next step was try to develop the application. By the way, I could not figure out sleep quality logs, so for now I skip that. In this project, my goal was make a project using CoreBluetooth and get used to using RxSwift.

The process of connecting a device and receiving data or writing the data is shown in the flow. Scanned peripherals can be listened to by implementing the CBCentralManagerDelegate methods, and discovered services and characteristics can be listened to by implementing CBPeripheralDelegate methods.

The only type of peripheral I am currently interested in is a heart rate monitor, so I need to look for only heart rate monitors. With Bluetooth-speak, I only look for peripherals that offer heart rate services. From previous service discoveries, we already know the UUID of heart rate service is 0x180D. In CoreBluetooth CBUUID is representing the UUID. By using the string value of UUID we can create a CBUUID object and pass it to scanForPeripherals(withServices:) function as an array. In this way, we only list heart rate monitors.

Instead of implementing mentioned delegate methods, I used DelegateProxy which is a core feature in RxSwift to wrap the return value from the original delegate to an observable sequence. Let me show you how it works.

Below you can see a part from RxCBCentralManagerDelegateProxy. methodInvoked(selector:) has ability to forward delegate method to observable. didDiscoverPeripheral returns Observable<Peripheral> by taking parameters of centralManager(_:didDiscover:advertisementData:rssi:).

When I try to forward centralManagerDidUpdateState(_:) to an observable, I also had to add delegate implementation in proxy because it is a required delegate method.

But I got the following warning.

When you implement required delegate methods in your proxy, since it’s already implemented rx_delegate.observe() will not receive any events for that method. That’s why I got the warning. Below you can find the solution for it. _forwardToDelegate clears delegate and that means some of the features that depend on that delegate being set will likely stop working. So we should declare a PublishSubject in our proxy and sent an event to it before calling _forwardToDelegate method.

I also create a proxy for CBPeripheral. Below you can see RxCBPeripheralDelegateProxy. As a reminder, if there is another function with the same name and parameter names as the function that you try to get via selector, you should add the function definition to not get “Ambiguous use of function()” compile error.

I created BluetoothService to define the Bluetooth-related operations; start/stop scanning, connect to peripheral, get services, get characteristics, notify characteristic, etc. BluetoothService was shared between view models to keep one instance of CBCentralManager. Although Apple says you can have more than one CBCentralManager instance in a single app (Here is the link), I also had problems when I used more than one instance in my previous projects. So anyway, I shared BluetoothService between view models to use one instance of the central manager.

Below you can see startScaning(:) function and usage of it. To start scanning, it is required that central manager state must be poweredOn. That’s why observable of the state is subscribed and if next element is poweredOn scan is started. The result of startScanning(:) is observable sequence of Peripheral and it is used as data source of tableview.rx.items.

Below you can see setNotify function and usage of it. SetNotify returns observable of characteristic value that come from peripheral.rx.didUpdateValue. PeripheralViewModel call setNotify function and pass the heart rate service UUID and characteristic UUID as parameters, and using flatMap() operator it returns an observable of Int value that is used to drive the heartRate BehaviorRelay. In view controller drive() method is used to bind heartRate observable with the UI components. In this way, heart rate text and activity indicator are updated in a reactive way.

When you try to bind an Observable with a UI element you can use both bindTo and drive methods. drive ensures that observe occurs only on the main thread, so for driving a UI component it is better to use drive.

I am fairly new to RxSwift. That’s why I tried to mention the errors and warnings I got and how I came up with a solution. You can find more from the Github repo.

Here are some screenshots from the application.

Display scanned peripherals and display heart rate measurement
Display step count
Setting the alarm clock

And that’s it!

In this project, I learned to use PacketLogger and how to live track the Bluetooth packets which really helped me to complete this app with lots of features. Implementing this application with RxSwift helped me to get used to this framework. Although I had used CoreBluetooth in previous projects, with RxSwift and DelegateProxy feature, it is easier to handle asynchronous processes and develop dynamic applications that respond to data changes.

Working with BLE is really fun and interesting. But it also challenging if you don’t have any documentation about the services of the device. I hope this project helps you to get a general understanding of how to dig into developing an app for a BLE device.

If you have any questions or suggestions please share them with me.

--

--