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>
scripts/build/i18n-audit.py reported the same 18 keys missing in all
14 language files -- the background picker (Background, Background
colour, Still/Moving aurora, Image, None, Choose an image, Choose the
colour, Set as background, the JPEG hints and the restart notice), the
GPS battery saver (Save battery, Check position every, the two
descriptions, "GPS: resting, next check in %lum%02lus") and the
"remote view open" touch-sleep block reason.
Each file gets the 18 rows in its alphabetical slot, translated rather
than left as English placeholders, reusing the terms the files already
use for the older wallpaper strings (Hintergrund, fond d'écran, sfondo,
achtergrond, háttérkép, pozadí, papel de parede/fundo, fundal, фон,
позадина, φόντο ...) and for the battery saver. "%lum%02lus" is kept
verbatim in every language. src/ui-touch/i18n_builtin.h regenerated
with scripts/build/gen-lang-builtin.py; the audit now passes for all
14 languages. "# ver: 23" left alone, since that is bumped at release.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
Frame rate: full-width PSRAM draw band (6 bands a frame instead of 14 on the
T-Deck), the still aurora drawn once and copied, LVGL at -O2 with its blend and
label loops in IRAM (scripts/build/perf_lvgl.py, 1 KB more loop stack), circle
and gradient caches, newlib memcpy. Rolled out to the M9, Attaky, RAK, Wio,
CrowPanel and V4-R8; the Pager gets the band only, the V4 the caches only.
Frame benchmark tour (DOC_PERF_BENCH) for measuring it.
Fixes: hop counts read from the packed path length (#621); Arabic shaping
rows patched in LVGL (patch_lvgl_arabic.py), Arabic 101 shift layer, kaf,
symbols, text sent as standard letters (#582); info popup colour runs that
never wrap (#602); keypad nav reaches the chat header (#623); the browser
opens the changelog (#622, gen-changelog.py at every site deploy); the
Theme background colour picker no longer edits the accent; flasher entries
for P4 units with factory ESP-AT Wi-Fi chips (#612, #619).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The image viewer's Set as background called into the Theme page, which the
e-paper builds do not compile. CAP_BG_IMAGE and CAP_BG_MOVING are 0 there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home is a grid of arrangeable widgets (24 kinds, presets, a layout per
orientation, Lua app widgets via wada.widget); Unread, Chat and Mentions
get a large size. Theme > Background puts an aurora (still by default, or
moving), a colour, a picture or nothing behind every screen, with glass
cards and a smoked-glass top bar. A GPS icon in the bar, a GPS battery
saver (u-blox GNSS stop/start where there is no power switch), and a
battery saver that no longer waits for Bluetooth and Wi-Fi to be off.
Also: navigation bar position, every settings page on the redesign's
cards, a faster app drawer, slower screensaver drift for burn-in, text
wrapping at spaces only, the V4-R8 Store Apps tab, and SDK Test marked
extended-SDK only (keeps the 2 MB V4 at 99.8%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Testers said the colour was what set wadamesh apart, and the beta_89 redesign
took most of it out. This puts it back as choices on one new page.
Settings > Theme (CAT_THEME, Device group): a live preview drawn from a copy of
the palette (themeTokensFor), Night/Day cards, High contrast, the accent swatches
inline with Custom, MORE COLORS! with Regular/Neon/Pastel, Taste the rainbow, and
the chat looks. Mode, accent and look are pending until one Apply and restart on
the preview box; scrolled past it, a slim bar under the title carries the button
with six dots in the picked look. Chat icons, bubbles and compact apply live. The
look rows left Display; the control panel's theme chip long-press opens Theme.
The colour engine: ThemeTokens + lookAdjust over the mode and accent at boot
(applyBootTheme), section hues for the launcher, Settings, tab bar and status
icons, per-name colours (FNV hue) for names, glyphs, initials balls and bubble
tints, Neon lit edges and gradient primaries, Pastel tints, and a deeper Day ink
for names and glyphs on light lists. MORE COLORS! defaults on (Regular); HC and
e-paper keep their own colours. New prefs are standalone u8 keys (chat_icons,
col_more, col_style, col_rb), no schema bump.
Taste the rainbow: per-item hues round the wheel, a 10 Hz hue drift on the tab
marker, chats count and focus ring (rests when off, locked or screensaver), press
dip, an arrival flash on new bubbles (draw-only props, the virtual list never
sees it), rainbow lock mark, boot mark and Constellation nodes (CstTheme
node_sat/node_val), and a dot burst on enabling it.
Chat icons: Glyphs or Initials through one avatarApplyLook; AVATAR_SQUARE_FLAG
moved off LV_OBJ_FLAG_USER_1, which is the nav's NAV_SKIP_FLAG.
Size: wadamesh_mark_rgb.h was a static array included by main.cpp and
UITask.cpp, so every S3 image carried the 30 KB boot mark twice. One definition
now (UITask.cpp; the P4 never compiles main.cpp). The V4 goes from overflowing to
~30 KB free with all of the above.
Capture tour: wakes and ends the screensaver before shooting, DOC_LOOK forces a
look, DOC_LOOK_AUDIT walks the looks and the Theme page, DOC_MAP_SHOT waits for
Wi-Fi and for every tile before shooting the map.
25 new strings as English placeholders in every .lang file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Three fixes.
#589/#588, and this one is a disclosure rather than a glitch. TippyLion28 had a
channel go invalid (#588), then sent to it from the web UI; the messages went
out on the PUBLIC channel with nothing on screen to say so. The web path did
resolve the channel by name at TX, with a comment citing the stale-slot trap --
and then threw that away:
if (slot < 0) slot = threadMeshChannelSlot(tidx); // fall back to cached
A name that does not resolve means the channel is gone. The cached slot then
points at some OTHER still-valid channel, in practice slot 0, public. It now
refuses and says so, which is what the touch composer already did ("Channel not
found"); the web UI had no business being the laxer path.
termDoSend() also only checked that the slot resolved to A channel, never that
it was the RIGHT one, so a stale slot sailed through it. Every caller already
passes the channel the user chose, so it compares now -- enforced at the one
point every sender passes through, not only in the callers.
#583, T-LoRa Pager losing ~30 min/day while powered. jcyrio read the source and
called it exactly: HardwareRtcClock copies the chip into the ESP32 clock once at
boot and getCurrentTime() never touches I2C again, so the software clock
free-runs at the RC oscillator's ~2% while an accurate crystal sits unread on
the bus. Keeping I2C out of the hot path is right -- it runs on every message,
UI tick and advert -- but once an hour is not that path. tick() now re-reads the
chip hourly and pulls the software clock back. Only when a chip was adopted at
boot (a read refused at boot is not a trustworthy source and must not drag a
good clock backwards), only past a 2 s threshold, and it corrects the software
clock alone: calling setCurrentTime() would write the chip's own value back to
itself and a read-back failure would then mark a healthy retention path broken.
P4 tile cache on a board with no SD card. The fallback set s_tile_fs = &FFat,
but main.cpp mounts that partition with LittleFS -- 'storage' was converted from
FAT and this line was not updated with it. The tile cache was handed an FS
object nothing had mounted, so every tile failed with "File system is not
mounted" as the only clue. Uses the filesystem that is actually mounted.
Compile-checked on T-Deck, T-LoRa Pager and ThinkNode M9.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beta_86 flipped the T-Display P4's default C6 backend to esp-hosted. Current
V1 units ship the C6 with esp-hosted firmware and are fine; OLDER units still
carry the factory ESP-AT firmware, and on those the SDIO stream never frames.
That turned into a board that will not boot at all, reported by three people
so far (#600 jalevdev, myshoeisonfire on Discord, and Kaj's own unit), each
with the same signature:
H_SDIO_DRV: len[25970]>max[1524] OR offset[25697] != exp[12], Drop
Two separate things made a co-processor mismatch fatal, and both had to go.
1. The UI was behind the C6. hostedInitBLE() ran unconditionally at setup, a
hosted handshake, with ui_task.begin() ~80 lines later. The LEGACY backend
always started its worker AFTER the UI, which is exactly why a dead C6 used
to cost Wi-Fi and nothing else; the hosted path was wired in ahead of it.
Both backends now start at the end of setup, after the UI, with a note
saying nothing C6-dependent may move above it.
2. esp-hosted resets the host. CONFIG_ESP_HOSTED_HOST_RESTART_NO_COMMUNICATION_
WITH_SLAVE=n covers one path; the one that actually fires is
H_TRANSPORT_RESTART_ON_FAILURE in sdio_drv.c, about 10 s after the connect
attempt, and it is on by default. Now off. The failure still arrives as
ESP_HOSTED_EVENT_TRANSPORT_FAILURE, which is what an application should act
on instead of the transport rebooting the board underneath it.
Measured on an ESP-AT unit, 45 s captures, same hardware:
as shipped 5 resets, host restarts, UI never appears
reorder only 2 resets, host restarts, UI appears then dies
reorder + config 0 resets, 0 restarts, UI stays up
The C6 is still unreachable on that unit, as it must be - it is the wrong
firmware for it. It now costs Wi-Fi and BLE and nothing else.
Also here, both needed before this can ship:
* The ESP-AT build gets its own artifact name (tdisplay-p4-at, and the matching
-lcd/-lr2021 combinations). Both backends used to answer "tdisplay-p4", so an
ESP-AT unit's self-update would have fetched the hosted bin and put itself
straight back into the loop, recoverable only over USB. That is the T-Deck
Max bug again (a Max self-updating to Pro firmware) and would have been the
fourth time this table shipped wrong.
* The keyboard expansion probe prints a one-shot bus scan. "It does not work"
has three causes that are indistinguishable from outside: not clipped on,
clipped on but unpowered, or present and failing bring-up. An unpowered board
does not NACK cleanly either - its pull-ups are dead, the lines sit low
through its ESD diodes, and every transaction returns ESP_ERR_INVALID_STATE,
which reads like a driver fault. The scan says which it is in one line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The logo is painted before SPIFFS/SD mount, so it read rotation from the
legacy NVS settings copy, not the real file-backed settings. Any device
whose two copies disagreed (the Attaky after its v68 landscape reset, or
anyone who changed orientation since settings moved to files) showed the
logo in the old orientation until the UI took over.
Keep a raw-NVS boot copy of the rotation (touch/boot_rot): written by
touchPrefsSetUiRotation() and re-synced after touchPrefsReload() every
boot, so schema resets and restored backups catch up too. The logo reads
it, falling back to the old read when absent (first boot after update,
or Launcher installs without NVS). On the Attaky any landscape value is
treated as rotation 1, as UI init does.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Backup runs the same export as Settings > Backups > Export into a hidden
temp file and streams it to the browser as meshcore-<date>.json.
Restore uploads a backup .json through the existing chunked/CRC path,
then applies it via the shared backupImportAndReboot() helper (same
sections/replace flags as Import backup) and reboots. Temp files holding
key material are removed after use and on page open.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Portrait came up as a shrunken landscape screen. The boot logo takes its
rotation from the settings copy in legacy NVS, read before main.cpp moves
settings to SPIFFS. On the Attaky that copy says landscape (the new
default, and the v68 reset), so the logo turned the panel to rotation 1.
The real setting, loaded from SPIFFS afterwards, said portrait: LVGL was
built at 240x320, but nothing turned the panel back, so the UI was drawn
upright for landscape, 240 px wide, with its bottom cut off.
UI init now turns the panel to the build portrait rotation
(DISPLAY_ROTATION, 2) when the UI is portrait, as the CrowPanel already
does for the same reason. Portrait there needs no touch transform, as on
the V4: the Attaky's landscape touch map is the V4's, so its touch frame
is too. The D-pad remap from the orientation commit follows from the
same map.
The boot logo still follows the NVS copy, so in portrait it shows in
landscape for a moment before the UI takes over.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
File Transfer needed a removable card, and the Mesh Deck has no card
slot. Give it an internal backend (WADA_WEB_FILE_TRANSFER_INTERNAL):
- Files go to /transfer on the internal LittleFS "tiles" partition, never
SPIFFS, which holds the settings, contacts and chats.
- The page shows "Internal storage used / total". An upload that would
leave less than the map tile cache's 320 KB floor is refused up front
("not enough free internal storage") instead of failing part-way.
- Map tile folders are refused ("map tiles need an SD card"): the map
only reads uploaded tile libraries from a card.
- If the partition did not mount, opening the app says so instead of
asking for an SD card.
- The browser page now says files go to "the device's storage" rather
than "the device's SD card", which is right on every board.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
The Mesh Deck UI was pinned to landscape (LV_DISP_ROT_90), and the
build without a keyboard is held upright, so it needs portrait too.
- CAP_ROTATABLE on the Attaky: Settings > Display > Orientation toggles
landscape (ROT_90, panel rotation 1) and portrait (the panel's
DISPLAY_ROTATION=2, no touch transform, as on the V4).
- Landscape stays the default: DEFAULT_UI_ROTATION is 1 on this board,
and schema v68 (no new field) resets the stored value once, since the
UI ignored it until now. The pre-LVGL boot logo already follows the
setting, so it is upright in both.
- The front D-pad is named for landscape, so in portrait a press turns
with the screen (right -> up, down -> right, left -> down, up -> left).
- The terminal (Console Mode) keeps the panel's build orientation,
whatever the UI orientation is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
The wheel is the Pager's only way to move the highlight, which is slow
and imprecise. Bring the T-Deck's keyboard navigation to it:
- With no text field focused, programmable letters move the highlight
(default I up, K down, J left, L right) and scroll (U / O). Select
and Back default to no letter, since Enter and Backspace-hold (the
wheel's click / long-press) already do them. On the Map the letters
move the highlight; W/A/X/D stay the map pan.
- The tab keys are programmable like the T-Deck's, defaulting to the
Pager's M/C/H/A/S, and the bar's labels show whatever is bound.
- Settings > Keyboard gets the same "Keyboard navigation" switch (on by
default here) and the same tap-a-row-then-press-a-key remap rows; the
T-Deck's rows move into settingsNavKeyRows() so both boards share them.
The Pager's switch only turns the letters on or off; it never empties
the focus group, which the wheel always needs.
- A remap refuses letters the Pager uses for fixed actions (Q/E slider
nudge, W/A/X/D map pan; A stays allowed as a tab key).
- In wheel edit mode on a slider, J/L preview like a turn and the
select key closes the edit, matching the wheel.
Settings: the Pager's kbd_nav and key bindings were never shown on that
board, so they held the T-Deck defaults. Schema v67 (no new field) puts
the Pager defaults in once on that board only. Two new strings,
translated in every deploy/apps/lang file; i18n_builtin.h regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
On the M9, V4-R8 and Pager the text presets grow the status-bar offset
ladder with the font (SBX), and the builder places the Bluetooth glyph
with it. The clock placement then re-aligned the clock with plain SC(),
which leaves it at its 100% spot, so at Large / Huge the Bluetooth glyph
moved left onto the time. The charging slide also used raw 100% offsets,
so plugging in or unplugging snapped the cluster back under the grown
glyphs.
- The wide-bar clock uses SBX() like the icons it sits beside.
- The charging slide uses exactly the builder's offsets (SBX plus the
battery-% overflow); the narrow V4 portrait bar keeps its raw values.
- Safety net for every board: after placing the clock, if it overlaps a
visible icon on its row (Bluetooth, DND, sleep, Wi-Fi, signal,
battery), move it just left of the leftmost one. Covers the 12-hour
clock, the Bluetooth-keyboard glyph and the centred hide-name clock.
Re-checked only when something that moves or resizes them changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
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
A plain wheel turn moves focus, and a click sends Enter, which a slider
ignores, so no slider on the Pager (Screen timeout, Brightness, the
Control Center sliders, Map zoom) could be adjusted with the wheel; only
the hidden Q/E keys moved them.
Clicking the wheel (or Enter) on a focused slider now enters an edit
mode, shown with a bigger contrasting knob. Turns move the value with a
live preview (VALUE_CHANGED) and save nothing. Clicking again saves
(RELEASED, the same commit a touch drag ends with) and returns the wheel
to focus navigation. Back (long-press or Backspace-hold) restores the
original value without saving, as does the screen timing out or focus
leaving the slider. Q/E preview instead of saving while in edit mode.
The slider step moves out of navMoveDir() into navSliderNudge() so the
wheel and Q/E share it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
The idle power-saver (touchSleep) already ran on every touch build: the
hooks are installed in UITask::begin and main.cpp calls loopEnd() every
tick, and its gates are board-neutral. Only the Settings switch, its
callback and the amber battery tint were behind a T-Deck / V4-R8 / M9
allowlist, so on every other board (T-Pager, CrowPanel, T-Display P4,
T-Deck Pro, ...) the pref defaulted OFF with no way to turn it on --
the same bug the M9 had until 2026-09-02.
Drop the allowlist from the switch row, sleepIdleToggleCb, applyBattColor
and the status-bar re-tint. Also rejoin the lockOnScreenOffToggleCb
comment the old #if had split in two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
The M9 is the one board with no bottom bar (TABBAR_H == 0, tab_btns hidden
outright), so it never got the numeric badges the others have. beta_75 put
three glyph labels on layer_top across the bottom instead, OVER page content,
and blinked two of them at 1 Hz. With nothing else down there to read them
against, that came back reported as the navigation icons flashing.
They now carry a count and are shown only while they mean something:
* The person glyph was driven by discoveredCount(), a standing total that is
non-zero on any live mesh from the first advert heard and only ever falls
when you add or purge. It therefore blinked forever. It now counts what has
not been looked at: watermarked at boot, because a ring restored from flash
is not news, and again whenever the Discovered list is opened, with a clamp
so purge, adding to Contacts and ring eviction cannot leave the next real
arrival reading as nothing new.
* It also blinked while you were standing on Contacts with that list open.
Suppressed there, the way the mail glyph already was on the inbox tab.
* Two style writes and a z-reorder ran on every 250 ms tick regardless of
whether anything was pending, invalidating the strip for nothing.
m9NoticeSet() only touches LVGL on a real change, and takes a refront flag
so a live notice still lifts over the app drawer, which is the one overlay
notices are allowed to sit on top of.
Also: "100%" was clipped by the signal bars. The percentage is right-anchored
at SBX(22) and grows left, the bars are pinned at SBX(54), so the text gets
SBX(32) and no more; "100%" is a whole glyph wider than "99%", and the bars
are created after the label so they paint over the leading 1. Reported by
buko84 and confirmed by jd1227, both on the M9, which is a board whose text
presets grow the font and leave the geometry at 100%, so it runs out of room
first. statusPctOverflow() measures the real width and slides the bars, the
Wi-Fi glyph and the SD dot left by the overflow, capped, and gated to those
font-only-preset boards. Zero where the text already fits, so no board moves
that did not have to, and the clock, the DND moon and the BLE glyph never
move. The round panel already solved this with statusClusterStart(); this is
the same idea for the single-row ladder.
Built for the M9, the V4-R8, the Pager and the T-Deck. The T-Deck binary is
byte-identical, which is the check that the ladder change reaches nothing
outside its gate. Not run on hardware: needs an M9 tester.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Switching on a secondary layout changes nothing until you cycle to it with
double-tap SPACE, and the sentence under "Secondary keyboards" is where that
is explained. It was gated on HAS_TDECK_KEYBOARD || HAS_M9_KEYBOARD, so the
T-LoRa Pager, T-Deck Pro and T-Deck Max (HAS_PAGER_KEYBOARD) fell through to
the on-screen wording and told the user to tap an "EN" key their physical
keyboard does not show, while their keys go through the same handleHwKey()
double-space path as the T-Deck. The setting read as broken.
Gate the hint on CAP_KEYBOARD instead, which is exactly the set of boards
whose keys reach that handler (the Attaky module keyboard has its own loop
and really does type next to the on-screen EN key, so it keeps the touch
wording). And make the "on" toast say how to switch, since "Russian: on"
alone was the moment the user was left guessing; "off" keeps the short form.
The cycle itself is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: jcyrio <50239349+jcyrio@users.noreply.github.com>
beta_86 put the emoji picker on the long-press menu, which covers the firmware's
own field and so covers wada.ui.input(). It does not cover an app that builds its
own buffer from key events, because there is no textarea to long-press -- and
that is exactly the case Jade asked about: their composer shows a pad-bytes
counter the firmware's field cannot, so it takes the custom path and the menu
never applies to it.
wada.ui.emoji(cb) opens the picker and hands the glyph back as UTF-8, nil if
dismissed. Same discipline as wada.ui.input's callback: one ref, one call,
released either way, refused if one is already open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported by Jade as "screen memory errors, worse the longer the app has been
open", with an app that rebuilds its screens on a 120 ms debounce.
Button and list-row callbacks live in one registry table keyed by the LVGL
object POINTER. clear() calls lv_obj_clean(), which frees every one of those
objects, and bumps s_ui_gen so stale handles go inert, but it never touched the
table. So each rebuild added an entry and kept the Lua closure, with its
upvalues, alive forever. Unbounded, and worst for exactly the apps that are
built properly: a list of twenty contacts redrawn on a timer leaks twenty
closures a go.
It was also a correctness bug in waiting. LVGL reuses freed addresses, so a new
button created WITHOUT a callback could land on an address a dead one left
behind and fire that stale closure on the next tap.
clear() now replaces the table. Every entry in it describes an object
lv_obj_clean just destroyed, so there is nothing there worth keeping.
Found by reading the host after Jade asked what the memory budget was; the
budget was never the problem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jade asked for three things for Lua apps. Two already existed and the answer is
documentation, not code: wada.sys.epoch() and wada.sys.datetime() have returned
the clock since #245, and wada.ui.input() is the firmware's own field, so it
already carries the long-press Cut/Copy/Paste/All/Sym menu, which is exactly the
"copy/paste from chats into a Lua text field" that was asked for.
The third is here. The long-press menu gains Emoji, so the picker reaches every
field in the firmware rather than only the chat composer: a Lua app's text entry,
a node name, a Wi-Fi password. The row is now sized to the panel instead of a
fixed 48 px per cell. It was already 268 px at five cells, wider than a 240 px
screen, so a sixth would have hung off the M9 and the V4 entirely.
Status bar: the right-hand ladder (battery, signal bars, Wi-Fi, Bluetooth, sleep,
clock) is a chain of fixed offsets tuned for 12 px text. The boards with
font-only UI-size presets grow the font and deliberately leave geometry at 100%,
so the labels widened and the ladder did not, and the battery percentage ran into
the signal bars (jd1227, M9, beta_85 -- the release that gave that board the
presets). SBX() scales those offsets by how much the text actually grew and
returns exactly SC() at Normal, so nobody who kept the default size sees any
change. Verified on the M9, V4-R8, Pager, T-Deck and V4.
SDK docs: corrected the app path. They said to copy the file to /apps/ on the SD
card; a card-backed device roots everything at /meshcomod, so it is
/meshcomod/apps/ there and a bare /apps/ only on boards that store internally.
Following the old wording cost gottlieb8849 an evening.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The confirm says "The chat itself stays", and beta_38's notes say the
contact stays, but on a DM the row disappeared: the inbox lists a DM only
while threadHasMessageHistory() is true, and refreshThreadsFromMesh() (every
4 s) reclaims any DM slot with no history, so the emptied thread was hidden
and then freed. Channels were unaffected because the inbox lists them
regardless.
The thread record had nothing to tell an emptied chat from a contact never
chatted with, so this adds keep_when_empty: set by clearThreadHistory() on
a DM, honoured by getCombinedInboxCount(), getUnreadTotal() and the reclaim
pass, cleared when the slot is reused, removed with the thread by "Delete
chat". It is appended to UiHistoryThread; thread_rec_size already lets older
records read it back as 0 and older builds skip it, so the history version
is not bumped (a bump would make a downgrade quarantine both history files).
Only threads the user explicitly emptied are pinned, so contacts without a
conversation still stay out of the inbox and clearThreadHistory() still
removes every message.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: jcyrio <50239349+jcyrio@users.noreply.github.com>
The five apps from #585 and #586 all defaulted to being seeded, which bakes
their source into the boards that cannot reach the Store. That is 40 KB of Lua,
and it took the Heltec V4 from 95.4% of flash to 96.7%: the tightest board in
the matrix paying for apps two of which it cannot use.
gen-lua-builtin.py states the rule: an app that is large, or that needs hardware
those boards do not have, earns its place rather than getting it by being in the
catalog. ProTreck (17 KB, altimeter/chrono/sensors) and Trip Odometer (GPS-only)
are both that, so they are seed:false, exactly like gpscompass and sdscan. They
remain in the Store for every board that can download.
Tetris, Breakout and Ping stay baked: no hardware needs, small, and the boards
that get seeded apps are precisely the ones that cannot fetch them otherwise.
V4 lands at 96.2%.
lua_builtin.h is regenerated here because it is tracked and the build rewrites
it. Leaving it out is how Wardrive and Nearby ended up missing from the seeded
boards after beta_68.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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
The P4's larger UI scale grew the button over the swatch, which sat at a fixed
x. On the P4 the button is narrower and the swatch sits a gap past its end, in
every orientation. Other boards are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On touch boards without a built-in keyboard, opening a chat focused the
composer and that focus raised the on-screen keys, covering the conversation.
The focus now only binds the composer (cursor shown, a Bluetooth, clip-on or
CardKB keyboard types straight in); tapping the composer brings the keys up.
T-Deck, T-Deck Pro and M9 are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the keyboard expansion arrives while the UI is portrait, a dialog counts
down 15 s and then saves landscape and restarts (the Settings orientation
switch's path); "Reboot Now" does it at once. Cancel, or taking the keyboard
off again, keeps portrait. A keyboard already attached at boot is not an
arrival, so a Cancel is not asked again on every start, and nothing is offered
under the lock screen. showConfirm() gains confirmSetMessage() to update the
open dialog's text in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GRWzzRs7oRvFRNcHUZF4c
Lock screen (all boards, #579): the default view is now drawn from parts
instead of the old 320x240 bitmap with the padlock and wordmark baked in,
which cover-scaled into a jumble on any other panel shape. WADAMESH sits at
the top, the clock below it (clear of the status bar), a smaller padlock in
the centre with the unread count to its left and a new battery readout to its
right, each centred between the padlock and the screen edge. Short panels get
a smaller wordmark and padlock. The clock's burn-in drift now moves around its
new place instead of snapping it back to y=30 every minute. T-Deck Pro keeps
its black-on-white e-paper colours; custom wallpapers are unchanged.
T-Display P4: the BOOT button (GPIO35) now works like the Wio Tracker L2's
wake button -- a press wakes or lights the lock screen, a 2 s hold locks or
unlocks -- so the P4 gets the lock screen and the "Lock when screen off"
setting. It gets no Power-off row: GPIO35 cannot wake the P4 from deep sleep.
Also: the chat list's per-row gear clears the list scrollbar on large panels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The T-Display P4 keyboard expansion (TCA8418 + its own XL9555, ported from
camillia-mt) now works. It is looked for every 1.5 s, so it can be clipped on
or off while running, and is routed like the CardKB and Bluetooth keyboards:
it types into the focused field and keeps the on-screen keyboard down. The
external-keyboard routing was gated on Bluetooth, which the P4 lacks, so it
now follows a new CAP_EXT_KEYBOARD; the Bluetooth-only parts stay on
CAP_BLE_KEYBOARD.
- Keys: Shift/Sym/Alt one-shot, Caps Lock lights the expansion LEDs, arrows
and Backspace repeat, Alt+Backspace backs out, F1-F5 open the tabs left to
right, and F11 steps the keyboard light through off/low/medium/high (saved
across reboots).
- Wire1 is shared with the ES8311 codec on other pins; P4I2c1 switches it
under a lock, and the codec's lazy bring-up now goes through it.
- With the keyboard attached the tab bar labels F1-F5 beside each icon, and
Enter on a chat bubble opens its action menu.
Also, on any keyboard build, Enter runs the command in the terminal and the
repeater admin console (the admin console's Enter hook sat on its Send button,
where nothing raised it). And on the P4: the status-bar name shrinks to fit the
bar, the Discover buttons are spaced out, contact names clear their icon in
landscape, and leaving an app returns the focus to its tile in the app drawer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keyboard/trackball cursor is a 3 px accent outline, and on an accent
fill it disappears. setSelectionGlow already swaps to the contrast
outline (white, black on Day) there, but only for a fully opaque fill;
the Map's side buttons are accent at 70% over the tiles, so they kept
the accent outline and the selected one barely stood out. Any fill that
is at least half accent now takes the contrast outline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The accent popup keeps a pointer to the text box it edits, and about 25
separate paths close the popup by hand. A path that deleted the text
box without closing it (e.g. a form closing after its send) left the
popup on screen pointing at freed memory; tapping an accent letter then
ran lv_textarea_del_char on it. That is the beta_85 T-Deck crash in
#574 (loopTask LoadProhibited at 0x3, accentBoxCellCb ->
lv_textarea_del_char), matched against the release image.
Tie the popup to its text box with an LV_EVENT_DELETE hook that clears
the pointer and hides the popup (hide only: in a layer clean the popup
may be deleted in the same pass). The cell handler also checks the text
box is still valid, and accentBoxHide no longer deletes a popup that
something else already removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The right-aligned peak readout shares the RBW/span line, and on the
narrow portrait panel it ran over that text. In portrait the RBW/span
line, chart and waterfall start one text line lower; the peak stays put
and the tall panel has the room. Landscape, the T-Deck Pro (peak on its
own line already) and the other boards are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page offset its content by a second STATUSBAR_H to clear the tall
status bar's lower row, which overlaps the page on square panels. The
round panel's bar is already its full two rows and never doubles, so on
the P4 that only left an empty band above the Home button, in both
orientations. Start 8 px below the bar there instead.
In portrait, Flood and Zero-hop stack as two full-width buttons instead
of sharing a cramped row. Landscape and the other boards are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sheet's two-column grid of half-width buttons cut most labels off
with "..." on the narrow portrait panel. In portrait every action gets
its own full-width row; the card grows to fit and its body scrolls past
the screen height like any tall sheet. Delete stays the red bottom row.
Landscape and the other boards keep the grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The P4 is a large-screen board, so its quick-reply/emoji chips were
56 px against a 34x30 send button, all pinned to the composer's bottom
edge, where the landscape row sat under the panel's bottom corner arcs.
All three buttons are now 45 px (the average) in both orientations and
vertically centred on the text box, re-centred as it grows. That lifts
them ~9 px off the screen edge, with a floor matching the status bar's
top-corner clearance. In landscape the quick-reply button also moves
16 px in from the bottom-left corner. Replaces the 32 px the portrait
two-row change assumed from the small boards' sizes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node name and advert location save on blur, and every blur saved and
showed "Name saved" / "Position saved", edited or not. On the M9 (no
touch) moving focus past a field with the d-pad is a blur, so the
alert flashed for each field crossed.
On blur, skip the save and the alert when the field still holds what
it was filled with: the name with missing glyphs replaced, the
position as "%.6f". An edit still saves and says so. This also stops a
bare blur writing the glyph-replaced display form over the real name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chan add <name> [key], chan add #tag, chan del <slot|name> and
chan key <slot|name>, in the on-device console and remote terminal.
They reuse the Channels screen's save and delete paths, so the chats
list updates at once. No rename: chat threads are matched to channels
by name, and the Channels screen has no rename for the same reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The channel/sender line was pinned at y=30 and the message at y=60,
so a title taller than 30 px (larger UI-size presets, bigger panels)
was drawn over the message, and the rest of a tall screen stayed empty.
Centre both horizontally and centre the pair vertically with 16 px
between them; the message box takes the height its text needs, capped
to what fits and cut at a whole line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The name started a fixed 24 px after the icon, but the antenna and room
glyphs are wider than the person glyph and all three grow with the
UI-size preset, so some names touched their icon. Start the name 8 px
past the widest of the three, never closer than before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The send button sat at the composer's bottom-right, under the panel's
rounded corner. Pull it in by the same 16 px safe-area the tab bar
uses, and narrow the text box to match.
Portrait: the single row (QR | emoji | text | send) was crunched on the
284-px panel. Split it into two rows: quick-reply and emoji
right-aligned on top, text and send below. All three buttons are 32 px,
midway between the old 30-px chips and the 34-px send button.
Landscape keeps the single row, and chatComposerApplyRows re-lays it
out when the keyboard's rotate button flips orientation with a chat
open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>