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.
2100 mm is an example, not your wheel: measure one full roll-out of your tire and enter it. Speed and distance scale with it; cadence does not use it.
Paste two values
Cadence and speed need two consecutive notifications from the same sensor. Paste them as hex, in order. The samples are synthetic, built from the SIG’s structure by the module that decodes them.
Sample: Crank revolutions go 65535 → 1 and the crank event time 0xFC00 → 0x0400: both uint16 counters wrapped, and the step is still 2 revolutions in 2048 ticks.
Crank: +2 rev / 2048 ticks (2 s) 60 rpm
Wheel: +4 rev / 2048 ticks (2 s) 15.12 km/h (120 wheel rpm)
rpm = Δrevolutions ÷ (Δticks × tick) × 60, with both counters wrapped at their own width; km/h = Δwheel revolutions × circumference ÷ Δt × 3.6.
Previous: 03 E8 03 00 00 00 04 FF FF 00 FC
CSC Measurement (0x2A5B) · 11 octets · little-endian
| Field | Present when | Octets | Raw | Value |
|---|---|---|---|---|
| Flagsboolean[8] | alwayspresent | 0–0 | 3 | 0x03 |
| Wheel Revolution Datastruct | bit 0 of Flags is 1present | 1–6 | 1000 / 1024 |
|
| Crank Revolution Datastruct | bit 1 of Flags is 1present | 7–10 | 65535 / 64512 |
|
Flags bit by bit 0x03
| Bit | Value | The SIG’s description |
|---|---|---|
| 0 | 1 | Wheel Revolution Data Present |
| 1 | 1 | Crank Revolution Data Present |
| 2–7 | 000000b | Reserved for Future Use |
Next: 03 EC 03 00 00 00 0C 01 00 00 04
CSC Measurement (0x2A5B) · 11 octets · little-endian
| Field | Present when | Octets | Raw | Value |
|---|---|---|---|---|
| Flagsboolean[8] | alwayspresent | 0–0 | 3 | 0x03 |
| Wheel Revolution Datastruct | bit 0 of Flags is 1present | 1–6 | 1004 / 3072 |
|
| Crank Revolution Datastruct | bit 1 of Flags is 1present | 7–10 | 1 / 1024 |
|
Flags bit by bit 0x03
| Bit | Value | The SIG’s description |
|---|---|---|
| 0 | 1 | Wheel Revolution Data Present |
| 1 | 1 | Crank Revolution Data Present |
| 2–7 | 000000b | Reserved for Future Use |
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 DataCumulative Wheel Revolutions (uint32); Last Wheel Event Time (uint16, × 0.000488) | struct | 0 or 6 | bit 4 of Flags is 1 | — |
| Crank Revolution DataCumulative Crank Revolutions (uint16); Last Crank Event Time (uint16, × 0.000977) | struct | 0 or 4 | bit 5 of Flags is 1 | — |
| Extreme Force MagnitudesMaximum Force Magnitude (sint16); Minimum Force Magnitude (sint16) | struct | 0 or 4 | bit 6 of Flags is 1 | — |
| Extreme Torque MagnitudesMaximum Torque Magnitude (sint16, × 0.03125); Minimum Torque Magnitude (sint16, × 0.03125) | struct | 0 or 4 | bit 7 of Flags is 1 | — |
| Extreme AnglesMaximum 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 · the SIG’s file 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 DataCumulative Wheel Revolutions (uint32); Last Wheel Event Time (uint16, × 0.000977) | struct | 0 or 6 | bit 0 of Flags is 1 | — |
| Crank Revolution DataCumulative 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 · the SIG’s file 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 · the SIG’s file 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 · the SIG’s file 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 · the SIG’s file gss/org.bluetooth.characteristic.sensor_location.yaml
The arithmetic, step by step
- 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.
- Difference two readings, with the wrap. Cumulative Crank Revolutions is a
uint16and every event time auint16, so they roll over: a later reading smaller than the earlier one means the counter wrapped, and the step is the difference modulo 216. 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. - Scale the time. CSC’s wheel event time:
Represented values: M = 1, d = 0, b = -10
, besideUnit is 1/1024th of a second
. Cycling Power’s:Represented values: M = 1, d = 0, b = -11
, besideUnit is 1/2048 second
. The lab multiplies by M × 10d × 2b; a parser that assumes one tick for both reads Cycling Power wheel speed at half or double its value. - 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.
- 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 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, 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.
- Smart-trainer data that arrives as FTMS Indoor Bike Data instead: that has its own FTMS lab, and the trainer side is in indoor bike trainer integration.
The radio choice is in ANT+ vs Bluetooth, CI without a sensor in testing BLE fitness devices, and any other characteristic in the 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.