BLE Cycling Power meter support #50

Closed
opened 2026-08-31 17:25:16 +02:00 by robert · 1 comment
robert commented 2026-08-31 17:25:16 +02:00 (Migrated from git.butzei.de)

Goal

Support a power meter over the standard BLE Cycling Power profile. Unlike cadence this is a genuine addition: a separate profile, its own client and its own derived metrics.

Acceptance criteria

  • Cycling Power service 0x1818 discovered and connected
  • Cycling Power Measurement 0x2A63 parsed, including the variable-length flags field
  • Instantaneous power in watts sent to the watch and shown as a ride data field
  • 3-second rolling average power computed, since raw power is far too jittery to read while riding
  • Normalised power and average power for the ride computed and shown in the summary
  • Crank-based cadence taken from the power meter when it supplies it, in preference to a separate CSC sensor
  • Power written into the recorded GPX
  • Pairing and calibration flow in the companion, including zero-offset where the meter supports it
  • Sits behind the same sensor interface as speed and cadence, so dropout handling is shared

Files

  • companion/.../sensors/CyclingPowerClient.kt
  • watchapp/src/c/view_ride.c

Notes

The flags field makes this parser materially more involved than CSC - fields are optional and their offsets shift. See docs/DECISIONS.md D16.

## Goal Support a power meter over the standard BLE Cycling Power profile. Unlike cadence this is a genuine addition: a separate profile, its own client and its own derived metrics. ## Acceptance criteria - [ ] Cycling Power service `0x1818` discovered and connected - [ ] `Cycling Power Measurement` `0x2A63` parsed, including the variable-length flags field - [ ] Instantaneous power in watts sent to the watch and shown as a ride data field - [ ] 3-second rolling average power computed, since raw power is far too jittery to read while riding - [ ] Normalised power and average power for the ride computed and shown in the summary - [ ] Crank-based cadence taken from the power meter when it supplies it, in preference to a separate CSC sensor - [ ] Power written into the recorded GPX - [ ] Pairing and calibration flow in the companion, including zero-offset where the meter supports it - [ ] Sits behind the same sensor interface as speed and cadence, so dropout handling is shared ## Files - `companion/.../sensors/CyclingPowerClient.kt` - `watchapp/src/c/view_ride.c` ## Notes The flags field makes this parser materially more involved than CSC - fields are optional and their offsets shift. See docs/DECISIONS.md D16.
Owner

Closed by PR #113 (merged). Cycling Power Measurement (0x2A63) format verified live against two independent sources -- confirmed three real differences from CSC's layout rather than assuming symmetry: a 2-byte Flags field (not CSC's 1 byte), signed sint16 instantaneous power (a negative reading decodes as negative, tested explicitly, not left to wrap), and Wheel Revolution Data's event-time at 1/2048s resolution vs CSC's 1/1024s (crank data keeps 1/1024s, correctly reused). Decoded: instantaneous power, pedal power balance/reference (no consumer yet, carried through honestly), crank data (feeds a real CrankCadenceTracker since resolution matches CSC). Correctly walked past but did NOT decode wheel revolution data from a power meter -- feeding it through the CSC-tuned WheelSpeedTracker would have silently halved the computed speed given the resolution mismatch; a real bug avoided by cross-checking rather than assuming code reuse was safe.

SensorPermissions reused unchanged, not duplicated. Dropout/reconnect mirrors CscClient's proven structure exactly. NormalizedPowerCalculator and a new Power3sAverage are now genuinely fed live power samples; AvgPowerAccumulator exists with the same honest "no ride orchestrator yet" gap as AvgCadenceAccumulator/AvgHrAccumulator.

Verified for real: :companion:core:test (CyclingPowerMeasurementTest 14, Power3sAverageTest 6, AvgPowerAccumulatorTest 4), :companion:pebble:testDebugUnitTest, :companion:sensors:assembleDebug and :companion:assembleDebug all green. No Bluetooth hardware in this sandbox -- AndroidCyclingPowerClient is compile-verified only, same honest disclosure as #18/#49.

Closed by PR #113 (merged). Cycling Power Measurement (0x2A63) format verified live against two independent sources -- confirmed three real differences from CSC's layout rather than assuming symmetry: a 2-byte Flags field (not CSC's 1 byte), signed sint16 instantaneous power (a negative reading decodes as negative, tested explicitly, not left to wrap), and Wheel Revolution Data's event-time at 1/2048s resolution vs CSC's 1/1024s (crank data keeps 1/1024s, correctly reused). Decoded: instantaneous power, pedal power balance/reference (no consumer yet, carried through honestly), crank data (feeds a real CrankCadenceTracker since resolution matches CSC). Correctly walked past but did NOT decode wheel revolution data from a power meter -- feeding it through the CSC-tuned WheelSpeedTracker would have silently halved the computed speed given the resolution mismatch; a real bug avoided by cross-checking rather than assuming code reuse was safe. SensorPermissions reused unchanged, not duplicated. Dropout/reconnect mirrors CscClient's proven structure exactly. NormalizedPowerCalculator and a new Power3sAverage are now genuinely fed live power samples; AvgPowerAccumulator exists with the same honest "no ride orchestrator yet" gap as AvgCadenceAccumulator/AvgHrAccumulator. Verified for real: :companion:core:test (CyclingPowerMeasurementTest 14, Power3sAverageTest 6, AvgPowerAccumulatorTest 4), :companion:pebble:testDebugUnitTest, :companion:sensors:assembleDebug and :companion:assembleDebug all green. No Bluetooth hardware in this sandbox -- AndroidCyclingPowerClient is compile-verified only, same honest disclosure as #18/#49.
Sign in to join this conversation.
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
robert/PedalPebble#50
No description provided.