Settings held live status (GPS fix, storage/RAM/identity) that belongs
on the Status screen, plus a per-second tick() doing SPI flash stat
reads and label churn mid-scroll — the main cause of laggy scrolling.
- GPS section (sats/location/altitude/HDOP/time) -> StatusScreen
- System Info (firmware build, storage, RAM) -> StatusScreen, with
storage/RAM stat reads throttled to ~5s and stack-buffer snprintfs
instead of Arduino String concatenation
- Settings gains a Status link row (trackball-reachable) that opens
Route::STATUS; the per-second SettingsScreen tick/refresh is deleted
- Reordered sections by frequency of use: General (name/brightness/
timeout/kb-light), Notifications, Network (now includes the
TCP/Auto/BLE interface switches), Radio (LoRa + params), Delivery,
Advanced, DANGER: Transport Mode (still final)
- Identity/LXMF hashes shown in Settings were truncated duplicates of
the Status screen's full display; removed
- main.cpp publishes firmware build + GPS to the Status screen
- Contract test for the storage readout follows the code to StatusScreen
Uses the microLXMF in-process message-metadata cache (bumped pin
6bea23c -> 58a6eb0, branch feat/conversation-preview-cache):
- Reopening a conversation (and background page fill / paging) is now
O(1) in-memory once warmed instead of re-reading each message file
from SPI LittleFS (~230ms/read, ~2.3s per open measured on the T-Deck).
- The cache table is PSRAM-allocated on ESP32; a static .bss placement
starved internal DRAM and the LVGL task's 8 KiB stack allocation
failed at boot ("Failed to create LVGL task", hang at startup logo).
- Long-press full-message view now defers to the main loop: the LVGL
event handler only records the hash; tick_pending_full_message() does
load_message_content() (uncapped, no msgpack unpack) off the LVGL
task, then builds the modal. Fixes the capped (600-char) text that
the in-memory rows carry.
- A peer change cancels an in-flight background fill from the previous
conversation (it would otherwise prepend the old conversation's
rows into the new one).
Validated on the T-Deck (T-Deck Plus, 8MB PSRAM): warm opens
reads=3 sync=0-2ms total=15-35ms (was ~2.3s); cold first open per
conversation still pays ~0.6-1.2s disk while warming the cache; no
panics/watchdogs over a ~6-minute interaction session.
Host gates: microLXMF conformance 6/6 (incl. 32-assertion
test_message_metadata_cache), native reference test against pinned
58a6eb0, tdeck build SUCCESS.
On-device capture confirmed the fix (cold-boot tap: fallbacks=0,
gather=2ms, total=48ms vs 2086ms pre-fix; repeat taps no longer
perpetually fall back). All [PERF] timing markers and stage
variables are removed from refresh()/show()/render_route; the
branch is now clean of diagnostic code.
On-device [PERF] capture decomposed the remaining ~1s tap latency:
each uncached conversation costs one SPI-LittleFS metadata read
(~230ms). Repeat taps still paid 2 perpetual fallbacks (empty-
content tails never got cached), and the first tap after every
boot paid all 9 (the in-memory repop was never committed to the
index).
- bump microLXMF pin c8d3156 -> 6bea23c (feat/conversation-
preview-cache): preview_valid index flag so 'cached empty'
differs from 'unpopulated'; bounded preview copy (the old
strncpy read past the non-terminated content Bytes); public
commit_index().
- refresh() re-pops empty-content tails as a valid cached
preview and arms a one-shot deferred index commit;
UIManager::update() drains it out-of-lock (flush_pending_
index_commit, between drops and mark-read) so the warmed
previews persist and the next cold boot reads them from the
index.
- fix the [PERF] skip-log total= wrap (printed p_t_diff - p_t0,
a uint32 underflow; total was already p_t_diff).
[PERF] instrumentation stays in this commit (temporary,
marked); it is removed before merge once the fix is validated
on-device.
Temporary, removable instrumentation (marked [PERF] throughout):
- refresh(): gather / diff / rebuild stage ms + metadata-fallback count,
one [PERF] convlist line per call (skip vs build)
- show(): unhide + focus-group ms
- render_route(MESSAGES): whole route window incl. hide_all_screens
All INFO-level so they survive DEBUG-off and land in the serial capture.
When a conversation's newest message is unreadable, walk the index
newest-to-oldest for the newest readable preview and queue the
unreadable messages for deletion (deferred out of the LVGL lock,
drained by UIManager::update()). The store's delete_message() commits
the index and updates last_message_hash, so the next refresh converges
to the same preview and the row never disappears over one bad message.
refresh() did heavy unnecessary work per conversation, on the LVGL
render task under the render lock:
- get_messages_for_conversation() copied the full 256-slot hash array
(8KB) just to read the newest hash (messages.back());
- load_message() on that hash read the payload file ~3x, JSON-parsed
it twice (including the large hex 'packed' blob), hex-decoded and
msgpack-unpacked the whole message — to extract a 30-char preview
and a timestamp;
- the unread badge was rendered from a hardwired 0 even though the
store maintains and persists unread_count.
Now:
- newest hash via the new O(1) MessageStore::get_last_message_hash
(index tail — always hot-tier, no I/O, no array copy);
- preview/timestamp via load_message_metadata (single open + filtered
parse, the fast path ChatScreen already uses for the same fields);
- unread badge from MessageStore::get_conversation_unread_count;
- badge cleared + mark-read on open (click) and when a message lands
in the currently-viewed chat, with the LittleFS index commit
deferred out of the LVGL lock (UIManager::update), matching the
existing deferred display-name write-through pattern.
Pins microLXMF 59ca70a (PR #10, temporary branch head) for the two new
accessors.
A message whose source identity is KNOWN but whose signature fails to
validate is spoofed or malicious and must not be rendered. The
opportunistic (on_packet) and direct (on_resource_concluded) router
paths already reject these, but the propagated (store-and-forward) path
in process_propagated_lxmf queues them without a signature check, so
UIManager::on_message_received is the single choke point that covers
all three inbound routes.
Drop the message at the top of on_message_received — before the key
request, location ingest, persistence, chat render, and notification
beep — when !signature_validated() && reason == SIGNATURE_INVALID.
SOURCE_UNKNOWN (first contact) is untouched: those still render and
trigger the bounded key request from PR #92. Validated messages are
unaffected.
Add a source-level contract test locking in the drop gate's ordering
relative to every side effect and its enum specificity.
Greptile P2 on PR #92: the per-inbound-location-frame INFOF logged peer
bytes, precise coordinates, and source-clock skew unconditionally in the
release build. The missing-map-pin investigation is closed (announce
timing, verified physically), so remove the temporary instrumentation
and its ingest_diag_result plumbing, and update the feature comment to
describe the final rate-limit semantics instead of 'remove with the
fix'.
Move the rate-limit/cooldown/cap decision out of the UIManager.cpp
anonymous namespace into a pure, header-only policy
(UI/LXMF/UnknownSourceKeyRequest.h) so it is host-testable without the
ESP/microReticulum stack. UIManager keeps only the side effect
(Transport::request_path).
Adds tests/native/test_unknown_source_key_request.{cpp,py} (24 checks,
ASan+UBSan): new-source request, 5-min cooldown boundary, re-record
resets the window, per-source independence, 64-entry cap with oldest
eviction, and steady-state cost.
on_message_received now fires a rate-limited RNS path request when an
LXMF message arrives from a SOURCE_UNKNOWN sender, mirroring Sideband's
'Query Network For Keys' button (RNS.Transport.request_path). Any peer
that already knows the source's announce (hub, phone, other node) can
answer with the cached identity, letting the NEXT message from that
peer validate and reach location ingest.
5-minute per-source cooldown, 64-entry cap. App-only diagnostic;
remove with the other ingest diagnostics once the root cause closes.
Add bounded compact page rendering, serialized Link and Resource lifecycle handling, TCP reconnect corrections, and deterministic Python RNS conformance coverage.
Physically verified anonymous index retrieval and rendering on T-Deck. Same-destination Link reuse and italic rendering remain follow-up fixes.