189 Commits
Author SHA1 Message Date
rich 233dfc813c Merge upstream MeshTerm 0.11.5
CI / lint (push) Canceled after 0s
CI / test macos-latest py3.14 (push) Canceled after 0s
CI / test ubuntu-24.04 py3.10 (push) Canceled after 0s
CI / test ubuntu-latest py3.11 (push) Canceled after 0s
CI / test ubuntu-latest py3.12 (push) Canceled after 0s
CI / test ubuntu-latest py3.13 (push) Canceled after 0s
CI / test ubuntu-latest py3.14 (push) Canceled after 0s
CI / test windows-latest py3.10 (push) Canceled after 0s
CI / test windows-latest py3.14 (push) Canceled after 0s
CI / build (push) Canceled after 0s
Issue trailers / check (push) Canceled after 0s
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.
2026-10-22 16:53:37 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 0b931e7cc4 The Courier outbox is a TARGET and TRIGGER table, and Enter opens the message
Each section of the outbox now has column headers: TARGET and TRIGGER for
waiting entries, TARGET and OUTCOME for finished ones. The rows no longer
show the message. Enter on a waiting entry opens it to edit (the courier
holds the entry while the prompt is open, so the old text cannot go out
meanwhile), and Enter on a finished one shows it. Del deletes an entry
after a red confirm, and ^R sends a waiting entry now; on the PicoCalc
they ride F3 as Send, with Delete behind it. On a narrow screen a long
name gives cells to the trigger and breaks off on its crack.

