1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-01-06 21:40:56 +00:00

feat(lincoln): initial commit

- build variables, lincoln UI selected for compilation only with
`UI_LINCOLN_DEV=1`
- lincoln directory structure
- bootloader UI and assets copied from mercury
- FirmwareUI trait functions are empty
- Python layout functions are copied from mercury except some of more
complicated ones which raise NotImplemented for now
This commit is contained in:
obrusvit 2024-12-19 16:38:30 +01:00
parent 7eb0b4d689
commit 7f240247a4
62 changed files with 4427 additions and 22 deletions

View File

@ -33,6 +33,7 @@ BOOTLOADER_QA ?= 0
BOOTLOADER_DEVEL ?= 0 BOOTLOADER_DEVEL ?= 0
DISABLE_OPTIGA ?= 0 DISABLE_OPTIGA ?= 0
TREZOR_MODEL ?= T TREZOR_MODEL ?= T
UI_LINCOLN_DEV ?= 0
TREZOR_MEMPERF ?= 0 TREZOR_MEMPERF ?= 0
ADDRESS_SANITIZER ?= 0 ADDRESS_SANITIZER ?= 0
CMAKELISTS ?= 0 CMAKELISTS ?= 0
@ -69,7 +70,12 @@ MODEL_FEATURE = model_tr
else ifeq ($(TREZOR_MODEL),$(filter $(TREZOR_MODEL),T3W1)) else ifeq ($(TREZOR_MODEL),$(filter $(TREZOR_MODEL),T3W1))
MCU = STM32U5 MCU = STM32U5
OPENOCD_TARGET = target/stm32u5x.cfg OPENOCD_TARGET = target/stm32u5x.cfg
# model_tt by default so far, lincoln if requested
ifeq ($(UI_LINCOLN_DEV),1)
MODEL_FEATURE = model_lincoln
else
MODEL_FEATURE = model_tt MODEL_FEATURE = model_tt
endif
else ifeq ($(TREZOR_MODEL),$(filter $(TREZOR_MODEL),DISC1)) else ifeq ($(TREZOR_MODEL),$(filter $(TREZOR_MODEL),DISC1))
MCU = STM32F4 MCU = STM32F4
OPENOCD_TARGET = target/stm32f4x.cfg OPENOCD_TARGET = target/stm32f4x.cfg
@ -151,7 +157,8 @@ SCONS_VARS = \
TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" \ TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" \
TREZOR_EMULATOR_DEBUGGABLE=$(TREZOR_EMULATOR_DEBUGGABLE) \ TREZOR_EMULATOR_DEBUGGABLE=$(TREZOR_EMULATOR_DEBUGGABLE) \
TREZOR_MEMPERF="$(TREZOR_MEMPERF)" \ TREZOR_MEMPERF="$(TREZOR_MEMPERF)" \
TREZOR_MODEL="$(TREZOR_MODEL)" TREZOR_MODEL="$(TREZOR_MODEL)" \
UI_LINCOLN_DEV="$(UI_LINCOLN_DEV)"
SCONS_OPTS = -Q -j $(JOBS) SCONS_OPTS = -Q -j $(JOBS)
ifeq ($(QUIET_MODE),1) ifeq ($(QUIET_MODE),1)

View File

@ -11,6 +11,7 @@ crypto = ["zeroize"]
model_tt = ["jpeg"] model_tt = ["jpeg"]
model_tr = [] model_tr = []
model_mercury = ["jpeg", "dma2d"] model_mercury = ["jpeg", "dma2d"]
model_lincoln = ["jpeg", "dma2d"]
micropython = [] micropython = []
protobuf = ["micropython"] protobuf = ["micropython"]
ui = [] ui = []

View File

@ -86,12 +86,24 @@ const DEFAULT_BINDGEN_MACROS_T3T1: &[&str] = &[
#[cfg(not(feature = "model_mercury"))] #[cfg(not(feature = "model_mercury"))]
const DEFAULT_BINDGEN_MACROS_T3T1: &[&str] = &[]; const DEFAULT_BINDGEN_MACROS_T3T1: &[&str] = &[];
#[cfg(feature = "model_lincoln")]
const DEFAULT_BINDGEN_MACROS_T3W1: &[&str] = &[
"-DSTM32U5",
"-DTREZOR_MODEL_T3W1",
"-DFLASH_BIT_ACCESS=0", // FIXME: fill in correct value
"-DFLASH_BLOCK_WORDS=4",
"-DTREZOR_BOARD=\"T3W1/boards/t3w1-unix.h\"",
];
#[cfg(not(feature = "model_lincoln"))]
const DEFAULT_BINDGEN_MACROS_T3W1: &[&str] = &[];
fn add_bindgen_macros<'a>(clang_args: &mut Vec<&'a str>, envvar: Option<&'a str>) { fn add_bindgen_macros<'a>(clang_args: &mut Vec<&'a str>, envvar: Option<&'a str>) {
let default_macros = DEFAULT_BINDGEN_MACROS_COMMON let default_macros = DEFAULT_BINDGEN_MACROS_COMMON
.iter() .iter()
.chain(DEFAULT_BINDGEN_MACROS_T2T1) .chain(DEFAULT_BINDGEN_MACROS_T2T1)
.chain(DEFAULT_BINDGEN_MACROS_T2B1) .chain(DEFAULT_BINDGEN_MACROS_T2B1)
.chain(DEFAULT_BINDGEN_MACROS_T3T1); .chain(DEFAULT_BINDGEN_MACROS_T3T1)
.chain(DEFAULT_BINDGEN_MACROS_T3W1);
match envvar { match envvar {
Some(envvar) => clang_args.extend(envvar.split(',')), Some(envvar) => clang_args.extend(envvar.split(',')),

View File

@ -1,10 +1,14 @@
//! Reexporting the `constant` module according to the //! Reexporting the `constant` module according to the
//! current feature (Trezor model) //! current feature (Trezor model)
#[cfg(all(
feature = "model_lincoln",
not(any(feature = "model_mercury", feature = "model_tr", feature = "model_tt"))
))]
pub use super::model_lincoln::constant::*;
#[cfg(all( #[cfg(all(
feature = "model_mercury", feature = "model_mercury",
not(feature = "model_tr"), not(any(feature = "model_tr", feature = "model_tt"),)
not(feature = "model_tt")
))] ))]
pub use super::model_mercury::constant::*; pub use super::model_mercury::constant::*;
#[cfg(all(feature = "model_tr", not(feature = "model_tt")))] #[cfg(all(feature = "model_tr", not(feature = "model_tt")))]

View File

@ -30,6 +30,10 @@ const fn clamp(x: i16, min: i16, max: i16) -> i16 {
/// Relative offset in 2D space, used for representing translation and /// Relative offset in 2D space, used for representing translation and
/// dimensions of objects. Absolute positions on the screen are represented by /// dimensions of objects. Absolute positions on the screen are represented by
/// the `Point` type. /// the `Point` type.
///
/// Coordinate system orientation:
/// * x-axis: negative values go left, positive values go right
/// * y-axis: negative values go up, positive values go down
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Offset { pub struct Offset {
pub x: i16, pub x: i16,

View File

@ -17,6 +17,8 @@ pub mod layout;
mod api; mod api;
#[cfg(feature = "model_lincoln")]
pub mod model_lincoln;
#[cfg(feature = "model_mercury")] #[cfg(feature = "model_mercury")]
pub mod model_mercury; pub mod model_mercury;
#[cfg(feature = "model_tr")] #[cfg(feature = "model_tr")]
@ -32,10 +34,15 @@ pub mod ui_firmware;
pub use ui_common::CommonUI; pub use ui_common::CommonUI;
#[cfg(all(
feature = "model_lincoln",
not(any(feature = "model_mercury", feature = "model_tr", feature = "model_tt"))
))]
pub type ModelUI = crate::ui::model_lincoln::UILincoln;
#[cfg(all( #[cfg(all(
feature = "model_mercury", feature = "model_mercury",
not(feature = "model_tr"), not(any(feature = "model_tr", feature = "model_tt"))
not(feature = "model_tt")
))] ))]
pub type ModelUI = crate::ui::model_mercury::UIMercury; pub type ModelUI = crate::ui::model_mercury::UIMercury;

View File

@ -0,0 +1,113 @@
use crate::{
strutil::TString,
ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
constant::screen,
display::Icon,
geometry::{Alignment, Insets, Point, Rect},
shape::Renderer,
},
};
use super::super::{
component::{Button, ButtonMsg::Clicked},
constant::WIDTH,
theme::bootloader::{
button_bld, button_bld_menu, text_title, BLD_BG, BUTTON_AREA_START, BUTTON_HEIGHT,
CONTENT_PADDING, CORNER_BUTTON_AREA, MENU32, TEXT_NORMAL, TEXT_WARNING, TITLE_AREA,
},
};
#[repr(u32)]
#[derive(Copy, Clone, ToPrimitive)]
pub enum IntroMsg {
Menu = 1,
Host = 2,
}
pub struct Intro<'a> {
bg: Pad,
title: Child<Label<'a>>,
menu: Child<Button>,
host: Child<Button>,
text: Child<Label<'a>>,
warn: Option<Child<Label<'a>>>,
}
impl<'a> Intro<'a> {
pub fn new(title: TString<'a>, content: TString<'a>, fw_ok: bool) -> Self {
Self {
bg: Pad::with_background(BLD_BG).with_clear(),
title: Child::new(Label::left_aligned(title, text_title(BLD_BG)).vertically_centered()),
menu: Child::new(
Button::with_icon(Icon::new(MENU32))
.styled(button_bld_menu())
.with_expanded_touch_area(Insets::uniform(13)),
),
host: Child::new(
Button::with_text("INSTALL FIRMWARE".into())
.styled(button_bld())
.with_text_align(Alignment::Center),
),
text: Child::new(Label::left_aligned(content, TEXT_NORMAL).vertically_centered()),
warn: (!fw_ok).then_some(Child::new(
Label::new("FIRMWARE CORRUPTED".into(), Alignment::Start, TEXT_WARNING)
.vertically_centered(),
)),
}
}
}
impl<'a> Component for Intro<'a> {
type Msg = IntroMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.bg.place(screen());
self.title.place(TITLE_AREA);
self.menu.place(CORNER_BUTTON_AREA);
self.host.place(Rect::new(
Point::new(CONTENT_PADDING, BUTTON_AREA_START),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START + BUTTON_HEIGHT),
));
if self.warn.is_some() {
self.warn.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING),
Point::new(
WIDTH - CONTENT_PADDING,
TITLE_AREA.y1 + CONTENT_PADDING + 30,
),
));
self.text.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING + 30),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START - CONTENT_PADDING),
));
} else {
self.text.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START - CONTENT_PADDING),
));
}
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(Clicked) = self.menu.event(ctx, event) {
return Some(Self::Msg::Menu);
};
if let Some(Clicked) = self.host.event(ctx, event) {
return Some(Self::Msg::Host);
};
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.title.render(target);
self.text.render(target);
self.warn.render(target);
self.host.render(target);
self.menu.render(target);
}
}

View File

