Bluetooth Low Energy host stack in pure OCaml
0

Configure Feed

Select the types of activity you want to include in your feed.

ocaml-bluetooth / example
4 files
README.md

Driving blth without hardware#

macOS exposes no raw HCI on its built-in radio, and this stack is an HCI host -- it needs to speak H4 to a controller. This example stands up a software controller with Bumble so you can exercise the whole stack (scan, connect, GATT, pairing) over a socket, no dongle required.

virtual_controller.py builds one simulated RF link with two controllers on it:

  • a connectable, pairable peripheral FakeSensor (F0:F1:F2:F3:F4:F5) with a GATT service (0x1815) holding one readable/writable characteristic;
  • a bare controller exposed over TCP, which blth drives with --tcp.

Run it#

Start the controller (a venv keeps Bumble out of your system Python):

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python3 virtual_controller.py

Then, in another terminal:

blth scan --tcp 127.0.0.1:9000
blth gatt --tcp 127.0.0.1:9000 --peer F0:F1:F2:F3:F4:F5 --peer-random
blth pair --tcp 127.0.0.1:9000 --peer F0:F1:F2:F3:F4:F5 --peer-random \
    --random C0:11:22:33:44:55
  • scan lists FakeSensor with its RSSI and name.
  • gatt connects, discovers the services and characteristics, and reads a value.
  • pair connects and runs LE Secure Connections Just Works pairing, printing the bond.

Profile the connection data path:

obs run --out /tmp/blth_gatt -- blth gatt --tcp 127.0.0.1:9000 \
    --peer F0:F1:F2:F3:F4:F5 --peer-random
obs report /tmp/blth_gatt

Why the address flags#

Bumble's LocalLink sources every LE packet from the sending controller's random address, so a link peer is only reachable as a random-address device. That is why the peripheral advertises a static random address and the commands pass --peer-random; pair additionally needs --random ADDR so our own side is a random-address device too and the pairing DHKey check (which mixes both addresses) agrees with the wire. A real controller uses one consistent address and needs none of this.

Real hardware#

For real over-the-air scanning, plug a USB-UART HCI controller into the Mac (e.g. an nRF52840 running Zephyr's hci_uart, which enumerates as /dev/cu.usbmodem*) and drop --tcp for --device /dev/cu.usbmodemXXXX --baud 1000000. A plain USB Bluetooth dongle will not work: macOS claims it as the system adapter.

On a Linux host (e.g. a Raspberry Pi CM5)#

Linux exposes raw HCI, so blth can drive the host's own radio directly with --hci N (an HCI user channel over hci<N>). Bring the controller down first so nothing else manages it:

sudo hciconfig hci0 down        # or: sudo btmgmt --index 0 power off
sudo blth serve --hci 0 --name my-cm5

serve advertises a demo GATT server and prints each client connect / write / subscribe. Now scan and connect from your Mac or phone with any BLE app (or blth scan on the Mac with an external dongle). --hci also works with scan, gatt, and pair. On a CM5, plugging in the external antenna kit (and enabling it in the firmware config) gives that advertisement real range.