Heart Rate Monitor and Blood Oximeter in Swift
An Introduction to CoreBluetooth #
This tutorial will provide a general approach on how to get data out of Bluetooth Low Energy devices by using heart rate and blood oxygenation monitor as an example device.
For this tutorial I am using:
- Medisana PM100 Connect
- iPhone 11 Pro Max
- iOS 14.4
- MacBook Pro, 13-inch, 2016
- macOS Big Sur 11.2.2
- XCode 12.4
Although this tutorial is about a specific BLE-device, it should be quite easy to adapt it to any other BLE-device.
The complete project code can be downloaded from here.
Introduction #
For a long time I was searching for an affordable heart rate monitor and blood oximeter that can connect to a smartphone via Bluetooth.
Finally, I was lucky and found the “PM100 Connect” by Medisana.
The company claims they have an app called “VitaDock” that captures and displays the data from the device.
To make the story short, I was not able to get the device to work with this app. While reading through reviews on Amazon I have found out that I am not the only one with having problems getting it to work.
I was fed up and close to returning it when I thought to myself that this is an excellent opportunity to get to know Apples “Core Bluetooth” framework and to learn how Bluetooth Low Energy devices work.
It is time to dive right into coding and get the hands dirty, or the heart rate up, which, at the end of this tutorial can be measured with the code that is going to follow!
Prerequisites #
In order to allow the device to use Bluetooth, two keys need be set in the Info.plist
.
Privacy - Bluetooth Always Usage Description
Privacy - Bluetooth Peripheral Usage Description
CentralManagers and Peripherals #
The entry-point of this tutorial is the CBCentralManager
which is “an object that scans for, discovers, connects to, and manages peripherals.
Let’s create one and wire it up.
var centralManager: CBCentralManager?
centralManager = CBCentralManager(delegate: self, queue: nil)
Upon instantiation, an objects that will act as a delegate needs to be provided.
This delegate has to implement centralManagerDidUpdateState(_ central: CBCentralManager)
so it can be checked if the device the app will run on actually supports bluetooth.
func centralManagerDidUpdateState(_ central: CBCentralManager) {
guard central.state == .poweredOn else {
logger.debug("No Bluetooth available")
return
}
central.scanForPeripherals(withServices: nil, options: nil)
}
When Bluetooth is available, the app will scan for peripherals in its vicinity.
Another delegate method needs to be implemented so it can be checked which peripherals are available.
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
logger.debug("Found peripheral: \(peripheral.description)")
}
Running this with the PM100 turned on, I have quickly found out that it has the name “e-Oximeter”.
Let’s connect to it.
centralManager?.connect(peripheral, options: nil)
If the connection has been established will be received in this delegate method.
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
logger.debug("Connected to peripheral: \(peripheral.description)")
}
Done! We have successfully connected to the PM100, or any other device you might be using for this tutorial. In the next step we walk through all the steps required to actually discover all the data the device is able to send out.
Services and Characteristics #
To find out what the Bluetooth device, the peripheral, has to offer, we need to first discover its services:
peripheral.discoverServices(nil)
In the documentation we need to look for CBService
. A service is described as “A collection of data and associated behaviours that accomplish a function or feature of a device.”
A delegate method of CBPeripheralDelegate
needs to be implemented that is being called when a service has been discovered.
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
peripheral.services?.forEach { service in
logger.debug("Discovered service: \(service.description)")
}
}
The PM100 has four services:
Discovered service: <CBService: 0x2816b8700, isPrimary = YES, UUID = Device Information>
Discovered service: <CBService: 0x2816b82c0, isPrimary = YES, UUID = Battery>
Discovered service: <CBService: 0x2816b84c0, isPrimary = YES, UUID = 1822>
Discovered service: <CBService: 0x2816b8300, isPrimary = YES, UUID = 0000FEE8-0000-1000-8000-00805F9B34FB>
For every service we can discover the so called characteristics:
peripheral.discoverCharacteristics(nil, for: service)
We are coming very close to retrieving the heart rate and oxygenation data with CBCharacteristic
. Let’s look up the documentation again:
“CBCharacteristic and its subclass CBMutableCharacteristic represent further information about a peripheral’s service. In particular, CBCharacteristic objects represent the characteristics of a remote peripheral’s service. A characteristic contains a single value and any number of descriptors describing that value. The properties of a characteristic determine how you can use a characteristic’s value, and how you access the descriptors.”
Again, we need to implement a delegate method to retrieve all the discovered characteristics:
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
service.characteristics?.forEach { characteristic in
logger.debug("Discovered characteristic: \(characteristic.description) for service \(service.uuid)")
}
}
Now, it needs some digging in the Bluetooth specification to find out that we want the value of the characteristic 2A5F
of service 1822
. You can also google that ;-).
We want to be notified when the device sends out the heart rate and oxygenation data. Therefore we need to call the following:
if (characteristic.uuid == CBUUID(string: "2A5F")) {
peripheral.setNotifyValue(true, for: characteristic)
}
Finally, we can retrieve the values by implementing our last delegate method:
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
guard characteristic.service.uuid == CBUUID(string: "1822"),
characteristic.uuid == CBUUID(string: "2A5F"),
let data = characteristic.value else {
return
}
let numberOfBytes = data.count
var byteArray = [UInt8](repeating: 0, count: numberOfBytes)
(data as NSData).getBytes(&byteArray, length: numberOfBytes)
logger.debug("Data: \(byteArray)")
}
By comparing the values of the byteArray
with the display of my PM100 it was easy to find out which value is for the heart rate and which for the oxygenation:
let oxygenation = byteArray[1]
let heartRate = byteArray[3]
We are done! So, no matter what Bluetooth device you are interested in, the approach should always be the same for discovering its services and characteristics.
Actually, we are not quite done, yet …
Example Application #
The complete project code can be downloaded from here.
With this download you will get an Xcode-workspace to build the fully-working heart rate monitor iOS-application.
Thanks for reading and thank you for your support!
- If you enjoyed this, please follow me on Medium
- Buy me a coffee to keep me going
- Support me and other Medium writers by signing up here
https://twissmueller.medium.com/membership