BLE CSC wheel sensor client and wheel-circumference calibration (#18) #99
No reviewers
Labels
No labels
area:companion
area:docs
area:shared
area:tooling
area:watchapp
blocker
kind:chore
kind:feature
kind:spike
kind:test
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/PedalPebble!99
Loading…
Reference in a new issue
No description provided.
Delete branch "area/ble-csc-sensor"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #18.
Pure logic (
:companion:core,de.butzei.pedalpebble.core.sensors)CscMeasurement.kt—decodeCscMeasurement()parses the CSC Measurement characteristic (0x2A5B) byte layout: flags byte, then a little-endianuint32cumulative-wheel-revolutions +uint16last-wheel-event-time if flag bit 0 is set, then a little-endianuint16cumulative-crank-revolutions +uint16last-crank-event-time if flag bit 1 is set. Crank fields are decoded (the characteristic is shared with #49's cadence half) but not otherwise consumed by this issue.CscWraparound.kt—uint32Delta/uint16Deltacompute(current - previous) mod 2^nviaMath.floorMod, handling theuint32revolution-counter anduint16event-time rollovers explicitly.WheelSpeedTracker.kt— turns successive raw wheel samples into distance/speed deltas againstRideSettings.WheelCircumference(#44, the real settings type — no parallel type invented).WheelSpeedSampleisMeasured | NoNewRevolution; "connected but not turning" is never conflated with "no sensor at all" (that split happens one level up, inCscSensorState, so #19's arbitration can consume it correctly later).UUIDs (service
0x1816, CSC Measurement0x2A5B, CSC Feature0x2A5C) were verified 2026-09-04 against the Bluetooth SIG's own CSC Service GATT specification, not carried over unchecked from this project's earlier planning notes.Android BLE client (
:companion:sensors,de.butzei.pedalpebble.sensors)CscClient.kt(interface) /AndroidCscClient.kt(impl) — scans filtered on the CSC service UUID, connects viaBluetoothGatt, subscribes to CSC Measurement notifications, and reads Battery Service (0x180F/0x2A19) where the peripheral exposes it. Implements both the pre-API-33 and API-33+onCharacteristicChanged/onCharacteristicReadoverload pairs, and (a genuine surprise found by actually building against this repo's realcompileSdk 37SDK) the newerconnectGatt(BluetoothGattConnectionSettings, Executor, BluetoothGattCallback)overload that deprecates theContext/boolean/intone starting at API 37 — confirmed directly against this SDK'sapi-versions.xml(since="37.0"/deprecated="37.0").minSdkis 31, so the old overload is still what actually runs on real devices;AndroidCscClientbranches onSDK_INTand documents the exact versions in its KDoc.BluetoothDevice.connectGatt's ownautoConnectflag — first connect after a scan usesautoConnect=false; any unrequested disconnect re-connects withautoConnect=trueso the OS reconnects once the sensor is back in range ("wakes"), no polling loop of this client's own.WheelSpeedTrackerand movesstatetoReconnectingbefore any reconnect attempt — it never holds a staleConnectedvalue while no sensor is actually live. SeeCscSensorState's KDoc.SensorPermissions.kt—BLUETOOTH_SCAN/BLUETOOTH_CONNECTchecks, mirroring:companion:location'sRidePermissions.connect()reportsCscSensorState.PermissionRequiredrather than requesting permissions itself (NFR-S6, same pattern asRideSetupActivity) — there's no pairing UI yet to host a rationale, which is out of scope for this issue (its own "Files" note names onlyCscClient.kt).BLUETOOTH_SCANwithandroid:usesPermissionFlags="neverForLocation"(the scan filter only ever matches the CSC service UUID, never derives location) andBLUETOOTH_CONNECT.Wraparound handling and its test coverage
Math.floorMod-based modular delta, not naive subtraction. 23 new Kotest cases in:companion:core, including:uint16rollover (65530 → 10) and a realuint32rollover (4294967290 → 5), each asserted against the exact expected positive deltaNoNewRevolution, not a fabricated0.0)NoNewRevolution, not a divide-by-zero)reset()behaviour after a simulated disconnectuint32Delta/uint16Deltaalways land in[0, 2^n)for arbitrary inputsReal builds
./gradlew :companion:core:test— 23/23 passing (CscMeasurementTest: 8,WheelSpeedTrackerTest: 15)../gradlew :companion:sensors:assembleDebugand./gradlew :companion:assembleDebug— both build clean, zero warnings (after fixing theconnectGattdeprecation and twoOVERRIDE_DEPRECATIONwarnings the first real build surfaced).What's honestly unverified
This sandbox has no Bluetooth hardware. The scan filter, GATT callback wiring, notification subscription/parsing end-to-end, and the
autoConnectreconnect behaviour have never run against a physical CSC sensor — only compiled.autoConnectreconnect in particular is documented Android platform behaviour this class relies on, not something it reimplements.https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt