@@ -4522,17 +4522,54 @@ static int bt_avsrc_status() {
45224522 } catch (...) { return -1; }
45234523}
45244524
4525- // How many times in a row the ladder will stand aside for an attempt that is already in flight
4526- // before asking anyway. Bounded so a status wedged at 3 cannot silence the ladder for good — the
4527- // same rule the rest of this file applies to any service state it cannot verify.
4528- #define BT_AVSRC_MAX_SKIPS 6
4525+ // How long the ladder will stand aside for an attempt that is already in flight before asking
4526+ // anyway. Bounded so a status wedged at 3 cannot silence the ladder for good — the same rule the
4527+ // rest of this file applies to any service state it cannot verify. A TIME rather than a count of
4528+ // skips (it was 6 skips at 5 s apiece): the ladder now looks every second while a user is waiting,
4529+ // and a count would have made the same wedge five times shorter for exactly that case.
4530+ #define BT_AVSRC_MAX_BUSY_MS 30000
4531+ // How soon the ladder looks again after a Devices tap it could not act on.
4532+ #define BT_RECONNECT_SOON_S 2
4533+
4534+ // A TAP THE RADIO CANNOT TAKE IS HANDED TO THE LADDER, NOT DROPPED. Measured 2026-09-15 on the
4535+ // device, 24 taps on the headphones' row in 21 s:
4536+ //
4537+ // 377.557 bt-paired: row 1: RequestConnection rc=0 (REJECTED — nothing will reach the air …)
4538+ // … 22 more like it …
4539+ // 394.696 bt-paired: row 1: RequestConnection rc=1 (accepted)
4540+ // 401.904 bt-vol: rocker now drives BLUETOOTH (GetBtStatus=3, peer named) <- 7.2 s later
4541+ //
4542+ // rc=0 there is the busy refusal the ladder already stands aside for (see bt_reconnect_tick): a page
4543+ // was on the air, first the ladder's own earlier attempt and then the one tap that got through. The
4544+ // Devices-row path never asked, so each tap requested into the page, was refused, and nothing kept
4545+ // the device the user wanted — while every tap also re-armed the ladder and pushed its next look ten
4546+ // seconds out. The UI's spinner vanished with the list refresh each tap triggers, so nothing on
4547+ // screen said a connect was under way either; hence 24 taps.
4548+ //
4549+ // Now a tap during a page, or one refused for any other reason, makes that device the ladder's
4550+ // target, and the ladder looks every second until the radio is free and then asks for it.
4551+ static bool g_bt_user_pending = false;
45294552
45304553// Called wherever the user asks for a link, so the retry stops being parked.
45314554static void bt_reconnect_rearm() {
45324555 g_bt_user_disconnected = false;
45334556 g_bt_reconnect_at = 0;
45344557 g_bt_reconnect_wait_s = 0;
45354558 g_bt_reconnect_tries = 0; // a user gesture buys a fresh set of attempts
4559+ g_bt_user_pending = false; // …and replaces whatever an earlier gesture was still waiting on
4560+ }
4561+
4562+ // The ladder takes over a connect the user asked for and the radio refused. It sets up what the
4563+ // ladder's own first notice of a drop would (retry mode off; connect-wait on, but not while a link is
4564+ // up, which would let a second device walk in), aims at `addr`, and schedules a look soon.
4565+ static void bt_connect_when_free(const std::vector<unsigned char>& addr) {
4566+ g_bt_target_addr = addr;
4567+ bt_service_retry(false, false);
4568+ if (!g_bt_have_name) bt_connect_wait(true);
4569+ g_bt_reconnect_wait_s = BT_RECONNECT_SOON_S;
4570+ g_bt_reconnect_at = now_ms() + BT_RECONNECT_SOON_S * 1000L;
4571+ g_bt_reconnect_tries = 0;
4572+ g_bt_user_pending = true;
45364573}
45374574
45384575// Runs from the 1 Hz housekeeping. Cheap: the common paths are two boolean tests.
@@ -4543,6 +4580,7 @@ static void bt_reconnect_tick() {
45434580 // broken.
45444581 bt_connect_wait(false);
45454582 bt_service_retry(false, false);
4583+ g_bt_user_pending = false;
45464584 return;
45474585 }
45484586 if (g_bt_have_name) { // connected — reset so the next drop starts fresh
@@ -4551,6 +4589,7 @@ static void bt_reconnect_tick() {
45514589 g_bt_target_addr.clear(); // reached it; a later drop retries the ordinary way
45524590 g_bt_zeroarg_fails = 0;
45534591 g_bt_reconnect_tries = 0;
4592+ g_bt_user_pending = false;
45544593 if (g_bt_reconnect_wait_s) {
45554594 clog_("bt-reconnect: link is up again — ladder disarmed");
45564595 g_bt_reconnect_at = 0;
@@ -4606,16 +4645,19 @@ static void bt_reconnect_tick() {
46064645 // any non-1 as decisive and jumps to naming a device, on evidence that says nothing about which
46074646 // device is right. Standing aside costs one scalar IPC and keeps the two refusals apart.
46084647 {
4609- static int avsrc_skips = 0;
4648+ static long avsrc_busy_since = 0;
46104649 const int avsrc = bt_avsrc_status();
4611- if (avsrc == BT_AVSRC_CONNECTING && avsrc_skips < BT_AVSRC_MAX_SKIPS) {
4612- avsrc_skips++;
4613- g_bt_reconnect_at = now + 5000; // look again shortly; the page times out in ~5-20 s
4614- return;
4615- }
4616- if (avsrc_skips >= BT_AVSRC_MAX_SKIPS)
4650+ if (avsrc == BT_AVSRC_CONNECTING) {
4651+ if (avsrc_busy_since == 0) avsrc_busy_since = now;
4652+ if (now - avsrc_busy_since < BT_AVSRC_MAX_BUSY_MS) {
4653+ // The page times out in ~5-20 s. Look again in 5, or in 1 while a user is waiting on
4654+ // a tap, so the device they asked for is asked for the moment the radio is free.
4655+ g_bt_reconnect_at = now + (g_bt_user_pending ? 1000 : 5000);
4656+ return;
4657+ }
46174658 clog_("bt-reconnect: AvSrc has said 'connecting' for 30 s without a link — asking anyway");
4618- avsrc_skips = 0;
4659+ }
4660+ avsrc_busy_since = 0;
46194661 }
46204662 g_bt_reconnect_tries++;
46214663
@@ -4657,6 +4699,7 @@ static void bt_reconnect_tick() {
46574699 if (rc != 1) g_bt_zeroarg_fails = BT_ZEROARG_GIVEUP; // rejected: decisive
46584700 else g_bt_zeroarg_fails++;
46594701 }
4702+ if (rc == 1) g_bt_user_pending = false; // on the air now; the ordinary backoff takes over
46604703
46614704 if (g_bt_reconnect_wait_s < BT_RECONNECT_MAX_S) {
46624705 g_bt_reconnect_wait_s *= 2;
@@ -5396,6 +5439,28 @@ static int bt_request_connection(const std::vector<unsigned char>& addr, const c
53965439 return rc;
53975440}
53985441
5442+ // A connect the USER asked for: a Devices row, or an NFC tap on bonded headphones. It never asks into
5443+ // a page already on the air (the ladder's rule, which these paths lacked) and never drops a refusal:
5444+ // either way the device becomes the ladder's target, asked for as soon as the radio is free. See
5445+ // bt_connect_when_free for the 2026-09-15 log that found this.
5446+ static void bt_user_connect(const std::vector<unsigned char>& addr, const char* who) {
5447+ const bool again = g_bt_user_pending && addr == g_bt_target_addr;
5448+ bt_reconnect_rearm(); // an explicit connect re-arms the retry
5449+ char m[160];
5450+ if (bt_avsrc_status() == BT_AVSRC_CONNECTING) {
5451+ bt_connect_when_free(addr);
5452+ if (!again) { // a repeat tap on the same device says nothing new
5453+ std::snprintf(m, sizeof m, "%s: a connect is already on the air — asking for this "
5454+ "device the moment it ends", who);
5455+ clog_(m);
5456+ }
5457+ } else if (bt_request_connection(addr, who) != 1) {
5458+ bt_connect_when_free(addr);
5459+ std::snprintf(m, sizeof m, "%s: refused — asking again once the radio is free", who);
5460+ clog_(m);
5461+ }
5462+ }
5463+
53995464// Called from the render loop when a tap landed.
54005465//
54015466// A TAP IS NOT ALWAYS A PAIRING. This used to call `Pairing` unconditionally, which is right only
@@ -5499,8 +5564,7 @@ static void nfc_service_tap() {
54995564 // must stay armed — for the new one, which bt_request_connection is about to name.
55005565 refresh_bt_connected();
55015566 }
5502- bt_reconnect_rearm();
5503- bt_request_connection(addr, "nfc");
5567+ bt_user_connect(addr, "nfc");
55045568 // The link comes up asynchronously; the route poll notices it and refreshes the name.
55055569 return;
55065570 }
@@ -5532,10 +5596,9 @@ void apply_bt_connect_device() {
55325596 int i = cinder_pending_bt_device();
55335597 if (i < 0 || (size_t)i >= g_bt_paired.size()) { clog_("bt-paired: connect for an unknown row"); return; }
55345598 const std::vector<unsigned char> addr = g_bt_paired[(size_t)i];
5535- bt_reconnect_rearm(); // an explicit connect re-arms the retry
55365599 char who[48];
55375600 std::snprintf(who, sizeof who, "bt-paired: row %d", i);
5538- bt_request_connection (addr, who);
5601+ bt_user_connect (addr, who);
55395602 // The connection completes asynchronously; the 3 s route poll notices it and refreshes the name.
55405603 refresh_bt_paired();
55415604}
0 commit comments