Commit Graph
999 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 5.5 1914c19bf3 A test keeps the docstrings, comments, and documents in ASD-STE100
tests/test_writing_style.py rejects an em dash, a semicolon, a
contraction, or a Latin abbreviation in a docstring, a comment, or the
prose of a document. It does not read code font, quoted text, code blocks,
URLs, or legal text between <!-- ste: off --> and <!-- ste: on -->.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:45:04 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 efc02ddced The code text uses the glossary words in more places
A final pass over the docstrings and comments: "the own X of Y" is now
"Y's own X" (84 places), a navigation entry is a screen on the stack, our
node is our node, a paint is a paint, the quoted UX labels are in code
font, and a list of punctuation glyphs is in code font.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:45:04 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 bc51115d24 The changelog is in ASD-STE100
Each version, date, fact, and link stays. The version headings use the
Keep a Changelog form (## [x.y.z] - date); the release workflow reads only
the bracketed version, and each section still extracts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:44:55 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 26d422e651 The CLI manual and the cookbook are in ASD-STE100
Each option, field, unit, exit status, and example output stays exactly
as it was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:44:55 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 c0e7c17252 The guides and the device manuals are in ASD-STE100
The four user guides and the manuals for the PicoCalc, the uConsole, and
the Cardputer Zero. Procedures are numbered steps, with a WARNING or a
CAUTION before each step that has a risk. The step headings use a colon,
and each link to them changed with them. Quoted error messages are in
quotation marks, and the menu question is quoted as the app shows it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:44:55 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 e6f6cf2621 The README and the project documents are in ASD-STE100
The README, CONTRIBUTING, SECURITY, the docs index, the development
documents, the script READMEs, the comments of config.example.toml and
pyproject.toml, and the issue and pull request forms. The legal sections
of the README (attribution and licence) stay exactly as they were, between
ste: off and ste: on markers. The SignPath attribution line, the one-line
description of MeshTerm, every code block, and every URL do not change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:44:55 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 962d96f8d3 The writing standard is ASD-STE100
Docstrings, comments, documents, guides, and the changelog are written in
ASD-STE100 Simplified Technical English, with Canadian spelling.
docs/development/writing-style.md has the rules, the six rules that we
bend, the conventions for docstrings and comments, and the glossary: one
word for each concept, an extension of the lexicon in CLAUDE.md. CLAUDE.md
now points to it and says which texts are not in STE. The release skill
writes the changelog in STE, under headings in the Keep a Changelog form.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:44:54 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 99b991707e Comments of the scripts and the packaging files are in ASD-STE100
The Python scripts, the packaging code, and the full-line comments of the
shell scripts. Heredocs, marker lines, licence lines, and code do not
change. The help text of type-text.py now starts with a complete STE
sentence, and uart1-mux.py now says three writes, as the code does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:37:03 -04:00
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 Opus 5.5 981f4527e9 Docstrings and comments of the path line, theme, and font modules are in ASD-STE100
pathline.py, theme.py, termfont.py, fontset.py, widgets.py, and oklab.py.
These were done last, after the path chip work in them was committed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:36:49 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 5752bf7cf6 Docstrings and comments of the top-level meshterm modules are in ASD-STE100
cli.py, context.py, platforms.py, and __main__.py. The package
docstring (the fixed description of MeshTerm) does not change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 20:37:27 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 abfd32c491 Docstrings and comments of meshterm/ui are in ASD-STE100
The same rewrite as for the core package, for the screens, the dialogs,
the map, the charts, and the CLI renderers. The glossary words are used
for the screen: the user (not the reader), dialog (not popup), viewport,
list window, highlight, pointer, graph lane, marker, raster, result
screen, and key prefix (never hash for a truncated key).

The rewrite also corrects approximately 70 docstrings and comments that
no longer agreed with the code: rows that no longer exist (Back rows),
counts that changed, renamed constants, wrong cross-references, and
behaviour that changed. Each correction follows the code; no code
changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 20:37:26 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 16bc471d9d The console font's table matches the fontset on the Lyra
The rebuilt 6x12 font, with the two path chevrons, was installed on the PicoCalc and its Unicode table dumped: the same 519 codepoints as FONT_RANGES.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 17:40:51 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 d6d49df0a2 Docstrings and comments of meshterm/ui/tui are in ASD-STE100
The same rewrite as for the core package. A navigation entry is now
"a screen on the stack", the visible part of a page is its viewport, and
the visible rows of a ListWindow are its list window. Two statements now
agree with the theme: the selected chip style is a grey fill.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 04:17:05 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 3e251a9707 Docstrings and comments of meshterm/emulator are in ASD-STE100
The same rewrite as for the core package. The physical screen of a
handheld is now the display, and the computer is the handheld. The
comment on FONT_ENV now says that it is the environment variable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:51:55 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 6b90e0505b Docstrings and comments of meshterm/tools are in ASD-STE100
The same rewrite as for the core package. No help= string, epilog, or
other UX text changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:48:44 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 5fc224476c Docstrings and comments of meshterm/services are in ASD-STE100
The same rewrite as for the core package. Three comments now agree with
the code: the activity bucket of the header is one minute
(ACTIVITY_BUCKET_S), the RX log holds packets, and a reference to a
module note that does not exist is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:48:44 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 aff99b633f Docstrings and comments of meshterm/persistence are in ASD-STE100
The same rewrite as for the core package. Two docstrings now agree with
the code: node_names does one aggregate scan (the query is NOT INDEXED),
and the log file is plain text, not structured logging.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:29:03 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 ffe31dac36 Docstrings and comments of meshterm/core are in ASD-STE100
The prose of each docstring and comment in the core package now follows
ASD-STE100 Simplified Technical English, with Canadian spelling: short
sentences, simple verb tenses, no dashes or semicolons, and one word for
one concept (docs/development/writing-style.md has the rules and the
glossary). The meaning and each reason stay. The code does not change:
the AST of each file, without its docstrings, is the same as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:29:03 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 353dcb7792 A path chip is the node's name colour on a darker ground of itself
On the desktop and the Cardputer, a chip was the node's hue with dark
ink on it, so a node's colour moved from its name to the ground behind
it the moment a route drew it. Now the label is the hue, the colour the
name has in every list and in arrow mode, and the fill is the same hue
at 0.60 of its OKLab lightness (CHIP_LIGHTNESS): the rule the PicoCalc
draws with its dim twins, at full resolution. A hash label and an
annotation are the label colour throughout, with the lit bytes bold.
A keyless chip is the same pair in grey; our star, a faded hop and the
composer's white slot keep what they had.

The value came from two ladders over twelve hues. A fixed step down in
lightness made the blue fills almost black (blue is the wheel's darkest
hue), one lightness for every fill made the blue label hard to read on
its own ground, and 0.65 / 0.55 were too weak in blue / too dull in
yellow. Darkening at constant chroma leaves sRGB, and the clip merged
neighbouring greens into one #008000, so oklab.fitted lowers the chroma
instead and keeps the hue.

A thin seam between two chips of one colour is now drawn in the
previous chip's label colour, the light half of its own pair, on both
palettes; the shaded-fill rule (SEAM_SHADE) is gone with it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:25:13 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 69d9836546 The PicoCalc draws its path lines as chips
The console font gains the two powerline chevrons, drawn into the next
two donor slots (U+2559 and U+255B, which nothing draws), in both the
6x12 and the 6x8 builds. With them in its inventory, the PicoCalc's
verdict is core, so its routes are ribbons of chips like the
Cardputer's.

A chip there can only be filled from the eight background slots, since
under a 512-glyph font the VT has no bright backgrounds. So a node's
chip is the dim twin of its hue (theme.DIM_TWIN), and its label is the
hue itself: the colour the name has in every list and in arrow mode. A
hash label and an annotation are that colour throughout. A keyless chip
is white on light grey, the same pair one bank down. Our star, a faded
hop and the composer's slot have no dark grey to stand on, so they
stand on the page, the slot as a white + in a break of the ribbon. Two
chips of one colour join on a thin chevron in that colour.

Bold is brightness on the console, and Rich merges a selected row's
bold into every span on it, which turned the dim fills bright. Every
chip style there says "not bold"; the tests read the rendered row back
through the emulator's VT to hold that.

The new font takes effect once the font script runs again on the Lyra
(as root). Until then the PicoCalc's installed font lacks the chevrons.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:14:12 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 c07f15f4ec The Cardputer draws its path lines as chips
The Cardputer's panel is drawn in MeshTerm's own Terminus, which has the
powerline chevrons, yet its paths came out as arrows: the chips-or-arrows
verdict asked the terminal that started the app about its font, and on
the device that terminal is no one's, so the answer was unknown. In the
emulator it was worse, since the verdict was whatever the host terminal
could draw, a question about the wrong glass.

A handheld names its glyph inventory (Platform.font), and every frame is
folded down to that font anyway, so the inventory now answers: after the
MESHTERM_POWERLINE override, before any terminal probe. Terminus has
U+E0B0 and U+E0B1, so the Cardputer draws chips. The PicoCalc's 512-glyph
console font has neither, so its paths stay arrows, now said outright
rather than reached by not knowing. Diagnostics reads
"core (platform:cardputer-zero)". The verdict is bound on a platform
switch and its memo cleared there.

Checked by rasterizing gallery screens through the emulator's VT and
Terminus at 53x14; not yet on the device.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 02:16:44 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 b9585e9b59 Holding Esc quits wherever MeshTerm can tell the key is held
Hold-to-quit was the Cardputer's alone. It is now MeshTerm's gesture
everywhere a key can be seen coming up, which a terminal never reports:

- The emulator's window, for either handheld, which reads its own keys
  as the Cardputer's panel does.
- A terminal's Esc, where the keyboard can be asked whether it is still
  down: GetAsyncKeyState on Windows, as the right-Ctrl rescue already
  asks, and EVIOCGKEY on Linux wherever the keyboard's event device
  opens (the PicoCalc, a uConsole, a user in `input`). It reads the key
  state only, no events.

An Esc arriving while its key is still down is held back as a hold:
delivered as it comes up, a box after a second, gone at three. The
keyboard's repeats are part of the hold, a key typed during it goes in
behind the Esc, and on a VT stream the last repeat, which prompt_toolkit
can parse after the key is up, is dropped rather than going back a
screen behind a box the reader just let go of. On macOS and over SSH Esc
is unchanged.

SIGTERM is a quit for the interactive menu on every platform now, not
only under the Cardputer's launcher: a system shutting down gets the
same clean exit, terminal and console font restored.

EVIOCGKEY checked on the Cardputer's arm64 kernel; the Lyra was offline.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 01:20:53 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 9d16e0722a Holding Esc on the Cardputer shows the quit coming, and MeshTerm leaves in time
The Cardputer Zero's launcher ends any app whose Esc is held: SIGTERM to
its process group at three seconds, SIGKILL three seconds after that
(cp0_esc_exit_policy.hpp in CardputerZero/launcher; M5's packaging guide
still describes an older Home-for-five-seconds rule). Its own hint never
reaches the panel while an app owns it, and MeshTerm took 3.4 s to exit,
so a held Esc ended in SIGKILL.

Now a tap of Esc is typed on its release, and a hold of one second
floats a "Hold Esc to quit" box whose bar fills over the two seconds
left. While it is up the app holds the device's transmit lock: nothing
new goes on the air, and a scoped channel send under way closes its
window first. Letting go gives the lock back and types nothing. Held to
the end, MeshTerm leaves the way Quit does, and the launcher's SIGTERM
joins that exit instead of interrupting it wherever the main thread
happened to be.

The exit itself fits: openhop-core's cleanup joined the radio's IRQ
thread, parked in an uninterruptible 30 s poll, for its full 2 s
timeout. The node drops that daemon thread from the books first. On the
device, under the real launcher, the process group now drains 1.3 s
after the SIGTERM, with no SIGKILL.

The emulator's window does the same on a held Esc, debounced for an X
server's release-press repeats.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 00:59:38 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 5d2c1908e8 A uConsole is recognised by four marks, not one
Naming the AIO rested on the uConsole's panel alone. It now takes every
mark, from two independent sources: ClockworkPi's uConsole overlay
describing the hardware in the device tree -- the cwu50 screen, the
AXP228 power chip, the OCP8178 backlight -- and the hardware answering
for itself on USB, the built-in keyboard (1eaf:0024, made by
ClockworkPI, named uConsole; the ids alone are any LeafLabs Maple). An
overlay on the wrong machine has no uConsole keyboard, and the keyboard
on another machine has no uConsole overlay. Any one missing, and the
radio is listed unnamed on the AIO's wiring, as before.

`BuiltinRadio.compatible` gives way to `board`, a test only its machine
passes (`is_uconsole`).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 00:54:13 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 afc906cf2f On a uConsole, the AIO's radio is named the uConsole AIO
The radio on spidev1.0 was listed as a bare "SPI radio" and reported
"MeshTerm SPI node", because spidev1.0 alone could be any Pi. A
uConsole now proves itself by its own screen: ClockworkPi's uConsole
overlay declares the panel `cw,cwu50`, which no other machine carries
(the DevTerm's is cw,cwd686; the CM's model string and the AXP228 and
backlight are shared). Seen on JP's CM5 uConsole.

`BuiltinRadio` takes a device-tree `compatible` beside its marker path,
read once a run from /sys/firmware/devicetree/base. The named AIO comes
first in BUILTIN_RADIOS; without the panel, the unnamed entry still
lists spidev1.0 on the AIO's wiring as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 00:19:58 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 b5ca6ec8ab The Cap's node reports itself as the Cap LoRa-1262
The SPI node reported every radio as "MeshTerm SPI node", so the
Cardputer's Cap read that way wherever a device's model is shown. The
node is now told the shipped board it runs on (`board_name`, matched by
the SPI node on this machine's board, so a profile that adjusts the
Cap's pins is still the Cap) and reports that; a radio on a board
MeshTerm doesn't know by name keeps "MeshTerm SPI node".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 23:48:29 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 e608dcf3b9 A PIN-protected companion that hangs up on pairing is asked for its PIN
On the Cardputer Zero, connecting to an ESP32 companion that wants a PIN
stalled for a minute and never asked for one. Its link came up and its
services resolved; the UART subscribe made BlueZ ask MeshTerm's agent
for the PIN; with none given the agent refused, and the companion
disconnected -- it hangs up rather than refusing the subscribe. The
guard that holds back drops while bleak retries HCI 0x3e swallowed that
hang-up too, so meshcore waited out its timeout on a dead link.

The agent already records whether BlueZ asked it for the PIN. A drop
after that is now flagged, and the connect ends at once as a pairing
failure -- the words the PIN handling answers -- so the reader is asked
for the PIN in seconds. Drops with no pairing asked are held as before.

Also: a link that opens and then drops during service discovery
("failed to discover services, device disconnected", an HCI Connection
Timeout 1.7 s in, at -22 dBm as readily as at -86) is retried like a
link that never opened. It was surfacing on the first try, and the next
try usually connects.

Checked on the Cardputer against the companion: three connects with no
PIN each answer "requires a Bluetooth pairing PIN" in 2-7 s, where each
took the 60 s timeout before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 23:38:20 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 21d7c6cd20 Scripts with a shebang are executable
They were committed from Windows, which records no execute bit, so on
the device `scripts/cardputer-zero/launcher-entry.sh` -- run by path, as
its own usage line and the Cardputer guide say -- was refused with
"Permission denied" while the file sat right there. Every script under
scripts/ that starts with #! is now 100755, as the uConsole bridge
already was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 23:07:18 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 d34dd2f67d The Cardputer's launcher shows the website's logo as MeshTerm's icon
The launcher entry carried a placeholder. Its icon is now meshterm.net's
logo, the red skyline, resized to the launcher's 100x100 the way the
site makes its own small icons (Lanczos from the 410 px source).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 22:47:17 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 49d3eff212 The Cap's GPS moves the node, and is no longer offered as a companion
The Cardputer Zero's Cap carries a GPS on the header's UART, which the
device screen listed as "~ n/a" (pyserial's name for a port with no USB
identity): a companion to connect to, and with nothing remembered, the
lone port a command-line connect would pick. A wiring now names its
board's GPS (`gps_port`, `gps_baud`; the Cap's is /dev/serial0 at
115200), and wherever that radio is listed, its GPS port is not: the
device screen, `meshterm devices`, and autodetect.

The radio node reads it the way MeshCore companion firmware reads a
board's GPS: `gps` (off until switched on, remembered in prefs.json)
and `gps_interval` (0, every fix, once a second), reported and set as
custom variables, which is where Device config already shows them. A
valid RMC or GGA fix becomes the node's position, the one its
self-info reports and, with Share location on, its adverts carry. The
last fix is saved as the node shuts down. Standard library only, as
the node's contract asks; NMEA checksums are checked.

On MeshTerm's side, our own self-info is now refreshed behind the
answer after a minute, like the contacts, so screens opened after the
node moves show where it is; and the connect-time settings offer no
longer flags a position the running GPS moved.

Checked on the Cardputer: the ttyS0 row is gone from `meshterm
devices`; `config custom gps 1` opens the port, is saved, and reads
back on; the real-terminal test passes there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 22:02:05 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 c78a802ac6 The Cardputer's panel shows MeshTerm, not the launcher's loading screen
On the device MeshTerm ran, drew every frame, and stayed invisible: the
panel kept the launcher's last frame while a splash nobody could see
waited for a key. The panel is a DRM driver whose /dev/fb0 is fbdev
emulation, and the kernel copies to the glass only what it sees change
through the *mapping*. pwrite() put the pixels in the buffer, where
reading /dev/fb0 back showed them, and never on the panel. The frame
now goes through an mmap, as M5's own LVGL apps write theirs. Checked
on the panel.

Also scripts/cardputer-zero/type-text.py: paste a long string (an API
key, a Wi-Fi password) on another computer and it is typed on the
Cardputer's keyboard over SSH, into whatever app has the screen. Input
is hidden and travels on stdin, never on a command line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 17:41:07 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 db4cc657fe The Cardputer's battery reading survives the Cap's noisy bus
With the Cap powered, I2C bus 1 doesn't only fail reads: some come
back garbled (a capacity of 56414, clamped to a confident 100), and
the kernel's bq27xxx driver caches whatever it read, failure or
garbage, for five seconds. Retrying 0.1 s apart only re-read the
cache, so every retry missed.

Now an unreadable or out-of-range percent is asked again 5.5 s later,
past the cache; a step of more than 5 points between polls waits for
the next poll to confirm it, which catches garbage that lands inside
0-100; and the sysfs reads leave the event loop, since a failing
transfer on that bus takes its time. Measured on the device with the
Cap on: a steady 96%, where it showed nothing or 100 before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:13:02 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 3333201df8 MeshTerm starts from the Cardputer Zero's launcher, on its own battery
Tried on a real Cardputer Zero for the first time. It draws on the
panel, takes the keyboard, and opens from M5's app launcher:
scripts/cardputer-zero/launcher-entry.sh puts a source checkout there
(and `remove` takes it off) until there is a store package.

Three things the hardware showed:

- The splash opened with the highlight on /dev/ttyS0, the Cap's GPS
  UART, because serial is listed first as found. Below the companions
  we've used, anything that looks like one now comes before a port
  nothing vouches for, across transports.
- The title bar's battery is the handheld's own BQ27220, as the
  PicoCalc's is. The host pack is now found by what it is (a Battery
  supply that isn't a peripheral's), not by the PicoCalc's name.
- While the Cap is powered, I2C bus 1 turns noisy: a quarter to three
  quarters of the gauge's reads fail with an I/O error (one in ten of
  the clock's), none with the header off. A failed read is asked again,
  a status that won't read keeps its last verdict, and a good reading
  stands through six failed polls instead of blinking out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:00:13 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 85315dc3b7 The Cardputer Zero's Cap LoRa-1262 works with no setup
On a Cardputer Zero the device screen lists the Cap as "Cap LoRa-1262",
and --spi reaches it, with no profile: its wiring ships beside the
AIO's, listed when the board's own ext_5v_out switch says this is a
Cardputer Zero (spidev0.1 alone would claim every Pi with SPI on).

The Cap needs two things before the chip answers, and the wiring can
now say both. `leds` sets the switches the device tree hands to
gpio-leds — ext_5v_out powers the header, ext_usb_gpio_fun=0 keeps
the reset and interrupt pins on GPIO rather than USB — and puts them
back when the node ends, so the header is unpowered again after
MeshTerm quits. `pi4io_*` drives the Cap's PI4IOE5V6408 at 0x43, which
enables the RF path; a silent expander is reported as the Cap not
being attached.

Checked on the hardware: reset 26, busy 22, irq 23 on spidev0.1 (the
lines M5's factory test drives), the node comes up from a cold
header, and it hears the mesh at +12 dB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 15:29:40 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 7bcb1db360 The battery shows its last digit on Linux and macOS
A full T1000-E read 9 or 10% in the header on Ubuntu, while the radio
reported 4.21 V. The battery was never the fault. A full pack's "100"
lost its last zero, and 99% lost its 9 to the % sign that followed it.

MeshTerm adds one column to the width the terminal reports, because
prompt_toolkit's Windows console output reports one column fewer than
the window has, on purpose. That was done on every desktop, but a Linux
or macOS terminal reports its width exactly. There the extra column fell
off the screen. prompt_toolkit draws with autowrap off, so each row's
last character landed on top of the one before it, and whatever sat
flush right lost its end. The battery gauge sits flush right.

The column is now added only where the terminal hid one, which is the
Windows console. The full_width preference and MESHTERM_FULL_WIDTH still
override either way. Driven into an exact 80-column terminal under
--mock, the header now ends "100" at 4.21 V and "98%" at 4.18 V, where
it ended "10" and "9%". In a real Windows console the reported 119
columns still widen to 120.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 04:21:38 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 44e60c4054 Acks get the wait a flood needs, and a late one still counts
Only 24 of 144 direct messages and posts in the history were ever marked
delivered. Three faults, each fixed:

- The wait was the radio's own estimate times 1.2: about 5.7 s for a
  flood, while a flooded round trip from a few hops out takes ten (a
  room's answer from five hops landed at 10.1 s on hardware). It is now
  the login's patience, the estimate stretched a quarter plus a round
  trip sized to the route, about 15 s for a flood.
- An ack that landed after the wait was dropped. The radio pushes one
  whenever it arrives, for any of its last eight sends, but nothing was
  listening, so a delivered message stayed failed for good. The history
  shows ACK and PATH packets heard seconds after messages recorded as
  failed. The send now returns a Delivery carrying the ack code, and the
  chat service keeps the last sixteen messages still waiting. A late ack
  flips one to delivered, in history and in the message the chat screen
  holds, and every code a retried message went out under counts.
- The Courier trusted the failure and sent the message again: a
  duplicate for the recipient whenever the ack was only late. A late ack
  now marks the entry delivered.

The ack listener is also armed before the send rather than after it,
so a neighbour's ack quicker than the send's own return is not missed.
The debug log names how a message went out and the radio's own
round-trip time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 03:00:48 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 07e1792a4a A busy card's caption breaks where it means to, and hangs under its text
A long room name ran the join's caption ("logging in to YJN-ROOM-OBS
St-Jean along its 6-hop route… 37 s") one cell past its box, and the
count's last letter wrapped alone to column zero. A busy card's caption
may now carry line feeds, each later line hanging under the caption's
text rather than under the spinner. The card is as wide as its widest
line, and a line too long for the screen wraps under the text too. The
join puts the room on the first line, and how the login went out with
its running count on the second.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 02:20:09 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 19c910bbde Rooms get their own page, and a room that doesn't answer is explained
Joining a room moves out of Chat into a Rooms page beside Channels. It
lists every room server the radio knows, joined ones first, with each
room's access, unread count, last post and last-heard time. Each room
has a page: join, log in again, change password, open the board, forget
it, or delete its stored posts. Chat now lists only joined rooms, the way
it lists only channels on a slot, and only a joined room raises the
unread badge. chat join becomes rooms join, alongside rooms list and
rooms forget.

"No reply" told the reader nothing, because a room is just as silent for
a wrong password as for a login that never reached it. Testing on
hardware showed the commonest cause is neither: a login sent along the
route the radio had learned met silence, and the same login by flood was
accepted in eleven seconds. A silent login is now explained from what is
known: how it went out, when the room was last heard, and whether this
password has worked before. It offers the one next step that evidence
points at:
- Try by flood, after a learned route met silence. The radio forgets
  the route (local, nothing transmitted), then sends the login once.
- Try again…, after a flood met silence. The prompt reopens with the
  password filled in, so Enter resends it and typing replaces it.
- Add it & try again, when the radio doesn't hold the room as a contact.
Every retry is a button press, and none is ever sent on its own. A
password over 15 bytes is refused at the prompt, since MeshCore sends
only the first 15.

On the board, a failed or unfinished login used to leave the compose
line taking posts the room would drop. It now opens only while we are in
and may post, and otherwise says why and what to press. Opening a joined
room that has gone quiet logs in in the background. ^L runs the
explained flow.

Underneath, the login exchange listens for the radio's own confirmation,
matched to the login by its expected ack. It records whether the login
went by flood or along a route, or the radio's reason for not sending it
at all. This also corrects the debug log: "send unacknowledged" read the
library's wait for the *answer* as the send. On hardware the radio
confirms in under 0.1 s. Device.reset_route is new. The simulator learns
routes, can be given a stale one, and refloods.

Refs: #24
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 02:07:10 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 4f72210927 Room logins keep each password for what it proved, and send one at a time
A review of the room work turned up ways the remembered passwords could
go wrong, and a doubled login:

- A room that lets readers in lets any password in, so a login that only
  earned read-only proved nothing about the password. It replaced the
  room password anyway, so one mistyped ^L left a member read-only for
  good, with no prompt to fix it. It now never replaces a password that
  is already kept, and ^L asks for one while you are read-only.
- A blank login only asks "do you know me?". Accepted, it overwrote the
  remembered room password, and as an admin it emptied the admin
  password Repeater admin shares. It now replaces neither.
- A room's login replaces a known member's role with the one the
  password earns, so an owner who held both passwords was demoted to
  member each time the room password was tried first. The admin password
  now goes first.
- Closing a room view and opening it again mid-login sent a second
  login. The room service now shares a login already in flight, and a
  room with one under way is not due another.
- A refusal forgot the room password even when it refused a different
  one. ^R is no longer offered to a read-only member, whose resend the
  room would drop. Enter on a picked post opens its paths when there is
  nowhere to put a reply, and a paste is refused there too.

Refs: #24
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 00:49:08 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 1a101613b0 The command line joins rooms, posts to them, and reads them by author
chat join logs in to a room server and prints what it allowed: member,
admin or read-only. It uses --password, or the password MeshTerm
remembers for the room. A room says nothing to a wrong password, so a
join that hears nothing fails (exit 4) and names both possible reasons.
send --to a room posts to it, and its receipt says kind "room". A room's
history names each post's author where a direct transcript names the
peer, drawn the way a route draws a hop: "Alice (d4e5f6a7)", or the bare
hash when nothing names the key. The document carries the author on
every row, null outside a room, so history answers in one shape for
every kind of conversation; listen streams it too. chat list calls a
room a room.

The CLI manual, the features guide and the README describe rooms, and
CLAUDE.md adds room and post to the lexicon, 📌 to the icons, and ^L to
the chord list.

Refs: #24
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 00:30:48 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 1ceda7e22f Chat joins room servers, and reads each post under its author
A room server is a message board on a radio: it keeps its members' last
posts and sends each member the ones they missed. MeshTerm could see rooms
and run your own, but not join one. Posts that reached the radio were filed
as direct messages from the room, with their authors thrown away.

Chat now has a Rooms section between Channels and Direct. Opening a room
shows its board at once, each post under the member who wrote it. A post
carries the first four bytes of its author's key, so it is named by any
contact or overheard advert with that key and coloured by the key itself.
An author nothing names stands as the hash, in the unknown-node grey. The
first visit asks for the room's password over the board. After that,
opening a room logs in again only once it has been quiet for
room_relogin_minutes (a new preference, 30 by default), and ^L (F6 on the
handhelds) logs in on demand. The title says what the room allowed:
member, admin or read-only. A read-only member gets a line saying the room
drops their posts, in place of a compose box. A post goes out once, never
soft-retried, since a duplicate would reach every member.

Underneath: messages carry an author (schema 18), a room's board leaves
out its command replies, and a re-sent post is recorded once. Room posts
raise the unread badge and a room's command replies don't. The device
gains room_login, sharing admin_login's exchange and reading the access
the room granted. Room passwords are remembered in rooms.json; an admin
login is kept in admin.json, shared with Repeater admin. The simulator
has a room, Lakeside BBS, to join with "hello".

Two fixes the research turned up. An admin command took the first direct
message from anyone as its reply, so a room post or a companion's message
landing mid-command was read as the node's answer. It now listens before
sending, and only for a reply from that node that is not a post. And
Repeater admin names a room server's guest password "Room password", the
password members join with.

Refs: #24
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 00:21:34 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 ff5315ffcf A per-hop median's path squares only the route's own ends
Each hop row drew its link as a whole route, square at both ends, so every
hop read as a walk that began and ended there. A hop is one link of the
walk: only the first hop's start and the last hop's finish are the route's
own ends. Every other end is a node the route runs on through, and now
wears the notch or the point (a bare → in arrow mode), the marks a path
line already uses for "this goes on".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 22:32:02 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 ad036a89aa The trace screen's per-hop medians name their links as path lines
Each hop used to read as "origin (hash) → destination (hash)" in a
four-column table, which ran past the edge on any link between two long
names. The link now draws through the path widget the way the route lane
above it does: names with no hash after them, an unnamed node by its hash
(the only identity it has), and our own end as the bare star.

Where every row fits on one line the hops stay a table, one line each. Where
any row doesn't, every row takes two lines, all alike so the readings never
jump between lines: the hop number and its path on the first, and the
reading with its meter flush right on the second. A path still wider than
its lane breaks off on its cut mark and slides under ←→, with the hop number
pinned and each path stopping at its own tail. The footer offers
"←→ scroll" only while something can slide, and sliding lets go of the
action cursor so the page stays on the hops being read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 22:29:04 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 f8aabb9d6a The trace screen's results are page, so edge scroll reaches them
Once a trace had run, ↓ on the last action did nothing. The results sat in a
window of their own under the actions, sized to whatever rows the controls
left, so the page came out exactly as tall as the screen: the frame had
nothing to scroll, and edge scroll with it, while "↓ n more" sat under the
last action. Only PgUp/PgDn slid that window.

The results now follow the actions on the page. ↓ off the last action
scrolls on down through them to the oldest trace, the snap-back brings the
actions back, and PgUp/PgDn/Home/End page the whole screen, the way the TX
sweep and Records pages already work.

So that no other screen hides rows this way, ListWindow.fit and fit_blocks
take the highlight with no default: a window is for a list the highlight
walks, since every row it hides must be one an arrow reaches by walking onto
it. Anything nothing highlights belongs on the page, where the frame scrolls
it. The trace results were the only window over rows no highlight walks.
CLAUDE.md states the rule beside edge scroll.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:59:17 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 db7e42497f The device splash pans as one table, its badge column first
The transport badge now leads each row with no heading over it. A list
whose rows declare `pans` slides its column header and every table row by
one shared shift, kept as the highlight moves and stopping where the
widest line meets the edge, so the splash box keeps its width. Rows
outside the table, like Add a network device and Quit, stay put.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:31:32 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 8fbf5b372f The device splash draws every field whole, with the address last
Each lane was squeezed against a budget so the row fitted the box, which
cut names to "Johnp…" beside a long address. Now every lane is sized to
its longest value, the header keeps its full labels, and a row wider than
the box runs off its edge for ←→ to read. The address moves to the end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 19:47:29 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 604ad3067a The freeze watchdog logs how many of its tiles each raster had
A raster drawn with fewer tiles than its view needs is a frame with black
where the missing ones belong. Logging have= beside tiles= makes that frame
visible, which is how the zoom's black screen was measured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 17:49:25 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 858053f5fa A zoom goes from the ghost straight to the map, never through black
Zooming in showed the ghost, then a black screen, then the map drawn in. The
black was a raster started the moment the view changed, before the new tile
zoom's tiles had loaded. It had nothing to draw, and being the view's own
frame it replaced the ghost. On the PicoCalc it stayed up 0.4 to 1.0 s, and
sometimes a half-drawn frame followed it before the real one. The same
happened when the map first opened.

The map now starts no raster while a tile of its view is still on its way.
It keeps standing on what it has (the aligned frame, or the ghost), and the
first raster it draws is the real picture. A tile that comes back as silence
is no longer on its way, so a map with no network still draws what it can.

Measured with a zoom tour on the PicoCalc: in to z11, z12 and z13, then back
out to z10. Before, every zoom across a tile zoom published a raster with
none of its tiles. Now each zoom draws one raster with all of them. The
finished picture arrives as soon as it did before (0.8 to 1.35 s in, 0.4 to
0.7 s out), with 6 rasters drawn instead of 13.

Tests that exercised raster scheduling now let their tiles answer first,
since a captured load used to stay on its way for ever.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 17:49:24 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 af815cf2f2 The emulator's lane keys can be clicked, and wear the lane's colours
A click on one of the Cardputer Zero's keys drawn under the panel, or on a
chip of the lane along the panel's bottom row, types that slot's key. The
PicoCalc has no keys drawn, so its chips are where you click. Shift held
through the click types the slot's Shift companion. The app reads the bank
off the key that arrives, not off the Shift it saw, so the window spells the
shifted key itself, the way the desktop's Shift+F-key already was: F6 to F10
on the PicoCalc, xterm's shifted F4 to F8 on the Cardputer.

The drawn keys take the lane's own fills, resolved from the deck's styles
through the panel's palette, so a key and the chip above it are one colour.
They turn blue with the lane while Shift is down, and fill solid while
pressed. Losing focus lets go of Shift, which a release in another window
never would.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 17:24:10 -04:00