Core Bluetooth Tutorial for iOS: Heart Rate Monitor
In this Core Bluetooth tutorial, you’ll learn how to discover, connect to, and retrieve data from compatible devices like a chest-worn heart rate sensor. By Jawwad Ahmad.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Core Bluetooth Tutorial for iOS: Heart Rate Monitor
30 mins
- Centrals and Peripherals
- Advertising Packets
- Services and Characteristics
- Getting Started
- Preparing for Core Bluetooth
- Scanning for Peripherals
- Scanning for Peripherals with Specific Services
- Connecting to a Peripheral
- Discovering a Peripheral’s Services
- Discovering a Service’s Characteristics
- Checking a Characteristic’s Properties
- Obtaining the Body Sensor Location
- Interpreting the Binary Data of a Characteristic’s Value
- Obtaining the Heart Rate Measurement
- Where to Go From Here?
Scanning for Peripherals
For many of the methods you’ll be adding, instead of giving you the method name outright, I’ll give you a hint on how to find the method that you would need. In this case, you want to see if there is a method on centralManager
with which you can scan.
On the line after initializing centralManager
, start typing centralManager.scan and see if you can find a method you can use:
The scanForPeripherals(withServices: [CBUUID]?, options: [String: Any]?)
method looks promising. Select it, use nil
for the withServices:
parameter and remove the options:
parameter since you won’t be using it. You should end up with the following code:
centralManager.scanForPeripherals(withServices: nil)
Build and run. Take a look at the console and note the API MISUSE
message:
API MISUSE: <CBCentralManager: 0x1c4462180> can only accept this command while in the powered on state
Well, that certainly makes sense right? You’ll want to scan after central.state
has been set to .poweredOn
.
Move the scanForPeripherals
line out of viewDidLoad()
and into centralManagerDidUpdateState(_:)
, right under the .poweredOn
case. You should now have the following for the .poweredOn
case:
case .poweredOn:
print("central.state is .poweredOn")
centralManager.scanForPeripherals(withServices: nil)
}
Build and run, and then check the console. The API MISUSE
message is no longer there. Great! But has it found the heart rate sensor?
It probably has; you simply need to implement a delegate method to confirm that it has found the peripheral. In Bluetooth-speak, finding a peripheral is known as discovering, so the delegate method you’ll want to use will have the word discover in it.
Below the end of the centralManagerDidUpdateState(_:)
method, start typing the word discover
. The method is too long to read fully, but the method starting with centralManager
will be the correct one:
Select that method and replace the code
placeholder with print(peripheral)
.
You should now have the following:
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any], rssi RSSI: NSNumber) {
print(peripheral)
}
Build and run; you should see a variety of Bluetooth devices depending on how many gadgets you have in your vicinity:
<CBPeripheral: 0x1c4105fa0, identifier = D69A9892-...21E4, name = Your Computer Name, state = disconnected>
<CBPeripheral: 0x1c010a710, identifier = CBE94B09-...0C8A, name = Tile, state = disconnected>
<CBPeripheral: 0x1c010ab00, identifier = FCA1F687-...DC19, name = Your Apple Watch, state = disconnected>
<CBPeripheral: 0x1c010ab00, identifier = BB8A7450-...A69B, name = Polar H7 DCB69F17, state = disconnected>
One of them should be your heart rate monitor, as long as you are wearing it and have a valid heart rate.
Scanning for Peripherals with Specific Services
Wouldn’t it be better if you could only scan for heart rate monitors, since that is the only kind of peripheral you are currently interested in? In Bluetooth-speak, you only want to scan for peripherals that provide the Heart Rate service. To do that, you’ll need the UUID for the Heart Rate service. Search for heart rate in the list of services on the Bluetooth services specification page and note the UUID for it; 0x180D
.
From the UUID, you’ll create a CBUUID
object and pass it to scanForPeripherals(withServices:)
, which actually takes an array. So, in this case, it will be an array with a single CBUUID
object, since you’re only interested in the heart rate service.
Add the following to the top of the file, right below the import
statements:
let heartRateServiceCBUUID = CBUUID(string: "0x180D")
Update the scanForPeripherals(withServices: nil)
line to the following:
centralManager.scanForPeripherals(withServices: [heartRateServiceCBUUID])
Build and run, and you should now only see your heart rate sensor being discovered:
<CBPeripheral: 0x1c0117220, identifier = BB8A7450-...A69B, name = Polar H7 DCB69F17, state = disconnected>
<CBPeripheral: 0x1c0117190, identifier = BB8A7450-...A69B, name = Polar H7 DCB69F17, state = disconnected>
Next you’ll store a reference to the heart rate peripheral and then can stop scanning for further peripherals.
Add a heartRatePeripheral
instance variable of type CBPeripheral
at the top, right after the centralManager
variable:
var heartRatePeripheral: CBPeripheral!
Once the peripheral is found, store a reference to it and stop scanning. In centralManager(_:didDiscover:advertisementData:rssi:)
, add the following after print(peripheral)
:
heartRatePeripheral = peripheral
centralManager.stopScan()
Build and run; you should now see the peripheral printed just once.
<CBPeripheral: 0x1c010ccc0, identifier = BB8A7450-...A69B, name = Polar H7 DCB69F17, state = disconnected>
Connecting to a Peripheral
To obtain data from a peripheral you’ll need to connect to it. Right below centralManager.stopScan()
, start typing centralManager.connect
and you should see connect(peripheral: CBPeripheral, options: [String: Any]?)
appear:
Select it, use heartRatePeripheral
for the first parameter and delete the options:
parameter so that you end up with the following:
centralManager.connect(heartRatePeripheral)
Great! Not only have you discovered your heart rate sensor, but you have connected to it as well! But how can you confirm that you are actually connected? There must be a delegate method for this with the word connect in it. Right after the centralManager(_:didDiscover:advertisementData:rssi:)
delegate method, type connect
and select centralManager(_:didConnect:)
:
Replace the code placeholder as follows:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("Connected!")
}
Build and run; you should see Connected! printed to the console confirming that you are indeed connected to it.
Connected!
Discovering a Peripheral’s Services
Now that you’re connected, the next step is to discover the services of the peripheral. Yes, even though you specifically requested a peripheral with the heart rate service and you know that this particular peripheral supports this, you still need to discover the service to use it.
After connecting, call discoverServices(nil)
on the peripheral to discover its services:
heartRatePeripheral.discoverServices(nil)
You can pass in UUIDs for the services here, but for now you’ll discover all available services to see what else the heart rate monitor can do.
Build and run and note the two API MISUSE
messages in the console:
API MISUSE: Discovering services for peripheral <CBPeripheral: 0x1c010f6f0, ...> while delegate is either nil or does not implement peripheral:didDiscoverServices:
API MISUSE: <CBPeripheral: 0x1c010f6f0, ...> can only accept commands while in the connected state
The second message indicates that the peripheral can only accept commands while it’s connected. The issue is that you initiated a connection to the peripheral, but didn’t wait for it to finish connecting before you called discoverServices(_:)
!
Move heartRatePeripheral.discoverServices(nil)
into centralManager(_:didConnect:)
right below print("Connected!")
. centralManager(_:didConnect:)
should now look like this:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("Connected!")
heartRatePeripheral.discoverServices(nil)
}
Build and run. Now you should only see the other API MISUSE
message which is:
API MISUSE: Discovering services for peripheral <CBPeripheral: ...> while delegate is either nil or does not implement peripheral:didDiscoverServices:
The Core Bluetooth framework is indicating that you’ve asked to discover services, but you haven’t implemented the peripheral(_:didDiscoverServices:)
delegate method.
The name of the method tells you that this is a delegate method for the peripheral, so you’ll need to conform to CBPeripheralDelegate
to implement it.
Add the following extension to the end of the file:
extension HRMViewController: CBPeripheralDelegate {
}
Xcode doesn’t offer to add method stubs for this since there are no required delegate methods.
Within the extension, type discover
and select peripheral(_:didDiscoverServices:)
:
Note that this method doesn’t provide you a list of discovered services, only that one or more services has been discovered by the peripheral. This is because the peripheral object has a property which gives you a list of services. Add the following code to the newly added method:
guard let services = peripheral.services else { return }
for service in services {
print(service)
}
Build and run, and check the console. You won’t see anything printed and, in fact, you’ll still see the API MISUSE
method. Can you guess why?
It’s because you haven’t yet pointed heartRatePeripheral
at its delegate
. Add the following after heartRatePeripheral = peripheral
in centralManager(_:didDiscover:advertisementData:rssi:)
:
heartRatePeripheral.delegate = self
Build and run, and you’ll see the peripheral’s services printed to the console:
<CBService: 0x1c046f280, isPrimary = YES, UUID = Heart Rate>
<CBService: 0x1c046f5c0, isPrimary = YES, UUID = Device Information>
<CBService: 0x1c046f600, isPrimary = YES, UUID = Battery>
<CBService: 0x1c046f680, isPrimary = YES, UUID = 6217FF4B-FB31-1140-AD5A-A45545D7ECF3>
To get just the services you’re interested in, you can pass the CBUUID
s of those services into discoverServices(_:)
. Since you only need the Heart Rate service, update the discoverServices(nil)
call in centralManager(_:didConnect:)
as follows:
heartRatePeripheral.discoverServices([heartRateServiceCBUUID])
Build and run, and you should only see the Heart Rate service printed to the console.
<CBService: 0x1c046f280, isPrimary = YES, UUID = Heart Rate>