Bring in the 65 upstream commits since 0.11.1, with ^K lock and the self chip.
Both sides kept their features. React gives way to Lock in two places. On the
PicoCalc lane, Lock keeps the Shift half of F2 and React is ^E only. In the
hint, ^E react is the first atom to go when ^K lock has no room.
The long-message code now refills the compose line with MentionEditor, which
replaced LineEditor upstream.
Passwords, channel secrets, pairing PINs, and private keys reached the DEBUG log and
the runs table in plain text: every chat run stored its channel secret inside the
Conversation repr, and --password, --secret, channel links, and config set device_pin
were stored as typed. meshterm.core.secrets now hides each one as <secret> by its
shape (a secret-named key, record, set operation, or dataclass field; bytes; a Secret;
a remote `password`/`set guest.password` command; name=value in text), and by value
for each secret MeshTerm has read or sent. Both log handlers and the runs table go
through it, always. The remote CLI history keeps `password ` without its value. Schema
v20 hides the secrets that older versions stored.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Off by default. Each masked value keeps a tidbit (j*********u, DA:xx:xx:xx:7C:7C,
192.xxx.x.180, m******e.local), so two radios stay two radios. The log masks each
line as it is written; diagnostics take --anonymize/--no-anonymize, and the page
toggles with `a` (Shift+F3 on the PicoCalc). Refs #31.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
A message that is too long for one packet now goes out as up to
message_parts_max parts (default 3, 1 turns it off), each ending with
its part number: [1/3], [2/3], [3/3]. Each part, with its part number,
fits in the byte limit of the conversation (130 for a channel, 150 for
a direct message). A cut goes at a space, and a word with no space to
cut at is cut between characters, never inside an emoji sequence or
after a combining accent.
Channel parts wait the transmit cooldown between them, so that the
floods do not go out as a burst. A direct part waits for the
acknowledgement of the part before it, or the end of its wait. An
unacknowledged part still lets the rest go out, and ^R retries each
one. A hard failure stops the rest and puts their text back in the
compose line. The byte gauge shows the number of parts and the bytes
of the last part. Rooms are not changed.
Co-Authored-By: Claude <noreply@anthropic.com>
Select a message in a channel or a DM and press ^E (Shift+F2 on the handhelds) to
react with an emoji. A reaction is a usual message on the wire: MeshTerm writes the
format of MeshCore One and reads it and the two formats of meshcore-open. The
transcript folds each reaction under the message that it names, with a count, or
the names of the nodes while that message is selected. A reaction does not raise
the unread badge, and the Messages list previews it as "reacted 👍".
A reaction names its target by the sender's timestamp. Thus MeshTerm now gives the
device the timestamp of each message it sends and stores exactly that second. A
retry of a DM keeps the timestamp and counts the try number, as the protocol
specifies. The emoji that ^E offers are the reaction_emoji preference. Rooms have
no reactions.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
MeshTerm already stores every channel packet it hears, including those
on channels the device doesn't have. A packet names its channel only by
a one-byte hash, but a public channel's key is derived from its name, so
a candidate name can be tested: derive the key and check the packet's
MAC with it, exactly as the firmware does before decoding. The guide
tries the device's channels, Public, every #name mentioned in a stored
message, and every known region. On one desktop history that names 25
public channels.
A guessed name has to match at least two messages before the guide
shows it. The check compares three bytes, and with thousands of names
against thousands of packets roughly one false match is expected. The
candidates are grouped by their hash byte, so each packet only checks
the ~1/256 that could match; the naive loop took 40 s, this takes 1 s
on 31,000 messages, in a worker thread under a busy card, and the result
is kept for the session while its inputs are unchanged.
"Channel guide…" is the first row of "Add a channel" on the Channels
page, and it stays there when every slot is full. Channels not on the
device come first, and Enter adds one to the next free slot. Channels
the device already has follow with a ✓ the highlight doesn't walk, and a
closing line counts the messages no name matched. Each row shows the
message count, the age of the last message and seven days of activity.
`channels guide` lists the same channels on the command line.
Refs: #25
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A new device ships with Public and nothing else, so changing devices
meant adding every channel again by hand: each private key pasted, each
# name typed, each send scope and mute set again. The Channels page now
has an "Export and import" section. Export writes every channel to one
TOML file, in slot order, with its key, its send scope and its mute.
Import reads such a file and adds its channels to the device.
Import never removes a channel. The channels of the file go first, in
the file's order, and whatever else the device holds follows them. A
channel with no free slot is reported, not forced in. Before anything is
written, a dialog shows the plan (what is added into which slot, what
moves) and ends on "Nothing is removed from the device." The slots are
read again in full before the plan is made, because a probe cut short
makes an occupied slot look free.
The file uses the same [[channels]] table as config backup, so a config
backup imports too. A # channel carries no secret, since the device
derives its key from the name. A private channel carries its key, so the
file is written readable by its owner only, and the export says how many
private keys it holds.
On the command line, `channels export FILE` writes the file and prints
its path, and `channels import --file FILE [--dry-run]` prints the
device's layout after the import, one row per channel with add, move,
keep or skip. `channels import INDEX URL` still imports a single link.
Refs: #25
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>
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 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>
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>
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>
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>