Skip to content

Fix Zwift Click V2: keepalive + adaptive unlock detection (no 24h Zwift requirement) - #4743

Open
cagnulein wants to merge 1 commit into
masterfrom
fix/zwift-click-v2-keepalive-unlock-detection
Open

Fix Zwift Click V2: keepalive + adaptive unlock detection (no 24h Zwift requirement)#4743
cagnulein wants to merge 1 commit into
masterfrom
fix/zwift-click-v2-keepalive-unlock-detection

Conversation

@cagnulein

Copy link
Copy Markdown
Owner

Problem

The Zwift Click V2 required opening Zwift every ~24h to re-unlock the device. Two root causes:

  1. The RIGHT controller deep-sleeps after ~56s idle, causing HCI 0x08 supervision timeout and a dropped BLE link. Without keepalive the user would lose the connection regularly.
  2. The LEFT controller needs a hardware unlock (set by Zwift, persists ~24h). Once expired, its buttons stop working.

Solution

Analysis of MaximumTrainer/MaximumTrainer_Redux#307 and OpenBikeControl/bikecontrol commit 2cb079fc revealed the following:

  • The RIGHT controller never needs unlock — its 0x23 button frames decode without any crypto.
  • The LEFT controller is the pair's anchor: connecting it too keeps the RIGHT solid (hardware-proven). Our bluetooth.cpp already connects both; the missing piece was the keepalive.
  • OpenBikeControl (commit 2cb079fc) discovered that an already-unlocked device recognises a short ff 04 00 ack on SYNC_RX and stays in unlocked mode across reconnects.
  • Unlock detection: an unlocked device echoes back the RideOn handshake starting with 0x52 ('R') on SYNC_TX; a locked device sends a crypto challenge (0xFF).

Changes (zwiftclickremote.cpp/.h)

What Why
Keepalive: re-send RideOn every 3s (LEFT + RIGHT) Prevents Click V2 deep-sleep → no more 0x08 HCI drops
ff0400 to LEFT on initial handshake Maintains unlock state on already-unlocked devices; no-op on locked ones
deviceUnlocked flag + SYNC_TX echo detection Sets flag and shows toast when LEFT confirms unlocked (0x52 echo); resets on disconnect
NONE type excluded from keepalive Old Zwift Click uses a different protocol

Behaviour after this fix

  • RIGHT controller: works indefinitely without Zwift ✓
  • LEFT controller (unlocked by Zwift): ff0400 ack keeps it unlocked across reconnects; user sees "Left controller unlocked!" toast ✓
  • LEFT controller (locked): silently falls back to right-only — our existing 0x23 parser already ignores LEFT frames when the device is locked ✓

Test plan

  • Connect Click V2 pair, verify RIGHT buttons work without opening Zwift
  • Leave idle >60s, verify connection stays solid (no HCI 0x08 drop)
  • Unlock via Zwift, then reconnect QZ: verify LEFT "unlocked" toast appears and LEFT buttons work
  • After 24h without Zwift: verify RIGHT still works, LEFT gracefully degrades to no events

🤖 Generated with Claude Code

…quirement

The Click V2 RIGHT controller alone drops after ~56s idle (HCI 0x08 supervision
timeout) because it deep-sleeps. The LEFT is the anchor: connecting it too keeps
the RIGHT solid. Both are already connected by our bluetooth.cpp scan; the
missing piece was the RideOn keepalive that prevents deep-sleep.

Changes:
- Keepalive: re-send RideOn every 3s (LEFT and RIGHT types; NONE = old Click
  is unchanged). Prevents the 0x08 HCI supervision timeout that caused the drop.
- ff0400 unlock-ack to LEFT on initial handshake: OpenBikeControl (commit
  2cb079fc) discovered that a device previously unlocked by Zwift recognises
  this short ack and stays in unlocked mode. No-op on a locked device.
- Unlock detection via SYNC_TX echo: when unlocked the device echoes back the
  RideOn handshake starting with 0x52 ('R') on SYNC_TX; a locked device sends
  a crypto challenge (0xFF). Sets deviceUnlocked flag and shows toast when the
  LEFT controller is confirmed unlocked, so the user gets feedback. When locked
  the LEFT implicit fallback is right-only (our 0x23 parser already ignores
  LEFT frames that carry no decodable button data).

Result: RIGHT buttons work indefinitely without ever opening Zwift. LEFT buttons
work too when the device was unlocked by Zwift within the last ~24h (ff0400 ack
maintains the hardware-persistent unlock state across reconnects).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@stale

stale Bot commented Jul 3, 2026

Copy link
Copy Markdown

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale Bot added the wontfix This will not be worked on label Jul 3, 2026
@stale stale Bot closed this Jul 17, 2026
@cagnulein cagnulein reopened this Jul 17, 2026
@stale stale Bot removed the wontfix This will not be worked on label Jul 17, 2026
@stale stale Bot closed this Jul 24, 2026
@cagnulein cagnulein reopened this Jul 24, 2026
@stale

stale Bot commented Aug 8, 2026

Copy link
Copy Markdown

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale Bot added the wontfix This will not be worked on label Aug 8, 2026
@stale stale Bot closed this Aug 16, 2026
@cagnulein cagnulein reopened this Aug 16, 2026
@stale stale Bot removed the wontfix This will not be worked on label Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant