-
Notifications
You must be signed in to change notification settings - Fork 310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added support for Google´s Bumble Bluetooth Controller stack #1681
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ Contributors | |
* David Johansen <[email protected]> | ||
* JP Hutchins <[email protected]> | ||
* Bram Duvigneau <[email protected]> | ||
* Victor Chavez <[email protected]> | ||
|
||
Sponsors | ||
-------- | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
# SPDX-License-Identifier: MIT | ||
# Copyright (c) 2024 Victor Chavez | ||
"""Bumble backend.""" | ||
import os | ||
from enum import Enum | ||
from typing import Dict, Final, Optional | ||
|
||
from bumble.controller import Controller | ||
from bumble.link import LocalLink | ||
from bumble.transport import Transport, open_transport | ||
|
||
transports: Dict[str, Transport] = {} | ||
_link: Final = LocalLink() | ||
_scheme_delimiter: Final = ":" | ||
|
||
_env_transport_cfg: Final = os.getenv("BLEAK_BUMBLE") | ||
_env_host_mode: Final = os.getenv("BLEAK_BUMBLE_HOST") | ||
|
||
|
||
class TransportScheme(Enum): | ||
"""The transport schemes supported by bumble. | ||
|
||
https://google.github.io/bumble/transports | ||
""" | ||
|
||
SERIAL = "serial" | ||
""": The serial transport implements sending/receiving HCI | ||
packets over a UART (a.k.a serial port). | ||
""" | ||
UDP = "udp" | ||
""": The UDP transport is a UDP socket, receiving packets on a specified port number, | ||
and sending packets to a specified host and port number. | ||
""" | ||
TCP_CLIENT = "tcp-client" | ||
""": The TCP Client transport uses an outgoing TCP connection. | ||
""" | ||
TCP_SERVER = "tcp-server" | ||
""": The TCP Server transport uses an incoming TCP connection. | ||
""" | ||
WS_CLIENT = "ws-client" | ||
""": The WebSocket Client transport is WebSocket connection | ||
to a WebSocket server over which HCI packets are sent and received. | ||
""" | ||
WS_SERVER = "ws-server" | ||
""": The WebSocket Server transport is WebSocket server that accepts | ||
connections from a WebSocket client. HCI packets are sent and received over the connection. | ||
""" | ||
PTY = "pty" | ||
""": The PTY transport uses a Unix pseudo-terminal device to communicate | ||
with another process on the host, as if it were over a serial port. | ||
""" | ||
FILE = "file" | ||
""": The File transport allows opening any named entry on a filesystem | ||
and use it for HCI transport I/O. This is typically used to open a PTY, | ||
or unix driver, not for real files. | ||
""" | ||
VHCI = "vhci" | ||
""": The VHCI transport allows attaching a virtual controller | ||
to the Bluetooth stack on operating systems that offer a | ||
VHCI driver (Linux, if enabled, maybe others). | ||
""" | ||
HCI_SOCKET = "hci-socket" | ||
""": An HCI Socket can send/receive HCI packets to/from a | ||
Bluetooth HCI controller managed by the host OS. | ||
This is only supported on some platforms (currently only tested on Linux). | ||
""" | ||
USB = "usb" | ||
""": The USB transport interfaces with a local Bluetooth USB dongle. | ||
""" | ||
ANDROID_NETSIM = "android-netsim" | ||
""": 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. | ||
""" | ||
|
||
@classmethod | ||
def from_string(cls, value: str) -> "TransportScheme": | ||
try: | ||
return cls(value) | ||
except ValueError: | ||
raise ValueError(f"'{value}' is not a valid TransportScheme") | ||
|
||
|
||
class BumbleTransportCfg: | ||
"""Transport configuration for bumble. | ||
|
||
Args: | ||
scheme (TransportScheme): The transport scheme supported by bumble. | ||
args (Optional[str]): The arguments used to initialize the transport. | ||
See https://google.github.io/bumble/transports/index.html | ||
""" | ||
|
||
def __init__(self, scheme: TransportScheme, args: Optional[str] = None): | ||
self.scheme: Final = scheme | ||
self.args: Final = args | ||
|
||
def __str__(self): | ||
return f"{self.scheme.value}:{self.args}" if self.args else self.scheme.value | ||
|
||
|
||
def get_default_transport_cfg() -> BumbleTransportCfg: | ||
if _env_transport_cfg: | ||
scheme_val, *args = _env_transport_cfg.split(_scheme_delimiter, 1) | ||
return BumbleTransportCfg( | ||
TransportScheme.from_string(scheme_val), args[0] if args else None | ||
) | ||
|
||
return BumbleTransportCfg(TransportScheme.TCP_SERVER, "127.0.0.1:1234") | ||
|
||
|
||
def get_default_host_mode() -> bool: | ||
return True if _env_host_mode else False | ||
|
||
|
||
async def start_transport( | ||
cfg: BumbleTransportCfg, host_mode: bool = get_default_host_mode() | ||
) -> Transport: | ||
transport_cmd = str(cfg) | ||
if transport_cmd not in transports.keys(): | ||
transports[transport_cmd] = await open_transport(transport_cmd) | ||
if not host_mode: | ||
Controller( | ||
"ext", | ||
host_source=transports[transport_cmd].source, | ||
host_sink=transports[transport_cmd].sink, | ||
link=_link, | ||
) | ||
return transports[transport_cmd] | ||
|
||
|
||
def get_link(): | ||
# Assume all transports are linked | ||
return _link |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
# SPDX-License-Identifier: MIT | ||
# Copyright (c) 2024 Victor Chavez | ||
|
||
from typing import Callable, Final, List, Union | ||
from uuid import UUID | ||
|
||
from bumble.gatt import Characteristic | ||
from bumble.gatt_client import CharacteristicProxy, ServiceProxy | ||
|
||
from bleak import normalize_uuid_str | ||
from bleak.backends.bumble.utils import bumble_uuid_to_str | ||
from bleak.backends.characteristic import BleakGATTCharacteristic | ||
from bleak.backends.descriptor import BleakGATTDescriptor | ||
|
||
|
||
class BleakGATTCharacteristicBumble(BleakGATTCharacteristic): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been hoping we could get rid of the subclasses of Not sure if that is something that is feasible to do before this PR or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently I put this PR in Draft to get some feedback. If you plan to refactor the classes I am open to rebase my fork and change the backend. |
||
"""GATT Characteristic implementation for the Bumble backend.""" | ||
|
||
def __init__( | ||
self, | ||
obj: CharacteristicProxy, | ||
max_write_without_response_size: Callable[[], int], | ||
svc: ServiceProxy, | ||
): | ||
super().__init__(obj, max_write_without_response_size) | ||
self.__descriptors: List[BleakGATTDescriptor] = [] | ||
props = [flag for flag in Characteristic.Properties if flag in obj.properties] | ||
self.__props: Final = [str(prop) for prop in props] | ||
self.__svc: Final = svc | ||
uuid = bumble_uuid_to_str(obj.uuid) | ||
self.__uuid: Final = normalize_uuid_str(uuid) | ||
|
||
@property | ||
def service_uuid(self) -> str: | ||
"""The uuid of the Service containing this characteristic""" | ||
return bumble_uuid_to_str(self.__svc.uuid) | ||
|
||
@property | ||
def service_handle(self) -> int: | ||
"""The integer handle of the Service containing this characteristic""" | ||
return self.__svc.handle | ||
|
||
@property | ||
def handle(self) -> int: | ||
"""The handle of this characteristic""" | ||
return int(self.obj.handle) | ||
|
||
@property | ||
def uuid(self) -> str: | ||
"""The uuid of this characteristic""" | ||
return self.__uuid | ||
|
||
@property | ||
def properties(self) -> List[str]: | ||
"""Properties of this characteristic""" | ||
return self.__props | ||
|
||
@property | ||
def descriptors(self) -> List[BleakGATTDescriptor]: | ||
"""List of descriptors for this characteristic""" | ||
return self.__descriptors | ||
|
||
def get_descriptor( | ||
self, specifier: Union[int, str, UUID] | ||
) -> Union[BleakGATTDescriptor, None]: | ||
"""Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" | ||
try: | ||
if isinstance(specifier, int): | ||
return next(filter(lambda x: x.handle == specifier, self.descriptors)) | ||
else: | ||
return next( | ||
filter(lambda x: x.uuid == str(specifier), self.descriptors) | ||
) | ||
except StopIteration: | ||
return None | ||
|
||
def add_descriptor(self, descriptor: BleakGATTDescriptor): | ||
"""Add a :py:class:`~BleakGATTDescriptor` to the characteristic. | ||
|
||
Should not be used by end user, but rather by `bleak` itself. | ||
""" | ||
self.__descriptors.append(descriptor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate all the work done to make this less error-prone; but I'd like to understand why the user cannot simply import their "open transport function" - like
open_serial_transport
,open_udp_transport
, etc.It's taking the burden off of Bleak to maintain. Because the function is awaitable and has side effects, it would likely be passed as a partial or lambda (a closure):
The specific transport functions are still poorly typed with serialized strings as arguments, but at least now they could change upstream without Bleak having any responsibility to maintain them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am using the
open_transport
instead of specificopen_transport_xyz
because all the bumble examples and applications use this function. So I assumed this was the official Bumble way 🐝 .There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldnt we map the possible transports functions to an Enum, then the user does not need to pass a partial or callback but just an enum plus string arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that would be cool, but still requires maintenance here when upstream changes. The closure is perhaps confusing, and an alternative would be that start_transport takes the open_transport_func and spec as args. If one of the open transport functions upstream changes its number of args, this could be a problem, but it feels like they are using serialized strings with the goal of the function signature being general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My intention with the current implementation is to reuse
open_transport
with the string syntax used in bumble's examples and applications. This provides familiarity for bumble users. The specific string syntax for each transport is documented here. If I tell the user to import theopen_transport_xyz
, additional documentation needs to be written telling them to look in the source ofbumble/transport
and list which functions are available.I understand your point about upstream changes. Bumble is an experimental project, so there is no guarantee of stability. Since it is still a work in progress, I could also keep this backend as an external module to avoid maintenance costs due to the maturity of the bumble project and allow users to experiment with the backend.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point and I agree it's a good justification to use the serialized strings!