Devices / WEAR API LAB

Bluetooth heart rate monitors: a developer’s decoding guide

Walk through a real packet shape, convert RR intervals correctly, and keep sensor state, connection state, and displayed heart rate distinct.

Neon heart rate monitor and packet symbols framed by multicolor stripes with WearAPI.com branding.

A Bluetooth heart rate integration has two jobs: interpret a small binary message correctly and maintain an honest account of when measurements were available. The first job is easy to demonstrate at a desk. The second becomes visible when a runner walks out of range, adjusts a strap, locks a phone, or reconnects after a break. A dependable implementation needs both.

This guide covers the standard Heart Rate Measurement format and the application decisions around it. The heart rate monitor wear API overview explains where direct Bluetooth access fits alongside platform health stores and vendor cloud APIs. Choose the route that matches your feature's timing requirements before writing a decoder.

Discover and subscribe to the measurement

The Heart Rate Service uses UUID 0x180D; its Heart Rate Measurement characteristic uses 0x2A37. Discover the service and characteristic through the platform's Bluetooth API, then enable notifications through the supported platform workflow. A device name containing heart rate is not a substitute for service discovery. Names are useful for selection, but the discovered service determines which parser applies.

The Bluetooth SIG Heart Rate Service specification defines notification behavior, optional fields, and little-endian byte order. It does not give this measurement a timestamp or guarantee recovery of values missed during a lost connection. Treat the receiver's arrival time as arrival time, with its own uncertainty, rather than silently presenting it as a sensor-provided measurement timestamp.

Keep discovery, subscription, and data reception as separate states. A successful connection can precede completed discovery. A subscription request can fail. A subscribed device may not yet have produced a useful measurement. Exposing these distinctions makes troubleshooting easier and prevents a green connected indicator from claiming that a live measurement stream is already flowing.

Read flags before values

The first byte is a set of flags. Bit 0 chooses an unsigned heart rate value: 0 means UINT8, and 1 means UINT16. Bit 2 says whether contact detection is supported; when supported, bit 1 indicates detected contact. Bit 3 marks an Energy Expended field, and bit 4 marks one or more RR intervals. The heart rate format and optional-field presence must be evaluated for each notification.

Advance a cursor through the bytes in the declared order. After flags, read heart rate using the selected width. If energy is present, consume its two bytes before reading any RR data. RR values are unsigned sixteen-bit integers, each measured in units of 1/1024 second. Multiple values occupy the remaining field and retain their transmitted order, from older to newer.

Keep unsupported contact distinct from contact not detected. A Boolean alone loses that distinction. Prefer an internal state such as unsupported, detected, or not_detected. Decide separately how your interface reacts to each state. Unsupported contact must not trigger an instruction to tighten a strap when the device never claimed to assess contact.

A six-byte example, decoded carefully

Consider this synthetic notification, written as hexadecimal bytes:

16 48 00 04 20 03

The first byte, 0x16, sets bits 1, 2, and 4. That selects an eight-bit heart rate, supported and detected contact, no Energy Expended field, and available RR intervals. The next byte, 0x48, is decimal 72, so the heart rate is 72 beats per minute.

Two byte pairs remain. Little-endian 00 04 becomes 0x0400, or 1024. Dividing by 1024 yields one second. The pair 20 03 becomes 0x0320, or 800; its interval is 0.78125 seconds, or 781.25 milliseconds. The conversion is milliseconds = encoded_value * 1000 / 1024. Treating 800 as milliseconds would quietly change the data.

This fixture is useful for a unit test because it exercises several paths while remaining easy to inspect by hand. Expect one heart rate value, two RR values, a supported contact state, and no energy value. Do not force the heart rate number to equal the reciprocal of one selected RR interval. Preserve the reported values; any derived comparison needs an explicitly defined calculation and context.

Reject broken packets without breaking the session

Check length before every read. A payload containing only flags is incomplete. A sixteen-bit heart rate requires two available bytes. If an energy flag is present, ensure that its field exists. When RR data is declared, require at least one complete two-byte value; an odd remaining byte count is an invalid shape for that field.

Separate parsing errors from physiological interpretation. A structurally valid number can still be unsuitable for a particular feature, but that decision belongs in a quality layer with its own reason codes. Do not make a binary decoder clamp values into a convenient display range, because doing so destroys the evidence needed to investigate the device or adapter.

Return an explicit parse result containing either decoded fields or a failure reason. Log the parser version, payload length, and connection context without routinely collecting an identifiable health history in diagnostic logs. Rate-limit repetitive error reporting so a faulty device does not overwhelm the application. The developer checklists provide a place to record these error-handling decisions.

Track continuity as its own data

A line chart should distinguish observed measurements from an interval with no observations. Define a freshness policy for the feature, then change the visual state when the last measurement becomes stale. The threshold is a product decision informed by the devices you test; do not hide it as an unexplained constant inside the rendering code.

Record connection sessions with start, end, and termination reason where available. After reconnecting, begin a new continuity segment. Repeating the last heart rate throughout the gap invents observations. Drawing a smooth line across the break can suggest the same thing. A visible gap, combined with an age label for the last known value, gives the user a more faithful account.

Use a monotonic clock for elapsed-time calculations within a running session, alongside wall-clock timestamps for storage and presentation. If the phone's clock changes, elapsed-time logic should not suddenly decide that a fresh reading is hours old. Where precise alignment with another sensor matters, document the limits of arrival timestamps and test the full transport path.

RR data is not an ECG trace

A list of beat intervals does not contain an ECG waveform. The integration should label the measurement it has and avoid implying additional sensing capabilities from the presence of an RR field. Likewise, successfully reading standard Bluetooth data does not establish that a device or application is suitable for diagnosis.

If your product calculates a variability metric, define the input eligibility, observation window, artifact policy, and minimum usable coverage. Store the derived value with the algorithm version and its input interval. Missing or filtered intervals should affect availability according to that documented policy, rather than being silently replaced with values that make the output look stable.

The same principle applies to coaching features: a calculation needs a specified purpose and validation appropriate to that purpose. Start with a transparent display of measurements and continuity. Add interpretation only when you can explain how the input supports it. The wearable API integration map helps keep source access separate from application-level interpretation.

Test across transitions, not just sample values

  • Switch between eight-bit and sixteen-bit heart rate formats within one simulated connection.
  • Alternate packets with and without optional energy and RR fields.
  • Send several identical measurements in sequence and verify that legitimate observations remain visible.
  • Test unsupported contact, detected contact, and lost contact independently.
  • Truncate a packet at each field boundary and verify controlled failure.
  • Disconnect during activity, reconnect, and confirm that the chart preserves the gap.
  • Change the wall clock during a session and check measurement freshness.

Include representative physical devices after deterministic parser tests pass. Capture the observed behavior for each supported combination of device, firmware, phone, and operating system. A passing synthetic fixture proves the arithmetic in that case; it cannot establish how a phone manages an active connection when the application changes state.

Ship a measurement you can explain

A good decoder reads each flag, advances through the correct fields, and preserves units and optionality. A good product also shows stale data honestly and records continuity separately from value. Combine those practices with the data quality workflow, and debugging becomes a matter of inspectable records instead of guessing why a chart looks wrong.