
Like many people who like to get out and about mountain biking, road cycling or running these days of technology, I, like many others, use Strava to track my rides and use my heart rate as a measure to track my fitness.
When the weather is terrible, or I run out of time or daylight, I also use an app called Zwift that lets me go on a virtual ride and join others for a ride around Waitopia, London, Paris, Scotland and a bunch of other places.
All this data syncs back to Strava for virtual rides, real rides (or the occasional run/walk). In order to track my heart rate I have used chest straps and also tried a WHOOP band (which also lets me track my sleep).
I found that chest straps are ok for my cycling, but a bit of a pain when just off for a hike somewhere. The WHOOP band is very good, but the subscription model is pretty spendy and probably overkill for what I was using it for.
So along comes the Apple Watch. It tracks my sleep (✅), it tracks my heart rate when I use Strava (✅), and it has support for Zwift (✅ 🤔).
It turned out that although Zwift supports taking heart rate from the Apple Watch, it can be difficult to get working, and when it does not work, it's tough to understand why.
So there are lots of apps in the app store that say they sort this issue. “Turn your watch into a heart rate monitor”, “Works with Zwift”.
But for my day job when I am not out cycling the hills around Wellington or navigating the roads or Waitopia on Zwift I develop software for Apple products. Surely I can write an app to run on my Apple Watch so I can hook my heart rate into Zwift, and learn some new stuff on the journey.
I can fix this, right?
So the problem I am trying to solve is that I need to be able to confidently get my current heart rate from my Apple Watch into my Zwift app.
So the first problem I bumped into was that watchOS does not support acting as a Bluetooth LE peripheral, so you cannot broadcast data over Bluetooth from the watch; however, you can do this in iOS 😬.
The plan then is to create an Apple Watch App with a companion iOS app. The watch app will get the heart rate data and share it with the iOS app. The iOS app will then act as a Bluetooth accessory that publishes my watch heart rate.
Starting with the watch
The Apple Watch app is designed to monitor your health during a workout. When you start a workout session on your watch, the app automatically tracks several important health metrics, including heart rate.
As you exercise, the app continuously collects these values from the watch’s sensors.
The app we are creating will ask for more permissions to monitor the user's heart rate and start a workout, and send this to the iPhone in real time.
In order to achieve this, I am using 2 main Apple libraries, HealthKit and WatchConnectivity.
HealthKit is Apple’s system for collecting and managing health data from your device’s sensors (like heart rate, steps, and more).
The app asks for permission to access the user's health data. The app then starts a workout; it uses HealthKit to start a session and begin collecting data. In order to monitor the heart rate, you create a query to listen for new heart rate data as it’s recorded by the watch.
Whenever new data is available, the app processes it and prepares it to be sent to your iPhone.
Request user permission to access heart rate data

Starting the heart rate query

WatchConnectivity is Apple’s system for communication between your Apple Watch and iPhone.
The watch app sets up a connection between the watch app and the iPhone app. When new health data is collected, the watch app sends it instantly to the iPhone using this connection. If the iPhone app is not reachable, the watch app waits and tries again later.
Starting a workout

Sending data to the iPhone app

Next up is the iPhone app
The iPhone app is responsible for bridging Apple Watch health data with external Bluetooth devices. It serves as both a WatchConnectivity receiver and a Bluetooth Low Energy peripheral, implementing the standard Heart Rate Service protocol.
The iPhone app uses two main Apple frameworks to achieve this: WatchConnectivity and CoreBluetooth.
- WCSessionDelegate: For receiving data from Apple Watch
- CBPeripheralManagerDelegate: For Bluetooth LE peripheral operations
Watch connectivity integration
The iPhone app receives health metrics from the Apple Watch through the WCSessionDelegate protocol:

Bluetooth low energy implementation
In order to ensure that the heart rate data that we are sending can be understood by any app that is listening for it, we need to know the standardised codes to send. The hex codes are standardised identifiers defined by the Bluetooth Special Interest Group (SIG).
The Bluetooth SIG has officially assigned the hex code 180D to mean "Heart Rate." Any central device (like a phone or computer) that scans and finds a peripheral advertising this service will immediately know it's a heart rate device.2A37: This is the standardised UUID for the Heart Rate Measurement characteristic. This specific characteristic is designed to hold the actual heart rate value (e.g., 75 beats per minute).
You can find the official, searchable list on the Bluetooth SIG website: Bluetooth GATT Services and Bluetooth GATT Characteristics.
The iPhone app implements the standard Bluetooth Heart Rate Service (UUID: 180D) and Heart Rate measurement characteristic:

Advertisement Configuration
The iPhone advertises itself as a Heart Rate Monitor:

Data Format Conversion
Heart rate data sent over Bluetooth has a specific format to ensure any device or app can understand it. This standardisation is crucial for interoperability.
The format uses a flexible byte array where the very first byte, called the Flags byte, acts as a "table of contents" for the data that follows. This allows the same characteristic to send simple or complex heart rate information efficiently.
Why a Standard Format is Needed 🤝
Think of it as a universal language. Without a standard, every heart rate monitor manufacturer would invent their own data format. An app developer would then have to write custom code to parse the data from a Garmin, different code for a Polar, and yet another for an Apple Watch.
By having one official format defined by the Bluetooth Special Interest Group (SIG), everyone agrees on the rules. This ensures that any heart rate monitor can communicate with any fitness app or device that supports the standard Heart Rate Service. It creates a seamless, plug-and-play ecosystem.
How the Heart Rate Measurement data is structured 📊
The data you send for the Heart Rate Measurement characteristic (2A37) is a small array of bytes.
Here’s a breakdown of its structure.
Byte 0: The Flags Byte
This first byte is a collection of 8 single-bit flags. Each bit tells the receiving device what kind of data is included in the rest of the packet.

Bytes 1 and onward: The Data Payload
The data that follows the Flags byte depends entirely on which flags were set.
1. Heart Rate Value (Required): This is always present.
- If Flag Bit 0 is 0, this will be one byte (a
UInt8), representing heart rates from 0-255. - If Flag Bit 0 is 1, this will be two bytes (a
UInt16), used for values over 255 (rare, but possible). The value is sent in little-endian format (least significant byte first).
2. Energy Expended (Optional):
- This field is only present if Flag Bit 3 is 1.
- It is always two bytes (
UInt16) and represents the cumulative energy burned in kilojoules.
3. RR-Intervals (Optional):
- This field is only present if Flag Bit 4 is 1.
- RR-Interval is the time between consecutive heartbeats, used for calculating Heart Rate Variability (HRV).
- Each RR-Interval value is two bytes (
UInt16), with units of 1/1024 of a second. The packet can contain multiple RR-Interval values, one after another.
Putting it all together: A simple example
Let's say your app needs to send a heart rate of 75 bpm. You are using the simple 8-bit format and your sensor has good skin contact.
1. Calculate the Flags Byte (Byte 0):
- Heart Rate Format is
UInt8-> Bit 0 = 0 - Sensor Contact is detected -> Bit 2 = 1
- No Energy Expended or RR-Interval data is being sent.
- The binary flag is
00000100(only bit 2 is set). This is 4 in decimal or0x04in hex.
2. Get the Heart Rate Value (Byte 1):
- The heart rate is 75. This is
0x4Bin hex.
3. Construct the Final Data Packet:
- The complete byte array to send over Bluetooth would be [
0x04,0x4B].
When a receiving device gets this data, it first reads 0x04, understands that the next byte is an 8-bit heart rate value, and then reads 0x4B to get the value of 75.
The result
So once I have this up and running and up I can quickly start up the heart rate app on my watch and the companion app on my phone. Then I have control over sending my heart rate over bluetooth. I successfully connected it to my Zwift app and to my Garmin cycling head unit. I can now use my watch app to share my heart rate from my watch to my cycling apps.

