You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/trezor/ui/__init__.py

362 lines
12 KiB

# pylint: disable=wrong-import-position
import utime
from micropython import const
from trezorui import Display
from typing import TYPE_CHECKING, Any, Awaitable, Generator
from trezor import io, loop, res, utils, workflow
# all rendering is done through a singleton of `Display`
display = Display()
# re-export constants from modtrezorui
NORMAL: int = Display.FONT_NORMAL
BOLD: int = Display.FONT_BOLD
MONO: int = Display.FONT_MONO
WIDTH: int = Display.WIDTH
HEIGHT: int = Display.HEIGHT
# channel used to cancel layouts, see `Cancelled` exception
layout_chan = loop.chan()
# allow only one alert at a time to avoid alerts overlapping
_alert_in_progress = False
# in debug mode, display an indicator in top right corner
if __debug__:
def refresh() -> None:
from apps.debug import screenshot
if not screenshot():
side = Display.WIDTH // 30
display.bar(Display.WIDTH - side, 0, side, side, 0xF800)
display.refresh()
else:
refresh = display.refresh # type: ignore [obscured-by-same-name]
# in both debug and production, emulator needs to draw the screen explicitly
if utils.EMULATOR or utils.MODEL in ("1", "R"):
loop.after_step_hook = refresh
def rgb(r: int, g: int, b: int) -> int:
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)
# import style later to avoid circular dep
from trezor.ui import style # isort:skip
# import style definitions into namespace
from trezor.ui.style import * # isort:skip # noqa: F401,F403
async def _alert(count: int) -> None:
short_sleep = loop.sleep(20)
long_sleep = loop.sleep(80)
for i in range(count * 2):
if i % 2 == 0:
display.backlight(style.BACKLIGHT_MAX)
await short_sleep
else:
display.backlight(style.BACKLIGHT_DIM)
await long_sleep
display.backlight(style.BACKLIGHT_NORMAL)
global _alert_in_progress
_alert_in_progress = False
def alert(count: int = 3) -> None:
global _alert_in_progress
if _alert_in_progress:
return
_alert_in_progress = True
loop.schedule(_alert(count))
def backlight_fade(val: int, delay: int = 14000, step: int = 15) -> None:
if __debug__:
if utils.DISABLE_ANIMATION:
display.backlight(val)
return
current = display.backlight()
if current > val:
step = -step
for i in range(current, val, step):
display.backlight(i)
utime.sleep_us(delay)
def header(
title: str,
icon: str = style.ICON_DEFAULT,
fg: int = style.FG,
bg: int = style.BG,
ifg: int = style.GREEN,
) -> None:
if icon is not None:
display.icon(14, 15, res.load(icon), ifg, bg)
display.text(44, 35, title, BOLD, fg, bg)
# Common for both header functions
MODEL_HEADER_HEIGHTS = {"1": 12, "R": 15, "T": 30}
MODEL_Y_BASELINES = {"1": 10, "R": 11, "T": 22}
def header_warning(message: str) -> None:
height = MODEL_HEADER_HEIGHTS[utils.MODEL]
y_baseline = MODEL_Y_BASELINES[utils.MODEL]
display.bar(0, 0, WIDTH, height, style.YELLOW)
display.text_center(
WIDTH // 2, y_baseline, message, BOLD, style.BLACK, style.YELLOW
)
def header_error(message: str) -> None:
height = MODEL_HEADER_HEIGHTS[utils.MODEL]
y_baseline = MODEL_Y_BASELINES[utils.MODEL]
display.bar(0, 0, WIDTH, height, style.RED)
display.text_center(WIDTH // 2, y_baseline, message, BOLD, style.WHITE, style.RED)
def get_header_height() -> int:
return MODEL_HEADER_HEIGHTS[utils.MODEL]
# Component events. Should be different from `io.TOUCH_*` events.
# Event dispatched when components should draw to the display, if they are
# marked for re-paint.
RENDER = const(-255)
# Event dispatched when components should mark themselves for re-painting.
REPAINT = const(-256)
# How long, in milliseconds, should the layout rendering task sleep between
# the render calls.
_RENDER_DELAY_MS = const(10)
class Component:
"""
Abstract class.
Components are GUI classes that inherit `Component` and form a tree, with a
`Layout` at the root, and other components underneath. Components that
have children, and therefore need to dispatch events to them, usually
override the `dispatch` method. Leaf components usually override the event
methods (`on_*`). Components signal a completion to the layout by raising
an instance of `Result`.
"""
def __init__(self) -> None:
self.repaint = True
if utils.MODEL in ("T",):
def dispatch(self, event: int, x: int, y: int) -> None:
if event is RENDER:
self.on_render()
elif event is io.TOUCH_START:
self.on_touch_start(x, y)
elif event is io.TOUCH_MOVE:
self.on_touch_move(x, y)
elif event is io.TOUCH_END:
self.on_touch_end(x, y)
elif event is REPAINT:
self.repaint = True
def on_touch_start(self, x: int, y: int) -> None:
pass
def on_touch_move(self, x: int, y: int) -> None:
pass
def on_touch_end(self, x: int, y: int) -> None:
pass
elif utils.MODEL in ("1", "R"):
def dispatch(self, event: int, x: int, y: int) -> None:
if event is RENDER:
self.on_render()
elif event is io.BUTTON_PRESSED:
self.on_button_pressed(x)
elif event is io.BUTTON_RELEASED:
self.on_button_released(x)
elif event is REPAINT:
self.repaint = True
def on_button_pressed(self, button_number: int) -> None:
pass
def on_button_released(self, button_number: int) -> None:
pass
def on_render(self) -> None:
pass
if __debug__:
def read_content(self) -> list[str]:
return [self.__class__.__name__]
class Result(Exception):
"""
When components want to trigger layout completion, they do so through
raising an instance of `Result`.
See `Layout.__iter__` for details.
"""
def __init__(self, value: Any) -> None:
super().__init__()
self.value = value
class Cancelled(Exception):
"""
Layouts can be explicitly cancelled. This usually happens when another
layout starts, because only one layout can be running at the same time,
and is done by raising `Cancelled` on the cancelled layout. Layouts
should always re-raise such exceptions.
See `Layout.__iter__` for details.
"""
class Layout(Component):
"""
Abstract class.
Layouts are top-level components. Only one layout can be running at the
same time. Layouts provide asynchronous interface, so a running task can
wait for the layout to complete. Layouts complete when a `Result` is
raised, usually from some of the child components.
"""
BACKLIGHT_LEVEL = style.BACKLIGHT_NORMAL
RENDER_SLEEP: loop.Syscall = loop.sleep(_RENDER_DELAY_MS)
async def __iter__(self) -> Any:
"""
Run the layout and wait until it completes. Returns the result value.
Usually not overridden.
"""
if __debug__:
# we want to call notify_layout_change() when the rendering is done;
# but only the first time the layout is awaited. Here we indicate that we
# are being awaited, and in handle_rendering() we send the appropriate event
self.should_notify_layout_change = True
value = None
try:
# If any other layout is running (waiting on the layout channel),
# we close it with the Cancelled exception, and wait until it is
# closed, just to be sure.
if layout_chan.takers:
await layout_chan.put(Cancelled())
# Now, no other layout should be running. In a loop, we create new
# layout tasks and execute them in parallel, while waiting on the
# layout channel. This allows other layouts to cancel us, and the
# layout tasks to trigger restart by exiting (new tasks are created
# and we continue, because we are in a loop).
while True:
await loop.race(layout_chan.take(), *self.create_tasks())
except Result as result:
# Result exception was raised, this means this layout is complete.
value = result.value
return value
if TYPE_CHECKING:
def __await__(self) -> Generator:
return self.__iter__() # type: ignore [Expression of type "Coroutine[Any, Any, Any]" cannot be assigned to return type "Generator[Unknown, Unknown, Unknown]"]
else:
__await__ = __iter__
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
"""
Called from `__iter__`. Creates and returns a sequence of tasks that
run this layout. Tasks are executed in parallel. When one of them
returns, the others are closed and `create_tasks` is called again.
Usually overridden to add another tasks to the list."""
return self.handle_input(), self.handle_rendering()
if utils.MODEL in ("T",):
def handle_input(self) -> Generator:
"""Task that is waiting for the user input."""
touch = loop.wait(io.TOUCH)
while True:
# Using `yield` instead of `await` to avoid allocations.
event, x, y = yield touch
workflow.idle_timer.touch()
self.dispatch(event, x, y)
# We dispatch a render event right after the touch. Quick and dirty
# way to get the lowest input-to-render latency.
self.dispatch(RENDER, 0, 0)
elif utils.MODEL in ("1", "R"):
def handle_input(self) -> Generator:
"""Task that is waiting for the user input."""
button = loop.wait(io.BUTTON)
while True:
event, button_num = yield button
workflow.idle_timer.touch()
self.dispatch(event, button_num, 0)
self.dispatch(RENDER, 0, 0)
else:
raise ValueError("Unknown Trezor model")
def _before_render(self) -> None:
# Before the first render, we dim the display.
backlight_fade(style.BACKLIGHT_DIM)
# Clear the screen of any leftovers, make sure everything is marked for
# repaint (we can be running the same layout instance multiple times)
# and paint it.
display.clear()
self.dispatch(REPAINT, 0, 0)
self.dispatch(RENDER, 0, 0)
if __debug__ and self.should_notify_layout_change:
from apps.debug import notify_layout_change
# notify about change and do not notify again until next await.
# (handle_rendering might be called multiple times in a single await,
# because of the endless loop in __iter__)
self.should_notify_layout_change = False
notify_layout_change(self)
# Display is usually refreshed after every loop step, but here we are
# rendering everything synchronously, so refresh it manually and turn
# the brightness on again.
refresh()
backlight_fade(self.BACKLIGHT_LEVEL)
def handle_rendering(self) -> loop.Task: # type: ignore [awaitable-is-generator]
"""Task that is rendering the layout in a busy loop."""
self._before_render()
sleep = self.RENDER_SLEEP
while True:
# Wait for a couple of ms and render the layout again. Because
# components use re-paint marking, they do not really draw on the
# display needlessly. Using `yield` instead of `await` to avoid allocations.
# TODO: remove the busy loop
yield sleep
self.dispatch(RENDER, 0, 0)
def wait_until_layout_is_running() -> Awaitable[None]: # type: ignore [awaitable-is-generator]
while not layout_chan.takers:
yield