Derived metric producers: MAX_SPEED, AVG_HR, HR_ZONE, AVG_CADENCE, NORM_POWER (#65) #104
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!104
Loading…
Reference in a new issue
No description provided.
Delete branch "area/derived-metrics"
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 #65.
What this adds
companion/core/src/main/kotlin/de/butzei/pedalpebble/core/ride/DerivedMetrics.kt— a producer perFieldIdD28 flagged as missing a computation:MaxSpeedTracker,AverageAccumulator(shared time-weighted-mean engine behindAvgHrAccumulator/AvgCadenceAccumulator),deriveHrZone,NormalizedPowerCalculator. Pure Kotlin, noandroid.*, following this module's existingSpeedPipeline/StopDetectorpattern.companion/pebble/src/main/kotlin/de/butzei/pedalpebble/pebble/DerivedMetricsWireEncoding.kt— the wire-encoding half, mirroring #19'sSpeedWireEncoding.kt: converts each producer's output into thedocs/PROTOCOL.md§2.2 key/value pairs, sentinel-for-null per §1 rule 3, plus a genericchangedKeysOnlydiff for §4's policy. Nothing sends anAppMessageyet — #4/Spike A is still a placeholder, same caveatSpeedWireEncodingalready documents.Real input today vs. honestly un-fed
MAX_SPEEDSpeedPipeline's real arbitrated speed stream (#17/#19/#69, all merged). "Ignoring accuracy-gate-rejected fixes" holds by construction (a rejected fix never produces aSpeedPipelineSample), proven with an integration test against a realSpeedPipeline, not just argued.AVG_HR/HR_ZONEcompanion/yet — heart rate is watch-local (D3) and only reaches the phone as batchedHR_SAMPLES.AVG_CADENCECscMeasurement.kt(#18) decodes raw crank-revolution fields but nothing turns them into an instantaneous RPM.NORM_POWER0x1818) client exists anywhere incompanion/.Aggregation math and interfaces for all five are real, working, and independently tested — checked against the actual codebase (grep for
HR_BPM/AVG_HR/heart.?rate/HR_SAMPLES, and for a crank-cadence tracker) before writing anything, not assumed.AvgCadenceAccumulatoralso resolves the acceptance criterion's own "both conventions exist" flag: zero-cadence (coasting) samples are excluded from the average, matching the criterion's own "over moving time, excluding coasting" wording — documented and tested (AvgCadenceAccumulatorTest).Normalised power
Standard method, verified against two independent canonical sources (checked 2026-09-04, not recalled):
Both agree: 30 s rolling average power (recalculated every second), raised to the 4th power, averaged, 4th-rooted.
Hand-checked worked example (
NormalizedPowerCalculatorTest, small injected window for tractable arithmetic — same "inject a smaller constant for a fast test" precedentStopDetectorTest/SpeedPipelineTestalready use):Asserted against the exact
100.0 * kotlin.math.sqrt(3.5)value and the decimal expansion, both within 1e-9/1e-6. A separate test proves the real 30 sROLLING_WINDOW_SECONDSconstant on a constant-power identity case (NP == the constant exactly). No live power meter exists to run it against yet (#50) — documented explicitly in the class KDoc, not faked.Also documented explicitly (with citation reasoning): NP deliberately does not exclude
MovementState.STOPPEDtime itself, unlikeAvgHrAccumulator/AvgCadenceAccumulator— neither cited source describes excluding stopped/zero-power spans from the published algorithm, so silently adding that exclusion would be an undocumented deviation from the cited method. Whether a future power-meter consumer (#50) gates its own calls onMovementStateis that issue's decision.Test runs (real, not hand-reviewed)
Test result XMLs confirmed individually (
tests="N" failures="0" errors="0"per new class) — not just a green top-level task.https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt