What changed, and why it matters
This commit refreshes the on-device settings menu for the Trezor Safe 7 (Eckhart layout). It lets the menu restart on a specific submenu after actions like disconnecting a Bluetooth device or toggling haptics/LED, and removes the confirmation prompt for some toggle buttons. The changes are almost entirely user-interface plumbing; there is no obvious security bug, but the new loop and direct hardware calls deserve a quick sanity check.
Treat as a normal feature commit. Reviewers should verify that removing the confirmation for LED/haptic toggles is an intentional UX change and that the new menu loop cannot be re-entered in a way that bypasses authentication or leaves stale state. No security patch or incident response is indicated by the diff alone.
Security signals we found
Removal of confirmation prompts for haptic and LED toggles (direct hardware enable/disable)
New persistent loop in device_menu.py that re-shows the menu after actions; must ensure no state is stale or re-entrant
BLE connection-change events now trigger a MenuRefresh result; need to confirm no event spoofing path exists
Direct `storage_device.set_haptic_feedback` / `set_rgb_led` writes without going through `apply_settings` validation
No input validation visible for `init_submenu` beyond a `TryFrom<usize>` that falls back to Root on failure
Evidence from the diff
The patch rewrites the Eckhart device menu as a persistent loop in Python (handle_device_menu) and adds a static submenu ID map in Rust so the UI can be recreated at a specific submenu. It introduces DeviceMenuResult::MenuRefresh, passes init_submenu through the firmware UI trait, and handles BLE connect/disconnect events by returning to the caller and re-showing the menu. Several settings actions now use try/except ActionCancelled and set init_submenu to return to the right screen. Toggle actions for haptic feedback and RGB LED no longer show a confirmation screen and instead call io.haptic.haptic_set_enabled / io.rgb_led.rgb_led_set_enabled directly after inverting the stored setting.
Changed components
core/src/apps/homescreen/device_menu.pycore/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rscore/embed/rust/src/ui/layout_eckhart/component_msg_obj.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rscore/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/layout/device_menu_result.rscore/embed/rust/src/ui/ui_firmware.rscore/embed/rust/src/ui/layout_bolt/ui_firmware.rscore/embed/rust/src/ui/layout_caesar/ui_firmware.rscore/embed/rust/src/ui/layout_delizia/ui_firmware.rscore/mocks/generated/trezorui_api.pyicore/embed/rust/librust_qstr.hInspect captured patch +592 / −398
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 5e70b5ad..331a44dc 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -44,6 +44,7 @@ static void _librust_qstrs(void) {
MP_QSTR_MAX;
MP_QSTR_MESSAGE_NAME;
MP_QSTR_MESSAGE_WIRE_TYPE;
+ MP_QSTR_MenuRefresh;
MP_QSTR_MessageType;
MP_QSTR_Msg;
MP_QSTR_MsgDef;
@@ -373,6 +374,7 @@ static void _librust_qstrs(void) {
MP_QSTR_info;
MP_QSTR_info_button;
MP_QSTR_init;
+ MP_QSTR_init_submenu;
MP_QSTR_inputs__back;
MP_QSTR_inputs__cancel;
MP_QSTR_inputs__delete;
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 639ede92..67c4d64a 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -936,6 +936,8 @@ 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 init_submenu: Option<usize> =
+ kwargs.get(Qstr::MP_QSTR_init_submenu)?.try_into_option()?;
let failed_backup: bool = kwargs.get(Qstr::MP_QSTR_failed_backup)?.try_into()?;
let paired_devices: Obj = kwargs.get(Qstr::MP_QSTR_paired_devices)?;
let paired_devices: Vec<TString, MAX_PAIRED_DEVICES> = util::iter_into_vec(paired_devices)?;
@@ -958,6 +960,7 @@ extern "C" fn new_show_device_menu(n_args: usize, args: *const Obj, kwargs: *mut
let led_enabled: Option<bool> = kwargs.get(Qstr::MP_QSTR_led_enabled)?.try_into_option()?;
let about_items: Obj = kwargs.get(Qstr::MP_QSTR_about_items)?;
let layout = ModelUI::show_device_menu(
+ init_submenu,
failed_backup,
paired_devices,
connected_idx,
@@ -1913,6 +1916,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// def show_device_menu(
/// *,
+ /// init_submenu: int | None,
/// failed_backup: bool,
/// paired_devices: Iterable[str],
/// connected_idx: int | None,
@@ -2149,5 +2153,6 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// Reboot: ClassVar[DeviceMenuResult]
/// RebootToBootloader: ClassVar[DeviceMenuResult]
/// TurnOff: ClassVar[DeviceMenuResult]
+ /// MenuRefresh: 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 9c029c6a..8c6d3136 100644
--- a/core/embed/rust/src/ui/layout/device_menu_result.rs
+++ b/core/embed/rust/src/ui/layout/device_menu_result.rs
@@ -31,6 +31,8 @@ pub static WIPE_DEVICE: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_B
pub static TURN_OFF: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
pub static REBOOT: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
pub static REBOOT_TO_BOOTLOADER: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
+// Misc
+pub static MENU_REFRESH: 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! {
@@ -55,6 +57,7 @@ static DEVICE_MENU_RESULT_TYPE: Type = obj_type! {
Qstr::MP_QSTR_TurnOff => TURN_OFF.as_obj(),
Qstr::MP_QSTR_Reboot => REBOOT.as_obj(),
Qstr::MP_QSTR_RebootToBootloader => REBOOT_TO_BOOTLOADER.as_obj(),
+ Qstr::MP_QSTR_MenuRefresh => MENU_REFRESH.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 a7b6977d..6ed1dfcc 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -934,6 +934,7 @@ impl FirmwareUI for UIBolt {
}
fn show_device_menu(
+ _init_submenu: Option<usize>,
_failed_backup: bool,
_paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
_connected_idx: Option<usize>,
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 982e0b0d..0fc7c709 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -1131,6 +1131,7 @@ impl FirmwareUI for UICaesar {
}
fn show_device_menu(
+ _init_submenu: Option<usize>,
_failed_backup: bool,
_paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
_connected_idx: Option<usize>,
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 842b0cf9..ca58cf01 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -1016,6 +1016,7 @@ impl FirmwareUI for UIDelizia {
}
fn show_device_menu(
+ _init_submenu: Option<usize>,
_failed_backup: bool,
_paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
_connected_idx: Option<usize>,
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 38dfcf0f..8e18ab1f 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
@@ -183,7 +183,11 @@ impl ComponentMsgObj for DeviceMenuScreen {
DeviceMenuMsg::TurnOff => Ok(TURN_OFF.as_obj()),
DeviceMenuMsg::Reboot => Ok(REBOOT.as_obj()),
DeviceMenuMsg::RebootToBootloader => Ok(REBOOT_TO_BOOTLOADER.as_obj()),
- // nothing selected
+ // Misc
+ DeviceMenuMsg::MenuRefresh(submenu_id) => {
+ let submenu_idx: u8 = submenu_id.into();
+ Ok(new_tuple(&[MENU_REFRESH.as_obj(), submenu_idx.into()])?)
+ }
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 746cc7d6..f65b0e8b 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
@@ -1,4 +1,7 @@
-use core::ops::{Deref, DerefMut};
+use core::{
+ convert::TryFrom,
+ ops::{Deref, DerefMut},
+};
use crate::{
error::Error,
@@ -20,6 +23,9 @@ use crate::{
},
};
+#[cfg(feature = "ble")]
+use crate::ui::event::BLEEvent;
+
use super::{
super::{
component::{Button, ButtonStyleSheet, FuelGauge},
@@ -33,16 +39,47 @@ use super::{
};
use heapless::Vec;
-// - root
-// - pair & connect
-// - settings
-// - security
-// - pin code
-// - wipe code
-// - device
-// - power
+#[repr(u8)]
+#[derive(Copy, Clone, Default)]
+#[cfg_attr(test, derive(Debug))]
+pub enum DeviceMenuId {
+ #[default]
+ Root = 0,
+ PairAndConnect,
+ Settings,
+ Security,
+ PinCode,
+ WipeCode,
+ Device,
+ Power,
+}
+
+impl TryFrom<usize> for DeviceMenuId {
+ type Error = ();
+ fn try_from(v: usize) -> Result<Self, Self::Error> {
+ match v {
+ 0 => Ok(DeviceMenuId::Root),
+ 1 => Ok(DeviceMenuId::PairAndConnect),
+ 2 => Ok(DeviceMenuId::Settings),
+ 3 => Ok(DeviceMenuId::Security),
+ 4 => Ok(DeviceMenuId::PinCode),
+ 5 => Ok(DeviceMenuId::WipeCode),
+ 6 => Ok(DeviceMenuId::Device),
+ 7 => Ok(DeviceMenuId::Power),
+ _ => Err(()),
+ }
+ }
+}
+
+impl From<DeviceMenuId> for u8 {
+ #[inline]
+ fn from(id: DeviceMenuId) -> Self {
+ id as u8
+ }
+}
+
+// FIXME: use mem::variant_count when it becomes stable
const MAX_SUBMENUS: usize = 8;
-const MAX_DEPTH: usize = 3;
// submenus, device screens, regulatory and about screens
const MAX_SUBSCREENS: usize = MAX_SUBMENUS + MAX_PAIRED_DEVICES + 2;
@@ -50,7 +87,9 @@ const DISCONNECT_DEVICE_MENU_INDEX: usize = 0;
#[derive(Clone)]
enum Action {
- /// Go to another registered subscreen
+ /// Go to a registered submenu by id (static)
+ GoToSubmenu(DeviceMenuId),
+ /// Go to an arbitrary subscreen index (kept for device/about/regulatory)
GoTo(usize),
/// Return a DeviceMenuMsg to the caller
Return(DeviceMenuMsg),
@@ -89,7 +128,8 @@ pub enum DeviceMenuMsg {
LedEnabled,
WipeDevice,
- // nothing selected
+ // Misc
+ MenuRefresh(DeviceMenuId),
Close,
}
@@ -164,7 +204,7 @@ impl Submenu {
// Each subscreen of the DeviceMenuScreen is one of these
enum Subscreen {
// A registered submenu
- Submenu(usize),
+ Submenu(usize, DeviceMenuId),
// A screen allowing the user to to disconnect a device
DeviceScreen(
@@ -182,7 +222,7 @@ enum Subscreen {
// Used to preallocate memory for the largest enum variant
#[allow(clippy::large_enum_variant)]
enum ActiveScreen {
- Menu(VerticalMenuScreen<MediumMenuVec>),
+ Menu(VerticalMenuScreen<MediumMenuVec>, DeviceMenuId),
Device(VerticalMenuScreen<ShortMenuVec>),
About(TextScreen<Paragraphs<PropsList>>),
Regulatory(RegulatoryScreen),
@@ -202,15 +242,18 @@ pub struct DeviceMenuScreen {
// Information needed to construct any subscreen on demand
submenus: GcBox<Vec<Submenu, MAX_SUBMENUS>>,
subscreens: Vec<Subscreen, MAX_SUBSCREENS>,
+
+ // Sparse map from SubmenuId -> subscreen index
+ submenu_index: [Option<usize>; MAX_SUBMENUS],
+
// index of the current subscreen in the list of subscreens
active_subscreen: usize,
- // stack of parents that led to the current subscreen
- parent_subscreens: Vec<usize, MAX_DEPTH>,
}
impl DeviceMenuScreen {
#[allow(clippy::too_many_arguments)]
pub fn new(
+ init_submenu: Option<usize>,
failed_backup: bool,
paired_devices: Vec<TString<'static>, MAX_PAIRED_DEVICES>,
connected_idx: Option<usize>,
@@ -231,30 +274,15 @@ impl DeviceMenuScreen {
active_subscreen: 0,
submenus: GcBox::new(Vec::new())?,
subscreens: Vec::new(),
- parent_subscreens: Vec::new(),
+ submenu_index: [None; MAX_SUBMENUS],
};
- let about = screen.add_subscreen(Subscreen::AboutScreen);
- let regulatory = screen.add_subscreen(Subscreen::RegulatoryScreen);
- 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_enabled,
- regulatory,
- about,
- );
- let settings = screen.add_settings_menu(security, device);
- let power = screen.add_power_menu();
+ if pin_code.is_some() || auto_lock_delay.is_some() || wipe_code.is_some() || check_backup {
+ screen.register_security_menu(pin_code, auto_lock_delay, wipe_code, check_backup);
+ }
+ screen.register_device_menu(device_name, screen_brightness, haptic_feedback, led_enabled);
+ screen.register_settings_menu();
+ screen.register_power_menu();
let is_connected = connected_idx.is_some_and(|idx| idx < paired_devices.len());
let connected_subtext: Option<TString<'static>> =
@@ -267,29 +295,44 @@ impl DeviceMenuScreen {
.push(screen.add_subscreen(Subscreen::DeviceScreen(*device, connected, i))));
}
- let pair_and_connect =
- screen.add_pair_and_connect_menu(paired_devices, submenu_indices, connected_idx);
+ screen.register_pair_and_connect_menu(paired_devices, submenu_indices, connected_idx);
let pin_unset = pin_code == Some(false);
- let root = screen.add_root_menu(
- failed_backup,
- pin_unset,
- pair_and_connect,
- settings,
- connected_subtext,
- power,
- );
+ screen.register_root_menu(failed_backup, pin_unset, connected_subtext);
- screen.set_active_subscreen(root);
+ // Activate the init submenu
+ let init_submenu_id = init_submenu
+ .and_then(|v| DeviceMenuId::try_from(v).ok())
+ .unwrap_or_default();
+
+ let init_subscreen = unwrap!(screen.try_resolve_submenu(init_submenu_id));
+ screen.set_active_subscreen(init_subscreen);
Ok(screen)
}
- fn add_pair_and_connect_menu(
+ #[inline]
+ fn register_submenu(&mut self, id: DeviceMenuId, submenu: Submenu) {
+ let idx_in_submenus = self.add_submenu(submenu);
+ let subscreen_idx = self.add_subscreen(Subscreen::Submenu(idx_in_submenus, id));
+ self.submenu_index[id as usize] = Some(subscreen_idx);
+ }
+
+ #[inline]
+ fn try_resolve_submenu(&self, id: DeviceMenuId) -> Option<usize> {
+ self.submenu_index[id as usize]
+ }
+
+ #[inline]
+ fn has_submenu(&self, id: DeviceMenuId) -> bool {
+ self.try_resolve_submenu(id).is_some()
+ }
+
+ fn register_pair_and_connect_menu(
&mut self,
paired_devices: Vec<TString<'static>, MAX_PAIRED_DEVICES>,
submenu_indices: Vec<usize, MAX_PAIRED_DEVICES>,
connected_idx: Option<usize>,
- ) -> usize {
+ ) {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
for ((device_idx, device), submenu_idx) in
paired_devices.iter().enumerate().zip(submenu_indices)
@@ -315,28 +358,30 @@ impl DeviceMenuScreen {
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))
+ self.register_submenu(DeviceMenuId::PairAndConnect, Submenu::new(items));
}
- fn add_settings_menu(&mut self, security_index: Option<usize>, device_index: usize) -> usize {
+ fn register_settings_menu(&mut self) {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
- if let Some(security_index) = security_index {
+
+ if self.has_submenu(DeviceMenuId::Security) {
unwrap!(items.push(MenuItem::new(
TR::words__security.into(),
- Some(Action::GoTo(security_index))
+ Some(Action::GoToSubmenu(DeviceMenuId::Security))
)));
}
- unwrap!(items.push(MenuItem::new(
- 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))
+ if self.has_submenu(DeviceMenuId::Device) {
+ unwrap!(items.push(MenuItem::new(
+ TR::words__device.into(),
+ Some(Action::GoToSubmenu(DeviceMenuId::Device))
+ )));
+ }
+
+ self.register_submenu(DeviceMenuId::Settings, Submenu::new(items));
}
- fn add_power_menu(&mut self) -> usize {
+ fn register_power_menu(&mut self) {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
unwrap!(items.push(MenuItem::new(
TR::buttons__turn_off.into(),
@@ -351,11 +396,10 @@ impl DeviceMenuScreen {
Some(Action::Return(DeviceMenuMsg::RebootToBootloader))
)));
- let submenu_index = self.add_submenu(Submenu::new(items));
- self.add_subscreen(Subscreen::Submenu(submenu_index))
+ self.register_submenu(DeviceMenuId::Power, Submenu::new(items));
}
- fn add_code_menu(&mut self, wipe_code: bool) -> usize {
+ fn register_code_menu(&mut self, wipe_code: bool) {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
let change_text = match wipe_code {
true => TR::wipe_code__change,
@@ -382,23 +426,26 @@ impl DeviceMenuScreen {
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))
+ let id = match wipe_code {
+ true => DeviceMenuId::WipeCode,
+ false => DeviceMenuId::PinCode,
+ };
+ self.register_submenu(id, Submenu::new(items));
}
- fn add_security_menu(
+ fn register_security_menu(
&mut self,
pin_code: Option<bool>,
auto_lock_delay: Option<TString<'static>>,
wipe_code: Option<bool>,
check_backup: bool,
- ) -> usize {
+ ) {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = 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);
+ self.register_code_menu(false);
+ let action = Action::GoToSubmenu(DeviceMenuId::PinCode);
let subtext = (
TR::words__enabled.into(),
Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN),
@@ -426,8 +473,8 @@ impl DeviceMenuScreen {
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);
+ self.register_code_menu(true);
+ let action = Action::GoToSubmenu(DeviceMenuId::WipeCode);
let subtext = (
TR::words__enabled.into(),
Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN),
@@ -451,19 +498,16 @@ impl DeviceMenuScreen {
)));
}
- let submenu_index = self.add_submenu(Submenu::new(items));
- self.add_subscreen(Subscreen::Submenu(submenu_index))
+ self.register_submenu(DeviceMenuId::Security, Submenu::new(items));
}
- fn add_device_menu(
+ fn register_device_menu(
&mut self,
device_name: Option<TString<'static>>,
screen_brightness: Option<TString<'static>>,
haptic_feedback: Option<bool>,
led_enabled: Option<bool>,
- regulatory_index: usize,
- about_index: usize,
- ) -> usize {
+ ) {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
if let Some(device_name) = device_name {
let mut item_device_name = MenuItem::new(
@@ -516,11 +560,13 @@ impl DeviceMenuScreen {
unwrap!(items.push(led_item));
}
+ let regulatory_index = self.add_subscreen(Subscreen::RegulatoryScreen);
unwrap!(items.push(MenuItem::new(
TR::regulatory_certification__title.into(),
Some(Action::GoTo(regulatory_index))
)));
+ let about_index = self.add_subscreen(Subscreen::AboutScreen);
unwrap!(items.push(MenuItem::new(
TR::words__about.into(),
Some(Action::GoTo(about_index))
@@ -533,58 +579,63 @@ impl DeviceMenuScreen {
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))
+ self.register_submenu(DeviceMenuId::Device, Submenu::new(items));
}
- fn add_root_menu(
+ fn register_root_menu(
&mut self,
failed_backup: bool,
pin_unset: bool,
- pair_and_connect_index: usize,
- settings_index: usize,
connected_subtext: Option<TString<'static>>,
- power_index: usize,
- ) -> usize {
+ ) {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
+
if failed_backup {
- let mut item_backup_failed = MenuItem::new(
+ let mut item = MenuItem::new(
TR::homescreen__title_backup_failed.into(),
Some(Action::Return(DeviceMenuMsg::BackupFailed)),
);
- item_backup_failed.with_subtext(Some((TR::words__review.into(), None)));
- item_backup_failed.with_stylesheet(MENU_ITEM_ERROR);
- unwrap!(items.push(item_backup_failed));
+ item.with_subtext(Some((TR::words__review.into(), None)));
+ item.with_stylesheet(MENU_ITEM_ERROR);
+ unwrap!(items.push(item));
}
+
if pin_unset {
- let mut item_pin_unset = MenuItem::new(
+ let mut item = MenuItem::new(
TR::homescreen__title_pin_not_set.into(),
Some(Action::Return(DeviceMenuMsg::PinCode)),
);
- item_pin_unset.with_subtext(Some((TR::words__set.into(), None)));
- item_pin_unset.with_stylesheet(MENU_ITEM_LIGHT_WARNING);
- unwrap!(items.push(item_pin_unset));
+ item.with_subtext(Some((TR::words__set.into(), None)));
+ item.with_stylesheet(MENU_ITEM_LIGHT_WARNING);
+ unwrap!(items.push(item));
}
- let mut item_pair_and_connect = MenuItem::new(
- TR::ble__pair_title.into(),
- Some(Action::GoTo(pair_and_connect_index)),
- );
- item_pair_and_connect.with_subtext(
- connected_subtext.map(|t| (t, Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN))),
- );
- unwrap!(items.push(item_pair_and_connect));
- unwrap!(items.push(MenuItem::new(
- TR::words__settings.into(),
- Some(Action::GoTo(settings_index)),
- )));
- unwrap!(items.push(MenuItem::new(
- TR::words__power.into(),
- Some(Action::GoTo(power_index)),
- )));
+ if self.has_submenu(DeviceMenuId::PairAndConnect) {
+ let mut it = MenuItem::new(
+ TR::ble__pair_title.into(),
+ Some(Action::GoToSubmenu(DeviceMenuId::PairAndConnect)),
+ );
+ it.with_subtext(
+ connected_subtext.map(|t| (t, Some(&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN))),
+ );
+ unwrap!(items.push(it));
+ }
+
+ if self.has_submenu(DeviceMenuId::Settings) {
+ unwrap!(items.push(MenuItem::new(
+ TR::words__settings.into(),
+ Some(Action::GoToSubmenu(DeviceMenuId::Settings)),
+ )));
+ }
+
+ if self.has_submenu(DeviceMenuId::Power) {
+ unwrap!(items.push(MenuItem::new(
+ TR::words__power.into(),
+ Some(Action::GoToSubmenu(DeviceMenuId::Power)),
+ )));
+ }
- let submenu_index = self.add_submenu(Submenu::new(items).with_battery());
- self.add_subscreen(Subscreen::Submenu(submenu_index))
+ self.register_submenu(DeviceMenuId::Root, Submenu::new(items).with_battery());
}
fn add_submenu(&mut self, submenu: Submenu) -> usize {
@@ -603,10 +654,18 @@ impl DeviceMenuScreen {
self.build_active_subscreen();
}
+ fn activate_subscreen(&mut self, idx: usize, ctx: &mut EventCtx) {
+ self.set_active_subscreen(idx);
+ self.place(self.bounds);
+ if let ActiveScreen::Menu(screen, ..) = self.active_screen.deref_mut() {
+ screen.initialize_screen(ctx);
+ }
+ }
+
fn build_active_subscreen(&mut self) {
match self.subscreens[self.active_subscreen] {
- Subscreen::Submenu(ref mut submenu_index) => {
- let submenu = &self.submenus[*submenu_index];
+ Subscreen::Submenu(submenu_index, id) => {
+ let submenu = &self.submenus[submenu_index];
let mut menu = VerticalMenu::<MediumMenuVec>::empty();
for item in &submenu.items {
let button = if let Some(connected) = item.connection_status {
@@ -641,7 +700,7 @@ impl DeviceMenuScreen {
);
}
*self.active_screen.deref_mut() =
- ActiveScreen::Menu(VerticalMenuScreen::new(menu).with_header(header));
+ ActiveScreen::Menu(VerticalMenuScreen::new(menu).with_header(header), id);
}
Subscreen::DeviceScreen(device, connected, _) => {
let mut menu = VerticalMenu::empty();
@@ -694,20 +753,21 @@ impl DeviceMenuScreen {
fn handle_submenu(&mut self, ctx: &mut EventCtx, idx: usize) -> Option<DeviceMenuMsg> {
match self.subscreens[self.active_subscreen] {
- Subscreen::Submenu(ref mut submenu_index) => {
- match self.submenus[*submenu_index].items[idx].action {
- Some(Action::GoTo(menu)) => {
- unwrap!(self.parent_subscreens.push(self.active_subscreen));
- self.set_active_subscreen(menu);
- self.place(self.bounds);
- if let ActiveScreen::Menu(screen) = self.active_screen.deref_mut() {
- screen.initialize_screen(ctx);
+ Subscreen::Submenu(submenu_index, ..) => {
+ match self.submenus[submenu_index].items[idx].action {
+ Some(Action::GoToSubmenu(id_new)) => {
+ if let Some(menu) = self.try_resolve_submenu(id_new) {
+ self.activate_subscreen(menu, ctx);
}
return None;
}
+ Some(Action::GoTo(menu)) => {
+ self.activate_subscreen(menu, ctx);
+ return None;
+ }
Some(Action::Return(msg)) => return Some(msg),
None => {}
- };
+ }
}
_ => {
panic!("Expected a submenu!");
@@ -718,16 +778,23 @@ impl DeviceMenuScreen {
}
fn go_back(&mut self, ctx: &mut EventCtx) -> Option<DeviceMenuMsg> {
- if let Some(parent) = self.parent_subscreens.pop() {
- self.set_active_subscreen(parent);
- self.place(self.bounds);
- if let ActiveScreen::Menu(screen) = self.active_screen.deref_mut() {
- screen.initialize_screen(ctx);
- }
- None
- } else {
- Some(DeviceMenuMsg::Close)
- }
+ let parent = match self.subscreens[self.active_subscreen] {
+ Subscreen::Submenu(_, id) => match id {
+ DeviceMenuId::Root => return Some(DeviceMenuMsg::Close),
+ DeviceMenuId::PairAndConnect => DeviceMenuId::Root,
+ DeviceMenuId::Settings => DeviceMenuId::Root,
+ DeviceMenuId::Security => DeviceMenuId::Settings,
+ DeviceMenuId::PinCode => DeviceMenuId::Security,
+ DeviceMenuId::WipeCode => DeviceMenuId::Security,
+ DeviceMenuId::Device => DeviceMenuId::Settings,
+ DeviceMenuId::Power => DeviceMenuId::Root,
+ },
+ Subscreen::DeviceScreen(..) => DeviceMenuId::PairAndConnect,
+ Subscreen::AboutScreen | Subscreen::RegulatoryScreen => DeviceMenuId::Device,
+ };
+
+ self.activate_subscreen(unwrap!(self.try_resolve_submenu(parent)), ctx);
+ None
}
}
@@ -742,7 +809,7 @@ impl Component for DeviceMenuScreen {
self.bounds = bounds;
match self.active_screen.deref_mut() {
- ActiveScreen::Menu(menu) => {
+ ActiveScreen::Menu(menu, ..) => {
menu.place(bounds);
}
ActiveScreen::Device(device) => {
@@ -761,21 +828,38 @@ impl Component for DeviceMenuScreen {
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
+ #[cfg(feature = "ble")]
+ if matches!(
+ event,
+ Event::BLE(BLEEvent::Connected | BLEEvent::Disconnected | BLEEvent::ConnectionChanged)
+ ) {
+ let submenu_idx = match self.active_screen.deref_mut() {
+ ActiveScreen::Menu(_, id) => *id,
+ ActiveScreen::Device(_) => DeviceMenuId::PairAndConnect,
+ ActiveScreen::Regulatory(_) | ActiveScreen::About(_) => DeviceMenuId::Device,
+ ActiveScreen::Empty => DeviceMenuId::Root,
+ };
+
+ return Some(DeviceMenuMsg::MenuRefresh(submenu_idx));
+ }
+
// Handle the event for the active menu
let subscreen = &self.subscreens[self.active_subscreen];
match (subscreen, self.active_screen.deref_mut()) {
- (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::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(button_idx)) => match button_idx {
@@ -813,7 +897,7 @@ impl Component for DeviceMenuScreen {
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
match self.active_screen.deref() {
- ActiveScreen::Menu(menu) => menu.render(target),
+ ActiveScreen::Menu(menu, ..) => menu.render(target),
ActiveScreen::Device(device) => device.render(target),
ActiveScreen::About(about) => about.render(target),
ActiveScreen::Regulatory(regulatory) => regulatory.render(target),
@@ -828,7 +912,7 @@ impl crate::trace::Trace for DeviceMenuScreen {
t.component("DeviceMenuScreen");
match self.active_screen.deref() {
- ActiveScreen::Menu(ref screen) => {
+ ActiveScreen::Menu(ref screen, ..) => {
t.child("Menu", screen);
}
ActiveScreen::Device(ref screen) => {
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 e58c29a1..bc556f42 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -1200,6 +1200,7 @@ impl FirmwareUI for UIEckhart {
}
fn show_device_menu(
+ init_submenu: Option<usize>,
failed_backup: bool,
paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
connected_idx: Option<usize>,
@@ -1214,6 +1215,7 @@ impl FirmwareUI for UIEckhart {
about_items: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
let layout = RootComponent::new(DeviceMenuScreen::new(
+ init_submenu,
failed_backup,
paired_devices,
connected_idx,
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 34401d45..68eb9fff 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -361,6 +361,7 @@ pub trait FirmwareUI {
#[allow(clippy::too_many_arguments)]
fn show_device_menu(
+ init_submenu: Option<usize>,
failed_backup: bool,
paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
connected_idx: Option<usize>,
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index 965c4b74..bcc1a6d8 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -624,6 +624,7 @@ def show_homescreen(
# rust/src/ui/api/firmware_micropython.rs
def show_device_menu(
*,
+ init_submenu: int | None,
failed_backup: bool,
paired_devices: Iterable[str],
connected_idx: int | None,
@@ -881,3 +882,4 @@ class DeviceMenuResult:
Reboot: ClassVar[DeviceMenuResult]
RebootToBootloader: ClassVar[DeviceMenuResult]
TurnOff: ClassVar[DeviceMenuResult]
+ MenuRefresh: ClassVar[DeviceMenuResult]
diff --git a/core/src/apps/homescreen/device_menu.py b/core/src/apps/homescreen/device_menu.py
index 7f75cb8a..d9248bf0 100644
--- a/core/src/apps/homescreen/device_menu.py
+++ b/core/src/apps/homescreen/device_menu.py
@@ -1,13 +1,27 @@
+from micropython import const
+
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.ui.layouts import interact, raise_if_cancelled
+from trezor.wire import ActionCancelled, PinCancelled
from trezorui_api import CANCELLED, DeviceMenuResult
BLE_MAX_BONDS = 8
+class SubmenuId:
+ ROOT = const(0)
+ PAIR_AND_CONNECT = const(1)
+ SETTINGS = const(2)
+ SECURITY = const(3)
+ PIN_CODE = const(4)
+ WIPE_CODE = const(5)
+ DEVICE = const(6)
+ POWER = const(7)
+
+
def _get_hostname(ble_addr: bytes, hostname_map: dict[bytes, str]) -> str:
if (hostname := hostname_map.get(ble_addr)) is None:
# Internal MAC address representation is using reversed byte order.
@@ -30,255 +44,329 @@ async def handle_device_menu() -> None:
assert utils.USE_THP and utils.USE_BLE
from ..thp import paired_cache
- is_initialized = storage_device.is_initialized()
- led_configurable = is_initialized and utils.USE_RGB_LED
- haptic_configurable = is_initialized and utils.USE_HAPTIC
- failed_backup = is_initialized and storage_device.unfinished_backup()
-
- bonds = ble.get_bonds()
- if __debug__:
- log.debug(__name__, "bonds: %s", bonds)
-
- connected_addr = ble.connected_addr()
- connected_idx = _find_device(connected_addr, bonds)
- if __debug__:
- log.debug(__name__, "connected: %s (%s)", connected_addr, connected_idx)
-
- hostname_map = {e.mac_addr: e.host_name for e in paired_cache.load()}
- if __debug__:
- log.debug(__name__, "hostname_map: %s", hostname_map)
-
- paired_devices = [_get_hostname(bond, hostname_map) for bond in bonds]
-
- bluetooth_version = "2.3.1.1"
- # ###
- firmware_version = ".".join(map(str, utils.VERSION))
- 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
- )
-
- if __debug__:
- log.debug(
- __name__,
- f"device menu, BLE state: {ble.connection_flags()} (peers: {ble.peer_count()})",
+ init_submenu = None
+
+ # Remain in the device loop until the menu is explicitly closed
+ while True:
+
+ is_initialized = storage_device.is_initialized()
+ led_configurable = is_initialized and utils.USE_RGB_LED
+ haptic_configurable = is_initialized and utils.USE_HAPTIC
+ failed_backup = is_initialized and storage_device.unfinished_backup()
+
+ bonds = ble.get_bonds()
+ if __debug__:
+ log.debug(__name__, "bonds: %s", bonds)
+
+ connected_addr = ble.connected_addr()
+ connected_idx = _find_device(connected_addr, bonds)
+ if __debug__:
+ log.debug(__name__, "connected: %s (%s)", connected_addr, connected_idx)
+ hostname_map = {e.mac_addr: e.host_name for e in paired_cache.load()}
+ if __debug__:
+ log.debug(__name__, "hostname_map: %s", hostname_map)
+ paired_devices = [_get_hostname(bond, hostname_map) for bond in bonds]
+
+ bluetooth_version = "2.3.1.1"
+ # ###
+ firmware_version = ".".join(map(str, utils.VERSION))
+ 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
)
- menu_result = await interact(
- trezorui_api.show_device_menu(
- failed_backup=failed_backup,
- paired_devices=paired_devices,
- connected_idx=connected_idx,
- pin_code=config.has_pin() if is_initialized else None,
- auto_lock_delay=auto_lock_delay,
- wipe_code=config.has_wipe_code() if is_initialized else None,
- check_backup=is_initialized,
- device_name=(
- (storage_device.get_label() or utils.MODEL_FULL_NAME)
- if is_initialized
- else None
+ menu_result = await interact(
+ trezorui_api.show_device_menu(
+ init_submenu=init_submenu,
+ failed_backup=failed_backup,
+ paired_devices=paired_devices,
+ connected_idx=connected_idx,
+ pin_code=config.has_pin() if is_initialized else None,
+ auto_lock_delay=auto_lock_delay,
+ wipe_code=config.has_wipe_code() if is_initialized else None,
+ check_backup=is_initialized,
+ device_name=(
+ (storage_device.get_label() or utils.MODEL_FULL_NAME)
+ if is_initialized
+ else None
+ ),
+ screen_brightness=TR.brightness__title if is_initialized else None,
+ haptic_feedback=(
+ storage_device.get_haptic_feedback()
+ if haptic_configurable
+ else None
+ ),
+ led_enabled=(
+ storage_device.get_rgb_led() if led_configurable else None
+ ),
+ about_items=[
+ (TR.homescreen__firmware_version, firmware_version, False),
+ (TR.homescreen__firmware_type, firmware_type, False),
+ (TR.ble__version, bluetooth_version, False),
+ ],
),
- screen_brightness=TR.brightness__title if is_initialized else None,
- haptic_feedback=(
- storage_device.get_haptic_feedback() if haptic_configurable else None
- ),
- led_enabled=(storage_device.get_rgb_led() if led_configurable 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",
- raise_on_cancel=None,
- )
- # Root menu
- if menu_result is DeviceMenuResult.BackupFailed and failed_backup:
- from trezor.messages import WipeDevice
- from trezor.ui.layouts import raise_if_cancelled
-
- from apps.management.wipe_device import wipe_device
-
- await raise_if_cancelled(
- trezorui_api.show_warning(
- title=TR.homescreen__title_backup_failed,
- button=TR.words__wipe,
- description=TR.wipe__start_again,
- danger=True,
- ),
- "prompt_device_wipe",
+ "device_menu",
+ raise_on_cancel=None,
)
-
- await wipe_device(WipeDevice())
- # Pair & Connect
- elif menu_result is DeviceMenuResult.DeviceDisconnect and ble.is_connected():
-
- ble.disconnect()
- elif menu_result is DeviceMenuResult.DevicePair:
- from trezor.ui.layouts import raise_if_cancelled, show_warning
-
- from apps.management.ble.pair_new_device import pair_new_device
-
- if ble.is_connected():
- ble.disconnect()
-
- if len(paired_devices) < BLE_MAX_BONDS:
- await pair_new_device()
+ # Root menu
+ if menu_result is DeviceMenuResult.BackupFailed and failed_backup:
+ from trezor.messages import WipeDevice
+
+ from apps.management.wipe_device import wipe_device
+
+ try:
+ await raise_if_cancelled(
+ trezorui_api.show_warning(
+ title=TR.homescreen__title_backup_failed,
+ button=TR.words__wipe,
+ description=TR.wipe__start_again,
+ danger=True,
+ ),
+ "prompt_device_wipe",
+ )
+ await wipe_device(WipeDevice())
+ except ActionCancelled:
+ init_submenu = SubmenuId.ROOT
+ else:
+ break
+ # Pair & Connect
+ elif menu_result is DeviceMenuResult.DeviceDisconnect and ble.is_connected():
+ init_submenu = SubmenuId.PAIR_AND_CONNECT
+ try:
+ ble.disconnect()
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.PAIR_AND_CONNECT
+ elif menu_result is DeviceMenuResult.DevicePair:
+ from trezor.ui.layouts import show_warning
+
+ from apps.management.ble.pair_new_device import pair_new_device
+
+ try:
+ if ble.is_connected():
+ ble.disconnect()
+
+ if len(paired_devices) < BLE_MAX_BONDS:
+ await pair_new_device()
+ else:
+ await show_warning(
+ "device_pair",
+ TR.ble__limit_reached,
+ button=TR.buttons__confirm,
+ )
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.PAIR_AND_CONNECT
+ elif menu_result is DeviceMenuResult.DeviceUnpairAll:
+ from trezor.messages import BleUnpair
+
+ from apps.management.ble.unpair import unpair
+
+ try:
+ await unpair(BleUnpair(all=True))
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.PAIR_AND_CONNECT
+ elif isinstance(menu_result, tuple):
+ from trezor.messages import BleUnpair
+
+ from apps.management.ble.unpair import unpair
+
+ # It's a tuple with (result_type, index)
+ result_type, index = menu_result
+ if result_type is DeviceMenuResult.DeviceUnpair and index < len(bonds):
+ try:
+ await unpair(BleUnpair(addr=bonds[index]))
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.PAIR_AND_CONNECT
+ # Refresh only
+ elif result_type is DeviceMenuResult.MenuRefresh:
+ init_submenu = index
+ else:
+ raise RuntimeError(f"Unknown menu {result_type}, {index}")
+ # Security settings
+ elif menu_result is DeviceMenuResult.PinCode and is_initialized:
+ from trezor.messages import ChangePin
+
+ from apps.management.change_pin import change_pin
+
+ try:
+ await change_pin(ChangePin())
+ except (ActionCancelled, PinCancelled):
+ pass
+ finally:
+ init_submenu = SubmenuId.SECURITY
+ elif menu_result is DeviceMenuResult.PinRemove and config.has_pin():
+ from trezor.messages import ChangePin
+
+ from apps.management.change_pin import change_pin
+
+ try:
+ await change_pin(ChangePin(remove=True))
+ except (ActionCancelled, PinCancelled):
+ pass
+ finally:
+ init_submenu = SubmenuId.SECURITY
+ elif menu_result is DeviceMenuResult.AutoLockDelay and config.has_pin():
+ from trezor.messages import ApplySettings
+
+ from apps.management.apply_settings import apply_settings
+
+ try:
+ 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,
+ )
+ # Necessary for the style check not to raise type error
+ assert isinstance(auto_lock_delay_ms, int)
+ await apply_settings(
+ ApplySettings(
+ auto_lock_delay_ms=auto_lock_delay_ms,
+ )
+ )
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.SECURITY
+ elif menu_result is DeviceMenuResult.WipeCode and is_initialized:
+ from trezor.messages import ChangeWipeCode
+
+ from apps.management.change_wipe_code import change_wipe_code
+
+ try:
+ await change_wipe_code(ChangeWipeCode())
+ except (ActionCancelled, PinCancelled):
+ pass
+ finally:
+ init_submenu = SubmenuId.SECURITY
+ elif menu_result is DeviceMenuResult.WipeRemove and config.has_wipe_code():
+ from trezor.messages import ChangeWipeCode
+
+ from apps.management.change_wipe_code import change_wipe_code
+
+ try:
+ await change_wipe_code(ChangeWipeCode(remove=True))
+ except (ActionCancelled, PinCancelled):
+ pass
+ finally:
+ init_submenu = SubmenuId.SECURITY
+ elif menu_result is DeviceMenuResult.CheckBackup and is_initialized:
+ from trezor.enums import RecoveryType
+ from trezor.messages import RecoveryDevice
+
+ from apps.management.recovery_device import recovery_device
+
+ try:
+
+ await recovery_device(
+ RecoveryDevice(
+ type=RecoveryType.DryRun,
+ )
+ )
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.SECURITY
+ # Device settings
+ elif menu_result is DeviceMenuResult.DeviceName and is_initialized:
+ from trezor.messages import ApplySettings
+
+ from apps.management.apply_settings import apply_settings
+
+ try:
+ label = await interact(
+ trezorui_api.request_string(
+ prompt=TR.device_name__enter,
+ max_len=storage_device.LABEL_MAXLENGTH,
+ allow_empty=True,
+ prefill=storage_device.get_label(),
+ ),
+ "device_name",
+ )
+ # Necessary for the style check not to raise type error
+ assert isinstance(label, str)
+ await apply_settings(ApplySettings(label=label))
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.DEVICE
+ elif menu_result is DeviceMenuResult.ScreenBrightness and is_initialized:
+ from trezor.messages import SetBrightness
+
+ from apps.management.set_brightness import set_brightness
+
+ try:
+ await set_brightness(SetBrightness())
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.DEVICE
+ elif menu_result is DeviceMenuResult.HapticFeedback and haptic_configurable:
+ from trezor import io
+
+ try:
+ enable = not storage_device.get_haptic_feedback()
+ io.haptic.haptic_set_enabled(enable)
+ storage_device.set_haptic_feedback(enable)
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.DEVICE
+ elif menu_result is DeviceMenuResult.LedEnabled and led_configurable:
+ from trezor import io
+
+ try:
+ enable = not storage_device.get_rgb_led()
+ io.rgb_led.rgb_led_set_enabled(enable)
+ storage_device.set_rgb_led(enable)
+ except ActionCancelled:
+ pass
+ finally:
+ init_submenu = SubmenuId.DEVICE
+ elif menu_result is DeviceMenuResult.WipeDevice:
+ from trezor.messages import WipeDevice
+
+ from apps.management.wipe_device import wipe_device
+
+ try:
+ await wipe_device(WipeDevice())
+ except ActionCancelled:
+ init_submenu = SubmenuId.DEVICE
+ else:
+ break
+ # Power settings
+ elif menu_result is DeviceMenuResult.TurnOff:
+ from trezor import io
+
+ io.pm.hibernate()
+ raise RuntimeError
+ elif menu_result is DeviceMenuResult.Reboot:
+ from trezor.utils import reboot_to_bootloader
+
+ # Empty boot command results to a normal reboot
+ reboot_to_bootloader()
+ raise RuntimeError
+ elif menu_result is DeviceMenuResult.RebootToBootloader:
+ from trezor.enums import BootCommand
+ from trezor.utils import reboot_to_bootloader
+
+ reboot_to_bootloader(BootCommand.STOP_AND_WAIT)
+ raise RuntimeError
+ elif menu_result is CANCELLED:
+ return
else:
- await show_warning(
- "device_pair",
- TR.ble__limit_reached,
- button=TR.buttons__confirm,
- )
- elif menu_result is DeviceMenuResult.DeviceUnpairAll:
- from trezor.messages import BleUnpair
-
- from apps.management.ble.unpair import unpair
-
- await unpair(BleUnpair(all=True))
-
- elif isinstance(menu_result, tuple):
- from trezor.messages import BleUnpair
-
- from apps.management.ble.unpair import unpair
-
- # It's a tuple with (result_type, index)
- result_type, index = menu_result
- if result_type is DeviceMenuResult.DeviceUnpair and index < len(bonds):
-
- await unpair(BleUnpair(addr=bonds[index]))
- else:
- raise RuntimeError(f"Unknown menu {result_type}, {index}")
- # Security settings
- elif menu_result is DeviceMenuResult.PinCode and is_initialized:
- from trezor.messages import ChangePin
-
- from apps.management.change_pin import change_pin
-
- await change_pin(ChangePin())
- elif menu_result is DeviceMenuResult.PinRemove and config.has_pin():
- 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 and config.has_pin():
- from trezor.messages import ApplySettings
-
- from apps.management.apply_settings import apply_settings
-
- 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,
- )
- # Necessary for the style check not to raise type error
- 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 and is_initialized:
- from trezor.messages import ChangeWipeCode
-
- from apps.management.change_wipe_code import change_wipe_code
-
- await change_wipe_code(ChangeWipeCode())
- elif menu_result is DeviceMenuResult.WipeRemove and config.has_wipe_code():
- from trezor.messages import ChangeWipeCode
-
- from apps.management.change_wipe_code import change_wipe_code
-
- await change_wipe_code(ChangeWipeCode(remove=True))
- elif menu_result is DeviceMenuResult.CheckBackup and is_initialized:
- 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 and is_initialized:
- from trezor.messages import ApplySettings
-
- from apps.management.apply_settings import apply_settings
-
- label = await interact(
- trezorui_api.request_string(
- prompt=TR.device_name__enter,
- max_len=storage_device.LABEL_MAXLENGTH,
- allow_empty=True,
- prefill=storage_device.get_label(),
- ),
- "device_name",
- )
- # Necessary for the style check not to raise type error
- assert isinstance(label, str)
- await apply_settings(ApplySettings(label=label))
- elif menu_result is DeviceMenuResult.ScreenBrightness and is_initialized:
- from trezor.messages import SetBrightness
-
- from apps.management.set_brightness import set_brightness
-
- await set_brightness(SetBrightness())
- elif menu_result is DeviceMenuResult.HapticFeedback and haptic_configurable:
- from trezor.messages import ApplySettings
-
- from apps.management.apply_settings import apply_settings
-
- await apply_settings(
- ApplySettings(
- haptic_feedback=not storage_device.get_haptic_feedback(),
- )
- )
- elif menu_result is DeviceMenuResult.LedEnabled and led_configurable:
- 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())
- # Power settings
- elif menu_result is DeviceMenuResult.TurnOff:
- from trezor import io
-
- io.pm.hibernate()
- elif menu_result is DeviceMenuResult.Reboot:
- from trezor.utils import reboot_to_bootloader
-
- # Empty boot command results to a normal reboot
- reboot_to_bootloader()
- elif menu_result is DeviceMenuResult.RebootToBootloader:
- from trezor.enums import BootCommand
- from trezor.utils import reboot_to_bootloader
-
- reboot_to_bootloader(BootCommand.STOP_AND_WAIT)
- elif menu_result is CANCELLED:
- return
- else:
- raise RuntimeError(f"Unknown menu {menu_result}")
+ raise RuntimeError(f"Unknown menu {menu_result}")
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.