Buttons read Cancel then Quit, with Quit highlighted so Enter commits it
and Esc cancels. The question is orange, the selected button cyan, and the
idle button gray, so the current choice is unmistakable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The braille base map leans on glyphs a terminal font may lack; the terminal
draws each missing glyph as a double-width fallback, which shoves the row and
smears the panel's right padding and border. prompt_toolkit's differential
paint models those cells as single-width, so its picture of the screen diverges
from the terminal and it never rewrites the smear.
The previous fix forced a full-frame repaint every paint while in braille mode,
which the 1s header-refresh tick turned into a visible periodic flicker. Replace
it with a targeted scrub: after a move, force just the two right-edge columns
(padding + border) to repaint by poking a sentinel into pt's cached frame. The
map body redraws wholesale as it pans, so its interior self-heals; only the
static edge needed help. Block mode can't smear, so it scrubs nothing.
Also:
- Persist the braille/block mode (app_state.map_block) so the map reopens the
way it was left; braille stays the default.
- Blank the frame on exit (drop pt's cached frame once) so the menu drawn
underneath doesn't inherit leftover braille garbage.
- Reword the t hint to "toggle braille/block".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Choosing "quit" or pressing Esc at the top menu asked to leave through a
horizontal Yes/No toggle that sat at odds with the rest of the TUI, which is
select lists everywhere else. Replace it with a small select dialog in that
same style: "Quit and disconnect" is the highlighted default so Enter leaves,
while Esc — or choosing "Stay" — returns to the menu.
select() grows optional filterable/footer_hint passthroughs so this short,
fixed choice skips type-to-filter and shows a tailored "Enter quit · Esc stay"
hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The braille base map and its labels lean on Unicode a terminal font may not
have: 8-dot braille (U+2840-28FF) that many fonts omit, and place/node names
in scripts or emoji outside a Latin monospace font. A missing glyph renders as
double-width tofu, which both looks wrong and shoves the panel right border.
- Prefer a romanized name (name:latin/name:en) over the local script, and strip
labels to single-cell characters, so foreign names and emoji node names can no
longer tofu or knock the grid out of alignment.
- Add a block-element base-map mode, toggled with t: 2x2 quadrant blocks instead
of 2x4 braille - half the vertical resolution, but glyphs essentially every
monospace font has. Braille stays the default.
- Zoom with PageUp/PageDown as well as +/-.
- While braille mode is active, force a full repaint each paint so the
double-width-fallback corruption on the right border is scrubbed on every move.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make sticky headers a reusable Screen feature and lift paging and
section jumps onto the base Screen, so every scrollable surface shares
one implementation: the frame records each render's body height and
viewport, and the shared helpers page by a screenful, clamp to the
content, and walk between the boundaries a screen records in
_sticky_headers (a select list's group dividers, the chat transcript's
day dividers).
Remap keys to match a good editor:
- PageUp/PageDown scroll by a screenful rather than a fixed step.
- Ctrl+Home/End reach the top/bottom (the live tail in chat).
- Ctrl+PageUp/PageDown jump to the current/previous and next section.
- In chat, Home/End and Ctrl+Left/Right act on the compose line; word
motion (start of current word, else previous/next) is added to the
shared line editor, so every text prompt gains it too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Color-code the chat picker rows: a contact's dot takes that person's
chat hue (filled once talked-to, hollow before), previews light their
sender names and @mentions in-hue, and a relative age column sits
between the unread badge and the message text.
Pin a grouped list's section heading to the top row once it scrolls off
(sticky header in the frame), add PageUp/PageDown to SelectScreen, and
drop arrow wraparound in the conversation picker so it doesn't jump
across the section dividers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app sometimes hung on exit. The device/async teardown is clean; the
culprit is prompt_toolkit's Windows input reader, which runs on a
non-daemon executor thread blocked in a Win32 wait. In a rare teardown
race that thread stays blocked and the interpreter's atexit thread joins
hang the process after cleanup is already done.
Arm a daemon watchdog once the user commits to quitting: if a clean exit
doesn't complete within a grace period it flushes and os._exit(0)s. A
daemon thread still runs while the main thread is stuck in that join, so
it always terminates. The happy path is untouched (the process is gone
before the timer fires) and committed DB writes are durable regardless.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrapped message bodies now align their continuation lines under the body
of the first line instead of falling back to the timestamp gutter, via a
new render_hanging helper. And an @[Name] token primed by the reply flow
renders as a bare @Name in that sender's stable hue, so a mention reads
cleanly and is tied to the person it names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the static ⏳ hourglass on an in-flight direct message with the
reusable tui.Spinner, animated for the whole wait-for-ack window:
- _delivery_glyph is now a method returning the spinner's current frame when
acked is None (✅/❌ once resolved); the _SENDING constant is gone.
- _spin_while wraps the awaited send and retry, running a background ticker
that advances the shared spinner and repaints, then cancels-and-awaits it
(same no-stray-task discipline as the startup splash). Only one send or
retry is ever in flight (both gated by _sending), so one spinner suffices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework the startup device picker into a self-contained splash flow:
- probe_meshcore (renamed from smoke_test_meshcore) now leaves the confirmed
connection open and returns it, so AppContext.adopt_device reuses that live
radio instead of reopening (many boards reset on each serial open). Restore
the library's normal per-command timeout once the handshake succeeds.
- Extract the Braille animation into a reusable tui.Spinner; BusyScreen now
delegates to it.
- Pin the wordmark to a fixed vertical anchor so it never shifts as the box
swaps between the device list, spinner, and messages.
- Esc or a trailing Quit row leaves the splash and exits the program.
- Show the copyright only on the opening splash; retire it once a device is
chosen, even across a failed smoke test and re-pick.
- Disable prompt_toolkit's loop exception handler and route asyncio (and the
awaited spinner ticker) errors to the file log, so a stray background-task
error can't dump a traceback over the full-screen UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reserve the "MeshCore device" tag for hardware we have actually talked to,
rather than guessing from the USB vendor ID:
- DeviceStore now keeps a registry of every confirmed device (keyed by
stable serial id), remembered forever, migrating the old single-record
file transparently.
- Selecting a device on the splash runs an immediate smoke test
(smoke_test_meshcore): a genuine companion is remembered and confirmed;
anything else re-prompts with a message to choose another. The probe uses
a short handshake window so a non-MeshCore port is rejected cleanly in a
few seconds instead of blocking on meshcore's full default timeout.
- The probe renders in place on the chromeless splash via a new BusyScreen
(animated Braille spinner) driven by session.busy_startup, so the wordmark
and box stay put instead of a separate progress dialog.
- Silence the meshcore library's console logging (its import calls
logging.basicConfig, which printed "INFO:meshcore:..." over the TUI):
route it to the JSON-lines file log with a NullHandler guard on root.
- The devices tool now marks confirmed devices and stops asserting "LoRa"
before a device is proven.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The companion-device picker no longer borrows the in-menu frame. It is now a
chromeless splash: the MeshTerm wordmark, centered, above a box sized to the
device list rather than stretched across the terminal, with no header/footer
status bars.
A new `chrome` flag on Screen routes a base screen to `frame.compose_startup`,
which draws the banner and centers a content-sized panel. The device rows are
laid out in aligned columns (name / port / vendor / tag) under a muted header,
and type-to-filter is disabled (new `filterable` flag on SelectScreen) since the
list is short and fixed. The no-devices case gets the same splash treatment.
The wordmark lives in a code constant (meshterm/ui/logo.py, generated from
ansilogo.txt) so it survives installation regardless of packaged data files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default view fitted every located node, so a few distant outliers
(here a node near Toronto, 500+ km from the Montréal core) zoomed the
whole mesh out to a continental scale. Frame the densest half instead —
the nodes nearest the median centre — so the map opens on a useful local
region; the rest simply fall off the edges. Verified against a live
159-node mesh: 0.5 lands on the Greater Montréal metro (~77 km across,
two-thirds of nodes visible) rather than eastern Canada.
Viewport.fit gains a `fraction` argument (default 1.0, unchanged for
existing callers); the map's opening view and `r` reset pass 0.5, and
`meshterm map --fraction/-f` exposes the knob on the CLI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The picker now lists only real discovered devices. Cancelling with Esc
still falls back to --mock / configure-later, so no behavior is lost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The devices tool doubled as a mid-session device switcher: its menu
prompt re-ran the picker and reassigned the active device. Drop that so
selection is a startup-only concern (or --port on the CLI); the tool is
now a read-only inventory in both the menu and CLI.
With the re-pick gone, startup is the sole caller of prompt_device, so
its cancel_label parameter (added only to swap in a "Back" label) is
removed and the "Skip…" label inlined.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the default config directory from ~/.config/meshterm (with
XDG_CONFIG_HOME support) to a plain ~/.meshterm dotfolder under the
OS-defined home directory.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the package directory, imports, brand strings, env-var prefixes,
console-script entry point, and the config/data dir (~/.config/meshterm).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The interactive map now reopens on the centre and zoom the user left it
at, instead of always refitting to the nodes. Adds a generic app_state
key/value table (schema v6) and typed repository accessors; MapScreen
restores a saved view on open and persists every pan/zoom. Pressing r
still refits to the nodes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A braille cell shows a single glyph, so nodes landing on the same spot
hid the crowd behind one marker. Count the nodes per cell and wash the
surviving glyph toward white on a log2 ramp, so a bright marker reads as
a busy cluster. Shape still encodes node type; only brightness encodes
count. A lone node keeps its exact base colour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Channel messages carry only a slot index on the wire, which the firmware
assigns from the channel's current position. The old idx->identity cache
was trusted on a hit, so a slot reorder or re-key (in-app, via the CLI/
config tool, or on another client) left the cache stale and misfiled
later messages under the wrong channel.
channel_id_for now reads the occupying slot fresh from the device on
every call, so identity always reflects who is actually in the slot right
now; _channel_ids is demoted to a priming/backfill warm map and a
read-failure fallback. Inbound recording moves onto a single serialized
worker draining an asyncio.Queue, so channel messages still persist in
arrival order despite each needing an async device read, and a slow read
can no longer stall the event pump.
Second root cause, same symptom: a Public message could still land in
another channel (e.g. #bots) even after the fresh-read fix. The meshcore
library's get_channel waits for "the next CHANNEL_INFO event" with no
correlation to the slot it asked for, and the dispatcher fans that event
to every in-flight waiter. Two concurrent get_channel calls therefore both
resolve on whichever response arrives first, and one caller silently
receives the other's channel — which fired whenever the chat worker's read
raced any other channel read (conversation-list builder, channels screen,
refresh_channels). Fixed in the device wrapper: a per-device asyncio.Lock
serializes channel reads so at most one is outstanding and the response is
unambiguously ours, plus get_channel now validates the response's
channel_idx matches the requested slot and raises rather than handing back
foreign channel data (the chat layer then falls back to the last-known
identity instead of misrouting).
Adds tests for filing by the current slot occupant after an out-of-band
reorder, for arrival-order preservation across a burst, and for the
serialized/index-validated channel read (no cross-talk, mismatch rejected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive map markers from the device's contact list, the authoritative
source for a node's name, type, and advertised position, enriching each
with reception detail from passive-monitor observations when overheard.
Contacts advertising 0.0/0.0 (null island) are treated as unlocated.
On crowded maps, draw every marker glyph but place labels highest-
priority first (self, then repeaters, then leaf nodes), dropping labels
that would collide so important ones win the space. Labels now also keep
a one-row vertical gap so dense areas don't silt into a block of text.
Add Shift+arrow / uppercase WASD to pan by a single character cell for
fine positioning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New `map` tool (menu + `meshtools map` CLI) that plots the mesh's
location-sharing nodes over a real OSM street basemap rendered as Unicode
braille — streets, rivers, water, and place names — the mapscii idea, but
able to plot our own nodes and live inside the TUI.
- core/mvt.py: dependency-free MVT/protobuf vector-tile decoder.
- core/geo.py: Web Mercator + a pannable/zoomable Viewport and tile math.
- services/basemap.py: OpenFreeMap vector tiles (free, no API key), resolved
from the versioned TileJSON, disk-cached, offline-safe (stdlib urllib only).
- ui/mapcanvas.py: colored braille canvas (per-cell priority, line raster,
polygon fill, collision-avoided labels), emits truecolor ANSI.
- ui/map_render.py: compositor — basemap styling + node markers with names.
- ui/map_screen.py: full-screen screen; wasd/arrows pan, =/- zoom, r reset,
Esc/q exit; background tile loading. Offline falls back to nodes on a grid.
- tools/map.py: interactive in the menu, one-shot static render on the CLI.
Repeaters are prioritised over leaf nodes via advert type: threaded `node_type`
through Observation/HeardNode and the persistence layer (schema v4->v5, with an
idempotent migration adding observations.node_type). Added
TuiSession.base_body_size() so a full-screen screen can size itself to the frame.
Tests: tests/test_map.py (24) cover MVT decode against a real cached tile
fixture, projection, the canvas, the compositor, offline behavior, and the
interactive pan/zoom/reset logic. Full suite: 261 passing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three interrelated improvements to the chat/channel experience:
Outgoing byte budget & unified transcript (ui/chat.py, ui/tui/prompt.py):
- Cap outgoing messages at MeshCore's UTF-8 limits (150B direct, 130B
channel) and block over-limit sends, keeping the buffer intact to trim.
- Show a live `used/limit` gauge in the compose bar whose used count steps
green→yellow→orange→red by bytes remaining, with truer yellow/orange hues.
- Color the overflow characters red in the compose line (multibyte-safe).
- Render direct chats with the same grouped, per-sender layout as channels
(headers, day dividers) instead of one line per message.
Direct-message delivery retry (services/chat_service.py, persistence,
core/models.py, tools/chat.py, ui/tui/session.py):
- Track outbound acked state (None awaiting / True / False) with a persisted
row_id, add resend_direct + update_chat_ack, and wire Ctrl-R to retry the
latest unacknowledged message in place with ⏳/✅/❌ delivery glyphs.
Channel slot capacity discovery (core/channels.py, core/connection.py,
ui/channels.py, tools):
- Discover the firmware's real channel-slot count via a bounded read-only
probe (Device.channel_capacity) instead of assuming 8; show used/total in
the manager and refresh the chat slot→identity cache after any mutation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rich counts a VS16 emoji like the sun as 2 columns (Unicode TR51), but
many terminals paint it as 1, so padded content and the panel border that
follows it landed a column early.
Add a startup calibration that decides the VS16 width in order of trust:
an env override, a fingerprint of split-renderer terminals (VS Code,
Windows Terminal) whose PTY cursor accounting disagrees with what they
paint, then a live cursor probe where that is valid. When the width is 1,
patch Rich's string measurement so every panel, table, and chat bubble
lines up while genuine wide emoji stay 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Store channel messages under a slot-independent channel identity (sha256 of
the effective secret) instead of the raw slot index, so reordering or re-keying
slots no longer scrambles history. DB schema v3->v4 adds a channel_id column
with an idempotent migration and a lazy backfill that maps legacy rows to the
channel currently in each slot.
Recognize the firmware's fixed-key default "Public" channel as public: add
DEFAULT_PUBLIC_SECRET / KNOWN_PUBLIC_SECRETS and split is_name_derived (key
reproducible from the name, so it can be re-derived on write) from
is_public_channel (shared meshwide). The default channel no longer mislabels
itself as private, and its well-known key is preserved on edit/reorder.
Lead each chat picker row with a glyph: the channel's openness marker
(# name-derived, globe fixed-key public, lock private) or, for a direct chat,
a speech balloon once we've exchanged messages and a person before. A shared
channel_glyph() keeps the channel manager and the picker in sync.
Round out the messaging UX: live callable row titles and plain-text filtering
in the TUI select screen, plus assorted device-picker, monitor, route-map, and
config-editor menu refinements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Improve the device picker and inventory in three ways:
- Remember a device's mesh node name. On a successful connect, learn the
radio's own name (adv_name/name, best-effort) and persist it with the
remembered device; the picker leads its row with it and the devices table
shows it. A failed probe never blocks the connection and preserves any
name already on file.
- Qualify devices in tiers instead of one over-generous "likely LoRa" flag.
A native-USB dev board is a strong "LoRa device"; a bare USB-UART bridge
(CP210x/CH340/FTDI) is only a "serial adapter" / "maybe", since the same
chip shows up on plenty of non-LoRa hardware. Sorting and the table follow
the board > bridge > unknown ranking; JSON gains a `confidence` field.
- Stop repeating the COM port. Windows descriptions often already end with
"(COM11)", so the label no longer renders "… (COM11) (COM11)".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the primitive channel editor that lived inside the config menu with
a dedicated `channels` tool: list the device's slots and, per channel, show a
scannable QR share code, reveal the key, open it in chat, rename/re-key, or
clear it. Create channels four ways — new private (random key), public `#`
(key derived from the name), join-by-key, or import a `meshcore://` link — with
every change written to the device immediately.
- core/channels.py: pure, hardware-free logic (secret derivation, tolerant key
parsing, share-URL round-tripping, channel hash). full_channel_hash exposes
the complete digest; the short channel_hash is its leading byte.
- ui/qr.py: half-block QR rendering (segno), drawn black-on-white so it scans
regardless of terminal theme.
- ui/channels.py: the interactive manager. Channels display by their real name;
reorder rows by dragging with the arrow keys; the key view shows the full hash
with the first byte highlighted.
- ui/tui: a ReorderScreen (grab/move/drop) plus a ctx.ui.reorder surface method.
- tools/channels.py: the tool + `channels list/add/join/import/share` CLI.
- Pin Chat + Channels to the top of the main menu via an explicit category order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce channel and direct messaging: an always-on ChatService that
records inbound messages to history, a live full-screen chat screen, and
a chat tool (interactive picker plus send/history/list/listen CLI). This
bumps the DB schema to v3 and adds the supporting models, repository
queries, and connection wiring.
Also polish the monitor/menu UX:
- Stop console log handlers from corrupting the full-screen TUI by
detaching the RichHandler for the life of the session (file logging
continues); most visible when toggling the monitor.
- Colour only the header's monitor glyph (green ● on, red ○ off).
- Crop the header to a single line instead of wrapping on narrow terminals.
- Drop the redundant toggle-confirmation window so toggling returns
straight to the main menu.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decouple listening from consuming as the first step toward a full MeshCore
client. A new always-on EventHub owns the single device subscription and fans
typed MeshEvents out to any number of subscribers; passive-monitor history
logging becomes just one subscriber, so listening is now unconditional while the
on/off toggle controls only persistence.
- core/events.py: MeshEvent envelope + EventKind (OBSERVATION/MESSAGE/ACK)
- services/event_hub.py: subscribe/stream/wait_for fan-out, always-on pump
- connection.py: Device.subscribe_observations -> unified subscribe_events
emitting observations, inbound messages, and acks; add message_from_event /
ack_from_event mappers and a simulated message stream on MockDevice
- core/models.py: Message and Ack domain models
- monitor_service.py: slim to a hub subscriber (recording, not listening)
- context.py/menu.py: wire ctx.events into the session lifecycle
- config.py: connect_on_start setting (default on) to allow lazy connect
- Remove monitor --seconds / foreground capture and Device.listen; monitoring
is passive/background only
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a UI surface abstraction (ctx.ui) that tools use for all input
and output instead of touching questionary or the console directly. Two
backends implement it: PlainUi for scripted CLI runs (prints immediately,
byte-for-byte unchanged) and TuiUi for the interactive menu (bounded,
scrollable result windows over a full-screen TuiSession).
The new meshtools/ui/tui package reimplements the slice of questionary the
app used — validated text input, confirm, autocomplete, and selectable
lists — plus framing, progress, and screen management. Swap the dependency
from questionary to prompt_toolkit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The info tool showed both the device config and the node/contact list. Move
the list into a dedicated nodes tool (Device category, right after info) so
each feature does one thing; info now shows identity and radio config only.
- nodes tool renders this node + contacts with the path-hash prefix
highlighted, reading self-info + contacts + path-hash mode directly.
- info drops the contacts read and nodes_table render (and its contacts
summary field); the nodes_table widget and its test are unchanged.
- README: add a Node list feature row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The monitor was a blocking listen window that monopolized the app. Convert
it to an always-on background subscription that logs overheard adverts/
telemetry while the user keeps using the menu.
- Device: add subscribe_observations() as the non-blocking capture primitive;
listen() is now a thin foreground wrapper over it. Mock streams a live
packet burst + cadence instead of a bounded synth loop.
- MonitorService (ctx.monitor): owns the subscription, session/total counters,
and the on/off preference; logs each packet as it arrives.
- MonitorStore: persist on/off in <config_dir>/monitor.json, default off,
remembered between sessions and auto-resumed at launch.
- Menu: the monitor entry is now a toggle/heard-nodes control panel; the
header shows live session/total counts and repaints ~1s via refresh_interval.
- CLI monitor gains --on/--off (preference) and keeps --seconds for a scripted
foreground capture.
- Add Repository.observation_count(); README + tests (129 pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
19 tests covering the persistence layer: field round-trip for successful
and failed traces (hops, RTT, tx_power, timestamp), recent_traces
ordering/limit/target filtering, all_traces cross-target span,
traced_targets count-ordering, latest_trace success_only variants,
and full run lifecycle (start → finish, error without summary, limit,
ordering).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces `route-map`, which turns stored traces into a directed mesh graph
with per-link traversal counts, median SNR, and route-churn metrics (stability
score, Shannon entropy, distinct-route count). Includes pyvis HTML renderer
and a refactored _hydrate_trace helper in repository.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Splits a target's persisted trace history into a recent window and an older
baseline and flags end-to-end (bottleneck SNR, success rate) and per-hop link
SNR metrics that dropped beyond a threshold, with warn/critical severity.
Adds Repository.recent_traces / traced_targets, a pure link_quality service,
and a `link-health` tool that can analyze stored history or take fresh traces
first. Also tidies the repository import ordering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a Device.listen capability (real + mock), an Observation/HeardNode
domain model, an observations table with Repository.record_observation /
heard_nodes aggregation, and a `monitor` tool that listens for a window and
logs every overheard advert/telemetry (SNR, RSSI, shared location) to the
database. This is the longitudinal substrate later features read back.
The real-hardware event-payload mapping is best-effort and, like the trace
mapping, flagged for validation against the firmware's event schema; the mock
device synthesizes plausible adverts so the tool and tests run without a radio.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Predict time-on-air, receiver sensitivity, max path loss, range, and
duty-cycle/dwell headroom for a LoRa radio config with no radio attached.
Radio params default to the connected device's config but every value is
overridable; a fully specified invocation never opens the serial port.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two correctness fixes for remote-admin TX optimization:
1. Reverse/round-trip measurement path. The optimizer traced a one-way path
ending at the (possibly far) target and waited for the target itself to
originate the trace reply, which never returns when the target is only
reachable via other repeaters, so every trace timed out. Now we trace out
to the target and back (the MeshCore A,B,A convention) so a reachable
near node answers; the target only forwards. Its received SNR is read from
its hop by hash (the round trip turn-around) rather than by position.
2. Restore original TX on no result. If not one trace gets through at any
level there is no meaningful winner, so leave the node at its original
power (applied=False) and warn, instead of writing a bogus value.
Mock updated to model each hop SNR from the node that transmitted into it and
to drop traces on the bottleneck hop. Added round-trip / target-hash tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove unused utcnow / HopEdge imports (F401).
- Assert DeviceCommandError instead of bare Exception in the admin-login test (B017).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tune the TX power of a remote node we have admin rights on (the hop before
a chosen target) to maximize the SNR the target receives from it, instead of
tuning the local device against a path bottleneck.
- Device gains admin_login / get_remote_tx_power / set_remote_tx_power
(MeshCoreDevice via send_login_sync + `set tx <dbm>` send_cmd; MockDevice
models target SNR as an inverted-U in the admin node's TX with a reliability
dropoff at the band edges).
- tx_optimizer: forced-path sweep with a lexicographic objective (reliability
first, then SNR), median-based levels, coarse+refine search, and a verify
pass; near-SNR ties break toward lower power. Outlier-robust by construction.
- AdminStore remembers per-node admin passwords in <config_dir>/admin.json
(owner-only perms), cleared automatically when a login is rejected.
- Configurable 12-28 dBi default range (tx_opt_min/tx_opt_max).
- tx-optimize tool: path/password prompts, live progress, persistence, applies
the winner on finish, and renders an interactive HTML chart.
- Rewrote TX result models, widgets, plot, repository.record_tx_sample, and the
TX tests for the new flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Annotate our own device with its hash on the route line, the per-trace
From→To table, and the per-hop medians table, truncated to the command's
path-hash width like every other hop. Unify link rendering on
_route_node_text so all three reads consistently as name (hash).
In the info panel, replace the standalone node-hash line with a Nodes
table: our node listed first with its name highlighted, and every node's
full public key shown with its path-hash prefix highlighted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trace:
- Auto-path now ends every route at the target's own hash (verified on
hardware: a trace only replies when its destination is the final hop), so
direct neighbors and learned multi-hop routes both resolve correctly.
- Retry the transient "no event received during contacts retrieval" blip and
surface a clean DeviceCommandError instead of a raw traceback.
- Route summary annotates each node with its hash at the command's path-hash
width, wrapping after an arrow when the line is too long.
- Default trace count changed from 5 to 3.
Also includes the in-progress device-config feature (config_io, device_config,
config tool, config editor UI, tests) and related supporting edits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MeshTools is a Rich+Questionary interactive / Typer-scriptable toolkit for
tuning a MeshCore mesh over a serial companion device, with SQLite-logged runs
and Plotly visualizations. Includes the skeleton (trace, info, history),
TX-power optimization, and a MockDevice simulator (--mock).
Device discovery & selection:
- core/discovery.py: enumerate serial ports (pyserial), flag/sort likely LoRa
hardware by USB vendor id, stable_id for replug-safe identity.
- core/device_store.py: remember the last successfully connected device in
<config_dir>/devices.json (independent of --db).
- core/selection.py: non-interactive resolver (--port > profile > remembered >
sole device > guidance error) for the CLI.
- ui/device_picker.py + menu startup: interactive picker preselecting the
remembered default; banner reflects the choice.
- tools/devices.py: `meshtools devices` lists/marks attached devices.
- AppContext resolves the port and records last-known-good on connect success.
Fixes:
- theme.py: force UTF-8 on stdout/stderr so glyphs don't crash legacy consoles.
- connection.py: friendly message when meshcore is missing; pin meshcore>=2.3.
- trace/tx-optimize: fall back to free-text target entry when the device
reports no contacts (autocomplete requires non-empty choices).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>