-
Notifications
You must be signed in to change notification settings - Fork 24
docs: add Bumble transport documentation #110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| # Bumble (BLE) | ||
|
|
||
| The bumble transport uses [Google's bumble Bluetooth stack](https://github.com/google/bumble) to | ||
| talk directly to an HCI controller, bypassing the host OS Bluetooth stack entirely. This is handy | ||
| when you're on a machine where the system BLE stack is flaky or unavailable — CI machines, embedded | ||
| Linux, or just wanting a more predictable setup with a USB dongle. | ||
|
|
||
| ## Install | ||
|
|
||
| ``` | ||
| smpclient[bumble] | ||
| ``` | ||
|
|
||
| ### HCI firmware extra | ||
|
|
||
| The `hci` parameter takes any transport spec that bumble's `open_transport()` understands, like | ||
| `"usb:0"` for the first USB Bluetooth dongle. If you need firmware for your controller, there's an | ||
| optional extra that bundles a pre-built Zephyr HCI image: | ||
|
|
||
| ``` | ||
| smpclient[hci_firmware] | ||
| ``` | ||
|
|
||
| Or grab both at once: | ||
|
|
||
| ``` | ||
| smpclient[bumble,hci_firmware] | ||
| ``` | ||
|
|
||
| #### Flashing an nRF52840 DK | ||
|
|
||
| The nRF52840 DK is the hardware the bumble transport has been tested on. Flash the bundled firmware | ||
| with [nrfutil](https://docs.nordicsemi.com/bundle/nrfutil/page/README.html): | ||
|
|
||
| ```python | ||
| # save the firmware bytes to a file | ||
| from smpclient.transport.firmware.hci import firmware | ||
|
|
||
| with open("hci_firmware.hex", "wb") as f: | ||
| f.write(firmware) | ||
| ``` | ||
|
|
||
| ```bash | ||
| # flash via JLink | ||
| nrfutil device program --firmware hci_firmware.hex --traits jlink | ||
| ``` | ||
|
|
||
| After flashing, the DK shows up as a USB HCI device. Use `hci="usb:0"` (or a higher index if | ||
| you have multiple USB Bluetooth devices plugged in). | ||
|
|
||
| Note: bumble supposedly supports some "ready made" consumer dongles too, but that hasn't been | ||
| verified with this transport. | ||
|
|
||
| ## smpbumble CLI | ||
|
|
||
| There's a small CLI app included — `smpbumble` — that lets you scan, pair, and send a quick echo | ||
| without writing any code. Good for testing your setup before building anything. | ||
|
|
||
| ```bash | ||
| # see what's advertising nearby ([SMP] marks devices with the SMP service) | ||
| smpbumble scan --hci usb:0 | ||
|
|
||
| # pair with a device (will prompt you to enter the PIN shown on the peripheral) | ||
| smpbumble pair AA:BB:CC:DD:EE:FF --hci usb:0 | ||
|
|
||
| # send an SMP echo to verify the connection works | ||
| smpbumble echo AA:BB:CC:DD:EE:FF "hello" --hci usb:0 | ||
| ``` | ||
|
|
||
| Run `smpbumble --help` for the full list of options. | ||
|
|
||
| ## Basic usage | ||
|
|
||
| ```python | ||
| import asyncio | ||
| from smpclient import SMPClient | ||
| from smpclient.transport.bumble import SMPBumbleTransport | ||
|
|
||
| async def main() -> None: | ||
| async with SMPClient(SMPBumbleTransport(hci="usb:0"), "AA:BB:CC:DD:EE:FF") as client: | ||
| # use client... | ||
| pass | ||
|
|
||
| asyncio.run(main()) | ||
| ``` | ||
|
|
||
| You can pass a device name instead of a MAC address and the transport will scan for it: | ||
|
|
||
| ```python | ||
| async with SMPClient(SMPBumbleTransport(hci="usb:0"), "MyDevice") as client: | ||
| ... | ||
| ``` | ||
|
|
||
| ## Scanning | ||
|
|
||
| ```python | ||
| from smpclient.transport.bumble import SMPBumbleTransport | ||
|
|
||
| results = await SMPBumbleTransport.scan(hci="usb:0", timeout_s=5.0) | ||
| for r in results: | ||
| print(r.address, r.name, r.rssi, r.has_smp_service) | ||
| ``` | ||
|
|
||
| ## Pairing | ||
|
|
||
| Pairing with a PIN isn't fully automatic — the PIN either needs a human to type it in, or your | ||
| code needs to read it from somewhere (like the peripheral's serial console). There's no way to | ||
| just "auto-pair" with PIN-based security. | ||
|
|
||
| ### User types the PIN | ||
|
|
||
| Use `KeyboardOnly` when someone will be at the terminal to enter the 6-digit PIN: | ||
|
|
||
| ```python | ||
| import asyncio | ||
| from smpclient.transport.bumble.pairing import KeyboardOnly, pair_device | ||
|
|
||
| async def prompt_pin() -> int | None: | ||
| raw = (await asyncio.to_thread(input, "PIN: ")).strip() | ||
| return int(raw) if raw.isdigit() and len(raw) == 6 else None | ||
|
|
||
| result = await pair_device("AA:BB:CC:DD:EE:FF", KeyboardOnly(prompt_pin), hci="usb:0") | ||
| ``` | ||
|
|
||
| `smpbumble pair` does the same thing from the command line. | ||
|
|
||
| ### Reading the PIN over serial (OOB) | ||
|
|
||
| If you're running automated tests or the peripheral prints the PIN on a serial port, you can read | ||
| it programmatically instead: | ||
|
|
||
| ```python | ||
| import asyncio | ||
| import serial_asyncio | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not cross platform, do not suggest it. I think that the programmatic example with the input callback is enough to get anyone started. |
||
| from smpclient.transport.bumble.pairing import KeyboardOnly, pair_device | ||
|
|
||
| async def read_pin_from_serial() -> int | None: | ||
| reader, _ = await serial_asyncio.open_serial_connection(url="/dev/ttyACM0", baudrate=115200) | ||
| async for line in reader: | ||
| text = line.decode().strip() | ||
| if text.isdigit() and len(text) == 6: | ||
| return int(text) | ||
| return None | ||
|
|
||
| result = await pair_device("AA:BB:CC:DD:EE:FF", KeyboardOnly(read_pin_from_serial), hci="usb:0") | ||
| ``` | ||
|
|
||
| ### Pairing on connect | ||
|
|
||
| Some Zephyr peripherals (built with `CONFIG_BT_SMP_ENFORCE_MITM=y`) issue a security request the | ||
| moment you connect, before GATT discovery. Pass `pair_on_connect` to handle that: | ||
|
|
||
| ```python | ||
| from smpclient.transport.bumble import SMPBumbleTransport | ||
| from smpclient.transport.bumble.pairing import KeyboardOnly | ||
|
|
||
| transport = SMPBumbleTransport(hci="usb:0", pair_on_connect=KeyboardOnly(prompt_pin)) | ||
| ``` | ||
|
|
||
| Bond keys are stored via the `keystore` strategy — see `smpclient.transport.bumble.keystore` for | ||
| the options (`Tempfile`, `Local`, `Custom`, `Memory`). | ||
|
Comment on lines
+160
to
+161
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally best avoid naming things in docs unless they are under test to avoid maintenance burden of the SSOT violation. |
||
|
|
||
| ## API Reference | ||
|
|
||
| ::: smpclient.transport.bumble | ||
|
|
||
| ::: smpclient.transport.bumble.scan | ||
|
|
||
| ::: smpclient.transport.bumble.keystore | ||
|
|
||
| ::: smpclient.transport.bumble.pairing | ||
|
|
||
| ::: smpclient.transport.bumble.device | ||
|
|
||
| ::: smpclient.transport.firmware.hci | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool! This can be simplified to grab the hci firmware path from its CLI via subshell:
Because each zephyr HCI package has a
__main__that defaults to outputting the path.