Commit Graph
299 Commits
Author SHA1 Message Date
Torlando d33ae5bb6f fix(settings): recreate entry scroll-reset timer after one-shot delete
The one-shot LVGL timer's callback deleted the timer but left
_entry_scroll_reset_timer dangling. On the 2nd+ screen entry show()
took the 'else' path and called lv_timer_reset() on freed memory, so
the view was never reset to the top — it stayed wherever the group
re-focus had auto-scrolled it (the bottom). Null the pointer before
lv_timer_del so each entry recreates a fresh timer.
2026-09-04 13:40:45 +00:00
Torlando f62f465958 fix(settings): don't auto-scroll to bottom on screen entry
LVGL re-focuses the default input group when the screen is shown; the
focused member is the last focusable object (the transport switch), so
the view landed at the bottom of the list. A one-shot 50ms LVGL timer
after show() scrolls the content back to the top without touching the
focus state, and leaves subsequent user scrolling alone.
2026-09-04 05:52:56 +00:00
Torlando 28ea218a18 refactor(settings): move status readouts to Status screen, reorganize by use
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
2026-09-04 04:56:07 +00:00
Torlando 8c0d38ee2e Merge pull request #95 from torlando-tech/perf/conversation-list-load
Build and Deploy Firmware / build-and-deploy (push) Canceled after 0s
Test / Pyxis pytest suite (build_scripts + native) (push) Canceled after 0s
Test / microReticulum native unit tests (PlatformIO native17) (push) Canceled after 0s
perf(messages): fast conversation-list load + real unread badges
2026-09-03 23:00:32 -04:00
Torlando 362db2c805 fix(ui): fail closed when the queue mutex is unavailable
Greptile follow-up on 67c1083: if xSemaphoreCreateMutex() failed,
QueueGuard silently locked nothing and the deferred queues still ran
unsynchronized. The constructor now logs the allocation failure and
every guarded site refuses mutation when _queue_mutex is null:
requesters stop enqueuing, flushers stop draining (nothing queued can
exist), and the one-shot preview commit is skipped. The screen keeps
rendering and chatting; only the deferred list mutations degrade.
2026-09-04 02:49:26 +00:00
Torlando 67c1083f22 fix(ui): mutex the conversation-list deferred queues
Greptile P1 on the list-perf commit: the deferred-queue pair
(requester task -> main-loop flush) was unsynchronized. Producers
push from the LVGL task (click handlers, refresh() during navigation)
while flush_*() swaps the queues on the main loop without the LVGL
lock, so concurrent push/swap is a data race.

A single FreeRTOS mutex (QueueGuard, fail-closed) now guards all
four shared queues: _pending_mark_reads, _pending_drops,
_pending_name_writes, and the _index_commit_pending flag. Critical
sections are bounded vector operations only — no store I/O, no LVGL
lock, so acquisition cannot deadlock and the LVGL task is never
stalled on LittleFS. Lock order is LVGL-lock -> queue-mutex only;
queue sections never take the LVGL lock.

The mutex is created before the screen's LVGL_LOCK section and
deleted after it in the destructor.
2026-09-04 02:32:57 +00:00
Torlando 403035f115 perf(messages): instant chat open/reopen + deferred full-message view
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.
2026-09-04 01:30:16 +00:00
Pike 09bf277aa4 chore(messages): remove temporary [PERF] instrumentation
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.
2026-09-03 17:46:38 +00:00
Pike 8204506b3b perf(messages): persist preview warm-up; cache empty-content tails
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.
2026-09-03 17:12:52 +00:00
Pike af4019f933 diag(messages): [PERF] stage timings for tap-to-visible path
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.
2026-09-03 16:02:35 +00:00
Pike ff79a87e42 perf(messages): revalidate conversation rows, skip LVGL rebuild when unchanged
refresh() unconditionally ran lv_obj_clean(_list) and recreated 5-7 LVGL
objects per row on every call - every navigation back to Messages, every
750ms coalesced inbound batch, and the periodic name-resolution sweep -
even when nothing had changed. With the store read now O(1) (index
preview cache), that widget churn was the remaining gap vs NomadNet /
Network / Maps, which build once and just unhide.

refresh() now gathers row data first (index preview + hash + unread, no
message-file I/O) and diffs it against the rendered rows; the rebuild
only happens when data actually changes. Rows keep focus-group
membership and the screen keeps its scroll position on the no-change
path. The per-refresh 'Found N conversations' log drops from INFO to
DEBUG (serial output under the LVGL lock stalls the render task).

