From df5a2a918c7913dad19ccf1662a8aea334af9746 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Wed, 26 Mar 2025 22:53:50 +0100 Subject: [PATCH] feat(core/tools): BlueZ-emulator bridge [no changelog] --- core/tools/bluez-emu-bridge.py | 197 +++++++++++++++++ core/tools/bluez_emu_bridge/LICENSE | 21 ++ core/tools/bluez_emu_bridge/README.md | 36 ++++ core/tools/bluez_emu_bridge/__init__.py | 5 + core/tools/bluez_emu_bridge/adapter1.py | 82 +++++++ core/tools/bluez_emu_bridge/dbus-daemon.conf | 29 +++ core/tools/bluez_emu_bridge/device1.py | 202 ++++++++++++++++++ .../bluez_emu_bridge/gattcharacteristic1.py | 93 ++++++++ core/tools/bluez_emu_bridge/gattservice1.py | 29 +++ core/tools/bluez_emu_bridge/message_bus.py | 49 +++++ poetry.lock | 14 +- pyproject.toml | 1 + 12 files changed, 757 insertions(+), 1 deletion(-) create mode 100755 core/tools/bluez-emu-bridge.py create mode 100644 core/tools/bluez_emu_bridge/LICENSE create mode 100644 core/tools/bluez_emu_bridge/README.md create mode 100644 core/tools/bluez_emu_bridge/__init__.py create mode 100644 core/tools/bluez_emu_bridge/adapter1.py create mode 100644 core/tools/bluez_emu_bridge/dbus-daemon.conf create mode 100644 core/tools/bluez_emu_bridge/device1.py create mode 100644 core/tools/bluez_emu_bridge/gattcharacteristic1.py create mode 100644 core/tools/bluez_emu_bridge/gattservice1.py create mode 100644 core/tools/bluez_emu_bridge/message_bus.py diff --git a/core/tools/bluez-emu-bridge.py b/core/tools/bluez-emu-bridge.py new file mode 100755 index 0000000000..05f77ea80f --- /dev/null +++ b/core/tools/bluez-emu-bridge.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 + +import asyncio +import atexit +import logging +import subprocess +from pathlib import Path + +import click +from bluez_emu_bridge import MessageBus # normally lives in dbus_next.aio +from bluez_emu_bridge import Adapter1, Device1, GattCharacteristic1, GattService1 + +from trezorlib.transport.emu_ble import Command, Event + +HERE = Path(__file__).parent.resolve() + +SERVICE_UUID = "8c000001-a59b-4d58-a9ad-073df69fa1b1" +CHARACTERISTIC_RX = "8c000002-a59b-4d58-a9ad-073df69fa1b1" +CHARACTERISTIC_TX = "8c000003-a59b-4d58-a9ad-073df69fa1b1" + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + handlers=[logging.StreamHandler()], +) +LOG = logging.getLogger(__name__) + + +class TrezorUDP(asyncio.DatagramProtocol): + @classmethod + async def create(cls, ip, port): + loop = asyncio.get_running_loop() + addr = (ip, port) + return await loop.create_datagram_endpoint( + lambda: TrezorUDP(addr), + remote_addr=addr, + ) + + def __init__(self, addr): + self.addr = addr + self.transport = None + self.queue = asyncio.Queue() + + def ipport(self): + return f"{self.addr[0]}:{self.addr[1]}" + + def connection_made(self, transport: asyncio.DatagramTransport): + self.transport = transport + + def connection_lost(self, exc: Exception | None): + # Does this ever happen? + LOG.error(f"{self.ipport()} Connection lost", exc_info=exc) + + def datagram_received(self, data, addr): + if addr != self.addr: + LOG.error(f"{self.ipport()} Stray datagram from {addr}?") + return + LOG.debug(f"{self.ipport()} Received len={len(data)}") + self.queue.put_nowait(data) + + def error_received(self, exc: Exception | None): + LOG.error(f"{self.ipport()} UDP error", exc_info=exc) + + def write(self, value: bytes): + assert self.transport + LOG.debug(f"{self.ipport()} Sending len={len(value)}") + self.transport.sendto(value) + + def close(self): + if self.transport: + self.transport.close() + self.queue.shutdown() + + +class TrezorEmulator: + def __init__(self, data_transport, data_protocol, event_transport, event_protocol): + self._data_transport = data_transport + self.data_protocol = data_protocol + self._event_transport = event_transport + self.event_protocol = event_protocol + + def close(self): + self.data_transport.close() + self.event_transport.close() + + async def print_commands(self): + while True: + data = await self.event_protocol.queue.get() + command = Command.parse(data) + LOG.info(f"Emulator command: {command}") + + @classmethod + async def create(cls, emulator_port, device, char_tx, char_rx): + localhost = "127.0.0.1" + data_transport, data_protocol = await TrezorUDP.create(localhost, emulator_port) + + char_rx.send_value = data_protocol.write + data_read_task = asyncio.create_task( + char_tx.update_from_queue(data_protocol.queue) + ) + + remote_addr = ("127.0.0.1", emulator_port + 1) + event_transport, event_protocol = await TrezorUDP.create( + localhost, emulator_port + 1 + ) + + obj = cls(data_transport, data_protocol, event_transport, event_protocol) + + event_read_task = asyncio.create_task(device.connection_state_task(event_protocol.write, event_protocol.queue)) + # obj.event_protocol.write(Event.ping().build()) + + return obj + + +async def emulator_main(bus_address: str, emulator_port: int): + bus = await MessageBus(bus_address=bus_address).connect() + + hci0 = Adapter1(bus, "hci0") + device = Device1(bus, hci0.path, "01:02:03:04:05:06") + service = GattService1(bus, device.path, 0, SERVICE_UUID) + char_tx = GattCharacteristic1( + bus, service.path, 0, CHARACTERISTIC_TX, flags=["read", "notify"] + ) + char_rx = GattCharacteristic1( + bus, + service.path, + 1, + CHARACTERISTIC_RX, + flags=["write", "write-without-response"], + ) + + service.add_characteristic(char_tx) + service.add_characteristic(char_rx) + device.add_service(service) + hci0.add_device(device) + hci0.export() + + ### might make it possible to drop the message_bus.py hack + # from dbus_next.service import ServiceInterface + # bus.export("/", ServiceInterface("io.trezor.Empty")) + + emulator = await TrezorEmulator.create(emulator_port, device, char_tx, char_rx) + + await bus.request_name("org.bluez") + await bus.wait_for_disconnect() + + +def start_bus() -> str: + daemon = subprocess.Popen( + ( + "dbus-daemon", + "--print-address", + "--config-file", + HERE / "bluez_emu_bridge" / "dbus-daemon.conf", + ), + stdout=subprocess.PIPE, + encoding="utf-8", + ) + + def callback(): + daemon.terminate() + daemon.kill() + + atexit.register(callback) + address = daemon.stdout.readline().strip() + LOG.info(f"dbus-daemon listening at {address}") + parts = address.split(",") + parts = filter(lambda p: not p.startswith("guid="), parts) + address = ",".join(parts) + return address + + +@click.command() +@click.option( + "--bus-address", + help="Connect to D-Bus address. If not provided, private D-Bus instance will be launched.", +) +@click.option("-v", "--verbose", is_flag=True, help="Show additional info.") +@click.option( + "-p", + "--emulator-port", + type=int, + default=21328, + help="Trezor emulated BLE port to connect to.", +) +def cli(verbose: bool, emulator_port: int, bus_address: str | None): + if verbose: + logging.getLogger().setLevel(logging.DEBUG) + if not bus_address: + bus_address = start_bus() + click.echo(f"DBUS_SYSTEM_BUS_ADDRESS={bus_address}") + # asyncio.get_event_loop().run_until_complete(emulator_main(bus_address, emulator_port)) + asyncio.run(emulator_main(bus_address, emulator_port)) + + +if __name__ == "__main__": + cli() diff --git a/core/tools/bluez_emu_bridge/LICENSE b/core/tools/bluez_emu_bridge/LICENSE new file mode 100644 index 0000000000..b12c360573 --- /dev/null +++ b/core/tools/bluez_emu_bridge/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 OpenBluetoothToolbox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/core/tools/bluez_emu_bridge/README.md b/core/tools/bluez_emu_bridge/README.md new file mode 100644 index 0000000000..7765e189ed --- /dev/null +++ b/core/tools/bluez_emu_bridge/README.md @@ -0,0 +1,36 @@ +# bluez_emu_bridge + +Most of the files in this directory are based on the +[bluez-dbus-emulator](https://pypi.org/project/bluez-dbus-emulator/) python package +(GitHub: [python_bluez_dbus_emulator](https://github.com/simpleble/python_bluez_dbus_emulator)). + +Original README below. + +# bluez_dbus_emulator + +A simple set of libraries to allow emulating the behavior of a BlueZ +Bluetooth device over DBus. + +## Prerequisites + +Before you begin, ensure you have met the following requirements: +- [dbus_next](https://github.com/altdesktop/python-dbus-next) + +## Installation + +``` +pip3 install bluez_dbus_emulator +``` + +## Usage + +For usage instructions, just follow the examples provided in the `examples` folder. + +## Contributors + +Thanks to the following people who have contributed to this project: +* [@Andrey1994](https://github.com/Andrey1994) + +## License + +This project is licensed under the terms of the [MIT Licence](LICENCE.md). diff --git a/core/tools/bluez_emu_bridge/__init__.py b/core/tools/bluez_emu_bridge/__init__.py new file mode 100644 index 0000000000..b6bffc098a --- /dev/null +++ b/core/tools/bluez_emu_bridge/__init__.py @@ -0,0 +1,5 @@ +from bluez_emu_bridge.adapter1 import Adapter1 +from bluez_emu_bridge.device1 import Device1 +from bluez_emu_bridge.gattcharacteristic1 import GattCharacteristic1 +from bluez_emu_bridge.gattservice1 import GattService1 +from bluez_emu_bridge.message_bus import MessageBus diff --git a/core/tools/bluez_emu_bridge/adapter1.py b/core/tools/bluez_emu_bridge/adapter1.py new file mode 100644 index 0000000000..433c9dfdc8 --- /dev/null +++ b/core/tools/bluez_emu_bridge/adapter1.py @@ -0,0 +1,82 @@ +import asyncio +import logging +import random + +from dbus_next.service import PropertyAccess, ServiceInterface, dbus_property, method + +LOG = logging.getLogger(__name__) + + +class Adapter1(ServiceInterface): + def __init__(self, bus, path, address="00:00:00:12:34:56"): + self.bus = bus + self.path = path + super().__init__("org.bluez.Adapter1") + self._discovering = False + self._address = address + + self._devices = [] + + def export(self): + self.bus.export(f"/org/bluez/{self.path}", self) + + def add_device(self, device): + self._devices.append(device) + + @method() + def SetDiscoveryFilter(self, properties: "a{sv}"): + return + + @method() + async def StartDiscovery(self): + LOG.debug("StartDiscovery") + await self._update_discoverying(True) + for device in self._devices: + await device.task_scanning_start() + return + + @method() + async def StopDiscovery(self): + LOG.debug("StopDiscovery") + await self._update_discoverying(False) + for device in self._devices: + device.task_scanning_stop() + return + + @dbus_property(access=PropertyAccess.READ) + def Discovering(self) -> "b": + return self._discovering + + @dbus_property(access=PropertyAccess.READ) + def Address(self) -> "s": + return self._address + + @dbus_property(access=PropertyAccess.READ) + def AddressType(self) -> "s": + return "public" + + @dbus_property(access=PropertyAccess.READ) + def Modalias(self) -> "s": + return "usb:v1D6Bp0246d054F" + + @dbus_property(access=PropertyAccess.READ) + def Name(self) -> "s": + return "fake-ble-adapter" + + @dbus_property(access=PropertyAccess.READ) + def Alias(self) -> "s": + return "fake-ble-adapter-4real" + + @dbus_property(access=PropertyAccess.READWRITE) + def Powered(self) -> "b": + return True + + @Powered.setter + def Powered(self, value: "b"): + LOG.debug(f"Trying to set Powered to {value}") + + async def _update_discoverying(self, new_value: bool): + await asyncio.sleep(random.uniform(0.5, 1.5)) + self._discovering = new_value + self.emit_properties_changed({"Discovering": self._discovering}) + LOG.debug(f"Discovering changed: {self._discovering}") diff --git a/core/tools/bluez_emu_bridge/dbus-daemon.conf b/core/tools/bluez_emu_bridge/dbus-daemon.conf new file mode 100644 index 0000000000..ff17446867 --- /dev/null +++ b/core/tools/bluez_emu_bridge/dbus-daemon.conf @@ -0,0 +1,29 @@ + + + + + session + + + unix:path=/tmp/dbus-bluez-emu-bridge + + + EXTERNAL + + + + + + + + + + + diff --git a/core/tools/bluez_emu_bridge/device1.py b/core/tools/bluez_emu_bridge/device1.py new file mode 100644 index 0000000000..44b6e129cf --- /dev/null +++ b/core/tools/bluez_emu_bridge/device1.py @@ -0,0 +1,202 @@ +import asyncio +import logging +import random + +from dbus_next import Variant +from dbus_next.service import PropertyAccess, ServiceInterface, dbus_property, method + +from trezorlib.transport.emu_ble import Command, CommandType, Event, EventType + +LOG = logging.getLogger(__name__) + + +class Device1(ServiceInterface): + def __init__(self, bus, parent_path, mac_address="00:00:00:00:00:00"): + self.bus = bus + self.path = f"{parent_path}/dev_{'_'.join(mac_address.split(':'))}" + super().__init__("org.bluez.Device1") + self._exported = False + self._connected = False + self._paired = False + self._pairing_result = asyncio.Queue(1) + self._services_resolved = False + self._rssi = -66 + self._address = mac_address + self._name = "Trezor Emulator" # Suite looks for Trezor prefix + self._services = [] + + self.send_event_fn = None + self.command_queue = None + + self.__task_scanning_active = False + + async def export(self): + if not self._exported: + self._exported = True + await asyncio.sleep(random.uniform(0.5, 1.5)) + self.bus.export(f"/org/bluez/{self.path}", self) + + def add_service(self, service): + self._services.append(service) + + async def task_scanning_start(self): + await self.export() + self.__task_scanning_active = True + asyncio.create_task(self._task_scanning_run()) + + def task_scanning_stop(self): + self.__task_scanning_active = False + + async def _task_scanning_run(self): + await asyncio.sleep(random.uniform(0.02, 0.2)) + # Execute scanning tasks + await self._update_rssi(random.uniform(-90, -60)) + if self.__task_scanning_active: + asyncio.create_task(self._task_scanning_run()) + + @method() + async def Connect(self): + LOG.debug("Connect") + await self.do_connect() + + async def do_connect(self): + if not self._connected: + self.send_event(Event.new(event_type=EventType.CONNECTED)) + await self._update_connected(True) + for service in self._services: + service.export() + await self._update_services_resolved(True) + + @method() + async def Disconnect(self): + LOG.debug("Disconnect") + await self.do_disconnect() + + async def do_disconnect(self): + if self._connected: + self.send_event(Event.new(event_type=EventType.DISCONNECTED)) + await self._update_services_resolved(False) + await self._update_connected(False) + + @method() + async def Pair(self): + LOG.debug("Pair") + if not self._connected: + await self.do_connect() + + if not self._paired: + self.send_event(Event.new(event_type=EventType.PAIRING_REQUEST, data=b"999999")) + is_paired_now = await self._pairing_result.get() + await self._update_paired(is_paired_now) + if not is_paired_now: + await self.do_disconnect() + + @method() + async def CancelPairing(self): + LOG.debug("CancelPairing") + self.send_event(Event.new(event_type=EventType.PAIRING_CANCELLED)) + + @dbus_property(access=PropertyAccess.READ) + def ManufacturerData(self) -> "a{qv}": + return {65535: Variant("ay", b"\x01\x00\x54\x32\x57\x31")} + + @dbus_property(access=PropertyAccess.READ) + def Connected(self) -> "b": + return self._connected + + @dbus_property(access=PropertyAccess.READ) + def ServicesResolved(self) -> "b": + return self._services_resolved + + @dbus_property(access=PropertyAccess.READ) + def RSSI(self) -> "n": + return self._rssi + + @dbus_property(access=PropertyAccess.READ) + def Name(self) -> "s": + return self._name + + @dbus_property(access=PropertyAccess.READ) + def Address(self) -> "s": + return self._address + + @dbus_property(access=PropertyAccess.READ) + def UUIDs(self) -> "as": + uuids = [] + for srv in self._services: + uuids.append(srv._uuid) + for chr in srv._characteristics: + uuids.append(chr._uuid) + return uuids + + @dbus_property(access=PropertyAccess.READ) + def AddressType(self) -> "s": + return "random" + + @dbus_property(access=PropertyAccess.READ) + def Paired(self) -> "b": + return self._paired + + @dbus_property(access=PropertyAccess.READ) + def Trusted(self) -> "b": + return True + + @dbus_property(access=PropertyAccess.READ) + def Blocked(self) -> "b": + return False + + @dbus_property(access=PropertyAccess.READ) + def LegacyPairing(self) -> "b": + return False + + async def _update_connected(self, new_value: bool): + await asyncio.sleep(random.uniform(0.5, 1.5)) + self._connected = new_value + property_changed = {"Connected": self._connected} + self.emit_properties_changed(property_changed) + LOG.debug(f"Property changed: {property_changed}") + + async def _update_services_resolved(self, new_value: bool): + await asyncio.sleep(random.uniform(0.0, 0.5)) + self._services_resolved = new_value + property_changed = {"ServicesResolved": self._services_resolved} + self.emit_properties_changed(property_changed) + LOG.debug(f"Property changed: {property_changed}") + + async def _update_paired(self, new_value: bool): + # TODO return if not changed? + self._paired = new_value + property_changed = {"Paired": self._paired} + self.emit_properties_changed(property_changed) + LOG.debug(f"Property changed: {property_changed}") + + async def _update_rssi(self, new_value: int): + return # FIXME skip + self._rssi = int(new_value) + property_changed = {"RSSI": self._rssi} + self.emit_properties_changed(property_changed) + + async def connection_state_task(self, write_fn, queue): + self.send_event_fn = write_fn + self.command_queue = queue + self.send_event(Event.ping()) + while True: + command = await queue.get() + command = Command.parse(command) + LOG.debug(f"Emulator sent command: {command}") + t = command.command_type + if t == CommandType.ALLOW_PAIRING: + self._pairing_result.put_nowait(True) + elif t == CommandType.REJECT_PAIRING: + self._pairing_result.put_nowait(False) + elif t == CommandType.EMULATOR_PONG: + LOG.info("Emulator pong") + else: + LOG.error(f"Command not implemented: {command}") + + def send_event(self, event): + if self.send_event_fn is None: + LOG.error(f"Cannot send event {event}") + else: + LOG.debug(f"Sending event {event}") + self.send_event_fn(event.build()) diff --git a/core/tools/bluez_emu_bridge/gattcharacteristic1.py b/core/tools/bluez_emu_bridge/gattcharacteristic1.py new file mode 100644 index 0000000000..f2de3ea4a6 --- /dev/null +++ b/core/tools/bluez_emu_bridge/gattcharacteristic1.py @@ -0,0 +1,93 @@ +import asyncio +import logging +import random + +from dbus_next.service import PropertyAccess, ServiceInterface, dbus_property, method + +LOG = logging.getLogger(__name__) + + +class GattCharacteristic1(ServiceInterface): + def __init__(self, bus, parent_path, id_num, uuid, flags=None): + self.bus = bus + self.path = f"{parent_path}/char{id_num:04x}" + super().__init__("org.bluez.GattCharacteristic1") + self._service = f"/org/bluez/{parent_path}" + self._uuid = uuid + self._value = bytes() + self._flags = flags if flags is not None else [] + self._notifying = False + self._exported = False + self.send_value = None + + def export(self): + if not self._exported: + self.bus.export(f"/org/bluez/{self.path}", self) + self._exported = True + + def update_value(self, new_value: bytes): + self._update_value(new_value) + + @method() + async def StartNotify(self): + LOG.debug(f"{self.path}: StartNotify") + await self._update_notifying(True) + + @method() + async def StopNotify(self): + LOG.debug(f"{self.path}: StartNotify") + await self._update_notifying(False) + + @method() + def ReadValue(self, options: "a{sv}") -> "ay": + LOG.debug(f"{self.path}: ReadValue (len={len(self._value)})") + return self._value + + @method() + def WriteValue(self, value: "ay", options: "a{sv}"): + LOG.debug(f"{self.path}: WriteValue (len={len(value)})") + if not self.send_value: + self._update_value(value) + else: + self.send_value(value) + + # TODO: AcquireWrite, AcquireNotify for trezorlib+tealblue + + @dbus_property(access=PropertyAccess.READ) + def Notifying(self) -> "b": + return self._notifying + + @dbus_property(access=PropertyAccess.READ) + def UUID(self) -> "s": + return self._uuid + + @dbus_property(access=PropertyAccess.READ) + def Value(self) -> "ay": + return self._value + + @dbus_property(access=PropertyAccess.READ) + def Flags(self) -> "as": + return self._flags + + @dbus_property(access=PropertyAccess.READ) + def Service(self) -> "s": + return self._service + + def _update_value(self, new_value: bytes): + self._value = new_value + if self._notifying: + property_changed = {"Value": self._value} + self.emit_properties_changed(property_changed) + + async def _update_notifying(self, new_value: bool): + # await asyncio.sleep(random.uniform(0.0, 0.2)) + self._notifying = new_value + property_changed = {"Notifying": self._notifying} + self.emit_properties_changed(property_changed) + + async def update_from_queue(self, queue): + while True: + val = await queue.get() + if not self._notifying: + LOG.warning("Got message from emulator while Notifying=false") + self.update_value(val) diff --git a/core/tools/bluez_emu_bridge/gattservice1.py b/core/tools/bluez_emu_bridge/gattservice1.py new file mode 100644 index 0000000000..c8f15ee3bf --- /dev/null +++ b/core/tools/bluez_emu_bridge/gattservice1.py @@ -0,0 +1,29 @@ +from dbus_next.service import PropertyAccess, ServiceInterface, dbus_property + + +class GattService1(ServiceInterface): + def __init__(self, bus, parent_path, id_num, uuid): + self.bus = bus + self.path = f"{parent_path}/service{id_num:04x}" + super().__init__("org.bluez.GattService1") + self._uuid = uuid + self._exported = False + self._characteristics = [] + + def export(self): + if not self._exported: + self.bus.export(f"/org/bluez/{self.path}", self) + for char in self._characteristics: + char.export() + self._exported = True + + def add_characteristic(self, characteristic): + self._characteristics.append(characteristic) + + @dbus_property(access=PropertyAccess.READ) + def UUID(self) -> "s": + return self._uuid + + @dbus_property(access=PropertyAccess.READ) + def Primary(self) -> "b": + return True diff --git a/core/tools/bluez_emu_bridge/message_bus.py b/core/tools/bluez_emu_bridge/message_bus.py new file mode 100644 index 0000000000..d990511b58 --- /dev/null +++ b/core/tools/bluez_emu_bridge/message_bus.py @@ -0,0 +1,49 @@ +import logging + +from dbus_next import aio +from dbus_next.message import Message +from dbus_next.service import ServiceInterface + +LOG = logging.getLogger(__name__) + + +class MessageBus(aio.MessageBus): + def _emit_interface_added(self, path: str, interface: str) -> None: + if self._disconnected: + return + + def get_properties_callback(interface, result, user_data, e): + if e is not None: + try: + raise e + except Exception: + logging.error( + "An exception ocurred when emitting ObjectManager.InterfacesAdded for %s. " + "Some properties will not be included in the signal.", + interface.name, + exc_info=True, + ) + + body = {interface.name: result} + + # BlueZ's InterfacesAdded signal has different path in the message body and + # in the metadata. However with dbus-next they are always the same, and such + # signal will get ignored by btleplug and other BlueZ clients. Patch it here. + envelope_path = path + if "/dev_" in envelope_path: + envelope_path = "/" + LOG.debug( + f"InterfacesAdded: replacing path {path} with {envelope_path}" + ) + + self.send( + Message.new_signal( + path=envelope_path, + interface="org.freedesktop.DBus.ObjectManager", + member="InterfacesAdded", + signature="oa{sa{sv}}", + body=[path, body], + ) + ) + + ServiceInterface._get_all_property_values(interface, get_properties_callback) diff --git a/poetry.lock b/poetry.lock index e9cfc6be08..e1a75f30ab 100644 --- a/poetry.lock +++ b/poetry.lock @@ -402,6 +402,18 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "dbus-next" +version = "0.2.3" +description = "A zero-dependency DBus library for Python with asyncio support" +category = "main" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b"}, + {file = "dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5"}, +] + [[package]] name = "demjson3" version = "3.0.5" @@ -2342,4 +2354,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "3cf7446762b2697275d563511071e6368edafb8c86eeb88a402bbe7bfb9ba6ac" +content-hash = "4c701042885cd1939fda08e842ddd65a0d44fca5c7ad82cc373fb05f40b0e8d1" diff --git a/pyproject.toml b/pyproject.toml index 0d893c9769..9852129d22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ trezor-core-tools = {path = "./core/tools", develop = true} flake8-annotations = "^3.1.1" pyelftools = "^0.32" pytest-retry = "^1.7.0" +dbus-next = "^0.2.3" [tool.poetry.dev-dependencies] scan-build = "*"