Skip to content

distantnative/kettler-ride-apple-watch-ble

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kettler Ride 300 to Apple Watch BLE Adapter

An Arduino firmware for the Seeed Studio XIAO BLE (nRF52840) that acts as a wireless bridge between a Kettler Ride 300 / situs fitness stationary trainer and Apple Watch. It makes the trainer's power and cadence data visible natively in Apple Watch workouts, without any proprietary app.

The board runs as a BLE central (connecting to the trainer) and a BLE peripheral (advertising to Apple Watch) simultaneously. Apple Watch sees it as a standard Cycling Power + Cadence sensor and picks it up automatically when starting an Indoor Cycling workout.

This project was built with the assistance of Claude Code. It is a personal side project, not a commercial product. I am sharing the code as I got stuck with this. Maybe there is a niche of 2-3 other people out there who would like to do this as well. This repository might help them to get started. Use at your own risk. No guarantees are made regarding reliability or safety for any purpose. I am not planning to extend/develop this much further. Only if for myself a need comes up to do so.

Released under the MIT License.


Hardware

  • Board: Seeed Studio XIAO BLE nRF52840
  • Power: USB-C — plug into any USB wall adapter and leave it running permanently
  • Connection to Mac: USB-C (for flashing firmware and the Serial Monitor during first-time pairing)

Daily Use

Once commissioned, no manual steps are needed:

  1. Plug in the bridge (or leave it always powered)
  2. Turn on the trainer — select the Bluetooth screen on the trainer display
  3. On Apple Watch, start an Indoor Cycling workout
  4. Within a few seconds the watch sees the bridge as a power/cadence sensor
  5. Start pedaling — power and cadence appear on the watch

The bridge auto-connects to the trainer and auto-reconnects if either side drops. A hardware watchdog resets the chip automatically if the BLE stack ever locks up.


First-Time Commissioning (one-time only)

The trainer requires a 6-digit PIN pairing the first time. After that, the bond is stored in flash and PIN entry is never needed again.

Steps:

  1. Set CLEAR_BONDS 1 at the top of Ride.ino, flash
  2. Open the Serial Monitor at 115200 baud
  3. Power the trainer and select its Bluetooth screen
  4. The bridge connects and subscribes to the trainer's auth characteristic (1531), triggering the PIN flow
  5. The trainer display shows a 6-digit code — type it into Serial Monitor and press Enter
  6. Serial output confirms [AUTH] Pairing complete — bond stored
  7. Start pedaling to verify data flows
  8. Set back to CLEAR_BONDS 0 and flash again to ensure the bond isn't wiped on the next power up

If something goes wrong (e.g. trainer disconnects immediately with reason 0x13), set CLEAR_BONDS 1, reflash, then repeat from step 1.


Build & Flash

./build.sh

Requires arduino-cli with the Seeeduino nRF52 board support installed. The script compiles, detects the USB port, and uploads. If it can't find the port, double-tap the reset button on the board to enter DFU mode and re-run.

Dev setup (once)

brew install arduino-cli
arduino-cli config init
arduino-cli config add board_manager.additional_urls \
  https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json
arduino-cli core update-index
arduino-cli core install Seeeduino:nrf52

Serial Output Reference

Connect at 115200 baud to see status. Output is minimal during normal riding.

Prefix When
[BOOT] Startup
[SCAN] Scanning for / found trainer
[CONN] Trainer connected / disconnected, service discovery
[HAND] Handshake characteristic (1531) subscribe status
[AUTH] Pairing events (PIN prompt, complete, failed)
[PERI] Apple Watch connected / disconnected / subscribed
[DATA] Trainer data timeout (stopped pedaling)
[IDLE] Status heartbeat every 10s when not riding

Architecture

Trainer (situs fitness / Kettler Ride 300)
    │
    │  BLE — central role on nRF52840
    │  Scan for "situs fitness" → connect
    │  Subscribe: 1531 (auth trigger), 2A63 (power), 2A5B (cadence)
    │
 nRF52840 (XIAO BLE)
    │
    │  BLE — peripheral role on nRF52840
    │  Advertise as "Ride 300"
    │  Cycling Power Service (0x1818) + CSC Service (0x1816)
    │
Apple Watch (Indoor Cycling workout)

BLE Services — Trainer Side (central)

Service UUID Name
1818 Cycling Power
1816 Cycling Speed & Cadence
00001530-1212-EFDE-1523-785FEABCD123 Vendor (auth)

Characteristics subscribed:

UUID Service Data
2A63 1818 Power (sint16 W) + optional crank data
2A5B 1816 Crank revolutions (uint16) + crank event time (uint16, 1/1024s)
00001531-1212-EFDE-1523-785FEABCD123 Vendor Auth trigger — subscribe on connect to initiate PIN flow

BLE Services — Apple Watch Side (peripheral)

Advertised name: Ride 300

Cycling Power Service (0x1818)

Characteristic UUID Properties Value
CP Measurement 2A63 Notify 8 bytes — see packet layout below
CP Feature 2A65 Read 0x00000008 (crank revolution data supported)
Sensor Location 2A5D Read 0x0D (rear hub)

Cycling Speed & Cadence Service (0x1816)

Characteristic UUID Properties Value
CSC Measurement 2A5B Notify 5 bytes — see packet layout below
CSC Feature 2A5C Read 0x0002 (crank revolution data supported)
Sensor Location 2A5D Read 0x0D (rear hub)

Packet Layouts

Cycling Power Measurement 2A63 (8 bytes)

Bytes Field Value
0–1 Flags 0x0020 (crank revolution data present)
2–3 Instantaneous power sint16, little-endian, watts
4–5 Cumulative crank revolutions uint16, little-endian, wraps at 65535
6–7 Last crank event time uint16, little-endian, 1/1024 s units

CSC Measurement 2A5B (5 bytes)

Bytes Field Value
0 Flags 0x02 (crank data present, no wheel data)
1–2 Cumulative crank revolutions uint16, little-endian, wraps at 65535
3–4 Last crank event time uint16, little-endian, 1/1024 s units

Source priority: crank event time is taken from 2A5B (CSC); 2A63 (CPS) is the authoritative source for power and is also used as fallback for crank event time if CSC reports zero.

Cumulative crank revolution counters are never reset between workouts — this is correct per BLE spec. Apple Watch computes cadence from the delta between successive values.


Timing

Parameter Value
Notify rate to Apple Watch 4 Hz (250 ms cap)
Trainer data timeout 5 s — stops notifications if no trainer packets
Idle status log Every 10 s (Serial only, when not riding)
Watchdog timeout 8 s — resets chip if loop stalls

Always-Powered Operation

Leaving the bridge plugged into a USB wall adapter is the recommended setup. The board draws only a few milliamps at idle. Both BLE roles stay active continuously:

  • Peripheral advertising restarts automatically if Apple Watch disconnects
  • Central scanning restarts automatically if the trainer disconnects
  • The hardware watchdog resets the chip if the BLE stack locks up (typically after days of uptime)

Use a stable wall adapter rather than a laptop USB port, which may cut power during sleep.


Key Constants (Ride.ino)

Constant Default Purpose
CLEAR_BONDS 0 Set to 1 + reflash to wipe trainer bond (use only to fix stale pairing)
kTrainerTimeoutMs 5000 ms of silence before data stream is paused
kNotifyIntervalMs 250 Minimum ms between Apple Watch notifications (4 Hz cap)
kStatusIntervalMs 10000 ms between idle status log lines

About

Kettler Ride 300 to Apple Watch BLE Adapter

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors