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 anon_packet(packet: bytes)method throughcontroller.set_packet_sink(sink)before starting, then send complete H4 packets withcontroller.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:
RadioSessiondoes not start the PHY; usebscontroller.runtime.PhyProcess.- With
clock_dev_id, the Host event loop must be aRadioEventLoop. 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.
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.
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.