Skip to content

Commit 31b1df6

Browse files
committed
Added support for Google´s Bumble Bluetooth Controller stack
The backend supports direct use with Bumble. The HCI Controller is managed by the Bumble stack and the transport layer can be defined by the user (e.g. VHCI, Serial, TCP, android-netsim).
1 parent c98883b commit 31b1df6

20 files changed

+2138
-9
lines changed

Diff for: AUTHORS.rst

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Contributors
2424
* David Johansen <[email protected]>
2525
* JP Hutchins <[email protected]>
2626
* Bram Duvigneau <[email protected]>
27+
* Victor Chavez <[email protected]>
2728

2829
Sponsors
2930
--------

Diff for: CHANGELOG.rst

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
99

1010
`Unreleased`_
1111
=============
12+
Added
13+
-----
14+
* Added support for Google's Bumble Bluetooth stack.
15+
1216

1317
`0.22.3`_ (2024-10-05)
1418
======================

Diff for: bleak/backends/bumble/__init__.py

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (c) 2024 Victor Chavez
3+
"""Bumble backend."""
4+
from enum import Enum
5+
from typing import Dict, Final, Optional
6+
7+
from bumble.controller import Controller
8+
from bumble.link import LocalLink
9+
from bumble.transport import Transport, open_transport
10+
11+
transports: Dict[str, Transport] = {}
12+
_link: Final = LocalLink()
13+
14+
15+
class TransportScheme(Enum):
16+
SERIAL = "serial"
17+
UDP = "udp"
18+
TCP_CLIENT = "tcp-client"
19+
TCP_SERVER = "tcp-server"
20+
WS_CLIENT = "ws-client"
21+
WS_SERVER = "ws-server"
22+
PTY = "pty"
23+
FILE = "file"
24+
VHCI = "vhci"
25+
HCI_SOCKET = "hci-socket"
26+
USB = "usb"
27+
PYUSB = "pyusb"
28+
ANDROID_EMULATOR = "android-emulator"
29+
ANDROID_NETSIM = "android-netsim"
30+
UNIX = "unix"
31+
32+
33+
class BumbleTransport:
34+
def __init__(self, scheme: TransportScheme, args: Optional[str] = None):
35+
"""
36+
Args:
37+
scheme: TransportScheme: The transport scheme supported by bumble
38+
args: Optional[str]: The arguments used to initialize the transport.
39+
See https://google.github.io/bumble/transports/index.html
40+
"""
41+
self.scheme: Final = scheme
42+
self.args: Final = args
43+
44+
def __str__(self):
45+
return f"{self.scheme.value}:{self.args}" if self.args else self.scheme.value
46+
47+
48+
def get_default_transport() -> BumbleTransport:
49+
return BumbleTransport(TransportScheme.TCP_SERVER, "127.0.0.1:1234")
50+
51+
52+
async def start_transport(transport: BumbleTransport) -> None:
53+
transport_cmd = str(transport)
54+
if transport_cmd not in transports.keys():
55+
transports[transport_cmd] = await open_transport(transport_cmd)
56+
Controller(
57+
"ext",
58+
host_source=transports[transport_cmd].source,
59+
host_sink=transports[transport_cmd].sink,
60+
link=_link,
61+
)
62+
63+
64+
def get_link():
65+
# Assume all transports are linked
66+
return _link

Diff for: bleak/backends/bumble/characteristic.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (c) 2024 Victor Chavez
3+
4+
from typing import Callable, Final, List, Union
5+
from uuid import UUID
6+
7+
from bumble.gatt import Characteristic
8+
from bumble.gatt_client import CharacteristicProxy, ServiceProxy
9+
10+
from bleak import normalize_uuid_str
11+
from bleak.backends.bumble.utils import bumble_uuid_to_str
12+
from bleak.backends.characteristic import BleakGATTCharacteristic
13+
from bleak.backends.descriptor import BleakGATTDescriptor
14+
15+
16+
class BleakGATTCharacteristicBumble(BleakGATTCharacteristic):
17+
"""GATT Characteristic implementation for the Bumble backend."""
18+
19+
def __init__(
20+
self,
21+
obj: CharacteristicProxy,
22+
max_write_without_response_size: Callable[[], int],
23+
svc: ServiceProxy,
24+
):
25+
super().__init__(obj, max_write_without_response_size)
26+
self.__descriptors = []
27+
props = [flag for flag in Characteristic.Properties if flag in obj.properties]
28+
self.__props: Final = [str(prop) for prop in props]
29+
self.__svc: Final = svc
30+
uuid = bumble_uuid_to_str(obj.uuid)
31+
self.__uuid: Final = normalize_uuid_str(uuid)
32+
33+
@property
34+
def service_uuid(self) -> str:
35+
"""The uuid of the Service containing this characteristic"""
36+
return self.__svc.uuid
37+
38+
@property
39+
def service_handle(self) -> int:
40+
"""The integer handle of the Service containing this characteristic"""
41+
return self.__svc.handle
42+
43+
@property
44+
def handle(self) -> int:
45+
"""The handle of this characteristic"""
46+
return int(self.obj.handle)
47+
48+
@property
49+
def uuid(self) -> str:
50+
"""The uuid of this characteristic"""
51+
return self.__uuid
52+
53+
@property
54+
def properties(self) -> List[str]:
55+
"""Properties of this characteristic"""
56+
return self.__props
57+
58+
@property
59+
def descriptors(self) -> List[BleakGATTDescriptor]:
60+
"""List of descriptors for this characteristic"""
61+
return self.__descriptors
62+
63+
def get_descriptor(
64+
self, specifier: Union[int, str, UUID]
65+
) -> Union[BleakGATTDescriptor, None]:
66+
"""Get a descriptor by handle (int) or UUID (str or uuid.UUID)"""
67+
try:
68+
if isinstance(specifier, int):
69+
return next(filter(lambda x: x.handle == specifier, self.descriptors))
70+
else:
71+
return next(
72+
filter(lambda x: x.uuid == str(specifier), self.descriptors)
73+
)
74+
except StopIteration:
75+
return None
76+
77+
def add_descriptor(self, descriptor: BleakGATTDescriptor):
78+
"""Add a :py:class:`~BleakGATTDescriptor` to the characteristic.
79+
80+
Should not be used by end user, but rather by `bleak` itself.
81+
"""
82+
self.__descriptors.append(descriptor)

0 commit comments

Comments
 (0)