Commit Graph
379 Commits
Author SHA1 Message Date
István def866d07b Merge branch 'main' into pisti87-hungarian-lang 2026-08-04 07:43:02 +02:00
Kaj SchittecatandClaude Opus 4.8 169971205d touch: run the SD format + paste workers on every board that offers them (#172)
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>
2026-08-03 17:02:14 +02:00
Kaj SchittecatandClaude Opus 4.8 f4a6c1b3ad touch: recolor-safe node names in Discover feed + msg-info trace (#223)
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>
2026-08-03 16:52:34 +02:00
István 4b752919a4 Update i18n.cpp 2026-08-03 16:32:50 +02:00
István 402585d0b5 Update i18n.cpp 2026-08-03 16:25:52 +02:00
Kaj Schittecat e1aceec8a8 Merge pull request #212 from PixPMusic/pixpmusic/sd-prefs-concurrency 2026-08-03 15:52:04 +02:00
Kaj Schittecat 829278c9a2 Merge pull request #213 from PixPMusic/pixpmusic/storage-wdt-state 2026-08-03 15:51:55 +02:00
Kaj Schittecat c64a6eae49 Merge pull request #211 from PixPMusic/pixpmusic/datastore-pref-rename 2026-08-03 15:45:48 +02:00
Kaj SchittecatandClaude Opus 4.8 78fed3a09d touch: Radio & Mesh toggle for the #207 auto-retry (default ON, opt-out)
'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>
2026-08-03 15:23:19 +02:00
István 568001e1db feat(lang): add Hungarian language v0.3 2026-08-03 11:19:04 +02:00
Pixel Perfect afd41c393f fix(storage): make the heavy-WDT depth counter atomic
_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>
2026-08-02 13:15:06 -07:00
István b7d55db5e6 feat(lang): add Hungarian language v0.2
v0.1 6-303 ok
v0.2 305-386 ok
2026-08-02 21:52:52 +02:00
István 0d5b1ec9c6 add lang: hu
hungary lang added 0.1 1-303. ok
2026-08-02 19:09:20 +02:00
Pixel Perfect 4418b1f598 fix(prefs): avoid heap churn on cached reads
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 23:57:39 -07:00
Pixel Perfect 42a545a570 fix(storage): keep IDF watchdog guards nontrivial
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 22:34:41 -07:00
Pixel Perfect 7f4776e20d fix(storage): let clean preference flushes succeed
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 22:03:33 -07:00
Pixel Perfect a92afa191c fix(storage): gate loop watchdog APIs in IDF apps
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 21:59:17 -07:00
Pixel Perfect 8252e43b32 fix(storage): deduplicate concurrent preference loads
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 21:20:48 -07:00
Pixel Perfect 1e6a6b4b35 fix(storage): inspect the Arduino loop task watchdog
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 21:20:48 -07:00
Pixel Perfect 958ace84aa fix(storage): make file-backed preferences concurrency-safe
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 20:37:01 -07:00
Pixel Perfect 9202ad0a10 fix(storage): preserve loop watchdog state
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 20:33:06 -07:00
Pixel Perfect 603217312e fix(storage): commit rooted preference updates
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 20:33:04 -07:00
mikecarper 04d5f33575 Retry messages until an echo is heard 2026-08-01 17:13:03 -07:00
Kaj SchittecatandClaude Opus 4.8 bcc084820c touch: radio presets set a REAL duty limit — airtime factor was 10x off (#161)
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>
2026-08-01 12:23:18 +02:00
Kaj SchittecatandClaude Opus 4.8 a2548c39ab touch: T-Pager gets the full store-on-SD machinery (#193)
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>
2026-08-01 12:19:46 +02:00
Kaj SchittecatandClaude Opus 4.8 badad25305 touch: issue batch — unlock branding (#165), Discover row overflow (#191), scope in details (#157)
- #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>
2026-08-01 12:13:08 +02:00
Kaj SchittecatandClaude Opus 4.8 51617b404e touch: SD mount ladders renegotiate the clock upward after a slow-rung init (#194)
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>
2026-08-01 11:36:24 +02:00
Kaj Schittecat 67b863d93e Merge branch 'main' of https://github.com/ALLFATHER-BV/wadamesh 2026-08-01 11:29:52 +02:00
Kaj 44a4a354b0 Merge pull request #197 from PixPMusic/pixpmusic/pager-ui-size
feat(touch): add T-Pager UI size presets
2026-08-01 11:29:50 +02:00
Kaj Schittecat a3b535237e Merge branch 'main' of https://github.com/ALLFATHER-BV/wadamesh 2026-08-01 11:18:54 +02:00
Pixel Perfect 05f4ba6542 feat(touch): add T-Pager UI size presets
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-01 02:14:24 -07:00
Kaj SchittecatandClaude Opus 4.8 c0d702a8a7 touch: control-centre GPS tile long-press opens the GPS page, not Radio
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>
2026-08-01 10:15:26 +02:00
István 2270c5d2cc Update i18n.cpp 2026-08-01 05:34:55 +02:00
Pixel Perfect 2001ef72dd refactor(esp32): persist preferences off the UI task
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-07-31 18:08:27 -07:00
Pixel Perfect 540084a718 fix(touch): stop persisting unused tab state 2026-07-31 17:10:55 -07:00
Pixel Perfect 1c761e7846 fix(esp32): make file-backed preferences crash-safe
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-07-31 09:57:44 -07:00
Kaj SchittecatandClaude Opus 4.8 40c4f205d1 touch: V4-R8 landscape touch was point-mirrored; boot forces portrait as rescue
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>
2026-07-31 17:44:11 +02:00
Kaj Schittecat d285e85d72 Merge branch 'main' of https://github.com/ALLFATHER-BV/wadamesh 2026-07-31 17:31:50 +02:00
Kaj 72231fd5aa Merge pull request #189 from jcyrio/romanian-keyboard
Add Romanian keyboard
2026-07-31 17:31:47 +02:00
Kaj 3c5a6aa658 Merge pull request #188 from jcyrio/romanian-translation
Add Romanian (Română) UI translation
2026-07-31 17:31:44 +02:00
Kaj SchittecatandClaude Opus 4.8 83887976f0 p4: fix the storage dispatcher (it never engaged) + the measured #167 mechanism
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>
2026-07-31 17:00:27 +02:00
Kaj SchittecatandClaude Opus 4.8 099ccd8e3c p4: core-0 storage dispatcher + the completed #167 experiment matrix
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>
2026-07-31 15:37:05 +02:00
Kaj SchittecatandClaude Opus 4.8 3bf56cdd37 p4: #167 resolved -- SD writes flash the AMOLED; hot store moved to internal LittleFS
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>
2026-07-31 14:50:37 +02:00
Kaj SchittecatandClaude Opus 4.8 c1d166e5e7 Revert: p4 prefs-on-SD, wider LVGL bands, XIP-off (#167)
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>
2026-07-31 12:02:57 +02:00
Kaj SchittecatandClaude Opus 4.8 1e6e121c85 p4: blank the framebuffer before display-on; widen LVGL bands (#167)
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>
2026-07-31 11:40:40 +02:00
Kaj SchittecatandClaude Opus 4.8 1001ff7ce9 touch: GPS settings page never refreshed while open
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>
2026-07-31 10:27:04 +02:00
Kaj SchittecatandClaude Opus 4.8 55e9484234 touch: keep the GPS line one-line in the top-bar dropdown, and stop over-claiming
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>
2026-07-31 10:11:48 +02:00
Kaj SchittecatandClaude Opus 4.8 5c9f9cffef touch: GPS page says what it is actually doing, not just "acquiring..."
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>
2026-07-31 09:53:52 +02:00
Kaj SchittecatandClaude Opus 4.8 4942fcf5aa touch: configurable per-chat history limit, default 250, warns when turned off
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>
2026-07-30 17:27:32 +02:00
Kaj SchittecatandClaude Opus 4.8 b846dc62fc touch: chat-list preview stuck on an old message (T-Display P4)
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>
2026-07-30 17:22:16 +02:00