Skip to content

Keep the BLE workflow connection alive across VM restarts - #11255

Merged
dhalbert merged 6 commits into
adafruit:mainfrom
dhalbert:ble-reset-keep-workflow
Aug 27, 2026
Merged

Keep the BLE workflow connection alive across VM restarts#11255
dhalbert merged 6 commits into
adafruit:mainfrom
dhalbert:ble-reset-keep-workflow

Conversation

@dhalbert

@dhalbert dhalbert commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Claude wrote the code and found the underlying bugs; I directed, reviewed, and tested, and rewrote this post and some code comments.

(Part of the series of small PRs replacing #11178, following #11225, #11236, and #11237.)

Problem

Typing at the "Press any key to enter the REPL" prompt over BLE workflow serial closed the connection after exactly one character: the first byte breaks main.c's wait loop, which calls bleio_reset(). That disables and re-enables the full BLE stack, which drops every connection, including the workflow's. Ctrl-D reloads dropped it the same way.

Reset Fixes

  • bleio_reset() now restarts the BLE stack only when user code created a GATT service. That is tracked by a flag set in the shared-bindings Service constructor. BLE workflow services are created through common-hal directly and do not set the flag.
    The only reason to restart the BLE stack is to clear user services from the GATT table. There is no way on nordic to clear those services; there is such an API on espressif, but we don't use it yet (could be a future PR).
  • bleio_user_reset() now tears down user BLE state individually on every VM reset: it stops user scanning and advertising (as before), and now also disconnects the connections that user code initiated or accepted with its own advertising. A user_owned flag was added to the Connection object. The BLE workflow connection is not user-owned and stays up. This implements espressif's old "TODO: Don't stop BLE workflow connection."
    Since the restart no longer always runs, bleio_user_reset() also clears the adapter's and connections' pointers into the VM heap, which the restart used to clear implicitly.

Related cleanup

  • Cleaned up ble_hci reset by deleting reset_devices() in main.c and moving some code from bleio_reset() to common_hal_bleio_init(). This avoided a double reset (which was harmless). These changes don't affect the other ports. (Noticed by @dhalbert)
  • Improved comments in various places.
  • Removed dead vm_used_ble and cccd_uuid declarations from silabs, copied long ago from ble_hci and never used.

Service Changed Problem

(This was an additional problem discovered while debugging. It's related to resets so @dhalbert included it in this PR.)

If you run _bleio code that creates services while the BLE workflow is running, the set of services presented by your board changes. A bonded host caches the GATT table across reconnects, and assumes the services are the same. To tell the host that the services change, the device should send a Service Changed indication.
Service Changed was done wrong on nordic and not done at all on espressif, so a reload that added or removed services left hosts writing to stale handles. That is unreportable at the ATT layer and caused the REPL serial connection to look dead.

Service Changed Fixes

  • Nordic sent the indication only when the bonded system-attribute load succeeded. It also didn't use precise arguments for the service attribute range that changed: it used 0xC-0xFFFF for the range. The call to sd_ble_gatts_service_changed() failed, but we ignored the error. Now the populated range is tracked as attributes are added, and Service Changed is sent with that range on every bonded reconnect.
  • Espressif never sent Service Changed at all. Now ble_svc_gatt_changed() is called after the cycle rebuilds the table; NimBLE indicates connected peers immediately and records the change for bonded peers, indicating them when they reconnect.

Both verified with btmon: a correct Service Changed goes out, and the host confirms, rediscovers, and resubscribes.

Not being afraid of disconnects

In the #11178 review, @tannewt suggested accepting connection drops on reload, since BLE is designed for flaky connections and bonding makes reconnection cheap. This PR takes a middle position: it keeps the disconnects that mean something and removes the ones that don't.

  • If a reload changed the board's services, the connection still drops, because the stack restart requires it. That disconnect now works properly end to end: bonds survive, the host reconnects, and the Service Changed indication tells it to rediscover.
  • If a reload changed nothing, the connection now stays up. Dropping it would tell the host nothing, and would cost a full reconnect: connection setup, re-encryption, rediscovery, and resubscription. That's several seconds of dead air on every save-and-reload during development, which is exactly when the serial connection is being watched.

Testing

Tested on a Feather nRF52840 Express and a Metro ESP32-S3 with the web editor on Linux, verified with btmon. Some testing on Windows and macOS also done. macOS is by far the most robust.

  • If there is no user code that creates services, a disconnect does not need to happen, and doesn't.
  • A test code.py created services that would need to be cleaned up when it stopped. In that case a clean disconnect is done on reload (not a supervision timeout). Reconnecting works: the host receives Service Changed, rediscovers, and file transfer works.
  • A heap-churn soak (scan, connections tuple, allocation churn, 10+ reloads) showed no corruption or safe-mode resets.

Note: a known, pre-existing espressif PacketBuffer send bug, to be fixed in a following PR, can hang reloads while the web editor is connected. It also affects long file-directory listings on current main.

There are still problems with web-editor: it is slow to notice board-initiated disconnects, and after its automatic reconnect the serial pane does not re-acquire its characteristic, so typing goes nowhere. I'll file fixes for that later. But the fixes here will allow those fixes to succeed.

dhalbert and others added 4 commits August 26, 2026 13:35
Typing at the "Press any key to enter the REPL" prompt over BLE workflow
serial ended the session after exactly one character: the first byte breaks
main.c's wait loop, which calls bleio_reset(), and the full stack
disable/enable cycle there drops every connection, the workflow's included.
Ctrl-D reloads dropped it the same way.

bleio_user_reset() now tears down user BLE state individually on every VM
reset: it stops user scanning and advertising (as before), and now also
disconnects the connections that user code initiated or accepted with its own
advertising. Connections are marked as user-owned when they are created: only
user code connects in the central role, and a peripheral connection belongs to
whoever started the advertising it answered. The BLE workflow connection is
not user-owned and stays up. This does espressif's old "TODO: Don't stop BLE
workflow connection."

bleio_reset() still runs the full stack cycle, but only when user code
created a GATT service, tracked by a flag set in the shared-bindings Service
constructor. The cycle exists only to clear user services from the GATT
table: the SoftDevice can do that no other way, and espressif for now matches
it, though its NimBLE can delete individual services (a possible follow-up).
When no user services exist, there is nothing to tear down and the workflow
session survives. The supervisor constructs its workflow services through
common-hal directly and does not set the flag.

Since the full cycle no longer always runs, bleio_user_reset() also clears
the adapter's and connections' pointers into the VM heap (connection objects,
remote service lists, nordic's advertising data buffers), which the cycle
used to clear implicitly. The adapter struct is a GC root, so a pointer left
over from a dead heap would be scanned as a live object in the next VM.

Also make the Service Changed indication work, so a bonded host discards its
cached GATT table when a reload changed the services. nordic sent it only
when the bonded system-attribute load succeeded, and the call never worked
anyway: sd_ble_gatts_service_changed() rejects handles outside the
application-populated attribute range, and it was called with (0xC, 0xFFFF),
the error ignored. The populated range is now tracked as attributes are added
and indicated on every bonded reconnect. espressif never signaled at all; it
now calls ble_svc_gatt_changed() after the cycle rebuilds the table, which
indicates connected peers immediately and bonded peers when they reconnect.
Verified with btmon on both ports: the indication goes out with the real
range, and the host confirms, rediscovers, and resubscribes.

Applies to nordic and espressif, the ports that run the BLE workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete main.c's reset_devices(), which only handled CIRCUITPY_BLEIO_HCI. At
VM cleanup it double-reset those builds, because ble_hci's bleio_user_reset()
does a full reset itself; at startup it stood in for HCI initialization,
which now happens in common_hal_bleio_init() when user code imports _bleio.
HCI is unusable before user code supplies an adapter, so import time is early
enough, and bleio_reset() no longer re-creates the CCCD UUID that init now
owns.

Remove silabs declarations of vm_used_ble and cccd_uuid, copied long ago from
ble_hci and never defined or used in that port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…flow

# Conflicts:
#	ports/nordic/common-hal/_bleio/Adapter.h
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two minor things. Thanks for the fixes.

Comment thread shared-bindings/_bleio/__init__.c Outdated
Comment thread shared-bindings/_bleio/__init__.h Outdated
Comment thread main.c
dhalbert and others added 2 commits August 26, 2026 16:25
…ed()

Per review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you!

@dhalbert

Copy link
Copy Markdown
Collaborator Author

I'm going to merge this anyway because the zephyr test timeouts I think are fixed by changes in #11226.

@dhalbert
dhalbert merged commit de6b473 into adafruit:main Aug 27, 2026
26 of 28 checks passed
@dhalbert
dhalbert deleted the ble-reset-keep-workflow branch August 27, 2026 00:40
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.

BLE Workflow using Chrome pairs but doesn't respond

2 participants