Skip to content
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

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Contributors
* David Johansen <[email protected]>
* JP Hutchins <[email protected]>
* Bram Duvigneau <[email protected]>
* Victor Chavez <[email protected]>

Sponsors
--------
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0

`Unreleased`_
=============
Added
-----
* Added support for Google's Bumble Bluetooth stack.


`0.22.3`_ (2024-10-05)
======================
Expand Down
66 changes: 66 additions & 0 deletions bleak/backends/bumble/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2024 Victor Chavez
"""Bumble backend."""
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()


class TransportScheme(Enum):
SERIAL = "serial"
UDP = "udp"
TCP_CLIENT = "tcp-client"
TCP_SERVER = "tcp-server"
WS_CLIENT = "ws-client"
WS_SERVER = "ws-server"
PTY = "pty"
FILE = "file"
VHCI = "vhci"
HCI_SOCKET = "hci-socket"
USB = "usb"
PYUSB = "pyusb"
ANDROID_EMULATOR = "android-emulator"
ANDROID_NETSIM = "android-netsim"
UNIX = "unix"


class BumbleTransport:
def __init__(self, scheme: TransportScheme, args: Optional[str] = None):
"""
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
"""
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() -> BumbleTransport:
return BumbleTransport(TransportScheme.TCP_SERVER, "127.0.0.1:1234")


async def start_transport(transport: BumbleTransport) -> None:
transport_cmd = str(transport)
if transport_cmd not in transports.keys():
transports[transport_cmd] = await open_transport(transport_cmd)
Copy link
Contributor

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):

async def start_transport(
    open_transport_func: partial[Coroutine[None, None, Transport]]
) -> Transport:
    await open_transport_func()
    ...

my_transport = partial(open_serial_transport, "my serial transport args")

...

await start_transport(my_transport)

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.

Copy link
Author

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 specific open_transport_xyz because all the bumble examples and applications use this function. So I assumed this was the official Bumble way 🐝 .

Copy link
Author

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.

Copy link
Contributor

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.

Controller(
"ext",
host_source=transports[transport_cmd].source,
host_sink=transports[transport_cmd].sink,
link=_link,
)


def get_link():
# Assume all transports are linked
return _link
82 changes: 82 additions & 0 deletions bleak/backends/bumble/characteristic.py
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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been hoping we could get rid of the subclasses of BleakGATTService, BleakGATTCharacteristic and BleakGATTDescriptor and just use that class directly everywhere since the subclasses are 90% duplicate code.

Not sure if that is something that is feasible to do before this PR or not.

Copy link
Author

Choose a reason for hiding this comment

The 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 = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All use of name mangling should be removed. _ is sufficient to indicate privacy, the name mangling only makes things hard to test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs type annotation

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 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)
Loading