@ -0,0 +1,111 @@
use crate::{
trezorhal::secbool::{secbool, sectrue},
ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
constant::{screen, WIDTH},
display::Icon,
geometry::{Insets, Point, Rect},
shape::Renderer,
},
};
use super::super::{
component::{Button, ButtonMsg::Clicked, IconText},
theme::bootloader::{
button_bld, button_bld_menu, text_title, BLD_BG, BUTTON_HEIGHT, CONTENT_PADDING,
CORNER_BUTTON_AREA, CORNER_BUTTON_TOUCH_EXPANSION, FIRE24, REFRESH24, TITLE_AREA, X32,
},
};
const BUTTON_AREA_START: i16 = 56;
const BUTTON_SPACING: i16 = 8;
#[repr(u32)]
#[derive(Copy, Clone, ToPrimitive)]
pub enum MenuMsg {
Close = 0xAABBCCDD,
Reboot = 0x11223344,
FactoryReset = 0x55667788,
}
pub struct Menu {
bg: Pad,
title: Child<Label<'static>>,
close: Child<Button>,
reboot: Child<Button>,
reset: Child<Button>,
}
impl Menu {
pub fn new(firmware_present: secbool) -> Self {
let content_reboot = IconText::new("REBOOT TREZOR", Icon::new(REFRESH24));
let content_reset = IconText::new("FACTORY RESET", Icon::new(FIRE24));
let mut instance = Self {
bg: Pad::with_background(BLD_BG),
title: Child::new(
Label::left_aligned("BOOTLOADER".into(), text_title(BLD_BG)).vertically_centered(),
),
close: Child::new(
Button::with_icon(Icon::new(X32))
.styled(button_bld_menu())
.with_expanded_touch_area(Insets::uniform(CORNER_BUTTON_TOUCH_EXPANSION)),
),
reboot: Child::new(
Button::with_icon_and_text(content_reboot)
.styled(button_bld())
.initially_enabled(sectrue == firmware_present),
),
reset: Child::new(Button::with_icon_and_text(content_reset).styled(button_bld())),
};
instance.bg.clear();
instance
}
}
impl Component for Menu {
type Msg = MenuMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.bg.place(screen());
self.title.place(TITLE_AREA);
self.close.place(CORNER_BUTTON_AREA);
self.reboot.place(Rect::new(
Point::new(CONTENT_PADDING, BUTTON_AREA_START),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START + BUTTON_HEIGHT),
));
self.reset.place(Rect::new(
Point::new(
CONTENT_PADDING,
BUTTON_AREA_START + BUTTON_HEIGHT + BUTTON_SPACING,
),
Point::new(
WIDTH - CONTENT_PADDING,
BUTTON_AREA_START + 2 * BUTTON_HEIGHT + BUTTON_SPACING,
),
));
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(Clicked) = self.close.event(ctx, event) {
return Some(Self::Msg::Close);
}
if let Some(Clicked) = self.reboot.event(ctx, event) {
return Some(Self::Msg::Reboot);
}
if let Some(Clicked) = self.reset.event(ctx, event) {
return Some(Self::Msg::FactoryReset);
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.title.render(target);
self.close.render(target);
self.reboot.render(target);
self.reset.render(target);
}
}

View File

@ -0,0 +1,473 @@
use heapless::String;
use crate::{
trezorhal::secbool::secbool,
ui::{
component::{connect::Connect, Label},
display::{self, Color, Font, Icon},
geometry::{Alignment, Offset, Point, Rect},
layout::simplified::{run, show},
},
};
use super::{
bootloader::welcome::Welcome,
component::{
bl_confirm::{Confirm, ConfirmTitle},
Button, ResultScreen, WelcomeScreen,
},
theme,
theme::{
bootloader::{
button_bld, button_bld_menu, button_confirm, button_wipe_cancel, button_wipe_confirm,
BLD_BG, BLD_FG, BLD_TITLE_COLOR, BLD_WIPE_COLOR, CHECK24, CHECK40, DOWNLOAD24, FIRE32,
FIRE40, RESULT_FW_INSTALL, RESULT_WIPE, TEXT_BOLD, TEXT_NORMAL, TEXT_WIPE_BOLD,
TEXT_WIPE_NORMAL, WARNING40, WELCOME_COLOR, X24,
},
GREEN_LIGHT, GREY,
},
UILincoln,
};
use crate::ui::{ui_bootloader::BootloaderUI, CommonUI};
use crate::ui::{
display::{toif::Toif, LOADER_MAX},
geometry::Alignment2D,
model_mercury::cshape::{render_loader, LoaderRange},
shape,
shape::render_on_display,
};
use ufmt::uwrite;
use super::theme::bootloader::BLD_WARN_COLOR;
use intro::Intro;
use menu::Menu;
pub mod intro;
pub mod menu;
pub mod welcome;
pub type BootloaderString = String<128>;
const RECONNECT_MESSAGE: &str = "PLEASE RECONNECT\nTHE DEVICE";
const SCREEN: Rect = UILincoln::SCREEN;
const PROGRESS_TEXT_ORIGIN: Point = Point::new(2, 28);
impl UILincoln {
fn screen_progress(
text: &str,
progress: u16,
initialize: bool,
fg_color: Color,
bg_color: Color,
icon: Option<(Icon, Color)>,
center_text: Option<&str>,
) {
if initialize {
Self::fadeout();
}
display::sync();
render_on_display(None, Some(bg_color), |target| {
shape::Text::new(PROGRESS_TEXT_ORIGIN, text)
.with_font(Font::NORMAL)
.with_fg(BLD_FG)
.render(target);
let loader_offset: i16 = 19;
let center_text_offset: i16 = 10;
let center = SCREEN.center() + Offset::y(loader_offset);
let inactive_color = bg_color.blend(fg_color, 85);
let end = 360.0 * progress as f32 / 1000.0;
render_loader(
center,
inactive_color,
fg_color,
bg_color,
if progress >= LOADER_MAX {
LoaderRange::Full
} else {
LoaderRange::FromTo(0.0, end)
},
target,
);
if let Some((icon, color)) = icon {
shape::ToifImage::new(center, icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(color)
.render(target);
}
if let Some(center_text) = center_text {
shape::Text::new(
SCREEN.center() + Offset::y(loader_offset + center_text_offset),
center_text,
)
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(GREY)
.render(target);
}
});
display::refresh();
if initialize {
Self::fadein();
}
}
}
impl BootloaderUI for UILincoln {
fn screen_welcome() {
let mut frame = Welcome::new();
show(&mut frame, true);
}
fn screen_install_success(restart_seconds: u8, initial_setup: bool, complete_draw: bool) {
let mut reboot_msg = BootloaderString::new();
let bg_color = if initial_setup { WELCOME_COLOR } else { BLD_BG };
let fg_color = if initial_setup { GREEN_LIGHT } else { BLD_FG };
if restart_seconds >= 1 {
// in practice, restart_seconds is 5 or less so this is fine
let seconds_char = b'0' + restart_seconds % 10;
unwrap!(reboot_msg.push(seconds_char as char));
let progress = (5 - (restart_seconds as u16)).clamp(0, 5) * 200;
Self::screen_progress(
"Restarting device",
progress,
complete_draw,
fg_color,
bg_color,
None,
Some(reboot_msg.as_str()),
);
} else {
Self::screen_progress(
"Firmware installed",
1000,
complete_draw,
fg_color,
bg_color,
Some((Icon::new(CHECK24), BLD_FG)),
None,
);
}
}
fn screen_install_fail() {
let mut frame = ResultScreen::new(
&RESULT_FW_INSTALL,
Icon::new(WARNING40),
"Firmware installation was not successful".into(),
Label::centered(RECONNECT_MESSAGE.into(), RESULT_FW_INSTALL.title_style())
.vertically_centered(),
true,
);
show(&mut frame, true);
}
fn screen_install_confirm(
vendor: &str,
version: &str,
fingerprint: &str,
should_keep_seed: bool,
is_newvendor: bool,
is_newinstall: bool,
version_cmp: i32,
) -> u32 {
let mut version_str: BootloaderString = String::new();
unwrap!(version_str.push_str("Firmware version "));
unwrap!(version_str.push_str(version));
unwrap!(version_str.push_str("\nby "));
unwrap!(version_str.push_str(vendor));
let title_str = if is_newinstall {
"INSTALL FIRMWARE"
} else if is_newvendor {
"CHANGE FW\nVENDOR"
} else if version_cmp > 0 {
"UPDATE FIRMWARE"
} else if version_cmp == 0 {
"REINSTALL FW"
} else {
"DOWNGRADE FW"
};
let title = Label::left_aligned(title_str.into(), TEXT_BOLD).vertically_centered();
let msg = Label::left_aligned(version_str.as_str().into(), TEXT_NORMAL);
let alert = (!should_keep_seed).then_some(Label::left_aligned(
"SEED WILL BE ERASED!".into(),
TEXT_BOLD,
));
let (left, right) = if should_keep_seed {
let l = Button::with_text("CANCEL".into())
.styled(button_bld())
.with_text_align(Alignment::Center);
let r = Button::with_text("INSTALL".into())
.styled(button_confirm())
.with_text_align(Alignment::Center);
(l, r)
} else {
let l = Button::with_icon(Icon::new(X24))
.styled(button_bld())
.with_text_align(Alignment::Center);
let r = Button::with_icon(Icon::new(CHECK24))
.styled(button_confirm())
.with_text_align(Alignment::Center);
(l, r)
};
let mut frame = Confirm::new(BLD_BG, left, right, ConfirmTitle::Text(title), msg)
.with_info(
"FW FINGERPRINT".into(),
fingerprint.into(),
button_bld_menu(),
);
if let Some(alert) = alert {
frame = frame.with_alert(alert);
}
run(&mut frame)
}
fn screen_wipe_confirm() -> u32 {
let icon = Icon::new(FIRE40);
let msg = Label::centered(
"Are you sure you want to factory reset the device?".into(),
TEXT_WIPE_NORMAL,
);
let alert = Label::centered("SEED AND FIRMWARE\nWILL BE ERASED!".into(), TEXT_WIPE_BOLD);
let right = Button::with_text("RESET".into())
.styled(button_wipe_confirm())
.with_text_align(Alignment::Center);
let left = Button::with_text("CANCEL".into())
.styled(button_wipe_cancel())
.with_text_align(Alignment::Center);
let mut frame = Confirm::new(BLD_WIPE_COLOR, left, right, ConfirmTitle::Icon(icon), msg)
.with_alert(alert);
run(&mut frame)
}
fn screen_unlock_bootloader_confirm() -> u32 {
let title =
Label::left_aligned("UNLOCK BOOTLOADER".into(), TEXT_BOLD).vertically_centered();
let msg = Label::centered("This action cannot be undone!".into(), TEXT_NORMAL);
let right = Button::with_text("UNLOCK".into())
.styled(button_confirm())
.with_text_align(Alignment::Center);
let left = Button::with_text("CANCEL".into())
.styled(button_bld())
.with_text_align(Alignment::Center);
let mut frame = Confirm::new(BLD_BG, left, right, ConfirmTitle::Text(title), msg);
run(&mut frame)
}
fn screen_unlock_bootloader_success() {
let mut frame = ResultScreen::new(
&RESULT_FW_INSTALL,
Icon::new(CHECK40),
"Bootloader unlocked".into(),
Label::centered(RECONNECT_MESSAGE.into(), RESULT_FW_INSTALL.title_style())
.vertically_centered(),
true,
);
show(&mut frame, true);
}
fn screen_menu(firmware_present: secbool) -> u32 {
run(&mut Menu::new(firmware_present))
}
fn screen_intro(bld_version: &str, vendor: &str, version: &str, fw_ok: bool) -> u32 {
let mut title_str: BootloaderString = String::new();
unwrap!(title_str.push_str("BOOTLOADER "));
unwrap!(title_str.push_str(bld_version));
let mut version_str: BootloaderString = String::new();
unwrap!(version_str.push_str("Firmware version "));
unwrap!(version_str.push_str(version));
unwrap!(version_str.push_str("\nby "));
unwrap!(version_str.push_str(vendor));
let mut frame = Intro::new(
title_str.as_str().into(),
version_str.as_str().into(),
fw_ok,
);
run(&mut frame)
}
fn screen_boot_stage_1(fading: bool) {
if fading {
Self::fadeout();
}
let mut frame = WelcomeScreen::new();
show(&mut frame, false);
if fading {
Self::fadein();
} else {
display::set_backlight(theme::backlight::get_backlight_normal());
}
}
fn screen_wipe_progress(progress: u16, initialize: bool) {
Self::screen_progress(
"Resetting Trezor",
progress,
initialize,
BLD_FG,
BLD_WIPE_COLOR,
Some((Icon::new(FIRE32), BLD_FG)),
None,
)
}
fn screen_install_progress(progress: u16, initialize: bool, initial_setup: bool) {
let bg_color = if initial_setup { WELCOME_COLOR } else { BLD_BG };
let fg_color = if initial_setup { GREEN_LIGHT } else { BLD_FG };
let icon_color = BLD_FG;
Self::screen_progress(
"Installing firmware",
progress,
initialize,
fg_color,
bg_color,
Some((Icon::new(DOWNLOAD24), icon_color)),
None,
)
}
fn screen_connect(initial_setup: bool) {
let bg = if initial_setup { WELCOME_COLOR } else { BLD_BG };
let mut frame = Connect::new("Waiting for host...", BLD_TITLE_COLOR, bg);
show(&mut frame, true);
}
fn screen_wipe_success() {
let mut frame = ResultScreen::new(
&RESULT_WIPE,
Icon::new(CHECK40),
"Trezor reset\nsuccessfully".into(),
Label::centered(RECONNECT_MESSAGE.into(), RESULT_WIPE.title_style())
.vertically_centered(),
true,
);
show(&mut frame, true);
}
fn screen_wipe_fail() {
let mut frame = ResultScreen::new(
&RESULT_WIPE,
Icon::new(WARNING40),
"Trezor reset was\nnot successful".into(),
Label::centered(RECONNECT_MESSAGE.into(), RESULT_WIPE.title_style())
.vertically_centered(),
true,
);
show(&mut frame, true);
}
fn screen_boot(
warning: bool,
vendor_str: Option<&str>,
version: [u8; 4],
vendor_img: &'static [u8],
wait: i32,
) {
let bg_color = if warning { BLD_WARN_COLOR } else { BLD_BG };
display::sync();
render_on_display(None, Some(bg_color), |target| {
// Draw vendor image if it's valid and has size of 120x120
if let Ok(toif) = Toif::new(vendor_img) {
if (toif.width() == 120) && (toif.height() == 120) {
// Image position depends on the vendor string presence
let pos = if vendor_str.is_some() {
Point::new(SCREEN.width() / 2, 30)
} else {
Point::new(SCREEN.width() / 2, 60)
};
shape::ToifImage::new(pos, toif)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(BLD_FG)
.render(target);
}
}
// Draw vendor string if present
if let Some(text) = vendor_str {
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5 - 50);
shape::Text::new(pos, text)
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(BLD_FG) //COLOR_BL_BG
.render(target);
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5 - 25);
let mut version_text: BootloaderString = String::new();
unwrap!(uwrite!(
version_text,
"{}.{}.{}",
version[0],
version[1],
version[2]
));
shape::Text::new(pos, version_text.as_str())
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(BLD_FG)
.render(target);
}
// Draw a message
match wait.cmp(&0) {
core::cmp::Ordering::Equal => {}
core::cmp::Ordering::Greater => {
let mut text: BootloaderString = String::new();
unwrap!(uwrite!(text, "starting in {} s", wait));
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5);
shape::Text::new(pos, text.as_str())
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(BLD_FG)
.render(target);
}
core::cmp::Ordering::Less => {
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5);
shape::Text::new(pos, "click to continue ...")
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(BLD_FG)
.render(target);
}
}
});
display::refresh();
}
}

