Skip to Content
This documentation is provided with the HEAT environment and describes this HEAT instance.

Interface control document

1. Identification

ItemValue
DocumentICD-CMBLEHR-001 rev 1
ProductBluetooth wearable module (HEAT.BluetoothWearableModule in the module registry, module id BleWearable)
Product versions covered79b0738
PeerAny Bluetooth Low Energy device exposing the Bluetooth SIG Heart Rate Service
HostHEAT Capture, HEAT.Capture.ModuleSDK 2.0.5
Data modelSimulationFrame carrying one Entity with a BiometricSensorComponent
Settings sectionHEAT.BluetoothWearableModule
Date2026-09-02
OwnerVRAI Simulation

2. Scope

This document defines how the module finds and selects a BLE device, which GATT objects it reads, how it decodes the Heart Rate Measurement characteristic, and what it publishes to HEAT Capture.

The module is receive-only with respect to measurement data. The only thing it writes to the device is the Client Characteristic Configuration Descriptor that enables notifications. It defines no remote control protocol for the wearable and exposes no network endpoint.

The interface is the public Bluetooth SIG Heart Rate Service, not a VRAI-defined protocol. The authority for field meaning is the Bluetooth SIG Heart Rate Service and Heart Rate Measurement characteristic specifications. What this document fixes is which parts of that specification the module actually implements, and how it maps them onto HEAT Capture readings.

3. Interface inventory

IDNameDirectionTransportPeer
IF-CMBLEHR-01Advertisement scan and device selectionDevice to moduleBLE advertising, active scanConfigured wearable
IF-CMBLEHR-02GATT discovery and notification subscriptionModule to deviceBLE GATT over Windows BluetoothLEDeviceConfigured wearable
IF-CMBLEHR-03Heart Rate Measurement notification streamDevice to moduleBLE GATT notificationsConfigured wearable
IF-CMBLEHR-04Frame publicationModule to hostModule SDK publication pathHEAT Capture
IF-CMBLEHR-05Settings-time device listModule to hostSettings UI async value sourceHEAT Capture

4. IF-CMBLEHR-01 Advertisement scan and device selection

4.1 Transport and addressing

ItemValue
MechanismBluetoothLEAdvertisementWatcher with ScanningMode = BluetoothLEScanningMode.Active
Address formSix bytes, uppercase hex, colon separated, most significant byte first, for example AA:BB:CC:DD:EE:FF
Selection ruleAn advertisement is accepted when its formatted address is exactly equal to the DeviceID setting
Service filterNone. The scan does not require the advertisement to list the Heart Rate Service

The comparison is an ordinal string equality against the uppercase form the module produces itself. A DeviceID written by hand in lowercase, without separators, or in reverse byte order never matches, and the scan therefore never completes. An empty DeviceID is not a wildcard: it matches nothing, so there is no “first available device” behaviour.

4.2 Lifecycle and timing

The watcher is created and started in InitialiseModule, which returns immediately after logging Scanning for Device.... Scanning therefore runs while the module is still starting.

StartCapture awaits the first matching advertisement. When one arrives the module logs Found device: LOCAL_NAME, Address: HEX_ADDRESS, stops the watcher, records the device name (falling back to the formatted address when the advertisement carries no LocalName), and proceeds to IF-CMBLEHR-02.

The whole sequence of scan, connect, discover and subscribe is bounded by the module’s own startup timeout, which is raised to 30 seconds from the SDK default of 5. If no matching advertisement arrives within that window, the SDK logs a warning naming StartCapture and the 30-second timeout, and the module does not start.

4.3 Authentication

None at this interface. Selection is by advertised address only, and a BLE advertising address is not an authenticator. See section 13.

5. IF-CMBLEHR-02 GATT discovery and notification subscription

5.1 GATT objects used

RoleUUIDAccess performed
Heart Rate service0000180D-0000-1000-8000-00805F9B34FBLooked up with BluetoothCacheMode.Uncached; the first service in the result is used
Heart Rate Measurement characteristic00002A37-0000-1000-8000-00805F9B34FBLooked up with the default cache mode; the first characteristic in the result is used
That characteristic’s CCCDWritten implicitlySet to Notify

No other service or characteristic is touched at capture time. The module does not read Body Sensor Location (0x2A38), does not use the Heart Rate Control Point (0x2A39), and does not read Battery Service or Device Information.

