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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The September messages reached the device picker and the command line, but
three other places still lost them. The reconnect dialog retried every
failure in silence, so a device that came back and refused (its port taken
by the MeshCore app, a stale pairing) kept the spinner up forever; from the
second failure in a row it now shows the reason under the spinner, and goes
back to waiting if the device is unplugged again. A device named on the
command line skipped the picker, and a failed start was dropped unlogged; it
is now shown before the menu opens. And a failed open left the dead device
behind, so is_connected said yes and every later tool failed with "not
connected": it is dropped, and the next tool tries again and says why.
TCP failures are named one by one (host not found, nothing on the port, no
route, no answer, hung up) instead of all reading "no response from a
MeshCore companion". A failure MeshTerm has no name for is no longer called
"not a MeshCore device": it is said in its own words, its traceback is
logged once, and the dialog says where the log file is. The tool-failure
dialog stops reading an error's brackets as markup, which had silently
deleted "[org.bluez.Error.Failed]" from Linux Bluetooth errors.
docs/guide/connecting.md lists the TCP messages and where each message
appears.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`meshterm emulate cardputer-zero --mock` and `meshterm emulate picocalc-lyra --mock`
open a desktop window drawing MeshTerm as that device's own screen draws it. An
EmulatedDevice (emulator/devices.py) holds what differs per handheld: the panel, a
Linux console's top-left grid and bold-as-bright 4-bit palette for the PicoCalc, the
lane keys drawn under the Cardputer's panel, and how each keyboard spells Shift with
an F-key (the PicoCalc's F6-F10, the Cardputer's xterm-shifted F4-F8).
It is not a strict emulator, and the docs say so wherever they offer it: MeshTerm runs
on the desktop as itself, and the window mimics the display's and keyboard's
constraints, never the CPU or the OS. Inside it the platform resolves with own_display
(platforms.drawn_by_meshterm), so nothing offers to move to another terminal.
Global options typed anywhere (--mock, --port, --db...) reach the emulated MeshTerm.
The command refuses --json like specimen. The Terminus fetch moved into the package as
`meshterm emulate --fetch-fonts` (with --archive for a copy fetched another way),
replacing scripts/cardputer-zero/fetch-terminus.py. A build without Tk says to install
with pip or pipx instead of failing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"Host" already meant two things here: a TCP companion's network host,
and battery="host" for this machine itself. And with a PicoCalc preview
coming, the package is less a console host than an emulator of a
handheld's display: its panel, grid, font, colours and keys. So it is
meshterm.emulator, Platform.console_host is own_display, its desktop
window is window.py (EmulatorWindow) and its device front end
framebuffer.py, since "sim" and "device" now mean the radio simulator and
the device being emulated. MESHTERM_HOST_FONTS is MESHTERM_EMULATOR_FONTS.
The package's own description opens on what it is not: nothing runs the
handheld's processor or operating system; it mimics the display's
constraints so the window obeys the device's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
M5Stack makes three Cardputers and only the Zero runs Linux; the PicoCalc
takes several cores and only the Luckfox Lyra runs MeshTerm. So the bare
names were ambiguous, and every reference to the two devices now carries
the variant: the --platform values and MESHTERM_PLATFORM, the Platform
constants (PICOCALC_LYRA, CARDPUTER_ZERO), their lane decks and every
screen's picocalc_lyra_lane / cardputer_zero_lane, the font inventories,
the lane chips' theme styles, the scripts/ folders, the two chrome test
files, and the docs and CLAUDE.md that name them.
There is no alias: --platform picocalc is refused with the three real
choices listed, rather than falling back to the desktop layout. The
PicoCalc is auto-detected from its device tree, so it needs nothing.
What names hardware or a user's data keeps its name: the PicoCalc
keyboard's input-device name, its battery's sysfs supply, and the serial
profile the Lyra setup script writes. .gitignore follows the firmware's
folder, and the changelog keeps the words each release used.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/ was one flat folder: the user guides, the CLI manual, the device
manuals, the code notes and an old survey side by side. It is now
guide/ (features, configuration, connecting, terminals), cli/ (the
manual as its README, the cookbook), devices/ (which manual is yours
as its README, then one per device) and development/ (architecture,
the survey); screenshots/ stays put. The index follows the folders.
The device manuals are named for the device they are about: the
PicoCalc's is picocalc-lyra.md, since MeshTerm runs on the PicoCalc's
Luckfox Lyra core and on none of the others, and the uConsole's stays
uconsole.md, since its CM4 and CM5 run it alike. A new cardputer-zero.md
opens on a warning that the device is not supported yet, says what has
been built and tested only in the simulator, and how to try it there.
Every relative link was resolved from where it was written and rewritten
from where it now lives, and the repo-root spellings in code, config and
the release skill moved with them; the changelog's GitHub URLs too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The AIO's pins sit on whichever /dev/gpiochip carries the Raspberry Pi's
40-pin header, and that number is no fact about the board: chip 0 on a
uConsole's CM4, chip 4 on a CM5 under its early kernels, chip 0 once
Raspberry Pi aliased the RP1 there, and another number again on some
later kernels. gpio_chip now defaults to -1, which asks each chip for its
label and takes the one called pinctrl-rp1, pinctrl-bcm2711 or
pinctrl-bcm2835, falling back to chip 0 where none is; a number in the
profile still pins it. The chip is settled once before the node starts,
so the node, every error and the lsof hint all name the same one.
The uConsole guide says CM4 and CM5 both work, that ClockworkPi's own
A-04, A-06 and R-01 cores are not supported, and explains which chip and
why; config.example.toml and the configuration guide say the same, and
the guide's test record now names the CM5 it has run on since 09-24.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three pull requests against toy issues arrived from agents that farm open
issues for contribution stats. None had said a word to the project, and
none disclosed that AI wrote them.
CONTRIBUTING.md now says, in plain words, how MeshTerm was made and who
it wants: people who use it, AI-assisted or not. Before writing code, a
contributor says hello in the new #contributing channel on Discord and
waits for a yes; the pull request carries their Discord username and a
link to that yes. AI use has to be disclosed in the commits and the pull
request; undisclosed or unattended agent output is closed without review.
The PR template, the README's two notes on pull requests, and the docs
index all said small fixes could skip the conversation, and now match.
The release skill's commit carries the AI trailer its seven release
commits went out without.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every list of three or more takes the serial comma: the one-liner in
pyproject.toml, meshterm/__init__.py, and --help ("USB, Bluetooth, or TCP",
"Windows, macOS, and Linux"), the README, the changelog, the manuals, the
issue and PR templates, and six strings the app shows (the region-name
rule, a repeater setting's help, the diagnostics tool's description, and
the archive confirms). The Windows-console notice is rewrapped so its
comma keeps it inside 72 columns.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A new page, docs/connecting.md, collects every connection failure MeshTerm
reports, with its cause and remedy: who pairs a Bluetooth companion on each
system (MeshTerm on Windows and Linux, macOS's own dialog there, where
--ble-pin does nothing), the terminal's Bluetooth permission on macOS and
its refusal over SSH, reading a screenless companion's PIN over USB,
forgetting a pairing on each system, and the dialout group and ModemManager
for USB on Linux. The README, the docs index, hardware.md, and the CLI
manual's failure section link to it, and --ble-pin says it has no effect
on macOS. The CLI manual's no-radio example shows today's message.
The changelog covers the Linux Bluetooth pairing and link fixes and the
per-step failure messages. configuration.md shows db_path, and a few lists
gain their Oxford comma, WiFi becomes Wi-Fi, and "never-before-seen"
becomes "heard for the first time".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
It came and went with the terminal's width: hidden between 70 and 83 columns, where the class label won, and always on the PicoCalc. It is now drawn at every width, and the class label is what gives way on a narrow screen, the icon still saying what the packet is. Beside the subject, the lane is on screen even at 53 columns; there it is the readings after it that scroll off.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The chat title reads "#devtest · onqc" rather than "#devtest · scope onqc". `channels scope N` on a channel with no scope prints - (null in --json) and exits 0: sending under the device default is an answer. Only an empty slot still exits 5.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The menu's This node / Other nodes / This app sections were called scopes; they now name whose a setting is, and scope is left to a flood's region alone. CLAUDE.md's lexicon says so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The packet viewer's route row reads "flood · scope harbour" (or
"scoped · 3fa1" for a region nobody here has named, or "unscoped")
instead of the library's bare "tc flood"; a direct frame keeps its
route word and states no scope. The live feed gains a SCOPE lane where
the width holds it, 72 columns included (the subject lends it one
cell); the PicoCalc's row already scrolls at 53 and leaves the scope to
the viewer. The message paths dialog states a message's scope once, as
a title atom, read off its arrivals' frames. monitor streams a SCOPE
lane before NAME, and the scope object (or null) under --json.
The viewer and the feed take the region store's scope_of by injection,
falling back to reading the frame alone, and the viewer's card cache
keys on the scope so a region learned while it is open renames it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--spi used to take whichever SPI profile config.toml listed first, a
coin toss that transmits once two radios are configured. It now counts
the radios actually attached: exactly one is used whether or not a
profile names it, which keeps the common single-radio case to a bare
--spi. Two attached, or two SPI profiles with nothing attached to count,
is a selection error (exit 3) that lists them and asks for -p, the same
way several serial devices with no default already are.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The device screen listed an SPI radio but the CLI inventory didn't, so
the one command for "what is attached?" was silent about it. The listing
moves into spiradio.spi_radios, which both now use, so the two can never
disagree; the row is marked active under --spi or an SPI profile.
resolve_spi becomes public for that.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
configuration.md now takes a non-AIO board from its meshtasticd config
to a working profile: the key-by-key mapping, a whole example, how to
check it, and what the profile deliberately doesn't hold (radio
settings, the preamble, identity). It warns that a key left out keeps
the AIO's value, unlike meshtasticd where it means off. --spi joins the
option lists in cli.md, the README, and the cookbook, radio/ joins the
state-files table, and config.example.toml carries a full wiring example.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MeshCore transmits a 32-symbol preamble up to SF8 (16 above), but the
node and the bridge configured the SX1262 for the library's fixed 12.
The chip waits for the sync word only about as long as the preamble it
expects, so it locked onto each packet, gave up, and locked on again
further along the same preamble - decoding a packet only when it
happened to catch the tail. On the uConsole that meant 0 of 5 detected
packets decoded after a send; with 32 symbols, 4 of 4.
The node now derives the preamble from the spreading factor as MeshCore
does, and re-sends it to the listening chip when Device config changes
the SF. preamble_length leaves the SPI wiring table (it was protocol,
never wiring) and is retired; the bridge defaults to the same rule.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A LoRa chip on the host's own SPI bus (the uConsole AIO) is now a
transport like serial, BLE, and TCP: --spi, an SPI profile, or the
picker's row for /dev/spidev1.0. MeshTerm starts a software node as a
child process under whichever Python has openhop-core, talks to it over
a loopback port, and ends it on disconnect; the node also exits with its
parent, so the pins are free whenever MeshTerm is not running.
The node keeps identity, prefs, channels, and contacts under
~/.meshterm/radio/<spidev>/, and on first use copies the bridge's
identity, contacts, and radio settings so it stays the same node.
Board wiring lives in a profile's spi table; radio settings are the
node's own, edited on Device config. Closes#21.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A MeshCore companion never advertises by itself, so the hourly zero-hop and
daily flood adverts MeshTerm sent by default were the only automatic adverts
on the air, and more than a companion needs. The Background adverts rows leave
Device config and `config advert-cadence` goes with them.
In their place, two preferences under Sending: weekly_flood_advert (off by
default) and advert_quiet_s (5/15/30/60 s, default 30). Each device keeps its
own real-time week, running from the latest of the switch-on, its last flood
advert (automatic or by hand), and its first connection; a due advert survives
a restart. Before sending, each connection must hear a packet, then the air
must stay quiet (nothing heard, nothing of ours sent) for the spell plus a
random 0-5 s, drawn afresh at every break so two waiting radios part.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
0.9.0 called the same advert type three things: `companion` in info, `client` in
diagnostics, and `node` in contacts and every other screen that names a node's type.
Room servers were `room server` in two places and `room` in the rest. JP picked
companion. It is the ecosystem's word -- companion firmware, companion apps -- and the
one MeshTerm's own docs already used most. "client" now belongs to MeshTerm itself, which
the one-liner calls a MeshCore client, and "node" is the lexicon's umbrella for every type,
so a TYPE column that says "node" for one of them tells a reader nothing.
There is now one table, NODE_TYPE_LABELS, and one helper, node_type_label(), which gives
"type N" for an advert type MeshTerm does not know. info and diagnostics lose the two
role tables that had drifted apart, and the packet viewer its own copy of the fallback.
The longer names made the node-type legend 59 cells wide, which the PicoCalc's 53 would
have cropped, cutting "sensor" off. node_type_legend() now takes the width it has and
breaks between entries, never inside one, so it stays on one line on a desktop and takes
two on the PicoCalc. The four screens that draw it pass their width and take its lines.
This changes --json output, so it goes under [Unreleased] for 0.9.1. docs/cli.md's samples,
the value lists and CLAUDE.md's lexicon say companion and room server. Two tests pin the
names and the PicoCalc legend.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It read as working notes between JP and the tool that wrote it -- "Question for JP"
blocks, and calls that were his to make -- rather than as a record for a stranger. The UI
consistency survey stays, and docs/README.md and that survey no longer point at the one
that went.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.claude/settings.json is tracked, so it goes public with the repository and applies to
anyone who opens it in Claude Code. It carried JP's own working allowlist -- one-off test
invocations, a `python -c` wildcard, a console beep -- and "defaultMode": "dontAsk",
which would have quietly changed how Claude Code behaves for a contributor.
All of that moved to the gitignored settings.local.json on JP's machine, where Claude Code
still reads it, so nothing changes for him. What stays shared is what a contributor
actually needs to be allowed without asking: the three gates CONTRIBUTING promises,
pytest, ruff check and ruff format.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every manual was read against the code, the scripts and the upstream sources, and the
samples in docs/cli.md were recaptured under --mock where they had drifted.
The one a newcomer would hit first: config.example.toml set default_profile to a COM5
profile. Copied verbatim, as configuration.md says to, it broke every device command on a
machine with no COM5. default_profile is commented out now, so the copied file leaves
discovery working. default_tx_power goes from both, because nothing reads it.
docs/cli.md: config show has 22 settings and preferences show 21, not 20 and 8; monitor's
blocks have no blank line between them; the diagnostics sample is labelled as a real-radio
example with absolute instants and every table; an unknown key exits 1; devices, role and
records describe the values they really produce; no maintainer paths; and the "used to"
passages describe the present, since a first-time reader never saw the past.
picocalc.md: the Calculinux image carries a build stamp, so the gunzip and dd lines take
a wildcard; the front SD slot stays empty on first boot; masking wifi-kick is what stops
it, and time-sync is disabled with it; updates run the scripts from the pulled checkout
with TMPDIR set; the tested list names only what a test checks; git ships in the base
image; a bare meshterm shows the splash, and -p picocalc skips it.
uconsole.md: an AIO v2 needs MESHCORE_EN_PINS=27 whichever runtime is installed; the
bookworm paragraph states what upstream actually ships; the bridge shares the key but
keeps its own contacts and radio settings; the defaults are the US/Canada preset; Step 7
downloads what it installs; updating ends with a restart; no claim of test coverage the
suite does not have.
terminals.md, features.md, hardware.md, cookbook.md, configuration.md and the scripts
READMEs get the smaller corrections their audits found.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first public release. The version goes to 0.9.0 and the changelog carries one
entry, which the release job lifts verbatim as the release notes.
[Unreleased] had refilled since the last fold with one change -- --mock keeping its
invented mesh out of your real history. On a first release there is no previous
behaviour for it to have changed from, so it is folded into the entry's existing --mock
sentence, in that entry's voice, and [Unreleased] is empty again.
Reading the notes as a newcomer would, three claims were not true, and each was checked
against the running app rather than argued:
- `meshterm map` is not a command. It was the first example a stranger would type, and
it answers "No such command". The map is a picture, so it lives in the menu; the
example is `meshterm info` now.
- The Time Machine does not wind the map back. It is braille charts of recorded history
over a day, a week, a month or all of it -- no map in it at all.
- Only the PicoCalc gets the handheld layout. It is chosen solely by the device tree
reading "Luckfox Lyra"; the uConsole has no detection and runs the desktop layout on
its 1280x720 screen, which has the room for it.
The manual's three sample outputs move from 0.3.7 to 0.9.0 with it, so the version a
reader sees in docs/cli.md matches the one they downloaded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--mock` is a fake radio, not a fake MeshTerm: what it adverted was recorded to
`meshterm.db` like anything a real companion said, so its four invented contacts
turned up in the mesh walk, the dashboard and the map of the mesh actually being
run. A `--mock` run that names no database now records to
`<config_dir>/meshterm-mock.db`, created on first use.
`--db` still wins — where a run records and which radio it talks to are different
questions, so an explicit path is a precedence rule and not the usage error
`--mock --port` is. `$MESHTERM_HOME` remains the only thing that isolates a run
whole: the contact and channel caches, the outbox and the remembered devices are
untouched by this, and the docs say so where they used to warn that `--mock`
recorded into the real history.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_CATEGORY_ORDER lists six: Message, Watch, Explore, This node, Other nodes,
This app. Every table we have ever published stopped at five. So Preferences,
Diagnostics and the four written pages -- About, About the author, Join
Discord, Support -- were documented nowhere, while the CLI has answered
`meshterm about` and the rest all along.
Folding the changelog is what surfaced it: writing diagnostics into the 0.9.0
entry meant naming where it lives, and where it lives is a section the
features table did not have.
The framing is fixed too, and the fix comes from base.py's own comment rather
than from me. "Five sections named for the doing" was never quite right --
three of them name a doing (Message, Watch, Explore) and three name the scope
a setting belongs to: the radio in your hand, someone else's over the mesh,
and the program in front of you. That is why This app sorts last, and it now
reads that way in both the README summary and the catalogue.
Every command in the new table was checked against `meshterm --help` rather
than inferred from a tool's name attribute, `diagnostics --out` and the four
`preferences` subcommands included.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It had grown to 580 lines, and most of what a newcomer scrolled past was
reference material: the five feature tables, the CLI cookbook with both
output faces and the exit statuses, the configuration reference, and the
package layout. None of that is what somebody reads while deciding whether
to install this.
It is 375 lines now and it does onboarding and install -- what MeshTerm is,
the screenshots, how to get it running, a quick start, and how the project
is run. The rest moved under docs/ behind a new docs/README.md index, which
GitHub renders when you browse to the folder. The index also lists the two
code surveys, which the README's own table never did.
The prose moved by extracting line ranges rather than retyping, so anything
that did not need rewording is byte-identical. All 255 relative links across
the 13 markdown files resolve, anchors included.
Two changes are not relocations. "## Development" is deleted rather than
moved, because CONTRIBUTING.md already carried every command in it plus the
blame-ignore config and the 72-column gallery gate. And the Windows console
section is summarised in Install with the full explanation in
docs/terminals.md, since a Windows user meeting empty boxes cannot afford to
miss it.
Licensing stays where it was. Acknowledgements, the map and font credits, the
dependency table and the licence are all still in the README, which is where
a redistributor looks and where SignPath Foundation will look for its credit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The block is a title, a standfirst, sections and a list under each. That
is a page of writing, not a screenful of rows, and the app already knows
how to draw one: the About pages' markdown renderer. So the tool states
its report as before and a third projection turns it into markdown, which
is exactly what the report seam was built for -- no tool touched, no
report changed, one new function beside the other two faces.
Being a written page buys the two things a long block most needs and that
a hand-built screen had neither of. Its ## headings are landmarks, so each
pins to the top row while its own section scrolls under it and ^PgUp/^PgDn
step section by section; and the PicoCalc lane picks up Sect up / Sect
down for the same reason a grouped list does. AboutPage already does all
of it, so the page is that page with one verb added.
The frame comes back with it. The bare frame was there to keep a border
off every line a mouse would drag through, which mattered while copying
off the screen was the only way out; saving is the way out now, so the
page can be a normal screen and the save can be advertised where this app
advertises verbs -- the footer hint on a desktop, the lane's one free F3
slot on the console -- instead of in the page's own body, which belongs to
the report. The answer moves to a popup with it: a path is worth reading
carefully, a write can fail, and the page underneath is unchanged and
still there when the popup goes.
Saving markdown is the same decision as drawing it. One source is what the
page renders and what the file holds, and an issue tracker renders it as
written, with no fence and no apology. Every value is a code span, which
is correctness and not taste: a Windows path is full of backslashes that
markdown reads as escapes, and an error a stranger's firmware wrote can
hold an asterisk. A value already holding a backtick gets a longer fence.
Rows are a list and not a table, measured rather than assumed -- a
markdown table's cells are cropped with an ellipsis at 53 columns, and a
bug report whose one long value is cut is missing the half that mattered.
A list item wraps and hangs under its own text at any width.
Two smaller things fell out. A block may carry a caption, the heading a
format that has headings draws above it -- plain and JSON have no use for
one and ignore it. And the host block splits in two, because a "it looks
wrong" report is about the terminal and its reader should not have to pass
the processor architecture on the way to it.
The command line is untouched: aligned records, because that face is a
stream to grep and headings are furniture on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>