Silently reconnect BLE after autoreload-induced disconnect#511
Merged
makermelissa merged 2 commits intoJun 8, 2026
Merged
Conversation
…uitpython#377) CircuitPython firmware autoreloads on every BLE-FT mutating operation (write, move, delete, mkdir), tearing down the GATT server. Before this change, every save/rename/etc. required the user to click 'Connect' again, even though the device handle and FileTransferClient state were still usable. This patch detects mutating-op-induced disconnects and silently reconnects to the same paired device without prompting the user. The existing FileTransferClient instance is reused so open file dialogs keep their bound method references; the upstream library lazily re-fetches characteristics on the next op via checkConnection(). Mutating ops in the BLE-FT wrapper now await the silent reconnect before resolving, so callers (e.g. _openFolder after a rename) see a live GATT connection when they proceed. A post-reconnect settle delay gives the freshly-rebooted firmware VM time to mount CIRCUITPY and reach a writable filesystem state before the next op fires. Also fixes an operator-precedence bug in connect() where 'instanceof' bound tighter than '=', leaving result as a boolean instead of the actual super.connect() return value, masking real connect errors. Tested on CLUE (nRF52840) with CircuitPython 10.0.0: rename and auto-save chain through autoreload cleanly without user intervention. Closes circuitpython#509 (duplicate).
Replace block comments that re-explained the what with terse single-line notes focused on the why. PR description retains the full context for reviewers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #377. Closes #509 as duplicate.
Problem
CircuitPython firmware autoreloads on every BLE-FT mutating
operation —
WRITE,WRITE_DATA,DELETE,MKDIR,MOVE—which tears down the GATT server. See
supervisor/shared/bluetooth/file_transfer.c(search forautoreload_trigger).Before this PR, every save / rename / delete / mkdir over BLE
required the user to click Connect again, even though the
bleDevicehandle was still valid and theFileTransferClientstate was salvageable. The file dialog also dropped its handle and
operations failed silently if attempted during reconnect.
Fix
Silent reconnect path in
BLEWorkflow.connect(): when thedisconnect happens within ~5s of a known mutating op, reconnect
to the same
bleDevice(already paired) without a usergesture. Backoff retries at 1500 / 2500 / 4000 ms.
_rebindAfterSilentReconnect(): re-fetch BLE-FT service andNUS serial characteristics. Crucially, reuse the existing
FileTransferClientinstance instead of callingswitchToDevice()— the openFileDialogholds bound methodreferences that would be orphaned by client replacement. The
upstream
@adafruit/ble-file-transfer-jslibrary lazily re-fetches its internal
_transfercharacteristic viacheckConnection()on the next op when_transferis null(which
onDisconnectedalready sets), so this works cleanly.Mutating-op wrapper in
ble-file-transfer.js:writeFile,move,delete,makeDirnow:await super.<op>()to fire the firmware commandawait workflow.awaitPostOpReconnect()so the caller sees alive GATT connection when the promise resolves
awaitPostOpReconnect()pollsbleDevice.gatt.connectedfor up to 4s post-op, then awaits
_silentReconnectPromiseifone is in flight. This handles the race between
super.<op>()resolving andgattserverdisconnectedfiring.Post-reconnect settle delay (2s): even after GATT is up,
the firmware VM is still booting (running
boot.py, mountingCIRCUITPY, initializing BLE service). If the next mutating op
fires too early,
filesystem_lock()returns false and BLE-FTreturns
STATUS_ERROR_READONLY. The settle delay buys time forthe filesystem mount/lock state to stabilize.
Operator-precedence fix in
connect(): original code wasinstanceofbinds tighter than=, soresultwas alwaysa boolean. Parenthesized to:
Testing
On CLUE (nRF52840) running CircuitPython 10.0.0 over BLE:
updates with new path → auto-save fires → second autoreload →
silent reconnect → fully back online
manual reconnect
(out-of-range / device powered off) — those still surface the
normal manual-reconnect UI
Out of scope (not addressed by this PR)
writes are read-only by design. Already addressed in Cannot Save code.py Edits on Adafruit Qualia ESP32-S3 for TTL RGB-666 Displays #460 / Broaden read-only filesystem messaging to cover non-host-mounted cases (refs #460) #506.
filesystem_is_writable_by_python()regressionfrom CP PR #10659: tracked in BLE File Transfer writes blocked as STATUS_ERROR_READONLY when no USB host attached adafruit/circuitpython#10972. That
affects the web workflow PUT path, not BLE-FT (which uses
filesystem_lock(), a different gate)._resolve is not a function: noticed duringtesting that late notifications arriving during reconnect can
crash
processListDirEntryand friends. Pre-existing, notregressed by this PR; worth a follow-up upstream PR to guard
all
process*handlers.