fix(ble): require fresh advertisement for auto-reconnect#5912
Conversation
Rewrite the bonded branch of BleRadioTransport.findDevice() so a bonded device is only returned after a fresh advertisement is observed, escalating from the targeted scan to a bounded scan before failing with a clear error. BleRadioTransport.kt: - findDevice() bonded branch runs the targeted scan (TARGETED_SCAN_TIMEOUT) first, then escalates to a bounded scan at SCAN_TIMEOUT before throwing RadioNotConnectedException; the stale bonded handle is never returned - Extract scanForFreshDevice(timeout: Duration) helper that performs a single scan attempt and returns the first matching BleDevice or null; shared by the bonded and non-bonded branches so retry policy stays in findDevice() - Non-bonded branch keeps SCAN_RETRY_COUNT attempts at SCAN_TIMEOUT with SCAN_RETRY_DELAY between attempts, now expressed via scanForFreshDevice - RadioNotConnectedException for the bonded-not-advertising case reports "Bonded device is not advertising" - SCAN_TIMEOUT gains a KDoc explaining its dual role (bonded escalation and non-bonded retry) and the bounded-scan budget rationale - Preserve address.anonymize() at every log site in findDevice() and scanForFreshDevice() (bonded targeted miss, bonded escalated hit, bonded scan timeout, non-bonded scan entry, scan failure catch) Tests: - BleRadioTransportReconnectCrashTest.kt adds a local sequenced scanner driving two cases: a bonded device missed by the targeted scan but surfaced by the escalated scan returns the fresh scanned device, and a bonded device that never advertises produces zero connectAndAwait calls and a bounded failure, proving no stale-handle fallback - BleRadioTransportTest.kt aligns existing assertions with the scanForFreshDevice helper and the bounded-escalation flow No behavior change for: KableBleConnection, BleReconnectPolicy, the attemptConnection race/gate logic, CONNECTION_TIMEOUT, UI code, TCP, USB, unified transport work.
Stop active discovery scans on the Connections screen once app-level connection state reaches Connected, and skip screen-entry BLE auto-discovery when a device is already selected so the selected device can reconnect through the transport's fresh-advertisement scan. ScannerViewModel.kt: - init block collects serviceRepository.connectionState and calls stopAllScans() on ConnectionState.Connected - Add startBleAutoScan() that starts a BLE scan only when no device is selected (selectedAddressFlow null or NO_DEVICE_SELECTED); manual scan toggles still call startBleScan() directly so users can discover and switch devices while connected - KDoc on startBleAutoScan() documents that Connections-screen discovery and selected-device reconnect scan are independent ConnectionsScreen.kt: - LifecycleStartEffect screen-entry auto-start calls scanModel.startBleAutoScan() instead of startBleScan() - Refresh the surrounding comment to describe discovery semantics and the skip-when-selected behavior Tests: - Rename and invert the existing connectionState-transition test to assert that a Connected transition cancels an active BLE scan - Add a mirror case asserting a Connected transition cancels an active network scan - Add two startBleAutoScan cases: scan is skipped when a device is already selected, and scan starts when no device is selected - Replace the "Scanning allowed in any connection state" section header with "Manual scan control" No behavior change for: manual scan start/stop, BLE scan permissions, scan lifecycle outside the Connected transition, transport-level fresh-advertisement scan in BleRadioTransport.findDevice().
📄 Docs staleness check — advisoryThis PR modifies user-facing UI source files but does not update any page under
Changed source files: What to check:
New page checklist (if adding a new doc page):
If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the
|
🖼️ Preview staleness check — advisoryThis PR modifies UI composables but does not update any
Changed UI files: What to check:
Adding previews checklist:
If this PR does not require preview updates (e.g., logic-only change, non-visual refactor), add the |
|
Nit — stale KDoc in
After this PR, Suggested replacement: /**
* Unified [BleDevice] implementation for all BLE devices — scanned, bonded, or both.
*
* When created from a live BLE scan, [advertisement] is populated and used for optimal peripheral
* construction via `Peripheral(advertisement)` with a direct (non-autoConnect) connection attempt.
*
* Bonded-only devices (address only, [advertisement] null) can still be constructed — e.g. for
* display in the device list — but [BleRadioTransport.findDevice] requires a fresh advertisement
* before connecting and will throw [RadioNotConnectedException] rather than returning a stale handle.
*
* @param address The device's MAC address (or platform identifier string).
* @param name The device's display name, if known.
* @param advertisement The Kable [Advertisement] from a live scan, or `null` for bonded-only devices.
*/Fine to handle in a separate follow-up; just flagging so the doc doesn't mislead the next reader. |
…econnect flow (meshtastic#5913) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Overview
This PR fixes BLE auto-reconnect for bonded radios when the initial targeted scan misses a low-duty advertising window.
Previously,
BleRadioTransport.findDevice()fell back to the stale bonded device handle after a missed 2-second targeted scan. That handle could lack a fresh advertisement, causingKableBleConnectionto enterautoConnect = trueand wait forCONNECTION_TIMEOUTbefore the reconnect loop could try again.Bonded reconnect now requires a fresh advertisement before connecting. The reconnect path keeps the fast targeted scan, escalates once to a bounded scan when needed, and reports the bonded device as unavailable if it still is not advertising.
Key changes
RadioNotConnectedExceptionif the bonded device is still not advertising.autoConnect = truehangs.scanForFreshDevice(timeout)helper so bonded and non-bonded discovery use the same single-scan primitive.Connected.Testing
connectAndAwaitis not called.Connected.Connected.Breaking changes / migration
No breaking API or user-facing migration steps are required.