View File

@ -0,0 +1,67 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
constant::screen,
display::Font,
geometry::{Offset, Point, Rect},
shape,
shape::Renderer,
};
use super::super::theme::{BLACK, GREY, WHITE};
const TEXT_ORIGIN: Point = Point::new(0, 105);
const STRIDE: i16 = 22;
pub struct Welcome {
bg: Pad,
}
impl Welcome {
pub fn new() -> Self {
Self {
bg: Pad::with_background(BLACK).with_clear(),
}
}
}
impl Component for Welcome {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.bg.place(screen());
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
shape::Text::new(TEXT_ORIGIN, "Get started")
.with_font(Font::NORMAL)
.with_fg(GREY)
.render(target);
shape::Text::new(TEXT_ORIGIN + Offset::y(STRIDE), "with your Trezor")
.with_font(Font::NORMAL)
.with_fg(GREY)
.render(target);
shape::Text::new(TEXT_ORIGIN + Offset::y(2 * STRIDE), "at")
.with_font(Font::NORMAL)
.with_fg(GREY)
.render(target);
let at_width = Font::NORMAL.text_width("at ");
shape::Text::new(
TEXT_ORIGIN + Offset::new(at_width, 2 * STRIDE),
"trezor.io/start",
)
.with_font(Font::NORMAL)
.with_fg(WHITE)
.render(target);
}
}

View File

@ -0,0 +1,470 @@
#[cfg(feature = "haptic")]
use crate::trezorhal::haptic::{play, HapticEffect};
use crate::{
strutil::TString,
time::Duration,
ui::{
component::{Component, Event, EventCtx, Timer},
display::{toif::Icon, Color, Font},
event::TouchEvent,
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
shape::{self, Renderer},
util::split_two_lines,
},
};
use super::theme;
pub enum ButtonMsg {
Pressed,
Released,
Clicked,
LongPressed,
}
pub struct Button {
area: Rect,
touch_expand: Option<Insets>,
content: ButtonContent,
styles: ButtonStyleSheet,
text_align: Alignment,
radius: Option<u8>,
state: State,
long_press: Option<Duration>,
long_timer: Timer,
haptic: bool,
}
impl Button {
pub const BASELINE_OFFSET: Offset = Offset::new(2, 6);
pub const fn new(content: ButtonContent) -> Self {
Self {
content,
area: Rect::zero(),
touch_expand: None,
styles: theme::button_default(),
text_align: Alignment::Start,
radius: None,
state: State::Initial,
long_press: None,
long_timer: Timer::new(),
haptic: true,
}
}
pub const fn with_text(text: TString<'static>) -> Self {
Self::new(ButtonContent::Text(text))
}
pub const fn with_icon(icon: Icon) -> Self {
Self::new(ButtonContent::Icon(icon))
}
pub const fn with_icon_and_text(content: IconText) -> Self {
Self::new(ButtonContent::IconAndText(content))
}
pub const fn empty() -> Self {
Self::new(ButtonContent::Empty)
}
pub const fn styled(mut self, styles: ButtonStyleSheet) -> Self {
self.styles = styles;
self
}
pub const fn with_text_align(mut self, align: Alignment) -> Self {
self.text_align = align;
self
}
pub const fn with_expanded_touch_area(mut self, expand: Insets) -> Self {
self.touch_expand = Some(expand);
self
}
pub fn with_long_press(mut self, duration: Duration) -> Self {
self.long_press = Some(duration);
self
}
pub fn with_radius(mut self, radius: u8) -> Self {
self.radius = Some(radius);
self
}
pub fn without_haptics(mut self) -> Self {
self.haptic = false;
self
}
pub fn enable_if(&mut self, ctx: &mut EventCtx, enabled: bool) {
if enabled {
self.enable(ctx);
} else {
self.disable(ctx);
}
}
pub fn initially_enabled(mut self, enabled: bool) -> Self {
if !enabled {
self.state = State::Disabled;
}
self
}
pub fn enable(&mut self, ctx: &mut EventCtx) {
self.set(ctx, State::Initial)
}
pub fn disable(&mut self, ctx: &mut EventCtx) {
self.set(ctx, State::Disabled)
}
pub fn is_enabled(&self) -> bool {
matches!(
self.state,
State::Initial | State::Pressed | State::Released
)
}
pub fn is_disabled(&self) -> bool {
matches!(self.state, State::Disabled)
}
pub fn set_content(&mut self, content: ButtonContent) {
if self.content != content {
self.content = content
}
}
pub fn content(&self) -> &ButtonContent {
&self.content
}
pub fn set_stylesheet(&mut self, ctx: &mut EventCtx, styles: ButtonStyleSheet) {
if self.styles != styles {
self.styles = styles;
ctx.request_paint();
}
}
pub fn style(&self) -> &ButtonStyle {
match self.state {
State::Initial | State::Released => self.styles.normal,
State::Pressed => self.styles.active,
State::Disabled => self.styles.disabled,
}
}
pub fn area(&self) -> Rect {
self.area
}
fn set(&mut self, ctx: &mut EventCtx, state: State) {
if self.state != state {
self.state = state;
ctx.request_paint();
}
}
pub fn render_background<'s>(
&self,
target: &mut impl Renderer<'s>,
style: &ButtonStyle,
alpha: u8,
) {
if self.radius.is_some() {
shape::Bar::new(self.area)
.with_bg(style.background_color)
.with_radius(self.radius.unwrap() as i16)
.with_thickness(2)
.with_fg(style.button_color)
.with_alpha(alpha)
.render(target);
} else {
shape::Bar::new(self.area)
.with_bg(style.button_color)
.with_fg(style.button_color)
.with_alpha(alpha)
.render(target);
}
}
pub fn render_content<'s>(
&self,
target: &mut impl Renderer<'s>,
style: &ButtonStyle,
alpha: u8,
) {
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => {
let y_offset = Offset::y(self.style().font.allcase_text_height() / 2);
let start_of_baseline = match self.text_align {
Alignment::Start => {
self.area.left_center() + Offset::x(Self::BASELINE_OFFSET.x)
}
Alignment::Center => self.area.center(),
Alignment::End => self.area.right_center() - Offset::x(Self::BASELINE_OFFSET.x),
} + y_offset;
text.map(|text| {
shape::Text::new(start_of_baseline, text)
.with_font(style.font)
.with_fg(style.text_color)
.with_align(self.text_align)
.with_alpha(alpha)
.render(target);
});
}
ButtonContent::Icon(icon) => {
shape::ToifImage::new(self.area.center(), icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(style.icon_color)
.with_alpha(alpha)
.render(target);
}
ButtonContent::IconAndText(child) => {
child.render(
target,
self.area,
self.style(),
Self::BASELINE_OFFSET,
alpha,
);
}
}
}
pub fn render_with_alpha<'s>(&self, target: &mut impl Renderer<'s>, alpha: u8) {
let style = self.style();
self.render_background(target, style, alpha);
self.render_content(target, style, alpha);
}
}
impl Component for Button {
type Msg = ButtonMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let touch_area = if let Some(expand) = self.touch_expand {
self.area.outset(expand)
} else {
self.area
};
match event {
Event::Touch(TouchEvent::TouchStart(pos)) => {
match self.state {
State::Disabled => {
// Do nothing.
}
_ => {
// Touch started in our area, transform to `Pressed` state.
if touch_area.contains(pos) {
#[cfg(feature = "haptic")]
if self.haptic {
play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Pressed);
if let Some(duration) = self.long_press {
self.long_timer.start(ctx, duration);
}
return Some(ButtonMsg::Pressed);
}
}
}
}
Event::Touch(TouchEvent::TouchMove(pos)) => {
match self.state {
State::Pressed if !touch_area.contains(pos) => {
// Touch is leaving our area, transform to `Released` state.
self.set(ctx, State::Released);
return Some(ButtonMsg::Released);
}
_ => {
// Do nothing.
}
}
}
Event::Touch(TouchEvent::TouchEnd(pos)) => {
match self.state {
State::Initial | State::Disabled => {
// Do nothing.
}
State::Pressed if touch_area.contains(pos) => {
// Touch finished in our area, we got clicked.
self.set(ctx, State::Initial);
return Some(ButtonMsg::Clicked);
}
State::Pressed => {
// Touch finished outside our area.
self.set(ctx, State::Initial);
self.long_timer.stop();
return Some(ButtonMsg::Released);
}
_ => {
// Touch finished outside our area.
self.set(ctx, State::Initial);
self.long_timer.stop();
}
}
}
Event::Swipe(_) => {
// When a swipe is detected, abort any ongoing touch.
match self.state {
State::Initial | State::Disabled => {
// Do nothing.
}
State::Pressed => {
// Touch aborted
self.set(ctx, State::Initial);
self.long_timer.stop();
return Some(ButtonMsg::Released);
}
_ => {
// Irrelevant touch abort
self.set(ctx, State::Initial);
self.long_timer.stop();
}
}
}
Event::Timer(_) if self.long_timer.expire(event) => {
if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")]
if self.haptic {
play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
}
}
_ => {}
};
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = self.style();
self.render_background(target, style, 0xFF);
self.render_content(target, style, 0xFF);
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Button {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Button");
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => t.string("text", *text),
ButtonContent::Icon(_) => t.bool("icon", true),
ButtonContent::IconAndText(content) => {
t.string("text", content.text);
t.bool("icon", true);
}
}
}
}
#[derive(PartialEq, Eq, Clone)]
enum State {
Initial,
Pressed,
Released,
Disabled,
}
#[derive(PartialEq, Eq, Clone)]
pub enum ButtonContent {
Empty,
Text(TString<'static>),
Icon(Icon),
IconAndText(IconText),
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct ButtonStyleSheet {
pub normal: &'static ButtonStyle,
pub active: &'static ButtonStyle,
pub disabled: &'static ButtonStyle,
}
#[derive(PartialEq, Eq)]
pub struct ButtonStyle {
pub font: Font,
pub text_color: Color,
pub button_color: Color,
pub icon_color: Color,
pub background_color: Color,
}
#[derive(PartialEq, Eq, Clone)]
pub struct IconText {
text: TString<'static>,
icon: Icon,
}
impl IconText {
const ICON_SPACE: i16 = 46;
const ICON_MARGIN: i16 = 4;
const TEXT_MARGIN: i16 = 6;
pub fn new(text: impl Into<TString<'static>>, icon: Icon) -> Self {
Self {
text: text.into(),
icon,
}
}
pub fn render<'s>(
&self,
target: &mut impl Renderer<'s>,
area: Rect,
style: &ButtonStyle,
baseline_offset: Offset,
alpha: u8,
) {
let mut show_text = |text: &str, rect: Rect| {
let text_pos = rect.left_center() + baseline_offset;
let text_pos = Point::new(rect.top_left().x + Self::ICON_SPACE, text_pos.y);
shape::Text::new(text_pos, text)
.with_font(style.font)
.with_fg(style.text_color)
.with_alpha(alpha)
.render(target)
};
self.text.map(|t| {
let (t1, t2) = split_two_lines(
t,
style.font,
area.width() - Self::ICON_SPACE - Self::TEXT_MARGIN,
);
if t1.is_empty() || t2.is_empty() {
show_text(t, area);
} else {
show_text(t1, Rect::new(area.top_left(), area.right_center()));
show_text(t2, Rect::new(area.left_center(), area.bottom_right()));
}
});
let icon_pos = Point::new(
area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2),
area.center().y,
);
shape::ToifImage::new(icon_pos, self.icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(style.icon_color)
.with_alpha(alpha)
.render(target);
}
}

View File

@ -0,0 +1,94 @@
use crate::{
strutil::TString,
ui::{
component::{Component, Event, EventCtx, Label, Never, Pad},
constant::screen,
geometry::{Alignment2D, Point, Rect},
shape,
shape::Renderer,
},
};
use super::{
constant::WIDTH,
theme::{FATAL_ERROR_COLOR, ICON_WARNING40, RESULT_FOOTER_START, RESULT_PADDING, WHITE},
ResultFooter, ResultStyle,
};
const ICON_TOP: i16 = 23;
const TITLE_AREA_START: i16 = 70;
const MESSAGE_AREA_START: i16 = 90;
#[cfg(feature = "bootloader")]
const STYLE: &ResultStyle = &crate::ui::model_mercury::theme::bootloader::RESULT_WIPE;
#[cfg(not(feature = "bootloader"))]
const STYLE: &ResultStyle = &super::theme::RESULT_ERROR;
pub struct ErrorScreen<'a> {
bg: Pad,
title: Label<'a>,
message: Label<'a>,
footer: ResultFooter<'a>,
}
impl<'a> ErrorScreen<'a> {
pub fn new(title: TString<'a>, message: TString<'a>, footer: TString<'a>) -> Self {
let title = Label::centered(title, STYLE.title_style());
let message = Label::centered(message, STYLE.message_style()).vertically_centered();
let footer = ResultFooter::new(
Label::centered(footer, STYLE.title_style()).vertically_centered(),
STYLE,
);
Self {
bg: Pad::with_background(FATAL_ERROR_COLOR).with_clear(),
title,
message,
footer,
}
}
}
impl<'a> Component for ErrorScreen<'a> {
type Msg = Never;
fn place(&mut self, _bounds: Rect) -> Rect {
self.bg.place(screen());
let title_area = Rect::new(
Point::new(RESULT_PADDING, TITLE_AREA_START),
Point::new(WIDTH - RESULT_PADDING, MESSAGE_AREA_START),
);
self.title.place(title_area);
let message_area = Rect::new(
Point::new(RESULT_PADDING, MESSAGE_AREA_START),
Point::new(WIDTH - RESULT_PADDING, RESULT_FOOTER_START),
);
self.message.place(message_area);
let (_, bottom_area) = ResultFooter::<'a>::split_bounds();
self.footer.place(bottom_area);
screen()
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
let icon = ICON_WARNING40;
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)
.with_fg(WHITE)
.with_bg(FATAL_ERROR_COLOR)
.with_align(Alignment2D::TOP_CENTER)
.render(target);
self.title.render(target);
self.message.render(target);
self.footer.render(target);
}
}

