* BLE background presence: pending-connect wake-on-proximity + wake-window maintenance (#1395)
iOS cancels nothing for us: pending CBCentralManager connects never expire,
complete whenever the peer reappears in range, and relaunch the app via the
existing state-restoration path. Use that as the wake-on-proximity mechanism:
- BLERecentPeripheralCache: retains handles to recently seen/dropped
peripherals (LRU 16, 15 min max age to respect BLE address rotation)
- On backgrounding, arm indefinite pending connects to cached peripherals
within a slot budget (2 of 6 central slots reserved for live background
discovery); armed entries carry lastConnectionAttempt == nil so a quick
background/foreground bounce can't strand them as connecting
- The 8s app-level connect timeout defers while backgrounded so
discovery-driven background connects also stay pending
- Foreground return cancels stale pending connects (including connecting
entries rebuilt by state restoration after a relaunch) and hands control
back to the scanner/scheduler
- A link dropped while backgrounded re-arms after the disconnect-settle
window, so a peer walking away and returning wakes us again
- Packet ingress while backgrounded triggers a catch-up maintenance pass
(announce/flush/drain) since the maintenance timer is suspended with the
app; rate-limited to the normal 5s cadence
Battery cost ~0: pending connects live in the controller's allowlist (no
scanning, no app CPU), and the catch-up pass only runs inside wake windows
the radio already granted.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Restore: seed wake-on-proximity cache and resume service discovery
Field finding from on-device testing: after a state-restoration relaunch,
the recent-peripheral cache starts empty, so backgrounding shortly after a
restore armed no pending connects. Seed the cache from the restored
peripherals — they are the freshest proximity candidates we have.
Also resume service discovery for peripherals restored as connected with no
characteristic: the CBCharacteristic reference dies with the old process,
and without rediscovery the link sits connected-but-unusable until the peer
drops it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Defer restored-link service rediscovery until poweredOn
Field finding: CBPeripheral.discoverServices issued inside willRestoreState
fires before the central manager reaches poweredOn — CoreBluetooth drops the
command with an API MISUSE warning, leaving restored-connected links
characteristic-less after all. Move the rediscovery to
centralManagerDidUpdateState(.poweredOn), which restoration guarantees runs
afterwards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Let disconnect re-arms use the freed slot (Codex P2 on #1396)
The background-entry arm reserves 2 of 6 central slots for live discovery,
but the disconnect re-arm path shared that budget: with 4+ links remaining
the budget hit zero and the just-dropped peer was never armed — defeating
walk-away/walk-back re-arming in dense meshes. The disconnect path now arms
with no reserve, consuming the slot the disconnect itself freed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
iOS cancels nothing for us: pending CBCentralManager connects never expire,
complete whenever the peer reappears in range, and relaunch the app via the
existing state-restoration path. Use that as the wake-on-proximity mechanism:
- BLERecentPeripheralCache: retains handles to recently seen/dropped
peripherals (LRU 16, 15 min max age to respect BLE address rotation)
- On backgrounding, arm indefinite pending connects to cached peripherals
within a slot budget (2 of 6 central slots reserved for live background
discovery); armed entries carry lastConnectionAttempt == nil so a quick
background/foreground bounce can't strand them as connecting
- The 8s app-level connect timeout defers while backgrounded so
discovery-driven background connects also stay pending
- Foreground return cancels stale pending connects (including connecting
entries rebuilt by state restoration after a relaunch) and hands control
back to the scanner/scheduler
- A link dropped while backgrounded re-arms after the disconnect-settle
window, so a peer walking away and returning wakes us again
- Packet ingress while backgrounded triggers a catch-up maintenance pass
(announce/flush/drain) since the maintenance timer is suspended with the
app; rate-limited to the normal 5s cadence
Battery cost ~0: pending connects live in the controller's allowlist (no
scanning, no app CPU), and the catch-up pass only runs inside wake windows
the radio already granted.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>