5.2 Connection lifecycle

  1. Any previously held device object is disposed.
  2. BluetoothLEDevice.FromBluetoothAddressAsync is called with the matched address.
  3. The pairing state is logged: Device is paired; connecting. or Device is not paired; attempting GATT access without pairing. The module does not initiate pairing in either case.
  4. If the connection status is not already Connected, the module waits a fixed 1000 ms and then logs Device Connected. Getting GATT Services... unconditionally. That log line is emitted whether or not the link actually came up, so it is not evidence of a connection.
  5. The Heart Rate service and Heart Rate Measurement characteristic are discovered.
  6. The notification handler is attached and the CCCD is written to Notify.
  7. On success the module reports the connection state Connected and StartCapture returns true.

Capture stop detaches the notification handler, disposes the device, cancels the pending scan result, stops the watcher, and sets the reported state to Stopped. The CCCD is not written back to None; notifications end because the device object is disposed and the link is released.

5.3 Reconnect behaviour

There is no reconnect. The module subscribes to no connection-status change event, and the connect routine is only ever called from StartCapture. If the wearable moves out of range, powers off, or drops the link, notifications stop: nothing is logged, no event is published, and the reported connection state stays Connected.

Recovery requires stopping and restarting the capture session. That works because the SDK clears its initialised flag on stop, so the next start re-runs InitialiseModule and begins a fresh scan.

5.4 Startup timeout and orphaned connections

The SDK’s startup timeout races the start operation but does not cancel it, and the module’s connect routine accepts no cancellation token. After a 30-second startup timeout the host is told the module did not start, while the scan and connect sequence continues running. If the configured device appears later, that orphaned sequence can still complete the subscription and, in native-rate mode, start publishing frames, because the SDK publication path has no running-state guard. This is a property of this revision and is not one of the existing audit findings.

5.5 Authentication and pairing

The module never requests pairing. It attempts GATT access on an unpaired link and reports what happens. A device that gates its Heart Rate service behind bonding returns AccessDenied, which the module logs as a pairing hint and treats as a start failure. Pairing such a device is an operator action in Windows Bluetooth settings, taken before the capture session.

6. IF-CMBLEHR-03 Heart Rate Measurement notification stream

6.1 Wire format

Each notification is a Heart Rate Measurement value. The module reads it with a WinRT DataReader over the characteristic buffer, in the order below. It does not set DataReader.ByteOrder, so multi-byte fields are decoded using the WinRT default.

OrderFieldPresent whenRead as
1FlagsAlwaysuint8
2Heart Rate MeasurementAlwaysuint8, or uint16 when flags bit 0 is set
3Energy ExpendedFlags bit 3 setuint16, kilojoules
4R-R IntervalFlags bit 4 set and at least 2 bytes remainuint16, units of 1/1024 s

Flags handling:

BitSpecification meaningModule handling
0Value format, 0 = uint8, 1 = uint16Honoured; selects the read width
1Sensor contact detectedNot read
2Sensor contact supportedNot read
3Energy Expended presentHonoured; gates the uint16 read
4R-R Interval presentHonoured; gates the uint16 read
5 to 7ReservedNot read

Because bits 1 and 2 add no bytes, ignoring them does not misalign the remaining fields. It does mean the module cannot report whether the strap is in contact with skin, so a device that keeps notifying while off-body produces readings that look valid.

The R-R field may carry several intervals in one notification. The module reads exactly one and leaves the rest in the buffer, so batched intervals after the first are lost.

6.2 Unit conversion

Source fieldConversion appliedPublished as
Heart Rate MeasurementNoneHeartRate
Energy Expended, kilojoulesDivided by 4.184kcal
R-R Interval, 1/1024 sMultiplied by 1000 and divided by 1024RR

Each decoded notification overwrites the module’s cached value for that reading and stamps a last-reading time used only by the host’s debug view.

6.3 Cadence

The notification rate is device-defined; the module neither requests nor enforces one. Most straps notify about once per second. The module does not detect or report a missed notification.

7. IF-CMBLEHR-04 Frame publication and data model reference

7.1 Frame contents

ItemValue
FrameSimulationFrame with one EntityState
Entity nameNot set; the entity carries an empty name
Entity idA 31-bit value computed once when the module instance is constructed, from the hash of a freshly generated GUID masked with 0x7FFFFFFF
ComponentBiometricSensorComponent
ReadingsHeartRate, RR, kcal, always all three, in that order
Frame capture timeUTC Unix milliseconds, taken when the frame is built
Packet timestampUTC Unix milliseconds, taken again by the SDK when the packet is wrapped
Node idNodeId when OverrideNodeId is set, otherwise -1

Reading values are protobuf floats. HeartRate is decoded as an integer and widened on publication.

No EntityEvent is ever published by this module.

The entity id is not derived from the device address and is not stable across host processes, so a consumer cannot use it to correlate the same wearable between capture sessions. With no entity name set, that unstable number is the entity’s only identifier. This is a property of this revision and is not one of the existing audit findings.

7.2 Native rate versus polled

