Compare commits

..

13 Commits

Author SHA1 Message Date
uier 8b2bd06393 Force RNT to 0 for unidirectional CIS, keeping PHY valid 2026-07-14 07:51:11 +00:00
TzuWei ab0e5801a2 Revert "CIG: Fix CIG parameters for unidirectional CIS" 2026-07-13 21:15:28 +08:00
Josh Wu 1d26b99865 Merge pull request #950 from dcaayushd/fix/cs-channel-map-excludes-channel-76
fix: exclude channel 76 from default CS channel_map (Core Spec 6.0 compliance)
2026-07-08 15:35:38 +08:00
TzuWei c75402d29a Merge pull request #952 from google/usb-interrupt-event-handler-fallback
transport/usb: Handle missing interruptEventHandler gracefully
2026-07-08 15:30:40 +08:00
Josh Wu c8360b7a06 Merge pull request #951 from zxzxwu/pasync
Fix race condition in PeriodicAdvertisingSync state transition
2026-07-08 15:24:28 +08:00
uier 13c6090f8c transport/usb: Handle missing interruptEventHandler gracefully
Wrap `interruptEventHandler()` in a try-catch block to handle
unsupported operations on older libusb versions.
2026-07-08 07:22:22 +00:00
dcaayushd 6fd08b709a Fix CS default channel map 2026-07-06 19:23:53 +05:45
Josh Wu 55d8171ad8 Fix race condition in PeriodicAdvertisingSync state transition
In PeriodicAdvertisingSync.establish, the state was unconditionally
set to PENDING after sending the create sync command.
If the sync was established immediately (e.g. HCI_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED_EVENT
received before establish completed), the state transition to ESTABLISHED
would be overwritten back to PENDING, causing timeouts in listeners
waiting for establishment.

Only set state to PENDING if it is still INIT.

TAG=agy
CONV=e61bef8b-ae31-48da-9bac-74f55d92568d
2026-07-06 16:06:44 +08:00
Josh Wu 2111c49a2c Merge pull request #897 from zxzxwu/dependency
Refine dependencies
2026-07-02 14:11:46 +08:00
Josh Wu adef270195 Refine dependencies 2026-07-02 13:48:25 +08:00
TzuWei 8015e528f2 Merge pull request #943 from google/cis-fix-unidirectional
CIG: Fix CIG parameters for unidirectional CIS
2026-07-01 14:21:43 +08:00
uier 9f2fdf5309 Add explanatory comments in post init method
Clarify why we reset retransmission count and PHY to 0 when the SDU size
is 0 for a direction.
2026-07-01 05:57:48 +00:00
uier 7d816b0c6c CIG: Fix CIG parameters for unidirectional CIS
When setting up a CIG for a unidirectional CIS (e.g.,
Central-to-Peripheral only), the unused direction's max SDU size is
set to 0. However, the other parameters (retransmission count and PHY)
defaulted to non-zero values. This caused controllers to reject the
configuration during CIG setup or CIS establishment with error 0x30
(Parameter Out Of Mandatory Range).

This change resets the retransmission count and PHY to 0 for any
direction where max SDU is 0. A test_cis_parameters_unidirectional test
is included with this change.
2026-07-01 05:57:33 +00:00
18 changed files with 158 additions and 58 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand":
"python -m pip install '.[build,test,development,documentation]'",
"python -m pip install '.[all]' --group dev",
// Configure tool-specific properties.
"customizations": {
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[build,test,development]"
python -m pip install ".[all]" --group dev
- name: Check
run: |
invoke project.pre-commit
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Install
run: |
python -m pip install --upgrade pip
python -m pip install .[avatar]
python -m pip install .[all,avatar]
- name: Rootcanal
run: nohup python -m rootcanal > rootcanal.log &
- name: Test
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[build,test,development,documentation]"
python -m pip install ".[all]" --group dev
- name: Test
run: |
invoke test
@@ -62,7 +62,7 @@ jobs:
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[build,test,development,documentation]"
python -m pip install ".[all]" --group dev
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
+1 -1
View File
@@ -33,7 +33,7 @@ To install package dependencies needed to run the bumble examples, execute the f
```
python -m pip install --upgrade pip
python -m pip install ".[test,development,documentation]"
python -m pip install ".[all]" --group dev
```
### Examples
+1 -3
View File
@@ -40,9 +40,7 @@ else:
try:
import lc3 # type: ignore # pylint: disable=E0401
except ImportError as e:
raise ImportError(
"Try `python -m pip install \"git+https://github.com/google/liblc3.git\"`."
) from e
raise ImportError("Try `python -m pip install '.[auracast]'`.") from e
import bumble.device
import bumble.logging
+1 -1
View File
@@ -30,7 +30,7 @@ from importlib import resources
try:
import lc3 # type: ignore # pylint: disable=E0401
except ImportError as e:
raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e
raise ImportError("Try `python -m pip install \".[auracast]\"`.") from e
import aiohttp.web
import click
+12 -2
View File
@@ -913,7 +913,8 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
)
)
self.state = self.State.PENDING
if self.state == self.State.INIT:
self.state = self.State.PENDING
async def terminate(self) -> None:
if self.state in (self.State.INIT, self.State.CANCELLED, self.State.TERMINATED):
@@ -1575,6 +1576,15 @@ class CigParameters:
rtn_c_to_p: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of C->P retransmissions
rtn_p_to_c: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of P->C retransmissions
def __post_init__(self) -> None:
# For unidirectional CIS (e.g., Central-to-Peripheral only), the unused direction's
# SDU size is 0. If SDU size is 0, we set RTN to 0 as well for maintaining
# compatibility with older firmware that has error 0x30 bug.
if self.max_sdu_c_to_p == 0:
self.rtn_c_to_p = 0
if self.max_sdu_p_to_c == 0:
self.rtn_p_to_c = 0
cig_id: int
cis_parameters: list[CisParameters]
sdu_interval_c_to_p: int # C->P SDU interval, in microseconds
@@ -5430,7 +5440,7 @@ class Device(utils.CompositeEventEmitter):
role: int = hci.CsRole.INITIATOR,
rtt_type: int = hci.RttType.AA_ONLY,
cs_sync_phy: int = hci.CsSyncPhy.LE_1M,
channel_map: bytes = b'\x54\x55\x55\x54\x55\x55\x55\x55\x55\x15',
channel_map: bytes = b'\x54\x55\x55\x54\x55\x55\x55\x55\x55\x05',
channel_map_repetition: int = 0x01,
channel_selection_type: int = hci.HCI_LE_CS_Create_Config_Command.ChannelSelectionType.ALGO_3B,
ch3c_shape: int = hci.HCI_LE_CS_Create_Config_Command.Ch3cShape.HAT,
+12 -7
View File
@@ -28,13 +28,18 @@ from bumble.transport.common import (
)
# pylint: disable=no-name-in-module
from bumble.transport.grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
from bumble.transport.grpc_protobuf.emulated_bluetooth_pb2_grpc import (
EmulatedBluetoothServiceStub,
)
from bumble.transport.grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import (
VhciForwardingServiceStub,
)
try:
from bumble.transport.grpc_protobuf.emulated_bluetooth_packets_pb2 import HCIPacket
from bumble.transport.grpc_protobuf.emulated_bluetooth_pb2_grpc import (
EmulatedBluetoothServiceStub,
)
from bumble.transport.grpc_protobuf.emulated_bluetooth_vhci_pb2_grpc import (
VhciForwardingServiceStub,
)
except ImportError as e:
raise ImportError(
'The bumble[android] extra is required to use the Android emulator transport'
) from e
# -----------------------------------------------------------------------------
# Logging
+21 -12
View File
@@ -37,18 +37,27 @@ from bumble.transport.common import (
)
# pylint: disable=no-name-in-module
from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
from bumble.transport.grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2 import (
PacketRequest,
PacketResponse,
)
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
PacketStreamerServicer,
PacketStreamerStub,
add_PacketStreamerServicer_to_server,
)
from bumble.transport.grpc_protobuf.netsim.startup_pb2 import Chip, ChipInfo, DeviceInfo
try:
from bumble.transport.grpc_protobuf.netsim.common_pb2 import ChipKind
from bumble.transport.grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2 import (
PacketRequest,
PacketResponse,
)
from bumble.transport.grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
PacketStreamerServicer,
PacketStreamerStub,
add_PacketStreamerServicer_to_server,
)
from bumble.transport.grpc_protobuf.netsim.startup_pb2 import (
Chip,
ChipInfo,
DeviceInfo,
)
except ImportError as e:
raise ImportError(
'The bumble[android] extra is required to use the Android netsim transport'
) from e
# -----------------------------------------------------------------------------
# Logging
+4 -1
View File
@@ -729,7 +729,10 @@ class UsbTransport(Transport):
# We no longer need the event loop to run
with self.lock:
self.event_loop_should_exit = True
self.context.interruptEventHandler()
try:
self.context.interruptEventHandler()
except (AttributeError, usb1.USBError) as error:
logger.warning(f"Failed to interrupt event handler: {error}")
self.device.releaseInterface(self.acl_interface.getNumber())
if self.sco_interface:
+3 -3
View File
@@ -29,10 +29,10 @@ Try installing the optional `[auracast]` dependencies:
## LC3
The `auracast` app depends on the `lc3` python module, which is available
either as PyPI module (currently only available for Linux x86_64).
as a PyPI module `lc3py` (currently available for Linux x86_64 and macOS arm64).
When installing Bumble with the optional `auracast` dependency, the `lc3`
module will be installed from the `lc3py` PyPI package if available.
If not, you will need to install it separately. This can be done with:
module will be installed automatically if your platform is supported.
If not, you will need to install it separately. This can be done by building it from source:
```bash
$ python3 -m pip install "git+https://github.com/google/liblc3.git"
```
+6
View File
@@ -71,6 +71,12 @@ $ python3 -m pip install git+https://github.com/google/bumble.git@27c0551
When you work on the Bumble code itself, and run some of the tests or example apps, or import the
module in your own code, you typically either install the package from source in "development mode" as described above, or you may choose to skip the install phase.
!!! tip "Installing Development Dependencies"
To install all optional dependencies and development tools (like `pytest`, `black`, `invoke`, etc.), run:
```bash
python3 -m pip install -e ".[all]" --group dev
```
If you plan on contributing to the project, please read the [contributing](development/contributing.md) section.
## Without Installing
+14
View File
@@ -24,6 +24,20 @@ stack via a virtual HCI interface.
Both ways are controlled via gRPC requests to the Android emulator controller and/or
from the Android emulator.
## Prerequisites
Using the Android Emulator or Netsim transports requires the `android` optional dependencies (which include `grpcio` and `protobuf`).
If you installed Bumble from PyPI:
```bash
python3 -m pip install "bumble[android]"
```
If you are running from source:
```bash
python3 -m pip install ".[android]"
```
## Launching the Emulator
If the version of the emulator you are running does not yet support enabling
@@ -8,6 +8,9 @@ ANDROID EMULATOR TRANSPORT
Use the `android-netsim` transport name instead.
!!! note
This transport requires the `android` extra. See the [Android Platform](../platforms/android.md) page for installation instructions.
The Android "netsim" transport either connects, as a host, to a **Netsim** virtual controller
("host" mode), or acts as a virtual controller itself ("controller" mode) accepting host
connections.
+35 -22
View File
@@ -24,23 +24,42 @@ dependencies = [
# updated. Relax the version requirement since it's better than being completely unable
# to import the package in case of version mismatch.
"cryptography >= 42.0.8; platform_system=='Android'",
"grpcio >= 1.62.1; platform_system!='Emscripten'",
"humanize >= 4.6.0; platform_system!='Emscripten'",
"libusb1 >= 2.0.1; platform_system!='Emscripten'",
"libusb-package == 1.0.26.4; platform_system!='Emscripten' and platform_system!='Android'",
"platformdirs >= 3.10.0; platform_system!='Emscripten'",
"prompt_toolkit >= 3.0.16; platform_system!='Emscripten'",
"prettytable >= 3.6.0; platform_system!='Emscripten'",
"protobuf >= 3.12.4; platform_system!='Emscripten'",
"pyee >= 13.0.0",
"pyserial-asyncio >= 0.5; platform_system!='Emscripten'",
"pyserial >= 3.5; platform_system!='Emscripten'",
"pyusb >= 1.2; platform_system!='Emscripten'",
"tomli ~= 2.2.1; platform_system!='Emscripten' and python_version<'3.11'",
"websockets >= 15.0.1; platform_system!='Emscripten'",
# Serial
"pyserial-asyncio >= 0.5; platform_system!='Emscripten'",
"pyserial >= 3.5; platform_system!='Emscripten'",
# USB
"libusb1 >= 2.0.1; platform_system!='Emscripten'",
"libusb-package == 1.0.26.4; platform_system!='Emscripten' and platform_system!='Android'",
"pyusb >= 1.2; platform_system!='Emscripten'",
]
[project.optional-dependencies]
android = [
"grpcio >= 1.62.1; platform_system!='Emscripten'",
"protobuf >= 3.12.4; platform_system!='Emscripten'",
]
avatar = [
"pandora-avatar == 0.0.10",
"rootcanal == 1.11.1 ; python_version>='3.10'",
]
pandora = ["bt-test-interfaces >= 0.0.6"]
auracast = [
"lc3py >= 1.1.3; python_version>='3.10' and ((platform_system=='Linux' and platform_machine=='x86_64') or (platform_system=='Darwin' and platform_machine=='arm64'))",
"sounddevice >= 0.5.1",
]
all = [
"bumble[android]",
"bumble[auracast]",
]
[dependency-groups]
build = ["build >= 0.7"]
test = [
"pytest >= 9.0",
@@ -48,7 +67,15 @@ test = [
"pytest-html >= 4.2",
"coverage >= 6.4",
]
development = [
docs = [
"mkdocs >= 1.6.0",
"mkdocs-material >= 9.6",
"mkdocstrings[python] >= 0.27.0",
]
dev = [
{include-group = "build"},
{include-group = "test"},
{include-group = "docs"},
"black ~= 25.1",
"bt-test-interfaces >= 0.0.6",
"grpcio-tools >= 1.62.1",
@@ -63,20 +90,6 @@ development = [
"types-invoke >= 1.7.3",
"types-protobuf >= 4.21.0",
]
avatar = [
"pandora-avatar == 0.0.10",
"rootcanal == 1.11.1 ; python_version>='3.10'",
]
pandora = ["bt-test-interfaces >= 0.0.6"]
documentation = [
"mkdocs >= 1.6.0",
"mkdocs-material >= 9.6",
"mkdocstrings[python] >= 0.27.0",
]
auracast = [
"lc3py >= 1.1.3; python_version>='3.10' and ((platform_system=='Linux' and platform_machine=='x86_64') or (platform_system=='Darwin' and platform_machine=='arm64'))",
"sounddevice >= 0.5.1",
]
[project.scripts]
bumble-auracast = "bumble.apps.auracast:main"
+1 -1
View File
@@ -47,7 +47,7 @@ def build(ctx, install=False):
if install:
ctx.run('python -m pip install .[build]')
ctx.run("python -m build")
ctx.run("python -I -m build")
# -----------------------------------------------------------------------------
+39
View File
@@ -17,6 +17,7 @@
# -----------------------------------------------------------------------------
import asyncio
import functools
import inspect
import logging
import os
from unittest import mock
@@ -595,6 +596,25 @@ async def test_cis_setup_failure():
await asyncio.wait_for(cis_create_task, _TIMEOUT)
# -----------------------------------------------------------------------------
def test_cis_parameters_unidirectional():
# Test C2P unidirectional (P to C not used)
cis_c2p = CigParameters.CisParameters(cis_id=1, max_sdu_p_to_c=0)
assert cis_c2p.max_sdu_c_to_p != 0
assert cis_c2p.rtn_c_to_p != 0
assert cis_c2p.phy_c_to_p != hci.PhyBit(0)
assert cis_c2p.rtn_p_to_c == 0
assert cis_c2p.phy_p_to_c != hci.PhyBit(0)
# Test P2C unidirectional (C to P not used)
cis_p2c = CigParameters.CisParameters(cis_id=2, max_sdu_c_to_p=0)
assert cis_p2c.max_sdu_p_to_c != 0
assert cis_p2c.rtn_p_to_c != 0
assert cis_p2c.phy_p_to_c != hci.PhyBit(0)
assert cis_p2c.rtn_c_to_p == 0
assert cis_p2c.phy_c_to_p != hci.PhyBit(0)
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_enter_and_exit_sniff_mode():
@@ -674,6 +694,25 @@ async def test_power_on_default_static_address_should_not_be_any():
assert devices[0].static_address != Address.ANY_RANDOM
# -----------------------------------------------------------------------------
def test_cs_channel_map_excludes_forbidden_channels():
forbidden = {0, 1, 23, 24, 25, 76, 77, 78, 79}
default_map = (
inspect.signature(Device.create_cs_config).parameters['channel_map'].default
)
enabled = {
byte_idx * 8 + bit
for byte_idx, byte in enumerate(default_map)
for bit in range(8)
if byte & (1 << bit)
}
assert enabled.isdisjoint(
forbidden
), f"Default channel_map enables forbidden CS channels: {enabled & forbidden}"
# -----------------------------------------------------------------------------
def test_gatt_services_with_gas_and_gatt():
device = Device(host=Host(None, None))