View File

@ -0,0 +1,11 @@
mod button;
mod error;
mod result;
mod welcome_screen;
pub use button::{ButtonStyle, ButtonStyleSheet};
pub use error::ErrorScreen;
pub use result::{ResultFooter, ResultScreen, ResultStyle};
pub use welcome_screen::WelcomeScreen;
use super::{constant, theme};

View File

@ -0,0 +1,175 @@
use crate::{
strutil::TString,
ui::{
component::{text::TextStyle, Component, Event, EventCtx, Label, Never, Pad},
constant::screen,
display::{Color, Font, Icon},
geometry::{Alignment2D, Insets, Offset, Point, Rect},
shape,
shape::Renderer,
},
};
use super::{
constant::WIDTH,
theme::{FG, RESULT_FOOTER_START, RESULT_PADDING},
};
const MESSAGE_AREA_START: i16 = 97;
const ICON_CENTER_Y: i16 = 62;
pub struct ResultStyle {
pub fg_color: Color,
pub bg_color: Color,
pub divider_color: Color,
}
impl ResultStyle {
pub const fn new(fg_color: Color, bg_color: Color, divider_color: Color) -> Self {
Self {
fg_color,
bg_color,
divider_color,
}
}
pub const fn message_style(&self) -> TextStyle {
TextStyle::new(Font::NORMAL, self.fg_color, self.bg_color, FG, FG)
}
pub const fn title_style(&self) -> TextStyle {
TextStyle::new(Font::BOLD, self.fg_color, self.bg_color, FG, FG)
}
}
pub struct ResultFooter<'a> {
style: &'a ResultStyle,
text: Label<'a>,
area: Rect,
}
impl<'a> ResultFooter<'a> {
pub fn new(text: Label<'a>, style: &'a ResultStyle) -> Self {
Self {
style,
text,
area: Rect::zero(),
}
}
}
impl ResultFooter<'_> {
pub const fn split_bounds() -> (Rect, Rect) {
let main_area = Rect::new(
Point::new(RESULT_PADDING, 0),
Point::new(WIDTH - RESULT_PADDING, RESULT_FOOTER_START),
);
let footer_area = Rect::new(
Point::new(RESULT_PADDING, RESULT_FOOTER_START),
Point::new(WIDTH - RESULT_PADDING, screen().height()),
);
(main_area, footer_area)
}
}
impl Component for ResultFooter<'_> {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.text.place(bounds);
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// divider line
let bar = Rect::from_center_and_size(
Point::new(self.area.center().x, self.area.y0),
Offset::new(self.area.width(), 1),
);
shape::Bar::new(bar)
.with_fg(self.style.divider_color)
.render(target);
// footer text
self.text.render(target);
}
}
pub struct ResultScreen<'a> {
bg: Pad,
footer_pad: Pad,
style: &'a ResultStyle,
icon: Icon,
message: Label<'a>,
footer: ResultFooter<'a>,
}
impl<'a> ResultScreen<'a> {
pub fn new(
style: &'a ResultStyle,
icon: Icon,
message: TString<'a>,
footer: Label<'a>,
complete_draw: bool,
) -> Self {
let mut instance = Self {
bg: Pad::with_background(style.bg_color),
footer_pad: Pad::with_background(style.bg_color),
style,
icon,
message: Label::centered(message, style.message_style()),
footer: ResultFooter::new(footer, style),
};
if complete_draw {
instance.bg.clear();
} else {
instance.footer_pad.clear();
}
instance
}
}
impl<'a> Component for ResultScreen<'a> {
type Msg = Never;
fn place(&mut self, _bounds: Rect) -> Rect {
self.bg.place(screen());
let (main_area, footer_area) = ResultFooter::split_bounds();
self.footer_pad.place(footer_area);
self.footer.place(footer_area);
let message_area = main_area.inset(Insets::top(MESSAGE_AREA_START));
self.message.place(message_area);
screen()
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.footer_pad.render(target);
shape::ToifImage::new(
Point::new(screen().center().x, ICON_CENTER_Y),
self.icon.toif,
)
.with_align(Alignment2D::CENTER)
.with_fg(self.style.fg_color)
.with_bg(self.style.bg_color)
.render(target);
self.message.render(target);
self.footer.render(target);
}
}

View File

@ -0,0 +1,68 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never},
display::font::Font,
geometry::{Alignment, Alignment2D, Offset, Rect},
shape,
shape::Renderer,
};
use super::theme;
const TEXT_BOTTOM_MARGIN: i16 = 54;
const ICON_TOP_MARGIN: i16 = 48;
#[cfg(not(feature = "bootloader"))]
const MODEL_NAME_FONT: Font = Font::DEMIBOLD;
use crate::trezorhal::model;
pub struct WelcomeScreen {
area: Rect,
}
impl WelcomeScreen {
pub fn new() -> Self {
Self { area: Rect::zero() }
}
}
impl Component for WelcomeScreen {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
shape::ToifImage::new(
self.area.top_center() + Offset::y(ICON_TOP_MARGIN),
theme::ICON_LOGO.toif,
)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(theme::FG)
.with_bg(theme::BG)
.render(target);
shape::Text::new(
self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN),
model::FULL_NAME,
)
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(theme::FG)
.render(target);
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for WelcomeScreen {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("WelcomeScreen");
#[cfg(not(feature = "bootloader"))]
t.string("model", model::FULL_NAME.into());
}
}

View File

@ -0,0 +1,34 @@
use crate::{
error::Error,
micropython::obj::Obj,
ui::{
component::{
text::paragraphs::{ParagraphSource, Paragraphs},
Component, Timeout,
},
layout::{obj::ComponentMsgObj, result::CANCELLED},
},
};
// Clippy/compiler complains about conflicting implementations
// TODO move the common impls to a common module
#[cfg(not(feature = "clippy"))]
impl<'a, T> ComponentMsgObj for Paragraphs<T>
where
T: ParagraphSource<'a>,
{
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
unreachable!()
}
}
// Clippy/compiler complains about conflicting implementations
#[cfg(not(feature = "clippy"))]
impl<T> ComponentMsgObj for (Timeout, T)
where
T: Component<Msg = ()>,
{
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
Ok(CANCELLED.as_obj())
}
}

View File

@ -0,0 +1,24 @@
use crate::ui::geometry::{Offset, Point, Rect};
use crate::trezorhal::display::{DISPLAY_RESX, DISPLAY_RESY};
pub const WIDTH: i16 = DISPLAY_RESX as _;
pub const HEIGHT: i16 = DISPLAY_RESY as _;
// TODO: below constants copied from mercury
pub const LINE_SPACE: i16 = 4;
pub const FONT_BPP: i16 = 4;
pub const LOADER_OUTER: i16 = 60;
pub const LOADER_INNER: i16 = 52;
pub const LOADER_ICON_MAX_SIZE: i16 = 64;
pub const fn size() -> Offset {
Offset::new(WIDTH, HEIGHT)
}
pub const SIZE: Offset = size();
pub const fn screen() -> Rect {
Rect::from_top_left_and_size(Point::zero(), SIZE)
}
pub const SCREEN: Rect = screen();

View File

@ -0,0 +1,38 @@
use crate::ui::{display::Color, geometry::Point, model_lincoln::constant, shape, shape::Renderer};
pub enum LoaderRange {
Full,
FromTo(f32, f32),
}
pub fn render_loader<'s>(
center: Point,
inactive_color: Color,
active_color: Color,
background_color: Color,
range: LoaderRange,
target: &mut impl Renderer<'s>,
) {
shape::Circle::new(center, constant::LOADER_OUTER)
.with_bg(inactive_color)
.render(target);
match range {
LoaderRange::Full => {
shape::Circle::new(center, constant::LOADER_OUTER)
.with_bg(active_color)
.render(target);
}
LoaderRange::FromTo(start, end) => {
shape::Circle::new(center, constant::LOADER_OUTER)
.with_bg(active_color)
.with_start_angle(start)
.with_end_angle(end)
.render(target);
}
}
shape::Circle::new(center, constant::LOADER_INNER + 2)
.with_bg(background_color)
.render(target);
}