SendAtNativeRate selects which of two mutually exclusive publication paths is live.

SendAtNativeRatePublication triggerPer-cycle capture callEffect
true (default)Each decoded notification publishes a frame immediately, from the GATT callback threadReturns nothing every cycleFrame rate follows the device; Interval only controls how often the idle loop ticks
falseThe per-cycle capture call publishes the cached valuesBuilds a frame every cycleFrame rate is Interval; between notifications the same values republish unchanged

In polled mode nothing marks a republished frame as unchanged, so a stalled device is indistinguishable from a steady heart rate.

7.3 Publication before the first notification

This is audit finding BLE-01, and it is a defect in the module rather than a device behaviour.

The three cached readings are instance fields with these initial values:

ReadingSeed value
HeartRate60
RR0
kcal0

StartCapture returns as soon as the CCCD write succeeds, which is before any notification has arrived, and in polled mode a frame is built on every capture cycle unconditionally. A consumer therefore cannot distinguish “no measurement yet” from a real resting heart rate of 60 bpm with a zero R-R interval and zero energy expended, and those values are persisted as measurements.

Two further consequences follow from the same design:

  • The caches are never reset on stop or on start. A second capture session in the same host process seeds from the last values of the previous session rather than from 60, 0 and 0.
  • A device that never sets flags bit 3 or bit 4 publishes RR and kcal as 0 for the entire session. An absent optional field is indistinguishable from a measured zero.

In native-rate mode the first published frame is triggered by a real notification, so HeartRate in it is genuine, but RR and kcal in that frame are still the seed zeros unless the device included them.

Until this is corrected, do not treat an early frame as evidence of a live sensor. Correlate with the Device Connected. Getting GATT Services... and connection-state logging, or with the host debug view’s receiving indicator, which reports true only while a notification has arrived within the last 5 seconds.

8. IF-CMBLEHR-05 Settings-time device list

The DeviceID setting is populated by an asynchronous value source that the host calls when the settings UI needs the device list. It is not part of the capture path.

ItemValue
Scan modeBluetoothLEAdvertisementWatcher, active scanning
Scan durationFixed 5000 ms
Acceptance ruleThe advertisement lists service UUID 0000180d-0000-1000-8000-00805f9b34fb, or it has a non-blank local name and lists no service UUIDs at all
Option valueThe device address in AA:BB:CC:DD:EE:FF form
Option labelThe resolved device name, or the address when no name resolves
FailureAny exception is rethrown as InvalidOperationException with the message BLE device scan failed. Verify Bluetooth access and packaged WinRT dependencies.

Name resolution order for each accepted address: the advertisement’s local name; otherwise connect and use the device’s own name; otherwise read Device Name (00002A00-0000-1000-8000-00805F9B34FB) from the Generic Access service, uncached; otherwise no name.

The scan also enumerates Windows’ known BLE devices to build an address-to-name map, but that map is not consulted when labelling options, so a paired device that advertises without a local name still costs a GATT round trip to name. This is a property of this revision and is not one of the existing audit findings.

9. Error behaviour

ConditionLog messageReported connection stateOutcome
No advertisement matches DeviceIDSDK warning naming StartCapture and the 30-second timeoutScanningModule does not start; see 5.4
Device object could not be openedFailed to open BLE device.Disconnected: failed to open deviceStart fails
Heart Rate service lookup deniedAccess denied to Heart Rate service. Device may require pairing.Disconnected: access deniedStart fails
Heart Rate service absent or other statusHeart Rate service not found. Status: {Status}Disconnected: HR service not foundStart fails
Measurement characteristic lookup deniedAccess denied to Heart Rate Measurement characteristic. Device may require pairing.Disconnected: access deniedStart fails
Measurement characteristic absentHeart Rate Measurement characteristic not found. Status: {Status}Disconnected: HR characteristic not foundStart fails
CCCD write did not succeedFailed to subscribe to Heart Rate notifications. Status: {Status}Disconnected: subscribe failedStart fails
Exception while decoding a notificationFailed to parse HR Data from BLE WearableUnchangedNotification dropped; caches keep their previous values; nothing publishes for it in native-rate mode
Exception while building a polled frameFailed to parse HR Data from BLE deviceUnchangedThat cycle publishes nothing
Link lost while capturingNoneStays ConnectedNotifications stop silently; no reconnect; polled mode keeps republishing the last values
Capture stopNoneStoppedHandler detached, device disposed, scan stopped

An exception escaping the per-cycle capture call would be logged by the SDK as “Error in module” and the loop would continue, but the module catches its own exceptions before that point.

10. Units and conventions

