Four small UX gaps closed together:
- Dashboard's footer hint was missing Home/End even though the screen
already handled them.
- The packet viewer's paging keys were reversed relative to the list
that opens it: PgUp/PgDn walked packets while the dashboard feed
walks with up/down. Swapped so up/down page through packets (matching
the feed) and PgUp/PgDn scroll a tall body (matching every other
screen's scroll keys).
- A raw "packet" entry (an overheard RX_LOG_DATA frame) now shows its
parsed payload class and route, and — for a channel-text frame naming
a channel we hold the key for — its decrypted text. The AES/HMAC
logic lives in core/channels.py as pure, unit-tested crypto
(decrypt_channel_text), independent of the vendored meshcore
library's own reader internals; the dashboard fetches the device's
channel secrets once at open time and hands them down to each
PacketViewer it opens. Raw-field dumps now skip fields already folded
into a flavoured row instead of repeating them.
- The Time Machine's whole-mesh charts (packets/day, nodes/day, rhythm)
gained the same mirrored numeric y-axis the dashboard's activity
chart already had. That gutter-drawing logic moved into
braillechart.axis_chart, a shared widget, and the dashboard's own
activity chart was refactored to use it too instead of hand-rolling
its own copy.
meshterm/ui/channels.py's _read_slots is now public (read_channel_slots)
since the dashboard opener needs it too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The monitor seeds its six-hour activity buckets and its by-kind tallies
from stored observations at session start, so the header pulse, the
dashboard chart, and the traffic panel open mid-story after a restart
instead of counting from zero. A once-per-session sweep prunes
observations past the new history_days retention setting (default 365,
0 = keep forever), keeping the always-recording database bounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Queue a direct message for a node that isn't reachable right now and a
session-long background loop delivers it when the recipient is next
heard — or at a scheduled local time ("next 07:00", "in 3 h", any
HH:MM). Each attempt is an ordinary chat send (it lands in the
conversation history with its ack state, exactly as if typed); failures
back off exponentially and, after the first shot, wait until the node
is heard again, so an absent node is never hammered on faith. One
attempt per pass keeps a backlog draining politely, and after five
unacknowledged tries the courier gives up and says so. Outcomes raise
Watchtower alerts, so the header's badge lights when an overnight
delivery finally lands.
The outbox persists in courier.json (a queued message must survive
restarts or the promise is hollow), a Mesh-section screen carries the
queueing flow, per-entry Send now / Cancel, and the finished history,
and a scripted `courier <node> <text…> [--at HH:MM]` queues without
transmitting anything.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Star the nodes that matter (the roof repeater, the far gateway) and
session-long rules watch what the always-on hub already hears: a
silence alarm when a starred node goes quiet past its threshold
(latched per quiet spell, re-armed — with a recovery note — when it
returns), an SNR-sag warning when its receptions' median drops well
below the run before it, and a mesh-wide heads-up the first time a
never-before-seen node id appears (baselined against the whole stored
observation history, announced at most once ever). Nothing transmits.
Alerts persist in watchtower.json alongside the watchlist and rules,
show as a red ▲ badge in the header, and are read, acknowledged, and
configured in a new Mesh-section Watchtower screen. A remote-battery
rule was considered and dropped: observations carry no parsed battery
telemetry, so there is no honest data source for it yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new Device management entry for administering repeaters and room
servers. The shared credentialed-first picker (extracted from TX
optimize) chooses the node, login runs on remembered passwords with a
one-time prompt otherwise (forgotten on rejection), and the editor
lands in the local device-configuration screen's exact lanes — but
speaking the node's text CLI through a new public
Device.send_remote_command, which brings the repeater-only knobs the
local editor never had: TX delay, Direct TX delay, RX delay, airtime
factor, advert intervals, flood max, repeat, guest password.
Reads cost one paced mesh round trip each, so the editor never
bulk-reads on open: values show what the last read said, age-stamped,
from a new per-node remote.json cache; Read settings refreshes them
under an abortable progress dialog and Apply sends the staged sets
the same way, folding confirmed values back into the cache and
keeping refused ones staged. Advert, clock sync, password change, and
reboot run as confirmed popups.
The Command line is a readline-style screen onto the node's raw CLI:
per-node history persisted across sessions (↑↓ recall, adjacent-dupe
suppression, capped), Tab completion over the known verbs plus every
catalog get/set spelling with ghost-text preview, one command in
flight at a time. The simulator grows a full repeater CLI so all of
it drives against --mock, and a scripted one-shot
(repeater-admin <node> <command…>) covers automation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MeshTerm now announces the connected node by itself: a zero-hop advert
every 1/2/3/4 hours (default hourly) and a flood advert every
3/6/12/24/48/168 hours (default daily), each switchable off. The
cadences are staged and applied from a new Background adverts section
in the device-configuration editor, and persist per device (keyed by
public key) in adverts.json alongside the last-sent mark for each type
— so the countdown survives restarts, and a manual send from any
advert flow resets the same clock the scheduler reads.
The scheduler stays transmission-shy: at most one advert per 30 s pass
(a flood due together with a direct waits a pass), and a never-sent
device is armed to count from now rather than advertised immediately.
It holds no device subscription — each pass just skips when nothing is
connected — so reconnects need no special handling, and scripted CLI
runs never start it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An auto (device-routed) Trace target went on the air as a bare
destination hash — an observer confirmed no repeater hops — so any
target past a direct neighbour could never answer. Dumping the
radio's contact table showed why: every contact reports
out_path_len -1 (flood). MeshCore firmware only learns a contact's
out_path from two-way addressed traffic, and tracing repeaters
teaches it nothing, so "device-routed" has nothing to route with.
Auto now resolves a route the session can prove, in order: the
device's learned route when one genuinely exists, else the outbound
leg of the last successful stored walk (_previous_outbound extracts
it from the recorded boomerang), else a bare direct attempt. One
resolver feeds both the wire and the screen, so the planned route
line renders exactly what Trace will transmit, with its provenance
under it — (auto · device route), (auto · last trace · stamp), or
(auto · direct — no known route) — and the summary's path row says
the same instead of claiming a device route that never existed.
The connection layer's path-less fallback keeps the mirrored return
leg for contacts that do carry a learned route (the trace protocol
reflects nothing home), and its docstring now says the firmware
never routes a trace itself. Resolution verified against the real
database and radio: a multi-hop target resolves to its full stored
boomerang, the direct gateway stays a single-hop spec.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A trace is one walked path and "target" is pure UX (hardware-verified),
so the feature now splits along that line: Trace target keeps the
symmetric mirror-and-return flow (picker, Explore paths), while the new
Trace path skips the picker entirely and walks a hand-composed route —
no Auto, no Explore, Trace gated until a path exists, history recorded
under the "(path)" sentinel so composed walks never pollute the target
picker. The composer loses its mode toggle: a pinned target means
mirrored, no target means verbatim, and its preview opens as us → us.
Sampling returns, deliberately: a Sample count action (1/2/3/5/8) runs
that many traces per commit, paced by trace_cooldown_s between sends and
counted off on the in-flight dialog — never a burst. Actions reorder to
compose/explore/width/samples/trace with a Back row; the w/p/x hotkeys
retire.
Display: route nodes drop the underscore (prompt_toolkit draws U+00A0 as
"_", so name (hash) now joins with plain spaces), the composer renders
nodes exactly like the route line, the (previous · …) stamp gets its own
line, the path row ends with the displayed hop count, Explore rows lead
with the route instead of an "observed path" label, and the empty Traces
placeholder is gone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two long-standing flakes, one root cause each:
Quit with a serial companion often froze until the exit watchdog killed
the process. The meshcore dispatcher's stop() awaits queue.join(), but
its processor task exits after draining at most one event once stopped —
so a routine advert/RX-log burst (two+ events queued at that instant)
deadlocks the join forever (repro confirmed against the installed
library). MeshCoreDevice.disconnect now gives the graceful teardown two
seconds, then abandons it: the dispatcher task is cancelled synchronously
and the raw transport closed directly, so the port is still released and
even the forced path finishes well under the watchdog.
BLE startup often failed until the user re-selected the device. Connecting
by bare address makes bleak re-discover the peripheral with a fresh
internal scan, which on Windows intermittently misses a slow-advertising
companion. The discovery scan's live BLEDevice now rides along
(DiscoveredDevice -> probe/context -> create_ble) so the connect opens the
peripheral directly, and a transport-level ConnectionError is retried once
before surfacing — the manual re-select was never anything but a retry.
PIN/bond rejections still surface on the first attempt, unlooped.
Verified against hardware: BLE info read off the Waymarker connects and
exits cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The topology graph gains its fourth evidence source — the only one not
derived from our own reception. A repeater we hold admin rights on
reports its neighbour table (who it hears directly, at what SNR) when
asked over the mesh, revealing links and whole nodes our radio has
never received anything from: exactly the second vantage point the
composer runs dry without, two hops out.
Probed live against YUL-Cartierville (v1.15.0) through the Waymarker
before building: firmware answers the binary NEIGHBOURS request but
silently ignores guests, so the flow is login-first, tx-optimize style
(stored password, one-time prompt, forget on rejection). An empty table
is a normal answer — the gateway's own table was empty during the probe
(confirmed via its text CLI: '-none-') — so the UI says "ask again
later" instead of erroring.
Standing on a fetchable repeater, the composer shows a "fetch
neighbours" row with the suggestions it extends; the fetch runs under
the abortable spinner dialog, persists as a runs-row-owned snapshot
(schema v8, neighbour_reports), and the composer reopens mid-thought
with the same hops and refreshed suggestions tagged N. Refetching
supersedes the old snapshot per (repeater, neighbour) pair rather than
inflating the link's sample count: a table is current state, not an
event stream.
Verified end-to-end on hardware via MeshCoreDevice over BLE (login +
fetch + clean empty-table return) and in the mock (guest refusal,
discovery of a never-heard node, composer render + hand-off). 441
tests pass; the real database migrated v7 -> v8 cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything we receive carries a fragment of the mesh's shape: successful
traces crossed every link in their path (out and back — the reply retraces
the reverse path, so links read both ways), each contact's out_path is a
route the firmware distilled from received floods, and RX-logged packets
report the relay chain they rode in on. A new topology service folds those
into one undirected evidence graph (schema v7 persists packet paths).
The trace screen stops auto-starting on target pick. Enter runs a burst
under a floating tracing dialog (spinner chip, live progress, Abort); "p"
opens a hop-by-hop path composer whose suggestions are the tail's observed
neighbours, strongest link first, with raw hex entry for unseen nodes; "x"
browses ranked path scenarios (device route, direct, best observed
alternatives) and can probe them all — a measured burst per candidate,
recorded as path_candidates rows, reliability-first ranking, winner offered
for adoption.
Composed specs are outbound-only, always ending at the target's hash: the
stored data shows the firmware auto-replies along the reversed path, and
user-composed "there and back" loops (3d,f2,3d) never succeeded. Specs
collapse to a uniform per-hop width the narrowest known hash can honour.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The on/off switch never made sense for a pure listener: recording is
now unconditional, so the toggle submenu, the persisted preference
(MonitorStore/monitor.json), and the header's ON/OFF framing are gone.
The monitor registers its hub subscription at startup — a device-free
step that cannot fail — and its run row opens lazily on the first
observation, so recording follows the hub wherever it opens (eagerly,
or deferred by connect_on_start) and silent sessions leave no empty
runs. The menu entry is now just the read side, "Heard nodes", and the
header shows live heard counters instead of a switch state.
meshterm monitor on the CLI drops --on/--off for a bounded foreground
capture (--seconds N, 0 = until Ctrl-C, mirroring chat listen): it
tails each overheard packet live and summarizes the window's heard
nodes when it ends, while persisting history exactly like a session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checked against companion firmware (MyMesh.cpp CMD_SET_TUNING_PARAMS):
rx_delay_base and airtime_factor are floats carried x1000 on the wire
(constrained to 0-20 s and 0-9 respectively), so the settings now speak
real units — previously the raw wire integers were shown and edited,
making "2" mean 0.002.
The TX delay factors added in the previous commit are removed: the
companion handler reads exactly the two fields and ignores trailing
bytes, so they were silent no-ops. They exist only as remote-CLI
settings on repeater/room-server/sensor firmware.
Airtime factor now leads the Tuning section, ahead of RX delay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New staged settings: contact auto-add bitmask, default flood scope
(hashtag-normalized, 31-byte slot), and the two TX delay factors the
library always zeroed (write-only on hardware, so unknown values fall
back to 0 exactly as before). TX power is now capped by the board's
reported max_tx_power instead of a hardcoded 22, with the prompt hint
and validation sharing one effective_maximum.
Device actions gain Sync clock (drift shown before writing host time;
also a config sync-clock CLI command), and Device info now leads with
a live Status panel: role, model/firmware, battery, storage, clock
drift, uptime, and radio/packet statistics — every read best-effort so
older firmware still renders.
The simulator models all of it, including a seeded clock drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enforce one voice across every screen: column and section headers are
UPPERCASE, menu items are Capitalized buttons, and short descriptions
drop their trailing period (with real ellipses instead of "...").
Tools gain a display `title` separate from their kebab-case CLI name,
so a menu item now reads exactly like the screen it opens ("Device
configuration" opens Device configuration) and result windows inherit
the same name. Every sub-screen backs out via a "Back" row below a
blank separator (the config editor was missing both; its Close is now
Back, spelling out the discard when changes are staged), while the main
menu and device picker keep "Quit". Panel and dialog titles now wear a
brighter tint of their border colour via new title.* theme styles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A direct message was transmitted once and marked failed the moment its
single delivery-ack window lapsed, leaving Ctrl-R as the only recourse.
Now the send path re-sends an unacknowledged message automatically before
giving up, matching the mesh convention of a few tries.
_deliver_direct loops send_direct_message until an ack lands or the budget
is spent; both the initial send and the manual Ctrl-R resend route through
it. Each attempt already blocks a full ack window before the next, so the
retries are spaced by that window rather than hammering the radio, and a
hard send rejection propagates without retry.
The budget is the new direct_message_soft_retries setting: retries + 1
tries, clamped to 0..2 so a message is never re-broadcast more than twice
(0 = one shot, default 2 = up to three tries).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The quit dialog grows a third button, shown only when the session is on a
Bluetooth link that Windows actually holds a bond for (never serial, an open
PIN-less companion, or a non-Windows platform). Choosing it disconnects the
link and then drops the OS-level pairing during teardown, so the next launch
re-runs the PIN ceremony. MeshTerm's remembered-device record is deliberately
kept — unpairing forgets the credential, not the device identity.
Backing this: static WinRT helpers is_ble_paired / unpair_ble mirroring
_pair_ble_windows (Windows-gated, best-effort), an active_address on the
context, and _can_unpair / _unpair_on_exit in the menu.
Also close the WinRT BluetoothLEDevice handles in every helper. Each
from_bluetooth_address_async returns an IClosable that pins the OS ACL link
open; unpair removes the bond but not the link, so a leaked handle left the
device "unpaired but still connected" and refusing to re-pair until it was
power-cycled. Closing the handle in a finally is what finally tears the link
down.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bleak's pair() on Windows only performs the CONFIRM_ONLY ("Just Works")
ceremony and never sends a passkey, so a PIN-protected MeshCore companion
bonds without authentication and then rejects the GATT subscribe on its
authenticated UART characteristic with Insufficient Authentication -- a
correct --ble-pin was reported as "rejected" and the device could never
connect.
MeshCoreDevice now runs the WinRT PROVIDE_PIN ceremony itself before opening
the link, creating the ENCRYPTION_AND_AUTHENTICATION bond the characteristic
requires; Windows persists it, so later reconnects need no PIN. A first GATT
auth failure with a supplied PIN also triggers one unpair-and-re-pair retry to
heal a stale unauthenticated bond from an older attempt. The step is a
best-effort no-op off Windows, when no PIN is set, or when already bonded.
Verified on hardware (AA:BB:CC:DD:EE:FF, MeshCore-Homestead): correct PIN
pairs and connects (reads model Seeed Tracker T1000-E), wrong PIN fails clean
with the "rejected" DeviceAuthenticationError, no PIN reports "requires a PIN",
and a bonded device reconnects with no PIN. Full CLI path exercised end to end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a companion needs pairing, the interactive picker now opens a centered PIN
popup (its own bordered box under the wordmark, matching the splash) and retries the
connection with the entered code — re-asking with a "rejected" note on a wrong PIN,
and returning to the device list on Esc. The masked field clears its stale error as
soon as the user types a fresh code.
The connect failure is now a distinct DeviceAuthenticationError (a DeviceCommandError
subclass), so the picker can catch "needs a PIN" specifically while the scripted CLI
still catches the base class, prints the remedy (pass --ble-pin), and bails without a
prompt — no interactivity for scripted runs. --ble-pin already feeds the first attempt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A BLE companion that requires pairing accepts the link-layer connection but rejects
the GATT subscribe with "Insufficient Authentication", which bleak raised as a bare
BleakGATTProtocolError — escaping connect() as an unhandled traceback.
Translate that (and related "Insufficient Encryption"/"not paired" rejections) into
a clean DeviceCommandError that names the fix: pass --ble-pin, and pair once in the
OS Bluetooth settings on Windows. It's classified as its own actionable case, never
as a dropped link, so the session offers a PIN rather than a pointless reconnect.
The startup probe now lets this actionable error through instead of collapsing every
failure to a bare "didn't answer", so the picker shows the real remedy in the splash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Query MeshCore's device-info frame (send_device_query) on connect to learn the
board's own model string (e.g. "Seeed Tracker T1000-E") — the only reliable source,
since BLE companions advertise no manufacturer data, use randomized addresses with
no IEEE OUI, and expose no GATT Device Information Service.
The probe folds the model into the identity dict, so it's captured during the
connection that already happens, then remembered in the device store (preserved
across reconnects like the node name). The device list and splash picker rename
their "Vendor" column to "Hardware": it shows the firmware model for confirmed
devices and falls back to the USB vendor name for merely-attached serial ports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the startup device picker so it reads honestly at a glance:
- VENDOR is now just the hardware maker. A BLE advert rarely carries one
(confirmed against real hardware: the companion advertises no
manufacturer_data), so it stays blank instead of mislabelling the
transport "Bluetooth" as a vendor.
- A new TYPE column marks the transport with an icon: a plug emoji for
wired serial, and the Bjarkan rune the Bluetooth logo is drawn from,
rendered white on Bluetooth blue and flanked by half-block slivers so
it reads as a slim rounded badge.
- Every confirmed companion (not just the most-recent one) is shown by
the mesh node name learned at connect time, so a known COM11 reads as
its node name rather than the OS's generic "USB Serial Device".
- Confirmed companions sort to the top, most-recently-used first, and
wear their name in white so they stand out from merely-detected ports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Companions can now be reached over Bluetooth as well as USB. Rather than
a parallel BLE path, this generalizes the existing serial abstractions so
a device is identified by a transport (serial | ble) plus a target (COM
port or Bluetooth address); every feature rides the same Device interface
and works over either link unchanged.
- discovery: DiscoveredDevice gains transport/address/name and
transport-aware stable_id/label/confidence/target; discover_ble_devices
scans for MeshCore* adverts via bleak, discover_all merges both.
- connection: MeshCoreDevice opens create_ble vs create_serial by
transport; new link_present() liveness (OS port enumeration for serial,
the BLE client's is_connected flag for Bluetooth); is_connection_lost
recognizes Bleak errors and BLE transport callbacks; probe_device is the
transport-aware smoke test.
- disconnect: the session watcher polls transport-neutral link_alive(), so
a dropped BLE link raises the same reconnect dialog and auto-resume as an
unplugged cable; reconnect rebuilds the same endpoint by address.
- selection/context/store/profiles: --ble address, remembered BLE default,
and BLE device profiles all resolve to the right transport.
- UI/CLI: startup picker scans both transports behind a spinner and tags
Bluetooth companions; `devices` lists them (--no-ble to skip); global
--ble/--ble-pin flags; README and example config updated.
Validated on hardware: discovery, connect, reconnect, and channel
messaging (send + receive) over BLE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give the device-config table the same frameless SIMPLE_HEAD look as the
nodes list: left-justified accent title, muted header rule, no borders.
Indent each setting under its section heading, drop the redundant
(key) suffixes, and tighten the descriptions (no trailing periods).
Collapse the adv_loc_policy labels to off/on so the value cell no longer
duplicates the description.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Watch the companion link while the menu runs; if it disconnects, float a
centered dialog with a spinner and a single Abort button while a
background task retries the connection. The dialog dismisses itself the
moment the device returns, so the user's only action is to give up:
Enter aborts, and Esc is deliberately inert so a stray keypress can't
drop the session while a replug may be a second away.
Give every popup dialog the same cyan reverse-highlight button. This
adds a "selected" theme style (and "err.reverse"): Rich silently renders
a style string that mixes a theme name with an attribute ("reverse
brand") as plain text, so the reverse is baked into a single-name theme
entry instead. Center the button chips with manual padding rather than
Text(justify="center"), which strips a styled span's trailing whitespace
and left the label hugging the fill's left edge.
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>
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 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>
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>