Commit Graph
703 Commits
Author SHA1 Message Date
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 Fable 5.1 f16a7c4738 The shared font generator is offered under GPL-2.0-only too, so the 6x8 script may run it
The GPL-2.0-only 6x8 script executes the generator block out of the Apache-2.0 6x12 script, and GPLv2 and Apache-2.0 do not mix inside one program. Both files are the author's, so the block between the BEGIN/END markers is additionally available under GPL-2.0-only for that use; both headers and the hardware manual say so. No code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:18 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 d078e2379f The XIAO firmware patch carries MeshCore's MIT notice beside it
meshcore-uart1.patch quotes upstream lines and build-firmware.sh produces a MeshCore binary, so the licence those copies travel under now sits beside them as scripts/xiao-radio/LICENSE.MeshCore, verbatim from upstream, and is named from the bench card and the hardware manual.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:17 -04:00
Jean-Pierre MartineauandClaude Opus 5 266aca08fb Preferences are kept in TOML, like every other file a person edits by hand
preferences.yaml becomes preferences.toml: config.toml and the device-config
backups were already TOML, and tomli/tomli-w already dependencies, so PyYAML
existed for this one file and is dropped. The file keeps its shape - flat
keys, only the overrides, grouped under comment headings with each entry's
help and default - and one name, PREFERENCES_FILENAME, for both call sites.

TOML refuses a whole document over one bad line, so a refused file is read
again a line at a time: a valid line keeps its typed value, a refused one
hands its right-hand side to parse_value as text. One broken hand edit still
costs only its line, and a word left unquoted (log_level = DEBUG) still reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 12:45:22 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 ec21d03c03 The SPI bridge drives openhop_core, and stands its shims down where the library caught up
pymc_core was renamed openhop_core in 2026 and the current meshcore-uconsole
package depends on the new name, so a bridge that only knew the old one failed
its own preflight on a fresh install. It now finds either runtime, newest first
(an interpreter with openhop_core beats an earlier candidate with pymc_core;
MESHTERM_PYMC_PYTHON still overrides), imports through the name it found, and
lets two of its three compat shims stand down under openhop_core, which accepts
long ACK payloads and pushes completed trace replies itself; pushing a trace
twice was the alternative. The raw RX-log push is still the bridge's.

What the port found on the device shaped the rest. The 1.12.0 .deb is built for
trixie against Python 3.13 and cannot run on a bookworm uConsole, so the bridge
looks first in a venv of its own (~/.local/share/meshterm-spi-bridge/venv, made
with openhop-core[hardware]) and, without the GUI's helpers there, configures
the radio itself with the uConsole AIO preset rather than the runtime's
Waveshare defaults (TX-enable on pin 6, no DIO2 switch, no TCXO -- deaf on the
AIO), every MESHCORE_* knob the GUI reads honoured and passed only where the
constructor accepts it. It also loads the GUI's identity key first, so a bridge
in its own venv is still the same node. Verified on the uConsole: openhop-core
1.1.3, radio up, 159 contacts restored, info and contacts answered over TCP,
and the boot service now runs from that venv. The manual and the index say all
of this instead of the rename warning.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 01:29:28 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 097d2ef495 Under --mock, devices lists the simulator and scans nothing
--mock promises a session with no real hardware, and a scan was the one thing
the devices tool did regardless: it walked the serial ports and switched the
Bluetooth radio on, which broke the promise on the first command a stranger
without a radio would try, and which the test sweep had to work around with
--no-ble. Now the simulator is the inventory under --mock: one row with the
flag itself as its target, since that is how this device is selected, and a
stable id of its own. The sweep's workaround is gone and a contract test
replaces both discovery entry points with ones that fail if reached.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 01:16:27 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 356ca6a94a The widgets CLAUDE.md names as THE widgets have public names
A leading underscore says "internal to this file, don't import me", and the
column-age formatter and the node glyph table wore one while seventeen modules
imported them and CLAUDE.md named them as the one place each concept lives.
format_age sits beside format_ago now (prose age, column age), NODE_GLYPHS and
DEFAULT_GLYPH beside them, age_seconds with the ages. A mechanical rename, 140
references across 24 files, CLAUDE.md's three lines with them so the guide and
the code move as one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 01:13:18 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 c974085384 The README holds four screenshot slots, named for what each one shows
Four image links under docs/screenshots/ (dashboard, map, trace, picocalc)
with a comment above them saying what to capture, so taking the pictures is
the only step left. They render as soon as the files exist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 00:58:10 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 646a0b7b7c Picking a console font is its own preview, and a discard puts the saved one back
The Preferences page loads the font the moment its value is picked, rather than
after Apply: a VT has one font for the whole screen, so the only honest way to
show what the 6x8 build looks like is to load it, and the page is the preview,
repainting at the new row count on the kernel's SIGWINCH. The page tracks what
the console is drawn in right now; staging the saved value again reloads it at
once, Apply keeps whatever is showing (the tool's re-apply after the write then
finds nothing to change), and leaving with a discard reloads the saved font.
Off the handheld the service says no once at page open and is asked nothing
more. Four tests drive the page through each path with the service stubbed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 00:40:55 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 3b67c6813e The PicoCalc's console font is a preference, and the shell gets its own back
A Linux VT loads one font for the whole console, so asking for the 6x8 build
(53x40, fourteen rows more) changes what the shell is drawn in too. The new
console_font preference (6x12 default, 6x8 the alternative, offered on the
PicoCalc only, round-tripping through preferences.yaml on every platform)
therefore comes with services/consolefont.py: remember the font the shell had
with `setfont -O`, apply the one the preference names, and restore it on every
way out -- the menu's finally and an atexit hook, since ^Q and an unhandled
error leave by other doors. Verified on the device that setfont -O captures
the current font with its unicode table and that feeding it back reproduces it
exactly, with no sudo, because the VT is the process's own controlling
terminal. Only the interactive menu switches; a scripted run never touches the
console it was invited into. Changing the preference on its page applies live.