ConceptConvention
Heart rateBeats per minute, as reported by the device, unvalidated and unrange-checked
R-R intervalMilliseconds, converted from the device’s 1/1024 s units
Energy expendedKilocalories, converted from the characteristic’s kilojoule field. Per the Bluetooth specification this is cumulative since the device’s own reset, not a per-notification delta
Address textSix bytes, uppercase hex, colon separated, most significant byte first
Time baseHost wall clock, UTC Unix milliseconds. The Heart Rate Measurement characteristic carries no device timestamp, so no device time is available
CoordinatesNone. The module publishes no spatial data

11. Configuration affecting the interfaces

Module settings live under HEAT.BluetoothWearableModule.

SettingDefaultInterface effect
DeviceIDempty stringThe only address the advertisement filter accepts. Compared as an exact uppercase colon-separated string. Empty matches nothing
SendAtNativeRatetrueSelects the publication path; see 7.2
EnabledtrueWhether the module runs at all
Interval1000 msPoll cadence in polled mode. In native-rate mode the loop still ticks at this interval and publishes nothing
OverrideNodeId, NodeIdfalse, 0Node id carried on the published packet
Timeout30 sNot used by this module

The 30-second startup allowance, the 1000 ms post-connect wait, the 5000 ms settings-scan duration and both GATT UUIDs are compile-time constants, not settings.

See Manage capture modules for the generic settings and the SDK contract for the lifecycle these settings drive.

12. Compatibility and versioning

There is no protocol version negotiation. The module assumes a device that exposes the Bluetooth SIG Heart Rate Service and a Heart Rate Measurement characteristic whose flags follow the published bit assignments. Any conformant device works; no vendor-specific service is used.

The module targets net10.0-windows10.0.26100.0 and depends on HEAT.Capture.ModuleSDK 2.0.5 and Microsoft.Windows.CsWinRT 2.2.0. It uses Windows’ own Bluetooth LE API, so it runs only on Windows hosts with a BLE adapter and the projected WinRT dependencies present in the installed package.

12.1 Repository README discrepancies

The repository README disagrees with the code at this revision. Where they differ, the code is authoritative and this document follows the code.

  • The README names the settings type HRWearableSettings; the actual type is BluetoothWearableSettings.
  • The README describes DeviceID as a device name and states that leaving it empty connects to the first available device. Neither is implemented: the value is an address, and empty matches nothing.
  • The README’s testing and dependency notes require an ANT+ USB stick and ANT+ libraries in a Process/ directory. This module uses no ANT+ hardware and the repository contains no such directory.

13. Security considerations

The module attempts GATT access without pairing and never initiates pairing, so on an unpaired device the link is unencrypted and the heart rate stream is readable by any radio in range. Where a device supports bonding, pair it in Windows Bluetooth settings before capture so the link is encrypted; the module logs which of the two cases applies.

Device selection is by advertised address alone, with no service filter and no authentication. A radio that advertises the configured address would be connected to and subscribed. Treat the configured address as an identifier, not a credential.

The module exposes no network endpoint and holds no application credential. Heart rate, R-R interval and energy expenditure are personal health data: restrict access to the workstation and to captured sessions according to the deployment’s own data-handling policy.

14. Conformance

  1. Power on a BLE strap that advertises the Heart Rate Service. Open the module settings and confirm the Bluetooth device list offers it within the 5-second scan, labelled by name where the device provides one.
  2. Select the device so DeviceID holds the uppercase colon-separated address, then start a capture session. Confirm the log records Scanning for Device..., then Found device: NAME, Address: HEX_ADDRESS, then the pairing-state line, then Device Connected. Getting GATT Services..., and that the reported state reaches Connected.
  3. Confirm one entity carrying a BiometricSensorComponent with exactly the readings HeartRate, RR and kcal appears in the captured data.
  4. Wear the strap and confirm HeartRate changes across successive frames.
  5. Confirm the seed behaviour in 7.3. With SendAtNativeRate false, capture a frame before the first notification and confirm it reports HeartRate 60, RR 0 and kcal 0. Until BLE-01 is fixed, discard frames captured before the receiving indicator turns true.
  6. Confirm optional-field behaviour for your device. If it never sets flags bit 3 or bit 4, confirm kcal and RR remain 0 for the whole session and are not reported downstream as measurements.
  7. Walk the strap out of range while capturing. Confirm notifications stop with no warning and no reconnect, that the reported state still reads Connected, and that in polled mode the last values continue republishing. Stop and restart the capture session and confirm the scan and connection sequence runs again.
  8. Set DeviceID to the same address in lowercase and start a session. Confirm the module fails to start after the 30-second startup timeout, demonstrating that the address comparison is case sensitive.

15. Revision history

RevDateProduct versionChange
12026-09-0279b0738Initial issue.
Opens your browser's print dialog. Choose "Save as PDF" as the destination.