13 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 5.5 11b5274b31 Docstrings and comments of the tests are in ASD-STE100
Each test docstring states the behaviour that the test proves, in the
simple present. No assertion message, test id, or other string literal
changed. Four statements now agree with the code: the Trace highlight
stays on the last row, a menu count, and two others.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:36:50 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 c9f5a71ce7 Every Python file names its licence with an SPDX identifier
Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:18 -04:00
Jean-Pierre MartineauandClaude Opus 5 92d31a3191 Every file is spelled the way the formatter spells it
`ruff format .` across the repo: 153 files, no behaviour change. This is the
second gate CONTRIBUTING.md promises, and until now it reported 154 files needing
reformat — so a first contributor ran the command the guide told them to run and
could not tell their own diff from the background.

Landed on its own, with nothing else in it, because a whole-repo reformat is only
safe to skip past in blame if it is provably nothing but layout. It was checked
that way rather than assumed: every changed file was formatted in memory and
compared by full `ast.dump()`, and 152 of 153 come out structurally identical
(the one exception was a docstring opening on a quote character, fixed in the
commit before this one). The string and comment token streams were diffed
separately, so a quote flip or an edited comment could not hide inside a
re-layout — five quote-style normalisations and sixteen implicit-concatenation
joins, all value-identical, and one commented-out line gaining the space after
its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite
is 1888 green, gallery gate and `test_theme16` included.

What actually changed, in order of how much of the diff it is: wrapped calls and
signatures rejoin onto one line where they fit inside the hundred columns; the
magic trailing comma expands what it is there to expand; complex slices gain
their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are
laid out to be read were fenced in the previous commit and are untouched here.

The aligned trailing-comment column is gone from about ninety lanes that were not
fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced
where it sits. Every comment stays on the entry it describes, so nothing there
reads wrong; it just no longer lines up.

Add this commit to `.git-blame-ignore-revs`, which follows immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 01:07:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 81e58803ee Heard: our own ears outrank the sender's clock
A contact's heard time came straight from the firmware's last_advert,
which the advertising node stamps with its own clock. That makes it
hearsay, and advert_time can only refuse a stamp from the future — a
node whose RTC runs days behind reports a plausible-looking time that
never catches up, so the lane sits at "4d ago" while we are receiving
from it right now. Our own history, stamped when we actually received
something, could only fill the gap when the device reported nothing at
all.

So merge instead of fill: take the later of the device's time and ours.
A device time still wins whenever the firmware caught an advert we never
recorded, and we only override it with proof of a later reception.

A direct message now counts too. In this app's lexicon "heard" means
received from, and a message is received from its sender — but a DM
never touches last_advert and is stored as a message, not an
observation, so a node we chat with daily could read stale and be swept
by the purge ladder's quiet rungs while talking to us. Read from the
messages table rather than recording an observation: a routed message's
SNR belongs to its last relay, which is why heard_nodes excludes packet
rows, and counting it would inflate a lane that means overheard traffic.

The merge runs on every contacts fetch now, so both halves are GROUP BY
queries that aggregate in SQLite and yield one row per node, instead of
heard_nodes streaming all history into Python to build stat objects.

Node detail takes the merged contact time, so it and the contact list
can no longer disagree about one node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 03:49:46 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 434ffda253 Observations: capture the full public key, keep node as the 12-hex id
Adverts carry the transmitting node's whole public key, but only its 12-hex
prefix was ever stored (observation_from_event truncated it with [:12]). That
prefix is the app's identity/join key — heard_nodes groups on it, and the map,
nodes, and watchtower screens correlate it against contacts' 12-hex key_prefix
— so it must stay short. Instead, record the whole key alongside it in a new
observations.public_key column (schema v12) and keep the prefix as node.

The Time Machine picker prefers the captured full key, then the live contact
resolver, then the bare prefix — so a heard node's key now shows as fully as
the hash lane fits, offline included. HeardNode surfaces the key from whichever
row captured it; the simulator's observations carry it too.

No backfill: the full keys past adverts arrived in were never persisted (the
raw payload wasn't stored) and no local source has them, so old rows can't be
completed. The database was wiped to start fresh under the new schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:38:32 -04:00
Jean-Pierre MartineauandClaude Fable 5 5ff3d7ebd0 Dashboard activity chart: newest left, minute buckets, full braille width
The activity histogram moves to one-minute buckets held six hours deep,
so both the dashboard and the header can draw a dot column per minute
and still have history in reserve on wide terminals. The chart now
anchors "now" at the left edge (matching the header sparkline's
direction), stretches across every cell the terminal offers, and
mirrors its count scale on both gutters with per-row axis marks.

braille_bars gains the font's full horizontal resolution — each value
is one dot column, two per cell — and the traffic bars step in half
cells the same way. The pulse line quotes rates over exactly the span
the chart shows and sheds the word "heard" rather than wrap when the
terminal is too narrow for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 05:03:13 -04:00
Jean-Pierre MartineauandClaude Fable 5 b27af22f36 Header shows the node, its port, unread mail, and a mesh pulse
The status bar drops the static "device:" prefix and the heard
counters. The device segment now leads with the node's own mesh name —
the remembered registry name, then the profile alias, then the
hardware name — with where it's reached in parentheses (the COM port,
or BLE), cached per connection so repaints never re-read the registry
file.

The heard numbers give way to an unread badge (channels and direct,
shown only when something waits) and a two-hour braille activity
indicator: every packet the hub hears — adverts, telemetry, messages,
acks — lands in a five-minute bucket, drawn twelve characters wide at
1/5/21/89 thresholds with the ●/○ live-light in front. The braille
sparkline renderer moves from the channel manager to widgets so both
draw with one brush.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:23:32 -04:00
Jean-Pierre MartineauandClaude Fable 5 3502a2e09d Trace opens idle: compose the path from received topology, probe scenarios
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>
2026-07-10 21:00:00 -04:00
Jean-Pierre MartineauandClaude Fable 5 0f49cf0a2d Passive monitoring is always on; CLI monitor becomes a timed capture
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>
2026-07-10 02:07:23 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 2ec18138a7 Rename project from MeshTools to MeshTerm
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>
2026-07-07 19:40:49 -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 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.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