get_conversations() ordering is deterministic (last_activity desc with
peer-hash tie-break), so the index-aligned comparison is stable.
2026-09-03 15:20:24 +00:00
Pike cb6a9d38e6 perf(messages): read conversation preview from store index cache
refresh() previously opened + parsed each conversation's newest message
file (load_message_metadata) on every list refresh, which dominated
list-load time on SPI LittleFS. It now reads the per-conversation
last-message preview + timestamp from the store's in-memory index
(O(1), zero I/O) and falls back to load_message_metadata only when the
index has no cached preview — the first refresh after a firmware
upgrade or after a corrupt-tail drop — then writes the preview back
through so the fallback happens at most once per conversation per
firmware generation. The write-through is skipped when drops were
queued (the drained deletes move the tail, so a preview written for the
old tail would be stale for one refresh).

Host benchmark (x86 + POSIX fs, 24 msgs/conv): the per-conversation
store-load work drops from ~0.149ms (9 convs) / ~0.342ms (20 convs) to
~0.002ms / ~0.005ms, and the cold-boot path (store reconstructed from
disk) matches the warm path because the preview now persists in the
index.

Bumps the microLXMF pin to c8d3156 (feat/conversation-preview-cache)
in platformio.ini, the release audit, and the native reference test.
Adds tests/microlxmf/bench_conversation_list_load.cpp (baseline vs
index warm/cold) and the bench target in tests/microlxmf/CMakeLists.txt.
2026-09-03 14:30:03 +00:00
Torlando 918f89d63e fix(messages): drop corrupt last message instead of hiding the row
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.
2026-09-03 04:14:52 +00:00
Torlando 8ed9551a02 perf(messages): fast conversation-list load + real unread badges
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.
2026-09-03 02:38:03 +00:00
Pike 902df6554c ui(map): contrast pin labels against active basemap
Marker (pin) labels previously inherited the app's default text color,
which reads white and disappears on light basemaps. Set each label's
text color in applyFrame() against the active style: black by default
(light basemaps osm-bright/positron/toner) and white only on the one
dark basemap (dark-matter). Re-evaluated every frame so a style switch
re-colors visible labels on the next applied frame.

Add a contract test pinning the dark-basemap detection and the
black-by-default / white-on-dark ternary.
2026-09-02 21:20:24 +00:00
Torlando 40c1869765 fix(lxmf): drop SIGNATURE_INVALID inbound messages
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.
2026-09-02 17:51:52 +00:00
Torlando 11fd91bf03 fix(lxmf): make key-request policy C++11-compatible
The tdeck toolchain rejects aggregate initialization of Entry (NSDMI
makes the class non-aggregate under C++11). Use a default constructor
and field assignment in record_request.
2026-09-02 17:06:02 +00:00
Torlando 97fc8b4562 chore(lxmf): drop temporary location-ingest diagnostic from release path
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'.
2026-09-02 17:05:23 +00:00
Torlando 3de986daed fix(lxmf): bound automatic path requests to 30 min per identity
Greptile P2 on PR #92: with 64 tracked sources, evicting the oldest
entry discarded its open cooldown, so a flood of distinct bogus
identities reset other sources' windows and forced unbounded path
requests (each answered by every peer holding the announce).

- Per-identity cooldown 5 min -> 30 min.
- The table no longer evicts an open window: while all 64 slots hold
  unexpired windows, never-before-seen identities are deferred until a
  slot frees (at most one 30-minute window) instead of dropping
  someone else's cooldown. Open windows are only ever pruned after
  they expire.
- Aggregate bound: at most kMaxTrackedSources automatic path requests
  per rolling 30-minute window, capping the worst-case network cost of
  a rotating-identity flood.

Host tests extended: saturated-table deferral, 512-identity flood bound
(one window and across consecutive windows), lazy expiry/prune,
boundary checks at the 30-minute cooldown.
2026-09-02 17:05:14 +00:00
Torlando 2eec34dfe1 test: extract unknown-source key-request policy + host tests
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.
2026-09-02 16:22:56 +00:00
Torlando e9fe63e9e4 diag: request network keys for unknown LXMF sources (Sideband request-keys equivalent)
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.
2026-09-02 16:22:23 +00:00
Torlando 0f6309c5a6 fix: widen MapTileStore path buffer for 31-char pack IDs
Raise MapTileStore::PATH_CAPACITY from 64 to 80 so the mount buffer
(PATH_CAPACITY + 4) holds the 80-character mounted tile path produced by
a maximum-length (31-char) pack ID with full-width tile coordinates.
Pre-fix, any tile with a two-digit x or y returned INVALID_ARGUMENT from
the mount-prefix snprintf, which MapTilePack maps to IO_ERROR and the UI
shows as 'Tile I/O error' even though the file exists and is intact.

