Files
BikeControl/crates/ble
dtourolleandClaude Opus 5 e589086078 Stop the scanner throttling itself off the air
Android counts an app's scan *starts*: five in any thirty seconds and the
platform answers `SCAN_FAILED_SCANNING_TOO_FREQUENTLY` and stops
returning results. It does this silently as far as btleplug is concerned,
so the app goes on asking and simply stops being told about anything.

`scan_loop` opened a session every 2.9 s — a 2.5 s window plus 400 ms
idle. **Ten starts per thirty seconds, twice the limit, with nothing else
running.** Add the trainer's 15 s search or a pod's 20 s one, which
`click.rs` notes runs to the full timeout because a Click only advertises
after a button press, and the app spends much of its life muted. A rider
who wakes the pods first is doing exactly the thing that pushes the count
over, and then the trainer cannot be found — not because it is not
advertising, but because the app is no longer allowed to hear it.

Starting a scan is the expensive act, not running one, so hold the
session and sample it:

- scan.rs splits `scan` into `begin` / `peek` / `end`, sharing one
  describe-filter-rank path (`collect`). `scan` stays as the one-shot
  form for the probe tool.
- `scan_loop` opens one session per SCAN_WINDOW (now 20 s) and peeks
  every 700 ms, publishing each time. The radio starts a seventh as
  often and the list updates four times *quicker* than the old
  whole-pass cadence.
- The session is recycled rather than held forever: a new adapter each
  cycle is what drops peripherals that have left the room, so 20 s is
  how stale a departed device may look. That was ~3 s before, and it is
  the one thing this trade gives up.
- The sample loop selects on the scan switch, so a suspension still
  lands immediately. A connect suspends this loop precisely so the two
  do not fight over the radio, and a suspension that took twenty seconds
  to arrive would be no suspension at all.

Instrumented at the choke point every caller passes through, because
"the radio is busy" and "the peripheral is asleep" look identical from
outside: every session start logs the concurrent depth and how many
starts there have been in the last thirty seconds, and warns when either
number is a problem. Measured on the tablet after this change — one
start per ~21 s, `recent=2`, against a limit of five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 13:05:53 +02:00
..