tools/gpx_replay.py: replay a GPX through the location pipeline (#23) #105
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!105
Loading…
Reference in a new issue
No description provided.
Delete branch "tooling/gpx-replay"
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 #23.
Scope decision — read this first
#23's own listed file path is
tools/gpx_replay.py, implying Python. Its actual acceptance criteria (read from the API, not guessed) are:These are not about JVM-side pipeline verification (that concern is already covered by
SpeedPipelineGpxReplayTest.kt/RealWorldFixtureTest.kt, which I did not touch or refactor — there is no shared-harness duplication to pay down here, because I did not build a second JVM replay harness). They are about driving the real, unmodified Android location stack from the outside:adb emu geo fixand a debug broadcast. Neither needs SpeedPipeline/StopDetector's algorithm reimplemented in Python — the real pipeline always runs in-process on the real device/emulator, unmodified. So: built the literaltools/gpx_replay.pyPython path, not a Kotlin harness, because that's what these specific criteria actually call for once you separate "emit positions" from "process positions."What's here
tools/gpx_replay.py(stdlib-only,uv run, same convention astools/gen_message_keys.py):<trkpt>sequences (same "raw points, not RDP-simplified routes" basis asSpeedPipelineGpxReplayTest.kt's ownparseRawTrackPoints).--speed-multiplierpaces emission by each point's real recorded<time>gap (falls back to NFR-B2's 1 Hz cadence for untimed GPX).--start-offset-seconds/--start-offset-indexjump into a ride.--excursion-at-index/--excursion-offset-meters/--excursion-pointssplices a synthetic perpendicular-drift-and-return excursion into the point sequence, for testing off-route detection without a second recorded ride.p/q(pause/resume, quit) from stdin when it's a terminal.--sink print(stdout, default),--sink emulator(adb emu geo fix <lon> <lat>—AndroidLocationSourceis untouched and can't tell the difference from real GPS),--sink debug-broadcast(adb shell am broadcastto the new debug hook below).tools/test_gpx_replay.py: 36 stdlib-unittesttests (Robert's xUnit/NUnit), including a cross-check of this script's GPX parsing against the same real ~119 km komoot fixture and ~119,020 m ground truthSpeedPipelineGpxReplayTest.kt/RealWorldFixtureTest.ktalready established — proof this tool reads the identical point sequence, explicitly not a claim about SpeedPipeline itself (see the test's own docstring on why a haversine cross-check is not the kind of algorithm-duplication #23's own task brief was worried about).The debug hook (
:companion:location,:companion:ride):DebugBridgeLocationSource— a plain-Kotlin (noandroid.*)LocationSourceyou caninject()a fix into. Unit-tested for real (:companion:locationgained a Kotest source set, same pattern as:companion:pebble's).parseDebugFixExtras— pure function parsing the broadcast's String extras into aLocationFix, unit-tested (14 tests: valid/missing/malformed/out-of-range/boundary lat-lon).DebugFixReceiver— thinBroadcastReceiver, dynamically registered byRideService.onCreate()only whenApplicationInfo.FLAG_DEBUGGABLEis set (never true for a release build; noBuildConfig.DEBUGsince no module enables that build feature). Wired additively: debug-injected fixes merge alongside real GPS into the sameSpeedPipeline, not a mode switch.CI:
slow-lane.yml'sreplay-regressionandtag-lane.yml'sreplay-and-emulator-gateswere both pre-existing placeholder jobs literally named after #23, deliberately left hard-failing with "update this step in the same PR that adds #23's harness." Both now run the tool's own test suite plus a--sink printpass over every real (non-adversarial) GPX fixture in the repo.tag-lane.ymlstill correctly hard-fails — now because G6's cue-position/leg-divergence/link-rate clauses and G7 (the no-network ride) need a cue engine/message scheduler that doesn't exist yet (#38 and friends), not because of anything #23 was responsible for.docs/CI.mdupdated to match.docs/DEV.md: new "GPX replay tool" section with usage examples and an explicit manual checklist for the two things I could not verify live in this session (see below).
Honestly unverified (no device/emulator attached in this session)
--sink emulatorand--sink debug-broadcastagainst a real live target.adb shell am broadcastactually reaches a dynamically-registeredRECEIVER_NOT_EXPORTEDreceiver on this project's target API levels — I chose the safe default (RECEIVER_NOT_EXPORTED) rather than guess at a platform detail I couldn't confirm;DebugFixReceiver's KDoc documents theRECEIVER_EXPORTEDfallback and its trade-off if this turns out not to work. Worth a look from bastion-security before this is relied on for anything beyond a developer's own machine.Both are called out in
docs/DEV.md's manual checklist and in code KDoc, not silently assumed.Verified for real, this session
uv run python -m unittest tools/test_gpx_replay.py -v— 36/36 pass../gradlew :companion:location:test— 14/14 pass (new Kotest source set)../gradlew :companion:core:testand:companion:pebble:test— unaffected, still green../gradlew :companion:ride:compileDebugKotlinand:companion:assembleDebug— compile clean with the new wiring.tools/gpx_replay.pyend-to-end against the real Havelchaussee fixture and every other real GPX fixture in the repo (--sink print), including an--excursion-at-indexrun showing a visible perpendicular drift-and-return in the printed coordinates.https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt