This tutorial provides a general approach for retrieving data from Bluetooth Low Energy devices, using a heart rate and blood oxygenation monitor as an example.
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 focuses on a specific BLE device, it should be easy to adapt to other BLE devices.

The complete project code can be downloaded from here.
Introduction
For a long time I searched for an affordable heart rate monitor and pulse oximeter that connects to a smartphone via Bluetooth.
Finally, I found the “PM100 Connect” by Medisana.~~medisana.com/en/Health-control/Pulsoximeter/PM-100-connect-Pulse-oximeter.html?force_sid=bpi0b7tpenm3flllv90g1452r5).data from the thewasable to While reading through reviews onhave out that amwith havinggetting it to workthought to myself thatisget to know Apples ""to learn It isright the, theup, atcanmeasured withthat is going to follow!In order toneed.-~~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 object that will act as a delegate must be provided.
This delegate must implement centralManagerDidUpdateState(_ central: CBCentralManager) so the app can check whether the device it is running 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 scans for nearby peripherals.
You must implement an additional delegate method to determine 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 quickly discovered that it identifies itself as “e-Oximeter”.
Let’s connect to it.
centralManager?.connect(peripheral, options: nil)If the connection is established, it will be reported 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’ll walk through the steps required to discover all the data the device can send.
Services and Characteristics
To find out what the Bluetooth device (the peripheral) offers, first discover its services:
peripheral.discoverServices(nil)See the documentation for CBService. A service is “a collection of data and associated behaviors that accomplish a function or feature of a device.”
Implement the appropriate CBPeripheralDelegate method that is called when services are discovered.
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
peripheral.services?.forEach { service in
logger.debug("Discovered service: \(service.description)")
}
}The PM100 offers 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 ‘re getting close to retrieving the heart rate and oxygenation data with CBCharacteristic. Let’s review the documentation:
“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.”
We need to implement the delegate method to receive all 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 we need to consult the Bluetooth specification to determine that we want the value of characteristic 2A5F within service 1822. You can also Google that.
We want to be notified when the device sends heart rate and oxygenation data. Therefore we need to call the following:
if (characteristic.uuid == CBUUID(string: "2A5F")) {
peripheral.setNotifyValue(true, for: characteristic)
}Finally, retrieve the values by implementing the final 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 in the byteArray with the display of my PM100, it was easy to determine which value corresponded to heart rate and which corresponded to oxygenation:
let oxygenation = byteArray[1]
let heartRate = byteArray[3]No matter which Bluetooth device you ‘re interested in, the approach to discovering its services and characteristics is always the same.
Actually, we ‘re not quite done yet…
Example Application
The complete project code can be downloaded here.
This download includes an Xcode workspace to build the fully working heart rate monitor iOS application.

Thanks for reading, and thank you for your support!