View File

@ -0,0 +1,3 @@
mod loader;
pub use loader::{render_loader, LoaderRange};

View File

@ -0,0 +1,71 @@
use super::{geometry::Rect, CommonUI};
use crate::ui::model_lincoln::theme::backlight;
#[cfg(feature = "bootloader")]
pub mod bootloader;
pub mod component;
pub mod constant;
pub mod theme;
#[cfg(feature = "micropython")]
pub mod component_msg_obj;
pub mod cshape;
#[cfg(feature = "micropython")]
pub mod flow;
pub mod screens;
#[cfg(feature = "micropython")]
pub mod ui_firmware;
pub struct UILincoln;
impl CommonUI for UILincoln {
#[cfg(feature = "backlight")]
fn fadein() {
crate::ui::display::fade_backlight_duration(backlight::get_backlight_normal(), 150);
}
#[cfg(feature = "backlight")]
fn fadeout() {
crate::ui::display::fade_backlight_duration(backlight::get_backlight_dim(), 150);
}
#[cfg(feature = "backlight")]
fn backlight_on() {
crate::ui::display::set_backlight(backlight::get_backlight_normal());
}
#[cfg(feature = "backlight")]
fn get_backlight_none() -> u8 {
backlight::get_backlight_none()
}
#[cfg(feature = "backlight")]
fn get_backlight_normal() -> u8 {
backlight::get_backlight_normal()
}
#[cfg(feature = "backlight")]
fn get_backlight_low() -> u8 {
backlight::get_backlight_low()
}
#[cfg(feature = "backlight")]
fn get_backlight_dim() -> u8 {
backlight::get_backlight_dim()
}
#[cfg(feature = "backlight")]
fn get_backlight_max() -> u8 {
backlight::get_backlight_max()
}
const SCREEN: Rect = constant::SCREEN;
fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
screens::screen_fatal_error(title, msg, footer);
}
fn screen_boot_stage_2() {
screens::screen_boot_stage_2();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

View File

@ -0,0 +1,32 @@
use crate::ui::{component::Component, constant::screen, display};
use super::{
component::{ErrorScreen, WelcomeScreen},
constant,
};
use crate::ui::{display::Color, shape::render_on_display};
pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
let mut frame = ErrorScreen::new(title.into(), msg.into(), footer.into());
frame.place(constant::screen());
render_on_display(None, Some(Color::black()), |target| {
frame.render(target);
});
display::refresh();
}
pub fn screen_boot_stage_2() {
let mut frame = WelcomeScreen::new();
frame.place(screen());
display::sync();
render_on_display(None, Some(Color::black()), |target| {
frame.render(target);
});
display::refresh();
}

View File

@ -0,0 +1,50 @@
#[cfg(not(feature = "bootloader"))]
use crate::storage;
// Typical backlight values.
const BACKLIGHT_NORMAL: u8 = 150;
const BACKLIGHT_LOW: u8 = 45;
const BACKLIGHT_DIM: u8 = 5;
const BACKLIGHT_NONE: u8 = 0;
const BACKLIGHT_MIN: u8 = 10;
const BACKLIGHT_MAX: u8 = 255;
#[cfg(feature = "bootloader")]
pub fn get_backlight_normal() -> u8 {
BACKLIGHT_NORMAL
}
#[cfg(not(feature = "bootloader"))]
pub fn get_backlight_normal() -> u8 {
storage::get_brightness()
.unwrap_or(BACKLIGHT_NORMAL)
.clamp(BACKLIGHT_MIN, BACKLIGHT_MAX)
}
#[cfg(feature = "bootloader")]
pub fn get_backlight_low() -> u8 {
BACKLIGHT_LOW
}
#[cfg(not(feature = "bootloader"))]
pub fn get_backlight_low() -> u8 {
storage::get_brightness()
.unwrap_or(BACKLIGHT_LOW)
.clamp(BACKLIGHT_MIN, BACKLIGHT_LOW)
}
pub fn get_backlight_dim() -> u8 {
BACKLIGHT_DIM
}
pub fn get_backlight_none() -> u8 {
BACKLIGHT_NONE
}
pub fn get_backlight_max() -> u8 {
BACKLIGHT_MAX
}
pub fn get_backlight_min() -> u8 {
BACKLIGHT_MIN
}

View File

@ -0,0 +1,249 @@
use crate::ui::{
component::{text::TextStyle, LineBreaking::BreakWordsNoHyphen},
constant::{HEIGHT, WIDTH},
display::{Color, Font},
geometry::{Offset, Point, Rect},
util::include_res,
};
use super::super::{
component::{ButtonStyle, ButtonStyleSheet, ResultStyle},
theme::{BLACK, FG, GREY_DARK, GREY_LIGHT, WHITE},
};
pub const BLD_BG: Color = Color::rgb(0x00, 0x1E, 0xAD);
pub const BLD_FG: Color = WHITE;
pub const BLD_WIPE_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E);
pub const BLD_WIPE_TEXT_COLOR: Color = WHITE;
pub const BLD_WARN_COLOR: Color = Color::rgb(0xFF, 0x00, 0x00);
pub const BLD_WIPE_BTN_COLOR: Color = WHITE;
pub const BLD_WIPE_BTN_COLOR_ACTIVE: Color = Color::rgb(0xFA, 0xCF, 0xCF);
pub const BLD_WIPE_CANCEL_BTN_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41);
pub const BLD_WIPE_CANCEL_BTN_COLOR_ACTIVE: Color = Color::rgb(0xAE, 0x09, 0x09);
pub const BLD_INSTALL_BTN_COLOR_ACTIVE: Color = Color::rgb(0xCD, 0xD2, 0xEF);
pub const BLD_BTN_COLOR: Color = Color::rgb(0x2D, 0x42, 0xBF);
pub const BLD_BTN_COLOR_ACTIVE: Color = Color::rgb(0x04, 0x10, 0x58);
pub const BLD_TITLE_COLOR: Color = WHITE;
pub const WELCOME_COLOR: Color = BLACK;
pub const WELCOME_HIGHLIGHT_COLOR: Color = Color::rgb(0x28, 0x28, 0x28);
// Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2;
// Commonly used constants for UI elements.
pub const CONTENT_PADDING: i16 = 6;
pub const TITLE_AREA: Rect = Rect::new(
Point::new(CONTENT_PADDING, CONTENT_PADDING),
Point::new(WIDTH, CORNER_BUTTON_SIZE + CONTENT_PADDING),
);
pub const CORNER_BUTTON_TOUCH_EXPANSION: i16 = 13;
pub const CORNER_BUTTON_SIZE: i16 = 44;
pub const CORNER_BUTTON_AREA: Rect = Rect::from_top_left_and_size(
Point::new(
WIDTH - CORNER_BUTTON_SIZE - CONTENT_PADDING,
CONTENT_PADDING,
),
Offset::uniform(CORNER_BUTTON_SIZE),
);
pub const BUTTON_AREA_START: i16 = HEIGHT - 56;
pub const BUTTON_HEIGHT: i16 = 50;
// BLD icons
pub const X24: &[u8] = include_res!("model_mercury/res/x24.toif");
pub const X32: &[u8] = include_res!("model_mercury/res/x32.toif");
pub const FIRE24: &[u8] = include_res!("model_mercury/res/fire24.toif");
pub const FIRE32: &[u8] = include_res!("model_mercury/res/fire32.toif");
pub const FIRE40: &[u8] = include_res!("model_mercury/res/fire40.toif");
pub const REFRESH24: &[u8] = include_res!("model_mercury/res/refresh24.toif");
pub const MENU32: &[u8] = include_res!("model_mercury/res/menu32.toif");
pub const INFO32: &[u8] = include_res!("model_mercury/res/info32.toif");
pub const DOWNLOAD24: &[u8] = include_res!("model_mercury/res/download24.toif");
pub const WARNING40: &[u8] = include_res!("model_mercury/res/warning40.toif");
pub const CHECK24: &[u8] = include_res!("model_mercury/res/check24.toif");
pub const CHECK40: &[u8] = include_res!("model_mercury/res/check40.toif");
pub fn button_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_BG,
button_color: WHITE,
icon_color: BLD_BG,
background_color: BLD_BG,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_BG,
button_color: BLD_INSTALL_BTN_COLOR_ACTIVE,
icon_color: BLD_BG,
background_color: BLD_BG,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: GREY_DARK,
icon_color: BLD_BG,
background_color: FG,
},
}
}
pub fn button_wipe_cancel() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: WHITE,
button_color: BLD_WIPE_CANCEL_BTN_COLOR,
icon_color: WHITE,
background_color: BLD_WIPE_COLOR,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: WHITE,
button_color: BLD_WIPE_CANCEL_BTN_COLOR_ACTIVE,
icon_color: WHITE,
background_color: BLD_WIPE_COLOR,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: GREY_LIGHT,
button_color: GREY_DARK,
icon_color: GREY_LIGHT,
background_color: WHITE,
},
}
}
pub fn button_wipe_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_WIPE_COLOR,
button_color: BLD_WIPE_BTN_COLOR,
icon_color: BLD_WIPE_COLOR,
background_color: BLD_WIPE_COLOR,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_WIPE_COLOR,
button_color: BLD_WIPE_BTN_COLOR_ACTIVE,
icon_color: BLD_WIPE_COLOR,
background_color: BLD_WIPE_COLOR,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: GREY_DARK,
icon_color: FG,
background_color: FG,
},
}
}
pub fn button_bld_menu() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BG,
icon_color: BLD_FG,
background_color: BLD_BG,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BG,
icon_color: BLD_FG,
background_color: BLD_BG,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: GREY_LIGHT,
button_color: BLD_BG,
icon_color: GREY_LIGHT,
background_color: BLD_BG,
},
}
}
pub fn button_bld() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BTN_COLOR,
icon_color: BLD_FG,
background_color: BLD_BG,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BTN_COLOR_ACTIVE,
icon_color: BLD_FG,
background_color: BLD_BG,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: GREY_LIGHT,
button_color: BLD_BTN_COLOR,
icon_color: GREY_LIGHT,
background_color: BLD_BG,
},
}
}
pub const fn text_title(bg: Color) -> TextStyle {
TextStyle::new(
Font::BOLD,
BLD_TITLE_COLOR,
bg,
BLD_TITLE_COLOR,
BLD_TITLE_COLOR,
)
}
pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, BLD_FG, BLD_BG, BLD_FG, BLD_FG);
pub const TEXT_WARNING: TextStyle = TextStyle::new(
Font::BOLD,
BLD_WARN_COLOR,
BLD_BG,
BLD_WARN_COLOR,
BLD_WARN_COLOR,
);
pub const fn text_fingerprint(bg: Color) -> TextStyle {
TextStyle::new(Font::NORMAL, BLD_FG, bg, BLD_FG, BLD_FG).with_line_breaking(BreakWordsNoHyphen)
}
pub const TEXT_BOLD: TextStyle = TextStyle::new(Font::BOLD, BLD_FG, BLD_BG, BLD_FG, BLD_FG);
pub const TEXT_WIPE_BOLD: TextStyle = TextStyle::new(
Font::BOLD,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_COLOR,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_TEXT_COLOR,
);
pub const TEXT_WIPE_NORMAL: TextStyle = TextStyle::new(
Font::NORMAL,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_COLOR,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_TEXT_COLOR,
);
pub const RESULT_WIPE: ResultStyle = ResultStyle::new(
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_COLOR,
BLD_WIPE_CANCEL_BTN_COLOR,
);
pub const RESULT_FW_INSTALL: ResultStyle = ResultStyle::new(BLD_FG, BLD_BG, BLD_BTN_COLOR);
pub const RESULT_INITIAL: ResultStyle =
ResultStyle::new(FG, WELCOME_COLOR, WELCOME_HIGHLIGHT_COLOR);

View File

