Skip to content

Silently reconnect BLE after autoreload-induced disconnect#511

Merged
makermelissa merged 2 commits into
circuitpython:mainfrom
makermelissa-piclaw:fix/issue-377-ble-autoreconnect
Jun 8, 2026
Merged

Silently reconnect BLE after autoreload-induced disconnect#511
makermelissa merged 2 commits into
circuitpython:mainfrom
makermelissa-piclaw:fix/issue-377-ble-autoreconnect

Conversation

@makermelissa-piclaw

Copy link
Copy Markdown
Contributor

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 for
autoreload_trigger).

Before this PR, every save / rename / delete / mkdir over BLE
required the user to click Connect again, even though the
bleDevice handle was still valid and the FileTransferClient
state was salvageable. The file dialog also dropped its handle and
operations failed silently if attempted during reconnect.

Fix

  1. Silent reconnect path in BLEWorkflow.connect(): when the
    disconnect happens within ~5s of a known mutating op, reconnect
    to the same bleDevice (already paired) without a user
    gesture. Backoff retries at 1500 / 2500 / 4000 ms.

  2. _rebindAfterSilentReconnect(): re-fetch BLE-FT service and
    NUS serial characteristics. Crucially, reuse the existing
    FileTransferClient instance
    instead of calling
    switchToDevice() — the open FileDialog holds bound method
    references that would be orphaned by client replacement. The
    upstream @adafruit/ble-file-transfer-js library lazily re-
    fetches its internal _transfer characteristic via
    checkConnection() on the next op when _transfer is null
    (which onDisconnected already sets), so this works cleanly.

  3. Mutating-op wrapper in ble-file-transfer.js: writeFile,
    move, delete, makeDir now:

    • Signal the workflow that a mutating op is in flight
    • await super.<op>() to fire the firmware command
    • await workflow.awaitPostOpReconnect() so the caller sees a
      live GATT connection when the promise resolves
  4. awaitPostOpReconnect() polls bleDevice.gatt.connected
    for up to 4s post-op, then awaits _silentReconnectPromise if
    one is in flight. This handles the race between
    super.<op>() resolving and gattserverdisconnected firing.

  5. Post-reconnect settle delay (2s): even after GATT is up,
    the firmware VM is still booting (running boot.py, mounting
    CIRCUITPY, initializing BLE service). If the next mutating op
    fires too early, filesystem_lock() returns false and BLE-FT
    returns STATUS_ERROR_READONLY. The settle delay buys time for
    the filesystem mount/lock state to stabilize.

  6. Operator-precedence fix in connect(): original code was

    if (result = await super.connect() instanceof Error) { ... }

    instanceof binds tighter than =, so result was always
    a boolean. Parenthesized to:

    if ((result = await super.connect()) instanceof Error) { ... }

Testing

On CLUE (nRF52840) running CircuitPython 10.0.0 over BLE:

  • ✅ Rename file → autoreload → silent reconnect → file dialog
    updates with new path → auto-save fires → second autoreload →
    silent reconnect → fully back online
  • ✅ Mutating ops on a single editor session no longer require
    manual reconnect
  • ✅ No regression on first-time connect or genuine disconnects
    (out-of-range / device powered off) — those still surface the
    normal manual-reconnect UI

Out of scope (not addressed by this PR)

…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.

@makermelissa makermelissa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you

@makermelissa makermelissa merged commit 4fa7932 into circuitpython:main Jun 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bluetooth workflow disconnects after every write/rename/delete operation (UX paper cut, fixable editor-side) BLE Disconnect on File Write Operations

2 participants