About (Build) and the boot log name the meshcomod release the core is:
"on meshcomod v1.18.0.1". release.sh reads it from the pinned core's own
examples/companion_radio/MyMesh.h, keeps the in-tree defaults in step and checks
every staged image carries it. Language store v26 (this build's new texts),
release notes for beta_95.
Bench (T-Deck, 1.18): device and self info, 404-contact sync, the companion CLI
(ver, board, get name/radio/tx/freq, unknown command), 4 packets received and a
zero-hop advert sent in 90 s, correct device time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins move from core-v1.17.4 to core-v1.18.0, cut from the main meshcomod
companion-v1.18.0.1 was released from. The companion changes are ported from
meshcomod's rebase into wadamesh's own MyMesh:
- onCLICommandRecv: a contact's remote CLI command runs when the user granted
that contact remote CLI, and lands in the chat as a CLI message otherwise.
- CMD_RUN_CLI_COMMAND (66) / RESP_CODE_CLI_REPLY (29) and the local companion CLI
(name, pin, Wi-Fi, radio, af/dutycycle, rxgain, tx, rxdelay, path.hash.mode,
multi.acks, board, ver), also reachable from the serial CLI. Settings without a
prefs field answer "unsupported". NodePrefs layout unchanged.
- TXT_TYPE_CLI_COMMAND is accepted for sending; sendCommandData (and our queue-
preserving wrapper) carry the text type.
- Channel data is delivered to the app (onChannelDataRecv), unsolicited responses
(pushed telemetry) reach it as PUSH_CODE_BINARY_RESPONSE, and raw-data sends
decode multi-byte-hash paths.
Clock: 1.18 seeds an unsynced ESP32 clock at 1 Mar 2026, so ClockFloorRTC's
MIN_VALID_EPOCH follows it (a set equal to the seed must stay invalid).
Pager LR1121: the AGC reset re-applies the boosted-gain setting (new core arg).
IDF builds exclude the new CommonRadioPrefs.cpp (needs <target.h>/radio_driver).
The 2 MB Heltec V4 went to 100.0% with the CLI. Its built-in language table is now
one raw-deflated blob per language (895 KB -> 312 KB), inflated into PSRAM by the
S3 ROM on first use (~10 ms, ~75 KB for the active language): 91.7% flash. Checked
on a T-Deck forced onto the table (nl, hu, el) and by the post-link key check.
Builds: all 15 S3 touch envs, Tanmatsu (3,237,024 B), T-Display P4 SX1262 + LR2021.
T-Deck boots and runs on 1.18.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spectrum: stat tiles (peak, floor, our channel, busy), plot and waterfall drawn
at screen size (the lv_chart and zoomed waterfall were the page's render cost),
dark-to-yellow waterfall, peak hold, our-channel marker, Band/Channel and
Live/1 min/10 min chips. RF Monitor 1.4 (tiles, filled chart, typed feed, packet
detail card; the old type table did not match MeshCore payload types) and
Airtime 1.6 (big figure, tiles, duty line, rx/tx split).
Lua SDK: ui.panel, bold labels and a size-24 figure font, colors.warn/border,
app.on_audio (fires with the screen off), read-only wada.sd.list and sd: audio on
the SD_MMC boards (P4, Tanmatsu, Wio L2), caps panel/audio_events/epaper, a
"music" icon and an "audio" Store requirement. button:set() no longer writes
label fields into the button object (heap corruption, every earlier firmware).
The SDK page's input table described events the firmware never sent.
Music 1.0 (Store app): plays MP3/WAV from the card's Music folder, shuffle,
repeat, keeps going through the list with the screen off. Verified on the
T-Display P4 end to end.
Store: the update check also fetches the app catalog and badges the Store tile
with the number of installed apps that have a newer version. Baked Lua apps are
raw-deflated and inflated by the S3 ROM (14.6 KB back on the V4); the V4 language
table keeps only texts of apps it can run.
Fixes: the SD-card notice no longer fires when the card mounts fine (#667); the
Control Center clock keeps time while open (#666); M9 keyboard brightness (#664);
the battery widget's number no longer sits on a ring too small for it (Discord);
M9 shortcuts doc follows #669's composer order. New texts in all 14 languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Non-touch M9: Quick Reply | text | Emoji | # | Send. Touch boards: Quick
Reply | text | Emoji | Send. Emoji and # align off Send's slot so they
follow the text box's new width on a rotation; the P4's landscape row
matches. The M9's arrow keys walk the new order: RIGHT from the end of
the text goes to Emoji, then #, then Send; LEFT walks back # → Emoji →
text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Us6CjkLFga9GkFJmmr3rrq
Notification chimes, custom WAV notification sounds and Lua wada.audio
playback through the ES8311 codec + NS4150B amp, mirroring LilyGo's
examples/ES8311/playWAV bring-up. I2S DOUT is GPIO40, not the header
name's 17 (measured in GlomarGadaffi/tdeck-max-phone#34); the XL9555
amp enable (line 6) is bracketed around playback and the shared-speaker
mux (line 10) held to the ES8311. Reuses the pager's Es8311Codec driver
and enables CAP_SOUND/CAP_SOUND_FILES/CAP_AUDIO_STREAM for the Max.
Signed-off-by: Ender436 <68835715+Ender436@users.noreply.github.com>
#662's test queued three keys and read them all after one poll; #628 (for #625,
keys typing twice) reads one key per poll and drops the same key again within
70 ms. The test now polls per key and also checks that a repeat inside 70 ms is
dropped and one after it is a new press. Host tests: 18 pass, 2 skips.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#628 dropped the close X from actions-only confirms on every CAP_KEYPAD_NAV board,
which includes the V4, the T-Deck and the touch-only boards that take a Bluetooth
keyboard. Now only a board without touch drops it; touch boards keep main's X,
skipped by keypad focus.
#580's chatCreateLinkLabel called lv_obj_update_layout on the message label to read
its height, laying out the whole screen for every link bubble as it scrolled in.
The caller already measured the wrapped text; it passes that height in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Radio widget drew its airtime as a 5 px bar pinned to the bottom edge, which
on the usual widget height landed on the detail line (SF / BW) and looked like a
stray purple line through the text. Gone from Radio; and hwCardBuild now leaves
the bar out wherever it would not fit under the text (the Lua app widget's bar).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chatComposerChromeH() is the row padding plus the top row, used for the
text box height, the auto-grow and the one-line radius. A board-specific
inset (the P4's bottom lift in #611) then changes one helper instead of
four expressions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- M9 shortcuts guide: the function row is Back, Ctrl and Triangle (no Mic).
- The X-last action lists, the bubble-only chat cursor and the lifted
message-menu X are keypad behaviour, so they follow CAP_KEYPAD_NAV
(Pager, Tanmatsu, T-Deck, ...) rather than HAS_THINKNODE_M9.
- Actions-only confirms (#603) drop the X on keypad boards only; touch-only
boards keep it.
- M9 map buttons: focus keeps the accent fill but no local outline, so the
shared keypad cursor is not outranked.
- M9 chat header: compare the bar's style height, not its stale coords.
- Contacts bulk delete ends with onThreadsChanged() so web clients refetch.
- The contacts selection buffer (~12 KB at MAX_CONTACTS) is allocated when
select mode opens and freed when it closes, not at boot.
- Boot time sync: disconnect before the saved-network scan, so a first
attempt still mid-handshake cannot fail it with ESP_ERR_WIFI_STATE.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
They sat on the title's line. They now centre on the header's two lines,
level with the back arrow and the avatar, so the whole header is centred
vertically. The step in from the rounded corner stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
The keyboard controller latches a single key. When one was pending,
m9KeyboardPoll read the register up to three times back to back, a
"drain" meant to catch a second key, but with one slot a second read
can only return nothing or the same key again if the controller has not
cleared it yet. Keys came out twice, often and on any key.
- One read per poll, as the Launcher and Meshtastic M9 ports do.
- The same key reported again within 70 ms of the last accepted one is
dropped as the same press. That covers a stale latch read on the next
poll and the matrix bouncing on a slow, deliberate press, which the
report found worse. Nobody presses one key twice that fast on purpose.
The guard runs from the last accepted key, so bounces cannot stretch it.
Fixes#625
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
The M9's UI sizes grow the fonts but not the geometry, so the header's
title and subtitle, centred in a bar of fixed height, clipped the
subtitle ("3 nodes heard today"). The back arrow and avatar sat low, the
clock and battery sat mid-bar while the GPS glyph kept the top row, and
the message list started several px below the header, leaving a strip of
bare background.
- m9ChatHeadH() gives the header's height from its fonts: the two lines
from 3 px down, 2 px under them. The bar takes that height in a
conversation, and makeChatDetail starts the list's content at the same
line. The list is virtualised and places its rows against that padding
once, so it is set when the list is built.
- The back arrow and avatar centre on the two lines.
- The clock, battery and GPS glyph share the title's line, with the GPS
just left of the clock. The group steps in from the panel's rounded
corner, which clipped the battery, and the title stays short of it.
- Leaving the conversation puts the bar's height, the battery and the
GPS glyph back.
Other boards are unchanged.
Fixes#627
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
In a tall bar, updateGlobalStatusBar shifts each child of the bar up half
a row to put the system icons on the top row. It skipped the full-bar
backdrops (fade, dim) but not the shade drawn over a background picture,
so the shade sat half a row too high. The bottom of the bar showed the
picture unshaded, a strip over the top of the content below it. Measured
on the M9's conversation header: bar y0-38, shade y-11-27.
The shade is skipped like the other backdrops. All boards with a
background picture and a tall bar (conversations, settings pages).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
The Wio Tracker L2 has no battery-backed clock, so it gets the same
opt-in saved-Wi-Fi time sync at power-on as the T-Deck
(CAP_BOOT_TIME_SYNC / BootTimeSync).
Split out of 4844a9b (Touch UI follow-ups 4).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The composer reads QR | # | emoji | text | Send. With the caret at the
start of the input, LEFT walks to emoji, then #; RIGHT walks back.
Split out of 4844a9b (Touch UI follow-ups 4).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CMD_SEND_CHANNEL_TXT_MSG called sendGroupMessage directly, so a post from
the phone app on a scoped channel went out on the default scope. It now
pushes the channel's scope for the send and pops it afterwards, as the
on-device composer, web remote, terminal and Lua sends do. Touch builds
only, where per-channel scopes are stored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
The key is printed with a triangle, not a microphone, so the docs and the
code now call it the triangle key (M9_KEY_TRIANGLE). In an open chat it
used to open the thread's options; keypad navigation reaches the
conversation header for that now (#623), so the key keeps only its other
job, the focused item's hold action. M9_PORT.md no longer calls it
unbound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
- M9: double-press Home opens the launcher settings (what Home opens, icon size)
- M9: "Mark all as read" is the first item in the chats menu
- M9: MIC key opens the chat options in a chat, else the focused item's hold action
- M9: stronger focus on the map overlay buttons
- M9: the at-a-glance card shows even when the message-flash wake lit the screen first
- Tanmatsu: screensaver preview ignores input for its first 1.2 s so it no longer ends at once
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit f479382c9b12070cffae530e3b4789d689c441c5)
- Map: the transparent tab bar shows white icons from opening the Map tab until
its first tile render finishes, then returns to its map colours.
- Home: a second press within 450 ms opens Settings. The keyboard controller
sends no key-up or long-press code for Home, so a hold can't be detected.
- Chat: the keypad cursor surrounds the message bubble only, not the info
line under it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 9523475232aa1a73e2c7c3395888407dfd4020c8)
The ⋯ / thread action list creates its close X after the rows on the M9, so
keypad focus opens on the first action and reaches the X last. The message
action menu's X moves up into the card padding so the first button row no
longer clips its focus cursor. Other boards unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 8fdb23ce5f5dbf58514304ab3be7a5dd04c4cdd8)
The first attempt on the active network brings the radio up from cold and
searches every channel for the AP, inside the same 4 s as every other
attempt. A T-Deck that joins in about 7 s by hand never made it, and the
scan retry then skipped that network as already tried, so a home with one
saved network always ended "no association".
The first attempt now gets 8 s. The scan retry keeps the active network
and joins each candidate on the channel and BSSID the scan found it on,
which skips the all-channel search. The whole session is capped at 16 s
(was 12 s) so the retry and SNTP still fit.
Fixes#620
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
(cherry picked from commit 477e0163ccf64d65706737a9558f1ef9ab439539)
- Compact rows that carry a colour emoji get its overhang above the text
line as extra top padding, measured into the row height so the
virtualizer agrees (chatCompactEmojiLift).
- When the composer grows while the list is at the bottom, the list is
scrolled back to the newest message instead of being covered.
- The composer's corner radius stays at its one-line size, so a four-line
box no longer curves through the start and end of its lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkSqsocfpbhCD2AtpskvQo
(cherry picked from commit 867db9917e682ac24f2542ecd77224fcf27f232a)
The chat composer tagged a channel post with the channel's own region scope,
but termDoSend (web remote and terminal) and luaHostMeshSendChannel called
sendGroupMessage directly, so those posts went out on the default scope.
All three now go through sendGroupMessageScoped, which pushes the channel's
scope for the send and pops it afterwards.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit fa1ec9868fdee31922ee57a5ed0a940522bb86c1)
- Name (A-Z) no longer sinks "?" (never heard) contacts below the heard
ones; that rule now applies to the Last heard / Last message modes
only. Favourites with and without a last-heard sort together by name.
- Select all selected at most 128 contacts, in storage order, while the
list shows the first 128 in sorted order. Past 128 contacts it picked
mostly rows that were not on screen, so nothing visible was ticked and
a delete seemed to do nothing. The selection is now sized to
MAX_CONTACTS and Select all takes every non-favourite that passes the
filter.
- Bulk delete removes without saving (uiRemoveContactNoSave) and writes
/contacts3 once at the end, keeps only the public keys for the run,
and matches the selection by binary search.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkSqsocfpbhCD2AtpskvQo
(cherry picked from commit f30ca794039ff11358903938d70e402019908776)
On the T-LoRa Pager the "Import this backup?" confirm started on the
close X, then stopped on the message text before reaching the buttons:
keypad navigation follows the card's tree order (X, message, Cancel,
Import), and confirmBackupImport() used none of showConfirm()'s options
for that.
- confirmBackupImport() now passes actions_only_nav (the message box is
not a focus stop) and focus_confirm (focus opens on Import). With two
stops, the group's wrap loops Import <-> Cancel in both directions.
- showConfirm() no longer creates the X at all for actions_only_nav
dialogs, rather than only skipping it in navigation. Cancel and the
Back key already dismiss them. The Purge-discovered dialog uses the
same flag and loses its X too, which keeps the two consistent.
Fixes#603
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
(cherry picked from commit 1df7b93ef91d5ca3790271e3d216409988cabf1c)
(cherry picked from commit 29a842b4a2de8bb2ff8a849b14d57955d17b2729)
chatHeaderApply now resets chat_back's translate_y, which updateGlobalStatusBar
skips in a chat, so a chat opened from a settings or app page no longer keeps
the half-bar upward shift. DEVICES.md and the site describe the hardware
testing, and the site counts 14 boards.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
It was shown from setupWizardMaybeOpen(), before the drawer (the default Home)
was built on the same top layer, so the drawer covered it. It now opens after
the drawer, in place of the crash-report prompt for that boot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Short strings in all 14 languages; the long SD-fallback and track-stopped texts
as English placeholders until translated. v25 snapshot kept equal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Asked for on Discord (prepper use: books and documentation offline on the card).
- The Reader opens a .txt or .md file on the card as a 16 KB window at an
offset carried in the URL (sd:/x.txt?o=49152), cut back to a line (or a whole
UTF-8 character), with Next page / Previous page / Back to the start links and
the position in percent. A book of any size pages through; Back works as usual.
- Files opens HTML on the card in the Reader (its links work there, so a folder of
pages browses offline) and text or markdown too big for the 8 KB editor; small
text files keep the editor.
- scripts/sdpack.py (standard library only): an EPUB becomes /library/<name>/ with
one page per chapter (long chapters split under 30 KB, linked in order) and a
contents page; .txt/.md are copied as they are; /home.htm lists everything, so
the Reader, which opens sd:/home.htm, starts on the library. Tested with a
generated EPUB.
ZIM (Wikipedia) files are not read on the device: their clusters are zstd/xz
blobs of 1-2 MB and the files run to tens of GB on FAT32; converting on a
computer keeps the device side small.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported on Discord: a T-Deck that kept its data on a card that died booted
into first-time setup with a NEW private key, and the next good card was then
given the new key, so the old one was gone for good.
- DataStore keeps a copy of the main identity in NVS (namespace mc_id, 96 B):
every save writes it, a load with no identity file falls back to it and puts
it back into the active store, and a successful load seeds it when missing. A
different copy is never replaced silently; only an explicit save changes it.
Verified on a T-Deck: one boot with the card seeded mc_id.
- When the SD store was chosen but the card did not mount, the boot is flagged
(g_sd_fallback_boot) and, instead of the setup wizard (whose done-mark is on
the card too), a dialog says the card could not be read, that the key is kept
(or, on a device that never had a copy, that it started with a new one while
the card keeps the old), and offers Try the card again.
The retry ladder at lower SPI clocks already existed. Not yet done: copying the
data off a card that mounts but refuses writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Asked for on Discord. A switch in the map options records where you go into
/meshcomod/tracks/<date-time>.gpx on the SD card, a standard GPX 1.1 track that
other mapping software opens, with elevation and time per point.
- Runs from the UI loop beside the GPS saver, so it keeps recording with the
screen off; the GPS stays awake while it records (gpsWantedLive).
- A point once we have moved 10 m (GPS jitter stays out of the distance), or
every five minutes after at least 3 m. Climb counts steps of 3 m or more.
- The file is complete after every point: the closing tags are rewritten behind
each new one (r+ and seek), so a flat battery still leaves a file that opens.
- The map draws this session's track as a red line (one lv_line, thinned to 256
points) and the status corner shows the distance and climb while recording.
- Three failed writes in a row stop it with a message; no card, no start.
Boards with an SD card and GPS (CAP_SD or the Pager). The app route was ruled out:
Lua apps stop ticking with the screen off and their files cap at 32 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
beta_93 pinned the Heltec V4 to the beta_92 .lang files because the full table
no longer fit. lang_reach.py now works out which keys any source can look up:
a .lang key is kept when it equals a C string literal anywhere in the sources
(every #if branch, adjacent literals joined, LV_SYMBOL_*/UI_ICON_* and other
string macros expanded, escapes decoded as GCC does), or that literal with TR()'s
glyph prefix stripped, or a string in any Lua app. 1818 of 1925 keys survive; the
107 dropped look unused (old dropdown strings, texts the source has reworded).
The trimmed live table is 15.5 KB smaller than the pinned one (V4 4,025,365 B
used, 37.9 KB free) and carries every translation added since beta_92.
Guards: the generator refuses to trim when an input is missing (lv_symbol_def.h,
UITask.cpp, or under 80% kept) and leaves the header alone; after linking the V4,
--check-objects reads the strings the compiler emitted and fails the build if a
translated key among them was dropped (proved by dropping a live one). The
pre-build hook regenerates on source changes too, and the header is only
rewritten when its content changes. The pinned copy is gone (git history).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"The Bluetooth chip is not responding." and "Bluetooth could not start.
Restart the device and try again." in all 14 languages, v25 snapshot kept equal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since beta_88 (b52c71f) the P4 stored its BLE name and PIN only when BLE was on
at boot and the C6 came up for it. A unit that booted with BLE off (or once had
it off, since that is saved) then failed every enable at once, and the UI read
every refusal as "Not enough free memory for Bluetooth", Wi-Fi off or not.
- main.cpp stores the parameters on every boot; storing them does not touch the C6.
- enableBle() brings the C6's BLE controller up on a runtime cold start
(hostedInitBLE) before NimBLE, or NimBLE would start against no controller.
- The transport records why a cold start did not happen (no parameters, the
memory guard, the radio) and the toast says so; only the guard is about memory.
Not run on a P4 yet: the P4 build has both strings in it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An app's manifest can list files beside its code as one quoted string,
"files":"beep.wav,track.mp3" (the manifest reader takes strings only). The
Store installs each into /apps/<id>.d/, the folder wada.fs and
wada.audio.play("beep.wav") already read, so an app can carry its sounds. On
the T-Display P4, whose Lua SD API is off, that was the only way to get a sound
there (#lua-app-dev). Files stream to storage through a 4 KB internal buffer
(SD writes from PSRAM come back short), the server must send a length, a
short read deletes the partial file and fails the install. Up to 8 files of
4 MB, names as wada.audio's. No app uses it yet, so existing installs are
unchanged. SDK page documents it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>