Review follow-up (B0N3-GD): Fn is the Pager's only symbol key, so a
held-Fn letter chord takes that letter's symbol away -- Fn+V cost the
question mark with Fn held. Fn+Enter types nothing today (Enter has no
entry in the symbol map), so the chord moves there and costs nothing.
- PagerKeyboardState: V_POS -> ENTER_POS (row 1, col 9); the chord is
still resolved by matrix position before the symbol map, only while
Fn is physically held, and cleared by discardAlt().
- PagerKeyboard.h/.cpp, UITask.cpp: renamed to *AltEnterChord*; the
paste path itself (txtPasteInto, hex-key extraction, toasts, drain
sites) is unchanged.
- test_pager_keyboard_state: chord on Enter fires once and swallows the
press; a plain Enter still returns '\r'; tap-Fn-then-Enter is not a
chord and types nothing, as before; Fn held + V types '?' again.
- TLORA_PAGER_SHORTCUTS.md: both rows now say Fn held + Enter, and the
em dash is gone from the prose.
Rebuilt and flashed on a Pager (RAM 37.8 %, Flash 90.0 %, unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
Contact nicknames: TouchPrefsStore nick blob (48 x 38 B), display-only lookups
(contactShownName, threadShownName, senderShownName), Contact options >
Nickname on the shared text prompt (an empty OK clears it).
Control panel restyled in the launcher's tile language, with a Battery saver
tile; keyboard tooltips (navSetTip), an amber cursor and touch long-press
tooltips (the T-Deck touch read hid them on every poll).
T-LoRa Pager: M/C/A/S on Home, H/S tap and hold, Backspace = Back, N = jump to
new, held Enter = long press, per-tab focus memory, Power off confirm; LVGL
-O2 and IRAM, INT-gated keyboard, idle yield, status-bar setter guards, instant
tabs; PagerGps (u-blox software standby for rests, GPS rail cut when off),
panel sleep, amp off at boot, power-off parks the radio (NSS held), GPS, panel
and keyboard light.
Fixes: the DM duplicate check keys on the attempt and a path reset clears it,
the direct timeout masks the packed path byte, web remote hops (#636);
changelog over http (#638); Lua SDK send budgets and duplicate guard (#606);
GPS speed note (#613, CAP_GPS_BAUD_AUTO); emoji chat icon (#640); default
wallpaper (#626).
Heltec V4: Tetris, 2048 and Breakout unseeded and the built-in Lua stripped of
comments (identical bytecode), 27 KB freed; the baked language table is pinned
to beta_92's files (scripts/build/lang-pin-beta92) so it still fits.
Translations: 39 new keys in 14 languages, language store v24.
Site: theme, dark mode, aurora and guide refresh, SDK send limits, nicknames.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the T-LoRa Pager there was no way to paste: the in-RAM clipboard is
only consumed by the Cut/Copy/Paste edit menu, which opens solely from a
touch long-press on a textarea, and the Pager's encoder never reaches
LVGL's press events. Copy from a message bubble worked; nothing could
paste the result -- the 64-hex contact key and 32-hex channel secret
fields being the painful cases.
Physical Fn(Alt) held + V now pastes into the field being edited:
- PagerKeyboardState: V_POS; a held-Alt V press is resolved by matrix
position before the symbol map, swallowed, and reported through
alt_v_chord_pending_/consumeAltVChord() (same shape as Alt+Backspace
and the Pro's Alt+B). discardAlt() clears it. !HAS_TDECK_PRO only.
- PagerKeyboard.h/.cpp: pagerKeyboardConsumeAltVChord().
- UITask: the Paste cell's logic is factored into txtPasteInto(ta)
(selection replaced, pasteTextFor hex-key extraction kept) and reused
by updatePagerAltVChord(), which uses the same "actually editing" test
as the Alt+Shift Caps chord, hides any open edit menu/accent popup,
and toasts "Pasted" / "Clipboard empty". Drained at the remote-mirror
and screen-off discard sites like the other chords.
- TLORA_PAGER_SHORTCUTS.md: documented.
- test_pager_keyboard_state: chord fires once and swallows V;
tap-Fn-then-V still returns '?'; discardAlt() drops a pending chord.
Trade-off: Fn+V is '?' on this layout, so only a HELD Fn chords. The
one-shot (tap Fn, then V) and locked (double-tap Fn) symbol layers still
type '?', the same compromise the Pro's physical Alt+B makes with '!'.
Tested on tlora_pager_sx1262_companion_radio_touch: Copy from a bubble
then Fn+V in the composer pastes; Fn+V in a key field pastes just the
key out of a copied message; empty clipboard toasts; tap-Fn-then-V
still types '?'.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
Every screen redesigned to the Oct 2026 mockups, on one design system:
palette, type roles (generated Montserrat faces with Lucide line icons on the
LV_SYMBOL codepoints), a theme apply-cb that styles every LVGL widget, button
kinds, list rows, sheets as action lists, toasts and key-glyph avatars.
Rebuilt: status bar, Chats (filters, + and menu), conversation (route header,
hop meta, "Heard by N repeaters", delivery states, pill composer), lock screen
(mark, clock, 24 h activity, unread cards), Constellation screensaver, Home
launcher and launcher sheet, Status page, grouped Settings home, animated boot
splash, and the message glance as a notification card.
New: optional PIN lock (salted SHA-256, keyboard / touch pad / number pad per
board, backoff that survives reboot), screensaver on battery with its own
duration, T-Deck text sizes, trackball hold to switch Scroll and Cursor (Scroll
default, prefs v69/v70), hold Settings tab for the control panel. M9 bottom
notices removed. Every screen measured for the Large and Huge text sizes.
Fix: adverts over 2/3-byte path hashes never reached the recently heard table
(packed path_len compared as a byte count); ADVERT_PATH replies now carry the
whole multi-byte path.
English placeholders for the 204 untranslated keys in every .lang file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes#591, #592, #593 and the M9 Bluetooth status-bar overlap museifu696
reported. Also the T-Pager battery saver zomgugoff asked for and the status-bar
clock fix.
Review notes, since this landed on top of a day of P4 and M9 work:
* The prefs change is done the right way. condense_nav is appended at the TAIL,
CURRENT_VERSION goes 65 -> 68 with v66/v67/v68 each described, and the
trailing static_assert moves to the new last field. v67 and v68 are one-time
per-board resets with no new field, which is the pattern v63 and v65 used.
* Six conflicts, all in test_touch_prefs_schema.cpp, all the same shape: we had
both repaired the same broken sums. His version is a superset (it adds
condense_nav on top of the report_ping/report_done_n I added), so his wins.
Verified afterwards that both sides survived rather than trusting the merge:
the channel-send guard, the P4 tile-FS fix, statusPctOverflow, m9NoticeSet,
m9DiscoveredUnseen and TDP4_C6_ID are all still present, as are his kbd_nav
and Condense Nav work. Host suite 17 pass / 0 fail at v68.
Also here: PUBLISH the translations the PR added.
It added five strings to all fourteen language files with real translations, but
left "# ver: 22" alone and did not touch langs.json. Devices fetch lang/<ver>/,
so the version is the only thing that makes one re-download: without a bump the
work sits in the repo and reaches nobody, and every non-English device shows
those strings in English. The baked-in table was regenerated, so compiled-in
languages were fine -- it is the downloaded ones that would have silently
stayed behind. Bumped to 23, snapshotted into lang/23/, and langs.json points
at it. deploy-apps.sh validates: 14 languages and 14 apps, every referenced
file present.
That store still has to be pushed with the release; editing deploy/apps/ is not
publishing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The T-Display P4 ships EIGHT images: panel (AMOLED/LCD) x radio (SX1262/LR2021)
x C6 stack (esp-hosted/ESP-AT). The C6 axis is new and it is the dangerous one,
because the two builds are identical apart from the artifact name they carry,
and flashing the wrong one at an older unit used to boot-loop the board.
scripts/build/p4-release.sh builds all eight and VERIFIES each image against the
OTA name baked into it before accepting it. That is the same string the device
builds its self-update URL from, so a disagreement with the filename is exactly
the failure that bricks people -- the T-Deck Max self-updating to Pro firmware,
and this table has been wrong three times. A mismatch aborts the release.
It earned that check immediately: building the eight by hand produced two
mislabelled images, and every mechanism meant to prevent it was itself broken.
* build.sh fullclean has NEVER worked from a plain shell. It ran `cmake -E
remove_directory` above the esp-idf/export.sh that puts cmake on PATH, so it
printed that it had removed the tree, exited 0, and removed nothing. Now a
plain rm -rf, and it verifies the directory is gone instead of assuming.
This matters beyond tidiness: the hosted and ESP-AT builds SHARE a build dir
(the C6 axis does not change P4_BUILD_TARGET), the backend is read from the
environment at CMake CONFIGURE time, and CMake does not reconfigure because
an env var changed -- so a clean that silently did nothing yields an
incremental build of the OTHER variant, with no configure output to show for
it. That is precisely how the two mislabelled images were produced.
* Deleting CMakeCache.txt alone is not a clean either; it leaves CMakeFiles/
behind and the next configure dies in CMakeDetermineCCompiler.
* Detecting failure by grepping a log for "error:" misses a failed CMake
configure, which prints "Configuring incomplete" and no such line -- and the
previous application.bin is still sitting in the directory to be picked up.
Exit codes now.
gen-flasher-meta.py gains the four ESP-AT entries, labelled "older unit, ESP-AT
C6": someone choosing in the web flasher has no serial log and needs to pick on
something they can check.
scripts/run-host-tests.sh runs test/ as one command. Nothing ever did, which is
why test_touch_prefs_schema had been failing to COMPILE since v64 appended
report_ping and report_done_n without updating its sums -- and a static_assert
that does not compile is not a guard. That one exists to catch the mid-struct
insert that broke v44 and shipped the poisoned beta_57 layout. Repaired, with a
note where the next person appending a field will read it. 17 pass, 0 fail, 2
skipped with their reasons stated rather than quietly dropped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On short landscape panels (the T-Pager is 480x222) the bottom nav bar
spends a full-width strip on five icons. New Settings > Display switch
"Condense Nav", off by default, applies after a restart:
- The nav bar becomes a rail on the right edge (LV_DIR_RIGHT tabview).
Pager cells show the key letter under the icon; the Tanmatsu keeps its
coloured F-key shapes; the T-Deck / P4 key hints sit under the icons.
- Home's launcher column (Advert, Terminal, Files, Apps, Control) shows
icons only; the buttons keep their height and spacing, and the status
text, chart and info card widen into the freed space.
- The active-tab marker turns vertical beside the active cell, the
unread and update badges move to the top-right of their cells, and the
rail stays solid (and the marker visible) on the Map.
- Content that kept the bottom bar visible now stops at the rail
instead: tabContentW() subtracts the rail, and the map canvas, Settings
landing grid, chat-list rows, Mentions and the app drawer use it.
Swipe-left on the rail opens the drawer (swipe-up on the bottom bar).
Portrait layouts and the M9 are unchanged: the mode is decided once in
buildUiTree from the pref and the orientation at boot, the switch is
greyed out in portrait and absent on the M9.
TABBAR_H becomes a runtime value (0 with the rail) next to a new
TABBAR_W. The pref is a new trailing field, condense_nav (schema v66);
the schema test's layout asserts now count the v64 fields they missed.
Strings added to every deploy/apps/lang file and i18n_builtin.h
regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
- New crowpanel_35 variant: display, SD, board and target files, plus board JSON
- Portrait mode is drawn rotated 180°; landscape stays at rotation 3
- Share the GT911 touch driver between Wio Tracker L2 and CrowPanel
(WioTrackerL2Touch.cpp -> src/helpers/input/LgfxGt911Touch.cpp)
- platformio envs, flasher/site entries, release and merge-bin support
- Tests for the CrowPanel SD codec and merge-bin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132MkJ7RUV32mV9F7o4PJMm
M9 Back and Bluetooth-keyboard Esc stop at the locked app-drawer root, M9
focus fixes, optional M5Stack CardKB on the V4 and V4-R8, V4-R8 touch and
text-size accessibility, Wio L2 SD retry, high-contrast day and night themes
(#544), channel region discovery (#541) and the Czech translation
contributed in #540 by brebtatv.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A built-in USB Files app (T-Deck and Heltec V4 for now) lets the page at
files.wadamesh.com browse, upload, download, rename and delete files on the
SD card, internal storage and the map tiles over Web Serial. File level, not
USB mass storage: the device keeps its filesystems mounted, the radio keeps
running, and the firmware enforces what may change. Live data (identity,
contacts, settings, history; /meshcomod on the card) is download-only.
- UsbFilesProtocol.h: framed messages (E7 5A, CRC32), a resyncing parser,
path checks and the access policy; host tests in test/.
- UsbFilesSession: one request at a time (both Arduino USB serial drivers
drop bytes when their RX queue is full), RX queue sized per session, SD
data through a DMA-capable bounce buffer, FatFs listings with sizes, a
beacon so the page never talks first, SD Scan's malware verdict per file.
- The companion link and MyMesh's console step off USB while it runs.
- Website: labelled side buttons, a USB Files button and card; the
files.wadamesh.com page, vhost and deploy script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Some ThinkNode M9 cards shipped with a dormant Windows worm (Elecrow
security advisory, September 2026). An infected card seen since carries
autorun.inf in the root, launching xlfqf.pif on open, explore and autoplay
with random-junk comment lines in between: the Sality autorun pattern.
- SD Scan store app (deploy/apps/sdscan/1.0, requires "sd", not seeded):
walks the card a small page per tick, lists what it finds and why, and
removes it after a confirmation screen with Cancel first. It says on
every screen that it only removes files it recognises and that
formatting the card is the safe fix. On older firmware it still finds
threats by name but cannot remove them.
- Firmware: wada.sd.check(path) and wada.sd.remove(path), plus paging for
wada.sd.list(path, start, max) and caps().sd_clean. What counts as a
threat lives in SdThreat.h: autorun.inf, Windows program, script and
shortcut extensions, or a real MZ+PE header under any name. remove()
classifies again in firmware and refuses anything else, so no app can
use it to delete tiles, backups or chat history. It clears read-only,
hidden and system first, because FAT refuses to delete a read-only file.
- A warning when a card with Windows malware in its top folder is mounted,
at boot or on insert, offering SD Scan (or the Store).
- Tests: test/test_sd_threat.cpp, and SD Scan harness scenarios including
the real infected card's root. Removal checked on a T-Deck.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Web interface: unlock a manually locked screen (#506). The device publishes
its lock state over the mirror socket and the page shows an Unlock button
while it is set. The lock screen absorbs taps by design and only a held
trackball or BOOT press unlocks on the device, so a browser had no way back
from a screen it had locked itself.
- Console mode: hold the panel for three seconds to leave it (#507). The
banner and `help` both say so. Console mode also applies the "Older keyboard
protocol" setting now: that is applied in the graphical startup path, which
console mode returns before, so the console ran on protocol detection alone.
On a T-Deck that needs the older protocol every keystroke there is garbage,
which is why `ui` could not be typed and the reporter had to side-load a
second firmware to get the device back.
- Console mode: a touch wakes a dark panel again on the touch-only boards,
where the keyboard and button wake paths sit below the console branch's
return. The waking press is swallowed so it cannot also type.
- Paste into a key field lifts the key out of the surrounding text (#526): a
32 hex digit channel secret or a 64 hex digit public key, spaced keys
included. Before, the field filled with prose and the length cap cut the key
off.
- ThinkNode M9: a waiting firmware update is visible (#443). The red "!" over
the bottom-bar gear is built in the #else of that board's block, so the M9
had no update signal at all; it gets a third slot in its own notice row,
steady amber rather than blinking. The Settings tile in the app drawer
carries an "!" on every board.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Pager: the SD card mounts again after the card was used on a computer
(#522): one SD-rail power cycle and retry, and power-off unmounts and powers
the card down
- At a glance: emoji render instead of squares (#521)
- Transfer page: upload an offline OSM tile folder to /tiles on the SD card
(#515), with strict path checks and resumable, atomic writes
- T-Deck Pro: touch release debounce, hardware-timed chat-row holds, and a
typed space no longer starts the screen lock
- Heltec V4 with the original Expansion Kit: the IO button goes back (tap)
or home (hold), larger keys with a magnified preview, and larger status-bar
targets (#525)
Bluetooth serves either the phone app or a keyboard (Settings > Bluetooth).
Pairing lists keyboards in pairing mode and pairs with or without a code;
the paired keyboard reconnects by itself.
Keys are read the way phones and computers read them: the keyboard's Report
Map says which report carries the keys and how, so media keys and touchpads
are left alone, and the boot protocol is only the fallback. Key positions are
translated with the chosen layout (US, UK, German, French, Belgian), with
AltGr and dead keys.
While a keyboard is connected:
- text goes into the focused field, and touchscreen-only boards keep the
on-screen keyboard down (a second tap on a field brings it up anyway)
- the arrows, Tab, Enter and Page Up/Down drive the focus highlight
- Esc is the back button; a "Back key" setting picks another key for
keyboards whose Esc key types a character
- tab hotkeys and their hints over the tab bar are switched on
- Command tapped alone opens the emoji picker in a chat
- the status bar shows a keyboard instead of the Bluetooth glyph
The Bluetooth page is now a flex column with the choice on top, and the
settings page refit keeps it scrollable when its content grows.
Settings: prefs v61 (mode, layout, paired keyboard) and v62 (Back key).
Tests: test/test_hid_report_map.cpp, test_touch_prefs_schema.cpp.
Not on the P4 boards (Tanmatsu, T-Display P4).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pressing down past the last message page-scrolls the chat list with an
LVGL animation. In a long chat the list uses compressed scroll
coordinates, and chatVirtRemap1To1Scroll re-anchored the position on
every LV_EVENT_SCROLL with lv_obj_scroll_to_y(LV_ANIM_OFF). That deletes
the running animation from inside its own step. LVGL 8.4 reads the
animation again after the step, and when the step was also its last
(the UI loop was busy for longer than the animation) it finished the
freed animation and freed it a second time. By the next round that
memory belonged to something else, and anim_timer called a garbage
get_value_cb: the jump to 0x00020000 in the beta_79 and beta_80 M9 dumps,
both at the same call site.
- chatVirtRemap1To1Scroll follows a scroll that an animation drives and
leaves the re-anchor to chatVirtOnScrollEnd. The page scroll now also
completes on long chats; the early re-anchor stopped it after a frame.
- scripts/build/patch_lvgl_anim_uaf.py skips LVGL's completion check when
the animation list changed during the step, as LVGL 9 does. Applied to
every touch env as a pre-script and to the vendored P4 copy
(fetch-deps.sh, build.sh). Idempotent, fails closed on source drift.
- test/lvgl_anim_uaf/run.sh reproduces it on the host under
AddressSanitizer: stock LVGL with the old handler reports the
use-after-free in anim_timer, and either fix alone runs clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#464 (PD0RCM, T-Deck Plus): "I can hear the sample sound and the volume change,
but not incoming messages." That board has the hardware, and previews and
arrivals go through the same playback function on it, so the audio path was
never the problem.
Every preview on the Sound page played UNCONDITIONALLY: the sample, the volume
steps, and each per-type toggle. An arriving message checks the master Sound
switch and Do Not Disturb. So with the master off the page chimes at you the
entire time you configure it while nothing ever sounds on arrival, which reads
exactly like a broken notification path and is almost certainly what he hit.
A preview is now silent precisely when a real notification would be, and says
which of the two silenced it rather than just going quiet. The master switch
keeps its own confirmation chime, because that fires exactly when you turn it
ON, which is the one case where a sound is the right answer.
Telemetry position (v59, honza_87628, via Discord): the advert displacement of
#399 was extended to telemetry answers because a privacy setting a telemetry
request walks straight around is a hole rather than a feature. He then made the
fair counter-argument that an answer is not a broadcast: it is encrypted to one
contact who already holds the permission, and it only goes out because they
asked. Losing the ability to give trusted contacts a real fix is a real loss.
So make it a choice instead of an assumption. New tail field telem_loc_exact,
OFF by default, so the private behaviour is what you get unless you say
otherwise, and the broadcast advert stays displaced either way. The switch sits
directly under "Share my location when asked", where the decision belongs.
Schema 58 -> 59. The host test enumerates every appended field and caught the
change, as designed; all three of its migration assertions are updated and it
passes.
Reported-by: PD0RCM, honza_87628
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#415: a Lua app was covered by the app drawer whenever a message arrived.
Reported by jadestarwatcher, reproduced by mysterywavi, both on T-Deck. Mine,
from the #393 badge fix. Apps launched from the drawer deliberately leave it
alive underneath (appTileCb says so outright), and my badge refresh rebuilt it on
every unread-count change, with openAppDrawer() ending in a move_foreground that
threw it on top of the running app.
The refresh now runs only when nothing is drawn in front of the drawer, tested by
sibling order rather than by listing the tools, so a tool added later is covered
without anyone remembering to update a list. The status bar is excluded, since it
legitimately floats above the drawer at all times and treating it as covering
would have stopped the badges refreshing at all. Leaving the signature stale is
what makes it self-healing: the check runs again each tick, so the count is right
by the time the drawer is back in front, with no need to hook every close path.
Scroll position is now preserved across the rebuild too, which my change had also
been resetting.
#410: on the M9 the accent variants appeared but could not be selected. Also mine,
from the #387 work. The picker was handled below m9HandleArrowKey, which takes
LEFT and RIGHT for the caret and returns, so the arrows never reached it: the box
was drawn and nothing could touch it. Lifted above that call, as its own function
rather than a second copy of the logic.
#399, requested by @Danie10: an option to advertise a position near you rather
than your address. Settings, GPS, cycling exact / 100 m / 250 m / 1 km.
The displacement is derived from the node identity, so it is the same offset
every time. That is the whole point rather than an implementation detail: a fresh
random offset per advert would scatter points around the true position, and
averaging a night of them would recover the centre exactly. A fixed displacement
instead looks like a node that sits somewhere else, which is what a manually set
location already looks like. It applies only to our own adverts; the map and the
GPS page keep the real fix, because the aim is to tell other people less, not to
lie to yourself.
Schema v57. The host test caught the new field the moment it was added, which is
what it is for.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oumike. A firmware-wide appearance setting: Night stays the default and renders
exactly as before, Day is a low-glare light palette with its own semantic roles
for text, panels, fields, borders, controls, focus, charts, status colours and
chat surfaces. Selecting a mode restarts, so every LVGL object is rebuilt from
one coherent palette rather than half-repainted. There is a Night/Day selector
in Settings, Display, the Control Center Theme chip is now a direct toggle whose
sun/moon icon shows the active mode, and the standalone console UI has a
matching Day palette.
Main already carried the palette scaffolding for this, pinned to Night behind a
comment saying it was waiting on this branch, so the merge mostly replaces those
stubs with the real thing.
The prefs schema needed care. The branch appended theme_mode as v54, but v54 and
v55 were taken by boot_wifi_time/boot_wifi_open and loud_alerts before this
merged, so the field moves to the tail behind them and the version becomes v56,
with the trailing-field assert and the migration step moved to match. A packed
struct read back at the wrong offsets is the failure this schema's asserts exist
to prevent, so the invariants from both sides are kept rather than one replacing
the other.
That also surfaced a stale test: the v53 case asserted that v53 plus exactly two
bytes was the whole struct, which stopped being true when loud_alerts landed and
was not caught because the host test was not run then. It now counts every byte
appended since and asserts the new fields come back at their defaults. The
schema host tests pass. While there, the size comment said "about 500 bytes";
the struct is 114.
Built on all nine S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MeshCore names run to 31 characters (ContactInfo::name[32]), but
UIMessage::sender held 24, so every longer name was cut on the way in.
d304d3f made the cut visible instead of letting it push the author into
the message body; this makes it stop happening. MAX_SENDER_NAME goes to
31 — exactly the wire width, so nothing is lost.
Truncation could never fix three of these. "Ack" and "Mention" insert
@[<sender>] into the composer, and textMentionsMe needs the whole name
followed by ']', so a mention of a long-named node never reached them.
Block-by-name compares the stored name against the sender on the RX
path; for ROOM posts those two came from different widths already — the
check sees the untruncated 31-char author while the stored entry came
from the 24-char field — so a room author with a long name has simply
been unblockable. Info and the chat-list preview showed a cut name.
The obvious change — widening UiHistoryMsg::sender — would have been
quietly destructive, so the on-disk width is frozen at 25 instead and
the overflow is APPENDED to UiSegMsg, after seq. sender sits between
thread and text, so widening it in place shifts text for every record
already on disk, and nothing would have caught that: the segment store
is what actually holds messages now, and uiSegOpenValidated checks only
magic/version/rec_size — it never consults k_ui_history_version or
k_ui_history_min_version, which guard the legacy files alone. An old
segment would have passed validation, prefix-read into the new offsets,
lost the first 8 characters of every message body and read seq as 0 (so
loadMsgsFromSegments renumbers the lot), and the next compaction would
have written that back permanently.
Appending at the tail is the evolution path UiSegHeader already
documents, and it is correct in both directions: an old 233-byte record
zero-fills the new field and yields its original short name, and older
firmware reading a new 240-byte record copies the first 233 bytes and
ignores the tail. That second half is why k_ui_seg_version STAYS 1 —
bumping it would make older firmware reject the file outright and
quarantine a history it could have read. So there is no migration, no
version bump, and no wiped chat history. static_asserts now pin the
offsets an already-written record depends on.
TOUCH_IGNORED_NAME_LEN has to move in lockstep (28 -> 32) or blocking
breaks: a slot narrower than a sender stores a cut name the full-width
check can never match, and the block sits in Settings doing nothing.
Its NVS blob has no header — the entry count is its length divided by
the slot width — so re-slotting in place would mangle every entry after
the first and the next write would make that stick. The key is renamed
ign_nm -> ign_nm2 and the old blob folded in once at first read, which
is unambiguous in a way that sniffing the blob length is not (28 and 32
share multiples at 224 and 448, both inside the 16-entry cap). The
invariant the header only stated in prose is a static_assert now.
Split/rejoin is a host-tested header next to ChannelSenderSplit, with
the field-width bounds spelled out: a corrupt record can leave the name
field unterminated, and the message body is what follows it on disk.
Signed-off-by: Ruben Laban <ruben@tun0.nl>
The split accepted any "X: " prefix once the length test came out, so a post
that carries no author but does contain ": " — "the repeater at Ouderkerk is
back up: full quieting again" — got read as an author of 36 characters. That
invents a sender AND hides the start of the body.
The old `slen <= MAX_SENDER_NAME` test used to catch this as a side effect;
dropping it for long names dropped the plausibility check with it. Restore the
check at the WIRE width (31 — MeshCore keeps every name in a char[32]) rather
than the destination width, so a real 31-char name still splits and truncates
while a 32-plus "prefix" is left in the body where it belongs.
A static_assert pins kMaxWireName >= MAX_SENDER_NAME, so widening the field can
never leave the split rejecting names the field holds fine.
A channel post arrives on the wire as "SenderName: body" — the author is
in the text, not a separate field. Both split sites gave up when the name
was longer than MAX_SENDER_NAME (24), and giving up meant the sender fell
back to from_name, which for a channel is the CHANNEL name. The bubble
then read
#channel date
nick: message
instead of the author over their own message. MeshCore names run to 31
chars (ContactInfo::name[32]), so this hit every post from a node with a
25+ char name — the "occasionally" in the report.
Truncate the label instead of rejecting the split, backing off to a UTF-8
character boundary so a multi-byte name is never cut mid-sequence (the
missing-glyph sanitiser would render the tail as '*'), and mark the cut
with "..." so a shortened name does not read as a different node.
Widening MAX_SENDER_NAME is the real cure but not an option here: sender[]
sits in the MIDDLE of the persisted UiHistoryMsg record, and this file
format only tolerates fields APPENDED to the end — moving `text` would
force k_ui_history_min_version up and discard everyone's chat history.
The two copies of the split are now one host-tested header, matching how
Utf8Text/ReaderContent are factored.
Signed-off-by: Ruben Laban <ruben@tun0.nl>
oumike. Closes#315 (pisti87's request).
Two conflicts, both resolved by keeping BOTH sides rather than choosing:
- sdRuntimeLifecycleBusy() gained an audio-playback source here and a web
reader source in #317. They are independent consumers of the same card and
both have to gate the mount lifecycle. The reader's self-exclusion is kept:
it calls this from its own task while holding the card and would otherwise
deadlock against itself.
- The Lua harness caps table needed sd_list from #312 as well as the audio
flags, and the test order needed the wardrive suite from #324 as well as
audio_api.
Built on all eight S3 envs and both ESP32-P4 targets.
Beta 57 inserted retry_echo into the middle of the packed TouchCfg blob, shifting the remote-mode and later fields during migration. Define a v45 layout with retry_echo appended at the true tail and recover ambiguous v44 suffix bytes to safe defaults while preserving the stable prefix. Add a host regression test for v43, broken v44, and v45 blobs.
Signed-off-by: Pixel Perfect <me@pixp.cc>