@ -0,0 +1,66 @@
pub mod bootloader;
pub mod backlight;
use crate::{
time::Duration,
ui::{display::{Color, Font}, util::include_icon},
};
use super::component::{ButtonStyle, ButtonStyleSheet, ResultStyle};
pub const ERASE_HOLD_DURATION: Duration = Duration::from_millis(1500);
// Color palette.
// TODO: colors
pub const WHITE: Color = Color::rgb(0xFF, 0xFF, 0xFF);
pub const BLACK: Color = Color::rgb(0, 0, 0);
pub const FG: Color = WHITE; // Default foreground (text & icon) color.
pub const BG: Color = BLACK; // Default background color.
pub const GREY_DARK: Color = Color::rgb(0x46, 0x48, 0x4A);
pub const GREY_LIGHT: Color = Color::rgb(0xC7, 0xCD, 0xD3);
pub const FATAL_ERROR_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E);
pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41);
// UI icons (white color).
// TODO: icons
// Welcome screen.
include_icon!(ICON_LOGO, "model_lincoln/res/lock_full.toif");
// Homescreen notifications.
include_icon!(ICON_WARNING40, "model_lincoln/res/warning40.toif");
// TODO: button styles
pub const fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::DEMIBOLD,
text_color: FG,
button_color: BG,
icon_color: FG,
background_color: BG,
},
active: &ButtonStyle {
font: Font::DEMIBOLD,
text_color: FG,
button_color: BG,
icon_color: FG,
background_color: BG,
},
disabled: &ButtonStyle {
font: Font::DEMIBOLD,
text_color: FG,
button_color: BG,
icon_color: FG,
background_color: BG,
},
}
}
pub const RESULT_PADDING: i16 = 6;
pub const RESULT_FOOTER_START: i16 = 171;
pub const RESULT_FOOTER_HEIGHT: i16 = 62;
pub const RESULT_ERROR: ResultStyle =
ResultStyle::new(FG, FATAL_ERROR_COLOR, FATAL_ERROR_HIGHLIGHT_COLOR);

View File