Shipping both fonts needed the licence untangled: the 6x8 base is the Linux
kernel's font_6x8 bitmap, GPL-2.0, so its build moves into
calculinux-console-font-6x8.sh under that licence with the GPLv2 text beside it
in scripts/LICENSE.GPL-2.0. The shared generator stays in the 6x12 script
between BEGIN/END markers and the 6x8 script executes that block, so the marks,
donors and aliases the font contract pins exist in one place. The produced PSF
is a data file the kernel console reads; MeshTerm never links or bundles it, and
NOTICE and the hardware manual say so. PrefSpec gains a platforms gate for the
page; the labels use an ASCII x because U+00D7 is not in the console font.
The 6x8 base has no Cyrillic; the manual says so and the owner accepts it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 23:52:54 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 ffd2649a35 Shared logic sits in the layer that reads it, and the CLI answers --version
A pre-release architecture and QA pass, applied as one change because the moves
share files and only the whole set was gate-tested (2480 passed, ruff clean).

Three new modules pull logic down out of layers that should not own it:
core/channel_probe.py (ChannelSlot, read_channel_slots, probe_channel_slots)
so services/device_state.py no longer imports a screen; services/markers.py
(gather_markers, with MapMarker a core/models dataclass) so two screens stop
importing a tool; core/tracing.py (trace_timeout, path_hash_flags) so
core/connection.py no longer imports services. The routing-prefix width is
DeviceState.routing_prefix_bytes(), replacing five private cross-module imports.

One definition per thing: is_path_hash, the NodeResolver/NameKeyResolver
aliases, TILE_RETRY_SECONDS, haversine_km, marks.SELF_MARK, MENTION; nine
private names that crossed module boundaries got public ones and menus.__all__
lists what other modules import. A preference's default is written once
(ALERT_CAP, DONE_CAP, DEFAULT_VIEW_FRACTION read the registry), the stranded
PREFERENCES doc comment sits on its subject, and three docstrings stop naming
questionary, a library dropped long ago.

The command line gains --version (eager, exit 0, documented, tested) and drops
the -p short form on trace, trace-path and tx-optimize: the root lifts -p as
--profile before a leaf ever sees it, so the leaf's could never work, and a
derived test now checks every leaf against every root short option.
tx-optimize with a one-hop --path is a usage error, exit 2, because nothing
transmitted is not a device failure. Log handlers are closed before they are
cleared (197 unclosed-file warnings gone), chat's fire-and-forget tasks keep a
reference, coroutines use get_running_loop(), the never-awaited MapScreen._load
in a test is awaited, and the CLI sweep passes --no-ble to devices so a test
run stops turning on the Bluetooth radio three times (the suite is 40 % faster).
The about page lists every dependency, and the release skill bumps the README's
download names, which still said 0.2.8.

