Smart Bike Trainer Integration: FTMS and Cycling Power
Last verified August 14, 2026 · 5 min read
Smart trainers are the most generous devices in the FTMS family — they usually give you more than one way to read the same effort — and that generosity is the first design problem you have to solve.
Two services, frequently on one device#
Per the Bluetooth SIG's public assigned-numbers registry:
- The Fitness Machine Service is 0x1826, and Indoor Bike Data is characteristic 0x2AD2.
- Cycling Power is a separate service, 0x1818, with Cycling Power Measurement at 0x2A63.
Many trainers expose the standalone Cycling Power service alongside FTMS. That is a feature, not a bug — Cycling Power is the older, narrower profile that head units and cycling apps have consumed for years, and keeping it means the trainer works with things that never heard of FTMS. But it means your app can end up subscribed to two characteristics that are both describing one rider's legs.
Pick one source of truth, and write it down#
Our recommendation, as judgement: choose a source per metric per session, decide it at connection time from what the device advertises, persist that decision alongside the session, and do not switch mid-ride. Switching sources mid-session produces a discontinuity that looks like a physiological event and is not one.
The reconciliation this avoids is the same problem in a different costume from watch-and-phone double counting, and the general design lives in deduplicating health data. The trainer-specific twist is that both readings come from one device, so a naive "different device, different source" heuristic will not save you. Key on the service, not on the peripheral.
For the standalone sensor side of this — a separate power meter, a cadence sensor, a speed sensor — see cycling power and cadence sensors, which covers services 0x1818, 0x1816 and 0x1814 and the pairing UX that goes with owning several of them.
Discover capability before you render controls#
FTMS supplies Fitness Machine Feature, 0x2ACC, for the machine's feature declaration, and two range characteristics that matter for trainers in particular: Supported Resistance Level Range, 0x2AD6, and Supported Power Range, 0x2AD8. There is also Supported Heart Rate Range, 0x2AD7, though heart rate itself belongs to its own service.
Read those at connection time, cache them per device, and drive the interface from them. A resistance slider on a trainer that has not confirmed a resistance range is a control that will disappoint someone, and a power target field with no discovered power bounds gives you nothing to clamp against. This is the same rule as everywhere else in the cluster: nothing in the UI that the connected hardware has not confirmed.
Control: the characteristic exists, the guarantee does not#
FTMS defines Fitness Machine Control Point, 0x2AD9, for writing commands to a machine, and Fitness Machine Status, 0x2ADA, for the machine to report state changes back.
We are deliberately not documenting command layouts, op codes or specific control modes here, because we have not read the specification PDF and inventing that detail is how broken integrations get born. The FTMS specification published by the Bluetooth SIG is the source of truth for every wire format, and the trainer manufacturer's own documentation is the source of truth for what that particular trainer implements.
What we can say about the ecosystem is reported rather than verified: integrators and reviewers widely report that many machines broadcast FTMS data but ignore or restrict remote-control commands outside the brand's own app, with support varying by model and region. Capability and exact command support vary per trainer. So build the resistance and target-power features behind a per-model capability flag you set from testing, not from the presence of a characteristic, and make "this trainer reports but does not accept control" a first-class supported state in your product rather than an error screen.
If you do write to a trainer, the conservative practices from treadmill integration carry over in spirit: clamp to discovered ranges, debounce and serialize writes rather than emitting one per slider pixel, and update the UI from Machine Status and the data characteristic rather than optimistically from what you sent. The consequences are lower than on a treadmill — nobody falls off a trainer because a resistance command landed twice — but a control loop with no feedback leg still lies to the user.
Cadence, speed and the metrics you will be asked for#
Cyclists ask for more than watts. Some of what you want may arrive through Indoor Bike Data, some through Cycling Power Measurement, and some only from a separate sensor. Rather than guessing which fields a given trainer populates, do the boring thing: read the spec for the exact contents of each characteristic, then confirm empirically per model what is actually present, and let your UI reflect what arrived rather than what you hoped would.
Heart rate is not part of this. It has its own standardized service, 0x180D, with its own connection lifecycle — see Bluetooth heart rate monitors. A trainer session that also shows heart rate is two independent connections that can fail independently, and your session model should survive one of them dropping.
Platform notes#
On iOS this is Core Bluetooth work. Apple requires NSBluetoothAlwaysUsageDescription in Info.plist since iOS 13 (NSBluetoothPeripheralUsageDescription on iOS 12 and earlier) and states that "your app will crash if its Info.plist doesn't include usage description keys for the types of data it needs to access". Apple also documents that you should not subclass Core Bluetooth classes, because "overriding these classes isn't supported and results in undefined behavior". See iOS BLE fitness devices.
For a browser-based trainer app, the caniuse dataset puts Web Bluetooth in Chrome 56+, Edge 79+, Opera 43+ and Samsung Internet 6.2+, with no support in Firefox at any version and none in Safari on desktop or iOS. That is a product decision, not a polyfill problem — see Web Bluetooth for fitness.
On radios: BLE is the default for new fitness-sensor development in 2026, and the ANT+ program wind-down is reported rather than verified at source. ANT+ versus Bluetooth lays out what is reported and what follows for a trainer app.
Testing#
Build the seam. A physical trainer is the only reliable target for the radio layer, but parsing, source selection, range clamping, reconnect behavior and session assembly can all run deterministically in CI from recorded streams. Reserve the hardware for the capability matrix, which is the part no fixture can answer. See testing BLE fitness devices and mocking wearable data.
Frequently asked questions
- Should a smart trainer app read Indoor Bike Data (0x2AD2) or the Cycling Power service (0x1818)?
- Many trainers expose both, so the answer is to choose deliberately rather than subscribe to everything. Indoor Bike Data 0x2AD2 sits under the Fitness Machine Service 0x1826; Cycling Power Measurement 0x2A63 sits under the standalone Cycling Power service 0x1818. Both can describe the same pedaling, so decide a source per metric at connection time based on what the device advertises, persist that decision with the session, and do not switch mid-ride, because switching produces a discontinuity that looks like a physiological event. Key the decision on the service rather than the peripheral, since here both interfaces belong to one device.
- Does every FTMS smart trainer accept resistance or target power commands from a third-party app?
- No, and you should assume nothing from the characteristic list. The Fitness Machine Control Point 0x2AD9 exists in the service, with Fitness Machine Status 0x2ADA for the machine to report changes back, but capability and exact command support vary per trainer. Integrators and reviewers widely report that many machines broadcast FTMS data while ignoring or restricting remote-control commands outside the brand's own app, varying by model and region. Verify against the actual hardware and the manufacturer's documentation, gate control features behind a per-model capability flag you set from testing, and make a report-only trainer a supported state rather than an error.
- Which characteristics tell me a smart trainer's resistance and power limits?
- Supported Resistance Level Range 0x2AD6 and Supported Power Range 0x2AD8, both under the Fitness Machine Service 0x1826, alongside Fitness Machine Feature 0x2ACC for the machine's feature declaration. Supported Heart Rate Range 0x2AD7 also exists, though heart rate itself is served by its own service 0x180D. Read these once at connection time and cache them per device. They do two jobs: they tell you which controls are worth rendering at all, and they give you the bounds to clamp any value you send, applied close to the write rather than in the view layer.
Keep reading
Next steps
Was this page useful?
Independent comparison, last reviewed August 14, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.
← All connected devices · by AIFitnessAPI