- BleReconnectPolicy: clarify maxFailures param documents Int.MAX_VALUE
disables the give-up path; remove misleading 'give up permanently'
- StreamTransport.onDeviceDisconnect: fix confusing 'wait for it to come
back' sentence in KDoc summary
- BleRadioTransportTest: import VerifyMode instead of fully-qualifying
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Android 12+ delivers ACTION_USB_DEVICE_ATTACHED only to manifest-declared
receivers; the runtime-registered UsbBroadcastReceiver inside UsbRepository
never sees this event. Forward it explicitly from MainActivity so the
serialDevices StateFlow refreshes and the device appears in the Connect →
Serial tab without requiring the user to replug.
Also re-poll in onResume() to handle process-restart or returning from
another app while a USB device was already attached.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Serial transports defaulted to isPermanent=true for any disconnect path,
including USB unplug and port-open failure. Both conditions can resolve
without explicit user re-selection: replug, OS re-enumeration, permission
grant. Only an explicit close() (user disconnects) is a true permanent
disconnect.
- StreamTransport: flip onDeviceDisconnect default isPermanent to false;
close() now passes isPermanent=true explicitly.
- SerialRadioTransport (Android): pass isPermanent=false explicitly on
USB unplug callback path.
- SerialTransport (JVM): flip both the open-failure path and the read-
loop teardown to isPermanent=false; both are recoverable conditions.
TcpRadioTransport.close() unconditionally emitted onDisconnect with
isPermanent=true. The 'closing' guard at the listener level already
suppresses the transient signal during teardown, and the explicit-
disconnect emit is owned by SharedRadioInterfaceService.stopTransportLocked.
The double-emit caused two terminal disconnect events for one user action
and prevented the auto-reconnect loop from cleanly owning its lifecycle.
The reconnect policy previously capped at 10 consecutive failures and
emitted a permanent disconnect, which terminated the reconnect loop and
required the user to manually re-select the device. BleRadioTransport is
only ever instantiated for the user-selected address (verified via
SharedRadioInterfaceService.startTransportLocked), so the only legitimate
permanent-disconnect path is explicit close() owned by the service layer.
- BleRadioTransport: pass maxFailures = Int.MAX_VALUE; backoff still
caps at 60 s so battery impact remains bounded.
- BleExceptionClassifier: flip UnmetRequirementException (BT off /
permission missing) to non-permanent — both can resolve without the
user re-selecting the device.
- Test: replace the old 'gives up after DEFAULT_MAX_FAILURES' test with
an inverted contract test that runs past the legacy threshold and
asserts the policy never emits isPermanent=true on its own.
Empirically, RAK4631/nRF52840 firmware does not respond to WAKE_BYTES
unless DTR is asserted on open. DTR maps to USB SET_CONTROL_LINE_STATE,
which the firmware uses to detect host presence and activate its serial-
side Meshtastic protocol. Bridge-chip boards (CH340, CP210x, FTDI)
tolerate the assertion.
The settle-delay bump in 3bf1dd868 changed the iteration cadence but the
two virtual-time budgets in BleRadioTransportTest still assumed 1 s
settles, so the threshold/max-failure assertions fired before any
onDisconnect call had been made. Recompute the budgets (3 iterations now
finish at ~24 001 ms; 10 iterations at ~405 000 ms) and update the KDoc
breakdowns to match. Also picks up a trivial spotless reflow in
BleReconnectPolicy's KDoc.
With a 1 s pause between disconnect/reconnect cycles, 3–4 out of 5
attempts failed mid-handshake (Stage1Draining timeouts) because the
firmware had not yet released its GATT session. 3 s is a conservative
compromise validated against a strong (-53 dBm) RAK4631 link.