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

feat(python): add debuglink helpers to get layout information

This commit is contained in:
grdddj 2022-10-21 19:07:42 +02:00 committed by Martin Milata
parent 2cbb9efeca
commit e9a1bcc951

View File

@ -15,8 +15,8 @@
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>. # If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
import logging import logging
import re
import textwrap import textwrap
from collections import namedtuple
from copy import deepcopy from copy import deepcopy
from datetime import datetime from datetime import datetime
from enum import IntEnum from enum import IntEnum
@ -56,13 +56,129 @@ if TYPE_CHECKING:
EXPECTED_RESPONSES_CONTEXT_LINES = 3 EXPECTED_RESPONSES_CONTEXT_LINES = 3
LayoutLines = namedtuple("LayoutLines", "lines text")
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def layout_lines(lines: Sequence[str]) -> LayoutLines: class LayoutContent:
return LayoutLines(lines, " ".join(lines)) """Stores content of a layout as returned from Trezor.
Contains helper functions to extract specific parts of the layout.
"""
def __init__(self, lines: Sequence[str]) -> None:
self.lines = list(lines)
self.text = " ".join(self.lines)
def get_title(self) -> str:
"""Get title of the layout.
Title is located between "title" and "content" identifiers.
Example: "< Frame title : RECOVERY SHARE #1 content : < SwipePage"
-> "RECOVERY SHARE #1"
"""
match = re.search(r"title : (.*?) content :", self.text)
if not match:
return ""
return match.group(1).strip()
def get_content(self, tag_name: str = "Paragraphs", raw: bool = False) -> str:
"""Get text of the main screen content of the layout."""
content = "".join(self._get_content_lines(tag_name, raw))
if not raw and content.endswith(" "):
# Stripping possible space at the end
content = content[:-1]
return content
def get_button_texts(self) -> List[str]:
"""Get text of all buttons in the layout.
Example button: "< Button text : LADYBUG >"
-> ["LADYBUG"]
"""
return re.findall(r"< Button text : +(.*?) >", self.text)
def get_seed_words(self) -> List[str]:
"""Get all the seed words on the screen in order.
Example content: "1. ladybug 2. acid 3. academic 4. afraid"
-> ["ladybug", "acid", "academic", "afraid"]
"""
return re.findall(r"\d+\. (\w+)\b", self.get_content())
def get_page_count(self) -> int:
"""Get number of pages for the layout."""
return self._get_number("page_count")
def get_active_page(self) -> int:
"""Get current page index of the layout."""
return self._get_number("active_page")
def _get_number(self, key: str) -> int:
"""Get number connected with a specific key."""
match = re.search(rf"{key} : +(\d+)", self.text)
if not match:
return 0
return int(match.group(1))
def _get_content_lines(
self, tag_name: str = "Paragraphs", raw: bool = False
) -> List[str]:
"""Get lines of the main screen content of the layout."""
# First line should have content after the tag, last line does not store content
tag = f"< {tag_name}"
if tag in self.lines[0]:
first_line = self.lines[0].split(tag)[1]
all_lines = [first_line] + self.lines[1:-1]
else:
all_lines = self.lines[1:-1]
if raw:
return all_lines
else:
return [_clean_line(line) for line in all_lines]
def _clean_line(line: str) -> str:
"""Cleaning the layout line for extra spaces, hyphens and ellipsis.
Line usually comes in the form of " <content> ", with trailing spaces
at both ends. It may end with a hyphen (" - ") or ellipsis (" ... ").
Hyphen means the word was split to the next line, ellipsis signals
the text continuing on the next page.
"""
# Deleting space at the beginning
if line.startswith(" "):
line = line[1:]
# Deleting a hyphen at the end, together with the space
# before it, so it will be tightly connected with the next line
if line.endswith(" - "):
line = line[:-3]
# Deleting the ellipsis at the end (but preserving the space there)
if line.endswith(" ... "):
line = line[:-4]
return line
def multipage_content(layouts: List[LayoutContent]) -> str:
"""Get overall content from multiple-page layout."""
final_text = ""
for layout in layouts:
final_text += layout.get_content()
# When the raw content of the page ends with ellipsis,
# we need to add a space to separate it with the next page
if layout.get_content(raw=True).endswith("... "):
final_text += " "
# Stripping possible space at the end of last page
if final_text.endswith(" "):
final_text = final_text[:-1]
return final_text
class DebugLink: class DebugLink:
@ -114,14 +230,14 @@ class DebugLink:
def state(self) -> messages.DebugLinkState: def state(self) -> messages.DebugLinkState:
return self._call(messages.DebugLinkGetState()) return self._call(messages.DebugLinkGetState())
def read_layout(self) -> LayoutLines: def read_layout(self) -> LayoutContent:
return layout_lines(self.state().layout_lines) return LayoutContent(self.state().layout_lines)
def wait_layout(self) -> LayoutLines: def wait_layout(self) -> LayoutContent:
obj = self._call(messages.DebugLinkGetState(wait_layout=True)) obj = self._call(messages.DebugLinkGetState(wait_layout=True))
if isinstance(obj, messages.Failure): if isinstance(obj, messages.Failure):
raise TrezorFailure(obj) raise TrezorFailure(obj)
return layout_lines(obj.layout_lines) return LayoutContent(obj.layout_lines)
def watch_layout(self, watch: bool) -> None: def watch_layout(self, watch: bool) -> None:
"""Enable or disable watching layouts. """Enable or disable watching layouts.
@ -163,7 +279,7 @@ class DebugLink:
y: Optional[int] = None, y: Optional[int] = None,
wait: Optional[bool] = None, wait: Optional[bool] = None,
hold_ms: Optional[int] = None, hold_ms: Optional[int] = None,
) -> Optional[LayoutLines]: ) -> Optional[LayoutContent]:
if not self.allow_interactions: if not self.allow_interactions:
return None return None
@ -176,13 +292,13 @@ class DebugLink:
) )
ret = self._call(decision, nowait=not wait) ret = self._call(decision, nowait=not wait)
if ret is not None: if ret is not None:
return layout_lines(ret.lines) return LayoutContent(ret.lines)
return None return None
def click( def click(
self, click: Tuple[int, int], wait: bool = False self, click: Tuple[int, int], wait: bool = False
) -> Optional[LayoutLines]: ) -> Optional[LayoutContent]:
x, y = click x, y = click
return self.input(x=x, y=y, wait=wait) return self.input(x=x, y=y, wait=wait)