The pairing PIN waits to be asked for
Device info is the page you open to read your radio out to somebody, the one that gets screenshotted into a forum thread and left up while a colleague leans over your shoulder. Every value on it is yours to give away except one: the BLE pairing PIN is what lets another phone onto the radio, and it was sitting in the Identity section in plain digits alongside the node name and the coordinates. It now draws as the field rather than the value — six mask bullets, the same glyph a password prompt types in, promoted to marks.MASK_MARK so the two can never drift apart. Six is the width of the *field* (the setting's own bound is 999999), not of what is in it: masking per character is right while someone is typing and counting their own digits, and wrong for a stored secret, where it would say how many to guess. A device that reports no PIN keeps its "?" — an absence is not a secret, and a "?" behind bullets would promise something to uncover. `p` uncovers it, and `p` puts it back. A bare letter is a read-only page's idiom for its own verb (the Time Machine's `w`) and this page has no find-as-you-type to compete for it; the footer names what the press would do *now* — `p show PIN`, then `p hide PIN` — and on the console the same toggle is F3, `Reveal` then `Hide`, since the lane is the only place that platform can learn the key exists. A device with no PIN advertises neither: the atom drops and the slot stays empty, because the action isn't a thing on that page at all. The page is therefore its own screen now (ui/device_info_screen.py) rather than a generic result window, and ScrollScreen grows `replace_content` — replace_items for a read-only body — so the toggle swaps the page under a reader who keeps their scroll position; nothing reflows, six cells become six. The CLI has no key to press, so `meshterm info` and `config show` print it masked and `config get device_pin` is the way to name it deliberately: one rule, which is that the whole-device dump never carries the PIN. The editor one menu over still lists it plainly — you go there to change it, a row at a time, and its letters already belong to find-as-you-type. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1d951052bf
commit
42937ce411
8 files changed
+368
-10
No files matched your search
+25
-3
@@ -41,15 +41,37 @@ class InfoTool(Tool):
|
||||
Returns:
|
||||
A :class:`ToolResult` summarizing the device name.
|
||||
"""
|
||||
from ..ui.config_editor import cached_snapshot, config_table
|
||||
from rich.console import Group
|
||||
|
||||
from ..ui.config_editor import cached_snapshot, config_table, has_pin
|
||||
|
||||
device = await ctx.device()
|
||||
snapshot = await cached_snapshot(ctx, device)
|
||||
custom = await device.get_custom_vars()
|
||||
|
||||
# A live status panel (role, firmware, battery, clock, radio/packet statistics)
|
||||
# above every current setting with a short explanation of each.
|
||||
ctx.ui.show(await _status_panel(device, snapshot), Text(""), config_table(snapshot, custom))
|
||||
# above every current setting with a short explanation of each. The panel is read
|
||||
# once; only the table differs between the two states, and it is cheap to rebuild.
|
||||
panel = await _status_panel(device, snapshot)
|
||||
|
||||
def page(reveal_pin: bool) -> Group:
|
||||
return Group(panel, Text(""), config_table(snapshot, custom, reveal_pin=reveal_pin))
|
||||
|
||||
# In the menu the page is its own screen, because it has a key of its own: the
|
||||
# pairing PIN is masked until ``p`` uncovers it. On the CLI there is no keyboard to
|
||||
# press, so the dump prints as it always did — with the PIN masked there too, and
|
||||
# ``meshterm config get device_pin`` as the way to name it deliberately.
|
||||
session = getattr(ctx.ui, "session", None)
|
||||
if session is None:
|
||||
ctx.ui.show(page(False))
|
||||
else:
|
||||
from ..ui.device_info_screen import DeviceInfoScreen
|
||||
|
||||
await session.run_screen(
|
||||
DeviceInfoScreen(
|
||||
session, page, title=self.title, conceals=has_pin(snapshot)
|
||||
)
|
||||
)
|
||||
|
||||
return ToolResult(summary={"name": snapshot.get("name")})
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ from ..core.device_config import (
|
||||
settings_by_category,
|
||||
)
|
||||
from ..platforms import Platform, on_platform
|
||||
from .marks import MASK_MARK
|
||||
from .menus import (
|
||||
confirm_discard,
|
||||
exit_rows,
|
||||
@@ -205,6 +206,8 @@ def config_table(
|
||||
snapshot: dict,
|
||||
custom: dict[str, str],
|
||||
pending: Optional[dict[str, Any]] = None,
|
||||
*,
|
||||
reveal_pin: bool = False,
|
||||
) -> Table:
|
||||
"""Build the full configuration table, overlaying any staged changes.
|
||||
|
||||
@@ -212,6 +215,13 @@ def config_table(
|
||||
snapshot: Device snapshot from ``build_snapshot``.
|
||||
custom: Current custom variables.
|
||||
pending: Optional staged changes (setting key -> new value).
|
||||
reveal_pin: Whether to print the BLE pairing PIN. It is concealed by default —
|
||||
this table is the whole-device dump, the thing that gets read over a
|
||||
shoulder, screenshotted, and pasted into a bug report, and a pairing code is
|
||||
the one value on it that lets someone else's phone onto the radio. The
|
||||
Device info page reveals it on a keypress (see
|
||||
:class:`~meshterm.ui.device_info_screen.DeviceInfoScreen`); the CLI names it
|
||||
one value at a time with ``meshterm config get device_pin``.
|
||||
|
||||
Returns:
|
||||
A Rich :class:`Table` of every setting's current (and staged) value plus custom
|
||||
@@ -259,10 +269,10 @@ def config_table(
|
||||
header.append("")
|
||||
table.add_row(*row_of(header, ""))
|
||||
for spec in specs:
|
||||
row = [
|
||||
_fold_label(spec.label, describe),
|
||||
_fold_value(format_value(spec, spec.getter(snapshot)), describe),
|
||||
]
|
||||
current = format_value(spec, spec.getter(snapshot))
|
||||
if spec.key == PIN_KEY and not reveal_pin:
|
||||
current = conceal(current)
|
||||
row = [_fold_label(spec.label, describe), _fold_value(current, describe)]
|
||||
if show_staged:
|
||||
row.append(
|
||||
_fold_value(format_value(spec, pending[spec.key]), describe)
|
||||
@@ -282,6 +292,36 @@ def config_table(
|
||||
return table
|
||||
|
||||
|
||||
#: The one setting this table holds back: the BLE pairing PIN (the firmware reports it as
|
||||
#: ``ble_pin``; :data:`~meshterm.core.device_config.SETTINGS` keys it ``device_pin``).
|
||||
PIN_KEY = "device_pin"
|
||||
|
||||
|
||||
#: How wide a concealed PIN draws: the width of the *field*, taken from the setting's own
|
||||
#: upper bound (999999), not the length of the value sitting in it. A typed password masks
|
||||
#: per character because the typist needs to count what they have entered; a stored one
|
||||
#: shows its field, so the row can't be read for how many digits to guess — and a PIN of
|
||||
#: ``0`` behind a single bullet would have read as a stray dot rather than a covered value.
|
||||
_PIN_WIDTH = len(str(get_spec(PIN_KEY).maximum))
|
||||
|
||||
|
||||
def conceal(value: str) -> str:
|
||||
"""``value`` as a row of :data:`_PIN_WIDTH` mask bullets.
|
||||
|
||||
A value the device could not report is not a secret, it is an absence: ``format_value``
|
||||
already renders that as ``?``, and a ``?`` behind bullets would claim there is
|
||||
something to reveal when there is nothing. So only a real value is concealed, and
|
||||
:func:`has_pin` asks the same question the other way round — a page with nothing to
|
||||
conceal advertises no key for it.
|
||||
"""
|
||||
return value if value == "?" else MASK_MARK * _PIN_WIDTH
|
||||
|
||||
|
||||
def has_pin(snapshot: dict) -> bool:
|
||||
"""Whether ``snapshot`` carries a PIN at all — i.e. whether concealing it means anything."""
|
||||
return get_spec(PIN_KEY).getter(snapshot) is not None
|
||||
|
||||
|
||||
#: Cell caps for the table's two narrow lanes, indent included. Sized to the widest label
|
||||
#: and value the settings actually carry, less the few longest — the four that overflow
|
||||
#: fold onto a second line, which costs nothing on rows whose description was wrapping
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""The Device info page: everything the companion reports, its pairing PIN concealed.
|
||||
|
||||
The page itself is the ``info`` tool's — a live status panel over the full configuration
|
||||
table (see :mod:`meshterm.tools.info`). What lives here is the one thing that page does
|
||||
*beyond* scrolling: it holds the BLE pairing PIN back behind a row of mask bullets until
|
||||
the reader asks for it.
|
||||
|
||||
Why this page and not the editor: this is the whole-device dump, the screen someone opens
|
||||
to read their radio out to somebody else, screenshots for a forum post, or leaves up while
|
||||
a colleague looks over their shoulder — and the pairing PIN is the one value on it that
|
||||
lets another phone onto the radio. The editor one menu over still lists it plainly: you go
|
||||
there to *change* it, a row at a time, and its letters already belong to find-as-you-type
|
||||
so there is no key left to reveal with.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from rich.console import RenderableType
|
||||
|
||||
from .tui.screen import ScrollScreen
|
||||
|
||||
#: The key that reveals the PIN, and the word the footer names it by. A bare letter is the
|
||||
#: idiom for a read-only page's own verb (the Time Machine's ``w``); ``p`` is the value it
|
||||
#: uncovers, and nothing else on a scrolling body claims it — the page has no
|
||||
#: find-as-you-type, and ``^P`` (paths) is a different key.
|
||||
REVEAL_KEY = "p"
|
||||
|
||||
|
||||
class DeviceInfoScreen(ScrollScreen):
|
||||
"""The Device info result window, plus a keypress that uncovers the pairing PIN.
|
||||
|
||||
Everything else is :class:`~meshterm.ui.tui.screen.ScrollScreen`: the body scrolls,
|
||||
Esc closes, the pager rides F4/F5. The reveal is a *toggle* on one key and one chip —
|
||||
press it again and the bullets come back — because a reader who uncovered a secret to
|
||||
read it out wants to put it away without leaving the page.
|
||||
|
||||
The body is rebuilt through the caller's ``build`` rather than patched in place, so
|
||||
this screen never has to know what the page is made of. Both states are kept once
|
||||
built: the two differ by six cells and nothing reflows, so the scroll position stays
|
||||
exactly where the reader left it across a press.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session, # noqa: ANN001 - TuiSession, imported lazily to avoid a cycle
|
||||
build: Callable[[bool], RenderableType],
|
||||
*,
|
||||
title: str = "",
|
||||
conceals: bool = True,
|
||||
) -> None:
|
||||
"""Wrap the page in its screen.
|
||||
|
||||
Args:
|
||||
session: The running TUI session (repainted when the PIN is toggled).
|
||||
build: Renders the page body for ``reveal_pin``; called once per state.
|
||||
title: Heading for the window.
|
||||
conceals: Whether there is a PIN to conceal at all. ``False`` — a device that
|
||||
never reported one — leaves the page with nothing to reveal, so neither
|
||||
the footer nor the lane offers a key that would do nothing.
|
||||
"""
|
||||
super().__init__(build(False), title=title)
|
||||
self._session = session
|
||||
self._build = build
|
||||
self._conceals = conceals
|
||||
self._revealed = False
|
||||
self._bodies: dict[bool, RenderableType] = {False: self._renderable}
|
||||
|
||||
@property
|
||||
def footer_hint(self) -> str: # type: ignore[override]
|
||||
"""Scrolling, then the reveal, then Esc — the standard's order.
|
||||
|
||||
The reveal atom names what the press does *now* (``show``/``hide``), not what the
|
||||
screen is showing, so it is never a restatement of the bullets already on screen.
|
||||
"""
|
||||
atoms = []
|
||||
if self.content_overflows:
|
||||
atoms.append("↑↓ PgUp/PgDn scroll")
|
||||
if self._conceals:
|
||||
atoms.append(f"{REVEAL_KEY} {'hide' if self._revealed else 'show'} PIN")
|
||||
atoms.append("Esc close" if self.floating else "Esc back")
|
||||
return " · ".join(atoms)
|
||||
|
||||
@property
|
||||
def fkey_lane(self):
|
||||
"""The shared pager, plus the reveal on F3 — this screen's own verb slot.
|
||||
|
||||
The chip is the only place the PicoCalc can learn the key exists, and it names the
|
||||
action rather than its object: ``Reveal`` while the PIN is masked, ``Hide`` once it
|
||||
is out, both inside the six cells a chip has. A device with no PIN leaves the slot
|
||||
*empty* rather than dim — the action isn't a thing on this page at all.
|
||||
"""
|
||||
from .tui.fkeys import FPair, default_lane
|
||||
|
||||
lane = list(default_lane(nav=self.content_overflows))
|
||||
if self._conceals:
|
||||
lane[2] = FPair("Hide" if self._revealed else "Reveal", "reveal")
|
||||
return lane
|
||||
|
||||
def handle(self, action: str, data: str = "") -> None:
|
||||
"""Toggle the PIN, or scroll/dismiss as any result window does.
|
||||
|
||||
The key and the lane's chip dispatch into the same branch — one behaviour, two ways
|
||||
in, so the chip is not a second implementation of the letter.
|
||||
"""
|
||||
if action == "reveal" or (action == "text" and data.lower() == REVEAL_KEY):
|
||||
if not self._conceals:
|
||||
return
|
||||
self._revealed = not self._revealed
|
||||
body = self._bodies.get(self._revealed)
|
||||
if body is None:
|
||||
body = self._bodies[self._revealed] = self._build(self._revealed)
|
||||
self.replace_content(body)
|
||||
self._session.invalidate()
|
||||
return
|
||||
super().handle(action, data)
|
||||
@@ -38,6 +38,13 @@ REPEATER_MARK = ("▲", "#a78bfa")
|
||||
NODE_MARK = ("●", "#f472b6")
|
||||
UNKNOWN_MARK = ("○", "node.unknown")
|
||||
|
||||
#: THE concealed character: one bullet stands in for one character of something the screen
|
||||
#: is holding back — a password being typed (``ui/tui/prompt.py``), the BLE pairing PIN on
|
||||
#: the Device info page. One glyph for the concept, so a row of them reads the same way
|
||||
#: wherever it appears, and it is deliberately *not* the ``●`` of an unread message or a
|
||||
#: node: those are marks that mean something, this one means "not shown".
|
||||
MASK_MARK = "•"
|
||||
|
||||
#: Sentinels naming a path's endpoints on the route graph. ``\x00`` never appears in a
|
||||
#: node hash, so they can share the node-id namespace without colliding with one.
|
||||
SRC_NODE = "\x00src"
|
||||
|
||||
@@ -13,6 +13,7 @@ from rich.cells import cell_len
|
||||
from rich.console import Group, RenderableType
|
||||
from rich.text import Text
|
||||
|
||||
from ..marks import MASK_MARK
|
||||
from .render import render_lines, right_aligned_tail
|
||||
from .screen import Screen
|
||||
from .spinner import Spinner
|
||||
@@ -189,7 +190,7 @@ class _LineEditor:
|
||||
"""
|
||||
if slots is not None:
|
||||
return self._render_slots(slots)
|
||||
shown = "•" * len(self.text) if mask else self.text
|
||||
shown = MASK_MARK * len(self.text) if mask else self.text
|
||||
text = Text("› ", style="accent")
|
||||
for i, ch in enumerate(shown):
|
||||
over = overflow_at is not None and i >= overflow_at
|
||||
@@ -213,7 +214,7 @@ class _LineEditor:
|
||||
for i in range(slots):
|
||||
if i:
|
||||
text.append(" ")
|
||||
glyph = "•" if i < filled else "·"
|
||||
glyph = MASK_MARK if i < filled else "·"
|
||||
if i == self.cursor:
|
||||
text.append(glyph, style="reverse") # the active slot
|
||||
elif i < filled:
|
||||
|
||||
@@ -641,6 +641,18 @@ class ScrollScreen(Screen):
|
||||
verb = "Esc close" if self.floating else "Esc back"
|
||||
return f"↑↓ PgUp/PgDn scroll · {verb}" if self.content_overflows else verb
|
||||
|
||||
def replace_content(self, renderable: RenderableType) -> None:
|
||||
"""Swap the body in place, keeping the reader where they were.
|
||||
|
||||
The select list's :meth:`~meshterm.ui.tui.select.SelectScreen.replace_items` for a
|
||||
read-only body: the same page with something in it changed — a value uncovered, a
|
||||
figure refreshed — is still the page the reader was part-way down, so the scroll
|
||||
offset stands. (It is clamped on the next scroll anyway, so a body that did change
|
||||
height cannot strand the viewport past its end.) Rebuild the screen instead when
|
||||
what is on it is genuinely a different subject.
|
||||
"""
|
||||
self._renderable = renderable
|
||||
|
||||
def render_body(self, width: int) -> list[str]:
|
||||
"""Render the wrapped content to ANSI lines and remember the total count."""
|
||||
lines = render_lines(self._renderable, width)
|
||||
|
||||
+113
-1
@@ -27,11 +27,14 @@ from meshterm.ui.config_editor import (
|
||||
_parse_coords,
|
||||
_valid_coords,
|
||||
config_table,
|
||||
has_pin,
|
||||
contact_share_url,
|
||||
device_actions,
|
||||
edit_config,
|
||||
send_advert,
|
||||
)
|
||||
from meshterm.ui.device_info_screen import DeviceInfoScreen
|
||||
from meshterm.ui.marks import MASK_MARK
|
||||
from meshterm.ui.theme import active_theme
|
||||
from meshterm.ui.tui.prompt import TypedConfirmDialog
|
||||
from meshterm.ui.tui.screen import CANCEL
|
||||
@@ -65,7 +68,7 @@ _SNAPSHOT = {
|
||||
"name": "Homestead-Hub",
|
||||
"adv_lat": 45.5017,
|
||||
"adv_lon": -73.5673,
|
||||
"pin_code": 123456,
|
||||
"ble_pin": 123456,
|
||||
"radio_freq": 869525,
|
||||
"radio_bw": 250,
|
||||
"radio_sf": 11,
|
||||
@@ -146,6 +149,115 @@ def test_config_table_drops_the_description_lane_beside_a_staged_column() -> Non
|
||||
assert "STAGED" in header and "DESCRIPTION" not in header
|
||||
|
||||
|
||||
# -- the concealed pairing PIN --------------------------------------------------
|
||||
|
||||
|
||||
class _StubSession:
|
||||
"""A minimal session: the screen only ever asks it to repaint."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.repaints = 0
|
||||
|
||||
def invalidate(self) -> None:
|
||||
self.repaints += 1
|
||||
|
||||
|
||||
def _pin_row(rendered) -> str: # noqa: ANN001 - lines or an already-joined render
|
||||
"""The Device PIN row out of a rendered table or screen body."""
|
||||
return next(line for line in _plain(rendered).split("\n") if "Device PIN" in line)
|
||||
|
||||
|
||||
def _info_screen(snapshot: dict) -> DeviceInfoScreen:
|
||||
return DeviceInfoScreen(
|
||||
_StubSession(),
|
||||
lambda reveal: config_table(snapshot, {}, reveal_pin=reveal),
|
||||
title="Device info",
|
||||
conceals=has_pin(snapshot),
|
||||
)
|
||||
|
||||
|
||||
def test_the_pin_is_bullets_until_it_is_asked_for() -> None:
|
||||
# One bullet per digit — the app's own mask glyph, the one a password prompt types in.
|
||||
masked = _pin_row(_table_lines(72))
|
||||
assert f"{MASK_MARK * 6}" in masked
|
||||
assert "123456" not in masked
|
||||
assert "123456" in _pin_row(_table_lines(72, reveal_pin=True))
|
||||
|
||||
|
||||
def test_the_mask_shows_the_field_not_the_pins_length() -> None:
|
||||
# A stored secret draws its field: a one-digit PIN behind one bullet would have told
|
||||
# you how many digits to guess (and read as a stray dot besides).
|
||||
short = {**_SNAPSHOT, "ble_pin": 0}
|
||||
console = Console(
|
||||
width=72, file=io.StringIO(), theme=active_theme(), legacy_windows=False
|
||||
)
|
||||
with console.capture() as capture:
|
||||
console.print(config_table(short, {}))
|
||||
assert MASK_MARK * 6 in _pin_row(capture.get())
|
||||
|
||||
|
||||
def test_a_device_with_no_pin_has_nothing_to_conceal() -> None:
|
||||
# "?" is an absence, not a secret: masking it would claim there is something behind it.
|
||||
snapshot = {k: v for k, v in _SNAPSHOT.items() if k != "ble_pin"}
|
||||
console = Console(
|
||||
width=72, file=io.StringIO(), theme=active_theme(), legacy_windows=False
|
||||
)
|
||||
with console.capture() as capture:
|
||||
console.print(config_table(snapshot, {}))
|
||||
assert "?" in _pin_row(_plain(capture.get()).split("\n"))
|
||||
assert not has_pin(snapshot)
|
||||
|
||||
|
||||
def test_p_uncovers_the_pin_and_puts_it_back() -> None:
|
||||
screen = _info_screen(_SNAPSHOT)
|
||||
assert MASK_MARK in _pin_row(screen.render_body(72))
|
||||
|
||||
screen.handle("text", "p")
|
||||
assert "123456" in _pin_row(screen.render_body(72))
|
||||
|
||||
screen.handle("text", "p") # the same key puts it away again
|
||||
assert MASK_MARK in _pin_row(screen.render_body(72))
|
||||
|
||||
|
||||
def test_the_lane_chip_and_the_key_are_one_behaviour() -> None:
|
||||
# The PicoCalc's chip dispatches the action the letter does, not a second copy of it.
|
||||
screen = _info_screen(_SNAPSHOT)
|
||||
assert screen.fkey_lane[2].label == "Reveal"
|
||||
|
||||
screen.handle("reveal")
|
||||
assert "123456" in _pin_row(screen.render_body(72))
|
||||
assert screen.fkey_lane[2].label == "Hide"
|
||||
|
||||
|
||||
def test_the_footer_names_what_the_press_would_do_now() -> None:
|
||||
screen = _info_screen(_SNAPSHOT)
|
||||
assert "p show PIN" in screen.footer_hint
|
||||
screen.handle("text", "p")
|
||||
assert "p hide PIN" in screen.footer_hint
|
||||
|
||||
|
||||
def test_a_page_with_no_pin_advertises_no_key_for_it() -> None:
|
||||
# The standing rule: a footer never names a key that would do nothing, and a lane slot
|
||||
# for an action this screen doesn't have stays empty rather than dim.
|
||||
screen = _info_screen({k: v for k, v in _SNAPSHOT.items() if k != "ble_pin"})
|
||||
assert "PIN" not in screen.footer_hint
|
||||
assert screen.fkey_lane[2] is None
|
||||
screen.handle("text", "p")
|
||||
assert screen.fkey_lane[2] is None
|
||||
|
||||
|
||||
def test_uncovering_the_pin_keeps_the_reader_where_they_were() -> None:
|
||||
# Nothing reflows — six cells become six cells — so the scroll must not jump.
|
||||
screen = _info_screen(_SNAPSHOT)
|
||||
screen.note_viewport(10)
|
||||
screen.render_body(72)
|
||||
screen.scroll_lines(8)
|
||||
where = screen.scroll
|
||||
screen.handle("text", "p")
|
||||
assert screen.scroll == where
|
||||
assert len(screen.render_body(72)) == len(_table_lines(72))
|
||||
|
||||
|
||||
# -- contact share URL ----------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -62,10 +62,12 @@ from meshterm.ui.about import (
|
||||
support_project,
|
||||
)
|
||||
from meshterm.ui.chat import ChatScreen
|
||||
from meshterm.ui.config_editor import config_table, has_pin
|
||||
from meshterm.ui.contactlist import SORT_COLUMNS, SORT_OPENS_ASCENDING
|
||||
from meshterm.ui.contacts_screen import ContactsScreen
|
||||
from meshterm.ui.courier_screen import CourierOutboxScreen
|
||||
from meshterm.ui.dashboard_screen import DashboardScreen
|
||||
from meshterm.ui.device_info_screen import DeviceInfoScreen
|
||||
from meshterm.ui.livefeed_screen import LiveFeedScreen
|
||||
from meshterm.ui.map_render import MapMarker
|
||||
from meshterm.ui.map_screen import MapScreen
|
||||
@@ -486,6 +488,49 @@ def _tx_sweep(cols: int, rows: int) -> Screen:
|
||||
return screen
|
||||
|
||||
|
||||
#: A device snapshot shaped like a companion's, for the Device info page. Every field the
|
||||
#: config table draws, including the pairing PIN it conceals.
|
||||
_DEVICE_SNAPSHOT = {
|
||||
"name": "Homestead-Hub",
|
||||
"adv_lat": 45.5017,
|
||||
"adv_lon": -73.5673,
|
||||
"ble_pin": 123456,
|
||||
"radio_freq": 869525,
|
||||
"radio_bw": 250,
|
||||
"radio_sf": 11,
|
||||
"radio_cr": 5,
|
||||
"tx_power": 22,
|
||||
"max_tx_power": 30,
|
||||
"airtime_factor": 1.0,
|
||||
"rx_delay": 0.0,
|
||||
"manual_add_contacts": 0,
|
||||
"autoadd_config": 0,
|
||||
"flood_scope": "",
|
||||
"adv_loc_policy": 1,
|
||||
"multi_acks": 0,
|
||||
"telemetry_mode_base": 1,
|
||||
"telemetry_mode_loc": 0,
|
||||
"telemetry_mode_env": 0,
|
||||
"path_hash_mode": 1,
|
||||
}
|
||||
|
||||
|
||||
def _device_info(cols: int, rows: int) -> Screen:
|
||||
return DeviceInfoScreen(
|
||||
_GallerySession(cols, rows),
|
||||
lambda reveal: config_table(_DEVICE_SNAPSHOT, {}, reveal_pin=reveal),
|
||||
title="Device info",
|
||||
conceals=has_pin(_DEVICE_SNAPSHOT),
|
||||
)
|
||||
|
||||
|
||||
def _device_info_revealed(cols: int, rows: int) -> Screen:
|
||||
# The same page after ``p``: the widest state of the row the other case masks.
|
||||
screen = _device_info(cols, rows)
|
||||
screen.handle("text", "p")
|
||||
return screen
|
||||
|
||||
|
||||
def _about_meshterm(cols: int, rows: int) -> Screen:
|
||||
return AboutPage("About MeshTerm", about_meshterm())
|
||||
|
||||
@@ -522,6 +567,8 @@ _ENTRIES: list[_Entry] = [
|
||||
_Entry("packet_viewer", _packet_viewer),
|
||||
_Entry("trace", _trace),
|
||||
_Entry("tx_sweep", _tx_sweep),
|
||||
_Entry("device_info", _device_info),
|
||||
_Entry("device_info_revealed", _device_info_revealed),
|
||||
_Entry("about_meshterm", _about_meshterm),
|
||||
_Entry("about_author", _about_author),
|
||||
_Entry("join_discord", _join_discord),
|
||||
|
||||
Reference in new issue
Block a user