---
title: "Cycling power and cadence lab: from revolution counts to rpm"
canonical: "https://aifitnessapi.com/tools/cycling-power-lab"
markdown: "https://aifitnessapi.com/tools/cycling-power-lab.md"
type: "tool"
last_reviewed: "2026-11-05"
description: "Decode Bluetooth Cycling Power (0x2A63) and CSC (0x2A5B) in the browser, and turn wheel and crank revolution counts into cadence and speed, live or pasted."
publisher: "AIFitnessAPI — funded by KinesteX; first-party pages disclose it"
cite_as: "\"Cycling power and cadence lab: from revolution counts to rpm\", AIFitnessAPI, https://aifitnessapi.com/tools/cycling-power-lab"
---

# Cycling power and cadence lab: from revolution counts to rpm

> Neither Cycling Power Measurement (0x2A63) nor CSC Measurement (0x2A5B) sends cadence or speed. Each sends a cumulative revolution count and the time of the last revolution event, so cadence is the change in crank revolutions divided by the change in event time between two notifications, and speed is the change in wheel revolutions times your wheel’s circumference over the same interval. The Bluetooth SIG’s structures give the tick: 1/1024 s for both CSC event times, but 1/2048 s for the Cycling Power wheel and 1/1024 s for its crank. This lab connects a power meter or speed/cadence sensor over Web Bluetooth and does that arithmetic in the open, or on two hex values you paste.

