mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-04-26 03:59:08 +00:00
feat(core/tools): BlueZ-emulator bridge
[no changelog]
This commit is contained in:
parent
47c673f875
commit
cb3d68be02
197
core/tools/bluez-emu-bridge.py
Executable file
197
core/tools/bluez-emu-bridge.py
Executable file
@ -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()
|
21
core/tools/bluez_emu_bridge/LICENSE
Normal file
21
core/tools/bluez_emu_bridge/LICENSE
Normal file
@ -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.
|
36
core/tools/bluez_emu_bridge/README.md
Normal file
36
core/tools/bluez_emu_bridge/README.md
Normal file
@ -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).
|
5
core/tools/bluez_emu_bridge/__init__.py
Normal file
5
core/tools/bluez_emu_bridge/__init__.py
Normal file
@ -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
|
82
core/tools/bluez_emu_bridge/adapter1.py
Normal file
82
core/tools/bluez_emu_bridge/adapter1.py
Normal file
@ -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}")
|
29
core/tools/bluez_emu_bridge/dbus-daemon.conf
Normal file
29
core/tools/bluez_emu_bridge/dbus-daemon.conf
Normal file
@ -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>
|
202
core/tools/bluez_emu_bridge/device1.py
Normal file
202
core/tools/bluez_emu_bridge/device1.py
Normal file
@ -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())
|
93
core/tools/bluez_emu_bridge/gattcharacteristic1.py
Normal file
93
core/tools/bluez_emu_bridge/gattcharacteristic1.py
Normal file
@ -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)
|
29
core/tools/bluez_emu_bridge/gattservice1.py
Normal file
29
core/tools/bluez_emu_bridge/gattservice1.py
Normal file
@ -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
|
49
core/tools/bluez_emu_bridge/message_bus.py
Normal file
49
core/tools/bluez_emu_bridge/message_bus.py
Normal file
@ -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)
|
14
poetry.lock
generated
14
poetry.lock
generated
@ -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"
|
||||
|
@ -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 = "*"
|
||||
|
Loading…
Reference in New Issue
Block a user