What changed, and why it matters
This commit adds a software-only Bluetooth Low Energy (BLE) emulator for Trezor hardware wallets. It is a testing/development feature that lets a desktop emulator simulate BLE connections over local UDP network sockets. It does not change real hardware behavior and is not a security fix or a disclosed vulnerability. The main risk is that any emulator-only code could, in theory, contain bugs that affect testing environments, but there is no direct evidence this introduces an exploitable flaw in production devices.
No immediate action required for production security. If reviewing for test-harness hardening, consider adding input validation for received UDP packets, binding only to loopback unless explicitly configured otherwise, and documenting that the emulator BLE channel is unauthenticated and should not be exposed to untrusted networks.
Security signals we found
New network-facing code in emulator build only (UDP sockets bound to loopback by default, configurable via TREZOR_UDP_IP)
Pairing code is explicitly ignored in emulated ble_allow_pairing (documented as NOTE: pairing code ignored)
No input length validation on received ble_event_t beyond a simple size check
No authentication or encryption on the UDP control/data channels
Code is not present in production hardware builds
Evidence from the diff
The patch replaces stub BLE functions in the Unix emulator build with a working UDP-based BLE simulation. It adds a C driver (core/embed/io/ble/unix/ble.c) that binds two UDP sockets (data and events) and exchanges structured packets with a new Python helper (python/src/trezorlib/_internal/emu_ble.py). The emulator now initializes BLE during startup when USE_BLE is defined, and test port allocation is widened from 3 to 6 consecutive ports per emulator instance. The code is guarded by TREZOR_EMULATOR/USE_BLE preprocessor flags and is not compiled into production firmware.
Changed components
core/embed/io/ble/unix/ble.ccore/embed/io/ble/inc/io/ble.hcore/embed/projects/unix/main.cpython/src/trezorlib/_internal/emu_ble.pytests/emulators.pyInspect captured patch +868 / −29
diff --git a/core/embed/io/ble/inc/io/ble.h b/core/embed/io/ble/inc/io/ble.h
index 17e8145dd..daf89df41 100644
--- a/core/embed/io/ble/inc/io/ble.h
+++ b/core/embed/io/ble/inc/io/ble.h
@@ -107,6 +107,9 @@ typedef enum {
BLE_PAIRING_NOT_NEEDED = 6, /**< Pairing is not needed */
BLE_CONNECTION_CHANGED =
7, /**< Connection change (e.g. different device connected) */
+#ifdef TREZOR_EMULATOR
+ BLE_EMULATOR_PING = 255, /**< Ping request, emulator only */
+#endif
} ble_event_type_t;
/**
diff --git a/core/embed/io/ble/unix/ble.c b/core/embed/io/ble/unix/ble.c
index be8b583a2..b58770a8f 100644
--- a/core/embed/io/ble/unix/ble.c
+++ b/core/embed/io/ble/unix/ble.c
@@ -1,66 +1,613 @@
#include <io/ble.h>
+#include <sys/sysevent_source.h>
#include <trezor_rtl.h>
-bool ble_init(void) { return true; }
+#include <arpa/inet.h>
+#include <stdlib.h>
+#include <sys/poll.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <unistd.h>
-void ble_deinit(void) {}
+static const uint16_t DATA_PORT_OFFSET = 4; // see usb_config.c
+static const uint16_t EVENT_PORT_OFFSET = 5;
-void ble_start(void) {}
+typedef struct {
+ ble_mode_t mode_current;
+ bool initialized;
+ bool enabled;
+ bool pairing_requested;
+ uint8_t adv_name[BLE_ADV_NAME_LEN];
+ bool connected;
+ bt_le_addr_t connected_addr;
+ bt_le_addr_t bonds[BLE_MAX_BONDS];
+ size_t bonds_len;
-void ble_stop(void) {}
+ uint16_t data_port;
+ int data_sock;
+ struct sockaddr_in data_si_me, data_si_other;
+ socklen_t data_slen;
-bool ble_switch_off(void) { return true; }
+ uint16_t event_port;
+ int event_sock;
+ struct sockaddr_in event_si_me, event_si_other;
+ socklen_t event_slen;
+} ble_driver_t;
-bool ble_switch_on(void) { return true; }
+typedef struct {
+ uint8_t cmd;
+ uint8_t mode;
+ uint8_t connected;
+ uint8_t adv_name[BLE_ADV_NAME_LEN];
+ uint8_t bonds_len;
+ uint8_t bonds[6 * BLE_MAX_BONDS];
+} emu_cmd_t;
+
+static ble_driver_t g_ble_driver = {0};
+
+static const syshandle_vmt_t ble_handle_vmt;
+static const syshandle_vmt_t ble_iface_handle_vmt;
+
+static bool bonds_lookup(const ble_driver_t *drv, const bt_le_addr_t *addr,
+ size_t *out_index) {
+ for (size_t i = 0; i < drv->bonds_len; i++) {
+ if (0 == memcmp(&addr->addr, &drv->bonds[i].addr, sizeof(addr->addr))) {
+ if (out_index) {
+ *out_index = i;
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool bonds_add(ble_driver_t *drv, const bt_le_addr_t *addr) {
+ if (bonds_lookup(drv, addr, NULL)) {
+ return true;
+ }
+ size_t len = drv->bonds_len;
+ if (len >= BLE_MAX_BONDS) {
+ return false;
+ }
+ drv->bonds[len] = *addr;
+ drv->bonds_len++;
+ return true;
+}
+
+static void bonds_remove(ble_driver_t *drv, const bt_le_addr_t *addr) {
+ size_t i;
+ bool found = bonds_lookup(drv, addr, &i);
+ if (!found) {
+ return;
+ }
+ size_t last = drv->bonds_len - 1;
+ if (i != last) {
+ drv->bonds[i] = drv->bonds[last];
+ }
+ drv->bonds_len--;
+}
+
+static bool is_enabled(const ble_driver_t *drv) {
+ return (drv->initialized && drv->enabled);
+}
+
+bool ble_init(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!syshandle_register(SYSHANDLE_BLE, &ble_handle_vmt, drv)) {
+ goto cleanup;
+ }
+
+ if (!syshandle_register(SYSHANDLE_BLE_IFACE_0, &ble_iface_handle_vmt, drv)) {
+ goto cleanup;
+ }
+ return true;
+
+cleanup:
+ memset(drv, 0, sizeof(ble_driver_t));
+ printf("unix/ble: init failed\n");
+ return false;
+}
+
+void ble_deinit(void) {
+ syshandle_unregister(SYSHANDLE_BLE_IFACE_0);
+ syshandle_unregister(SYSHANDLE_BLE);
+}
+
+void ble_start(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ memset(drv, 0, sizeof(*drv));
+ drv->data_sock = -1;
+ drv->event_sock = -1;
+
+ const char *ip = getenv("TREZOR_UDP_IP");
+ const char *port_base_str = getenv("TREZOR_UDP_PORT");
+ uint16_t port_base = port_base_str ? atoi(port_base_str) : 21324;
+
+ drv->data_port = port_base + DATA_PORT_OFFSET;
+ drv->event_port = port_base + EVENT_PORT_OFFSET;
+ drv->data_sock = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP);
+ drv->event_sock = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP);
+
+ ensure(sectrue * (drv->data_sock >= 0), NULL);
+ ensure(sectrue * (drv->event_sock >= 0), NULL);
+
+ drv->data_si_me.sin_family = drv->event_si_me.sin_family = AF_INET;
+ drv->data_si_me.sin_addr.s_addr = ip ? inet_addr(ip) : htonl(INADDR_LOOPBACK);
+ drv->event_si_me.sin_addr.s_addr =
+ ip ? inet_addr(ip) : htonl(INADDR_LOOPBACK);
+ drv->data_si_me.sin_port = htons(drv->data_port);
+ drv->event_si_me.sin_port = htons(drv->event_port);
+
+ int ret = -1;
+ ret = bind(drv->data_sock, (struct sockaddr *)&(drv->data_si_me),
+ sizeof(struct sockaddr_in));
+ ensure(sectrue * (ret == 0), NULL);
+ ret = bind(drv->event_sock, (struct sockaddr *)&(drv->event_si_me),
+ sizeof(struct sockaddr_in));
+ ensure(sectrue * (ret == 0), NULL);
+
+ drv->initialized = true;
+}
+
+void ble_stop(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!drv->initialized) {
+ return;
+ }
+
+ if (drv->data_sock >= 0) {
+ close(drv->data_sock);
+ drv->data_sock = -1;
+ }
+ if (drv->event_sock >= 0) {
+ close(drv->event_sock);
+ drv->event_sock = -1;
+ }
+ drv->initialized = false;
+}
+
+static bool send_to_emu(char cmdtype) {
+ ble_driver_t *drv = &g_ble_driver;
+ emu_cmd_t command = {
+ .cmd = cmdtype,
+ .mode = drv->mode_current,
+ .connected = drv->connected,
+ .bonds_len = drv->bonds_len,
+ };
+ for (size_t i = 0; i < drv->bonds_len; i++) {
+ memcpy(&command.bonds[6 * i], drv->bonds[i].addr, 6);
+ }
+ memcpy(&command.adv_name, drv->adv_name, BLE_ADV_NAME_LEN);
+
+ ssize_t r = -2;
+ if (drv->event_slen > 0) {
+ r = sendto(drv->event_sock, &command, sizeof(command), MSG_DONTWAIT,
+ (const struct sockaddr *)&(drv->event_si_other),
+ drv->event_slen);
+ }
+ if (r != sizeof(command)) {
+ printf("unix/ble: failed to write command %c: %d\n", cmdtype, (int)r);
+ }
-bool ble_enter_pairing_mode(const uint8_t *name, size_t name_len) {
return true;
}
-bool ble_disconnect(void) { return true; }
+bool ble_switch_off(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return false;
+ }
+ drv->mode_current = BLE_MODE_OFF;
+ drv->connected = false;
+ return send_to_emu(' ');
+}
-bool ble_erase_bonds(void) { return true; }
+bool ble_switch_on(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return false;
+ }
+ if (drv->connected) {
+ drv->mode_current = BLE_MODE_KEEP_CONNECTION;
+ } else {
+ drv->mode_current = BLE_MODE_CONNECTABLE;
+ }
+ return send_to_emu(' ');
+}
-bool ble_allow_pairing(const uint8_t *pairing_code) { return true; }
+bool ble_enter_pairing_mode(const uint8_t *name, size_t name_len) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!drv->initialized) {
+ return false;
+ }
+ drv->mode_current = BLE_MODE_PAIRING;
+ memcpy(drv->adv_name, name, MIN(name_len, BLE_ADV_NAME_LEN));
+ return send_to_emu('p');
+}
-bool ble_reject_pairing(void) { return true; }
+bool ble_disconnect(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return false;
+ }
+ drv->connected = false;
+ drv->mode_current = BLE_MODE_CONNECTABLE; // more complicated in real driver
+ return send_to_emu('d');
+}
-bool ble_keep_connection(void) { return true; }
+bool ble_erase_bonds(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return false;
+ }
+ printf("unix/ble: erase bonds\n");
+ memset(drv->bonds, 0, sizeof(drv->bonds));
+ drv->bonds_len = 0;
+ drv->connected = false;
+ drv->mode_current = BLE_MODE_OFF;
+ return send_to_emu('d');
+}
-void ble_set_name(const uint8_t *name, size_t len) {}
+bool ble_allow_pairing(const uint8_t *pairing_code) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return false;
+ }
+ drv->pairing_requested = false;
+ drv->connected = true;
+ // NOTE: pairing code ignored
+ return send_to_emu('a');
+}
-bool ble_get_event(ble_event_t *event) { return false; }
+bool ble_reject_pairing(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return false;
+ }
+ drv->pairing_requested = false;
+ drv->connected = false;
+ drv->mode_current = BLE_MODE_CONNECTABLE;
+ return send_to_emu('r');
+}
+
+bool ble_keep_connection(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return false;
+ }
+ drv->mode_current = BLE_MODE_KEEP_CONNECTION;
+ return send_to_emu(' ');
+}
+
+bool ble_get_event(ble_event_t *event) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return false;
+ }
+ struct sockaddr_in si;
+ socklen_t sl = sizeof(si);
+ uint8_t buf[sizeof(ble_event_t)] = {0};
+ ssize_t r = recvfrom(drv->event_sock, buf, sizeof(buf), MSG_DONTWAIT,
+ (struct sockaddr *)&si, &sl);
+ if (r <= 0) {
+ return false;
+ } else if (r > sizeof(ble_event_t)) {
+ printf("unix/ble: event packet too long: %zd\n", r);
+ return false;
+ }
+
+ drv->event_si_other = si;
+ drv->event_slen = sl;
+
+ const ble_event_t *e = (ble_event_t *)buf;
+
+ switch (e->type) {
+ case BLE_CONNECTED:
+ drv->connected = true;
+ if (drv->mode_current != BLE_MODE_PAIRING) {
+ drv->mode_current = BLE_MODE_KEEP_CONNECTION;
+ }
+ if (e->data_len == 6) {
+ memcpy(&drv->connected_addr.addr, e->data, 6);
+ } else {
+ memset(&drv->connected_addr.addr, '\xff', 6);
+ }
+ drv->pairing_requested = false;
+ send_to_emu(' ');
+ break;
+ case BLE_DISCONNECTED:
+ drv->connected = false;
+ drv->mode_current = BLE_MODE_CONNECTABLE;
+ drv->pairing_requested = false;
+ send_to_emu(' ');
+ break;
+ case BLE_PAIRING_REQUEST:
+ drv->pairing_requested = true;
+ break;
+ case BLE_PAIRING_CANCELLED:
+ drv->pairing_requested = false;
+ drv->mode_current = BLE_MODE_CONNECTABLE;
+ break;
+ case BLE_PAIRING_COMPLETED:
+ drv->pairing_requested = false;
+ drv->mode_current = BLE_MODE_KEEP_CONNECTION;
+ bonds_add(drv, &drv->connected_addr);
+ send_to_emu(' ');
+ break;
+ case BLE_CONNECTION_CHANGED:
+ printf("unix/ble: CONNECTION_CHANGED not implemented\n");
+ break;
+ case BLE_EMULATOR_PING:
+ send_to_emu(' ');
+ return ble_get_event(event); // do not forward to app
+ break;
+ default:
+ printf("unix/ble: unknown event type\n");
+ break;
+ }
+
+ memcpy(event, buf, sizeof(ble_event_t));
+ return true;
+}
void ble_get_state(ble_state_t *state) {
+ const ble_driver_t *drv = &g_ble_driver;
memset(state, 0, sizeof(ble_state_t));
+
+ if (!is_enabled(drv)) {
+ return;
+ }
+
+ state->connected = drv->connected;
+ if (drv->connected) {
+ state->connected_addr = drv->connected_addr;
+ }
+ state->peer_count = drv->bonds_len;
+ state->pairing = drv->mode_current == BLE_MODE_PAIRING;
+ state->connectable = drv->mode_current == BLE_MODE_CONNECTABLE;
+ state->pairing_requested = drv->pairing_requested;
+
+ state->state_known = true;
+}
+
+void ble_set_name(const uint8_t *name, size_t len) {
+ ble_driver_t *drv = &g_ble_driver;
+
+ memcpy(drv->adv_name, name, MIN(len, BLE_ADV_NAME_LEN));
}
-bool ble_can_write(void) { return true; }
+void ble_get_advertising_name(char *name, size_t max_len) {
+ ble_driver_t *drv = &g_ble_driver;
-bool ble_write(const uint8_t *data, uint16_t len) { return len; }
+ if (max_len < sizeof(drv->adv_name)) {
+ memset(name, 0, max_len);
+ return;
+ }
-bool ble_can_read(void) { return false; }
+ if (!is_enabled(drv)) {
+ memset(name, 0, max_len);
+ return;
+ }
-uint32_t ble_read(uint8_t *data, uint16_t max_len) { return 0; }
+ memcpy(name, drv->adv_name, sizeof(drv->adv_name));
+}
-bool ble_get_mac(bt_le_addr_t *addr) { return false; }
+bool ble_can_write(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv) || !drv->connected) {
+ return false;
+ }
-void ble_event_flush(void) {}
+ struct pollfd fds[] = {
+ {drv->data_sock, POLLOUT, 0},
+ };
+ int r = poll(fds, 1, 0);
+ return (r > 0);
+}
-void ble_get_advertising_name(char *name, size_t max_len) {
- memset(name, 0, max_len);
+bool ble_write(const uint8_t *data, uint16_t len) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return false;
+ }
+
+ if (!drv->connected) {
+ printf("unix/ble: ble_write while disconnected\n");
+ return false;
+ }
+
+ ssize_t r = len;
+ if (drv->data_slen > 0) {
+ r = sendto(drv->data_sock, data, len, MSG_DONTWAIT,
+ (const struct sockaddr *)&(drv->data_si_other), drv->data_slen);
+ }
+ return r;
}
-bool ble_unpair(const bt_le_addr_t *addr) { return false; }
+bool ble_can_read(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv) || !drv->connected) {
+ return false;
+ }
-uint8_t ble_get_bond_list(bt_le_addr_t *bonds, size_t count) { return 0; }
+ struct pollfd fds[] = {
+ {drv->data_sock, POLLIN, 0},
+ };
+ int r = poll(fds, 1, 0);
+ return (r > 0);
+}
+
+uint32_t ble_read(uint8_t *data, uint16_t max_len) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!is_enabled(drv)) {
+ return 0;
+ }
-void ble_set_high_speed(bool enable){};
+ if (!drv->connected) {
+ printf("unix/ble: ble_read while disconnected\n");
+ return false;
+ }
-void ble_notify(const uint8_t *data, size_t len){};
+ struct sockaddr_in si;
+ socklen_t sl = sizeof(si);
+ uint8_t buf[max_len];
+ memset(buf, 0, max_len);
+ ssize_t r = recvfrom(drv->data_sock, buf, sizeof(buf), MSG_DONTWAIT,
+ (struct sockaddr *)&si, &sl);
+ if (r <= 0) {
+ return 0;
+ }
+
+ drv->data_si_other = si;
+ drv->data_slen = sl;
+ memcpy(data, buf, r);
+ return r;
+}
-void ble_set_enabled(bool enabled) {}
+bool ble_get_mac(bt_le_addr_t *addr) {
+ ble_driver_t *drv = &g_ble_driver;
-bool ble_get_enabled(void) { return false; }
+ if (!is_enabled(drv)) {
+ memset(addr, 0, sizeof(*addr));
+ return false;
+ }
+
+ printf("unix/ble: ble_get_mac not implemented\n");
+ for (size_t i = 0; i < sizeof(addr->addr); i++) {
+ addr->addr[i] = i + 0xe1;
+ }
+ addr->type = 0x00;
+ return true;
+}
bool ble_wait_until_ready(void) { return true; }
+
+uint8_t ble_get_bond_list(bt_le_addr_t *bonds, size_t count) {
+ ble_driver_t *drv = &g_ble_driver;
+ size_t copied = MIN(count, drv->bonds_len);
+ memcpy(bonds, &drv->bonds, sizeof(bonds[0]) * copied);
+ return copied;
+}
+
+void ble_set_high_speed(bool enable) {
+ printf("unix/ble: set_high_speed not implemented\n");
+}
+
+bool ble_unpair(const bt_le_addr_t *addr) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (addr) {
+ bonds_remove(drv, addr);
+ } else if (drv->connected) {
+ bonds_remove(drv, &drv->connected_addr);
+ }
+ send_to_emu(' ');
+ return true;
+}
+
+void ble_notify(const uint8_t *data, size_t len) {
+ printf("unix/ble: ble_notify not implemented\n");
+}
+
+void ble_set_enabled(bool enabled) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (drv->enabled && !enabled) {
+ drv->mode_current = BLE_MODE_OFF;
+ drv->connected = false;
+ send_to_emu(' ');
+ }
+ drv->enabled = enabled;
+}
+
+bool ble_get_enabled(void) {
+ ble_driver_t *drv = &g_ble_driver;
+ return drv->enabled;
+}
+
+static void on_ble_poll(void *context, bool read_awaited, bool write_awaited) {
+ ble_driver_t *drv = (ble_driver_t *)context;
+
+ UNUSED(write_awaited);
+
+ // Until we need to poll BLE events from multiple tasks,
+ // the logic here can remain very simple. If this assumption
+ // changes, the logic will need to be updated (e.g., task-local storage
+ // with an independent queue for each task).
+
+ if (read_awaited) {
+ bool ready = false;
+
+ // check if you can read from event socket
+
+ if (is_enabled(drv)) {
+ struct pollfd fds[] = {
+ {drv->event_sock, POLLIN, 0},
+ };
+ int r = poll(fds, 1, 0);
+ ready = (r > 0);
+ }
+
+ syshandle_signal_read_ready(SYSHANDLE_BLE, &ready);
+ }
+}
+
+static bool on_ble_check_read_ready(void *context, systask_id_t task_id,
+ void *param) {
+ UNUSED(context);
+ UNUSED(task_id);
+
+ bool ready = *(bool *)param;
+ return ready;
+}
+
+static const syshandle_vmt_t ble_handle_vmt = {
+ .task_created = NULL,
+ .task_killed = NULL,
+ .check_read_ready = on_ble_check_read_ready,
+ .check_write_ready = NULL,
+ .poll = on_ble_poll,
+};
+
+static void on_ble_iface_event_poll(void *context, bool read_awaited,
+ bool write_awaited) {
+ UNUSED(context);
+
+ syshandle_t handle = SYSHANDLE_BLE_IFACE_0;
+
+ // Only one task can read or write at a time. Therefore, we can
+ // assume that only one task is waiting for events and keep the
+ // logic simple.
+
+ if (read_awaited && ble_can_read()) {
+ syshandle_signal_read_ready(handle, NULL);
+ }
+
+ if (write_awaited && ble_can_write()) {
+ syshandle_signal_write_ready(handle, NULL);
+ }
+}
+
+static bool on_ble_iface_read_ready(void *context, systask_id_t task_id,
+ void *param) {
+ UNUSED(context);
+ UNUSED(task_id);
+ UNUSED(param);
+
+ return true;
+}
+
+static bool on_ble_iface_check_write_ready(void *context, systask_id_t task_id,
+ void *param) {
+ UNUSED(context);
+ UNUSED(task_id);
+ UNUSED(param);
+
+ return true;
+}
+
+static const syshandle_vmt_t ble_iface_handle_vmt = {
+ .task_created = NULL,
+ .task_killed = NULL,
+ .check_read_ready = on_ble_iface_read_ready,
+ .check_write_ready = on_ble_iface_check_write_ready,
+ .poll = on_ble_iface_event_poll,
+};
diff --git a/core/embed/projects/unix/main.c b/core/embed/projects/unix/main.c
index a7d515ab0..6e1ff081b 100644
--- a/core/embed/projects/unix/main.c
+++ b/core/embed/projects/unix/main.c
@@ -59,6 +59,10 @@
#include <io/touch.h>
#endif
+#ifdef USE_BLE
+#include <io/ble.h>
+#endif
+
#ifdef USE_TROPIC
#include <sec/tropic.h>
#endif
@@ -525,6 +529,10 @@ void drivers_init(uint16_t tropic_model_port) {
#endif
usb_configure(NULL);
+
+#ifdef USE_BLE
+ ble_init();
+#endif
}
// Initialize the system and drivers for running tests in the Rust code.
diff --git a/python/src/trezorlib/_internal/emu_ble.py b/python/src/trezorlib/_internal/emu_ble.py
new file mode 100644
index 000000000..590780b9b
--- /dev/null
+++ b/python/src/trezorlib/_internal/emu_ble.py
@@ -0,0 +1,280 @@
+# This file is part of the Trezor project.
+#
+# Copyright (C) 2025 SatoshiLabs and contributors
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the License along with this library.
+# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
+
+from __future__ import annotations
+
+import logging
+import socket
+import time
+from enum import Enum
+from typing import TYPE_CHECKING, Iterable, Tuple
+
+import construct as c
+from construct_classes import Struct
+
+from ..log import DUMP_PACKETS
+from ..tools import EnumAdapter
+from ..transport import Timeout, Transport, TransportException
+from ..transport.udp import UdpTransport
+
+if TYPE_CHECKING:
+ from ..models import TrezorModel
+
+SOCKET_TIMEOUT = 0.1
+
+LOG = logging.getLogger(__name__)
+
+
+class EventType(Enum):
+ NONE = 0
+ CONNECTED = 1
+ DISCONNECTED = 2
+ PAIRING_REQUEST = 3
+ PAIRING_CANCELLED = 4
+ PAIRING_COMPLETED = 5
+ CONNECTION_CHANGED = 6
+ EMULATOR_PING = 255
+
+
+class ModeType(Enum):
+ OFF = 0
+ KEEP_CONNECTION = 1
+ CONNECTABLE = 2
+ PAIRING = 3
+ DFU = 4
+
+
+class CommandType(Enum):
+ STATUS = ord(" ")
+ PAIRING_MODE = ord("p")
+ DISCONNECT = ord("d")
+ ALLOW_PAIRING = ord("a")
+ REJECT_PAIRING = ord("r")
+
+
+class Event(Struct):
+ event_type: EventType
+ connection_id: int
+ data: bytes
+
+ # fmt: off
+ SUBCON = c.Struct(
+ "event_type" / EnumAdapter(c.Int32ul, EventType),
+ "connection_id" / c.Int32ul,
+ "data" / c.Prefixed(c.Int8ul, c.GreedyBytes),
+ )
+ # fmt: on
+
+ @staticmethod
+ def new(
+ event_type: EventType, connection_id: int = 0, data: bytes | None = None
+ ) -> Event:
+ return Event(
+ event_type=event_type, connection_id=connection_id, data=data or bytes()
+ )
+
+ @staticmethod
+ def ping() -> Event:
+ return Event.new(EventType.EMULATOR_PING)
+
+
+BLE_ADV_NAME_LEN = 20
+
+
+class Command(Struct):
+ command_type: CommandType
+ mode: ModeType
+ connected: int
+ adv_name: bytes
+ bonds: list[bytes]
+
+ # fmt: off
+ SUBCON = c.Struct(
+ "command_type" / EnumAdapter(c.Int8ul, CommandType),
+ "mode" / EnumAdapter(c.Int8ul, ModeType),
+ "connected" / c.Int8ul,
+ "adv_name" / c.Bytes(20),
+ "bonds" / c.PrefixedArray(c.Int8ul, c.Bytes(6)),
+ )
+ # fmt: on
+
+
+# You should probably use bluez-emu-bridge instead of this transport directly
+# as it does not implement any BLE connection management logic.
+class EmuBleTransport(Transport):
+
+ DEFAULT_HOST = "127.0.0.1"
+ DEFAULT_PORT = 21328
+ PATH_PREFIX = "emuble"
+ ENABLED: bool = False
+ CHUNK_SIZE = 244
+
+ def __init__(self, device: str | None = None) -> None:
+ if not device:
+ host = EmuBleTransport.DEFAULT_HOST
+ port = EmuBleTransport.DEFAULT_PORT
+ else:
+ devparts = device.split(":")
+ host = devparts[0]
+ port = (
+ int(devparts[1]) if len(devparts) > 1 else EmuBleTransport.DEFAULT_PORT
+ )
+ self.device: Tuple[str, int] = (host, port)
+
+ self.data_socket: socket.socket | None = None
+ self.event_socket: socket.socket | None = None
+ super().__init__()
+
+ @classmethod
+ def _try_path(cls, path: str) -> "EmuBleTransport":
+ d = cls(path)
+ try:
+ d.open()
+ if d.ping():
+ return d
+ else:
+ raise TransportException(
+ f"No Trezor device found at address {d.get_path()}"
+ )
+ except Exception as e:
+ raise TransportException(f"Error opening {d.get_path()}") from e
+
+ finally:
+ d.close()
+
+ @classmethod
+ def enumerate(
+ cls, _models: Iterable["TrezorModel"] | None = None
+ ) -> Iterable["EmuBleTransport"]:
+ default_path = f"{cls.DEFAULT_HOST}:{cls.DEFAULT_PORT}"
+ try:
+ return [cls._try_path(default_path)]
+ except TransportException:
+ return []
+
+ @classmethod
+ def find_by_path(cls, path: str, prefix_search: bool = False) -> "EmuBleTransport":
+ try:
+ address = path.replace(f"{cls.PATH_PREFIX}:", "")
+ return cls._try_path(address)
+ except TransportException:
+ if not prefix_search:
+ raise
+
+ assert prefix_search # otherwise we would have raised above
+ return super().find_by_path(path, prefix_search)
+
+ def get_path(self) -> str:
+ return "{}:{}:{}".format(self.PATH_PREFIX, *self.device)
+
+ def open(self) -> None:
+ try:
+ self.data_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.data_socket.connect(self.device)
+ self.data_socket.settimeout(SOCKET_TIMEOUT)
+ self.event_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.event_socket.connect((self.device[0], self.device[1] + 1))
+ self.event_socket.settimeout(SOCKET_TIMEOUT)
+ except Exception:
+ self.close()
+ raise
+
+ def close(self) -> None:
+ if self.data_socket is not None:
+ self.data_socket.close()
+ self.data_socket = None
+ if self.event_socket is not None:
+ self.event_socket.close()
+ self.event_socket = None
+
+ def write_chunk(self, chunk: bytes) -> None:
+ assert self.data_socket is not None
+ if len(chunk) != self.CHUNK_SIZE:
+ raise TransportException("Unexpected data length")
+ LOG.log(DUMP_PACKETS, f"sending packet: {chunk.hex()}")
+ self.data_socket.sendall(chunk)
+
+ def read_chunk(self, timeout: float | None = None) -> bytes:
+ assert self.data_socket is not None
+ start = time.time()
+ while True:
+ try:
+ chunk = self.data_socket.recv(self.CHUNK_SIZE or 1)
+ break
+ except socket.timeout:
+ if timeout is not None and time.time() - start > timeout:
+ raise Timeout(f"Timeout reading UDP packet ({timeout}s)")
+ LOG.log(DUMP_PACKETS, f"received packet: {chunk.hex()}")
+ if len(chunk) != self.CHUNK_SIZE:
+ raise TransportException(f"Unexpected chunk size: {len(chunk)}")
+ return chunk
+
+ def find_debug(self) -> "UdpTransport":
+ host, port = self.device
+ return UdpTransport(f"{host}:{port - 3}")
+
+ def wait_until_ready(self, timeout: float = 10) -> None:
+ try:
+ self.open()
+ start = time.monotonic()
+ while True:
+ if self.ping():
+ break
+ elapsed = time.monotonic() - start
+ if elapsed >= timeout:
+ raise Timeout("Timed out waiting for connection.")
+
+ time.sleep(0.05)
+ finally:
+ self.close()
+
+ def ping(self) -> bool:
+ """Test if the device is listening."""
+ assert self.event_socket is not None
+ resp = None
+ try:
+ self.event_socket.sendall(Event.ping().build())
+ resp = self.read_command()
+ except Exception:
+ pass
+ return (resp is not None) and (resp.command_type == CommandType.STATUS)
+
+ def ble_connected(self) -> None:
+ assert self.event_socket is not None
+ self.event_socket.sendall(Event.new(EventType.CONNECTED).build())
+
+ def ble_disconnected(self) -> None:
+ assert self.event_socket is not None
+ self.event_socket.sendall(Event.new(EventType.DISCONNECTED).build())
+
+ def ble_pairing_request(self, pairing_code: bytes) -> None:
+ assert self.event_socket is not None
+ assert len(pairing_code) == 6
+ self.event_socket.sendall(
+ Event.new(EventType.PAIRING_REQUEST, data=pairing_code).build()
+ )
+
+ def ble_pairing_cancel(self) -> None:
+ assert self.event_socket is not None
+ self.event_socket.sendall(Event.new(EventType.PAIRING_CANCELLED).build())
+
+ def read_command(self) -> Command | None:
+ assert self.event_socket is not None
+ try:
+ data = self.event_socket.recv(64)
+ except TimeoutError:
+ return None
+ return Command.parse(data)
diff --git a/tests/emulators.py b/tests/emulators.py
index 90eecbc54..1ca74aa7c 100644
--- a/tests/emulators.py
+++ b/tests/emulators.py
@@ -85,7 +85,8 @@ def _get_port(worker_id: int) -> int:
"""
# One emulator instance occupies 3 consecutive ports:
# 1. normal link, 2. debug link and 3. webauthn fake interface
- return 20000 + worker_id * 3
+ # 4. USB serial 5. ble-emulator-data 6. ble-emulator-events
+ return 20000 + worker_id * 6
class EmulatorWrapper:
Why this scored 21/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.