Skip to content

feat: bumble BLE transport for external-HCI SMP#99

Draft
JPHutchins wants to merge 8 commits into
mainfrom
feature/bumble-transport
Draft

feat: bumble BLE transport for external-HCI SMP#99
JPHutchins wants to merge 8 commits into
mainfrom
feature/bumble-transport

Conversation

@JPHutchins
Copy link
Copy Markdown
Collaborator

Summary

Adds an external-HCI BLE transport via Google's bumble stack, so users can run SMP over the same dongle on every host OS — useful when the system BLE stack is unavailable (Linux without BlueZ, certain CI runners) or when reproducible cross-platform behaviour matters.

The transport is selected with --bumble <BD_ADDR|local-name>. A new smpmgr bumble subcommand group covers the transport-level verbs that don't fit the SMP request/response shape:

  • smpmgr bumble scan — discover advertising devices; marks SMP servers.
  • smpmgr bumble pair <addr|name> [--force] — one-shot bonding via PIN entry; useful when the SMP characteristic requires encryption on first connect.
  • smpmgr bumble bonds list|clear <addr>|clear-all — manage the local keystore.
  • smpmgr bumble firmware paths|extract <board> — surface the bundled Zephyr HCI controller images shipped via smpclient[hci_firmware].

What's in the PR