apply_ops stays in tools/: it and its helpers build report blocks and open two
screens, so moving it to core/ would invert core over ui. The widgets CLAUDE.md
names by their private spellings (_format_age, _NODE_GLYPHS) keep them until
CLAUDE.md is edited alongside.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 19:19:45 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 8b80a88f03 The sdist is the package's source, not the repository's
Hatchling's default sdist is everything git does not ignore, which packed the
Claude skills, the GitHub configuration, the PyInstaller spec, the device-side
scripts and the developer environment template into an archive whose only job
is to build and test the wheel. An exclude list keeps the package, the tests
and their fixture, the manual and the files a build reads. The build backend
floor is hatchling 1.26, the first to write the PEP 639 licence fields into the
metadata; an older one built a wheel that said nothing about the licence.
Workflow comments that reasoned from a private repo's metered minutes are
reworded for the public one, a skill's usage line stops naming one machine's
venv, and the scheduler's lock file is ignored in the tree rather than only in
one clone's info/exclude.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 19:02:02 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 8d3f5d8ed3 MeshTerm on hardware: one manual for the PicoCalc, the XIAO radio and the SPI bridge
docs/hardware.md is the single place a reader goes to put MeshTerm on a
PicoCalc or a uConsole, written in the shape of the command-line manual: what
the three targets are and which exclude each other, why the console font is
built on the device, every phase of the Calculinux bring-up and its knobs, the
XIAO UART radio from parts to a verified link, the SPI bridge with an honest
note that it was written against pymc_core 1.0.x while today's uConsole stack
depends on the renamed openhop-core, and a closing list of what has only ever
been proven on one bench. scripts/README.md and xiao-radio/README.md become
short indexes pointing at it; the old ones were stale in four places (trace
through the bridge works, the firmware env is _serial, two scripts were
unlisted). README links the manual, gains the two dependency rows it lacked
(markdown-it-py, PyYAML) and a fixed fonts anchor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 19:02:02 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 9ebc4bd2be The device scripts say what they do, and the bring-up no longer needs a deploy key
A read-through of scripts/ ahead of the public release, applying only what could
be verified from the code. The setup script clones over public HTTPS when no
deploy key sits at KEY_PATH and keeps the SSH path when one does, and its
timezone is a TIMEZONE knob rather than a hardcoded city. The flasher recognises
the plain XIAO nRF52840's bootloader (PID 0x0044) as well as the Sense's
(0x0045), both from Adafruit's board.h. The mux unit was After= the very target
that WantedBy= pulls it into, an ordering cycle; it now orders on basic.target,
which still wants a check on the device. The console-font header stopped
claiming a default run installs /etc/vtrgb (it removes the custom remap), and
states that the font it builds is an OFL-1.1 Terminus derivative that must not
be called Terminus and that the optional 6x8 candidate embeds a GPL-2.0 kernel
bitmap. The radio-bridge header describes the four phases the script actually
runs instead of a pin-routing phase it never contained. Personal remarks in
comments are reworded impersonally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 19:02:02 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 72fc0665c5 A record's header reads as a sentence, and the bar says only Record
"🛣 Longest distance — 5th place: 112.0 km": the discipline written the way its board
heading writes it — discipline_label, the mark padded to the measured lane so a
one-cell 🛣 and a two-cell 🎯 start their titles in the same cell — then the standing
and the metric. The bare mark-and-number it replaces was dry, and it had to lean on the
title bar for the discipline; now the header names the record and the bar names the
kind of page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 13:38:40 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 6e84c7fe54 A record's metric heads the page, above the tabs, under its discipline's mark
The node page's identity header, for a record: one line above the strip carrying the
discipline's own mark (the one its board heading wears) and the score in the
discipline's unit, which is the one thing the record is — the title bar already names
the discipline and the standing. The score lane that repeated it on Info is gone, and
the metric's spelling is one function, record_score, shared with the board's score
lane so a partial Longest-distance walk reads ≥ in both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 13:22:45 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 aae3d3f0fe A record's tabs are Info, Route and Area, organized like the node page
Info is the record's stats with the page's actions at its foot, the cursor opening on
Trace; Route is the walk two ways — THE route graph, its key, the route line — with
Trace this path as its one row, so Enter there walks the route on show, the node page's
rule for its Routes tab; Area is the ground drawing, unchanged. Each tab's stage is
composed once per width and cached, and the footer names only what the tab answers: a
lone action row earns no ↑↓ atom. The gallery gains the Route tab as a specimen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 13:13:01 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 ea8ae61812 A record is a tabbed page: Stats, then the walk's ground on an Area tab of its own
The node page's shape (widgets.tab_strip, Tab/Shift+Tab, the next tab on F3): Stats is
the record read — its stats, identity first, then the route two ways — with the action
rows at its foot; Area is the ground drawing across the whole stage the viewport leaves
under the strip, so the shape gets every row and column the page has rather than the
block a stacked layout could spare it. The Area tab is offered only for a walk with a
drawable shape; a record without one keeps a lone-tab heading and nothing to switch. A
page too narrow to draw on says so instead of squeezing.

widgets.tab_air is the strip's platform-bound air, hoisted from the node page so both
tabbed screens spend it the same way. The gallery gains the Area tab as a specimen on
both platforms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 04:00:22 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 197af90c8e A share code is centred as a block, never justified — the bottom finder was skewed
Rich's centre justify strips each row's trailing spaces before it pads it, so a
code row whose right edge was light modules lost cells and landed a column off
its neighbours; the bottom-left finder square came out one cell skewed, and a
camera could not lock on. The About pages' codes were never centred, which is
why only the full-screen share codes broke.

A code is now moved across a line one way only: the same indent in front of
every row, computed once from the fitted code's width. The test pins that all
rows share a width and every finder starts in the same column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 03:29:46 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 4b605c1da7 The record page reads by its sections, and the ground drawing spends the page's width
Three body headings — Stats, Area walked, Route — each with the note that used to be a
faint caption under its block, so a page this tall can be skimmed by its landmarks. The
record's identity leads its stats: the spec walked and when it was set, then what the
walk measured. The area drawing takes the page's whole width and rows in step with it,
a quarter of the columns (half as tall as wide in braille dots), so a wider page is a
bigger shape rather than a padded one; rows a flat shape leaves blank are still
trimmed, so a tall box costs it nothing.

widgets.body_heading is THE form for a heading inside a page's body — accent title,
muted note — hoisted from the Time Machine's private copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 03:10:01 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 b56987b24b A QR code is white on black, and the whole screen when it is the answer
A camera reads contrast, and a light-on-dark code is what a scanner expects
of a screen, so every code MeshTerm draws is now pure white ink on a pure
black field (the theme's `qr` style, both ends named on both themes), never
black on white.

The share views — a channel's share link, a contact card, the CLI's share
commands run in the menu — are no longer a titled popup with an instruction
over the code. They are `QrScreen`, a **bare frame**: `Screen.bare` and
`frame.compose_bare` draw the body alone on blank rows, with no header, no
footer, no title bar, no box, no hint and no F-key lane, and nothing on it
but the code and the URL it encodes, sat a little above centre. Esc leaves.

The code fits itself to the frame every paint, width and rows both: the
standard fit first, then a lighter error level, then a narrower quiet zone,
before it would ever be cut — a contact card is 57 cells and 29 rows at the
standard fit, the PicoCalc is 53 across and a regular terminal 24 tall. When
code and URL can't share the frame the code is whole at the top and the URL
a page down. The frame states its height before it asks for the body, which
is what lets the screen size to it.

A `qr` fence in a written page is the one exception and keeps its place in
the prose — the About pages' codes flip to white on black like the rest.

Found on the way: the PicoCalc's SGR quantizer matched a colour only when it
was a whole sequence, and Rich writes a style's foreground and background as
one sequence (`ESC[38;2;…;48;2;…m`), so a white-on-black module reached the
console in truecolor. It now walks every sequence's parameter list.

The standard is written into CLAUDE.md under Screens and lists; the gallery
carries the share screen as a specimen on both platforms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 03:05:34 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 121f75c2a0 A record's ground is drawn under the stats, each pin labelled with its hash byte
The area drawing rode beside the stats in a third of the width, which squeezed the
shape and left no cell for a label on any pin. It is a block of its own now, between
the stat lanes and the route graph — page-wide up to a cap, a fixed eight rows, north
up, the shape against the left edge under the left-aligned stats — and every hop's pin
carries its first hash byte, placed the way the map places a name (beside the pin,
clear of the drawn lines where it can, dropped rather than overprinting), so a pin and
the graph node under it match by eye. The star stays bare: it already says who that is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:50:50 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 07a566d49d A trophy case record is a page, not a card floating over the list
The reader is in a record — reading its stats, scrolling the route, arming a trace from
it — so it fills the frame like the browser under it and Esc reads back. It used to
float as a 62-column card, which read as a question over the list rather than as the
page it is. RecordDialog is RecordScreen, full-frame, and the reading width it asked for
as a dialog goes with the name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:35:52 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 6bdfa26160 The trace target picker is a popup, gone before the trace screen opens
The third lead-in picker kept pushed as a hub, and the one the ledger said would stay:
a contact list under the trace screen, so Esc out of a trace landed on the list — two
places where the trace is the whole visit. It is a question, and asks like the other
two: the contact list floats over the menu as a one-shot (run_dialog, public now; the
list stays a full-screen place everywhere else), the trace screen opens over the menu,
and Esc from it lands there. The typed fallback floats the same way instead of filling
the frame.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:25:17 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 83e2aa80ae The trophy case fills the frame whichever way in
A bare SelectScreen floats by default, and the browser was full-frame from the menu
only because nothing sat under it; opened from a trace, over the still-pushed trace
screen, the same page came up as a content-sized box. It is a place, not a question,
and declares so like the contact list does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:21:42 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 1f34d35d2a Survey ledger: the base under a popup is the menu
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:16:13 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 b1dbbe5a5a A popup with nothing under it floats over the menu, never over an empty frame
The menu is popped while a tool runs, so a lead-in question — the TX optimize link, the
node to manage, the password that follows it — had a blank base pushed under it to draw
as a box at all. A dialog is smaller than the frame, and what shows around it should be
the page it was reached from: the root the menu already declares for ^W is now the base
_floated pushes on an empty stack, and the reconnect dialog draws over it too, so the
menu loop stops forgetting its root on the way out. A session with no root declared
keeps the blank fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:16:01 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 13f0c647de The navigation standard says what a popup is: a question, not a place
Two flows were rebuilt on it today and nothing had it written down — the stack is for
places, a popup informs, confirms or asks and is then gone, and a question draws as a
box even as the only frame on the stack. The entry-chain rule gains its list form,
run_wizard, one popup turning its page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:05:49 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 a8b69efa73 The repeater admin's node picker is a popup, gone before the admin page opens
The same shape TX optimize just lost, with one popup instead of two: the node list was
kept pushed as a hub under the password prompt and the admin page, so Esc from the page
landed on the list it was picked from — two places where there is one. A popup asks and
is gone: the pick is now a floating one-shot (session.select gains text's floating
flag, so a lead-in question draws as a box even as the only frame on the stack), the
admin page opens over the main menu, and a refused login re-asks the question with the
same node highlighted. admin_node_visit is deleted; nothing keeps a popup pushed now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 02:05:21 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 222679c5a3 The stepped dialog draws as a popup, not as the frame
The menu is popped while a tool runs, so a floating screen that is the only frame on the
stack was painted as the full-frame background — the TX optimize link dialog opened as
a page and only its sub-prompts looked like popups. The blank base _run_dialog_screen
puts under a one-shot dialog is now one context manager, and stay(dialog=True) puts the
same base under a popup's whole visit, which is what run_wizard asks for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:58:38 -04:00
Jean-Pierre MartineauandClaude Fable 5.1 930596130f The TX optimize link is picked in one dialog that turns its page, gone before the sweep
Two pickers stacked one over the other, each kept pushed under the sweep as a hub, read
as two places to be — and a popup is not a place: it asks, confirms or informs, then it
is gone. The node to tune and the node to measure at are now one floating list that turns
its page ("… · step 1 of 2"): Esc on the second step turns back to the first with the
picked node still highlighted, Esc on the first leaves, and the box is popped before the
sweep screen opens, so Esc from the sweep lands on the main menu.

menus.run_wizard is the dialog form of run_steps — the same stack semantics drawn in one
box — and SelectScreen.turn_page is what it turns on: the opposite claim from
replace_items, since a page turned is a new question and neither the typed filter nor the
highlight rides along. A step with nothing to list (the typed hex prefix when the tuned
node is the only contact) runs as an awaitable floated over the box's last page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:52:37 -04:00
Jean-Pierre MartineauandClaude Opus 5 e24e41178a The delay settings round to one decimal, on the repeater admin and device config
TX delay, direct TX delay and RX delay show, stage and send at one place instead
of two. Device config's RX delay had no rounding at all, so SettingSpec gains
the same decimals field the remote catalog carries, applied on parse and display.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 01:19:30 -04:00
Jean-Pierre MartineauandClaude Opus 5 09956c764c MeshTerm 0.3.2
Installers / windows-x64 (push) Canceled after 0s
Installers / macos-x64 (push) Canceled after 0s
Installers / macos-arm64 (push) Canceled after 0s
Installers / linux-x64 (push) Canceled after 0s
Installers / linux-arm64 (push) Canceled after 0s
GitHub release / build (push) Canceled after 0s
GitHub release / create the release (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.3.2
2026-09-11 04:29:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 7afd231bc7 A read-logs skill turns the log into incidents before anyone reasons about it
The log misleads read raw: a count hides its span (204 identical records that
were three seconds), a failed CLI run is logged twice, the suite's runs from
before 59f10f9 sit in the real file, and the runs table keeps UTC while the
log keeps local time. logscan.py groups tracebacks, warnings and the DEBUG
failures that lost their stack, each with first and last; folds the CLI's
reports onto the run they repeat; hides fixture and other-database runs and
says how many; and joins the runs table by run id. SKILL.md is the method:
pin the question, fetch the log (this machine, the PicoCalc, an attachment),
summary then around/run/show, the reading traps, and checking HEAD before
calling a finding live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 04:29:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 10f48dbffc A relay chain opens and closes on the chevron, since its ends are not the route's
A packet's via field names the repeaters that forwarded it and neither the
node that sent it nor the one that heard it, yet its ribbon opened and closed
square, the mark that says the route began and ended there. PathLine takes
from_origin / to_destination: a cleared end opens on the notch or closes on
the point, and arrow mode spells it with a bare leading or trailing arrow.
The packet viewer's via chain and the TX sweep's composed relays draw both
ends open. Under the chain, the "reception describes..." note gives way to
the hop count atom the node page and trace scenarios already carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 04:29:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 e5231687de The location picker opens at once instead of waiting on the radio for contacts
Pick location on map gathered its surrounding nodes before opening, and that
meant reading the contact list from the radio whenever the session hadn't
cached it. A companion refusing that read (ERR_CODE_BAD_STATE, which the log
shows all session) held the picker closed through every retry, twenty-odd
seconds, only to open with no contacts anyway. Our own position waited the
same way after Device config dropped its cached self-info.

The picker now opens on what is already in hand: cached contacts and
self-info (DeviceState.peek_contacts / peek_self_info) and our own history,
via gather_markers(wait=False). Whatever the cache lacked is fetched behind
the open map and joins it through MapScreen.set_markers when the radio
answers. The read is never cancelled, so a fetch another screen shares
survives the picker closing. The Map tool and the node page keep waiting,
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 03:58:46 -04:00
Jean-Pierre MartineauandClaude Opus 5 74eabff25f A repeater setting its firmware lacks reads n/a, not ?
v1.15 matches get keys by prefix, so a key it lacks falls into a shorter
sibling's branch: get radio.fem.rxgain answers with the get radio line, and
gps on a board without its receiver answers "Can't find GPS". Neither is an
error string and neither parses, and an unparseable reply left the cache
alone, so those rows stayed on ?, which says never asked. Any reply that
can't be the setting's value now marks it n/a; only no reply keeps ?.

The fold itself rode along in 564ac0e; this adds the regression test, the
catalog's note on prefix-shadowed replies, and reflows a docstring line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 03:42:43 -04:00
Jean-Pierre MartineauandClaude Opus 5 564ac0e6ac Both editor pages pick a location on the map above separate coordinate rows
Device config folded latitude and longitude into one Location row behind a
Pick / Type / Clear dialog. It now lists them as two rows, as repeater admin
always did. Above them, on both pages, a "Pick location on map…" row opens
the map picker and stages both coordinates at once. The map opens on the
position as staged, or as the device (or node) last reported it. It opens on
the mesh where there is no fix (0, 0 or unread), via map_screen.coords_or_none.

On repeater admin a picked coordinate is stored the way a CLI read would spell
it, so picking the spot the node already holds unstages rather than queueing a
no-op set. Typing a pair and the Clear button went with the dialog. Each
coordinate is typed on its own row now, and 0 in both still means no fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 03:39:39 -04:00
Jean-Pierre MartineauandClaude Opus 5 f27492b2e4 Device config carries the device actions and every setting the firmware has
The Device actions menu entry is gone: clock sync, backup/restore, the identity
key, reboot and factory reset are an Actions section on the Device config page,
which now takes the repeater admin page's shape. It is full-screen, titled with
the node's name and a staged count, and Apply sends in place, one value at a
time; a value the device refuses stays staged with its reason. Read settings
re-reads the device, and unread and blank values read ? and empty.

The settings were checked against the companion firmware (MyMesh.cpp). New:
client repeat, the radio command's trailing byte. Every radio edit now restates
it, because the firmware reads a missing byte as off and a retune silently
stopped a relaying companion. Also new: the auto-add hop limit, read off the raw
frame the library half-parses, and a row for each custom variable the device
reports. The firmware's own bounds now apply: a PIN of 0 or six digits,
path-hash modes 0-2, telemetry deny / by contact / allow all, 150-2500 MHz,
7-500 kHz, and TX power from -9 dBm.

Neither editor page slides its rows sideways any more. The Actions rows pin a
head block, which on its own turned on horizontal scrolling for repeater admin's
whole list and slid its setting labels out of view. SelectScreen(hscroll=False)
now holds against the rows, and both pages use it, so long rows end at the edge
the way Device config's always did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 03:23:21 -04:00
Jean-Pierre MartineauandClaude Opus 5 c9325d0c5d Repeater admin is a full-screen page, not a popup over the node picker
The editor floated as a content-sized box over the picker it was opened from,
spending the frame's width on a backdrop while the reader worked through a
whole node's catalog. It now fills the frame like the local Device config
editor it mirrors; the picker, value prompts, confirms and progress dialogs
still float. The gallery gains a repeater_admin case at both platforms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 02:40:08 -04:00
Jean-Pierre MartineauandClaude Opus 5 61dc9dc110 Every icon-led list measures its icon column, so no label starts a column early
The terminal draws some icons in one cell (↻ ⌨ 🗑 ✓ ✗ ⚠) and most emoji in two, so
a row written icon-plus-space started its words a column left of its siblings. The
fix kept landing one screen at a time. menus.align_icons now pads every icon in a
list out to the widest one, and menu_rows applies it itself, so a list built there
cannot misalign.

Hand-built lists are brought along: the courier outbox and entry actions, the
watchtower actions and rule popover, the record card's actions, the main menu's
Quit row and the device splash's action rows. The repeater admin's regression
test, dropped by a concurrent rewrite of its file, is restored.

tests/test_icon_alignment.py sweeps every gallery screen on both platforms and
fails, naming the rows and their start cells, when a list's icon-led rows start
their words in different cells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 02:02:05 -04:00
Jean-Pierre MartineauandClaude Opus 5 79efc4b0e0 Repeater admin speaks the firmware's own CLI, every setting of it
The catalog was written from convention, and the firmware disagreed: the delays
are floats (0.5 showed as 0 and typing it back was refused), bw/sf/cr are not
keys at all (the radio is one `get radio` / `set radio f,bw,sf,cr`), the guest
password is readable, and path.hash.mode and twenty-odd others were missing.
It is now transcribed from CommonCLI.cpp, with a test pinning the key list.

Read settings asks each question once and leaves every row answered: a value,
`empty`, or `n/a` where the node's firmware lacks the setting; reads that got
no reply are listed. ^R (F3 Read) re-reads the highlighted row alone. The value
lane drops its age stamp. Delays round to two places.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 01:49:51 -04:00
Jean-Pierre MartineauandClaude Opus 5 0257c53d8e The repeater admin actions start every label in the same cell
↻ and ⌨ draw one cell among two-cell siblings, so the rows' icon-plus-space
labels started Read settings and Command line a column left of the rows
under them. The Actions list now measures its icon column once and pads
every mark out to it, like channels and the node page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 01:35:12 -04:00
Jean-Pierre Martineau a849556a33 A release with notes should not publish 'No changelog entry' for losing a race 2026-09-09 02:43:07 -04:00
Jean-Pierre Martineau 8875a45070 MeshTerm 0.3.1
Installers / windows-x64 (push) Canceled after 0s
Installers / macos-x64 (push) Canceled after 0s
Installers / macos-arm64 (push) Canceled after 0s
Installers / linux-x64 (push) Canceled after 0s
Installers / linux-arm64 (push) Canceled after 0s
GitHub release / build (push) Canceled after 0s
GitHub release / create the release (push) Canceled after 0s
v0.3.1
2026-09-09 02:32:47 -04:00
Jean-Pierre Martineau d41111bd5f The missing link refs are cosmetic, and the check now says so 2026-09-09 02:32:47 -04:00
Jean-Pierre Martineau c2fb49a001 Skill: cutting a release, and the two workflows it must never confuse 2026-09-09 02:23:45 -04:00
Jean-Pierre MartineauandClaude Opus 5 398a184eed The splash says bye, and keeps the reader's place when a row goes
Three passes over what landed yesterday.

The hidden-count line stops standing in the list. With devices still
listed the footer already names ⇧H on every row, so the line was a row
of the box spent saying it twice; it stays only for a list hiding has
emptied, where there is no footer atom to read it off and "no
companion devices detected" would be a lie about a machine with a
radio plugged into it.

Hiding now leaves the highlight where the row was - the next device
down, or the one above at the end, never rolling round to the top - so
a run of adapters clears with a run of presses instead of the cursor
jumping back to the remembered default after each one.

And Esc says bye here rather than quit: the splash is the door, and
the reader leaving it has not started anything to quit out of.

One consequence, on the console: the splash border is 47 cells and the
sentence now sheds the hide key too when it must. Between the two
shortcuts the survivor is the way back - ⇧H only appears once
something is hidden, which is exactly when a reader needs to be told
how to undo it, and by then they have found h.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 01:45:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 4109c73522 A full pack should not rescale the pulse it sits beside
The charger cuts out at full, the pack settles back to 99, the charger
restarts - and the companion reports that flip every poll for as long
as the thing is plugged in. The gauge is pinned to the header's right
edge and the pulse takes whatever is left, so the fourth cell 100%
needed came off the sparkline and redrew the whole activity history at
a new scale, twice a minute, on a device sitting still.

A full pack reads `100` now, with no sign: three cells, exactly as the
`99%` it keeps flipping back to. It is the one reading whose `%` can
be inferred - a bare 100 beside a full block is a percentage without
being told - and the sign goes on doing its work at every charge that
could be mistaken for something else.

Only that pair is answered, deliberately. The other width change in
the range, 9 to 10, is a boundary a discharging pack crosses once and
never comes back over: one repaint, no jitter, nothing to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 01:45:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 3d3506be4f A route that has finished does not end on the mark for "there is more"
The chevron is the ribbon's continuation cue: it is what a wrapped line
closes with to say the path runs on past the edge, and it is the same
shape as every seam between two hops. So the last chip of a finished
route wearing one read as a route cut off mid-walk. Where the font has
the rounded caps nothing changed - they close the lozenge and always
did. Where it hasn't, the widget already squared the *opening* off and
documented that it squares both; only the close never followed. It now
appends nothing at all and the last chip's own pad is the edge.

The cell that frees up goes back to the route. `_measure` used to
report one closing figure for every line, which was true while the
close was always a point; it reports two now - what a line the path
runs on past closes with, and what the line that *ends* it closes with
- so a wrapped path measures its own last line honestly instead of
holding a cell back for a glyph that is no longer drawn. The one caller
that flows a leg of a route rather than a whole one says so, because
its final line is a continuation and still pays the point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 01:36:03 -04:00