Skip to main content

BSController

English | 简体中文

BSController is a Bluetooth LE Controller written in Python. Your Host stack talks to it over standard H4 HCI, and it transmits and receives over a simulated radio (BabbleSim). Controllers on the same simulated radio communicate with each other. Simulation time is discrete, so runs are reproducible.

Quick start

BSController needs Linux, Python 3.11 or newer, and a BabbleSim build. On Ubuntu 24.04 x86_64, install the build tools:

sudo apt-get update
sudo apt-get install -y git repo build-essential gcc-multilib g++-multilib python3-venv

Fetch BabbleSim and build its PHY, modem and channel models:

mkdir -p "$HOME/bsim" && cd "$HOME/bsim"
repo init -u https://github.com/BabbleSim/manifest.git -m everything.xml -b master
repo sync
export BSIM_OUT_PATH="$PWD/build"
export BSIM_COMPONENTS_PATH="$PWD/components"
make -j"$(nproc)" ext_2G4_phy_v1 ext_2G4_modem_BLE_simple ext_2G4_channel_multiatt

Keep BSIM_OUT_PATH exported whenever you run BSController. See the BabbleSim fetching and building guides for other options.

Then install BSController from this directory:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install .

Standalone Controller

Start a Controller and connect your Host to its socket:

bscontroller serve --hci-socket /tmp/radio.sock --btsnoop-file /tmp/hci.snoop

serve starts a BabbleSim PHY and serves H4 HCI on the Unix socket. It accepts one Host and exits when that Host disconnects; the socket path must not exist. --btsnoop-file saves the HCI traffic for Wireshark. See bscontroller serve --help.

The Controller runs on simulation time, which advances as fast as the machine allows. A Host that uses wall-clock time sees the Controller run ahead, so timers and timeouts on the two sides disagree.

For reproducible results, the Host must join the same discrete-time schedule. For a quick manual test, add BabbleSim's handbrake device instead, which holds simulation time close to wall-clock time. Start the PHY with one extra device, then join it:

cd "$BSIM_OUT_PATH/bin"
./bs_2G4_phy_v1 -s=demo -D=3 &           # 0: Controller, 1: serve clock, 2: handbrake
./bs_device_handbrake -s=demo -d=2 -r=1 &  # -r: simulation/wall-clock ratio
bscontroller serve --join --sim-id demo --dev-id 0 --clock-id 1 \
  --hci-socket /tmp/radio.sock --btsnoop-file /tmp/hci.snoop

Timing then still depends on host load, so use this only for exploration.

Using BSController from Python

RadioController runs one Controller in its own worker thread. Create it inside a running asyncio event loop. RadioSession joins it to a PHY that is already running. The example below expects a PHY started with the same sim_id (example) and two devices: device 0 for the Controller and device 1 for the Host clock.

import asyncio
from pathlib import Path

from bscontroller import RadioController, RadioEventLoop, RadioSession


async def main():
    controller = RadioController(
        "controller", "F0:F1:F2:F3:F4:01", hci_socket=Path("/tmp/radio.sock")
    )
    async with RadioSession("example", [(0, controller)], clock_dev_id=1):
        await controller.terminated


with asyncio.Runner(loop_factory=RadioEventLoop) as runner:
    runner.run(main())

examples/advertise_scan.py is a complete program built on these APIs: it starts the PHY with PhyProcess, runs an advertising and a scanning Controller in one RadioSession, and a mock Host scans over the scanner's socket. Run it with python examples/advertise_scan.py; see examples/README.md.

There are two ways to connect a Host:

  • Socket. Pass hci_socket, as above. Any Host that speaks H4 over a Unix socket can connect. Startup returns once the socket is listening. HCI commands are processed after the Controller has synchronized with the PHY.
  • In process. Omit hci_socket. Register an object with an on_packet(packet: bytes) method through controller.set_packet_sink(sink) before starting, then send complete H4 packets with controller.on_packet(packet).

Pass hci_snoop_file=Path(...) to record a BTSnoop capture, timestamped with simulation time.

Other public names are ControllerConfig (see Configuration), RadioEventLoop, Radio, Phy, Received and hci (HCI packet classes).

