---
title: "FTMS lab: decode treadmill, bike and rower Bluetooth data"
canonical: "https://aifitnessapi.com/tools/ftms-lab"
markdown: "https://aifitnessapi.com/tools/ftms-lab.md"
type: "tool"
last_reviewed: "2026-11-05"
description: "Decode Bluetooth FTMS Treadmill, Indoor Bike, Rower and Cross Trainer Data in the browser: each flag bit and field, live from a machine or from pasted hex."
publisher: "AIFitnessAPI — funded by KinesteX; first-party pages disclose it"
cite_as: "\"FTMS lab: decode treadmill, bike and rower Bluetooth data\", AIFitnessAPI, https://aifitnessapi.com/tools/ftms-lab"
---

# FTMS lab: decode treadmill, bike and rower Bluetooth data

> An FTMS machine data value is a Flags field followed only by the fields its bits announce, so the same characteristic arrives in different lengths and you cannot read it at fixed offsets. In Treadmill Data (0x2ACD), Instantaneous Speed is present when bit 0 (“More Data”) is 0, every other field when its own bit is 1, and the Bluetooth SIG’s structure gives each field’s size and resolution: speed in 1/100 km/h, Indoor Bike cadence in 1/2 rpm. This lab connects a treadmill, indoor bike, rower or cross trainer over Web Bluetooth and decodes every value against that structure, or decodes hex you paste, with no machine at all.

