From 87cd6e276e1c223f93db47094a3493c143132599 Mon Sep 17 00:00:00 2001 From: tychovrahe Date: Mon, 8 Jan 2024 23:40:25 +0100 Subject: [PATCH] feat(core): added user adjustable brightness setting --- common/protob/messages-management.proto | 1 + core/.changelog.d/3208.added | 1 + core/embed/firmware/main.c | 4 + core/embed/rust/librust_qstr.h | 2 + .../rust/src/ui/model_tt/component/mod.rs | 2 + .../model_tt/component/number_input_slider.rs | 209 ++++++++++++++++++ core/embed/rust/src/ui/model_tt/layout.rs | 52 ++++- core/embed/trezorhal/stm32f4/backlight_pwm.c | 37 +++- core/mocks/generated/trezorui2.pyi | 12 + core/src/apps/management/apply_settings.py | 26 ++- core/src/storage/common.py | 8 +- core/src/storage/device.py | 15 ++ core/src/trezor/messages.py | 2 + core/src/trezor/ui/__init__.py | 2 +- core/src/trezor/ui/layouts/tt/__init__.py | 45 +++- core/src/trezor/ui/layouts/tt/homescreen.py | 4 +- core/src/trezor/ui/layouts/tt/progress.py | 0 core/src/trezor/ui/style.py | 17 ++ python/src/trezorlib/cli/settings.py | 7 + python/src/trezorlib/device.py | 2 + python/src/trezorlib/messages.py | 3 + .../protos/generated/messages_management.rs | 39 +++- 22 files changed, 474 insertions(+), 16 deletions(-) create mode 100644 core/.changelog.d/3208.added create mode 100644 core/embed/rust/src/ui/model_tt/component/number_input_slider.rs create mode 100644 core/src/trezor/ui/layouts/tt/progress.py diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index e2c084175..9ecb44e25 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -180,6 +180,7 @@ message ApplySettings { optional SafetyCheckLevel safety_checks = 9; // Safety check level, set to Prompt to limit path namespace enforcement optional bool experimental_features = 10; // enable experimental message types optional bool hide_passphrase_from_host = 11; // do not show passphrase coming from host + optional uint32 brightness = 12; // display brightness, 0 = select on device } /** diff --git a/core/.changelog.d/3208.added b/core/.changelog.d/3208.added new file mode 100644 index 000000000..995ba08ab --- /dev/null +++ b/core/.changelog.d/3208.added @@ -0,0 +1 @@ +[T2T1] Added user adjustable brightness setting. diff --git a/core/embed/firmware/main.c b/core/embed/firmware/main.c index 64e42da49..7ab54086a 100644 --- a/core/embed/firmware/main.c +++ b/core/embed/firmware/main.c @@ -128,6 +128,10 @@ int main(void) { dma2d_init(); #endif +#if defined TREZOR_MODEL_T + set_core_clock(CLOCK_180_MHZ); +#endif + display_reinit(); #ifdef STM32U5 diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index bd7a90819..991daece7 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -93,6 +93,7 @@ static void _librust_qstrs(void) { MP_QSTR_bounds; MP_QSTR_button; MP_QSTR_button_event; + MP_QSTR_callback; MP_QSTR_buttons__abort; MP_QSTR_buttons__access; MP_QSTR_buttons__again; @@ -382,6 +383,7 @@ static void _librust_qstrs(void) { MP_QSTR_request_bip39; MP_QSTR_request_complete_repaint; MP_QSTR_request_number; + MP_QSTR_request_number_slider; MP_QSTR_request_passphrase; MP_QSTR_request_pin; MP_QSTR_request_slip39; diff --git a/core/embed/rust/src/ui/model_tt/component/mod.rs b/core/embed/rust/src/ui/model_tt/component/mod.rs index ec6d1b0b7..994accd6e 100644 --- a/core/embed/rust/src/ui/model_tt/component/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/mod.rs @@ -16,6 +16,7 @@ mod keyboard; mod loader; #[cfg(feature = "translations")] mod number_input; +mod number_input_slider; #[cfg(feature = "translations")] mod page; mod progress; @@ -50,6 +51,7 @@ pub use keyboard::{ pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; #[cfg(feature = "translations")] pub use number_input::{NumberInputDialog, NumberInputDialogMsg}; +pub use number_input_slider::{NumberInputSliderDialog, NumberInputSliderDialogMsg}; #[cfg(feature = "translations")] pub use page::ButtonPage; pub use progress::Progress; diff --git a/core/embed/rust/src/ui/model_tt/component/number_input_slider.rs b/core/embed/rust/src/ui/model_tt/component/number_input_slider.rs new file mode 100644 index 000000000..8cc1db674 --- /dev/null +++ b/core/embed/rust/src/ui/model_tt/component/number_input_slider.rs @@ -0,0 +1,209 @@ +use crate::ui::{ + component::{base::ComponentExt, Child, Component, Event, EventCtx}, + constant::screen, + display, + event::TouchEvent, + geometry::{Grid, Insets, Point, Rect}, +}; + +use super::{theme, Button, ButtonMsg}; + +pub enum NumberInputSliderDialogMsg { + Confirmed, + Cancelled, +} + +pub struct NumberInputSliderDialog +where + F: Fn(u32), +{ + area: Rect, + callback: F, + input: Child, + cancel_button: Child>, + confirm_button: Child>, +} + +impl NumberInputSliderDialog +where + F: Fn(u32), +{ + pub fn new(min: u32, max: u32, init_value: u32, callback: F) -> Self { + Self { + area: Rect::zero(), + callback, + input: NumberInputSlider::new(min, max, init_value).into_child(), + cancel_button: Button::with_text("CANCEL") + .styled(theme::button_cancel()) + .into_child(), + confirm_button: Button::with_text("CONFIRM") + .styled(theme::button_confirm()) + .into_child(), + } + } + + pub fn value(&self) -> u32 { + self.input.inner().value + } +} + +impl Component for NumberInputSliderDialog +where + F: Fn(u32), +{ + type Msg = NumberInputSliderDialogMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + let button_height = theme::BUTTON_HEIGHT; + let content_area = self.area.inset(Insets::top(2 * theme::BUTTON_SPACING)); + let (_, content_area) = content_area.split_top(30); + let (input_area, _) = content_area.split_top(15); + let (_, button_area) = content_area.split_bottom(button_height); + // let (content_area, button_area) = content_area.split_bottom(button_height); + // let content_area = content_area.inset(Insets::new( + // theme::BUTTON_SPACING, + // 0, + // theme::BUTTON_SPACING, + // theme::CONTENT_BORDER, + // )); + + let grid = Grid::new(button_area, 1, 2).with_spacing(theme::KEYBOARD_SPACING); + self.input.place(input_area.inset(Insets::sides(20))); + self.cancel_button.place(grid.row_col(0, 0)); + self.confirm_button.place(grid.row_col(0, 1)); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Some(NumberInputSliderMsg::Changed(i)) = self.input.event(ctx, event) { + (self.callback)(i); + } + if let Some(ButtonMsg::Clicked) = self.cancel_button.event(ctx, event) { + return Some(Self::Msg::Cancelled); + } + if let Some(ButtonMsg::Clicked) = self.confirm_button.event(ctx, event) { + return Some(Self::Msg::Confirmed); + }; + None + } + + fn paint(&mut self) { + self.input.paint(); + // self.paragraphs_pad.paint(); + // self.paragraphs.paint(); + self.cancel_button.paint(); + self.confirm_button.paint(); + } + + #[cfg(feature = "ui_bounds")] + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area); + self.input.bounds(sink); + // self.paragraphs.bounds(sink); + self.cancel_button.bounds(sink); + self.confirm_button.bounds(sink); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for NumberInputSliderDialog +where + F: Fn(u32), +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("NumberInputSliderDialog"); + t.child("input", &self.input); + t.child("cancel_button", &self.cancel_button); + t.child("confirm_button", &self.confirm_button); + } +} + +pub enum NumberInputSliderMsg { + Changed(u32), +} + +pub struct NumberInputSlider { + area: Rect, + touch_area: Rect, + min: u32, + max: u32, + value: u32, +} + +impl NumberInputSlider { + pub fn new(min: u32, max: u32, value: u32) -> Self { + let value = value.clamp(min, max); + Self { + area: Rect::zero(), + touch_area: Rect::zero(), + min, + max, + value, + } + } + + pub fn slider_eval(&mut self, pos: Point, ctx: &mut EventCtx) -> Option { + if self.touch_area.contains(pos) { + let filled = pos.x - self.area.x0; + let filled = filled.clamp(0, self.area.width()); + let val_pct = (filled as u32 * 100) / self.area.width() as u32; + let val = (val_pct * (self.max - self.min)) / 100 + self.min; + + if val != self.value { + self.value = val; + ctx.request_paint(); + return Some(NumberInputSliderMsg::Changed(self.value)); + } + } + None + } +} + +impl Component for NumberInputSlider { + type Msg = NumberInputSliderMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + self.touch_area = bounds.outset(Insets::new(40, 20, 40, 20)).clamp(screen()); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Event::Touch(touch_event) = event { + return match touch_event { + TouchEvent::TouchStart(pos) => self.slider_eval(pos, ctx), + TouchEvent::TouchMove(pos) => self.slider_eval(pos, ctx), + TouchEvent::TouchEnd(pos) => self.slider_eval(pos, ctx), + }; + } + None + } + + fn paint(&mut self) { + let val_pct = (100 * (self.value - self.min)) / (self.max - self.min); + let fill_to = (val_pct as i16 * self.area.width()) / 100; + + display::bar_with_text_and_fill::<&str>( + self.area, + None, + theme::FG, + theme::BG, + 0, + fill_to as _, + ); + } + + #[cfg(feature = "ui_bounds")] + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area) + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for NumberInputSlider { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("NumberInput"); + t.int("value", self.value as i64); + } +} diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index 2bcb17728..dcca147e7 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -44,7 +44,7 @@ use super::{ CancelConfirmMsg, CancelInfoConfirmMsg, CoinJoinProgress, Dialog, DialogMsg, FidoConfirm, FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg, - PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress, + NumberInputSliderDialog, NumberInputSliderDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress, SelectWordCount, SelectWordCountMsg, SelectWordMsg, SimplePage, Slip39Input, }, theme, @@ -245,6 +245,19 @@ where } } +impl ComponentMsgObj for NumberInputSliderDialog +where + F: Fn(u32), +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + let value = self.value().try_into()?; + match msg { + NumberInputSliderDialogMsg::Confirmed => Ok((CONFIRMED.as_obj(), value).try_into()?), + NumberInputSliderDialogMsg::Cancelled => Ok((CANCELLED.as_obj(), value).try_into()?), + } + } +} + impl ComponentMsgObj for Border where T: ComponentMsgObj, @@ -1341,6 +1354,31 @@ extern "C" fn new_request_number(n_args: usize, args: *const Obj, kwargs: *mut M unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_request_number_slider(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let min_count: u32 = kwargs.get(Qstr::MP_QSTR_min_count)?.try_into()?; + let max_count: u32 = kwargs.get(Qstr::MP_QSTR_max_count)?.try_into()?; + let count: u32 = kwargs.get(Qstr::MP_QSTR_count)?.try_into()?; + let value_callback: Obj = kwargs.get(Qstr::MP_QSTR_callback)?; + assert!(value_callback != Obj::const_none()); + + let callback = move |i: u32| { + value_callback + .call_with_n_args(&[i.try_into().unwrap()]) + .unwrap(); + }; + + let obj = LayoutObj::new(Frame::centered( + theme::label_title(), + title, + NumberInputSliderDialog::new(min_count, max_count, count, callback), + ))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; @@ -2036,6 +2074,18 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Number input with + and - buttons, description, and info button.""" Qstr::MP_QSTR_request_number => obj_fn_kw!(0, new_request_number).as_obj(), + + /// def request_number_slider( + /// *, + /// title: str, + /// count: int, + /// min_count: int, + /// max_count: int, + /// callback: Callable[[int], None] | None = None, + /// ) -> object: + /// """Number input with slider.""" + Qstr::MP_QSTR_request_number_slider => obj_fn_kw!(0, new_request_number_slider).as_obj(), + /// def show_checklist( /// *, /// title: str, diff --git a/core/embed/trezorhal/stm32f4/backlight_pwm.c b/core/embed/trezorhal/stm32f4/backlight_pwm.c index e8dcff1d2..a7387cf5a 100644 --- a/core/embed/trezorhal/stm32f4/backlight_pwm.c +++ b/core/embed/trezorhal/stm32f4/backlight_pwm.c @@ -4,9 +4,11 @@ #include STM32_HAL_H #include TREZOR_BOARD -#define TIM_FREQ 1000000 +#define TIM_FREQ 10000000 -#define LED_PWM_PRESCALER (SystemCoreClock / TIM_FREQ - 1) // 1 MHz +#define LED_PWM_PRESCALER (SystemCoreClock / TIM_FREQ - 1) + +#define LED_PWM_PRESCALER_SLOW (SystemCoreClock / 1000000 - 1) // 1 MHz #define LED_PWM_TIM_PERIOD (TIM_FREQ / BACKLIGHT_PWM_FREQ) @@ -16,6 +18,35 @@ static int pwm_period = 0; int backlight_pwm_set(int val) { if (BACKLIGHT != val && val >= 0 && val <= 255) { + // TPS61043: min 1% duty cycle + if (val < BACKLIGHT_PWM_TIM->ARR / 100) { + val = 0; + } + + // TPS61043 goes to shutdown when duty cycle is 0 (after 32ms), + // so we need to set GPIO to high for at least 500us + // to wake it up. + if (BACKLIGHT_PWM_TIM->BACKLIGHT_PWM_TIM_CCR == 0) { + GPIO_InitTypeDef GPIO_InitStructure = {0}; + + HAL_GPIO_WritePin(BACKLIGHT_PWM_PORT, BACKLIGHT_PWM_PIN, GPIO_PIN_SET); + // LCD_PWM/PA7 (backlight control) + GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; + GPIO_InitStructure.Pull = GPIO_NOPULL; + GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + GPIO_InitStructure.Pin = BACKLIGHT_PWM_PIN; + HAL_GPIO_Init(BACKLIGHT_PWM_PORT, &GPIO_InitStructure); + + hal_delay_us(500); + + GPIO_InitStructure.Mode = GPIO_MODE_AF_PP; + GPIO_InitStructure.Pull = GPIO_NOPULL; + GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + GPIO_InitStructure.Alternate = BACKLIGHT_PWM_TIM_AF; + GPIO_InitStructure.Pin = BACKLIGHT_PWM_PIN; + HAL_GPIO_Init(BACKLIGHT_PWM_PORT, &GPIO_InitStructure); + } + BACKLIGHT = val; BACKLIGHT_PWM_TIM->CCR1 = (pwm_period * val) / 255; } @@ -157,6 +188,7 @@ void backlight_pwm_reinit(void) { BACKLIGHT = prev_val; pwm_period = LED_PWM_TIM_PERIOD; + BACKLIGHT_PWM_TIM->PSC = LED_PWM_PRESCALER; BACKLIGHT_PWM_TIM->CR1 |= TIM_CR1_ARPE; BACKLIGHT_PWM_TIM->CR2 |= TIM_CR2_CCPC; BACKLIGHT_PWM_TIM->BACKLIGHT_PWM_TIM_CCR = (pwm_period * prev_val) / 255; @@ -176,6 +208,7 @@ void backlight_pwm_set_slow(void) { prev_val = prev_val > 255 ? 255 : prev_val; pwm_period = LED_PWM_SLOW_TIM_PERIOD; + BACKLIGHT_PWM_TIM->PSC = LED_PWM_PRESCALER_SLOW; BACKLIGHT_PWM_TIM->CR1 |= TIM_CR1_ARPE; BACKLIGHT_PWM_TIM->CR2 |= TIM_CR2_CCPC; BACKLIGHT_PWM_TIM->ARR = LED_PWM_SLOW_TIM_PERIOD - 1; diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 00e6279a9..38569a514 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -866,6 +866,18 @@ def request_number( """Number input with + and - buttons, description, and info button.""" +# rust/src/ui/model_tt/layout.rs +def request_number_slider( + *, + title: str, + count: int, + min_count: int, + max_count: int, + callback: Callable[[int], None] | None = None, +) -> object: + """Number input with slider.""" + + # rust/src/ui/model_tt/layout.rs def show_checklist( *, diff --git a/core/src/apps/management/apply_settings.py b/core/src/apps/management/apply_settings.py index 684beb35f..4070920f6 100644 --- a/core/src/apps/management/apply_settings.py +++ b/core/src/apps/management/apply_settings.py @@ -3,8 +3,10 @@ from typing import TYPE_CHECKING import storage.device as storage_device import trezorui2 from trezor import TR +from trezor import utils from trezor.enums import ButtonRequestType -from trezor.ui.layouts import confirm_action +from trezor.ui import display, style +from trezor.ui.layouts import confirm_action, request_number_slider from trezor.wire import DataError if TYPE_CHECKING: @@ -50,6 +52,7 @@ async def apply_settings(msg: ApplySettings) -> Success: msg_safety_checks = msg.safety_checks # local_cache_attribute experimental_features = msg.experimental_features # local_cache_attribute hide_passphrase_from_host = msg.hide_passphrase_from_host # local_cache_attribute + brightness = msg.brightness if ( homescreen is None @@ -61,6 +64,7 @@ async def apply_settings(msg: ApplySettings) -> Success: and msg_safety_checks is None and experimental_features is None and hide_passphrase_from_host is None + and (brightness is None or not utils.USE_BACKLIGHT) ): raise ProcessError("No setting provided") @@ -114,6 +118,10 @@ async def apply_settings(msg: ApplySettings) -> Success: await _require_confirm_hide_passphrase_from_host(hide_passphrase_from_host) storage_device.set_hide_passphrase_from_host(hide_passphrase_from_host) + if brightness is not None and utils.USE_BACKLIGHT: + new_brightness = await _require_set_brightness() + storage_device.set_brightness(new_brightness) + reload_settings_from_storage() return Success(message="Settings applied") @@ -255,3 +263,19 @@ async def _require_confirm_hide_passphrase_from_host(enable: bool) -> None: description=TR.passphrase__hide, br_code=BRT_PROTECT_CALL, ) + + +if utils.USE_BACKLIGHT: + + async def _require_set_brightness() -> int: + def callback(val: int) -> None: + display.backlight(val) + + return await request_number_slider( + "Set brightness", + callback, + min_count=style.BACKLIGHT_MIN, + max_count=style.BACKLIGHT_MAX, + count=style.get_backlight_normal(), + br_name="set_brightness", + ) diff --git a/core/src/storage/common.py b/core/src/storage/common.py index ba6c4b89e..a9f3364af 100644 --- a/core/src/storage/common.py +++ b/core/src/storage/common.py @@ -49,12 +49,12 @@ def get_bool(app: int, key: int, public: bool = False) -> bool: return get(app, key, public) == _TRUE_BYTE -def set_uint8(app: int, key: int, val: int) -> None: - set(app, key, val.to_bytes(1, "big")) +def set_uint8(app: int, key: int, val: int, public: bool = False) -> None: + set(app, key, val.to_bytes(1, "big"), public) -def get_uint8(app: int, key: int) -> int | None: - val = get(app, key) +def get_uint8(app: int, key: int, public: bool = False) -> int | None: + val = get(app, key, public) if not val: return None return int.from_bytes(val, "big") diff --git a/core/src/storage/device.py b/core/src/storage/device.py index cf6ba0e92..5e52d2e28 100644 --- a/core/src/storage/device.py +++ b/core/src/storage/device.py @@ -35,6 +35,7 @@ INITIALIZED = const(0x13) # bool (0x01 or empty) _SAFETY_CHECK_LEVEL = const(0x14) # int _EXPERIMENTAL_FEATURES = const(0x15) # bool (0x01 or empty) _HIDE_PASSPHRASE_FROM_HOST = const(0x16) # bool (0x01 or empty) +_BRIGHTNESS = const(0x17) # int SAFETY_CHECK_LEVEL_STRICT : Literal[0] = const(0) SAFETY_CHECK_LEVEL_PROMPT : Literal[1] = const(1) @@ -344,3 +345,17 @@ def get_hide_passphrase_from_host() -> bool: Whether we should hide the passphrase from the host. """ return common.get_bool(_NAMESPACE, _HIDE_PASSPHRASE_FROM_HOST) + + +def set_brightness(brightness: int) -> None: + """ + Set the display brightness setting. + """ + common.set_uint8(_NAMESPACE, _BRIGHTNESS, brightness, True) + + +def get_brightness() -> int | None: + """ + Get the display brightness setting. + """ + return common.get_uint8(_NAMESPACE, _BRIGHTNESS, True) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 1ca0b322e..1d978592d 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -2231,6 +2231,7 @@ if TYPE_CHECKING: safety_checks: "SafetyCheckLevel | None" experimental_features: "bool | None" hide_passphrase_from_host: "bool | None" + brightness: "int | None" def __init__( self, @@ -2244,6 +2245,7 @@ if TYPE_CHECKING: safety_checks: "SafetyCheckLevel | None" = None, experimental_features: "bool | None" = None, hide_passphrase_from_host: "bool | None" = None, + brightness: "int | None" = None, ) -> None: pass diff --git a/core/src/trezor/ui/__init__.py b/core/src/trezor/ui/__init__.py index 109ed105b..23aa5fbf8 100644 --- a/core/src/trezor/ui/__init__.py +++ b/core/src/trezor/ui/__init__.py @@ -67,7 +67,7 @@ async def _alert(count: int) -> None: else: display.backlight(style.BACKLIGHT_DIM) await long_sleep - display.backlight(style.BACKLIGHT_NORMAL) + display.backlight(style.get_backlight_normal()) global _alert_in_progress _alert_in_progress = False diff --git a/core/src/trezor/ui/layouts/tt/__init__.py b/core/src/trezor/ui/layouts/tt/__init__.py index 919f9a962..f6454f82e 100644 --- a/core/src/trezor/ui/layouts/tt/__init__.py +++ b/core/src/trezor/ui/layouts/tt/__init__.py @@ -9,7 +9,7 @@ from trezor.wire.context import wait as ctx_wait from ..common import button_request, interact if TYPE_CHECKING: - from typing import Any, Awaitable, Iterable, NoReturn, Sequence, TypeVar + from typing import Any, Awaitable, Callable, Iterable, NoReturn, Sequence, TypeVar from ..common import ExceptionType, PropertyType @@ -36,13 +36,13 @@ if __debug__: class RustLayout(LayoutParentType[T]): - BACKLIGHT_LEVEL = ui.style.BACKLIGHT_NORMAL # pylint: disable=super-init-not-called def __init__(self, layout: trezorui2.LayoutObj[T]): self.layout = layout self.timer = loop.Timer() self.layout.attach_timer_fn(self.set_timer) + self.backlight_level = ui.style.get_backlight_normal() def set_timer(self, token: int, deadline: int) -> None: self.timer.schedule(deadline, token) @@ -191,7 +191,7 @@ class RustLayout(LayoutParentType[T]): notify_layout_change(self, event_id) # Turn the brightness on again. - ui.backlight_fade(self.BACKLIGHT_LEVEL) + ui.backlight_fade(self.backlight_level) def handle_input_and_rendering(self) -> loop.Task: # type: ignore [awaitable-is-generator] from trezor import workflow @@ -231,7 +231,7 @@ def draw_simple(layout: trezorui2.LayoutObj[Any]) -> None: ui.backlight_fade(ui.style.BACKLIGHT_DIM) layout.paint() ui.refresh() - ui.backlight_fade(ui.style.BACKLIGHT_NORMAL) + ui.backlight_fade(ui.style.get_backlight_normal()) async def raise_if_not_confirmed( @@ -1525,3 +1525,40 @@ def confirm_firmware_update(description: str, fingerprint: str) -> Awaitable[Non BR_TYPE_OTHER, ) ) + + +async def request_number_slider( + title: str, + callback: Callable[[int], None], + count: int, + min_count: int, + max_count: int, + br_name: str, +) -> int: + num_input = RustLayout( + trezorui2.request_number_slider( + title=title.upper(), + callback=callback, + count=count, + min_count=min_count, + max_count=max_count, + ) + ) + while True: + result = await interact( + num_input, + br_name, + BR_TYPE_OTHER, + ) + # if __debug__: + # if not isinstance(result, tuple): + # # DebugLink currently can't send number of shares and it doesn't + # # change the counter either so just use the initial value. + # result = (result, count) + status, value = result + + if status == CONFIRMED: + assert isinstance(value, int) + return value + + raise ActionCancelled diff --git a/core/src/trezor/ui/layouts/tt/homescreen.py b/core/src/trezor/ui/layouts/tt/homescreen.py index c59abf1f1..e6da99996 100644 --- a/core/src/trezor/ui/layouts/tt/homescreen.py +++ b/core/src/trezor/ui/layouts/tt/homescreen.py @@ -85,7 +85,6 @@ class Homescreen(HomescreenBase): class Lockscreen(HomescreenBase): RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON - BACKLIGHT_LEVEL = ui.style.BACKLIGHT_LOW def __init__( self, @@ -94,8 +93,9 @@ class Lockscreen(HomescreenBase): coinjoin_authorized: bool = False, ) -> None: self.bootscreen = bootscreen + self.backlight_level = ui.style.get_backlight_low() if bootscreen: - self.BACKLIGHT_LEVEL = ui.style.BACKLIGHT_NORMAL + self.backlight_level = ui.style.get_backlight_normal() skip = ( not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR diff --git a/core/src/trezor/ui/layouts/tt/progress.py b/core/src/trezor/ui/layouts/tt/progress.py new file mode 100644 index 000000000..e69de29bb diff --git a/core/src/trezor/ui/style.py b/core/src/trezor/ui/style.py index 70b97d6ee..f8f32caca 100644 --- a/core/src/trezor/ui/style.py +++ b/core/src/trezor/ui/style.py @@ -1,8 +1,25 @@ from micropython import const +import storage.device + # backlight brightness BACKLIGHT_NORMAL = const(150) BACKLIGHT_LOW = const(45) BACKLIGHT_DIM = const(5) BACKLIGHT_NONE = const(0) +BACKLIGHT_MIN = const(10) BACKLIGHT_MAX = const(255) + + +def get_backlight_normal() -> int: + val = storage.device.get_brightness() + if val is None: + return BACKLIGHT_NORMAL + return val + + +def get_backlight_low() -> int: + val = storage.device.get_brightness() + if val is None or val > BACKLIGHT_LOW: + return BACKLIGHT_LOW + return val diff --git a/python/src/trezorlib/cli/settings.py b/python/src/trezorlib/cli/settings.py index 19231f3a7..0619497e6 100644 --- a/python/src/trezorlib/cli/settings.py +++ b/python/src/trezorlib/cli/settings.py @@ -206,6 +206,13 @@ def label(client: "TrezorClient", label: str) -> str: return device.apply_settings(client, label=label) +@cli.command() +@with_client +def brightness(client: "TrezorClient") -> str: + """Set display brightness.""" + return device.apply_settings(client, brightness=0) + + @cli.command() @click.argument("path_or_url", required=False) @click.option( diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index dbea87e80..48b875782 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -47,6 +47,7 @@ def apply_settings( safety_checks: Optional[messages.SafetyCheckLevel] = None, experimental_features: Optional[bool] = None, hide_passphrase_from_host: Optional[bool] = None, + brightness: Optional[int] = None, ) -> "MessageType": if language is not None: warnings.warn( @@ -63,6 +64,7 @@ def apply_settings( safety_checks=safety_checks, experimental_features=experimental_features, hide_passphrase_from_host=hide_passphrase_from_host, + brightness=brightness, ) out = client.call(settings) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index a8ae02096..b0b079471 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -3358,6 +3358,7 @@ class ApplySettings(protobuf.MessageType): 9: protobuf.Field("safety_checks", "SafetyCheckLevel", repeated=False, required=False, default=None), 10: protobuf.Field("experimental_features", "bool", repeated=False, required=False, default=None), 11: protobuf.Field("hide_passphrase_from_host", "bool", repeated=False, required=False, default=None), + 12: protobuf.Field("brightness", "uint32", repeated=False, required=False, default=None), } def __init__( @@ -3374,6 +3375,7 @@ class ApplySettings(protobuf.MessageType): safety_checks: Optional["SafetyCheckLevel"] = None, experimental_features: Optional["bool"] = None, hide_passphrase_from_host: Optional["bool"] = None, + brightness: Optional["int"] = None, ) -> None: self.language = language self.label = label @@ -3386,6 +3388,7 @@ class ApplySettings(protobuf.MessageType): self.safety_checks = safety_checks self.experimental_features = experimental_features self.hide_passphrase_from_host = hide_passphrase_from_host + self.brightness = brightness class ChangeLanguage(protobuf.MessageType): diff --git a/rust/trezor-client/src/protos/generated/messages_management.rs b/rust/trezor-client/src/protos/generated/messages_management.rs index c163b5ea7..33029c894 100644 --- a/rust/trezor-client/src/protos/generated/messages_management.rs +++ b/rust/trezor-client/src/protos/generated/messages_management.rs @@ -2956,6 +2956,8 @@ pub struct ApplySettings { pub experimental_features: ::std::option::Option, // @@protoc_insertion_point(field:hw.trezor.messages.management.ApplySettings.hide_passphrase_from_host) pub hide_passphrase_from_host: ::std::option::Option, + // @@protoc_insertion_point(field:hw.trezor.messages.management.ApplySettings.brightness) + pub brightness: ::std::option::Option, // special fields // @@protoc_insertion_point(special_field:hw.trezor.messages.management.ApplySettings.special_fields) pub special_fields: ::protobuf::SpecialFields, @@ -3235,8 +3237,27 @@ impl ApplySettings { self.hide_passphrase_from_host = ::std::option::Option::Some(v); } + // optional uint32 brightness = 12; + + pub fn brightness(&self) -> u32 { + self.brightness.unwrap_or(0) + } + + pub fn clear_brightness(&mut self) { + self.brightness = ::std::option::Option::None; + } + + pub fn has_brightness(&self) -> bool { + self.brightness.is_some() + } + + // Param is passed by value, moved + pub fn set_brightness(&mut self, v: u32) { + self.brightness = ::std::option::Option::Some(v); + } + fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(11); + let mut fields = ::std::vec::Vec::with_capacity(12); let mut oneofs = ::std::vec::Vec::with_capacity(0); fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( "language", @@ -3293,6 +3314,11 @@ impl ApplySettings { |m: &ApplySettings| { &m.hide_passphrase_from_host }, |m: &mut ApplySettings| { &mut m.hide_passphrase_from_host }, )); + fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( + "brightness", + |m: &ApplySettings| { &m.brightness }, + |m: &mut ApplySettings| { &mut m.brightness }, + )); ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( "ApplySettings", fields, @@ -3344,6 +3370,9 @@ impl ::protobuf::Message for ApplySettings { 88 => { self.hide_passphrase_from_host = ::std::option::Option::Some(is.read_bool()?); }, + 96 => { + self.brightness = ::std::option::Option::Some(is.read_uint32()?); + }, tag => { ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; }, @@ -3389,6 +3418,9 @@ impl ::protobuf::Message for ApplySettings { if let Some(v) = self.hide_passphrase_from_host { my_size += 1 + 1; } + if let Some(v) = self.brightness { + my_size += ::protobuf::rt::uint32_size(12, v); + } my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); my_size @@ -3428,6 +3460,9 @@ impl ::protobuf::Message for ApplySettings { if let Some(v) = self.hide_passphrase_from_host { os.write_bool(11, v)?; } + if let Some(v) = self.brightness { + os.write_uint32(12, v)?; + } os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) } @@ -3456,6 +3491,7 @@ impl ::protobuf::Message for ApplySettings { self.safety_checks = ::std::option::Option::None; self.experimental_features = ::std::option::Option::None; self.hide_passphrase_from_host = ::std::option::Option::None; + self.brightness = ::std::option::Option::None; self.special_fields.clear(); } @@ -3472,6 +3508,7 @@ impl ::protobuf::Message for ApplySettings { safety_checks: ::std::option::Option::None, experimental_features: ::std::option::Option::None, hide_passphrase_from_host: ::std::option::Option::None, + brightness: ::std::option::Option::None, special_fields: ::protobuf::SpecialFields::new(), }; &instance