mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-18 04:18:10 +00:00
feat(python): add debuglink helpers to get layout information
This commit is contained in:
parent
2cbb9efeca
commit
e9a1bcc951
@ -15,8 +15,8 @@
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
import logging
|
||||
import re
|
||||
import textwrap
|
||||
from collections import namedtuple
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
@ -56,13 +56,129 @@ if TYPE_CHECKING:
|
||||
|
||||
EXPECTED_RESPONSES_CONTEXT_LINES = 3
|
||||
|
||||
LayoutLines = namedtuple("LayoutLines", "lines text")
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def layout_lines(lines: Sequence[str]) -> LayoutLines:
|
||||
return LayoutLines(lines, " ".join(lines))
|
||||
class LayoutContent:
|
||||
"""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:
|
||||
@ -114,14 +230,14 @@ class DebugLink:
|
||||
def state(self) -> messages.DebugLinkState:
|
||||
return self._call(messages.DebugLinkGetState())
|
||||
|
||||
def read_layout(self) -> LayoutLines:
|
||||
return layout_lines(self.state().layout_lines)
|
||||
def read_layout(self) -> LayoutContent:
|
||||
return LayoutContent(self.state().layout_lines)
|
||||
|
||||
def wait_layout(self) -> LayoutLines:
|
||||
def wait_layout(self) -> LayoutContent:
|
||||
obj = self._call(messages.DebugLinkGetState(wait_layout=True))
|
||||
if isinstance(obj, messages.Failure):
|
||||
raise TrezorFailure(obj)
|
||||
return layout_lines(obj.layout_lines)
|
||||
return LayoutContent(obj.layout_lines)
|
||||
|
||||
def watch_layout(self, watch: bool) -> None:
|
||||
"""Enable or disable watching layouts.
|
||||
@ -163,7 +279,7 @@ class DebugLink:
|
||||
y: Optional[int] = None,
|
||||
wait: Optional[bool] = None,
|
||||
hold_ms: Optional[int] = None,
|
||||
) -> Optional[LayoutLines]:
|
||||
) -> Optional[LayoutContent]:
|
||||
if not self.allow_interactions:
|
||||
return None
|
||||
|
||||
@ -176,13 +292,13 @@ class DebugLink:
|
||||
)
|
||||
ret = self._call(decision, nowait=not wait)
|
||||
if ret is not None:
|
||||
return layout_lines(ret.lines)
|
||||
return LayoutContent(ret.lines)
|
||||
|
||||
return None
|
||||
|
||||
def click(
|
||||
self, click: Tuple[int, int], wait: bool = False
|
||||
) -> Optional[LayoutLines]:
|
||||
) -> Optional[LayoutContent]:
|
||||
x, y = click
|
||||
return self.input(x=x, y=y, wait=wait)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user