Transport selection (smpmgr/main.py, smpmgr/common.py)

  • New root flags: --bumble, --hci (env-var SMPMGR_BUMBLE_HCI, default usb:0), --keystore local|tempfile|memory|<path> (default local — persistent platformdirs user data dir; diverges from smpclient's tempfile default since smpmgr is a daily-driver), --pair-on-connect keyboard|display|nio|none (default keyboard), --pair-timeout-s.
  • get_custom_smpclient gains a bumble branch using _resolve_keystore_strategy + _build_pair_delegate (both exhaustive match over the smpclient sum types).
  • connect_with_spinner now catches SMPBumbleTransportDeviceNotFound / SMPBumbleTransportNotSMPServer / generic SMPBumbleTransportException with friendly messages instead of tracebacks.

Bumble subcommand (smpmgr/bumble.py)

  • Rich-table output for scan / bonds / firmware-paths.
  • PairingResult exhaustive match maps PairingSucceeded / PairingAlreadyBonded / PairingTimedOut / PairingFailed to exit codes and human-readable lines.
  • firmware extract does a straight shutil.copy of the bundled .hex to a user-supplied destination so portable-binary users can flash without an installed smpclient.

Lifecycle fix (connect_with_spinner@asynccontextmanager, all 17 callsites across 9 files)

  • Bumble's libusb USB transport owns background threads that post into the asyncio loop. Without an explicit smpclient.disconnect() before asyncio.run() returns, the loop is torn down while libusb callbacks are still in flight, producing RuntimeError: Event loop is closed and a hung thread-pool join at process exit.
  • Every subcommand now uses async with connect_with_spinner(smpclient): and the connect helper auto-disconnects on context exit.

Interactive prompt fix (smpmgr/common.py)

  • The rich Progress spinner was repainting over the bumble pair delegate's PIN prompt during connect. A small module-level stack of active spinners + a _with_paused_spinner helper now pauses the spinner around the prompt and resumes after.
  • Stdin is read via a daemon thread + asyncio.Future so a pair_on_connect timeout doesn't leave a non-daemon worker blocked on input() — which previously deadlocked interpreter shutdown with _enter_buffered_busy.

Dependency pin (pyproject.toml, poetry.lock)

  • smpclient is temporarily pinned to the intercreate/smpclient git branch feature/bumble-transport (the corresponding PR is intercreate/smpclient#107).
  • The TODO(bumble): revert to a tagged PyPI release once intercreate/smpclient#107 lands comment on the pin marks the spot for un-pinning before this PR merges.

Test plan

  • mypy smpmgr — clean (16 source files).
  • flake8 smpmgr — clean.
  • pytest — existing suite green.
  • Hardware: first-connect pairing via smpmgr --bumble "<name>" os echo hi against a Zephyr-SMP-secure peer. PIN prompt visible, bond stored, echo round-trips, clean exit.
  • Hardware: subsequent connect auto-encrypts from stored LTK, no PIN re-prompt.
  • Hardware: pre-pair workflow via smpmgr bumble pair "<name>" + smpmgr --bumble "<name>" --pair-on-connect none os echo hi.
  • Hardware: bonds management via bumble bonds list / bumble bonds clear <addr>.
  • Hardware: graceful teardown — no Event loop is closed errors, no Ctrl-C needed.
  • Hardware: no spurious WARNINGs on clean disconnect (validated against the smpclient disconnect log-level fix).
  • Optional: regression smoke on --port, --ble, --ip after the connect_with_spinner refactor (mechanical change, low risk).
  • Optional: portable-binary build (portable.py) — bumble pulls libusb1, platformdirs, and the per-board firmware packages; PyInstaller spec may need additional hidden imports / data files.
  • Optional: README section documenting --bumble and smpmgr bumble.

Blockers before un-draft

  1. intercreate/smpclient#107 must merge and a tagged release cut. Then revert the git-source pin in pyproject.toml back to smpclient = { extras = ["all"], version = "==X.Y.Z" } and refresh the lockfile.
  2. Optional README + portable-binary validation per the checkboxes above.

Out of scope

  • Replacing --ble (bleak) with bumble. Both coexist; user picks per invocation.
  • Auto-flashing the HCI dongle (firmware extraction is exposed; west flash / nrfjprog is left to the user).
  • Non-tty PIN entry (GUI / passkey-file delegate variants).

🤖 Generated with Claude Code

JPHutchins and others added 6 commits May 27, 2026 12:37
…ration

Temporarily switch smpclient to the feature/bumble-transport branch on the
intercreate fork so smpmgr can pull in SMPBumbleTransport, the bumble
keystore/pairing modules, and the bundled zephyr-4-4-0-hci firmware packages.
Reverts to a tagged PyPI release once intercreate/smpclient#107 merges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an external-HCI BLE path via Google's bumble stack so users can run SMP
over the same dongle on every host OS, independent of the system BLE stack.
The transport is selected with --bumble <BD_ADDR|name>; HCI source, keystore,
pair-on-connect delegate, and pair timeout are tunable from the root callback.

Surface a `smpmgr bumble` subcommand group covering the transport-level
verbs that don't fit the SMP request/response shape: scan, pair (one-shot
bonding via PIN entry by default), bonds list/clear/clear-all, and firmware
paths/extract for the bundled Zephyr HCI controller images.

Convert connect_with_spinner into an @asynccontextmanager so every subcommand
disconnects the SMPClient before asyncio.run returns. This is required for
bumble: its libusb USB transport owns background threads that post into the
event loop, and without an explicit disconnect the loop is closed while
callbacks are still in flight, producing RuntimeError: Event loop is closed
and a hung thread-pool join at process exit.

Pause the active rich Progress spinner around interactive PIN prompts so the
delegate's "Enter PIN" line is actually visible during connect. Read stdin
via a daemon-thread + asyncio.Future pattern so a pair_on_connect timeout
no longer leaves a non-daemon worker blocked on input() — which previously
deadlocked interpreter shutdown with the _enter_buffered_busy abort.

Refresh the smpclient pin to the PR #107 race-fix tip (5f92a75 -> 7506224)
so first-connect pairing works without a pre-pair step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls in the smpclient PR #107 follow-up where _on_disconnection pattern-matches
HCI_ErrorCode by reason: local-host-terminated drops to DEBUG, peer-initiated
graceful disconnects go to INFO, real failures stay WARNING. End users no longer
see a spurious "Peer disconnected: reason=0x16" warning after every successful
SMP exchange.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… table

The rich.Table layout shrank the only column that matters — the HEX path —
to `/home/jp/repos/smpmgr/.venv/lib/py…`. Replace with `<name>  <absolute
path>` one-per-line so the output is greppable and copy-pasteable into
`west flash --hex-file=...`. Move board and full SHA256 behind --verbose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use the typed `Firmware` NamedTuple and `FirmwareModule` Protocol from the
upstream zephyr_4_4_0_hci package (re-exported via
`smpclient.transport.firmware.hci`) instead of stringly-keyed `getattr`
introspection over `Firmware._fields`.

Drop the `paths` and `extract` subcommands. Register one subcommand per
firmware variant on `smpmgr bumble firmware`, so the variants are
discoverable via `--help` and individually invokable (e.g.
`smpmgr bumble firmware nrf52840dk_default ~/hci.hex`). Each subcommand
defaults to verifying the embedded SHA-256 before writing; pass
`--no-verify` to skip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The most pipeable thing a firmware subcommand can do is emit the absolute
path of its bundled .hex on stdout — no decoration, no markup — so it
composes with shell substitution:

    west flash --hex-file=$(smpmgr bumble firmware nrf52840dk_default)

Move the copy-out behaviour behind --extract <PATH>. SHA-256 verification
still runs by default during --extract; --no-verify skips. The wrote-N-bytes
status line goes to stderr so it doesn't contaminate stdout for callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a Bumble-based BLE transport path for SMP operations using an external HCI controller, alongside new Bumble-specific CLI utilities for scanning, pairing, bond management, and firmware access.

Changes:

  • Adds root transport options for --bumble, HCI selection, keystore behavior, and pairing mode/timeouts.
  • Refactors SMP command call sites to use connect_with_spinner as an async context manager with explicit disconnect.
  • Introduces the smpmgr bumble command group and temporarily updates smpclient to a Git branch dependency.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
smpmgr/common.py Adds Bumble transport construction, pairing helpers, keystore resolution, prompt handling, and disconnect-on-exit connection lifecycle.
smpmgr/main.py Wires Bumble CLI/options into the main app and updates upgrade flow to the new connection context manager.
smpmgr/bumble.py Adds Bumble scan, pair, bonds, and firmware commands.
smpmgr/image_management.py Updates image commands to use the connection context manager.
smpmgr/file_management.py Updates file commands to use the connection context manager.
smpmgr/stat_management.py Updates statistics commands to use the connection context manager.
smpmgr/shell_management.py Updates shell command execution to use the connection context manager.
smpmgr/os_management.py Updates OS commands to use the connection context manager.
smpmgr/enumeration_management.py Updates enumeration commands to use the connection context manager.
smpmgr/user/intercreate.py Updates Intercreate upload command to use the connection context manager.
pyproject.toml Temporarily switches smpclient to the Bumble feature branch.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread smpmgr/common.py
Comment thread pyproject.toml
Comment on lines +33 to +34
# TODO(bumble): revert to a tagged PyPI release once intercreate/smpclient#107 lands.
smpclient = { git = "https://github.com/intercreate/smpclient.git", branch = "feature/bumble-transport", extras = ["all"] }
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — this is the explicit un-draft blocker called out in the PR description. The pin will be reverted to a tagged PyPI smpclient release once intercreate/smpclient#107 merges and a release is cut; the TODO(bumble): revert to a tagged PyPI release comment on the pin marks the spot. Leaving this thread open so it remains visible until that swap happens.

Comment thread smpmgr/bumble.py Outdated
JPHutchins and others added 2 commits May 27, 2026 17:37
- Update bundled plugin examples to call `connect_with_spinner` as an
  async context manager. The previous `await connect_with_spinner(...)`
  pattern raised TypeError after the ctx-manager refactor; Copilot review
  flagged this against `plugins/example_group.py` and
  `plugins/another/another_group.py`.
- Apply black + isort reflows in `smpmgr/{bumble,common,main}.py` so the
  Lint workflow passes on Python 3.11-3.14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wildcard the per-board firmware sibling packages via the upstream typed
`Firmware` NamedTuple from `zephyr_4_4_0_hci` and emit a `--collect-data`
flag for each, so new board variants are picked up automatically without
editing this script.

Without these data files the .py wrappers ship but their .hex resources
are missing, and `smpmgr bumble firmware <name>` raises FileNotFoundError
inside the portable bundle.

Verified locally: build succeeds, all 5 variants resolvable, extracted
.hex SHA-256 matches the embedded constant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants