Skip to content

Commit 168ed72

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 168ed72

File tree

20 files changed

+1741
-445
lines changed

20 files changed

+1741
-445
lines changed

‎AUTHORS.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Contributors
2424
* David Johansen <davejohansen@gmail.com>
2525
* JP Hutchins <jphutchins@gmail.com>
2626
* Bram Duvigneau <bram@bramd.nl>
27+
* Victor Chavez <vchavezb@protonmail.com>
2728

2829
Sponsors
2930
--------

‎CHANGELOG.rst‎

Lines changed: 4 additions & 0 deletions
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
======================

‎bleak/backends/bumble/__init__.py‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (c) 2024 Victor Chavez
3+
"""Bumble backend."""
4+
import os
5+
from enum import Enum
6+
from typing import Dict, Final, Optional
7+
8+
from bumble.controller import Controller
9+
from bumble.link import LocalLink
10+
from bumble.transport import Transport, open_transport
11+
12+
transports: Dict[str, Transport] = {}
13+
_link: Final = LocalLink()
14+
_scheme_delimiter: Final = ":"
15+
16+
_env_transport_cfg: Final = os.getenv("BLEAK_BUMBLE")
17+
_env_host_mode: Final = os.getenv("BLEAK_BUMBLE_HOST")
18+
19+
20+
class TransportScheme(Enum):
21+
"""The transport schemes supported by bumble.
22+
23+
https://google.github.io/bumble/transports
24+
"""
25+
26+
SERIAL = "serial"
27+
""": The serial transport implements sending/receiving HCI
28+
packets over a UART (a.k.a serial port).
29+
"""
30+
UDP = "udp"
31+
""": The UDP transport is a UDP socket, receiving packets on a specified port number,
32+
and sending packets to a specified host and port number.
33+
"""
34+
TCP_CLIENT = "tcp-client"
35+
""": The TCP Client transport uses an outgoing TCP connection.
36+
"""
37+
TCP_SERVER = "tcp-server"
38+
""": The TCP Server transport uses an incoming TCP connection.
39+
"""
40+
WS_CLIENT = "ws-client"
41+
""": The WebSocket Client transport is WebSocket connection
42+
to a WebSocket server over which HCI packets are sent and received.
43+
"""
44+
WS_SERVER = "ws-server"
45+
""": The WebSocket Server transport is WebSocket server that accepts
46+
connections from a WebSocket client. HCI packets are sent and received over the connection.
47+
"""
48+
PTY = "pty"
49+
""": The PTY transport uses a Unix pseudo-terminal device to communicate
50+
with another process on the host, as if it were over a serial port.
51+
"""
52+
FILE = "file"
53+
""": The File transport allows opening any named entry on a filesystem
54+
and use it for HCI transport I/O. This is typically used to open a PTY,
55+
or unix driver, not for real files.
56+
"""
57+
VHCI = "vhci"
58+
""": The VHCI transport allows attaching a virtual controller
59+
to the Bluetooth stack on operating systems that offer a
60+
VHCI driver (Linux, if enabled, maybe others).
61+
"""
62+
HCI_SOCKET = "hci-socket"
63+
""": An HCI Socket can send/receive HCI packets to/from a
64+
Bluetooth HCI controller managed by the host OS.
65+
This is only supported on some platforms (currently only tested on Linux).
66+
"""
67+
USB = "usb"
68+
""": The USB transport interfaces with a local Bluetooth USB dongle.
69+
"""
70+
ANDROID_NETSIM = "android-netsim"
71+
""": The Android "netsim" transport either connects, as a host, to a
72+
Netsim virtual controller ("host" mode), or acts as a virtual
73+
controller itself ("controller" mode) accepting host connections.
74+
"""
75+
76+
@classmethod
77+
def from_string(cls, value: str) -> "TransportScheme":
78+
try:
79+
return cls(value)
80+
except ValueError:
81+
raise ValueError(f"'{value}' is not a valid TransportScheme")
82+
83+
84+
class BumbleTransportCfg:
85+
"""Transport configuration for bumble.
86+
87+
Args:
88+
scheme (TransportScheme): The transport scheme supported by bumble.
89+
args (Optional[str]): The arguments used to initialize the transport.
90+
See https://google.github.io/bumble/transports/index.html
91+
"""
92+
93+
def __init__(self, scheme: TransportScheme, args: Optional[str] = None):
94+
self.scheme: Final = scheme
95+
self.args: Final = args
96+
97+
def __str__(self):
98+
return f"{self.scheme.value}:{self.args}" if self.args else self.scheme.value
99+
100+
101+
def get_default_transport_cfg() -> BumbleTransportCfg:
102+
if _env_transport_cfg:
103+
scheme_val, *args = _env_transport_cfg.split(_scheme_delimiter, 1)
104+
return BumbleTransportCfg(
105+
TransportScheme.from_string(scheme_val), args[0] if args else None
106+
)
107+
108+
return BumbleTransportCfg(TransportScheme.TCP_SERVER, "127.0.0.1:1234")
109+
110+
111+
def get_default_host_mode() -> bool:
112+
return True if _env_host_mode else False
113+
114+
115+
async def start_transport(
116+
cfg: BumbleTransportCfg, host_mode: bool = get_default_host_mode()
117+
) -> Transport:
118+
transport_cmd = str(cfg)
119+
if transport_cmd not in transports.keys():
120+
transports[transport_cmd] = await open_transport(transport_cmd)
121+
if not host_mode:
122+
Controller(
123+
"ext",
124+
host_source=transports[transport_cmd].source,
125+
host_sink=transports[transport_cmd].sink,
126+
link=_link,
127+
)
128+
return transports[transport_cmd]
129+
130+
131+
def get_link():
132+
# Assume all transports are linked
133+
return _link
Lines changed: 82 additions & 0 deletions
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: List[BleakGATTDescriptor] = []
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 bumble_uuid_to_str(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)