Refuse to join the pair, rather than undoing it a second later

Connecting both pods is the configuration in which the `−` pod stops
reporting its own paddle (§2.3.1). `Cmd::Seen` has always known that and
declined; an explicit `Cmd::Connect` walked straight past it, and
housekeeping then closed the redundant link a second or so later — which
looks like the app handling the case and is not the same thing. On the
tablet, 2026-08-27 15:55:39, the pair was joined for 1.4 s and the `−`
pod did not report another press for the rest of the session, across two
fresh links and an app reinstall.

So the rule now lives on both paths. `auto` is still armed by the refused
request, so the fallback stands: the moment the `−` pod goes away, the
`+` pod is taken on its next advertisement.

And the UI stops offering the trap. The `+` pod's "Connect anyway" sat
next to a working controller, which put the one action that breaks
shifting a single tap from a rider hunting for a way to fix shifting. It
now reads "Held in reserve", which is what that pod is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 18:04:16 +02:00
co-authored by Claude Opus 5
parent 53e7cd05fc
commit 92ce4ba2ae
3 changed files with 42 additions and 5 deletions
+26
View File
@@ -646,10 +646,36 @@ async fn run(
};
match cmd {
Cmd::Connect { pod, address } => {
// The rule `Cmd::Seen` has always enforced, applied
// here too: with the `−` pod up there is nothing for a
// `+` link to add and one specific thing for it to
// break — connected as a pair, the `−` pod stops
// reporting its own paddle (§2.3.1).
//
// Only the scan-driven path was guarded, so an explicit
// request walked straight past it. Housekeeping then
// closed the redundant link a second or so later, which
// reads as the app handling it and is not the same
// thing at all: on the tablet, 2026-08-27 15:55:39, the
// pair was joined for 1.4 s and the `−` pod did not
// report a press again for the rest of the session.
//
// `auto` is still armed below, so the fallback stands:
// the moment the `−` pod goes away, the `+` pod is
// taken on its next advertisement.
let minus_up = !minus.idle();
let slot = slot_mut(&mut minus, &mut plus, pod);
// Asking for it by hand re-arms auto-connect, whatever
// came before.
slot.auto = true;
if pod == PodId::Plus && minus_up {
tracing::info!(
"controller: refusing a + pod link while the − pod is up — \
it relays the pair, and joining both stops it reporting \
its own paddle"
);
continue;
}
// A second click while a search is running means
// "connect", which is what is already happening.
if slot.connecting() {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "BikeControl",
"version": "0.2.4",
"version": "0.2.5",
"identifier": "paris.tourolle.bikecontrol",
"build": {
"frontendDist": "../ui/dist",
@@ -43,7 +43,7 @@
],
"category": "Utility",
"android": {
"versionCode": 1204
"versionCode": 1205
},
"shortDescription": "Indoor cycling trainer control",
"longDescription": "Control a smart trainer over BLE, ride gradient profiles and synthetic waveforms, and record the result."
+14 -3
View File
@@ -158,10 +158,15 @@
<button class="btn ghost" onclick={() => disconnect(pod.pod)}>Disconnect</button>
{:else if pod.state === 'searching'}
<button class="btn ghost" onclick={() => disconnect(pod.pod)}>Stop</button>
{:else if dormant}
<!-- No "connect anyway". Joining the pair is the one action that
stops the − pod reporting its own paddle (§2.3.1), and offering
it beside a working controller put the failure one tap from a
rider hunting for a fix. Rust refuses it too. The fallback is
not lost: the moment the − pod goes, this pod is taken. -->
<span class="held">Held in reserve</span>
{:else}
<button class="btn ghost" onclick={() => connect(pod.pod)}>
{dormant ? 'Connect anyway' : 'Find it'}
</button>
<button class="btn ghost" onclick={() => connect(pod.pod)}>Find it</button>
{/if}
</div>
@@ -353,6 +358,12 @@
font-size: 0.82rem;
}
.held {
align-self: center;
font-size: 0.8rem;
color: var(--ink-faint);
}
.note {
flex: 1 1 100%;
margin: 0;