---
title: "Bluetooth heart-rate lab: see every byte your strap sends"
canonical: "https://aifitnessapi.com/tools/heart-rate-lab"
markdown: "https://aifitnessapi.com/tools/heart-rate-lab.md"
type: "tool"
last_reviewed: "2026-10-31"
description: "Connect a BLE heart-rate strap in the browser: live BPM, every 0x2A37 byte decoded, and whether it sends the RR intervals HRV needs. Nothing is uploaded."
publisher: "AIFitnessAPI — funded by KinesteX; first-party pages disclose it"
cite_as: "\"Bluetooth heart-rate lab: see every byte your strap sends\", AIFitnessAPI, https://aifitnessapi.com/tools/heart-rate-lab"
---

# Bluetooth heart-rate lab: see every byte your strap sends

> Whether a Bluetooth heart-rate device can give you HRV is written in its bytes: each Heart Rate Measurement (0x2A37) notification carries RR intervals only when bit 4 of its Flags octet is set, and without RR intervals there is no beat-to-beat timing to compute HRV from. This page connects a strap or a broadcasting watch over Web Bluetooth, decodes every notification against the Bluetooth SIG’s published structure, and tells you which kind your device is. Browsers without Web Bluetooth can replay a synthetic stream instead. The bytes never leave the tab.