- Canonical: https://aifitnessapi.com/tools/ftms-lab
- Last reviewed: 2026-11-05
- Publisher: AIFitnessAPI (https://aifitnessapi.com)
- Cite as: "FTMS lab: decode treadmill, bike and rower Bluetooth data", AIFitnessAPI, https://aifitnessapi.com/tools/ftms-lab

---

*[Interactive: the Web Bluetooth connection, live decoder, paste box and synthetic replays, in the browser at https://aifitnessapi.com/tools/ftms-lab; not reproduced in this mirror.]*

## The structures it decodes against

Each table is the layout the lab compiled from the Bluetooth SIG’s GATT Specification Supplement files at commit 408f4a0 of the SIG’s public repository (read 2026-10-17), through this site’s generated copy. The machine data characteristics, then the Supported Range characteristics the lab reads once after connecting:

### Treadmill Data (0x2ACD) · 19 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Flags | `boolean[16]` | 2 | always | flags |
| Instantaneous Speed | `uint16` | 0 or 2 | bit 0 of Flags is 0 | `kilometre per hour`, × 0.01 (“Represented values: M = 1, d = -2, b = 0”) |
| Average Speed | `uint16` | 0 or 2 | bit 1 of Flags is 1 | `kilometre per hour`, × 0.01 (“Represented values: M = 1, d = -2, b = 0”) |
| Total Distance | `uint24` | 0 or 3 | bit 2 of Flags is 1 | `metre` |
| Inclination | `sint16` | 0 or 2 | bit 3 of Flags is 1 | `percentage`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Ramp Angle Setting | `sint16` | 0 or 2 | bit 3 of Flags is 1 | `degree`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Positive Elevation Gain | `uint16` | 0 or 2 | bit 4 of Flags is 1 | `metre`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Negative Elevation Gain | `uint16` | 0 or 2 | bit 4 of Flags is 1 | `metre`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Instantaneous Pace (Time per 500 meters) | `uint16` | 0 or 2 | bit 5 of Flags is 1 | `second` |
| Average Pace (Time per 500 meters) | `uint16` | 0 or 2 | bit 6 of Flags is 1 | `second` |
| Total Energy | `uint16` | 0 or 2 | bit 7 of Flags is 1 | `kilogram calorie` |
| Energy Per Hour | `uint16` | 0 or 2 | bit 7 of Flags is 1 | `kilogram calorie` |
| Energy Per Minute | `uint8` | 0 or 1 | bit 7 of Flags is 1 | `kilogram calorie` |
| Heart Rate | `uint8` | 0 or 1 | bit 8 of Flags is 1 | `beats per minute` |
| Metabolic Equivalent | `uint8` | 0 or 1 | bit 9 of Flags is 1 | `metabolic equivalent` |
| Elapsed Time | `uint16` | 0 or 2 | bit 10 of Flags is 1 | `second` |
| Remaining Time | `uint16` | 0 or 2 | bit 11 of Flags is 1 | `second` |
| Force On Belt | `sint16` | 0 or 2 | bit 12 of Flags is 1 | `newton` |
| Power Output | `sint16` | 0 or 2 | bit 12 of Flags is 1 | `watt` |

[Treadmill Data on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/treadmill-data) · the SIG’s file [`gss/org.bluetooth.characteristic.treadmill_data.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.treadmill_data.yaml)

### Cross Trainer Data (0x2ACE) · 21 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Flags | `boolean[24]` | 3 | always | flags |
| Instantaneous Speed | `uint16` | 0 or 2 | bit 0 of Flags is 0 | `kilometre per hour`, × 0.01 (“Represented values: M = 1, d = -2, b = 0”) |
| Average Speed | `uint16` | 0 or 2 | bit 1 of Flags is 1 | `kilometre per hour`, × 0.01 (“Represented values: M = 1, d = -2, b = 0”) |
| Total Distance | `uint24` | 0 or 3 | bit 2 of Flags is 1 | `metre` |
| Steps Per Minute | `uint16` | 0 or 2 | bit 3 of Flags is 1 | `per minute` |
| Average Step Rate | `uint16` | 0 or 2 | bit 3 of Flags is 1 | `per minute` |
| Stride Count | `uint16` | 0 or 2 | bit 4 of Flags is 1 | —, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Positive Elevation Gain | `uint16` | 0 or 2 | bit 5 of Flags is 1 | `metre` |
| Negative Elevation Gain | `uint16` | 0 or 2 | bit 5 of Flags is 1 | `metre` |
| Inclination | `sint16` | 0 or 2 | bit 6 of Flags is 1 | `percentage`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Ramp Setting | `sint16` | 0 or 2 | bit 6 of Flags is 1 | `degree`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Resistance Level | `uint8` | 0 or 1 | bit 7 of Flags is 1 | —, × 10 (“Represented values: M = 1, d = 1, b = 0”) — the same field also says “Unit is 1” |
| Instantaneous Power | `sint16` | 0 or 2 | bit 8 of Flags is 1 | `watt` |
| Average Power | `sint16` | 0 or 2 | bit 9 of Flags is 1 | `watt` |
| Total Energy | `uint16` | 0 or 2 | bit 10 of Flags is 1 | `kilogram calorie` |
| Energy Per Hour | `uint16` | 0 or 2 | bit 10 of Flags is 1 | `kilogram calorie` |
| Energy Per Minute | `uint8` | 0 or 1 | bit 10 of Flags is 1 | `kilogram calorie` |
| Heart Rate | `uint8` | 0 or 1 | bit 11 of Flags is 1 | `beats per minute` |
| Metabolic Equivalent | `uint8` | 0 or 1 | bit 12 of Flags is 1 | `metabolic equivalent`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Elapsed Time | `uint16` | 0 or 2 | bit 13 of Flags is 1 | `second` |
| Remaining Time | `uint16` | 0 or 2 | bit 14 of Flags is 1 | `second` |

[Cross Trainer Data on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/cross-trainer-data) · the SIG’s file [`gss/org.bluetooth.characteristic.cross_trainer_data.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.cross_trainer_data.yaml)

### Step Climber Data (0x2ACF) · 13 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Flags | `boolean[16]` | 2 | always | flags |
| Floors | `uint16` | 0 or 2 | bit 0 of Flags is 0 | — |
| Step Count | `uint16` | 0 or 2 | bit 0 of Flags is 0 | — |
| Steps Per Minute | `uint16` | 0 or 2 | bit 1 of Flags is 1 | `per minute` |
| Average Step Rate | `uint16` | 0 or 2 | bit 2 of Flags is 1 | `per minute` |
| Positive Elevation Gain | `uint16` | 0 or 2 | bit 3 of Flags is 1 | `metre` |
| Total Energy | `uint16` | 0 or 2 | bit 4 of Flags is 1 | `kilogram calorie` |
| Energy Per Hour | `uint16` | 0 or 2 | bit 4 of Flags is 1 | `kilogram calorie` |
| Energy Per Minute | `uint8` | 0 or 1 | bit 4 of Flags is 1 | `kilogram calorie` |
| Heart Rate | `uint8` | 0 or 1 | bit 5 of Flags is 1 | `beats per minute` |
| Metabolic Equivalent | `uint8` | 0 or 1 | bit 6 of Flags is 1 | `metabolic equivalent`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Elapsed Time | `uint16` | 0 or 2 | bit 7 of Flags is 1 | `second` |
| Remaining Time | `uint16` | 0 or 2 | bit 8 of Flags is 1 | `second` |

[Step Climber Data on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/step-climber-data) · the SIG’s file [`gss/org.bluetooth.characteristic.step_climber_data.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.step_climber_data.yaml)

### Stair Climber Data (0x2AD0) · 13 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Flags | `boolean[16]` | 2 | always | flags |
| Floors | `uint16` | 0 or 2 | bit 0 of Flags is 0 | — |
| Steps Per Minute | `uint16` | 0 or 2 | bit 1 of Flags is 1 | `per minute` |
| Average Step Rate | `uint16` | 0 or 2 | bit 2 of Flags is 1 | `per minute` |
| Positive Elevation Gain | `uint16` | 0 or 2 | bit 3 of Flags is 1 | `metre` |
| Stride Count | `uint16` | 0 or 2 | bit 4 of Flags is 1 | — |
| Total Energy | `uint16` | 0 or 2 | bit 5 of Flags is 1 | `kilogram calorie` |
| Energy Per Hour | `uint16` | 0 or 2 | bit 5 of Flags is 1 | `kilogram calorie` |
| Energy Per Minute | `uint8` | 0 or 1 | bit 5 of Flags is 1 | `kilogram calorie` |
| Heart Rate | `uint8` | 0 or 1 | bit 6 of Flags is 1 | `beats per minute` |
| Metabolic Equivalent | `uint8` | 0 or 1 | bit 7 of Flags is 1 | `metabolic equivalent`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Elapsed Time | `uint16` | 0 or 2 | bit 8 of Flags is 1 | `second` |
| Remaining Time | `uint16` | 0 or 2 | bit 9 of Flags is 1 | `second` |

[Stair Climber Data on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/stair-climber-data) · the SIG’s file [`gss/org.bluetooth.characteristic.stair_climber_data.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.stair_climber_data.yaml)

### Rower Data (0x2AD1) · 17 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Flags | `boolean[16]` | 2 | always | flags |
| Stroke Rate | `uint8` | 0 or 1 | bit 0 of Flags is 0 | `per minute`, × 0.5 (“Represented values: M = 1, d = 0, b = -1”) |
| Stroke Count | `uint16` | 0 or 2 | bit 0 of Flags is 0 | — |
| Average Stroke Rate | `uint8` | 0 or 1 | bit 1 of Flags is 1 | `per minute`, × 0.5 (“Represented values: M = 1, d = 0, b = -1”) |
| Total Distance | `uint24` | 0 or 3 | bit 2 of Flags is 1 | `metre` |
| Instantaneous Pace (Time per 500 meters) | `uint16` | 0 or 2 | bit 3 of Flags is 1 | `second` |
| Average Pace (Time per 500 meters) | `uint16` | 0 or 2 | bit 4 of Flags is 1 | `second` |
| Instantaneous Power | `sint16` | 0 or 2 | bit 5 of Flags is 1 | `watt` |
| Average Power | `sint16` | 0 or 2 | bit 6 of Flags is 1 | `watt` |
| Resistance Level | `uint8` | 0 or 1 | bit 7 of Flags is 1 | —, × 10 (“Represented values: M = 1, d = 1, b = 0”) — the same field also says “Unit is 1” |
| Total Energy | `uint16` | 0 or 2 | bit 8 of Flags is 1 | `kilogram calorie` |
| Energy Per Hour | `uint16` | 0 or 2 | bit 8 of Flags is 1 | `kilogram calorie` |
| Energy Per Minute | `uint8` | 0 or 1 | bit 8 of Flags is 1 | `kilogram calorie` |
| Heart Rate | `uint8` | 0 or 1 | bit 9 of Flags is 1 | `beats per minute` |
| Metabolic Equivalent | `uint8` | 0 or 1 | bit 10 of Flags is 1 | `metabolic equivalent`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Elapsed Time | `uint16` | 0 or 2 | bit 11 of Flags is 1 | `second` |
| Remaining Time | `uint16` | 0 or 2 | bit 12 of Flags is 1 | `second` |

[Rower Data on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/rower-data) · the SIG’s file [`gss/org.bluetooth.characteristic.rower_data.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.rower_data.yaml)

### Indoor Bike Data (0x2AD2) · 16 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Flags | `boolean[16]` | 2 | always | flags |
| Instantaneous Speed | `uint16` | 0 or 2 | bit 0 of Flags is 0 | `kilometre per hour`, × 0.01 (“Represented values: M = 1, d = -2, b = 0”) |
| Average Speed | `uint16` | 0 or 2 | bit 1 of Flags is 1 | `kilometre per hour`, × 0.01 (“Represented values: M = 1, d = -2, b = 0”) |
| Instantaneous Cadence | `uint16` | 0 or 2 | bit 2 of Flags is 1 | `revolution per minute`, × 0.5 (“Represented values: M = 1, d = 0, b = -1”) |
| Average Cadence | `uint16` | 0 or 2 | bit 3 of Flags is 1 | `revolution per minute`, × 0.5 (“Represented values: M = 1, d = 0, b = -1”) |
| Total Distance | `uint24` | 0 or 3 | bit 4 of Flags is 1 | `metre` |
| Resistance Level | `uint8` | 0 or 1 | bit 5 of Flags is 1 | —, × 10 (“Represented values: M = 1, d = 1, b = 0”) — the same field also says “Unit is 1” |
| Instantaneous Power | `sint16` | 0 or 2 | bit 6 of Flags is 1 | `watt` |
| Average Power | `sint16` | 0 or 2 | bit 7 of Flags is 1 | `watt` |
| Total Energy | `uint16` | 0 or 2 | bit 8 of Flags is 1 | `kilogram calorie` |
| Energy Per Hour | `uint16` | 0 or 2 | bit 8 of Flags is 1 | `kilogram calorie` |
| Energy Per Minute | `uint8` | 0 or 1 | bit 8 of Flags is 1 | `kilogram calorie` |
| Heart Rate | `uint8` | 0 or 1 | bit 9 of Flags is 1 | `beats per minute` |
| Metabolic Equivalent | `uint8` | 0 or 1 | bit 10 of Flags is 1 | `metabolic equivalent`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Elapsed Time | `uint16` | 0 or 2 | bit 11 of Flags is 1 | `second` |
| Remaining Time | `uint16` | 0 or 2 | bit 12 of Flags is 1 | `second` |

[Indoor Bike Data on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/indoor-bike-data) · the SIG’s file [`gss/org.bluetooth.characteristic.indoor_bike_data.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.indoor_bike_data.yaml)

### Supported Speed Range (0x2AD4) · 3 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Minimum Speed | `uint16` | 2 | always | `kilometre per hour`, × 0.01 (“Represented values: M = 1, d = -2, b = 0”) |
| Maximum Speed | `uint16` | 2 | always | `kilometre per hour`, × 0.01 (“Represented values: M = 1, d = -2, b = 0”) |
| Minimum Increment | `uint16` | 2 | always | `kilometre per hour`, × 0.01 (“Represented values: M = 1, d = -2, b = 0”) |

[Supported Speed Range on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/supported-speed-range) · the SIG’s file [`gss/org.bluetooth.characteristic.supported_speed_range.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.supported_speed_range.yaml)

### Supported Inclination Range (0x2AD5) · 3 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Minimum Inclination | `sint16` | 2 | always | `percentage`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Maximum Inclination | `sint16` | 2 | always | `percentage`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |
| Minimum Increment | `uint16` | 2 | always | `percentage`, × 0.1 (“Represented values: M = 1, d = -1, b = 0”) |

[Supported Inclination Range on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/supported-inclination-range) · the SIG’s file [`gss/org.bluetooth.characteristic.supported_inclination_range.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.supported_inclination_range.yaml)

### Supported Resistance Level Range (0x2AD6) · 3 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Minimum Resistance Level | `uint8` | 1 | always | —, × 10 (“Represented values: M = 1, d = 1, b = 0”) — the same field also says “Unit is 1” |
| Maximum Resistance Level | `uint8` | 1 | always | —, × 10 (“Represented values: M = 1, d = 1, b = 0”) — the same field also says “Unit is 1” |
| Minimum Increment | `uint8` | 1 | always | —, × 10 (“Represented values: M = 1, d = 1, b = 0”) — the same field also says “Unit is 1” |

[Supported Resistance Level Range on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/supported-resistance-level-range) · the SIG’s file [`gss/org.bluetooth.characteristic.supported_resistance_level_range.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.supported_resistance_level_range.yaml)

### Supported Heart Rate Range (0x2AD7) · 3 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Minimum Heart Rate | `uint8` | 1 | always | `beats per minute` |
| Maximum Heart Rate | `uint8` | 1 | always | `beats per minute` |
| Minimum Increment | `uint8` | 1 | always | `beats per minute` |

[Supported Heart Rate Range on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/supported-heart-rate-range) · the SIG’s file [`gss/org.bluetooth.characteristic.supported_heart_rate_range.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.supported_heart_rate_range.yaml)

### Supported Power Range (0x2AD8) · 3 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Minimum Power | `sint16` | 2 | always | `watt` |
| Maximum Power | `sint16` | 2 | always | `watt` |
| Minimum Increment | `uint16` | 2 | always | `watt` |

[Supported Power Range on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/supported-power-range) · the SIG’s file [`gss/org.bluetooth.characteristic.supported_power_range.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.supported_power_range.yaml)

### Fitness Machine Feature (0x2ACC) · no structure: shown raw

The SIG's public repository has no gss structure for this characteristic. The lab shows its bytes as hex.

[Fitness Machine Feature on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness#char-fitness-machine-feature)

### Training Status (0x2AD3) · no structure: shown raw

The SIG's public repository has no gss structure for this characteristic. The lab shows its bytes as hex.

[Training Status on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness#char-training-status)

### Fitness Machine Control Point (0x2AD9) · no structure: shown raw

The SIG's public repository has no gss structure for this characteristic. The lab shows its bytes as hex.

[Fitness Machine Control Point on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness#char-fitness-machine-control-point)

### Fitness Machine Status (0x2ADA) · no structure: shown raw

The SIG's public repository has no gss structure for this characteristic. The lab shows its bytes as hex.

[Fitness Machine Status on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness#char-fitness-machine-status)

## How to read an FTMS value

1. **Flags first.** Treadmill, Indoor Bike and Rower Data open with a two-octet Flags field (`boolean[16]`); Cross Trainer Data’s is three octets (`boolean[24]`). Read it as a little-endian number.
2. **Walk the fields in order.** For each field, its “Present if” line names one bit and the value that makes it present. Present: read its size and move on. Absent: it takes no octets. Several fields can share one bit (Total Energy, Energy Per Hour and Energy Per Minute all hang on one), and bit 0 is the odd one out: “Present if bit 0 of Flags field is set to 0”.
3. **Scale each value.** The SIG gives a “Represented values: M = …, d = …, b = …” line where the raw number is not the unit, and the lab multiplies by M × 10^d × 2^b. Instantaneous Speed: “Represented values: M = 1, d = -2, b = 0”, beside “Unit is 1/100 of a kilometer per hour”. Instantaneous Cadence: “Represented values: M = 1, d = 0, b = -1”, beside “Unit is 1/2 of a revolution per minute”.
4. **Merge across notifications.** A machine with more to say than fits in one value can set bit 0 (“More Data”), which drops Instantaneous Speed from that value. The lab’s “Latest values” panel keeps the newest reading of each field from whichever notification carried it; the treadmill replay sends such a split packet every fifth second.

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

- **Byte order.** The YAML gives sizes, not byte order. The lab reads 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 scale formula.** The files do not spell out how M, d and b combine. The lab uses M × 10^d × 2^b, and its test suite checks that product against every “Unit is” sentence in the dataset it can parse (b = −10 beside “1/1024 second”, M = 5 and d = −3 beside “0.005 kilogram”, and so on): 60 lines, all agreeing except 8.
- **Resistance Level.** 6 of those 8 are FTMS resistance fields, where the SIG’s own lines disagree: “Represented values: M = 1, d = 1, b = 0” (a factor of 10) sits beside “Unit is 1”. The lab shows the value both ways and picks neither.
- **No structure, no decoding.** At this commit the SIG’s repository has no gss file for Fitness Machine Feature (0x2ACC), Training Status (0x2AD3), Fitness Machine Control Point (0x2AD9), Fitness Machine Status (0x2ADA). The lab shows their bytes as hex and does not decode them from memory. That is also why it has no control panel: setting a target speed or power means writing op codes to the Control Point, and the layout of those op codes is not in the files this site reads. The lab never writes to the machine.

## Which browsers can connect

Web Bluetooth is a Chromium feature. MDN's compatibility data lists it in Chrome (requestDevice since version 56), Edge, Opera and Samsung Internet, with the note that Chrome's Linux support is not enabled by default, and marks the API experimental. It lists no support in Firefox, in Safari on macOS or iOS, or in Android's WebView.

Source: [MDN browser-compat-data, api/Bluetooth.json](https://github.com/mdn/browser-compat-data/blob/main/api/Bluetooth.json), read 2026-11-05. Elsewhere the paste box and replays still work.

## What the lab cannot tell you

- Accuracy. The bytes say what the machine reports, not whether its belt speed or power reading is right.
- Which fields a model supports before it sends them. Fitness Machine Feature declares that, and it is one of the characteristics shown raw here.
- Anything about a machine that does not advertise the Fitness Machine service: the browser’s chooser only lists devices that do. For any other device, use the [BLE GATT explorer](https://aifitnessapi.com/tools/ble-gatt-explorer).

The integration side is in [what FTMS is](https://aifitnessapi.com/devices/ftms-fitness-machine-service), [treadmill app integration](https://aifitnessapi.com/devices/treadmill-app-integration), [indoor bike trainer integration](https://aifitnessapi.com/devices/indoor-bike-trainer-integration) and [rowing machine data](https://aifitnessapi.com/devices/rowing-machine-data); the radio question in [ANT+ vs Bluetooth](https://aifitnessapi.com/devices/ant-plus-vs-bluetooth). Power meters and cadence sensors have their own [cycling power and cadence lab](https://aifitnessapi.com/tools/cycling-power-lab).

## How this works, and what it sends

Nothing leaves your device. The browser shows its own device chooser, and the page can reach only the machine you pick and only its Fitness Machine service. It subscribes to the characteristics that notify (except the Control Point), reads the Supported Range and Feature characteristics once, and writes nothing. Values are decoded in this tab and held in memory until you close it; this site has no server code behind the page and stores nothing.

The samples and replays are generated, not recorded from a machine: their numbers are made up, and they are built by the same module that decodes them, from the field names in the SIG’s structure.

The structures come 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).
