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:
parent
2cbb9efeca
commit
e9a1bcc951
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user