When managing the runtime yourself:

  • RadioSession does not start the PHY; use bscontroller.runtime.PhyProcess.
  • With clock_dev_id, the Host event loop must be a RadioEventLoop. Without it, you provide the clock and a distinct PHY device ID for every participant.
  • Stop the Controllers before closing the Host clock and the PHY; async with RadioSession(...) does this.

Configuration

Values the Bluetooth specification leaves to the Controller form a ControllerConfig. Unset fields keep their defaults.

JSON key Default Range Spec (Vol 4, Part E)
hciVersion 0x0F (6.1) 0x06–0x10 7.4.1
hciRevision 0 0–0xFFFF 7.4.1
lmpVersion 0x0F (6.1) 0x06–0x10 7.4.1
lmpSubversion 0 0–0xFFFF 7.4.1
manufacturer 0xFFFF 0–0xFFFF 7.4.1
aclPacketCount 8 1–255 7.8.2
aclPacketLength 251 27–65535 7.8.2
maxTxOctets 251 27–251 7.8.46
maxTxTime 17040 328–17040 7.8.46
maxRxOctets 251 27–251 7.8.46
maxRxTime 17040 328–17040 7.8.46
advertisingSetCount 16 1–240 7.8.58
maxAdvertisingDataLength 1650 31–1650 7.8.57
filterAcceptListSize 16 1–255 7.8.14
resolvingListSize 16 1–255 7.8.41
periodicAdvertiserListSize 8 1–255 7.8.73
maxConnections 64 1–3839 5.3.1
maxPeriodicSyncs 16 1–3839 5.3.1

A configuration file has a defaults object for every Controller, and a controllers object with one entry per Controller name:

{
  "defaults": {"aclPacketCount": 16},
  "controllers": {"hci0": {"maxConnections": 2}, "hci1": {"maxTxOctets": 27}}
}

Keys are camelCase; snake_case also works. For each value, the most specific source wins: built-in default, then defaults, then controllers.<name>, then Python keyword arguments. An unknown key, or a value outside its range, is skipped with a warning; that field keeps its previous value and the rest of the file still applies.

On the command line:

bscontroller config > bsc.json        # every default, ready to edit
bscontroller serve --hci-socket /tmp/radio.sock --config bsc.json --name hci0

In Python, pass the file (or an equivalent dict) to RadioController. Each Controller picks the controllers entry with its own name:

from bscontroller import ControllerConfig, RadioController

central = RadioController("hci0", "F0:F1:F2:F3:F4:01", config="bsc.json")
peripheral = RadioController("hci1", "F0:F1:F2:F3:F4:02", config="bsc.json")
other = RadioController("x", "F0:F1:F2:F3:F4:03", config=ControllerConfig(max_connections=2))

ControllerConfig(...) raises ValueError for an invalid value.

Supported features

These are the Link Layer features of the Bluetooth Core specification, listed by the version that introduced them. * marks implemented features. Assigned Link Layer feature bits are reported in the LE FeatureSet (Vol 6, Part B, 4.6). Bluetooth Classic (BR/EDR) is not.

Core spec version Link Layer feature Supported
4.0 LE Encryption *
4.1 Connection Parameters Request Procedure *
4.1 Extended Reject Indication *
4.1 Peripheral-initiated Features Exchange *
4.1 LE Ping Procedure *
4.2 LE Data Packet Length Extension *
4.2 LL Privacy *
4.2 Extended Scanner Filter Policies *
5.0 LE 2M PHY *
5.0 LE Coded PHY *
5.0 Channel Selection Algorithm #2 *
5.0 Minimum Number of Used Channels Procedure *
5.0 Extended Advertising *
5.0 Periodic Advertising *
5.1 Connection CTE Request
5.1 Connection CTE Response
5.1 Connectionless CTE Transmitter
5.1 Connectionless CTE Receiver
5.1 Periodic Advertising Sync Transfer
5.1 Initiating Periodic Advertising Sync Transfer for Local Periodic Advertising
5.1 Initiating Periodic Advertising Sync Transfer for Remote Periodic Advertising
5.2 Isochronous Broadcaster
5.2 Synchronized Receiver
5.2 Connected Isochronous Stream (CIS)
5.3 Periodic Advertising ADI Support
5.3 LE Channel Classification
5.3 Connection Subrating
5.4 Periodic Advertising with Responses
6.0 Channel Sounding
6.0 Decision-Based Advertising Filtering *
6.0 Monitoring Advertisers
6.0 Frame Space Update
6.1 Randomized RPA Updates *
6.2 Shorter Connection Intervals
6.2 Channel Sounding Amplitude-based Attack Resilience
6.3 Channel Sounding Inline Phase Correction Term Transfer