The host regression test added in the prior commit now passes under both
strict C++11 and ASan/UBSan; reverting this change fails it.
2026-08-28 22:44:27 +00:00
Torlando 7c96ffac82 fix(maps): cancel stale tile rendering safely 2026-08-20 04:53:00 +00:00
Torlando d9cb64a1ea feat(maps): resolve visible tiles without span indexes 2026-08-20 04:53:00 +00:00
torlando-agent[bot] 26de7c3e41 fix(nomadnet): keep partial refresh status in chrome 2026-08-18 01:30:14 +00:00
torlando-agent[bot] 1027667f5a feat(nomadnet): integrate dynamic partial rendering 2026-08-17 23:49:13 +00:00
torlando-agent[bot] b6faec8104 feat(nomadnet): add bounded partial core 2026-08-17 19:54:41 +00:00
torlando-agent[bot] 27ac127f5e fix: preserve eight-column NomadNet tables 2026-08-17 16:50:21 +00:00
torlando-agent[bot] ca73ff2e3f fix: show NomadNet navigation progress promptly 2026-08-17 15:47:41 +00:00
torlando-agent[bot] 5b76abfdbe fix: align NomadNet cache response policy 2026-08-17 14:50:00 +00:00
torlando-agent[bot] 2f0f7900ca feat: enforce NomadNet limits and observability 2026-08-17 09:11:10 +00:00
torlando-agent[bot] 3658c54cc7 fix: retry cache lookup after busy operation 2026-08-17 06:03:38 +00:00
torlando-agent[bot] 16af3b53af feat: add bounded NomadNet SD page cache 2026-08-17 05:48:30 +00:00
torlando-agent[bot] f40fec17c4 fix: refine NomadNet form input handling 2026-08-17 01:07:19 +00:00
torlando-agent[bot] 98dca8e516 feat: add bounded NomadNet forms 2026-08-16 21:36:30 +00:00
torlando-agent[bot] 64eec3acd7 fix: isolate NomadNet table cell styles 2026-08-16 15:48:55 +00:00
torlando-agent[bot] d9945bce24 feat: add bounded NomadNet table rendering 2026-08-16 15:33:52 +00:00
torlando-agent[bot] 9ce2f10107 fix: add NomadNet anchor navigation 2026-08-16 02:40:10 +00:00
torlando-agent[bot] 48349a1df4 fix: match NomadNet heading presentation 2026-08-16 01:25:37 +00:00
torlando-agent[bot] 5f3696716f fix: consume unknown NomadNet modifiers 2026-08-16 01:25:07 +00:00
torlando-agent[bot] cf72c9b399 fix: support NomadNet grayscale colors 2026-08-16 01:25:07 +00:00
torlando-agent[bot] 97e3e8b09a fix: preserve NomadNet divider characters 2026-08-16 01:24:13 +00:00
torlando-agent[bot] 5961c791d6 Virtualize NomadNet page layout 2026-08-15 02:33:37 +00:00
torlando-agent[bot] fd776d7b78 Bound NomadNet pages and preserve decompression headroom 2026-08-14 22:50:00 +00:00
torlando-agent[bot] b50ae852ac fix(nomadnet): submit configured link variables 2026-08-14 18:11:32 +00:00
torlando-agent[bot] 76eae01ef3 fix(nomadnet): coalesce link focus outlines 2026-08-14 04:59:02 +00:00
torlando-agent[bot] 427206ef8f fix(nomadnet): honor micron colors 2026-08-14 01:59:25 +00:00
torlando-agent[bot] 42f87b8c75 fix(nomadnet): preserve navigation glyph coverage 2026-08-14 01:19:37 +00:00
torlando-agent[bot] 5826ad9b6a fix(nomadnet): scope page fonts to rendered content 2026-08-14 00:47:52 +00:00
torlando-agent[bot] 12def2c746 fix(nomadnet): use JetBrains Mono NL faces 2026-08-14 00:47:52 +00:00