Pair the pod that works
Both pods, same bench, minutes apart, no writes: − pod buttons stop at ~51 s, every session; flag 0 -> 1; 2 key offers + pod 133 s, 78 paddle edges, flag never flipped, no key offer at all So the `−` pod is not the controller. It relays more when it works — all ten buttons, its twin's included — but "when it works" is under a minute without a Zwift blessing in the last day, and the `+` pod alone is a complete shifter: its paddle shifts up, `Y` shifts down, both already mapped. Shifting is what a ride cannot do without. `take_plus_pod` becomes `take_minus_pod` — the same rule with the pods exchanged — and the housekeeping, the connect guard and the no-pod-named default follow it. Opening both is still what stops the `−` pod reporting its own paddle, so it is still one link, just the other one. The UI stops telling riders to press the pod that dies: the panel asks for the `+` pod, the tile prefers it, and the `−` pod's row says what it actually offers — all ten buttons, for about fifty seconds. This settles A-2 from the other end too. The keep-alive that "removes the daily unlock, but only for the right controller" removes nothing. The right controller never needed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -200,6 +200,26 @@ button we have found drives them.
|
|||||||
> so it is evidence of a wrong-way-round pair only while that pod has never sent its own —
|
> so it is evidence of a wrong-way-round pair only while that pod has never sent its own —
|
||||||
> otherwise the connection screen asks the rider to swap a pair that is filed correctly.
|
> otherwise the connection screen asks the rider to swap a pair that is filed correctly.
|
||||||
|
|
||||||
|
> **The `+` pod is the controller — measured 2026-08-27, and this reverses the note
|
||||||
|
> below.** Both pods on the same bench, minutes apart, `probe unlock` with no writes:
|
||||||
|
>
|
||||||
|
> | | `−` pod | `+` pod |
|
||||||
|
> |---|---|---|
|
||||||
|
> | button reporting stops | **~51 s**, every session | **never** — 133 s, 78 paddle edges |
|
||||||
|
> | status flag `0 → 1` | yes, at the cliff | **never flipped** |
|
||||||
|
> | key offers | 2 per session | **none** |
|
||||||
|
>
|
||||||
|
> The `−` pod does relay more when it works — all ten buttons, its twin's included — but
|
||||||
|
> "when it works" is under a minute without a Zwift blessing in the last day. The `+` pod
|
||||||
|
> alone is a complete shifter: its paddle shifts up and `Y` shifts down, both of which the
|
||||||
|
> app already maps. Shifting is the thing a ride cannot do without, so the app now pairs the
|
||||||
|
> `+` pod and the `−` pod is the substitute. `take_minus_pod` is the old rule with the pods
|
||||||
|
> exchanged; opening both is still the configuration that breaks the `−` pod's own paddle,
|
||||||
|
> so it is still one link.
|
||||||
|
>
|
||||||
|
> This also settles A-2 from the other end. The keep-alive that "removes the daily unlock,
|
||||||
|
> but only for the right controller" removes nothing — the right controller never needed it.
|
||||||
|
|
||||||
> **One link is the whole controller — confirmed 2026-08-21.** Pairing the `−` pod *alone*
|
> **One link is the whole controller — confirmed 2026-08-21.** Pairing the `−` pod *alone*
|
||||||
> delivers all ten buttons on this hardware: its own paddle and D-pad, plus the `+` paddle and
|
> delivers all ten buttons on this hardware: its own paddle and D-pad, plus the `+` paddle and
|
||||||
> face buttons relayed from its twin. There is nothing a second link adds, and there is one
|
> face buttons relayed from its twin. There is nothing a second link adds, and there is one
|
||||||
|
|||||||
@@ -795,8 +795,11 @@ pub fn connect_controller(
|
|||||||
if address.is_some() {
|
if address.is_some() {
|
||||||
return Err("An address names one pod, so say which pod it is".into());
|
return Err("An address names one pod, so say which pod it is".into());
|
||||||
}
|
}
|
||||||
|
// The `+` pod, since 2026-08-27: it is the one that keeps
|
||||||
|
// reporting (§2.3.3). Naming no pod means "connect the
|
||||||
|
// controller", and the controller is the pod that works.
|
||||||
let known = state.lock().devices.click_pod_addresses();
|
let known = state.lock().devices.click_pod_addresses();
|
||||||
controller.connect(PodId::Minus, known.get(&PodId::Minus).cloned());
|
controller.connect(PodId::Plus, known.get(&PodId::Plus).cloned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+65
-53
@@ -663,16 +663,16 @@ async fn run(
|
|||||||
// `auto` is still armed below, so the fallback stands:
|
// `auto` is still armed below, so the fallback stands:
|
||||||
// the moment the `−` pod goes away, the `+` pod is
|
// the moment the `−` pod goes away, the `+` pod is
|
||||||
// taken on its next advertisement.
|
// taken on its next advertisement.
|
||||||
let minus_up = !minus.idle();
|
let plus_up = !plus.idle();
|
||||||
let slot = slot_mut(&mut minus, &mut plus, pod);
|
let slot = slot_mut(&mut minus, &mut plus, pod);
|
||||||
// Asking for it by hand re-arms auto-connect, whatever
|
// Asking for it by hand re-arms auto-connect, whatever
|
||||||
// came before.
|
// came before.
|
||||||
slot.auto = true;
|
slot.auto = true;
|
||||||
if pod == PodId::Plus && minus_up {
|
if pod == PodId::Minus && plus_up {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"controller: refusing a + pod link while the − pod is up — \
|
"controller: refusing a − pod link while the + pod is up — \
|
||||||
it relays the pair, and joining both stops it reporting \
|
the + pod is the controller, and joining both stops the \
|
||||||
its own paddle"
|
− pod reporting its own paddle anyway"
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -689,7 +689,7 @@ async fn run(
|
|||||||
let selector = selector_for(pod, address, &status_tx.borrow(), swapped);
|
let selector = selector_for(pod, address, &status_tx.borrow(), swapped);
|
||||||
tracing::info!(pod = pod.as_str(), selector = %selector.describe(), "controller: connecting");
|
tracing::info!(pod = pod.as_str(), selector = %selector.describe(), "controller: connecting");
|
||||||
start_attempt(slot, pod, selector, &attempt_tx);
|
start_attempt(slot, pod, selector, &attempt_tx);
|
||||||
if pod == PodId::Minus {
|
if pod == PodId::Plus {
|
||||||
plus_gate = tokio::time::Instant::now() + PLUS_GRACE;
|
plus_gate = tokio::time::Instant::now() + PLUS_GRACE;
|
||||||
}
|
}
|
||||||
status_tx.send_modify(|s| s.get_mut(pod).reset_link(PodState::Searching));
|
status_tx.send_modify(|s| s.get_mut(pod).reset_link(PodState::Searching));
|
||||||
@@ -717,10 +717,10 @@ async fn run(
|
|||||||
// exists to paper over, and it is also the
|
// exists to paper over, and it is also the
|
||||||
// configuration in which the `−` pod stops reporting
|
// configuration in which the `−` pod stops reporting
|
||||||
// its own paddle — the failure that cost an evening.
|
// its own paddle — the failure that cost an evening.
|
||||||
if pod == PodId::Plus
|
if pod == PodId::Minus
|
||||||
&& !take_plus_pod(
|
&& !take_minus_pod(
|
||||||
minus.idle(),
|
plus.idle(),
|
||||||
status_tx.borrow().minus.address.is_some(),
|
status_tx.borrow().plus.address.is_some(),
|
||||||
tokio::time::Instant::now() >= plus_gate,
|
tokio::time::Instant::now() >= plus_gate,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -733,8 +733,8 @@ async fn run(
|
|||||||
// were reading the log for.
|
// were reading the log for.
|
||||||
if !plus_declined {
|
if !plus_declined {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"controller: + pod seen; the − pod speaks for the pair \
|
"controller: − pod seen; the + pod is the controller and \
|
||||||
(silenced until this changes)"
|
is up (silenced until this changes)"
|
||||||
);
|
);
|
||||||
plus_declined = true;
|
plus_declined = true;
|
||||||
}
|
}
|
||||||
@@ -840,7 +840,7 @@ async fn run(
|
|||||||
// A `−` pod that is up, or on its way up, is a `−` pod worth
|
// A `−` pod that is up, or on its way up, is a `−` pod worth
|
||||||
// waiting for. Only a slot that has been idle for the whole
|
// waiting for. Only a slot that has been idle for the whole
|
||||||
// grace period lets the `+` pod in.
|
// grace period lets the `+` pod in.
|
||||||
if !minus.idle() {
|
if !plus.idle() {
|
||||||
plus_gate = tokio::time::Instant::now() + PLUS_GRACE;
|
plus_gate = tokio::time::Instant::now() + PLUS_GRACE;
|
||||||
} else {
|
} else {
|
||||||
// The − pod is no longer speaking for the pair, so the next
|
// The − pod is no longer speaking for the pair, so the next
|
||||||
@@ -854,19 +854,20 @@ async fn run(
|
|||||||
// and is exactly the configuration that breaks the `−` paddle.
|
// and is exactly the configuration that breaks the `−` paddle.
|
||||||
// Dropping it leaves `auto` alone, so if the `−` pod later goes
|
// Dropping it leaves `auto` alone, so if the `−` pod later goes
|
||||||
// away the `+` pod is picked up again on its next advertisement.
|
// away the `+` pod is picked up again on its next advertisement.
|
||||||
if !minus.idle() && plus.client.is_some() {
|
if !plus.idle() && minus.client.is_some() {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"controller: − pod is up and relays the pair; closing the redundant + link"
|
"controller: + pod is up and is the controller; closing the − link, \
|
||||||
|
which is the pairing that stops the − pod reporting its own paddle"
|
||||||
);
|
);
|
||||||
forget(PodId::Plus, &mut buttons, &input_tx);
|
forget(PodId::Minus, &mut buttons, &input_tx);
|
||||||
plus.generation += 1;
|
minus.generation += 1;
|
||||||
plus.events = None;
|
minus.events = None;
|
||||||
plus.last_seen = None;
|
minus.last_seen = None;
|
||||||
plus.connected_at = None;
|
minus.connected_at = None;
|
||||||
if let Some(client) = plus.client.take() {
|
if let Some(client) = minus.client.take() {
|
||||||
tokio::spawn(async move { client.shutdown().await });
|
tokio::spawn(async move { client.shutdown().await });
|
||||||
}
|
}
|
||||||
status_tx.send_modify(|s| s.get_mut(PodId::Plus).reset_link(PodState::Idle));
|
status_tx.send_modify(|s| s.get_mut(PodId::Minus).reset_link(PodState::Idle));
|
||||||
}
|
}
|
||||||
|
|
||||||
// A link that is plainly alive and has never carried a button
|
// A link that is plainly alive and has never carried a button
|
||||||
@@ -944,23 +945,33 @@ async fn run(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// May a `+` pod the scan has just seen be connected?
|
/// May a `−` pod the scan has just seen be connected?
|
||||||
///
|
///
|
||||||
/// The `−` pod is the controller (§2.3.1): connected on its own it delivers all
|
/// **The `+` pod is the controller.** This is the reverse of what this module
|
||||||
/// ten buttons, its twin's included. So a `+` link is only ever a *substitute*,
|
/// assumed until 2026-08-27, and the measurement that turned it over is in
|
||||||
/// and opening one alongside a working `−` link is the configuration in which
|
/// §2.3.3: the `−` pod stops reporting buttons about fifty seconds into every
|
||||||
/// the `−` pod stops reporting its own paddle.
|
/// session — a status flag flips, and the link stays up and healthy and mute —
|
||||||
|
/// while the `+` pod ran 133 s with 78 paddle edges, no key offer and no flag,
|
||||||
|
/// on the same bench, minutes apart.
|
||||||
///
|
///
|
||||||
/// Three inputs, in the order they decide:
|
/// The `−` pod does deliver more when it works: all ten buttons, its twin's
|
||||||
/// - `minus_idle` — false when the `−` pod is connected or being connected.
|
/// relayed. But "when it works" is under a minute without the daily Zwift
|
||||||
/// Nothing else matters then: it is already speaking for both.
|
/// blessing, and the `+` pod alone is a complete shifter — its paddle shifts
|
||||||
/// - `minus_known` — we have an address for a `−` pod, from this session or
|
/// up, `Y` shifts down — which is the thing a ride cannot do without.
|
||||||
/// from the remembered-device store. With none, there is no `−` pod to wait
|
///
|
||||||
/// for and the `+` pod is the whole controller.
|
/// So the `−` pod is now the substitute, and this is the old rule with the
|
||||||
/// - `gate_expired` — the `−` pod has been unreachable for [`PLUS_GRACE`].
|
/// pods exchanged. Opening both is still the configuration that breaks the `−`
|
||||||
/// A flat or lost `−` pod must not cost the rider their `+` paddle too.
|
/// pod's own paddle, so still only one link.
|
||||||
fn take_plus_pod(minus_idle: bool, minus_known: bool, gate_expired: bool) -> bool {
|
///
|
||||||
minus_idle && (!minus_known || gate_expired)
|
/// - `plus_idle` — false when the `+` pod is connected or being connected.
|
||||||
|
/// Nothing else matters then: it is the controller and it is up.
|
||||||
|
/// - `plus_known` — we have an address for a `+` pod. With none there is
|
||||||
|
/// nothing to wait for, and the `−` pod is all there is.
|
||||||
|
/// - `gate_expired` — the `+` pod has been unreachable for [`PLUS_GRACE`].
|
||||||
|
/// A flat `+` pod must not cost the rider the fifty working seconds the `−`
|
||||||
|
/// pod still offers.
|
||||||
|
fn take_minus_pod(plus_idle: bool, plus_known: bool, gate_expired: bool) -> bool {
|
||||||
|
plus_idle && (!plus_known || gate_expired)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn slot_mut<'a>(minus: &'a mut Slot, plus: &'a mut Slot, pod: PodId) -> &'a mut Slot {
|
fn slot_mut<'a>(minus: &'a mut Slot, plus: &'a mut Slot, pod: PodId) -> &'a mut Slot {
|
||||||
@@ -1720,28 +1731,29 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn one_link_is_the_whole_controller() {
|
fn one_link_is_the_whole_controller_and_it_is_the_plus_pod() {
|
||||||
// Confirmed in the field 2026-08-21: pairing the − pod alone gives all
|
// Measured 2026-08-27, both pods on the same bench minutes apart: the −
|
||||||
// ten buttons, because it relays its twin (§2.3.1). So the + pod is a
|
// pod goes mute ~51 s into every session while its link stays up, and
|
||||||
// substitute, never a second half.
|
// the + pod ran 133 s with 78 paddle edges and never flipped a flag
|
||||||
|
// (§2.3.3). The − pod relays more; the + pod keeps working. Shifting is
|
||||||
|
// what a ride cannot do without, so the + pod is the controller.
|
||||||
|
|
||||||
// The − pod is up, or on its way up. Nothing else matters.
|
// The + pod is up, or on its way up. Nothing else matters.
|
||||||
assert!(!take_plus_pod(false, true, true));
|
assert!(!take_minus_pod(false, true, true));
|
||||||
assert!(!take_plus_pod(false, false, true));
|
assert!(!take_minus_pod(false, false, true));
|
||||||
|
|
||||||
// We know a − pod exists and it has not been out of reach for long. It
|
// We know a + pod exists and it has not been out of reach for long. It
|
||||||
// is almost certainly just asleep — a Click only advertises while awake
|
// is almost certainly just asleep — a Click only advertises while awake
|
||||||
// (A-4) — so wait rather than open a link we would only close again.
|
// (A-4) — so wait rather than open a link we would only close again.
|
||||||
assert!(!take_plus_pod(true, true, false));
|
assert!(!take_minus_pod(true, true, false));
|
||||||
|
|
||||||
// No − pod has ever been seen or remembered: this rider's + pod *is*
|
// No + pod has ever been seen or remembered: this rider's − pod is all
|
||||||
// their controller, and making them wait for a pod they do not own
|
// there is, fifty working seconds and all.
|
||||||
// would be waiting forever.
|
assert!(take_minus_pod(true, false, false));
|
||||||
assert!(take_plus_pod(true, false, false));
|
|
||||||
|
|
||||||
// The − pod is known but has stayed out of reach. Flat, or left in the
|
// The + pod is known but has stayed out of reach. Flat, or left in the
|
||||||
// garage. Half a controller beats none.
|
// garage. Fifty seconds of shifting beats none.
|
||||||
assert!(take_plus_pod(true, true, true));
|
assert!(take_minus_pod(true, true, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -35,11 +35,12 @@
|
|||||||
* pod stays missing — and the fix belongs next to the symptom. */
|
* pod stays missing — and the fix belongs next to the symptom. */
|
||||||
const scanning = $derived(app.devices.scanning);
|
const scanning = $derived(app.devices.scanning);
|
||||||
const anyConnected = $derived(pods.some((p) => p.state === 'connected'));
|
const anyConnected = $derived(pods.some((p) => p.state === 'connected'));
|
||||||
/** The pod that speaks for the pair. Connected, this is the whole controller. */
|
/** The controller. Measured 2026-08-27: the `+` pod keeps reporting where the
|
||||||
const minusLive = $derived(controller?.minus.state === 'connected');
|
* `−` pod goes mute about fifty seconds in (§2.3.3). */
|
||||||
/** Running on the fallback: the `+` pod alone, with no `−` paddle to shift
|
const plusLive = $derived(controller?.plus.state === 'connected');
|
||||||
* down with beyond its `Y` button. Worth saying out loud. */
|
/** Running on the fallback: the `−` pod, which relays everything but stops
|
||||||
const plusOnly = $derived(!minusLive && controller?.plus.state === 'connected');
|
* reporting about fifty seconds in unless Zwift has blessed it today. */
|
||||||
|
const minusOnly = $derived(!plusLive && controller?.minus.state === 'connected');
|
||||||
const busy = $derived(pods.some((p) => p.state === 'searching'));
|
const busy = $derived(pods.some((p) => p.state === 'searching'));
|
||||||
/** A pod reporting the other's paddle: the pair may be filed the wrong way
|
/** A pod reporting the other's paddle: the pair may be filed the wrong way
|
||||||
* round, and the rider is the only one who can say. */
|
* round, and the rider is the only one who can say. */
|
||||||
@@ -63,8 +64,8 @@
|
|||||||
|
|
||||||
/** What each pod is for, so a rider who has lost one knows what they lost. */
|
/** What each pod is for, so a rider who has lost one knows what they lost. */
|
||||||
const PURPOSE: Record<Pod, string> = {
|
const PURPOSE: Record<Pod, string> = {
|
||||||
minus: 'All ten buttons',
|
plus: 'Shift up · A B Y Z',
|
||||||
plus: 'Fallback · shift up, A B Y Z',
|
minus: 'All ten buttons, for ~50 s',
|
||||||
};
|
};
|
||||||
|
|
||||||
function connect(pod: Pod) {
|
function connect(pod: Pod) {
|
||||||
@@ -84,11 +85,11 @@
|
|||||||
<!-- Nothing can be picked up automatically while the scan is off, so
|
<!-- Nothing can be picked up automatically while the scan is off, so
|
||||||
the way to fix that sits here rather than only in the header. -->
|
the way to fix that sits here rather than only in the header. -->
|
||||||
<button class="btn" onclick={() => app.run(() => api.startScan())}>Start scan</button>
|
<button class="btn" onclick={() => app.run(() => api.startScan())}>Start scan</button>
|
||||||
{:else if !minusLive}
|
{:else if !plusLive}
|
||||||
<!-- The − pod, not both: it is the one that carries the whole
|
<!-- The + pod, not both: joining the pair is what stops the − pod
|
||||||
controller. The + pod has its own button on its own row. -->
|
reporting its own paddle, and the + pod is the one that lasts. -->
|
||||||
<button class="btn" disabled={busy} onclick={() => app.run(() => api.connectController())}>
|
<button class="btn" disabled={busy} onclick={() => app.run(() => api.connectController())}>
|
||||||
{busy ? 'Searching…' : 'Find − pod'}
|
{busy ? 'Searching…' : 'Find + pod'}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if anyConnected}
|
{#if anyConnected}
|
||||||
@@ -103,21 +104,23 @@
|
|||||||
the badge above has already said it. -->
|
the badge above has already said it. -->
|
||||||
{#if !scanning}
|
{#if !scanning}
|
||||||
<p class="lede"><strong>The scan is off</strong> — pods will not be picked up.</p>
|
<p class="lede"><strong>The scan is off</strong> — pods will not be picked up.</p>
|
||||||
{:else if plusOnly}
|
{:else if minusOnly}
|
||||||
<p class="lede">
|
<p class="lede">
|
||||||
Running on the <strong>+ pod alone</strong>: shift down with <span class="kbd">Y</span>.
|
Running on the <strong>− pod</strong>, which relays all ten buttons but stops reporting
|
||||||
Press a button on the − pod for the D-pad.
|
after about a minute unless Zwift has blessed it today. Press a button on the
|
||||||
|
<strong>+ pod</strong> for shifting that lasts.
|
||||||
</p>
|
</p>
|
||||||
{:else if !minusLive}
|
{:else if !plusLive}
|
||||||
<p class="lede">
|
<p class="lede">
|
||||||
<strong>Press any button on the − pod.</strong> It only advertises while awake; the running
|
<strong>Press any button on the + pod.</strong> It only advertises while awake; the running
|
||||||
scan connects it as soon as it does.
|
scan connects it as soon as it does. Its paddle shifts up and
|
||||||
|
<span class="kbd">Y</span> shifts down.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="pods">
|
<div class="pods">
|
||||||
{#each pods as pod (pod.pod)}
|
{#each pods as pod (pod.pod)}
|
||||||
{@const dormant = pod.pod === 'plus' && minusLive && pod.state !== 'connected'}
|
{@const dormant = pod.pod === 'minus' && plusLive && pod.state !== 'connected'}
|
||||||
<article class="pod" class:live={pod.state === 'connected'}>
|
<article class="pod" class:live={pod.state === 'connected'}>
|
||||||
<!-- The paddle glyph is the pod's identity — big enough to match
|
<!-- The paddle glyph is the pod's identity — big enough to match
|
||||||
against the one printed on the hardware at arm's length. -->
|
against the one printed on the hardware at arm's length. -->
|
||||||
@@ -128,8 +131,8 @@
|
|||||||
<span class="purpose">
|
<span class="purpose">
|
||||||
{#if dormant}
|
{#if dormant}
|
||||||
<!-- Not a fault, and the panel must not let it read as one: this
|
<!-- Not a fault, and the panel must not let it read as one: this
|
||||||
pod is idle because the − pod is already sending its buttons. -->
|
pod is idle because joining the pair is what breaks it. -->
|
||||||
Relayed by the − pod
|
Held back — the + pod is the controller
|
||||||
{:else if pod.confirmed}
|
{:else if pod.confirmed}
|
||||||
Confirmed — sent its own {pod.symbol} paddle
|
Confirmed — sent its own {pod.symbol} paddle
|
||||||
{:else if pod.state === 'connected'}
|
{:else if pod.state === 'connected'}
|
||||||
@@ -193,7 +196,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !minusLive}
|
{#if !plusLive}
|
||||||
<!--
|
<!--
|
||||||
FR-1.8. "Not connected" on its own reads as a broken app, and the real
|
FR-1.8. "Not connected" on its own reads as a broken app, and the real
|
||||||
cause — a pod that is simply asleep — is something only the rider can fix.
|
cause — a pod that is simply asleep — is something only the rider can fix.
|
||||||
@@ -204,13 +207,13 @@
|
|||||||
<details class="help">
|
<details class="help">
|
||||||
<summary>Still not connecting?</summary>
|
<summary>Still not connecting?</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Press a button.</strong> A pod that is asleep does not advertise at all.</li>
|
<li><strong>Press a button on the + pod.</strong> An asleep pod does not advertise.</li>
|
||||||
<li><strong>Keep the scan on.</strong> Auto-connect runs off it.</li>
|
<li><strong>Keep the scan on.</strong> Auto-connect runs off it.</li>
|
||||||
<li><strong>Close Zwift.</strong> One app at a time holds a pod.</li>
|
<li><strong>Close Zwift.</strong> One app at a time holds a pod.</li>
|
||||||
<li><strong>Charge it.</strong> A flat pod stops advertising, and the app stops chasing.</li>
|
<li><strong>Charge it.</strong> A flat pod stops advertising, and the app stops chasing.</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Or use the + pod</strong> — you lose the D-pad, <span class="kbd">Y</span> still
|
<strong>Or use the − pod</strong> — it relays all ten buttons, but goes quiet after
|
||||||
shifts down.
|
about a minute unless Zwift has blessed it in the last day.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
The keyboard mirrors every Click action; <span class="kbd">?</span> lists them. A ride
|
The keyboard mirrors every Click action; <span class="kbd">?</span> lists them. A ride
|
||||||
|
|||||||
@@ -166,7 +166,8 @@
|
|||||||
|
|
||||||
const minus = app.controller?.minus;
|
const minus = app.controller?.minus;
|
||||||
const plus = app.controller?.plus;
|
const plus = app.controller?.plus;
|
||||||
const live = minus?.state === 'connected' ? minus : plus?.state === 'connected' ? plus : null;
|
// The `+` pod first: it is the one that keeps reporting (§2.3.3).
|
||||||
|
const live = plus?.state === 'connected' ? plus : minus?.state === 'connected' ? minus : null;
|
||||||
const searching = minus?.state === 'searching' || plus?.state === 'searching';
|
const searching = minus?.state === 'searching' || plus?.state === 'searching';
|
||||||
out.push({
|
out.push({
|
||||||
key: 'click',
|
key: 'click',
|
||||||
@@ -177,13 +178,13 @@
|
|||||||
primary: live ? `Zwift Click · ${live.symbol} pod` : searching ? 'Searching…' : 'Not connected',
|
primary: live ? `Zwift Click · ${live.symbol} pod` : searching ? 'Searching…' : 'Not connected',
|
||||||
detail: live
|
detail: live
|
||||||
? [
|
? [
|
||||||
live.pod === 'minus' ? 'All ten buttons' : 'Fallback — no D-pad',
|
live.pod === 'plus' ? 'Shift up · Y shifts down' : 'All ten buttons, for ~50 s',
|
||||||
live.batteryPercent != null ? `${live.batteryPercent}%` : null,
|
live.batteryPercent != null ? `${live.batteryPercent}%` : null,
|
||||||
live.buttonsSeen > 0 ? `${live.buttonsSeen} presses` : 'no presses yet',
|
live.buttonsSeen > 0 ? `${live.buttonsSeen} presses` : 'no presses yet',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' · ')
|
.join(' · ')
|
||||||
: 'Press a button to wake it — the keyboard works meanwhile',
|
: 'Press a button on the + pod — the keyboard works meanwhile',
|
||||||
tone: live ? 'tone-ok' : 'tone-idle',
|
tone: live ? 'tone-ok' : 'tone-idle',
|
||||||
// Connected, the useful action is *rebuild the link*. A Click can wedge
|
// Connected, the useful action is *rebuild the link*. A Click can wedge
|
||||||
// while looking perfectly healthy — battery arriving every five seconds,
|
// while looking perfectly healthy — battery arriving every five seconds,
|
||||||
@@ -229,7 +230,7 @@
|
|||||||
|
|
||||||
/** The Click panel is a repair manual, so it appears when there is something
|
/** The Click panel is a repair manual, so it appears when there is something
|
||||||
* to repair. Connected, the tile above has already said everything it knows. */
|
* to repair. Connected, the tile above has already said everything it knows. */
|
||||||
const clickNeedsHelp = $derived(app.controller?.minus.state !== 'connected');
|
const clickNeedsHelp = $derived(app.controller?.plus.state !== 'connected');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What the row has to say, as badges. Nothing is emitted for a device that is
|
* What the row has to say, as badges. Nothing is emitted for a device that is
|
||||||
|
|||||||
Reference in New Issue
Block a user