The message field has the colours of the chat's compose line: a white
prompt mark and white text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 00:22:05 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 bdc60481ec A serial link that dies while its port stays listed opens the reconnect dialog
When a laptop wakes from sleep, the kernel can reset the USB device and hang
up the open port, but keep the same device file. The next write fails with
EIO, and the serial transport closes itself. The serial liveness check read
only the OS port list, so it said the link was up: no reconnect dialog
opened, and each command failed with no_event_received (issue #32).

The serial check now also reads the meshcore client's is_connected flag,
which the client clears when its transport is lost, as the BLE and TCP
checks already did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 00:42:19 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 1d983013d7 No log keeps a password or a secret: the log file, the run log, or the remote history
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>
2026-10-18 00:20:22 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 9e579424b4 The Anonymize preference masks the home folder and addresses in the log and diagnostics
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>
2026-10-17 21:30:26 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 d81f3c8618 A demo mesh over Montréal, for screenshots that stay the same
meshterm --demo runs the simulator in a synthetic Montréal mesh: 14
repeaters on the high points (Mont-Royal to Mont-St-Hilaire), rooms,
sensors, and about twenty companions, all invented. A log-distance
link model decides who hears whom, so floods arrive on several real
paths and traces walk real routes.

At each start the demo writes its own directory (<config>/demo) again:
300 days of adverts and a dense week of traffic, bilingual channel,
direct, and room conversations whose packets decrypt, traces with
their trophy-case records, neighbour tables, a joined room, admin
passwords, regions, and Watchtower alerts. Every value comes from a
fixed seed and an age before now, so each run gives the same data.
The device plays the live traffic from the same seeds.

The repository writers that stamp now take an optional time, and
record_observations stores a batch in one commit. The test world of
--mock is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 00:38:47 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 820740c9a0 A blank line before each section is one preference, app-wide
"Section spacing" (section_spacing_min_rows, in the Display group)
replaces "Author spacing" and "Spacing from". It is the smallest
terminal that gets the line: any height, 20, 24, 30, or 40 rows, or
never (default 24). Each section heading of a list gets the line, and
the chat keeps it between authors and before a new day.

The frame records the terminal rows on the screen before the body
renders (note_terminal_rows), and Screen.section_spacing is the one
test. section_heading marks its row as spaced, and the select list
adds the line, never at the top, after a blank line, or under a
heading. The hand spacers before headings are gone, except the seam
between the region table and its actions, which is two lists.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-15 22:28:23 -04:00
rich c5a2fb3746 Merge branch 'long-messages'
CI / lint (push) Canceled after 0s
CI / test macos-latest py3.14 (push) Canceled after 0s
CI / test ubuntu-24.04 py3.10 (push) Canceled after 0s
CI / test ubuntu-latest py3.11 (push) Canceled after 0s
CI / test ubuntu-latest py3.12 (push) Canceled after 0s
CI / test ubuntu-latest py3.13 (push) Canceled after 0s
CI / test ubuntu-latest py3.14 (push) Canceled after 0s
CI / test windows-latest py3.10 (push) Canceled after 0s
CI / test windows-latest py3.14 (push) Canceled after 0s
CI / build (push) Canceled after 0s
Issue trailers / check (push) Canceled after 0s
# Conflicts:
#	docs/development/writing-style.md
#	docs/guide/features.md
#	meshterm/core/preferences.py
#	meshterm/ui/chat.py
2026-10-15 21:30:10 -04:00
richandClaude f7f0036d0f Send a long chat message as numbered parts
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>
2026-10-15 21:26:49 -04:00
richandClaude Opus 5.5 f3aa8e5aef Allow channel sender names of up to 31 characters
The firmware keeps a node name in 32 bytes, but the sender prefix and the @[Name]
mention matched only 20 characters. A longer name stayed in the body: the transcript
showed no sender for it, and its reactions showed as new messages. Found on hardware
with "Hazelwood r0b3.Anduril" (22 characters).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:47:13 -04:00
richandClaude Haiku 5.5 a323342971 Add emoji reactions in the wire format of MeshCore One
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>
2026-10-15 07:47:13 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 0c3a8b5b5c The space between authors in a chat is a preference
Two preferences in the Display group: "Author spacing" (auto, on, off;
default auto) and "Spacing from" (the smallest terminal, in rows, that gets
the blank line with auto; default 24). The test reads the real rows of the
terminal before each paint, so a small desktop window drops the line as the
Cardputer Zero does, and a change applies at the next paint. The transcript
cache stores the spacing, so a change draws the lines again. The CLI
manual's sample of `preferences show` is the real output again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:16:15 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 1c678d43f3 An imported seed stays a seed on the uConsole and the Cardputer Zero
The key parsers now return the key in the form that the user gave. Each
device takes the form that it keeps: a firmware companion expands a seed
to the 64 bytes that it accepts, and the node of an SPI radio saves the
seed as it is, the same form as a key that it makes itself. A seed is the
more complete form, and the radio library signs with it through PyNaCl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:10:40 -04:00
Jean-Pierre MartineauandClaude Haiku 5.5 723f7bdfef The identity key shows, imports from a file, and imports on an SPI radio
Show private key drew its warning and no key in the menu since 0.10.0.
On the uConsole and the Cardputer Zero, MeshTerm now writes an imported
key into the node's identity.key and restarts the node. An import also
reads a file (hex text or raw bytes), expands a 32-byte seed, refuses a
key of the wrong length before the confirmation, and names a firmware
that has its import turned off. The run log no longer stores the key of
config import-key (a migration removes the stored ones), and an exported
key file is written with mode 0600. Refer to #26.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-12 23:51:03 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 9ae0947717 A shared name goes to the newest key unless two keys used it within a week
Greying every name that two keys ever used was too strict: a reflash with an
erase makes a new key, and 34 names on this mesh have one, JP's own among
them. Smog went grey this morning because of two keys silent since September.

The newest key now gets the name, and the name is grey only when another key
used it within name_overlap_days (a new Display preference, "Shared names",
default 7) of it: two live claimants. A contact that was never heard never
wins. On this machine's real data, LOG.IC 3R goes to its active key 47368a7c,
Smog to fa331c83, and 8 of 883 names stay grey.

The stored names come with their times from the one scan node_names() already
ran (node_names_with_times), so opening a chat costs no second scan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 09:49:21 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 73be9692b4 A channel guide lists the public channels heard on the mesh
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>
2026-10-07 01:27:11 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 2d30e8e392 Channels export to a file, and import onto another device
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>
2026-10-07 01:15:58 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 eb55b51a12 The simulator refuses a channel slot the way the companion does
MockDevice raised DeviceCommandError for an index past its last slot.
The slot probe reads that error as a read that failed, not as the
firmware's "no such slot", so a probe on the simulator never finished.
The session never cached the slots under --mock, and anything that
insists on a complete read could not run there. The simulator now raises
the same RuntimeError that the companion's _ok raises for
ERR_CODE_NOT_FOUND, and the probe ends cleanly.

Refs: #25
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 01:15:58 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 4e22f29a74 The exit status docstring says what grep does
It said that grep treats "found nothing" and "worked" as the same
result. grep exits with 0 when it finds a line, with 1 when it finds
nothing, and with 2 on an error, so it is the usual exception. The text
now says so, and it says why MeshTerm uses 5: 1 is already FAILURE.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 23:48:50 -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 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 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 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 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 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 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 db6370407a A map moved faster than it can draw no longer freezes the PicoCalc
Panning or zooming the map quickly on the PicoCalc could stop the whole app
for many seconds. A watchdog run on the device found two causes behind it,
plus the port walk fixed in the previous commit.

Memory. A decoded tile holds 1 to 4.6 MB of RAM on the device (65 bytes a
point), not the 0.9 MB the code assumed. The source kept up to 24 of them,
counted as tiles, and the map screen kept 8 or more of its own on top, so a
few pans and a zoom out filled a 104 MB device. Swap is on the SD card, so
the process then stopped while the kernel read its code back in: 16,000
major page faults and a 15 s freeze in one run. Now the source holds decoded
tiles to a byte budget, one eighth of the machine's RAM (12.8 MB on the
PicoCalc, at most 128 MB on a desktop), weighed by mvt.resident_bytes. That
estimate was fitted against tracemalloc on the device's 157 cached tiles: 1%
under to 9% over there, and 6% under to 21% over on a 64-bit desktop. The
screen keeps only the tiles in view, and takes any tile already in RAM
straight from the source on the paint (BasemapSource.resident), with no trip
through a thread. The prefetcher warms the disk cache (BasemapSource.warm)
instead of filling the RAM with guesses, and passes over any tile already
written down. Tile loads take turns, two at a time, and a tile the view has
left before its turn comes is dropped, not loaded.

