The GPS enable pin (GPIO11) is active-LOW on the original ThinkNode-M9 but
active-HIGH on the newer revision (keyboard controller at 0x6D, e.g. Elecrow's
Meshtastic SKU LMM15409D2). Driven LOW on those units the GPS never gets a
fix: on one unit, same spot, 0 satellites with EN active-LOW vs 15 with
active-HIGH, and 34 after 20 minutes outdoors with the GPS toggled off and on
again from the UI. Meshtastic's ELECROW-ThinkNode-M9 variant makes the same
split, keyed on the same keyboard address (m9Version > 1 ? HIGH : LOW), and
Elecrow engineers have confirmed the split.
WadaNmeaLocationProvider: keep the enable level in a member that defaults to
the compile-time GPS_EN_ACTIVE, add setEnActive() to override it at runtime
(re-driving the pin so a running GPS stays on), and an optional
setPostEnableHook() run at the end of begin(). Every other board is unaffected.
thinknode_m9: m9KeyboardAddr() and a one-shot found-callback on the keyboard
driver; target.cpp uses it, when the controller is at 0x6D, to call
gps.setEnActive(true) and to register a GPIO10 wake pulse (50 ms, HIGH 5 ms,
LOW, 50 ms) after enabling, as Elecrow engineers confirmed. Any other address
keeps the existing default (LOW, no pulse), so the 0x6C path is unchanged; it
is not tested here, no 0x6C unit was available.
Depends on the keyboard 0x6D probe change.
Signed-off-by: Pratik Patel <Pratik@MakerNova.io>
Guide (wadamesh.com/docs.html): contact nicknames, the redesigned control
panel with the Battery saver tile, keyboard tooltips and the amber cursor,
automatic chat icons, the default lock wallpaper, the GPS serial speed and
battery notes, the V4 built-in apps and the Lua send budget, and a new
T-LoRa Pager section (hotkeys, navigation memory, Power off confirm, power
saving). Three new screenshots, GPS position and IP masked.
Repo docs: the Pager and M9 key guides describe tooltips, per-tab memory
and Power off, and README, TLORA_PAGER_SHORTCUTS and THINKNODE_M9_SHORTCUTS
no longer use em dashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
lib_deps takes RadioLib ^7.6.0, and a fresh fetch now brings 7.8.1. Its
LR11x0::config() wraps driveDiosInSleepMode in its own skip for LR1110
firmware below 0x0306, so the call sits one level deeper and the patch
script no longer found it. It failed closed, stopping every M9 build at
its pre-script.
The script now knows both shapes. The patch still matters on 7.8.1:
firmware 0x0306 and 0x0307 get past RadioLib's own skip, send the
command, answer CMD_PERR and would abort radio init with -706.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
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
The launcher's AppFS bootloader maps only the first 50 x 64 KB of an app
while it checks and starts it (appfsBlMmap in badgeteam/appfs 2.1.0).
beta_91 (3,329,568 B) and the first beta_92 build (3,331,120 B) were past
it, so neither would start on a Tanmatsu.
On the Tanmatsu the launcher updates the app (CAP_OTA 0) and the only OTA
slots are the launcher's own 2 MB ones, so the core's "ota start" upload
page and URL install could never work there. DISABLE_WIFI_OTA stubs them
("ERR: OTA not supported in this build"), and with them go ElegantOTA,
the async web server and AsyncTCP: the app drops to 3,200,864 B, 75,936 B
under the window.
tanmatsu-appstore-publish.sh now refuses a binary over the window, and
its PR text no longer has an em dash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A Colours & theme section: the Theme page (preview, Apply and restart on the
box and in the bar under the title), MORE COLORS! in Regular, Neon and Pastel,
Taste the rainbow, chat icons and colourful bubbles, with a gallery of the
looks. A Theme card in Settings pages; the look rows left the Display card.
Chats describes the per-chat colours and the Initials option.
The main screenshots (Home, Chats, conversation, Contacts, Settings, Map) are
retaken in the new default look. The map is fully loaded this time (the beta_89
shot was taken mid-download), with its coordinates pixelated as before.
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>
New sections for the lock screen and PIN, the Constellation screensaver,
new-message cards, the control panel, text size and the T-Deck trackball;
Home, Chats, Contacts and Settings rewritten for the new screens; Settings
pages regrouped as on the device; every screenshot recaptured on a T-Deck
running beta_89 (location, Wi-Fi name and pairing code masked).
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>
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>
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>
7ce1b3f turned off CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE so a wrong-
firmware C6 could not reboot the board. It stopped the boot loop and I called
it fixed on a 45 s capture. That was the wrong test. Disabling the restart did
not stop esp-hosted, it only removed its exit: sdio_read_task posts
ESP_HOSTED_EVENT_TRANSPORT_FAILURE and then `continue`s straight back into a
register read that is going to fail again. Measured on the same board:
731 failed register reads in 45 s, ~2.2 KB/s of log, forever
A boot loop and an unbounded retry storm are the same mistake: an unreachable
co-processor is a PERMANENT condition and the driver treats it as transient.
* The rx task now parks after the failure event is posted (build-time patch to
the esp-hosted fork, same idempotent mechanism build.sh already uses for
libsodium and the SD_MMC pull-ups; it fails loudly if upstream moves that
code rather than silently not applying). No reboot, no retry: Wi-Fi and BLE
are unavailable and everything else carries on.
* hostedInitBLE() no longer runs unconditionally. It is a full esp-hosted
handshake and it ran on every boot even though the banner advertises lazy
initialisation -- so on a mismatched unit it started a transport that can
never come up, for a radio the user may have had switched off. It now
honours the same stored intent beginBle() already checked.
* The keyboard hot-plug probe backs off from 1.5 s to 15 s once the expansion
has been absent a few times, and returns to the fast rate the moment one
succeeds. The probe is a real I2C transaction and on an empty bus Arduino's
Wire layer logs two [E] lines per attempt: a permanent drip down the USB-CDC
the companion app talks over, for an accessory most people do not own.
Measured on an ESP-AT unit running the HOSTED image, 150 s captures:
shipped 7ce1b3f only this
resets 5 0 0
host restarts yes yes 0
failed reg reads - 731/45s 1
steady log rate - 2.2 KB/s ~14 B/s
UI never appears stays up
The remaining ~14 B/s is the backed-off keyboard probe on a board with no
expansion attached, which is the expected floor.
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
Two things made a full harness run untrustworthy, and together they produced a
false accusation against a community app.
wada.fs was stubs. append threw the data away, remove always said true, and
read, write and list were not there at all — although the firmware registers
all five (LuaAppHost.cpp). Any app that writes a file and reads it back
therefore failed here while working perfectly on device. wardrive 1.2 is
exactly that app: it errors on a nil fs.read at its first log flush, long
before it renders anything, and the emoji scenario then reported the repeater
name as unrendered. Read as "1.2 regressed the beta_70 emoji fix", which it
did not. It passes untouched once fs is real.
wada.fs is now an in-memory filesystem following LuaAppHost's semantics: read
returns the data plus the file's TOTAL size so a caller can window a growing
log, an offset at or past the end gives an empty string rather than nil, a
missing file gives nil, and list is sorted so a hash order cannot make a test
flaky.
The second: the gpscompass scenario list was acting as the dispatch default,
so an app with no branch of its own was run against tests written for a
different app. Six store apps have no branch (2048, airtime, monitor, nearby,
sdktest, snake), so a full sweep came back with six red apps as its normal
state — and a real regression would not have stood out against that. Those
apps now say they have no scenarios and run none. They are untested, which is
worth saying out loud, but that is not the same as broken.
A sweep of every store app is now 8 apps passing their own scenarios, 6
reported untested, 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Dropped the fault framing. The notes describe the mechanism and the change,
not whose code it was.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.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>
Curated from the two beta notes. Leads with the two things a user must know
before this reaches their device: do not switch a T-Display P4 to landscape on
this build (fixed in beta_86), and a T-Deck Max on beta_84 must be updated over
USB rather than from the device.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>