- Canonical: https://aifitnessapi.com/tools/cycling-power-lab
- Last reviewed: 2026-11-05
- Publisher: AIFitnessAPI (https://aifitnessapi.com)
- Cite as: "Cycling power and cadence lab: from revolution counts to rpm", AIFitnessAPI, https://aifitnessapi.com/tools/cycling-power-lab

---

*[Interactive: the Web Bluetooth connection, live cadence and speed, paste boxes and synthetic ride, in the browser at https://aifitnessapi.com/tools/cycling-power-lab; not reproduced in this mirror.]*

## The structures it decodes against

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 two measurements, then the characteristics the lab reads once after connecting:

### Cycling Power Measurement (0x2A63) · 12 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Flags | `boolean[16]` | 2 | always | flags |
| Instantaneous Power | `sint16` | 2 | always | `watt` |
| Pedal Power Balance | `uint8` | 0 or 1 | bit 0 of Flags is 1 | `percentage`, × 0.5 (“Represented values: M = 1, d = 0, b = -1”) |
| Accumulated Torque | `uint16` | 0 or 2 | bit 2 of Flags is 1 | `newton metre`, × 0.03125 (“Represented values: M = 1, d = 0, b = -5”) |
| Wheel Revolution Data: Cumulative Wheel Revolutions (uint32); Last Wheel Event Time (uint16, × 0.000488) | `struct` | 0 or 6 | bit 4 of Flags is 1 | — |
| Crank Revolution Data: Cumulative Crank Revolutions (uint16); Last Crank Event Time (uint16, × 0.000977) | `struct` | 0 or 4 | bit 5 of Flags is 1 | — |
| Extreme Force Magnitudes: Maximum Force Magnitude (sint16); Minimum Force Magnitude (sint16) | `struct` | 0 or 4 | bit 6 of Flags is 1 | — |
| Extreme Torque Magnitudes: Maximum Torque Magnitude (sint16, × 0.03125); Minimum Torque Magnitude (sint16, × 0.03125) | `struct` | 0 or 4 | bit 7 of Flags is 1 | — |
| Extreme Angles: Maximum Angle (uint12); Minimum Angle (uint12) | `struct` | 0 or 3 | bit 8 of Flags is 1 | — |
| Top Dead Spot Angle | `uint16` | 0 or 2 | bit 9 of Flags is 1 | `degree` |
| Bottom Dead Spot Angle | `uint16` | 0 or 2 | bit 10 of Flags is 1 | `degree` |
| Accumulated Energy | `uint16` | 0 or 2 | bit 11 of Flags is 1 | `joule`, × 1000 (“Represented values: M = 1, d = 3, b = 0”) |

[Cycling Power Measurement on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/cycling-power-measurement) · the SIG’s file [`gss/org.bluetooth.characteristic.cycling_power_measurement.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.cycling_power_measurement.yaml)

### CSC Measurement (0x2A5B) · 3 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Flags | `boolean[8]` | 1 | always | flags |
| Wheel Revolution Data: Cumulative Wheel Revolutions (uint32); Last Wheel Event Time (uint16, × 0.000977) | `struct` | 0 or 6 | bit 0 of Flags is 1 | — |
| Crank Revolution Data: Cumulative Crank Revolutions (uint16); Last Crank Event Time (uint16, × 0.000977) | `struct` | 0 or 4 | bit 1 of Flags is 1 | — |

[CSC Measurement on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness/csc-measurement) · the SIG’s file [`gss/org.bluetooth.characteristic.csc_measurement.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.csc_measurement.yaml)

### Cycling Power Feature (0x2A65) · 1 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Cycling Power Feature | `boolean[32]` | 4 | always | flags |

[Cycling Power Feature on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness#char-cycling-power-feature) · the SIG’s file [`gss/org.bluetooth.characteristic.cycling_power_feature.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.cycling_power_feature.yaml)

### CSC Feature (0x2A5C) · 1 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| CSC Feature | `boolean[16]` | 2 | always | flags |

[CSC Feature on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness#char-csc-feature) · the SIG’s file [`gss/org.bluetooth.characteristic.csc_feature.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.csc_feature.yaml)

### Sensor Location (0x2A5D) · 1 fields

| Field | Type | Size | Present when | Unit and resolution |
|---|---|---|---|---|
| Sensor Location | `uint8` | 1 | always | — |

[Sensor Location on the Bluetooth GATT reference](https://aifitnessapi.com/bluetooth-fitness#char-sensor-location) · the SIG’s file [`gss/org.bluetooth.characteristic.sensor_location.yaml`](https://bitbucket.org/bluetooth-SIG/public/src/408f4a0305d87ea75d9f700e3c8f85aa1d3a74fd/gss/org.bluetooth.characteristic.sensor_location.yaml)

## The arithmetic, step by step

1. **Find the struct.** In CSC Measurement, Wheel Revolution Data is present when bit 0 of Flags is 1 and Crank Revolution Data when bit 1 is 1. In Cycling Power Measurement they hang on bits 4 and 5 of a two-octet Flags field, after Instantaneous Power, which is always there.
2. **Difference two readings, with the wrap.** Cumulative Crank Revolutions is a `uint16` and every event time a `uint16`, so they roll over: a later reading smaller than the earlier one means the counter wrapped, and the step is the difference modulo 2^16. At 1/1024 s the event time wraps every 64 s; the Cycling Power wheel’s, at 1/2048 s, every 32 s. The synthetic ride wraps both.
3. **Scale the time.** CSC’s wheel event time: “Represented values: M = 1, d = 0, b = -10”, beside “Unit is 1/1024th of a second”. Cycling Power’s: “Represented values: M = 1, d = 0, b = -11”, beside “Unit is 1/2048 second”. The lab multiplies by M × 10^d × 2^b; a parser that assumes one tick for both reads Cycling Power wheel speed at half or double its value.
4. **Divide.** Cadence in rpm is Δcrank revolutions ÷ Δt × 60. Wheel speed is Δwheel revolutions × circumference ÷ Δt; the lab shows km/h (× 3.6 from m/s). The circumference is not in either characteristic, which is why the lab asks for it.
5. **Treat an unchanged event time as no new event.** Two notifications with the same event time mean no revolution finished between them (coasting, or a notification interval shorter than one revolution). The lab reports “no new event” and keeps the last cadence on screen labeled as such, rather than dividing by zero or showing 0 rpm.

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

- **Byte order.** Little-endian, as the [Web Bluetooth specification’s heart-rate example](https://webbluetoothcg.github.io/web-bluetooth/#example-heart-rate-monitor) reads every multi-octet field (`result.heartRate = data.getUint16(index, /*littleEndian=*/true);`, read 2026-10-31); the YAML gives sizes only.
- **Extreme Angles.** Two 12-bit members packed into three octets. The SIG’s file gives the example itself (Maximum Angle 0xABC and Minimum Angle 0x123 make 0x123ABC), and the test suite decodes exactly that example.
- **Control points.** Cycling Power Control Point and SC Control Point have no member layout for their Parameter in the files this site reads, so the lab neither decodes nor writes them. It writes nothing to a sensor.

## 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 boxes and the replay still work.

## What the lab cannot tell you

- Accuracy. Nothing here compares a power meter or sensor with a reference.
- Which source to trust when a trainer, a power meter and a cadence sensor all report the same pedal stroke. The lab shows each characteristic separately and never merges them; picking one source per metric is covered in [reading cycling power, cadence and speed sensors](https://aifitnessapi.com/devices/cycling-sensors-power-cadence).
- Smart-trainer data that arrives as FTMS Indoor Bike Data instead: that has its own [FTMS lab](https://aifitnessapi.com/tools/ftms-lab), and the trainer side is in [indoor bike trainer integration](https://aifitnessapi.com/devices/indoor-bike-trainer-integration).

The radio choice is in [ANT+ vs Bluetooth](https://aifitnessapi.com/devices/ant-plus-vs-bluetooth), CI without a sensor in [testing BLE fitness devices](https://aifitnessapi.com/devices/testing-ble-fitness-devices), and any other characteristic in the [BLE GATT explorer](https://aifitnessapi.com/tools/ble-gatt-explorer).

## How this works, and what it sends

Nothing leaves your device. The browser’s own chooser lists devices advertising the Cycling Power or Cycling Speed and Cadence service; the page can reach only the one you pick and only those services. It subscribes to the two measurements, reads Cycling Power Feature, CSC Feature and Sensor Location once where present, and writes nothing. Values stay in this tab’s memory; this site has no server code behind the page and stores nothing.

The synthetic ride and the sample pairs are generated, not recorded from a sensor: the numbers are made up, encoded by the same module that decodes them, with a coasting stretch where the crank event time stops.

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).