CPU. fill_polygon asked every edge about every scanline, which at z7 means
coastlines with tens of thousands of edges: 6.8 s of a 7.6 s frame. Each
edge is now filed under only the rows it crosses, and draws the same dots
(checked against the old fill on 3,000 random polygons, and in a test).
_draw_tile also tests each ring's bounds before projecting it, and skips a
ring wholly outside the view. That changes no dot: 1,500 frames from the
device's tiles match rendering with no culling at all. The one frame that
differed from HEAD was HEAD's own building cull, which dropped a footprint
whose fill rounds onto the canvas edge.

Measured on the PicoCalc with the profile-picocalc map_burst tour (keys sent
at 10 a second, from a pinned z10 view, real radio and database). The new
code ran first, so the baseline had the warmer tile cache:

  pans, catch-up after a burst     HEAD 1.0-1.8 s      now 0.4-0.6 s
  longest silence, whole tour      HEAD 11.6 s         now 0.6 s
  whole-process freezes            HEAD 8.0, 3.1, 2.1, 11.6 s
                                   now one, 1.6 s (idle pages swapped back in)
  swap at its peak                 HEAD 25 MB          now 7 MB
  warm raster, z7 / z8 / z9        HEAD 7.6 / 1.9 / 1.8 s
                                   now 0.57 / 0.29 / 0.81 s
  warm raster, z10 / z13           HEAD 0.49 / 0.56 s  now 0.26 / 0.26 s

Still open: z9 spends 0.23 s drawing lines; a prefetch decode of a heavy z7
tile is still 1.3 to 1.7 s on a worker thread; and pages swapped out while
the map sits idle can still cost a second or two to come back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 17:15:19 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 bdbf65312a The serial link check walks the ports off the event loop
Every two seconds the liveness watcher asks whether the companion's serial
port is still there, and that question is a walk over /sys/class/tty: about
30 ms of small reads on an idle PicoCalc. It ran on the event loop. Each read
hands the GIL back and has to wait its turn to take it again, and while a
worker thread is busy (a map frame being drawn, a tile being decoded) that
turn is up to 5 ms away, hundreds of times over. A freeze watchdog on the
device caught the main thread inside this walk in 20 of the 21 stack samples
taken while the map was stalled, for up to 1.2 s at a time.

