diff --git a/core/embed/io/ble/inc/io/ble.h b/core/embed/io/ble/inc/io/ble.h index 17e8145dd46..daf89df41d6 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 be8b583a2e0..6564345d671 100644 --- a/core/embed/io/ble/unix/ble.c +++ b/core/embed/io/ble/unix/ble.c @@ -1,66 +1,548 @@ #include +#include +#include #include -bool ble_init(void) { return true; } +#include +#include +#include +#include +#include +#include -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; + emu_sock_t data_sock; + emu_sock_t event_sock; +} ble_driver_t; -void ble_stop(void) {} +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; -bool ble_switch_off(void) { return true; } +static ble_driver_t g_ble_driver = {0}; -bool ble_switch_on(void) { return true; } +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; + sock_init(&drv->data_sock); + sock_init(&drv->event_sock); + 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)); + + 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; + + sock_start(&drv->data_sock, ip, port_base + DATA_PORT_OFFSET); + sock_start(&drv->event_sock, ip, port_base + EVENT_PORT_OFFSET); + + drv->initialized = true; +} + +void ble_stop(void) { + ble_driver_t *drv = &g_ble_driver; + if (!drv->initialized) { + return; + } + + sock_stop(&drv->data_sock); + sock_stop(&drv->event_sock); + 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 = sock_sendto(&drv->event_sock, &command, sizeof(command)); + if (r != sizeof(command)) { + printf("unix/ble: failed to write command %c: %zd\n", cmdtype, 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_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_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_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_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'); +} + +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_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_erase_bonds(void) { return true; } +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_allow_pairing(const uint8_t *pairing_code) { return true; } +bool ble_get_event(ble_event_t *event) { + ble_driver_t *drv = &g_ble_driver; + if (!is_enabled(drv)) { + return false; + } -bool ble_reject_pairing(void) { return true; } + uint8_t buf[sizeof(ble_event_t)] = {0}; + ssize_t r = sock_recvfrom(&drv->event_sock, buf, sizeof(buf)); + if (r <= 0) { + return false; + } else if (r > sizeof(ble_event_t)) { + printf("unix/ble: event packet too long: %zd\n", r); + return false; + } -bool ble_keep_connection(void) { return true; } + const ble_event_t *e = (ble_event_t *)buf; -void ble_set_name(const uint8_t *name, size_t len) {} + 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; + } -bool ble_get_event(ble_event_t *event) { return false; } + 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; } -bool ble_can_write(void) { return 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_write(const uint8_t *data, uint16_t len) { return len; } +void ble_get_advertising_name(char *name, size_t max_len) { + ble_driver_t *drv = &g_ble_driver; -bool ble_can_read(void) { return false; } + if (max_len < sizeof(drv->adv_name)) { + memset(name, 0, max_len); + return; + } -uint32_t ble_read(uint8_t *data, uint16_t max_len) { return 0; } + if (!is_enabled(drv)) { + memset(name, 0, max_len); + return; + } -bool ble_get_mac(bt_le_addr_t *addr) { return false; } + memcpy(name, drv->adv_name, sizeof(drv->adv_name)); +} -void ble_event_flush(void) {} +bool ble_can_write(void) { + ble_driver_t *drv = &g_ble_driver; + if (!is_enabled(drv) || !drv->connected) { + return false; + } -void ble_get_advertising_name(char *name, size_t max_len) { - memset(name, 0, max_len); + return sock_can_send(&drv->data_sock); } -bool ble_unpair(const bt_le_addr_t *addr) { return false; } +bool ble_write(const uint8_t *data, uint16_t len) { + ble_driver_t *drv = &g_ble_driver; + if (!is_enabled(drv)) { + return false; + } -uint8_t ble_get_bond_list(bt_le_addr_t *bonds, size_t count) { return 0; } + if (!drv->connected) { + printf("unix/ble: ble_write while disconnected\n"); + return false; + } -void ble_set_high_speed(bool enable){}; + ssize_t r = sock_sendto(&drv->data_sock, data, len); + return r == len; +} + +bool ble_can_read(void) { + ble_driver_t *drv = &g_ble_driver; + if (!is_enabled(drv) || !drv->connected) { + return false; + } + + return sock_can_recv(&drv->data_sock); +} -void ble_notify(const uint8_t *data, size_t len){}; +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_enabled(bool enabled) {} + if (!drv->connected) { + printf("unix/ble: ble_read while disconnected\n"); + return false; + } + + uint8_t buf[max_len] = {}; + ssize_t r = sock_recvfrom(&drv->data_sock, buf, sizeof(buf)); + if (r <= 0) { + return 0; + } + + memcpy(data, buf, r); + return r; +} -bool ble_get_enabled(void) { return false; } +bool ble_get_mac(bt_le_addr_t *addr) { + ble_driver_t *drv = &g_ble_driver; + + 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)) { + ready = sock_can_recv(&drv->event_sock); + } + + 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/io/usb/inc/io/unix/sock.h b/core/embed/io/usb/inc/io/unix/sock.h new file mode 100644 index 00000000000..ac8174feb65 --- /dev/null +++ b/core/embed/io/usb/inc/io/unix/sock.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +/// Emulator datagram socket, for USB and BLE. Currently uses UDP but can be +/// possibly switched to unix datagram sockets. +typedef struct { + /// Port number. + uint16_t port; + /// Socket file descriptor. + int sock; + /// Emulator host+port. + struct sockaddr_in si_me; + /// Address of the other side of the connection. Set based on the last packet + /// received. + struct sockaddr_in si_other; + /// Length of si_other. Before first packet is received this is 0 meaning we + /// don't know the addres of the other side. + socklen_t slen; +} emu_sock_t; + +void sock_init(emu_sock_t *sock); + +void sock_start(emu_sock_t *sock, const char *ip, uint16_t port); + +void sock_stop(emu_sock_t *sock); + +bool sock_can_send(emu_sock_t *sock); + +bool sock_can_recv(emu_sock_t *sock); + +ssize_t sock_sendto(emu_sock_t *sock, const void *data, size_t len); + +ssize_t sock_recvfrom(emu_sock_t *sock, uint8_t *data, size_t max_len); diff --git a/core/embed/io/usb/unix/sock.c b/core/embed/io/usb/unix/sock.c new file mode 100644 index 00000000000..ecf9a36e7f9 --- /dev/null +++ b/core/embed/io/usb/unix/sock.c @@ -0,0 +1,80 @@ +#include "io/unix/sock.h" +#include "trezor_rtl.h" + +#include + +void sock_init(emu_sock_t *sock) { + memset(sock, 0, sizeof(*sock)); + sock->sock = -1; +} + +void sock_start(emu_sock_t *sock, const char *ip, uint16_t port) { + sock->port = port; + sock->sock = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP); + + ensure(sectrue * (sock->sock >= 0), NULL); + + int ret = fcntl(sock->sock, F_SETFL, O_NONBLOCK); + ensure(sectrue * (ret != -1), NULL); + + sock->si_me.sin_family = AF_INET; + sock->si_me.sin_addr.s_addr = ip ? inet_addr(ip) : htonl(INADDR_LOOPBACK); + sock->si_me.sin_port = htons(sock->port); + + ret = bind(sock->sock, (struct sockaddr *)&(sock->si_me), + sizeof(struct sockaddr_in)); + ensure(sectrue * (ret == 0), NULL); +} + +void sock_stop(emu_sock_t *sock) { + if (sock->sock >= 0) { + close(sock->sock); + sock->sock = -1; + } +} + +bool sock_can_send(emu_sock_t *sock) { + if (sock->slen == 0) { + return true; + } + struct pollfd fds[] = { + {sock->sock, POLLOUT, 0}, + }; + int r = poll(fds, 1, 0); + return (r > 0); +} + +bool sock_can_recv(emu_sock_t *sock) { + struct pollfd fds[] = { + {sock->sock, POLLIN, 0}, + }; + int r = poll(fds, 1, 0); + return (r > 0); +} + +ssize_t sock_sendto(emu_sock_t *sock, const void *data, size_t len) { + if (sock->slen > 0) { + ssize_t r = sendto(sock->sock, data, len, MSG_DONTWAIT, + (const struct sockaddr *)&(sock->si_other), sock->slen); + if (r != len) { + return -1; + } + return r; + } + return len; +} + +ssize_t sock_recvfrom(emu_sock_t *sock, uint8_t *data, size_t max_len) { + struct sockaddr_in si; + socklen_t sl = sizeof(si); + memset(data, 0, max_len); + ssize_t r = recvfrom(sock->sock, data, max_len, MSG_DONTWAIT, + (struct sockaddr *)&si, &sl); + if (r <= 0) { + return 0; + } + + sock->si_other = si; + sock->slen = sl; + return r; +} diff --git a/core/embed/io/usb/unix/usb.c b/core/embed/io/usb/unix/usb.c index b45a7553a3b..5663761710e 100644 --- a/core/embed/io/usb/unix/usb.c +++ b/core/embed/io/usb/unix/usb.c @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -50,9 +51,7 @@ typedef struct { syshandle_t handle; usb_iface_type_t type; uint16_t port; - int sock; - struct sockaddr_in si_me, si_other; - socklen_t slen; + emu_sock_t sock; uint8_t msg[64]; int msg_len; } usb_iface_t; @@ -69,11 +68,8 @@ secbool usb_init(const usb_dev_info_t *dev_info) { iface->handle = 0; iface->type = USB_IFACE_TYPE_DISABLED; iface->port = 0; - iface->sock = -1; - memzero(&iface->si_me, sizeof(struct sockaddr_in)); - memzero(&iface->si_other, sizeof(struct sockaddr_in)); + sock_init(&iface->sock); memzero(&iface->msg, sizeof(usb_ifaces[i].msg)); - iface->slen = 0; iface->msg_len = 0; } return sectrue; @@ -94,22 +90,7 @@ secbool usb_start(const usb_start_params_t *params) { continue; } - iface->sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - ensure(sectrue * (iface->sock >= 0), NULL); - - fcntl(iface->sock, F_SETFL, O_NONBLOCK); - - iface->si_me.sin_family = AF_INET; - if (ip) { - iface->si_me.sin_addr.s_addr = inet_addr(ip); - } else { - iface->si_me.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - } - iface->si_me.sin_port = htons(iface->port); - - ensure(sectrue * (0 == bind(iface->sock, (struct sockaddr *)&iface->si_me, - sizeof(struct sockaddr_in))), - NULL); + sock_start(&iface->sock, ip, iface->port); ensure(sectrue * syshandle_register(iface->handle, &usb_iface_handle_vmt, iface), @@ -122,11 +103,8 @@ secbool usb_start(const usb_start_params_t *params) { void usb_stop(void) { for (int i = 0; i < USBD_MAX_NUM_INTERFACES; i++) { usb_iface_t *iface = &usb_ifaces[i]; - if (iface->sock >= 0) { - close(iface->sock); - iface->sock = -1; - syshandle_unregister(iface->handle); - } + sock_stop(&iface->sock); + syshandle_unregister(iface->handle); } } @@ -174,48 +152,31 @@ static secbool usb_emulated_poll_read(usb_iface_t *iface) { return sectrue; } - struct pollfd fds[] = { - {iface->sock, POLLIN, 0}, - }; - int res = poll(fds, 1, 0); - - if (res <= 0) { + if (!sock_can_recv(&iface->sock)) { return secfalse; } - struct sockaddr_in si; - socklen_t sl = sizeof(si); - ssize_t r = recvfrom(iface->sock, iface->msg, sizeof(iface->msg), - MSG_DONTWAIT, (struct sockaddr *)&si, &sl); - if (r <= 0) { + size_t len = sock_recvfrom(&iface->sock, iface->msg, sizeof(iface->msg)); + if (!len) { return secfalse; } - iface->si_other = si; - iface->slen = sl; static const char *ping_req = "PINGPING"; static const char *ping_resp = "PONGPONG"; - if (r == strlen(ping_req) && + if (len == strlen(ping_req) && 0 == memcmp(ping_req, iface->msg, strlen(ping_req))) { - if (iface->slen > 0) { - sendto(iface->sock, ping_resp, strlen(ping_resp), MSG_DONTWAIT, - (const struct sockaddr *)&iface->si_other, iface->slen); - } + sock_sendto(&iface->sock, (const uint8_t *)ping_resp, strlen(ping_resp)); memzero(iface->msg, sizeof(iface->msg)); return secfalse; } - iface->msg_len = r; + iface->msg_len = len; return sectrue; } static secbool usb_emulated_poll_write(usb_iface_t *iface) { - struct pollfd fds[] = { - {iface->sock, POLLOUT, 0}, - }; - int r = poll(fds, 1, 0); - return sectrue * (r > 0); + return sectrue * sock_can_send(&iface->sock); } static int usb_emulated_read(usb_iface_t *iface, uint8_t *buf, uint32_t len) { @@ -238,14 +199,9 @@ static int usb_emulated_read(usb_iface_t *iface, uint8_t *buf, uint32_t len) { return 0; } -static int usb_emulated_write(usb_iface_t *iface, const uint8_t *buf, - uint32_t len) { - ssize_t r = len; - if (iface->slen > 0) { - r = sendto(iface->sock, buf, len, MSG_DONTWAIT, - (const struct sockaddr *)&iface->si_other, iface->slen); - } - return r; +static ssize_t usb_emulated_write(usb_iface_t *iface, const uint8_t *buf, + uint32_t len) { + return sock_sendto(&iface->sock, buf, len); } secbool usb_configured(void) { diff --git a/core/embed/projects/unix/main.c b/core/embed/projects/unix/main.c index a7d515ab045..6e1ff081b9d 100644 --- a/core/embed/projects/unix/main.c +++ b/core/embed/projects/unix/main.c @@ -59,6 +59,10 @@ #include #endif +#ifdef USE_BLE +#include +#endif + #ifdef USE_TROPIC #include #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/core/site_scons/models/unix_common.py b/core/site_scons/models/unix_common.py index 5b5b9c76860..c5006fcabf3 100644 --- a/core/site_scons/models/unix_common.py +++ b/core/site_scons/models/unix_common.py @@ -33,6 +33,7 @@ def unix_common_files(env, features_wanted, defines, sources, paths): sources += [ "embed/io/display/unix/display_driver.c", + "embed/io/usb/unix/sock.c", "embed/sec/random_delays/unix/random_delays.c", "embed/sec/secret/unix/secret.c", "embed/sec/secret/unix/secret_keys.c", diff --git a/core/tools/bluez-emu-bridge.py b/core/tools/bluez-emu-bridge.py new file mode 100755 index 00000000000..c60cb0109ec --- /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 00000000000..b12c360573d --- /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 00000000000..a3fc7424a7e --- /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 00000000000..9e9b43836c8 --- /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 00000000000..ad640dca32d --- /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 00000000000..ff174468676 --- /dev/null +++ b/core/tools/bluez_emu_bridge/dbus-daemon.conf @@ -0,0 +1,29 @@ + + + + + session + + + unix:path=/tmp/dbus-bluez-emu-bridge + + + EXTERNAL + + + + + + + + + + + diff --git a/core/tools/bluez_emu_bridge/device1.py b/core/tools/bluez_emu_bridge/device1.py new file mode 100644 index 00000000000..2740c4b86ce --- /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 00000000000..db07be5c3d8 --- /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 00000000000..bb85fae1a80 --- /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 00000000000..4100108ae50 --- /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) diff --git a/python/src/trezorlib/_internal/emu_ble.py b/python/src/trezorlib/_internal/emu_ble.py new file mode 100644 index 00000000000..590780b9b53 --- /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 . + +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 90eecbc54cd..1ca74aa7c8e 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: diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index f1d1f374b2f..1bdfa9786b8 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -28856,50 +28856,50 @@ }, "T3W1": { "click_tests": { -"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "3cf2edad6b40b72ad8f3be8eef014f6489b9ee59c7e2ae98df3b33cba0354942", -"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "a69f9f5a0d184af95f11d8a52b5e4194db8219a5ba454db9487ce1813bab0595", -"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "2392668fc2526ab6d351a9552cae050bdd7e0f3b3ec08f66695da7aa073dd8d0", -"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "f1f72385de6e1a7d61854552d8708d89fc3d7c49c61a278d4ed59f830710b258", -"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "86a0abae6a673a33127d52c3f22369554b531a3f696859f10b7bc66680200a82", -"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "0ca250efc6b66175e029d50e213dd00e5ef3bb18b94089ccee1efedd12860819", -"T3W1_cs_device_menu-test_check_backup.py::test_backup_check_cancel": "c6d6265d7ee7cdc6998bfde8712b401fb73e4851f312c36f36d04d36a5260294", +"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "32e17d479015b411cf57cde2c491a4a9365a638e6d8310351580e96d6900b82b", +"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "3f7dcb5b6ef0658001ef3de0842790394d1824c0ee4f10115fc17f7fc763de76", +"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "962356f43ce68b12c05e8dccd942347521fa9760b1997bdc83e7587a27bec5ea", +"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "8aeab4f7461d7a90585f108db7358d8476d574340447b38eadf363f1f35b990b", +"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "78e7032d1ae6b20498eddb764e8da59f92fcc3ca43f300ea7512d4da89a0159d", +"T3W1_cs_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "3a01a6175d2bde7bce131f8a894334aa016b32f5dffe8f9acb048f8e8ddd703a", +"T3W1_cs_device_menu-test_check_backup.py::test_backup_check_cancel": "05d70f5f3c358c1b9e0fc918b89941644737ac3125477ae0629b1123de6ec8be", "T3W1_cs_device_menu-test_check_backup.py::test_backup_needed_fails": "13c0e7145d4e402b32f8b11c87e885beb26da0604eb0b022bce3b43f3bf5e23e", "T3W1_cs_device_menu-test_check_backup.py::test_no_backup_fails": "d19e7595c0cf0e67ba02f980571b6a2ac492b16326eac379415774139a24d30b", "T3W1_cs_device_menu-test_check_backup.py::test_uninitialized_fails": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_cs_device_menu-test_device_settings.py::test_brightness": "3a4f199c79f208e95b6ef07e8f017d8f657b9059457ac8b156041b9af4ede5e1", -"T3W1_cs_device_menu-test_device_settings.py::test_device_settings_uninitialized": "bbfa56211e4a3b78483f6858241c21eba7f5cd1a679dcf11f30ef42f0ef434b6", +"T3W1_cs_device_menu-test_device_settings.py::test_brightness": "ba1e579587447f8199b9f8c294c922e9378e66ac3e47e912bad162fdf8fcabd2", +"T3W1_cs_device_menu-test_device_settings.py::test_device_settings_uninitialized": "e76152a5c121dac5f7f44e4a111c90723dcf26c2d35016c70bd2d098eefcee16", "T3W1_cs_device_menu-test_device_settings.py::test_toggle_haptic": "c24521e569c08e3605b164212c876f8ac57c5eef6cca6f2ca53a635a883ebc4b", -"T3W1_cs_device_menu-test_device_settings.py::test_toggle_led": "c76333b6790fe10c3c60b047da237ac18282e76579ecfa44839e236f81884348", -"T3W1_cs_device_menu-test_label.py::test_change_label": "1f5b41d8d2ed13465165279c8906c0981ab0535271c3f076b5153e131a10e9bd", -"T3W1_cs_device_menu-test_label.py::test_label_cancel": "28dd33adf4e72719d612ff51b913b527cf4063b3338d3f598f86de27092dfb8e", -"T3W1_cs_device_menu-test_label.py::test_label_click_same_button_many_times": "d990d8699016be4befcb0e7ea4687162677dd3a8b81aeef7a9433c3442433ceb", -"T3W1_cs_device_menu-test_label.py::test_label_cycle_through_last_character": "7bef52669897ac1d0309a9219915cf26752c6891902e1b40ac2ac5d8c4295182", -"T3W1_cs_device_menu-test_label.py::test_label_empty": "8b8c28e703b181f184c10ccfa93e66e9d83ebf70f07c3f23ac420d7e15c6f679", -"T3W1_cs_device_menu-test_label.py::test_label_loop_all_categories": "ab01ad7f1294040038a9c831cf5812bce67c8a555dc3144dc4684ed689affedb", -"T3W1_cs_device_menu-test_label.py::test_label_over_32_chars": "14bd229902adbbc5a60f2bb6d5e6fb7fc67d1511c7e068adfa68e39c769888d3", +"T3W1_cs_device_menu-test_device_settings.py::test_toggle_led": "537be0b8c9098007fd19cd8d901a9cadf8031be5a86388c1503b743df4a1cee1", +"T3W1_cs_device_menu-test_label.py::test_change_label": "05aa9d12d58d8de48a4ed94fb076dde01fb38de8fd01d6e39b7883285fd44b34", +"T3W1_cs_device_menu-test_label.py::test_label_cancel": "ce800142172bc9435f9cfe12f2bb15832899af598196bdce0ca62d2e22632581", +"T3W1_cs_device_menu-test_label.py::test_label_click_same_button_many_times": "840ece359786584c0b8896b570b546606ce8b2a0cc473344633be7adb6034a53", +"T3W1_cs_device_menu-test_label.py::test_label_cycle_through_last_character": "0b6a7eb49fcf10ecbd4c242830547b6f0e3f757aa77c9a499c275a1061495adf", +"T3W1_cs_device_menu-test_label.py::test_label_empty": "a50f41300fddc407585269fda203678ef39b56fe3a6374ea076df816f38a6af0", +"T3W1_cs_device_menu-test_label.py::test_label_loop_all_categories": "dccc91d884346fedbf11a8fc9ef5acb708cfaab732fab449f73e74d0f7e853ef", +"T3W1_cs_device_menu-test_label.py::test_label_over_32_chars": "96dc6a9c434573a199032fbccf66915b7d56fdf769e12dce83fdf77463d8f96f", "T3W1_cs_device_menu-test_label.py::test_label_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", "T3W1_cs_device_menu-test_notifications.py::test_backup_failed": "534ccea40a7e0dd84614452606538486f71882f061afa9222ea782dd33a5887e", "T3W1_cs_device_menu-test_notifications.py::test_backup_needed": "3e9219faf28dc045827113d9a0ebec8e42437bc39cb3e437cbdb6b3992e89538", "T3W1_cs_device_menu-test_notifications.py::test_pin_not_set": "f55955e30b3b7b22393ef8cfeaca5d7e6e9b8d7f3345a11953066db56b65a813", "T3W1_cs_device_menu-test_notifications.py::test_seedless": "f47325f31b7cff66ad1ed9a2d98659b7161306440ae40e503ceac2407f8ffa92", -"T3W1_cs_device_menu-test_pin.py::test_pin_cancel_intro": "5074c4419e9938388f6cc4a6cf743508b7ced41d757b22142790b85c4cc4f5cf", -"T3W1_cs_device_menu-test_pin.py::test_pin_cancel_keyboard": "124d1ad20a9b3195993823ba14e94b08016cbb420408fa9840039873793fb4d1", -"T3W1_cs_device_menu-test_pin.py::test_pin_change": "81e6669b955fa5d08a15c211ade5ae4cf128ba004f15ff037c5c85573edc5749", -"T3W1_cs_device_menu-test_pin.py::test_pin_remove": "789b15001e41c3529921caf6bfb102a7ba239a575785a89a56737aa4425f805b", -"T3W1_cs_device_menu-test_pin.py::test_pin_setup": "bf5f8c94e020810fefecba266575b4e286095d620d94ad971406aa4a1c2bdaa1", +"T3W1_cs_device_menu-test_pin.py::test_pin_cancel_intro": "41ad0217db0ce0aaaddfd7e91a675e2352fb94fa7a3ef8978851567268296730", +"T3W1_cs_device_menu-test_pin.py::test_pin_cancel_keyboard": "67b01ab3195452d7f031fd4c3b80afc6f15b8d3a1e723d7603e5984dd111403c", +"T3W1_cs_device_menu-test_pin.py::test_pin_change": "f6ade206304c08c9f5716872f560035aa9a402311d895574d1ce42cad07594b1", +"T3W1_cs_device_menu-test_pin.py::test_pin_remove": "d72a27002db672610b8255f20df897ff8ca6a3c5b1bdfea74d37e41c1a889310", +"T3W1_cs_device_menu-test_pin.py::test_pin_setup": "fd606de05bb1cde14ba1e87c5b8c7e5d5f67524d01606e792886eff1d0d2aa5c", "T3W1_cs_device_menu-test_pin.py::test_pin_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_cs_device_menu-test_traverse_menu.py::test_traverse_initialized": "7920c3013a2c9ea06e9774160dca8fc2f52ed303e48ea3f6d7e876c21a93a75a", -"T3W1_cs_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "eb9e3362321a6d2c14b6c6556adf323a4708d959867adcd7c00cbc07d7fe138e", -"T3W1_cs_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "77211cb4d0e79c26666cb0e61e94b5817c0911fe019093fcdc365acdcae5fb01", -"T3W1_cs_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "4fa01778ff5fef9acec3c26b87793e0367db2ac6c70dccf4109a0094c421f64d", -"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "037742cc9c1b5531b4b92680db09650f5b061d53c3af359b2b766fa87158e59e", -"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "b1b9e64d9c8e29cb7fcfa124cd84743f84b5a6e542ce9afb4914ff6942401431", -"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_change": "448c37e9a52dc69eb58d6057c9586aa13192a33f30258e3c6a385e9311153239", -"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_remove": "52453c2b6cae6616baf38acbee1bd4d6816b614ee48f5d6220658c6c41dcd49a", -"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "9787377424eff840bfcb63ccb4ba222eb59047273f2bc62b55b596a2d715c85a", -"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_setup": "3e8535987b8b0e01cfd7d5167e67e4bac3f3c585c38d47466a43a07f2fa707a3", +"T3W1_cs_device_menu-test_traverse_menu.py::test_traverse_initialized": "232ccfcca666b9062f599249f34904402e4d7d98351e5a0912cf7d51f00b084d", +"T3W1_cs_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "54d4730aab5ebdce81b009d3e80df9b8d842c6f2390a973304222f18a86fef39", +"T3W1_cs_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "dfec6ae47cb1808e5c415a8ac2acc1aee7a768196282e0592e1a7cf3576ede58", +"T3W1_cs_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "ccd5d4fbe74c4ad28e33b33cb22d890a61c352842b07ea6f4de2ef0a1a8645e5", +"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "5b7dffec3a06e745a3f74b891548a565997dcbb9476a3d1111ca21dd352e6f56", +"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "dc4c3184569241beb07d1e1492cd4289b51adef08172e2f0aeafb1580ec448e5", +"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_change": "3d8ad6bc72f7831bc9c02f894d7e59e7064fb7fe04a55a272840358a6ef29e32", +"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_remove": "0e330ebf4280d63b5123ff9c44130f64fdd6ae5d533a5ce47ede63d90a2fdd16", +"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "cfff2e49bbaa1604514ed5343b04494f34458283444b8e424d52bb77a6cc97eb", +"T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_setup": "71d538b6e79f05f04e55b8ea5c4452815779d5f4063ff815e1ea48db20703b9f", "T3W1_cs_device_menu-test_wipe_code.py::test_wipe_code_setup_pin_unset": "c24521e569c08e3605b164212c876f8ac57c5eef6cca6f2ca53a635a883ebc4b", -"T3W1_cs_device_menu-test_wipe_device.py::test_wipe": "11b0f861d848983927f245dcfd2b3893f2917bfa335fb143cbebc0b98a0d4d8e", +"T3W1_cs_device_menu-test_wipe_device.py::test_wipe": "e2886cfb4ebea7ca4833137be41163a07f1bac24c8c25fd578aa75266562ae52", "T3W1_cs_test_autolock.py::test_autolock_does_not_interrupt_preauthorized": "e0fcd87bfd002df2a083f1331587484c960da9e794eb6df3a468e62db117ab74", "T3W1_cs_test_autolock.py::test_autolock_does_not_interrupt_signing": "fd87f971971c70f32a6dc671cdc59a6a17a62a8a7853f8b72ddee66cd4fecbab", "T3W1_cs_test_autolock.py::test_autolock_interrupts_passphrase": "82a86665ef03e6972069159b9a8272448ff52539ea35d2660f605da4a1d7689b", @@ -28960,50 +28960,50 @@ "T3W1_cs_test_tutorial_eckhart.py::test_tutorial_menu_close": "78cb5f9003ff80d63c162551f29fef295f6ad9b7168fb584ae730b9a7c8eb71f", "T3W1_cs_test_tutorial_eckhart.py::test_tutorial_menu_tropic": "77aeafb12b4c42364a160913c968142b05d6e3a2abdb250d4abf705c476f0e38", "T3W1_cs_test_tutorial_eckhart.py::test_tutorial_restart": "a604465720404d5fca83bc996e9ea1bda6d2824b02a40ef9ac94714986aaf578", -"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "11d3b157867889bd894fb55be54cc44b2025ea133856abdea6f683315acd21bd", -"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "77a048baa1f5ea62a256552742b6ed2ce058a06314e61d2eba120b45d31a35c6", -"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "918cce11b5a6890cae342a867f28ca48302aa06ab3d8c27e0f58f33fb6b315ba", -"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "b4c84ad5f4b2afba8c0fdcc5a36a2d3f0ae8dcd300174e3302f054fdef7c2fb1", -"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "d242748e3c5585f241444cb90b36798e3427080c726a6f4bf930b9deced5a407", -"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "c7928278e3948a5615bf9dedc268010bf8ad33b3af7b7a1d5b283bc6d729b4b1", -"T3W1_de_device_menu-test_check_backup.py::test_backup_check_cancel": "cfa8cdb8a1755faf22b17367eb607bd5a50f11a2b471fddcfec8286b38197944", +"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "4b21d20d21482a2a4fc839bc04c2e863a583113c5dc3d8b816710e197fca4a70", +"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "58a501eeea5b6cf8b7e0d9ebda27f4456b43615abd864ea86e4b93d6f30362c2", +"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "50d185e007dce9cccd6113fd7550e34ccf004217a0148f517ee21dcff3b01aaa", +"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "1c0bd23709ca93d33860f8bef63b5896b6f952c370ad93eaa37ae725a99dbfbe", +"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "ec9611a78cd0a186560cdef5233f5e4356af0f147b1779159b026ff56c1947b7", +"T3W1_de_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "eed9b0bdffa27201c298729b229f9890e580b0b8fa77b50f2e0e26c932591404", +"T3W1_de_device_menu-test_check_backup.py::test_backup_check_cancel": "acb7510c9604d766a9ab0d98f175f492c2533c6a72b1b94bb90545ffa1c4fe8e", "T3W1_de_device_menu-test_check_backup.py::test_backup_needed_fails": "c94696a52bb66f5a1c5ec8694749b6c5798319df1807a59fd939d67007e97a1a", "T3W1_de_device_menu-test_check_backup.py::test_no_backup_fails": "32f7ece1a421303cf214be5af0d36255a4ae85750129df37a4862c1e540b00e5", "T3W1_de_device_menu-test_check_backup.py::test_uninitialized_fails": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_de_device_menu-test_device_settings.py::test_brightness": "5f604f7f441508188c2c32a842bda58c6526c5a880e64df5f54ae3680f421968", -"T3W1_de_device_menu-test_device_settings.py::test_device_settings_uninitialized": "87ceb7382279132b6f62597e3a18bddf1397d52417dbd85e0adfa99043db295d", +"T3W1_de_device_menu-test_device_settings.py::test_brightness": "3ce646a8917d1cd4dc988423810fda0356a0df8701a63686e60d6338771efdca", +"T3W1_de_device_menu-test_device_settings.py::test_device_settings_uninitialized": "6923ade8cfb5070ef34de78c6a0800c2c1a1fc2e559d450fef66ae96653169a2", "T3W1_de_device_menu-test_device_settings.py::test_toggle_haptic": "98735f888f827501b25c78c6a737c1983a800c620f709c65b1d95f6c8c9a2b90", -"T3W1_de_device_menu-test_device_settings.py::test_toggle_led": "274cf63c67d00d404438e39696d477731b6b56d510d0d48810db7a463fb3ce14", -"T3W1_de_device_menu-test_label.py::test_change_label": "75c783a6b975931587db3eb63a4530a54353a990b913fe87caafa99c4ddd9b55", -"T3W1_de_device_menu-test_label.py::test_label_cancel": "d83ca63809fbf543a7aa92065c970ed940ae531f4232bbb0c273427d9f03440b", -"T3W1_de_device_menu-test_label.py::test_label_click_same_button_many_times": "6a3a84fad26df0c1714ff6d2a1189e3411efc341f16d1486c5d6e25c90236b50", -"T3W1_de_device_menu-test_label.py::test_label_cycle_through_last_character": "43f88bdf586123209ae0c06e1bef6c4fbd2adf3df0b242292c3729e9bfebc721", -"T3W1_de_device_menu-test_label.py::test_label_empty": "26b39485897d4b073f816e0756273eac67bf3cbfaa14eb0d08d78a9b0dde102a", -"T3W1_de_device_menu-test_label.py::test_label_loop_all_categories": "e4d99483f4433687ccbf8750d917ec9830d355037fdd79f9b78f95a085fbe5b6", -"T3W1_de_device_menu-test_label.py::test_label_over_32_chars": "bb6e8a1766c6d23d0913bcc7562179812bf88d1ebd78115e79241da49e012d0b", +"T3W1_de_device_menu-test_device_settings.py::test_toggle_led": "bdcb55ae1f9e8e4784000de053f518832070e73424d5d1940c02a3a72aee032b", +"T3W1_de_device_menu-test_label.py::test_change_label": "190dfb9f765e75a0578c5c020ba0440f01976a4d2833fc02883e28d85c08d385", +"T3W1_de_device_menu-test_label.py::test_label_cancel": "92be0e74c53bfcaf0286808bd301aed58e590bb3aca423fcd2f3754017952443", +"T3W1_de_device_menu-test_label.py::test_label_click_same_button_many_times": "048a48c9240f0bc34b6ae5f02e55241177850c1ef5fa00dce34d4645d571bc05", +"T3W1_de_device_menu-test_label.py::test_label_cycle_through_last_character": "0346712256e2013dc663dcaa0eb8b9de2e6b280ddf006db670e640b3095de844", +"T3W1_de_device_menu-test_label.py::test_label_empty": "10529f5e1e87f1a5ecc8515dc9ba1ed4ea7b9292de488fb929fb087316951749", +"T3W1_de_device_menu-test_label.py::test_label_loop_all_categories": "1b3ad41e470c62108de842bae2f3518e0f546167286c5f1b6f2b070a4ca1de43", +"T3W1_de_device_menu-test_label.py::test_label_over_32_chars": "b6bc70469b32b52ccd5fdf9e48a2512162dbeb797bf526091bb9645d503d1682", "T3W1_de_device_menu-test_label.py::test_label_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", "T3W1_de_device_menu-test_notifications.py::test_backup_failed": "209f632d766cb211d41ee7ebb5c0fa38249e558d48c1e6b8af93ee3e3253abd9", "T3W1_de_device_menu-test_notifications.py::test_backup_needed": "ecb0fe08d4aa61ebfb7c91da6eb271744f2d50e9dd3e71552a75703417c17d61", "T3W1_de_device_menu-test_notifications.py::test_pin_not_set": "777cfc100e2a4626b9faa244997dcb39f61f0cafdf12cba0d1728e140c1b08c8", "T3W1_de_device_menu-test_notifications.py::test_seedless": "a5f1836c258f20a18e2be5dffc5583f684df48c7688a734f3fca89c01d613996", -"T3W1_de_device_menu-test_pin.py::test_pin_cancel_intro": "8a9cab8ec76fa3ccebbf4f84ec3b4faa1aa5b3f72b4bcd0efbc1fd7d1ea41dc6", -"T3W1_de_device_menu-test_pin.py::test_pin_cancel_keyboard": "1889a76ec6c2c2d8ca63870b4b8d5eb92529cb66b1b09f3a2f6767fd5cfcedf8", -"T3W1_de_device_menu-test_pin.py::test_pin_change": "8516bcde966af2de680efc0a13317e475a528cdecbe25ce91faa7a3f6f4ab282", -"T3W1_de_device_menu-test_pin.py::test_pin_remove": "a0a4c0fc15d07faa6cd84ce2e6e8eb862d086eeb3d68ed340d6b1c638a4462f7", -"T3W1_de_device_menu-test_pin.py::test_pin_setup": "c4d2131240f6048ddb703a88291d6b185cc5f02d8f26d2aba4fd99b5fac666f9", +"T3W1_de_device_menu-test_pin.py::test_pin_cancel_intro": "7bdec71b72cfe26bcb56568a76dc22ee727521d610e057ba45a261fe879aab7d", +"T3W1_de_device_menu-test_pin.py::test_pin_cancel_keyboard": "4cc146e87e03137e6272443a2689ecc0e28baa0f25a3e9565c214537198647c6", +"T3W1_de_device_menu-test_pin.py::test_pin_change": "9451618776f47826a420239c5420d87b64db8ac7e79993a26b8f97b6fe083331", +"T3W1_de_device_menu-test_pin.py::test_pin_remove": "1c852d3892096c546e409904337522abda9852674e6b77705fd802087551484c", +"T3W1_de_device_menu-test_pin.py::test_pin_setup": "d544512c120689941066a840b7514386125c377dc1b861905ddf0c57d08dd819", "T3W1_de_device_menu-test_pin.py::test_pin_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_de_device_menu-test_traverse_menu.py::test_traverse_initialized": "82742fddf4e2190a56465733148373109796144eb49c217471e6f11ce1b7144e", -"T3W1_de_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "d9019882771518985efb5f6d15d7e3e38abde13edc5c6c81bc49d3a9e215dbb5", -"T3W1_de_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "18b84cb8a94c007fc67708fdd0d87cebfed3317521625a0dedee4c5ea14d88fa", -"T3W1_de_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "7c9e49228751d3d5f6534ef5034b58e44c2ae1fc4eaa7c270b02b64b85637e4f", -"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "9cdaa3daf286b90676a2894decc62fa2c4574541ae9f1536f091cc7c174be894", -"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "d2e2c6499d8bb962ae8aa665f474c9242851cdc922550182c554966071bc29fe", -"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_change": "4bd9f01bd023996ca1be9c7469932b7adb00c2d98cb1e53a0fa48d5b5fae3aa5", -"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_remove": "850e177a634c65271f9953138ad3bbe076d6e937256ff247611e858eea36d089", -"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "92051d91a0bd31b4cffc7d109f37306ab330268a091da17a1e166ab1205f197d", -"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_setup": "33900c7314f3a3bc3ef55e63cf35ef38b9dbc4479c5352b8d30e35a578b8c355", +"T3W1_de_device_menu-test_traverse_menu.py::test_traverse_initialized": "c91c9033db04a0d7dc1a3edcf1bbd03398a92112f02cbb52bb4876cc282c851e", +"T3W1_de_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "dec85a3c0d4b25843deb443e08f2e072e612b47a0344f308c8b3122efbdfe908", +"T3W1_de_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "7b3cb7e1105fdb13eb0aa77a97811b95bd2b4d9e2fa47d8f06ee1bef229ed327", +"T3W1_de_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "a039e541bbefaa463a3908f5288edc53b2772cbf4868f3a0aa796e2665e1d9d5", +"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "e9e1a1be83ac680a3632aa41fdf0f638d419888fef4be3cc24cb4ae0453d631a", +"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "68179ddbe124878115326839e98fff8ccbeb2e8eccc69ba8caa7e809ed1a7392", +"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_change": "580aec09b24bc8189dcb765bf17a69768b66c01d343481d548fcd1e42339d912", +"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_remove": "7682881add5bde57fe1395ab90601edc4d27f46c0f6809a6bca3786d7bba2d83", +"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "9f22abc5c269a91cfa0f9a880548533f12fbe23fea60323b413cc9ca3f5028f3", +"T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_setup": "2ac3d0c24b24266e577239413528dd7b8cc5737987f30373b089a3ecc4df8e38", "T3W1_de_device_menu-test_wipe_code.py::test_wipe_code_setup_pin_unset": "98735f888f827501b25c78c6a737c1983a800c620f709c65b1d95f6c8c9a2b90", -"T3W1_de_device_menu-test_wipe_device.py::test_wipe": "4604f1f133467c1414e60ecedbd043ea4c1dd1ba9104f7c552137f57d0e4c880", +"T3W1_de_device_menu-test_wipe_device.py::test_wipe": "1ed7eddc8729c4efdadd47916e71ca7d03cdafb8d74b93929be55b4ac126a1b2", "T3W1_de_test_autolock.py::test_autolock_does_not_interrupt_preauthorized": "e62dc520721b417c0f21afb46102480398cbbc1ec8c59d0f6de03fc75b386a92", "T3W1_de_test_autolock.py::test_autolock_does_not_interrupt_signing": "b96999bdf3d7dd55da99d083452def6cb633b0459b5c72e310e3a8b3297bf726", "T3W1_de_test_autolock.py::test_autolock_interrupts_passphrase": "44cf95c18e7e659f8c9e64768fe7a3059a0e3dfcd0bf080fb180c74bf7d4b43b", @@ -29064,50 +29064,50 @@ "T3W1_de_test_tutorial_eckhart.py::test_tutorial_menu_close": "7688c261a8b5d78b23334e63aa06527b6a33d3131d34889300f208a2ed2cff79", "T3W1_de_test_tutorial_eckhart.py::test_tutorial_menu_tropic": "b478a5c5b8fab845dab87bf1b7e6b58c3e163e42a5a76a08a4994bece47859a1", "T3W1_de_test_tutorial_eckhart.py::test_tutorial_restart": "cce1f633042dfbc4dc4d62ff58fb23bfa8f877333fc71466834cd8fa86945c3c", -"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "1fba799e2a25332613e36618a9dd74c55206e93331e38af6699305bacc52fee8", -"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "1a61349335d26cc96f3618535f374f8bd14e20430e7a67a061712ef0b2c70b67", -"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "7be059b233385cb2dbb061aa39fc5c38c0a9ecd608efa0f27e89d042f0f94223", -"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "ebebe8c22b86b875a9a512e0136baf12077ea74e476739f41474ca250396e84b", -"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "74ba9cf7d1164c926c233cd7f3987a61065b78da866a0b2560eb5613492a2bf7", -"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "8c8109c3435eb8d4c62e3496002c9960571a0bea74ab3616d46ce7f11c9799c8", -"T3W1_en_device_menu-test_check_backup.py::test_backup_check_cancel": "35434b8c2cf22388b32c637158827c6acbc237d559af459c37e724212d476c02", +"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "743b294a275ac0832e91fa8c4dc98efaf2aefd9788a6a59f0eff043e06292f80", +"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "576864fa134ba5496fb9b245764d7d947835b9aec1a6dbce02eafc262f7dc2f5", +"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "cee6176825f8cd2cfb5e1d6eeb42d323a5a16cd5ff39da27194abadbb24384c1", +"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "14b8b28646079f64ff3b314db3adca9e6e525e8d862b9ecb8ab9c3d4f8fc4072", +"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "613b4ab921a26ceaf537626255c7cfde0d4f5f2b58a719698949f57d5ed97ec9", +"T3W1_en_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "4873caf6db6da17cf4cd0699d4370ef468c48c4d179696309f1dd677146ba337", +"T3W1_en_device_menu-test_check_backup.py::test_backup_check_cancel": "ad38ff29c91860048e0feb85c99ac49700408eb5fbfa1bdd5ca349a249ee32c7", "T3W1_en_device_menu-test_check_backup.py::test_backup_needed_fails": "fb3120f1899ceab19654335ed391516b9030737762120bae07161f497deefcb0", "T3W1_en_device_menu-test_check_backup.py::test_no_backup_fails": "395556180048c5471d78770521fc625d75062b1774b3a64f0e0bb758faae556a", "T3W1_en_device_menu-test_check_backup.py::test_uninitialized_fails": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_en_device_menu-test_device_settings.py::test_brightness": "ed4b410d6a63400c394c5cb0a4007fe025c0c0ec5a78394195728507cae39de0", -"T3W1_en_device_menu-test_device_settings.py::test_device_settings_uninitialized": "87c1a197d1b6c9ab87c6db6c1008532698de8749917a401de46613a3b0fc5ccf", +"T3W1_en_device_menu-test_device_settings.py::test_brightness": "99fa8120b2443054843036e5c0969f63496d41dbad88c569e419d654a468ac32", +"T3W1_en_device_menu-test_device_settings.py::test_device_settings_uninitialized": "72dc172785fc784be222584920fabef2c186a7006fc8969d3eb2a9ce38b8db9f", "T3W1_en_device_menu-test_device_settings.py::test_toggle_haptic": "931d9afceb0ba1e4faae891775819277242d889644d5c0c5863fc8c9fcf859b1", -"T3W1_en_device_menu-test_device_settings.py::test_toggle_led": "ee1f2dd52d7ee7d4ffe7d6df94f6b24cc0d0712a1adb6b0d720765b0c0dc1a81", -"T3W1_en_device_menu-test_label.py::test_change_label": "f26cf588578984955b27a4508fb746f3a45103b24d706360eed8a2f5d753854a", -"T3W1_en_device_menu-test_label.py::test_label_cancel": "4dc180383a76e154c0783c9444c1f9d3ed87ec1beaa79bcaffce371e8f97d346", -"T3W1_en_device_menu-test_label.py::test_label_click_same_button_many_times": "4519449c082500819de4fc6a4137e3f4a6664c4a752fcbe13c10f960a8e4d8dc", -"T3W1_en_device_menu-test_label.py::test_label_cycle_through_last_character": "2a00afb9d73cff97320abbe092800950ed8c92ac712e80fea94590d5d91f2aca", -"T3W1_en_device_menu-test_label.py::test_label_empty": "b901446d197cd04b2edf49986b5733bc0de00009c619889684cdf065e145bde1", -"T3W1_en_device_menu-test_label.py::test_label_loop_all_categories": "1eed85e8ae0dd214b564dc6836b0d4920b1a9c2cb6c1efab9a8d0e70bf8b5327", -"T3W1_en_device_menu-test_label.py::test_label_over_32_chars": "3ec7b8d6650342a0c779f1855414626fdb958db77087461a670dbd924e4a6cd9", +"T3W1_en_device_menu-test_device_settings.py::test_toggle_led": "b82c16d6ab606cbd2d05bb89b585de026c594c82dae5f5fd385ea62f7959cbed", +"T3W1_en_device_menu-test_label.py::test_change_label": "ea8ea8cbff14199420ff3e53fcbe432beb1bcbf9d67b15067adc8bb548e04b14", +"T3W1_en_device_menu-test_label.py::test_label_cancel": "4bf42a5395be14438166ecd3b0cf9b73b29164b1d99a960cebeb399d062da015", +"T3W1_en_device_menu-test_label.py::test_label_click_same_button_many_times": "89757e3b3d281d01716fa2e7d065413b5bef6a5ea9fea080c493b16923f56bfa", +"T3W1_en_device_menu-test_label.py::test_label_cycle_through_last_character": "f0727a3035a08ace8ba553a6c2540ff3e3e986e6685c58fccc6276d4c90b7cbf", +"T3W1_en_device_menu-test_label.py::test_label_empty": "9fc4c352f0c574c0480bec2ddacc17b3bb45ddd463c2ebce155da5cf78086a03", +"T3W1_en_device_menu-test_label.py::test_label_loop_all_categories": "a07f72c45320f4f1f322ed16225c0a1bda4174dcf46c7fd8c18e363b1d0f8760", +"T3W1_en_device_menu-test_label.py::test_label_over_32_chars": "62f9dab438d22d69016b9acde255748212087c9bdd836a39f5d6638cdaaca4b6", "T3W1_en_device_menu-test_label.py::test_label_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", "T3W1_en_device_menu-test_notifications.py::test_backup_failed": "006ebc715431b9c0beebd7e1c063b0d3d5f3320d8e4d86bd806f6de841dd8053", "T3W1_en_device_menu-test_notifications.py::test_backup_needed": "110d3832f3581896f926f20a84201be89efea5040d7cb89bdb5a9e7b85182e45", "T3W1_en_device_menu-test_notifications.py::test_pin_not_set": "9a7a3422d6addf9fd84dd57aef1c94929fc83a34704ed33db1438cee37b6356b", "T3W1_en_device_menu-test_notifications.py::test_seedless": "49d1a24f6f35086b8819dca2e26593e4dbe921bfdc8e4af1ee44f6e71fd14a4c", -"T3W1_en_device_menu-test_pin.py::test_pin_cancel_intro": "5d610c1b0e07b199317fa70ecb56a9e25b441e5c7d00f808239b34233c0d0f68", -"T3W1_en_device_menu-test_pin.py::test_pin_cancel_keyboard": "c84e61597e1215d735e6a19d92115e7e14949ecac3852b384c6ae66a2f559a22", -"T3W1_en_device_menu-test_pin.py::test_pin_change": "10dd68bdfeae75b1017a5a024bbb68691abf083801e653c9fa5584e6e6f8f0a3", -"T3W1_en_device_menu-test_pin.py::test_pin_remove": "1d3ded45a8d33059f076e98c7df73a93b3cf68e0a879896b96aac7a0c7b14368", -"T3W1_en_device_menu-test_pin.py::test_pin_setup": "1c85050bb11604d6e0956e152dee4670a80d8b8232f4644395c300c6e6415d10", +"T3W1_en_device_menu-test_pin.py::test_pin_cancel_intro": "2cbd718afa2134c6905f2a32d1a39187c702591cb301c1aba40b3a91308d0c7b", +"T3W1_en_device_menu-test_pin.py::test_pin_cancel_keyboard": "88e1cbf11e7960f34047581f204189a3d75da71a0a1b3bffc8dde0ebf7df501c", +"T3W1_en_device_menu-test_pin.py::test_pin_change": "1df8dee22ba0654039857e700363d7548e45ee19975d97b4473287dc7f26be90", +"T3W1_en_device_menu-test_pin.py::test_pin_remove": "9eed511b691a10f63a16e774435b48ae86f8363657498ad29e7541a15ad37c92", +"T3W1_en_device_menu-test_pin.py::test_pin_setup": "d2ab5a53b75280c1aac10ca49ca5a7d93565edd60b07c156340a5c2edf7e033e", "T3W1_en_device_menu-test_pin.py::test_pin_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_en_device_menu-test_traverse_menu.py::test_traverse_initialized": "5bf76b9a984530bb1cc23bc92cc4ee1e3d9ec6b1dceb0aef7adb6d8cf102c67f", -"T3W1_en_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "1c550b50495b3b218082b998304d30599bac14062fc499fc68698855323ab432", -"T3W1_en_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "74a8687dae37c019c2e792218637bf7ca41de4493e3625aaf1c8d964222aa2c3", -"T3W1_en_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "445126332dbaae92848e5ba0fafcfb7a6b919ba17b6376966c2fcd92b58017b8", -"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "a7cc0d3abe3cabf737a7b27b5554c42ee1f2d704ecc7268224e6993642a50287", -"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "7df38b9b71add914eaa21ac7a9d12d2abc2c9ffc9bba01017665aee200174f07", -"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_change": "8e0036c56cf40d48ff62b7a1bf88363ec263bafc4eccc62bc6852a72cd935e7a", -"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_remove": "f544e7c5aaa29cf9d39c2bc4f7dc5d3d8525da210a02f79b0e537ad63e0f95cc", -"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "b474c7f6bdc85c692e53e5ebe5860396adf891320a0f86c67baf4d8db440f26b", -"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_setup": "7f62317780b1b03abcf73257b398ccbbfb6c377f17929c82201c942cba12f2b7", +"T3W1_en_device_menu-test_traverse_menu.py::test_traverse_initialized": "5bf64f7196aeb435b0e7c02cea43bc2b043d910d82c25e635a4665ac560a49fe", +"T3W1_en_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "518c8eb226e878926a28338f6fb8f6684d8c18ee696c7cd116a4520eb89d6277", +"T3W1_en_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "a6a1208a182f5ec6487d97969bdb6099e8c33a22ed13a6d69e0971efee08173a", +"T3W1_en_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "a29e0d8bf7569003fb3e998cec7b4b8422dcafac6f7098e8e4cb2f47bab89275", +"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "5fa6cf25dc33a38d25dfdb8e7bff066c9018d42f25365773a7973befacb6b168", +"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "ff70e282a496ffe05feb2682eee6711be347f4b87c7e334666cde71241ae3696", +"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_change": "edb20cd521a1f0399349ca60d7769274248aa0075659ec43f4c1debb50f5a5db", +"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_remove": "f73dbdfca8aaf2fa26d04ad10e8d03190a6cb7f72405712df2ceeed9e9686519", +"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "8d62ef9895209b20b46f20b3f1d5d26dfbb0544ed437aeab9e7670283927026c", +"T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_setup": "a6ed7a4c8539b986c320a8ce37ec3e3f71edadb960a8911e54dab660945f5fe1", "T3W1_en_device_menu-test_wipe_code.py::test_wipe_code_setup_pin_unset": "931d9afceb0ba1e4faae891775819277242d889644d5c0c5863fc8c9fcf859b1", -"T3W1_en_device_menu-test_wipe_device.py::test_wipe": "6fdc7430b50bbf030c52d210d527479af94c17bcc5fc4995940286c037b47854", +"T3W1_en_device_menu-test_wipe_device.py::test_wipe": "0e31129ce37f1cae01f2c81038c3338c9aec6cbf0caccbc48cf40750b216a998", "T3W1_en_test_autolock.py::test_autolock_does_not_interrupt_preauthorized": "557779ea86851374f928fdb294f40b423cad0f65b5278b39f2ddee3667856342", "T3W1_en_test_autolock.py::test_autolock_does_not_interrupt_signing": "29b8b844596bd3c60c37b82c2eadf379f032fd8c5940458f2592ba3145a45011", "T3W1_en_test_autolock.py::test_autolock_interrupts_passphrase": "15a27c56e2c6cce8aa40c2cd28464eaba4e03bbc6d858f7ff70106df51aabdfe", @@ -29168,50 +29168,50 @@ "T3W1_en_test_tutorial_eckhart.py::test_tutorial_menu_close": "25fa818c041533307c80efb73d493c61412383f9f6c817ab9195da67e74183d5", "T3W1_en_test_tutorial_eckhart.py::test_tutorial_menu_tropic": "29b8551a52eb50330baaa67f7a28a76b145637980bbe0cb20035c77eced80a91", "T3W1_en_test_tutorial_eckhart.py::test_tutorial_restart": "4275c0e10ac27947c77716470235bc63fc653683cb2360a51574fe35dd11a862", -"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "4b24d554ce95a138e32d54aba57d3f29db9c07542d808e0e4d3b620c95dd7f7c", -"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "653d55e808811821bdb0b3a0db27df639fdfcd891a5dc241a5ce7ac050847fe8", -"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "61a51a060aebe45e6b08670001917c67ee22da70843d90b6a2a7a160c52daada", -"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "57bfeec20e3b76eff5ec3e4c51c625b0121dab5b409287d540dd3287f8f04f93", -"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "ba7822eb76f6a6e794e2d0c2a304d0550c95a29dc8c19d7b01e63f5a909edd89", -"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "83f8eebdc91accc7d99b527906a18f9b34afb94d9460237bd4cc2fb303e370a0", -"T3W1_es_device_menu-test_check_backup.py::test_backup_check_cancel": "b3f6ffadaca8f775511561604baf7f20087bda3befd06cb935033312a0d342a1", +"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "4961b7136b7ffd31fc168ba629253afc31eca957b6a17fcb18879fdcc859f02e", +"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "f3edb224259ccc2824637733d44b8eb5a1ea07a23ac19acbd9f452511ec1c6fe", +"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "4642e2e972ea1ee3921f461e1fe792a415b4cad6a772da67a8f69dccdffc5b3d", +"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "cf1e6d02406018a1d857fefd4ff2927130f4a51233518d7ab18c6d2aa3db33ea", +"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "d412c85266717a8704a3ee403a4acccefb327c6396e3164dc80fcbf983a27b2e", +"T3W1_es_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "81c134eaeb479181b2c471382c56b89fd3e0f950eba43c8817691df9e829fb9a", +"T3W1_es_device_menu-test_check_backup.py::test_backup_check_cancel": "9d23f6602ee9595fe3fe26d266462aa1a2a7d282cc92c860485c19caded72f42", "T3W1_es_device_menu-test_check_backup.py::test_backup_needed_fails": "3bfddf8b7921ff08a6a9f36a1ef82bca928dca2948d71ca6506cca0aaf0d5455", "T3W1_es_device_menu-test_check_backup.py::test_no_backup_fails": "b793498c5928d31ce35348ef10b92121136c03de9ad435c906ee8091b79aa553", "T3W1_es_device_menu-test_check_backup.py::test_uninitialized_fails": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_es_device_menu-test_device_settings.py::test_brightness": "d2306b25a2783aaa3d897e6636826084c32a67b9dd05fb7cbbbd4e66ffbbc99e", -"T3W1_es_device_menu-test_device_settings.py::test_device_settings_uninitialized": "3f4d8fabff97fec043224b3756d5895119bc51523938f6d3a328efdefe474428", +"T3W1_es_device_menu-test_device_settings.py::test_brightness": "97b1c74ee1de886a91c02fab016913fbc7b67aa151766f050008f2f71095ce34", +"T3W1_es_device_menu-test_device_settings.py::test_device_settings_uninitialized": "eb4c495993d0a126ba37baf909856499de964ecb830246e00a1e6de43aa72b05", "T3W1_es_device_menu-test_device_settings.py::test_toggle_haptic": "56536ae9cd7c4ff8022def4ec3350031d3614064d961f53a2850f2be425af201", -"T3W1_es_device_menu-test_device_settings.py::test_toggle_led": "3287ab9943eac4ac1fc23d537078d4d688fcb54006bbc428abfd0378ba95f88c", -"T3W1_es_device_menu-test_label.py::test_change_label": "9d5471464c576da57f679a0520e7c50b56510260efdb48788ba96b3207c27519", -"T3W1_es_device_menu-test_label.py::test_label_cancel": "f57b8364eefdf75cfc73f2b412e19db08ba1f95b621e740ab37198eb99caf140", -"T3W1_es_device_menu-test_label.py::test_label_click_same_button_many_times": "a1889d56b782191ccb7df23b92935bfbceda56cd1fdc6ceca23811a93e2f87bf", -"T3W1_es_device_menu-test_label.py::test_label_cycle_through_last_character": "6bd494c712310ddc92552deff19af53c6f7b19bc064dd675afdfdd829a35d3d4", -"T3W1_es_device_menu-test_label.py::test_label_empty": "3131405e61436c6beffecd537d14157d4afff8243578ca1832247c6bffd279b5", -"T3W1_es_device_menu-test_label.py::test_label_loop_all_categories": "5cf3cb644694270a071f0ec2a0b4b48cc26aff1940b5475a71a11494d606c176", -"T3W1_es_device_menu-test_label.py::test_label_over_32_chars": "d8d5913022d280580939cb0d2e51715c21464493ed86caeb7c94d2390db8d159", +"T3W1_es_device_menu-test_device_settings.py::test_toggle_led": "16e78f6ed4bed2ac1aac6ffcba76bc1bc70ed5afbb8ca9b7bda14bf5269a9a54", +"T3W1_es_device_menu-test_label.py::test_change_label": "cdcc884f557dc1bc0374983e685b2f6837134e4db1a2fb06e40e196049b23931", +"T3W1_es_device_menu-test_label.py::test_label_cancel": "24ba1f211f398bedf5bba828e8980af0999bf8651d8956bffdf96502866d0a1a", +"T3W1_es_device_menu-test_label.py::test_label_click_same_button_many_times": "8e4becc15f4ec7b6ef5eec73d5b087b2ee887bd7e9d9f9866df9b0e38853a950", +"T3W1_es_device_menu-test_label.py::test_label_cycle_through_last_character": "fa633a1a9842db01a9fe8c3613243b216429de00d0832544ce2a665f2ceafbcd", +"T3W1_es_device_menu-test_label.py::test_label_empty": "3bf1f3e273f38bc9488a490b0d67f268e8237692c0e26086ba2819f89697637a", +"T3W1_es_device_menu-test_label.py::test_label_loop_all_categories": "80e5ce308cfdf3915d41d3ae771b84377851e81c3ff232995c5da5ee3eca73d5", +"T3W1_es_device_menu-test_label.py::test_label_over_32_chars": "c477265ae3c44fb700ddd5ab5d34607190a30742fbb5f4360401e6b5a5e5ee9f", "T3W1_es_device_menu-test_label.py::test_label_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", "T3W1_es_device_menu-test_notifications.py::test_backup_failed": "d8ea6a8c2f64754415607f9cf2cb0763893ba332c486e6aef6e7de467e4ef0bd", "T3W1_es_device_menu-test_notifications.py::test_backup_needed": "6aa38827cb41ae91cd198cc32c55daa19850442faf10587b751ef3f7ffb803bb", "T3W1_es_device_menu-test_notifications.py::test_pin_not_set": "2ab1512bcd289eddf3f436eddbdee5640a028cd8ae23132cb620f595bb51b7e8", "T3W1_es_device_menu-test_notifications.py::test_seedless": "bbe2a9d5b69d39d082b3c798a613e150a3ba9ce31e4c3dcff38a4d169f699ce3", -"T3W1_es_device_menu-test_pin.py::test_pin_cancel_intro": "f6c8b46097b3eedd6b4cfc6d3451b1f3f24a2af61e4cc22a0876e55b5d643a35", -"T3W1_es_device_menu-test_pin.py::test_pin_cancel_keyboard": "09b1aaca4c04598e785abd46dea2dd18f836fdb2a1880777dcaaab74dd3360d4", -"T3W1_es_device_menu-test_pin.py::test_pin_change": "6e9f353a7798e083e47f82bb8dde6383d55d54683778b76615fe4d8fce197b03", -"T3W1_es_device_menu-test_pin.py::test_pin_remove": "5925c4ab4348db24d2e7001a4d594cb90abdd3c1f0a83177ee6a5065bf89ed37", -"T3W1_es_device_menu-test_pin.py::test_pin_setup": "f8e9f9b2b3a53c9185598cd8f7a98cb43e94843ef2fa4f36205c11294b9178b7", +"T3W1_es_device_menu-test_pin.py::test_pin_cancel_intro": "adbcaae7106446dcc8317527065061d2266b4985470574b40804132b3a920551", +"T3W1_es_device_menu-test_pin.py::test_pin_cancel_keyboard": "ca0e38299b740e7167bb32bc26fc7c18fd8123136cd21ffe5db597cdebfa6091", +"T3W1_es_device_menu-test_pin.py::test_pin_change": "de270fc21371002244ce64471fb9556d4884d86ac0255c5fc74b331dad0ab1f1", +"T3W1_es_device_menu-test_pin.py::test_pin_remove": "98a5623b549e48bc5089e85b3b73a97535bc6dc75d9303ff57fa2c5e0c200f5c", +"T3W1_es_device_menu-test_pin.py::test_pin_setup": "e9b82a4aacedc0fcdc749d503f143b4325c03fdba000717c1e77b57fce38e678", "T3W1_es_device_menu-test_pin.py::test_pin_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_es_device_menu-test_traverse_menu.py::test_traverse_initialized": "b2845c5d7f125efabc7638eb8f9d55b12bd450d70777bb19992b542a798ed462", -"T3W1_es_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "641aa6e0e47c100dbcee594fbf1be7d9d24f6d9b0a401853ff12d1b6190956c1", -"T3W1_es_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "ef489556ff58179af446660c5cd255d57192816cfe66537bf656d44eeedb0d29", -"T3W1_es_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "2bbf5a2417a2a9a7f687ef05373317d08e0ac645267958da253039b372ad6c9e", -"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "24d4ca4b581892e44535cb91d5c62ea816b223b054ef780373a6648b6236cacd", -"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "613423ebe548376bd0c83da18b346bd47f0058b2ece4acf4fb77297629b68753", -"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_change": "00fae5dbda9eb0edd85c296f0f4acc9e6a0262169ed939d4779204bf56cacf2a", -"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_remove": "d7dcac3c4e6af0c744df4251be57c9fc5fdb6a46c4b79cf1cff00d16694cef3d", -"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "1f6a2c011ecb430334c4905361740455c8490c63bff28a11d5e4b2437ffdda11", -"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_setup": "d92f79637db1bb5d3a100c3f2cca1e39d6e3861b2542b71f423dd310c58724a8", +"T3W1_es_device_menu-test_traverse_menu.py::test_traverse_initialized": "daa7b7d329ffe248607d7e411703542ce5099d0d9b51003d569775e2c5700047", +"T3W1_es_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "c27260fa34a1e2df5a558d86cf836d0b6743a400743eb9d4d89b80e133971d6a", +"T3W1_es_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "c515aedd4c0753b9d13a099389a1f6f0ee1a1528c0bdb382c48e7700dce107f6", +"T3W1_es_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "d07aa98b36fdf0fae6c3b016b49a48e67ce26b0469924f6793b5bc3e7066731d", +"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "b0b9c77c941dec3f4516112a80a62b5d6af5ca340403299fab2ce276d908cd82", +"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "c77465d3f59a98d2a4d01d880ecd4f5f2847e3ec1c163ca4d99abb0de9afa62b", +"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_change": "88983a9c9f11f0c94ca31ce542f2f1f0d03fd2afcd19288822b8d93cdb306757", +"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_remove": "831ffef2040cc1f5da776f2fedd0fe4dabcae653073c2685ac535cf588ca26fa", +"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "59b4659fc5420569fdadb05ec886048954ef069363f3fbb2c23ccf5e130cf999", +"T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_setup": "2e3408025221a3a83405f6fc410678ccae5175a17b21e5dc287ee65d3ad40ac4", "T3W1_es_device_menu-test_wipe_code.py::test_wipe_code_setup_pin_unset": "56536ae9cd7c4ff8022def4ec3350031d3614064d961f53a2850f2be425af201", -"T3W1_es_device_menu-test_wipe_device.py::test_wipe": "8ebeabf8c5d62dc8f5102e761939d1765144f271354a424ce278b87cde4a22ec", +"T3W1_es_device_menu-test_wipe_device.py::test_wipe": "cf9d976a7b9d1d9f564887520443777962c79e8737a9c40ab3a9dff19456a6f0", "T3W1_es_test_autolock.py::test_autolock_does_not_interrupt_preauthorized": "8f5cc21473185596d28a7436215061ac9b7153d19b847c01a0c6b583b99134a7", "T3W1_es_test_autolock.py::test_autolock_does_not_interrupt_signing": "88db62680fc9cb956e2f1a3d0a40b6165b8fa8f358e07c8e211374885bd9c6df", "T3W1_es_test_autolock.py::test_autolock_interrupts_passphrase": "5a14c63ae00f2550de53c6133b5381e90f54a34653f682ee744d2ad24431ee18", @@ -29272,50 +29272,50 @@ "T3W1_es_test_tutorial_eckhart.py::test_tutorial_menu_close": "e7afb35ed2282c1698d35e4ed42ff1d21fa5d52fa630ded1d3a1bdafbb27c442", "T3W1_es_test_tutorial_eckhart.py::test_tutorial_menu_tropic": "a315c66064496afaf4e4ccb6a7900678470aae7c3462afa32f8b1efb6417f86a", "T3W1_es_test_tutorial_eckhart.py::test_tutorial_restart": "a40bd2d7b42521e139317fb6fe51507c3ea4c63f5e069632a00ee42a18317202", -"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "eb59f464f9ee4438479b782580530c6b27bd4f19b3d0d6cd5e1a17eaf5b8e480", -"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "a5b662c912fd0ba20ec3c76a55d6e4783c3613958cbde76a30df26105adf954f", -"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "b2cd2c3f1fab2ce82dc9827d8c7310abefda2537e0c23642568061e4d7e2388a", -"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "40718df3fe67f1e8a9570d7c876b9da07a0c440076559779ecac1c92390fbbb9", -"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "49bf82d1bf00e65e807e8ce8b1d475010ac1c61564de0edcc5199425cbb91740", -"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "055e1f0bc639f331b61578a44d1f429c0e3865f966515afa391bbcb7d05eec84", -"T3W1_fr_device_menu-test_check_backup.py::test_backup_check_cancel": "a920f389f05b312f49bfac0dffc4a525a398e66afdb3365b56759250ee1f6981", +"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "611b0e34cf1c96f8556d0660a695bdccd0b5f1645922fffc46c8a6c5b4dbf746", +"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "5e199698b3ac10f9b2cd8d6d0f6ce751adb8ed13788413fb0c6acfa083839e20", +"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "f986e7de21bb064aebafabfac8262ad2b9863d747b3524047b0eb7cac0faf748", +"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "c94af2d66d33d35cfee8f442298ba6b41d67a2a1fcd28f41bbdbff6c94c11b22", +"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "98655e618761a207d4f67ee296217f7b200d07a1fbe5599b28e9fb391bae8f95", +"T3W1_fr_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "c44fa39e47c5b8f77dc8b3a1ab68b107091fb7c7ed8f8ed566854f2d1b06aff5", +"T3W1_fr_device_menu-test_check_backup.py::test_backup_check_cancel": "7251a1a0375b4c9b9c35e92eab4c1f6982c9d69f7e7e45fa901cb5c42092286c", "T3W1_fr_device_menu-test_check_backup.py::test_backup_needed_fails": "127e600409c0d92ab7ca95ae516b91d97b34aa4d1b8c38271d70b17810188cdc", "T3W1_fr_device_menu-test_check_backup.py::test_no_backup_fails": "3ca3d8dcf5e57671e92ae1dca97e7766dfbd9e3ffaf9343731d4f06580235fc9", "T3W1_fr_device_menu-test_check_backup.py::test_uninitialized_fails": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_fr_device_menu-test_device_settings.py::test_brightness": "95293bb472ace62de68569f60e798ac2956d8e4aea6ad050b593d60cdcad22e3", -"T3W1_fr_device_menu-test_device_settings.py::test_device_settings_uninitialized": "9723a8ec59a7fd1a787913c0f209393fa1b323564be0d284230d5660ee8a42d7", +"T3W1_fr_device_menu-test_device_settings.py::test_brightness": "048ac1daf4e95c11385d35db5f81ae498fd4498311bda9081444978426f5a56d", +"T3W1_fr_device_menu-test_device_settings.py::test_device_settings_uninitialized": "6f5509828cbf7732c119ef1fea4245009195ca815036d7475307b3ab4138a36c", "T3W1_fr_device_menu-test_device_settings.py::test_toggle_haptic": "e8156cf4eda1d29060f05b4a18d50032b0b344230609b7de7cababfe0a86b20b", -"T3W1_fr_device_menu-test_device_settings.py::test_toggle_led": "d996488b5e7e338375f8dd573ce3468eb376e0e71ee9b7b72b78d7d0bebe29d7", -"T3W1_fr_device_menu-test_label.py::test_change_label": "22ee92b8e8e1ac215e94615f726f4dd5e6e47f589b818046dd38e822381ce28f", -"T3W1_fr_device_menu-test_label.py::test_label_cancel": "75308e0761fd988f1e0714ecb613c89f52c2f4e2b426bc94ece2db0acbbdfe3e", -"T3W1_fr_device_menu-test_label.py::test_label_click_same_button_many_times": "ef9bc6d40a882fd298f2d0987f07df925f29a0186709127505ce5e8b3a010a7a", -"T3W1_fr_device_menu-test_label.py::test_label_cycle_through_last_character": "8e56a7c8493384f4dfdd2cd45285a1fd5ed3d235c196032c89ca617d8b8919e2", -"T3W1_fr_device_menu-test_label.py::test_label_empty": "896dc0078df96eb02bcf04c7cfb2181d6a8b8c88e265b44ee24f1d112048ebbf", -"T3W1_fr_device_menu-test_label.py::test_label_loop_all_categories": "2ce734077ad08b5a0534c847ca78ccee1745618c23772c3dc2a59151be96a68f", -"T3W1_fr_device_menu-test_label.py::test_label_over_32_chars": "7610f137e478464e7131451e3a5f289a81f5653c414991f58899f0b1a69e5273", +"T3W1_fr_device_menu-test_device_settings.py::test_toggle_led": "d1449b3ba32aa7ac0fde5c9977aed14a6bf9de87a00db101c3ddcf7b99b0a245", +"T3W1_fr_device_menu-test_label.py::test_change_label": "8db21e8395f164a3b117d2f7564fafec20639656d7ceda7349b0519f49833bbd", +"T3W1_fr_device_menu-test_label.py::test_label_cancel": "a083869a0233da9895440b30de0dc99fe3b9d817a8780788c2d7a383a9c33c7c", +"T3W1_fr_device_menu-test_label.py::test_label_click_same_button_many_times": "76166c0c38390db6b69cccaf14e9dafad8985d34be9214d8b637cd10927dbbfe", +"T3W1_fr_device_menu-test_label.py::test_label_cycle_through_last_character": "0f05e7ebd8392493a7fef3902f8f4b7cf64124de095be75ab9196a452830c71d", +"T3W1_fr_device_menu-test_label.py::test_label_empty": "249cc5a2799f89a13ad1de5f22363bb32f9e298090bdbddec7f19dd4250941ca", +"T3W1_fr_device_menu-test_label.py::test_label_loop_all_categories": "a886b9ea4888d4140f75af9aecdf342a6112e8622f5ff41216572ba90c610895", +"T3W1_fr_device_menu-test_label.py::test_label_over_32_chars": "8e4c7e34b54fabbac094eae895cc83a167116068546a917cc0ac14043136fe1c", "T3W1_fr_device_menu-test_label.py::test_label_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", "T3W1_fr_device_menu-test_notifications.py::test_backup_failed": "12fa35f18054ec9daef26ee418c82f861c1bd417004788635eecbbd4c8e9269d", "T3W1_fr_device_menu-test_notifications.py::test_backup_needed": "55f2d653365928a4598ac9f4cb769dc4d72558d50011f5e27ecfa11ec1ef763e", "T3W1_fr_device_menu-test_notifications.py::test_pin_not_set": "784e30b2b94071e1210aeba841cd59adbe560106673d61142923a212351a25d6", "T3W1_fr_device_menu-test_notifications.py::test_seedless": "e6dd2332fbc9992e5418d109ba6d3b9089712500dc0ba9cb4a33fcf3d91ba552", -"T3W1_fr_device_menu-test_pin.py::test_pin_cancel_intro": "0f22f9a94a4e5e94a5310e2e2a43248be1b193092deba5d74b00d752252b8001", -"T3W1_fr_device_menu-test_pin.py::test_pin_cancel_keyboard": "770db9f4e899fa49bb6c5b520b9dab1c45769918680d820c2463160957246ddc", -"T3W1_fr_device_menu-test_pin.py::test_pin_change": "6bc4478284667fa259a50c67b8ed77bb106c791f6fb4611a266cd367a5868894", -"T3W1_fr_device_menu-test_pin.py::test_pin_remove": "94b4dac0087fcb1626347f3e51b6553a9bbccde10b7e9c721652b56047d18463", -"T3W1_fr_device_menu-test_pin.py::test_pin_setup": "e5ab511c96e71d964ba36a61359230d61449b5e99bf3aa6c4f9a37db7911a989", +"T3W1_fr_device_menu-test_pin.py::test_pin_cancel_intro": "af3902476a5ff36ba0c45df26f53c96445be9604e9b857dffdca4bb7a56aee76", +"T3W1_fr_device_menu-test_pin.py::test_pin_cancel_keyboard": "a562f2d0fc7f3e9a9c4fe822e0ac1565e8597e4e4a30153a393589dc74f51024", +"T3W1_fr_device_menu-test_pin.py::test_pin_change": "3769806f321adeb54705e5803d3a83b336de8e3dabd69b3db8809927e92ed4d8", +"T3W1_fr_device_menu-test_pin.py::test_pin_remove": "b2fa76439f7181bbfd691943a25ffd69f43bf1cca14789b8e3378e89610312e7", +"T3W1_fr_device_menu-test_pin.py::test_pin_setup": "3a2d6490b01e57440f082211b4d1a00aec6d4082c815ca667e53ee5343d83628", "T3W1_fr_device_menu-test_pin.py::test_pin_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_fr_device_menu-test_traverse_menu.py::test_traverse_initialized": "b631a78f4d12f7de508911a12bcba5ced3011e4c17cce0fb8b6877f9d66263e2", -"T3W1_fr_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "858c0c9ab1527349e29b2bc12a1b0fbd77b9ed564beb043f4a6f9b8caaa3f70e", -"T3W1_fr_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "fbc10b1d40964afbdc1296b91df3056dc73cdb2d1ac639be161fbcc7ac8e3724", -"T3W1_fr_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "1705d60beaa7e546b96a8e5e54e46664964b435bb5142893a58db113c355c45e", -"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "55786b82b490e093317c10f04fd56f3660f50f86b10d78ace52343a3b8f5d472", -"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "8495618ac88d40eb5465c9bb0ddf6bdb43e01610ac4e6ac3d1e5bc3f9a20b2b1", -"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_change": "f195c1e916adab63cd58cdd80676f0174fb69c6b6caf742f772c35eaf84e6e9b", -"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_remove": "74e1d651f748b5901e509a3bf511f4bef148f5f60321cb8ae2d2df6df2c582b2", -"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "cbc2a1a6917d7fa182c1e1bd3debb9449d697a4a127ec9f86e810d3bdb022cec", -"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_setup": "e5a9b0f20ae0be26fcfc32804662586f442b9769e62242e0524cd900edb54152", +"T3W1_fr_device_menu-test_traverse_menu.py::test_traverse_initialized": "35a32708b08d57f72a760db25c92b3021db8dcca6cf6cea78067bf0e9e089c7a", +"T3W1_fr_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "20244d57db9436630cb77d4e66d7533b19c1f98ddc84af43386cc590e3a0e4bd", +"T3W1_fr_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "a7ae8ed320c50b59d0b12d8f4cd5892facdca13c112fec0ae2772ccdfcb018a0", +"T3W1_fr_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "466268fc083b569a2de045cf9ef345f969d24ee0e6e5062fede2d3ed4285bb56", +"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "3a569f712f249fc543af91693048f11612283c0e2ca39e31fe631e2979bd1c4d", +"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "52b01ab0f7553d1e96f27b6c03a7289c0ccc4904dbdb0d423a246bb8ce23641b", +"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_change": "ad83b5a4c6c55143821f60632aba262ba2d56d250eab611887d5677f4d10e940", +"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_remove": "df95979d0b1c334f7afcbafcadfda62ca07ac762185556566dadb93b136f7dfb", +"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "ad79ad9afbec340acea558c62bb8ebe952663eb903b9215ce17fcba2167b92f6", +"T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_setup": "35b78db724cd70fe66ddd6fdcd4700b6aee08ee40d40e8eb3f139433874b05b8", "T3W1_fr_device_menu-test_wipe_code.py::test_wipe_code_setup_pin_unset": "e8156cf4eda1d29060f05b4a18d50032b0b344230609b7de7cababfe0a86b20b", -"T3W1_fr_device_menu-test_wipe_device.py::test_wipe": "7df04602f8f76c40b9c46309137c2b3cbf79ecb10e7143ba1f06ee2e57533fb3", +"T3W1_fr_device_menu-test_wipe_device.py::test_wipe": "da91c5617c9d666912bda1ab2a6e93a7ded957fce648efc27f3d435eb155d5ad", "T3W1_fr_test_autolock.py::test_autolock_does_not_interrupt_preauthorized": "033d25920de623b8532034e81a42b29ad3f722b0db51e766c1e8b4b2c1cb0551", "T3W1_fr_test_autolock.py::test_autolock_does_not_interrupt_signing": "dd70255d551f4329e319440e3d6f8fd0bcece222b23f4463227589e5ee4bf3c1", "T3W1_fr_test_autolock.py::test_autolock_interrupts_passphrase": "1478ed476972b9b84057b9da68a9f7e7528ed24576b857deca78cfad33bc0aaf", @@ -29376,50 +29376,50 @@ "T3W1_fr_test_tutorial_eckhart.py::test_tutorial_menu_close": "701080e2d1fbaf8ce5aa02e0325d118e3497f9cb6f443c87f2084a352fccc30f", "T3W1_fr_test_tutorial_eckhart.py::test_tutorial_menu_tropic": "a78479d4d1728fdb4f33db301e7c77e8a93b3fb1dacdaf056a578b0fb3bb3c0e", "T3W1_fr_test_tutorial_eckhart.py::test_tutorial_restart": "dc02c3bfef15c0c89e16ecea9d1d4a52b626a33cbe775673b401e7861bfed24a", -"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "d13b1589af5355523f27393d9af5cd5ba064ec13427275fd8d00c2e970a2fed4", -"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "87af5d356c5837012020f3833a4fbcb48013af9c18d6754d6473f068721f58ef", -"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "d2d314dd3a9eda620e52d260495865a58e419b35e5effdc6902c16acd992d105", -"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "832aab8dbd483accdd003a5beb047a67d0fbd3a71cbe99def833f70effee201f", -"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "ff96545a413bb5610934d69e787107f1e4d1618c056dbf1768452bf747d4517a", -"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "27f003b28330f931ca270da0da713bc40852cbd635892ed1e68acb7563161efd", -"T3W1_pt_device_menu-test_check_backup.py::test_backup_check_cancel": "78231bbb1e25a13d6b12e364bdfbd5bbb33f5a4f9c5380032baba4ccffba3e3b", +"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_battery_cancel": "aec992d55ffdf2987f1f3ee3c70e73d59b13d30cc8187b83ca741c18ad9c8a26", +"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_battery_change": "4aa67ed11e1513009ede4bb0989b36aebad9b692245bfe449e7d5bae2685f898", +"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_pin_not_set": "d4a5c52b735617ebdbcc1b283b984748245187d8ab0ecbfe945ba2f34f2d1cec", +"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_uninitialized": "e898f13b43819800c787a9f8362817ac5450fefae6ab007d13b7fe59434c17ef", +"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_usb_cancel": "1ecd9a8a30ec6e969fd7ee74d61b7be08e240318f217737671d6d262d56bd642", +"T3W1_pt_device_menu-test_auto_lock.py::test_auto_lock_usb_change": "d489cfd4dea07e6ee843818fe8eada919db90fce93c3ea2d2c8ceb2dda5b0652", +"T3W1_pt_device_menu-test_check_backup.py::test_backup_check_cancel": "11ba5fee06096ee59de1cf057147632fae4d4f9574c06740f0225562a306a7e3", "T3W1_pt_device_menu-test_check_backup.py::test_backup_needed_fails": "ebf419d5203b62a9a6cb6874dfebce5b4ee78d089b5aa4a40fccf8bfb11c6fc9", "T3W1_pt_device_menu-test_check_backup.py::test_no_backup_fails": "3ed92e9990189e2e2ca4614e55e9abee6f5373778f3501a53cd4074f3d07d02b", "T3W1_pt_device_menu-test_check_backup.py::test_uninitialized_fails": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_pt_device_menu-test_device_settings.py::test_brightness": "88a816a1fcd3aa783240bc84ffe3400ebcfb696615c8047446b6a6d1d26938ba", -"T3W1_pt_device_menu-test_device_settings.py::test_device_settings_uninitialized": "30424ee2d056184e35c844976b71acf82e17f582330ebbb43f1a2162fea03171", +"T3W1_pt_device_menu-test_device_settings.py::test_brightness": "fa2a78c12767bfa55b494efaa4e007b932da5f3332cdce16044fdbab375c18f0", +"T3W1_pt_device_menu-test_device_settings.py::test_device_settings_uninitialized": "e5597116053a77ddee44c714dc1b23af0821205d60aad0abff736242b9cb3553", "T3W1_pt_device_menu-test_device_settings.py::test_toggle_haptic": "1c74667c078e25e7e0d37c7b2aa35f7c8ab02cd88e74695ecb355c82a293b0cc", -"T3W1_pt_device_menu-test_device_settings.py::test_toggle_led": "057fb267dca037c97131293bc56ffcb46020a9a712c2388d763a74a5885c17d0", -"T3W1_pt_device_menu-test_label.py::test_change_label": "ad6c7252bcd2c1182406fb9a38711a83a2c0752c34ebe1de07850fe17dcf961f", -"T3W1_pt_device_menu-test_label.py::test_label_cancel": "fef0dd521c457d96c1a52d429d3bfb9886aba8558b5d78f34be017c0222b9585", -"T3W1_pt_device_menu-test_label.py::test_label_click_same_button_many_times": "a1e7a9dd6bdddbe8f1b0767852209ac6319686a48f62252a9d7c157aca9131de", -"T3W1_pt_device_menu-test_label.py::test_label_cycle_through_last_character": "b014008eaf5ca8b2c9d47dbeed3596d6c4fee70e20ae0047709a2a6e313e44c1", -"T3W1_pt_device_menu-test_label.py::test_label_empty": "d70c7e543a57b5e8a54d1dfbb699bc7949b1ca4a0abd21c4d9df42953575b18a", -"T3W1_pt_device_menu-test_label.py::test_label_loop_all_categories": "551ac62bae4dad82cada8cc5d1c6d40fb8690b70435b7b8284096226d3990cef", -"T3W1_pt_device_menu-test_label.py::test_label_over_32_chars": "7a44adc93f3b7c9d704d84abf7f6ca76e6f3d5e042bfb4e8f7a61adcf62311ab", +"T3W1_pt_device_menu-test_device_settings.py::test_toggle_led": "48f33df5bb15b870c587a8ad36159ceef7fe18166513c6acd9153e98aafd66a1", +"T3W1_pt_device_menu-test_label.py::test_change_label": "8bb394259731ee7db752f5199b9a562d217b9403a63ef8595c7ffb3eb5114622", +"T3W1_pt_device_menu-test_label.py::test_label_cancel": "554ec35274e5489824c5eadb484e6b9b80ff5928f27628db92c9b18680c47667", +"T3W1_pt_device_menu-test_label.py::test_label_click_same_button_many_times": "6e8ee3390bb06ec10f9e096ba19be636c473f3c5bf6eca56ec0a34814c34533b", +"T3W1_pt_device_menu-test_label.py::test_label_cycle_through_last_character": "5649b3e2bcec511fa73082f427322398b49e01c6361c3000ea9cd6effbc359d4", +"T3W1_pt_device_menu-test_label.py::test_label_empty": "3ade2d3808ff9e40d17bddd7f1c88a400650c01393d16686541ad3450aab1bef", +"T3W1_pt_device_menu-test_label.py::test_label_loop_all_categories": "d38464b21a537af35410f1b237bf5972c124f7a3f60bbbe754d82a9910514454", +"T3W1_pt_device_menu-test_label.py::test_label_over_32_chars": "df6173241280cb1e94ff4623337eb32b947fd2f4c5747ac87c9f6297288269fa", "T3W1_pt_device_menu-test_label.py::test_label_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", "T3W1_pt_device_menu-test_notifications.py::test_backup_failed": "3b0b7ffb7cc02b9d281c08617edcdea4afc79ff87e3876bba6010a3350ff83e3", "T3W1_pt_device_menu-test_notifications.py::test_backup_needed": "29bc804809d98b87ecb26b473ae96585859e4f85cc9704c346531a312e27ef3b", "T3W1_pt_device_menu-test_notifications.py::test_pin_not_set": "eb23fffc070fe8a95c022175ba5a49e488e836f9a0912e9ad99c5c45de9c27e8", "T3W1_pt_device_menu-test_notifications.py::test_seedless": "27b59acec95fd7a89dbd4b4b85c45f4e21313c568c84c1de0a984eed084506ad", -"T3W1_pt_device_menu-test_pin.py::test_pin_cancel_intro": "d367ae043c0bf8c6b8a7564a9b71cb610abb7f500aff1ef13bbfc39025e8d604", -"T3W1_pt_device_menu-test_pin.py::test_pin_cancel_keyboard": "915edd96afbfad0da047c7959a64403280bf491aaa5661d3ce58052f4970f3b4", -"T3W1_pt_device_menu-test_pin.py::test_pin_change": "61b41f6fc72137b9dc4c73a6b460417011656de30de6faf531ae45b923eddb1e", -"T3W1_pt_device_menu-test_pin.py::test_pin_remove": "13653996f5ad426fa22580c8e7445852e26551971e0c43966463077b5eb9ed25", -"T3W1_pt_device_menu-test_pin.py::test_pin_setup": "226b317394ac6ae368f0291ea981e5bdfba5bdb731fd9912f47770e4e5ada3e3", +"T3W1_pt_device_menu-test_pin.py::test_pin_cancel_intro": "86f0769236d0bc9ab2404e32cba68b1f508f869c139091f1c50998daa81377aa", +"T3W1_pt_device_menu-test_pin.py::test_pin_cancel_keyboard": "4036b012c90e706cbebcc586aef89420a0aecc203f2c6a1e8495cd17f6cd2e29", +"T3W1_pt_device_menu-test_pin.py::test_pin_change": "97f95779c2dec54245da0a3f4827adc8b42557a0db608128d45d05c14308e37b", +"T3W1_pt_device_menu-test_pin.py::test_pin_remove": "6944f5ec693db48443ba9953ff46fb9109824a33b5933a37aad882ae0db8d6a9", +"T3W1_pt_device_menu-test_pin.py::test_pin_setup": "8c3326df890d11de0f5ec7eefe472b55cc722afe8669159a2f9d0f0eda68c67b", "T3W1_pt_device_menu-test_pin.py::test_pin_uninitialized": "987d1c62e6576b9cc24373e00df522efdc9736af978d6032f7d319af8daaef5d", -"T3W1_pt_device_menu-test_traverse_menu.py::test_traverse_initialized": "510be3f4148429af7d44b4d437d80516d6751da4aba9e33c33834a20054b8a2a", -"T3W1_pt_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "e3afb7c189951e06815d4c0d084043b4e243f28d88c306fadd5a82781f130d1c", -"T3W1_pt_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "0c8e4c4d67d5a995fd9d8b9fcee774bc72fe3f26b95fec1bb53ab2e1f5ddb8a1", -"T3W1_pt_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "ed22eb8f18cf6eef781d5ca0ec9455b8786cb872dc43fee5710cc9a4599bd597", -"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "2a4b7a16aeb25dcdbe17de4497bec86ed858bdc52ece0f41bf037ea0477b4812", -"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "c67afc5e8dbec303de29ce2bf85cddeb9f05a7ae5eca58447b0210e7b6b89b28", -"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_change": "6be8313c93fd4679aa31423e7ad00454b9954b40c01074a71629637880b7c8f5", -"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_remove": "64f009c98ec61567bc71d1d8e7993056b459c85e71851dba04501e59df699afe", -"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "7b04c10bc3be50fe2805e7fcb5b06cbbaa63aa5fec4b66ecee24532139d3cd7d", -"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_setup": "e70074e8d17b0db3d78a59adb3b43a2a8f7b5fc8fd470d13bb4077820dcb6732", +"T3W1_pt_device_menu-test_traverse_menu.py::test_traverse_initialized": "e951ce3c7b58ebc4068b0fb3ed67643729f88a6f2d79d41daf4ddc2aa715bde1", +"T3W1_pt_device_menu-test_traverse_menu.py::test_traverse_initialized_no_pin": "8102becacc271e965dce0becb002b5a29a7f7f42b72e7b4b778919951f038a71", +"T3W1_pt_device_menu-test_traverse_menu.py::test_traverse_uninitialized": "f97631b14121c5888fbe22bb4864fa068bc0fc73574c56e29a53ef59a9b94979", +"T3W1_pt_device_menu-test_wipe_code.py::test_remove_pin_without_removing_wipe_code": "4473e5dcb237af7c7c0ca7850bcd72930911e8b924391f11e93a8f872bb8d272", +"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_cancel_intro": "ee3e66538810f9ca4a3486abc8ef3de7f27f8c34347da098f93d15fbe302e782", +"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_cancel_keyboard": "02ac5d15ea59ee44f17a4589e44c17f35dc22d0d78df5b7bf0d83f00e601bde1", +"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_change": "3a41a4ec48690c80d5e170eab104b9bc1f5bad27b8a31f77c5953d81003abcb2", +"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_remove": "054bb7bc0c54733019baa61c665d4c055cc260bc20b4da5e67ba6730b4aacac0", +"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_same_as_pin": "2ab87670ce2bb327010cb8440132e7353bb8dfe620962d1df742ce1eb5d28cbb", +"T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_setup": "bfd6628fac442c91489a50d81776072e42cf9ab9f7866377c7b22471e12570db", "T3W1_pt_device_menu-test_wipe_code.py::test_wipe_code_setup_pin_unset": "1c74667c078e25e7e0d37c7b2aa35f7c8ab02cd88e74695ecb355c82a293b0cc", -"T3W1_pt_device_menu-test_wipe_device.py::test_wipe": "2a75811b7f84dc217f882102e6fb7081c1776349b32723c1a07e9cfeea89eafc", +"T3W1_pt_device_menu-test_wipe_device.py::test_wipe": "48674a9ba0a64170c8c72f15aa9611492bd2423f25203da9efa8b1eedf911b1f", "T3W1_pt_test_autolock.py::test_autolock_does_not_interrupt_preauthorized": "7bb8b0d985bebc7ed0d7c7af782f24986c0e5a87b1823885ac7e4c19b839471b", "T3W1_pt_test_autolock.py::test_autolock_does_not_interrupt_signing": "fea34a2d4fe164668747c17e39fbd45e66f8cc22d11fdf7ab2d050fa933daadd", "T3W1_pt_test_autolock.py::test_autolock_interrupts_passphrase": "f29b2fa0d6cac2c357ac0684b75298af71e39f7ceae99fa8e460367f84470fef",