diff --git a/core/.changelog.d/3735.added b/core/.changelog.d/3735.added new file mode 100644 index 00000000000..efa452a5838 --- /dev/null +++ b/core/.changelog.d/3735.added @@ -0,0 +1 @@ +[T3T1,T3W1] Haptic support in emulator. diff --git a/core/SConscript.unix b/core/SConscript.unix index 644b741e097..5d0d70ed40d 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -26,6 +26,7 @@ FEATURES_WANTED = [ "ble", "display", "dma2d", + "haptic", "input", "kernel_mode", "optiga", diff --git a/core/embed/io/display/inc/io/unix/sdl_display.h b/core/embed/io/display/inc/io/unix/sdl_display.h index 4530a95c349..74c99e2ba52 100644 --- a/core/embed/io/display/inc/io/unix/sdl_display.h +++ b/core/embed/io/display/inc/io/unix/sdl_display.h @@ -1,8 +1,17 @@ #pragma once +#ifdef USE_HAPTIC +#include +#endif /* USE_HAPTIC */ #include #ifdef USE_RGB_LED // Update the RGB LED color in the emulator void display_rgb_led(uint32_t color); #endif + +#ifdef USE_HAPTIC +// Update the haptic color in the emulator +void display_haptic_effect(haptic_effect_t effect); +void display_custom_effect(uint32_t duration_ms); +#endif /* USE_HAPTIC */ diff --git a/core/embed/io/display/unix/display_driver.c b/core/embed/io/display/unix/display_driver.c index e610116949a..3a37238a1db 100644 --- a/core/embed/io/display/unix/display_driver.c +++ b/core/embed/io/display/unix/display_driver.c @@ -82,6 +82,12 @@ typedef struct { // Color of the RGB LED uint32_t led_color; #endif + +#ifdef USE_HAPTIC + uint32_t haptic_color; + uint32_t haptic_expire_time; +#endif /* USE_HAPTIC */ + } display_driver_t; static display_driver_t g_display_driver = { @@ -91,6 +97,7 @@ static display_driver_t g_display_driver = { //!@# TODO get rid of this... int sdl_display_res_x = DISPLAY_RESX, sdl_display_res_y = DISPLAY_RESY; int sdl_touch_offset_x, sdl_touch_offset_y; +int sdl_touch_x = -1, sdl_touch_y = -1; static void display_exit_handler(void) { display_deinit(DISPLAY_RESET_CONTENT); @@ -189,6 +196,11 @@ bool display_init(display_content_mode_t mode) { drv->led_color = 0; #endif +#ifdef USE_HAPTIC + drv->haptic_color = 0; + drv->haptic_expire_time = 0; +#endif /* USE_HAPTIC */ + gfx_bitblt_init(); drv->initialized = true; @@ -386,6 +398,102 @@ void draw_rgb_led() { } #endif // USE_RGB_LED +#ifdef USE_HAPTIC + +void display_haptic_effect(haptic_effect_t effect) { + display_driver_t *drv = &g_display_driver; + if (!drv->initialized) { + return; + } + + switch (effect) { + case HAPTIC_BUTTON_PRESS: + drv->haptic_color = 0xFF0000; // Red + drv->haptic_expire_time = SDL_GetTicks() + 200; // 200 ms duration + break; + case HAPTIC_HOLD_TO_CONFIRM: + drv->haptic_color = 0xFF00; // Green + drv->haptic_expire_time = SDL_GetTicks() + 500; // 500 ms duration + break; + case HAPTIC_BOOTLOADER_ENTRY: + drv->haptic_color = 0x0000FF; // Blue + drv->haptic_expire_time = SDL_GetTicks() + 500; // 500 ms duration + break; + default: + drv->haptic_color = 0x0; + break; + } + + display_refresh(); +} + +void display_custom_effect(uint32_t duration_ms) { + display_driver_t *drv = &g_display_driver; + if (!drv->initialized) { + return; + } + + drv->haptic_color = 0xFFA500; // Orange + drv->haptic_expire_time = SDL_GetTicks() + duration_ms; + display_refresh(); +} + +void draw_haptic() { + display_driver_t *drv = &g_display_driver; + if (!drv->initialized) { + return; + } + + if (SDL_GetTicks() > drv->haptic_expire_time) { + drv->haptic_color = 0; // Clear + return; + } + + const uint32_t color = drv->haptic_color; + + if (color == 0) { + return; // No LED color set + } + + // Extract RGB components + uint32_t r = (color >> 16) & 0xFF; + uint32_t g = (color >> 8) & 0xFF; + uint32_t b = color & 0xFF; + + // Define touch circle properties + const int radius = 5; + +#ifdef USE_TOUCH + int center_x = sdl_touch_x; + int center_y = sdl_touch_y; +#else + int center_x = DISPLAY_RESX / 2; + int center_y = DISPLAY_RESY + 20; +#endif /* USE_TOUCH */ + + // Position based on background + if (drv->background) { + center_x += TOUCH_OFFSET_X; + center_y += TOUCH_OFFSET_Y; + } else { + center_x += EMULATOR_BORDER; + center_y += EMULATOR_BORDER; + } + + // // Draw the touch circle + SDL_SetRenderDrawColor(drv->renderer, r, g, b, 255); + + for (int y = -radius; y <= radius; y++) { + for (int x = -radius; x <= radius; x++) { + if (x * x + y * y <= radius * radius) { + SDL_RenderDrawPoint(drv->renderer, center_x + x, center_y + y); + } + } + } + SDL_SetRenderDrawColor(drv->renderer, 0, 0, 0, 255); +} +#endif /* USE_HAPTIC */ + void display_refresh(void) { display_driver_t *drv = &g_display_driver; @@ -424,6 +532,10 @@ void display_refresh(void) { draw_rgb_led(); #endif +#ifdef USE_HAPTIC + draw_haptic(); +#endif + SDL_RenderPresent(drv->renderer); } @@ -538,6 +650,28 @@ void display_save(const char *prefix) { drv->buffer->format->Rmask, drv->buffer->format->Gmask, drv->buffer->format->Bmask, drv->buffer->format->Amask); SDL_BlitSurface(drv->buffer, &rect, crop, NULL); + +#ifdef USE_HAPTIC + // === Static haptic dots === + if (SDL_GetTicks() < drv->haptic_expire_time) { + uint32_t color = drv->haptic_color; + + if (color != 0) { + // Extract RGB components + uint32_t r = (color >> 16) & 0xFF; + uint32_t g = (color >> 8) & 0xFF; + uint32_t b = color & 0xFF; + // Draw a one-pixel dot + if (sdl_touch_x >= 0 && sdl_touch_x < rect.w && sdl_touch_y >= 0 && + sdl_touch_y < rect.h) { + Uint8 *pixel_ptr = (Uint8 *)crop->pixels + (sdl_touch_y * crop->pitch) + + (sdl_touch_x * crop->format->BytesPerPixel); + *(Uint32 *)pixel_ptr = SDL_MapRGBA(crop->format, r, g, b, 255); + } + } + } +#endif // USE_HAPTIC + // compare with previous screen, skip if equal if (drv->prev_saved != NULL) { if (memcmp(drv->prev_saved->pixels, crop->pixels, crop->pitch * crop->h) == diff --git a/core/embed/io/haptic/unix/haptic_driver.c b/core/embed/io/haptic/unix/haptic_driver.c new file mode 100644 index 00000000000..079fbcceaad --- /dev/null +++ b/core/embed/io/haptic/unix/haptic_driver.c @@ -0,0 +1,136 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include +#include + +#ifdef KERNEL_MODE + +// Driver state +typedef struct { + bool initialized; + bool enabled; +} haptic_driver_t; + +// Haptic driver instance +static haptic_driver_t g_haptic_driver = { + .initialized = true, + .enabled = false, +}; + +bool haptic_init(void) { + haptic_driver_t *driver = &g_haptic_driver; + + if (driver->initialized) { + return true; + } + + memset(driver, 0, sizeof(haptic_driver_t)); + + driver->initialized = true; + driver->enabled = true; + + return true; +} + +void haptic_deinit(void) { + haptic_driver_t *driver = &g_haptic_driver; + memset(driver, 0, sizeof(haptic_driver_t)); +} + +void haptic_set_enabled(bool enabled) { + haptic_driver_t *driver = &g_haptic_driver; + + if (!driver->initialized) { + return; + } + + driver->enabled = enabled; +} + +bool haptic_get_enabled(void) { + haptic_driver_t *driver = &g_haptic_driver; + + if (!driver->initialized) { + return false; + } + + return driver->enabled; +} + +bool haptic_test(uint16_t duration_ms) { + haptic_driver_t *driver = &g_haptic_driver; + + if (!driver->initialized) { + return false; + } + + if (!driver->enabled) { + return true; + } + + // display_effect(0xff, duration_ms); + return true; +} + +bool haptic_play(haptic_effect_t effect) { + haptic_driver_t *driver = &g_haptic_driver; + + if (!driver->initialized) { + return false; + } + + if (!driver->enabled) { + return true; + } + + switch (effect) { + case HAPTIC_BUTTON_PRESS: + display_haptic_effect(HAPTIC_BUTTON_PRESS); + return true; + case HAPTIC_HOLD_TO_CONFIRM: + display_haptic_effect(HAPTIC_HOLD_TO_CONFIRM); + return true; + case HAPTIC_BOOTLOADER_ENTRY: + display_haptic_effect(HAPTIC_BOOTLOADER_ENTRY); + return true; + default: + return false; + } +} + +bool haptic_play_custom(int8_t amplitude_pct, uint16_t duration_ms) { + haptic_driver_t *driver = &g_haptic_driver; + + if (!driver->initialized) { + return false; + } + + if (!driver->enabled) { + return true; + } + + display_custom_effect(duration_ms); + return true; +} + +#endif // KERNEL_MODE diff --git a/core/embed/io/rgb_led/inc/io/rgb_led.h b/core/embed/io/rgb_led/inc/io/rgb_led.h index 8f12c004022..824f5ce48c9 100644 --- a/core/embed/io/rgb_led/inc/io/rgb_led.h +++ b/core/embed/io/rgb_led/inc/io/rgb_led.h @@ -30,15 +30,22 @@ void rgb_led_init(void); // Deinitialize RGB LED driver void rgb_led_deinit(void); -#endif +#endif // KERNEL_MODE -#define RGBLED_GREEN 0x00FF00 -#define RGBLED_RED 0xFF0000 -#define RGBLED_BLUE 0x0000FF -#define RGBLED_YELLOW 0xFFFF00 +// Set RGB LED enabled state +// enabled: true to enable, false to disable +void rgb_led_set_enabled(bool enabled); + +// Get RGB LED enabled state +bool rgb_led_get_enabled(void); // Set RGB LED color // color: 24-bit RGB color, 0x00RRGGBB void rgb_led_set_color(uint32_t color); +#define RGBLED_GREEN 0x040D04 +#define RGBLED_RED 0x640603 +#define RGBLED_BLUE 0x050532 +#define RGBLED_YELLOW 0x161000 + #endif // TREZORHAL_RGB_LED_H diff --git a/core/embed/io/rgb_led/stm32/rgb_led.c b/core/embed/io/rgb_led/stm32/rgb_led.c index 5d78290d192..ddff616de6f 100644 --- a/core/embed/io/rgb_led/stm32/rgb_led.c +++ b/core/embed/io/rgb_led/stm32/rgb_led.c @@ -12,6 +12,7 @@ typedef struct { TIM_HandleTypeDef tim; bool initialized; + bool enabled; } rgb_led_t; static rgb_led_t g_rgb_led = {0}; @@ -64,6 +65,7 @@ void rgb_led_init(void) { HAL_TIM_PWM_Start(&drv->tim, TIM_CHANNEL_3); drv->initialized = true; + drv->enabled = true; } void rgb_led_deinit(void) { @@ -82,12 +84,41 @@ void rgb_led_deinit(void) { drv->initialized = false; } +void rgb_led_set_enabled(bool enabled) { + rgb_led_t* drv = &g_rgb_led; + + if (!drv->initialized) { + return; + } + + // If the RGB LED is to be disabled, turn off the LED + if (!enabled) { + rgb_led_set_color(0); + } + + drv->enabled = enabled; +} + +bool rgb_led_get_enabled(void) { + rgb_led_t* drv = &g_rgb_led; + + if (!drv->initialized) { + return false; + } + + return drv->enabled; +} + void rgb_led_set_color(uint32_t color) { rgb_led_t* drv = &g_rgb_led; if (!drv->initialized) { return; } + if (!drv->enabled) { + return; + } + TIM4->CCR1 = ((color >> 16) & 0xFF) * TIMER_PERIOD / 255; TIM4->CCR2 = ((color >> 8) & 0xFF) * TIMER_PERIOD / 255; TIM4->CCR3 = (color & 0xFF) * TIMER_PERIOD / 255; diff --git a/core/embed/io/rgb_led/stm32u5/rgb_led_lp.c b/core/embed/io/rgb_led/stm32u5/rgb_led_lp.c index 7233bf65ff6..5454794b2bb 100644 --- a/core/embed/io/rgb_led/stm32u5/rgb_led_lp.c +++ b/core/embed/io/rgb_led/stm32u5/rgb_led_lp.c @@ -46,6 +46,7 @@ typedef struct { LPTIM_HandleTypeDef tim_1; LPTIM_HandleTypeDef tim_3; bool initialized; + bool enabled; } rgb_led_t; static rgb_led_t g_rgb_led = {0}; @@ -164,6 +165,7 @@ void rgb_led_init(void) { HAL_GPIO_Init(RGB_LED_BLUE_PORT, &GPIO_InitStructure); drv->initialized = true; + drv->enabled = true; } void rgb_led_deinit(void) { @@ -192,12 +194,41 @@ void rgb_led_deinit(void) { memset(drv, 0, sizeof(*drv)); } +void rgb_led_set_enabled(bool enabled) { + rgb_led_t* drv = &g_rgb_led; + + if (!drv->initialized) { + return; + } + + // If the RGB LED is to be disabled, turn off the LED + if (!enabled) { + rgb_led_set_color(0); + } + + drv->enabled = enabled; +} + +bool rgb_led_get_enabled(void) { + rgb_led_t* drv = &g_rgb_led; + + if (!drv->initialized) { + return false; + } + + return drv->enabled; +} + void rgb_led_set_color(uint32_t color) { rgb_led_t* drv = &g_rgb_led; if (!drv->initialized) { return; } + if (!drv->enabled) { + return; + } + uint32_t red = (color >> 16) & 0xFF; uint32_t green = (color >> 8) & 0xFF; uint32_t blue = color & 0xFF; diff --git a/core/embed/io/rgb_led/unix/rgb_led.c b/core/embed/io/rgb_led/unix/rgb_led.c index 1a856a542a8..5718e0c91d3 100644 --- a/core/embed/io/rgb_led/unix/rgb_led.c +++ b/core/embed/io/rgb_led/unix/rgb_led.c @@ -17,12 +17,64 @@ * along with this program. If not, see . */ +#include + #include #include #ifdef KERNEL_MODE -void rgb_led_init(void){}; -void rgb_led_deinit(void){}; -#endif -void rgb_led_set_color(uint32_t color) { display_rgb_led(color); } +// Driver state +typedef struct { + bool initialized; + bool enabled; +} rgb_led_driver_t; + +// RGB LED driver instance +static rgb_led_driver_t g_rgb_led_driver = { + .initialized = true, + .enabled = false, +}; + +void rgb_led_init(void) { + rgb_led_driver_t *driver = &g_rgb_led_driver; + + driver->initialized = true; + driver->enabled = true; +} + +void rgb_led_deinit(void) { + rgb_led_driver_t *driver = &g_rgb_led_driver; + memset(driver, 0, sizeof(rgb_led_driver_t)); +} + +void rgb_led_set_enabled(bool enabled) { + rgb_led_driver_t *driver = &g_rgb_led_driver; + + if (!driver->initialized) { + return; + } + + driver->enabled = enabled; +} + +bool rgb_led_get_enabled(void) { + rgb_led_driver_t *driver = &g_rgb_led_driver; + + if (!driver->initialized) { + return false; + } + + return driver->enabled; +} + +void rgb_led_set_color(uint32_t color) { + rgb_led_driver_t *driver = &g_rgb_led_driver; + if (!driver->initialized || !driver->enabled) { + return; + } + + display_rgb_led(color); +} + +#endif /* KERNEL_MODE */ diff --git a/core/embed/io/touch/unix/touch.c b/core/embed/io/touch/unix/touch.c index 24aad4e67b8..9ff0b2bc0a3 100644 --- a/core/embed/io/touch/unix/touch.c +++ b/core/embed/io/touch/unix/touch.c @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -28,6 +29,7 @@ extern int sdl_display_res_x, sdl_display_res_y; extern int sdl_touch_offset_x, sdl_touch_offset_y; +extern int sdl_touch_x, sdl_touch_y; // distance from the edge where arrow button swipe starts [px] static const int _btn_swipe_begin = 120; @@ -78,6 +80,11 @@ static bool is_inside_display(int x, int y) { static void handle_mouse_events(touch_driver_t* drv, SDL_Event* event) { bool inside_display = is_inside_display(event->button.x, event->button.y); + sdl_touch_x = inside_display ? event->button.x - sdl_touch_offset_x + : touch_unpack_x(drv->last_event); + sdl_touch_y = inside_display ? event->button.y - sdl_touch_offset_y + : touch_unpack_y(drv->last_event); + switch (event->type) { case SDL_MOUSEBUTTONDOWN: if (inside_display) { diff --git a/core/embed/projects/bootloader/main.c b/core/embed/projects/bootloader/main.c index e8d80142cf6..5963d4d9d02 100644 --- a/core/embed/projects/bootloader/main.c +++ b/core/embed/projects/bootloader/main.c @@ -216,7 +216,9 @@ static secbool boot_sequence(void) { if (state.charging_status == PM_BATTERY_CHARGING) { // charing screen - rgb_led_set_color(0x0000FF); +#ifdef USE_RGB_LED + rgb_led_set_color(RGBLED_BLUE); +#endif } else { if (!btn_down && !state.usb_connected && !state.wireless_connected) { // device in just intended to be turned off @@ -229,20 +231,34 @@ static secbool boot_sequence(void) { } } +#ifdef USE_RGB_LED rgb_led_set_color(0); +#endif while (pm_turn_on() != PM_OK) { - rgb_led_set_color(0x400000); +#ifdef USE_RGB_LED + rgb_led_set_color(RGBLED_RED); +#endif systick_delay_ms(400); +#ifdef USE_RGB_LED rgb_led_set_color(0); +#endif systick_delay_ms(400); - rgb_led_set_color(0x400000); +#ifdef USE_RGB_LED + rgb_led_set_color(RGBLED_RED); +#endif systick_delay_ms(400); +#ifdef USE_RGB_LED rgb_led_set_color(0); +#endif systick_delay_ms(400); - rgb_led_set_color(0x400000); +#ifdef USE_RGB_LED + rgb_led_set_color(RGBLED_RED); +#endif systick_delay_ms(400); +#ifdef USE_RGB_LED rgb_led_set_color(0); +#endif pm_hibernate(); systick_delay_ms(1000); reboot_to_off(); diff --git a/core/embed/projects/prodtest/main.c b/core/embed/projects/prodtest/main.c index 90e9abd0fb7..71dfcfd10ea 100644 --- a/core/embed/projects/prodtest/main.c +++ b/core/embed/projects/prodtest/main.c @@ -338,15 +338,21 @@ int prodtest_main(void) { } else if (btn_event.event_type == BTN_EVENT_UP) { if (ticks_expired(btn_deadline)) { pm_hibernate(); +#ifdef USE_RGB_LED rgb_led_set_color(RGBLED_YELLOW); +#endif systick_delay_ms(1000); +#ifdef USE_RGB_LED rgb_led_set_color(0); +#endif } } } } if (button_is_down(BTN_POWER) && ticks_expired(btn_deadline)) { +#ifdef USE_RGB_LED rgb_led_set_color(RGBLED_RED); +#endif } #endif diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 143ece1882e..0133d0ae6c1 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -23,20 +23,26 @@ static void _librust_qstrs(void) { MP_QSTR_BacklightLevels; MP_QSTR_BackupFailed; MP_QSTR_BleInterface; + MP_QSTR_Bluetooth; MP_QSTR_CANCELLED; MP_QSTR_CONFIRMED; MP_QSTR_CheckBackup; MP_QSTR_DIM; MP_QSTR_DONE; + MP_QSTR_DeviceConnect; MP_QSTR_DeviceDisconnect; MP_QSTR_DeviceMenuResult; MP_QSTR_DeviceName; MP_QSTR_DevicePair; + MP_QSTR_DeviceUnpair; + MP_QSTR_DeviceUnpairAll; + MP_QSTR_HapticFeedback; MP_QSTR_INFO; MP_QSTR_INITIAL; MP_QSTR_LOW; MP_QSTR_LayoutObj; MP_QSTR_LayoutState; + MP_QSTR_Led; MP_QSTR_MAX; MP_QSTR_MESSAGE_NAME; MP_QSTR_MESSAGE_WIRE_TYPE; @@ -45,6 +51,8 @@ static void _librust_qstrs(void) { MP_QSTR_MsgDef; MP_QSTR_NONE; MP_QSTR_NORMAL; + MP_QSTR_PinCode; + MP_QSTR_PinRemove; MP_QSTR_RESUME; MP_QSTR_RX_PACKET_LEN; MP_QSTR_SWIPE_DOWN; @@ -56,10 +64,13 @@ static void _librust_qstrs(void) { MP_QSTR_TRANSITIONING; MP_QSTR_TX_PACKET_LEN; MP_QSTR_TranslationsHeader; + MP_QSTR_WipeCode; MP_QSTR_WipeDevice; + MP_QSTR_WipeRemove; MP_QSTR___del__; MP_QSTR___dict__; MP_QSTR___name__; + MP_QSTR_about_items; MP_QSTR_account; MP_QSTR_account_items; MP_QSTR_account_path; @@ -160,10 +171,18 @@ static void _librust_qstrs(void) { MP_QSTR_bitcoin__unverified_external_inputs; MP_QSTR_bitcoin__valid_signature; MP_QSTR_bitcoin__voting_rights; + MP_QSTR_ble__disable; + MP_QSTR_ble__enable; + MP_QSTR_ble__forget_all; + MP_QSTR_ble__manage_paired; + MP_QSTR_ble__pair_new; + MP_QSTR_ble__pair_title; MP_QSTR_ble__unpair_all; MP_QSTR_ble__unpair_current; MP_QSTR_ble__unpair_title; + MP_QSTR_ble__version; MP_QSTR_ble_event; + MP_QSTR_bluetooth; MP_QSTR_bootscreen; MP_QSTR_br_code; MP_QSTR_br_name; @@ -220,6 +239,7 @@ static void _librust_qstrs(void) { MP_QSTR_cancel; MP_QSTR_cancel_text; MP_QSTR_case_sensitive; + MP_QSTR_check_backup; MP_QSTR_check_homescreen_format; MP_QSTR_chunkify; MP_QSTR_code; @@ -253,6 +273,7 @@ static void _librust_qstrs(void) { MP_QSTR_confirm_value; MP_QSTR_confirm_value_intro; MP_QSTR_confirm_with_info; + MP_QSTR_connected_idx; MP_QSTR_connection_flags; MP_QSTR_continue_recovery_homepage; MP_QSTR_count; @@ -298,7 +319,6 @@ static void _librust_qstrs(void) { MP_QSTR_firmware_update__restart; MP_QSTR_firmware_update__title; MP_QSTR_firmware_update__title_fingerprint; - MP_QSTR_firmware_version; MP_QSTR_flow_confirm_output; MP_QSTR_flow_confirm_set_new_pin; MP_QSTR_flow_get_address; @@ -306,6 +326,7 @@ static void _librust_qstrs(void) { MP_QSTR_get; MP_QSTR_get_language; MP_QSTR_get_transition_out; + MP_QSTR_haptic_feedback; MP_QSTR_haptic_feedback__disable; MP_QSTR_haptic_feedback__enable; MP_QSTR_haptic_feedback__subtitle; @@ -315,6 +336,8 @@ static void _librust_qstrs(void) { MP_QSTR_hold_danger; MP_QSTR_homescreen__click_to_connect; MP_QSTR_homescreen__click_to_unlock; + MP_QSTR_homescreen__firmware_type; + MP_QSTR_homescreen__firmware_version; MP_QSTR_homescreen__set_default; MP_QSTR_homescreen__settings_subtitle; MP_QSTR_homescreen__settings_title; @@ -381,6 +404,10 @@ static void _librust_qstrs(void) { MP_QSTR_language__changed; MP_QSTR_language__progress; MP_QSTR_language__title; + MP_QSTR_led; + MP_QSTR_led__disable; + MP_QSTR_led__enable; + MP_QSTR_led__title; MP_QSTR_lines; MP_QSTR_load_from_flash; MP_QSTR_lockable; @@ -447,6 +474,7 @@ static void _librust_qstrs(void) { MP_QSTR_pin__cancel_info; MP_QSTR_pin__cancel_setup; MP_QSTR_pin__change; + MP_QSTR_pin__change_question; MP_QSTR_pin__changed; MP_QSTR_pin__cursor_will_change; MP_QSTR_pin__diff_from_wipe_code; @@ -464,6 +492,7 @@ static void _librust_qstrs(void) { MP_QSTR_pin__reenter; MP_QSTR_pin__reenter_new; MP_QSTR_pin__reenter_to_confirm; + MP_QSTR_pin__remove; MP_QSTR_pin__setup_completed; MP_QSTR_pin__should_be_long; MP_QSTR_pin__title_check_pin; @@ -473,6 +502,8 @@ static void _librust_qstrs(void) { MP_QSTR_pin__turn_off; MP_QSTR_pin__turn_on; MP_QSTR_pin__wrong_pin; + MP_QSTR_pin_code; + MP_QSTR_pin_unset; MP_QSTR_plurals__contains_x_keys; MP_QSTR_plurals__lock_after_x_days; MP_QSTR_plurals__lock_after_x_hours; @@ -680,6 +711,7 @@ static void _librust_qstrs(void) { MP_QSTR_safety_checks__enforce_strict; MP_QSTR_safety_checks__title; MP_QSTR_safety_checks__title_safety_override; + MP_QSTR_screen_brightness; MP_QSTR_sd_card__all_data_will_be_lost; MP_QSTR_sd_card__card_required; MP_QSTR_sd_card__disable; @@ -852,7 +884,9 @@ static void _librust_qstrs(void) { MP_QSTR_wipe__info; MP_QSTR_wipe__title; MP_QSTR_wipe__want_to_wipe; + MP_QSTR_wipe_code; MP_QSTR_wipe_code__change; + MP_QSTR_wipe_code__change_question; MP_QSTR_wipe_code__changed; MP_QSTR_wipe_code__diff_from_pin; MP_QSTR_wipe_code__disabled; @@ -863,6 +897,7 @@ static void _librust_qstrs(void) { MP_QSTR_wipe_code__mismatch; MP_QSTR_wipe_code__reenter; MP_QSTR_wipe_code__reenter_to_confirm; + MP_QSTR_wipe_code__remove; MP_QSTR_wipe_code__title_check; MP_QSTR_wipe_code__title_invalid; MP_QSTR_wipe_code__title_settings; @@ -871,6 +906,7 @@ static void _librust_qstrs(void) { MP_QSTR_wipe_code__wipe_code_mismatch; MP_QSTR_word_count__title; MP_QSTR_words; + MP_QSTR_words__about; MP_QSTR_words__account; MP_QSTR_words__account_colon; MP_QSTR_words__address; @@ -881,28 +917,39 @@ static void _librust_qstrs(void) { MP_QSTR_words__assets; MP_QSTR_words__authenticate; MP_QSTR_words__blockhash; + MP_QSTR_words__bluetooth; MP_QSTR_words__buying; MP_QSTR_words__cancel_and_exit; MP_QSTR_words__cancel_question; MP_QSTR_words__chain; MP_QSTR_words__confirm; MP_QSTR_words__confirm_fee; + MP_QSTR_words__connect; + MP_QSTR_words__connected; MP_QSTR_words__contains; MP_QSTR_words__continue_anyway; MP_QSTR_words__continue_anyway_question; MP_QSTR_words__continue_with; + MP_QSTR_words__device; + MP_QSTR_words__disconnect; + MP_QSTR_words__disconnected; MP_QSTR_words__error; MP_QSTR_words__fee; + MP_QSTR_words__forget; MP_QSTR_words__from; MP_QSTR_words__good_to_know; MP_QSTR_words__important; MP_QSTR_words__instructions; MP_QSTR_words__keep_it_safe; MP_QSTR_words__know_what_your_doing; + MP_QSTR_words__led; + MP_QSTR_words__manage; MP_QSTR_words__my_trezor; MP_QSTR_words__name; MP_QSTR_words__no; MP_QSTR_words__not_recommended; + MP_QSTR_words__off; + MP_QSTR_words__on; MP_QSTR_words__operation_cancelled; MP_QSTR_words__outputs; MP_QSTR_words__pay_attention; @@ -913,6 +960,8 @@ static void _librust_qstrs(void) { MP_QSTR_words__receive; MP_QSTR_words__recipient; MP_QSTR_words__recovery_share; + MP_QSTR_words__review; + MP_QSTR_words__security; MP_QSTR_words__send; MP_QSTR_words__settings; MP_QSTR_words__sign; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index b3ce0d8b0fb..37b743fa482 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -704,7 +704,7 @@ pub enum TranslatedString { passphrase__title_source = 444, // "Passphrase source" passphrase__turn_off = 445, // "Turn off passphrase protection?" passphrase__turn_on = 446, // "Turn on passphrase protection?" - pin__change = 447, // "Change PIN?" + pin__change = 447, // "Change PIN" pin__changed = 448, // "PIN changed." pin__cursor_will_change = 449, // "Position of the cursor will change between entries for enhanced security." pin__diff_from_wipe_code = 450, // "The new PIN must be different from your wipe code." @@ -1107,7 +1107,7 @@ pub enum TranslatedString { wipe__info = 771, // "All data will be erased." wipe__title = 772, // "Wipe device" wipe__want_to_wipe = 773, // "Do you really want to wipe the device?\n" - wipe_code__change = 774, // "Change wipe code?" + wipe_code__change = 774, // "Change wipe code" wipe_code__changed = 775, // "Wipe code changed." wipe_code__diff_from_pin = 776, // "The wipe code must be different from your PIN." wipe_code__disabled = 777, // "Wipe code disabled." @@ -1461,6 +1461,36 @@ pub enum TranslatedString { device_name__enter = 1077, // "Enter device name" regulatory_certification__title = 1078, // "Regulatory certification" words__name = 1079, // "Name" + led__disable = 1080, // "Disable LED?" + led__enable = 1081, // "Enable LED?" + led__title = 1082, // "LED" + words__led = 1083, // "LED" + words__off = 1084, // "OFF" + words__on = 1085, // "ON" + ble__manage_paired = 1086, // "Manage paired devices" + ble__pair_new = 1087, // "Pair new device" + ble__pair_title = 1088, // "Pair & connect" + homescreen__firmware_version = 1089, // "Firmware version" + words__about = 1090, // "About" + words__connected = 1091, // "Connected" + words__device = 1092, // "Device" + words__disconnect = 1093, // "Disconnect" + words__manage = 1094, // "Manage" + words__review = 1095, // "Review" + words__security = 1096, // "Security" + homescreen__firmware_type = 1097, // "Firmware type" + ble__version = 1098, // "Bluetooth version" + pin__change_question = 1099, // "Change PIN?" + pin__remove = 1100, // "Remove PIN" + wipe_code__change_question = 1101, // "Change wipe code?" + wipe_code__remove = 1102, // "Remove wipe code" + ble__disable = 1103, // "Turn Bluetooth off?" + ble__enable = 1104, // "Turn Bluetooth on?" + words__bluetooth = 1105, // "Bluetooth" + words__disconnected = 1106, // "Disconnected" + ble__forget_all = 1107, // "Forget all" + words__connect = 1108, // "Connect" + words__forget = 1109, // "Forget" } impl TranslatedString { @@ -2265,7 +2295,7 @@ impl TranslatedString { (Self::passphrase__title_source, "Passphrase source"), (Self::passphrase__turn_off, "Turn off passphrase protection?"), (Self::passphrase__turn_on, "Turn on passphrase protection?"), - (Self::pin__change, "Change PIN?"), + (Self::pin__change, "Change PIN"), (Self::pin__changed, "PIN changed."), (Self::pin__cursor_will_change, "Position of the cursor will change between entries for enhanced security."), (Self::pin__diff_from_wipe_code, "The new PIN must be different from your wipe code."), @@ -2780,7 +2810,7 @@ impl TranslatedString { (Self::wipe__info, "All data will be erased."), (Self::wipe__title, "Wipe device"), (Self::wipe__want_to_wipe, "Do you really want to wipe the device?\n"), - (Self::wipe_code__change, "Change wipe code?"), + (Self::wipe_code__change, "Change wipe code"), (Self::wipe_code__changed, "Wipe code changed."), (Self::wipe_code__diff_from_pin, "The wipe code must be different from your PIN."), (Self::wipe_code__disabled, "Wipe code disabled."), @@ -3241,6 +3271,36 @@ impl TranslatedString { (Self::device_name__enter, "Enter device name"), (Self::regulatory_certification__title, "Regulatory certification"), (Self::words__name, "Name"), + (Self::led__disable, "Disable LED?"), + (Self::led__enable, "Enable LED?"), + (Self::led__title, "LED"), + (Self::words__led, "LED"), + (Self::words__off, "OFF"), + (Self::words__on, "ON"), + (Self::ble__manage_paired, "Manage paired devices"), + (Self::ble__pair_new, "Pair new device"), + (Self::ble__pair_title, "Pair & connect"), + (Self::homescreen__firmware_version, "Firmware version"), + (Self::words__about, "About"), + (Self::words__connected, "Connected"), + (Self::words__device, "Device"), + (Self::words__disconnect, "Disconnect"), + (Self::words__manage, "Manage"), + (Self::words__review, "Review"), + (Self::words__security, "Security"), + (Self::homescreen__firmware_type, "Firmware type"), + (Self::ble__version, "Bluetooth version"), + (Self::pin__change_question, "Change PIN?"), + (Self::pin__remove, "Remove PIN"), + (Self::wipe_code__change_question, "Change wipe code?"), + (Self::wipe_code__remove, "Remove wipe code"), + (Self::ble__disable, "Turn Bluetooth off?"), + (Self::ble__enable, "Turn Bluetooth on?"), + (Self::words__bluetooth, "Bluetooth"), + (Self::words__disconnected, "Disconnected"), + (Self::ble__forget_all, "Forget all"), + (Self::words__connect, "Connect"), + (Self::words__forget, "Forget"), ]; #[cfg(feature = "micropython")] @@ -3318,9 +3378,16 @@ impl TranslatedString { (Qstr::MP_QSTR_bitcoin__unverified_external_inputs, Self::bitcoin__unverified_external_inputs), (Qstr::MP_QSTR_bitcoin__valid_signature, Self::bitcoin__valid_signature), (Qstr::MP_QSTR_bitcoin__voting_rights, Self::bitcoin__voting_rights), + (Qstr::MP_QSTR_ble__disable, Self::ble__disable), + (Qstr::MP_QSTR_ble__enable, Self::ble__enable), + (Qstr::MP_QSTR_ble__forget_all, Self::ble__forget_all), + (Qstr::MP_QSTR_ble__manage_paired, Self::ble__manage_paired), + (Qstr::MP_QSTR_ble__pair_new, Self::ble__pair_new), + (Qstr::MP_QSTR_ble__pair_title, Self::ble__pair_title), (Qstr::MP_QSTR_ble__unpair_all, Self::ble__unpair_all), (Qstr::MP_QSTR_ble__unpair_current, Self::ble__unpair_current), (Qstr::MP_QSTR_ble__unpair_title, Self::ble__unpair_title), + (Qstr::MP_QSTR_ble__version, Self::ble__version), (Qstr::MP_QSTR_brightness__change_title, Self::brightness__change_title), (Qstr::MP_QSTR_brightness__changed_title, Self::brightness__changed_title), (Qstr::MP_QSTR_brightness__title, Self::brightness__title), @@ -3853,6 +3920,8 @@ impl TranslatedString { (Qstr::MP_QSTR_haptic_feedback__title, Self::haptic_feedback__title), (Qstr::MP_QSTR_homescreen__click_to_connect, Self::homescreen__click_to_connect), (Qstr::MP_QSTR_homescreen__click_to_unlock, Self::homescreen__click_to_unlock), + (Qstr::MP_QSTR_homescreen__firmware_type, Self::homescreen__firmware_type), + (Qstr::MP_QSTR_homescreen__firmware_version, Self::homescreen__firmware_version), (Qstr::MP_QSTR_homescreen__set_default, Self::homescreen__set_default), (Qstr::MP_QSTR_homescreen__settings_subtitle, Self::homescreen__settings_subtitle), (Qstr::MP_QSTR_homescreen__settings_title, Self::homescreen__settings_title), @@ -3898,6 +3967,9 @@ impl TranslatedString { (Qstr::MP_QSTR_language__changed, Self::language__changed), (Qstr::MP_QSTR_language__progress, Self::language__progress), (Qstr::MP_QSTR_language__title, Self::language__title), + (Qstr::MP_QSTR_led__disable, Self::led__disable), + (Qstr::MP_QSTR_led__enable, Self::led__enable), + (Qstr::MP_QSTR_led__title, Self::led__title), (Qstr::MP_QSTR_lockscreen__tap_to_connect, Self::lockscreen__tap_to_connect), (Qstr::MP_QSTR_lockscreen__tap_to_unlock, Self::lockscreen__tap_to_unlock), (Qstr::MP_QSTR_lockscreen__title_locked, Self::lockscreen__title_locked), @@ -4081,6 +4153,7 @@ impl TranslatedString { (Qstr::MP_QSTR_pin__cancel_info, Self::pin__cancel_info), (Qstr::MP_QSTR_pin__cancel_setup, Self::pin__cancel_setup), (Qstr::MP_QSTR_pin__change, Self::pin__change), + (Qstr::MP_QSTR_pin__change_question, Self::pin__change_question), (Qstr::MP_QSTR_pin__changed, Self::pin__changed), (Qstr::MP_QSTR_pin__cursor_will_change, Self::pin__cursor_will_change), (Qstr::MP_QSTR_pin__diff_from_wipe_code, Self::pin__diff_from_wipe_code), @@ -4098,6 +4171,7 @@ impl TranslatedString { (Qstr::MP_QSTR_pin__reenter, Self::pin__reenter), (Qstr::MP_QSTR_pin__reenter_new, Self::pin__reenter_new), (Qstr::MP_QSTR_pin__reenter_to_confirm, Self::pin__reenter_to_confirm), + (Qstr::MP_QSTR_pin__remove, Self::pin__remove), (Qstr::MP_QSTR_pin__setup_completed, Self::pin__setup_completed), (Qstr::MP_QSTR_pin__should_be_long, Self::pin__should_be_long), (Qstr::MP_QSTR_pin__title_check_pin, Self::pin__title_check_pin), @@ -4611,6 +4685,7 @@ impl TranslatedString { (Qstr::MP_QSTR_wipe__title, Self::wipe__title), (Qstr::MP_QSTR_wipe__want_to_wipe, Self::wipe__want_to_wipe), (Qstr::MP_QSTR_wipe_code__change, Self::wipe_code__change), + (Qstr::MP_QSTR_wipe_code__change_question, Self::wipe_code__change_question), (Qstr::MP_QSTR_wipe_code__changed, Self::wipe_code__changed), (Qstr::MP_QSTR_wipe_code__diff_from_pin, Self::wipe_code__diff_from_pin), (Qstr::MP_QSTR_wipe_code__disabled, Self::wipe_code__disabled), @@ -4621,6 +4696,7 @@ impl TranslatedString { (Qstr::MP_QSTR_wipe_code__mismatch, Self::wipe_code__mismatch), (Qstr::MP_QSTR_wipe_code__reenter, Self::wipe_code__reenter), (Qstr::MP_QSTR_wipe_code__reenter_to_confirm, Self::wipe_code__reenter_to_confirm), + (Qstr::MP_QSTR_wipe_code__remove, Self::wipe_code__remove), (Qstr::MP_QSTR_wipe_code__title_check, Self::wipe_code__title_check), (Qstr::MP_QSTR_wipe_code__title_invalid, Self::wipe_code__title_invalid), (Qstr::MP_QSTR_wipe_code__title_settings, Self::wipe_code__title_settings), @@ -4628,6 +4704,7 @@ impl TranslatedString { (Qstr::MP_QSTR_wipe_code__turn_on, Self::wipe_code__turn_on), (Qstr::MP_QSTR_wipe_code__wipe_code_mismatch, Self::wipe_code__wipe_code_mismatch), (Qstr::MP_QSTR_word_count__title, Self::word_count__title), + (Qstr::MP_QSTR_words__about, Self::words__about), (Qstr::MP_QSTR_words__account, Self::words__account), (Qstr::MP_QSTR_words__account_colon, Self::words__account_colon), (Qstr::MP_QSTR_words__address, Self::words__address), @@ -4638,28 +4715,39 @@ impl TranslatedString { (Qstr::MP_QSTR_words__assets, Self::words__assets), (Qstr::MP_QSTR_words__authenticate, Self::words__authenticate), (Qstr::MP_QSTR_words__blockhash, Self::words__blockhash), + (Qstr::MP_QSTR_words__bluetooth, Self::words__bluetooth), (Qstr::MP_QSTR_words__buying, Self::words__buying), (Qstr::MP_QSTR_words__cancel_and_exit, Self::words__cancel_and_exit), (Qstr::MP_QSTR_words__cancel_question, Self::words__cancel_question), (Qstr::MP_QSTR_words__chain, Self::words__chain), (Qstr::MP_QSTR_words__confirm, Self::words__confirm), (Qstr::MP_QSTR_words__confirm_fee, Self::words__confirm_fee), + (Qstr::MP_QSTR_words__connect, Self::words__connect), + (Qstr::MP_QSTR_words__connected, Self::words__connected), (Qstr::MP_QSTR_words__contains, Self::words__contains), (Qstr::MP_QSTR_words__continue_anyway, Self::words__continue_anyway), (Qstr::MP_QSTR_words__continue_anyway_question, Self::words__continue_anyway_question), (Qstr::MP_QSTR_words__continue_with, Self::words__continue_with), + (Qstr::MP_QSTR_words__device, Self::words__device), + (Qstr::MP_QSTR_words__disconnect, Self::words__disconnect), + (Qstr::MP_QSTR_words__disconnected, Self::words__disconnected), (Qstr::MP_QSTR_words__error, Self::words__error), (Qstr::MP_QSTR_words__fee, Self::words__fee), + (Qstr::MP_QSTR_words__forget, Self::words__forget), (Qstr::MP_QSTR_words__from, Self::words__from), (Qstr::MP_QSTR_words__good_to_know, Self::words__good_to_know), (Qstr::MP_QSTR_words__important, Self::words__important), (Qstr::MP_QSTR_words__instructions, Self::words__instructions), (Qstr::MP_QSTR_words__keep_it_safe, Self::words__keep_it_safe), (Qstr::MP_QSTR_words__know_what_your_doing, Self::words__know_what_your_doing), + (Qstr::MP_QSTR_words__led, Self::words__led), + (Qstr::MP_QSTR_words__manage, Self::words__manage), (Qstr::MP_QSTR_words__my_trezor, Self::words__my_trezor), (Qstr::MP_QSTR_words__name, Self::words__name), (Qstr::MP_QSTR_words__no, Self::words__no), (Qstr::MP_QSTR_words__not_recommended, Self::words__not_recommended), + (Qstr::MP_QSTR_words__off, Self::words__off), + (Qstr::MP_QSTR_words__on, Self::words__on), (Qstr::MP_QSTR_words__operation_cancelled, Self::words__operation_cancelled), (Qstr::MP_QSTR_words__outputs, Self::words__outputs), (Qstr::MP_QSTR_words__pay_attention, Self::words__pay_attention), @@ -4670,6 +4758,8 @@ impl TranslatedString { (Qstr::MP_QSTR_words__receive, Self::words__receive), (Qstr::MP_QSTR_words__recipient, Self::words__recipient), (Qstr::MP_QSTR_words__recovery_share, Self::words__recovery_share), + (Qstr::MP_QSTR_words__review, Self::words__review), + (Qstr::MP_QSTR_words__security, Self::words__security), (Qstr::MP_QSTR_words__send, Self::words__send), (Qstr::MP_QSTR_words__settings, Self::words__settings), (Qstr::MP_QSTR_words__sign, Self::words__sign), diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs index 6998fb1de2e..66d4eec7a75 100644 --- a/core/embed/rust/src/ui/api/firmware_micropython.rs +++ b/core/embed/rust/src/ui/api/firmware_micropython.rs @@ -23,7 +23,8 @@ use crate::{ util::{upy_disable_animation, RecoveryType}, }, ui_firmware::{ - FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_WORD_QUIZ_ITEMS, + FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_PAIRED_DEVICES, + MAX_WORD_QUIZ_ITEMS, }, ModelUI, }, @@ -937,20 +938,45 @@ extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut extern "C" fn new_show_device_menu(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let failed_backup: bool = kwargs.get(Qstr::MP_QSTR_failed_backup)?.try_into()?; - let firmware_version: TString = kwargs.get(Qstr::MP_QSTR_firmware_version)?.try_into()?; + let pin_unset: bool = kwargs.get(Qstr::MP_QSTR_pin_unset)?.try_into()?; + let paired_devices: Obj = kwargs.get(Qstr::MP_QSTR_paired_devices)?; + let paired_devices: Vec = util::iter_into_vec(paired_devices)?; + let connected_idx: Option = + kwargs.get(Qstr::MP_QSTR_connected_idx)?.try_into_option()?; + let bluetooth: Option = kwargs.get(Qstr::MP_QSTR_bluetooth)?.try_into_option()?; + let pin_code: Option = kwargs.get(Qstr::MP_QSTR_pin_code)?.try_into_option()?; + let auto_lock_delay: Option = kwargs + .get(Qstr::MP_QSTR_auto_lock_delay)? + .try_into_option()?; + let wipe_code: Option = kwargs.get(Qstr::MP_QSTR_wipe_code)?.try_into_option()?; + let check_backup: bool = kwargs.get(Qstr::MP_QSTR_check_backup)?.try_into()?; let device_name: Option = kwargs.get(Qstr::MP_QSTR_device_name)?.try_into_option()?; - let paired_devices: Obj = kwargs.get(Qstr::MP_QSTR_paired_devices)?; - let paired_devices: Vec = util::iter_into_vec(paired_devices)?; - let auto_lock_delay: TString<'static> = - kwargs.get(Qstr::MP_QSTR_auto_lock_delay)?.try_into()?; + let screen_brightness: Option = kwargs + .get(Qstr::MP_QSTR_screen_brightness)? + .try_into_option()?; + let haptic_feedback: Option = kwargs + .get(Qstr::MP_QSTR_haptic_feedback)? + .try_into_option()?; + let led: Option = kwargs.get(Qstr::MP_QSTR_led)?.try_into_option()?; + let about_items: Obj = kwargs.get(Qstr::MP_QSTR_about_items)?; let layout = ModelUI::show_device_menu( failed_backup, - firmware_version, - device_name, + pin_unset, paired_devices, + connected_idx, + bluetooth, + pin_code, auto_lock_delay, + wipe_code, + check_backup, + device_name, + screen_brightness, + haptic_feedback, + led, + about_items, )?; + let layout_obj = LayoutObj::new_root(layout)?; Ok(layout_obj.into()) }; @@ -1878,10 +1904,19 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// def show_device_menu( /// *, /// failed_backup: bool, - /// firmware_version: str, - /// device_name: str | None, + /// pin_unset: bool, /// paired_devices: Iterable[str], - /// auto_lock_delay: str, + /// connected_idx: int | None, + /// bluetooth: bool | None, + /// pin_code: bool | None, + /// auto_lock_delay: str | None, + /// wipe_code: bool | None, + /// check_backup: bool, + /// device_name: str | None, + /// screen_brightness: str | None, + /// haptic_feedback: bool | None, + /// led: bool | None, + /// about_items: list[tuple[str | None, str | bytes | None, bool | None]], /// ) -> LayoutObj[UiResult | DeviceMenuResult | tuple[DeviceMenuResult, int]]: /// """Show the device menu.""" Qstr::MP_QSTR_show_device_menu => obj_fn_kw!(0, new_show_device_menu).as_obj(), @@ -2078,12 +2113,22 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// class DeviceMenuResult: /// """Result of a device menu operation.""" /// BackupFailed: ClassVar[DeviceMenuResult] - /// DevicePair: ClassVar[DeviceMenuResult] + /// DeviceConnect: ClassVar[DeviceMenuResult] /// DeviceDisconnect: ClassVar[DeviceMenuResult] - /// CheckBackup: ClassVar[DeviceMenuResult] - /// WipeDevice: ClassVar[DeviceMenuResult] - /// ScreenBrightness: ClassVar[DeviceMenuResult] + /// DevicePair: ClassVar[DeviceMenuResult] + /// DeviceUnpair: ClassVar[DeviceMenuResult] + /// DeviceUnpairAll: ClassVar[DeviceMenuResult] + /// Bluetooth: ClassVar[DeviceMenuResult] + /// PinCode: ClassVar[DeviceMenuResult] + /// PinRemove: ClassVar[DeviceMenuResult] /// AutoLockDelay: ClassVar[DeviceMenuResult] + /// WipeCode: ClassVar[DeviceMenuResult] + /// WipeRemove: ClassVar[DeviceMenuResult] + /// CheckBackup: ClassVar[DeviceMenuResult] /// DeviceName: ClassVar[DeviceMenuResult] + /// ScreenBrightness: ClassVar[DeviceMenuResult] + /// HapticFeedback: ClassVar[DeviceMenuResult] + /// Led: ClassVar[DeviceMenuResult] + /// WipeDevice: ClassVar[DeviceMenuResult] Qstr::MP_QSTR_DeviceMenuResult => DEVICE_MENU_RESULT.as_obj(), }; diff --git a/core/embed/rust/src/ui/layout/device_menu_result.rs b/core/embed/rust/src/ui/layout/device_menu_result.rs index dadb1320f21..5de19cc8650 100644 --- a/core/embed/rust/src/ui/layout/device_menu_result.rs +++ b/core/embed/rust/src/ui/layout/device_menu_result.rs @@ -7,27 +7,52 @@ use crate::micropython::{ static DEVICE_MENU_RESULT_BASE_TYPE: Type = obj_type! { name: Qstr::MP_QSTR_DeviceMenuResult, }; +// Root menu pub static BACKUP_FAILED: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +// Bluetooth +pub static BLUETOOTH: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +// "Pair & Connect" pub static DEVICE_PAIR: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); pub static DEVICE_DISCONNECT: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); -pub static CHECK_BACKUP: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); -pub static WIPE_DEVICE: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); -pub static SCREEN_BRIGHTNESS: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static DEVICE_CONNECT: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static DEVICE_UNPAIR: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static DEVICE_UNPAIR_ALL: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +// Security menu +pub static PIN_CODE: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static PIN_REMOVE: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); pub static AUTO_LOCK_DELAY: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static WIPE_CODE: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static WIPE_REMOVE: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static CHECK_BACKUP: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +// Device menu pub static DEVICE_NAME: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static SCREEN_BRIGHTNESS: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static HAPTIC_FEEDBACK: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static LED: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); +pub static WIPE_DEVICE: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE); // Create a DeviceMenuResult class that contains all result types static DEVICE_MENU_RESULT_TYPE: Type = obj_type! { name: Qstr::MP_QSTR_DeviceMenuResult, locals: &obj_dict! { obj_map! { Qstr::MP_QSTR_BackupFailed => BACKUP_FAILED.as_obj(), + Qstr::MP_QSTR_Bluetooth => BLUETOOTH.as_obj(), Qstr::MP_QSTR_DevicePair => DEVICE_PAIR.as_obj(), Qstr::MP_QSTR_DeviceDisconnect => DEVICE_DISCONNECT.as_obj(), - Qstr::MP_QSTR_CheckBackup => CHECK_BACKUP.as_obj(), - Qstr::MP_QSTR_WipeDevice => WIPE_DEVICE.as_obj(), - Qstr::MP_QSTR_ScreenBrightness => SCREEN_BRIGHTNESS.as_obj(), + Qstr::MP_QSTR_DeviceConnect => DEVICE_CONNECT.as_obj(), + Qstr::MP_QSTR_DeviceUnpair => DEVICE_UNPAIR.as_obj(), + Qstr::MP_QSTR_DeviceUnpairAll => DEVICE_UNPAIR_ALL.as_obj(), + Qstr::MP_QSTR_PinCode => PIN_CODE.as_obj(), + Qstr::MP_QSTR_PinRemove => PIN_REMOVE.as_obj(), Qstr::MP_QSTR_AutoLockDelay => AUTO_LOCK_DELAY.as_obj(), + Qstr::MP_QSTR_WipeCode => WIPE_CODE.as_obj(), + Qstr::MP_QSTR_WipeRemove => WIPE_REMOVE.as_obj(), + Qstr::MP_QSTR_CheckBackup => CHECK_BACKUP.as_obj(), Qstr::MP_QSTR_DeviceName => DEVICE_NAME.as_obj(), + Qstr::MP_QSTR_ScreenBrightness => SCREEN_BRIGHTNESS.as_obj(), + Qstr::MP_QSTR_HapticFeedback => HAPTIC_FEEDBACK.as_obj(), + Qstr::MP_QSTR_Led => LED.as_obj(), + Qstr::MP_QSTR_WipeDevice => WIPE_DEVICE.as_obj(), } }, }; diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs index 6784be0346d..5c0aac84220 100644 --- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs @@ -28,7 +28,7 @@ use crate::{ }, ui_firmware::{ FirmwareUI, ERROR_NOT_IMPLEMENTED, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, - MAX_MENU_ITEMS, MAX_WORD_QUIZ_ITEMS, + MAX_MENU_ITEMS, MAX_PAIRED_DEVICES, MAX_WORD_QUIZ_ITEMS, }, ModelUI, }, @@ -934,10 +934,19 @@ impl FirmwareUI for UIBolt { fn show_device_menu( _failed_backup: bool, - _firmware_version: TString<'static>, + _pin_unset: bool, + _paired_devices: heapless::Vec, MAX_PAIRED_DEVICES>, + _connected_idx: Option, + _bluetooth: Option, + _pin_code: Option, + _auto_lock_delay: Option>, + _wipe_code: Option, + _check_backup: bool, _device_name: Option>, - _paired_devices: heapless::Vec, 1>, - _auto_lock_delay: TString<'static>, + _screen_brightness: Option>, + _haptic_feedback: Option, + _led: Option, + _about_items: Obj, ) -> Result { Err::, Error>(Error::ValueError( c"show_device_menu not supported", diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs index 2815e7b2124..73bac6f892b 100644 --- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs @@ -27,7 +27,7 @@ use crate::{ }, ui_firmware::{ FirmwareUI, ERROR_NOT_IMPLEMENTED, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, - MAX_MENU_ITEMS, MAX_WORD_QUIZ_ITEMS, + MAX_MENU_ITEMS, MAX_PAIRED_DEVICES, MAX_WORD_QUIZ_ITEMS, }, ModelUI, }, @@ -1128,10 +1128,19 @@ impl FirmwareUI for UICaesar { fn show_device_menu( _failed_backup: bool, - _firmware_version: TString<'static>, + _pin_unset: bool, + _paired_devices: heapless::Vec, MAX_PAIRED_DEVICES>, + _connected_idx: Option, + _bluetooth: Option, + _pin_code: Option, + _auto_lock_delay: Option>, + _wipe_code: Option, + _check_backup: bool, _device_name: Option>, - _paired_devices: Vec, 1>, - _auto_lock_delay: TString<'static>, + _screen_brightness: Option>, + _haptic_feedback: Option, + _led: Option, + _about_items: Obj, ) -> Result { Err::, Error>(Error::ValueError( c"show_device_menu not supported", diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs index 26c218e69fa..fac6f721d92 100644 --- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs @@ -29,7 +29,7 @@ use crate::{ }, ui_firmware::{ FirmwareUI, ERROR_NOT_IMPLEMENTED, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, - MAX_MENU_ITEMS, MAX_WORD_QUIZ_ITEMS, + MAX_MENU_ITEMS, MAX_PAIRED_DEVICES, MAX_WORD_QUIZ_ITEMS, }, ModelUI, }, @@ -1018,10 +1018,19 @@ impl FirmwareUI for UIDelizia { fn show_device_menu( _failed_backup: bool, - _firmware_version: TString<'static>, + _pin_unset: bool, + _paired_devices: heapless::Vec, MAX_PAIRED_DEVICES>, + _connected_idx: Option, + _bluetooth: Option, + _pin_code: Option, + _auto_lock_delay: Option>, + _wipe_code: Option, + _check_backup: bool, _device_name: Option>, - _paired_devices: heapless::Vec, 1>, - _auto_lock_delay: TString<'static>, + _screen_brightness: Option>, + _haptic_feedback: Option, + _led: Option, + _about_items: Obj, ) -> Result { Err::, Error>(Error::ValueError( c"show_device_menu not supported", diff --git a/core/embed/rust/src/ui/layout_eckhart/component/button.rs b/core/embed/rust/src/ui/layout_eckhart/component/button.rs index d04bb1b4766..5f40ef15135 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/button.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/button.rs @@ -1,5 +1,8 @@ +#[cfg(feature = "translations")] +use crate::translations::TR; #[cfg(feature = "haptic")] use crate::trezorhal::haptic::{play, HapticEffect}; + use crate::{ strutil::TString, time::{Duration, ShortDuration}, @@ -48,6 +51,7 @@ impl Button { const MENU_ITEM_RADIUS: u8 = 12; const MENU_ITEM_ALIGNMENT: Alignment = Alignment::Start; pub const MENU_ITEM_CONTENT_OFFSET: Offset = Offset::x(12); + const CONN_ICON_WIDTH: i16 = 34; #[cfg(feature = "micropython")] const DEFAULT_STYLESHEET: ButtonStyleSheet = theme::firmware::button_default(); @@ -93,7 +97,40 @@ impl Button { subtext: TString<'static>, subtext_style: &'static TextStyle, ) -> Self { - Self::with_text_and_subtext(text, subtext, subtext_style) + Self::with_text_and_subtext(text, subtext, subtext_style, None) + .with_text_align(Self::MENU_ITEM_ALIGNMENT) + .with_content_offset(Self::MENU_ITEM_CONTENT_OFFSET) + .styled(stylesheet) + .with_radius(Self::MENU_ITEM_RADIUS) + } + + #[cfg(feature = "micropython")] + pub fn new_connection_item( + text: TString<'static>, + stylesheet: ButtonStyleSheet, + subtext: Option>, + connected: bool, + ) -> Self { + let (icon, subtext_style) = if connected { + ( + (theme::ICON_SQUARE, theme::GREEN_LIGHT), + &theme::TEXT_MENU_ITEM_SUBTITLE_GREEN, + ) + } else { + ( + (theme::ICON_SQUARE, theme::GREY_DARK), + &theme::TEXT_MENU_ITEM_SUBTITLE, + ) + }; + let subtext = if let Some(subtext) = subtext { + subtext + } else if connected { + TR::words__connected.into() + } else { + TR::words__disconnected.into() + }; + + Self::with_text_and_subtext(text, subtext, subtext_style, Some(icon)) .with_text_align(Self::MENU_ITEM_ALIGNMENT) .with_content_offset(Self::MENU_ITEM_CONTENT_OFFSET) .styled(stylesheet) @@ -112,11 +149,13 @@ impl Button { text: TString<'static>, subtext: TString<'static>, subtext_style: &'static TextStyle, + icon: Option<(Icon, Color)>, ) -> Self { Self::new(ButtonContent::TextAndSubtext { text, subtext, subtext_style, + icon, }) } @@ -289,7 +328,17 @@ impl Button { text.map(|t| self.text_height(t, *single_line, width)) } ButtonContent::Icon(icon) => icon.toif.height(), - ButtonContent::TextAndSubtext { text, .. } => { + ButtonContent::TextAndSubtext { + text, + subtext: _, + subtext_style: _, + icon, + } => { + let width = if icon.is_some() { + width - Self::CONN_ICON_WIDTH + } else { + width + }; text.map(|t| self.text_height(t, false, width) + self.baseline_subtext_height()) } #[cfg(feature = "micropython")] @@ -415,14 +464,14 @@ impl Button { text, subtext, subtext_style, + icon, } => { let text_baseline_height = self.baseline_text_height(); + let available_width = self.area.width() + - 2 * self.content_offset.x + - icon.map_or(0, |_| Self::CONN_ICON_WIDTH); let single_line_text = text.map(|t| { - let (t1, t2) = split_two_lines( - t, - stylesheet.font, - self.area.width() - 2 * self.content_offset.x, - ); + let (t1, t2) = split_two_lines(t, stylesheet.font, available_width); if t1.is_empty() || t2.is_empty() { show_text( t, @@ -444,8 +493,10 @@ impl Button { subtext.map(|subtext| { #[cfg(feature = "ui_debug")] - if subtext_style.text_font.text_width(subtext) > self.area.width() { - fatal_error!(&uformat!(len: 128, "Subtext too long: '{}'", subtext)); + { + if subtext_style.text_font.text_width(subtext) > available_width { + fatal_error!(&uformat!(len: 128, "Subtext too long: '{}'", subtext)); + } } shape::Text::new( render_origin(if single_line_text { @@ -465,6 +516,19 @@ impl Button { .with_alpha(alpha) .render(target); }); + + if let Some((icon, icon_color)) = icon { + shape::ToifImage::new( + self.area + .right_center() + .ofs(Offset::x(Self::CONN_ICON_WIDTH / 2).neg()) + .ofs(self.content_offset.neg()), + icon.toif, + ) + .with_align(Alignment2D::CENTER) + .with_fg(*icon_color) + .render(target); + } } ButtonContent::Icon(icon) => { shape::ToifImage::new(self.area.center() + self.content_offset, icon.toif) @@ -662,6 +726,7 @@ pub enum ButtonContent { text: TString<'static>, subtext: TString<'static>, subtext_style: &'static TextStyle, + icon: Option<(Icon, Color)>, }, Icon(Icon), #[cfg(feature = "micropython")] diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs index 2b85d539fe4..2cad0d0cffc 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs @@ -145,19 +145,37 @@ impl ComponentMsgObj for SetBrightnessScreen { } } -impl<'a> ComponentMsgObj for DeviceMenuScreen<'a> { +impl ComponentMsgObj for DeviceMenuScreen { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { match msg { + // Root menu DeviceMenuMsg::BackupFailed => Ok(BACKUP_FAILED.as_obj()), + // Bluetooth + DeviceMenuMsg::Bluetooth => Ok(BLUETOOTH.as_obj()), + // "Pair & Connect" DeviceMenuMsg::DevicePair => Ok(DEVICE_PAIR.as_obj()), - DeviceMenuMsg::DeviceDisconnect(index) => { - Ok(new_tuple(&[DEVICE_DISCONNECT.as_obj(), index.try_into()?])?) + DeviceMenuMsg::DeviceDisconnect => Ok(DEVICE_DISCONNECT.as_obj()), + DeviceMenuMsg::DeviceConnect(index) => { + Ok(new_tuple(&[DEVICE_CONNECT.as_obj(), index.try_into()?])?) } + DeviceMenuMsg::DeviceUnpair(index) => { + Ok(new_tuple(&[DEVICE_UNPAIR.as_obj(), index.try_into()?])?) + } + DeviceMenuMsg::DeviceUnpairAll => Ok(DEVICE_UNPAIR_ALL.as_obj()), + // Security menu + DeviceMenuMsg::PinCode => Ok(PIN_CODE.as_obj()), + DeviceMenuMsg::PinRemove => Ok(PIN_REMOVE.as_obj()), + DeviceMenuMsg::AutoLockDelay => Ok(AUTO_LOCK_DELAY.as_obj()), + DeviceMenuMsg::WipeCode => Ok(WIPE_CODE.as_obj()), + DeviceMenuMsg::WipeRemove => Ok(WIPE_REMOVE.as_obj()), DeviceMenuMsg::CheckBackup => Ok(CHECK_BACKUP.as_obj()), - DeviceMenuMsg::WipeDevice => Ok(WIPE_DEVICE.as_obj()), + // Device menu DeviceMenuMsg::DeviceName => Ok(DEVICE_NAME.as_obj()), DeviceMenuMsg::ScreenBrightness => Ok(SCREEN_BRIGHTNESS.as_obj()), - DeviceMenuMsg::AutoLockDelay => Ok(AUTO_LOCK_DELAY.as_obj()), + DeviceMenuMsg::HapticFeedback => Ok(HAPTIC_FEEDBACK.as_obj()), + DeviceMenuMsg::Led => Ok(LED.as_obj()), + DeviceMenuMsg::WipeDevice => Ok(WIPE_DEVICE.as_obj()), + // nothing selected DeviceMenuMsg::Close => Ok(CANCELLED.as_obj()), } } diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs index 09ff624fa9d..35965c2a81b 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs @@ -2,20 +2,21 @@ use core::ops::{Deref, DerefMut}; use crate::{ error::Error, - micropython::gc::GcBox, + micropython::{gc::GcBox, obj::Obj}, strutil::TString, translations::TR, - trezorhal::storage::has_pin, ui::{ component::{ text::{ - paragraphs::{Paragraph, Paragraphs}, + paragraphs::{ParagraphSource, Paragraphs}, TextStyle, }, Component, Event, EventCtx, }, - geometry::Rect, + geometry::{LinearPlacement, Rect}, + layout::util::PropsList, shape::Renderer, + ui_firmware::MAX_PAIRED_DEVICES, }, }; @@ -25,18 +26,28 @@ use super::{ constant::SCREEN, firmware::{ Header, HeaderMsg, RegulatoryMsg, RegulatoryScreen, TextScreen, TextScreenMsg, - VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, SHORT_MENU_ITEMS, + VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, MEDIUM_MENU_ITEMS, }, }, - theme, ShortMenuVec, + theme, MediumMenuVec, ShortMenuVec, }; use heapless::Vec; +// - root +// - pair & connect +// - settings +// - security +// - pin code +// - wipe code +// - device +const MAX_SUBMENUS: usize = 7; +// pin and wipe code const MAX_DEPTH: usize = 3; -const MAX_SUBSCREENS: usize = 8; -const MAX_SUBMENUS: usize = MAX_SUBSCREENS - 2 /* (about and device screen) */; +// submenus, device screens, regulatory and about screens +const MAX_SUBSCREENS: usize = MAX_SUBMENUS + MAX_PAIRED_DEVICES + 2; -const DISCONNECT_DEVICE_MENU_INDEX: usize = 1; +const DIS_CONNECT_DEVICE_MENU_INDEX: usize = 0; +const FORGET_DEVICE_MENU_INDEX: usize = 1; #[derive(Clone)] enum Action { @@ -51,20 +62,34 @@ pub enum DeviceMenuMsg { // Root menu BackupFailed, + // Bluetooth + Bluetooth, + // "Pair & Connect" - DevicePair, // pair a new device - DeviceDisconnect( - usize, /* which device to disconnect, index in the list of devices */ + DevicePair, // pair a new device + DeviceDisconnect, // disconnect a device + DeviceConnect( + usize, /* which device to connect, index in the list of devices */ + ), + DeviceUnpair( + usize, /* which device to unpair, index in the list of devices */ ), + DeviceUnpairAll, // Security menu + PinCode, + PinRemove, + AutoLockDelay, + WipeCode, + WipeRemove, CheckBackup, - WipeDevice, // Device menu DeviceName, ScreenBrightness, - AutoLockDelay, + HapticFeedback, + Led, + WipeDevice, // nothing selected Close, @@ -74,17 +99,21 @@ struct MenuItem { text: TString<'static>, subtext: Option<(TString<'static>, Option<&'static TextStyle>)>, stylesheet: &'static ButtonStyleSheet, + connection_status: Option, action: Option, } -const MENU_ITEM_TITLE_STYLE_SHEET: &ButtonStyleSheet = &theme::menu_item_title(); +const MENU_ITEM_NORMAL: &ButtonStyleSheet = &theme::menu_item_title(); +const MENU_ITEM_LIGHT_WARNING: &ButtonStyleSheet = &theme::menu_item_title_yellow(); +const MENU_ITEM_WARNING: &ButtonStyleSheet = &theme::menu_item_title_orange(); impl MenuItem { pub fn new(text: TString<'static>, action: Option) -> Self { Self { text, subtext: None, - stylesheet: MENU_ITEM_TITLE_STYLE_SHEET, + stylesheet: MENU_ITEM_NORMAL, action, + connection_status: None, } } @@ -96,6 +125,11 @@ impl MenuItem { self } + pub fn with_connection_status(&mut self, connection_status: Option) -> &mut Self { + self.connection_status = connection_status; + self + } + pub fn with_stylesheet(&mut self, stylesheet: &'static ButtonStyleSheet) -> &mut Self { self.stylesheet = stylesheet; self @@ -104,11 +138,11 @@ impl MenuItem { struct Submenu { show_battery: bool, - items: Vec, + items: Vec, } impl Submenu { - pub fn new(items: Vec) -> Self { + pub fn new(items: Vec) -> Self { Self { show_battery: false, items, @@ -129,6 +163,7 @@ enum Subscreen { // A screen allowing the user to to disconnect a device DeviceScreen( TString<'static>, /* device name */ + bool, /* is the device connected? */ usize, /* index in the list of devices */ ), @@ -140,23 +175,24 @@ enum Subscreen { // Used to preallocate memory for the largest enum variant #[allow(clippy::large_enum_variant)] -enum ActiveScreen<'a> { - Menu(VerticalMenuScreen), - About(TextScreen; 2]>>), +enum ActiveScreen { + Menu(VerticalMenuScreen), + Device(VerticalMenuScreen), + About(TextScreen>), Regulatory(RegulatoryScreen), // used only during `DeviceMenuScreen::new` Empty, } -pub struct DeviceMenuScreen<'a> { +pub struct DeviceMenuScreen { bounds: Rect, - firmware_version: TString<'static>, + about_items: Obj, // These correspond to the currently active subscreen, // which is one of the possible kinds of subscreens // as defined by `enum Subscreen` (DeviceScreen is still a VerticalMenuScreen!) // This way we only need to keep one screen at any time in memory. - active_screen: GcBox>, + active_screen: GcBox, // Information needed to construct any subscreen on demand submenus: GcBox>, subscreens: Vec, @@ -166,18 +202,27 @@ pub struct DeviceMenuScreen<'a> { parent_subscreens: Vec, } -impl<'a> DeviceMenuScreen<'a> { +impl DeviceMenuScreen { + #[allow(clippy::too_many_arguments)] pub fn new( failed_backup: bool, - firmware_version: TString<'static>, + pin_unset: bool, + paired_devices: Vec, MAX_PAIRED_DEVICES>, + connected_idx: Option, + bluetooth: Option, + pin_code: Option, + auto_lock_delay: Option>, + wipe_code: Option, + check_backup: bool, device_name: Option>, - // NB: we currently only support one device at a time. - paired_devices: Vec, 1>, - auto_lock_delay: TString<'static>, + screen_brightness: Option>, + haptic_feedback: Option, + led: Option, + about_items: Obj, ) -> Result { let mut screen = Self { bounds: Rect::zero(), - firmware_version, + about_items, active_screen: GcBox::new(ActiveScreen::Empty)?, active_subscreen: 0, submenus: GcBox::new(Vec::new())?, @@ -187,99 +232,220 @@ impl<'a> DeviceMenuScreen<'a> { let about = screen.add_subscreen(Subscreen::AboutScreen); let regulatory = screen.add_subscreen(Subscreen::RegulatoryScreen); - let security = screen.add_security_menu(); - let device = screen.add_device_menu(device_name, regulatory, about, auto_lock_delay); - let settings = screen.add_settings_menu(security, device); + let security = if pin_code.is_none() + && auto_lock_delay.is_none() + && wipe_code.is_none() + && !check_backup + { + None + } else { + Some(screen.add_security_menu(pin_code, auto_lock_delay, wipe_code, check_backup)) + }; + let device = screen.add_device_menu( + device_name, + screen_brightness, + haptic_feedback, + led, + regulatory, + about, + ); + let settings = screen.add_settings_menu(bluetooth, security, device); - let is_connected = !paired_devices.is_empty(); // FIXME after BLE API has this + let is_connected = connected_idx.is_some(); // FIXME after BLE API has this let connected_subtext: Option> = is_connected.then_some("1 device connected".into()); - let mut paired_device_indices: Vec = Vec::new(); + let mut paired_device_indices: Vec = Vec::new(); for (i, device) in paired_devices.iter().enumerate() { + let connected = connected_idx == Some(i); unwrap!(paired_device_indices - .push(screen.add_subscreen(Subscreen::DeviceScreen(*device, i)))); + .push(screen.add_subscreen(Subscreen::DeviceScreen(*device, connected, i)))); } - let devices = screen.add_paired_devices_menu(paired_devices, paired_device_indices); - let pair_and_connect = screen.add_pair_and_connect_menu(devices, connected_subtext); + let pair_and_connect = + screen.add_pair_and_connect_menu(paired_devices, paired_device_indices, connected_idx); - let root = - screen.add_root_menu(failed_backup, pair_and_connect, settings, connected_subtext); + let root = screen.add_root_menu( + failed_backup, + pin_unset, + pair_and_connect, + settings, + connected_subtext, + ); screen.set_active_subscreen(root); Ok(screen) } - fn add_paired_devices_menu( + fn add_pair_and_connect_menu( &mut self, - paired_devices: Vec, 1>, - paired_device_indices: Vec, + paired_devices: Vec, MAX_PAIRED_DEVICES>, + paired_device_indices: Vec, + connected_idx: Option, ) -> usize { - let mut items: Vec = Vec::new(); - for (device, idx) in paired_devices.iter().zip(paired_device_indices) { - let mut item_device = MenuItem::new(*device, Some(Action::GoTo(idx))); + let mut items: Vec = Vec::new(); + for ((device_idx, device), submenu_idx) in + paired_devices.iter().enumerate().zip(paired_device_indices) + { + let mut item_device = MenuItem::new(*device, Some(Action::GoTo(submenu_idx))); // TODO: this should be a boolean feature of the device - item_device.with_subtext(Some(( - "Connected".into(), - Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN), - ))); + let connection_status = match connected_idx { + Some(idx) if idx == device_idx => Some(true), + _ => Some(false), + }; + item_device.with_connection_status(connection_status); unwrap!(items.push(item_device)); } + unwrap!(items.push(MenuItem::new( + TR::ble__pair_new.into(), + Some(Action::Return(DeviceMenuMsg::DevicePair)), + ))); + let mut unpair_all_item = MenuItem::new( + TR::ble__forget_all.into(), + Some(Action::Return(DeviceMenuMsg::DeviceUnpairAll)), + ); + unpair_all_item.with_stylesheet(MENU_ITEM_WARNING); + unwrap!(items.push(unpair_all_item)); + let submenu_index = self.add_submenu(Submenu::new(items)); self.add_subscreen(Subscreen::Submenu(submenu_index)) } - fn add_pair_and_connect_menu( + fn add_settings_menu( &mut self, - manage_devices_index: usize, - connected_subtext: Option>, + bluetooth: Option, + security_index: Option, + device_index: usize, ) -> usize { - let mut items: Vec = Vec::new(); - let mut manage_paired_item = MenuItem::new( - "Manage paired devices".into(), - Some(Action::GoTo(manage_devices_index)), - ); - manage_paired_item.with_subtext( - connected_subtext.map(|t| (t, Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN))), - ); - unwrap!(items.push(manage_paired_item)); + let mut items: Vec = Vec::new(); + if let Some(bluetooth) = bluetooth { + let mut bluetooth_item = MenuItem::new( + TR::words__bluetooth.into(), + Some(Action::Return(DeviceMenuMsg::Bluetooth)), + ); + let subtext = if bluetooth { + ( + TR::words__on.into(), + Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN), + ) + } else { + (TR::words__off.into(), None) + }; + bluetooth_item.with_subtext(Some(subtext)); + unwrap!(items.push(bluetooth_item)); + } + if let Some(security_index) = security_index { + unwrap!(items.push(MenuItem::new( + TR::words__security.into(), + Some(Action::GoTo(security_index)) + ))); + } unwrap!(items.push(MenuItem::new( - "Pair new device".into(), - Some(Action::Return(DeviceMenuMsg::DevicePair)), + TR::words__device.into(), + Some(Action::GoTo(device_index)) ))); let submenu_index = self.add_submenu(Submenu::new(items)); self.add_subscreen(Subscreen::Submenu(submenu_index)) } - fn add_settings_menu(&mut self, security_index: usize, device_index: usize) -> usize { - let mut items: Vec = Vec::new(); - unwrap!(items.push(MenuItem::new( - "Security".into(), - Some(Action::GoTo(security_index)) - ))); - unwrap!(items.push(MenuItem::new( - "Device".into(), - Some(Action::GoTo(device_index)) - ))); + fn add_code_menu(&mut self, wipe_code: bool) -> usize { + let mut items: Vec = Vec::new(); + let change_text = match wipe_code { + true => TR::wipe_code__change, + false => TR::pin__change, + } + .into(); + let change_action = match wipe_code { + true => Action::Return(DeviceMenuMsg::WipeCode), + false => Action::Return(DeviceMenuMsg::PinCode), + }; + let change_pin_item = MenuItem::new(change_text, Some(change_action)); + unwrap!(items.push(change_pin_item)); + + let remove_text = match wipe_code { + true => TR::wipe_code__remove, + false => TR::pin__remove, + } + .into(); + let remove_action = match wipe_code { + true => Action::Return(DeviceMenuMsg::WipeRemove), + false => Action::Return(DeviceMenuMsg::PinRemove), + }; + let mut remove_pin_item = MenuItem::new(remove_text, Some(remove_action)); + remove_pin_item.with_stylesheet(MENU_ITEM_WARNING); + unwrap!(items.push(remove_pin_item)); let submenu_index = self.add_submenu(Submenu::new(items)); self.add_subscreen(Subscreen::Submenu(submenu_index)) } - fn add_security_menu(&mut self) -> usize { - let mut items: Vec = Vec::new(); - unwrap!(items.push(MenuItem::new( - "Check backup".into(), - Some(Action::Return(DeviceMenuMsg::CheckBackup)), - ))); - unwrap!(items.push(MenuItem::new( - "Wipe device".into(), - Some(Action::Return(DeviceMenuMsg::WipeDevice)) - ))); + fn add_security_menu( + &mut self, + pin_code: Option, + auto_lock_delay: Option>, + wipe_code: Option, + check_backup: bool, + ) -> usize { + let mut items: Vec = Vec::new(); + + if let Some(pin_code) = pin_code { + let (action, subtext) = if pin_code { + let pin_menu_idx = self.add_code_menu(false); + let action = Action::GoTo(pin_menu_idx); + let subtext = ( + TR::words__on.into(), + Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN), + ); + (action, subtext) + } else { + let action = Action::Return(DeviceMenuMsg::PinCode); + let subtext = (TR::words__off.into(), None); + (action, subtext) + }; + + let mut pin_code_item = MenuItem::new("PIN code".into(), Some(action)); + pin_code_item.with_subtext(Some(subtext)); + unwrap!(items.push(pin_code_item)); + } + + if let Some(auto_lock_delay) = auto_lock_delay { + let mut auto_lock_delay_item = MenuItem::new( + TR::auto_lock__title.into(), + Some(Action::Return(DeviceMenuMsg::AutoLockDelay)), + ); + auto_lock_delay_item.with_subtext(Some((auto_lock_delay, None))); + unwrap!(items.push(auto_lock_delay_item)); + } + + if let Some(wipe_code) = wipe_code { + let (action, subtext) = if wipe_code { + let wipe_menu_idx = self.add_code_menu(true); + let action = Action::GoTo(wipe_menu_idx); + let subtext = ( + TR::words__on.into(), + Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN), + ); + (action, subtext) + } else { + let action = Action::Return(DeviceMenuMsg::WipeCode); + let subtext = (TR::words__off.into(), None); + (action, subtext) + }; + + let mut wipe_code_item = MenuItem::new("Wipe code".into(), Some(action)); + wipe_code_item.with_subtext(Some(subtext)); + unwrap!(items.push(wipe_code_item)); + } + + if check_backup { + unwrap!(items.push(MenuItem::new( + TR::reset__check_backup_title.into(), + Some(Action::Return(DeviceMenuMsg::CheckBackup)), + ))); + } let submenu_index = self.add_submenu(Submenu::new(items)); self.add_subscreen(Subscreen::Submenu(submenu_index)) @@ -288,11 +454,13 @@ impl<'a> DeviceMenuScreen<'a> { fn add_device_menu( &mut self, device_name: Option>, + screen_brightness: Option>, + haptic_feedback: Option, + led: Option, regulatory_index: usize, about_index: usize, - auto_lock_delay: TString<'static>, ) -> usize { - let mut items: Vec = Vec::new(); + let mut items: Vec = Vec::new(); if let Some(device_name) = device_name { let mut item_device_name = MenuItem::new( TR::words__name.into(), @@ -302,18 +470,46 @@ impl<'a> DeviceMenuScreen<'a> { unwrap!(items.push(item_device_name)); } - unwrap!(items.push(MenuItem::new( - "Screen brightness".into(), - Some(Action::Return(DeviceMenuMsg::ScreenBrightness)), - ))); + if let Some(brightness) = screen_brightness { + let brightness_item = MenuItem::new( + brightness, + Some(Action::Return(DeviceMenuMsg::ScreenBrightness)), + ); + unwrap!(items.push(brightness_item)); + } - if has_pin() { - let mut autolock_delay_item = MenuItem::new( - "Auto-lock delay".into(), - Some(Action::Return(DeviceMenuMsg::AutoLockDelay)), + if let Some(haptic_feedback) = haptic_feedback { + let mut haptic_item = MenuItem::new( + TR::haptic_feedback__title.into(), + Some(Action::Return(DeviceMenuMsg::HapticFeedback)), + ); + let subtext = if haptic_feedback { + ( + TR::words__on.into(), + Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN), + ) + } else { + (TR::words__off.into(), None) + }; + haptic_item.with_subtext(Some(subtext)); + unwrap!(items.push(haptic_item)); + } + + if let Some(led) = led { + let mut led_item = MenuItem::new( + TR::words__led.into(), + Some(Action::Return(DeviceMenuMsg::Led)), ); - autolock_delay_item.with_subtext(Some((auto_lock_delay, None))); - unwrap!(items.push(autolock_delay_item)); + let subtext = if led { + ( + TR::words__on.into(), + Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN), + ) + } else { + (TR::words__off.into(), None) + }; + led_item.with_subtext(Some(subtext)); + unwrap!(items.push(led_item)); } unwrap!(items.push(MenuItem::new( @@ -322,10 +518,17 @@ impl<'a> DeviceMenuScreen<'a> { ))); unwrap!(items.push(MenuItem::new( - "About".into(), + TR::words__about.into(), Some(Action::GoTo(about_index)) ))); + let mut wipe_device_item = MenuItem::new( + TR::wipe__title.into(), + Some(Action::Return(DeviceMenuMsg::WipeDevice)), + ); + wipe_device_item.with_stylesheet(MENU_ITEM_WARNING); + unwrap!(items.push(wipe_device_item)); + let submenu_index = self.add_submenu(Submenu::new(items)); self.add_subscreen(Subscreen::Submenu(submenu_index)) } @@ -333,22 +536,32 @@ impl<'a> DeviceMenuScreen<'a> { fn add_root_menu( &mut self, failed_backup: bool, + pin_unset: bool, pair_and_connect_index: usize, settings_index: usize, connected_subtext: Option>, ) -> usize { - let mut items: Vec = Vec::new(); + let mut items: Vec = Vec::new(); if failed_backup { let mut item_backup_failed = MenuItem::new( - "Backup failed".into(), + TR::homescreen__title_backup_failed.into(), Some(Action::Return(DeviceMenuMsg::BackupFailed)), ); - item_backup_failed.with_subtext(Some(("Review".into(), None))); - item_backup_failed.with_stylesheet(MENU_ITEM_TITLE_STYLE_SHEET); + item_backup_failed.with_subtext(Some((TR::words__review.into(), None))); + item_backup_failed.with_stylesheet(MENU_ITEM_NORMAL); unwrap!(items.push(item_backup_failed)); } + if pin_unset { + let mut item_pin_unset = MenuItem::new( + TR::homescreen__title_pin_not_set.into(), + Some(Action::Return(DeviceMenuMsg::PinCode)), + ); + item_pin_unset.with_subtext(Some((TR::words__review.into(), None))); + item_pin_unset.with_stylesheet(MENU_ITEM_LIGHT_WARNING); + unwrap!(items.push(item_pin_unset)); + } let mut item_pair_and_connect = MenuItem::new( - "Pair & connect".into(), + TR::ble__pair_title.into(), Some(Action::GoTo(pair_and_connect_index)), ); item_pair_and_connect.with_subtext( @@ -356,7 +569,7 @@ impl<'a> DeviceMenuScreen<'a> { ); unwrap!(items.push(item_pair_and_connect)); unwrap!(items.push(MenuItem::new( - "Settings".into(), + TR::words__settings.into(), Some(Action::GoTo(settings_index)), ))); @@ -384,9 +597,16 @@ impl<'a> DeviceMenuScreen<'a> { match self.subscreens[self.active_subscreen] { Subscreen::Submenu(ref mut submenu_index) => { let submenu = &self.submenus[*submenu_index]; - let mut menu = VerticalMenu::::empty().with_separators(); + let mut menu = VerticalMenu::::empty().with_separators(); for item in &submenu.items { - let button = if let Some((subtext, subtext_style)) = item.subtext { + let button = if let Some(connected) = item.connection_status { + Button::new_connection_item( + item.text, + *item.stylesheet, + item.subtext.map(|(t, _)| t), + connected, + ) + } else if let Some((subtext, subtext_style)) = item.subtext { let subtext_style = subtext_style.unwrap_or(&theme::TEXT_MENU_ITEM_SUBTITLE); Button::new_menu_item_with_subtext( @@ -398,6 +618,7 @@ impl<'a> DeviceMenuScreen<'a> { } else { Button::new_menu_item(item.text, *item.stylesheet) }; + menu.item(button); } let mut header = Header::new(TR::buttons__back.into()).with_close_button(); @@ -412,33 +633,47 @@ impl<'a> DeviceMenuScreen<'a> { *self.active_screen.deref_mut() = ActiveScreen::Menu(VerticalMenuScreen::new(menu).with_header(header)); } - Subscreen::DeviceScreen(device, _) => { + Subscreen::DeviceScreen(device, connected, _) => { let mut menu = VerticalMenu::empty().with_separators(); - menu.item(Button::new_menu_item(device, theme::menu_item_title())); + let text = if connected { + TR::words__disconnect + } else { + TR::words__connect + }; + menu.item(Button::new_menu_item(text.into(), theme::menu_item_title())); menu.item(Button::new_menu_item( - "Disconnect".into(), - theme::menu_item_title_red(), + TR::words__forget.into(), + theme::menu_item_title_orange(), )); - *self.active_screen.deref_mut() = ActiveScreen::Menu( - VerticalMenuScreen::new(menu).with_header( - Header::new("Manage".into()) - .with_close_button() - .with_left_button( - Button::with_icon(theme::ICON_CHEVRON_LEFT), - HeaderMsg::Back, - ), - ), + *self.active_screen.deref_mut() = ActiveScreen::Device( + VerticalMenuScreen::new(menu) + .with_header( + Header::new(TR::buttons__back.into()) + .with_close_button() + .with_left_button( + Button::with_icon(theme::ICON_CHEVRON_LEFT), + HeaderMsg::Back, + ), + ) + .with_subtitle(device), ); } Subscreen::AboutScreen => { - let about_content = Paragraphs::new([ - Paragraph::new(&theme::firmware::TEXT_REGULAR, "Firmware version"), - Paragraph::new(&theme::firmware::TEXT_REGULAR, self.firmware_version), - ]); - *self.active_screen.deref_mut() = ActiveScreen::About( - TextScreen::new(about_content) - .with_header(Header::new("About".into()).with_close_button()), + TextScreen::new( + PropsList::new_styled( + self.about_items, + &theme::TEXT_SMALL_LIGHT, + &theme::TEXT_MONO_MEDIUM_LIGHT, + &theme::TEXT_MONO_MEDIUM_LIGHT_DATA, + theme::PROP_INNER_SPACING, + theme::PROPS_SPACING, + ) + .unwrap_or_else(|_| unwrap!(PropsList::empty())) + .into_paragraphs() + .with_placement(LinearPlacement::vertical()), + ) + .with_header(Header::new(TR::words__about.into()).with_close_button()), ); } Subscreen::RegulatoryScreen => { @@ -486,7 +721,7 @@ impl<'a> DeviceMenuScreen<'a> { } } -impl<'a> Component for DeviceMenuScreen<'a> { +impl Component for DeviceMenuScreen { type Msg = DeviceMenuMsg; fn place(&mut self, bounds: Rect) -> Rect { @@ -500,6 +735,9 @@ impl<'a> Component for DeviceMenuScreen<'a> { ActiveScreen::Menu(menu) => { menu.place(bounds); } + ActiveScreen::Device(device) => { + device.place(bounds); + } ActiveScreen::About(about) => { about.place(bounds); } @@ -516,17 +754,32 @@ impl<'a> Component for DeviceMenuScreen<'a> { // Handle the event for the active menu let subscreen = &self.subscreens[self.active_subscreen]; match (subscreen, self.active_screen.deref_mut()) { - (Subscreen::Submenu(..) | Subscreen::DeviceScreen(..), ActiveScreen::Menu(menu)) => { + (Subscreen::Submenu(..), ActiveScreen::Menu(menu)) => match menu.event(ctx, event) { + Some(VerticalMenuScreenMsg::Selected(button_idx)) => { + return self.handle_submenu(ctx, button_idx); + } + Some(VerticalMenuScreenMsg::Back) => { + return self.go_back(ctx); + } + Some(VerticalMenuScreenMsg::Close) => { + return Some(DeviceMenuMsg::Close); + } + _ => {} + }, + (Subscreen::DeviceScreen(_, connected, device_idx), ActiveScreen::Device(menu)) => { match menu.event(ctx, event) { - Some(VerticalMenuScreenMsg::Selected(index)) => { - if let Subscreen::DeviceScreen(_, i) = subscreen { - if index == DISCONNECT_DEVICE_MENU_INDEX { - return Some(DeviceMenuMsg::DeviceDisconnect(*i)); - } - } else { - return self.handle_submenu(ctx, index); + Some(VerticalMenuScreenMsg::Selected(button_idx)) => match button_idx { + DIS_CONNECT_DEVICE_MENU_INDEX if *connected => { + return Some(DeviceMenuMsg::DeviceDisconnect); } - } + DIS_CONNECT_DEVICE_MENU_INDEX if !*connected => { + return Some(DeviceMenuMsg::DeviceConnect(*device_idx)); + } + FORGET_DEVICE_MENU_INDEX => { + return Some(DeviceMenuMsg::DeviceUnpair(*device_idx)); + } + _ => {} + }, Some(VerticalMenuScreenMsg::Back) => { return self.go_back(ctx); } @@ -555,6 +808,7 @@ impl<'a> Component for DeviceMenuScreen<'a> { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { match self.active_screen.deref() { ActiveScreen::Menu(menu) => menu.render(target), + ActiveScreen::Device(device) => device.render(target), ActiveScreen::About(about) => about.render(target), ActiveScreen::Regulatory(regulatory) => regulatory.render(target), ActiveScreen::Empty => {} @@ -563,8 +817,26 @@ impl<'a> Component for DeviceMenuScreen<'a> { } #[cfg(feature = "ui_debug")] -impl<'a> crate::trace::Trace for DeviceMenuScreen<'a> { +impl crate::trace::Trace for DeviceMenuScreen { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("DeviceMenuScreen"); + + match self.active_screen.deref() { + ActiveScreen::Menu(ref screen) => { + t.child("Menu", screen); + } + ActiveScreen::Device(ref screen) => { + t.child("Device", screen); + } + ActiveScreen::About(ref screen) => { + t.child("About", screen); + } + ActiveScreen::Regulatory(ref screen) => { + t.child("Regulatory", screen); + } + ActiveScreen::Empty => { + t.string("ActiveScreen", "None".into()); + } + } } } diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs index 78972cf180e..779c7288c5c 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs @@ -52,8 +52,8 @@ pub use value_input_screen::{ DurationInput, NumberInput, ValueInput, ValueInputScreen, ValueInputScreenMsg, }; pub use vertical_menu::{ - LongMenuGc, MenuItems, ShortMenuVec, VerticalMenu, VerticalMenuMsg, LONG_MENU_ITEMS, - SHORT_MENU_ITEMS, + LongMenuGc, MediumMenuVec, MenuItems, ShortMenuVec, VerticalMenu, VerticalMenuMsg, + LONG_MENU_ITEMS, MEDIUM_MENU_ITEMS, SHORT_MENU_ITEMS, }; pub use vertical_menu_screen::{VerticalMenuScreen, VerticalMenuScreenMsg}; diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs index 50b61882eb0..d400cae4930 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs @@ -20,10 +20,12 @@ use heapless::Vec; /// Number of buttons. /// Presently, VerticalMenu holds only fixed number of buttons. pub const LONG_MENU_ITEMS: usize = 100; +pub const MEDIUM_MENU_ITEMS: usize = 10; pub const SHORT_MENU_ITEMS: usize = 5; pub type LongMenuGc = GcBox>; pub type ShortMenuVec = Vec; +pub type MediumMenuVec = Vec; pub trait MenuItems: Default { fn empty() -> Self { @@ -36,7 +38,7 @@ pub trait MenuItems: Default { fn get_last(&self) -> Option<&Button>; } -impl MenuItems for ShortMenuVec { +impl MenuItems for Vec { fn push(&mut self, button: Button) { unwrap!(self.push(button)); } diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu_screen.rs index 067b78b5cc0..09cd4eb81b8 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu_screen.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu_screen.rs @@ -3,11 +3,12 @@ use crate::{ ui::{ component::{ swipe_detect::{SwipeConfig, SwipeSettings}, - Component, Event, EventCtx, SwipeDetect, + text::{layout::LayoutFit, TextStyle}, + Component, Event, EventCtx, Label, SwipeDetect, TextLayout, }, event::SwipeEvent, flow::Swipable, - geometry::{Alignment2D, Direction, Rect}, + geometry::{Alignment2D, Direction, Offset, Rect}, shape::{Renderer, ToifImage}, util::{animation_disabled, Pager}, }, @@ -17,6 +18,8 @@ use super::{constant::SCREEN, theme, Header, HeaderMsg, MenuItems, VerticalMenu, pub struct VerticalMenuScreen { header: Header, + /// Optional subtitle label + subtitle: Option>, /// Scrollable vertical menu menu: VerticalMenu, /// Base position of the menu sliding window to scroll around @@ -39,9 +42,14 @@ pub enum VerticalMenuScreenMsg { impl VerticalMenuScreen { const TOUCH_SENSITIVITY_DIVIDER: i16 = 15; + const SUBTITLE_STYLE: TextStyle = theme::TEXT_MEDIUM_GREY; + const SUBTITLE_HEIGHT: i16 = 68; + const SUBTITLE_DOUBLE_HEIGHT: i16 = 100; + pub fn new(menu: VerticalMenu) -> Self { Self { header: Header::new(TString::empty()), + subtitle: None, menu, offset_base: 0, swipe: None, @@ -56,6 +64,14 @@ impl VerticalMenuScreen { self } + pub fn with_subtitle(mut self, subtitle: TString<'static>) -> Self { + if !subtitle.is_empty() { + self.subtitle = + Some(Label::left_aligned(subtitle, Self::SUBTITLE_STYLE).vertically_centered()); + } + self + } + /// Update swipe detection and buttons state based on menu size pub fn initialize_screen(&mut self, ctx: &mut EventCtx) { if animation_disabled() { @@ -169,7 +185,30 @@ impl Component for VerticalMenuScreen { debug_assert_eq!(bounds.height(), SCREEN.height()); debug_assert_eq!(bounds.width(), SCREEN.width()); - let (header_area, menu_area) = bounds.split_top(Header::HEADER_HEIGHT); + let (header_area, rest) = bounds.split_top(Header::HEADER_HEIGHT); + + let menu_area = if let Some(subtitle) = &mut self.subtitle { + // Choose appropriate height for the subtitle + let subtitle_height = if let LayoutFit::OutOfBounds { .. } = + subtitle.text().map(|text| { + TextLayout::new(Self::SUBTITLE_STYLE) + .with_bounds( + Rect::from_size(Offset::new(bounds.width(), Self::SUBTITLE_HEIGHT)) + .inset(theme::SIDE_INSETS), + ) + .fit_text(text) + }) { + Self::SUBTITLE_DOUBLE_HEIGHT + } else { + Self::SUBTITLE_HEIGHT + }; + + let (subtitle_area, rest) = rest.split_top(subtitle_height); + subtitle.place(subtitle_area.inset(theme::SIDE_INSETS)); + rest + } else { + rest + }; self.header.place(header_area); self.menu.place(menu_area); @@ -202,6 +241,7 @@ impl Component for VerticalMenuScreen { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { self.header.render(target); + self.subtitle.render(target); self.menu.render(target); self.render_overflow_arrow(target); } diff --git a/core/embed/rust/src/ui/layout_eckhart/res/square.png b/core/embed/rust/src/ui/layout_eckhart/res/square.png new file mode 100644 index 00000000000..55af05f3dfc Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/square.png differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/square.toif b/core/embed/rust/src/ui/layout_eckhart/res/square.toif new file mode 100644 index 00000000000..98d4f584c8d Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/square.toif differ diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs index d34e7fd36bb..89758dd64a0 100644 --- a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs @@ -168,6 +168,9 @@ include_icon!(ICON_EUROPE, "layout_eckhart/res/europe.toif"); include_icon!(ICON_RCM, "layout_eckhart/res/rcm.toif"); include_icon!(ICON_FCC, "layout_eckhart/res/fcc.toif"); +// Square icon for BLE connection items +include_icon!(ICON_SQUARE, "layout_eckhart/res/square.toif"); + // Common text styles and button styles must use fonts accessible from both // bootloader and firmware diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs index 8ab822c275b..93a456d9ce9 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -27,7 +27,7 @@ use crate::{ }, ui_firmware::{ FirmwareUI, ERROR_NOT_IMPLEMENTED, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, - MAX_MENU_ITEMS, MAX_WORD_QUIZ_ITEMS, + MAX_MENU_ITEMS, MAX_PAIRED_DEVICES, MAX_WORD_QUIZ_ITEMS, }, ModelUI, }, @@ -51,14 +51,12 @@ use super::{ UIEckhart, }; -use heapless::Vec; - impl FirmwareUI for UIEckhart { fn confirm_action( title: TString<'static>, action: Option>, description: Option>, - subtitle: Option>, + _subtitle: Option>, verb: Option>, _verb_cancel: Option>, hold: bool, @@ -106,15 +104,12 @@ impl FirmwareUI for UIEckhart { } }; - let mut screen = TextScreen::new(paragraphs) + let screen = TextScreen::new(paragraphs) .with_header(Header::new(title)) .with_action_bar(ActionBar::new_double( Button::with_icon(theme::ICON_CROSS), right_button, )); - if let Some(subtitle) = subtitle { - screen = screen.with_hint(Hint::new_instruction(subtitle, None)); - } let layout = RootComponent::new(screen); Ok(layout) } @@ -1195,17 +1190,35 @@ impl FirmwareUI for UIEckhart { fn show_device_menu( failed_backup: bool, - firmware_version: TString<'static>, + pin_unset: bool, + paired_devices: heapless::Vec, MAX_PAIRED_DEVICES>, + connected_idx: Option, + bluetooth: Option, + pin_code: Option, + auto_lock_delay: Option>, + wipe_code: Option, + check_backup: bool, device_name: Option>, - paired_devices: Vec, 1>, - auto_lock_delay: TString<'static>, + screen_brightness: Option>, + haptic_feedback: Option, + led: Option, + about_items: Obj, ) -> Result { let layout = RootComponent::new(DeviceMenuScreen::new( failed_backup, - firmware_version, - device_name, + pin_unset, paired_devices, + connected_idx, + bluetooth, + pin_code, auto_lock_delay, + wipe_code, + check_backup, + device_name, + screen_brightness, + haptic_feedback, + led, + about_items, )?); Ok(layout) } diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs index 0f488cc1a60..7549f3abe4e 100644 --- a/core/embed/rust/src/ui/ui_firmware.rs +++ b/core/embed/rust/src/ui/ui_firmware.rs @@ -16,6 +16,8 @@ pub const MAX_WORD_QUIZ_ITEMS: usize = 3; pub const MAX_GROUP_SHARE_LINES: usize = 4; pub const MAX_MENU_ITEMS: usize = 5; +pub const MAX_PAIRED_DEVICES: usize = 4; // Maximum number of paired devices in the device menu + pub const ERROR_NOT_IMPLEMENTED: Error = Error::ValueError(c"not implemented"); pub trait FirmwareUI { @@ -358,12 +360,22 @@ pub trait FirmwareUI { lockable: bool, ) -> Result; + #[allow(clippy::too_many_arguments)] fn show_device_menu( failed_backup: bool, - firmware_version: TString<'static>, + pin_unset: bool, + paired_devices: heapless::Vec, MAX_PAIRED_DEVICES>, + connected_idx: Option, + bluetooth: Option, + pin_code: Option, + auto_lock_delay: Option>, + wipe_code: Option, + check_backup: bool, device_name: Option>, - paired_devices: Vec, 1>, - auto_lock_delay: TString<'static>, + screen_brightness: Option>, + haptic_feedback: Option, + led: Option, + about_items: Obj, ) -> Result; fn show_pairing_device_name( diff --git a/core/embed/sys/syscall/inc/sys/syscall_numbers.h b/core/embed/sys/syscall/inc/sys/syscall_numbers.h index 183fa0e4bc8..61fa289fbef 100644 --- a/core/embed/sys/syscall/inc/sys/syscall_numbers.h +++ b/core/embed/sys/syscall/inc/sys/syscall_numbers.h @@ -105,6 +105,8 @@ typedef enum { SYSCALL_TOUCH_GET_EVENT, + SYSCALL_RGB_LED_SET_ENABLED, + SYSCALL_RGB_LED_GET_ENABLED, SYSCALL_RGB_LED_SET_COLOR, SYSCALL_HAPTIC_SET_ENABLED, diff --git a/core/embed/sys/syscall/stm32/syscall_dispatch.c b/core/embed/sys/syscall/stm32/syscall_dispatch.c index e93cf8572a8..269787dc811 100644 --- a/core/embed/sys/syscall/stm32/syscall_dispatch.c +++ b/core/embed/sys/syscall/stm32/syscall_dispatch.c @@ -454,6 +454,15 @@ __attribute((no_stack_protector)) void syscall_handler(uint32_t *args, #endif #ifdef USE_RGB_LED + case SYSCALL_RGB_LED_SET_ENABLED: { + bool enabled = (args[0] != 0); + rgb_led_set_enabled(enabled); + } break; + + case SYSCALL_RGB_LED_GET_ENABLED: { + args[0] = rgb_led_get_enabled(); + } break; + case SYSCALL_RGB_LED_SET_COLOR: { uint32_t color = args[0]; rgb_led_set_color(color); diff --git a/core/embed/sys/syscall/stm32/syscall_stubs.c b/core/embed/sys/syscall/stm32/syscall_stubs.c index 12e2150ebe6..5802ca0b186 100644 --- a/core/embed/sys/syscall/stm32/syscall_stubs.c +++ b/core/embed/sys/syscall/stm32/syscall_stubs.c @@ -420,6 +420,15 @@ uint32_t touch_get_event(void) { #ifdef USE_RGB_LED #include + +void rgb_led_set_enabled(bool enabled) { + syscall_invoke1((uint32_t)enabled, SYSCALL_RGB_LED_SET_ENABLED); +} + +bool rgb_led_get_enabled(void) { + return (bool)syscall_invoke0(SYSCALL_RGB_LED_GET_ENABLED); +} + void rgb_led_set_color(uint32_t color) { syscall_invoke1(color, SYSCALL_RGB_LED_SET_COLOR); } diff --git a/core/embed/sys/task/sysevent.c b/core/embed/sys/task/sysevent.c index b79eec43ec0..1adad935d2e 100644 --- a/core/embed/sys/task/sysevent.c +++ b/core/embed/sys/task/sysevent.c @@ -27,6 +27,7 @@ #include #ifdef TREZOR_EMULATOR +#include #include #endif @@ -188,6 +189,8 @@ void sysevents_poll(const sysevents_t *awaited, sysevents_t *signalled, #ifdef TREZOR_EMULATOR // Poll SDL events and dispatch them sdl_events_poll(); + // Refresh display to catch the end of the haptic feedback + display_refresh(); #endif syshandle_mask_t handles_to_read = 0; @@ -233,6 +236,8 @@ void sysevents_poll(const sysevents_t *awaited, sysevents_t *signalled, } #ifdef TREZOR_EMULATOR + // Refresh display to catch the end of the haptic feedback + display_refresh(); // Wait a bit to not consume 100% CPU systick_delay_ms(1); #else diff --git a/core/embed/upymod/modtrezorio/modtrezorio-rgb_led.h b/core/embed/upymod/modtrezorio/modtrezorio-rgb_led.h new file mode 100644 index 00000000000..1489dbe7785 --- /dev/null +++ b/core/embed/upymod/modtrezorio/modtrezorio-rgb_led.h @@ -0,0 +1,47 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +/// package: trezorio.rgb_led + +/// def rgb_led_set_enabled(enable: bool) -> None: +/// """ +/// Enable/Disable the RGB LED. +/// """ +STATIC mp_obj_t mod_trezorio_rgb_led_set_enabled(mp_obj_t enable) { + rgb_led_set_enabled(mp_obj_is_true(enable)); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorio_rgb_led_set_enabled_obj, + mod_trezorio_rgb_led_set_enabled); + +STATIC const mp_rom_map_elem_t mod_trezorio_rgb_led_globals_table[] = { + {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_rgb_led)}, + {MP_ROM_QSTR(MP_QSTR_rgb_led_set_enabled), + MP_ROM_PTR(&mod_trezorio_rgb_led_set_enabled_obj)}, + +}; +STATIC MP_DEFINE_CONST_DICT(mod_trezorio_rgb_led_globals, + mod_trezorio_rgb_led_globals_table); + +STATIC const mp_obj_module_t mod_trezorio_rgb_led_module = { + .base = {&mp_type_module}, + .globals = (mp_obj_dict_t *)&mod_trezorio_rgb_led_globals, +}; diff --git a/core/embed/upymod/modtrezorio/modtrezorio.c b/core/embed/upymod/modtrezorio/modtrezorio.c index dd2ba5487de..479a58a46d2 100644 --- a/core/embed/upymod/modtrezorio/modtrezorio.c +++ b/core/embed/upymod/modtrezorio/modtrezorio.c @@ -59,12 +59,15 @@ uint32_t last_touch_sample_time = 0; #ifdef USE_HAPTIC #include "modtrezorio-haptic.h" #endif +#ifdef USE_RGB_LED +#include "modtrezorio-rgb_led.h" +#endif #ifdef USE_POWER_MANAGER #include "modtrezorio-pm.h" #endif /// package: trezorio.__init__ -/// from . import fatfs, haptic, sdcard, ble, pm +/// from . import fatfs, haptic, sdcard, ble, pm, rgb_led /// POLL_READ: int # wait until interface is readable and return read data /// POLL_WRITE: int # wait until interface is writable @@ -101,6 +104,10 @@ STATIC const mp_rom_map_elem_t mp_module_trezorio_globals_table[] = { {MP_ROM_QSTR(MP_QSTR_haptic), MP_ROM_PTR(&mod_trezorio_haptic_module)}, #endif +#ifdef USE_RGB_LED + {MP_ROM_QSTR(MP_QSTR_rgb_led), MP_ROM_PTR(&mod_trezorio_rgb_led_module)}, +#endif + #ifdef USE_BLE {MP_ROM_QSTR(MP_QSTR_BLE_EVENT), MP_ROM_INT(SYSHANDLE_BLE)}, #endif diff --git a/core/embed/upymod/modtrezorutils/modtrezorutils.c b/core/embed/upymod/modtrezorutils/modtrezorutils.c index 9c3e3c0716c..1e224f51858 100644 --- a/core/embed/upymod/modtrezorutils/modtrezorutils.c +++ b/core/embed/upymod/modtrezorutils/modtrezorutils.c @@ -603,6 +603,8 @@ STATIC mp_obj_tuple_t mod_trezorutils_version_obj = { /// """Whether the hardware supports backlight brightness control.""" /// USE_HAPTIC: bool /// """Whether the hardware supports haptic feedback.""" +/// USE_RGB_LED: bool +/// """Whether the hardware supports RGB LED.""" /// USE_OPTIGA: bool /// """Whether the hardware supports Optiga secure element.""" /// USE_TROPIC: bool @@ -709,6 +711,11 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = { #else {MP_ROM_QSTR(MP_QSTR_USE_HAPTIC), mp_const_false}, #endif +#ifdef USE_RGB_LED + {MP_ROM_QSTR(MP_QSTR_USE_RGB_LED), mp_const_true}, +#else + {MP_ROM_QSTR(MP_QSTR_USE_RGB_LED), mp_const_false}, +#endif #ifdef USE_OPTIGA {MP_ROM_QSTR(MP_QSTR_USE_OPTIGA), mp_const_true}, #else diff --git a/core/mocks/generated/trezorio/__init__.pyi b/core/mocks/generated/trezorio/__init__.pyi index 287fd95e684..1c136e1087d 100644 --- a/core/mocks/generated/trezorio/__init__.pyi +++ b/core/mocks/generated/trezorio/__init__.pyi @@ -166,7 +166,7 @@ class WebUSB: """Length of one USB RX packet.""" TX_PACKET_LEN: ClassVar[int] """Length of one USB TX packet.""" -from . import fatfs, haptic, sdcard, ble, pm +from . import fatfs, haptic, sdcard, ble, pm, rgb_led POLL_READ: int # wait until interface is readable and return read data POLL_WRITE: int # wait until interface is writable diff --git a/core/mocks/generated/trezorio/rgb_led.pyi b/core/mocks/generated/trezorio/rgb_led.pyi new file mode 100644 index 00000000000..8844686ad11 --- /dev/null +++ b/core/mocks/generated/trezorio/rgb_led.pyi @@ -0,0 +1,8 @@ +from typing import * + + +# upymod/modtrezorio/modtrezorio-rgb_led.h +def rgb_led_set_enabled(enable: bool) -> None: + """ + Enable/Disable the RGB LED. + """ diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi index 28ac8add2ee..eff9700ee5a 100644 --- a/core/mocks/generated/trezorui_api.pyi +++ b/core/mocks/generated/trezorui_api.pyi @@ -623,10 +623,19 @@ def show_homescreen( def show_device_menu( *, failed_backup: bool, - firmware_version: str, - device_name: str | None, + pin_unset: bool, paired_devices: Iterable[str], - auto_lock_delay: str, + connected_idx: int | None, + bluetooth: bool | None, + pin_code: bool | None, + auto_lock_delay: str | None, + wipe_code: bool | None, + check_backup: bool, + device_name: str | None, + screen_brightness: str | None, + haptic_feedback: bool | None, + led: bool | None, + about_items: list[tuple[str | None, str | bytes | None, bool | None]], ) -> LayoutObj[UiResult | DeviceMenuResult | tuple[DeviceMenuResult, int]]: """Show the device menu.""" @@ -843,10 +852,20 @@ class LayoutState: class DeviceMenuResult: """Result of a device menu operation.""" BackupFailed: ClassVar[DeviceMenuResult] - DevicePair: ClassVar[DeviceMenuResult] + DeviceConnect: ClassVar[DeviceMenuResult] DeviceDisconnect: ClassVar[DeviceMenuResult] - CheckBackup: ClassVar[DeviceMenuResult] - WipeDevice: ClassVar[DeviceMenuResult] - ScreenBrightness: ClassVar[DeviceMenuResult] + DevicePair: ClassVar[DeviceMenuResult] + DeviceUnpair: ClassVar[DeviceMenuResult] + DeviceUnpairAll: ClassVar[DeviceMenuResult] + Bluetooth: ClassVar[DeviceMenuResult] + PinCode: ClassVar[DeviceMenuResult] + PinRemove: ClassVar[DeviceMenuResult] AutoLockDelay: ClassVar[DeviceMenuResult] + WipeCode: ClassVar[DeviceMenuResult] + WipeRemove: ClassVar[DeviceMenuResult] + CheckBackup: ClassVar[DeviceMenuResult] DeviceName: ClassVar[DeviceMenuResult] + ScreenBrightness: ClassVar[DeviceMenuResult] + HapticFeedback: ClassVar[DeviceMenuResult] + Led: ClassVar[DeviceMenuResult] + WipeDevice: ClassVar[DeviceMenuResult] diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi index 3798ac5fec3..a45faed55ee 100644 --- a/core/mocks/generated/trezorutils.pyi +++ b/core/mocks/generated/trezorutils.pyi @@ -191,6 +191,8 @@ USE_BACKLIGHT: bool """Whether the hardware supports backlight brightness control.""" USE_HAPTIC: bool """Whether the hardware supports haptic feedback.""" +USE_RGB_LED: bool +"""Whether the hardware supports RGB LED.""" USE_OPTIGA: bool """Whether the hardware supports Optiga secure element.""" USE_TROPIC: bool diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index f10b1bd7c85..635aabe0695 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -75,9 +75,16 @@ class TR: bitcoin__unverified_external_inputs: str = "The transaction contains unverified external inputs." bitcoin__valid_signature: str = "The signature is valid." bitcoin__voting_rights: str = "Voting rights to" + ble__disable: str = "Turn Bluetooth off?" + ble__enable: str = "Turn Bluetooth on?" + ble__forget_all: str = "Forget all" + ble__manage_paired: str = "Manage paired devices" + ble__pair_new: str = "Pair new device" + ble__pair_title: str = "Pair & connect" ble__unpair_all: str = "Unpair all bluetooth devices" ble__unpair_current: str = "Unpair connected device" ble__unpair_title: str = "Unpair" + ble__version: str = "Bluetooth version" brightness__change_title: str = "Change display brightness" brightness__changed_title: str = "Display brightness changed" brightness__title: str = "Display brightness" @@ -381,6 +388,8 @@ class TR: haptic_feedback__title: str = "Haptic feedback" homescreen__click_to_connect: str = "Click to Connect" homescreen__click_to_unlock: str = "Click to Unlock" + homescreen__firmware_type: str = "Firmware type" + homescreen__firmware_version: str = "Firmware version" homescreen__set_default: str = "Change wallpaper to default image?" homescreen__settings_subtitle: str = "Settings" homescreen__settings_title: str = "Homescreen" @@ -426,6 +435,9 @@ class TR: language__changed: str = "Language changed successfully" language__progress: str = "Changing language" language__title: str = "Language settings" + led__disable: str = "Disable LED?" + led__enable: str = "Enable LED?" + led__title: str = "LED" lockscreen__tap_to_connect: str = "Tap to connect" lockscreen__tap_to_unlock: str = "Tap to unlock" lockscreen__title_locked: str = "Locked" @@ -538,7 +550,8 @@ class TR: pin__cancel_description: str = "Continue without PIN" pin__cancel_info: str = "Without a PIN, anyone can access this device." pin__cancel_setup: str = "Cancel PIN setup" - pin__change: str = "Change PIN?" + pin__change: str = "Change PIN" + pin__change_question: str = "Change PIN?" pin__changed: str = "PIN changed." pin__cursor_will_change: str = "Position of the cursor will change between entries for enhanced security." pin__diff_from_wipe_code: str = "The new PIN must be different from your wipe code." @@ -556,6 +569,7 @@ class TR: pin__reenter: str = "Re-enter PIN" pin__reenter_new: str = "Re-enter new PIN" pin__reenter_to_confirm: str = "Please re-enter PIN to confirm." + pin__remove: str = "Remove PIN" pin__setup_completed: str = "PIN setup completed." pin__should_be_long: str = "PIN should be 4-50 digits long." pin__title_check_pin: str = "Check PIN" @@ -956,7 +970,8 @@ class TR: wipe__info: str = "All data will be erased." wipe__title: str = "Wipe device" wipe__want_to_wipe: str = "Do you really want to wipe the device?\n" - wipe_code__change: str = "Change wipe code?" + wipe_code__change: str = "Change wipe code" + wipe_code__change_question: str = "Change wipe code?" wipe_code__changed: str = "Wipe code changed." wipe_code__diff_from_pin: str = "The wipe code must be different from your PIN." wipe_code__disabled: str = "Wipe code disabled." @@ -967,6 +982,7 @@ class TR: wipe_code__mismatch: str = "The wipe codes you entered do not match." wipe_code__reenter: str = "Re-enter wipe code" wipe_code__reenter_to_confirm: str = "Please re-enter wipe code to confirm." + wipe_code__remove: str = "Remove wipe code" wipe_code__title_check: str = "Check wipe code" wipe_code__title_invalid: str = "Invalid wipe code" wipe_code__title_settings: str = "Wipe code settings" @@ -974,6 +990,7 @@ class TR: wipe_code__turn_on: str = "Turn on wipe code protection?" wipe_code__wipe_code_mismatch: str = "Wipe code mismatch" word_count__title: str = "Number of words" + words__about: str = "About" words__account: str = "Account" words__account_colon: str = "Account:" words__address: str = "Address" @@ -984,28 +1001,39 @@ class TR: words__assets: str = "Assets" words__authenticate: str = "Authenticate" words__blockhash: str = "Blockhash" + words__bluetooth: str = "Bluetooth" words__buying: str = "Buying" words__cancel_and_exit: str = "Cancel and exit" words__cancel_question: str = "Cancel?" words__chain: str = "Chain" words__confirm: str = "Confirm" words__confirm_fee: str = "Confirm fee" + words__connect: str = "Connect" + words__connected: str = "Connected" words__contains: str = "Contains" words__continue_anyway: str = "Continue anyway" words__continue_anyway_question: str = "Continue anyway?" words__continue_with: str = "Continue with" + words__device: str = "Device" + words__disconnect: str = "Disconnect" + words__disconnected: str = "Disconnected" words__error: str = "Error" words__fee: str = "Fee" + words__forget: str = "Forget" words__from: str = "from" words__good_to_know: str = "Good to know" words__important: str = "Important" words__instructions: str = "Instructions" words__keep_it_safe: str = "Keep it safe!" words__know_what_your_doing: str = "Continue only if you know what you are doing!" + words__led: str = "LED" + words__manage: str = "Manage" words__my_trezor: str = "My Trezor" words__name: str = "Name" words__no: str = "No" words__not_recommended: str = "Not recommended!" + words__off: str = "OFF" + words__on: str = "ON" words__operation_cancelled: str = "Operation cancelled" words__outputs: str = "outputs" words__pay_attention: str = "Pay attention" @@ -1016,6 +1044,8 @@ class TR: words__receive: str = "Receive" words__recipient: str = "Recipient" words__recovery_share: str = "Recovery share" + words__review: str = "Review" + words__security: str = "Security" words__send: str = "Send" words__settings: str = "Settings" words__sign: str = "Sign" diff --git a/core/site_scons/models/T3T1/emulator.py b/core/site_scons/models/T3T1/emulator.py index 2b005959b26..e247f375575 100644 --- a/core/site_scons/models/T3T1/emulator.py +++ b/core/site_scons/models/T3T1/emulator.py @@ -71,6 +71,14 @@ def configure( features_available.append("touch") defines += [("USE_TOUCH", "1")] + if "haptic" in features_wanted: + sources += [ + "embed/io/haptic/unix/haptic_driver.c", + ] + paths += ["embed/io/haptic/inc"] + features_available.append("haptic") + defines += [("USE_HAPTIC", "1")] + features_available.append("backlight") defines += [("USE_BACKLIGHT", "1")] diff --git a/core/site_scons/models/T3W1/emulator.py b/core/site_scons/models/T3W1/emulator.py index 8b5afc1c6cb..e959999a015 100644 --- a/core/site_scons/models/T3W1/emulator.py +++ b/core/site_scons/models/T3W1/emulator.py @@ -109,6 +109,14 @@ def configure( features_available.append("button") defines += [("USE_BUTTON", "1")] + if "haptic" in features_wanted: + sources += [ + "embed/io/haptic/unix/haptic_driver.c", + ] + paths += ["embed/io/haptic/inc"] + features_available.append("haptic") + defines += [("USE_HAPTIC", "1")] + if "ble" in features_wanted: sources += ["embed/io/ble/unix/ble.c"] paths += ["embed/io/ble/inc"] diff --git a/core/src/apps/homescreen/device_menu.py b/core/src/apps/homescreen/device_menu.py index 2f81e52241e..1a4549f930a 100644 --- a/core/src/apps/homescreen/device_menu.py +++ b/core/src/apps/homescreen/device_menu.py @@ -1,53 +1,39 @@ -import storage.device +import storage.device as storage_device import trezorble as ble import trezorui_api from trezor import TR, config, log, utils from trezor.ui.layouts import interact -from trezor.wire import ActionCancelled from trezorui_api import DeviceMenuResult - -async def _prompt_auto_lock_delay() -> int: - auto_lock_delay_ms = await interact( - trezorui_api.request_duration( - title=TR.auto_lock__title, - duration_ms=storage.device.get_autolock_delay_ms(), - min_ms=storage.device.AUTOLOCK_DELAY_MINIMUM, - max_ms=storage.device.AUTOLOCK_DELAY_MAXIMUM, - description=TR.auto_lock__description, - ), - br_name=None, - ) - - if auto_lock_delay_ms is not trezorui_api.CANCELLED: - assert isinstance(auto_lock_delay_ms, int) - assert auto_lock_delay_ms >= storage.device.AUTOLOCK_DELAY_MINIMUM - assert auto_lock_delay_ms <= storage.device.AUTOLOCK_DELAY_MAXIMUM - return auto_lock_delay_ms - else: - raise ActionCancelled # user cancelled request number prompt +MAX_PAIRED_DEVICES = 4 async def handle_device_menu() -> None: from trezor import strings - # TODO: unify with notification handling in `apps/homescreen/__init__.py:homescreen()` failed_backup = ( - storage.device.is_initialized() and storage.device.unfinished_backup() + storage_device.is_initialized() and storage_device.unfinished_backup() ) + pin_unset = storage_device.is_initialized() and not config.has_pin() # MOCK DATA - paired_devices = ["Trezor Suite"] if ble.is_connected() else [] + paired_devices = [ + "Trezor Suite", + "2nd device iiiiiiiiiiiii", + "3rd Device iiiiiiiiiiiiii", + "4th Device forcedmultiline", + ] + connected_idx = 1 + bluetooth_version = "2.3.1.1" # ### firmware_version = ".".join(map(str, utils.VERSION)) - device_name = ( - (storage.device.get_label() or "Trezor") - if storage.device.is_initialized() + firmware_type = "Bitcoin-only" if utils.BITCOIN_ONLY else "Universal" + + auto_lock_delay = ( + strings.format_autolock_duration(storage_device.get_autolock_delay_ms()) + if config.has_pin() else None ) - auto_lock_ms = storage.device.get_autolock_delay_ms() - auto_lock_delay = strings.format_autolock_duration(auto_lock_ms) - if __debug__: log.debug( __name__, @@ -57,61 +43,223 @@ async def handle_device_menu() -> None: menu_result = await interact( trezorui_api.show_device_menu( failed_backup=failed_backup, + pin_unset=pin_unset, paired_devices=paired_devices, - firmware_version=firmware_version, - device_name=device_name, + connected_idx=connected_idx, + bluetooth=True, # TODO implement bluetooth handling + pin_code=config.has_pin() if storage_device.is_initialized() else None, auto_lock_delay=auto_lock_delay, + wipe_code=( + config.has_wipe_code() if storage_device.is_initialized() else None + ), + check_backup=storage_device.is_initialized(), + device_name=( + (storage_device.get_label() or "Trezor") + if storage_device.is_initialized() + else None + ), + screen_brightness=( + TR.brightness__title if storage_device.is_initialized() else None + ), + haptic_feedback=( + storage_device.get_haptic_feedback() + if (storage_device.is_initialized() and utils.USE_HAPTIC) + else None + ), + led=( + storage_device.get_rgb_led() + if (storage_device.is_initialized() and utils.USE_RGB_LED) + else None + ), + about_items=[ + (TR.homescreen__firmware_version, firmware_version, False), + (TR.homescreen__firmware_type, firmware_type, False), + (TR.ble__version, bluetooth_version, False), + ], ), "device_menu", ) + # Root menu + if menu_result is DeviceMenuResult.BackupFailed: + from apps.management.backup_device import perform_backup + + assert storage_device.unfinished_backup() + # If the backup failed, we can only perform a repeated backup. + await perform_backup(is_repeated_backup=True) + # Pair & Connect + elif menu_result is DeviceMenuResult.DeviceDisconnect: + from trezor.ui.layouts import confirm_action + + await confirm_action( + "device_disconnect", + "device_disconnect", + "disconnect currently connected device?", + ) + # TODO implement device disconnect handling + elif menu_result is DeviceMenuResult.DevicePair: + from trezor.ui.layouts import show_warning - if menu_result is DeviceMenuResult.DevicePair: from apps.management.ble.pair_new_device import pair_new_device - await pair_new_device() - elif menu_result is DeviceMenuResult.ScreenBrightness: - from trezor.ui.layouts import set_brightness + if len(paired_devices) < MAX_PAIRED_DEVICES: + await pair_new_device() + else: + await show_warning( + "device_pair", + "Limit of paired devices reached.", + button=TR.buttons__continue, + ) + elif menu_result is DeviceMenuResult.DeviceUnpairAll: + from trezor.messages import BleUnpair - await set_brightness() - elif menu_result is DeviceMenuResult.WipeDevice: - from trezor.messages import WipeDevice + from apps.management.ble.unpair import unpair - from apps.management.wipe_device import wipe_device + await unpair(BleUnpair(all=True)) + elif isinstance(menu_result, tuple): + from trezor.ui.layouts import confirm_action - await wipe_device(WipeDevice()) + # It's a tuple with (result_type, index) + result_type, index = menu_result + if result_type is DeviceMenuResult.DeviceConnect: + await confirm_action( + "device_connect", + "device_connect", + f"connect {index} device?", + "The currently connected device will be disconnected.", + ) + # TODO implement device connect handling + elif result_type is DeviceMenuResult.DeviceUnpair: + await confirm_action( + "device_unpair", + "device_unpair", + f"unpair {index} device?", + ) + # TODO implement device unpair handling + else: + raise RuntimeError(f"Unknown menu {result_type}, {index}") + # Bluetooth + elif menu_result is DeviceMenuResult.Bluetooth: + from trezor.ui.layouts import confirm_action + + turned_on = ble.is_connected() + + await confirm_action( + "ble__settings", + TR.words__bluetooth, + TR.ble__enable if turned_on else TR.ble__disable, + ) + pass # TODO implement bluetooth handling + # Security settings + elif menu_result is DeviceMenuResult.PinCode: + from trezor.messages import ChangePin + + from apps.management.change_pin import change_pin + + await change_pin(ChangePin()) + elif menu_result is DeviceMenuResult.PinRemove: + from trezor.messages import ChangePin + + from apps.management.change_pin import change_pin + + await change_pin(ChangePin(remove=True)) elif menu_result is DeviceMenuResult.AutoLockDelay: + from trezor.messages import ApplySettings + + from apps.management.apply_settings import apply_settings + + assert config.has_pin() + auto_lock_delay_ms = await interact( + trezorui_api.request_duration( + title=TR.auto_lock__title, + duration_ms=storage_device.get_autolock_delay_ms(), + min_ms=storage_device.AUTOLOCK_DELAY_MINIMUM, + max_ms=storage_device.AUTOLOCK_DELAY_MAXIMUM, + description=TR.auto_lock__description, + ), + br_name=None, + ) + assert isinstance(auto_lock_delay_ms, int) + await apply_settings( + ApplySettings( + auto_lock_delay_ms=auto_lock_delay_ms, + ) + ) + elif menu_result is DeviceMenuResult.WipeCode: + from trezor.messages import ChangeWipeCode + + from apps.management.change_wipe_code import change_wipe_code - if config.has_pin(): + await change_wipe_code(ChangeWipeCode()) + elif menu_result is DeviceMenuResult.WipeRemove: + from trezor.messages import ChangeWipeCode - auto_lock_delay_ms = await _prompt_auto_lock_delay() - storage.device.set_autolock_delay_ms(auto_lock_delay_ms) + from apps.management.change_wipe_code import change_wipe_code + + await change_wipe_code(ChangeWipeCode(remove=True)) + elif menu_result is DeviceMenuResult.CheckBackup: + from trezor.enums import RecoveryType + from trezor.messages import RecoveryDevice + + from apps.management.recovery_device import recovery_device + + await recovery_device( + RecoveryDevice( + type=RecoveryType.DryRun, + ) + ) + # Device settings elif menu_result is DeviceMenuResult.DeviceName: from trezor.messages import ApplySettings from apps.management.apply_settings import apply_settings - assert storage.device.is_initialized() + assert storage_device.is_initialized() label = await interact( trezorui_api.request_string( prompt=TR.device_name__enter, - max_len=storage.device.LABEL_MAXLENGTH, + max_len=storage_device.LABEL_MAXLENGTH, allow_empty=False, - prefill=storage.device.get_label(), + prefill=storage_device.get_label(), ), "device_name", ) assert isinstance(label, str) await apply_settings(ApplySettings(label=label)) - elif isinstance(menu_result, tuple): - # It's a tuple with (result_type, index) - result_type, index = menu_result - if result_type is DeviceMenuResult.DeviceDisconnect: - from trezor.messages import BleUnpair + elif menu_result is DeviceMenuResult.ScreenBrightness: + from trezor.messages import SetBrightness - from apps.management.ble.unpair import unpair + from apps.management.set_brightness import set_brightness - await unpair(BleUnpair(all=False)) # FIXME we can only unpair current - else: - raise RuntimeError(f"Unknown menu {result_type}, {index}") + await set_brightness(SetBrightness()) + elif menu_result is DeviceMenuResult.HapticFeedback: + from trezor.messages import ApplySettings + + from apps.management.apply_settings import apply_settings + + assert storage_device.is_initialized() + await apply_settings( + ApplySettings( + haptic_feedback=not storage_device.get_haptic_feedback(), + ) + ) + elif menu_result is DeviceMenuResult.Led: + from trezor import io + from trezor.ui.layouts import confirm_action + + enable = not storage_device.get_rgb_led() + await confirm_action( + "led__settings", + TR.led__title, + TR.led__enable if enable else TR.led__disable, + ) + + io.rgb_led.rgb_led_set_enabled(enable) + storage_device.set_rgb_led(enable) + elif menu_result is DeviceMenuResult.WipeDevice: + from trezor.messages import WipeDevice + + from apps.management.wipe_device import wipe_device + + await wipe_device(WipeDevice()) else: raise RuntimeError(f"Unknown menu {menu_result}") diff --git a/core/src/apps/management/change_pin.py b/core/src/apps/management/change_pin.py index 418ac43e1d0..ff26fbaf4c0 100644 --- a/core/src/apps/management/change_pin.py +++ b/core/src/apps/management/change_pin.py @@ -79,7 +79,7 @@ def _require_confirm_change_pin(msg: ChangePin) -> Awaitable[None]: return confirm_change_pin( "change_pin", TR.pin__title_settings, - description=TR.pin__change, + description=TR.pin__change_question, ) if not msg.remove and not has_pin: # setting new pin diff --git a/core/src/apps/management/change_wipe_code.py b/core/src/apps/management/change_wipe_code.py index b5256fd4592..d4f136a9e7d 100644 --- a/core/src/apps/management/change_wipe_code.py +++ b/core/src/apps/management/change_wipe_code.py @@ -75,7 +75,7 @@ def _require_confirm_action( return confirm_action( "change_wipe_code", TR.wipe_code__title_settings, - description=TR.wipe_code__change, + description=TR.wipe_code__change_question, verb=TR.buttons__change, ) diff --git a/core/src/boot.py b/core/src/boot.py index 051aa3605b7..3a0cdedfb84 100644 --- a/core/src/boot.py +++ b/core/src/boot.py @@ -65,6 +65,8 @@ async def bootscreen() -> None: ui.display.orientation(storage.device.get_rotation()) if utils.USE_HAPTIC: io.haptic.haptic_set_enabled(storage.device.get_haptic_feedback()) + if utils.USE_RGB_LED: + io.rgb_led.rgb_led_set_enabled(storage.device.get_rgb_led()) lockscreen = Lockscreen( label=storage.device.get_label(), bootscreen=True ) diff --git a/core/src/storage/device.py b/core/src/storage/device.py index 1aca32498d8..668d6c24356 100644 --- a/core/src/storage/device.py +++ b/core/src/storage/device.py @@ -41,6 +41,7 @@ # unused from python: # _BRIGHTNESS = const(0x19) # int _DISABLE_HAPTIC_FEEDBACK = const(0x20) # bool (0x01 or empty) +_DISABLE_RGB_LED = const(0x21) # bool (0x01 or empty) SAFETY_CHECK_LEVEL_STRICT : Literal[0] = const(0) @@ -393,3 +394,17 @@ def get_haptic_feedback() -> bool: Get haptic feedback enable, default to true if not set. """ return not common.get_bool(_NAMESPACE, _DISABLE_HAPTIC_FEEDBACK, True) + + +def set_rgb_led(enable: bool) -> None: + """ + Enable or disable RGB LED. + """ + common.set_bool(_NAMESPACE, _DISABLE_RGB_LED, not enable, True) + + +def get_rgb_led() -> bool: + """ + Get RGB LED enable, default to true if not set. + """ + return not common.get_bool(_NAMESPACE, _DISABLE_RGB_LED, True) diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py index fa1046374c0..cec8e0d2b21 100644 --- a/core/src/trezor/utils.py +++ b/core/src/trezor/utils.py @@ -17,6 +17,7 @@ USE_HAPTIC, USE_OPTIGA, USE_POWER_MANAGER, + USE_RGB_LED, USE_SD_CARD, USE_THP, USE_TOUCH, diff --git a/core/translations/cs.json b/core/translations/cs.json index 39dc638430b..4ce0e2f7cd0 100644 --- a/core/translations/cs.json +++ b/core/translations/cs.json @@ -556,7 +556,7 @@ "pin__cancel_description": "Pokračovat bez PIN kódu", "pin__cancel_info": "Bez PIN kódu může k tomuto zařízení přistupovat kdokoli.", "pin__cancel_setup": "Zrušit nastavení PIN kódu", - "pin__change": "Změnit PIN?", + "pin__change_question": "Změnit PIN?", "pin__changed": "PIN byl změněn.", "pin__cursor_will_change": "Poloha kurzoru se mezi jednotlivými položkami změní pro zvýšení bezpečnosti.", "pin__diff_from_wipe_code": "Nový PIN se musí lišit od kódu pro vymazání.", @@ -949,7 +949,7 @@ "wipe__info": "Všechna data budou smazána.", "wipe__title": "Vymazat zařízení", "wipe__want_to_wipe": "Opravdu chcete vymazat zařízení?\n", - "wipe_code__change": "Změnit kód pro vymazání?", + "wipe_code__change_question": "Změnit kód pro vymazání?", "wipe_code__changed": "Kód pro vymazání změněn.", "wipe_code__diff_from_pin": "Kód pro vymazání se musí lišit od PIN kódu.", "wipe_code__disabled": "Kód pro vymazání zakázán.", diff --git a/core/translations/de.json b/core/translations/de.json index aa60f8cba9c..f6dbb4455b2 100644 --- a/core/translations/de.json +++ b/core/translations/de.json @@ -556,7 +556,7 @@ "pin__cancel_description": "Ohne PIN fortfahren", "pin__cancel_info": "Ohne PIN kann jeder auf dieses Gerät zugreifen.", "pin__cancel_setup": "PIN-Einrichtung abbrechen", - "pin__change": "PIN ändern?", + "pin__change_question": "PIN ändern?", "pin__changed": "PIN geändert.", "pin__cursor_will_change": "Die Cursorposition ändert sich für mehr Sicherheit zwischen den Einträgen.", "pin__diff_from_wipe_code": "Die neue PIN muss sich von deinem Löschcode unterscheiden.", @@ -949,7 +949,7 @@ "wipe__info": "Alle Daten werden gelöscht.", "wipe__title": "Gerät löschen", "wipe__want_to_wipe": "Möchtest du die Gerätedaten wirklich löschen?\n", - "wipe_code__change": "Löschcode ändern?", + "wipe_code__change_question": "Löschcode ändern?", "wipe_code__changed": "Löschcode geändert.", "wipe_code__diff_from_pin": "Der Löschcode muss sich von deiner PIN unterscheiden.", "wipe_code__disabled": "Löschcode deaktiviert.", diff --git a/core/translations/en.json b/core/translations/en.json index f0b4ec743c6..f0235f374fc 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -107,9 +107,16 @@ "bitcoin__unverified_external_inputs": "The transaction contains unverified external inputs.", "bitcoin__valid_signature": "The signature is valid.", "bitcoin__voting_rights": "Voting rights to", + "ble__disable": "Turn Bluetooth off?", + "ble__enable": "Turn Bluetooth on?", + "ble__forget_all": "Forget all", + "ble__manage_paired": "Manage paired devices", + "ble__pair_new": "Pair new device", + "ble__pair_title": "Pair & connect", "ble__unpair_all": "Unpair all bluetooth devices", "ble__unpair_current": "Unpair connected device", "ble__unpair_title": "Unpair", + "ble__version": "Bluetooth version", "brightness__change_title": "Change display brightness", "brightness__changed_title": "Display brightness changed", "brightness__title": "Display brightness", @@ -443,6 +450,8 @@ "haptic_feedback__title": "Haptic feedback", "homescreen__click_to_connect": "Click to Connect", "homescreen__click_to_unlock": "Click to Unlock", + "homescreen__firmware_type": "Firmware type", + "homescreen__firmware_version": "Firmware version", "homescreen__set_default": "Change wallpaper to default image?", "homescreen__settings_subtitle": "Settings", "homescreen__settings_title": "Homescreen", @@ -493,6 +502,9 @@ "language__changed": "Language changed successfully", "language__progress": "Changing language", "language__title": "Language settings", + "led__disable": "Disable LED?", + "led__enable": "Enable LED?", + "led__title": "LED", "lockscreen__tap_to_connect": "Tap to connect", "lockscreen__tap_to_unlock": "Tap to unlock", "lockscreen__title_locked": "Locked", @@ -655,7 +667,8 @@ "Delizia": "Cancel PIN setup", "Eckhart": "Cancel PIN setup?" }, - "pin__change": "Change PIN?", + "pin__change": "Change PIN", + "pin__change_question": "Change PIN?", "pin__changed": "PIN changed.", "pin__cursor_will_change": "Position of the cursor will change between entries for enhanced security.", "pin__diff_from_wipe_code": "The new PIN must be different from your wipe code.", @@ -678,6 +691,7 @@ "pin__reenter": "Re-enter PIN", "pin__reenter_new": "Re-enter new PIN", "pin__reenter_to_confirm": "Please re-enter PIN to confirm.", + "pin__remove": "Remove PIN", "pin__setup_completed": "PIN setup completed.", "pin__should_be_long": "PIN should be 4-50 digits long.", "pin__title_check_pin": "Check PIN", @@ -1173,7 +1187,8 @@ "wipe__info": "All data will be erased.", "wipe__title": "Wipe device", "wipe__want_to_wipe": "Do you really want to wipe the device?\n", - "wipe_code__change": "Change wipe code?", + "wipe_code__change": "Change wipe code", + "wipe_code__change_question": "Change wipe code?", "wipe_code__changed": "Wipe code changed.", "wipe_code__diff_from_pin": "The wipe code must be different from your PIN.", "wipe_code__disabled": "Wipe code disabled.", @@ -1184,6 +1199,7 @@ "wipe_code__mismatch": "The wipe codes you entered do not match.", "wipe_code__reenter": "Re-enter wipe code", "wipe_code__reenter_to_confirm": "Please re-enter wipe code to confirm.", + "wipe_code__remove": "Remove wipe code", "wipe_code__title_check": "Check wipe code", "wipe_code__title_invalid": "Invalid wipe code", "wipe_code__title_settings": "Wipe code settings", @@ -1191,6 +1207,7 @@ "wipe_code__turn_on": "Turn on wipe code protection?", "wipe_code__wipe_code_mismatch": "Wipe code mismatch", "word_count__title": "Number of words", + "words__about": "About", "words__account": "Account", "words__account_colon": "Account:", "words__address": "Address", @@ -1201,28 +1218,39 @@ "words__assets": "Assets", "words__authenticate": "Authenticate", "words__blockhash": "Blockhash", + "words__bluetooth": "Bluetooth", "words__buying": "Buying", "words__cancel_and_exit": "Cancel and exit", "words__cancel_question": "Cancel?", "words__chain": "Chain", "words__confirm": "Confirm", "words__confirm_fee": "Confirm fee", + "words__connect": "Connect", + "words__connected": "Connected", "words__contains": "Contains", "words__continue_anyway": "Continue anyway", "words__continue_anyway_question": "Continue anyway?", "words__continue_with": "Continue with", + "words__device": "Device", + "words__disconnect": "Disconnect", + "words__disconnected": "Disconnected", "words__error": "Error", "words__fee": "Fee", + "words__forget": "Forget", "words__from": "from", "words__good_to_know": "Good to know", "words__important": "Important", "words__instructions": "Instructions", "words__keep_it_safe": "Keep it safe!", "words__know_what_your_doing": "Continue only if you know what you are doing!", + "words__led": "LED", + "words__manage": "Manage", "words__my_trezor": "My Trezor", "words__name": "Name", "words__no": "No", "words__not_recommended": "Not recommended!", + "words__off": "OFF", + "words__on": "ON", "words__operation_cancelled": "Operation cancelled", "words__outputs": "outputs", "words__pay_attention": "Pay attention", @@ -1233,6 +1261,8 @@ "words__receive": "Receive", "words__recipient": "Recipient", "words__recovery_share": "Recovery share", + "words__review": "Review", + "words__security": "Security", "words__send": "Send", "words__settings": "Settings", "words__sign": "Sign", diff --git a/core/translations/es.json b/core/translations/es.json index 464748f0aaf..071d15ed639 100644 --- a/core/translations/es.json +++ b/core/translations/es.json @@ -556,7 +556,7 @@ "pin__cancel_description": "Continuar sin PIN", "pin__cancel_info": "Sin un PIN, cualquiera puede acceder al dispositivo.", "pin__cancel_setup": "Cancelar config. de PIN", - "pin__change": "¿Cambiar PIN?", + "pin__change_question": "¿Cambiar PIN?", "pin__changed": "PIN cambiado.", "pin__cursor_will_change": "La posición del cursor irá cambiando para mejorar la seguridad.", "pin__diff_from_wipe_code": "El nuevo PIN no debe ser el código de borrar.", @@ -954,7 +954,7 @@ "wipe__info": "Se borrarán todos los datos.", "wipe__title": "Borrar dispositivo", "wipe__want_to_wipe": "¿Quieres borrar el dispositivo?\n", - "wipe_code__change": "¿Cambiar el código de borrar?", + "wipe_code__change_question": "¿Cambiar el código de borrar?", "wipe_code__changed": "El código de borrar se ha cambiado.", "wipe_code__diff_from_pin": "El código de borrar debe ser diferente del PIN.", "wipe_code__disabled": "El código de borrar se ha desactivado.", diff --git a/core/translations/fr.json b/core/translations/fr.json index a17bd3eb6bb..2cab621b5c1 100644 --- a/core/translations/fr.json +++ b/core/translations/fr.json @@ -556,7 +556,7 @@ "pin__cancel_description": "Continuer sans PIN", "pin__cancel_info": "Sans PIN, tout le monde peut accéder à ce dispositif.", "pin__cancel_setup": "Annuler la config. du PIN", - "pin__change": "Modifier le PIN ?", + "pin__change_question": "Modifier le PIN ?", "pin__changed": "PIN modifié.", "pin__cursor_will_change": "La position du curseur change entre les entrées pour plus de sécurité.", "pin__diff_from_wipe_code": "Le nouveau PIN doit être différent de votre code d'eff.", @@ -949,7 +949,7 @@ "wipe__info": "Toutes les données seront effacées.", "wipe__title": "Effacer disp.", "wipe__want_to_wipe": "Voulez-vous vraiment effacer le disp. ?\n", - "wipe_code__change": "Modifier le code d'eff. ?", + "wipe_code__change_question": "Modifier le code d'eff. ?", "wipe_code__changed": "Code d'eff. modifié.", "wipe_code__diff_from_pin": "Le code d'eff. doit être différent de votre PIN.", "wipe_code__disabled": "Code d'eff. désactivé.", diff --git a/core/translations/it.json b/core/translations/it.json index 1170f6fd1a8..298fc8acbdc 100644 --- a/core/translations/it.json +++ b/core/translations/it.json @@ -549,7 +549,7 @@ "pin__cancel_description": "Continua senza PIN", "pin__cancel_info": "Senza PIN, tutti possono accedere al dispositivo.", "pin__cancel_setup": "Annulla impostazione PIN", - "pin__change": "Modificare PIN?", + "pin__change_question": "Modificare PIN?", "pin__changed": "PIN modificato.", "pin__cursor_will_change": "La posizione del cursore cambia tra le voci per maggiore sicurezza.", "pin__diff_from_wipe_code": "Il nuovo PIN deve essere diverso dal codice di eliminaz.", @@ -923,7 +923,7 @@ "wipe__info": "Tutti i dati verranno cancellati.", "wipe__title": "Elimina dati disp.", "wipe__want_to_wipe": "Vuoi eliminare i dati dal dispositivo?\n", - "wipe_code__change": "Modificare il codice di eliminazione?", + "wipe_code__change_question": "Modificare il codice di eliminazione?", "wipe_code__changed": "Codice di eliminazione modificato.", "wipe_code__diff_from_pin": "Il codice di eliminazione deve essere diverso dal PIN.", "wipe_code__disabled": "Codice di eliminazione disabilitato.", diff --git a/core/translations/order.json b/core/translations/order.json index afba5a1dbf8..ae7584a5541 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -1078,5 +1078,35 @@ "1076": "device_name__continue_with_empty_label", "1077": "device_name__enter", "1078": "regulatory_certification__title", - "1079": "words__name" + "1079": "words__name", + "1080": "led__disable", + "1081": "led__enable", + "1082": "led__title", + "1083": "words__led", + "1084": "words__off", + "1085": "words__on", + "1086": "ble__manage_paired", + "1087": "ble__pair_new", + "1088": "ble__pair_title", + "1089": "homescreen__firmware_version", + "1090": "words__about", + "1091": "words__connected", + "1092": "words__device", + "1093": "words__disconnect", + "1094": "words__manage", + "1095": "words__review", + "1096": "words__security", + "1097": "homescreen__firmware_type", + "1098": "ble__version", + "1099": "pin__change_question", + "1100": "pin__remove", + "1101": "wipe_code__change_question", + "1102": "wipe_code__remove", + "1103": "ble__disable", + "1104": "ble__enable", + "1105": "words__bluetooth", + "1106": "words__disconnected", + "1107": "ble__forget_all", + "1108": "words__connect", + "1109": "words__forget" } diff --git a/core/translations/pt.json b/core/translations/pt.json index 3bc79daec8c..832a865393f 100644 --- a/core/translations/pt.json +++ b/core/translations/pt.json @@ -557,7 +557,7 @@ "pin__cancel_description": "Continuar sem PIN", "pin__cancel_info": "Sem um PIN qualquer pessoa pode acessar este dispositivo.", "pin__cancel_setup": "Cancelar configuração PIN", - "pin__change": "Alterar PIN?", + "pin__change_question": "Alterar PIN?", "pin__changed": "PIN alterado.", "pin__cursor_will_change": "A posição do cursor mudará entre as entradas para maior segurança.", "pin__diff_from_wipe_code": "O novo PIN deve ser diferente do seu código de limpeza.", @@ -953,7 +953,7 @@ "wipe__info": "Todos os dados serão apagados.", "wipe__title": "Limpar dispositivo", "wipe__want_to_wipe": "Deseja mesmo limpar o dispositivo?\n", - "wipe_code__change": "Alterar código de limpeza?", + "wipe_code__change_question": "Alterar código de limpeza?", "wipe_code__changed": "O código de limpeza foi alterado.", "wipe_code__diff_from_pin": "O código de limpeza deve ser diferente do seu PIN.", "wipe_code__disabled": "Código de limpeza desativado.", diff --git a/core/translations/signatures.json b/core/translations/signatures.json index 84faf9f5ffc..84bf1000a20 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,8 +1,8 @@ { "current": { - "merkle_root": "2ccd8252bccecdb7fd94da5b7f6b504aee0abf62adc7dfa68e8d6ced638acb82", - "datetime": "2025-08-20T06:07:28.795050+00:00", - "commit": "0e72c2178cd24750deeda1b4264933946285bcc7" + "merkle_root": "291306c4eb09120d125b5858f73fef4f1f152dfd82dfa79d1fed3566748a509c", + "datetime": "2025-08-21T19:11:52.539243+00:00", + "commit": "a360dc1e7088e035f7a1f002e8c0b0f0c95feca2" }, "history": [ { diff --git a/tests/click_tests/test_pin.py b/tests/click_tests/test_pin.py index 2786aaa4fc2..b9a7f3da90f 100644 --- a/tests/click_tests/test_pin.py +++ b/tests/click_tests/test_pin.py @@ -120,7 +120,7 @@ def prepare( device_handler.run_with_provided_session(device_handler.client.get_seedless_session(), device.change_pin) # type: ignore _assert_pin_entry(debug) _input_see_confirm(debug, old_pin) - debug.synchronize_at(TR.pin__change) + debug.synchronize_at(TR.pin__change_question) go_next(debug) _input_see_confirm(debug, old_pin) elif situation == Situation.WIPE_CODE_SETUP: diff --git a/tests/device_tests/test_msg_applysettings.py b/tests/device_tests/test_msg_applysettings.py index 4b18520145b..5a9673076c2 100644 --- a/tests/device_tests/test_msg_applysettings.py +++ b/tests/device_tests/test_msg_applysettings.py @@ -542,3 +542,43 @@ def test_set_brightness_cancel(session: Session): IF = InputFlowCancelBrightness(client) client.set_input_flow(IF.get()) device.set_brightness(session, None) + + +@pytest.mark.models( + "delizia", + "eckhart", + reason="other devices do not have haptic feedback feature", +) +def test_set_haptic_feedback(client: Client): + with client: + client.use_pin_sequence([PIN4]) + session = client.get_session() + + # Haptic feedback is by default turned on + assert session.client.features.haptic_feedback is True + + with session.client as client: + # Disable haptic feedback on initialized device with pin + device.apply_settings(session, haptic_feedback=False) + assert client.features.haptic_feedback is False + + # Enable haptic feedback on initialized device without pin + client.use_pin_sequence([PIN4]) + device.change_pin(session, remove=True) + assert client.features.pin_protection is False + device.apply_settings(session, haptic_feedback=True) + assert client.features.haptic_feedback is True + + # Wipe device + device.wipe(session) + client = client.get_new_client() + with client: + session = client.get_seedless_session() + assert session.client.features.initialized is False + + # Haptic feedback setting is not supported on uninitialized devices + with pytest.raises( + exceptions.TrezorFailure, match="not initialized" + ), session.client as client: + client.set_expected_responses([messages.Failure]) + device.apply_settings(session, haptic_feedback=False) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index b2bc348e369..62a56e3ab2e 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -20926,6 +20926,7 @@ "T3T1_cs_test_msg_applysettings.py::test_set_brightness[None-False]": "07404e652f7ebf9ac9342e7e5c0e458260ad64bc97512ed2b67e2369836737a3", "T3T1_cs_test_msg_applysettings.py::test_set_brightness_cancel": "98c69911efebc150625402517151585a6528924762223877d78cd09a65c2406e", "T3T1_cs_test_msg_applysettings.py::test_set_brightness_negative": "592adf9c06f8f39132298b46037dd524fac99e58237a0c023197bf47df67d632", +"T3T1_cs_test_msg_applysettings.py::test_set_haptic_feedback": "30a04a7f2bf072ba20d0a2374a496114e8811998e3bb1a966671b0f1bc4d08b0", "T3T1_cs_test_msg_backup_device.py::test_backup_bip39": "52b6af87f1fc89114a0991add339b77b7aaf88fb93dc536841030a848bd6c08d", "T3T1_cs_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "aa2f954d32fbe2ef095666ee3cd8e86dbda4c27d37be6cfac740156f506a1e23", "T3T1_cs_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "62866b5f85287777fd151a5ee9b889f6cbf0c0e3dd1d92e6331579c60ffec352", @@ -22396,6 +22397,7 @@ "T3T1_de_test_msg_applysettings.py::test_set_brightness[None-False]": "1efb9f564495021e1e9e85bc9b3ca8fb89a50348d6daf4007b6bc2e986149a9c", "T3T1_de_test_msg_applysettings.py::test_set_brightness_cancel": "cff1265a6c670d579e6bbc5e6148d1bd10bdba2b313291eb1b7ff72fbbf36495", "T3T1_de_test_msg_applysettings.py::test_set_brightness_negative": "f2fcbbd7ad83aff9adc0ee97fe98e7272d8bcfcc4a023798e2ab1c9895fda70f", +"T3T1_de_test_msg_applysettings.py::test_set_haptic_feedback": "ebfd392d52e3c2474011d32dedbfe9a386772e11f46cb83c5b1ec9bb7a79464e", "T3T1_de_test_msg_backup_device.py::test_backup_bip39": "ce848cdd01a9c47e34c565af4cbe2df4d13bc79601fcc0bea71726599f553c0e", "T3T1_de_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "95c8617bf4b1e22fbe038e92c2c5b4ce4451ba6b8678655fb2060ed488017193", "T3T1_de_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "ec7d9d544629262f4cfc1d2333d8f3ac166fb8d222e94b6f12a37f2a762971b4", @@ -23866,6 +23868,7 @@ "T3T1_en_test_msg_applysettings.py::test_set_brightness[None-False]": "b04bd5c35fc3a2885664d3bca8e9105c731e3d207265896f711bccd9b33d3b78", "T3T1_en_test_msg_applysettings.py::test_set_brightness_cancel": "75a8ef597972f9e84f318c82dba178bf473def4c6074821154c894a4af56edad", "T3T1_en_test_msg_applysettings.py::test_set_brightness_negative": "cb8641952bec9e793e7d19f281a85a0ca1be2c3397ca5c0cf4ee7ad905429984", +"T3T1_en_test_msg_applysettings.py::test_set_haptic_feedback": "490143465488d6520b310b5817f51cfd31a5a1e0cf7125f266878296cfe5e92f", "T3T1_en_test_msg_backup_device.py::test_backup_bip39": "6a903c11a3f487fe09f8cd08ddfa6877275fce673c4de6a3ac1aae1e301943e1", "T3T1_en_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "58f6c959e0e69f7c8c79fba20786e22af27c262ebc78f1e1e176665c0bd3d6e6", "T3T1_en_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "98b4abf939944a6a6c688c6e1355eeb2e2fe57be869051b858d338bd021199fc", @@ -25336,6 +25339,7 @@ "T3T1_es_test_msg_applysettings.py::test_set_brightness[None-False]": "ec97bc91bdfe8c237134ced18ef64d9fe310fc2151de7ab1695fd8278220411e", "T3T1_es_test_msg_applysettings.py::test_set_brightness_cancel": "41aab6b44b81d07f2b28899aee9ae4644c3f5e08f85ff4b84e7c1e128e7c9aa2", "T3T1_es_test_msg_applysettings.py::test_set_brightness_negative": "c568b82943d9f63ad6b29b7dee6f5ed9dca78fc9610d4fa7f12bb8a061a6882a", +"T3T1_es_test_msg_applysettings.py::test_set_haptic_feedback": "9a9813b5564b5f7d93b2395aee86326ac1a233490da6658a677a724a2cab4bf2", "T3T1_es_test_msg_backup_device.py::test_backup_bip39": "35b72244efaf51187f2f5a4520cebf82b420887d1e8f8f81cb00e85302eb246f", "T3T1_es_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "b7b833f594d12930fa9673dffb1c638ea7c9df54f1c1813bbad464a6b5a7fe33", "T3T1_es_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "2b94bd26d5bfbc66b6381267d48aa7ca7ef93d7fb4c95053b5e8b9172ab98552", @@ -26806,6 +26810,7 @@ "T3T1_fr_test_msg_applysettings.py::test_set_brightness[None-False]": "f761bc60d66c2acaa4067eab0bf8b66212f7bc1770a964a938b024b39341ef8c", "T3T1_fr_test_msg_applysettings.py::test_set_brightness_cancel": "5cf5dc35925b632e05c2605167e69d3227e37402b9c26957012f8e12b0f51533", "T3T1_fr_test_msg_applysettings.py::test_set_brightness_negative": "c4dffa9195d175f9bc4a23e9e0f80b92f7c6687128918a5a70c14b4a6e7e3694", +"T3T1_fr_test_msg_applysettings.py::test_set_haptic_feedback": "d1cfd6e379d9462d40de2874cd47d1056791aadb0302d3771bdbcc7b4dc9c2d4", "T3T1_fr_test_msg_backup_device.py::test_backup_bip39": "4f50fa35be7f08c3d90b7afaf36b2bba5363d0a57682c8bb8da0813fb3178ee3", "T3T1_fr_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "b693d96ee36b5c992d1614fbf3ee70ffe0194aab14c8a7b1824c47ae36e1fb71", "T3T1_fr_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "8463f12db670b0049238a67e803433084fc1a142d75e3673f9ea9f82d52f0c18", @@ -28276,6 +28281,7 @@ "T3T1_pt_test_msg_applysettings.py::test_set_brightness[None-False]": "4b623055ffba42054f7ea4c9146376041a0716b2efcd6c1710d2f98d1887f825", "T3T1_pt_test_msg_applysettings.py::test_set_brightness_cancel": "f7a90d85693d04080c9f823185a9ef52c2d34d35afd402f4835e0d8c14815b67", "T3T1_pt_test_msg_applysettings.py::test_set_brightness_negative": "c15216b8aa033bd81f0a8fb6415c6728320b0c30fe3150eeb8a1749ae0a1a83c", +"T3T1_pt_test_msg_applysettings.py::test_set_haptic_feedback": "45187c96d4808778cd76871d6021885820b19c432059c14df55130a7cb9ac04d", "T3T1_pt_test_msg_backup_device.py::test_backup_bip39": "3081673ce14a39e8e4e63c2b07eebc5c98d5c8dca63c47baa5f5055721156fe5", "T3T1_pt_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "29a4eb269693be88a115763a3260680f8ddc18405cf90ca3f1806a00f586f13f", "T3T1_pt_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "fdcc4ee6498c270e5652926ce9f6960f0c2ea09e735dd76e9ab1ede43f598fc8", @@ -30097,7 +30103,7 @@ "T3W1_cs_test_msg_applysettings.py::test_apply_settings": "f9ee942904684327da920e212fe66dffdbbdfbb4f0aeef533a4e35d88a3bc349", "T3W1_cs_test_msg_applysettings.py::test_apply_settings_passphrase": "9b52f18a83c93f52a8dadfc8cb9282d2de8500fc2cccd9e0dfd5302f745884ce", "T3W1_cs_test_msg_applysettings.py::test_apply_settings_passphrase_on_device": "f28dbad67dbe84cf16e851ec3f3578c72d9ae5f6b8d026173730564f3f7d1295", -"T3W1_cs_test_msg_applysettings.py::test_apply_settings_rotation": "6abc1f8e1e745b42ecaadc0c7a13daf4f76941162e9b55be3ab93e54ca003a87", +"T3W1_cs_test_msg_applysettings.py::test_apply_settings_rotation": "2fd6a41c7de4d545d853ff60c5e1360c48790e5f427970a1360932ced7806262", "T3W1_cs_test_msg_applysettings.py::test_experimental_features": "462668fbd435e915242109c4bfa102c9b1443ea3e1bb99f6324d498a3cad382d", "T3W1_cs_test_msg_applysettings.py::test_label_too_long": "535037bfe5f1459cfdf305915835d8bf2a9a427c3f60264a8cc3ca6f306a61b1", "T3W1_cs_test_msg_applysettings.py::test_safety_checks": "6d643f029291b0772a8fe72317bf2d8f35d4407a5d594346fcc75cc6d73c1b95", @@ -30107,6 +30113,7 @@ "T3W1_cs_test_msg_applysettings.py::test_set_brightness[256-True]": "535037bfe5f1459cfdf305915835d8bf2a9a427c3f60264a8cc3ca6f306a61b1", "T3W1_cs_test_msg_applysettings.py::test_set_brightness[None-False]": "559c942026590c2f32aadaa570bb2d2ca57462839a8cca4b51ddfe7139fe0b49", "T3W1_cs_test_msg_applysettings.py::test_set_brightness_negative": "535037bfe5f1459cfdf305915835d8bf2a9a427c3f60264a8cc3ca6f306a61b1", +"T3W1_cs_test_msg_applysettings.py::test_set_haptic_feedback": "6cc7ea93b9976589703bda5099f10808cbdc2f4f939070d75e3c587ac389d52f", "T3W1_cs_test_msg_backup_device.py::test_backup_bip39": "8755747f5c7b503d9c5debadbbde137e1b1aeeca5d36fcd6babb539763772bbf", "T3W1_cs_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "57cf5d287f4f5a1576d8be8a1e5e5833d76733aebb9fb34176c234c4487fda7c", "T3W1_cs_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "7b6659619cdc394de0b7ae1f0e741a40f2cef55bdb30c7d65546bc40c84c137d", @@ -31556,7 +31563,7 @@ "T3W1_de_test_msg_applysettings.py::test_apply_settings": "19b181664ebd7481452a454b314cd09af266b604f85336e91a758fe87e5f652a", "T3W1_de_test_msg_applysettings.py::test_apply_settings_passphrase": "00869db8c7a414f8dcb2aa90cb7692935653937d65e9d328c2307132ca35e70f", "T3W1_de_test_msg_applysettings.py::test_apply_settings_passphrase_on_device": "95cfec0695a8900dc8f2008bc9eca0a506d7f27d833175cb5efb00f300fb730a", -"T3W1_de_test_msg_applysettings.py::test_apply_settings_rotation": "278bbd9bd1c0f800a4cfc8ea3b1eefc5117c472f2db65795051920fe9561a762", +"T3W1_de_test_msg_applysettings.py::test_apply_settings_rotation": "d4d3cde779c36f63e5a5f08d230f33b56ef67a355786c814719d7fa8c5de9084", "T3W1_de_test_msg_applysettings.py::test_experimental_features": "3b2794d6eafc1521f407624c4dd53f686088c182364aee0a45a58e9c3e1c7056", "T3W1_de_test_msg_applysettings.py::test_label_too_long": "2bf2200d3f158d1cffae639cd632a3b651fe53af5942b65eb881cfce12817592", "T3W1_de_test_msg_applysettings.py::test_safety_checks": "1cba549ce0cadc4c141a022e852c17593fc4073678d80545e2ba41e800773a50", @@ -31566,6 +31573,7 @@ "T3W1_de_test_msg_applysettings.py::test_set_brightness[256-True]": "2bf2200d3f158d1cffae639cd632a3b651fe53af5942b65eb881cfce12817592", "T3W1_de_test_msg_applysettings.py::test_set_brightness[None-False]": "722a9dbc51b0c39981f24fa1634de7579c88c161bb461b40f0b23d033c33881b", "T3W1_de_test_msg_applysettings.py::test_set_brightness_negative": "2bf2200d3f158d1cffae639cd632a3b651fe53af5942b65eb881cfce12817592", +"T3W1_de_test_msg_applysettings.py::test_set_haptic_feedback": "5fd9030d99eeeefc8364071e8d2961c58c6fed55bb22b43226852ee9ebe81cb1", "T3W1_de_test_msg_backup_device.py::test_backup_bip39": "822db0ae7a82970cfebff51a485ab379121bee0c9c5846826f4a283a802338c1", "T3W1_de_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "e9514704cab65f7f7a7d6f3f405c59def6c4946645399bdeb87e5e4e80ffad46", "T3W1_de_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "abe8b05b8b3fdd305f582356c3743bb5db2dfe2406cba58f23fd4ae662c1fa51", @@ -33015,7 +33023,7 @@ "T3W1_en_test_msg_applysettings.py::test_apply_settings": "15d484ff00ff53aa9aa7a753b54624ec4a222a15ece28e66cf68f4df262a106c", "T3W1_en_test_msg_applysettings.py::test_apply_settings_passphrase": "e5f5ad5186f68f9a0a53b1413f9f2b3f865c3d8406918ba61fa334ca92d08074", "T3W1_en_test_msg_applysettings.py::test_apply_settings_passphrase_on_device": "c6b54d3a964739b7ce35f4ce40fc3e279b75073cf36b07e23b906e2546b5ede0", -"T3W1_en_test_msg_applysettings.py::test_apply_settings_rotation": "097e2261ba115c61a4a7a27d597de2d5fee898534ae7189e57e10bd4c2110700", +"T3W1_en_test_msg_applysettings.py::test_apply_settings_rotation": "ddef723fd399a503aaa62356af81b081d4d67d5acbb0125cf1e746082d0bc9e0", "T3W1_en_test_msg_applysettings.py::test_experimental_features": "5cd968966853d592bf7018571644fc48937937760b5fdcac4717a39b1a109e82", "T3W1_en_test_msg_applysettings.py::test_label_too_long": "2b19d878184abddf53159d4acb504a1e86a7c2d5fd15de433495742ba7df9cc8", "T3W1_en_test_msg_applysettings.py::test_safety_checks": "7e42df278fe1ff2d6e7af71ac403c249edf86e0bdd049ecca1e45b1a3df62f50", @@ -33025,6 +33033,7 @@ "T3W1_en_test_msg_applysettings.py::test_set_brightness[256-True]": "2b19d878184abddf53159d4acb504a1e86a7c2d5fd15de433495742ba7df9cc8", "T3W1_en_test_msg_applysettings.py::test_set_brightness[None-False]": "b706c1bc8d414beaa250982a22258f5c551e21268b7c7c15a9e443fc345e93fa", "T3W1_en_test_msg_applysettings.py::test_set_brightness_negative": "2b19d878184abddf53159d4acb504a1e86a7c2d5fd15de433495742ba7df9cc8", +"T3W1_en_test_msg_applysettings.py::test_set_haptic_feedback": "571351c80b833b41edf4a053132518b87232c4a63805a3c0bcdd0d87726b0951", "T3W1_en_test_msg_backup_device.py::test_backup_bip39": "3359eabf1f094d58a7de9cf9f14486f47b1328c15ff39b1d7ca69a1d6d30428d", "T3W1_en_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "234f10ddf14a70cf63d91f8570471fb6ce5c50679fedbe381a35a40522c7befe", "T3W1_en_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "bb3b9fc552ea5ff45dbc3655122bfb88af90403db9d9d3efa8d466217affd957", @@ -34474,7 +34483,7 @@ "T3W1_es_test_msg_applysettings.py::test_apply_settings": "f5485dac17b4e97e26fbef30e6ba0baa1a4ce46e97a0d390b7ec3d2bb3a8d878", "T3W1_es_test_msg_applysettings.py::test_apply_settings_passphrase": "84a56e1dea0820c0711ed849a152853695884f0a9ee8bc15835451999a048404", "T3W1_es_test_msg_applysettings.py::test_apply_settings_passphrase_on_device": "c42f19c26aa7b03b45e0ed64e354013990d760045fd4f591986b7905bb996428", -"T3W1_es_test_msg_applysettings.py::test_apply_settings_rotation": "c430cc339c3de5ecd39d868a8a745ce924bd300494ddced39f70a8f99aee86b7", +"T3W1_es_test_msg_applysettings.py::test_apply_settings_rotation": "b6d140f75bb869f007a6e8d64c9b85bcf518242eb68e6eff062be8aef67112b0", "T3W1_es_test_msg_applysettings.py::test_experimental_features": "3690c1f26c485e4b739835a025727db5a5a30cb4da2b3b71b13a6da22ba84fef", "T3W1_es_test_msg_applysettings.py::test_label_too_long": "991c2fedae415c4284948d276edc08daeea466ca5849934ed2784cc8884ee589", "T3W1_es_test_msg_applysettings.py::test_safety_checks": "8ac9aa4d9327982dcab5075522573d304435beddb4c4b7dbdd8f4fa2cae569b7", @@ -34484,6 +34493,7 @@ "T3W1_es_test_msg_applysettings.py::test_set_brightness[256-True]": "991c2fedae415c4284948d276edc08daeea466ca5849934ed2784cc8884ee589", "T3W1_es_test_msg_applysettings.py::test_set_brightness[None-False]": "13b52c1232a199c9f31d1e8ac9f9fbe8310787f1bcc06f2da4608bf5e2990f00", "T3W1_es_test_msg_applysettings.py::test_set_brightness_negative": "991c2fedae415c4284948d276edc08daeea466ca5849934ed2784cc8884ee589", +"T3W1_es_test_msg_applysettings.py::test_set_haptic_feedback": "d13890323e27f97f170a82f0c3a54e9691f851c201d2a3e96af4aca9a1b2f12f", "T3W1_es_test_msg_backup_device.py::test_backup_bip39": "df52e24ceca275669f807b4e95095ba2bebe01d1256428abbd211b855cbe64e8", "T3W1_es_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "40d6cd05f0672b3a6f10cb46629a08bd70d1d3e31e1effd51d9d8df5d193558a", "T3W1_es_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "c5182486073114b9fe5eadd6305f7b7813d2504809fc07bc2c058e526645a1f9", @@ -35933,7 +35943,7 @@ "T3W1_fr_test_msg_applysettings.py::test_apply_settings": "c4a3b1aba49f15a967e9fa15c95f4e4f3ae619c3c78b84074b51f453d01d8358", "T3W1_fr_test_msg_applysettings.py::test_apply_settings_passphrase": "efa0ac5a5ec50b7c183bc0ff660fece5f19a25b7e4d82e28bba3a16fdb5bb40a", "T3W1_fr_test_msg_applysettings.py::test_apply_settings_passphrase_on_device": "c154edbc4d608b4ec87b8affdc04d39bb23da7b05e2cdcbad300938257f89663", -"T3W1_fr_test_msg_applysettings.py::test_apply_settings_rotation": "1f2ea0cff9a81b249b5c3d82c100f8edf677ccbaed99d307c8ca5721a8155562", +"T3W1_fr_test_msg_applysettings.py::test_apply_settings_rotation": "83380700ab3a450cc86743bdf88fc080732c65e795ed647b171c8135951ad1d0", "T3W1_fr_test_msg_applysettings.py::test_experimental_features": "a74c748d184d9566776aac8cf5fd97b619e1273363c2b6c3569cbb9ef78f2bbc", "T3W1_fr_test_msg_applysettings.py::test_label_too_long": "250f727140e1737e1e25e4620b4557d886c40e53f8205987dacb8904050dc474", "T3W1_fr_test_msg_applysettings.py::test_safety_checks": "8653389db626364adec89d1c2eef9c085ac416cf1c653a88d61465ade8696442", @@ -35943,6 +35953,7 @@ "T3W1_fr_test_msg_applysettings.py::test_set_brightness[256-True]": "250f727140e1737e1e25e4620b4557d886c40e53f8205987dacb8904050dc474", "T3W1_fr_test_msg_applysettings.py::test_set_brightness[None-False]": "46f62964f1e36fbc024ce3c69eecfcbde6fba24e7bce18e1e9c4355b97497e72", "T3W1_fr_test_msg_applysettings.py::test_set_brightness_negative": "250f727140e1737e1e25e4620b4557d886c40e53f8205987dacb8904050dc474", +"T3W1_fr_test_msg_applysettings.py::test_set_haptic_feedback": "dec4e79f96f529f2538d1c78529480223a8333a7c7996cf23a07bfd50399b798", "T3W1_fr_test_msg_backup_device.py::test_backup_bip39": "247dff0d4d44d42228ea1b40b2d90034b1a30ae77c0b41758ed39381b1864ca0", "T3W1_fr_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "e07e54434c0d0bf75e7ca70b56980048c51ae47d422193e18df476cc571601d0", "T3W1_fr_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "fcf5a1aea12c6194e70e66d7b64c36cf7f60a61eb1b3d1057d52adc497ebd2ec", @@ -37392,7 +37403,7 @@ "T3W1_pt_test_msg_applysettings.py::test_apply_settings": "391428ab7931665ae4dc6d4e54630cc9c2e6fe26cdab34644cc2ae786930e91e", "T3W1_pt_test_msg_applysettings.py::test_apply_settings_passphrase": "61f31f5d1acb3eac2796d38eddf34bbc3a6f353542e878d8f858c42cf7c2256a", "T3W1_pt_test_msg_applysettings.py::test_apply_settings_passphrase_on_device": "c4288fa48d00753f6748417a7477664547829a44b476011ad88ebdf3754f5039", -"T3W1_pt_test_msg_applysettings.py::test_apply_settings_rotation": "38dadb072ef1c5be04109b7f096a17cb81d48b837b3fc49b20f03e6fed4cc8a1", +"T3W1_pt_test_msg_applysettings.py::test_apply_settings_rotation": "c267ff270782c187dd23f54d2a97f613b6e43723c86d0e3aac735f8265eed367", "T3W1_pt_test_msg_applysettings.py::test_experimental_features": "890efa6c797dc05a9366cb03a434453ba8e207d6a2d47a294e34462cb7aad7e5", "T3W1_pt_test_msg_applysettings.py::test_label_too_long": "637658c0c6bbf3267f67895c12aeea91280f0471133d7865e4b0d19bdd1da874", "T3W1_pt_test_msg_applysettings.py::test_safety_checks": "08997648a904eafa26d969924728c45156de1d4627d253be2a63d8eace508e65", @@ -37402,6 +37413,7 @@ "T3W1_pt_test_msg_applysettings.py::test_set_brightness[256-True]": "637658c0c6bbf3267f67895c12aeea91280f0471133d7865e4b0d19bdd1da874", "T3W1_pt_test_msg_applysettings.py::test_set_brightness[None-False]": "961e8d6f581bd1e81c5feb0848903aa00a618ce4f1214dea3a59eeece53da614", "T3W1_pt_test_msg_applysettings.py::test_set_brightness_negative": "637658c0c6bbf3267f67895c12aeea91280f0471133d7865e4b0d19bdd1da874", +"T3W1_pt_test_msg_applysettings.py::test_set_haptic_feedback": "ccbd6e0d68ec0a8a6afc07eddbefc9907f02a57dc9429e05cd88a30ad8bb329a", "T3W1_pt_test_msg_backup_device.py::test_backup_bip39": "c7fe55a00f939a272bf12481a1c435f1df5037d694b2d038396e2dbf6f1df54c", "T3W1_pt_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "1ee667b07f1e803a99bc59fa15d26de01cf8196e12cbcd0700f410772fd3e3ed", "T3W1_pt_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "6a546f89e0ff92cc1a260da06fb0924d039220c043bf48b8edd4a3800bcb10cb",