How it works

Each Controller contains an HCI layer, a Link Layer and a radio adapter. All Controllers connect to one BabbleSim PHY, which carries their packets on shared channels and advances one common simulation clock. Protocol behaviour follows Bluetooth Core 6.3.

Multiple Controllers sharing a BabbleSim PHY
Figure 1: Controllers on a shared PHY

Inside one Controller, the Link Layer keeps a manager for each role (advertising, scanning, initiating, synchronizing, connections). Every ACL connection has its own control-procedure state, encryption state and connection-event executor. All roles share one radio scheduler, which decides what the single radio does next.

Internal modules, state ownership and interaction paths of one Controller

Figure 2: Inside one Controller. Nested boxes show ownership. Black arrows are calls, dashed green arrows are callbacks, orange marks scheduling, blue marks PDUs and radio results, and dotted gray marks runtime wiring.

Threads and time. Each RadioController runs in a dedicated thread with its own event loop and PHY connection. The Host thread and the Controller thread share no state; they only pass immutable H4 packets. Radio time is in integer microseconds. Every Controller, and an optional Host clock, synchronizes with the PHY before simulation time advances, so timing does not depend on how fast the machine is.

Object Owns
RadioController Worker thread, Controller event loop, radio connection
RadioSession Starting and stopping workers, optional Host clock
H4Server Unix socket, the Host connection, H4 framing
PhyProcess BabbleSim PHY process, its logs and simulation directory

Development

.
├── src/bscontroller/     # Package: HCI, Link Layer, radio, runtime, config
├── examples/             # Runnable two-Controller example with a mock Host
└── tests/
    ├── unit/             # Fast tests, no PHY needed
    └── qualification/    # HCI/LL protocol cases; radio cases need BabbleSim
python -m pip install -e '.[test]'
python -m pytest tests/unit -n auto -q                  # about 3 s
python -m pytest tests/qualification -n auto -v -rs     # a few minutes, needs BSIM_OUT_PATH
python -m build                                         # sdist and wheel in dist/

The qualification suite runs 612 selected HCI and Link Layer test cases from the Bluetooth test specifications against independent lower testers. Passing them does not make BSController Bluetooth-qualified. See tests/README.md for the test scope.

Licensing and third-party notices are in LICENSE and NOTICE.

Metadata

Release files for bscontroller 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bscontroller 0.1.0
File Size Uploaded Provenance Actions
bscontroller-0.1.0.tar.gz 864.1 kB With provenance Details for bscontroller-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for bscontroller 0.1.0
File Interpreter ABI Platform Actions
bscontroller-0.1.0-py3-none-any.whl Python 3 none any Details for bscontroller-0.1.0-py3-none-any.whl

Total release size: 1.1 MB

Release files / bscontroller-0.1.0.tar.gz

Download URL bscontroller-0.1.0.tar.gz
Size 864.1 kB
Tags Source
SHA-256 checksum
How to use checksums
035bc7ded650112e40e21985bb661e382a3506067c375821368512ed83f20629
BLAKE2b-256 checksum
How to use checksums
c4bedad080f241e128d9ee75795826fb0ea60f434b10421c2fa3bff9501d3a28
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Release files / bscontroller-0.1.0-py3-none-any.whl

Download URL bscontroller-0.1.0-py3-none-any.whl
Size 212.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
298def96c3056b8b63f5bacf44f941748e49e2e883bc92751c80e3b2cdc37767
BLAKE2b-256 checksum
How to use checksums
c9cfe4e7df479a7a89bef8bcdd7273216dcda25aebd1526e95f3616b332b7a65
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Security for bscontroller 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for bscontroller-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page