- App drawer: size tiles to fit so the bottom row's labels aren't clipped, and
show a persistent scrollbar when the grid overflows (discoverable scroll).
- Snake: steer via the hardware swipe detector (works on the V4 cap-touch and
the T-Deck), stop touch/swipe leaking to the screen behind the overlay, and
gate the status bar while playing.
- Snake app-drawer icon drawn from shapes — the U+1F40D emoji isn't in the baked
colour-emoji set, so it rendered as a tofu box.
- Add beta_7 release notes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
loadLegacyHistoryFromStorage() tried to read MAX_UI_MESSAGES (now 500)
records from a file that only contained 96 (old ring size). It read the
thread metadata fine — populating unread counts — then hit EOF on the
messages and returned false. The reconciliation pass that resets phantom
unread counts never ran, so the first boot after the ring bump showed
high unread counters against empty channels.
Fix: derive the actual slot count from the remaining file size and only
read that many message records, zero-filling the rest. The phantom-badge
reconciliation in loadHistoryFromStorage() then clears any thread whose
messages were already evicted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thread metadata (~4 KB, contains unread counts) and the message ring
(now ~115 KB at 500 slots) are now persisted to separate files
(/ui_threads_v1.bin and /ui_msgs_v1.bin).
Thread metadata flushes on the original 200 ms delay so unread counts
survive a hard reset. The message ring flushes lazily every 2 s,
batching burst arrivals into a single write instead of hammering SPIFFS
on every message — reducing flash wear and WdtHeavyGuard exposure as
the ring grows.
MAX_UI_MESSAGES 96 → 500 (PSRAM cost: 26 KB → 133 KB, well within
budget). Ring overflow within a session is now extremely unlikely on
LoRa; the cross-session phantom-badge fix (post-load reconciliation)
from the previous commit handles the remaining edge case.
Migration: on first boot the loader falls back to the legacy combined
file (/ui_chat_history_v1.bin), then schedules writes of the new split
files. Old file is left in place and ignored on subsequent boots.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The display ring is a fixed-size cache independent of the per-thread
unread counter. After a reboot the ring may not contain the messages
that were counted as unread (they were evicted before the save), leaving
a non-zero badge on a thread that opens to an empty chat.
Fix: after loading history, zero the unread counter for any thread whose
messages are absent from the ring. The unread counter and the ring are
independent — the ring is a display buffer only — so opening the thread
via the first fix (markThreadRead on close) handles the mid-session case;
this one-shot check handles the cross-reboot case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 96-slot message ring can overflow when there is sustained traffic
across many threads. When ring wraparound silently overwrites a channel's
messages, thread.unread stays non-zero but getThreadMessageIndexes
returns 0 — producing a badge with a count but an empty chat on open.
The same phantom persists across reboots because unread is saved to
disk independently of the ring contents.
Fix has three parts:
1. counted_as_unread per UIMessage (RAM-only): set true when a message
is the reason a thread's unread counter was incremented. Cleared when
the thread is marked read, so future evictions of those slots don't
double-decrement.
2. Eviction hook in appendMessage: before overwriting a ring slot,
decrement its thread's unread if the slot was counted_as_unread.
Keeps the counter in sync as the ring wraps.
3. Post-load reconciliation in loadHistoryFromStorage: after restoring
thread+message state from disk, any thread whose persisted unread
count exceeds its actual incoming messages in the ring gets clamped
(or reset to 0 if the ring has nothing for it). Also reconstructs
the counted_as_unread flags so evictions after boot are tracked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two related causes:
1. Messages arriving while a chat is open increment unread (appendMessage
always passes mark_unread=true). closeChatPanel never called
markThreadRead, so those in-session messages left a phantom badge in
the inbox after the user closed the chat.
2. Neither threadSelectCb (open) nor closeChatPanel (close) set
dirty_threads, so the inbox list could lag up to 4 s (the periodic
refreshThreadsFromMesh tick) before reflecting a cleared count.
Fix: call markThreadRead on the active thread in closeChatPanel and set
dirty_threads so the inbox rebuilds immediately on return.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Battery log/chart now compile on both builds: a small FS selector prefers the SD
card when present (T-Deck, /meshcomod/battery.log) and falls back to internal
SPIFFS (/battery.log) otherwise, so the 24h chart + battery tap work on the
Heltec V4 (no SD slot) too. The 5-min logger and the battery tap-wiring are no
longer gated behind HAS_TDECK_GT911.
Telemetry per-node logs + poll.cfg move from /telemetry to /meshcomod/telemetry
to sit with the other app files. Telemetry logging/window/auto-poll stay
T-Deck-only (V4 keeps the request/reply toast).
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
s_fm_img_dsc is a reused static, so LVGL's image cache (keyed on the src pointer)
returned the previous image's decoded geometry/data — the 'half old, half new'
render. Invalidate the cache entry when building the dsc and on close.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
On the map tab the immersive chrome forces black text/icons for legibility over
the light tiles. With night mode on the tiles are dark, so flip the status-bar
text and the bottom tab-bar icons to off-white instead. Only affects the map tab;
re-applied immediately when night mode is toggled.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
The viewer already routed image taps to fmOpenImage but only ran the JPEG
decoder and bailed on PNG with a stale 'not supported' note. Sniff the magic
bytes and route PNG -> decodePngToRgb565 (lodepng, already used for map tiles)
and BMP -> a small 16/24-bpp reader (so screenshots saved by the status-bar hold
are viewable). fmIsImage now also accepts .bmp.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
A 'zoom <level>' chip, centred above the slider, appears with it and updates
live while dragging so the target level is visible before release.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Add a one-glyph FontAwesome font (zoom_font.c, U+F002 magnifying-glass, 16 px bpp4,
generated with lv_font_conv from fa-solid-900.ttf) and splice it into the g_font_16
fallback chain alongside person_font. The map zoom button now shows a magnifier
(TOUCH_SYM_ZOOM) instead of the borrowed + glyph.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Battery chart: stats line spans the full width so the CPU MHz figure no longer
wraps; the CPU series + chip now use the RF-monitor noise-floor grey (0x4A5256).
Telemetry: the window no longer auto-requests on open — it shows history and a
Request button (async refresh glyph, left of the gear) sends a poll. Legend text
recoloured to the axis-note grey instead of near-black.
Map: the + / - overlay buttons are replaced by a single zoom button that toggles
a full-width zoom slider along the bottom of the map (tap again to hide); the
chosen level is persisted across reboots and wins over the first-open auto-snap.
New 'Night mode (invert)' toggle in Map options inverts tile colours at render
time only (cache untouched), persisted.
Snake: pause/resume button next to the close X.
Status bar: hold for 3 s to capture a full-screen screenshot to
/screenshots/<time>.bmp on the SD card (16-bit BMP — fastest viewable format;
the hold suppresses the control-center tap).
Prefs: cfg v5 adds map_night + map_zoom.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
The card was a fixed 246 px — taller than the screen — and not scrollable, so its
bottom-anchored estimate/stats/points labels were clipped and overlapped (the
narrower text width also let the estimate wrap onto the stats line). Switch the
card to content height with a max-height cap + vertical scrolling, and place the
chart, the three text lines and the CPU/trash buttons with a running y so they
stack cleanly and the window scrolls when it doesn't fit.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
Battery chart: a CPU chip beside the trash button shows/hides the CPU-MHz
series; the secondary axis, series and bottom-line CPU figure are dropped when
hidden and the chart redraws immediately.
Telemetry window: a gear opens a settings panel spawned on top with checkboxes
for the battery / temperature / humidity series (the chart underneath redraws
live as they toggle), a manual auto-poll interval field (minutes, 0 = off,
replacing the cycling button) and the clear-history button. The main window now
holds only the reading, chart and time-range inputs; its layout was reworked so
the chart clears the wrapped reading line and the time-range fields no longer
overlap their labels.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
- Manual telemetry opens a per-node window immediately (request-in-progress),
then updates to the reading on reply or "last request failed" on timeout — the
window shows history either way (issue: see history even on failure).
- Chart from /telemetry/<id>.log: battery V (left) + temperature/humidity (right),
RF-monitor-style axis ticks + grid, point markers.
- Display window chosen with two fields (now-X h .. now-Y h ago); "Show" reloads.
- Auto-poll per node (toggle + 1/5/15/30/120 min), persisted to /telemetry/poll.cfg;
due nodes are polled from UITask::loop — emits the async spinner, logs the reply,
does NOT open the window.
- Clear-history buttons (with confirmation) on both the telemetry and battery windows.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
- trackball now steers Snake (UITask routes motion via SnakeGame::isOpen()/steer()
and hides the soft cursor on that page).
- a "New game" button gates the start so it doesn't move on open; tap restarts
after a game over.
- slower tick (220 -> 300 ms).
- playfield fills the unoccupied screen (dynamic cols x rows at kCellPx instead of
a small fixed square).
- the tab bar is hard-locked while Snake is open so a stray swipe/tap can't slip
through to the app switcher (e.g. into the map).
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
- contacts: the "?" (unknown last-heard) sink-to-end now mirrors formatAgeBadge's
exact condition (also future/garbage/>400-day timestamps, not just ==0), using
the clock captured before the sort. Fixes "no change in behaviour".
- battery chart: add a universal close X (addCloseXBadge); lighter grid + time
(vertical) division lines; axis notes (V left, MHz right, time on X); point
dots so a single/sparse sample renders; second ORANGE CPU line on a secondary
Y axis. battery.log gains a CPU-MHz column (5th field); pre-upgrade 4-col lines
read as a CPU gap.
- telemetry window: add the same universal close X.
- hide device name: park the clock CENTRED on every screen (not just Home),
clear of the left-zone title (chat / Files / map credit) and the right icons.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
A small amber dot in the status bar (just left of the Wi-Fi glyph) lights
for ~180 ms after any microSD access: SD-backed tile cache, SD tile packs,
and card mount. markSdIo() stamps a timestamp on the I/O path (one 32-bit
store, safe from either core); UITask::loop toggles the dot edge-triggered,
so an idle device pays a single comparison per loop and never relayouts.
The ble / clock / keyboard-layout labels shift left to free the slot.
- 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>
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>
Community PR by @Vybo (Dan Vybiral). Fixes Launcher SD-fallback tile caching (tilesFsLowSpace read the unmounted LittleFS partition as full and dropped every download), plus write-verification, read/write cap match, SD PNG-pack support, and on-screen fetch diagnostics.
Position the byline relative to .brand (the whole mark+word lockup) instead of just the word, so it centres on the combined logo+title (x=640) rather than under the word alone (x=694). The mark stays aligned with the word.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>