mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-04-26 20:19:02 +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 = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||||
test-randomorder = ["pytest-randomly"]
|
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]]
|
[[package]]
|
||||||
name = "demjson3"
|
name = "demjson3"
|
||||||
version = "3.0.5"
|
version = "3.0.5"
|
||||||
@ -2342,4 +2354,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.9"
|
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"
|
flake8-annotations = "^3.1.1"
|
||||||
pyelftools = "^0.32"
|
pyelftools = "^0.32"
|
||||||
pytest-retry = "^1.7.0"
|
pytest-retry = "^1.7.0"
|
||||||
|
dbus-next = "^0.2.3"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
scan-build = "*"
|
scan-build = "*"
|
||||||
|
Loading…
Reference in New Issue
Block a user