Commit Graph
53 Commits
Author SHA1 Message Date
Christopher Van HooseandClaude Fable 5 16e883187c M9 + all boards: persist the companion app-sync ring across power cycles
User report: the M9 "does not remember chats or sync them to the app" while
the V4-R8 does. The companion sync ring (MyMesh::history_ring + per-client
cursors) was RAM-only on every board — invisible on a USB-powered V4 that
rarely loses power, fatal on the M9 whose only off is the hard-cut slider.

Shared fix: the ring is now an append log (/synchist) plus client cursors
(/synccur) on DataStore::getHotDataFS(), restored in MyMesh::begin() and
flushed by persistSyncHistoryNow() on the paths that lose RAM (deep sleep,
power-off, explicit save). Record + bench-test recipe: M9_PORT.md Deferred #13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 13:50:48 -04:00
Kaj SchittecatandClaude Opus 5 a96900d7ee core: patch changed contact records in place instead of rewriting the table (#222)
Yoss101 reported the beta_62/63 fix helped but did not cure it: the freezes got
rarer AND longer. Rarer is the blob-delete queue working; longer is what was
left, and it was always the bigger half.

MAX_CONTACTS is 2000 and a contact record is 152 bytes, so saveContacts was
rewriting a 304 KB file on every change, with a same-sized .tmp alongside it for
the atomic swap. On a card-less V4 that is ~608 KB of peak churn on a 3.375 MB
SPIFFS volume that is already carrying one blob file per contact. SPIFFS GC cost
scales with how full the volume is, and GC suspends the flash cache, which stalls
BOTH cores — so the fuller the table, the longer the device is simply gone.

The table never actually changes in bulk: eviction replaces contacts[oldest] in
place (the array is unsorted and never shifts) and an advert refresh touches one
entry. So compare each record against what is already on disk and write back only
the ones that differ. An eviction now writes 152 bytes instead of 304 KB, an
unchanged table writes nothing, and there is no .tmp and no free-space spike.
Reads never trigger GC, so the compare scan is cheap.

Falls back to the full atomic rewrite whenever the mapping is not provably safe
(no live file, ragged size, or a table that shrank — there is no truncate here),
and never truncates or renames, so a fallback always leaves a valid list on disk.
Verified against the real function source with an in-memory FS harness: steady
state, eviction, growth, shrink, filtered anon slots, missing file, ragged file,
and 167 scattered changes all match a full rewrite byte-for-byte.

Also: the blob-delete queue silently ORPHANED a blob on overflow — nothing else
ever deletes it, so it leaked flash permanently on the one metric that drives GC
cost. Depth 8 -> 32, and overflows are now counted and surfaced.

New About > diagnostics block "Contact store": contact count, approximate on-disk
size, internal-flash used percentage, and orphaned blobs. The used percentage is
the number that predicts these freezes, so a reporter can photograph it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:36:37 +02:00
Kaj SchittecatandClaude Opus 5 e1db4f50f2 feat: let a companion app opt into the full RX log over BLE (#256)
Since beta_23 the per-packet RX log (PUSH_CODE_LOG_RX_DATA, 0x88) is kept off
BLE, because it floods a ~16 frame/sec link and starves chat and admin traffic
(#46, #54). That is still the right default and is unchanged here.

But it is also the ONLY frame carrying the transport codes and the full relay
path — RESP_CODE_CHANNEL_MSG_RECV_V3 has neither — so coverage and region
mapping apps have had no way to reconstruct either over BLE. @marcelverdult,
who writes KiekR, traced this through our source and asked for an opt-in rather
than a revert.

A companion can now request it per session:

    CMD_SET_CUSTOM_VAR   "ble.rxlog:1"     (and "ble.rxlog:0" to stop)

Chosen over a user-facing setting because the tradeoff belongs to the app, not
the user: an app that wants the firehose knows it wants it, and nobody else
should have to understand the question.

Deliberately NOT persisted. A stored flag would silently reinstate the #46/#54
flood for someone who tried a coverage app once and moved on — on the one link
that cannot absorb it. Asking again after each connect is cheap for an app and
is the safe default for everyone else.

The existing #94 one-shot (echoes of our own sends, so "Repeats heard" keeps
working) is untouched and still applies when the firehose is off.

Builds on T-Deck, V4 and Pager.

Requested by @marcelverdult.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 09:30:19 +02:00
Kaj SchittecatandClaude Opus 5 48084e112b fix: an unconfigured channel slot is not a channel (#260)
Messages were arriving in a nameless "#unknown" thread on unrelated devices.
Root cause is a single missing check, with two amplifiers:

CMD_SEND_CHANNEL_TXT_MSG accepted any in-range channel index, because
getChannel() returns true for a slot that was never configured — whose secret
is all zeroes. Sending on one transmits a group message encrypted with an
all-zero key. Every other device holds that same all-zero secret in its own
spare slots (with hash byte 0x00), so searchChannelsByHash offers them as
candidates and MACThenDecrypt genuinely succeeds — the sender used the same
key. findChannelIdx then matches the first empty slot, whose name is "", and
the UI renders an empty channel name as "#unknown". That is why two people on
two different devices received the identical messages, and why some were empty.

Three fixes, all receiver- and sender-side hardening around one invariant: a
slot with an all-zero secret is not a channel.

  - the app-send path rejects an unconfigured slot (ERR_CODE_NOT_FOUND) instead
    of broadcasting on a zero key. A wadamesh device can no longer be a source.
  - searchChannelsByHash (virtual, so no core change needed) skips unconfigured
    slots, so they can never decrypt anything. This also fixes a SEPARATE latent
    bug: for a real channel whose hash byte is 0x00, up to four empty slots could
    fill the 4-entry candidate array and starve the real channel out, silently
    dropping the message — roughly 1 in 256 channels.
  - a failed lookup no longer silently becomes slot 0, and an unresolved channel
    is labelled rather than handed to the UI as an empty string.

Our own touch UI already refused to send on a nameless slot, so the emitting
device was an app with a stale channel list or another firmware; not reproduced
on hardware. All 8 S3 envs build.

Reported by D-Melhede, and seen by Marshal W7TER and PixPMusic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 09:12:09 +02:00
Kaj SchittecatandClaude Opus 5 2149184cf5 fix: don't delete a contact's blob from the packet path (#222)
When the contact table is full, every incoming advert from an unknown node
evicts the oldest contact, and onContactOverwrite() removed that contact's
stored blob inline — on the mesh receive path. On a Heltec V4 the store is
internal SPIFFS, where an unlink can trigger garbage collection, and SPIFFS GC
suspends the flash cache: both cores stall for the duration. That is the
reported symptom exactly — the whole device locks, Bluetooth and TCP drop, the
screen will not wake, and it comes back on its own once GC finishes. It gets
worse the fuller the table is (reports at ~350, ~600 and ~2000 contacts), and a
plain V4 has no SD slot, so there is no storage-side workaround.

Queue the delete instead and drain it from loop(), one blob per tick and no
faster than every 500 ms, under the same WdtHeavyGuard saveContacts uses. A
burst of evictions can no longer chain GC passes back to back, and a slow pass
stalls briefly instead of tripping the watchdog. If the queue (8 deep) fills,
the blob is left orphaned — harmless, and reclaimed on the next wipe.

Reported by Yoss101 and pisti87.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:00:32 +02:00
Pixel Perfect 0175cebcd8 fix(tpager): enable Wi-Fi without reboot 2026-08-05 17:56:11 -07:00
Pixel Perfect 3f222ceecd fix(tpager): close Wi-Fi BLE handoff bypasses
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-05 17:56:11 -07:00
Kaj SchittecatandClaude Opus 4.8 e0663eb9ee touch: drop the V4 memory/store diagnostics + fix a swallowed hex escape
The RAM investigation and the language-download debugging left Serial probes
across three files: the BOOTMEM/MESHMEM/MEMPROBE macros and their 48 call
sites, plus the [STORE]/[APPCAT]/[LANGDL]/[LANGSEL] traces. They have served
their purpose and were flooding the companion USB-CDC, so they all go. Two
locals in the .lang installer (wr, wr3) existed only to feed a trace and go
with it; the control-write repair path that actually rebuilds /lang stays.

Separately, the map tile-cache help text read "256\xC3\x97256": the 256 after
the multiplication sign is all hex digits, so the compiler folded it into the
escape and emitted one out-of-range byte instead of "256x256". Breaking the
literal after the escape terminates it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 22:43:49 +02:00
Kaj SchittecatandClaude Opus 4.8 4387e0d64b v4 ram: measure the boot budget end to end (probes + findings)
Attribution on the V4, per step, measured on hardware:
  SPIFFS mount            4.4 KB
  storage selection       7.5 KB
  store.begin()             0
  the_mesh.begin()         52 BYTES  (contacts load is 369 KB, all PSRAM)
  post-mesh init block   38.6 KB  <- Wi-Fi/TCP/MQTT/companion interface
  UI construction          38 KB
  Wi-Fi associate        ~55 KB

So MAX_CONTACTS and the mesh core are both exonerated: contacts already live
in PSRAM and MyMesh::begin() itself costs 52 bytes. The weight is Wi-Fi plus
the interface block that follows the mesh, and the UI.

Also checked and already optimal: LVGL's heap and the LVGL draw buffer are
on PSRAM, CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y, and Arduino defaults
useStaticBuffers to false (static TX 0 / static RX 4 / dynamic 32), so the
obvious Wi-Fi buffer lever is already pulled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 20:07:57 +02:00
Kaj SchittecatandClaude Opus 4.8 c49fac407c touch: auto-retry is opt-in (default OFF) on top of the #230 schema repair
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>
2026-08-04 10:24:25 +02:00
Kaj Schittecat cb17ee80a9 Revert "touch: repair the beta_57 prefs layout shift + auto-retry now opt-in (v45)"
This reverts commit b0c31c9105.
2026-08-04 10:21:41 +02:00
Kaj SchittecatandClaude Opus 4.8 b0c31c9105 touch: repair the beta_57 prefs layout shift + auto-retry now opt-in (v45)
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>
2026-08-04 08:33:51 +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
mikecarper 04d5f33575 Retry messages until an echo is heard 2026-08-01 17:13:03 -07:00
Kaj SchittecatandClaude Opus 4.8 162ec43bfa touch: cut the advert-driven SPIFFS churn behind the multi-second mesh stalls
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>
2026-07-30 12:47:20 +02:00
Kaj SchittecatandClaude Opus 4.8 8ae0df9400 touch: stop the deferred-login branch eating the companion app's responses (#124)
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>
2026-07-30 11:38:36 +02:00
Kaj SchittecatandClaude Opus 4.8 99423e5bf8 touch: beta_47 — Discover app (find/list/map nearby nodes + wardriving), Attaky release wiring, P4 LCD
- 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>
2026-07-19 17:45:05 +02:00
Kaj Schittecat be0a49255c Merge PR #149: LilyGo T-LoRa Pager port (LR1121 + SX1262) by @codemonkeybr
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
2026-07-15 19:16:51 +02:00
Kaj SchittecatandClaude Opus 4.8 2353c56adc p4: T-Display P4 port — web VNC/remote, time-sync hardening, brightness + sound
LilyGo T-Display P4 (ESP32-P4 + factory ESP-AT C6) reaches POC: LoRa mesh, Wi-Fi
companion (TCP:5000), GPS, SD, battery gauge, chat persistence, and now:

- Web VNC/REMOTE/terminal via a first-byte router on the single ESP-AT listener
  (HTTP verbs -> WS server, '<' frames -> phone companion; both live at once).
  C6Server gains a _begun gate so never-listening instances stay inert.
- VNC freeze fixed: bounded one-CIPSEND-chunk drains under the WS client mutex
  (a whole-band blocking write starved the main loop), fast-drop of stalled
  mirror clients, SEND-OK wait 8s->4s.
- Wi-Fi robustness: CWJAP? miss-streak tolerance, CIPSERVER liveness verify +
  re-arm (a re-association silently killed the listener until reboot).
- Time sync: HTTP-Date fallback when SNTP is blocked; ClockFloorRTC gains a
  MAX_PLAUSIBLE_EPOCH guard (all boards) after a garbage-future hardware RTC
  read (2043) latched the ratchet.
- Brightness: RM69A10 DCS 0x51 driven live (CC slider + persisted pref).
- Sound: ES8311 + NS4150B notification chimes (P4Audio, esp-bsp-derived
  register sequence on Wire1), full HAS_UI_SOUND surface + CC Screen/Sound
  sliders; ccVolumeReleaseCb's hardcoded tanBeep -> uiSoundPreview.
- tdisplay_p4/: IDF project (build.sh, c6_at AT-over-SDIO driver, sdkconfigs,
  fetch-deps.sh vendoring, .gitignore for local payloads).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:13:23 +02:00
Tesso M Costa 5c97d02fbe Merge remote-tracking branch 'origin/main' into tlora-pager-port-lr1121
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>

# Conflicts:
#	NOTICE
#	platformio.ini
#	src/ui-touch/UITask.cpp
#	src/ui-touch/device_caps.h
2026-07-14 10:45:53 -06:00
Kaj SchittecatandClaude Opus 4.8 831959ea5c touch: beta_43 - web control panel becomes a full remote app + V4 map-tile fix
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>
2026-07-12 16:29:08 +02:00
Kaj SchittecatandClaude Opus 4.8 946e73bb0b touch: beta_40 - card-less contacts-save stall fix + long-text auto-fit
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>
2026-07-10 18:31:26 +02:00
Kaj SchittecatandClaude Fable 5 d20241627c touch: flush chat history on the companion CMD_REBOOT (fix messages lost on app-initiated reboot)
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>
2026-07-09 12:48:53 +02:00
Tesso M Costa b3c638842e pager: fix LR1121 TX power default and RX-boosted-gain gating
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>
2026-07-07 15:50:07 -06:00
Kaj SchittecatandClaude Fable 5 01900296f3 touch: beta_35 — worker-thread history flush, radio memory guards, 30.5 KB DRAM freed, crash-safe prefs
- 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>
2026-07-06 13:34:24 +02:00
Kaj SchittecatandClaude Fable 5 4e1ac2ca6b touch: beta_34 — buffered LoRa receive ON by default (test channel) + core pin core-v1.16.5
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>
2026-07-06 11:15:52 +02:00
Kaj SchittecatandClaude Fable 5 8eb63a7edd touch: beta_29 — clock floor for room-server replay guards, BLE repeats-heard, deep SD history, one-screen popups
- ClockFloorRTC on all boards: persisted monotonic send-timestamp floor (touchPrefs v32),
  garbage-set rejection, trusted-backstep cap; Join no-reply watchdog + server clock
  skew warning from the LOGIN_OK timestamp prefix (issue #89 follow-up)
- BLE repeats-heard: own-echo RX-log frames pass to BLE one-shot; the #46/#54
  congestion skip stays for everything else (issue #94)
- Mirror app-sent channel messages on-device (was Tanmatsu-only from birth)
- Chat delete: persist the ring purge; mesh-channel drop prefers the pinned slot
- Deep SD history (5000-msg ring) + date separators; chat-list rows restyled compact
- Popup unification: popupClose() + k_popup_registry drive dismiss/count/swipe-block
- Thread/channel sheet as one-screen 2-col grid (contact-sheet layout)
- Keypad nav: strip LVGL SCROLL_ON_FOCUS from collected widgets (About-page jump);
  update dot no longer focusable; verchk waits for an IP + retries with backoff
- Tanmatsu: FIRMWARE_RELEASE_TAG from git describe (drop the hand-bumped hardcode)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 10:07:16 +02:00
Kaj SchittecatandClaude Fable 5 b90b8bac14 touch: beta_28 — room-server session fix (#89), ST7789 panel sleep, wyvern feedback batch
- 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>
2026-07-04 13:40:02 +02:00
Dan Vybiral 96dffaef58 touch: fix manual telemetry request reliability + async spinner blink
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.
2026-07-01 20:52:45 +02:00
Kaj SchittecatandClaude Opus 4.8 3a1495784e touch: beta_24 — signal probe uses standard node-discovery packet
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>
2026-07-01 14:46:33 +02:00
Kaj SchittecatandClaude Opus 4.8 0731b5ff67 touch: beta_22 (test) — trace-ping signal probe + companion contact-refresh (#73)
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>
2026-07-01 08:46:56 +02:00
Kaj SchittecatandClaude Opus 4.8 03e50b0609 touch: beta_21 — first stable build + test channel, MQTT bridge, contacts & signal-probe fixes
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>
2026-06-30 17:34:11 +02:00
Kaj SchittecatandClaude Opus 4.8 b4c941f954 touch: beta_20 - spectrum analyzer, custom sounds, saved Wi-Fi, V4 high-gain RX, flood-scope + channel-send fixes
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>
2026-06-28 23:33:44 +02:00
Kaj SchittecatandClaude Opus 4.8 f39e072bd7 touch: stop debug prints corrupting the binary companion stream (#25, #54, #23)
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>
2026-06-26 17:18:07 +02:00
Kaj SchittecatandClaude Opus 4.8 59b1b2ab09 touch(tanmatsu): beta_17 — Vol± gestures, message LED, microSD, backups on FFat, status-bar fixes
- 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>
2026-06-25 17:30:55 +02:00
Kaj SchittecatandClaude Opus 4.8 c8849916df touch: show companion-app-sent DMs on the device (issue #46, half 1)
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>
2026-06-24 09:54:38 +02:00
SamandClaude Sonnet 4.6 5fcf6ddac3 fix: block commands while transport-off confirmation is pending
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>
2026-06-23 23:25:02 +02:00
Kaj Schittecat 8322b5361d Merge PR #40: ui-touch — room-server posts show author, not room name (Yazutsu, closes #32) 2026-06-23 23:19:14 +02:00
Kaj SchittecatandClaude Opus 4.8 069aeb8908 touch: keyboard-nav typing fixes, dedicated symbol picker, 12h everywhere
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>
2026-06-23 23:18:34 +02:00
Yazutsu 61a914240f ui-touch: show post author instead of room name for room-server messages
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>
2026-06-23 22:45:39 +02:00
Kaj SchittecatandClaude Opus 4.8 1f755ec3b8 touch: beta_11 — bindable keyboard navigation, crash-report prompt, map zoom buttons, app-drawer-as-home; publish Tanmatsu port source
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>
2026-06-22 19:10:40 +02:00
Kaj SchittecatandClaude Opus 4.8 7a63ffe86c touch: beta_9 — crash/Wi-Fi reliability, crash-report export, room fixes, app-drawer icon size
- Stability: Wi-Fi auto-reconnect; corrupt chat-history quarantine (fixes reboot-on-
  receive / inaccessible messages); draw-buffer + flush NULL-guards (boot-panic).
- Crash-report export to SD/SPIFFS (Settings -> About).
- Map: reload purges on-disk tiles offline + visible zoom band; corrupt-tile self-heal
  on load; z15 dedup-key overflow fixed.
- Contacts: favourites always listed (#17).
- Chat: room message send-time (#26) + sender labels; QR share uses the official
  meshcore://contact/add format (#16); unread badge capped at 99+.
- App drawer: icon-size setting (Compact/Large) + smoother scrolling.
- ~5 KB internal DRAM reclaimed (backup arrays + msg_idx -> PSRAM).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:16:04 +02:00
Kaj SchittecatandClaude Opus 4.8 eca5281f22 touch: post-beta_6 batch — BLE PIN, wallpaper-from-files, storage & memory fixes
- BLE: user-settable 6-digit pairing code (Settings -> Bluetooth), MyMesh::setBLEPin.
- Lock wallpaper: pick via the file manager (fast, any folder) + 'Set as wallpaper'
  in the image viewer; downscaling JPEG decoder accepts images > 1024 px and keeps
  the held buffer small; clear message for progressive JPEGs.
- Map tiles: validate JPEG magic before caching + re-fetch bad cached tiles
  (fixes black quadrants).
- Channel @reply: quick-reply now appends at the cursor instead of wiping the
  mention.
- Audio: heap pre-flight before i2s_driver_install (fixes the esp_timer NO_MEM
  abort when toggling sound under BLE+Wi-Fi memory pressure).
- App drawer: squircle tiles. Log hygiene: CORE_DEBUG_LEVEL 1->0 (mute benign
  vfs/Preferences spam off the USB-CDC companion stream).
- Core bump to core-v1.16.4 (drops the 4-byte chat-payload trailer, issue #13).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:57:50 +02:00
Kaj SchittecatandClaude Opus 4.8 bb95aed673 touch: beta_6 — RF Monitor sniffer app + chat icons, popup & scroll fixes
- New Monitor app (app drawer): live RF sniffer — recently-heard feed
  (type / RSSI / SNR / hops, colour-coded), link-margin grade, packet rate,
  and an RSSI + noise-floor scope with an on-screen dBm scale; portrait /
  landscape aware. Backed by a recent-RX ring buffer in MyMesh (logRxRaw).
- Chat: DM person icon / channel group icon; block non-contact senders by
  name; keep scroll position when opening a thread; contacts sort popup
  closes on select; long-press popup close-X no longer overlaps buttons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:37:41 +02:00
Kaj SchittecatandClaude Opus 4.8 e9ece7bf6b touch: tester feedback batch + Launcher/RTC/BLE fixes + timezone picker
Core bump: lib_deps -> meshcomod core-v1.16.3 (BLE advertises the node
name not "NimBLE"; GPS won't push a pre-fix ~1902 date).

Tim's quick-wins: Enter-key-sends toggle, 12-hour clock, lock-when-screen-off,
reverse scrollball.

Launcher / map: fix Wi-Fi tiles never downloading under Launcher (the SD-fallback
cache made tilesFsLowSpace() read the unmounted LittleFS as "full"); plot contact
markers before the tile decode so dots aren't ~2s late.

RTC / time: anchored-time guard re-applies UTC when the clock reads garbage
(GPS clobber); blank pre-2020 timestamps so empty channels don't show 1969/1970;
timezone picker (named zones with correct DST) replacing the CET-only base, with
a v2->v3 config migration mapping prior manual offsets onto a Custom zone.

BLE: persist the random pairing PIN (was re-rolled every boot).

Chat: don't auto-jump to latest on a new message when scrolled up; block-by-name
for non-contact room/channel senders (un-blockable room bots).

Power: drop CPU to 80 MHz when the screen is off; enable Wi-Fi modem-sleep only
once associated (enabling it on the unassociated STA broke the setup-wizard scan).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:40:55 +02:00
Kaj SchittecatandClaude Opus 4.8 11bd8365d6 beta_5: official-app region scope + contacts/discovered/sound/map overhaul
Companion / core (ported from meshcomod):
- Region/default scope from the official MeshCore app now works: handle
  CMD_SET/GET_DEFAULT_FLOOD_SCOPE 63/64 (companion-v1.16.0.3, issue #31) —
  previously fell through with no reply ("no_event_received").
- US/Canada radio preset corrected to 910.525 MHz / 62.5 kHz / SF7.
- T-Deck +22 dBm TX fix (SX1262 DIO2-as-RF-switch, issue #6) retained.

Touch UI (since beta_4):
- Contacts: person/antenna + red-blocked icons, block/unblock in the popup,
  table with sort/filter + multi-select delete (favourites protected),
  compact time/distance columns, long-name marquee, instant fav-star toggle.
- Discovered: persists across reboots, holds 48, cogwheel settings
  (auto-delete oldest + auto-delete above N hops), "48!" full badge.
- Sound: +/- volume, master toggle, separate Messages/@mention switches.
- Chats: per-row last-message time, per-channel mute, @mention jump-to-message.
- Map: remembers zoom + pan; compact contact popup (no Block button).
- Auto-add on its own page; full-screen settings; refined tab bar; drawer polish.
- T-Deck keyboard backspace at the caret; quick-replies scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:34:04 +02:00
Kaj SchittecatandClaude Opus 4.8 75fe3b9e5b home: signal in the RX/TX graph legend + a tap-for-detail popup
The graph legend now leads with the live signal ('Sig -3dB  TX n  RX n  (tap)').
Tapping the graph (or the legend) opens a Signal & traffic popup: SNR, RSSI,
bars 0-4, how long ago we last heard the mesh, the flood/direct TX+RX totals
since boot, and the auto-discover interval. logRxRaw now also records RSSI for it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:50:47 +02:00
Kaj SchittecatandClaude Opus 4.8 dee2037310 topbar: mesh signal-strength bars driven by a periodic discover probe
Add a 4-bar signal indicator to the status row, left of the connection icon.
MyMesh::logRxRaw now records the SNR (+ timestamp) of every received packet;
updateGlobalStatusBar maps that to 0-4 lit bars and dims them when nothing's been
heard for 5 min. To keep it fresh when idle, UITask::loop sends a light zero-hop
'discover' advert every 60 s (neighbours only, not flooded -> minimal airtime) so
nearby repeaters/nodes are prompted and their SNR shows up. Home-name cap shrinks
on the narrow V4 to leave room for the bars.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:40:45 +02:00
Kaj SchittecatandClaude Opus 4.8 9e07a5768a feat: name the repeaters behind 'Repeats heard' on sent messages (issue #30)
Sent messages have no inbound route, so #30's repeater-names had nothing to show
there. Now when we hear an echo of our own flood, capture the re-flooding
repeater (the echo's last path hop) into the echo ring (deduped, bounded to 3),
and list those repeaters by name under 'Repeats heard' in the message Info.
Zero added risk: fixed-size store (no growth), hard buffer guards in the
renderer, read-only contact lookup. Covers #30 in both directions now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 08:57:21 +02:00
Kaj SchittecatandClaude Opus 4.8 ec3b47bbc2 fix: restore 'repeats heard' on scoped floods + guard trace-route crash
- MyMesh.cpp logRxRaw: the self-echo parser tested (raw[0] & 0x80) for transport
  codes, but 0x80 is the top of the payload VERSION field, not a flag. Transport
  codes are present iff route_type (raw[0]&0x03) is TRANSPORT_FLOOD/DIRECT. Since
  beta_12's region scope, scoped floods carry transport codes -> the parser hashed
  the wrong payload slice -> the echo never matched -> 'repeats heard' showed 0 for
  anyone with a region set. Now route-type aware (matches uiStashRxMeta). Also fixed
  the diagnostic dst= decoder + the header-layout comment.
- MyMesh.h uiSendTraceRoute: walking c.out_path with an unchecked out_path_len
  (uint8_t, can be 65..254 if a contact's path is corrupt) overran sendDirect's
  payload memcpy -> reboot when tapping 'Trace route' on a sent DM. Clamp to
  <= MAX_PATH_SIZE, else fall back to the single-hop trace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:11:17 +02:00