- Canonical: https://aifitnessapi.com/tools/heart-rate-lab
- Last reviewed: 2026-10-31
- Publisher: AIFitnessAPI (https://aifitnessapi.com)
- Cite as: "Bluetooth heart-rate lab: see every byte your strap sends", AIFitnessAPI, https://aifitnessapi.com/tools/heart-rate-lab

---

*[Interactive: the Web Bluetooth connection, live decoder and replay, in the browser at https://aifitnessapi.com/tools/heart-rate-lab; not reproduced in this mirror.]*

## The structure it decodes against

The decoder has no packet layout of its own. It reads the Bluetooth SIG’s GATT Specification Supplement file for Heart Rate Measurement, [`gss/org.bluetooth.characteristic.heart_rate_measurement.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.heart_rate_measurement.yaml) at commit 408f4a0 of the SIG’s public repository (read 2026-10-17), from this site’s generated copy, and builds the decoder from it. Every field after Flags is conditional:

| Field | Type | Size | Condition | Unit |
|---|---|---|---|---|
| Flags | boolean[8] | 1 | always | — |
| Heart Rate Measurement Value (8 bit resolution) | uint8 | 0 or 1 | Present if bit 0 of Flags field is set to 0 | period (beats per minute) |
| Heart Rate Measurement Value (16 bit resolution) | uint16 | 0 or 2 | Present if bit 0 of Flags field is set to 1 | period (beats per minute) |
| Energy Expended | uint16 | 0 or 2 | Present if bit 3 of Flags field is set to 1 | energy (joule) |
| RR-interval | uint16[n] | 0 or n*2 | Present if bit 4 of Flags field is set to 1 | 1/1024 s |

The Flags bits, as the SIG lists them: bit 0, “Heart Rate Value Format:”; bit 1, “Sensor Contact detected”; bit 2, “Sensor Contact Supported”; bit 3, “Energy Expended present”; bit 4, “RR-Interval present”; bit 5–7, “Reserved for Future Use”. Bits 2 and 1 carry no field of their own: the lab reads them as “contact not supported” when bit 2 is clear, and otherwise as detected or not detected. The full page for the characteristic, with the SIG’s notice, is [Heart Rate Measurement (0x2A37)](https://aifitnessapi.com/bluetooth-fitness/heart-rate-measurement); Body Sensor Location (0x2A38), which the lab reads once after connecting, is on the [Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness#char-body-sensor-location).

## Why RR intervals decide HRV

The SIG defines the field plainly: “The RR-Interval value represents the time between two R-Wave detections.” and “Each RR-Interval value is represented by a uint16 with 1/1024 second as the unit.” HRV is a statistic over exactly those beat-to-beat times. A heart-rate value is one number in beats per minute per notification; the timing of the individual beats is not in it, and no amount of processing on the app side puts it back. That is why the verdict above is a yes or a no: a device that never sets bit 4 gives you heart rate, and only heart rate.

A notification can carry several RR values or none. The SIG: “The number of RR-Interval sub-fields present is determined by a combination of the overall length of the characteristic and whether or not the characteristic contains the Energy Expended field.” The lab therefore counts the octets left after the fields the flags announce, two per value, and reports an odd one left over instead of guessing at it.

The RMSSD the lab shows is the root mean square of successive differences between RR intervals, computed only within runs of notifications that all carried RR values. It is there so you can see the arithmetic work on your own data, labeled a demo, and it is not a medical or training measurement: there is no artifact or ectopic-beat correction, no minimum recording length and no baseline. What HRV is, and how the platforms store it, is covered in [what is HRV](https://aifitnessapi.com/learn/what-is-hrv), [how to get HRV data from an API](https://aifitnessapi.com/data/hrv-api) and Health Connect’s [HeartRateVariabilityRmssdRecord](https://aifitnessapi.com/health-connect/heart-rate-variability-rmssd-record).

## Where the decoder reads past the SIG’s file

- **Byte order.** The structure gives sizes, not byte order. The lab reads multi-octet fields little-endian, as the [Web Bluetooth specification’s heart-rate example](https://webbluetoothcg.github.io/web-bluetooth/#example-heart-rate-monitor) does (`result.heartRate = data.getUint16(index, /*littleEndian=*/true);`, read 2026-10-31). The default value in the paste box, `1E 46 EE 02 7A 03 66 03`, decodes to that example’s published result: 70 bpm, contact detected, Energy Expended 750, RR values 890 and 870.
- **Energy Expended’s unit.** The SIG’s file gives `org.bluetooth.unit.energy.joule` (energy (joule)), and the lab shows that. The same Web Bluetooth example comments the value as `energyExpended: 750,     // Meaning 750kJ.`. The two sources disagree by a factor of a thousand; we report the disagreement rather than pick one.
- **Battery Level (0x2A19).** Outside this site’s Bluetooth dataset, so it was read on 2026-10-31 from the SIG’s own [`gss/org.bluetooth.characteristic.battery_level.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.battery_level.yaml) at the same commit: one `uint8`, “Allowed range is 0 to 100.” The lab asks for the Battery service (0x180F) as an optional service and says so when a device does not expose it.

## What the lab cannot tell you

- Whether the RR values are measured. The bytes say a device sent values in the RR field, not how it got them; the lab shows how many distinct values arrived so a suspiciously uniform stream is visible, and leaves the judgment to you.
- Accuracy. Nothing here compares a device with a reference; we publish no accuracy figure for any strap or watch.
- Anything in a browser without Web Bluetooth. The page detects the API and, where it is missing, offers the synthetic replay and the paste box. Which browsers ship it is set out in [Web Bluetooth for fitness devices](https://aifitnessapi.com/devices/web-bluetooth-fitness). Real Bluetooth cannot be exercised in an automated test either, which is why the decoder is tested on synthetic packets built from the SIG structure (all 32 combinations of the five defined Flags bits).

Building this into an app is the subject of [connecting a Bluetooth heart rate monitor](https://aifitnessapi.com/devices/bluetooth-heart-rate-monitor), with the radio choice in [ANT+ vs Bluetooth](https://aifitnessapi.com/devices/ant-plus-vs-bluetooth), the watch case in [Apple Watch live heart rate](https://aifitnessapi.com/devices/apple-watch-live-heart-rate), and CI without a strap in [testing BLE fitness devices](https://aifitnessapi.com/devices/testing-ble-fitness-devices). Every characteristic the SIG defines for fitness devices is in the [Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness).

## How this works, and what it sends

Nothing leaves your device. The browser shows its own device chooser; the page can talk only to the device you pick, and only to the Heart Rate service and, if present, the Battery service. Notifications are decoded in this tab and held in memory until you close it. This site has no server code behind the page, stores nothing, and logs no event from it beyond its ordinary page view. The CSV and the share card are made in the tab and go wherever you paste or save them, and nowhere else.

The synthetic replay is generated by the page, not recorded from a device: its heart rate and RR values are made up, and it switches between the uint8 and uint16 heart-rate formats, the three contact states, Energy Expended and a stretch with no RR intervals so that every part of the decoder runs.

The structure comes from the Bluetooth SIG’s public repository (https://bitbucket.org/bluetooth-SIG/public), read by this site’s generator on 2026-10-17. The SIG’s text is quoted with attribution: Copyright © Bluetooth SIG, Inc., which describes these files as proprietary to it. This site is not affiliated with Bluetooth SIG.

A free tool from AIFitnessAPI. More at [free tools for health-app builders](https://aifitnessapi.com/tools).

## Next step

Turning this into a choice of API:

- [The Best Wearable Fitness & Health-Data APIs (2026)](https://aifitnessapi.com/fitness-apis/wearable-data-apis)
- [The Best Wearable Health-Data Aggregator APIs (2026)](https://aifitnessapi.com/fitness-apis/health-data-aggregator-apis)
