Commit Graph
26 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 4.8 d652a4e012 Add an interactive braille OpenStreetMap map of mesh nodes
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>
2026-07-07 17:38:39 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 a03dedb09c Add message delivery retry, channel capacity discovery, and a compose byte budget
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>
2026-07-06 23:12:25 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 61101cf778 Align emoji cell width with the terminal's real rendering
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>
2026-07-06 21:47:10 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 4c725bd22a Key channel history by identity and mark channel openness in chat
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>
2026-07-06 20:31:44 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 b289565d96 Sharpen device discovery: node names, tiered confidence, no double port
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>
2026-07-06 19:15:33 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 7330796829 Add a first-class channel manager with QR sharing and reordering
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>
2026-07-06 18:23:47 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 6856b682b1 Add channel/direct chat feature and polish the passive-monitor UX
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>
2026-07-06 17:41:12 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 20c32f9642 Introduce an always-on event hub and broaden the device event surface
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>
2026-07-05 21:20:24 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 2a9facc56e Replace questionary with a full-screen prompt_toolkit TUI
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>
2026-07-05 14:09:18 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 2cd0fce032 Split the node/contact list out of info into its own nodes tool
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>
2026-07-04 13:49:58 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 1036ff52f6 Make passive monitor a non-blocking background toggle with live counts
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>
2026-07-04 13:35:43 -04:00
Jean-Pierre MartineauandClaude Opus 4.6 923c771817 Set permissions to dontAsk mode
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-21 19:25:03 -04:00
Jean-Pierre MartineauandClaude Opus 4.6 1add7abf92 Add route-map to README feature table
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-21 19:25:02 -04:00
Jean-Pierre MartineauandClaude Opus 4.6 fa585b333d Add Repository unit tests: hydration fidelity, ordering, limits, edge cases
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>
2026-06-21 19:09:28 -04:00
Jean-Pierre MartineauandClaude Opus 4.6 8d2320ba03 Harden route-stability: fix is_self detection bug, add coverage, tighten error handling
Fix: self_label was computed via _name(None, resolve) but edges already
resolve None→device_label, so the resolver could never rename the self
node. Use _name(device_label, resolve) instead.

Quality: replace assert on output_dir with graceful skip, validate
--samples requires --target, add _slug fallback for empty input, fix
Callable import (collections.abc).

Tests: 6 → 25 — adds three-way entropy, single-hop topology, median SNR,
link sorting, degree counting, resolver edge cases (returns None, renames
self), empty/mixed traces, multi-target graph, empty-graph render, output
dir creation, filename slug, SNR colour thresholds, Rich panel branches,
and the questionary validator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-21 18:11:41 -04:00
Jean-Pierre MartineauandClaude Opus 4.6 736fb4e762 Add route-stability tool: aggregate trace history into churn metrics and interactive graph
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>
2026-06-21 16:28:22 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 6405e7ec7b Add link-health regression detection against a rolling baseline
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>
2026-06-21 15:57:31 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 eba264385b Add passive mesh monitor (longitudinal advert/telemetry logger)
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>
2026-06-21 15:48:17 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 0cec67dd1f Add offline link-budget / airtime calculator tool
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>
2026-06-21 15:40:31 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 3f8c698625 Trace TX optimization round-trip; restore TX when no result
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>
2026-06-21 14:45:03 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 9a308096f0 Clean up: drop unused imports, tighten test exception assert
- 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>
2026-06-21 14:08:28 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 c75a2eb2e3 Replace local TX optimizer with remote-admin, path-based tuning
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>
2026-06-21 13:58:15 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 a3ab95fd26 Raise max trace repeats from 8 to 9
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 12:34:49 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 89e1076a06 Show node hashes in trace tables and info panel
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>
2026-06-21 12:26:09 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 421781f13d Fix auto-path traces, harden contacts retrieval, add device config tooling
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>
2026-06-21 11:19:21 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 43353f83a9 Initial commit: MeshTools with device discovery & selection
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>
2026-06-20 17:02:34 -04:00