@ -0,0 +1,495 @@
use crate::{
error::Error,
io::BinaryData,
micropython::{gc::Gc, list::List, obj::Obj},
strutil::TString,
ui::{
component::Empty,
layout::{
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
util::RecoveryType,
},
ui_firmware::{
FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_WORD_QUIZ_ITEMS,
},
ModelUI,
},
};
use super::UILincoln;
impl FirmwareUI for UILincoln {
fn confirm_action(
_title: TString<'static>,
_action: Option<TString<'static>>,
_description: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_hold: bool,
_hold_danger: bool,
_reverse: bool,
_prompt_screen: bool,
_prompt_title: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_address(
_title: TString<'static>,
_address: Obj,
_address_label: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_info_button: bool,
_chunkify: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_blob(
_title: TString<'static>,
_data: Obj,
_description: Option<TString<'static>>,
_text_mono: bool,
_extra: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_verb_info: Option<TString<'static>>,
_info: bool,
_hold: bool,
_chunkify: bool,
_page_counter: bool,
_prompt_screen: bool,
_cancel: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_blob_intro(
_title: TString<'static>,
_data: Obj,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_chunkify: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_homescreen(
_title: TString<'static>,
_image: BinaryData<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_coinjoin(
_max_rounds: TString<'static>,
_max_feerate: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_emphasized(
_title: TString<'static>,
_items: Obj,
_verb: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_fido(
_title: TString<'static>,
_app_name: TString<'static>,
_icon: Option<TString<'static>>,
_accounts: Gc<List>,
) -> Result<impl LayoutMaybeTrace, Error> {
#[cfg(feature = "universal_fw")]
return Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"));
#[cfg(not(feature = "universal_fw"))]
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(
c"confirm_fido not used in bitcoin-only firmware",
))
}
fn confirm_firmware_update(
_description: TString<'static>,
_fingerprint: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_modify_fee(
_title: TString<'static>,
_sign: i32,
_user_fee_change: TString<'static>,
_total_fee_new: TString<'static>,
_fee_rate_amount: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_modify_output(
_sign: i32,
_amount_change: TString<'static>,
_amount_new: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_more(
_title: TString<'static>,
_button: TString<'static>,
_button_style_confirm: bool,
_items: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_reset_device(_recovery: bool) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_summary(
_amount: TString<'static>,
_amount_label: TString<'static>,
_fee: TString<'static>,
_fee_label: TString<'static>,
_title: Option<TString<'static>>,
_account_items: Option<Obj>,
_extra_items: Option<Obj>,
_extra_title: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_properties(
_title: TString<'static>,
_items: Obj,
_hold: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_value(
_title: TString<'static>,
_value: Obj,
_description: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_info: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_info_button: bool,
_hold: bool,
_chunkify: bool,
_text_mono: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_with_info(
_title: TString<'static>,
_button: TString<'static>,
_info_button: TString<'static>,
_verb_cancel: Option<TString<'static>>,
_items: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn check_homescreen_format(_image: BinaryData, _accept_toif: bool) -> bool {
false // not implemented
}
fn continue_recovery_homepage(
_text: TString<'static>,
_subtext: Option<TString<'static>>,
_button: Option<TString<'static>>,
_recovery_type: RecoveryType,
_show_instructions: bool,
_remaining_shares: Option<Obj>,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn flow_confirm_output(
_title: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
_message: Obj,
_amount: Option<Obj>,
_chunkify: bool,
_text_mono: bool,
_account: Option<TString<'static>>,
_account_path: Option<TString<'static>>,
_br_code: u16,
_br_name: TString<'static>,
_address: Option<Obj>,
_address_title: Option<TString<'static>>,
_summary_items: Option<Obj>,
_fee_items: Option<Obj>,
_summary_title: Option<TString<'static>>,
_summary_br_code: Option<u16>,
_summary_br_name: Option<TString<'static>>,
_cancel_text: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn flow_confirm_set_new_pin(
_title: TString<'static>,
_description: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn flow_get_address(
_address: Obj,
_title: TString<'static>,
_description: Option<TString<'static>>,
_extra: Option<TString<'static>>,
_chunkify: bool,
_address_qr: TString<'static>,
_case_sensitive: bool,
_account: Option<TString<'static>>,
_path: Option<TString<'static>>,
_xpubs: Obj,
_title_success: TString<'static>,
_br_code: u16,
_br_name: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn multiple_pages_texts(
_title: TString<'static>,
_verb: TString<'static>,
_items: Gc<List>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn prompt_backup() -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_bip39(
_prompt: TString<'static>,
_prefill_word: TString<'static>,
_can_go_back: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_slip39(
_prompt: TString<'static>,
_prefill_word: TString<'static>,
_can_go_back: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_number(
_title: TString<'static>,
_count: u32,
_min_count: u32,
_max_count: u32,
_description: Option<TString<'static>>,
_more_info_callback: Option<impl Fn(u32) -> TString<'static> + 'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_pin(
_prompt: TString<'static>,
_subprompt: TString<'static>,
_allow_cancel: bool,
_warning: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_passphrase(
_prompt: TString<'static>,
_max_len: u32,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn select_word(
_title: TString<'static>,
_description: TString<'static>,
_words: [TString<'static>; MAX_WORD_QUIZ_ITEMS],
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn select_word_count(_recovery_type: RecoveryType) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn set_brightness(_current_brightness: Option<u8>) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_address_details(
_qr_title: TString<'static>,
_address: TString<'static>,
_case_sensitive: bool,
_details_title: TString<'static>,
_account: Option<TString<'static>>,
_path: Option<TString<'static>>,
_xpubs: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_checklist(
_title: TString<'static>,
_button: TString<'static>,
_active: usize,
_items: [TString<'static>; MAX_CHECKLIST_ITEMS],
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_danger(
_title: TString<'static>,
_description: TString<'static>,
_value: TString<'static>,
_verb_cancel: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_error(
_title: TString<'static>,
_button: TString<'static>,
_description: TString<'static>,
_allow_cancel: bool,
_time_ms: u32,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_group_share_success(
_lines: [TString<'static>; MAX_GROUP_SHARE_LINES],
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_homescreen(
_label: TString<'static>,
_hold: bool,
_notification: Option<TString<'static>>,
_notification_level: u8,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_info(
_title: TString<'static>,
_description: TString<'static>,
_button: TString<'static>,
_time_ms: u32,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_info_with_cancel(
_title: TString<'static>,
_items: Obj,
_horizontal: bool,
_chunkify: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_lockscreen(
_label: TString<'static>,
_bootscreen: bool,
_coinjoin_authorized: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_mismatch(_title: TString<'static>) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_progress(
_description: TString<'static>,
_indeterminate: bool,
_title: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_progress_coinjoin(
_title: TString<'static>,
_indeterminate: bool,
_time_ms: u32,
_skip_first_paint: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_share_words(
_words: heapless::Vec<TString<'static>, 33>,
_title: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_share_words_mercury(
_words: heapless::Vec<TString<'static>, 33>,
_subtitle: Option<TString<'static>>,
_instructions: Obj,
_text_footer: Option<TString<'static>>,
_text_confirm: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_remaining_shares(_pages_iterable: Obj) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_simple(
text: TString<'static>,
_title: Option<TString<'static>>,
_button: Option<TString<'static>>,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_success(
_title: TString<'static>,
_button: TString<'static>,
_description: TString<'static>,
_allow_cancel: bool,
_time_ms: u32,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_wait_text(_text: TString<'static>) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_warning(
_title: TString<'static>,
_button: TString<'static>,
_value: TString<'static>,
_description: TString<'static>,
_allow_cancel: bool,
_danger: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn tutorial() -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
}

View File

@ -57,18 +57,18 @@ pub const BUTTON_AREA_START: i16 = HEIGHT - 56;
pub const BUTTON_HEIGHT: i16 = 50; pub const BUTTON_HEIGHT: i16 = 50;
// BLD icons // BLD icons
pub const X24: &[u8] = include_res!("model_mercury/res/x24.toif"); pub const X24: &[u8] = include_res!("model_lincoln/res/bootloader/x24.toif");
pub const X32: &[u8] = include_res!("model_mercury/res/x32.toif"); pub const X32: &[u8] = include_res!("model_lincoln/res/bootloader/x32.toif");
pub const FIRE24: &[u8] = include_res!("model_mercury/res/fire24.toif"); pub const FIRE24: &[u8] = include_res!("model_lincoln/res/bootloader/fire24.toif");
pub const FIRE32: &[u8] = include_res!("model_mercury/res/fire32.toif"); pub const FIRE32: &[u8] = include_res!("model_lincoln/res/bootloader/fire32.toif");
pub const FIRE40: &[u8] = include_res!("model_mercury/res/fire40.toif"); pub const FIRE40: &[u8] = include_res!("model_lincoln/res/bootloader/fire40.toif");
pub const REFRESH24: &[u8] = include_res!("model_mercury/res/refresh24.toif"); pub const REFRESH24: &[u8] = include_res!("model_lincoln/res/bootloader/refresh24.toif");
pub const MENU32: &[u8] = include_res!("model_mercury/res/menu32.toif"); pub const MENU32: &[u8] = include_res!("model_lincoln/res/bootloader/menu32.toif");
pub const INFO32: &[u8] = include_res!("model_mercury/res/info32.toif"); pub const INFO32: &[u8] = include_res!("model_lincoln/res/bootloader/info32.toif");
pub const DOWNLOAD24: &[u8] = include_res!("model_mercury/res/download24.toif"); pub const DOWNLOAD24: &[u8] = include_res!("model_lincoln/res/bootloader/download24.toif");
pub const WARNING40: &[u8] = include_res!("model_mercury/res/warning40.toif"); pub const WARNING40: &[u8] = include_res!("model_lincoln/res/bootloader/warning40.toif");
pub const CHECK24: &[u8] = include_res!("model_mercury/res/check24.toif"); pub const CHECK24: &[u8] = include_res!("model_lincoln/res/bootloader/check24.toif");
pub const CHECK40: &[u8] = include_res!("model_mercury/res/check40.toif"); pub const CHECK40: &[u8] = include_res!("model_lincoln/res/bootloader/check40.toif");
pub fn button_confirm() -> ButtonStyleSheet { pub fn button_confirm() -> ButtonStyleSheet {
ButtonStyleSheet { ButtonStyleSheet {

View File

@ -502,6 +502,8 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_TR)}, {MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_TR)},
#elif UI_LAYOUT_MERCURY #elif UI_LAYOUT_MERCURY
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_MERCURY)}, {MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_MERCURY)},
#elif UI_LAYOUT_LINCOLN
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_LINCOLN)},
#else #else
#error Unknown layout #error Unknown layout
#endif #endif

View File

@ -31,6 +31,9 @@ def configure_board(
def get_model_ui() -> str: def get_model_ui() -> str:
from SCons.Script import ARGUMENTS
if ARGUMENTS.get('UI_LINCOLN_DEV', '0') == '1':
return "lincoln"
return "tt" return "tt"

View File

@ -44,11 +44,12 @@ def generate(env):
btc_only = env["bitcoin_only"] == "1" btc_only = env["bitcoin_only"] == "1"
backlight = env["backlight"] backlight = env["backlight"]
optiga = env["optiga"] optiga = env["optiga"]
layout_tt = env["ui_layout"] == "UI_LAYOUT_TT"
layout_tr = env["ui_layout"] == "UI_LAYOUT_TR"
touch = env["use_touch"] touch = env["use_touch"]
button = env["use_button"] button = env["use_button"]
layout_tt = env["ui_layout"] == "UI_LAYOUT_TT"
layout_tr = env["ui_layout"] == "UI_LAYOUT_TR"
layout_mercury = env["ui_layout"] == "UI_LAYOUT_MERCURY" layout_mercury = env["ui_layout"] == "UI_LAYOUT_MERCURY"
layout_lincoln = env["ui_layout"] == "UI_LAYOUT_LINCOLN"
thp = env["thp"] thp = env["thp"]
interim = f"{target[:-4]}.i" # replace .mpy with .i interim = f"{target[:-4]}.i" # replace .mpy with .i
sed_scripts = [ sed_scripts = [
@ -58,6 +59,7 @@ def generate(env):
rf"-e 's/utils\.UI_LAYOUT == \"TT\"/{layout_tt}/g'", rf"-e 's/utils\.UI_LAYOUT == \"TT\"/{layout_tt}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"TR\"/{layout_tr}/g'", rf"-e 's/utils\.UI_LAYOUT == \"TR\"/{layout_tr}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"MERCURY\"/{layout_mercury}/g'", rf"-e 's/utils\.UI_LAYOUT == \"MERCURY\"/{layout_mercury}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"LINCOLN\"/{layout_lincoln}/g'",
rf"-e 's/utils\.USE_BUTTON/{button}/g'", rf"-e 's/utils\.USE_BUTTON/{button}/g'",
rf"-e 's/utils\.USE_TOUCH/{touch}/g'", rf"-e 's/utils\.USE_TOUCH/{touch}/g'",
rf"-e 's/utils\.USE_THP/{thp}/g'", rf"-e 's/utils\.USE_THP/{thp}/g'",
@ -71,6 +73,7 @@ def generate(env):
"T": "T2T1", "T": "T2T1",
"R": "T2B1", "R": "T2B1",
"T3T1": "T3T1", "T3T1": "T3T1",
"T3W1": "T3W1",
} }
for model_sym, internal_model in MODEL_SYMS.items(): for model_sym, internal_model in MODEL_SYMS.items():

View File

@ -2,11 +2,12 @@ from __future__ import annotations
from site_scons import models from site_scons import models
from . import mercury, tr, tt from . import lincoln, mercury, tr, tt
def get_ui_module(model: str): def get_ui_module(model: str):
ui_modules = { ui_modules = {
"lincoln": lincoln,
"mercury": mercury, "mercury": mercury,
"tr": tr, "tr": tr,
"tt": tt, "tt": tt,

View File

@ -0,0 +1,63 @@
from __future__ import annotations
from .common import add_font
def init_ui(
stage: str,
config: list[str],
defines: list[str | tuple[str, str]],
sources: list[str],
rust_features: list[str],
):
rust_features.append("model_lincoln")
font_normal = None
font_demibold = None
font_bold = None
font_mono = None
font_big = None
font_normal_upper = None
font_bold_upper = None
font_sub = None
if stage == "bootloader":
font_normal = "Font_TTSatoshi_DemiBold_21"
font_demibold = "Font_TTSatoshi_DemiBold_21"
font_bold = "Font_TTHoves_Bold_17_upper"
font_mono = "Font_TTSatoshi_DemiBold_21"
font_bold_upper = "Font_TTHoves_Bold_17_upper"
if stage == "prodtest":
font_normal = "Font_TTSatoshi_DemiBold_21"
font_bold = "Font_TTSatoshi_DemiBold_21"
font_mono = "Font_RobotoMono_Medium_21"
if stage == "firmware":
font_normal = "Font_TTSatoshi_DemiBold_21"
font_demibold = "Font_TTSatoshi_DemiBold_21"
font_bold = "Font_TTSatoshi_DemiBold_21"
font_mono = "Font_RobotoMono_Medium_21"
font_big = "Font_TTSatoshi_DemiBold_42"
font_sub = "Font_TTSatoshi_DemiBold_18"
rust_features.append("ui_blurring")
rust_features.append("ui_jpeg_decoder")
rust_features.append("ui_image_buffer")
rust_features.append("ui_overlay")
# fonts
add_font("NORMAL", font_normal, defines, sources)
add_font("BOLD", font_bold, defines, sources)
add_font("DEMIBOLD", font_demibold, defines, sources)
add_font("MONO", font_mono, defines, sources)
add_font("BIG", font_big, defines, sources)
add_font("NORMAL_UPPER", font_normal_upper, defines, sources)
add_font("BOLD_UPPER", font_bold_upper, defines, sources)
add_font("SUB", font_sub, defines, sources)
def get_ui_layout() -> str:
return "UI_LAYOUT_LINCOLN"
def get_ui_layout_path() -> str:
return "trezor/ui/layouts/lincoln/"

View File

@ -173,6 +173,14 @@ trezor.ui.layouts.fido
import trezor.ui.layouts.fido import trezor.ui.layouts.fido
trezor.ui.layouts.homescreen trezor.ui.layouts.homescreen
import trezor.ui.layouts.homescreen import trezor.ui.layouts.homescreen
trezor.ui.layouts.lincoln
import trezor.ui.layouts.lincoln
trezor.ui.layouts.lincoln.fido
import trezor.ui.layouts.lincoln.fido
trezor.ui.layouts.lincoln.recovery
import trezor.ui.layouts.lincoln.recovery
trezor.ui.layouts.lincoln.reset
import trezor.ui.layouts.lincoln.reset
trezor.ui.layouts.mercury trezor.ui.layouts.mercury
import trezor.ui.layouts.mercury import trezor.ui.layouts.mercury
trezor.ui.layouts.mercury.fido trezor.ui.layouts.mercury.fido

View File

@ -10,5 +10,7 @@ elif utils.UI_LAYOUT == "TT":
from .tt import * # noqa: F401,F403 from .tt import * # noqa: F401,F403
elif utils.UI_LAYOUT == "MERCURY": elif utils.UI_LAYOUT == "MERCURY":
from .mercury import * # noqa: F401,F403 from .mercury import * # noqa: F401,F403
elif utils.UI_LAYOUT == "LINCOLN":
from .lincoln import * # noqa: F401,F403
else: else:
raise ValueError("Unknown layout") raise ValueError("Unknown layout")

View File

@ -6,3 +6,5 @@ elif utils.UI_LAYOUT == "TR":
from .tr.fido import * # noqa: F401,F403 from .tr.fido import * # noqa: F401,F403
elif utils.UI_LAYOUT == "MERCURY": elif utils.UI_LAYOUT == "MERCURY":
from .mercury.fido import * # noqa: F401,F403 from .mercury.fido import * # noqa: F401,F403
elif utils.UI_LAYOUT == "LINCOLN":
from .lincoln.fido import * # noqa: F401,F403

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
import trezorui_api
from trezor import ui
from trezor.enums import ButtonRequestType
from ..common import interact
async def confirm_fido(
header: str,
app_name: str,
icon_name: str | None,
accounts: list[str | None],
) -> int:
"""Webauthn confirmation for one or more credentials."""
confirm = trezorui_api.confirm_fido(
title=header,
app_name=app_name,
icon_name=icon_name,
accounts=accounts,
)
result = await interact(confirm, "confirm_fido", ButtonRequestType.Other)
if __debug__ and result is trezorui_api.CONFIRMED:
# debuglink will directly inject a CONFIRMED message which we need to handle
# by playing back a click to the Rust layout and getting out the selected number
# that way
# Or we can just return 0 because this only happens in U2F tests
# which don't use multiple credentials.
return 0
# The Rust side returns either an int or `CANCELLED`. We detect the int situation
# and assume cancellation otherwise.
if isinstance(result, int):
return result
# Late import won't get executed on the happy path.
from trezor.wire import ActionCancelled
raise ActionCancelled
async def confirm_fido_reset() -> bool:
from trezor import TR
confirm = ui.Layout(
trezorui_api.confirm_action(
title=TR.fido__title_reset,
action=TR.fido__erase_credentials,
description=TR.words__really_wanna,
reverse=True,
prompt_screen=True,
)
)
return (await confirm.get_result()) is trezorui_api.CONFIRMED

View File

@ -0,0 +1,147 @@
from typing import TYPE_CHECKING
import trezorui_api
from trezor import TR
from trezor.enums import ButtonRequestType, RecoveryType
from ..common import interact
from . import raise_if_not_confirmed
CONFIRMED = trezorui_api.CONFIRMED # global_import_cache
CANCELLED = trezorui_api.CANCELLED # global_import_cache
INFO = trezorui_api.INFO # global_import_cache
if TYPE_CHECKING:
from apps.management.recovery_device.layout import RemainingSharesInfo
async def request_word_count(recovery_type: RecoveryType) -> int:
count = await interact(
trezorui_api.select_word_count(recovery_type=recovery_type),
"recovery_word_count",
ButtonRequestType.MnemonicWordCount,
)
return int(count)
async def request_word(
word_index: int,
word_count: int,
is_slip39: bool,
send_button_request: bool,
prefill_word: str = "",
) -> str:
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
can_go_back = word_index > 0
if is_slip39:
keyboard = trezorui_api.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
else:
keyboard = trezorui_api.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
word: str = await interact(
keyboard,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
return word
def format_remaining_shares_info(
remaining_shares_info: "RemainingSharesInfo",
) -> list[tuple[str, str]]:
from trezor import strings
from trezor.crypto.slip39 import MAX_SHARE_COUNT
groups, shares_remaining, group_threshold = remaining_shares_info
pages: list[tuple[str, str]] = []
completed_groups = shares_remaining.count(0)
for group, remaining in zip(groups, shares_remaining):
if 0 < remaining < MAX_SHARE_COUNT:
title = strings.format_plural(
TR.recovery__x_more_items_starting_template_plural,
remaining,
TR.plurals__x_shares_needed,
)
words = "\n".join(group)
pages.append((title, words))
elif remaining == MAX_SHARE_COUNT and completed_groups < group_threshold:
groups_remaining = group_threshold - completed_groups
title = strings.format_plural(
TR.recovery__x_more_items_starting_template_plural,
groups_remaining,
TR.plurals__x_groups_needed,
)
words = "\n".join(group)
pages.append((title, words))
return pages
async def show_group_share_success(share_index: int, group_index: int) -> None:
await raise_if_not_confirmed(
trezorui_api.show_group_share_success(
lines=[
TR.recovery__you_have_entered,
TR.recovery__share_num_template.format(share_index + 1),
TR.words__from,
TR.recovery__group_num_template.format(group_index + 1),
],
),
"share_success",
ButtonRequestType.Other,
)
async def continue_recovery(
_button_label: str, # unused on mercury
text: str,
subtext: str | None,
recovery_type: RecoveryType,
show_instructions: bool = False,
remaining_shares_info: "RemainingSharesInfo | None" = None,
) -> bool:
result = await interact(
trezorui_api.continue_recovery_homepage(
text=text,
subtext=subtext,
button=None,
recovery_type=recovery_type,
show_instructions=show_instructions,
remaining_shares=(
format_remaining_shares_info(remaining_shares_info)
if remaining_shares_info
else None
),
),
None,
ButtonRequestType.Other,
raise_on_cancel=None,
)
return result is CONFIRMED
async def show_recovery_warning(
br_name: str,
content: str,
subheader: str | None = None,
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
button = button or TR.buttons__try_again # def_arg
await raise_if_not_confirmed(
trezorui_api.show_warning(
title=content or TR.words__warning,
value=subheader or "",
button=button,
description="",
danger=True,
),
br_name,
br_code,
)

View File

@ -0,0 +1,356 @@
from typing import Awaitable, Callable, Sequence
import trezorui_api
from trezor import TR, ui
from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
from ..common import interact
from . import raise_if_not_confirmed, show_success
CONFIRMED = trezorui_api.CONFIRMED # global_import_cache
def show_share_words(
share_words: Sequence[str],
share_index: int | None = None,
group_index: int | None = None,
) -> Awaitable[None]:
# FIXME: not implemented
raise NotImplemented
async def select_word(
words: Sequence[str],
share_index: int | None,
checked_index: int,
count: int,
group_index: int | None = None,
) -> str:
if share_index is None:
title: str = TR.reset__check_wallet_backup_title
elif group_index is None:
title: str = TR.reset__check_share_title_template.format(share_index + 1)
else:
title: str = TR.reset__check_group_share_title_template.format(
group_index + 1, share_index + 1
)
# It may happen (with a very low probability)
# that there will be less than three unique words to choose from.
# In that case, duplicating the last word to make it three.
words = list(words)
while len(words) < 3:
words.append(words[-1])
result = await interact(
trezorui_api.select_word(
title=title,
description=TR.reset__select_word_x_of_y_template.format(
checked_index + 1, count
),
words=(words[0], words[1], words[2]),
),
None,
)
if __debug__ and isinstance(result, str):
return result
assert isinstance(result, int) and 0 <= result <= 2
return words[result]
async def slip39_show_checklist(
step: int,
advanced: bool,
count: int | None = None,
threshold: int | None = None,
) -> None:
items = _slip_39_checklist_items(step, advanced, count, threshold)
result = await interact(
trezorui_api.show_checklist(
title=TR.reset__title_shamir_backup,
button=TR.buttons__continue,
active=step,
items=items,
),
"slip39_checklist",
ButtonRequestType.ResetDevice,
)
if result != CONFIRMED:
raise ActionCancelled
def _slip_39_checklist_items(
step: int,
advanced: bool,
count: int | None = None,
threshold: int | None = None,
) -> tuple[str, str, str]:
if not advanced:
entry_1 = (
TR.reset__slip39_checklist_num_shares_x_template.format(count)
if count
else TR.reset__slip39_checklist_set_num_shares
)
entry_2 = (
TR.reset__slip39_checklist_threshold_x_template.format(threshold)
if threshold
else TR.reset__slip39_checklist_set_threshold
)
entry_3 = TR.reset__slip39_checklist_write_down_recovery
return (entry_1, entry_2, entry_3)
else:
entry_1 = (
TR.reset__slip39_checklist_num_groups_x_template.format(count)
if count
else TR.reset__slip39_checklist_set_num_groups
)
entry_2 = (
TR.reset__slip39_checklist_threshold_x_template.format(threshold)
if threshold
else TR.reset__slip39_checklist_set_threshold
)
entry_3 = TR.reset__slip39_checklist_set_sizes_longer
return (entry_1, entry_2, entry_3)
async def _prompt_number(
title: str,
description: str,
info: Callable[[int], str],
count: int,
min_count: int,
max_count: int,
br_name: str,
) -> int:
result = await interact(
trezorui_api.request_number(
title=title,
count=count,
min_count=min_count,
max_count=max_count,
description=description,
more_info_callback=info,
),
br_name,
ButtonRequestType.ResetDevice,
raise_on_cancel=None,
)
if __debug__ and result is CONFIRMED:
# sent by debuglink. debuglink does not change the number of shares anyway
# so use the initial one
return count
if result is not trezorui_api.CANCELLED:
assert isinstance(result, int)
return result
else:
raise ActionCancelled # user cancelled request number prompt
def slip39_prompt_threshold(
num_of_shares: int, group_id: int | None = None
) -> Awaitable[int]:
count = num_of_shares // 2 + 1
# min value of share threshold is 2 unless the number of shares is 1
# number of shares 1 is possible in advanced slip39
min_count = min(2, num_of_shares)
max_count = num_of_shares
description = (
TR.reset__select_threshold
if group_id is None
else TR.reset__num_shares_for_group_template.format(group_id + 1)
)
def info(count: int) -> str:
return (
TR.reset__slip39_checklist_more_info_threshold
+ "\n"
+ TR.reset__slip39_checklist_more_info_threshold_example_template.format(
count, num_of_shares, count
)
)
return _prompt_number(
TR.reset__title_set_threshold,
description,
info,
count,
min_count,
max_count,
"slip39_threshold",
)
async def slip39_prompt_number_of_shares(
num_words: int, group_id: int | None = None
) -> int:
count = 5
min_count = 1
max_count = 16
description = (
TR.reset__num_of_shares_how_many
if group_id is None
else TR.reset__total_number_of_shares_in_group_template.format(group_id + 1)
)
if group_id is None:
info = TR.reset__num_of_shares_long_info_template.format(num_words)
else:
info = TR.reset__num_of_shares_advanced_info_template.format(
num_words, group_id + 1
)
return await _prompt_number(
TR.reset__title_set_number_of_shares,
description,
lambda i: info,
count,
min_count,
max_count,
"slip39_shares",
)
async def slip39_advanced_prompt_number_of_groups() -> int:
count = 5
min_count = 2
max_count = 16
description = TR.reset__group_description
info = TR.reset__group_info
return await _prompt_number(
TR.reset__title_set_number_of_groups,
description,
lambda i: info,
count,
min_count,
max_count,
"slip39_groups",
)
async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int:
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
description = TR.reset__required_number_of_groups
info = TR.reset__advanced_group_threshold_info
return await _prompt_number(
TR.reset__title_set_group_threshold,
description,
lambda i: info,
count,
min_count,
max_count,
"slip39_group_threshold",
)
async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None:
if single_share:
assert num_of_words is not None
description = TR.backup__info_single_share_backup.format(num_of_words)
else:
description = TR.backup__info_multi_share_backup
await interact(
trezorui_api.show_info(
title=TR.backup__title_create_wallet_backup,
description=description,
),
"backup_intro",
ButtonRequestType.ResetDevice,
)
def show_warning_backup() -> Awaitable[ui.UiResult]:
return interact(
trezorui_api.show_warning(
title=TR.words__important,
value=TR.reset__never_make_digital_copy,
button="",
allow_cancel=False,
danger=False, # Use a less severe icon color
),
"backup_warning",
ButtonRequestType.ResetDevice,
)
def show_success_backup() -> Awaitable[None]:
return show_success(
"success_backup",
TR.backup__title_backup_completed,
)
def show_reset_warning(
br_name: str,
content: str,
subheader: str | None = None,
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> Awaitable[None]:
return raise_if_not_confirmed(
trezorui_api.show_warning(
title=subheader or "",
description=content,
value="",
button="",
allow_cancel=False,
danger=True,
),
br_name,
br_code,
)
async def show_share_confirmation_success(
share_index: int | None = None,
num_of_shares: int | None = None,
group_index: int | None = None,
) -> None:
if share_index is None or num_of_shares is None:
# it is a BIP39 or a 1-of-1 SLIP39 backup
# mercury UI shows only final wallet backup confirmation screen later
return
# TODO: super-shamir copy not done
if share_index == num_of_shares - 1:
title = TR.reset__share_completed_template.format(share_index + 1)
if group_index is None:
footer_description = ""
else:
footer_description = TR.reset__finished_verifying_group_template.format(
group_index + 1
)
else:
if group_index is None:
title = TR.reset__share_completed_template.format(share_index + 1)
footer_description = (
TR.instructions__shares_continue_with_x_template.format(share_index + 2)
)
else:
title = TR.reset__continue_with_next_share
footer_description = (
TR.reset__group_share_checked_successfully_template.format(
group_index + 1, share_index + 1
)
)
await show_success("success_recovery", title, subheader=footer_description)
def show_share_confirmation_failure() -> Awaitable[None]:
return show_reset_warning(
"warning_backup_check",
TR.words__try_again,
TR.reset__incorrect_word_selected,
"",
ButtonRequestType.ResetDevice,
)

View File

@ -6,3 +6,5 @@ elif utils.UI_LAYOUT == "TR":
from .tr.recovery import * # noqa: F401,F403 from .tr.recovery import * # noqa: F401,F403
elif utils.UI_LAYOUT == "MERCURY": elif utils.UI_LAYOUT == "MERCURY":
from .mercury.recovery import * # noqa: F401,F403 from .mercury.recovery import * # noqa: F401,F403
elif utils.UI_LAYOUT == "LINCOLN":
from .lincoln.recovery import * # noqa: F401,F403

View File

@ -6,3 +6,5 @@ elif utils.UI_LAYOUT == "TR":
from .tr.reset import * # noqa: F401,F403 from .tr.reset import * # noqa: F401,F403
elif utils.UI_LAYOUT == "MERCURY": elif utils.UI_LAYOUT == "MERCURY":
from .mercury.reset import * # noqa: F401,F403 from .mercury.reset import * # noqa: F401,F403
elif utils.UI_LAYOUT == "LINCOLN":
from .lincoln.reset import * # noqa: F401,F403

View File

@ -49,6 +49,7 @@ MODEL_CHOICE = ChoiceType(
"T2T1": models.T2T1, "T2T1": models.T2T1,
"T2B1": models.T2B1, "T2B1": models.T2B1,
"T3T1": models.T3T1, "T3T1": models.T3T1,
"T3W1": models.T3W1,
# aliases # aliases
"1": models.T1B1, "1": models.T1B1,
"one": models.T1B1, "one": models.T1B1,

View File

@ -79,6 +79,7 @@ class LayoutType(Enum):
TT = auto() TT = auto()
TR = auto() TR = auto()
Mercury = auto() Mercury = auto()
Lincoln = auto()
@classmethod @classmethod
def from_model(cls, model: models.TrezorModel) -> "LayoutType": def from_model(cls, model: models.TrezorModel) -> "LayoutType":
@ -90,6 +91,8 @@ class LayoutType(Enum):
return cls.Mercury return cls.Mercury
if model in (models.T1B1,): if model in (models.T1B1,):
return cls.T1 return cls.T1
if model in (models.T3W1,):
return cls.Lincoln
raise ValueError(f"Unknown model: {model}") raise ValueError(f"Unknown model: {model}")

View File

@ -41,13 +41,14 @@ def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"):
models.T3B1: 500, models.T3B1: 500,
models.T2T1: 1000, models.T2T1: 1000,
models.T3T1: 1000, models.T3T1: 1000,
models.T3W1: 1000,
}[debug.model] }[debug.model]
lock_duration = { lock_duration = {
models.T1B1: 1200, models.T1B1: 1200,
models.T2B1: 1200, models.T2B1: 1200,
models.T3B1: 1200, models.T3B1: 1200,
models.T2T1: 3500, models.T2T1: 3500,
models.T3T1: 3500, models.T3W1: 3500,
}[debug.model] }[debug.model]
def hold(duration: int) -> None: def hold(duration: int) -> None:

View File

@ -185,6 +185,7 @@ class ModelsFilter:
"safe3": {models.T2B1, models.T3B1}, "safe3": {models.T2B1, models.T3B1},
"safe5": {models.T3T1}, "safe5": {models.T3T1},
"mercury": {models.T3T1}, "mercury": {models.T3T1},
"lincoln": {models.T3W1},
} }
def __init__(self, node: Node) -> None: def __init__(self, node: Node) -> None:

View File

@ -12,6 +12,7 @@ FIRMWARE_LENGTHS = {
models.T2B1: 13 * 128 * 1024, models.T2B1: 13 * 128 * 1024,
models.T3T1: 208 * 8 * 1024, models.T3T1: 208 * 8 * 1024,
models.T3B1: 208 * 8 * 1024, models.T3B1: 208 * 8 * 1024,
models.T3W1: 208 * 8 * 1024, # FIXME: fill in the correct value
} }

View File

@ -43,6 +43,7 @@ MAX_DATA_LENGTH = {
models.T2B1: 32 * 1024, models.T2B1: 32 * 1024,
models.T3T1: 256 * 1024, models.T3T1: 256 * 1024,
models.T3B1: 256 * 1024, models.T3B1: 256 * 1024,
models.T3W1: 256 * 1024, # FIXME: fill in correct value
} }