What changed, and why it matters
This commit adds a new developer/testing tool that creates a fake Bluetooth (BlueZ) interface on a developer's computer. It lets software talk to a Trezor emulator as if the emulator were a real Bluetooth device. It is not a fix for a security issue and does not change any production firmware, wallet, or device code. It is purely a testing/development helper.
No security action required. Review as normal tooling code if desired; ensure it is not shipped as part of production firmware builds.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces core/tools/bluez-emu-bridge.py and the bluez_emu_bridge package. The tool starts a private D-Bus daemon, exposes mock org.bluez.Adapter1/Device1/GattService1/GattCharacteristic1 objects via dbus-fast, and bridges BLE reads/writes/notifications to a Trezor emulator over UDP. It is based on the third-party python_bluez_dbus_emulator project (MIT licensed). No production firmware, bootloader, crypto, transport, or UI code is modified.
Changed components
core/tools/bluez-emu-bridge.pycore/tools/bluez_emu_bridge/*Inspect captured patch +930 / −0
diff --git a/core/tools/bluez-emu-bridge.py b/core/tools/bluez-emu-bridge.py
new file mode 100755
index 000000000..c60cb0109
--- /dev/null
+++ b/core/tools/bluez-emu-bridge.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python3
+"""
+The purpose of this script is to create a mock D-Bus API of BlueZ, the Linux Bluetooth protocol
+stack. Using environment variables you can trick programs to use this API instead of the system
+one to talk to Trezor emulator as if it was a BLE device.
+
+BlueZ API docs: https://github.com/bluez/bluez/tree/master/doc
+D-Bus: https://www.freedesktop.org/wiki/Software/dbus/
+Debugger: https://apps.gnome.org/en-GB/Dspy/
+Sniffer: https://dbus.freedesktop.org/doc/dbus-monitor.1.html
+Based on: https://github.com/simpleble/python_bluez_dbus_emulator
+"""
+
+import asyncio
+import atexit
+import logging
+import subprocess
+from pathlib import Path
+
+import click
+from bluez_emu_bridge import MessageBus # normally lives in dbus_fast.aio
+from bluez_emu_bridge import Adapter1, Device1, GattCharacteristic1, GattService1
+from typing_extensions import Self
+
+from trezorlib._internal.emu_ble import Event
+from trezorlib.transport.ble import (
+ TREZOR_CHARACTERISTIC_RX,
+ TREZOR_CHARACTERISTIC_TX,
+ TREZOR_SERVICE_UUID,
+)
+
+HERE = Path(__file__).parent.resolve()
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s %(levelname)s %(name)s %(message)s",
+ handlers=[logging.StreamHandler()],
+)
+LOG = logging.getLogger(__name__)
+
+
+class TrezorUDP(asyncio.DatagramProtocol):
+ @classmethod
+ async def create(cls, ip, port) -> Self:
+ 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) -> str:
+ 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: bytes, addr):
+ if addr != self.addr:
+ LOG.error(f"{self.ipport()} Stray datagram from {addr}?")
+ return
+ 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
+ self.transport.sendto(value)
+
+ def close(self):
+ if self.transport:
+ self.transport.close()
+ self.transport = None
+ self.queue.shutdown()
+
+
+class TrezorEmulator:
+ def __init__(
+ self,
+ data_transport,
+ data_protocol,
+ data_read_task,
+ event_transport,
+ event_protocol,
+ event_read_task,
+ ):
+ self._data_transport = data_transport
+ self.data_protocol = data_protocol
+ self.data_read_task = data_read_task
+ self._event_transport = event_transport
+ self.event_protocol = event_protocol
+ self.event_read_task = event_read_task
+
+ def close(self):
+ self.data_transport.close()
+ self.event_transport.close()
+
+ @classmethod
+ async def create(
+ cls,
+ emulator_port: int,
+ device: Device1,
+ char_tx: GattCharacteristic1,
+ char_rx: GattCharacteristic1,
+ ) -> Self:
+ 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)
+ )
+
+ event_transport, event_protocol = await TrezorUDP.create(
+ localhost, emulator_port + 1
+ )
+ event_read_task = asyncio.create_task(
+ device.connection_state_task(event_protocol.write, event_protocol.queue)
+ )
+ obj = cls(
+ data_transport,
+ data_protocol,
+ data_read_task,
+ event_transport,
+ event_protocol,
+ event_read_task,
+ )
+ # Ping the emulator so that it knows our UDP port and sends us the current state.
+ # NOTE: Assumes emulator is running, othewise a loop is needed.
+ 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)
+ service = GattService1(bus, device.path, 0, TREZOR_SERVICE_UUID)
+ char_tx = GattCharacteristic1(
+ bus, service.path, 0, TREZOR_CHARACTERISTIC_TX, flags=["read", "notify"]
+ )
+ char_rx = GattCharacteristic1(
+ bus,
+ service.path,
+ 1,
+ TREZOR_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()
+
+ emulator = await TrezorEmulator.create(emulator_port, device, char_tx, char_rx)
+
+ await bus.request_name("org.bluez")
+ await bus.wait_for_disconnect()
+ emulator.close()
+ LOG.info("End emulator_main")
+
+
+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.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 000000000..b12c36057
--- /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 000000000..a3fc7424a
--- /dev/null
+++ b/core/tools/bluez_emu_bridge/README.md
@@ -0,0 +1,37 @@
+# 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)).
+Based on commit 3a767035bf64faa1beb637100e3159b29a1392bf.
+
+Original README is reproduced below this line.
+
+# 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 000000000..9e9b43836
--- /dev/null
+++ b/core/tools/bluez_emu_bridge/__init__.py
@@ -0,0 +1,7 @@
+# flake8: noqa: F401
+
+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 000000000..ad640dca3
--- /dev/null
+++ b/core/tools/bluez_emu_bridge/adapter1.py
@@ -0,0 +1,112 @@
+# flake8: noqa: F722, F821
+
+import asyncio
+import logging
+import random
+
+from dbus_fast.service import PropertyAccess, ServiceInterface, dbus_property, method
+
+LOG = logging.getLogger(__name__)
+
+
+class Adapter1(ServiceInterface):
+ def __init__(self, bus, path, address="21:00:00:00:13:37"):
+ self.bus = bus
+ self.path = f"/org/bluez/{path}"
+ super().__init__("org.bluez.Adapter1")
+ self.address = address
+
+ self._discovering = False
+ self._devices = []
+
+ def export(self):
+ self.bus.export(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("dbus: StartDiscovery")
+ await self._update_discovering(True)
+ for device in self._devices:
+ await device.task_scanning_start()
+ return
+
+ @method()
+ async def StopDiscovery(self):
+ LOG.debug("dbus: StopDiscovery")
+ await self._update_discovering(False)
+ for device in self._devices:
+ device.task_scanning_stop()
+ return
+
+ @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 Alias(self) -> "s":
+ return "fake-ble-adapter-4real"
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Class(self) -> "u":
+ return 8126732
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Discoverable(self) -> "b":
+ return True
+
+ @dbus_property(access=PropertyAccess.READ)
+ def DiscoverableTimeout(self) -> "u":
+ return 180
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Discovering(self) -> "b":
+ return self._discovering
+
+ @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 Pairable(self) -> "b":
+ return True
+
+ @dbus_property(access=PropertyAccess.READ)
+ def PairableTimeout(self) -> "u":
+ return 0
+
+ @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}")
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Roles(self) -> "as":
+ return ["central", "peripheral"]
+
+ @dbus_property(access=PropertyAccess.READ)
+ def UUIDs(self) -> "as":
+ return []
+
+ async def _update_discovering(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 000000000..ff1744686
--- /dev/null
+++ b/core/tools/bluez_emu_bridge/dbus-daemon.conf
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE busconfig SYSTEM "busconfig.dtd">
+<busconfig>
+ <!-- Our well-known bus type, don't change this -->
+ <type>session</type>
+
+ <!-- <listen>unix:tmpdir=/tmp</listen> -->
+ <listen>unix:path=/tmp/dbus-bluez-emu-bridge</listen>
+
+ <!-- On Unix systems, the most secure authentication mechanism is
+ EXTERNAL, which uses credential-passing over Unix sockets.
+
+ This authentication mechanism is not available on Windows,
+ is not suitable for use with the tcp: or nonce-tcp: transports,
+ and will not work on obscure flavours of Unix that do not have
+ a supported credentials-passing mechanism. On those platforms/transports,
+ comment out the <auth> element to allow fallback to DBUS_COOKIE_SHA1. -->
+ <auth>EXTERNAL</auth>
+ <allow_anonymous/>
+
+ <policy context="default">
+ <!-- Allow everything to be sent -->
+ <allow send_destination="*" eavesdrop="true"/>
+ <!-- Allow everything to be received -->
+ <allow eavesdrop="true"/>
+ <!-- Allow anyone to own anything -->
+ <allow own="*"/>
+ </policy>
+</busconfig>
diff --git a/core/tools/bluez_emu_bridge/device1.py b/core/tools/bluez_emu_bridge/device1.py
new file mode 100644
index 000000000..2740c4b86
--- /dev/null
+++ b/core/tools/bluez_emu_bridge/device1.py
@@ -0,0 +1,319 @@
+# flake8: noqa: F722, F821
+
+import asyncio
+import logging
+import random
+
+from dbus_fast import DBusError, Variant
+from dbus_fast.service import PropertyAccess, ServiceInterface, dbus_property, method
+
+from trezorlib._internal.emu_ble import Command, CommandType, Event, EventType, ModeType
+
+LOG = logging.getLogger(__name__)
+PAIRING_TIMEOUT_SEC = 10
+
+
+def mac2bytes(mac_str):
+ return bytes.fromhex(mac_str.replace(":", ""))
+
+
+def bytes2mac(mac_bytes):
+ ":".join(map(hex, mac_bytes))
+
+
+class Device1(ServiceInterface):
+ def __init__(self, bus, adapter, mac_address="e1:e2:e3:e4:e5:e6"):
+ self.bus = bus
+ self.adapter = adapter.path
+ self.adapter_mac = adapter.address
+ self.path = f"{self.adapter}/dev_{'_'.join(mac_address.split(':'))}"
+ super().__init__("org.bluez.Device1")
+ self._exported = False
+
+ # controlled by emulator
+ self._mode = None
+ self._connected = False
+ self._bonds = []
+ self._name = "Not set yet"
+
+ self._pairing_result = asyncio.Queue(1) # XXX always replace?
+ self._services_resolved = False
+ self._rssi = -66
+ self._address = mac_address
+ self._services = []
+
+ self.send_event_fn = None
+ self.command_queue = None
+
+ self.__task_scanning_active = False
+
+ def is_bonded(self):
+ return self.adapter_mac in self._bonds
+
+ def is_pairing(self):
+ return self._mode == ModeType.PAIRING
+
+ def is_visible(self):
+ is_connectable = self._mode == ModeType.CONNECTABLE
+ return self.is_pairing() or (is_connectable and self.is_bonded())
+
+ async def export(self):
+ if not self._exported:
+ self._exported = True
+ await asyncio.sleep(random.uniform(0.5, 1.5))
+ self.bus.export(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))
+ if self.is_visible():
+ # We need to emit PropertyChanged signal for (at least) bleak to see the device.
+ # Like random RSSI.
+ 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("dbus: Connect")
+ await self.do_connect()
+
+ async def do_connect(self):
+ if not self._connected:
+ self.send_event(
+ Event.new(
+ event_type=EventType.CONNECTED,
+ data=mac2bytes(self.adapter_mac),
+ )
+ )
+ 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("dbus: 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) # not sure
+ await self._update_connected(False)
+
+ @method()
+ async def Pair(self):
+ LOG.debug("dbus: Pair")
+ if not self._connected:
+ await self.do_connect()
+
+ if self.is_bonded():
+ return
+
+ if not self.is_pairing():
+ raise DBusError("org.bluez.Device1", "not in pairing mode")
+
+ self.send_event(Event.new(event_type=EventType.PAIRING_REQUEST, data=b"999999"))
+ try:
+ is_paired_now = await asyncio.wait_for(
+ self._pairing_result.get(), PAIRING_TIMEOUT_SEC
+ )
+ except asyncio.TimeoutError:
+ LOG.error("Timed out waiting for Trezor to accept")
+ self.send_event(Event.new(event_type=EventType.PAIRING_CANCELLED))
+ await self.do_disconnect()
+ # TODO: check which error bluez actually returns
+ raise DBusError("org.bluez.Device1", "Timed out waiting for peripheral")
+
+ await self._update_paired(is_paired_now)
+ if is_paired_now:
+ self.send_event(Event.new(event_type=EventType.PAIRING_COMPLETED))
+ # we should receive updated bonds afterwards
+ else:
+ await self.do_disconnect()
+
+ @method()
+ async def CancelPairing(self):
+ LOG.debug("dbus: CancelPairing")
+ self.send_event(Event.new(event_type=EventType.PAIRING_CANCELLED))
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Adapter(self) -> "o":
+ return self.adapter
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Address(self) -> "s":
+ return self._address
+
+ @dbus_property(access=PropertyAccess.READ)
+ def AddressType(self) -> "s":
+ return "random"
+
+ @dbus_property(access=PropertyAccess.READ)
+ def AdvertisingFlags(self) -> "ay":
+ return b"\x06"
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Alias(self) -> "s":
+ return self._name
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Appearance(self) -> "q":
+ return 128
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Bonded(self) -> "b":
+ return self.is_bonded()
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Blocked(self) -> "b":
+ return False
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Connected(self) -> "b":
+ return self._connected
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Icon(self) -> "s":
+ return "computer"
+
+ @dbus_property(access=PropertyAccess.READ)
+ def LegacyPairing(self) -> "b":
+ return False
+
+ @dbus_property(access=PropertyAccess.READ)
+ def ManufacturerData(self) -> "a{qv}":
+ # 0xf29
+ return {3881: Variant("ay", b"\x01\x00\x06\x00\x00\x00")}
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Modalias(self) -> "s":
+ return "usb:v1D6Bp0246d054F"
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Name(self) -> "s":
+ return self._name
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Paired(self) -> "b":
+ return self.is_bonded()
+
+ @dbus_property(access=PropertyAccess.READ)
+ def RSSI(self) -> "n":
+ return self._rssi
+
+ @dbus_property(access=PropertyAccess.READ)
+ def ServicesResolved(self) -> "b":
+ return self._services_resolved
+
+ @dbus_property(access=PropertyAccess.READWRITE)
+ def Trusted(self) -> "b":
+ return True
+
+ @Trusted.setter
+ def Trusted(self, value: "b"):
+ LOG.debug(f"Trying to set Trusted to {value}")
+
+ @dbus_property(access=PropertyAccess.READ)
+ def TxPower(self) -> "n":
+ return 7
+
+ @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
+
+ async def _update_connected(self, new_value: bool):
+ await asyncio.sleep(random.uniform(0.5, 1.5))
+ property_changed = {"Connected": new_value}
+ 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):
+ property_changed = {"Paired": new_value}
+ self.emit_properties_changed(property_changed)
+ LOG.debug(f"Property changed: {property_changed}")
+
+ async def _update_rssi(self, new_value: int):
+ 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()
+ LOG.debug(f"Emulator sent command: {command}")
+ await self.handle_command(command)
+
+ async def handle_command(self, command):
+ # handle new status from mock driver
+ command = Command.parse(command)
+
+ LOG.debug(f"Command {command}")
+ t = command.command_type
+ if t == CommandType.STATUS:
+ pass
+ elif t == CommandType.PAIRING_MODE:
+ pass
+ elif t == CommandType.DISCONNECT:
+ await self.do_disconnect()
+ elif t == CommandType.ALLOW_PAIRING:
+ self._pairing_result.put_nowait(True)
+ elif t == CommandType.REJECT_PAIRING:
+ self._pairing_result.put_nowait(False)
+ else:
+ LOG.error(f"Command not implemented: {command}")
+
+ m = command.mode
+ if m == ModeType.PAIRING:
+ pass
+ # TODO emit property changed?
+ elif m == ModeType.DFU:
+ LOG.error("DFU mode not implemented")
+
+ if m != self._mode:
+ LOG.debug(f"Mode {self._mode} -> {m}")
+ self._mode = m
+
+ name = command.adv_name.rstrip(b"\x00").decode()
+ if name:
+ self._name = name
+ LOG.debug(f"Changed advertising name to {name}")
+ self._bonds = [bytes2mac(b) for b in command.bonds]
+
+ connected = command.connected
+ if connected != self._connected:
+ LOG.debug(f"Connected {self._connected} -> {connected}")
+ self._connected = bool(connected)
+
+ 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 000000000..db07be5c3
--- /dev/null
+++ b/core/tools/bluez_emu_bridge/gattcharacteristic1.py
@@ -0,0 +1,92 @@
+# flake8: noqa: F722, F821
+
+import logging
+
+from dbus_fast.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 = 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(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)
+
+ # unused, we're using notifications instead
+ @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)
+
+ @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) -> "o":
+ 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 000000000..bb85fae1a
--- /dev/null
+++ b/core/tools/bluez_emu_bridge/gattservice1.py
@@ -0,0 +1,40 @@
+# flake8: noqa: F722, F821
+
+from dbus_fast.service import PropertyAccess, ServiceInterface, dbus_property
+
+
+class GattService1(ServiceInterface):
+ def __init__(self, bus, parent_path, id_num, uuid):
+ self.bus = bus
+ self.parent_path = parent_path
+ 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(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 Device(self) -> "o":
+ return self.parent_path
+
+ @dbus_property(access=PropertyAccess.READ)
+ def UUID(self) -> "s":
+ return self._uuid
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Primary(self) -> "b":
+ return True
+
+ @dbus_property(access=PropertyAccess.READ)
+ def Includes(self) -> "as":
+ return []
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 000000000..4100108ae
--- /dev/null
+++ b/core/tools/bluez_emu_bridge/message_bus.py
@@ -0,0 +1,49 @@
+import logging
+
+from dbus_fast import aio
+from dbus_fast.message import Message
+from dbus_fast.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-fast 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)
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.