Never ride a link we did not open, and put a shifter on the other pod
🚴 Build and Test BikeControl / Workspace tests (push) Failing after 0s
🚴 Build and Test BikeControl / Android compile check (push) Skipped

Three changes, all from the same evening on the hardware.

**Never adopt an existing link.** `setup_session` skipped connecting when
it found the peripheral already connected, which is not a shortcut: it
means someone else left it that way, and after a shutdown that ran out of
budget that someone is our own previous run. The inherited session
answers the handshake and streams battery every five seconds while never
delivering a button, which reads as broken hardware — it was diagnosed as
a dead pod, a wrong bit map, mis-filed pods and a lapsed Zwift unlock
before anyone looked at what the last run failed to close. Any
pre-existing link is now dropped first, in all three actors, so every
connection starts identical.

**A watchdog for the same state, should it arise another way.**
Deliberately narrow: a link that is plainly alive — frames arriving
inside STALE_AFTER — and has never carried a button since it came up is
recycled after 150 s. Not "the rider has not shifted lately", which is
normal and would strand a pod that only advertises while awake. A link
that has delivered even one press is exempt for its lifetime.

**`Y` on the `+` pod shifts down.** Shifting down lived entirely on the
`−` pod's paddle, so one pod was a single point of failure for half the
drivetrain — and with no on-screen gear control on Android, a rider whose
left pod goes quiet is stuck in whatever gear they were in, mid-interval,
with no way out. This is RISK-9's documented mitigation and it should
have been there from the start. Applied in Rust beside the paddles so a
shift behaves the same wherever it comes from; removed from the webview
so it cannot fire twice. Mode cycling keeps the `m` key.

Verified on the tablet: `Y` moved the gear 12 -> 9, and the pods
reconnected cleanly with no stale link to purge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 21:05:40 +02:00
co-authored by Claude Opus 5
parent 87e97e104f
commit ff94b78375
7 changed files with 105 additions and 9 deletions
+3 -2
View File
@@ -190,8 +190,9 @@
return run(() => api.togglePause());
case 'b':
return run(() => api.markLap());
case 'y':
return run(() => api.cycleMode());
// 'y' is shift-down, applied in Rust beside the paddles — see
// `spawn_controller_loop`. Handling it here as well would shift twice per
// press. Mode cycling keeps the 'm' key.
case 'z':
app.showProfiles = !app.showProfiles;
return;
+1 -1
View File
@@ -14,7 +14,7 @@
['D-pad ← / →', 'Device screen / ride screen'],
['A', 'Pause / resume — save the FIT on the summary'],
['B', 'Insert lap marker — start a new ride on the summary'],
['Y', 'Cycle control mode'],
['Y', 'Shift down a gear — the + pod\u2019s spare shifter'],
['Z', 'Profiles and routes'],
];