backupInternalFs() still returned FFat on the T-Display P4, but the hot
store moved to LittleFS('storage') at beta_54 and FFat is never mounted
since - every profile export and backup failed at open with 'can't open
file'. The list and restore paths share the helper, so all three heal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PR inserted LANG_HU at slot 1 (shifting every stored language choice)
and dropped LANG_RO from the enum while the table still carries all 14
columns, so it did not compile as-is. Romanian is restored to the enum in
the table's column order, a one-time v45 remap keeps every existing user's
language selection, one row had its Hungarian cell pasted twice, and the
enum gained an append-only warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per user feedback on beta_57: the retry default is the user's choice to
make. Applies to the v45 migration line, the fresh-install default, and
the pre-UI mesh state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default lock wallpaper had MESHCOMOD baked into the image pixels
(both the rendered RGB565 embed and the SPIFFS-seeded JPEG) - the beta_56
branding fix only changed the animation text drawn over it. Regenerated
both assets with the WADAMESH wordmark and brand-teal padlock; the JPEG's
size change makes the SPIFFS seeder replace the old copy on next boot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
beta_57 inserted retry_echo MID-struct (between rx_queue and web_mirror)
instead of at the blob tail. The stored cfg is a raw memcpy, so on upgrade
every later u8 shifted one byte: remote_mode inherited remote_landscape's
force-flipped 1 (v37) - every upgraded device booted into remote mode -
map_tile_debug inherited hist_sync_after's 2 (debug overlay on), and
hist_sync_after fell to 0 (blocking chat-flush fallback disabled).
v45: retry_echo moved to the true tail, a one-time repair resets the
shifted run on v44 blobs (originals unrecoverable - beta_57 re-flushed
over them), and auto-retry is opt-in now (default OFF) per user feedback.
Struct gains a loud append-only warning. Layout verified with a host-side
byte-level test of all three upgrade paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The Format button (and its busy overlay) is compiled for T-Deck, M9 and
V4-R8, and Copy/Paste for every Files-app board, but both deferred workers
in UITask::loop were gated T-Deck-only. Elsewhere the tap set a pending
counter nothing consumed: the notice stayed up forever and the format
latch blocked SD remounting until reboot. Widen the format worker to the
button's gate (via the per-board sdSharedSPI accessor), ungate paste.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A repeater name with an emoji (multi-byte UTF-8 cut at a byte cap) or a
literal '#' desynced the LVGL recolor runs in the Discover feed, visually
killing every row after it; same class in the message-info repeat list.
Names are now UTF-8-boundary-cleaned and '#'-escaped at both sites, and
copyUtf8ReplacingMissingGlyphs no longer emits a split glyph at its cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'Retry sends until heard' switch under Radio & Mesh gates mikecarper's retry
train (#207): OFF stops new retry trains (an in-flight one finishes its
schedule), applied live and persisted (cfg v44 retry_echo, default ON).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_wdtHeavyDepth() is a plain int, but WdtHeavyGuard is taken from both the
UI/loop task and the core-1 history worker (histFlushTaskFn ->
uiSegRunArmedJob -> uiSegAppendRecords/uiSegCompactWrite, each of which holds
one). Both run at priority 1 on core 1, so ordinary tick round-robin can
preempt between the load and the store of d++ / --d.
That race predates this PR, but this PR makes it likely rather than
theoretical: the new guard in rebootDevice() is constructed immediately
before uiHistWaitWorkerIdle(), a deliberate wait of up to 9 s on the very
worker that also takes the guard. Interleave the two and either
- the depth strands above zero, so enableCore0WDT() is never called again
and IDLE0 stays unsubscribed from the task watchdog for the session, or
- the depth drops to zero while a guard is still held, re-arming the
watchdog during the flash burst it exists to cover -- which panics
mid-save and loses exactly the chat history rebootDevice() runs to
persist.
Use std::atomic with an acq_rel fetch_add and a compare-exchange decrement.
The CAS preserves the original `d > 0` underflow guard, which still matters:
doFactoryReset() calls wdtHeavyBegin() with no matching end because it
reboots, so the counter can legitimately stay elevated and an unbalanced end
must not drive it negative.
Signed-off-by: Pixel Perfect <me@pixp.cc>
MeshCore's airtime_factor is the Dispatcher's TX pacing multiplier: duty =
1/(1+af), so a 10% duty limit needs af = 9. The presets wrote af = pct/100
(0.10), which configured every preset user's radio to ~91% duty -- the Home
meter's 'always 91%' was truthfully reporting the misconfiguration, on every
region preset including UK/EU.
- applyMeshRadioPresetFields converts duty% -> factor correctly (clamped to
the core's 0..9).
- findMatchingMeshRadioPreset accepts both the correct and the old buggy value
so previously-configured devices still show their preset in the dropdown.
- healPresetAirtimeFactor (run once at UI begin) rewrites the factor for
devices whose params exactly match a preset and whose stored factor matches
the OLD buggy write -- the exact-match requirement is the guard against
touching deliberate user-set values.
NOTE for the web client: Meshcomod-client/shared/radio_presets.ts documents
the same pct/100 convention and needs the matching fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pager's SD support shipped in pieces (mount ladder with XL9555 card-detect,
tile-cache fallback, shared TFT_eSPI SPIClass) but CAP_SD/CAP_FILESYSTEM were
never flipped from the pre-wiring 0, so the Files app gates and the whole
'Store data on SD' machinery stayed compiled out -- an inserted, mounted,
browsable card that nothing would store to. Flipped the caps, extended the
boot storage selection + SPIFFS->SD migration + SdNvsPrefs routing gates to
TLORA_PAGER, and exported tloraPagerSharedSPI() from the variant so main.cpp
can mount the card at boot on the shared bus like the T-Deck.
Both pager envs + T-Deck + V4 + R8 build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- #165: the animated post-unlock overlay still said MESHCOMOD; now WADAMESH.
- #191: Discover app rows cap the name and use single-letter node types so the
trailing last-heard age always fits the line.
- #157: the Info popup only read the transport scope for FLOOD messages, so a
region-scoped DIRECT message showed no scope; and senders that carry the
region in transport_codes[1] with codes[0]=0 showed Scope 0000. The scope is
now read for any RX and falls back to codes[1] when codes[0] is zero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A card whose power-up handshake only completed at 400 kHz kept 400 kHz as its
SESSION clock — SD.begin's frequency is the operating clock, and neither the
boot ladder (main.cpp) nor the runtime ladder (fmSdTryMount) ever raised it
after a slow success. With the SD as the T-Deck's primary store that meant
loading contacts + history at ~25 KB/s: the reported ~3-minute boot, f_getfree
timeouts making an inserted card show as 0 KB/empty in the file manager, and
backups wedging. Launcher installs hit it most (the Launcher's own SD use
leaves the card needing the slow wake path more often).
Standard SD bring-up is init-slow-then-raise: after any sub-4MHz success both
ladders now re-begin at 4 MHz, falling back to the clock that just worked if
the fast re-begin fails. A healthy card is unaffected (mounts at 4 MHz on the
first rung). T-Deck + V4-R8 + M9 (the shared-SPI SD boards) build.
Likely also behind the long-standing Launcher-mode weirdness cluster together
with the torn-prefs fix (#195/#196): slow-clock sessions made every write
window enormous.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stale CAT_RADIO from before the settings reorg gave GPS its own category —
every other tile already pointed at its own page. All boards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The R8 env shipped HELTEC_V4_BUZZER_PIN=-1 because the Kit V2's PAM8904 wiring
was unconfirmed, so R8 users have never heard a notification. Heltec's
Expansion_board_V2.03.pdf schematic settles it: DIN = GPIO4, and EN1/EN2 are
hardware-strapped high (10K pull-ups, 3x boost) with DIP switch SW1 as a
hardware mute -- no enable code needed, the stock V4 tone() chime drives DIN
directly. GPIO4 is unclaimed on the R8 (board I2C moved to 17/18).
Tester-verify: chime on message. If still silent, SW1 on the expansion board
is the hardware mute -- check it is not switched to ground.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A tester who switched the V4-R8 to landscape got stuck: every touch registered
in the wrong place, so the setting could not be reached to switch back.
Cause: the shared cap-touch landscape transform (applyPointRotation +
rotateSwipeDelta in HeltecV4CapTouch.cpp) was tuned on the V4's ST7789 driver,
whose PORTRAIT baseline is panel rotation 2 -- a +180 offset baked into the
landscape maps ("+180 baseline" in the comments). The R8's LovyanGFX panel runs
baseline 0 (offset_rotation = 0, portrait = setRotation(0)), so
applyHardwarePanelRotation lands the R8 panel 180 degrees from where the V4
lands for the same requested rotation, and the shared maps point-mirror every
landscape touch.
Fixes:
- HeltecV4CapTouch.cpp: R8-gated landscape maps without the +180 baseline (the
V4's two landscape cases swapped) for both the touch-point transform and the
swipe-delta transform. Marked TESTER-VERIFY: geometry is confirmed, sign is
not -- if landscape is still mirrored on-device, cases 1 and 3 swap back.
- UITask.cpp: the V4-R8 forces PORTRAIT at every boot (and persists it), so the
stuck reporter recovers on their next reboot and a wrong sign guess in the
new maps costs a reboot instead of a stuck device.
V4 and T-Deck behavior unchanged (verified: both build; the new maps are
HELTEC_LORA_V4_R8-gated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The p4StorageCall dispatcher committed in 099ccd8 had an inverted guard: it
skipped marshalling when already on core 0 -- written believing the main task
ran on core 1, but ESP_MAIN_TASK_AFFINITY_CPU0 puts the UI+mesh main task on
core 0, so every "hopped" write actually ran inline on the main task (proven by
a sector-level SDMMC trace: all card I/O on task 'main'). Fixed: the guard is
task-identity (p4OnStorageTask), all 11 write wrappers use it, and the storage
task runs at priority 3 on core 0 -- core 1 was tried and is pathological for
SD I/O (the SDMMC completion ISR lives on core 0; cross-core wakeups made a
contact save take ~20 s).
With the dispatcher REALLY engaging, the mechanism of the SD-store flash was
finally measured instead of theorized: a gap tracker in the DSI frame-restart
ISR recorded 35-39 ms inter-frame gaps (vs the 20 ms cadence) exactly
interleaved with SD sector transactions. The panel's frame DMA stops after
every frame and waits for an ISR to re-arm it; anything that masks interrupts
long enough (SD-transaction critical sections -- and on RISC-V a critical
section masks ALL levels, which is why an ESP_INTR_FLAG_LEVEL3 experiment
changed nothing) makes the restart late, and the panel displays the extended
blanking as the whole-screen flash. No error register ever latches because
nothing fails; frames are merely late. This also finally explains the tile
asymmetry: a tile is one large sequential transfer (one interrupt burst per
file), a FATFS store save is dozens of small transactions and metadata
round-trips -- dozens of chances per save to push a frame restart late.
Ship state stays: hot store on internal LittleFS (no SD transactions on the
hot path -- no late frames), SD for tiles/bulk. The promising future fix for
restoring the T-Deck-style SD store is making the panel autonomous: a CIRCULAR
single-FB DMA descriptor (dw_gdma_lli_set_next(item, item), is_last=false) so
the hardware loops frames with no ISR in the path at all -- prototyped against
the local IDF's esp_lcd_panel_dpi.c and reverted with the other diagnostics
(sdmmc sector trace, DSILATE gap tracker, LEVEL3); it needs its own verified
session and a durable patch mechanism before it can ship.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to 3bf56cd, driven by the (correct) challenge that "SD writes upset
the panel electrically" could not be the whole story when tile writes never
flash and other P4 firmwares use their cards freely. The completed on-device
matrix now separates two distinct mechanisms:
M1 An SD write EXECUTING on core 1 (where display.begin() allocated the DSI
frame-restart ISR, and where the Arduino loop task runs) masks that
core's interrupts inside the SDMMC/FATFS critical sections long enough
to drop a display frame. Proven: 80 contact saves to SD from core 1
flash every time; the identical 80 saves hopped to core 0 never flash.
NOT electrical -- the challenge was right.
M2 An SD write CONCURRENT with live UI rendering still flashes even from
core 0 (manual add/delete flows pump LVGL while the hopped write runs;
the write-open trace shows those runs performed only correct core-0
writes and flashed anyway). The same manual flow with the card mounted
but ZERO SD writes (internal LittleFS store) is clean.
Shipped design therefore stays: hot store on internal LittleFS (immune to both
mechanisms, verified across every flow), SD for tiles/bulk. Tiles' long-standing
immunity is consistent: brief core-0 writes vs the store's long ones.
New in this commit, kept for good:
- p4StorageCall (DataStore.cpp): a core-0 storage task; every hot write entry
(saveContacts/saveChannels/savePrefs/blobs, history segs, threads file) hops
onto it, blocking the caller -- semantics unchanged, execution core moved.
With the internal store this is still wanted hygiene: no storage write ever
runs on the render core again.
- WADA_SD_STORE_TEST build gate: puts the store back on the SD card for future
M2 investigation (known to still flash on manual flows; do not ship).
- TDP4_POKE_TRACE now also logs every write-open with its issuing core ([SDW]),
which is what pinned M2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause, established by on-device elimination over one long session: SD-card
WRITE bursts electrically disturb the T-Display P4's AMOLED -- the whole-screen
blue flash of #167. The display stack itself measured clean at every layer while
flashes were visible: framebuffer content (flush tracer: every band black), DSI
bridge underrun interrupt (never fired), DSI host protocol errors incl. the host
payload-buffer underflow (never latched), DCS/brightness traffic and XL9535
expander writes (silent), disp on/off (never). The flash survived every digital
derate -- 4->1-bit SD bus, 20->10 MHz SD clock, 1000->750 Mbps DSI lane rate --
on every card tried, at any brightness (including near-zero, i.e. the panel is
being upset at the hardware level, not showing content), yet vanished the moment
the card was removed or left unwritten. The card's rail is switched off the same
3V3 domain as the panel (XL9535 SD_EN), so its program-current spikes reach the
panel and no firmware setting can filter them. Streaming tile writes never
flashed; rapid open-write-close store bursts always did. Meck-P4 not flashing
proves nothing about the board: it never writes the SD.
Fix: the hot store (contacts/channels/identity/blobs/prefs-file/chat history)
lives on the INTERNAL 'storage' partition; the SD keeps bulk, gentle writers
(map tiles, backups). The internal partition is mounted as LittleFS, not FAT:
the P4's FAT layer has documented broken metadata and the v3 migration proved
its writes are no better (a 102 KB contacts3 "copied" onto it read back as 0 B;
on LittleFS the same copy reads back byte-exact and loads all 689 contacts).
esp_littlefs looks partitions up by label with SUBTYPE_ANY, so the ex-FAT
partition converts in place on first boot (formatOnFail) -- no partition-table
change, OTA-safe.
One-time v4 migration (lessons v1-v3 encoded): substance-gated (only from a
card whose contacts3 beats the internal one -- a fresh test card once donated a
blank store), whitelist-only copy (v2 filled the partition with junk before
contacts3 landed), both historical card layouts probed (root vs /meshcomod),
marker written only after the copy, card never written (it remains the
rollback).
Also in this change, kept as opt-in diagnostics (dormant without their env
gates): WADA_P4_POKE_TRACE (DCS/expander/disp-on-off tracing + a DSI host
error-register poller), WADA_SD_KHZ / WADA_SD_1BIT (SD primary-rung derates),
WADA_DSI_MBPS (lane-rate override; default back at the vendor 1000 -- the 750
experiment changed nothing and is documented in the driver).
Verified on device: no flash on the contact-churn trigger with the store
internal (the previously 100%-reproducing trigger), 689/689 contacts after
migration, tiles still on SD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root-cause work, not another guess. The esp_lcd DSI driver's own ISR says it:
// when an underrun happens, the LCD display may already becomes blue
ESP_DRAM_LOGE(TAG, "can't fetch data from external memory fast enough, ...")
A DSI bridge FIFO underrun displays as exactly one blue frame -- the reported
whole-screen split-second blue flash -- and the driver logs an error line every
time it happens. Every serial capture until now grepped for tile/storage/trace
patterns that this line does not match, so it was never looked for.
The esp-iot-solution MIPI-DSI guide prescribes, for this exact error: L2 cache
256 KB, 128 B cache lines, and perf optimization (plus SPIRAM 200M + XIP from
PSRAM, already set). Applied all three for this board only:
- build.sh: sdkconfig fragment order general;wadamesh;tdisplay_p4 so the BOARD
fragment wins over the shared ones (it was last-loser before; the shared
wadamesh fragment's -Os, needed for the Tanmatsu AppFS ceiling, was
unoverridable). No net config change from the reorder itself: the board
fragment previously duplicated no shared keys.
- sdkconfigs/tdisplay_p4: COMPILER_OPTIMIZATION_PERF, CACHE_L2_CACHE_256KB,
CACHE_L2_CACHE_LINE_128B. App 2.99 -> 3.35 MB of the 4 MB slot.
- main/CMakeLists.txt: -O2's deeper analysis trips new warnings in vendored
components (esp-modbus etc.); same keep-visible-never-fatal policy as the
existing strips.
Verification harness (scratchpad, not shipped): the companion protocol driven
over the USB console (contact add/remove = the 100% reported trigger, pure
local DB ops, no LoRa TX) while the same session captures every console line.
Since the driver logs each underrun unconditionally, one watched run now gives
a decisive verdict whichever way it goes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
None of the three fixed the flash, and each carried a cost:
- prefs-on-SD (1bc0c7a) also moved main.cpp onto a different set of headers than
it had been compiling against. SdNvsPrefs.h's own comment warns that exact
mismatch previously caused a free()-of-garbage bootloop, and a map tile
regression showed up on these builds. Not worth the risk for no benefit.
- LVGL bands 24 -> 64 (1e6e121): a full-screen repaint went from ~26 transfers
to ~10 with no change on device, so burst count is not the mechanism.
- XIP-off (0695ca6): no change on device, so PSRAM bandwidth starving the DSI is
not the mechanism either.
Back to the configuration where map tiles were confirmed working. Kept the two
changes that stand on their own: the framebuffer is blanked before the panel is
switched on (it was streaming uninitialised PSRAM to the screen), and the
WADA_P4_FLUSH_TRACE build gate.
Standing evidence for whoever picks this up: the tracer shows every large band
LVGL pushes is 0x0000 black, including through the boot window where the flash
occurs, so the colour is never in the framebuffer and the UI layer is excluded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ran the flush tracer instead of theorising further. Result is decisive: every
large uniform band LVGL pushes to the panel is rgb565=0x0000 (black), across the
whole boot window where the flash occurs. LVGL never paints the flash colour, so
the panel is displaying something that was never written to the framebuffer.
That excludes the entire UI layer, and with it the redraw/repaint theories.
Adds WADA_P4_FLUSH_TRACE=1 as a build gate so the tracer can be run on demand
without shipping it (it slows the flush path, so never confirm a fix with it on).
With the UI excluded the remaining candidate is the DSI starving. The panel
streams ~84 MB/s out of PSRAM continuously, and XIP put every instruction fetch
and rodata read on that same bus. The triggers are all bursts of code and data
churn (rebuilding the contact/chat list); panning the map reuses a warm tile
cache and never flashes. Turning XIP off leaves the PSRAM bus essentially to the
display. Experiment, not a claimed fix.
Also supersedes the burst-count theory from 1e6e121: widening LVGL bands from 24
to 64 lines (26 transfers -> 10 for a full repaint) changed nothing on device.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two changes against the whole-screen colour flash.
1. Real fix for the flash testers see BEFORE the boot splash. The panel was
switched on and only then was the framebuffer cleared, so the DSI streamed a
freshly-allocated (garbage) PSRAM buffer to the screen until the clear caught
up. Blank it with a memset + cache flush while the panel is still dark. That
also removes 39 back-to-back draw_bitmap transfers into a buffer the panel is
scanning out.
2. Diagnostic, P4 only: LVGL bands 24 -> 64 lines. This panel is DSI video mode
with a SINGLE framebuffer, so every band is copied into the buffer being
actively scanned. At 24 lines a full-screen repaint is ~26 back-to-back
transfers into that live buffer. The reported triggers (message arrives,
opening a chat with unread, adding/deleting a contact) all rebuild the whole
screen; panning the map repaints only its own region and never flashes, which
is the shape this predicts. Wider bands cut the same repaint to ~10
transfers. Sized so the 2x upscale scratch still fits internal SRAM, so burst
count is the only variable that moves.
Supersedes the write-path theory: the flash still occurs with contacts, history
and prefs all on SD, and the pre-splash case involves no write at all.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The T-Display P4 never called SdNvsPrefs::useFile, so although contacts and chat
history correctly moved to the SD card, every settings/thread-state write still
landed in internal-flash NVS. On this board that is not just wear: the DSI panel
streams its framebuffer continuously out of PSRAM, and an internal-flash write
visibly disturbs it -- the whole-screen flash reported as #167. The trigger list
matches exactly: a message arriving, opening a chat with unread messages, and
deleting a contact (which writes repeatedly, hence flickering for the whole
operation). Map panning writes tiles to SD continuously and never flickers,
which is what pinned it down.
Only when a card is mounted. With no card there is nowhere better to put prefs,
and moving them from NVS to the internal FAT partition would trade one flash
write for another while needlessly relocating existing users' settings. Old NVS
values still load and migrate on their next save, so this is transparent.
Fixing it also required an include-path fix. The vendored core component
publishes core/src publicly and still carries OLD copies of the headers whose
.cpp files it deliberately excludes because our src/ ships them. main.cpp's
angle includes therefore resolved to the stale core headers while the linker
bound the new objects, so anything added since the core tag looked like it did
not exist (SdNvsPrefs::useFile, touchPrefsReload). Putting the repo's src/ ahead
of the core dir in main's INCLUDE_DIRS makes main.cpp and the app layer agree.
Verified on device: "[PREFS] backend = FILE /meshcomod".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three independent 100%-reproducible triggers finally pinned this down, and they
all turn out to be the same thing: a write to flash.
- every received message -> chat-history write
- opening a channel that has unread messages -> mark-read write
("if you open them back up and there aren't unread messages, there's no flash")
- deleting a contact -> the chunked bulk delete writes repeatedly, and
it flickers CONTINUOUSLY for the whole operation
Mechanism. This panel is a MIPI-DSI DPI/video one: its single ~1.4 MB framebuffer
can only live in PSRAM and the DSI DMA streams it to the glass continuously. PSRAM
sits behind the same cache as flash, and a flash write has to DISABLE that cache
while it runs. For the duration of the write the DMA cannot fetch the framebuffer,
the panel underruns, and a frame comes out as a whole-screen colour flash. Nothing
to do with what is being drawn, which is why it appeared on every screen, on every
board revision, and from the very start of the port.
Fix: CONFIG_SPIRAM_XIP_FROM_PSRAM=y. Code and constants are relocated into PSRAM
at startup so the cache no longer has to be dropped for a flash write. The SoC
supports it (CONFIG_SOC_SPIRAM_XIP_SUPPORTED=y) and IDF ships the same pairing as
a P4 reference config (esp_mm/test_apps/mm/sdkconfig.ci.xip_psram_esp32p4). It
pulls in SPIRAM_FETCH_INSTRUCTIONS / SPIRAM_RODATA / SPIRAM_FLASH_LOAD_TO_PSRAM.
This also explains why 7aaff80 (moving the flush scratch out of PSRAM) did not
help: that reduced PSRAM BANDWIDTH contention, but the cache is disabled outright
during a flash write, so bandwidth was never the constraint. That commit is still
worth keeping on its own merits, but it was not the cause.
Also restores CONFIG_ESP_HOSTED_RESET_SLAVE_USING_CALLBACK and its SDIO twin to
this fragment. They existed only in the GENERATED sdkconfig, so regenerating it
for the XIP change silently dropped them and esp-hosted fell back to resetting the
slave over GPIO 54 — a line this board does not have (C6_EN is on the XL9535, so
it can only be driven by callback). Inert today because TDP4_C6_READY is 0, but
the config is now reproducible from its fragments instead of depending on an
untracked file nobody could regenerate.
Boots clean with XIP enabled: display up, GPS probe reading, only rst:0x1
(POWERON). Needs field confirmation against the contact-delete trigger, which is
the reliable one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of "GPS on the P4 stays on searching forever", reported repeatedly and
chased through pins, baud, wake polarity, antennas and a comparison against the
Meck-P4 port. None of those were wrong. The loop simply never called
sensors.loop().
Every S3 board calls it once per iteration (src/main.cpp: STALL_SCOPE("sensors",
sensors.loop())). That file is excluded from this build and the P4 has its own
main loop, which is where the call went missing during the port: sensors.begin()
ran at boot and nothing ever pumped the manager again.
sensors.loop() is what drains the GPS UART into MicroNMEA. Without it the parser
never receives a byte, so isValid() is false forever and the UI shows "searching"
regardless of signal. It also starves the telemetry sensors and the GPS->RTC time
sync that hang off the same manager.
The evidence lines up exactly:
- a T-Deck with the same L76K fixes almost instantly, the P4 never does;
- 15 minutes under clear sky changed nothing, because no amount of signal helps
a parser that is never run;
- and a raw UART probe showed the MODULE reaching a valid 3D fix
($GNGGA,...,1,05,2.2 at HDOP 2.2, plus $GPTXT ANTENNA OK) that the firmware
had no idea about. That reading came from the probe, not the provider, which
is precisely why it looked like the GPS "worked" while the device disagreed.
Builds clean and boots clean on the P4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reported: the GPS page only updated when you closed and reopened it.
refreshSettingsSectionSubtitles() gated the GPS label update on
g_set_modal.kind == SettingsModalKind::Device. That stopped being true when the
settings reorg moved the GPS block into its own PAGE (CAT_GPS ->
buildDeviceSettings(DSEC_GPS)) — the label is created there, but the refresh
condition still described the old Device modal, so it matched nothing and the
text was written once at build time and never again.
Everything on that page was therefore frozen at whatever was true the instant it
opened, including the elapsed-search timer added in 5c9f9cf. That is worse than
useless for GPS specifically: a receiver that is acquiring normally looks
completely stuck, which is exactly the "8 minutes, still nothing" reading, since
the seconds counter could not move.
Now refreshes when either host is open, guarded with lv_obj_is_valid so a stale
pointer left behind by a closed page can never be written.
Builds clean on V4, T-Deck and P4; flashed to the P4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two corrections to 5c9f9cf, both reported from the device.
1. The dropdown got taller. gpsStatusStr() is shared: the top-bar control centre
uses it as well as the GPS settings page, so making it two lines pushed the
whole dropdown out. It now takes a `compact` flag — the dropdown gets one line
(and drops the altitude), the settings page keeps the full text.
2. "No satellite data yet" was a claim we cannot support. It was gated on the
receiver having decoded GPS time, on the reasoning that a receiver decodes time
before it can solve a position. That reasoning is right in general but wrong for
this parser: MicroNMEA reads RMC left to right and returns at the EMPTY latitude
field of an unfixed sentence, so it never reaches parseDate and the time stays
unset until a fix already exists. The branch was therefore always false while
acquiring, and the page told a user with a perfectly healthy receiver that there
was no satellite data. Removed.
What is left is what we can actually stand behind: how long it has been searching,
and that a cold start needs sky view and some minutes.
Recording the limitation properly in the comment, because it has now cost time
twice: from the UI layer a tracking receiver and an unpowered one are
indistinguishable. LocationProvider exposes only fix / sats-used / lat / lon / alt
/ timestamp, all empty until a fix, and the core keeps its MicroNMEA object
private. Satellites-in-view, SNR and HDOP need a monorepo core change plus a
core-* tag bump. Until then the serial boot probe (bdc0c7f) is the way to see the
real sky view.
Builds clean on V4, T-Deck and P4; flashed to the P4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The GPS settings page showed "GPS: acquiring..." and nothing else until a fix
landed. That single string covers two completely different situations: a receiver
happily tracking satellites that needs another minute, and a receiver that is not
talking at all. They look identical to the user, so there is no way to tell a
working device from a broken one by looking at it. That ambiguity is what turned a
healthy T-Display P4 GPS into a long investigation.
While acquiring, the page now shows:
- how long it has been searching, so a stuck receiver is obvious from elapsed
time alone rather than from staring at an unchanging word;
- whether the receiver has decoded satellite TIME. That is the useful tell: a
GPS decodes time/date off the air well before it has enough satellites to
solve a position, so valid time with no fix is positive proof the antenna and
the UART are fine and it is genuinely mid-acquisition. The wording then points
at sky view, which is the actual remedy;
- and when there is no satellite data at all, it says so plainly instead of
implying progress.
On a fix it now also shows altitude alongside the satellite count and position,
and the count is only printed when non-zero (it is satellites USED, so a bare
"0 sats" read as a fault).
Not included, and worth being clear about: satellites IN VIEW and per-satellite
SNR are NOT reachable from this repo. The core's MicroNMEALocationProvider keeps
its MicroNMEA object private and LocationProvider exposes only fix / sats-used /
lat / lon / alt / timestamp, so a full sky view needs a monorepo core change plus
a core-* tag bump. The boot probe added in bdc0c7f prints that data over serial in
the meantime.
Builds clean on V4, T-Deck and the P4; flashed and boot-verified on the P4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to 6430866. Two changes.
Print one example of each distinct NMEA sentence type, not just the first line.
A single GxGSV is not enough to judge satellite visibility: the first boot showed
"$GLGSV,1,1,00" and I read that as zero satellites, but GLGSV is GLONASS ONLY and
says nothing about GPS, Galileo or BeiDou. The full set showed 10 GPS + 5 GLONASS
in view with SNR 33/35/23, plus "$GPTXT,...,ANTENNA OK" from the module's own
antenna check, i.e. a healthy receiver mid-acquisition rather than a fault.
Shorten the drain from 3s to 1.2s. It runs on EVERY boot and it consumes bytes
the NMEA parser would otherwise see, so it only needs to be long enough to catch
one of each sentence type (the L76K repeats the full set about once a second).
1.2s still captured all nine types on the verification run.
Confirmed working on hardware: a later boot reported
$GNGGA,073620.000,5050.80018,N,00452.95932,E,1,05,2.2,36.0,M
$GNGSA,A,3,08,22,32,...,2.2
i.e. a valid 3D fix on 5 satellites at HDOP 2.2, down from no fix / HDOP 25.5 a
few minutes earlier. The GPS path was never broken; the old diagnostic just could
not tell anyone that, because it reported a flag ENV_SKIP_GPS_DETECT forces true.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The line here claimed "[GPS] L76K DETECTED (NMEA on GPIO22)" but never looked at
the UART. It reported whether the "gps" SETTING had been registered, and on this
board ENV_SKIP_GPS_DETECT forces that true unconditionally (we set it because the
P4's heavy boot misses the core's 1 s detect window, and a false negative hides
the GPS toggle entirely). So it printed DETECTED on every boot whether or not a
single byte had ever arrived, and it survived several rounds of "GPS still isn't
working" while proving nothing.
Replaced with a real 3 s drain of Serial1 that reports byte count, complete NMEA
sentence count, the configured baud and RX pin, and the first sentence seen. That
splits the two cases that were being conflated: bytes arriving means pins, rails,
wake and baud are all correct and any stuck "acquiring" is a FIX problem (antenna
or sky view); zero bytes means the link itself is dead.
First run on hardware answered the open question immediately:
[GPS] UART probe: 1985 bytes, 48 lines in 3s @ 9600 baud, module TX -> GPIO22
[GPS] first sentence: $GLGSV,1,1,00,0*79
1985 bytes of well-formed NMEA proves the link is good, and GLGSV ",00," is zero
satellites in view, i.e. an antenna/sky-view issue and not firmware.
For the record, the config was already identical to Meck-P4's working port
(components/meshcore/variant.h: PIN_GPS_TX 22 / PIN_GPS_RX 23, wake on XL9535
IO11), and their L76K driver confirms both our other values: begin() defaults to
9600 baud, and sleep(false) drives the wake pin HIGH.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second half of the "large message history slows the interface down" report. The
first half (4058f8d) removed the repeated ring scan; this bounds how much history
a single chat can accumulate in the first place, which is what was asked for in
the channel: a configurable limit, with a lower default, and a warning.
Until now the message store was one shared ring with no per-chat bound, so a busy
public channel could occupy essentially all of it. That both starved every other
chat of its history and kept the ring permanently full, which is the condition the
slow paths were suffering under. The reporter had 3800 messages in one channel.
- New pref hist_per_chat (touch-cfg v42), default 250 messages per chat. Chosen
to be deep enough to scroll back through a conversation while staying far below
the 5000-record SD ring, so no single chat can dominate it.
- Enforced on append: enforceHistoryCap() blanks that chat's oldest records until
it is back within the limit. It walks from the oldest end, so for the chat that
is actually over the cap (the one filling the ring) the records it wants are
found immediately rather than after a full scan.
- Settings -> Chats: "Keep per chat (messages)" — 100 / 250 / 500 / 1000 / 2000 /
No limit, with a line explaining that older messages are dropped past the limit.
- "No limit" is a deliberate choice, not a silent one: it raises a confirmation
saying a busy channel can then fill the whole store and make things slower. The
dropdown reverts first and only commits from the confirm handler, so dismissing
the dialog leaves the previous limit in place.
The thread-history cache from 4058f8d becomes a per-thread COUNT rather than a
bool, since the cap needs to know how many records a chat holds; "has history" is
just count > 0, so the O(1) inbox behaviour is unchanged.
Existing installs pick up the 250 default and will trim on the next message in an
over-long chat. Anyone who wants the old unbounded behaviour can select No limit.
Builds clean on V4 + T-Deck; flashed to the T-Deck.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reported on the P4: the preview line under a channel in the chats list showed an
older message instead of the latest, and never moved on.
getThreadLastMessage() chose the message with the highest m.ts, and m.ts is the
ESP32 SYSTEM clock. That clock is not monotonic across reboots — it restarts
from ESP32RTCClock::begin()'s power-on seed on every boot and then only climbs
with uptime until a real time source arrives. So a message received hours into
an earlier boot carries a LARGER timestamp than one received a minute into this
boot, wins the max(ts) comparison forever, and pins the preview to itself.
It showed up on the P4 because that board has an RTC chip, so the core wrote
time to the chip and never to the system clock, which sat on the seed
indefinitely (fixed separately in fe87ff0 by mirroring the mesh RTC into the
system clock). Any board can hit a milder version of this after a reboot with no
time source, so the selection itself is what needed fixing.
Now the newest message is taken by RING ORDER — walk back from the head and take
the first match. Ring order is the real arrival order and cannot be skewed by a
wrong clock. It is also faster: it exits on the first hit instead of scanning
every slot in the ring, which matters on the same busy-channel devices as the
inbox-scan fix in 4058f8d.
Builds clean on V4 + T-Deck.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Field report: a public channel that had accumulated 3800 messages made the
interface "slow down drastically", and clearing it restored normal speed. The
reporter also noticed it was still noticeable at only ~300 messages and asked
whether something loops over every message. It does.
threadHasMessageHistory() answered "does this thread have any stored message?"
by walking the ring via getThreadMessageIndexes(). That scan stops early once it
finds a match, so a thread WITH recent messages is cheap — but a thread with
none walks every record in the ring doing a strncmp per record, and "has no
history" is precisely what the callers are testing for. getCombinedInboxCount()
and getUnreadTotal() both call it once PER THREAD, so a full ring turned every
inbox refresh into threads x messages string compares. That is the reported
slowdown, and it explains why it is felt well below a full ring.
Now cached per thread:
- an append sets the owning thread's flag directly, so the common path never
scans at all;
- anything that REMOVES messages (ring eviction, clearThreadHistory,
removeThread, any history load) marks the cache dirty, and the next reader
rebuilds every thread's flag in ONE ring pass instead of one pass per
thread, stopping as soon as all live threads are resolved.
clearThreadHistory clears just its own flag (no other thread is touched), so the
common "clear one chat" action stays O(ring) once rather than forcing a rebuild.
Behaviour is unchanged; this only removes repeated work. The separate request
from the same thread — a configurable per-channel history limit with a lower
default and a warning — is NOT in this commit.
Builds clean on V4 + T-Deck; flashed to the T-Deck.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnostic scaffolding kept from chasing the "screen flashes teal on every
message" report, behind -DTDP4_FLUSH_TRACE so it is compiled out of normal
builds (verified: no FLUSHTRACE string in the shipped binary).
It logs any large, mostly-single-colour flush band with its RGB565 value. That
answers the one question that was otherwise unanswerable from the outside: is
the flash something LVGL painted, or something below it? A UI-painted flash
appears as a burst of bands carrying that colour; if the screen visibly flashes
and nothing logs, the cause is in the panel/DSI layer and hunting through UI
widgets is wasted effort.
Two notes recorded in the comment because both cost time here. One LVGL band on
this board is 284x24 = 6816 px, so a size gate has to sit well below that — an
earlier 8000 px threshold silently disabled the whole tracer and looked like
"nothing is drawing". And the per-band uniformity scan measurably slows the
flush path, so it can mask a timing-sensitive bug: any fix must be confirmed on
a build with the tracer OFF.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both P4 SKUs render at half resolution and nearest-neighbour-upscale each LVGL band
into a scratch buffer before handing it to the panel. That scratch was allocated with
MALLOC_CAP_SPIRAM, which put it on the worst possible bus.
These are DPI/DSI panels with num_fbs = 1: the ~1.4 MB framebuffer can only live in
PSRAM, and the DSI DMA streams it to the glass CONTINUOUSLY. Every draw_bitmap is a
copy INTO that framebuffer, so with the scratch also in PSRAM a single flush put three
streams on one bus at once — the scratch read, the framebuffer write, and the DSI's own
read. Anything else wanting PSRAM in that window can starve the DSI read, and a DPI
underrun presents as a whole-screen colour flash for one frame.
That is the reported "screen flashes teal on every RX" (#167 flicker): it happens on
ANY screen and was there from the start of the port, because it is a display-bus
problem and has nothing to do with what is being drawn. The RX correlation is the
priority-10 "lora_rx" drain task, which wakes on every received packet and is ON by
default (rx_queue defaults to 1).
Allocating the scratch in internal DMA RAM removes the scratch read from the PSRAM bus
entirely. The cost is bounded and small: one LVGL band is LV_DRAW_BUF_LINES = 24 rows,
so AMOLED 284x24 -> 568*48*2 = ~53 KB and LCD 270x24 -> 540*48*2 = ~51 KB, against a
348 KiB internal region. PSRAM stays as the fallback so a fragmented heap degrades to
the previous behaviour rather than dropping to the unscaled draw path.
Not done here: lowering the drain task's priority. It is created in the vendored core
(RadioLibWrappers.cpp), which is not tracked in this repo, so a change there cannot
ship from wadamesh — it needs a monorepo core change plus a core-* tag bump. It is also
the weaker lever: the core pins that task to core 0 while LVGL and loop run on core 1,
so it is not preempting the UI thread, and the contention this fixes is the memory bus.
Builds clean and boot-verified on the device (T-Display P4, MAC ...e1:c2:a7); no
allocation failures, only rst:0x1 (POWERON).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both were built from the same template and carried the same three defects, none of
which were really about their own code: they hand-rolled the page chrome that every
other app page gets from the shared machinery.
New src/ui-touch/AppPage.h exports that machinery (appPageCreateRoot / Begin / End /
DeleteRootAsync) so a self-contained module can build the same page the in-file tool
pages do, and both modules now use it.
1. GEOMETRY. Both positioned their overlay against a local `kTopBar = 22`.
STATUSBAR_H is a RUNTIME value: SC(22) once the UI scale is above 100%, and
SB_TOP_PAD + SB_ROW*2 on the T-Display P4's two-row bar. On those boards the
page sat too high and its own title + close button landed UNDERNEATH the real
status bar, which is what "the airtime app has issues" looked like. Geometry now
comes from the live bar height, so a page cannot disagree with the bar.
2. NAVIGATION. Neither installed s_apppage_title / s_apppage_close, so each needed
its own case in statusBarTapCb and its own clause in the bar-foreground gate, and
neither reached statusBarReaderBackCb — the PRESSED fallback that exists because
the cap-touch swipe detector can drop the lone CLICKED. On a touch-only board
that left no reliable exit at all. They now install the standard hook, get the
tall "< Airtime" / "< Snake" bar, and both special cases in UITask are deleted.
3. TEARDOWN. closeCb ran `lv_obj_del(root_)` and `delete this` synchronously from a
child button's own event callback, freeing the object LVGL was still dispatching
to. Teardown is now async throughout: lv_obj_del_async for the root, lv_async_call
for the instance, one single dismiss() path. Snake's canvas buffer moves to a
destructor so it is freed after the queued root delete released the canvas.
Also fixes what the rework exposed: applySwipeGesture had swallow-guards for Snake,
sliders, chats, popups, the control centre and settings sheets, but none for app pages
— so a horizontal swipe inside Airtime reached the tab switcher and jumped to the Map
(reported on device). An opaque CLICKABLE root only blocks CLICKS; that detector reads
the touch hardware directly and never consults LVGL hit-testing. One guard now covers
every full-screen page, Spectrum and the rest included, which had the same bug.
Bar titles are copied into module-owned storage rather than holding TR()'s pointer:
TR returns the static table cell for a plain key but a recycled 4-slot ring for an
icon-prefixed one, and s_apppage_title keeps the pointer for the page's lifetime.
Builds clean on V4, T-Deck and the P4 IDF target; flashed + boot-verified on the P4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b066c37 guessed the antenna mapping from field symptoms and guessed it BACKWARDS,
so "Internal" was selecting the external socket and the boot park pinned the
external connector — the exact hazard the change was meant to remove. Corrected
against LilyGo's own sources, which document this pin explicitly:
lilygo_device_driver src/device/t_display_p4/t_display_p4_config.h
inline constexpr auto kSky13453Vctl = cpp_bus_driver::Xl95x5::Pin::kIo1;
lilygobox-espidf main/hal/device/t_display_p4/t_display_p4_device.cpp
const uint8_t antenna_level = config.antenna == AntennaType::kExternal ? 0 : 1;
GpioWrite(gpio::xl9535::kSky13453Vctl, antenna_level);
... logged as kExternal ? "RF2" : "RF1"
So HIGH = RF1 = on-board antenna, LOW = RF2 = external socket. INTERNAL_LEVEL
flips false -> true. The one-constant escape hatch written into b066c37 is what
made this a one-line correction.
Two things this settles beyond the polarity. The vendor's AntennaType enum is
literally kInternal/kExternal, so the line IS an antenna select and not a TX/RX
path — the architectural half of the earlier reasoning was right. And LilyGo
preloads VCTL = 1 in their own XL9535 init as their stated safe state, so
booting on the on-board antenna is the vendor's choice too, not just ours.
It also explains the field report properly: the legacy per-TX toggle idled LOW
to receive (= EXTERNAL, where the reporter's antenna is fitted, hence a healthy
+12 dB inbound) and drove HIGH to transmit (= the on-board antenna, hence ~22 dB
down at the repeater). It transmits internal and listens external, which is the
opposite of what a3550d2 claimed. So auto is NOT a PA hazard after all — its
transmit lands on the permanently attached on-board antenna — it is just
guaranteed lopsided. Its confirmation text now says that instead of warning
about damage, and only External keeps the damage warning. UI strings say
"external antenna socket" rather than "MMCX", since the board has two MMCX
connectors and only one of them is the LoRa path.
Verified on hardware (T-Display P4, MAC ...e1:c2:a7): clean boot, only
rst:0x1 (POWERON), antenna park runs, SNTP -> rtc 1785413952.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto was still one tap away with no prompt, which contradicts the safety model
in b066c37: the legacy per-TX toggle writes the external level on EVERY single
transmit, so it keys the PA into the MMCX socket more often than "External"
does, not less. Both non-internal modes now require an explicit confirmation,
with wording that says what each one actually does to the transmit path.
Internal stays promptless (it is the safe state), and the revert-then-reapply
shape is unchanged, so a dismissed dialog still leaves the UI and the hardware
where they were. The pending mode moves into a static because showConfirm takes
a bare callback with no user data.
Verified on the device (T-Display P4, MAC ...e1:c2:a7): clean boot, only
rst:0x1 (POWERON) in the log, "[XL9535] power-on sequence done" so the antenna
park runs, and "[C6-AT] SNTP -> rtc 1785412930" decoding to 2026-07-30
12:02:10 UTC, which also confirms the ClockFloorRTC system-clock mirror from
fe87ff0 firing on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the P4 antenna setting so the failure mode it protects against is
impossible by default. Transmitting into an external connector with nothing
fitted is what destroys a PA, so the state you get for free after any power
cycle, crash or OTA has to be the on-board antenna.
- The on-board antenna is forced at EVERY boot, in two places: the park in
Xl9535::powerOnSequence() (before the radio object even exists) and a
re-assert in UITask::begin() in case anything touched IO1 in between.
- The choice is now session-only and is never written to flash. That is the
whole point: if "external" could survive a reboot, the safety property is
gone. touchPrefsGet/SetP4Antenna are removed; the p4_antenna byte stays in
the struct as reserved so TOUCH_CFG_VER does not have to rewind.
- Selecting External raises a confirmation naming the MMCX socket and the
risk. showConfirm has no cancel hook, so the dropdown reverts immediately
and is only re-selected from the confirm handler — a dismissed dialog
therefore leaves both the UI and the hardware on internal.
- The dropdown reads the live xl9535.antennaMode() rather than a stored pref,
so it can never claim an antenna the hardware is not actually on.
Also renames the modes from the opaque Auto/Pinned A/Pinned B to Internal /
External / Auto (legacy). The reasoning for treating IO1 as an antenna select
rather than a TX/RX path switch is written up on Xl9535.h: a per-transmit
switch must settle within microseconds of the PA ramping, and an I2C expander
write on a bus shared with touch, the RTC and the fuel gauge cannot do that,
while the SX1262 has DIO2 for exactly that job. Which LEVEL is which antenna is
still unconfirmed against LilyGo's schematic, but the SAFE level is known from
the field: the legacy mode sits LOW to receive and every P4 receives fine at
+12 dB, so LOW demonstrably radiates into something connected, while HIGH was
only ever used for transmit and is the suspect. One named constant
(INTERNAL_LEVEL) flips the mapping if the schematic ever says otherwise.
Builds clean on the P4 IDF target plus the V4 and T-Deck S3 envs (the shared
TouchPrefsStore change); compile-verified only, the P4 is off the USB bus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The UI reads the ESP32 *system* clock for every displayed timestamp (status
bar, chat bubbles) while protocol timestamps come from ClockFloorRTC. On a
board with no RTC chip those are the same clock, so they cannot disagree. The
T-Display P4 (and the ThinkNode M9) carry a PCF8563, and
AutoDiscoverRTCClock::setCurrentTime is an if/else chain: with a chip present
it writes ONLY the chip and never reaches the ESP32RTCClock fallback. So the
system clock kept ESP32RTCClock::begin()'s power-on seed forever.
That seed is 1715770351 == exactly ClockFloorRTC::MIN_VALID_EPOCH, which
explains both halves of the P4 field report:
- the UI showed 15 May 2024 while sent messages carried the correct time
- "Sync clock from system" then poisoned the good clock: it fed that seed
back in as a real set and cleared the MIN check by being precisely equal
to it (the old `t < 100000` guard only caught a clock counting up from 0)
Fixed in the one funnel every time source already goes through:
- ClockFloorRTC::setCurrentTime mirrors every ACCEPTED value into the system
clock, so NTP-via-C6, GPS and the phone app all correct the display too.
Validation runs first, so 1902/2043-class garbage never reaches it either.
- new seedSystemClock(), called at boot right after seedFloor(): a
battery-backed chip already knows the time, so the UI reads correctly with
no network and no GPS fix. After seedFloor, so a dead chip yields the
persisted floor rather than the seed.
- the sync button rejects an untouched seed and now reports honestly instead
of always claiming "Clock synced".
No-op on the seven chipless boards: the fallback already wrote the same value
through settimeofday. GPS re-syncs are rate-capped to ~30 min, so the mirror
adds no measurable work. Builds clean on all 8 S3 envs + the P4 IDF target;
compile-verified only, the P4 is still off the USB bus pending a power cycle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Field reports say a T-Display P4 transmits far weaker than it receives: one user's
repeater hears his P4 at -10 dB while the P4 hears the repeater at +12, and his
T-Deck is +12/+12. Decisively, the SAME P4 under other P4 firmware gives symmetric
numbers, so it is not his hardware, and that firmware also offers an
internal/external antenna choice which we do not.
Reading the variant, our whole RF path rests on one never-validated assumption.
XL9535 IO1 is labelled "SKY13453 VCTL (LoRa TX/RX path)" with the comment "polarity
TBD on-device", and we flip it around every transmit. But this expander has NO other
antenna-select line -- all 16 IOs are power rails, screen, touch, ethernet, C6, SD
and the SX1262 reset/DIO1. If IO1 is actually the internal/external ANTENNA select,
then toggling it per transmit means we send on one antenna and listen on the other,
which is exactly a large outbound-only deficit on a unit with an external antenna
fitted. A second user reporting symmetric +11.8/+12.0 does not refute it: that was a
two-floor link where SNR saturates in both directions and would mask the loss.
I will not gamble on that assumption: if IO1 really is a TX/RX switch, pinning it
would break transmit or receive outright. So this adds a Radio setting (P4 only):
Auto (switch per transmit) - the existing behaviour, and the DEFAULT, so an
untouched device is bit-for-bit unchanged
Pinned A / Pinned B - hold the line in one state for both TX and RX
Whichever pinned option gives symmetric Trace SNR both ways is the correct antenna,
and that result also tells us what IO1 actually is. The labels are deliberately A/B
rather than Internal/External because we do not yet know which is which.
Applied live on change and re-applied at boot, next to the V4 FEM-LNA precedent.
Stored as touch-cfg v41, a trailing field defaulting to 0, so existing installs
migrate with no behaviour change.
Builds clean on all 8 S3 envs plus the T-Display P4. Cannot be verified here: no P4
is on USB, and the answer needs an on-air SNR comparison anyway.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two reporters on card-less Heltec V4s saw the whole UI freeze for 5-60 s at a time
with BLE, WiFi and GPS all off and no user activity, and BOTH of a reporter's two
devices froze at the same moment. About -> "Loop stalls" named the culprit
unambiguously: tag `mesh`, with peaks of 11451 / 12175 / 14592 / 18063 / 19113 ms.
That is inside the_mesh.loop(), i.e. the advert receive path, and the cost is SPIFFS
garbage collection on a partition kept permanently churned by writes that happen per
advert. The simultaneity across two units is the giveaway: the trigger is a flooded
advert both of them receive, after which each runs the same deterministic write.
This is the residue of beta_40's "card-less contacts-save stall fix", which capped how
OFTEN the contacts rewrite happens without reducing its cost, and left the
highest-frequency writer untouched.
Two frequency reductions, both keeping writes ATOMIC:
1. putBlobByKey now persists an advert blob once per key per boot. The core calls it on
EVERY advert (outside the auto-add block, so for already-known contacts too) and each
call is a full create+truncate+write. With ~200 known nodes re-advertising that is a
flash write every few seconds, forever, purely to keep the raw advert packet fresh for
the Share-contact feature. The content is effectively static, and getBlobByKey still
reads the file we already wrote, so there is no cache to keep coherent. Steady state
on the packet path becomes zero flash I/O. Costs 2 KB of DRAM.
2. A save triggered by an add/remove now has its own 30 s floor. It previously bypassed
the 5-minute refresh window entirely, so on a growing mesh every newly-heard node
forced its own full 30 KB rewrite (200 contacts x 152 B). A burst of new nodes now
coalesces into one rewrite. Kept short on purpose: a new contact still needs to reach
flash promptly, and losing one to an unclean power cut only costs a rediscovery on that
node's next advert.
Deliberately NOT done: making the contacts save incremental (seek + one 152-byte record).
It is tempting since the file is a flat fixed-stride array, and it would cut churn far
harder, but it abandons the tmp+rename atomicity that beta_46 added to fix real contact
data loss. Both reporting devices show "Last reset: Brownout", so non-atomic in-place
writes on that hardware would trade a stall for corrupted contact records. Frequency
first, atomicity intact.
Also seen in those same screenshots and NOT addressed here: ui:gps stalls of 3077 /
3769 / 3848 ms on a device whose owner says GPS is switched off, and ui:lvgl stalls up
to 2227 ms. Separate issues, worth their own investigation.
Verifiable by the same diagnostic that found it: the `mesh` entries in About -> Loop
stalls should stop appearing.
Builds clean on Heltec V4 TFT and T-Deck.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Partial fix for "no ping and telemetry response" reported from the device AND the
phone app. onContactResponse's deferred-login branch runs before every pending_*
matcher and, while armed, consumed ANY response from that contact and returned. Its
comment justifies this with "we have sent no REQ to this contact, so any RESPONSE
here is the login reply" -- true of the UI, but the companion app can independently
have a STATUS / TELEMETRY / BINARY request in flight to the same node. There is also
no distinct login-failure code on the wire (a failure is just "not
RESP_SERVER_LOGIN_OK"), so a non-OK frame is indistinguishable from the app's reply,
and the app's response was swallowed with the phone left waiting.
A LOGIN_OK is unambiguously ours, so it is still always consumed. Otherwise the
branch now falls through when the app has a request pending on that same contact,
letting its matcher see the frame; the UI's own reply deadline disarms us, which is
all the early disarm here ever provided. Behaviour is unchanged whenever the app is
not waiting, which is the common case.
Deliberately NOT changed: sendStatusPingForUI / sendTelemetryRequestForUI zero the
app's pending_status / pending_telemetry on entry. That looks like deliberate
arbitration so a UI-initiated request routes to the UI rather than the phone, and
removing it could double-handle a response. Wants its own change and a bench test.
Note this is unlikely to be the whole of #124: the leading theory remains the
repeater's per-client replay high-water mark, which no client-side change can undo.
Builds clean on T-Deck and Heltec V4 TFT.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VE2CCK reported losing "a lot of characters" while typing into the web mirror from
a phone, and put it down to his phone's autocomplete not travelling over the link.
It is actually our bug. Browser keystrokes land in a 32-entry SPSC ring pushed from
the network thread, pushKey DISCARDS the key when that ring is full (silently, no
counter, no log), and the UI drained only 16 per loop iteration. A loop iteration
can be tens of ms on a T-Deck (LVGL frame, SD write, radio), and an autocomplete or
a paste delivers a whole word at once, so a burst overflowed 32 easily and the rest
vanished. Typing slowly worked, which is why it read as an autocomplete quirk.
Ring 32 -> 256 (512 bytes; the ceiling while _khead/_ktail remain uint8_t, noted in
the comment so nobody raises it further without widening them), and both drain sites
16 -> 64 so a burst clears in one pass instead of trickling while newer keys push the
tail along. No API or protocol change, and nothing else reads this ring.
This matters more than it looks: he is using the mirror to screen-record how-to
guides for his radio club, so typing fidelity is the feature.
Builds clean on T-Deck and Heltec V4 TFT.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found while fixing #177. A room thread is a CONTACT thread, so it took the DM
branch of ignoreSenderInActiveThread, which blocks the thread contact's pubkey.
For a room that contact is the SERVER, not the person who posted, and
newRoomMsgFromPubWithMeta drops on exactly that pubkey. So blocking one noisy
member silently killed every message from the entire room, with nothing in the UI
explaining why. Room posts carry only an author display name (passed to newMsgImpl
as the sender override), so the author is now blocked BY NAME and the room itself
is left alone. Tapping a sender whose name is the room's own name still falls
through to the pubkey block, so muting a whole room from inside it still works,
and the contacts-list block is untouched.
Second half of the same bug: the name filter in newMsgImpl was gated on `channel`,
which is false for room posts, so a room name-block was stored and then never
enforced. It now also applies when a sender override is present, which is exactly
the room case. A plain DM has neither flag, so it keeps using the pubkey filter and
is deliberately unaffected: a DM contact who happens to share a display name with a
blocked room bot must not disappear.
Builds clean on T-Deck and Heltec V4 TFT.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These three had fixes written back at beta_32 but the batch was parked and never
shipped. The stash no longer applies (twenty betas of drift; ClockFloorRTC.h and
UITask.cpp both conflict), so they are ported by hand onto current main. The five
other fixes in that stash are deliberately NOT ported: #98/#100/#116 already
shipped in beta_48 and #93/#113 are closed, so re-applying them would duplicate
or conflict.
#97 the crash-restart dialog's text ran down over its own Cancel/OK buttons.
showConfirm used a fixed PSC(160) card with a freely-wrapping label, so any long
message overflowed; the crash-report prompt is the worst case. The card height now
follows the wrapped text, the message gets its own box sized to exactly the space
above the buttons (so it physically cannot reach them) and scrolls when the card
hits the screen cap. Keeps the old 160 as a floor, so every short dialog renders
exactly as before and only the broken long-text case changes. Added a clamp the
original port did not have, so the message area cannot invert at Large/Huge UI
scale.
#109 the settings sheet opened from the Discovered screen looked truncated and
could not be scrolled: centring the card and then lifting it a fixed -46 pushed
its top off-screen on the 240px-tall T-Deck. Height is clamped to what the screen
has and the lift to the leftover slack.
#106 room servers were indistinguishable from companion contacts in the contacts
list (same person icon). Rooms now get their own glyph.
Not ported: #112's RX-activity dot. The logic is trivial but the status bar has
since grown a second placement pass for the tall two-row layout (P4 /
round-corners) that the stash predates, so the dot would strand itself at its
single-row position there, and its fixed offset collides with the BLE icon's
hardcoded narrow-screen position. Needs a device to eyeball, not a blind port.
Verified: all 8 S3 envs plus the T-Display P4 (which exercises the
CAP_LARGE_SCREEN branch of the reworked dialog) build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three community-reported bugs, two of which turned out to share one cause.
#177 blocking a channel sender did nothing. Group packets carry no per-sender
pubkey (MyMesh's channel branch passes from_pub = nullptr), so the prefix ignore
list is never consulted for channel traffic and the only live filter is the name
one. But the block path stored a PUBKEY whenever the sender happened to be a saved
contact, so the block was recorded, shown in the blocked list, and filtered
nothing. Channel blocks now always write the name entry; a saved contact also
keeps its pubkey entry so DMs stay blocked as before. Unblock is now paired, so
clearing one entry clears its twin instead of leaving the sender half-blocked.
#184 lost notification sound until a restart. s_tile_fetch_pending gates all four
chime paths and is only ever ++/--, never reset, so one leaked increment mutes the
device for the rest of the boot. queueTileForFetch ignored
ensureTileFetchTaskRunning()'s result, and that function creates the QUEUE before
it can still fail on the worker's 8 KB contiguous-DRAM stack: queue alive, worker
dead, every send incremented a counter nothing could drain. Honor the result.
Not a beta_50 regression, the sound path was untouched in that release.
#171 Wi-Fi never reconnecting after a reboot. Same worker. Opening the Wi-Fi page
drops the link and raises the scan gate, which is lowered only when the tile
worker reports s_wifiscan_done. If that worker never starts, the gate latches:
autoreconnect stays off and main.cpp suppresses its reconnect retry, so Wi-Fi is
dead until a reboot. Added a 30 s recovery deadline, far longer than any real
sweep, so it can only fire when the normal completion path is genuinely lost.
Also corrected the stale comment that described the signal probe as a flood. It
is a zero-hop NODE_DISCOVER_REQ that repeaters answer directly and never
re-broadcast, and it is skipped entirely when a direct neighbour was heard in the
poll window. That wording is where issue #80's "spams the mesh" concern came from.
Verified: all 8 S3 envs plus Tanmatsu build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lands @Vybo's storage-stack rework from #179 — runtime SD wedge/remount recovery,
chat-store health diagnostics, and a segmented, append-friendly message store that
replaces the whole-ring file rewrite (crash-atomic migration, per-segment
corruption quarantine) — plus a fix for the segment-table-cap latch that made SD
I/O fail after a few hours of uptime: at the cap, segBuildJob emitted a create-append
segCommitJob could not table, freezing the durability watermark into a permanent
write-spin that leaked orphan segment files until the card filled.
Full credit to Dan Vybiral (@Vybo) for the design and implementation. The cap-latch
fix is the only change on top of his branch. Landed directly (his fork branch could
not carry the fix), so #179 is closed rather than merged.
Co-Authored-By: Dan Vybiral <dan.vybiral@greencode.cz>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the reported "SD writes fail after a few hours of uptime": when the
segment table is full (s_seg_count == k_ui_seg_max), segBuildJob still emits a
CREATE-append, but segCommitJob only tables a create while s_seg_count < the cap.
The new segment file is written but never recorded, the durability watermark
(s_seg_flushed_seq) never advances, so the same append re-arms every flush cycle
(permanent write-spin). As the ring evicts the frozen records the target first_seq
shifts, so a new orphan seg_*.bin is written each cycle until the card fills and
every SD write fails; records evicted before they are credited are lost.
It surfaces only after hours because s_seg_count creeps from its ~20 steady state
(5000/256) up to the 24 cap slowly, via delete-driven fragmentation and
step-1-append starving step-2 retirement.
Fix: at the cap, defer the create-append instead of emitting an untableable one.
segBuildJob skips step 1 (before the gather/watermark advance, so the watermark is
never moved past un-written records) and falls through to step 2, which retires the
oldest segment first. At the cap the oldest 256 records have aged out of the ring
(24*256 > 5000), so that segment is fully evicted -> compact_dirty -> unlinked,
freeing a slot with no live-record loss; the deferred records are the newest (above
the watermark, evicted last) and land next cycle. saveMsgsToStorage's early
"armed == 0 -> durable" return is guarded with segMoreWorkPending so a deferred
append is reported as a failure, not a false success. Below the cap: no-op.
Builds green on T-Deck + Heltec V4 TFT. Needs a multi-hour on-device soak to
confirm against the original repro.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
beta_49 hard-pinned s_ui_fscale=100 on the P4 to stop >100% chrome overflow, but
the "UI size" dropdown stays shown (CAP_LARGE_SCREEN), so changing it did nothing
(reported: "changing text size doesn't work on the P4"). Honour the saved pref like
the Tanmatsu does. Large/Huge can still make some P4 chrome that uses unscaled dims
tight; that's a per-screen SC() follow-up, not a reason to disable scaling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a reinsert the chat store re-landed but the map stayed blank: the
tile layer was never told the storage changed. It keeps decoded tiles in
s_map_tiles across renders and only renders on pan / zoom / tab-open, so
a card that came back produced no reload; a boot with no card never
adopted a later-inserted one as the tile cache at all.
mapNoteStorageChanged() now runs from every mount-state transition
(wedge-recovery remount, 30 s reinsert watch, file-manager insert poll,
post-format, and both card-lost paths), right next to the existing
flushHistorySoon(). It adopts the card as the tile backend when this
boot resolved none, drops the decoded tiles (a swapped card holds
different content), clears the fetch dedup ring so downloads that failed
during the outage are retried instead of being remembered as in-flight
forever, and re-renders immediately when the map is the visible tab.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
The home chip now carries the clock time of the last successful save
(HH:MM, 12/24h-aware, with a '-<N>D' suffix once it is a day or more
old) instead of the segment count and byte total, and the About page's
'last save' line switches from 'OK 12s ago' / 'FAIL xN (12s ago)' to the
same timestamp form. A clock time answers 'is my history safe right
now?' directly, and the day suffix stops a store that quietly stopped
saving yesterday from reading as fresh. Segment count, byte total and
the failure stage + errno stay on the About panel.
Backed by new wall-clock stamps taken alongside the existing millis
ones in uiMsgsWriteResult; they read 0 (rendered '--:--') when the
system clock wasn't set at save time, since no meaningful timestamp
exists then.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
A second adversarial review pass (11 confirmed findings, 2 critical) hit
the crash and failure paths the on-device happy path never touches. Six
root causes, fixed at the root rather than per-symptom:
1. Migration was not crash-atomic (CRITICAL). Chunks landed one rename
at a time, so a power cut mid-migration left a valid oldest-first
PREFIX that the loader preferred over the still-intact old file --
permanently showing only the oldest few hundred messages. There is
now a commit marker (/msgs/store.ok) written only after full
verification: its absence means the segment set is provisional, and
the loader wipes those segments and re-reads the old file. The 60 s
migration retry on a sick card therefore no longer risks history.
2. Migration assumed a linear ring (CRITICAL) but its retry paths run
mid-session, after appendMessage may have wrapped it -- producing
segments in slot order (first_seq > last_seq, scrambled history that
per-segment read-back could not detect) before deleting the old
file. It now walks the ring chronologically, skips tombstones (which
it used to persist as ghost records), and verification additionally
requires strictly increasing seqs within and across segments.
3. A partially-failed append was retried by appending the same batch
again, duplicating records (or misaligning every later record if the
failure split one). Segments now carry a rewrite_open flag meaning
"file content untrusted": the scheduler REPAIRS such a segment with
a full rewrite (header + records, tmp+rename) before any append can
target it, and the repair absorbs the unflushed tail.
4. Post-resync appends targeted deleted files (headerless segments) and
could push a chunk's range past the 256-record gather cap, silently
dropping records the watermark already called durable. Fixed by the
same repair-first rule: retable marks every chunk rewrite_open.
5. Resync deleted every on-disk segment up front, so a same-card wedge
recovery left the whole history RAM-only for minutes. It no longer
deletes anything: same-key files are replaced by each repair's
rename, and leftovers are swept only once the re-land is complete.
6. A stale worker descriptor could be committed twice after
persistHistoryNow drained past a busy worker (double-counted totals,
duplicate table entries), and a leaked redirty flag could make the
drain re-compact one segment ~28 times and then report failure.
uiHistWaitWorkerIdle now always drops the descriptor and the flag,
marking the abandoned job's segment for repair instead. The drain
also refuses to run at all while a worker is still writing -- with
FF_FS_LOCK=0, removing or renaming a path under a stalled open
handle can cross-link clusters and corrupt the volume.
Also: the loader applies a strictly-monotonic seq filter, so duplicate
or out-of-order records from any interrupted write are dropped at load
and the affected segment is rewritten; append commits credit their own
segment by key instead of blindly crediting the last table entry.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Adds a compact store-status chip sharing the TX/RX legend row above the
home chart, parked on the right half of the chart width (fixed width +
right-aligned + CLIP, so it can never grow into the traffic counts at
any UI scale). Small font (g_font_12), not clickable — the legend still
owns this row's taps.
Shows a floppy glyph plus, in order of precedence: 'FAIL xN' in red
while saves are failing, 'migrating' in amber while an old-format store
hasn't converted yet, else the segment count and total bytes ('9s 512K'
/ '21s 1.1M') in the normal subdued colour. Refreshed independently of
the TX/RX chart so the big-screen scaled layout (which drops the chart)
still shows it, and re-flowed with the legend on Expansion-Kit boards.
The full diagnosis (backend, failure stage + errno) stays on the About
page's Chat store panel.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
An adversarial review pass over the refactor surfaced these; the first
is a silent-permanent-loss bug:
- CRITICAL: the finished-job check ran AFTER the failure branch had
already reset s_hist_flush_ok, so a FAILED worker job read as ok and
got committed — advancing the durability watermark over records that
never reached disk. The completion check now runs strictly first, and
a failed job just drops its descriptor (data stays pending).
- A message deleted while its APPEND job was in flight resurrected on
reboot: segMarkSeqDirty skipped it (seq above the watermark = 'never
reaches disk') but the in-flight job then put it on disk. After a
committed append the ring's tombstones inside the newly-flushed range
now get their compaction marks.
- segCommitJob cleared compact_dirty unconditionally, erasing marks for
deletes that landed WHILE the compact job was writing (its snapshot
predates them). A redirty flag keeps the segment dirty for a
follow-up pass.
- The sync drain shared the worker's job descriptor, snapshot buffer
and req flag: a worker stalled past the 9 s idle-wait cap could read
a torn buffer, and the raised req let the worker steal and
double-execute a job the drain was running inline. segBuildJob is now
pure (fills a caller buffer + local SegJob); only the async path arms
the worker statics, and the drain owns a separate buffer.
- An in-session FAT32 format erased all segments while the table (and
watermark) still claimed them — old records permanently lost and the
next append targeted a missing file. The format path now runs the
same resync as a card swap (retable + re-land off-thread).
- uiSegAppendRecords with create=true used FILE_APPEND, so a stale
same-key file (crash residue) got a second header mid-file; create
now opens truncating.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
- About 'Chat store' shows the segment count + writer-maintained byte
total (zero file I/O at 1 Hz — the old size probe blocked on the FatFs
volume lock behind a sick write) and flags a pending migration.
- uiDataEnsureDirs() runs at boot (before the loader scans) and on all
three SD remount paths, so a fresh replacement card gets the segment
dir immediately.
- sdEnsureMeshcomodFolders (post-FAT32-format) now also recreates
/meshcomod and /meshcomod/msgs — an in-session format used to break
every chat/telemetry write with ENOENT until a reboot.
- The boot SPIFFS->SD adoption creates /meshcomod/msgs so flat-named
SPIFFS segments ('/msgs/seg_*.bin') copy onto the FAT card instead of
being silently skipped (missing parent dir).
- sdRestoreApply ('Copy internal data to SD') persists chat history
BEFORE the copy — it used to reboot without persisting, silently
dropping everything since the last lazy flush.
- Dead s_ui_data_resolved removed; stale writer comments updated.
Builds verified: all five SD-relevant touch envs.
Per .notes/segmented-store-plan.md phase 5.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
The flush pipeline now moves one bounded JOB at a time instead of the
whole ring: an append batch into the active segment (a few hundred
bytes per message burst) or a one-segment compaction rewritten from the
RAM ring. The loop task builds and COMMITS jobs (single table writer);
the core-1 hist_flush worker only executes the armed job and reports —
same busy/req protocol, same failure ladder (retry backoff, warning
toast, hist_sync_after sync-fallback), all ported verbatim.
- Durability watermark (s_seg_flushed_seq): ring records above it are
the pending-append backlog. Records deleted before ever flushing
advance it with no write. Job snapshot buffer is one segment (~80 KB
PSRAM) replacing the whole-ring 1.3 MB snapshot.
- markMsgsDirty clamps drop from 30 s/10 s (whole-ring-rewrite pacing)
to 2 s SD / 5 s SPIFFS — the hard-cut loss window shrinks ~15x.
- Deletes (message/thread/purge/web) mark owning segments compact-dirty;
eviction unlinks a segment only when ALL its records aged out (no
per-message rewrite churn; a partially-evicted boundary segment is
trimmed by the next boot's loader).
- saveMsgsToStorage is now the synchronous drain (shutdown/reboot/
delete flows/sync-fallback): it retries a failed migration, honors
resync, and never uses FILE_APPEND (a worker stalled past the 9 s
idle wait could hold an append handle on the same file — the active
segment is rewritten via the .tm2 namespace instead, rename-over
wins with each candidate internally consistent).
- flushHistorySoon (remount paths) now sets s_seg_resync: the segment
table is rebuilt from the ring, stale/foreign on-disk segments are
removed, and the full history re-lands one segment at a time
off-thread — replacing the old 'full-ring write fixes everything'
property without the >30 s UI freeze.
- Retired: uiWriteMsgsFile, uiHistWorkerFlush, the ring-sized snapshot.
Builds verified: all five SD-relevant touch envs.
Per .notes/segmented-store-plan.md phase 4.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
loadHistoryFromStorage now prefers segments over the split v6 file over
the legacy combined file. The segment loader reads oldest-first into the
ring, quarantines corrupt segments INDIVIDUALLY (one bad record used to
discard the entire history), truncates crash-ragged tails at the record
boundary, keeps the newest <= cap records when the disk set outgrows
this boot's ring (three-reversal rotation restores the linear-ring
invariant), reconciles the segment table (fully-dropped segments deleted,
the boundary segment marked compact-dirty), restores real seqs and seeds
the generator, and sweeps orphaned segment tmps.
When an old format supplied the ring, migrateRingToSegments() converts
it: segments written from the ring, every one read back and verified
(count + seq bounds) before the old msgs file is deleted; any failure
rolls the whole segment set back so the intact old file stays
authoritative next boot, and s_seg_store_ready stays false so the flush
scheduler (phase 4) retries the migration instead of writing segments
that would shadow it.
The companion message counter rides the threads-file header now (the
segments don't store it); the mesh re-asserts it per message anyway.
Builds all five envs (loader wired; writers wire in phase 4).
Per .notes/segmented-store-plan.md phase 3.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
The file-layer building blocks for the segmented chat store, no callers
yet: UiSegHeader/UiSegMsg on-disk format (v6 record + appended seq,
self-describing rec_size), segment naming (/msgs/seg_<first_seq>.bin,
real subdir on FAT backends, legal flat name on SPIFFS), uiDataEnsureDirs,
chunked internal-RAM record writer, active-segment append (header-first
on create, retry-once after ensuring dirs, ragged-tail crash semantics),
one-segment compact (tmp+rename with the .tmp/.tm2 dual-namespace
discipline, n==0 removes the file), retired-segment delete, validated
open + size-agnostic record reader (File::size()-free — the Tanmatsu FFat
metadata layer lies), and segment discovery (dir listing on FAT, flat
prefix scan on SPIFFS, orphaned-tmp sweep, sorted ascending).
All ops report through uiMsgsWriteResult/uiMsgsWriteFail so the About
diagnostics, failure toasts, sync-fallback and SD-wedge arbitration work
unchanged; new stage codes a/c/d/s join chatSaveFailText.
Builds all five envs (expected unused-function warnings until the
scheduler wires these in).
Per .notes/segmented-store-plan.md phase 2.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Every ring record gets a monotonic uint32 seq at append time, and the
loader backfills seqs in chronological order for records loaded from the
pre-segment formats (which carry none on disk), seeding the generator
past them. seq is the segmented store's record key: segment membership
is a [first_seq, last_seq] range, stable across per-segment compaction.
Deliberately a dedicated counter (_ui_seq_next), NOT _msgcount: the
companion protocol can overwrite _msgcount (msgRead), so it cannot be a
unique key.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Extend the channel top-row timestamp layout to every bubble-style
thread so DMs and room messages match channels.
Co-authored-by: Cursor <cursoragent@cursor.com>
EIO persisted on the core-1 flush task, which narrows the failure to
shared-SPI interleave rather than core-0 starvation alone: the one
always-working configuration is the LOOP TASK writing (reboot persist)
-- when it writes, nothing else interleaves radio traffic between the
SD transactions on the shared bus. (The pager port notes flagged
exactly this: 'keep an eye on SD-write + RX overlap during history
flush'.)
The flush now self-heals: after N consecutive failed background writes
it retries synchronously on the loop task (one UI hitch per flush while
degraded), and a single success flips it back to the async task. N is a
new setting (Settings -> General -> 'Chat save fallback', dropdown
Off/1/2/3/5/8, default 2; Off = background retries only). Stored as
TouchCfg v40 trailing field hist_sync_after with getter/setter.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Make the shorter channel-bubble layout optional via a persisted
"Timestamp on top" switch (default ON), so classic footer layout remains available.
Co-authored-by: Cursor <cursoragent@cursor.com>
s_lock_wall_dsc is a static lv_img_dsc_t (stable address). LVGL's image
cache is keyed by that src pointer, not by dsc->data, so re-decoding a
custom lockscreen wallpaper into a fresh PSRAM buffer without first
invalidating the cache entry can leave LVGL drawing from a stale,
already-freed pointer on the next lockscreenShow(). The freed PSRAM is
commonly reclaimed soon after by the map's 128 KB tile buffers, so the
stale draw shows up as colorful RGB565 noise instead of the wallpaper --
exactly the symptom in #127. Only reboot 'fixed' it because the stale
LVGL cache entry otherwise persists indefinitely.
Same bug class already fixed for map tiles in freeMapTileSlot() (see
its comment); this applies the same lv_img_cache_invalidate_src() call
to the two places that free s_lock_wall: lockscreenShow() (re-decode)
and lockscreenHide() (teardown).
Fixes#127
Two failure signatures from on-device testing, both now explained and
closed:
- EIO ('card I/O error') on the periodic autosave while the identical
full-ring write succeeded from the reboot path: the flush rode the
core-0 tile_fetch worker -- the Wi-Fi core -- where the driver's
prio-23 tasks starve a prio-1 task for hundreds of ms. sd_diskio's
busy-waits measure WALL time, so timeouts expired while the task
simply wasn't scheduled, and healthy writes surfaced as EIO
(intermittently: the write sometimes won the timing, matching the
'succeeded after a few auto tries' observation). The flush now runs
on its own small task pinned to core 1 at the loop task's priority --
equal-priority time-slicing keeps the UI serviced during a write, and
chat persistence has no Wi-Fi affinity to lose.
- EBADF ('bad file number') after a shrink: FatFs FR_INVALID_OBJECT --
a handle whose volume was remounted underneath it. The file-manager
removal poll could probe + SD.end() while the flush held its open
temp file. All unmount paths now hold off under an in-flight flush
(the FM poll joins the sdHealthTick guard).
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Replaces the '@o e23' shorthand with a human-readable diagnosis
(chatSaveFailText): stage (open/header/write/rename) plus a plain-text
errno ('too many open files', 'card full', 'card I/O error', ...,
strerror fallback), e.g. 'open failed: too many open files (e23)'.
Shown as a second line on the About Chat-store section, on the repeated
autosave-failure alert, and on the reboot-time last-chance-save alert.
Diagnostic vocabulary stays English on purpose -- it is what ends up in
bug reports verbatim.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Field evidence reframed the chat-save failures: a soft reboot wrote the
FULL 515 KB ring to the same card without trouble, while the periodic
worker autosave could not even create the file. That rules out a
size/bad-region card problem and points at contention: the SD VFS was
mounted with max_files=3 (set when the card only served the file
manager), but the card now serves telemetry log rewrites (2 handles),
battery log rewrites (2), the history flush, tile reads/writes, WAV
chimes and the About readout concurrently -- the flush's open("w") loses
that race with ENFILE while the card itself is fine, and at reboot
(everything quiesced) the same write sails through. All four mount
sites now use max_files=6 (~+1.7 KB while mounted).
To make the next such failure self-explaining on a device with no
readable serial, the About Chat-store line now includes the failure
stage and errno ('FAIL x3 @o e23'): o/h/b/r = open/header/body/rename;
errno 23/24 = VFS file table full, 28 = card full, 5 = card I/O error.
The header-write failure path also joins the failure accounting (it
returned early without counting before) and cleans up its temp file.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Deleting messages tombstones them in place (thread[0]='\0'; the ring
cannot memmove — slot indexes are live UI handles), but the writer
emitted tombstones as full blank records: delete 1000+ messages and the
file never shrank, and every flush kept rewriting the high-water size.
The writer now skips tombstones (the file compacts to live records on
the next flush), and the loader accepts a legitimately empty header-only
file instead of quarantining it as corrupt.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Field trace with the new Chat-store diagnostics (FAILING x1, then an
escalating UI freeze on every ~30 s retry) exposed three stall sources
around a card that mounts fine but times out on large writes:
- The About page's Chat-store readout opened the msgs file at 1 Hz on
the UI task; that open blocks on the FatFs volume lock behind the
worker's slow failing write, so just WATCHING the diagnostics froze
the UI. The file size is now cached, refreshed at most every 30 s and
never while a flush is in flight.
- The persist-after-remount ran the full ring write synchronously on
the UI task -- >30 s freeze on a write-sick card. Replaced with
flushHistorySoon(): arms an immediate flush without the min-delay
clamp, actual write stays on the core-0 worker.
- Retries against a hopelessly failing store ran every 30 s forever,
each costing seconds of bus stalls. After 5 consecutive failures the
retry backs off to 5 min; the RAM ring keeps everything meanwhile,
the on-screen warning already fired, and one success resets the
counter.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Digging into 'messages still lost after a soft reboot': the loader is
sound (well-formed files load fully; a quarantine discards the whole
load, never a suffix) and the chat detail applies no time filter — so a
conversation missing only its recent messages means the ui_msgs file
itself is stale. Two ways that happens silently today:
- Persistent ring-write failures. uiWriteMsgsFile failing (full card,
FAT damage from hard cuts/card yanks, wedge) was 100% invisible: the
small threads file keeps succeeding, so the thread list shows fresh
last-message times while messages quietly stop persisting — exactly
the reported UX. Now every ring-write outcome is recorded
(uiMsgsWriteResult), repeated failures raise an on-screen warning, a
failure on the SD store stamps the SD health probe, and the About page
gets a 'Chat store' section showing the resolved backend (also exposes
the boot-with-unmountable-card trap where history silently goes to
internal flash), the msgs file size, and the last save result.
- The reboot-time persist had a hole and a corruption hazard. A worker
still busy past uiHistWaitWorkerIdle's 9 s cap left _msgs_dirty
cleared, so rebootDevice skipped the final sync save entirely; and
when the sync save DID overlap a stalled worker write, both truncated
the SAME temp file — interleaved garbage, and whichever rename landed
last installed a corrupt file the next boot quarantines (total
history loss). A still-busy worker now counts as unwritten data, the
synchronous writer uses its own temp (last-rename-wins, each candidate
internally consistent), a failed last-chance save shows a warning
before the reset, and boot reclaims orphaned temps.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Message-loss analysis from testing: messages live in a RAM ring and the
SD file is written lazily (>=30 s coalesce for the SD-sized ring), with a
synchronous persist only on SOFT reboot/shutdown. A hard power cut
(power switch, reset button, reflash) therefore loses everything since
the last SUCCESSFUL flush -- and while the card is out, flushes fail for
the whole outage, so the loss window grows to the entire card-out span
plus up to 30 s after remount. This also explains the confusing UX where
the thread list still shows the correct last-message time while the
conversation lacks the recent messages: thread metadata
(ui_threads_v1.bin, ~1.5 s coalesce) and the message ring
(ui_msgs_v1.bin, >=30 s) are separate files with different freshness.
All three remount paths (wedge recovery, reinsert watch, FM insert poll)
now call persistHistoryNow() right after a successful mount, so the
entire RAM ring -- including every message received while the card was
out -- lands on the card within the same second it returns. They also
SD.mkdir(/meshcomod) first: a fresh replacement card has no data root,
and without it every history flush failed silently forever.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
On-device test: after a failed remount ('SD card lost', SD.end() run) a
REINSERTED card looked recovered -- battery stats quietly fall back to
SPIFFS -- but chat history kept writing into the unregistered SD VFS and
silently failing: new messages were never persisted again until reboot.
Nothing outside the file manager ever re-attempted a mount, and
sdHealthTick early-returned while unmounted.
- sdHealthTick now runs a reinsert watch while unmounted: one light
single-attempt SD.begin (no ladder; an empty slot fails fast) every
30 s while awake. A healthy warm card mounts on the first 4 MHz
attempt -> 'SD card remounted', and the armed history flush retries
then land (messages received while the card was out are still in the
RAM ring and flush too). The FM poll / SD-row tap keep the full ladder
for stubborn cards; the watch skips while the FM is open or the pager
card-detect says the slot is empty.
- Hot paths no longer walk the mount ladder at all: map tile lookups
(loadTileJpeg / tileExistsAt) check s_sd_mounted only -- with the card
out, the post-backoff re-ladder read as recurring multi-second UI
freezes on the map. The notify-task WAV open also drops its
fmSdTryMount(): laddering SD.end()/SD.begin() from a second task races
the loop task's SD use. Mounting is owned by boot adoption, the
reinsert watch, the FM paths, and the SD-tiles toggle.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Adds a self-contained "Airtime" app that measures how busy the LoRa
channel actually is, so lost-message reports can be checked against real
data instead of assumed congestion.
It separates the two things that get conflated:
- Is the spectrum busy? channel utilization % = (RX airtime + TX
airtime) / wall-clock, sampled at 1 Hz, plotted over time, with a
session average and a congestion-threshold reference line.
- Are packets actually being lost at the radio? CRC errors
(collisions), packets heard but read too late (getRxEvents -
packetsRecv - recvErrors), and RX-queue drops.
A plain verdict combines both axes so a field tester can report
a concrete conclusion. Reads existing counters via the_mesh (Dispatcher
airtime/packet accumulators) and radio_driver (RadioLib wrapper stats);
no MeshCore core changes.
The 30 s background liveness probe now skips while touchSleep has the
device parked (screen off, loop throttled): nobody is watching for the
toast, and the periodic SEND_STATUS would keep the card from settling
into its own power-down. A card yanked while asleep is caught by the
first background probe after wake. The failure-driven probe stays active
during sleep on purpose -- scheduled telemetry keeps logging to SD while
parked, and a wedge there must still recover.
Builds verified: all five SD-relevant touch envs.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
On-device testing of the previous commit showed three gaps:
- No 'SD card lost' when the card is pulled while nothing writes:
detection was purely failure-driven, and the battery path never stamps
(battLogFs's SD.exists fails -> silently falls back to SPIFFS, which is
exactly the 'battery stats fail to load' symptom). sdHealthTick now
also runs a 30 s background probe (one sub-ms SEND_STATUS on a healthy
card), so idle removal is noticed and toasted; battLogFs stamps when
cardType() claims present but real I/O fails.
- Map froze the whole UI when the card was pulled with the map open:
every tile lookup ran up to five SD ops, each riding a full sd_diskio
timeout + re-init cycle, serializing the render pass into a
multi-minute freeze. Tile paths (loadTileJpeg / tileExistsAt / the
SD-backed tile cache) now short-circuit while a dead card is suspected,
and sdReadFailedCardDead() distinguishes a missing tile (healthy card:
cheap, silent) from a dead card (stamp + bail) on a failed read.
- Even after an unmount, hot paths calling fmSdTryMount() directly would
re-walk the multi-second mount ladder per call. fmSdTryMount() now
fast-fails inside the post-failure backoff window; the explicit user
retry (tap the SD row in the file manager) clears the backoff first.
Also renames sdNoteWriteFailure -> sdNoteIoFailure (it now covers reads).
Builds verified: LilyGo_TDeck_companion_radio_touch,
heltec_v4_tft_companion_radio_usb_tcp_touch,
heltec_v4_r8_tft_companion_radio_usb_tcp_touch,
tlora_pager_sx1262_companion_radio_touch,
ThinkNode_M9_companion_radio_touch.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
A runtime SD failure is a one-way door in arduino-esp32: one timed-out
command latches STA_NOINIT in sd_diskio (and a data-phase error leaves
every later op failing FR_DISK_ERR without even setting NOINIT, so FatFs
never re-inits the card). Meanwhile SD.cardType() returns the type CACHED
at mount -- only an explicit SD.end() ever makes it CARD_NONE again -- so
every 'cardType() != CARD_NONE' gate keeps passing, s_sd_mounted stays
true, fmSdTryMount() no-ops, and the file-manager removal poll (which
compared against CARD_NONE) could literally never fire. Net effect: after
one bad moment on the bus (heavy clustered writes -- scheduled telemetry
log rewrites, battery log, core-0 history flush / tile writes -- on the
shared rail) the card silently plays dead until a reboot power-cycles it.
Reported as: SD behaves as not present until the next reboot.
Fix, three parts:
- sdNoteWriteFailure(): any writer on any task (loop, core-0 worker)
stamps a volatile flag when an SD write open fails while the card is
supposedly mounted (telemetry log, battery log, wardrive CSV, chat
history/threads store, tile cache).
- sdHealthTick() in UITask:➿ verifies the stamp with real I/O --
sdProbeAlive() opens the root dir, whose opendir runs f_opendir ->
disk_status = a real SEND_STATUS transaction (and one re-init handshake
on a NOINIT'd card, so a recoverable card self-heals right there). If
the card stopped answering, drop the mount and re-run the full mount
ladder: exactly what the next reboot would have done, minus the reboot.
Success => 'SD card remounted' toast and every feature resumes
transparently (same VFS mountpoint, cached fs pointers stay valid).
Failure => honest 'SD card lost' with cardType() now reading CARD_NONE,
so features degrade gracefully and the existing remount paths (FM poll,
row tap, feature use) keep retrying with the usual backoff. Held off
while the core-0 worker owns the card (hist flush / FAT scan / firmware
download) and while a format is pending; probes rate-limited to 1/5 s.
- The FM removal poll now probes with real I/O instead of the dead
'SD.cardType() == CARD_NONE' check, so physically yanking the card is
finally detected too.
Builds verified: LilyGo_TDeck_companion_radio_touch,
heltec_v4_tft_companion_radio_usb_tcp_touch,
heltec_v4_r8_tft_companion_radio_usb_tcp_touch,
tlora_pager_sx1262_companion_radio_touch,
ThinkNode_M9_companion_radio_touch.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
OTA_BIN_NAME only branched T-Deck vs everything-else -> wadamesh-heltec-v4-tft, so
V4-R8, ThinkNode M9, RAK Tap V2, both T-LoRa Pagers and Attaky would self-update by
downloading the V4-TFT bin -- flashing the wrong firmware onto the wrong board.
All are dual-slot OTA-capable (CAP_OTA=1 + app0/app1 partitions + FIRMWARE_OTA_ENV),
so map each to its own release bin. Pager splits on USE_LR1121. Tanmatsu stays out
(AppFS/launcher, CAP_OTA=0, never reaches this file).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The update-channel UI (the "Get test builds (beta)" toggle + the Install-update
button) is gated on a non-empty FIRMWARE_OTA_ENV. That is set per-env in
platformio.ini for the 8 S3 boards but was never set for the P4 IDF build, so the
whole update UI was hidden — P4 testers had no way to opt into beta and stayed
stuck reading the stable channel (the beta_49 "no update, 48 up to date" report).
- tdisplay_p4 CMakeLists: define FIRMWARE_OTA_ENV so the UI is shown.
- UITask.cpp: add a P4 branch to OTA_BIN_NAME so the download URL points at
wadamesh-tdisplay-p4[-lcd].bin, not the V4-TFT bin. AMOLED vs LCD split on
HAS_TDP4_LCD.
The C6 WiFi shim already routes HTTPClient/Update.h and the P4 partition table has
dual ota_0/ota_1 slots, so the OTA path itself was wired; this only unhides and
correctly targets it. Builds green on P4 (AMOLED) + V4-TFT. On-device toggle/OTA
test still pending (P4 dropped off USB, needs a power cycle).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Backspace tap now peeks the lock screen (reveal without unlocking),
mirroring the T-Deck's trackball-click behavior; holding Backspace still
fully unlocks.
- New native 480x222 lock wallpaper asset so the icon/wordmark aren't
crushed by the shared 320x240 art's cover-fill on this wide/short panel.
- Lock text forced to pure white and repositioned for this layout: clock
top-left over the icon, unread count + "Screen locked" stacked
bottom-right directly above the unlock hint.
- Fixed lock text flipping from white to black a moment after every reveal:
the lock overlay is CLICKABLE (to absorb taps), so the keyboard/encoder
nav rebuild was collecting it as a focus target -- navFocusCb's focus
highlight then reverse-videoed it, flipping every label's text to dark.
Tagged the overlay NAV_SKIP so it's never a nav target and never inverted.
- serviceLockscreen() now runs in the pager's loop branch too, so the lock
clock/unread badge keep refreshing after the first reveal instead of
freezing there.
All changes gated to TLORA_PAGER; no other board's behavior changes.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
P4-focused beta. Non-P4 boards only pick up minor shared exit-trap code, nothing user-facing.
- Snake + full-screen apps (Monitor/Spectrum/Discover/Web mirror/Advert) exit on status-bar tap
- Screen timeout / standby / burn-in dimming wired for the AMOLED (setBrightness path)
- UI scale pinned to 100% on the P4 (text overflow above 100%)
- No hard-lock on screen-off (P4 has no lock screen yet)
- GPS: L76K detect at 9600 baud, skip boot-detect race, 4096 RX buffer
- Control center dropdown taller on tall screens so the GPS line clears the toggle buttons
- P4 LCD SKU: HI8561 backlight PWM on GPIO51 (fixes the black screen)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pager folds into Mesh America's existing 'LilyGo T-Lora Pager' tile
(exact name match). Generator now supports per-radio 'variants' — emits one
gui firmware entry per build under a single tile, [LR1121]/[SX1262] title
prefix matching their stock pager labelling. Stable (beta_46) bins verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the homepage device picker into collapsible per-manufacturer boxes
(LilyGo / Heltec / Elecrow / RAKwireless / Nicolai Electronics / Attaky) with
a live search that filters by device name and manufacturer and auto-expands
matching groups. Add the T-Display P4 TFT-LCD (HI8561) card, wired to a
client-built ESP32-P4 manifest + the immutable per-tag BETA archive bin (no
feed change needed), and relabel the existing P4 card (AMOLED). All existing
install-button + download + channel-toggle wiring preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New Discover app: active NODE_DISCOVER sweep -> signal-ranked nearby list, tap-to-add-contact, GPS wardriving log (SD CSV) + signal-coloured coverage overlay on the map. Board-agnostic.
- T-Display P4 TFT-LCD (HI8561) variant support (WADA_P4_LCD build; AMOLED bin untouched).
- Wire the Attaky Core board (#158, @attakygit) into the release matrix + web flasher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* attaky_mesh_series: add board variant with FT6636 touch and landscape UI
* attaky_mesh_series: P3 battery (MAX17048) + GPS + shared-I2C mutex
- AttakySharedI2C: one Wire.begin() + a mutex serializing the shared board
I2C bus (SDA=2/SCL=1) across the FT6636 touch poll (core 0), the MAX17048
fuel gauge (core 1), and the future AW9523 keyboard. Arduino's TwoWire
buffers are not concurrency-safe.
- board.begin() owns the authoritative bus init; the touch driver now takes
the lock around every transaction instead of its own Wire.begin().
- getBattMilliVolts(): real MAX17048 VCELL read (78.125 uV/LSB -> mV = raw*5/64),
2 s cache + lock, replacing the 4000 mV stub. Upstream maps mV -> % itself.
Hardware-verified ~4058 mV.
- GPS: rename the dead GPS_BAUD flag to GPS_BAUD_RATE (the core reads that; 9600
only worked via the #else default). Pins verified on hardware: ESP RX = IO17
receives ATGM336H NMEA; MeshCore's module-centric PIN_GPS_TX=17 maps correctly.
* attaky_mesh_series: Control_Keyboard_(Bottom) dual-AW9523 keyboard
Add the Control_Keyboard_(Bottom)_1.0 module as an ADDITIVE hardware
keyboard on the touch board. Two AW9523 expanders (0x5A left / 0x5B
right) each scan a 5x5 matrix over the shared board I2C bus, serialized
through the AttakySharedI2C mutex alongside the FT6636 touch poll and the
MAX17048 fuel gauge.
The driver emits final bytes (printable ASCII, backspace, enter, space)
with momentary Shift resolved to uppercase in-driver. UITask injects them
into the focused textarea exactly like the web-mirror keystroke path, so
CAP_KEYBOARD stays 0 and the on-screen keyboard is not suppressed (touch
typing does not regress). Focus-gated: the matrices are only scanned
while an editable field is bound, so a key has zero side effect when
nothing is focused.
Hardware-verified, both halves: q/w/e/r/t (0x5A) + y (0x5B), Shift
uppercase, backspace, space, enter; on-screen keyboard intact.
* attaky_mesh_series: use reliable 115200 upload speed
* attaky_mesh_series: use core default GPS baud
* attaky_mesh_series: restore GPS_BAUD setting
* attaky_mesh_series: rename PlatformIO environment
* pager: keep encoder focus inside the chat message list, remap composer nav
The rotary encoder's plain turn stepped LVGL's focus group directly, which
only ever contains the currently-materialized bubbles (the message list is
virtualized; loading more history is scroll-driven, not focus-driven). So
turning past the first/last *loaded* bubble escaped the list onto the
header/composer even with plenty of un-loaded history in that direction.
A turn at that edge now scrolls the list instead when more messages exist
(reusing the existing free-scroll helper), which triggers virtualization to
load the next batch; focus only leaves the list at the true oldest/newest
message. Fast turning folds into a pending target so it doesn't escape
mid-load, and the render's focus re-aim now runs synchronously so the
transient auto-focus LVGL assigns a deleted row's neighbor never paints.
Also removes the two touch-only jump-to-oldest/latest arrows on this board
(no touchscreen to tap them) and remaps the chat's key bindings now that
plain turns stay in the list:
- Orange/Alt tap: jump to the newest message + focus the composer (was
Backspace's job).
- Backspace tap: jump to the first unread message below the "NEW ----"
divider, or the newest message if nothing's unread.
- Composer row edges: Send+NEXT reaches the channel-settings gear (falls
back to the newest bubble in a DM, which has no gear); the quick-reply
chip+PREV goes straight to the newest bubble.
TLORA_PAGER_SHORTCUTS.md documents the new chat-screen behavior.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
* fix scroll
---------
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Rewrites the beta_45 notes as the full delta since the last stable (beta_40)
for the stable promotion: browser remote control (VNC/REMOTE + web app), the
on-device Web browser + tappable chat links, three new boards (T-LoRa Pager,
T-Display P4, V4-R8), and fixes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Contact-loss fix (microSD devices): atomic temp+swap saveContacts, unified robust
boot SD-mount ladder, and a storage-truth line in the Store-on-SD setting.
- Heltec V4-R8: route MISO for the shared FSPI bus so the microSD mounts; wake-from-sleep
freeze fixed (panel sleep goes through LovyanGFX's own bus, not the HSPI shim).
- Do Not Disturb (originally PR #156 by Tesso Codemonkey / @codemonkeybr): scheduled
sound-silencing window in Settings > Sound + a status-bar bell.
- Control-center redesign: round icon-only chips that spread to fill each row (T-Deck 2x4,
V4 portrait 2 rows), frosted-glass panel, real lock glyph + dynamic bell/bell-slash DND
chip + keyboard icon+word chip (new cc_icons_16 FontAwesome font), per-chip confirmation
toasts, press-grow clip fix, and a V4-portrait status-bar BLE/clock overlap fix.
- LilyGo T-Pager: map tile cache falls back to the microSD under the launcher (fixes the
"storage error / reflash the tiles partition" dead-end).
Co-Authored-By: Tesso Codemonkey <798026+codemonkeybr@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Newest-first: Signal Trek's 'The Firmware That Finally Makes It Worth It'
(~Jul 12) on top, then KDHD Stuff's 'WadaMesh Update! Supercharge Your T-Deck'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Heltec V4-R8: the CHSC6x touch ran on a 2nd I2C controller (Wire1) bound to the
SAME pads (17/18) as the board Wire (RTC + sensors), driven from two cores — a
classic SDA-stuck-low wedge = the tester's random hard-freezes-needing-reset.
Touch now shares the one board Wire (V4CAP_WIRE selector), so arduino-esp32's
per-instance TwoWire mutex serialises all access, the proven T-Deck pattern.
- Wi-Fi settings: the join/details/hidden sheet floats on the base layer above the
settings page; navMaybeRebuild only recognised the settings sheet + chat + top
layer as focus roots, so on no-touch/keypad-nav boards (Tanmatsu) its SSID/
password fields + Join button were never collected into the focus group and
couldn't be selected. Add s_wifi_sheet as a nav focus root (priority over the
settings sheet beneath it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every flasher board card now carries a support badge with a hover popover:
Heltec V4 + TFT and the T-Deck are 'Fully supported' (the two primary dev
boards); ThinkNode M9, RAK Tap V2, Heltec V4-R8, Tanmatsu and the new
T-Display P4 are 'Partially supported' — the popover spells out that the
core (LoRa mesh communication, chat, phone-app link) works while the rest
is still settling. New T-Display P4 card wired to the beta channel
(manifest-tdisplay-p4.json), same new-board pattern as the M9/RAK cards.
NOTICE: extend the ES8311 attribution to cover the P4's P4Audio.cpp
(esp-bsp-derived register sequence, re-based onto Arduino Wire).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full keyboard+encoder-driven board port: TCA8418 QWERTY matrix, rotary encoder
focus-nav, ST7796 display, ES8311 notification sound, both radio variants.
Hardware-verified by the author against a live mesh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
# src/ui-touch/UITask.cpp
Both floating jump arrows in the open-chat view are touch-only
affordances with no purpose on the T-LoRa Pager (no touchscreen), so
skip creating them on that board and leave the pointers null like
every other consumer already expects. The pager's existing
Backspace-tap "jump to latest" shortcut no longer depends on the
button object itself -- it checks chatVirtAwayFromBottom() directly
and calls chatVirtJumpToLatest(), the same virtualization-aware jump
the button's own click handler used.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Alt+Shift previously did double duty: toggle Caps Lock while editing a
field, or jump Home otherwise. Splitting these apart -- Alt+Backspace is
now the dedicated Home-jump gesture, and unlike Alt+Shift it isn't
context-dependent: it works whether or not a field is being edited, so
it's reachable from anywhere without first backing out of what you're
typing. Alt+Shift outside a field is now a no-op instead.
PagerKeyboard.cpp suppresses a Backspace press entirely while Alt is
held (no '\b' ring-push, no hold-to-back/hold-to-unlock tracking) and
reports it as a one-shot chord instead, mirroring the existing Alt+Shift
chord plumbing. Also closes the accent/@-mention popups first, since
those live on lv_layer_top() outside the tab content the Home jump
switches away from.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Previously kept local-only (see f18929b/cca9a44), moving pager tracker
docs under gitignored dev-docs/. This one now ships in the repo instead,
per explicit request -- keep it current alongside pager code changes.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
App tiles previously fell through to navFocusCb's generic white
reverse-video fill when focused via keyboard/encoder nav, instead of
the accent-tinted LV_STATE_PRESSED look they already show on a touch
press -- a solid white block over a small rounded icon chip reads as a
glitch, not a highlight, and looked inconsistent with the touch-driven
boards. Tiles now opt into the same accent tint for both states.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Typing "@partial" in a composer popped up a touch-only contact list
(NAV_SKIP_FLAG cells, same as the accent-variant box before it got the
same treatment) -- on this board's no-touch input model there was no
way to actually pick a name from it. Mirrors the accent box's pager nav
(rotary encoder walks the highlighted cell, Enter/click confirms,
Backspace cancels without touching the typed text), but grabs the
encoder the instant the list appears rather than requiring an explicit
Fn+Space arm: unlike the accent box (which pops up after almost every
letter typed and must not steal focus from ongoing typing), the mention
list only shows once the user has deliberately typed "@partial" looking
for someone to pick, so there's no ongoing typing to protect.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
The Fn(Alt)+Shift chord previously toggled Caps Lock unconditionally,
even with no text field focused to see the change reflected in --
reported as surprising and purposeless outside of typing. PagerKeyboard
now only reports the chord (pagerKeyboardConsumeAltShiftChord()); the UI
decides the effect: Caps Lock toggle while actually editing a field,
otherwise a direct jump to the Home screen (mirrors the "ta" derivation
handleHwKey() already uses to tell bound-but-unfocused fields apart from
one actually being edited).
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
A focused chat bubble already opened its Ack/Mention/Copy/Info/Block
action menu on Enter (navEnterBubble), but the encoder's short click
only sent a plain ENTER, which a bubble doesn't otherwise react to.
Mirrors handleHwKey()'s Enter branch so both inputs agree.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
User-facing reference for MeshCore users new to this board's touch-less
input model: keyboard layout, Shift/Caps, accent-variant picker, sliders,
map panning, keyboard backlight, and a full gesture quick-reference.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
The first cut used an inline-flex link that wrapped and scattered the icon, text
and version across the narrow card. Restructure as a clean teal 'Download .bin'
row with the version as a small muted caption underneath; drop the double spacing
(the .board flex gap already separates it from Connect).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Community feedback: the only .bin download was buried in the Launcher path, so
people couldn't find a download at all. Add a compact "or download the .bin" link
to every standalone board card, right under its Connect button. Each follows the
Stable/Beta toggle (beta-only boards stay pinned to latest-beta) and shows the
exact version that feed serves (both channel tags fetched once so every link is
accurate). Downloads the full merged image — the same build Connect flashes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Community feedback (gubbinsgalore): on the Launcher install page there was no way
to tell which version the "Download .bin" gives you (it's the version-less
wadamesh-tdeck.bin), and the Stable/Beta channel toggle only existed on the
Standalone page — so Launcher users were pinned to stable and couldn't grab a
beta app image.
- Add the same Stable/Beta toggle to the Launcher page (the existing wmSetChannel
JS already drives dl-tdeck + keeps both toggles in sync).
- Add a version box under the download, auto-filled from version.json and updating
with the toggle: "You're downloading version <tag> · <channel>".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The V4-R8 uses an ESP32-S3R8 (8 MB OCTAL PSRAM) which claims GPIO33-37, so
Heltec moved every control signal off them and the Expansion Kit V2 rebuilt the
display/touch/SD block onto a hardware-SPI bus:
Vext 36->40 (active-LOW) . GPS_EN 34->42 . LED 35->46 . ADC_Ctrl removed
TFT (LovyanGFX HW-SPI): MOSI=15 SCK=16 MISO=45 CS=47 DC=48 RST=21 BL=44
touch (CHSC6x, shares board I2C 17/18) . micro-SD (shared TFT SPI, CS=3)
The build defines HELTEC_LORA_V4_TFT too, so it reuses all the V4 touch-UI code;
HELTEC_LORA_V4_R8 gates only the deltas. 8 MB enables the on-device web browser
(CAP_WEB_BROWSER) and the SD card (CAP_SD). Also wires the board into the release
+ web-flasher pipeline (release.sh, gen-flasher-meta.py, the flasher picker).
UNTESTED — no V4-R8 hardware yet. Pin sources: Meshtastic heltec_v4_r8 variant,
Heltec V4-R8 pinmap, and the Expansion_board_V2.03 schematic. All three touch
envs build clean; V4/T-Deck unregressed. On-device TODO: CHSC6x shared-Wire on
17/18, SD_CS=3, and the V2 PAM8904 buzzer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reuses mapNudge() (previously Tanmatsu's Ctrl+Arrow-only helper, now
shared) so W/A/S/D pan the map north/west/south/east on this board's
touch-less Map tab. Frees up D from the slider-nudge binding, which
moves to Q/E so the two gestures don't collide when a slider (e.g. the
map zoom bar) is focused while WASD panning is in play.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Milestone 10: serve pager units sold with SX1262 instead of LR1121.
Same board JSON, same variant dir, same NSS/RESET/BUSY/DIO1/SCLK/MISO/
MOSI pins as the LR1121 env -- confirmed against trail-mate's own
working SX1262 pager reference that it's the same physical radio slot,
just a different chip populated. RADIO_CLASS/WRAPPER_CLASS swap to the
core fork's CustomSX1262/CustomSX1262Wrapper, the exact same generic
classes the T-Deck already uses, so radio_init() just calls
radio.std_init(nullptr) -- no new radio C++ code needed, only build
flags.
TCXO voltage and the DIO2-as-RF-switch setting are NOT copied from the
T-Deck wholesale: trail-mate's reference for this exact PCB uses
RadioLib's own 1.6V TCXO default with no override, so
SX126X_DIO3_TCXO_VOLTAGE is left undefined here too (std_init() already
retries at 0.0f on an SPI-cmd failure). SX126X_DIO2_AS_RF_SWITCH=true
and SX126X_CURRENT_LIMIT=140 ARE carried over from the T-Deck, but
because both are tied to the shared LORA_TX_POWER=22 target (RadioLib's
default 60mA current limit silently caps real TX power below 22dBm on
any board, not something T-Deck-specific), not board-specific analog
circuitry.
Also fixes a real bug found compiling this for the first time:
target.h only ever included the LR1121 wrapper (vendored locally in
this variant dir), never the core lib's CustomSX1262Wrapper -- unlike
T-Deck/Heltec V4's own target.h, which already include it. RADIO_CLASS/
WRAPPER_CLASS resolved to nothing and every radio_driver.* call site in
MyMesh.cpp failed to compile.
Hardware-verified: flashed a real SX1262 unit via the documented
out-of-box recipe (full erase + merged image), boot log confirmed
radio init succeeded alongside the rest of the board, and a live
mesh test confirmed send/receive on the public channel, a custom
channel, and DM.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
The accent-variant popup (issue #22) that appears above the field when
typing a letter with accented variants was touch-only -- its cells are
NAV_SKIP_FLAG, deliberately excluded from the keyboard/encoder focus
group. On the pager (no touch at all) this made the popup pure visual
noise: it would appear and just sit there unreachable.
Added a small parallel nav path scoped to this board: Fn (Alt) held +
Space jumps into the popup (instead of typing a space or the usual
double-tap-cycle-layout action); the rotary encoder then walks the
highlighted variant; the encoder's own click confirms (reusing the
existing accentBoxCellCb via a synthesized CLICKED event, rather than
duplicating its delete-base-letter + insert-variant logic); Backspace
cancels without deleting the base letter. Everything else is swallowed
while active so a stray keypress can't leak into the message mid-pick.
Focus never actually leaves the text field throughout -- the popup's
cells stay outside the normal nav group the whole time -- so nothing
extra is needed to "return" focus after a pick or cancel.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
New "Web browser" section in deploy/site/docs.html (and a TOC entry)
covering the on-device text browser and tappable chat links
("Open in web" / "Create QR"), with three new device screenshots
(app_web, app_web_links, app_web_qr). Existing screenshots unchanged.
Also render the reader page inline in the DOC_CAPTURE tour: the reader
paints from the UI-update poll, which doesn't run while the tour blocks
the loop, so the tour now drives the fetch-wait + render itself before
capturing the Web app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Shift/Caps key previously toggled Caps Lock on every bare press --
a deliberate design choice reused from a working reference build, but
not conventional keyboard behavior. Changed to: held alone, it's a
momentary Shift (uppercases letters typed while held, same as a real
Shift key); held Alt then a Shift press instead chords into a toggle
for persistent Caps Lock. kCapsPos renamed kShiftPos to match its new
primary role -- zero other call sites reference it, and s_caps itself
is unchanged (still just "is Caps Lock engaged", only the trigger
changed).
Alt (row2,col0) and Shift (row2,col8) share a matrix row, so holding
all three of Alt+Shift+O or Alt+Shift+L at once phantom-ghosts a
'q'/'a' (diode-less-matrix 3-key rectangle, no software fix possible)
-- documented in the matrix-legend comment; harmless in practice since
the intended gesture is hold-Alt-tap-Shift-release-both, not holding
all three down together.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
- New "Web" app: on-device HTTPS text browser (strip HTML to readable text,
tappable links, back/fwd/refresh, no proxy). Gated to 8 MB boards via
CAP_WEB_BROWSER; hidden on the 2 MB V4 (can't do the TLS handshake).
- Tappable URLs in chat -> Open in web (8 MB) / Create QR (all boards).
- Drawn vector globe icon for the Web tile.
- Fix: French AZERTY on-screen keyboard lost its spacebar/OK/kbd-switch row
(a stray "" mid-map terminated the LVGL button map early). Restored.
- V4 web fetch resilience (192->96->48->24 KB buffer fallback).
- Home signal box: "tap for details" cue back at the top.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DOC_CAPTURE tour re-captured the map from the live device, which showed a
less representative view. Keep the earlier curated map shot (tiles + markers).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New "Remote & web app" docs section covering the browser control panel
(Chats / Contacts / Discovered / Settings / Terminal), Screen mirror (VNC)
and Remote UI, with a 7-shot phone gallery of the real web app.
- Refreshed all device screenshots via the DOC_CAPTURE tour (now also
captures the Remote screen) and added the MQTT bridge settings page.
- New automated web-capture harness (scripts/doc/web-demo/): extracts the
real page from firmware, serves it with mock data, screenshots via Playwright.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The browser control panel gains full Chats / Contacts / Discovered / Settings
tabs (search, sort, filter, per-contact actions, notifications + live refresh,
ACK/repeats, add-from-discovered, live radio settings, quick-command picker).
Heltec V4 map tiles: fixed the "only the top renders" bug on the 2 MB-PSRAM
board with a persistent tile-buffer pool + viewport culling (no more per-render
128 KB churn/fragmentation). Adds an opt-in "Tile debug overlay" (Map options).
Also: V4 physical-keyboard input in remote/VNC, clearer remote-access screen,
complete Terminal help on device + web, WADAMESH header + status icons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remote mode showed a black screen on 2 MB-PSRAM boards (Heltec V4): the
mirror ring/shadow were too big to allocate. Size the ring to one frame
and drop the remote render to 400x240 so the whole footprint is smaller
than VNC's (which already worked). Add a touch-and-hold (3 s) exit for
keyboard-less touch boards and an Exit button on the web page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stream the live LVGL UI to a phone/desktop browser over the WS companion
server (VNC app) plus a headless REMOTE mode that renders the UI at a
browser-friendly resolution. Non-blocking per-client TX buffering, a
core-0 stream task guarded by a recursive mutex, an on-page Rotate
orientation toggle, and a small 480x288 render upscaled in the browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Tanmatsu (ESP32-P4) now bakes FIRMWARE_VERSION="wadamesh <tag>" like the S3
release path, and the app-store publish passes the exact beta tag into the IDF
build — so the About page shows the real beta number instead of the MeshCore
default "v1.16.0-touch".
- beta_40 stable release notes: rolls up beta_36 through beta_40.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the microSD work from the previous commit: wavOpen()'s
"sd:"-prefixed path branch, fmIsAudio()/fmOpenAudio() (the .wav ->
notification-sound chooser opened from the File Manager), and the
popup-dismiss registry entry for it (so Back/Escape via the encoder or
keyboard closes the picker, not just the on-screen X) are all widened
the same way as the previous commit's browsing support -- specific
call sites only, CAP_SD/CAP_FILESYSTEM stay 0 in device_caps.h.
Also updates that file's now-stale CAP_SOUND_FILES comment, which said
the pager's WAV picker only ever reads from SPIFFS -- it can read from
the SD card now too.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
pagerKeyboardSetBacklight-style dead-hardware situation: the board-level
SD wiring (power rail, card-detect, chip-select parking, shared-SPI bus
reuse) was already done during earlier bring-up, but nothing ever called
SD.begin(), so the slot was unusable. CAP_SD/CAP_FILESYSTEM already
existed for exactly this in device_caps.h, left at 0 with a "pager needs
its own SD wiring" comment.
CAP_SD turns out to gate ~37 call sites across UITask.cpp -- file
manager, WAV picker, DataStore contacts/channels routing, map-tile
fallback, wallpaper scanning, settings backup, screenshots. Scope for
this pass is file-manager browsing only (WAV picker follow-up commit);
flipping the top-level CAP_SD/CAP_FILESYSTEM macros would have silently
turned on all ~37 at once, several referencing T-Deck-only helpers never
vetted for this board. So device_caps.h is untouched -- CAP_SD/
CAP_FILESYSTEM stay 0 for the pager -- and only the specific call sites
needed for browsing are widened with `|| defined(TLORA_PAGER)`.
- fmSdTryMount()/fmSdUnmount()/fmSdClickCb(): widened, plus a pager-only
sdSharedSPI() (reuses TFT_eSPI::getSPIinstance(), the same shared bus
the radio already uses -- variants/lilygo_tlora_pager/target.cpp) and
a card-detect pre-check via the XL9555 expander (PAGER_EXPAND_SD_DET,
inverted polarity confirmed against LilyGoLib's own installSD()).
Format support is explicitly out of scope -- the pager's SD row is
tap-to-open only, no long-press-to-format.
- fmShowRoots(): pager SD row, shown only when a card is actually
detected (no T-Deck-style "tap to mount" fallback -- the detect line
makes "not present" unambiguous).
- fmIsSd(), the CAP_FILESYSTEM-gated Apps-drawer "Files" entry (dispatch
+ grid tile), and the insert/remove poll: widened the same way.
All ~30 other CAP_SD/CAP_FILESYSTEM sites (DataStore SD routing, tile
fallback, wallpaper, telemetry, settings backup, screenshots) are
untouched and stay exactly as unavailable on the pager as before.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
pagerKeyboardSetBacklight() existed but nothing called it. The mode
state, NVS persistence, and boot restore were already generic across
every CAP_KEYBOARD board (s_kb_bl_mode, touchPrefsGetKbBacklight()),
so only two pieces were missing for this board:
- updatePagerKbBacklight(): a per-tick apply step, the pager's
analog of the T-Deck's inline backlight calc / Tanmatsu's
tanKbBacklightTick(). No brightness curve needed here (the pager's
backlight is a plain LEDC PWM strip, not a dimmable slider like the
T-Deck's) -- on/auto-active drive full duty (255), everything else
(off, idle, screen-off, locked) drives 0. Runs unconditionally in
the pager's loop branch so it can force the light dark the instant
the screen goes off/locked.
- A "Keyboard backlight" row in Settings -> Keyboard (pager-only,
sibling to the T-Deck's slider block) that cycles off/on/auto,
mirroring the Control Center chip's semantics -- the Control Center
itself is unreachable on this board (its launcher is touch-only).
Also fixed auto mode actually responding to input: neither
handleHwKey() nor updatePagerEncoder() called noteKbActivity() (the
function that stamps the auto-mode idle timer) -- they only called
the separate noteUserInput() (the screen idle-dim timer), so auto
mode would light briefly at boot and then never again regardless of
typing or turning the encoder. Both now stamp it too.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Fixes:
- Card-less devices (Heltec V4) froze for seconds: a full contacts rewrite to
SPIFFS on every advert refresh triggered a multi-second GC in the mesh loop.
Coalesce advert-driven saves (a contact-set change still saves promptly); SD
boards unchanged. Flush on reboot/power-off so nothing is lost.
- Long translations (FR/DE/...) overflowed fixed-width labels: new
uiFitLabelWidth() shrinks the font to fit one line instead of clipping/wrapping
- applied to the message menu, confirm dialogs, and app-drawer tiles.
- Home signal graph: "tap for details" hint moved to bottom-right so it no longer
overlaps the "Sig" chip in longer languages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persistence (Leon P / MrWeavis, beta_38):
- Deleting a message or clearing a chat now persists synchronously, so it
survives an immediate hardware reset instead of "coming back" (the tombstone
used to ride the 30 s lazy SD flush, which a reset beats).
- The history file is now written compactly — only the used records,
oldest-first, instead of all ~5000 ring slots every time. A typical chat
writes ~48 KB instead of ~1.2 MB, so the synchronous delete/clear/reboot
flush no longer hitches the UI (and normal message flushes are lighter too).
Old history files still load (the reader derives its slot count from size).
Emoji picker:
- Baked the 14 glyphs that were listed but never in the font (umbrella, coffee,
pizza, cake, rocket, camera, phone, laptop, calendar, antenna, e-mail, house,
car, watch); they rendered as the notdef box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audited every user-visible string sink across the touch UI and closed the
gaps that had accumulated since the language table was last touched:
- TR() now strips a leading LVGL-symbol glyph (+ spaces) off the lookup key
and rebuilds the label, so icon-prefixed menu/sheet rows ("<glyph> Copy")
translate instead of silently falling back to English.
- Wrapped 95 bare literal sinks (settingsRowLabel / showAlert / showConfirm /
lv_label_set_text / dropdown / placeholder / checkbox) in TR(); unwrapped 29
format-only strings that never should have been keys.
- Added 307 missing rows to kI18n[] covering all 11 non-English languages
(NL DE FR ES IT RU UK BG SR EL PT-BR); keys byte-identical, printf
placeholders and %-order preserved, Serbian in Cyrillic.
Builds green on T-Deck + Heltec V4; Tanmatsu app 3,169,296 B (107 KB under
the AppFS 50-page load ceiling).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One page with every supported board, its install path, release channel
and maturity, plus the open hardware requests and the channel model.
The README board list now covers all five targets and links there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both new S3 boards are installable from the browser now: release.sh
builds and ships their bins per channel, gen-flasher-meta emits their
esp-web-tools manifests, and the site gets install cards for both.
The cards are pinned to the beta feed (with a note) until the first
stable promote that includes their bins; the wmSetChannel comment says
how to unpin then. beta_38's beta feed and GitHub release carry the
new bins retroactively.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The P4's internal FFat 'locfd' has a broken FAT metadata layer: open/
read/write work but f_stat/exists()/size() return garbage - and WHICH
garbage shifts with the build. At -Og the exists()-gated identity and
NodePrefs loads worked by luck; the -Os switch flipped the lie and the
device booted with a fresh identity and default name, and profile
changes never survived a reboot (the loads failed, not the saves).
Chat history was unaffected because it already lives on SD_MMC.
- DataStore::useSdMmcStorage(): full-store adoption of the card
(/meshcomod root, identity store included), mirroring the T-Deck's
useSdStorage(). The old card-root contacts3/channels2/adv_blobs from
the secondary-FS era are renamed under /meshcomod.
- Tanmatsu boot: one-time rescue of identity + prefs off FFat by OPEN
and READ probing (never exists()/size() on that FS), then adopt the
card. FFat remains the no-card fallback only.
- DataStore::loadPrefs() probes candidates by opening and reading a
byte instead of exists() - truthful on the P4, identical elsewhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Since the beta_35 lazy history writer, message-ring flushes are spaced
up to 30 s apart on the deep SD ring. Every on-device reboot/power path
compensates by calling persistHistoryNow() first, but the companion
protocol's CMD_REBOOT (the phone app's reboot button) went straight to
board.reboot(), dropping everything since the last flush - the newest,
just-read messages, which reads as 'my channel and DM messages get
deleted after a manual reboot' (Leon P, T-Deck under Launcher, beta_35
through beta_37).
persistHistoryNow() is now an AbstractUITask hook (default no-op, so
non-touch companions are unchanged) and CMD_REBOOT calls it before
rebooting, matching the power-menu contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app has been compiling at debug optimization since the port began,
costing ~300 KB of flash-mapped footprint against the launcher's hard
50-page (3,276,800 B) app-load ceiling. At -Os current main is
2,973,520 B, ~296 KB under the ceiling. Boot-verified on the device:
app starts, C6 esp-hosted link up, Wi-Fi associates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Users who flipped 'Store data on SD' before beta_36 ran with the toggle
IGNORED (the flag never survived a reboot), so their identity, prefs and
contacts kept living on SPIFFS. When beta_36 made the flag finally take
effect, useSdStorage() pointed the store at an EMPTY card, the identity
store minted a brand-new node, and the user 'lost' their profile. The
data was never gone - it was orphaned on internal flash.
Two-part fix:
- Boot: adopting the card while SPIFFS holds an identity the card lacks
now copies every SPIFFS file into SD:/meshcomod first (kv files
flatten to the card's SdNvsPrefs layout). If the identity file fails
to land, the device stays on SPIFFS for the boot instead of adopting
a card without it.
- Settings -> Storage gains 'Copy internal data to SD' for the users
already bitten (their card now holds a fresh identity the boot-time
no-clobber gate rightly won't touch): confirmation, overwrite-copy of
everything from internal flash, force the SD pref on, reboot.
Workaround until this ships: toggle 'Store data on SD' OFF and reboot -
the device reads internal flash again and the old profile returns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With Delete the menu carries up to 7 actions; a single column no longer
fit the 240-px screens without scrolling. Two per row (wider 200-px card,
odd last button on its own row) keeps the whole menu on one screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The chat jump buttons are bare arrows flush against the right edge now
(28x36 transparent touch target, accent-colored glyph) instead of 40 px
filled circles; they run at full opacity while the list scrolls and dim
to 50 percent one second after the last scroll activity. The Tanmatsu
keeps its F-key chip styling and geometry.
- The long-press message menu gains a Delete row: the message is
tombstoned in the ring (thread tag blanked, so every walker skips the
slot and live ring indexes stay valid) and both the chat and the thread
list refresh.
- The chat/channel settings sheet (in-chat gear and list long-press)
gains a 'Delete history' row for both DMs and channels: confirmation
prompt, then every message of the thread is tombstoned. The thread row,
channel membership and contact stay; only the conversation is wiped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app has been building at debug optimization since the port began.
At -Og the AppFS image sat 2.2 KB OVER the launcher's 50-page load
ceiling with the chat virtualization in; at -Os it is 2,980,688 bytes,
296 KB under the ceiling, retiring the size-locked-platform problem
(and likely speeding the UI up). Needs one on-device boot check before
anything ships with it - the P4 has never run an -Os build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- chatVirtReset no longer queues lv_obj_del_async on the divider: the
empty-thread paths lv_obj_clean two lines later freed it first and the
deferred lv_obj_del then ran on freed memory (confirmed UAF, plus a
cross-panel variant when both panels are open). Downstream cleans own
the widget; the reset only nulls the pointer now.
- Compact rows register bubbleLongPressMenuCb for LV_EVENT_LONG_PRESSED
(the only event the callback acts on) instead of PRESSED/RELEASED/
PRESS_LOST/DELETE, all of which it filtered out - the per-message menu
was completely dead in compact mode.
- Bubble mode gets tap-to-resend back on failed sends (CLICKED handler
plus the 'tap to resend' footer), matching the compact path and the
pre-virtualization behavior.
- need_layout gains a content-generation guard (endpoint ring slots): at
ring capacity n stays constant while content rotates, and equal-count
thread switches reused stale offsets - rows rendered the next message's
text at the previous message's position. The dead dirty_timeline rescue
(cleared before its async consumer could read it) is removed.
- New chatVirtTryAppendLayout fast path: pure appends extend offsets for
the new tail instead of re-measuring the whole thread (the full measure
is ~100 ms of UI stall per received message on a 5000-message SD ring).
- lv_async_call failure no longer wedges the render busy flag; the nav
focus fallback validates s_nav_objs[0] before focusing it (bubbles now
recycle on scroll-end, so slot 0 can be one tick stale); dead
chatVirtClearAllChildren/DeleteMsgsChild pair removed; stray
FIRMWARE_BUILD_DATE bump reverted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The control-center sizing lost its '#elif HAS_TDECK_GT911' line, which
put the 200 px set_size inside the Tanmatsu arm (shrinking its card
from 384 to 200) and dropped the T-Deck into the portrait else-arm.
Restored as TDECK || M9. Also removed the unreachable duplicate lines
after the M9_KEY_HOME return and fixed 'hole the d-pad' on the lock
screen hint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bare ')' under #if CAP_LARGE_SCREEN broke the Tanmatsu (P4) build,
the only target that compiles that arm. The power-menu #else arm went
back to the original card_h = 206 so T-Deck/V4 keep their exact layout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PIN_USER_BTN: -1 -> 0 (enable BOOT key on GPIO0)
- Short press (<1s): toggle screen on/off (no hard-lock)
- Long press (>=1s): open power menu (Power off / Reboot / Cancel)
- Add ARDUINO_USB_CDC_ON_BOOT=1 (stable USB CDC COM port)
- boards/rak_tap_v2.json: use_1200bps_touch=false, wait_for_upload_port=false
(manual download mode via BOOT+RESET, esptool upload on COM7)
- Remove unused boards/wiscore_rak3312.json
- Remove LovyanGFX from Heltec lib_deps (accidental regression in baseline)
Signed-off-by: Ethan-chen1234-zy <ethan.chen@rakwireless.com>
updatePagerEncoder() never special-cased an open lv_dropdown, so a
plain turn always sent LV_KEY_NEXT/PREV -- but lv_dropdown's own key
handling only understands LV_KEY_UP/DOWN to move the highlighted row
when its list is open, and ignores NEXT/PREV entirely. Those instead
got consumed by the focus group to move focus off the dropdown, so
turning the encoder never moved the highlight and the list stayed
stuck on whatever was already selected (reported: opens fine, but
can't scroll to a choice, in Radio & Mesh's Community Preset and
other dropdowns).
Capture the encoder the same way Tanmatsu's navPump() already does
for its own input path: while navOpenDropdown() is true, route turns
as LV_KEY_DOWN/UP instead of NEXT/PREV. The short-click path already
sends LV_KEY_ENTER, which lv_dropdown uses to confirm the highlighted
row and close, so no change needed there.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Radio & Mesh settings auto-saved (flash write + live LR1121 SPI
reconfigure) on every textarea LV_EVENT_DEFOCUSED, even when nothing
changed. Touch boards rarely trip this (a field only blurs on a
deliberate tap-out after editing), but the pager has no touch
fallback, so plain keyboard/encoder navigation blurs a field on every
step -- arrowing through the freq/bw/sf/cr/tx/airtime row re-ran a
real radio reinit up to six times with no edits made, observed on
hardware as escalating multi-second [STALL] ui:lvgl entries.
Skip the save/reapply on a silent (defocus) blur when the parsed
values already match NodePrefs, gated to TLORA_PAGER so touch boards
keep their existing always-save-on-blur behavior unchanged.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Enter on a focused chat bubble now opens the same Ack/Mention/Copy/
Info/Block action menu the T-Deck opens on a long-press, instead of
being a no-op. Hoisted navEnterBubble() out of its HAS_TANMATSU-only
guard (it was already board-agnostic) and reused it from the pager's
Enter branch in handleHwKey() -- it forwards Enter as a synthetic
LV_EVENT_LONG_PRESSED to the focused bubble, the same event the touch
long-press itself sends.
A plain Backspace tap also now does what tapping the T-Deck's floating
"scroll to bottom" circle does: jump the chat to the latest message
and hide the circle (mirroring the Tanmatsu F6 hardware-key handler,
which had the jump but was missing the hide), then move nav focus to
the composer so a reply can be typed immediately instead of leaving
focus on whatever bubble was selected pre-jump. Only intercepted while
the circle is actually showing, so Backspace at the bottom of a chat
(or outside one) keeps its normal no-op / hold-to-back behavior.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
handleHwKey() treated "a textarea is bound to the on-screen keyboard"
as "we're editing a field", but showKb() binds the composer as soon as
a chat panel opens and never unbinds it just because nav focus moved
elsewhere in the same panel. So with focus on, say, the quick-reply or
emoji icon, Enter fell into the composer's submit/newline handling
instead of activating the actually-focused widget -- the rotary
encoder's short click worked because it bypasses this function
entirely (navPushTap(LV_KEY_ENTER) goes straight into the focus
group). Now only treated as editing when nav focus is really on that
field, mirroring the CAP_TRACKBALL boards' existing s_kbd_nav/
s_nav_ta_editing distinction for the same problem.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
The pager's 480px landscape screen left most of the quick-reply card's
width empty, since it shared the same 220px single-column sizing as
the narrower T-Deck/Heltec V4 screens. Widened to 360px and switched
to a 2-column grid (3 rows instead of 6) for this board specifically;
the button-placement loop is now column/row-based instead of a single
vertical stack, driven by per-board cols/col_gap constants so the
other boards' layout is unchanged. Focus navigation needed no changes
since it's already purely position-based (navMoveDir scores by each
widget's actual on-screen rect).
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Mirrors the T-Deck's spacebar-lock/trackball-unlock experience for the
pager. Unlike the T-Deck's keyboard, the TCA8418 here reports real
press/release, so this is a genuine hold (with a live-filling
"Locking…" progress popup) rather than the T-Deck's tap+countdown
workaround. Backspace was chosen for unlock over the rotary encoder:
its raw held-state stays readable via PagerKeyboard's own poll loop
regardless of screen power state, while the encoder's poller currently
exits early whenever the screen is off.
Fixes three existing bypasses that would otherwise make the new lock
pointless -- on this board, a plain encoder turn, a BOOT-button press,
or any ordinary keypress all unconditionally called wakeScreen()
(which clears the manual-lock flag) with no hold requirement, so
anything at all would instantly unlock a "locked" screen. Each now
checks isManualLock() first; only the deliberate Backspace hold clears
it. The BOOT-button fix is scoped to TLORA_PAGER specifically, since
that same code path is shared with the Heltec V4, which has no
keyboard and relies on BOOT as its only lock/unlock control.
Also updates the two user-facing "how to unlock" hint strings (the
Settings lock-on-screen-off toggle, and the T-Deck-style lock overlay
text, the latter currently dead code on this board since that overlay
is never built here) so they don't keep pointing at a trackball this
board doesn't have.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
No touch or trackball on this board means a focused lv_slider (Control
Center brightness, a Settings slider, the Map zoom bar, ...) couldn't
be adjusted at all via keypad nav. D/F now drive it through
navMoveDir()'s existing slider-capture branch -- the same proportional
step, live-update, and persist-on-release the T-Deck trackball's
LEFT/RIGHT already does -- instead of a fixed one-unit-per-press step
that's wrong for every slider's range.
Also lower the pager's default notification volume to 50%: its ES8311
codec + NS4150B amp run louder at a given percentage than the T-Deck's
I2S amp/Tanmatsu's ES8156, so the shared 70% default clipped into
uncomfortable territory on this board specifically.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
A flash/reset landing mid-I2C-transaction can wedge the GT911 (the S3
resets, the touch controller keeps running) into repeating a stale
pressed frame. The driver's keep-prev-state-on-no-data poll then latched
an eternal press: real touches ignored, trackball clicks eaten (a live
touch outranks the click injection), idle timeout never firing, keyboard
lock/unlock still working - a dead-looking device that survived every
reboot and reflash because only a full power cycle resets the GT911.
Watchdogs in gt911Poll() now force a release when status reads fail
mid-press, when no frame arrives for 250 ms while pressed (a real held
finger produces a frame every GT911 scan cycle), or when one bit-exact
point stays pressed for 10 s (a human touch always jitters) - that last
point is then ignored until it moves or a real lift frame arrives. A
jitter-y phantom can still need a power cycle, but the common wedge
modes self-heal in under a second.
Init is no longer one-shot: the boot probe retries every 500 ms for
~6 s before caching failure, so a GT911 still resetting at the first
attempt no longer costs the session its touch AND keyboard (the kb
poll task only starts once touch inits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adding the Sound chip's HAS_UI_SOUND to the pager built put 6 chips
into a row whose width was hardcoded for 5 (a leftover from when the
row only served the Heltec V4), and the chip's gap constant didn't
match the row's own pad_column, so the extra chip wrapped onto a
clipped 2nd line. Chip width is now derived from the actual chip
count instead of a fixed divisor.
The card height itself was also a shared V4/T-Deck-landscape constant
tuned for a 240px-tall screen; the pager's screen is only 222px tall,
so that height overflowed the visible root and pushed the toggle row
and sysinfo line off the bottom of the physical display. The pager
now sizes its card from the actual available height.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Brings the T-LoRa Pager to feature parity with the T-Deck's notification
sound (built-in per-slot chimes, custom WAV playback, per-event on/off,
volume, Settings > Sound UI) using the board's ES8311 codec + NS4150B
Class-D amp, previously undriven.
- Es8311Codec: a minimal, hand-rolled I2C register driver (DAC/output path
only) rather than vendoring Espressif's much larger esp_codec_dev HAL,
which this single-chip playback-only board doesn't need. Register
sequence and MCLK clock-divider coefficients are derived from Espressif's
public es8311.c/es8311_reg.h (Apache-2.0, credited in NOTICE).
- TLoraPagerBoard::setAmpEnabled() toggles the amp around playback rather
than leaving it permanently on; the existing boot-time AMP_EN drive stays
as-is since it exists for shared-bus integrity reasons unrelated to audio.
A boot-time I2C presence probe logs [BOOT] es8311 ok/PROBE FAILED.
- UITask.cpp: pager I2S/codec playback functions mirroring the existing
T-Deck ones, plumbed into the same uiPlaySlot()/uiPreviewWavFile()
dispatch the other boards use (which also had two T-Deck-hardcoded call
sites generalized as part of this). New CAP_SOUND_FILES capability flag
gates the file-browsing sound picker without reopening real SD support
(the pager remains SPIFFS-only, decoupled from CAP_SD).
- platformio.ini: I2S pin flags for the pager env, confirmed against
arduino-esp32's canonical pins_arduino.h for this board.
Verified on hardware: boot log shows es8311 ok, and chimes/volume/WAV
playback/DM+mention sounds all confirmed audible. Build-green on all three
envs; Heltec V4 binary byte-identical, T-Deck flash +16 bytes.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
PagerKeyboard now distinguishes a solo Alt tap (press+release with
nothing else happening) from Alt held as a modifier (symbol-layer
typing, or the encoder's Alt+turn free-scroll) via
pagerKeyboardConsumeAltTap()/pagerKeyboardMarkAltUsed(), and exposes
pagerKeyboardBackspaceHeld() alongside the existing immediate-'\b'
press behavior.
Wires both into UITask: a solo Alt tap advances focus one step
(LV_KEY_NEXT), same as one rotary detent, and holding Backspace for
1s triggers the same "back" ladder as the encoder's long-press
(pagerNavGoBack(), factored out of updatePagerEncoder so both paths
agree exactly) — giving keyboard-only users a way back without
relying on the encoder. Also routes Enter through navPushTap when no
text field is focused (plain widget nav, including during the setup
wizard) and widens the radio settings SF/CR/TX/AF row's top margin
(was crowding the label above it).
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Replaces the three individually-named TLORA_PAGER_PORT*.md entries with
/dev-docs/ so any future tracker added under that directory is covered
without another gitignore edit; also ignore /crash-dumps/.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Viewport-based message virtualization for open chats: only the visible
window of LVGL widgets is materialised while layout offsets cover the
full thread (up to msgCap). Supports bubble and compact IRC modes, day
separators, jump to oldest/latest, and compressed LVGL scroll coords
for very long histories.
Finger drags advance layout scroll 1:1 even when coords are compressed,
so long histories scroll fluidly without micro-drags that trigger bubble
long-press menus.
Includes LVGL crash hardening from soak testing: defer refreshChatDetail
and bubble reflow off the indev/refresh path, sync-delete during async
render, size chatVirtEnsureMsgIdx to msgCap for the SD deep ring, and
open-to-bottom scroll that does not rely on premature scroll_bottom.
Fixes#126
Signed-off-by: Steve Glenner <steveglenner@hotmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
It is align_to'd against the clock's final translated coords each tick; the
tall-bar top-row translate on top of that pushed its upper half out of the
bar (clipped) inside a chat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Typing (and thus the active layout) only matters in an open conversation;
everywhere else the language name was permanent bar clutter next to the clock.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The EN/DE indicator sat at a fixed -182 right offset while the clock's left
edge reaches past that in 12-hour mode ('7:45 PM'), overlapping on the T-Deck
bar. The Tanmatsu branch already parked it align_to the clock; un-gate that so
all boards track the clock wherever it lands (12/24h width, charging slide,
centred hide-name mode).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the node's live GPS position as the first row of the chat quick-reply
picker: with a fix it shows the coordinates and a tap inserts them into the
composer at the cursor (same no-auto-send contract as the macro rows, so the
operator can add context before sending); without a live fix the row is
greyed out and disabled. Sharing where you are is one of the most common
things typed into a mesh chat, and hand-copying coordinates from the GPS
screen was the only way to do it.
Signed-off-by: Yaz <yazutsu@users.noreply.github.com>
Ensure the tile-fetch worker is running before arming a message-ring flush,
fall back to a synchronous write if spawn fails, write ui_msgs_v1.bin via a
.tmp rename for crash-safe replacement, and retry when the worker snapshot is
missing instead of treating the flush as successful.
Signed-off-by: Steve Glenner <steveglenner@hotmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up to the keyboard-backlight brightness control (issue #84). The Off
preset used to set the brightness percent to 0 while leaving the backlight
MODE untouched, inventing a second dark state: the keyboard went dark but the
control-center chip - which cycles and shows the mode - kept saying on/auto.
Off now switches the mode off, the same axis the CC chip shows, so the two
surfaces can never disagree. The brightness slider becomes 1-100: dark is
only expressible via the mode, and the on/auto modes always glow visibly at
the chosen level. The stored percent is left untouched by Off, so re-enabling
the backlight restores the previous brightness; a stored 0 from the previous
build is clamped to 1 at boot.
Signed-off-by: Yaz <yazutsu@users.noreply.github.com>
- ALT-accent picker on the Tanmatsu physical keyboard: hold ALT + letter
opens the accent popup with the first variant highlighted, the letter
cycles (auto-repeat walks the options), releasing ALT (its own scancode
event) inserts the highlighted choice. Popup is nav-skipped/non-clickable
so keypad-nav focus can't steal the cycle key, SC()-scaled, align-centred.
- Double-tap-space language cycle ported to the Tanmatsu typing path
(navPump), and keyboardLayoutsApply now updates the active layout even
with no on-screen keyboard object (was a permanent no-op there).
- Fonts: LV_USE_FONT_COMPRESSED on; extras_font_12/14/16 regenerated
compressed (identical text glyphs; redundant mono emoji dropped — the
colour imgfont covers emoji) freeing ~340 KB; new compressed Latin-accent
extras_lat_20/24/28 spliced into the Large/Huge fallback chain
(Tanmatsu-only) so umlauts size-match the scaled UI. Tanmatsu app.bin =
3,269,440 B, UNDER the known-booting 3,271,856 B (50-page AppFS ceiling).
- TEMP: TDECK_KEYCODE_PROBE toast (build-flag gated, inert in releases)
for mapping the T-Deck alt key.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TLORA_PAGER_M7_HW_DEBUG_LOG.md is a blow-by-blow hardware bring-up log,
not a published project doc -- same treatment already applied to
TLORA_PAGER_PORT.md/TLORA_PAGER_PORT_MILESTONES.md and CLAUDE.md. File
stays on disk locally, just out of version control going forward.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
TLORA_PAGER_PORT.md and TLORA_PAGER_PORT_MILESTONES.md are working
hardware bring-up notes, not published project docs -- same treatment
CLAUDE.md/.claude/ already get in this .gitignore. Files stay on disk
locally, just out of version control going forward.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Encoder direction was backwards on hardware (confirmed by user testing):
kQuadTable's CW/CCW sign didn't match this physical part's wiring, so
turning the way you'd expect to move forward/down moved focus and
scroll backward/up instead. Negated once in pagerEncoderReadDelta()
so every consumer (nav NEXT/PREV, Alt+turn tab-switch, Alt+turn page
scroll) inherits the corrected sense from one place.
Also scope the pager's Alt+encoder gesture: it used to always jump
between main tabs, even from inside a settings sheet or chat, silently
abandoning it. Now Alt+turn only tab-switches at the main-tab level
(navOnMainPage()); inside a nested scrollable screen it instead
free-scrolls the page like a touch drag (navScrollFocused()'s ANIMATED
lv_obj_scroll_by, unlike navFocusCb's one-shot LV_ANIM_OFF
scroll_to_view, reliably repaints on this board), then snaps focus to
whatever's now nearest the top of the visible viewport
(navRefocusFirstVisible()) so plain NEXT/PREV resumes from a visible
spot instead of a focus stuck off-glass.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Boot TX power was left at a conservative 10dBm (Heltec V4's pattern) with
no way to raise it, unlike the T-Deck which boots straight at the chip's
real 22dBm ceiling. On hardware this was silently degrading outbound
range: adverts/DMs sent from the pager were unreliable while inbound
reception was fine, since the far node's own TX power was unaffected.
22dBm is confirmed as this chip's actual sub-GHz HP-PA ceiling via
RadioLib's LR1120::checkOutputPower() (LR1121 inherits it) and matches
trail-mate's own working config for this board.
Also widen the three RX-boosted-gain gates in MyMesh.cpp/DataStore.cpp
that only checked USE_SX1262/USE_SX1268 to include USE_LR1121 --
CustomLR1121Wrapper already implements setRxBoostedGainMode/
getRxBoostedGainMode, so the setting was silently a no-op for this radio.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Two related first-boot-wizard bugs found on hardware:
- updatePagerEncoder() woke the screen on either a turn OR a bare click.
Waking via click let the SAME press-release cycle fall straight into
the short-click logic further down as an ordinary click on whatever was
already focused -- concretely, it selected "Skip" on the wizard's
welcome screen the instant the screen lit up. Only a turn wakes the
screen now; BOOT is unchanged and still wakes via its own path.
- The region step's 20-preset list auto-jumped focus to Next on the
FIRST click, before the row's colour change was even visible -- no way
to confirm the right region got picked before focus moved off the
list. Changed to two-step: the first click on a row selects/recolours
it and focus stays put; a second click on the row that's ALREADY
selected is treated as the explicit confirm and jumps focus to Next.
Both gated CAP_KEYPAD_NAV (Tanmatsu + pager); T-Deck/Heltec V4 touch
flows are unaffected either way (moving the internal group focus is
invisible without active keypad-nav).
User-verified fixed on hardware (fresh first-boot flash, region select,
Wi-Fi connect, and map tile auto-download all working).
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
The bottom tab bar (Chats/Contacts/Home/Map/Settings) is deliberately not
a nav-group focus target, same as T-Deck/Tanmatsu -- but those boards
reach it via their own dedicated hotkeys (programmable keys / coloured
shape keys), which the pager has none of. Plain rotary turning could
therefore only ever move focus within the current screen; the tab bar
icons were unreachable from Home (or anywhere else).
Exposed pagerKeyboardAltHeld() (raw modifier state already tracked by
pagerKeyboardPoll() to drive the symbol layer, just never surfaced
before) and used it in updatePagerEncoder(): holding Alt (the bottom-left
orange key) while turning the encoder now calls navSwitchTab(+-1) per
detent instead of the normal focus-move NEXT/PREV, cycling through the
5 main tabs directly. Alt alone still types nothing, so this doesn't
take anything away from the keyboard.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Two gaps left "hold to go back" not actually working anywhere but inside
a popup:
- The popup registry (anyPopupOpen()/hwKeyDismissTopPopup(), which drives
the actual dismiss) only recognized HAS_TDECK_GT911/HAS_TANMATSU, so it
never knew how to close the pager's own Terminal fullscreen view or its
command picker -- the encoder's long-press was sending a raw Esc key
that nothing handled. Widened to TLORA_PAGER.
- Even with that fixed, a BARE main tab (Chats, Contacts, Map, Settings)
is neither a popup nor an open chat/channel detail, so the back cascade
fell through to a no-op Esc. T-Deck/Tanmatsu don't hit this because they
have separate dedicated Home hotkeys; the pager has none (no
programmable keys, and the bottom tab bar is deliberately not a
nav-group target). Added a third rung: fall back to the Home tab before
plain Esc, matching every other "close" path in this app (the
Terminal/Files Home button, the fullscreen-view popup closer) already
landing on Home rather than a real back-stack.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Neither input path called noteUserInput() for the common no-field-focused
case, so the idle timeout kept counting down during active use:
- handleHwKey()'s no-textarea branch only calls noteUserInput() when
dismissing a popup or matching a tab hotkey. tabForKey() no longer maps
any letters (tab jumps moved to the trackball-only programmable
hotkeys) and isDismissKey() always returns false for the pager's full
QWERTY, so with no field focused, every keypress silently no-op'd here.
- updatePagerEncoder() only called noteUserInput() to wake an already-off
screen; turning or clicking the encoder while the screen was ON never
reset the timer.
This board has no touch and no trackball to fall back on for idle-reset,
so both gaps were fatal: the screen dimmed after ~30s of real use.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
navMaybeRebuild() re-roots keyboard/rotary focus onto lv_layer_top() as
soon as ANY child is visible there, but collected the WHOLE top layer.
Fullscreen views that spawn their own popups (Terminal's command picker,
file manager prompts/action sheets) add those popups as separate
lv_layer_top() siblings rather than nesting them in the view, so with two
visible top-layer children at once, nav walked the hidden view underneath
before ever reaching the popup on top of it (reported: opening the
terminal's command picker left rotary focus on the terminal behind it).
LVGL has no z-index — child order is stacking order — so the last
visible, non-NAV_SKIP child is the one actually on top. Added
navTopFrontmostChild() and root nav on it instead of the whole top layer.
Single-popup screens are unaffected (same one child either way).
Not board-gated: this is a general nav-focus correctness fix that
benefits any board using keypad/trackball navigation, not pager-specific.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Two related non-touch navigation bugs on the pager's Terminal screen:
- handleHwKey()'s ENTER handling only routed to terminalSubmit() under
HAS_TDECK_GT911, so physical-keyboard Enter fell through to hideKb()
instead of running the typed command. Widened to TLORA_PAGER.
- s_nav_show (whether the amber keypad-focus ring is painted) only
defaulted true for HAS_TANMATSU ("keyboard-only device: focus highlight
is always visible"); every other board, including the pager, defaulted
false and only flips true inside CAP_TRACKBALL-gated code, which never
runs on the pager (no trackball). Since the pager also has no touch,
rotary-driven focus was moving but never rendered, so the terminal's
send button looked unselectable. Widened to TLORA_PAGER, matching the
Tanmatsu's rationale exactly.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
makeHome()'s chart_w gate only widened the chart's cleared strip for
HAS_TDECK_GT911/HAS_TANMATSU, so the pager (landscape, same right-hand
Advert/Terminal/Apps/Control column) fell into the full-width else branch
and the chart border/Sig chip ran under the buttons. Add TLORA_PAGER to
the gate, matching the other landscape boards.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Six real bugs found and fixed across two hardware sessions (full account in
TLORA_PAGER_M7_HW_DEBUG_LOG.md): stale-NVS BLE bond crash, no screen-wake
path for keyboard/encoder input, wrong TFT_eSPI SPI-port index on ESP32-S3,
missing panel INVON for this ST7796 batch, two SPI hosts fighting over the
shared radio/display pins, and a floating XL9555 ch6 panel hardware-reset
line (the actual root cause of the intermittent black screen, not the SPI
race originally suspected). TLoraPagerBoard::begin() now drives the full
vendor rail set including the display reset pulse and parks shared-SPI
selects high before first bus traffic.
Boot logo, full LVGL UI, QWERTY keyboard navigation, and rotary encoder
nav/select are now verified working on real hardware. Trackers updated:
Milestone 7's UI portion is done; the remaining radio/USB-companion/SD
gates move to a new Milestone 7b so they don't block Milestone 8's
on-device UI pass, which is now active.
All three envs (Heltec V4, T-Deck, pager) build green.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
- Edit menu (Cut/Copy/Paste/All/Sym) + double-tap select on every settings
field via attachSettingsTaEvents; menu actions route through the keyboard
mirror when bound (#122)
- Wi-Fi join/hidden/details sheets: full-screen settings-style pages riding
the tall status bar title (s_apppage hook), on lv_scr_act so the bar's
glass row isn't clipped
- Lock screen unread badge (envelope + count, anti-burn-in drift) (#93)
- About: Save update bin to SD (/BINS/wadamesh-<tag>-<channel>.bin) for
Launcher installs; beta-channel toggle no longer gated on the OTA slot
- Australia (Mid) preset 915.075/125/SF9/CR5 (#121)
- M9: [M9KB] key debug print (bring-up)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two edges from the #123 review:
- The raw-NVS mirror is best-effort now: on installs with unusable NVS
(Launcher) the file write is the authoritative copy, so a failed mirror
no longer fails the setter.
- When prefs actively live on the SD card, the toggle also rewrites the
SPIFFS copy of touch.kv (record-preserving; new SdNvsPrefs::fileFs()
accessor tells the setter which fs is active). Without this, boot's
SPIFFS fallback kept reading the stale use_sd=true and SD storage could
be turned on but never off again where NVS is broken.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UI stores the SD toggle in /prefs/touch.kv via SdNvsPrefs; boot only read
raw NVS. Dual-write on change and read NVS with touch.kv fallback so SD
storage selection survives reboot.
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Steve Glenner <steveglenner@hotmail.com>
Adds the pager as a first-class non-touch UI target: new device_caps.h
block (no touch, hw keyboard + rotary-encoder nav, 480x222, GPS/OTA/lock
screen; SD/filesystem left off since the mount code is still hardcoded to
HAS_TDECK_GT911, not genuinely CAP_SD-generic), KEYPAD indev registration
reusing the Tanmatsu keypad plumbing, forced 480x222 landscape rotation,
keyboard/encoder input drain in the main loop, and AW9364-correct
brightness wiring ahead of the existing PWM backlight branch.
Also fixes two latent gaps this surfaced: navMaybeRebuild() was
unreachable for the pager's cap combination (would have left the focus
group permanently empty), and several HAS_TDECK_KEYBOARD-gated helpers
needed their reopened regions individually widened rather than the file
touched wholesale.
All three envs build green; Heltec V4 TFT and T-Deck binaries are
byte-identical to pre-change (25.3%/73.5%), confirming every edit stayed
behind the pager gate.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
PagerKeyboard.{h,cpp}: TCA8418 matrix keyboard over I2C. Unlike the
T-Deck's keyboard (a second MCU that resolves ASCII itself), the
TCA8418 only reports raw row/col events, so the keymap and
shift/sym/alt state machine live here -- keymap tables cribbed from
trail-mate's LilyGoKeyboard (same physical PCB). Press/release
polarity verified against the TCA8418 datasheet directly, since it
disagrees with the Adafruit library's own header comment. Backlight is
direct LEDC PWM on GPIO 46, using this framework version's actual
channel-based API (confirmed before writing the call, not assumed).
PagerEncoder.{h,cpp}: true A/B quadrature decode via a standard
Gray-code transition table, ISR-cheap-arithmetic-only like
TDeckTrackball's pattern (that driver's 4 discrete direction pins
aren't actually quadrature, so its exact shape doesn't transfer, only
the ISR/snapshot-read style does). Detent scaling (4 raw transitions
per detent) is an unverified assumption pending hardware bring-up.
Both gated on HAS_PAGER_KEYBOARD/HAS_PAGER_ENCODER, already reached by
every env's existing helpers/input build filter. All three envs build
green.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
[env:tlora_pager_lr1121_companion_radio_touch], cloned from the T-Deck
env with board/radio/display/GPS deltas for the LR1121 + ST7796 pager.
Two structural fixes were needed to actually get this to compile:
- pins_arduino.h moves to its own variants/lilygo_tlora_pager_pins/
folder (board JSON's "variant" field updated to match). PlatformIO's
arduino-esp32 build script unconditionally compiles every source
file under board_build.variants_dir/<board.variant>/ as a separate
framework-level library with none of our own lib_deps -- since this
board (unlike T-Deck/Heltec, which point at framework-bundled
variants) has no framework-bundled variant of its own, our
variants_dir override was colliding with our own board-glue files
and failing to find Wire.h/RadioLib.h.
- CustomLR1121Wrapper.h's bare quoted includes of RadioLibWrappers.h/
LR11x0Reset.h only resolve for core-lib files living in the same
directory as their targets; switched to angle-bracket includes with
the full core-lib subpath.
Also required one small, forced addition to UITask.cpp: its display
class selection only recognized the Tanmatsu vs. everything-else split,
which made this env's own hard compile gate unreachable. Added a single
#elif arm for the pager's display type: no other UITask.cpp behavior
changes, indev/resolution/layout wiring is still a later milestone.
All three envs (Heltec V4, T-Deck, pager) build green.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
ST7796LCDDisplay implements the core's DisplayDriver interface on
TFT_eSPI for the pager's 480x222 landscape panel -- the first real
TFT_eSPI consumer in this codebase, since ST7789LCDDisplay (the sibling
this was modeled on) turns out to be Adafruit_GFX-based for both
Heltec V4 and T-Deck. Every TFT_eSPI call was verified against the
pinned library source rather than assumed.
Backlight is the AW9364 stepped pulse-dimmer, wrapped directly in the
display class via SensorLib's AW9364LedDriver (already a dependency)
rather than a hand-rolled pulse timer, exposing a pct-based
setBrightness()/getBrightness() hook for a later UITask.cpp wiring
pass.
Also adds a compile-time guard for CGRAM_OFFSET: this panel's 222px
glass is narrower than the ST7796 controller's native GRAM, and
TFT_eSPI only applies the required coordinate offset when that flag is
set. Without the guard, a missing flag would build clean but silently
shift every frame by 49px.
Nothing references these files yet, so the shipping T-Deck/Heltec V4
envs are unaffected.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
TLORA_PAGER_PORT.md is the living status/decisions/risks tracker for
the T-LoRa Pager port; TLORA_PAGER_PORT_MILESTONES.md breaks the work
into ordered, agent-executable milestones with gates. Kept off main
until the port is further along.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
CustomLR1121{,Wrapper}.h are adapted from the core fork's CustomLR1110
pair (upstream MeshCore has no LR1121 support yet to crib instead).
target.cpp's radio_init() spells out the LR1121 bring-up sequence
directly since there's no std_init() helper for this chip: begin()
with the NodePrefs-driven params and TCXO voltage passed straight in,
the DIO5/DIO6 RF-switch table, and a CRC-length override to match the
other radio wrappers' wire-protocol framing. Nothing references these
files yet, so the shipping T-Deck/Heltec V4 envs are unaffected.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
New board: ThinkNode_M9_companion_radio_touch env, variants/thinknode_m9/
(board glue, LR1110 target, keyboard driver, partitions, M9_PORT.md pin
reference), boards/thinknode_m9.json, device_caps HAS_THINKNODE_M9 block,
HAS_M9_KEYBOARD input path in the touch UI (d-pad focus nav, tab jump keys,
QWERTY typing).
Based on a contributed bring-up patch by ded (SalishMesh), who also remote-
tested every build on a preproduction unit. Hard-won fixes on top of the
patch, each verified on hardware:
- LR1110 radio init -706: preprod chips run transceiver FW 0x0303, which
rejects DriveDiosInSleepMode (0x012A, added in FW 0x0308) that RadioLib
7.x sends unconditionally during begin(). New build-time patch
scripts/build/patch_radiolib_lr11x0.py tolerates it on old FW;
radio_init() prints the chip's device/FW/errors either way.
- TCXO: DIO3 at 3.3 V (the patch's active-oscillator/tcxo=0 theory gave
-707 on hardware).
- Display: rotation 1 (not 3), plus the missing landscape s_ui_rotation
override so LVGL renders 320x240 instead of portrait-into-landscape.
- Keyboard: the controller is a separate ESP32-S2 I2C slave (0x6C) with
addressed registers - reads select register 0x01 first (bare reads
return the HW-version register forever). Boot probe logs the
controller's HW/FW; backlight duty register wired.
- USB pad release: the keyboard bus lives on GPIO20/21 = the S3's native
USB D-/D+ pads, owned from reset by the ROM USB-Serial-JTAG (D+ pullup
on SDA). M9Board::begin() releases them before any bus init.
- GPIO35-37 are octal-PSRAM pins on the S3R8 - never drive them (the
patch's "SD CS = 36" misread package pin 36 = GPIO48; SD is on the
shared SPI bus, support returns with PIN_SD_CS=48 later).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Board class, pins, and partition table for the LilyGo T-LoRa Pager
(ESP32-S3, LR1121 radio variant), modeled on the T-Deck's shape.
TLoraPagerBoard brings up the XL9555 power-rail expander and BQ27220
fuel gauge over I2C (this board has no VBAT ADC divider). Nothing
references these files yet, so the shipping T-Deck/Heltec V4 envs are
unaffected; radio/display/env wiring land in follow-up commits.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
- History flush runs on the core-0 worker (snapshot + storage-busy gate +
bounded shutdown wait): kills the V4's multi-second ui:hist stalls and
the refuses-to-wake-after-message symptom (SPIFFS GC off the UI thread).
- BLE + Wi-Fi runtime enables share the boot co-init heap guard (50 KB
free + 20 KB block): refuse with a toast + revert the switch instead of
panicking (BLE) or silently not starting while claiming on (Wi-Fi).
- Internal DRAM: static footprint 95,365 -> 64,908 B (30.5 KB freed):
serial_interface -> PSRAM; 23 keyboard layout maps const'd -> flash;
nine rings/tables -> psAlloc PSRAM (UITask/TouchPrefsStore/MyMesh);
unused TinyUSB MSC + DFU-mode class drivers stubbed out of the link
(usb_unused_class_stubs.c) — CDC + runtime-DFU untouched, double-flash
verified over the stubbed stack.
- DataStore prefs writes are crash-safe (write .tmp, swap) with a
self-healing loader (.tmp recovery) and a truthful save result up to
the profile-name toast — fixes the boots-with-default-name-once report.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consumes the new core (buffered receive drain task + RX counters, #13
stock payload layout, GPS-date guard). Fork side:
- TouchPrefs v33 rx_queue, DEFAULT ON for the test channel (fresh
installs + a one-time migrate for upgraders); opt-out toggle
'Buffered receive (experimental)' in Radio & Mesh, applies live.
- About live tier shows LoRa RX heard/read/err/late-lost/qdrop, the
field evidence for the missed-messages fix.
- Spectrum parks/resumes the drain task around raw radio use; MyMesh
setParams sequences hold the radio mutex.
- TanmatsuLoraRadio: no-op API stubs (P4 keeps its own RX path).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- MqttBridge: reconnect() (blocking DNS + TCP + CONNACK, 2-10 s vs an
unreachable broker, every 15 s) now runs on a one-shot core-0 task;
_connecting hands the client between threads. Broker socket writes get
the same zero-timeout writability probe as the TCP/WS companion
servers, so a wedged broker fails a publish instead of stalling loop().
- About: ESP.getSketchSize()/getFreeSketchSpace() esp_image_verify-walk
the whole ~3 MB image (~190 ms each); the 1 Hz refresh cost ~390 ms/s
and flooded the stall ring (the ui:sbar entries field testers
photographed). Computed once and cached.
- Favorites: adding past the 16-cap now toasts 'Favorites full (max 16)'
instead of the wrong 'Removed from favorites'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the beta_31 periodic 1-2s freezes with Wi-Fi on: every
received mesh packet pushes an RX-log frame to all connected TCP/WS
companion clients from the loop thread, and writeAllBytes called
WiFiClient::write() unguarded — against a stale peer (phone asleep,
half-open socket) that blocks in the framework's 1s select() retries.
The 120ms app timeout only bounded attempts, not the first call.
- TCP+WS servers: probe writability (zero-timeout select on the client
fd) before every write; lwIP reports writable only with ~2.8KB free,
a whole frame is ~180B, so probed writes complete instantly.
- Reap clients whose writes fail continuously for 10s (half-open
sockets previously stayed "connected" forever, eating the write
budget on every pushed frame).
- Chat-history ring: chunked writer + wider flush spacing (same
treatment saveContacts got in beta_30).
- Stall tracer: 16-entry ring of >=0.2s loop stalls, shown in About.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Default chat list: 56px rows with a round initials avatar in the thread's
FNV-hue signature colour, last-message preview (getThreadLastMessage ring
accessor), time top-right, unread pill + @ below; compact chat mode keeps
the dense 34px contact-style rows (layout mode mixed into the rebuild sig)
- Per-channel avatar emoji: 'Chat icon' in the thread-settings sheet (tap =
emoji sheet in new pick mode, long-press = reset to letters); persisted in
a chmute-style keyed blob (touchPrefsGet/SetChannelEmoji, 24 x 48B)
- Fix: name/preview overlap (LONG_DOT labels need fixed one-line heights)
- Thread-sheet grid gap 6->4 so the 7th channel cell keeps one screen on T-Deck
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- DataStore::saveContacts packs byte-identical 152B records into ~5KB chunk
writes (was ~12 tiny FS writes per contact: 2.6-3.0s at ~570 contacts on the
loop thread — the #82 sluggishness); falls back to per-field writes on alloc failure
- Contacts 60s age tick updates the Heard labels in place (was a ~1.3s full
teardown+rebuild); advert-driven rebuilds coalesced to one per 2.5s
- Spectrum: rolling waterfall row + live trace every quarter sweep (~250ms),
8-bin/8ms sweep chunks (~20% faster full sweep); trace Y range now tracks the
live min/max — instant expand, 3dB/sweep contract, 30dB minimum span
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sticky top banner asking would-be donors to help the MeshCore founders instead — links to the blog post (Learn more) and the givealittle donation page (Donate). Dismissible (x + localStorage), and it shifts the corner FAB rails down while shown so they don't collide with the close button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Rooms: re-enable the keep-alive pinger (checkConnections), arm a 128s
keep-alive on LOGIN_OK (servers send 0), blank-relogin self-heal while a
room chat is open, 'Log in again' in the room sheet, read-only warning.
Root-caused against simple_room_server: 3 unACKed pushes freeze a client
until the server hears from it — and we never pinged (issue #89).
- Burn-in: ST7789 SLPIN/SLPOUT on screen-off (backlight-off alone kept the
panel driving the static image), lit-lock dim guard on all boards, 10s
notify-wake re-dim, lock clock pixel drift.
- wyvern.red batch: scroll-keys map zoom, backspace exits empty composer,
Reset path in the chat sheet, compact chat toggle (TouchPrefs v31),
clearer delivery glyphs + tap-to-resend on failed sends.
- OTA: hide Install-update when the running image's physical address falls
outside the esp_ota running slot (mixed/legacy tables; beta_21 coredump).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
touchHasOtaUpdateSlot() checked only that a spare OTA partition exists
(esp_ota_get_next_update_partition != NULL). Under bmorcelli's Launcher a
spare slot exists but is smaller than our image, so the check passed, the
Install-update button showed, and Update.begin() failed "won't fit: Bad Size
Given" (reported by Keith/KM1USR on beta_24 under Launcher). Require the target
slot be at least as large as the running slot; the whole OTA block (Install /
previous-version / beta toggle, already gated on this) then hides on Launcher
installs, where updates go through the Launcher. The update-available notice
still shows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The T-Keyboard's C3 firmware takes a 2-byte I2C command [0x01, level 0-255],
but the level the UI drove it at was hardcoded 0xFF, so the backlight was
always 100% when lit. Adds a Keyboard backlight control at the top of
Settings -> Keyboard: a live slider plus Off/25/50/75/Max one-tap presets,
scaling the lit level for the on and auto modes (and the new-message flash).
The percent persists in the existing kbd_bl pref shared with the Tanmatsu
slider; adjusting it while the mode is off flips the mode to on, mirroring
the Tanmatsu control-centre behaviour.
Three latency lessons learned on the way:
- pct->duty is gamma-2 mapped: perceived LED brightness is roughly
logarithmic in duty, so a linear map felt coarse at the dim end.
- the NVS save is debounced (one-shot timer re-armed per change): the pref
setter rewrites the whole TouchCfg blob via cfgFlush(), which can block
the UI thread for a good fraction of a second per tap.
- backlight requests now flush on every touch-poll tick (~8 ms) instead of
riding the ~32 ms keyboard-scan cadence, and the Settings controls request
the level directly instead of waiting for the next UITask::loop pass - so
the control feels as immediate as the screen-brightness slider (whose
ledcWrite hits the ESP32's own PWM peripheral synchronously).
Signed-off-by: Yaz <yazutsu@users.noreply.github.com>
Decoded a T-Deck coredump (Task WDT, loopTask stuck in spiffs_gc_clean under
DataStore::saveContacts): a contact save on a churned SPIFFS can trigger a
multi-second garbage-collection pass that starves core 0 and trips the task
watchdog, giving a reboot loop.
- Route contacts/channels to SD when a card is present, even on upgraded
devices whose data is still on SPIFFS (main.cpp mounts SD every boot; if not
fully adopting SD, store.setSecondaryFS(&SD)). DataStore::begin() migrates the
existing /contacts3 + /channels2 to the card once; identity/prefs stay put.
- Move the ref-counted WdtHeavyGuard into a shared header so the core
DataStore::saveContacts shares one count with the UI history/backup saves, and
wrap saveContacts with it (guaranteed fix for card-less devices).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recommend keeping a card inserted on SD-capable devices (T-Deck, Tanmatsu) so tiles,
contacts and chat history live on the card and avoid the limited/less-reliable internal flash.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TJ Downes flagged that provider device names must match MeshCore's canonical names exactly
or they list as NEW devices instead of folding into the existing tiles. Rename to the official
'Heltec v4 + Expansion Kit (Touch)' and 'LilyGo T-Deck' (verified against the live config), and
add the new optional 'description' blurb shown on the provider badge tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Explains the microSD tile-pack layout (/maps/osm or /tiles, z/x/y, PNG/JPEG), the
'Tiles from SD card' toggle, making a pack, and per-board differences (T-Deck / V4 / Tanmatsu).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Contacts list rebuilds on advert (force past the count-cache) + a count-delta safety
net, so a node discovered off-tab or added by the phone app shows immediately (#73)
- New "Advertise on boot" toggle in the Advertise app, off by default (#76)
- Tanmatsu: cache Wi-Fi map tiles on microSD (the P4's internal FFat metadata layer is
broken — f_getfree/f_stat lie); read tiles via open() not the misreporting exists()/size()
- Tanmatsu: persist contacts/channels/chat on microSD (DataStore secondary FS + chat
history) so they survive reboot; identity/prefs stay on the (working) internal FFat
- Tanmatsu: boot fix (drop Serial.begin re-init of USB-Serial-JTAG), MQTT disabled, popups
fit + scroll, keyboard-nav scoped to the open popup, 2000 contacts, Large UI default
- Emoji/quick-reply Esc/red-X closes the popup not the chat; lock-screen unlock after msg-wake
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two bugs in the manual telemetry request path (the Request button in the
telemetry window):
1. Usually only the second request succeeded. The guest LOGIN and the
telemetry REQ were sent back-to-back, but a repeater drops a REQ from a
sender it hasn't added to its ACL yet, and the ACL entry isn't committed
by the time the first REQ is processed — so the first request got no reply
and the user had to tap twice. Defer the REQ: uiSendRequestAfterGuestLogin()
sends only the guest LOGIN and fires the REQ from onContactResponse once the
LOGIN-OK arrives, by which point we're in the repeater's ACL and a direct
out_path has been learned, so it lands on the first try. Also adds a
single-flight guard on telemetryRequestNow(), holds auto-poll while a manual
request is pending (so it can't clobber the pending reply tag), and disarms
the deferred login on timeout so a late LOGIN-OK can't fire a stale REQ.
2. The status-bar async spinner only blinked for ~1.5s on a telemetry request
(the markMeshRequest window) then froze, instead of blinking for the whole
pending window like a status ping. Its active predicate watched the ping
deadline but not the telemetry timers; OR in the telemetry pending state.
Auto-poll's best-effort chained send and the admin-console login path are
left unchanged.
The signal probe now sends a zero-hop NODE_DISCOVER_REQ control packet
(the standard MeshCore node-discovery that the Ultra / KiekR apps use)
instead of a trace, which repeaters do not answer. Repeaters reply
directly with a NODE_DISCOVER_RESP, captured in onControlDataRecv by
matching the probe tag into _ui_sig_* SNR/RSSI. Thanks to Tarmo for
decoding the exact packet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The device broadcast a PUSH_CODE_LOG_RX_DATA frame to every connected companion —
including BLE — for every LoRa packet it heard. BLE's ~16 frames/sec budget got
saturated on a busy mesh, delaying/dropping the frames that matter: chat messages
(#46) and repeater/room-server admin responses (#54, the "Streamsink" disconnects).
Skip the RX log on the BLE transport in writeFrameToAll; USB/TCP/WS still get it for
the on-device Monitor app. The MeshCore app never consumed it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Landing page: a plain-language Stable vs Beta explainer under the install channel
toggle, covering both install paths (flash a channel here, or flip the on-device
"Get test builds" toggle). Userguide: a new "Updates & test builds" section (how
updates work, the two channels, how to turn on test builds) plus a Get-test-builds
line on the About settings card.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signal probe: replace the beta_21 zero-hop advert (which nothing replies to, so the
signal never updated) with a directed TRACE ping to the nearest reachable repeater.
It retransmits — a real reply we measure — without flooding the mesh. New
MyMesh::uiSendSignalProbe() picks the shortest-path repeater; the reply is captured
silently in onTraceRecv via _ui_sig_probe_tag. Falls back to a zero-hop advert when
no repeater is known. Thanks to Tarmo for the insight.
Contacts (#73 part A): a contact discovered while a companion app is connected over
BLE now refreshes the device's own Contacts list + notification. onDiscoveredContact
fired the UI notify only in the standalone branch — move it out so it always fires,
and flag the list dirty for UITask::loop to rebuild on the LVGL thread.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The beta_21 MQTT bridge is based on @samuelcoustet's PR #71, reworked for privacy
(consent gate, direct messages off by default, payload encryption). Record the
attribution in the release notes + the version.json "what's new".
Co-authored-by: Samuel Coustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two-channel rollout added releases/BETA + latest-beta, but the firmware
vhost only had exact-match handling for the old stable paths. Add:
- location = /releases/BETA -> serves releases/BETA/index.json at the no-slash
path the device requests when 'Get test builds (beta)' is on (mirrors TOUCH).
- location /latest-beta/ -> the beta web-flasher feed (was hitting the 404
catch-all).
Stable paths and all bins under /releases/ were already correct; this only wires
up the new beta directory-index + feed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New: stable/test update channels with an opt-in "Get test builds (beta)" toggle on
OTA devices (switches both the update check and what you install); experimental
opt-in MQTT bridge (consent-gated, payload encryption, direct messages off by
default); Portugal (Narrow) region preset (#74); saved-contacts counter in the
Contacts overflow menu (#72).
Fixed: the signal probe is now strictly zero-hop so a repeater can't re-flood it;
the contact list sorts before capping so the most relevant contacts show, with a
"+N more — search to narrow" footer (#73); the blocked-users list opened from
Contacts shows the two-line title bar with a back button; the Contacts overflow
popup fits the screen again.
Also lands the two-channel release tooling (scripts/release.sh stable/beta modes +
gen-flasher-meta channel arg), the docs site, and the MQTT reference decryptor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New: Spectrum analyzer app; custom notification sounds (SD WAV per event);
saved Wi-Fi networks with auto-join; unified chat settings screen; per-channel
mute; Heltec V4.3 high-gain receiver (FEM LNA) toggle; Apps button on the V4
home screen; Blocked list reachable from Contacts + chat settings; Portuguese
(BR) UI language.
Fixed: touch-screen flood adverts now region-scoped so region repeaters relay
them (#68); channel sends matched by name so a message can't go out on the
wrong channel's key; opt-in "scope direct messages to region" so room-server
logins work through a region-only path (#64); Wi-Fi scan no longer cancelled
by the reconnect retry (empty SSID list).
Under the hood: flood-scope rework (channel vs direct floods); Spectrum owns
the SX1262 while open and restores mesh RX on exit; wifiScanIsActive() gate;
FEM KCT8103L LNA wired; TouchPrefsStore migrations for the new prefs. Adds the
previously-untracked device_caps.h.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix LVGL scroll-momentum use-after-free reboot when a scrollable overlay
(emoji / quick-reply / add-channel / channel sheets, Wi-Fi scan, pickers,
app drawer) is freed mid-throw: new closeScrollOverlay() helper
(lv_indev_wait_release + lv_obj_del_async). Root-caused from field beta_18
coredumps.
- Radio & Mesh settings: airtime / duty-cycle readout + "Answer telemetry
requests" toggle, grouped into RADIO / MESH / SIGNAL sections.
- Accent popup dismissed on Enter-to-send (T-Deck physical keyboard).
- Heltec V4: Expansion-Kit Home env line + chart flow below the stats line
so they fit the 240x320 screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two firmware-only additions surfacing capability already in the stack:
- Airtime / duty-cycle readout: the radio modal exposed airtime_factor only as a raw
'AF' number. Add a derived line — '≈ N% max duty cycle · ~M ms / 40-byte packet' —
computing duty% from MeshCore's af=(100/duty)-1 and the per-packet time-on-air from
the Semtech closed-form (new loraToaMs helper). Lets EU-868 users see compliance at
a glance with no measurement.
- Answer telemetry requests toggle: MeshCore already gates telemetry responses on
_prefs.telemetry_mode_base/env (0=DENY/2=ALLOW_ALL) but it wasn't in the touch UI.
Add a switch (new UITask::setTelemetryAllow) that sets battery+environment modes;
location telemetry keeps its own separate setting so a simple 'allow' can't leak
position.
(RX boosted gain was already a switch in Experimental settings; per-packet RSSI/SNR
is already in the Monitor; region presets already bundle SF/BW/CR — so those research
items were already shipped and not re-added.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- List the Tanmatsu (ESP32-P4) in the hero + meta description.
- Add a Tanmatsu box to the standalone installer: no USB flashing — install from the Tanmatsu app store on-device — plus a link to Nicolai Electronics' shop. Boards grid now auto-fits 3 cards.
- Replace the Telegram floating button with Discord (discord.gg/T8X2UaQ7Sm).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Tanmatsu's LoRa modem lives on the C6 coprocessor, shared with the Tanmatsu OS's own
LoRa settings. pushConfig() no-op'd once RX polling started (the remote link can block
under RX load), so a runtime frequency/SF/BW/CR/power change from the wadamesh radio
settings only updated _cfg and took effect on the NEXT REBOOT — the radio stayed on
whatever the OS or boot last set. That is the 'change the wadamesh frequency but receive
nothing on it' report, and why wadamesh's settings never overrode the Tanmatsu OS's.
Now a runtime change marks _cfg dirty and recvRaw() applies it on the RX-poll thread,
between polls: drop to STANDBY (so the link isn't under active-RX load and lora_set_config
can't block), push, re-arm continuous RX. wadamesh's settings now reach the modem live
and win over the OS config. The '0.00001 MHz' the OS page showed is the C6 frequency
corrupted by the two configs fighting; wadamesh consistently owning the config should
clear it — needs on-device confirmation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b986f49 moved the_mesh to PSRAM and changed MyMesh.h's extern to 'MyMesh&', but the
Tanmatsu has its OWN main.cpp (tanmatsu/main/main.cpp, separate from src/main.cpp) that
still declared a plain 'MyMesh the_mesh' global — a conflicting declaration that broke
the P4 build (only the S3 envs were built for b986f49). Apply the same placement-new-
into-PSRAM + reference pattern here; the P4's 32 MB PSRAM gets the same DRAM win.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Settings landing tiles centred their label in card_w - 80 px (40 px margins each
side). On the T-Deck's 2-column landscape layout that left only ~64 px, so a single
word ('Bluetooth') split mid-word and 'Radio & Mesh' / 'Quick replies' wrapped to two
lines. Lay the label out to the RIGHT of the left accent icon and centre within that
region (~100 px usable) instead of symmetric centring — every English category name
now fits on one line, unsplit. Wide cards (V4 portrait) are unaffected; long
translations still wrap at word boundaries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
the_mesh was the single biggest static internal-DRAM consumer at 42 KB (dominated
by the MAX_CONTACTS=100 ContactInfo array inside it). Internal DRAM (~320 KB) is the
scarce resource on the ESP32-S3 — Wi-Fi/BLE DMA + heap fight over it, and the V4 was
hitting ~99% on map-tile load (issue #47b). PSRAM (8 MB) is plentiful.
Place the whole the_mesh object in PSRAM via placement-new into a heap_caps_malloc
(MALLOC_CAP_SPIRAM) block and bind a reference, so every the_mesh.foo() call site is
unchanged. The constructor still runs at the SAME static-init point as the old direct
global (PSRAM is already up then), so timing and behaviour are identical; only the
address moves off internal DRAM. Falls back to internal RAM if PSRAM is absent.
ESP32-only (STM32/RP2040 keep the plain global). The contacts array rides along
inside the object — no vendored-core change.
Measured on heltec_v4 (.dram0.bss): 124160 -> 82000 B = ~41 KB internal DRAM freed.
Verified on the T-Deck: clean boot, [BOOT] mesh ok, reason=0, zero panic markers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inline sort button sat after the empty-list early-return, so a Discovered
list with no entries (the common case when auto-add is on) never showed it, and
even when populated it didn't read as a control. Move it to a ⇄ icon button in the
modal header next to the cog — always present, same proven render path as the
cog/purge, opens the same sort sheet. Purge shifts one slot left to make room.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regression from the previous commit: making the Sort sheet stay open (for the
asc/desc flip) meant picking a sort no longer dismissed it, and the close-X was a
no-op because it reused the backdrop callback whose target==current_target guard
(meant to ignore child taps on the backdrop) rejected the tap on the badge.
- ctSortOptCb applies the choice and closes the sheet again; flip a mode's
direction by re-opening and tapping the active row (the ▲/▼ arrow shows it).
- The option-sheet close-X now uses an unconditional close handler instead of the
guarded backdrop one, so it always dismisses (also fixes the Filter + Discovered
sort sheets, which share the shell).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Emoji: bake 4 more Noto colour glyphs (soccer, american football, moai,
transgender flag) and add them to the picker. The trans flag is a ZWJ sequence
the single-codepoint imgfont lookup can't render directly, so its combined Noto
image is keyed on the lead codepoint (U+1F3F3) with the trailing symbol mapped to
the zero-width glyph — the whole sequence renders as the one flag. Only the 4 new
glyphs are baked + spliced (scripts/build/add-emoji.py); the existing 252 stay
byte-identical (no Noto-main art drift).
Chats header: group the three actions at the LEFT edge in the order + (add) /
✓ (mark all read) / QR (share), and draw a real baked QR glyph (qr_icon.h, from
scripts/build/gen-qr-icon.py, recoloured to the text colour) instead of the old
LV_SYMBOL_IMAGE 'picture' stand-in.
Contacts sort: every sort option now toggles ascending/descending — re-tapping the
active row in the Sort sheet flips it and shows a ▲/▼ arrow. New 'Nearest first'
(distance) sort orders by distance from our GPS and excludes contacts that have
shared no location.
Discovered list: gains the same sort sheet (Recently heard / Name / Nearest, each
asc/desc) via a sort button at the top of the list; distance likewise excludes
no-location entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
renderMapTiles already queues every visible tile (edges + corners) on pan,
and queueTileForFetch marks each one 'seen' the instant it is queued. But the
fetch worker only cleared that mark on success — so a tile whose download hit a
TRANSIENT failure stayed flagged forever and was never re-queued. The heap-
safety gate is the common trigger: during a pan the render thread is decoding a
burst of tiles (heavy internal-DRAM use), so the worker's <12 KB-free skip trips
for several of the freshly-queued edge/corner tiles, which then sit blank until
the user forces a full 'reload tiles'. Wi-Fi blips, a full tile cache and proxy
5xx/timeouts had the same one-shot-and-stuck behaviour.
Add tileFetchForget() and drop the 'seen' mark on every transient failure
(heap gate, Wi-Fi down, cache full, negative/5xx/408/429 HTTP, disk write
hiccup) so the next render miss re-queues the tile automatically. A permanent
4xx rejection still stays marked so we don't re-hammer genuinely-absent tiles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The companion link is a BINARY protocol over Serial (USB-CDC). Unconditional debug
prints in operational paths — [ROOM] login send/resp, [TILE] fetch, [JPG] decode —
interleaved text into that stream and corrupted it. The MeshCore app then throws
'Bad state: Streamsink is bound to a stream' (#54) and the PC-app connect (#25) and
member-room login (#23) fail. Route them through a gated WIRE_DBG macro
(COMPANION_WIRE_DEBUG, off in release) like the existing SYNC_DEBUG_PRINTLN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the 240px V4 bar the node-name window (74px) left only ~4px before the clock, so a
long name visually ran into the time. Shrink it to 66px for clearance. (Best-effort
without a V4 on the bench; clock position may need a follow-up once verified on-device.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
beta_15 preserved scroll on close, but turning keyboard-nav ON still jumped to the
top: the focus group was empty (cleared while nav was off) so navMaybeRebuild had no
focus to keep and fell to the first item, scrolling there. Add a one-shot focus hint
that pins focus to the switch you just toggled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'The device restarted after a crash' re-appeared after a manual reset / power-cycle:
the prompt gated only on a coredump EXISTING in flash, and the dump persists across
resets until exported. Gate crashReportMaybePrompt() on this boot's reset reason
(panic / task-WDT / int-WDT) instead. The dump stays exportable from About + esptool.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decoded coredumps (device + the beta_16 ELFs) are all task-watchdog panics:
CPU0=IDLE0 CPU1=IDLE1, core 0 in spi_flash_op_block_func. A legitimate-but-slow
flash burst (SPIFFS GC, or a bulk DataStore save = thousands of writes) parks both
cores with the cache disabled, starving both IDLE tasks past the ~5s default so the
TWDT panics. The burst can't be chunked under the limit easily and the per-core
WdtHeavyGuard only covers core 0, so re-init the TWDT with a 20s grace (still
catches a genuine multi-second hang) instead. esp_task_wdt_init re-init reconfigures
the running TWDT (old API on this arduino-esp32).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#33: tdeckKeyboardSetBacklight only marked the I2C write dirty on a value CHANGE, and
the cached default is 0. A reflash resets the ESP32 but not the keyboard's C3, which
keeps its lit backlight, so the boot 'off' request (0==0) was never sent. Force the
first write so the C3's retained state is always overwritten.
#55: a hard-locked LIT panel never dimmed (clock burning in) because reveals/taps kept
_last_input_ms fresh. Add a burn-in guard: arm _lock_lit_ms only on a real off->lit
transition and dim the locked panel after a bounded window regardless of the screen
timeout or held/ghost touches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Catch a comma/em-dash/over-length description locally and abort, instead of
discovering it as a failed verify_metadata check on the upstream PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The launcher store's verify_metadata check restricts description to
^[A-Za-z0-9_\- .!@#$%^&*()/\\]+$ (no commas, no em-dash). Reword so the
template passes on its own instead of needing a manual PR edit each release.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Vol+ short = top-bar dropdown toggle; long hold = sound master toggle
- Message-notification LED: flash on new message, soft green glow on unread; toggle in Display settings
- Backups use the internal FFat partition (export/scan/import) + Import button on the Backups page
- microSD browsable in the file manager (P4 SDMMC slot 0; the C6 radio's slot 1 untouched)
- Status-bar icon overlaps fixed (charging-slide + language indicator now SC-scaled with the cluster)
- Vol- screen sleep/wake/lock (screen-lock work)
- Companion link: TCP server start + BLE bond NVS persistence + app-sent channel-message mirror
All Tanmatsu changes gated #if defined(HAS_TANMATSU); T-Deck/V4 bins stay byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main committed tanmatsu/esp-idf + esp-idf-tools as symlinks pointing at their
own absolute path (mode 120000). A fresh 'git checkout main' materialises those
self-loops over the real local ESP-IDF install — breaking the build ('too many
levels of symbolic links' at source export.sh) and deleting the gitignored IDF.
Untrack + gitignore them like the build setup expects; the project-local
ESP-IDF is installed locally, never committed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reworks the earlier-version OTA UI from three inline buttons into a single
'Install a previous version' button that opens a popup of the available older
betas; picking one shows an are-you-sure confirm (reusing showConfirm) before it
closes and hands off to the existing download/verify/reboot flow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a 'Reinstall an earlier version' section to the Wi-Fi OTA UI (native
dual-OTA boards only): up to three buttons for beta_(latest-1..3), each flashing
that exact published bin via the same download/verify/reboot path. The worker URL
is parameterised on a target version; the existing 'Install update' button now
routes through the same otaStartInstall(latest) helper. Buttons auto-hide when
there aren't that many earlier releases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clock's runtime placement used the wide-bar -142 offset on every board,
overriding the V4's intended -126 build slot. On the narrow V4 portrait bar that
pulled the clock left into the width-capped node-name window — the long-standing
'clock overlaps the name' issue. Make the placement bar-width-aware: <300 px bars
(V4) use -126/-94, wide bars (T-Deck/Tanmatsu) keep -142/-110.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refines the Expansion-Kit UI (#29) so it doesn't intrude on a bare V4: the
Sensors tab and the Home environment widget now appear only when an actual
environment sensor (BME280 / GXHTV3) is detected at boot — battery and GPS no
longer count, so a V4 without the Expansion Kit board shows the normal 5-tab
layout and a clean Home (no empty "Batt ----" / "Temp ...." lines). Adds a
"Show Sensors tab" toggle in Display settings to hide it even when sensors are
present (applies on restart). Tab indices resolve at boot so Settings shifts
back to slot 4 when the Sensors tab is absent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Heltec V4 Expansion Kit local-sensor UI, board-gated behind HAS_EXPANSION_KIT
(V4 touch env only — T-Deck/Tanmatsu unaffected): a Sensors bottom tab
(atmosphere / GPS / battery + history charts), a Home env summary widget, an
Expansion Kit card in Device settings, and a Local Sensors detail page. Reads
on-board MeshCore sensors (BME280, SHT4x, battery, GPS, buzzer) via the standard
sensor/telemetry API — no new hardware drivers.
Mesh console: enable the existing Terminal on the V4 (HAS_TOUCH_UI), renamed
"Mesh console", with wifi/tcp/ble on-off + ota status quick commands. The
SD-backed file-manager pieces stay T-Deck-only (V4/Tanmatsu get a no-op path).
Tab indices are conditional so non-Expansion boards keep 5 tabs (Settings=4) and
the V4 gets 6 (Sensors=4, Settings=5). The PR's GPS-nav/waypoint and weather
pressure-alarm bundles were intentionally left out.
Ported from PR #29 by @samuelcoustet, re-applied against current main.
Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standalone dev tool (pyserial) that drives the companion binary protocol over a
serial port — DEVICE_QUERY/APP_START, battery/storage, stats, telemetry with a
Cayenne-LPP decoder, and meshcomod CLI commands. No firmware coupling; dev-only.
Ported verbatim from PR #29 by @samuelcoustet.
Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds French (AZERTY), Dutch, German (QWERTZ), Spanish and Italian layouts — both
the on-screen keyboards and the T-Deck physical-keyboard maps (KeyboardLayoutId
FR..IT). Picking a UI language now auto-enables and selects the matching keyboard
layout, so choosing French gives AZERTY without a separate trip to keyboard
settings. Accented letters still come from the long-press accent popup; no new
fonts required. Extended the settings layout-enable list to match the larger enum
(was a latent out-of-bounds once the layout count grew).
Ported from PR #29 by @samuelcoustet — the layout data applied cleanly; the
UITask wiring was re-applied by hand against current main (the PR was cut from
beta_12 and no longer merges).
Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapsible version picker in the standalone install guide: a dropdown of the latest 4 betas (from the GitHub releases API — the firmware-host listing has no CORS), rendered release notes per version, and selecting a version re-points both Install buttons at that build (client-built manifest -> the CORS-enabled firmware-host -merged.bin). Defaults to latest; switching back restores the /latest/ manifest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-enables the over-the-air update that #74 stubbed out — the block then was
undersized 1.5 MB OTA slots, and both touch tables are now 3.875 MB A/B. Wi-Fi
self-update runs again on natively-flashed boards (every V4 + standalone T-Deck);
Launcher single-slot installs and the Tanmatsu (AppFS) keep the manual path.
- Gated on touchHasOtaUpdateSlot() (a spare A/B slot exists) — already drives the
update badge, so Launcher/Tanmatsu never reach the install path.
- Self-contained Arduino Update flow in the touch app (the vendored core's OTA
allow-list is meshcomod-only, so we don't route through board.startHttpOtaFromUrl).
Fetches the immutable releases/TOUCH/beta_<N>/<board>.bin over plain HTTP from
firmware.wadamesh.com on the tile worker (off the UI thread); progress polled by
a UI timer.
- Update.begin(len) does the spare-slot select AND the size check, so a legacy
too-small slot fails cleanly and falls back to the manual flasher message.
- Verify (MD5) + set-bootable via Update.end(true), then rebootDevice() (saves
chat history first). A corrupt/short download leaves the running slot intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The grid_items count missed the conditional "Show on map" button,
causing the card height to be calculated short when the contact has a
GPS fix. Additionally, on landscape displays (sh=240) the backdrop is
only 218px, which repeater cards with many buttons already exceeded.
Fix both: pre-check the GPS condition and include it in grid_items,
and cap the card's displayed height to the available backdrop space
with vertical scroll enabled when the content overflows. The close
badge is FLOATING so it stays anchored regardless of scroll position.
Idle power-save (light-sleep) advertised itself with a separate crescent-moon
glyph in the status bar. Replace it with an iPhone-Low-Power-Mode-style amber
tint on the battery icon (iOS systemYellow): clearer, and frees the bar slot.
The moon stays in the tree but hidden, keeping the layout and its tap handler.
A single applyBattColor() helper owns the battery colour so the map-chrome
restyle (black/white over light tiles) and the per-tick refresh never fight —
the amber overlays whatever base colour the context wants when power-save is on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
If lv_layer_top() has a visible widget at the search coordinate (i.e. a
modal or dialog is open), stop the layer search there regardless of whether
a scrollable was found. Prevents the content behind the dialog from being
scrolled when the dialog itself has no more room to scroll.
Closing a settings item / modal / chat with keyboard-nav on snapped the
underlying list back to the top. navMaybeRebuild rebuilds the focus group when
the overlay closes, but the previously-focused element lived inside the overlay
and is gone, so focus fell to the first list item and the list scrolled up to
show it. Affected every screen because every overlay closes through this path.
Stash the page item that had focus when leaving for an overlay/chat, and restore
it on return — so focus, and the scroll position, land back where they were.
Falls back to the prior behaviour if that item is genuinely gone. Most visible on
beta_14 (keyboard-nav defaulted on); new installs now default it off.
Reported by ZorkaTheDog (#45).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a global region scope is configured, the per-channel override input
now shows it as greyed-out placeholder text instead of the generic "#region",
so users can see at a glance what scope an empty field will inherit.
Falls back to "(no default)" when no global scope is set.
A DM sent from the MeshCore companion app went out over the mesh but never
appeared on the T-Deck: the wire protocol deliberately doesn't echo sent
messages back to clients, so the on-device UI (a separate consumer) never saw
app-originated sends. Add an appSentMsgToContact() UI hook (default no-op for
non-touch UIs); the touch UI mirrors the message as a local outgoing bubble in
the recipient's thread. Called from the CMD_SEND_TXT_MSG handler on first
handling only (a client/transport retry was already mirrored).
Still open: the receive direction (T-Deck-received messages reaching the app) —
queueMessage already pushes RESP_CODE_CONTACT_MSG_RECV_V3 to all transports, so
that half needs a frame capture against the real app to pin down.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverses the v15 force-on. New installs and existing T-Deck/V4 devices now
default to keyboard-nav OFF (a v18 one-time migration flips existing installs
to off); the user's later explicit on/off then persists. The Tanmatsu is exempt
— it has no touchscreen, so keyboard nav is its only input and stays on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The touch-to-edit path keyed off navFocusCb (a focus CHANGE), so a field that was
already the selected one — e.g. the Profile node-name field, auto-selected on
entry — couldn't be made typable by tapping it (the tap doesn't re-focus, so
navFocusCb never fires). settingsFieldFocusCb already runs on CLICKED; now a real
tap also sets edit mode, so a selected field becomes typable on tap. Keyboard-nav
onto a field still starts in navigate mode (press Enter).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous fix returned 0 from the recursion for NAV_SKIP/HIDDEN subtrees, but
the parent loop still iterated over the added/removed child (h = h*prime + 0),
so the child COUNT still moved the signature. The accent box + @-mention picker
are created/deleted on every keystroke, so the count flipped each time → rebuild
→ edit-mode reset → typing broke mid-word (and Esc then read as navigate-back,
exiting the screen). Now skip those children in the loop entirely so they never
affect the signature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Non-chat text fields (add-channel, Wi-Fi, room login, settings modals) live on
the top UI layer, so keyboard nav is rooted there (useTop). The accent box and
@-mention picker are ALSO on the top layer, and they were counting toward
navTreeSig() — so each keystroke that showed/hid the box flipped the signature,
triggered a navMaybeRebuild(), and that rebuild's remove-all + re-focus churn
reset edit mode, dropping you out of typing mid-word.
- navTreeSig() now skips NAV_SKIP_FLAG subtrees (accent box, mention picker), so
those passive overlays never trigger a rebuild.
- navFocusCb() ignores the transient f==null a rebuild's remove-all emits, so any
other rebuild can't reset edit mode either.
(The T-Deck never used the keyboard mirror — kbMirrorActive() is false there; the
mirror is V4-only for the on-screen keyboard.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- @-mention picker (issue #42): typing "@" in a chat composer pops a touch list
of matching contacts (prefix-filtered, up to 6); tap one to insert "@Name ".
Passive NAV_SKIP_FLAG overlay like the accent box; touch-select for now. Takes
priority over the accent box while a mention is active (composerSuggestRefresh).
- Cursor now reflects edit mode: shown only while editing, hidden while a focused
field is just the keyboard-nav target — a reliable "ready to type" cue
(navSyncCursor() each nav tick).
- Edit mode is decided only on a genuine move to a different field, so a rebuild
that re-focuses the same field (e.g. while the accent box is up) no longer
kicks you out of typing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Keyboard nav: tapping a text field by touch now enters edit mode immediately
(focus came from the pointer indev) instead of requiring Enter. Reaching a
field with the letter-nav keys still starts in navigate mode (press Enter).
- Map: yield to IDLE after each tile in renderMapTiles() so a slow multi-tile
render on loopTask can't starve the task watchdog and panic — fixes a beta_14
TWDT reboot seen during map use (CPU1=loopTask). The download worker already
does the same vTaskDelay(1) on its side.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The keyboard-nav rework gated the physical-keyboard typing target on
s_nav_ta_editing (edit mode). That flag is only ever set while keyboard
navigation is ON (navFocusCb, driven by the nav focus group). With keyboard
navigation turned OFF the nav group isn't maintained, so the flag stayed false
and `ta` was always null — keys never reached the focused field and typing was
completely dead on the T-Deck (and Tanmatsu).
Fix: when keyboard-nav is OFF there is no navigation to protect, so a focused
field is always the typing target (ta = ta_focused). Edit-mode gating still
applies only when keyboard-nav is ON. Affects handleHwKey (T-Deck) + navPump
(Tanmatsu); the V4 (no keyboard nav) was never affected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New "Community videos" section under the install/flasher area: a JS-array-driven grid with a click-to-play facade (youtube-nocookie, lazy iframe). First entry is KDHD Stuff's review; add more by appending to the VIDEOS array.
- Float the quick-links into stacked left/right rails: left = Telegram + a YouTube button that jumps to the videos section; right = bug report + a Documentation button (placeholder — toasts "coming soon") + a Map button to t413.com's MapTileDl downloader.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The boot crash-report prompt + the About-page export button are only useful
where the saved coredump file can actually be retrieved and sent to the devs:
the T-Deck (SD card) and Tanmatsu (FFat, via the launcher file manager). On the
Heltec V4 there's no SD and no user-reachable internal storage, so the prompt
was a dead end. Gate crashDumpCheck() to those boards only — on the V4 the dump
is never detected, so the whole crash-report UI stays off (everything downstream
gates on s_crash_dump_size). The panic coredump still sits in its flash
partition for a USB/esptool pull if ever needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Any command arriving while s_meshcomod_pending_action is non-NONE and
the timeout hasn't expired previously fell through the confirmation
block and was processed normally, letting a second 'tcp off' / 'ble off'
silently overwrite the pending action and lose the original confirmation.
Add a final else clause that blocks the new command and reminds the user
to 'ok' or 'cancel' the in-flight request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- enableTcp() now calls startTcpServer(false) directly instead of relying
on the main loop — fixes TCP not restarting when wifi_started is false
(BLE-only mode, radio toggled off, or cold re-enable from USB).
- Add touchHasOtaUpdateSlot() guard to the OTA button creation block so
the update affordances stay hidden under Launcher (single app slot),
where ota url would fail silently.
- Fix toggleTcpCb alert race: snapshot isTcpEnabled() before the toggle
so the alert text reflects the transition, not a post-action re-read
that could mismatch during OTA suspension.
- Replace global -w with -Wno-deprecated-declarations -Wno-unused-parameter
in both touch environments to keep third-party lib noise down while
surfacing real regressions in OTA, network, and companion paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Post-beta_12 touch-UI fixes (T-Deck / Tanmatsu keyboard nav + composer):
Keyboard navigation
- Chat composer auto-edits on open (cursor shows, type immediately); every
OTHER field stays in navigate mode so the letter-nav keys keep working —
press select/Enter to edit it. Enter (or Esc/X on Tanmatsu) on an empty
composer drops back to navigate mode.
- Menu-bar tab-hotkey letters no longer fire while a text field is the focused
element (fixes a stray tab-jump while typing); directional nav still moves.
- New "Show menu-bar letters" setting (off by default) for the tab hotkeys.
Accent box
- The tap-to-pick accent box no longer steals the keyboard-nav focus group
(NAV_SKIP_FLAG) — it's a passive hint, so typing continues and a keybind no
longer navigates. Ready to wire arrow-key selection later.
Composer
- Dedicated "Special characters" picker (% $ @ # & ... currency/math/accents),
separate from the emoji picker.
- Long-press a sent message -> Resend.
Misc
- 12-hour clock honoured everywhere (top bar, chat bubbles, lock screen), not
just the chat overview.
- GPS UART RX buffer enlarged only when GPS is enabled (restores V4 RAM).
- Derive time-of-day from received mesh message timestamps when Wi-Fi/GPS off.
- Self-record the task-watchdog context on a TWDT reset for the crash report.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Room posts arrive as TXT_TYPE_SIGNED_PLAIN with the author in the 4-byte sender_prefix and a bare body. The display path passed the room contact's name, so every post showed the room name. Resolve the author from the prefix and pass it as the per-message sender via a new newRoomMsgFromPubWithMeta hook; the room stays the thread. Tested on T-Deck Plus.
Signed-off-by: Yazutsu <andrzej@gruziel.pl>
Contact list gains a "Direct (0-hop)" option in the Filter sheet (value 5)
that shows only nodes heard without any repeater hops (out_path_len == 0).
Header chip shows "Dir" when active. Selecting a filter now auto-dismisses
the sheet (matching sort behaviour).
Map options panel gets a "Direct (0-hop) only" toggle that hides both
contact markers and link lines for any multi-hop or path-unknown node.
When enabled in Settings → Keyboard, pushing the cursor past any screen
edge routes the overflow into the nearest scrollable widget rather than
absorbing it. Works on regular tab content (lv_scr_act children) and on
lv_layer_top() overlays such as the app drawer.
Implementation:
- tbFindScrollableAt() hit-tests at the vertical centre of the content
area (avoids status/tab-bar chrome and per-tab headers), searches
lv_layer_top() before lv_scr_act(), then walks ancestors to the first
scrollable with room in the overflow direction.
- scroll_by direction is negated relative to raw overflow (scroll_by(+dy)
moves children down = reveals earlier content = scroll up).
- Scroll amount is capped to lv_obj_get_scroll_top/bottom/left/right so
we never push past the widget's content bounds.
- Preference persisted as NVS key "tb_edgesc" (default off).
- Map, Snake, and emoji-picker modes are unaffected (they return before
cursor mode runs).
The GPS NMEA UART used Arduino's default 256-byte RX ring (~66 ms of slack at
38400 baud). On the busy touch UI a single long LVGL/map frame stalls the loop
past that window, dropping bytes and corrupting NMEA ephemeris subframes — each
loss costs the receiver ~30 s, turning a ~1-minute fix into several minutes or,
with frequent stalls, never acquiring. Confirmed on-device: peak UART backlog
hit 448 B at idle, well past the 256 B default. Bump the ring to 4096 B (~1 s of
slack) before the core opens Serial1 in sensors.begin(). App-side (no core
re-tag); gated to ENV_INCLUDE_GPS so both touch boards get it. A -DGPS_BUF_DEBUG
backlog probe is included (compiled out of releases).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- emoji: bake the beer mug U+1F37A and clinking beers U+1F37B as colour glyphs
into emoji_data.c (now 252) and add the beer mug to the composer picker; the
clinking pair was already listed but only rendered monochrome, now colour.
- status bar: tapping the unread badge (envelope + count) on the left now jumps
to the Chats inbox. The CLICKABLE flag is toggled per-tick so only the badge
state intercepts the tap — every other left-zone state (Back chevron, profile
name, OSM credit, blank) keeps the bar's control-center / Back tap.
- contacts: add a "Notify on new contact" toggle to the Auto-add page
(TouchCfg v8, default ON). When off it suppresses the auto-discovery toast
chip in UITask::notify(); the diag-log line and tab badge are unaffected.
- keyboard: add the Czech accents c-caron, e-caron, t-caron, z-caron, r-caron
and u-ring (plus uppercase) to the long-press / tap accent popup. Glyphs are
already baked in the extras fallback fonts, so no font regeneration needed.
Builds verified on both LilyGo_TDeck_companion_radio_touch and
heltec_v4_tft_companion_radio_usb_tcp_touch.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
The web-flasher manifest referenced https://firmware.wadamesh.com/latest/<board>-merged.bin
— a stable filename cached 4h (Cache-Control max-age=14400) and overwritten in place each
release. version.json (max-age=300) advertised the new tag within minutes, but the latest/
bin could keep serving the PREVIOUS beta's bytes per-POP for up to 4h, so a user flashing
right after a release could install the old version (reported: flashed beta_11, device showed
beta_10). Point the manifest at the immutable releases/TOUCH/<tag>/ bin instead — that URL is
never overwritten so its cache is always correct, and the 5-min manifest TTL propagates the
new path fast.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Headline: full keyboard navigation (on by default) — WASDZ spatial focus move, S select,
Q back, per-tab hotkeys (E/R/T/U/I) shown on the icons, plus scroll up/down (F/C). Every
key is remappable in Settings -> Keyboard. Foundation for keyboard-first devices like the
Tanmatsu T-Pager (no touchscreen) and a reliable keyboard workflow on the T-Deck today.
Also: #27 receive/flood reboot fix, on-boot crash-report prompt, map zoom +/- buttons (#26),
route-replay wide overview, 'App drawer as home' option. The in-progress Tanmatsu (ESP32-P4)
port source is now public. Mesh America catalog + release.sh kept from main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- T-Deck: trackball mouse/D-pad navigation toggle (Settings -> Keyboard).
- Map (#20): microSD-tile mode now caches Wi-Fi-fetched tiles onto the SD card so
the offline library fills in + grows instead of going blank past its edge.
- Power: PR #19 idle power-save (Dan Vybiral / @Vybo) relocated into a new
'Battery' group under Settings -> System, alongside a button that opens the
battery / power-history screen.
- TouchCfg v10 (ui_scale, tb_keypad, sleep_idle). Mesh America catalog -> beta_10.
Curated touch overlay onto main (the WIP Tanmatsu ESP32-P4 build system stays on
the tanmatsu-port branch). Co-Authored-By: Dan Vybiral <dan.vybiral@greencode.cz>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opt-in Idle power-save for the T-Deck: when parked (screen off, on battery, no
companion client, Wi-Fi+BLE off, mesh idle) the main loop is throttled with
vTaskDelay so the FreeRTOS idle task can halt the CPU between ticks, on top of the
existing 80 MHz screen-off downclock — without ever stopping mesh RX. Default OFF
(Settings -> Lock, persisted TouchCfg v8 sleep_idle); status-bar moon indicator +
idle stats. Heltec V4 compiles (gate never passes there). A plain scheduler throttle
rather than esp_light_sleep_start(), which trips INT_WDT in this build.
Co-authored-by: Dan Vybiral <dan.vybiral@greencode.cz>
release.sh now regenerates deploy/meshamerica-catalog.json for the tag and, when
run on main, commits + pushes that one file so apps.meshamerica.com (the flasher
Cascadia Mesh uses) always points at the newest immutable bins instead of going
stale on the previous beta. Idempotent (no-op when already current); non-fatal if
the push fails; also mirrors the catalog into the VPS publish tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Catalog JSON (Mesh America provider schema) + a reusable generator so the two
wadamesh touch boards appear in the Mesh America Device Configurator
(apps.meshamerica.com — the flasher Cascadia Mesh uses). Points at the immutable
per-tag bins on firmware.wadamesh.com (HTTPS + CORS). Served live over
raw.githubusercontent.com.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| Tanmatsu | ESP32-P4 + ESP32-C6, SX1262 | 4" 800x480, 69-key keyboard (no touch) | Tanmatsu app store on the device (runs under the badge.team launcher, not web-flashable) | Store tracks the test channel | Fully supported; LoRa + Wi-Fi + Bluetooth simultaneously, standalone and companion in one |
| Elecrow ThinkNode M9 | ESP32-S3, LR1110 | 2.4" 240x320 (no touch), I2C QWERTY + d-pad | Web flasher | Beta (new in beta_38) | Hardware-complete community port by ded (#138): GPS, microSD, buzzer, lock screen, d-pad navigation |
| RAK WisMesh Tap V2 (RAK3312) | ESP32-S3, SX1262 | Touch display (LovyanGFX, 30+ fps) | Web flasher | Beta (new in beta_38) | Early community port by Ethac.chen (#136); core mesh, chat and map working |
## Feature notes per board
- **T-Deck**: the everything device: touch, physical keyboard, trackball cursor
Goal: ship **test** builds to the community fast, let them shake out, then **promote**
a proven test build to **stable**. What ships to stable is exactly what was tested
(promote the binary, don't rebuild).
## Two concepts, kept separate
- **Branches** = where code lives.
- **Channels** = what a device / the flasher pulls (Stable vs Beta).
## Branches
- **`main`** — active line. All work lands here; every *test* release is cut from `main`. (Unchanged from today.)
- **`stable`** — promoted line. Moves only when a test build is blessed. Starts at the **beta_20** commit.
- Promote = `git branch -f stable <blessed beta commit>` + push, then run the stable-promotion publish.
## Versioning / tags
- **Test builds:** `beta_N` (immutable, incrementing), marked **GitHub pre-release**. Also mirrored to a rolling **`beta-latest`** pre-release (overwritten each time) for the Launcher beta sync.
- **Stable:** the blessed `beta_N`, marked **GitHub Latest**. Keep the `beta_N` id for now; friendly `vX.Y` names are an optional later polish (would need a rebuild-with-tag per promotion).
- First stable = **beta_20**.
## Distribution matrix
| Target | Stable | Test / Beta |
|---|---|---|
| GitHub release | blessed `beta_N`, marked **Latest** | every `beta_N`, **pre-release** + rolling `beta-latest` |
- Only remaining radio tuning (small, MeshCore-side): `sync_word` / `preamble_length` / `power` to match `RadioLibWrappers.cpp` so it interoperates with the existing mesh.
**Display/input — DECISION: keep wadamesh's LVGL 8.3 and write our own flush; do NOT adopt `badge-bsp-lvgl`.**
-`badge-bsp` exposes the screen as an `esp_lcd` panel (`bsp_display_get_panel` / `_get_panel_io` / `_get_parameters` → h_res/v_res/color_format) and the keyboard as a FreeRTOS event queue (`bsp_input_get_queue`). Reference wiring: `badgeteam/konsool-template-lvgl`.
- BUT the badge LVGL component (`badgeteam/esp32-component-badge-bsp-lvgl`) targets **LVGL 9.3**, and wadamesh's UITask is **LVGL 8.3**. Adopting it forces an 8→9 migration of ~30k lines — not worth it. So: bring up our own LVGL 8.3 (esp_lvgl_port supports 8.x, or a manual `flush_cb`) flushing to the badge-bsp `esp_lcd` panel, + an `lv_indev` that reads the `bsp_input` queue.
- **UI layout target: 800×480 landscape** — make UITask responsive for it (it already branches on `hor_res`).
- [ ]**Compile spike** — get `src/` to build for P4 under pioarduino (no HW needed; see below)
- [ ]`[env:tanmatsu_companion_radio_touch]` in `platformio.ini` — port the shared touch flags, drop S3 LoRa pins; **resolve the RadioLib build issue (next item)**
- [x] RadioLib build question — RESOLVED: keep RadioLib in `lib_deps` + the same `RADIOLIB_*` flags as the S3 envs. `RadioLibWrappers.cpp` still compiles, but since the Tanmatsu `target.cpp` never instantiates `WRAPPER_CLASS`/`RADIO_CLASS`, the unused code is linker-stripped. Net: the env mirrors the T-Deck's RadioLib deps/flags; only the `radio_driver`*type* differs (set in `variants/tanmatsu/target.h`). No `build_src_filter` gymnastics needed.
- [ ] Wire the IDF components into the pio build (component manager) ⚠️ main risk
board=tanmatsu ; boards/tanmatsu.json — model on pioarduino m5stack-tab5-p4
build_flags=
${env.build_flags} ; reuse the shared touch flags
-D HAS_TANMATSU=1
-I variants/tanmatsu
; board_build.partitions = variants/tanmatsu/partitions_tanmatsu.csv ; TODO
; the BSP/wifi/lora IDF components are pulled via variants/tanmatsu/idf_component.yml
```
## Risks / open questions
1.**IDF components inside PlatformIO** (`badge-bsp`/`tanmatsu-lora`) — the fiddliest part; pioarduino supports the component manager but expect friction. **Still the #1 risk.**
2.**Display = our own LVGL 8.3 flush onto the badge-bsp `esp_lcd` panel** (decided; don't use the LVGL-9.3 `badge-bsp-lvgl`). Work = the flush_cb + the `bsp_input`→`lv_indev` glue + an 800×480 layout pass on UITask. Bounded now that the BSP API + konsool reference are known.
3.**WiFi/BLE via the C6** (`esp_wifi_remote` + `esp_hosted` over SDIO). Arduino `WiFi.h` on the P4 *is* backed by `esp_wifi_remote` in arduino-esp32 3.x, so wadamesh's WiFi (tile fetch, setup wizard, network tab) can likely run **mostly unchanged** — but it needs the C6 on the right `esp_hosted` slave firmware + the components wired in, and it's a **known-finicky area** in practice (cf. esphome#10956). The template uses IDF wifi helpers (`wifi_remote.h`), not Arduino `WiFi.h`, so confirm Arduino WiFi coexists with `tanmatsu-wifi`'s C6 bring-up during the spike. A real task + risk, not free — but not a rewrite.
4.**Heap/PSRAM** — fine: **32 MB PSRAM**, and an 800×480 RGB565 buffer is only ~768 KB. Lots of headroom vs the S3 boards.
| Display | 2.33" IPS **ST7796U**, 480×222 (221 PPI, 262K colors, 450 cd/m²), **480×222 landscape**, SPI, **no touch**. CS 38, DC 37, backlight 42 (AW9364 16-level stepped driver). **Hardware reset is XL9555 ch6, NOT unwired** — `TFT_RST=-1` only because it isn't an ESP GPIO; `TLoraPagerBoard::begin()` owns the reset pulse (LOW→50ms→HIGH, vendor sequence). Leaving ch6 floating = intermittent black screen, clean boot log (M7 root cause) |
| Shared SPI bus | **SCK 35, MOSI 34, MISO 33** — display + LoRa + SD + ST25R3916 NFC all on it (like the T-Deck's 40/41/38 — solved pattern, CS discipline + SPI transactions) |
| Radio | **LR1121** (sub-GHz 830–945 MHz + 2.4 GHz; we use sub-GHz only). CS 36, RST 47, BUSY 48, IRQ/DIO1 14. **Also sold with SX1262** (the LilyGoLib page documents the SX1262 retail SKU as primary) — same board/pins, different defines (cheap 2nd env later) |
| Keyboard | Physical QWERTY via **TCA8418** I²C matrix controller (addr `0x34`), INT 6, backlight 46. Raw matrix events — keymap/shift/sym handled on our side |
| Encoder | Rotary A 40, B 41, **press 7** |
| Buttons | **BOOT = GPIO0** (usable as user button + sleep wake — matches both existing boards' `PIN_USER_BTN=0`). Physical power key is PMU QON, not a GPIO — can only wake the device (1s hold), never programmable |
| Power | **BQ25896** charger PMU (addr `0x6B`) + **BQ27220 fuel gauge** (addr `0x55`, battery % / mV over I²C — NOT an ADC divider). Battery: 3.7 V / 1500 mAh (5.55 Wh). DeepSleep ≈530 µA, LightSleep ≈2.26 mA, Power-off ≈26 µA |
| IO expander | **XL9555** (addr `0x20`): DRV_EN ch0, AMP_EN ch1, KB_RST ch2, LORA_EN ch3, GPS_EN ch4, NFC_EN ch5, **DISP_RST ch6 — the ST7796 panel's hardware reset** (missing from BOTH the LilyGoLib doc's channel table and arduino-esp32 master's pins header; confirmed from LilyGoLib's `LilyGo_LoRa_Pager.cpp` begin(), which pulses it LOW→50ms→HIGH before display init — leaving it floating was M7's intermittent-black-screen root cause, see `TLORA_PAGER_M7_HW_DEBUG_LOG.md` session 2), GPS_RST ch7, KB_EN ch8, GPIO_EN ch9, SD_DET ch10, SD_PULLEN ch11, SD_EN ch12 |
DAC-only, register sequence and MCLK clock-divider coefficients derived
from Espressif's public `es8311.c`/`es8311_reg.h` (see `NOTICE`), fixed to
a 256x-sample-rate MCLK ratio covering the standard 8k-48k rates
`wavParse()` already accepts. Matches this port's own precedent (T-Deck's
sound code is similarly hand-rolled, ~200 lines) and the milestone rule
against introducing new abstraction layers.
10. **AMP_EN toggled around playback, not left permanently on.** The
boot-time `TLoraPagerBoard::begin()` drive of every expander rail
(including AMP_EN) exists for bus-integrity reasons specific to the boot
window (unpowered chips clamping the shared I²C bus) — unrelated to
audio and left untouched.
`TLoraPagerBoard::setAmpEnabled()` is the runtime toggle the sound code
brackets each chime/WAV with (amp off at idle), matching both the
vendor's own `codec.setPaPinCallback()` pattern and this port's existing
on-demand-resource philosophy (T-Deck installs/uninstalls its I2S driver
per chime for the same reason). Cost is one extra I²C write per
chime start/end on a bus already used for every other rail toggle.
## Worklist (ordered, each step ends build-green for ALL envs)
- [x] ⓪ Research + board JSON verification; land `boards/lilygo-t-lora-pager.json` + this tracker.
- [x] ① **Variant skeleton**: `variants/lilygo_tlora_pager/{pins_arduino.h, TLoraPagerBoard.h/.cpp}` + `partitions_tlora_pager_touch.csv` (T-Deck's OTA/tiles/spiffs/coredump layout, byte-identical offsets). Board class (`TLoraPagerBoard : public ESP32Board`): `begin()` re-inits Wire on SDA3/SCL2, probes the XL9555 expander (addr 0x20) and enables LORA_EN/GPS_EN/KB_EN/KB_RST/SD_EN rails, probes the BQ27220 gauge, handles the deep-sleep RX-packet wake reason (mirrors `TDeckBoard.cpp`). `getBattMilliVolts()` reads the gauge (`refresh()` + `getVoltage()`), falling back to 3700 mV if the probe/refresh fails. `getManufacturerName()` → "LilyGo T-LoRa Pager". `enterDeepSleep()` mirrors `TDeckBoard.h`. BQ25896 charger deliberately left as a TODO (gauge alone covers the UI). `target.{h,cpp}` deferred to ② — nothing references the new files yet, so both shipping envs build unchanged (verified green).
- [x] ② **Radio**: `variants/lilygo_tlora_pager/{CustomLR1121.h, CustomLR1121Wrapper.h, target.h, target.cpp}`.
`CustomLR1121{,Wrapper}.h` authored by adapting the core fork's own
`CustomLR1110{,Wrapper}.h` (LR1110/LR1121 share RadioLib's `LR11x0` base —
confirmed by reading RadioLib 7.6.0 source directly: same protected
`freqMHz`/`spreadingFactor` members, same `getIrqStatus()`/`getRssiInst()`
inherited from `LRxxxx`/`LR11x0`), since upstream has nothing to crib (see
Decision ②). `radio_init()` in `target.cpp`: `spi.begin(...)` →
**radio joins the live mesh: DONE**, adverts/DMs confirmed reliable both
directions against a second node after the TX-power fix (risk 10,
retested 2026-07-11) — still watch upstream LR1121 ACK issue
meshcore-dev/MeshCore#1376 for anything beyond basic reliability. USB
companion link passing the large device-profile frame (see HW-CDC note
above): still untested. **SD storage: DONE** — real mount, browse,
read/write/delete, and insert/remove detection all confirmed on hardware
(risk 1h). (DataStore/NVS-level SD use — routing contacts/channels to the
card — is a separate, still-deferred feature, not part of this bring-up
gate.) Remove the temporary `[DISP]` register readback in
`ST7796LCDDisplay::begin()` once the USB piece passes.
- [~] ⑧ **On-device UI pass** — ACTIVE: nav-coverage audit screen by screen (every interactive control reachable via focus group — the Tanmatsu work paved this), chat layout at 222 px, map pan via encoder/keys, fonts legibility at 480-wide, plus fixing any weird UI/keyboard behaviors surfaced by manual testing (human provides photos/repro steps).
- [ ] ⑨ **Release pipeline**: add the env:binname pair to `release.sh` `ENVS`, flasher manifest (`deploy/flasher/manifest-tlora-pager.json`), OTA env name via `FIRMWARE_OTA_ENV`. Separate PR.
- [x] ⑩ (Optional, cheap) `tlora_pager_sx1262_...` env for SX1262-variant owners — **DONE, hardware-verified 2026-07-13** (`tlora_pager_sx1262_companion_radio_touch`, all 4 envs green; see `TLORA_PAGER_PORT_MILESTONES.md`'s Milestone 10 for the full writeup, including the `target.h` missing-include bug found + fixed). Radio gate (M7 step 3) confirmed on real hardware: user sent + received messages on public, a custom `#` channel, and DM.
## UI-changes inventory (what actually changes in `src/ui-touch/`)
| Area | Change | Size | Status |
|---|---|---|---|
| Indev registration (~35706) | Widened the Tanmatsu keypad-only branch's gate to `\|\| defined(TLORA_PAGER)`; no pointer indev registered | small | done |
| `handleHwKey()` (~27990) and its multiply-reopened `HAS_TDECK_KEYBOARD` regions | Widened each reopen's gate individually to admit `HAS_PAGER_KEYBOARD`; kept the T-Deck-only p/q/a dismiss mapping under its own narrower gate | small | done |
| Resolution block (~35604–35650) | New branch: forced `LV_DISP_ROT_270`, `hor_res=480/ver_res=222` | small | done |
| Draw buffer (~1399) | Sized off `hor_res`, no separate edit needed | trivial | done (no-op) |
| Brightness (`applyBrightness`~28906, `touchScreenBacklight`~36511) | New pager-first branches calling `display.setBrightness()` instead of the PWM `PIN_TFT_LEDA_CTL` path | small | done |
| `navMaybeRebuild()` reachability | Added `#elif defined(TLORA_PAGER)` arm — was unreachable, would've left focus group permanently empty | small | done (bug fix) |
| Vertical budget | Audit `STATUSBAR_H`, `TABBAR_H`, `CHAT_KB_H`, modal/chat height helpers for 222 px | **the real work** | deferred to M8 on-device (risk 8) |
| Focus-nav coverage | Screen-by-screen pass that every control is in `s_nav_group` | medium, on-device | deferred to M8 |
| device_caps.h | New `CAP_*` block: no touch, hw keyboard, encoder, 480×222, GPS, OTA, lock-screen; SD/filesystem left 0 by design — real SD support added via widened individual call sites instead (see risk 1h) | trivial | done |
| microSD (file manager + WAV picker) | `fmSdTryMount()`/`fmShowRoots()`/`fmIsAudio()` etc. widened to `\|\| defined(TLORA_PAGER)`; DataStore SD routing, chat-history cap, tile/wallpaper/backup SD use still deferred | medium | done, scoped (risk 1h) |
| Chat bubble Enter/Backspace (`handleHwKey()` ~28808) | Enter on a focused bubble opens the Ack/Mention/Copy/Info/Block menu (`navEnterBubble()`); Backspace jumps to the newest message when the scroll-to-bottom button is showing | small | done |
| Shift/Caps (`PagerKeyboard.cpp`) | Held Shift = momentary uppercase; held Alt+Shift = chord (see below for what it does) | small | done |
| @-mention picker keyboard nav | Auto-arms the instant the list appears (no Fn+Space needed); same encoder walk/confirm/cancel as the accent popup (`s_mentionnav_active`/`mentionNavRestyle()`/`mentionNavConfirm()`) | medium | done |
| Encoder short click on a focused bubble | Mirrors keyboard Enter — opens the same action menu instead of a plain ENTER a bubble doesn't react to | small | done |
| Fn+Shift chord effect | Toggles Caps Lock while editing a text field; jumps to Home (`navGoToMainTab`) everywhere else — `PagerKeyboard.cpp` only reports the chord, `UITask.cpp` decides the effect | small | done |
| Map pan (WASD) + slider nudge (Q/E) | WASD pans the Map tab (`mapNudge()`, shared with Tanmatsu's Ctrl+Arrow); Q/E adjust a focused slider (moved off D/F to avoid colliding with WASD) | small | done |
| App-drawer tile focus highlight | `NAV_ACCENTFOCUS_FLAG` makes `navFocusCb` use the tile's own accent tint instead of the generic white reverse-fill, on every board | small | done |
| SX1262-variant env | `tlora_pager_sx1262_companion_radio_touch` — same board/pins, `CustomSX1262`/`CustomSX1262Wrapper` instead of the LR1121 classes | medium | done (worklist ⑩) |
The LilyGo T-LoRa Pager has **no touchscreen and no trackball** — every
screen, every setting, and every chat is driven entirely by the physical
QWERTY keyboard and the rotary encoder knob. If you're coming from the
T-Deck or Heltec V4 (touch-first), this is the one board where learning a
handful of gestures up front pays off immediately. This guide covers both
radio variants (LR1121 and SX1262) — the UI and input handling are identical
on both.
## The two inputs
- **Keyboard** — a physical QWERTY matrix with a Space bar, Backspace, Enter,
a Shift key, and an orange **Fn/Alt** key (bottom-left). No dedicated Esc,
arrow keys, or number row — those are covered by combos below.
- **Rotary encoder** — turn to move the on-screen focus highlight; click
(press the knob) to select. It's your only pointing device, so almost
every screen is navigable with turn + click alone.
Everything below only fires while you're **not** actively typing into a text
field — if a field is focused and you're typing into it, letters type
normally and these combos step out of the way.
## Rotary encoder
| Gesture | Action | Keyboard equivalent |
|---|---|---|
| Turn | Move focus to the next/previous item on screen | **Fn (Alt) tapped alone** moves forward one step (NEXT only — no keyboard way to go backward) |
| Short click | Select / confirm the focused item | **Enter** |
| Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first) | **Backspace held ~1 s** |
| **Fn (Alt) + turn**, on a main tab | Jump directly between the 5 main tabs (Chats / Contacts / Home / Map / Settings) | none — encoder only |
| **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none — encoder only |
| Turn, with a dropdown open | Scroll through the dropdown's options | none — encoder only |
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none — encoder only (Fn+Space to enter is shared) |
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none — encoder only |
| Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going — see [Chat screen](#chat-screen) | none — encoder only |
The knob doubles as your only way to reach the bottom tab bar — since
there's no touch to tap an icon, **Fn+turn** while on any main tab is the
fastest way to switch sections.
## Chat screen
An open chat has no touch-only jump-to-oldest/jump-to-latest buttons on this
board — everything is reachable by turning and clicking, and the encoder
behaves a bit differently here than anywhere else in the app.
### Turning through messages
Message history is loaded lazily: only the messages currently near the
viewport actually exist as on-screen items, and older/newer ones are pulled in
as you approach them. Turning the encoder normally moves the focus highlight
one message at a time, and **at the edge of what's currently loaded, it keeps
going** — the turn instead scrolls the list to pull in the next batch of
history, then continues moving the highlight onto it. You'll only ever land
outside the message list (see below) once you've genuinely reached the very
oldest or very newest message in the whole conversation — never mid-history.
### Leaving the message list
Turning **past the oldest loaded message** (top edge), once there's truly
nothing more to load, wraps around to whatever the last focusable item on the
screen is — the channel-settings gear in an open **channel**, or the Send
button in a **DM** (a DM has no gear to reach). This is plain focus wrap, the
same as reaching the end of any other list in the app; there's no chat-specific
handling for this edge.
Turning **past the newest loaded message** (bottom edge) moves into the reply
row below the messages: quick-reply (△) → emoji → text box → Send. Turning
further past Send *is* chat-specific: in a channel it goes to the
channel-settings gear; in a DM (nothing to reach there) it returns you to the
text box → Send**, in that order, in both directions — nothing is skipped.
Turning past the △ chip (toward the messages) always jumps straight to the
newest message, regardless of channel or DM.
### Backspace and Fn (Alt) — leaving vs. catching up
These two keys are your shortcuts in and out of "reading mode" on this
screen, and they do different things depending on whether you've got unread
messages:
- **Fn (Alt) tapped alone** — jumps straight to the **newest** message and
moves focus into the text box, ready to type a reply. This is the
deliberate "I'm done reading, let me respond" gesture — it works regardless
of where your focus currently is in the chat.
- **Backspace (tap)** — jumps to whichever message needs your attention:
- If there are unread messages, it jumps to the first one — right below the
**"NEW ----"** divider — and selects it, so you can then turn forward
through your unread messages in order, oldest-of-the-unread first.
- If nothing is unread, it jumps to the newest message instead (same
destination as Fn+Alt, but leaves focus on the message itself rather than
the text box).
Backspace's override above only applies while you're inside an open chat and
*not* actively editing the text box — the usual rule from the top of this
guide; with a field focused, Backspace deletes a character as normal.
Fn (Alt) tap's override fires the same way whether or not the text box
already has focus, since landing there is the whole point of the gesture.
## Keyboard layout
Base layer (no modifier):
```
q w e r t y u i o p
a s d f g h j k l [Enter]
z x c v b n m
[Space]
```
Hold **Fn (Alt)** for numbers/symbols instead:
```
1 2 3 4 5 6 7 8 9 0
* / + - = : ' " @
_ $ ; ? ! , .
[Space]
```
### Shift and Caps Lock
- **Hold Shift + a letter**: that letter (or letters, for as long as you
hold Shift) types uppercase — released, typing goes back to lowercase.
Real momentary Shift, just like a normal keyboard.
- **Hold Fn (Alt), then press Shift**: while you're editing a text field,
toggles **Caps Lock** on/off — stays uppercase until you repeat the chord.
Anywhere else (not editing a field), the chord does nothing.
- Shift alone, tapped with nothing else, does nothing (as expected).
- **Hold Fn (Alt), then press Backspace**: jumps straight to the **Home**
screen — works everywhere, including while you're actively editing a field
(unlike Alt+Shift above, this one isn't context-dependent). Doesn't delete
a character or trigger the plain-Backspace gestures below.
### Special keys
| Key | While editing a text field | Otherwise |
|---|---|---|
| **Enter** | Send / submit / newline | Select the focused item — or, if a chat message bubble is focused, opens its action menu (Ack / Mention / Copy / Info / Block) |
| **Backspace** (tap) | Delete a character | In an open chat: jump to your first unread message, or the newest message if nothing's unread — see [Chat screen](#chat-screen) |
| **Backspace** (hold ~1 s) | — | Same as the encoder's long-press: **Back** |
| **Backspace** (hold ~1 s) *while the screen is locked* | — | Unlock the screen |
| **Space** (tap) | Types a space | — |
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | — |
| **Space** (hold ~1 s) | — | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
| **Fn (Alt)** tapped alone (press+release, nothing else) | — | Moves focus to the next field/item — a keyboard-only substitute for a turn of the encoder. In an open chat: jumps to the newest message and focuses the text box instead — see [Chat screen](#chat-screen) |
The **BOOT** button (top of the device) instantly wakes the screen from
idle-dim. It does *not* unlock a screen you've manually locked with the
Space-hold gesture above — for that, hold Backspace.
## Accented characters
Typing a letter that has accent variants (all vowels, plus `n c s y t z l r`,
both cases) pops up a small row of alternatives above the field. To pick one
without touch:
1. **Fn (Alt) + Space** — jumps focus into the popup.
2. **Turn the encoder** — cycles through the variants.
3. **Enter** — confirms your choice and replaces the letter you just typed.
4. **Backspace** — dismisses the popup and keeps the plain letter.
## @-mentions
Typing `@` followed by a few letters of a contact's name pops up a matching
list in a channel/group chat. Unlike the accent picker above, this one grabs
the encoder **immediately** — no Fn+Space needed, since it only appears once
you've deliberately started typing a mention:
1. **Turn the encoder** — cycles through the matching contacts.
"description":"WADAMESH is a full touch-screen interface for MeshCore: on-device chat, contacts, a live map with offline tiles, and complete radio settings. Same MeshCore protocol and phone-app companion as stock MeshCore, just a richer on-device front-end.",
"notes":"Fixed the on-device updater downloading the wrong firmware on several boards. The V4-R8, ThinkNode M9, RAK Tap V2, T-LoRa Pager and Attaky now each pull their own build instead of the Heltec V4 TFT one.\nT-Display P4: the \"Get test builds (beta)\" toggle and the on-device update now appear. They were hidden before, so the P4 could only be updated over USB. Newly enabled, please test and report.\nLock screen fixed: tap Backspace to peek the screen without unlocking, hold Backspace to unlock. The clock and text are repositioned to fit the wide screen and stay readable.\nOn-screen keyboard now works with the detachable keyboard module: the keys stay down while the module is attached, and the module's # key brings them back up for symbols. The POWER button toggles the screen and wakes the board.\nBoard fixes: correct LoRa clock (no false TCXO), GPS baud, and sensor probing that was colliding with the touch and keyboard chips.",
"notes":"Fixed the on-device updater downloading the wrong firmware on several boards. The V4-R8, ThinkNode M9, RAK Tap V2, T-LoRa Pager and Attaky now each pull their own build instead of the Heltec V4 TFT one.\nT-Display P4: the \"Get test builds (beta)\" toggle and the on-device update now appear. They were hidden before, so the P4 could only be updated over USB. Newly enabled, please test and report.\nLock screen fixed: tap Backspace to peek the screen without unlocking, hold Backspace to unlock. The clock and text are repositioned to fit the wide screen and stay readable.\nOn-screen keyboard now works with the detachable keyboard module: the keys stay down while the module is attached, and the module's # key brings them back up for symbols. The POWER button toggles the screen and wakes the board.\nBoard fixes: correct LoRa clock (no false TCXO), GPS baud, and sensor probing that was colliding with the touch and keyboard chips.",
"notes":"Fixed the on-device updater downloading the wrong firmware on several boards. The V4-R8, ThinkNode M9, RAK Tap V2, T-LoRa Pager and Attaky now each pull their own build instead of the Heltec V4 TFT one.\nT-Display P4: the \"Get test builds (beta)\" toggle and the on-device update now appear. They were hidden before, so the P4 could only be updated over USB. Newly enabled, please test and report.\nLock screen fixed: tap Backspace to peek the screen without unlocking, hold Backspace to unlock. The clock and text are repositioned to fit the wide screen and stay readable.\nOn-screen keyboard now works with the detachable keyboard module: the keys stay down while the module is attached, and the module's # key brings them back up for symbols. The POWER button toggles the screen and wakes the board.\nBoard fixes: correct LoRa clock (no false TCXO), GPS baud, and sensor probing that was colliding with the touch and keyboard chips.",
"notes":"Fixed the on-device updater downloading the wrong firmware on several boards. The V4-R8, ThinkNode M9, RAK Tap V2, T-LoRa Pager and Attaky now each pull their own build instead of the Heltec V4 TFT one.\nT-Display P4: the \"Get test builds (beta)\" toggle and the on-device update now appear. They were hidden before, so the P4 could only be updated over USB. Newly enabled, please test and report.\nLock screen fixed: tap Backspace to peek the screen without unlocking, hold Backspace to unlock. The clock and text are repositioned to fit the wide screen and stay readable.\nOn-screen keyboard now works with the detachable keyboard module: the keys stay down while the module is attached, and the module's # key brings them back up for symbols. The POWER button toggles the screen and wakes the board.\nBoard fixes: correct LoRa clock (no false TCXO), GPS baud, and sensor probing that was colliding with the touch and keyboard chips.",
<metaname="description"content="User guide for the WADAMESH touch firmware on the LilyGo T-Deck: home, chat, contacts, map, settings and the built-in apps. Real device screenshots.">
<aclass="home"href="index.html"><svgviewBox="0 0 24 24"fill="none"stroke="currentColor"stroke-width="2"stroke-linecap="round"stroke-linejoin="round"><pathd="m12 19-7-7 7-7"/><pathd="M19 12H5"/></svg> Back to site</a>
</div>
<divclass="shell">
<navclass="toc"id="toc">
<divclass="grp">Start here</div>
<ahref="#overview">Overview</a>
<divclass="grp">The five tabs</div>
<ahref="#home">Home</a>
<ahref="#chat">Chat</a>
<ahref="#contacts">Contacts</a>
<ahref="#map">Map</a>
<ahref="#settings">Settings</a>
<divclass="grp">With your phone</div>
<ahref="#companion">Companion mode</a>
<ahref="#remote">Remote & web app</a>
<ahref="#web">Web browser</a>
<divclass="grp">Reference</div>
<ahref="#updates">Updates & test builds</a>
<ahref="#settings-pages">Settings pages</a>
<ahref="#apps">Apps</a>
</nav>
<main>
<divclass="hero"id="overview">
<h1>WADAMESH user guide</h1>
<p>WADAMESH turns a LilyGo T-Deck — its landscape screen, BlackBerry-style keyboard and trackball — into a complete front-end for your MeshCore radio: chat, channels, contacts, a live map and deep radio settings, all on the device. (It also runs on the Heltec V4 and Tanmatsu.) Every screen below is a real screenshot from the device.</p>
<p>On devices with a microSD slot (the LilyGo T-Deck and Tanmatsu), wadamesh is best used with a card in place. It keeps map tiles, your contacts and your chat history on the card, which is roomier and far more reliable than the small internal flash, so a card prevents memory issues and stops that data being lost on reboot. The Heltec V4 has no card slot and manages fine on its internal storage.</p>
<p>The bar along the bottom is always there — it switches the five main tabs (Chat, Contacts, Home, Map, Settings) from anywhere. The top bar shows unread count, time, signal and battery.</p>
<p>Every conversation in one list, newest first — both <b>channels</b> (group chats like <code>#wadamesh</code>, <code>Public</code>) and <b>direct messages</b>. An unread badge shows the count.</p>
<h3>On each row</h3>
<ul>
<li><b>Tap</b> a row to open the conversation, then type on the keyboard to reply.</li>
<li>The <b>gear</b> on the right opens that chat's settings — mute, region & scope, share secret, blocked users, remove — the same screen as the cog inside a chat.</li>
</ul>
<p>The top-right buttons add a contact, mark everything read, and show your QR code.</p>
</div>
<divclass="shot"><divclass="device"><imgclass="screen"src="docs-img/chat.png"alt="Chat channel list"></div><divclass="cap">Chat — channels & DMs</div></div>
<p>Everyone your node knows — name, type and how recently you heard them. Tap a contact to message it or open its details: signal history, a route trace, telemetry, and share/favourite options.</p>
<p>The <b>overflow menu</b> (top right) holds Search, the <b>Discovered</b> list (nodes heard but not yet added), Add contact, Auto-add settings, and the Blocked list. Auto-add can pull new nodes in from their adverts, or you can add them by hand.</p>
<p>An OpenStreetMap / OpenTopoMap view with your position and your contacts plotted as markers, plus link lines from you to each node. Tiles are fetched over Wi-Fi through the wadamesh proxy and cached to flash (or microSD), so areas you've viewed work offline.</p>
<p>It also has a line-of-sight analyzer: pick a contact and it uses SRTM elevation to show whether terrain blocks the path.</p>
<p>You don't need Wi-Fi to see the map. Copy a standard OpenStreetMap tile pack onto the T-Deck's microSD card and the map reads straight off the card, fully offline.</p>
<p><b>Where they go.</b> Use the usual slippy-map <code>z/x/y</code> folder layout, in either of these locations on the card:</p>
<ul>
<li><code>/maps/osm/<z>/<x>/<y>.png</code>— the Meshtastic / MeshCore standard layout (recommended, so packs are interchangeable between apps).</li>
<li><code>/tiles/<z>/<x>/<y>.png</code>— also read. This is the same folder the Wi-Fi download cache uses, so your own tiles and downloaded ones live side by side.</li>
</ul>
<p><b>Format.</b> 256×256 <code>.png</code> tiles (<code>.jpg</code> also works). <code>z</code> is the zoom level and <code>x</code>/<code>y</code> the tile column/row at that zoom — the same scheme OSM's own tile server and every XYZ tile downloader use, so most ready-made packs drop in unchanged.</p>
<p><b>Turn it on.</b> Open the map's options (the gear button on the map) and switch on <b>Tiles from SD card</b>; the status line confirms <code>Map tiles: microSD</code>. Turn it back off to fetch live tiles over Wi-Fi again.</p>
<p><b>Making a pack.</b> Any XYZ / slippy-map tile downloader works — point it at OpenStreetMap for your area and a sensible zoom range, then copy the resulting <code>z/x/y</code> folders onto the card. Each extra zoom level roughly quadruples the tile count, so grab only the levels you'll actually use.</p>
<p>The "Tiles from SD card" toggle is a T-Deck feature. The Heltec V4 has no microSD slot, so it caches downloaded tiles to a small internal partition (no offline packs there). On the Tanmatsu, tiles — your own or downloaded — live at <code>/tiles/<z>/<x>/<y>.jpg</code> on the microSD card; drop JPEG tiles there and they show alongside the downloaded ones.</p>
<p>A tile for each category. Tap one to open its page; the title bar carries a back chevron. The full set is in <ahref="#settings-pages">Settings pages</a> below.</p>
<ul>
<li><b>Profile</b> — your name and node identity.</li>
<li><b>Radio & Mesh</b> — frequency, spreading factor, region scope, high-gain receiver, signal probe, auto-advert.</li>
<li><b>Auto-add, Wi-Fi, Bluetooth, GPS</b> — discovery and connectivity.</li>
<p>WADAMESH runs everything on its own, but it can also be the radio for the <b>official MeshCore app</b> on your phone (iOS and Android). Your phone brings the big screen and full keyboard; the device does the LoRa. Best of all it works <b>alongside</b> the on-device UI — you can keep using the touch screen while your phone is connected, and your contacts, channels and messages stay in sync between them.</p>
<h3>Three ways to connect</h3>
<ul>
<li><b>USB</b> — plug the device into an Android phone or a computer; the app connects over the cable. Nothing to set up on the device.</li>
<li><b>Bluetooth</b> — turn on Bluetooth in <b>Settings → Bluetooth</b>. The device appears in the app as <code>MeshCore-<your node name></code>; pair with the 6-digit code (default <code>123456</code>, changeable on the same page).</li>
<li><b>Wi-Fi</b> — put the device on your network in <b>Settings → Wi-Fi</b>. On the same Wi-Fi, the app connects to the device's IP address on port <code>5000</code> (the Wi-Fi page shows the IP).</li>
<b>Run Bluetooth and Wi-Fi together only when you need to</b>
<p>Bluetooth and Wi-Fi can be on at the same time, but each one reserves a good chunk of RAM. On the Heltec V4 especially — it has the least free memory of the three boards — keeping both on at once leaves little headroom and can lead to instability or odd behaviour. If you only want the phone app, pick one transport (Bluetooth or Wi-Fi) and leave the other off; if you mostly use the device on its own, keep both off.</p>
<p>Over Wi-Fi, a WADAMESH device can put its screen — or a whole browser app — on your phone or laptop. There is nothing to install: the device serves a web page you open in any browser on the same network. Turn it on from <b>Home → Remote</b> (or the Remote screen in Settings), pick a mode, and open the address it shows.</p>
<h3>Three ways to connect</h3>
<ul>
<li><b>Remote app</b> (recommended) — a full control panel, native to the browser: <b>Chats</b>, <b>Contacts</b>, a <b>Terminal</b> and <b>Settings</b>, with live message notifications. Run the device entirely from your phone.</li>
<li><b>Screen mirror (VNC)</b>— streams the device's live screen to the browser and sends your taps and key presses back, so you see and drive exactly what is on the device, in real time.</li>
<li><b>Remote UI</b>— renders the interface off-screen at desktop size (a wide landscape layout) and streams that instead of the small panel, for a roomier view on a big screen.</li>
</ul>
<p>Screen mirror and Remote UI both show the same interface you already know from the device — mirrored, or drawn larger. The <b>Remote app</b> is different: a browser-native panel built for a phone, shown below.</p>
<h3>Turning it on</h3>
<p>Connect the device to Wi-Fi first, then open <b>Home → Remote</b>. The screen shows the address to type into your browser (like <code>http://192.168.1.42</code>) and a switch for each mode. Only one streaming mode runs at a time, and the phone and the device must be on the same Wi-Fi network.</p>
</div>
<divclass="shot"><divclass="device"><imgclass="screen"src="docs-img/app_remote.png"alt="Remote access screen on the device"></div><divclass="cap">Remote — turn it on, then open the address</div></div>
</div>
<divclass="subh">The remote app in a browser</div>
<divclass="prose"style="max-width:70ch"><p>The remote app is phone-first. It carries the same status icons as the device (signal, Wi-Fi, Bluetooth, battery), pops up a notification when a message arrives, and refreshes an open chat on its own as replies come in — so you rarely need to touch the device itself.</p></div>
<divclass="webgrid">
<figureclass="phone"><imgsrc="docs-img/web_chats.png"alt="Chats tab in the web app"><figcaption>Chats — DMs, channels, unread badges</figcaption></figure>
<figureclass="phone"><imgsrc="docs-img/web_chat.png"alt="A conversation in the web app"><figcaption>A conversation, with delivery ticks</figcaption></figure>
<figureclass="phone"><imgsrc="docs-img/web_contacts.png"alt="Contacts tab in the web app"><figcaption>Contacts — search, sort, filter</figcaption></figure>
<figureclass="phone"><imgsrc="docs-img/web_contact_sheet.png"alt="Contact actions in the web app"><figcaption>Tap a contact for actions</figcaption></figure>
<figureclass="phone"><imgsrc="docs-img/web_discovered.png"alt="Discovered nodes in the web app"><figcaption>Discovered nodes — add with a tap</figcaption></figure>
<figureclass="phone"><imgsrc="docs-img/web_settings.png"alt="Settings in the web app"><figcaption>Settings — including live radio</figcaption></figure>
<figureclass="phone"><imgsrc="docs-img/web_terminal.png"alt="Terminal in the web app"><figcaption>Terminal — the MeshCore CLI</figcaption></figure>
<p>WADAMESH can read the web on its own screen. Open the <b>Web</b> app from the home launcher, type an address, and the device fetches the page and shows it as plain, readable text — articles, wikis, docs, simple sites. There is no proxy in the middle and nothing is tracked: it pulls the page straight from the site and strips it down to the words.</p>
<ul>
<li><b>Text only</b>— no JavaScript, no images, no ads. Pages load small and read clean.</li>
<li><b>Tappable links</b>— follow a link to the next page, with <b>back</b>, <b>forward</b> and <b>refresh</b> in the toolbar.</li>
<li><b>Direct & private</b>— the device connects to the site itself over Wi-Fi (HTTPS); no server of ours sees what you read.</li>
</ul>
<p>Reading the web on-device takes more memory than the smallest board carries, so the Web app is on the <b>T-Deck</b> and <b>Tanmatsu</b>. On the 2 MB Heltec V4 it is hidden — that board can't complete the secure handshake — but you can still bounce any link to your phone (below).</p>
</div>
<divclass="shot"><divclass="device"><imgclass="screen"src="docs-img/app_web.png"alt="The on-device Web app"></div><divclass="cap">Web — open an address, read it as text</div></div>
</div>
<divclass="subh">Links in chat</div>
<divclass="prose"style="max-width:70ch"><p>A link that arrives in a message is now tappable. Tap it for a small menu: <b>Open in web</b> loads the page in the on-device browser (T-Deck and Tanmatsu), and <b>Create QR</b> puts a scannable QR code on screen so you can open the link on your phone. Create QR works on <em>every</em> board, including the Heltec V4 — so even where the browser isn't available, a link is one tap from your phone.</p></div>
<divclass="gallery">
<divclass="mini"><imgsrc="docs-img/app_web_links.png"alt="Tapping a link in chat opens a menu"><divclass="cap">Tap a link — Open in web, or Create QR</div></div>
<divclass="mini"><imgsrc="docs-img/app_web_qr.png"alt="A QR code for a link from chat"><divclass="cap">Create QR — scan to open on your phone</div></div>
<p>On the boards that update over Wi-Fi (the Heltec V4 and the standalone T-Deck), WADAMESH checks for new firmware on its own. When one is available, <b>Settings → About</b> shows it and you can flash it straight from the device with <b>Install update</b>, no computer needed. (Under Launcher and on the Tanmatsu you update through Launcher or the app store instead.)</p>
<h3>Two channels: Stable and Test</h3>
<ul>
<li><b>Stable</b> is the default. It is the build that has been tested and is recommended for everyday use. Leave your device on this unless you want to help test.</li>
<li><b>Test (beta)</b> gets new features and fixes earlier, before they are proven. It can be rough, so it is meant for trying things early and reporting bugs, not for a device you rely on.</li>
</ul>
<h3>Turn on test builds</h3>
<p>Flip <b>Settings → About → "Get test builds (beta)"</b> on. From then on, both the update check and <b>Install update</b> follow the test channel, so the device offers the newest test build over Wi-Fi. Turn the switch back off to return to stable. You can switch either way at any time.</p>
<p>Prefer to start on a test build from scratch? Pick <b>Beta · testing</b> when you flash from <ahref="https://flasher.wadamesh.com"target="_blank"rel="noopener">flasher.wadamesh.com</a>.</p>
</div>
<divclass="shot"><divclass="device"><imgclass="screen"src="docs-img/set_about.png"alt="About page"></div><divclass="cap">The About page</div></div>
<divclass="prose"style="max-width:68ch"><p>WADAMESH splits its settings into nineteen pages. Here is what each one does and the controls you will find on it. Open any page from the Settings tab; a back chevron in the title bar returns you to the grid.</p></div>
<divclass="lede">Forward received messages to your own MQTT broker.</div>
<ul>
<li>Publishes the <b>text, sender and time</b> of messages your node hears to an MQTT broker</li>
<li>Broker <b>host, port and topic</b>, plus an <b>encryption key</b></li>
<li>Stays off until you <b>accept the privacy note</b>— anyone who can read the broker sees the messages, so use one you control, never a public broker</li>
</ul>
</div>
</div>
<divclass="sgrp">Time & power</div>
<divclass="scards">
<divclass="scard">
<divclass="thumb"><imgsrc="docs-img/set_clock___time.png"alt="Clock and time settings"></div>
<h3>Clock & time</h3>
<divclass="lede">Keep the clock right.</div>
<ul>
<li><b>Sync clock</b> from the system / NTP</li>
<li><b>Time zone</b> picker, or a custom UTC offset with steppers</li>
<p>Tools from the home launcher. <b>RF Monitor</b> is a repeater-style view of recent packets and radio stats; <b>Spectrum</b> sweeps the band so you can spot interference or find a clear frequency.</p>
var a = document.querySelector('nav.toc a[href="#' + e.target.id + '"]');
if (a) a.classList.add('on');
}});
}, { rootMargin: '-25% 0px -65% 0px' });
['overview','home','chat','contacts','map','settings','companion','settings-pages','apps'].forEach(function(id){ var el = document.getElementById(id); if (el) obs.observe(el); });
<title>WADAMESH — a touchscreen for your mesh radio</title>
<metaname="description"content="Open-source MeshCore touch-UI firmware for the LilyGo T-Deck and Heltec V4. Map, chat, channels, contacts — install from your browser.">
<metaname="description"content="Open-source MeshCore touch-UI firmware for the LilyGo T-Deck, Heltec V4 and Tanmatsu. Map, chat, channels, contacts — install from your browser.">
<span>Like the project and want to donate? Help out the <b>MeshCore</b> founders instead. <ahref="https://blog.meshcore.io/2026/07/04/help-us-save-meshcore"target="_blank"rel="noopener">Learn more</a></span>
<aclass="fab fab-bug"href="https://github.com/ALLFATHER-BV/wadamesh/issues"target="_blank"rel="noopener"aria-label="Report a bug on GitHub"title="Report a bug">
<aclass="fab"href="https://github.com/ALLFATHER-BV/wadamesh/issues"target="_blank"rel="noopener"aria-label="Report a bug on GitHub"title="Report a bug">
<pclass="lede">A real touchscreen UI for your mesh radio.</p>
<pclass="sub">MeshCore MESHCOMOD firmware for the <b>LilyGo T-Deck</b> and<b>Heltec V4</b> — map, chat, channels and contacts on the device itself, <b>and</b> a companion radio for the <b>MeshCore app</b> at the same time.</p>
<pclass="sub">MeshCore MESHCOMOD firmware for the <b>LilyGo T-Deck</b>,<b>Heltec V4</b> and <b>Tanmatsu</b> — map, chat, channels and contacts on the device itself, <b>and</b> a companion radio for the <b>MeshCore app</b> at the same time.</p>
<spanclass="pop"><b>Offline map</b>A real OpenStreetMap you can pan and zoom on the device. Pre-load tile packs to an SD card for fully offline use, or fetch over Wi-Fi — and see your contacts' GPS positions plotted on it.</span></span>
@@ -266,6 +374,12 @@
<buttonclass="back"data-back>← choose a different path</button>
<pstyle="margin:12px auto 0;max-width:600px;text-align:left;font-size:.88rem;line-height:1.6"><b>Stable</b> is the tested default. Pick it unless you want to help test. <b>Beta · testing</b> gets new features and fixes earlier, before they are proven, so it can be rough. It is for trying things early and reporting bugs.<br>Already running WADAMESH? You do not have to reflash to switch channels: turn on <b>Settings → About → "Get test builds (beta)"</b> on the device and it updates to test builds over Wi-Fi. Switch it off to go back to stable.</p>
<pdata-wm-betahinthiddenstyle="margin:8px auto 0;font-size:.84rem;color:#b4631a;max-width:560px">Beta builds are newer and less tested — for trying things early and reporting bugs. On a T-Deck under Launcher you download the .bin and side-load it.</p>
<divdata-whatsnewhiddenstyle="margin:18px auto 0;max-width:560px;background:rgba(21,182,166,.07);border:1px solid rgba(21,182,166,.28);border-radius:12px;padding:13px 16px;text-align:left">
<divstyle="font-weight:600">What's new in <spandata-wn-tagstyle="color:var(--teal)">the latest build</span></div>
<divclass="step"><divclass="num">2</div><div><h4>Pick your board & install</h4><p>Hit <b>Install</b> below, choose the serial port, and let it flash. It does a clean full install.</p></div></div>
<divclass="step"><divclass="num">3</div><div><h4>You're on the mesh</h4><p>The device reboots into WADAMESH. Set your name + region in the first-run wizard.</p></div></div>
</div>
<divclass="boards">
<divclass="board"><h4>Heltec V4 + TFT</h4><p>ESP32-S3 with CHSC6x touch.</p>
<spanclass="sup full"tabindex="0"><spanclass="dot"></span>Fully supported<spanclass="pop"><b>Fully supported</b>Everything wadamesh offers works here — this is one of the two primary development boards, tested on every release.</span></span>
<spanclass="sup part"tabindex="0"><spanclass="dot"></span>Partially supported<spanclass="pop"><b>Partially supported</b>Early port — it's known that not everything works yet, but the core does: LoRa mesh communication, chat, map, GPS and the phone-app link over Wi-Fi or USB. Bluetooth pairing isn't available on this board (a hardware limitation of its stock radio firmware).</span></span>
<spanclass="sup part"tabindex="0"><spanclass="dot"></span>Partially supported<spanclass="pop"><b>Partially supported</b>The LCD-screen model of the T-Display P4 (the AMOLED variant is listed above). Early port — the core works: LoRa mesh, chat, map, GPS and the phone-app link over Wi-Fi or USB. Bluetooth pairing isn't available on this board. Flash with care and report back.</span></span>
<pdata-wm-betaonlystyle="color:var(--mut);font-size:.78rem;margin:6px 0 0">New screen variant, not yet hardware-tested — beta only. Flash with care and report back.</p></div>
<spanclass="sup part"tabindex="0"><spanclass="dot"></span>Partially supported<spanclass="pop"><b>Partially supported</b>New community port (thanks @codemonkeybr). The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link. Pick the build that matches your radio chip.</span></span>
<spanclass="sup full"tabindex="0"><spanclass="dot"></span>Fully supported<spanclass="pop"><b>Fully supported</b>Everything wadamesh offers works here — this is one of the two primary development boards, tested on every release.</span></span>
<spanclass="sup part"tabindex="0"><spanclass="dot"></span>Partially supported<spanclass="pop"><b>Partially supported</b>The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link. This revision is still being hardware-verified — flash with care and report back.</span></span>
<pdata-wm-betaonlystyle="color:var(--mut);font-size:.78rem;margin:6px 0 0">New board, not yet hardware-tested — beta only. Please flash with care and report back.</p></div>
<spanclass="sup part"tabindex="0"><spanclass="dot"></span>Partially supported<spanclass="pop"><b>Partially supported</b>The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link. Not every feature has been ported or hardware-verified on this board yet.</span></span>
<divclass="board"data-search="rakwireless rak wismesh tap v2 esp32-s3 touch node"><h4>RAK WisMesh Tap V2</h4><p>ESP32-S3 touch node.</p>
<spanclass="sup part"tabindex="0"><spanclass="dot"></span>Partially supported<spanclass="pop"><b>Partially supported</b>The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link. Not every feature has been ported or hardware-verified on this board yet.</span></span>
<spanclass="sup part"tabindex="0"><spanclass="dot"></span>Partially supported<spanclass="pop"><b>Partially supported</b>The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link over Wi-Fi/USB. Some features are still settling on this board (Bluetooth pairing has known gaps).</span></span>
<pstyle="color:var(--mut);font-size:.84rem;margin:0;line-height:1.5">Already have one? No USB flashing needed — install WADAMESH straight from the <b>Tanmatsu app store</b> on the device.</p>
<spanclass="sup part"tabindex="0"><spanclass="dot"></span>Partially supported<spanclass="pop"><b>Partially supported</b>The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link. This community-contributed board (PR #158) isn't hardware-verified by us yet — flash with care and report back.</span></span>
<pdata-wm-betaonlystyle="color:var(--mut);font-size:.78rem;margin:6px 0 0">New board, not yet hardware-tested — beta only. Please flash with care and report back.</p></div>
<divclass="noserial">Your browser can't flash here — use <b>Chrome</b> or <b>Edge</b> on a desktop.</div>
<pclass="hint">T-Deck not detected? Hold the trackball while tapping reset to force download mode.</p>
</div>
@@ -290,6 +501,11 @@
<divclass="guide-head"><h3>Install with bmorcelli's Launcher</h3></div>
<pclass="p"><ahref="https://bmorcelli.github.io/Launcher/"target="_blank"rel="noopener">Launcher</a> is a multi-app boot menu for ESP32 devices — keep it, and run WADAMESH as one of your apps.</p>
<pclass="p"style="margin-top:10px"><b>This path is for the LilyGo T-Deck only.</b> On the Heltec V4, install <ahref="#standalone"onclick="choose('standalone');return false">Standalone</a> instead.</p>
<pstyle="margin:8px auto 0;max-width:560px;font-size:.84rem;color:var(--mut)"><b>Stable</b> is the tested default; <b>Beta</b> gets new builds earlier. The download below follows this choice.</p>
<divclass="steps"style="margin-top:22px">
<divclass="step"><divclass="num">1</div><div><h4>Install Launcher first</h4><p>Flash bmorcelli's Launcher to your device, following its own installer — <ahref="https://bmorcelli.github.io/Launcher/"target="_blank"rel="noopener">bmorcelli.github.io/Launcher</a>.</p></div></div>
<divclass="step"><divclass="num">2</div><div><h4>Add the WADAMESH app</h4><p>Grab the WADAMESH <b>app image</b> for your board below, then install it from Launcher (via its web UI / OTA, or copy the <code>.bin</code> to your SD card).</p></div></div>
<pstyle="margin:11px 0 0;font-size:.84rem;color:var(--mut);line-height:1.5">You're downloading version <bdata-wn-tagstyle="color:var(--teal)">…</b><spandata-wm-chlabel></span>.<br>The file saves as <codedata-dl-fnamestyle="font-size:.82em">wadamesh-tdeck.bin</code> — rename it if you keep several.</p></div>
</div>
<pclass="hint">New to Launcher? It's the easiest way to try WADAMESH without giving up your other firmware — and you can fully switch to Standalone any time.</p>
</div>
</div>
</section>
<sectionid="videos">
<divclass="wrap">
<h2class="h2">Community videos</h2>
<pclass="p">Walkthroughs, demos and reviews from the community.</p>
<divclass="vids"id="vids"></div>
</div>
</section>
<footer>
<divclass="wrap">
Built for the mesh · open source under GPL-3.0 ·
@@ -404,21 +629,197 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
try { if (!localStorage.getItem('wm-cookie-ack')) cbar.classList.add('show'); } catch(_) {}
# beta_10 — what's new (one user-facing note per non-blank, non-# line)
# --- T-Deck input ---
New trackball mode — switch the T-Deck trackball between a mouse cursor (the default) and D-pad navigation (roll to move the selection, click to choose), under Settings → Keyboard. Handy if you prefer button-style navigation.
# --- Map ---
microSD map tiles now fill in as you explore: with "Use microSD for tiles" on, the device also downloads any missing tiles over Wi-Fi and saves them straight onto the SD card, so your offline library grows instead of showing blank areas past its edge (thanks 1nerdherder, #20)
# --- Power ---
Experimental battery saver (idle power-save): throttles the CPU when the device is parked — screen off, on battery, no app connected and the mesh quiet — to trim idle power use, without ever missing a received packet. Off by default; enable it under Settings → System → Battery. (Dan Vybiral / @Vybo, #19)
# --- Settings ---
New "Battery" section under Settings → System — opens the battery & power-history screen (the same one you get by tapping the battery icon up top) and is where the experimental battery saver now lives (it moved out of the Lock settings).
# beta_11 — what's new (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Keyboard navigation (headline) ---
New keyboard navigation — drive the whole UI from the keyboard, no touchscreen needed: the WASDZ cluster moves the on-screen selection in the actual direction you press (W up, A left, Z down, D right), S selects, Q goes back, and each bottom tab has its own hotkey (E Messages, R Contacts, T Home, U Map, I Settings) shown as a small letter on its icon. On by default; toggle it any time in Settings → Keyboard.
Every navigation key is hotkey-bindable — the directions, select, back, the per-tab hotkeys, and the new scroll-up / scroll-down keys (default F / C) — remap them all under Settings → Keyboard.
This lays the groundwork for upcoming keyboard-first devices like the Tanmatsu T-Pager that have no touchscreen, and it already gives every T-Deck a fast, reliable way to use wadamesh entirely from the keyboard.
The selected item now shows as clean reverse-video (a bright outline for switches) instead of a coloured border, and the highlight only appears while you're actively navigating with the keys.
The Home hotkey mirrors tapping the Home tab — press it again on Home to flip between the Commander screen and the app drawer.
# --- Stability ---
Fixed a reboot that could occur when receiving a message, especially during advert/message floods — a use-after-free while a list rebuilt mid-scroll (#27).
After a crash, the device now detects it on the next boot and offers to save the crash report so you can send it in, instead of you having to dig through Settings.
# --- Map ---
Map options can now switch the zoom control between the slider and a +/- button pair, like older builds (#26).
Route replay now opens on a wide overview so you can see where the hops start and the whole path at a glance — and you can pan and zoom the map freely while it plays.
# --- Home ---
New "App drawer as home" option (in the app drawer's cog) makes the launcher your home screen instead of the Commander view, for people who prefer the app grid.
# --- Open source ---
The in-progress Tanmatsu (ESP32-P4) port source is now part of the public tree.
# beta_12 — GPS hotfix (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- GPS fix (headline) ---
Fixed GPS being slow to get a fix — or sometimes never acquiring at all — on the T-Deck. The GPS receives its data over a small serial buffer, and on the busy touch UI a heavy screen redraw could stall long enough to overflow it and drop the satellite data mid-stream, which forced the receiver to start over. The buffer is now 16x larger, so those redraws no longer cost you the fix — acquisition is back to normal (a fix in about a minute instead of several, or none).
# --- A note from Kaj ---
More and more people are picking up wadamesh lately — which is genuinely awesome — and it's surfacing feature requests and bug reports fast, in big numbers.
I can't get back to every post in every channel, but I'm on it: I'm prioritising the fixes and quality-of-life improvements, and I'll be working through them later today. Thanks for all the reports and for the patience!
# beta_13 — community drop (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- A note from Kaj (headline) ---
This one's a community release — thank you! A bunch of the features below came from contributor pull requests, and the rest are the keyboard, typing and quality-of-life fixes I promised after the last drop. Huge thanks to everyone sending PRs, opening issues and reporting bugs: Yazutsu, tun0, Vybo and samuelcoustet all have their work in this build. Keep it coming.
Open a chat or channel and the cursor is ready — you can type straight away. Every other text field now waits for Enter before it captures the keyboard, so the letter-navigation keys keep working until you actually want to type.
Pressing Enter (or Esc / the back button) on an empty message box drops the cursor and re-selects the field, so you can navigate away with the buttons again.
The menu-bar letter shortcuts no longer jump you to another page while you're typing in a field.
New "Show menu-bar letters" setting under Settings -> Keyboard (off by default) — turn it on to see the tab hotkey letters on the bar.
The accent pop-up no longer grabs the keyboard when it appears, so you can keep typing right through it.
# --- Composer ---
New dedicated "Special characters" picker for symbols that aren't on the keyboard (% $ @ # & =, currency, maths, accents) — separate from the emoji picker.
Long-press one of your sent messages to Resend it.
# --- Clock ---
The 12-hour clock setting now applies everywhere — the top bar, the chat bubbles and the lock screen — not just the chat list.
# --- From contributors ---
Room-server posts now show the actual author's name on each message instead of the room's own name. (Yazutsu, #40)
New "Direct (0-hop)" filter for the contact list and the map — show only nodes you're hearing directly, with no repeater hops in between. (tun0, #39)
Beer emoji in colour, tap the unread badge in the status bar to jump to your Chats, a new "notify on new contact" toggle, and Czech accents in the accent pop-up. (Vybo, #22)
Optional trackball "edge scroll" — push the cursor past a screen edge to scroll the list underneath it (off by default, under Settings -> Keyboard). (tun0, #38)
TCP now restarts immediately after you turn it off and on again, the OTA action is hidden when there's no update slot to use, and a TCP-toggle alert glitch is fixed. (samuelcoustet, #29)
# --- Fixes ---
Heltec V4: removed the after-crash report prompt — that board has no SD card or reachable storage to save the report to, so it was a dead end. (The T-Deck still offers it.)
When both Wi-Fi and GPS are off, the clock now sets itself from the timestamps on messages received over the mesh.
# beta_14 — hotfix (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Fix (headline) ---
Fixed a bug introduced in beta_13: if you had turned keyboard navigation OFF (Settings -> Keyboard), the physical keyboard stopped typing into text fields entirely on the T-Deck and Tanmatsu. Typing now works again with keyboard navigation off; with it on, nothing changed. (The Heltec V4 was never affected.)
# --- Also in this release ---
Includes everything from beta_13: room-server posts show the author's name, the Direct (0-hop) contact + map filter, beer emoji with a status-bar unread tap and a new-contact toast toggle and Czech accents, optional trackball edge-scroll, the dedicated special-characters picker, the 12-hour clock applied everywhere, and the keyboard navigation + typing improvements.
# beta_15 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Power-saving now shows as an amber battery icon in the status bar — like iPhone's Low Power Mode — instead of a separate moon icon (T-Deck).
New @-mention picker: type @ in the message box to pick a contact from a list and drop their name in (thanks robinshizuyo-dev, #42).
# --- Fixed ---
Closing a settings item, menu or chat no longer snaps the list back to the top — it stays where you left it (thanks ZorkaTheDog, #45).
Messages you send from the MeshCore companion app now also show up on the device, in the right conversation (thanks HonzaDandy, #46).
Typing is more reliable on the T-Deck and Tanmatsu: tapping a text field — even one that's already highlighted — starts typing right away, and the accent pop-up no longer interrupts typing outside of chats.
The text cursor now only appears when a field is actually ready for input, so it's a dependable "you can type now" cue.
Smoother map panning: tile rendering no longer trips the watchdog and reboots during heavy scrolling.
# --- Changed ---
Keyboard navigation is now OFF by default. If you prefer driving the UI with the trackball / arrow keys, switch it back on in Settings -> Keyboard (your choice sticks until you change it). The Tanmatsu keeps it on, since it has no touchscreen.
# --- Also in this release ---
Includes everything from beta_14 and the earlier betas.
# beta_16 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Heltec V4 Expansion Kit support: a dedicated Sensors tab (temperature, humidity, pressure, GPS and battery, with small history charts), a Home-screen environment readout, and an on-device "mesh console" for poking at the radio. It only turns up when a sensor board is actually attached, so a bare V4 looks exactly as it did before. This one was a real grind to land — it came in as a huge pull request built on an old beta, and getting it in cleanly meant re-doing most of it by hand against the current code and carefully fencing it all off so it can't bloat or disturb the T-Deck and Tanmatsu builds. Thanks to Samuel Coustet for the original work (#29).
French, Dutch, German, Spanish and Italian keyboards, on-screen and on the T-Deck's physical keys. Choosing one of those as your interface language now auto-selects the matching layout. Also from Samuel Coustet (#29).
Wi-Fi firmware updates are back: on a normally-flashed device (not the Launcher) you can update straight from Settings -> About, no cable needed.
You can also roll back now — "Install a previous version" lets you reinstall one of the last few releases over Wi-Fi, with a confirmation first since it's a downgrade.
# --- Fixed ---
The status-bar clock no longer overlaps the node name on the V4 (it had been crammed against it for a good while).
Closing a dialog no longer lets a scroll carry through to the page behind it (thanks tun0, #50).
The contact action sheet no longer overflows the screen in landscape (thanks tun0, #49).
The per-channel region field now shows your default region as the hint, instead of a generic placeholder (thanks tun0, #48).
# beta_18 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
A topographic map style: turn on Map -> Options -> "Topographic map" for terrain-relief tiles (OpenStreetMap stays the default). Thanks to Samuel Coustet for the proxy work (#52).
New emoji in the picker: soccer ball, american football, moai, and the transgender flag.
Contacts can now sort ascending or descending, and there's a new "Nearest first" sort that orders contacts by distance from you (contacts without a shared location are left out).
The Discovered list gets the same sorting — by most-recently-heard, name, or distance — from a new sort button in its header.
The Chats header is tidied up: the add / mark-all-read / share-contact buttons are grouped together on the left, and sharing your contact now uses a proper QR icon.
# --- Fixed ---
The USB/PC companion connection no longer drops or throws "bad state" errors mid-session. The same fix also clears up repeater admin mode disconnecting and not being able to log in to member rooms.
Map tiles along the edges and corners of the view now fill in automatically as you pan, instead of needing a manual "reload tiles".
Reclaimed about 41 KB of memory by moving a large internal buffer to PSRAM — this fixes the Heltec V4 running out of RAM and rebooting when it loaded map tiles, and leaves more headroom on every board.
The map's "Show link lines" toggle is now remembered across reboots. Thanks to Yazutsu (#61).
Settings category names (Bluetooth, Radio & Mesh, Quick replies, ...) now fit on one line instead of wrapping or splitting mid-word.
Turning keyboard navigation on or off no longer snaps the settings list back to the top.
Heltec V4: the node name in the status bar no longer covers the clock.
T-Deck: the keyboard backlight no longer gets stuck on after flashing when it's set to Off.
Crash reporting is more reliable (it only prompts after an actual crash, not on every restart), and there are fewer stray reboots while saving settings.
Tanmatsu: changing the LoRa radio settings (frequency and so on) now takes effect immediately, without needing a reboot, and the app's settings now drive the radio.
# --- Also ---
Everything from beta_16 (and the Tanmatsu's beta_17) is included.
# beta_19 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Radio & Mesh settings now show an airtime / duty-cycle readout: roughly how much of the airtime budget your current spreading factor, bandwidth and coding rate use, plus the time-on-air per packet — handy for staying inside regional limits like EU 868.
A new "Answer telemetry requests" toggle in Radio & Mesh settings controls whether other nodes can pull your battery and environment telemetry.
The Radio & Mesh settings page is now split into clear RADIO / MESH / SIGNAL sections so the options are easier to find.
# --- Fixed ---
Fixed a reboot that could happen when you closed a scrollable popup — the emoji picker, quick replies, the Wi-Fi scan list, the app drawer and similar — while it was still coasting from a flick. Thanks to everyone who sent in crash reports.
The accent popup no longer lingers on screen after you send a message on the T-Deck keyboard.
Heltec V4: the Expansion Kit environment line and mini chart on the Home screen no longer overlap the status text or run off the bottom of the display.
# beta_20 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Spectrum analyzer: a new app in the app drawer that sweeps the LoRa band and draws a live spectrum with a waterfall under it, a colour scale on the side and a peak-frequency readout. Handy for spotting interference or finding a quiet part of the band before you settle on a frequency.
Custom notification sounds: you can set your own notification sound from a 16-bit WAV file on the SD card, per event, instead of only the built-in chime. There is also a separate switch for the direct-message chime. Contributed by Yazutsu (#66).
Saved Wi-Fi networks: the device remembers more than one network now and can auto-join them. Each saved network has its own auto-join switch and can be forgotten on its own.
One chat settings screen: the gear on a chat or channel row, a long-press on that row, and the cog inside an open chat now all open the same screen. Everything is in one place: mark as read, mute messages, mute mentions, region and scope, share secret, blocked users, and remove or delete.
Per-channel mute: silence the notification sound for a single channel while the rest keep chiming.
High-gain receiver for the Heltec V4.3: the V4.3 front-end module has a roughly 17 dB receive amplifier that the firmware never switched on, so every V4.3 shipped with it bypassed. There is now a "High-gain receiver (FEM LNA)" toggle under Radio and Mesh, in the Signal section. Turning it on can noticeably improve weak-signal reception in quiet or remote spots; in a noisy area it can overload the front end, so it is off by default. V4.2 boards do not have this amplifier and do not show the toggle. Thanks to Wireless Rocks for the report and the measurements.
Apps button on the Heltec V4 home screen: the V4 home screen now has an Apps button next to Send advert, so the app drawer (Spectrum, RF Monitor, Snake and the rest) is one tap away. The T-Deck and Tanmatsu already had this.
Blocked list is easier to reach: open it from Contacts (the overflow menu) and from the chat settings screen, not only from where it used to live.
Portuguese (Brazil) is now a UI language, which brings the total to twelve. Community translation.
# --- Fixed ---
Adverts sent from the touch screen are now relayed by region-scoped repeaters. The advert went out without a region tag, so a repeater set to a single region dropped it, while the same advert sent from the phone app got through. The device now tags its flood advert the same way the phone companion does. (Issue #68, reported by EricMW61.)
A message could be transmitted on the wrong channel. After you added or removed channels the cached channel index could drift, so a message typed in one channel was sent on another channel's key. It looked correct on your own screen but arrived in the other channel. The send path now matches the target channel by name at the moment of sending, so this can no longer happen.
Room server logins could time out when the only route to the server was through a region-scoped repeater. A new opt-in toggle, "Scope direct messages to region" under Radio and Mesh, in the Signal section, tags your direct, login and admin messages with your region so a region-only repeater will relay them. It is off by default, so cross-region messaging is unchanged for anyone who does not need it. (Issue #64, reported by randelreiss.)
The Wi-Fi scan could come back empty. If a scan ran while the device was already connected, the automatic reconnect could cancel it mid-sweep and the list returned zero networks. The scan and the reconnect no longer fight over the radio, and the SSID list loads reliably again.
# --- Under the hood ---
Flood scoping was reworked so channel floods and direct floods are handled separately. Channel and group messages stay tagged with your region scope so public channels remain contained, while direct messages, logins and admin traffic stay unscoped by default to keep cross-region messaging working. The new region-scope toggle opts the direct traffic into the region tag (issue #64), and the advert path was moved onto the same scoping the phone companion already used (issue #68).
The Spectrum app and the mesh share one radio. While Spectrum is open it takes ownership of the SX1262 so the mesh loop does not re-tune or re-arm receive underneath the sweep, and leaving the app restores the mesh radio settings and resumes normal receive. The sweep also waits out the chip's RSSI settle time after each retune and takes a short peak reading per step, so the trace shows real energy instead of the reset floor.
A shared flag now tells the main loop when a Wi-Fi scan is running, so the periodic reconnect does not disconnect and reconnect during a sweep, and auto-reconnect is paused for the length of the scan.
The Heltec V4.3 front-end module (KCT8103L) is now driven for receive with its low-noise amplifier behind the new setting. This is the large amplifier and it is separate from the SX1262's own internal RX-boost bit, which is only a fraction of a dB.
The new settings (per-channel mute, custom sounds, saved Wi-Fi networks, the high-gain and region-scope toggles, and the trackball-navigation default) are stored with versioned migrations, so updating from an earlier beta keeps your existing settings.
# --- Changed ---
Trackball and keypad navigation is off by default now and marked experimental, because it still has rough edges. With it off you get the on-screen cursor as before. When it is on it can now reach the top-bar buttons and the back and cog buttons on settings and tool pages.
First-time setup no longer has a Wi-Fi step. Instead it explains that the device can run Wi-Fi and Bluetooth at the same time, as a standalone radio and a phone companion together, and that you set those up later in Settings.
RF Monitor and Spectrum now use the title bar with a back button, the same as the settings pages.
# beta_21 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Stable and test update channels. beta_21 is the first stable build. On devices that update over Wi-Fi there is now a "Get test builds (beta)" switch on the About screen: leave it off to stay on stable releases, or turn it on to receive test builds earlier — it switches both the "update available" check and the firmware you install to the test channel. You can go back to stable at any time.
MQTT bridge (highly experimental, off by default). An opt-in bridge that forwards the messages your node receives to an MQTT broker of your choice, so you can log your mesh or wire it into other tools. It is gated behind a privacy consent you have to tick, direct messages are excluded unless you deliberately enable them, and the payload can be sealed with an encryption key so only you can read it. Use a broker you control, never a public one. This is experimental and may change. Contributed by Samuel Coustet (@samuelcoustet, PR #71), reworked for privacy.
Portugal (Narrow) region preset, so Portuguese users can pick the local defaults in one tap. (Issue #74, requested by Salgueiron.)
Saved-contacts counter. The Contacts overflow menu now shows how many contacts you have saved out of the maximum it can hold (for example 42 / 2000). (Issue #72, asked by Buko84.)
# --- Fixed ---
The signal probe never travels past the first node now. The "auto-discover" signal pulse, and the probe behind the Signal popup, are sent strictly zero-hop, so a repeater can no longer re-flood them across the mesh. The probe only measures the link to whatever hears it first, which is what it was always meant to do.
Your most relevant contacts now show when you have a lot of them. The on-screen contact list is capped for performance, but it was trimming the list before sorting it, so some contacts could drop out of view. It now sorts first, shows the closest matches, and adds a "+N more — search to narrow the list" line at the bottom. (Issue #73, reported by 1nerdherder.)
The blocked-users list opened from Contacts now shows the proper two-line title bar with a back button, instead of an unlabelled screen.
The Contacts overflow popup fits the screen again after the saved-contacts counter was added.
# beta_22 (TEST build — one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Fixed ---
The signal probe reads a real signal again. In beta_21 the probe was changed to a zero-hop advert so it could never be re-flooded across the mesh — but nothing answers a zero-hop advert, so the signal reading stopped updating. It now sends a directed trace "ping" to your nearest repeater instead: the repeater replies directly (it still never floods the mesh) and the device reads the link signal from that reply. So you get both — no mesh flooding, and a live signal again. Thanks to Tarmo for spotting this and suggesting the fix.
Contacts discovered while your phone is connected now show on the device too. If the T-Deck was connected to the MeshCore phone app over Bluetooth and heard a new node, the node appeared on the phone but not on the device's own screen, and the Contacts list did not refresh. The device now updates its own Contacts list and notification whether or not a phone app is connected. (Issue #73, reported by 1nerdherder.)
# --- Also ---
This is a test-channel build (opt in with "Get test builds" on the About screen). Everything from beta_21, the first stable, is included.
# beta_23 (TEST build — one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Fixed ---
Bluetooth companion reliability, especially messages and repeater admin. When a phone or Mac was connected over Bluetooth, the device was sending it a log entry for every LoRa packet it heard — on a busy mesh that swamped the Bluetooth link and starved the frames that actually matter. The device no longer sends that packet log over Bluetooth (USB and Wi-Fi companions still get it), so incoming messages reach the app and repeater / room-server admin (Manage Regions, saving parameters) stops timing out and dropping the connection. (Issues #46 and #54.)
# --- Also in this test line (carried from beta_22) ---
The signal probe reads a real signal again: it sends a directed trace "ping" to your nearest repeater, which replies, instead of a zero-hop advert that nothing answers. Thanks to Tarmo for spotting it.
Contacts discovered while your phone is connected now also show on the device's own screen, not just in the phone app. (Issue #73.)
# --- Also ---
This is a test-channel build (opt in with "Get test builds" on the About screen). Everything from the stable beta_21 is included.
# beta_24 (TEST build — one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Fixed ---
The signal probe now reads a real signal to your nearest repeater. It sends the standard mesh node-discovery request that repeaters actually answer, instead of the trace packet from the last test build that they ignored, so the signal reading updates again. This is the same request the Ultra and KiekR apps use. Thanks to Tarmo for decoding the exact packet.
# --- Also ---
This is a test-channel build (opt in with "Get test builds" on the About screen). Everything from the stable beta_21 and the earlier test builds is included.
# beta_25 (STABLE — rolls up the beta_22..25 test builds on top of the previous stable, beta_21.
# One user-facing note per non-blank, non-# line; # lines are section comments.)
# --- Fixed ---
The signal probe reads a live signal to your nearest repeater again. It sends the standard mesh node-discovery request that repeaters actually answer (the same one the Ultra and KiekR apps use), so the signal reading updates. Thanks to Tarmo for decoding the exact packet.
Bluetooth companion reliability, especially messages and repeater / room-server admin. The device no longer floods the Bluetooth link with a per-packet log (USB and Wi-Fi companions still get it), so incoming messages reach the app and admin actions like Manage Regions stop timing out. (Issues #46 and #54.)
Contacts show up right away. A node you discover, even while you're on another screen or one the phone app adds over Bluetooth, now appears in your contact list immediately instead of only after you change the sort. Thanks to 1nerdherder for the report. (Issue #73.)
Manual telemetry and status requests now work on the first tap instead of needing a second. Thanks to Dan Vybiral (@Vybo) for the fix. (Issue #81.)
On the emoji picker and quick-chat replies, pressing Esc or the red X now closes just the popup instead of the whole chat screen.
Lock screen: you can now unlock the device after the screen wakes from an incoming message.
# --- New ---
New "Advertise on boot" option in the Advertise app (off by default): turn it on to send one advert when the device powers on, so nearby nodes refresh your details after a reflash. (Issue #76.)
# --- Tanmatsu ---
Tanmatsu: Wi-Fi map tiles now download and display, cached to the microSD card.
Tanmatsu: contacts and message history now survive a reboot (moved to the microSD card, so keep a card inserted).
Tanmatsu: fixed the crash that stopped the app from booting; popups now fit the screen and scroll, and keyboard navigation stays inside an open popup.
# --- Also ---
This is the new stable build, promoted from the beta_25 test channel. It includes everything from the beta_22, beta_23, beta_24 and beta_25 test builds.
# beta_26 (TEST) — storage-reliability fix on top of the current stable (beta_25).
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.
# --- Improved ---
On devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). That frees the small internal flash and puts the frequently-rewritten files on the more reliable card. Your existing contacts move to the card automatically the first time; your identity and settings stay in internal flash, so the node is unchanged.
# beta_27 (STABLE — rolls up the beta_26 + beta_27 test builds on top of the previous stable, beta_25.
# One user-facing note per non-blank, non-# line; # lines are section comments.)
# --- Fixed ---
Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.
Devices installed under the Launcher no longer show the on-device "Install update" button. That update path only works on a standalone flash; under the Launcher it failed with "won't fit: Bad Size Given". The "update available" note stays, but you update through the Launcher instead. Thanks to Keith (KM1USR) for the report.
# --- New ---
Keyboard backlight brightness control on the T-Deck: a live slider plus Off / 25 / 50 / 75 / Max presets in Settings > Keyboard. It dims smoothly (fine-grained at the low end), applies to the on and auto modes and the new-message flash, and survives a reboot. Thanks to Yazutsu (PR #85).
# --- Improved ---
On devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). Your existing contacts move over automatically on first boot; your identity and settings stay in internal flash, so the node is unchanged. Keep the card inserted afterwards - the contact list lives on it, and an older firmware won't look there if you ever downgrade.
# --- Also ---
This is the new stable build, promoted from the beta_27 test channel. It includes everything from the beta_26 and beta_27 test builds.
# beta_28 (TEST) — room-server session fix + the wyvern.red feedback batch, on top of stable beta_27.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
Room servers no longer go one-way "frozen". The device now keeps its room login session alive in the background and automatically logs back in when the session drops, so posts keep being delivered and new room messages keep arriving. There is also a new "Log in again" option in the room chat's menu, and a clear warning when the room only granted you read-only access (posts would previously just vanish). Thanks to EricMW61 for the report (issue #89).
The screen now truly switches off in standby. Before, only the backlight went dark while the panel kept displaying the last image - the actual cause of the LCD ghosting / burn-in some of you noticed. The panel itself now sleeps, and on wake your screen is back instantly. Also: a lit lock screen now always dims on every board, a screen woken by a message re-darkens after 10 seconds, and the lock-screen clock drifts a few pixels to avoid ghosting. Thanks to wyvern.red for pinning this down.
The on-device "Install update" now also hides on mixed or older partition layouts where an update could crash the device halfway through (found via a user crash report - thanks!).
# --- New ---
Compact messages (IRC style): a new toggle in chat settings shows one dense line per message instead of bubbles, fitting two to three times more history on screen. Off by default. (wyvern.red's suggestion.)
Clearer delivery ticks: an up-arrow means sent but not yet confirmed, a double check means the other side confirmed it, and a failed message now says "tap to resend" - one tap resends it. (wyvern.red)
Keyboard navigation: the scroll keys (F/C) now zoom the map, and backspace in an empty message box exits typing mode so the navigation keys work again. (wyvern.red)
"Reset path" is now also in the chat's own menu next to Blocked users, so you can force a fresh route without going through Contacts.
# beta_29 (TEST) — clock hardening for room servers, BLE repeats-heard fix, deeper SD chat history, one-screen popups. On top of beta_28.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
Room logins can no longer be silently locked out by the device clock. Room servers quietly reject anything older than the newest timestamp they have seen from you, so a clock that stepped backwards (a reboot without GPS or phone around, or a sync correcting a fast clock) black-holed every Join with zero feedback. The device now remembers the newest timestamp it has used across reboots so it can never fall behind itself, a Join that gets no reply shows a clear error after 20 seconds (including what the device thinks the time is) instead of spinning forever, and the device warns you when its clock and the room server's clock disagree. Thanks to EricMW61 for the follow-up testing on issue #89.
"Repeats heard" works again over Bluetooth. The companion app counts repeats from the per-packet RX log, which beta_23 stopped sending over BLE to fix congestion. The firmware now recognises packets that are repeats of your own messages and lets exactly those through to BLE, so the counter works and the congestion fix stays. Thanks to daryldakine for the clean bisect (issue #94).
Messages sent from the phone app through the device now also appear on the device itself, as your own outgoing message in that channel. This worked for direct messages but never for channels.
Deleting a chat or channel now sticks. Deleted messages could survive in storage and resurrect the chat later, and removing a channel could miss the underlying channel entry so the thread came right back.
Settings pages no longer jump down when opened on keyboard-navigation devices, and the red update dot can no longer steal the selection from the About button.
The update check no longer shows "couldn't reach the update server" for six hours after one failed attempt. It now waits for Wi-Fi to actually have an address and retries within a minute.
The Tanmatsu About page now shows the real firmware version instead of a stale hardcoded one.
# --- New ---
Much deeper chat history on devices with an SD card: up to 5000 messages instead of 500, with date separators (Today, Yesterday, day names) between messages from different days. Note for testers: if you later downgrade below beta_29, the older build will not read the larger history file and will start with an empty history.
Chat and channel rows in the messages tab now use the same compact style as the Contacts tab, fitting more conversations on screen.
The chat or channel settings sheet (long-press or the gear) now fits on one screen, laid out like the contact menu, instead of scrolling.
Compact chat mode is easier to read: messages alternate between two background tones.
# beta_30 (TEST) — the big-mesh sluggishness fix + a much faster Spectrum analyzer. On top of beta_29.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
The periodic multi-second freezes on busy meshes are gone. Saving the contact list to storage took up to 3 seconds at ~570 contacts and ran on the UI thread every couple of minutes, which is exactly the trackball and touch hesitation people felt since beta_25. The save now writes the same data in a few large chunks instead of thousands of tiny ones, cutting it to a fraction. Thanks to guinnessbear for the report and Yazutsu for the precise measurements that pinned this down (issue #82).
The Contacts tab no longer rebuilds itself every 60 seconds just to refresh the "heard 2h ago" labels - they update in place now. That rebuild cost over a second on large contact lists and could yank the list while you were scrolling.
A burst of adverts can no longer queue several back-to-back Contacts rebuilds; they are now coalesced.
# --- Improved ---
The Spectrum analyzer feels live now: the waterfall gets a new line about four times per second instead of once per sweep, the trace advances across the band as it sweeps (like a bench analyzer), and a full sweep is about 20 percent faster.
The Spectrum trace also scales itself to the signal now: the chart tracks the live minimum and maximum, jumping out instantly for a strong signal (no more clipping flat at the top) and easing back once it is gone.
# beta_31 (TEST) — the new chats tab: avatars, message previews, custom channel icons. On top of beta_30.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
The chats tab has a fresh look: each conversation now shows a round avatar in that chat's signature colour (the same colour its bubbles use) with the first two letters of the name, the last message previewed under the name, the time top right and the unread count below it. Compact chat mode keeps the dense single-line rows instead, so both styles are available.
Channels can have a custom icon: open the channel's settings (long-press or the gear) and tap "Chat icon" to pick any emoji as its avatar. Long-press the same entry to go back to the automatic letters. The choice is remembered per channel.
# --- Fixed ---
Long chat names no longer wrap onto the message preview underneath.
# beta_32 (TEST) — the Wi-Fi freeze fix: a stale phone/web connection can no longer stall the device. On top of beta_31.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
The 1-2 second freezes every ~10 seconds some users saw with Wi-Fi on are gone. When a phone app or web client was connected over Wi-Fi and that connection went stale (phone asleep, walked out of range), every received mesh packet made the device wait on the dead connection before the UI could continue. The device now only writes to a connection when it can accept data immediately, and a connection that stays unresponsive for 10 seconds straight is dropped so the client can reconnect cleanly.
Saving chat history could briefly hitch the UI on busy meshes: the history file now writes in a few large chunks instead of thousands of tiny ones (the same treatment the contact list got in beta_30), and background saves are spaced further apart while messages stream in.
# --- New ---
The About page now shows recent loop stalls (anything over 0.2 seconds, newest first), so freeze reports can be confirmed or ruled out at a glance.
# beta_33 (TEST) — the MQTT freeze fix + the About page no longer stalls itself. On top of beta_32.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
The freezes with MQTT enabled are gone. Connecting to the broker (DNS lookup, TCP connect, login) could block the device for 2-10 seconds and retried every 15 seconds while the broker was unreachable, all on the UI thread. Connection attempts now run fully in the background, and writes to a broker that stops responding are skipped instead of waited on (the same treatment beta_32 gave phone/web connections). Tip: if your bridge keeps reconnecting, check the broker address and credentials - a healthy connection does not retry.
The About page caused a ~0.4 second hitch every second while it was open: computing the firmware image size checksums the whole ~3 MB app in flash and ran twice per refresh. It is now computed once per boot, so About (and its stall list) no longer disturbs the very measurements it shows.
Favoriting a contact when the favorites list is full (16) now says "Favorites full (max 16)" instead of the incorrect "Removed from favorites".
# beta_34 (TEST) - buffered receive: the missed-messages fix, ON by default on this test build. On top of beta_33.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
This build changes how the device collects packets from the LoRa radio, and the new way is ON by default so the test channel can prove it. Background: the radio chip holds only one received packet at a time, and until now the firmware only collected it when the main loop got around to it, the same loop that draws the screen. If the interface kept the loop busy while two packets arrived back to back, the first one was silently overwritten. That is the "my device missed a message another device right next to it heard" problem some of you reported, and why it affected this firmware but not stock MeshCore, which has no screen to draw. A small dedicated background task now collects every packet within about a millisecond of it arriving, no matter what the interface is doing.
Because the receive path is the heart of the device, this ships with a safety hatch: if you see anything odd with receiving, sending, or the Spectrum analyzer, turn off "Buffered receive (experimental)" in Settings > Radio & Mesh (applies instantly, no reboot) and you are back on the old path. Please report it either way, that is exactly what the test channel is for.
The About page now shows LoRa receive counters (heard / read / late-lost). "late-lost" counts packets the radio heard but the firmware failed to collect in time. With buffered receive on it should stay at zero. If you turn the toggle off on a busy mesh you may watch it creep up, which is the old bug happening live.
# --- Fixed ---
Messages sent by wadamesh no longer carry a few stray bytes at the end that stock MeshCore devices and some companion apps displayed as garbage characters after the text (issue #13).
A GPS module reporting a bogus date can no longer drag the device clock back to the year 1902 (core clock guard).
# beta_35 (TEST) - V4 breathing room: freezes fixed, 30 KB of internal RAM back, crash-safe settings. On top of beta_34.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
Heltec V4: multi-second freezes right after receiving messages are gone, including the screen refusing to wake from dark mode. Saving chat history to the internal flash could stall the device for up to 6 seconds (the V4 has no SD card, and internal flash pauses for housekeeping); that save now runs on a background task, so the screen and touch stay responsive no matter what storage is doing.
Turning Bluetooth on while Wi-Fi is running no longer reboots the device when memory is tight: the switch now checks free memory first and tells you "Not enough free memory for Bluetooth. Turn Wi-Fi off first." instead of crashing.
The same protection applies the other way around: turning Wi-Fi on with Bluetooth active used to claim "Wi-Fi on" while the radio silently never started. It now either really starts or tells you why it cannot.
Settings could vanish for a single boot after an unlucky reboot (the device came up with a default name once, then recovered). Settings writes are now crash-safe: a reboot or power cut at any moment leaves at least one intact copy on storage, the loader self-heals from it, and a failed save now says "Couldn't save the name to storage" instead of pretending it worked.
# --- Improved ---
About 30 KB of the ESP32's scarce internal RAM has been freed by moving buffers to the external PSRAM and dropping unused USB components from the build. This is the headroom that makes Wi-Fi and Bluetooth run together comfortably again, especially on the Heltec V4.
# beta_36 (TEST) - Launcher SD storage really works now, Wi-Fi settings redesign, symbol passwords, lock-screen unread count. On top of beta_35.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
The "Store data on SD" toggle is now actually honored after a reboot (fix contributed by scratchdiver, PR #123). On Launcher installs the setting silently reverted to internal storage every boot, which also meant those devices kept the slow internal-flash writes. If you run wadamesh under the Launcher and your mesh is busy, this one is for you: flip the toggle once and contacts and settings really move to the SD card.
Turning "Store data on SD" off works reliably now too, including on Launcher installs (follow-up to PR #123).
Passwords with special characters can now be entered everywhere: long-press any settings field, including the Wi-Fi password, for the Cut / Copy / Paste / All / Sym menu. Sym opens a picker with the characters the on-screen keyboard does not have (issue #122). Double-tap selects a word.
# --- New ---
The Wi-Fi network screens (join a network, hidden network, saved-network details) are full-screen pages now instead of small popups: same style as the other settings pages, title in the top bar, tap the bar to go back, and everything scrolls properly with the keyboard open.
The lock screen shows an envelope with the number of unread messages under the clock, so you can check for news without unlocking (idea from issue #93). It disappears when there is nothing unread.
For Launcher installs: a "Save update bin to SD" button on the About page downloads the latest firmware of your selected channel into a BINS folder on the SD card, named with the version and channel, ready to flash from the Launcher. The test-builds toggle is now visible on Launcher installs as well.
New region preset "Australia (Mid)": 915.075 MHz, 125 kHz, SF9, CR5 (requested in issue #121).
# beta_37 (TEST) - Tanmatsu ALT accents + language switching, GPS quick reply, T-Deck touch self-heal, SD history reliability. On top of beta_36.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
Tanmatsu: hold ALT and press a letter to open the accent picker (a with ALT gives an accent popup, just like long-press on the touch boards). Keep ALT held and press the letter again to cycle through the options; release ALT to insert the highlighted one. Requested by jochentj in issue #129.
Tanmatsu: double-tap the space bar to switch between your enabled keyboard languages, same as on the T-Deck.
Accented letters on the Tanmatsu now render at full size at the Large and Huge UI scales instead of noticeably smaller (issue #129). Made possible by moving all boards to compressed UI fonts, which also frees a chunk of firmware space.
Your GPS position is now the first row in the quick-reply picker, so you can send your current coordinates with one tap (contributed by Yazutsu, PR #125).
While in a chat or channel, the top bar shows the active keyboard language next to the clock (only when more than one layout is enabled), so you can see what double-tap-space switched to.
# --- Fixed ---
The keyboard-backlight "Off" preset now actually switches the backlight off and stays off, instead of leaving the previous mode running (contributed by Yazutsu, PR #88).
Chat history saves to the SD card are more robust: files are written atomically and a fallback save path covers the background worker not being armed yet, so a badly timed reboot can no longer truncate your message history (contributed by scratchdiver, PR #135).
T-Deck: if the touch controller glitches into a stuck "phantom finger" state (everything looked frozen, touch dead, trackball clicks eaten, and the screen never timed out), the driver now detects it and recovers within seconds. Previously only a full power cycle would clear it.
T-Deck: touch initialisation at boot now retries for a few seconds instead of giving up after a single attempt. A slow-to-wake touch controller could previously cost you touch AND keyboard for the whole session.
# beta_38 (TEST) - Full-history chat scrolling, message delete, two new boards, storage integrity fixes. On top of beta_37.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
Chat scrolling is fully virtualized (contributed by scratchdiver, PR #130): a chat now scrolls through its ENTIRE stored history, up to 5000 messages on SD-card devices, smoothly in both bubble and compact mode, instead of only the most recent rows. Several LVGL crash fixes came along with it.
Long-press any message for a new Delete option that removes just that message. The message menu now lays its buttons out two per row so it always fits on one screen.
The chat and channel settings sheet gains "Delete history": wipes the whole conversation after a confirmation, while the channel membership or contact stays.
The in-chat jump buttons are now slim arrows tucked against the right edge of the screen: fully visible while you scroll, fading to half opacity a second after you stop.
ThinkNode M9 hardware support is complete (contributed by ded, PR #138): GPS, microSD, buzzer, backlight control, lock screen, long-press context menus, and a d-pad-driven setup wizard. His fixes for focus scrolling in nested pages and the lock-screen paint order benefit every keyboard-nav board.
New board in the source tree: RAK WisMesh Tap V2 (contributed by Ethac.chen, PR #136), with a 30+ fps display driver. Prebuilt images follow once the web-flasher integration lands.
Tanmatsu: the app now builds size-optimized, roughly 300 KB smaller, leaving real headroom under the launcher's app-size limit for future features.
# --- Fixed ---
Messages no longer vanish when you reboot the device from the phone app (reported by Leon P). The history writer batches saves up to 30 seconds apart, and the app's reboot command skipped the final save that every on-device reboot path does - so the newest messages, read or unread, were lost. The reboot command now saves first.
T-Deck: turning on "Store data on SD" now migrates your existing profile, settings and identity to the card FIRST. Upgrading to beta_36/37 with the toggle already on could adopt an empty card and boot as a brand-new node while your real profile sat orphaned on internal flash.
T-Deck: for anyone already hit by that: Set -> General -> "Copy internal data to SD" restores the profile from internal flash onto the card (confirmation, then automatic reboot). Until you run it, toggling "Store data on SD" OFF and rebooting also brings the old profile back.
Tanmatsu: profile name, settings and the node identity now persist reliably across reboots. They lived on the internal flash partition whose metadata layer is unreliable on this board; they now live on the SD card with everything else, and existing data is rescued automatically on first boot.
# beta_39 (TEST) - Translations, deletes that stick and are fast, emoji fixes. On top of beta_38.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
Big translation pass: hundreds of previously-English strings across menus, settings, dialogs and the setup flow are now translated into all 11 non-English languages (Dutch, German, French, Spanish, Italian, Russian, Ukrainian, Bulgarian, Serbian, Greek, Brazilian Portuguese). Menu rows that carry an icon now translate too, instead of silently falling back to English. Switch language under Settings.
ThinkNode M9 and RAK WisMesh Tap V2 can now be installed straight from the web flasher.
# --- Fixed ---
Deleting a message, or clearing a whole chat, now sticks: it survives a reboot or a hardware reset instead of coming back (reported by Leon P and MrWeavis). The delete used to sit in a save that was batched up to 30 seconds later, so a reset in that window undid it.
Deleting is also much faster now. The chat history is written compactly - only the messages you actually have, instead of the full 5000-slot ring every time - so a delete, a history wipe, or a reboot no longer pauses the screen while it saves. Normal message saving is lighter too.
Emoji picker: the food and object emoji that showed as empty boxes (umbrella, coffee, pizza, cake, rocket, camera, phone, laptop, calendar, antenna, e-mail, house, car, watch) now render properly - they were listed but never baked into the font.
# beta_40 (STABLE) - the first stable since beta_35. Rolls up everything from beta_36 through beta_40: reliable SD storage, message delete + full-history scrolling, a big translation pass, two new boards, and a pile of storage/perf/stability fixes.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
Big translation pass: hundreds of previously-English strings across menus, settings, dialogs and the setup flow are now translated into all 11 non-English languages (Dutch, German, French, Spanish, Italian, Russian, Ukrainian, Bulgarian, Serbian, Greek, Brazilian Portuguese). Menu rows that carry an icon translate too. Switch language under Settings.
Chat scrolling is fully virtualized (contributed by scratchdiver, PR #130): a chat now scrolls through its ENTIRE stored history, up to 5000 messages on SD-card devices, smoothly in both bubble and compact mode, instead of only the most recent rows.
Long-press any message for a Delete option that removes just that message; the message menu lays its buttons two per row so it fits on one screen.
The chat and channel settings sheet gains "Delete history": wipes the whole conversation after a confirmation, while the channel membership or contact stays.
The in-chat jump buttons are slim arrows tucked against the right edge: fully visible while you scroll, fading to half opacity a second after you stop.
ThinkNode M9 hardware support is complete (contributed by ded, PR #138): GPS, microSD, buzzer, backlight control, lock screen, long-press context menus, and a d-pad-driven setup wizard. It can be installed straight from the web flasher.
New board: RAK WisMesh Tap V2 (contributed by Ethac.chen, PR #136), with a 30+ fps display driver, also installable from the web flasher.
The Wi-Fi network screens (join a network, hidden network, saved-network details) are full-screen pages now instead of small popups, matching the other settings pages and scrolling properly with the keyboard open.
The lock screen shows an envelope with the number of unread messages under the clock, so you can check for news without unlocking (idea from issue #93).
Passwords with special characters can be entered everywhere: long-press any settings field, including the Wi-Fi password, for a Cut / Copy / Paste / All / Sym menu. Sym opens a picker with the characters the on-screen keyboard lacks (issue #122). Double-tap selects a word.
For Launcher installs: a "Save update bin to SD" button on the About page downloads the latest firmware of your selected channel into a BINS folder on the SD card, ready to flash from the Launcher. The test-builds toggle is visible on Launcher installs too.
New region preset "Australia (Mid)": 915.075 MHz, 125 kHz, SF9, CR5 (requested in issue #121).
Your GPS position is now the first row in the quick-reply picker, so you can send your current coordinates with one tap (contributed by Yazutsu, PR #125).
Tanmatsu: hold ALT and press a letter to open the accent picker; keep ALT held and press again to cycle, release to insert (requested by jochentj, issue #129). Double-tap the space bar to switch between your enabled keyboard languages, same as the T-Deck.
While in a chat, the top bar shows the active keyboard language next to the clock when more than one layout is enabled.
# --- Fixed ---
The "Store data on SD" toggle is now actually honored after a reboot (fix contributed by scratchdiver, PR #123). On Launcher installs it silently reverted to internal storage every boot, which also kept the slow internal-flash writes. Turning it off works reliably now too.
Turning on "Store data on SD" migrates your existing profile, settings and identity to the card FIRST, so upgrading no longer risks adopting an empty card and booting as a brand-new node. If you were already hit by that, Settings -> General -> "Copy internal data to SD" restores the profile from internal flash.
Card-less devices (Heltec V4, and any board without an SD card) no longer freeze for several seconds at a time. A busy mesh re-writes the whole contacts file to internal flash on every advert refresh, and that flash garbage-collection stalled the whole device; contact saves are now coalesced so only real changes (a new or removed node) are written promptly.
Deleting a message, or clearing a whole chat, now sticks: it survives a reboot or a hardware reset instead of coming back (reported by Leon P and MrWeavis). Deleting is also much faster - chat history is written compactly, only the messages you actually have, so a delete, a history wipe, or a reboot no longer pauses the screen while it saves.
Messages no longer vanish when you reboot the device from the phone app (reported by Leon P): the app's reboot command now saves chat history first, like every on-device reboot path already did.
Chat history saves to the SD card are more robust: files are written atomically with a fallback save path, so a badly timed reboot can no longer truncate your message history (contributed by scratchdiver, PR #135).
Longer translated labels (French, German, etc.) that used to clip or wrap now shrink to fit: the message action menu, confirmation dialogs, and the app-drawer tiles all keep their text on one line. The signal card's "tap for details" hint no longer overlaps in other languages.
Emoji picker: the food and object emoji that showed as empty boxes (umbrella, coffee, pizza, cake, rocket, camera, phone, laptop, calendar, antenna, e-mail, house, car, watch) now render properly.
Tanmatsu: profile name, settings and the node identity persist reliably across reboots (moved off the unreliable internal-flash partition onto the SD card, with existing data rescued on first boot). Accented letters render at full size at the Large and Huge UI scales. The About page shows the correct firmware version now.
T-Deck: if the touch controller glitches into a stuck "phantom finger" state (touch dead, trackball clicks eaten, screen never timing out), the driver detects it and recovers within seconds instead of needing a full power cycle. Touch initialisation at boot also retries for a few seconds instead of giving up after one attempt.
The keyboard-backlight "Off" preset now actually switches the backlight off and stays off (contributed by Yazutsu, PR #88).
# beta_41 (TEST) - experimental web control: view and drive the wadamesh UI from a phone or desktop browser over Wi-Fi. Two new apps, VNC and REMOTE.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
New VNC app (in the app drawer, between Advertise and Signal): it mirrors the live device screen into a web browser over Wi-Fi and lets you drive it. Taps and typing in the browser control the device, and anything that changes on the device shows up in the browser. Turn it on, then open http://<device-ip>:8765/ from any browser on the same network. The app page itself is scrollable.
New REMOTE app: a headless mode that renders the whole UI at a larger, browser-friendly resolution and serves it over Wi-Fi, while the device's own screen drops to a placeholder showing the address to open. Meant for driving a device as a web client, and as groundwork for screenless hardware. Turning it on or off reboots the device. On the web page a Rotate button flips between landscape (desktop) and portrait (phone); on the device, double-tap the space bar to leave remote mode.
Both features are served by the device itself over plain Wi-Fi - no phone app, no account, no cloud, just a browser on the same network.
# --- Note ---
This is an experimental first cut, and an honest heads-up from Kaj: for me the remote UI still feels too slow, and I will most likely move toward a dedicated, lighter remote client instead of streaming the entire live UI. It is in this test build mainly because community feedback on it would be genuinely useful - if you try it, let us know how it runs for you and what you would actually want from a web or remote client.
# beta_42 (TEST) - fixes for the beta_41 REMOTE mode: it works on the Heltec V4 now, and keyboard-less boards can leave it.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
REMOTE mode now works on the Heltec V4 TFT (and other 2 MB-PSRAM boards) instead of showing a black screen in the browser. Its off-screen renderer needed more free memory than those boards had, so nothing was streamed; the buffers are sized to fit now, and the render is a bit smaller and faster as a result. VNC was never affected and already worked.
Keyboard-less touch boards (Heltec V4, RAK WisMesh Tap) can leave REMOTE mode again: touch and hold the device screen for about 3 seconds. Those boards have no SPACE key, which used to be the only way out on the device.
# --- New ---
The web page has an "Exit remote" button (with a confirmation) that leaves REMOTE mode from any browser, on any board.
# beta_43 (TEST) - the browser control panel grows into a full remote app (Chats, Contacts, Discovered, Settings), plus a Heltec V4 map-tile fix.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New: web control panel ---
The browser control panel is now a full remote app - you can run the device from your phone or laptop without ever picking up the T-Deck. Proper Chats, Contacts, Terminal and Settings tabs.
Contacts tab: search, sort and filter your contacts, see each one's distance, coordinates and last-heard age, and tap a contact for the same actions you get on the device (favourite, block, reset path, remove).
Discovered nodes show up in the browser too, each with a one-tap "Add" to save it as a contact - so you can build your whole contact list from the web app.
Chats tab: pop-up notifications (with an optional beep) when a message arrives, and the open chat refreshes on its own as replies come in - no manual reload.
Acknowledgements and repeats-heard now show for messages you send from the web app, the same as on the device.
A Settings page for the everyday options that make sense remotely, including live radio changes.
A quick-command button in the Terminal that drops common commands straight into the input box.
# --- New: device ---
Map options has a "Tile debug overlay" toggle (off by default) that shows a live read-out of the map-tile pipeline, for diagnosing tile problems.
# --- Fixed ---
Heltec V4: map tiles downloaded but only the top of the map rendered and the rest stayed black. The 2 MB-PSRAM V4 was running out of contiguous memory for the tile grid; tiles now reuse a small fixed pool and only load what is on screen, so the whole map fills in smoothly again.
Heltec V4 in remote/VNC mode: typing from a physical keyboard in the browser now works (it previously only worked on the T-Deck).
The remote-access screen on the device is clearer: the IP address is shown at the top, the explanation is shorter and more accurate, the toggles sit above it, and the address box only appears once remote access is on.
The Terminal "help" command now lists every command, on both the device and the web terminal (it was missing a lot).
# --- Changed ---
The web app header now shows the same signal, Wi-Fi, Bluetooth and battery icons as the device status bar, and the title reads WADAMESH.
The contact and chat sort/filter controls open a menu to pick an option instead of cycling through them on each tap.
Loaded 100 of 332 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.