It now runs in asyncio.to_thread, as the device picker's and the reconnect
dialog's walks already did; the worker pays the wait and the screen doesn't.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 17:15:03 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 61d7463ddf A connection that won't open says why, wherever it fails
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>
2026-10-04 14:43:53 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 b6e7cd5c16 The handhelds are named for their variant: picocalc-lyra and cardputer-zero
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>
2026-10-04 11:52:41 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 22398128f5 An SPI radio's GPIO chip is found by its label, not trusted by its number
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>
2026-10-04 11:36:44 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 810232cfb6 A link written without a scheme is found too, by www. or a real TLD
meshterm.net/map and www.example.org are links now, as surely as their
https forms: a host starting www., or ending in a top-level domain IANA
has delegated, read from the registry itself, kept verbatim in
assets/tlds.txt. So file.txt stays text, txt being no TLD, and so do a
decimal, an abbreviation, an address, and a path. A scheme-less link
opens, and its QR code encodes, as https://. Link finding moves out of
the chat screen into core/links.py.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 00:55:59 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 c1d0772baf Lint and format the last few commits
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 02:09:04 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 0e9f41fb2a A reboot no longer waits for an acknowledgement the board never sends
Over Bluetooth every command is a write-with-response, and a companion that
restarts on the reboot command can drop the link before acknowledging the
write. The write then hung until the link supervision timeout, holding the
reboot dialog back by a second or more. The reboot now returns once the
write has had a quarter second to fail, and leaves the rest to the teardown
that follows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 01:20:05 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 854b7fd765 A T-Deck on MeshOS or wadamesh pairs over Bluetooth like any companion
Those firmwares guard only the UART write characteristic, so the
refusal arrives on the handshake's first write, which meshcore's send
swallows. The empty handshake read as "not a MeshCore companion", and
the PIN pairing, the stale-bond repair and the macOS Passkey wait never
ran. The write is now wrapped to keep its refusal, and an empty
handshake behind one raises it, so both firmware shapes take the same
pairing path.

bleak no longer gets the PIN on Windows either: its WinRT pair() is
Just Works only, and after a mistyped PIN it left Windows holding an
unauthenticated bond that the next, correct attempt reused and was
refused over.

Verified against a T-Deck on MeshOS 1.3.0: a planted Just Works bond
heals in one run, and an unbonded connect without a PIN now says the
device needs one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:26:14 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 0cd40bbd6c Fix three small CLI rough edges: records --width, the simulator's label, two help pages
records --width now refuses anything but 1, 2, or 4 with a usage error, the
way --category already refused an unknown discipline. A width that doesn't
exist used to come back as exit 5, indistinguishable from an empty database,
and --width 0 was falsy and so listed every width. (#15)

A device with no port is labelled by its name alone. The simulator fell
through to the serial branch and came out as "… (nothing transmits) ()". (#14)

platform and specimen pass their own one-line help, so Typer stops printing
their docstrings — notes for whoever reads cli.py, Sphinx roles and source
paths included — and their summaries fit in meshterm --help. A new test runs
--help on every registered command and refuses those markers anywhere. (#16)

Built from the issues alone; the pull requests filed against them were
closed unreviewed and their code was never read.

Closes #14, closes #15, closes #16.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 10:59:17 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 b7a1ad47c2 The Oxford comma, always, the one-line description included
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>
2026-09-25 21:28:00 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 79137598cf Clock sync explains a radio whose clock is ahead instead of calling it malformed
MeshCore firmware sets its clock only forward: CMD_SET_DEVICE_TIME with an earlier time is answered ERR_CODE_ILLEGAL_ARG, which read as 'the device rejected the request as malformed'. Since 2026-09-24 the companion's clock has been ahead of this computer's (itself within 0.7 s of NTP), so every connect's sync was refused. set_clock now reads the radio's clock first: within a minute ahead is in sync and nothing is written; further ahead raises ClockAheadError, which says by how much and that a reboot resets it. The device layer also maps the firmware's refusal to that error, and the simulator's clock now only moves forward, like the firmware's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 17:10:09 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 bd0c311dbc Bluetooth on Linux stops stranding its own link, and pairs with the PIN first time
Measured against a T1000-E from a uConsole (BlueZ 5.66, btmon on the side).

A connect that BlueZ had to retry stranded its link. bleak retries a link
attempt that failed to synchronise (le-connection-abort-by-local, 3-7 per
connect on a Pi radio) inside connect(), and each one fired meshcore's
disconnect handler, which resets BLEConnection.client to None. bleak's
next retry then connected a client nobody held, meshcore gave up as "not
established", and the live link kept the companion from advertising, so
the retry scanned for 30 s and reported it out of range. Three runs out of
three. While connect() runs, a disconnect is now only recorded, and the
teardown keeps the last live client if meshcore let go of it anyway.

A companion given a PIN after it was paired unauthenticated answers the
subscribe with ATT 0x0E, Unlikely Error (bond Authenticated=0). That is now
read as a pairing refusal, so the stale-bond message and the PIN repair
reach it; with the PIN it re-paired and connected.

BlueZ's Pair left its link up (the companion then stopped advertising to
the connect), and failed at once on the same link flake bleak retries. It
now puts the link down after pairing and retries a flake a bounded 4 times.

And BlueZ starts a pairing of its own when the subscribe is refused, asking
the default agent; with none (any headless machine) it waited on a yes/no
until the companion's 30 s SMP timeout. For the length of a connect
MeshTerm is now the default agent for that one device, KeyboardOnly: with a
PIN it answers, without one it refuses at once, and it never approves a
pairing without the passkey, which is how a bond ends up weak.

Unpaired, no PIN: 33 s -> 4 s to "requires a PIN". Unpaired with the PIN:
paired and connected in 11 s. Paired: connects in 7 s, where it failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 04:37:51 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 491325f995 Linux pairs a Bluetooth companion with its PIN, and a serial port says why it won't open
On Linux the PIN never reached the radio: bleak's BlueZ pair() calls
Device1.Pair and nothing else, BlueZ asks a pairing agent for the passkey,
and bleak registers none. Without a desktop agent BlueZ falls back to Just
Works, the firmware refuses a bond without MITM, and the picker looped on
"That PIN was rejected" for a correct PIN. MeshTerm now registers its own
Agent1 for the length of one pairing (core/bluez.py) and calls Pair from
the same bus connection, so BlueZ routes the passkey request to it and
never to the desktop's agent. The Windows repair contract carries over:
an existing bond is reused, force removes it and pairs afresh, and the
outcome is recorded so a refusal names it. Unpair & quit and the stale-
bond message work on Linux too, and the remedies there name Linux and
`bluetoothctl remove` instead of Windows' Settings. bleak no longer gets
the PIN on Linux, where it could only start a second pairing.

A serial port that refuses to open was reported as "didn't answer as a
MeshCore device", with nothing logged. Permission denied now names the
dialout group, a held port names the other program (and ModemManager on
Linux, also named when the port opens but never answers), and a vanished
port says so. A failed startup probe is logged at warning.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 03:58:26 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 ed2ad3b7ab A Bluetooth connect that fails says which step failed
One "requires a PIN" sentence covered every GATT refusal, and a link that
never opened read as "didn't answer as a MeshCore device". A Heltec V4 user
whose Windows bond had gone stale was told to pair a device Settings showed
as paired. Each failure point now has its own message and remedy: link never
opened, stale Windows bond, wrong PIN, device refused to pair (with the
WinRT status), paired but still refused, a BLE probe timeout by stage, and
link up but no identity reply. The PIN dialog shows the diagnosis instead of
calling every refusal a rejected PIN, and each case is logged at warning so
a log sent in from another machine names it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 03:43:15 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 01a8f711d6 The session flood scope is framed by MeshTerm, so an older meshcore 2.3 can send scoped
reset_flood_scope and force_unscoped arrived late in meshcore 2.3.x. The uConsole's 2.3.7 satisfies meshcore>=2.3 without them, so every scoped channel send would have crashed on restoring the scope. set_flood_scope now sends the firmware's command 54 itself ([54][0][key], [54][0], [54][1]), as the default scope's command 63 already was. Every other library call MeshTerm makes exists in 2.3.7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 02:06:44 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 326f6f5479 Another command's refusal no longer ends the channel list early
The meshcore library does not serialize commands, and every command
waiting for "OK or ERROR" takes the first ERROR to arrive. At connect the
channel prewarm runs beside the clock sync, which this companion refuses
as malformed; a slot read handed that refusal took it for "no such slot",
ended the probe there, and cached a list missing every channel after it
for the rest of the session.

Only the firmware's NOT_FOUND (or a code-less refusal from old firmware)
now ends the list. Any other refusal is asked again once, and a second
one, or no answer at all, is a failed read the probe reports as
unfinished and does not cache.

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