Compute crank cadence RPM from the CSC sensor (issue #49) #108
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!108
Loading…
Reference in a new issue
No description provided.
Delete branch "area/csc-cadence"
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 #49.
Crank-field decoding on the CSC Measurement characteristic (
0x2A5B) already landed in #18(
CscMeasurement.kt) but had no consumer —AvgCadenceAccumulator(#65) existed with no producer,per that issue's own "honestly un-fed" note. This wires the missing conversion.
What changed
companion/core/.../sensors/CrankCadenceTracker.kt(new): the crank-side mirror ofWheelSpeedTracker— instantaneousCadenceSample(Measured(rpm)/NoNewRevolution) fromsuccessive
CrankRevolutionDatasamples, reusinguint16Deltafor both 16-bit crank fields(
CscWraparound.ktalready flagged this reuse for #49). Covered byCrankCadenceTrackerTest,including real
uint16rollover on the revolution counter, the event-time clock, and both at once.companion/sensors/.../AndroidCscClient.kt/CscSensorState.kt: wires aCrankCadenceTrackerper connection, reset alongside the wheel tracker on disconnect. Found and fixed a real bug in
the process:
handleMeasurementpreviously didmeasurement.wheel ?: return, so a crank-onlysensor's notifications (which never carry a wheel field) never updated state at all — cadence was
unreachable for that sensor class. Wheel and crank are now updated independently, each keeping its
last-known value when the other field is absent from a given notification.
CscSensorState.Connectedgainedcadence: CadenceSample?—nullmeans "this sensor has neverreported crank data" (a wheel-only sensor, or no notification yet), kept distinct from
CadenceSample.NoNewRevolution("real cadence sensor, cranks currently stopped"). This is the"honestly absent, not zero" representation the issue asks for, matching issue #9's Effort page
"auto-hides without sensors" spec.
companion/pebble/.../DerivedMetricsWireEncoding.kt: newcadenceValue(CadenceSample?)forProto.Key.CADENCE_RPM(id 21, already generated from #7 /shared/message_keys.json) —null->UINT8sentinel,NoNewRevolution-> a genuine0(the "clamped to zero when the cranks stop"acceptance criterion),
Measured-> its rpm. Folded into the existingencode()/changedKeysOnlysix-key ride-metrics group alongside the pre-existing
AVG_CADENCE_RPMencoding.DerivedMetrics.kt's own KDoc (previously:AvgCadenceAccumulator"has no real input yet") isupdated to describe the new input. A real integration test in
DerivedMetricsTest.ktfeeds a realCrankCadenceTracker's output intoAvgCadenceAccumulatorend to end, proving coasting/stationaryspans are excluded from the moving average rather than averaged in as
0.Answers to the questions this issue asks explicitly
cumulativeCrankRevolutions/lastCrankEventTimeoff0x2A5B) alreadyexisted before this PR (#18,
CscMeasurement.kt) — nothing needed adding there. Confirmed byreading the file and its existing
CscMeasurementTestcoverage before writing anything new.CscSensorState.Connected.cadence == nullthrough to
DerivedMetricsWireEncoding.cadenceValue(null) == Proto.Sentinel.UINT8on the wire — nevera
0. A real cadence sensor with stationary cranks instead reportsCadenceSample.NoNewRevolution,which does encode to a genuine
0. The two are different types, not two branches of the samenullable rpm value, so this distinction can't silently collapse.
./gradlew :companion:core:test(all green, including the newCrankCadenceTrackerTestand theDerivedMetricsTestintegration test),./gradlew :companion:pebble:testDebugUnitTest(all green, including updatedDerivedMetricsWireEncodingTest), and./gradlew :companion:assembleDebug(succeeds — this PRtouches Android-SDK-bound code in
:companion:sensors).Explicitly out of scope for this PR (not touched, and said so in the commit message):
watchapp/src/c/view_ride.c(showing the field on the watch) and GPXTrackPointExtensionrecording— there is no ride-recording pipeline in
companion/yet to hang either of those onto, andview_ride.cis outsidecompanion/'s ownership. Both remain real gaps for a future issue, notsilently declared done here.
https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt