Compare commits

..
54 Commits
Author SHA1 Message Date
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 2aa8f59410 touch: beta_8 — Ack reply, map visibility toggles + auto-refresh, RAM trim
- Chat: quick replies insert at the cursor (no longer replace typed text);
  long-press 'Ack' drops a signal-strength + route reply into the composer
  (@mentions the sender on channels); fixed the action-menu overflowing the screen.
- Map: +/- zoom glyph; per-line on-map visibility toggles (coords / tile xyz /
  contacts); microSD-tiles regrouped under a heading; auto-refresh follow button
  with on/off toast; close-X overlap fix when scrolled.
- Perf: ~18.9 KB internal DRAM reclaimed (6 scratch buffers -> on-demand PSRAM).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:36:46 +02:00
Kaj SchittecatandClaude Opus 4.8 c8b451f14f release: beta_7 notes — credit contributors prominently (Vybo #12, Ruben Laban #15, marcelverdult #13)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:32:03 +02:00
Kaj SchittecatandClaude Opus 4.8 9e567e1789 touch: beta_7 — app-drawer fit + visible scroll, Snake touch steering & icon
- 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>
2026-06-18 12:21:49 +02:00
Kaj Schittecat bbcafa3088 Merge PR #12 (Vybo): status-bar tools, battery history, map night mode, image viewer, Snake
# Conflicts:
#	src/ui-touch/UITask.cpp
2026-06-18 11:04:08 +02:00
Kaj Schittecat b3af35666b Merge PR #15 (tun0): fix inaccurate unread badges + split history persistence
- closeChatPanel marks-read + flags dirty immediately (no stale badge)
- post-load reconciliation clears phantom unread for evicted threads
- split thread-meta (200ms flush) vs message-ring (lazy 2s) persistence
- ring 96 -> 500
2026-06-18 10:58:08 +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
Ruben LabanandClaude Sonnet 4.6 3a70a8ab0f touch: fix legacy history migration when ring size grew
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>
2026-06-18 10:45:30 +02:00
Ruben LabanandClaude Sonnet 4.6 2894776a11 touch: split history save into threads+msgs files; bump ring to 500
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>
2026-06-18 10:31:33 +02:00
Ruben LabanandClaude Sonnet 4.6 88a9d386f1 touch: reset phantom unread on load when ring has no messages for thread
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>
2026-06-18 09:05:36 +02:00
Ruben Laban a6647dcf55 Revert "touch: fix phantom unread badge when ring evicts channel messages"
This reverts commit 28879f55c4.
2026-06-18 09:04:37 +02:00
Ruben LabanandClaude Sonnet 4.6 28879f55c4 touch: fix phantom unread badge when ring evicts channel messages
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>
2026-06-18 08:34:37 +02:00
Ruben LabanandClaude Sonnet 4.6 d16c833171 touch: fix stale unread badge on per-channel/DM inbox rows
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>
2026-06-18 08:19:32 +02:00
Dan Vybiral 031cb03bd4 battery chart on V4 (SPIFFS fallback); telemetry log under /meshcomod/telemetry
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>
2026-06-17 14:59:30 +02:00
Dan Vybiral 5ec9176906 file browser: fix stale image when viewing several in a row
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>
2026-06-17 14:42:50 +02:00
Dan Vybiral ed9cf619ec map: invert status bar + tab bar chrome under night mode
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>
2026-06-17 14:32:18 +02:00
Dan Vybiral 460b82f9bc file browser: view PNG and BMP images, not just JPEG
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>
2026-06-17 14:30:36 +02:00
Dan Vybiral 0a4d8d5b2f map: live zoom-level readout above the zoom slider
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>
2026-06-17 14:23:21 +02:00
Dan Vybiral e72a1b1bd5 map: real magnifier glyph on the zoom button
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>
2026-06-17 13:56:37 +02:00
Dan Vybiral 30fe6fbd6f map/telemetry/battery/snake: visibility, zoom slider, night mode, screenshot
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>
2026-06-17 13:47:38 +02:00
Dan Vybiral 5e8dd55847 battery chart: make the window scrollable; lay the bottom block out top-down
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>
2026-06-17 13:19:39 +02:00
Dan Vybiral 26ae08101c telemetry/battery: data-visibility toggles + telemetry settings panel
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>
2026-06-17 13:05:07 +02:00
Dan Vybiral aa89572631 chore: gitignore custom build outputs and local notes
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 13:05:07 +02:00
Dan Vybiral ff5818f7f7 battery chart: stack bottom text clear of the trash button; dim the CPU line
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 12:48:14 +02:00
Dan Vybiral be7fb32f23 battery chart: estimated life left (d/h/m) from the discharge slope, skipping charge
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 12:43:02 +02:00
Dan Vybiral 17981029a7 telemetry: per-node window with battery/temp/humidity chart, auto-poll, clear
- 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>
2026-06-17 11:51:04 +02:00
Dan Vybiral 9bc9822111 telemetry: log each reply (battery/temp/humidity) to /telemetry/<id>.log per node
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 11:38:19 +02:00
Dan Vybiral 799a3754d0 battery chart: clamp charging spikes to top; RF-monitor-style axis ticks + grid
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 11:35:06 +02:00
Dan Vybiral c9e3de15d4 status bar: park the async spinner left of the clock so it never overlaps it
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:27:34 +02:00
Dan Vybiral be1c72fbb1 status bar: SD LED also covers file-manager reads + cross-volume copies
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:21:55 +02:00
Dan Vybiral b478851a21 status bar: async-request spinner (telemetry/admin/ping/trace/range/sightline)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:18:42 +02:00
Dan Vybiral 8ce6baf786 battery chart: blue series, calibrated range, visible points, per-point value labels
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:14:08 +02:00
Dan Vybiral b82f4a2363 apps: floor the drawer tile height so a full grid scrolls instead of shrinking
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:04:16 +02:00
Dan Vybiral 6e62d35328 contacts: add "Show on map" to the action sheet (centre the map on the node)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:45 +02:00
Dan Vybiral fe8925f91d apps: Snake review fixes — trackball, New-game gate, full field, tab-bar lock
- 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>
2026-06-17 09:54:44 +02:00
Dan Vybiral 7463530f63 ui: review fixes — contacts sort, battery chart, close buttons, clock centring
- 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>
2026-06-17 09:54:44 +02:00
Dan Vybiral d7151dbbf8 apps: extract Snake into a self-contained SnakeGame class (own files)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:44 +02:00
Dan Vybiral dd67336fdb apps: add a Snake game (canvas + swipe to steer)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:43 +02:00
Dan Vybiral 13b240062a telemetry: show the response in a dismissible window instead of a toast
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:04 +02:00
Dan Vybiral 9ddb08ba93 status bar: tap battery for a 24h history chart, logged to SD every 5 min
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:03 +02:00
Dan Vybiral 373be3e546 settings: option to hide the device name and park the clock left
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:03 +02:00
Dan Vybiral d2e776afe9 status bar: fix charging-shift offsets to match the SD-LED layout
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:03 +02:00
Dan Vybiral a5968afb70 status bar: light the SD LED on file-manager microSD access too
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:03 +02:00
Dan Vybiral 7aeee1f456 map: add a zoom + tile-path line under the OSM attribution
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:02 +02:00
Dan Vybiral ed72f15a81 about: tag this fork's builds with an "unofficial fork build" note
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:02 +02:00
Dan Vybiral 59896b6bdb about: show microSD card size + free space in the storage section
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:02 +02:00
Dan Vybiral 5ff7590168 chat: show hop count on received flood messages (next to the time)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:01 +02:00
Dan Vybiral 00f2f70800 contacts: sort unknown last-heard ("?") to the end of the list
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:01 +02:00
Dan Vybiral fdaf34575e status bar: microSD read/write activity LED left of Wi-Fi
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.
2026-06-17 09:54:01 +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
Dan Vybiral 976bf4022c map: fix server + SD tile rendering, add fetch diagnostics (#11)
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.
2026-06-16 22:39:35 +02:00
Kaj SchittecatandClaude Opus 4.8 b85e0a4849 site: centre the BY MESHCOMOD byline on the full lockup (mark + word)
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>
2026-06-15 23:58:43 +02:00
Kaj SchittecatandClaude Opus 4.8 e7d6c73d16 site: align mark with the WADAMESH word; byline floats beneath
The mark was vertically centring against the word+byline column, so it sat ~13px below the wordmark. Position the byline absolutely under the word so the mark centres on the word itself. ("Free — as it should be" was already centred on the mark+word lockup.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:49:49 +02:00
19 changed files with 4970 additions and 445 deletions

No files matched your search

+6
View File
@@ -28,3 +28,9 @@ scripts/build/wifi_secrets.h
# Local working notes + agent config (contain ops/infra details — never publish)
CLAUDE.md
.claude/
# Custom timestamped build outputs
/builds/
# Local reconstruction notes (scratch, never publish)
/.notes/
+6 -7
View File
@@ -49,12 +49,13 @@
.fab svg{width:21px; height:21px}
.fab-tg{left:16px}
.fab-bug{right:16px}
.brand{display:flex; align-items:center; justify-content:center; gap:clamp(12px,2.6vw,20px); flex-wrap:wrap}
.brand{display:flex; align-items:center; justify-content:center; gap:clamp(12px,2.6vw,20px); flex-wrap:wrap; position:relative}
.brand .mark{width:clamp(60px,13vw,88px); height:auto; flex:none}
.brand .word{font-weight:700; font-size:clamp(2rem,7.5vw,3.35rem); letter-spacing:3px; color:var(--ink); line-height:1}
.brand .word .t{color:var(--teal)}
.brand-text{display:flex; flex-direction:column; align-items:center; gap:5px}
.byline{font-family:'JetBrains Mono',monospace; font-size:clamp(.62rem,1.7vw,.8rem); letter-spacing:.2em; text-transform:uppercase; color:var(--mut); font-weight:600}
/* byline floats beneath the whole lockup, centred on mark+word combined (mark still centres on the word) */
.byline{position:absolute; top:100%; left:0; width:100%; margin-top:7px; text-align:center;
font-family:'JetBrains Mono',monospace; font-size:clamp(.62rem,1.7vw,.8rem); letter-spacing:.2em; text-transform:uppercase; color:var(--mut); font-weight:600}
/* animated mesh mark — repeaters broadcast pings; signal packets ride the trails */
.brand .mark{overflow:visible}
.wm-flow{display:none}
@@ -216,10 +217,8 @@
<!-- repeater nodes -->
<g class="wm-nodes" fill="#15B6A6"><circle cx="10" cy="60" r="7"/><circle cx="90" cy="60" r="7"/><circle cx="170" cy="60" r="7"/><circle cx="50" cy="12" r="7"/><circle cx="130" cy="12" r="7"/><circle cx="50" cy="108" r="7"/><circle cx="130" cy="108" r="7"/></g>
</svg>
<div class="brand-text">
<span class="word mono">WADA<span class="t">MESH</span></span>
<span class="byline">by meshcomod</span>
</div>
<span class="word mono">WADA<span class="t">MESH</span></span>
<span class="byline">by meshcomod</span>
</div>
<p class="lede">A real touchscreen UI for your mesh radio.</p>
<p class="sub">MeshCore MESHCOMOD firmware for the <b>LilyGo&nbsp;T-Deck</b> and <b>Heltec&nbsp;V4</b> — map, chat, channels and contacts on the device itself, <b>and</b> a companion radio for the <b>MeshCore app</b> at the same time.</p>
+19 -3
View File
@@ -19,6 +19,11 @@ extra_scripts = pre:scripts/inject_wifi_env.py, merge-bin.py
build_flags =
-w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-D MC_VENDORED_TOUCH_APP
; NOTE: tried -D ARDUINO_USB_MODE=1 (HW-CDC, drops unused TinyUSB MSC/DFU ≈ 12 KB internal
; DRAM) — but on the real V4 the binary companion link regressed: small per-message frames
; arrived yet the larger device-profile frame (node name + keys) dropped bytes over HW-CDC's
; smaller TX FIFO. Reverted to the board JSON's TinyUSB CDC. Revisit only with an HW-CDC TX
; buffer/DTR-wait fix verified on hardware. (T-Deck ships MODE=1 and is fine — board-specific.)
-D LORA_FREQ=869.618
-D LORA_BW=62.5
-D LORA_SF=8
@@ -126,6 +131,9 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
; Smoother scroll: lower deceleration than LVGL's default (10) so flicks glide
; longer instead of stopping abruptly. lv_conf leaves this unset, so this -D wins.
-D LV_INDEV_DEF_SCROLL_THROW=7
-D USER_SETUP_LOADED=1
-D ST7789_DRIVER=1
-D TFT_WIDTH=240
@@ -146,6 +154,7 @@ build_flags =
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADMIN_PASSWORD='"password"'
-D MULTI_TRANSPORT_COMPANION=1
-D WADAMESH_FORK_BUILD=1
-D TCP_PORT=5000
-D WS_PORT=8765
-D BLE_PIN_CODE=123456
@@ -166,7 +175,7 @@ lib_deps =
; MeshCore core = the meshcomod monorepo built as a lib, pinned by tag.
; Touch-app files (TouchPrefsStore, WifiRuntimeStore, transports, ...) are
; vendored in this app's src/ and dropped from the lib via -DMC_VENDORED_TOUCH_APP.
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.2
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.4
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -238,7 +247,10 @@ build_flags =
-I variants/lilygo_tdeck
-D LILYGO_TDECK
-D BOARD_HAS_PSRAM=1
-D CORE_DEBUG_LEVEL=1
; 0 = silence ARDUHAL [E] spam (benign vfs_api cache-miss "no permits for
; creation" + Preferences nvs_open NOT_FOUND on first-namespace-access) that
; also pollutes the USB-CDC companion stream. Our own Serial.printf logs stay.
-D CORE_DEBUG_LEVEL=0
-D ARDUINO_USB_CDC_ON_BOOT=1
-D PIN_USER_BTN=0
-D PIN_PERF_POWERON=10
@@ -307,11 +319,15 @@ build_flags =
-D PIN_TB_RIGHT=1
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
; Smoother scroll: lower deceleration than LVGL's default (10) so flicks glide
; longer instead of stopping abruptly. lv_conf leaves this unset, so this -D wins.
-D LV_INDEV_DEF_SCROLL_THROW=7
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADMIN_PASSWORD='"password"'
-D MULTI_TRANSPORT_COMPANION=1
-D WADAMESH_FORK_BUILD=1
-D TCP_PORT=5000
-D WS_PORT=8765
-D BLE_PIN_CODE=123456
@@ -329,7 +345,7 @@ build_src_filter =
+<../variants/lilygo_tdeck/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.2
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.4
SPI
Wire
jgromes/RadioLib @ ^7.6.0
+34
View File
@@ -0,0 +1,34 @@
# beta_6 — what's new (one user-facing note per non-blank, non-# line)
# --- New: RF Monitor app ---
New Monitor app on the app drawer: a live RF sniffer for your node — see exactly what it is pulling out of the air in real time
A "Recently heard" feed lists recent received frames by type (text, advert, ack, route-trace…) with RSSI, SNR and hop count, colour-coded by type
A link-margin readout grades how much headroom your last packet had (green / amber / red), alongside a live packet rate and a noise-floor + peak-hold RSSI scope with an on-screen dBm scale
The Monitor lays out cleanly in both portrait and landscape, so it is readable on the Heltec V4 and the T-Deck
# --- Chat & contacts ---
Direct-message chats now show a person icon and channels a group icon, instead of the old loop / envelope
You can block a non-contact sender by name — handy for silencing an automated room or repeater bot you could not block before
Opening a chat or channel no longer jumps you to the newest message; it keeps your scroll position
The contacts Sort & Filter popup now closes after you pick a sort option
The chat/channel long-press popup is fixed: the close X no longer overlaps the buttons while you scroll it
# --- Time & timezone ---
New time-zone picker with named zones (no longer CET-only) — set yours in Settings → System
Fixed the clock getting stuck at 1902 and channels showing 1969–1970 dates: GPS no longer clobbers the clock with an invalid date, and the time now survives a reboot
# --- Launcher & Wi-Fi ---
Map tiles now download when running under Launcher (the tile cache was wrongly being reported as full)
GPS now acquires a fix when running under Launcher
The setup-wizard Wi-Fi scan no longer comes up empty ("no networks found") — Wi-Fi modem sleep is deferred until after you are connected
# --- Bluetooth ---
Bluetooth now advertises under your node / profile name instead of "NimBLE"
The Bluetooth PIN no longer resets on its own — it is saved and reused across reboots
# --- Power & performance ---
Turning the screen off now downclocks the CPU and enables Wi-Fi modem sleep to save power
On the map, your position dot is drawn before the tiles so it no longer lags behind when you pan
# --- Community ---
Merged map-server and SD-tile rendering fixes contributed by Vybo (PR #11)
+48
View File
@@ -0,0 +1,48 @@
# beta_7 — what's new (one user-facing note per non-blank, non-# line)
# --- Community-built ---
Most of beta_7 was contributed by the community — Dan Vybiral (Vybo) and Ruben Laban. Thank you both!
# --- Chat reliability ---
Fixed garbled and duplicated messages — chat text is no longer corrupted in transit, so messages arrive intact and duplicates are filtered out correctly (thanks to marcelverdult for the report, issue #13)
Unread badges are accurate again: the phantom and stuck unread counts on chats and channels are fixed (Ruben Laban, PR #15)
More of your chat history is kept and reloaded across reboots, and it is saved more reliably than before (Ruben Laban, PR #15)
# --- New: Snake game ---
A Snake game is now in the app drawer — steer by swiping the screen or with the T-Deck trackball, with pause/resume and restart after a game over (Vybo, PR #12)
While you play, touch stays inside the game and no longer leaks through to the screen behind it
# --- Battery & telemetry ---
Tap the battery in the status bar for a 24-hour history chart with an estimated time-left readout; battery level is logged every 5 minutes (Vybo, PR #12)
New per-node telemetry window: poll a node's battery, temperature and humidity, watch them on a live chart, and keep a per-node log (Vybo, PR #12)
# --- Map ---
New map night mode — the status bar and tab bar invert to match the dark map instead of glaring (Vybo, PR #12)
A live zoom-level readout sits above the zoom slider, and the zoom button now shows a proper magnifier icon (Vybo, PR #12)
Contacts have a new "Show on map" action that centres the map on that node (Vybo, PR #12)
# --- Status bar & tools ---
A microSD activity LED lights whenever the card is accessed, including file-manager reads and copies (Vybo, PR #12)
An activity spinner shows while a telemetry, admin, ping, trace, range or sightline request is in flight, kept clear of the clock (Vybo, PR #12)
New option to hide the device name and move the clock to the left (Vybo, PR #12)
Press and hold the status bar to save a full-screen screenshot to the SD card (Vybo, PR #12)
# --- Files & images ---
The file browser can preview PNG and BMP images now, not just JPEG, and no longer shows a stale image when you open several in a row (Vybo, PR #12)
Lock-screen wallpaper: open Files, view any JPEG on your SD card, and tap "Set as wallpaper" — no more slow scan or "no image found"
# --- App drawer ---
The app drawer fits all the apps again — the bottom row's labels are no longer cut off
A scrollbar clearly shows when there are more apps than fit, so it is obvious the page scrolls
# --- Bluetooth ---
You can set your own Bluetooth pairing code in Settings now, instead of being stuck with the default
# --- Storage & stability ---
Fixed map tiles failing to cache (with a false "storage full"), and black or missing tiles now re-download instead of staying blank
Replying to a message with @ no longer overwrites text you were already typing
Fixed an out-of-memory crash and made notification audio more reliable
# --- Community ---
Huge thanks to Dan Vybiral (Vybo) — most of beta_7 is his work: the Snake game, battery history, per-node telemetry, map night mode, PNG/BMP image viewer and the whole status-bar toolkit (PR #12)
Thanks to Ruben Laban for the accurate unread-badge and chat-history-persistence fixes (PR #15)
+16
View File
@@ -0,0 +1,16 @@
# beta_8 — what's new (one user-facing note per non-blank, non-# line)
# --- Chat ---
Quick replies now insert at the cursor instead of wiping whatever you'd already typed — tap one mid-message and it's added in place
Long-press any message for a new one-tap "Ack": it drops a ready-to-send reply into the composer with that message's signal strength (SNR/RSSI) and the route it took, so you can instantly confirm someone's test came through — on a channel it @mentions the sender
Fixed the message menu (Ack / Copy / Info / Mention / Block) running off the bottom of the screen — it now fits, and scrolls if it ever needs to
# --- Map ---
The zoom button now shows a clear "+/-" icon
Each on-map readout can be shown or hidden independently in Map settings — the coordinates line, the tile x/y/z line, and the contact markers
Map settings are reorganised: the microSD-tiles switch sits at the top under its own heading, separated from the new show/hide toggles
New auto-refresh button: turn it on and the map re-centres on you automatically whenever your position changes, with an on-screen "Auto-refresh on/off" confirmation
Fixed the close (X) in Map settings overlapping the switches when the panel was scrolled
# --- Performance ---
Freed about 19 KB of internal memory, leaving more headroom for Wi-Fi and trimming the idle-memory growth seen in recent betas
+26
View File
@@ -0,0 +1,26 @@
# beta_9 — what's new (one user-facing note per non-blank, non-# line)
# --- Stability ---
Wi-Fi now reconnects on its own after the link drops — no more rebooting just to get back online
Fixed a bug where a corrupted chat-history file could make the device reboot on every received message (and hide your messages) — the bad file is now detected and reset automatically so it recovers on its own
Hardened the boot path so a device with quirky memory degrades gracefully instead of getting stuck on the boot logo
# --- Crash reports ---
After a crash, Settings -> About now shows an "Export crash report" button that saves the crash dump to the microSD card, so you can send it to the devs and get the bug fixed
# --- Map ---
The reload button for corrupt tiles now actually clears the cached tiles — including when you're offline — and missing/blank "twilight zone" tiles re-download on their own
Fixed tiles not re-fetching at zoom levels 13 to 15
# --- Contacts ---
Favourited contacts now always appear in the contact list, even with hundreds of similarly-named contacts (they used to stay hidden until you searched for them)
# --- Chat ---
Room-server messages now show the time each message was actually sent, instead of when it arrived
Room-server messages now show who sent them
Sharing your contact QR code now produces a valid code that the MeshCore phone app can scan and import
Unread counts now show "99+" instead of an alarming raw number on busy channels
# --- App drawer ---
New icon-size setting: tap the cog at the top-right of the app drawer to switch between Compact and Large — bigger icons and labels, easier to read
Smoother scrolling through the app list (and the other scrollable lists)
+35 -1
View File
@@ -1361,6 +1361,20 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
_ui_sig_snr_q4 = (int8_t)(snr * 4.0f);
_ui_sig_rssi = (int8_t)rssi;
_ui_sig_ms = millis();
// Recent-RX ring for the Monitor app (see UiRxRec): parse the minimal header
// (type / route / hops) and push newest-first. Header byte layout is
// [version:2][payload_type:4][route_type:2]
// with 4 transport-code bytes following iff route is a TRANSPORT_* one; the
// next byte's low 6 bits are the path length (hop count).
if (len > 0) {
const uint8_t rt = raw[0] & 0x03;
const bool xp = (rt == ROUTE_TYPE_TRANSPORT_FLOOD || rt == ROUTE_TYPE_TRANSPORT_DIRECT);
const int ps = 1 + (xp ? 4 : 0);
const uint8_t hops = (ps < len) ? (uint8_t)(raw[ps] & 0x3F) : 0;
uiRxLogPush(_ui_sig_ms, (int8_t)rssi, _ui_sig_snr_q4,
(uint8_t)((raw[0] >> 2) & 0x0F), rt, hops,
(uint8_t)(len > 255 ? 255 : len));
}
#if defined(DISPLAY_CLASS)
// Diagnostic: log EVERY received frame so we can prove what reaches the
// radio. Header byte layout is
@@ -1866,6 +1880,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
const int8_t rssi = (int8_t)(_radio->getLastRSSI());
const bool is_flood = pkt->isRouteFlood();
uiStashRxMeta(pkt); // capture route + scope for the per-message Info popup
_last_sender_ts = sender_timestamp; // embedded send-time -> UI bubble ts (room history replay)
_ui->newMsgFromPubWithMeta(path_len, is_flood, from.id.pub_key, from.name,
text, history_count, snr_q4, rssi);
}
@@ -2478,7 +2493,12 @@ void MyMesh::begin(bool has_display) {
#ifdef DISPLAY_CLASS
if (has_display && BLE_PIN_CODE == 123456) {
StdRNG rng;
_active_ble_pin = rng.nextInt(100000, 999999); // random pin each session
_active_ble_pin = rng.nextInt(100000, 999999); // random pin, generated ONCE
// Persist it so it stays the SAME across reboots — it used to be re-rolled
// every boot (ble_pin stayed 0), so a paired phone's saved PIN stopped
// matching and pairing broke (user report: "BT pin resets sometimes").
_prefs.ble_pin = _active_ble_pin;
savePrefs();
} else {
_active_ble_pin = BLE_PIN_CODE; // otherwise static pin
}
@@ -2534,6 +2554,20 @@ NodePrefs *MyMesh::getNodePrefs() {
uint32_t MyMesh::getBLEPin() {
return _active_ble_pin;
}
// User-chosen pairing code from the touch BLE settings page. Validated to a
// 6-digit value, persisted to _prefs.ble_pin, and applied at the next boot
// (begin() seeds _active_ble_pin from it) — same contract as the companion
// CMD_SET_DEVICE_PIN. Returns false on an out-of-range PIN.
bool MyMesh::setBLEPin(uint32_t pin) {
// Any 6-digit BLE passkey is valid, INCLUDING ones that start with 0 (e.g.
// "012345" == 12345) — the UI validates the 6-digit string and displays it
// zero-padded. Only reject 0 (the "use default/random" sentinel) and values
// that don't fit in 6 digits.
if (pin == 0 || pin > 999999) return false;
_prefs.ble_pin = pin;
savePrefs();
return true;
}
struct FreqRange {
uint32_t lower_freq, upper_freq;
+41
View File
@@ -113,6 +113,7 @@ public:
const char *getNodeName();
NodePrefs *getNodePrefs();
uint32_t getBLEPin();
bool setBLEPin(uint32_t pin); // user-chosen 6-digit pairing code (persisted; applies next boot)
// Live device info accessors (used by the touch Settings → Device modal to
// mirror the web client's "Device (live)" panel — public key prefix, channel
@@ -392,6 +393,7 @@ public:
uint8_t _last_rx_path_n = 0;
uint16_t _last_rx_scope = 0; // transport_codes[0] of the last RX flood ("scope")
bool _last_rx_has_scope = false; // false if the packet carried no transport codes
uint32_t _last_sender_ts = 0; // embedded send-time of the last inbound msg (UI bubble ts; 0 = use now)
volatile bool _echo_dirty = false; // a repeat was counted -> UI should refresh
// ---- Live signal strength (top-bar icon) ----
@@ -404,6 +406,41 @@ public:
int8_t uiSignalRssi() const { return _ui_sig_rssi; }
uint32_t uiSignalMs() const { return _ui_sig_ms; }
// ---- Recent-RX ring (RF Monitor app) ----
// One record per received frame, captured in logRxRaw(): payload type / route
// / hop count / length + signal, so the Monitor page can show a live "what am
// I hearing" feed without a core hook. Newest-first via uiRxLogGet (i=0 = most
// recent). Board-independent — populated on every board, not just touch.
struct UiRxRec {
uint32_t ms; // millis() at RX
int8_t rssi; // dBm
int8_t snr_q4; // SNR_dB * 4
uint8_t ptype; // payload type (raw[0]>>2)&0x0F
uint8_t route; // route type raw[0]&0x03
uint8_t hops; // path length carried (0 = heard direct from origin)
uint8_t len; // frame length (clamped to 255)
};
static const int UI_RXLOG_MAX = 16;
UiRxRec _ui_rxlog[UI_RXLOG_MAX];
uint8_t _ui_rxlog_head = 0; // next write slot
uint8_t _ui_rxlog_cnt = 0; // valid entries (<= UI_RXLOG_MAX)
uint8_t uiRxLogCount() const { return _ui_rxlog_cnt; }
bool uiRxLogGet(uint8_t i, UiRxRec& out) const {
if (i >= _ui_rxlog_cnt) return false;
uint8_t idx = (uint8_t)((_ui_rxlog_head + UI_RXLOG_MAX - 1 - i) % UI_RXLOG_MAX);
out = _ui_rxlog[idx];
return true;
}
// Record a reception into the ring (called from logRxRaw).
void uiRxLogPush(uint32_t ms, int8_t rssi, int8_t snr_q4,
uint8_t ptype, uint8_t route, uint8_t hops, uint8_t len) {
UiRxRec& r = _ui_rxlog[_ui_rxlog_head];
r.ms = ms; r.rssi = rssi; r.snr_q4 = snr_q4;
r.ptype = ptype; r.route = route; r.hops = hops; r.len = len;
_ui_rxlog_head = (uint8_t)((_ui_rxlog_head + 1) % UI_RXLOG_MAX);
if (_ui_rxlog_cnt < UI_RXLOG_MAX) _ui_rxlog_cnt++;
}
/** Fingerprint of the most-recently originated flood TXT payload (0 if none). */
uint32_t uiLastSentFp() const { return _last_sent_fp; }
@@ -445,6 +482,10 @@ public:
/** Scope (transport_codes[0]) of the last received flood; *has = false when
* the packet carried no transport codes. */
uint16_t lastRxScope(bool* has) const { if (has) *has = _last_rx_has_scope; return _last_rx_scope; }
/** Consume the embedded send-time stashed right before the last UI notify (room
* history replay carries old send-times; without this the UI stamps "now").
* Returns 0 when nothing was stashed -> the caller keeps the delivery time. */
uint32_t uiConsumeLastSenderTs() { uint32_t t = _last_sender_ts; _last_sender_ts = 0; return t; }
/** Capture route + scope of a just-received flood for the Info popup. Call
* synchronously right before the newMsg* notification. */
void uiStashRxMeta(mesh::Packet* pkt) {
+221 -6
View File
@@ -35,7 +35,7 @@ static bool s_begun = false;
// short read (→ treat as absent → defaults); `ver` lets later builds add fields.
static const char* KEY_CFG = "cfg";
static const uint16_t TOUCH_CFG_MAGIC = 0x5743; // 'WC' (WadaCfg)
static const uint8_t TOUCH_CFG_VER = 2; // v2 adds sig_probe_en + sig_poll_min
static const uint8_t TOUCH_CFG_VER = 7; // v2 sig_probe/poll; v3 tz_zone; v4 hide_node_name; v5 map_night/map_zoom; v6 map text/marker visibility; v7 app_grid_large
// Defaults (kept identical to the historical per-key defaults).
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
@@ -72,6 +72,14 @@ struct __attribute__((packed)) TouchCfg {
uint32_t gps_baud; // "gps_baud" 0 = unset -> caller fallback
uint8_t sig_probe_en; // signal auto-discover probe on/off (bool) — v2
uint16_t sig_poll_min; // minutes between signal probes, 1..1440 — v2
uint8_t tz_zone; // selected time-zone index (0 = Europe/CET) — v3
uint8_t hide_node_name; // hide the device name in the status bar + park clock left (bool) — v4
uint8_t map_night; // invert map tile colours at render time (bool) — v5
uint8_t map_zoom; // last map zoom level (0 = unset, auto-snap) — v5
uint8_t map_show_coords; // show the coords read-out on the map (bool) — v6
uint8_t map_show_tilexyz; // show the zoom + tile z/x/y line on the map (bool) — v6
uint8_t map_show_contacts;// show contact markers on the map (bool) — v6
uint8_t app_grid_large; // app drawer: large grid (one fewer column, bigger icons) — v7
};
static TouchCfg s_cfg;
@@ -120,6 +128,14 @@ static void cfgSetDefaults(TouchCfg& c) {
c.gps_baud = 0; // 0 sentinel -> getter returns caller fallback
c.sig_probe_en = DEFAULT_SIG_PROBE_EN;
c.sig_poll_min = DEFAULT_SIG_POLL_MIN;
c.tz_zone = 0; // 0 = Europe (CET/CEST) — preserves prior behaviour
c.hide_node_name = 0; // default: show the device name
c.map_night = 0; // default: normal (light) tiles
c.map_zoom = 0; // 0 = unset -> auto-snap on first map open
c.map_show_coords = 1; // default: show coords / tile line / contacts
c.map_show_tilexyz = 1;
c.map_show_contacts = 1;
c.app_grid_large = 0; // default: compact app grid (T-Deck 4 cols / V4 3 cols)
}
// Persist the whole blob using the same end()/begin(RW)/put/end()/begin(RO)
@@ -154,6 +170,12 @@ static void cfgLoadOrMigrate() {
// newer trailing fields at their default), then version-upgrade if needed.
memcpy(&s_cfg, &tmp, n < sizeof(s_cfg) ? n : sizeof(s_cfg));
if (s_cfg.ver < TOUCH_CFG_VER) {
// v2->v3: a manual hour offset used to mean "CET base + offset". Preserve
// that under the new zone picker by mapping such users onto the Custom
// (UTC-offset) zone, so their clock doesn't jump to CET. 0xFE is resolved
// to the real Custom index on the first touchPrefsGetTimezone() call (the
// zone count isn't known here). offset 0 stays zone 0 (Europe) = unchanged.
if (s_cfg.ver < 3 && s_cfg.time_offs != 0) s_cfg.tz_zone = 0xFE;
s_cfg.ver = TOUCH_CFG_VER;
s_cfg.magic = TOUCH_CFG_MAGIC;
cfgFlush(); // rewrite with new fields defaulted-in
@@ -489,13 +511,70 @@ bool touchPrefsSetTimeOffsetHours(int hours) {
s_cfg.time_offs = (int8_t)hours;
return cfgFlush();
}
// Curated time zones, each with the correct POSIX DST rules for that region, so
// non-European users get the right time year-round instead of the CET base + EU
// DST dates. Index 0 (Europe/CET) is the default and matches the old behaviour.
struct TzZone { const char* label; const char* posix; };
static const TzZone TZ_ZONES[] = {
{ "Europe (CET/CEST)", "CET-1CEST,M3.5.0,M10.5.0/3" },
{ "UK (GMT/BST)", "GMT0BST,M3.5.0/1,M10.5.0" },
{ "UTC", "UTC0" },
{ "US Eastern", "EST5EDT,M3.2.0,M11.1.0" },
{ "US Central", "CST6CDT,M3.2.0,M11.1.0" },
{ "US Mountain", "MST7MDT,M3.2.0,M11.1.0" },
{ "US Arizona (no DST)", "MST7" },
{ "US Pacific", "PST8PDT,M3.2.0,M11.1.0" },
{ "US Alaska", "AKST9AKDT,M3.2.0,M11.1.0" },
{ "US Hawaii", "HST10" },
{ "Canada Atlantic", "AST4ADT,M3.2.0,M11.1.0" },
{ "Brazil (Brasilia)", "<-03>3" },
{ "India (IST)", "IST-5:30" },
{ "China (CST)", "CST-8" },
{ "Japan (JST)", "JST-9" },
{ "Sydney (AEST/AEDT)", "AEST-10AEDT,M10.1.0,M4.1.0/3" },
};
static const int TZ_ZONE_N = (int)(sizeof(TZ_ZONES) / sizeof(TZ_ZONES[0]));
int touchPrefsTimezoneCount() { return TZ_ZONE_N + 1; } // +1 = "Custom (UTC offset)"
const char* touchPrefsTimezoneLabel(int idx) {
if (idx >= 0 && idx < TZ_ZONE_N) return TZ_ZONES[idx].label;
if (idx == TZ_ZONE_N) return "Custom (UTC offset)";
return "";
}
uint8_t touchPrefsGetTimezone() {
if (!s_begun) touchPrefsBegin();
uint8_t z = s_cfg.tz_zone;
if (z == 0xFE) { // v2->v3 migration sentinel: had a manual offset -> Custom zone
z = (uint8_t)(touchPrefsTimezoneCount() - 1);
s_cfg.tz_zone = z;
cfgFlush();
}
if (z >= (uint8_t)touchPrefsTimezoneCount()) z = 0; // stale/garbage -> default
return z;
}
void touchPrefsSetTimezone(uint8_t idx) {
if (!s_begun) touchPrefsBegin();
if (idx >= (uint8_t)touchPrefsTimezoneCount()) idx = 0;
s_cfg.tz_zone = idx;
cfgFlush();
}
void touchPrefsBuildLocalTz(char* out, int out_cap) {
if (!out || out_cap <= 0) return;
// CET base is UTC+1 (POSIX std offset -1). The displayed local time is
// UTC + (1 + userOffset), so the POSIX std-offset field is -(1 + userOffset).
// CEST (summer) auto-adds 1h via the DST rule. offset 0 == "CET-1CEST,...".
int off = touchPrefsGetTimeOffsetHours();
snprintf(out, out_cap, "CET%dCEST,M3.5.0,M10.5.0/3", -(1 + off));
const uint8_t z = touchPrefsGetTimezone();
if (z < (uint8_t)TZ_ZONE_N) {
snprintf(out, out_cap, "%s", TZ_ZONES[z].posix);
return;
}
// "Custom (UTC offset)": a fixed offset, no DST. POSIX std-offset is the
// negation of the UTC offset (it's the time to ADD to local to reach UTC), so
// UTC-7 -> "<-07>7", UTC+2 -> "<+02>-2".
const int off = touchPrefsGetTimeOffsetHours();
if (off == 0) { snprintf(out, out_cap, "UTC0"); return; }
snprintf(out, out_cap, "<%+03d>%d", off, -off);
}
uint32_t touchPrefsGetLockTextColor() {
@@ -617,6 +696,73 @@ bool touchPrefsSetTilesFromSd(bool from_sd) {
return cfgFlush();
}
bool touchPrefsGetHideNodeName() {
if (!s_begun) touchPrefsBegin();
return s_cfg.hide_node_name != 0; // default = show the name
}
bool touchPrefsSetHideNodeName(bool hide) {
if (!s_begun) touchPrefsBegin();
s_cfg.hide_node_name = hide ? 1 : 0;
return cfgFlush();
}
bool touchPrefsGetMapNight() {
if (!s_begun) touchPrefsBegin();
return s_cfg.map_night != 0;
}
bool touchPrefsSetMapNight(bool on) {
if (!s_begun) touchPrefsBegin();
s_cfg.map_night = on ? 1 : 0;
return cfgFlush();
}
uint8_t touchPrefsGetMapZoom() {
if (!s_begun) touchPrefsBegin();
return s_cfg.map_zoom;
}
bool touchPrefsSetMapZoom(uint8_t z) {
if (!s_begun) touchPrefsBegin();
s_cfg.map_zoom = z;
return cfgFlush();
}
bool touchPrefsGetMapShowCoords() {
if (!s_begun) touchPrefsBegin();
return s_cfg.map_show_coords != 0;
}
bool touchPrefsSetMapShowCoords(bool on) {
if (!s_begun) touchPrefsBegin();
s_cfg.map_show_coords = on ? 1 : 0;
return cfgFlush();
}
bool touchPrefsGetMapShowTileXYZ() {
if (!s_begun) touchPrefsBegin();
return s_cfg.map_show_tilexyz != 0;
}
bool touchPrefsSetMapShowTileXYZ(bool on) {
if (!s_begun) touchPrefsBegin();
s_cfg.map_show_tilexyz = on ? 1 : 0;
return cfgFlush();
}
bool touchPrefsGetMapShowContacts() {
if (!s_begun) touchPrefsBegin();
return s_cfg.map_show_contacts != 0;
}
bool touchPrefsSetMapShowContacts(bool on) {
if (!s_begun) touchPrefsBegin();
s_cfg.map_show_contacts = on ? 1 : 0;
return cfgFlush();
}
bool touchPrefsGetAppGridLarge() {
if (!s_begun) touchPrefsBegin();
return s_cfg.app_grid_large != 0;
}
bool touchPrefsSetAppGridLarge(bool on) {
if (!s_begun) touchPrefsBegin();
s_cfg.app_grid_large = on ? 1 : 0;
return cfgFlush();
}
// Store ALL device data (identity, prefs, contacts, channels) on the SD card
// under /meshcomod instead of internal SPIFFS. Read at boot (main.cpp) BEFORE
// the data loads, so changing it needs a reboot. Key "use_sd" in the "touch"
@@ -883,6 +1029,67 @@ bool touchPrefsSetIgnored(const uint8_t* pub_key6, bool ignored) {
}
}
// Ignored / blocked sender NAMES (channel/room senders that aren't contacts) ---
// One NVS blob "ign_nm" of up to TOUCH_IGNORED_NAMES_MAX fixed-width,
// NUL-padded TOUCH_IGNORED_NAME_LEN slots. Same read/replace/write scheme as
// the 6-byte prefix list above.
static const char* KEY_IGN_NAMES = "ign_nm";
static int ignNamesReadAll(char out[TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN]) {
if (!s_begun) touchPrefsBegin();
if (!s_prefs.isKey(KEY_IGN_NAMES)) return 0; // absent on a fresh device — skip [E] NOT_FOUND
size_t n = s_prefs.getBytes(KEY_IGN_NAMES, out, TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN);
if (n == 0 || n > (size_t)(TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN)) return 0;
return (int)(n / TOUCH_IGNORED_NAME_LEN);
}
static bool ignNamesWriteAll(const char* buf, int count) {
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok;
if (count <= 0) { s_prefs.remove(KEY_IGN_NAMES); ok = true; }
else ok = s_prefs.putBytes(KEY_IGN_NAMES, buf, (size_t)(count * TOUCH_IGNORED_NAME_LEN)) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
}
bool touchPrefsIsNameIgnored(const char* name) {
if (!name || !name[0]) return false;
char buf[TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN];
int n = ignNamesReadAll(buf);
for (int i = 0; i < n; ++i)
if (strncmp(&buf[i * TOUCH_IGNORED_NAME_LEN], name, TOUCH_IGNORED_NAME_LEN) == 0) return true;
return false;
}
int touchPrefsCopyIgnoredNames(char* out_buf) {
if (!out_buf) return 0;
return ignNamesReadAll(out_buf);
}
bool touchPrefsSetNameIgnored(const char* name, bool ignored) {
if (!name || !name[0]) return false;
char buf[TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN];
int n = ignNamesReadAll(buf);
int found = -1;
for (int i = 0; i < n; ++i)
if (strncmp(&buf[i * TOUCH_IGNORED_NAME_LEN], name, TOUCH_IGNORED_NAME_LEN) == 0) { found = i; break; }
if (ignored) {
if (found >= 0) return true;
if (n >= TOUCH_IGNORED_NAMES_MAX) return false; // cap reached, silently refuse
char* slot = &buf[n * TOUCH_IGNORED_NAME_LEN];
memset(slot, 0, TOUCH_IGNORED_NAME_LEN);
strncpy(slot, name, TOUCH_IGNORED_NAME_LEN - 1);
++n; ignNamesWriteAll(buf, n); return true;
} else {
if (found < 0) return false;
for (int i = found; i < n - 1; ++i)
memcpy(&buf[i * TOUCH_IGNORED_NAME_LEN], &buf[(i + 1) * TOUCH_IGNORED_NAME_LEN], TOUCH_IGNORED_NAME_LEN);
--n; ignNamesWriteAll(buf, n); return false;
}
}
// Notification-sound prefs (individual NVS keys — integer getters don't emit the
// [E] NOT_FOUND log that getString/getBytes do on a fresh device) -----------
static void prefsPutUChar(const char* key, uint8_t v) {
@@ -912,6 +1119,14 @@ uint8_t touchPrefsGetDiscoveredMaxHops() {
return s_prefs.getUChar("dsc_hops", 0); // 0 = off
}
void touchPrefsSetDiscoveredMaxHops(uint8_t hops) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("dsc_hops", hops); }
bool touchPrefsGetEnterSends() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("ent_send", 1) != 0; }
void touchPrefsSetEnterSends(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("ent_send", on ? 1 : 0); }
bool touchPrefsGetClock12h() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("clk_12h", 0) != 0; }
void touchPrefsSetClock12h(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("clk_12h", on ? 1 : 0); }
bool touchPrefsGetScrollReverse() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("tb_rev", 0) != 0; }
void touchPrefsSetScrollReverse(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("tb_rev", on ? 1 : 0); }
bool touchPrefsGetLockOnScreenOff() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("lock_off", 0) != 0; }
void touchPrefsSetLockOnScreenOff(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("lock_off", on ? 1 : 0); }
// Generic blob (used to persist the discovered-nodes list across reboots).
size_t touchPrefsGetBlob(const char* key, uint8_t* out, size_t maxlen) {
+65 -3
View File
@@ -80,6 +80,35 @@ bool touchPrefsSetUseMiles(bool use_miles);
bool touchPrefsGetTilesFromSd();
bool touchPrefsSetTilesFromSd(bool from_sd);
/** Map night mode: invert tile colours at render time (cache stays untouched).
* Default false. */
bool touchPrefsGetMapNight();
bool touchPrefsSetMapNight(bool on);
/** Last map zoom level, persisted so the map reopens where the user left it.
* 0 = unset (let the auto-snap pick a level for the available tile pack). */
uint8_t touchPrefsGetMapZoom();
bool touchPrefsSetMapZoom(uint8_t z);
/** Per-element visibility of the map's on-screen text/markers. All default true
* (shown), so existing installs are unchanged. */
bool touchPrefsGetMapShowCoords();
bool touchPrefsSetMapShowCoords(bool on);
bool touchPrefsGetMapShowTileXYZ();
bool touchPrefsSetMapShowTileXYZ(bool on);
bool touchPrefsGetMapShowContacts();
bool touchPrefsSetMapShowContacts(bool on);
/* App drawer: large grid (one fewer column → bigger icons + labels, for low vision).
* Default false = the compact grid (T-Deck 4 cols / Heltec V4 3 cols). */
bool touchPrefsGetAppGridLarge();
bool touchPrefsSetAppGridLarge(bool on);
/** Hide the device/profile name in the status bar and move the clock to the
* left where the name used to be. Default false (name shown). */
bool touchPrefsGetHideNodeName();
bool touchPrefsSetHideNodeName(bool hide);
/** Store all device data (identity/prefs/contacts/channels) on the SD card under
* /meshcomod instead of internal SPIFFS. T-Deck only; read at boot before data
* loads, so changing it requires a reboot. Default false (SPIFFS). */
@@ -156,6 +185,20 @@ bool touchPrefsIsIgnored(const uint8_t* pub_key6);
bool touchPrefsSetIgnored(const uint8_t* pub_key6, bool ignored);
int touchPrefsCopyIgnored(uint8_t* out_buf);
/** Ignored / blocked sender NAMES — for channel/room senders who are NOT saved
* contacts, so the 6-byte pubkey scheme above can't target them (e.g. a bot
* posting inside a joined room: posts carry only a display name, not a pubkey).
* Incoming channel/room messages whose parsed sender name is stored here are
* dropped (no bubble, no notification, no sound). Case-sensitive exact match.
* • touchPrefsIsNameIgnored / touchPrefsSetNameIgnored: membership + add/remove.
* • touchPrefsCopyIgnoredNames: copy every stored name into `out_buf`
* (>= TOUCH_IGNORED_NAMES_MAX * TOUCH_IGNORED_NAME_LEN bytes); returns count. */
constexpr int TOUCH_IGNORED_NAMES_MAX = 16;
constexpr int TOUCH_IGNORED_NAME_LEN = 28; // fixed NUL-padded slot (>= MAX_SENDER_NAME+1)
bool touchPrefsIsNameIgnored(const char* name);
bool touchPrefsSetNameIgnored(const char* name, bool ignored);
int touchPrefsCopyIgnoredNames(char* out_buf);
/** Notification-sound prefs (the message chime itself stays on the core
* buzzer_quiet flag). @-mention chime is a SEPARATE enable + a distinct sound,
* so an operator can mute everything but still hear @-mentions. Volume is the
@@ -170,6 +213,14 @@ bool touchPrefsGetSoundMentions(); // default true
void touchPrefsSetSoundMentions(bool on);
uint8_t touchPrefsGetSoundVolume(); // 0..100, default 70
void touchPrefsSetSoundVolume(uint8_t vol);
bool touchPrefsGetEnterSends(); // Enter key sends a chat message (default true)
void touchPrefsSetEnterSends(bool on);
bool touchPrefsGetClock12h(); // 12-hour clock (default false = 24h)
void touchPrefsSetClock12h(bool on);
bool touchPrefsGetScrollReverse(); // invert trackball/scrollball direction (default false)
void touchPrefsSetScrollReverse(bool on);
bool touchPrefsGetLockOnScreenOff(); // idle screen-off auto-locks; only a deliberate hold wakes (default false)
void touchPrefsSetLockOnScreenOff(bool on);
/** Per-channel mute, keyed by channel name. Bit 0 = mute messages, bit 1 =
* mute @-mentions. Suppresses the notification SOUND for that channel (the
@@ -252,9 +303,20 @@ bool touchPrefsSetAccentColor(uint32_t rgb);
int touchPrefsGetTimeOffsetHours();
bool touchPrefsSetTimeOffsetHours(int hours);
/** Build the POSIX TZ string for local-time display: the CET/CEST base plus the
* user's hour offset (see touchPrefsGetTimeOffsetHours). Used by both the
* boot-time setenv and the Wi-Fi NTP sync so the offset is honoured everywhere.
/** Time-zone picker. A curated list of named zones, each carrying the correct
* POSIX TZ string WITH that region's DST rules — so non-European users get the
* right time year-round instead of being stuck on the CET base + EU DST dates.
* Default 0 == "Europe (CET/CEST)" (unchanged behaviour). The LAST index is
* "Custom (UTC offset)", which uses touchPrefsGetTimeOffsetHours() as a fixed,
* no-DST offset. touchPrefsBuildLocalTz() resolves the selected zone. */
int touchPrefsTimezoneCount(); // number of pickable entries
const char* touchPrefsTimezoneLabel(int idx); // display label for the picker
uint8_t touchPrefsGetTimezone(); // selected index (default 0)
void touchPrefsSetTimezone(uint8_t idx);
/** Build the POSIX TZ string for local-time display from the selected time zone
* (touchPrefsGetTimezone), or a fixed UTC offset for the "Custom" entry. Used by
* both the boot-time setenv and the Wi-Fi NTP sync so it's honoured everywhere.
* `out` is always null-terminated when out_cap > 0. */
void touchPrefsBuildLocalTz(char* out, int out_cap);
+43 -1
View File
@@ -399,8 +399,14 @@ void setup() {
* happens later in loop(); this just inits the stack.) */
if (want_wifi) {
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(false);
WiFi.setAutoReconnect(true); // let esp_wifi rejoin on its own after a beacon loss / AP reboot
// (was false — the 10 s poll below was the ONLY recovery, and a
// bare begin() on a wedged supplicant is a no-op -> never reconnected)
WiFi.persistent(false);
// NOTE: do NOT enable modem-sleep here. On a fresh, *unassociated* STA (the
// setup wizard, no creds yet) DTIM modem-sleep naps the radio through the
// scan dwell, so WiFi.scanNetworks() comes back empty ("no networks found").
// It's enabled once we actually associate — see the GOT_IP handler below.
}
#if defined(BLE_PIN_CODE)
/* Always stash the BLE params so the toggle can bring BLE up live later, even
@@ -553,6 +559,10 @@ void loop() {
wifiConfigGetSsid(ssid, sizeof(ssid));
wifiConfigGetPwd(pwd, sizeof(pwd));
if (strlen(ssid) > 0) {
// A bare begin() on a supplicant wedged after a silent drop (AP reboot /
// beacon loss) can be a no-op — clear its state first so this forces a
// fresh association. Backs up setAutoReconnect(true) for the stuck case.
WiFi.disconnect(false, true);
WiFi.begin(ssid, pwd[0] ? pwd : nullptr);
}
}
@@ -563,6 +573,12 @@ void loop() {
static bool sntp_pushed = false;
static uint32_t sntp_kick_ms = 0;
if (WiFi.status() == WL_CONNECTED) {
// Now that we're associated, enable DTIM modem-sleep (saves power + gives
// BLE coexistence airtime). Deferred to here on purpose: enabling it on the
// unassociated STA naps the radio through a scan dwell and breaks the setup
// wizard's WiFi.scanNetworks() ("no networks found"). One-shot.
static bool modem_sleep_set = false;
if (!modem_sleep_set) { WiFi.setSleep(true); modem_sleep_set = true; }
if (!sntp_kicked) {
/* Brussels timezone with DST rules baked in (POSIX "CET-1CEST,...").
* On touch builds the base is shifted by the user's manual hour offset
@@ -605,6 +621,32 @@ void loop() {
#endif
the_mesh.loop();
sensors.loop();
#if defined(ESP32)
// GPS time guard (Ricky Leong's "stuck at 1902"): MicroNMEALocationProvider
// sets the mesh RTC from a GPS *position* fix even before the date fields are
// valid (getYear()==0 -> ~1902-10-11), and re-syncs every 30 min — so it
// periodically clobbers a good time. A 1902 clock stamps our adverts as
// decades old and every other node rejects them as stale.
//
// On the T-Deck the mesh RTC, NTP and GPS ALL share the one ESP32 system clock
// (ESP32RTCClock == settimeofday/gettimeofday), so we can't recover by
// "re-reading NTP" — it was already overwritten. Instead keep a millis()-
// anchored copy of the last good time and rebuild from it whenever the clock
// reads garbage, undoing the clobber before the next the_mesh.loop() sends an
// advert. The anchor refreshes every loop while the clock is sane, so the
// rebuilt time is accurate to the second.
{
static uint32_t good_epoch = 0, good_millis = 0;
const uint32_t live = rtc_clock.getCurrentTime();
if (live > 1700000000UL) { // clock sane -> remember it (anchor)
good_epoch = live;
good_millis = millis();
} else if (good_epoch != 0) { // clock went bad -> rebuild from anchor
const uint32_t rebuilt = good_epoch + (uint32_t)((millis() - good_millis) / 1000UL);
rtc_clock.setCurrentTime(rebuilt);
}
}
#endif
rtc_clock.tick();
// (1.16) sleep when there's no pending work — nRF power saving
+225
View File
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "SnakeGame.h"
#include <Arduino.h> // random()
#include <string.h> // memmove
#include <LvglPsramAlloc.h> // PSRAM-preferred buffer for the canvas
// Mirrors the app's status-bar height; the overlay starts below it so the bar
// stays visible. Kept local so this module needs nothing from UITask.
static constexpr lv_coord_t kTopBar = 22;
static constexpr uint32_t kStepMs = 300; // tick period (a touch slower than before)
// Palette (hex literals so the module is self-contained).
static constexpr uint32_t kColBg = 0x0A0B0C; // playfield background
static constexpr uint32_t kColFood = 0xE0584C; // food
static constexpr uint32_t kColBody = 0x15B6A6; // snake body (brand teal)
static constexpr uint32_t kColText = 0xE6EAEE;
SnakeGame* SnakeGame::s_active = nullptr;
bool SnakeGame::isOpen() { return s_active != nullptr && s_active->root_ != nullptr; }
void SnakeGame::steer(int dx, int dy) {
if (!s_active || !s_active->started_ || s_active->over_) return;
// Trackball deltas -> one cardinal direction (dominant axis).
if (dx == 0 && dy == 0) return;
const int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
if (adx >= ady) s_active->setDir(dx > 0 ? 1 : -1, 0);
else s_active->setDir(0, dy > 0 ? 1 : -1);
}
void SnakeGame::launch() {
if (s_active) return;
SnakeGame* g = new SnakeGame();
s_active = g;
if (!g->open()) { s_active = nullptr; delete g; }
}
void SnakeGame::placeFood() {
const int cells = cols_ * rows_;
for (int tries = 0; tries < 4 * cells + 16; ++tries) {
const uint8_t fx = (uint8_t)random(cols_);
const uint8_t fy = (uint8_t)random(rows_);
bool on = false;
for (int i = 0; i < len_; ++i) if (bx_[i] == fx && by_[i] == fy) { on = true; break; }
if (!on) { fx_ = fx; fy_ = fy; return; }
}
}
void SnakeGame::reset() {
len_ = 3;
const uint8_t cx = (uint8_t)(cols_ / 2), cy = (uint8_t)(rows_ / 2);
for (int i = 0; i < len_; ++i) { bx_[i] = (uint8_t)(cx - i); by_[i] = cy; }
dx_ = 1; dy_ = 0; ndx_ = 1; ndy_ = 0;
over_ = false; score_val_ = 0;
placeFood();
}
void SnakeGame::render() {
if (!canvas_) return;
const int c = kCellPx;
lv_canvas_fill_bg(canvas_, lv_color_hex(kColBg), LV_OPA_COVER);
lv_draw_rect_dsc_t d; lv_draw_rect_dsc_init(&d); d.radius = 2;
d.bg_color = lv_color_hex(kColFood);
lv_canvas_draw_rect(canvas_, fx_ * c + 1, fy_ * c + 1, c - 2, c - 2, &d);
for (int i = 0; i < len_; ++i) {
d.bg_color = lv_color_hex(kColBody);
d.bg_opa = (i == 0) ? LV_OPA_COVER : LV_OPA_80; // head reads brighter
lv_canvas_draw_rect(canvas_, bx_[i] * c + 1, by_[i] * c + 1, c - 2, c - 2, &d);
}
}
void SnakeGame::updateScoreLabel() {
if (!score_) return;
if (over_) lv_label_set_text_fmt(score_, "Game over \xe2\x80\x94 score %d (tap to restart)", score_val_);
else if (paused_) lv_label_set_text_fmt(score_, "Paused \xe2\x80\x94 score %d", score_val_);
else if (started_) lv_label_set_text_fmt(score_, "score %d", score_val_);
else lv_label_set_text(score_, "Snake \xe2\x80\x94 swipe or roll the trackball");
}
void SnakeGame::step() {
if (over_ || !started_ || paused_) return;
if (!(ndx_ == -dx_ && ndy_ == -dy_)) { dx_ = ndx_; dy_ = ndy_; }
const int nx = bx_[0] + dx_, ny = by_[0] + dy_;
const bool eat = (nx == fx_ && ny == fy_);
bool over = (nx < 0 || ny < 0 || nx >= cols_ || ny >= rows_);
const int clen = eat ? len_ : len_ - 1; // the tail vacates unless we grow
for (int i = 0; i < clen && !over; ++i) if (bx_[i] == nx && by_[i] == ny) over = true;
if (over) { over_ = true; updateScoreLabel(); return; }
int newlen = len_ + (eat ? 1 : 0);
if (newlen > cols_ * rows_) newlen = cols_ * rows_;
memmove(&bx_[1], &bx_[0], (size_t)(newlen - 1));
memmove(&by_[1], &by_[0], (size_t)(newlen - 1));
bx_[0] = (uint8_t)nx; by_[0] = (uint8_t)ny;
len_ = newlen;
if (eat) { score_val_++; placeFood(); updateScoreLabel(); }
render();
}
void SnakeGame::setDir(int dx, int dy) { ndx_ = dx; ndy_ = dy; }
void SnakeGame::startGame() {
if (started_) return;
started_ = true;
paused_ = false;
if (start_btn_) { lv_obj_del(start_btn_); start_btn_ = nullptr; }
if (pause_btn_) { // reveal the pause toggle now that we're playing
lv_obj_clear_flag(pause_btn_, LV_OBJ_FLAG_HIDDEN);
if (pause_lbl_) lv_label_set_text(pause_lbl_, LV_SYMBOL_PAUSE);
}
reset();
updateScoreLabel();
render();
if (!timer_) timer_ = lv_timer_create(timerCb, kStepMs, this);
}
void SnakeGame::togglePause() {
if (!started_ || over_) return;
paused_ = !paused_;
if (pause_lbl_) lv_label_set_text(pause_lbl_, paused_ ? LV_SYMBOL_PLAY : LV_SYMBOL_PAUSE);
updateScoreLabel();
}
bool SnakeGame::open() {
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
const lv_coord_t sh = lv_disp_get_ver_res(nullptr);
root_ = lv_obj_create(lv_layer_top());
lv_obj_remove_style_all(root_);
lv_obj_set_size(root_, sw, sh - kTopBar);
lv_obj_set_pos(root_, 0, kTopBar);
lv_obj_set_style_bg_color(root_, lv_color_hex(0x0E1216), LV_PART_MAIN);
lv_obj_set_style_bg_opa(root_, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_clear_flag(root_, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(root_, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(root_, gestureCb, LV_EVENT_GESTURE, this);
lv_obj_add_event_cb(root_, tapCb, LV_EVENT_CLICKED, this);
// Fill the unoccupied screen below a 24-px score/close row. Whole cells only.
const int avail_w = (int)sw;
const int avail_h = (int)(sh - kTopBar) - 24;
cols_ = avail_w / kCellPx; if (cols_ > kMaxCols) cols_ = kMaxCols; if (cols_ < 6) cols_ = 6;
rows_ = avail_h / kCellPx; if (rows_ > kMaxRows) rows_ = kMaxRows; if (rows_ < 6) rows_ = 6;
const int pw = cols_ * kCellPx, ph = rows_ * kCellPx;
buf_ = (lv_color_t*)lvglPsramAlloc((size_t)pw * ph * sizeof(lv_color_t));
if (!buf_) return false;
canvas_ = lv_canvas_create(root_);
lv_canvas_set_buffer(canvas_, buf_, pw, ph, LV_IMG_CF_TRUE_COLOR);
lv_obj_align(canvas_, LV_ALIGN_BOTTOM_MID, 0, 0); // playfield fills the lower area
score_ = lv_label_create(root_);
lv_obj_set_style_text_color(score_, lv_color_hex(kColText), LV_PART_MAIN);
lv_obj_align(score_, LV_ALIGN_TOP_LEFT, 6, 4);
lv_obj_t* x = lv_btn_create(root_);
lv_obj_set_size(x, 30, 24);
lv_obj_align(x, LV_ALIGN_TOP_RIGHT, -4, 0);
lv_obj_add_event_cb(x, closeCb, LV_EVENT_CLICKED, this);
lv_obj_t* xl = lv_label_create(x); lv_label_set_text(xl, LV_SYMBOL_CLOSE); lv_obj_center(xl);
// Pause/resume toggle, left of the X. Hidden until the game starts.
pause_btn_ = lv_btn_create(root_);
lv_obj_set_size(pause_btn_, 30, 24);
lv_obj_align(pause_btn_, LV_ALIGN_TOP_RIGHT, -40, 0);
lv_obj_add_event_cb(pause_btn_, pauseCb, LV_EVENT_CLICKED, this);
pause_lbl_ = lv_label_create(pause_btn_); lv_label_set_text(pause_lbl_, LV_SYMBOL_PAUSE); lv_obj_center(pause_lbl_);
lv_obj_add_flag(pause_btn_, LV_OBJ_FLAG_HIDDEN);
// Paused: draw the initial snake, then wait on a centred "New game" button.
reset();
updateScoreLabel();
render();
start_btn_ = lv_btn_create(root_);
lv_obj_set_size(start_btn_, 150, 44);
lv_obj_align(start_btn_, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(start_btn_, startCb, LV_EVENT_CLICKED, this);
lv_obj_t* sl = lv_label_create(start_btn_);
lv_label_set_text(sl, LV_SYMBOL_PLAY " New game");
lv_obj_center(sl);
return true;
}
void SnakeGame::close() {
if (timer_) { lv_timer_del(timer_); timer_ = nullptr; }
if (root_) { lv_obj_del(root_); root_ = nullptr; } // deletes canvas + button children
if (buf_) { lvglPsramFree(buf_); buf_ = nullptr; } // free AFTER the canvas is gone
canvas_ = nullptr; score_ = nullptr; start_btn_ = nullptr;
pause_btn_ = nullptr; pause_lbl_ = nullptr;
}
// ---- LVGL C-callback trampolines (user_data = the instance) ----
void SnakeGame::timerCb(lv_timer_t* t) {
auto* self = static_cast<SnakeGame*>(t->user_data);
if (self) self->step();
}
void SnakeGame::gestureCb(lv_event_t* e) {
auto* self = static_cast<SnakeGame*>(lv_event_get_user_data(e));
if (!self || !self->started_) return;
switch (lv_indev_get_gesture_dir(lv_indev_get_act())) {
case LV_DIR_TOP: self->setDir(0, -1); break;
case LV_DIR_BOTTOM: self->setDir(0, 1); break;
case LV_DIR_LEFT: self->setDir(-1, 0); break;
case LV_DIR_RIGHT: self->setDir(1, 0); break;
default: break;
}
}
void SnakeGame::tapCb(lv_event_t* e) {
auto* self = static_cast<SnakeGame*>(lv_event_get_user_data(e));
if (self && self->started_ && self->over_) { self->reset(); self->updateScoreLabel(); self->render(); }
}
void SnakeGame::startCb(lv_event_t* e) {
auto* self = static_cast<SnakeGame*>(lv_event_get_user_data(e));
if (self) self->startGame();
}
void SnakeGame::pauseCb(lv_event_t* e) {
auto* self = static_cast<SnakeGame*>(lv_event_get_user_data(e));
if (self) self->togglePause();
}
void SnakeGame::closeCb(lv_event_t* e) {
auto* self = static_cast<SnakeGame*>(lv_event_get_user_data(e));
if (!self) return;
self->close();
if (s_active == self) s_active = nullptr;
delete self;
}
+67
View File
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <lvgl.h>
// Self-contained Snake mini-game launched from the Apps drawer.
//
// Owns a full-screen overlay on lv_layer_top: a canvas playfield filling the
// unoccupied screen, a score line, and a close button. The game waits on a
// "New game" button (doesn't start moving on open). Steer by swipe OR trackball
// (UITask routes the trackball here via isOpen()/steer() and hides the cursor);
// tap to restart after a game over; the X closes it. One instance at a time.
//
// Decoupled from UITask internals — depends only on LVGL and lvglPsramAlloc.
class SnakeGame {
public:
static void launch(); // open (no-op if already open)
static bool isOpen(); // UITask: gate the trackball + tab bar
static void steer(int dx, int dy); // UITask: trackball motion -> direction
private:
// The playfield grid is sized to the screen at open() (cols_ x rows_), capped
// by these maxima so the body arrays are fixed-size.
static constexpr int kCellPx = 14; // px per cell
static constexpr int kMaxCols = 40;
static constexpr int kMaxRows = 30;
static constexpr int kMaxCells = kMaxCols * kMaxRows;
lv_obj_t* root_ = nullptr; // full-screen overlay (owns everything below)
lv_obj_t* canvas_ = nullptr;
lv_color_t* buf_ = nullptr; // canvas pixel buffer (PSRAM)
lv_obj_t* score_ = nullptr;
lv_obj_t* start_btn_ = nullptr; // "New game" gate (deleted once tapped)
lv_obj_t* pause_btn_ = nullptr; // pause/resume toggle (shown once playing)
lv_obj_t* pause_lbl_ = nullptr; // its glyph (play <-> pause)
lv_timer_t* timer_ = nullptr;
int cols_ = 14, rows_ = 14; // grid dimensions (set at open)
bool started_ = false; // false until "New game" is tapped
bool paused_ = false; // pause toggle (timer keeps ticking; step() no-ops)
uint8_t bx_[kMaxCells], by_[kMaxCells]; // snake body, [0] = head
int len_ = 0;
int dx_ = 1, dy_ = 0; // active direction
int ndx_ = 1, ndy_ = 0; // queued direction (applied next step)
uint8_t fx_ = 0, fy_ = 0; // food cell
bool over_ = false;
int score_val_ = 0;
bool open(); // build UI (paused); false on alloc failure
void startGame(); // begin the tick (from the New-game button)
void togglePause(); // pause/resume the running game
void close();
void reset();
void placeFood();
void render();
void step();
void setDir(int dx, int dy);
void updateScoreLabel();
static SnakeGame* s_active; // the one live game (or nullptr)
static void timerCb(lv_timer_t* t);
static void gestureCb(lv_event_t* e);
static void tapCb(lv_event_t* e);
static void closeCb(lv_event_t* e);
static void startCb(lv_event_t* e);
static void pauseCb(lv_event_t* e);
};
+3781 -413
View File
File diff suppressed because it is too large. Load diff
+17 -7
View File
@@ -29,7 +29,7 @@ enum class TouchUiScreen : uint8_t { Home = 0, ChatInbox = 1, Contacts = 2, Sett
class UITask : public AbstractUITask {
public:
static const int MAX_UI_MESSAGES = 96;
static const int MAX_UI_MESSAGES = 500;
static const int MAX_UI_THREADS = 48;
static const int MAX_THREAD_NAME = 32;
static const int MAX_SENDER_NAME = 24;
@@ -160,8 +160,10 @@ private:
TouchUiScreen _touch_screen;
bool _active_dm_contact_set;
uint8_t _active_dm_contact_pub[32];
bool _history_dirty;
unsigned long _next_history_flush_ms;
bool _threads_dirty;
unsigned long _next_threads_flush_ms;
bool _msgs_dirty;
unsigned long _next_msgs_flush_ms;
void userLedHandler();
@@ -213,10 +215,15 @@ private:
void appendComposerText(const char* text);
void backspaceComposerChar();
bool sendComposerToActiveThread();
void markHistoryDirty(unsigned long delay_ms = 600);
void markThreadsDirty(unsigned long delay_ms = 200);
void markMsgsDirty(unsigned long delay_ms = 2000);
void flushHistoryIfDue(unsigned long now);
bool loadHistoryFromStorage();
bool saveHistoryToStorage();
bool loadThreadsFromStorage();
bool loadMsgsFromStorage();
bool loadLegacyHistoryFromStorage();
bool saveThreadsToStorage();
bool saveMsgsToStorage();
void setCurrScreen(UIScreen* c);
@@ -230,8 +237,10 @@ public:
_next_mesh_thread_refresh = 0;
_active_dm_contact_set = false;
memset(_active_dm_contact_pub, 0, sizeof(_active_dm_contact_pub));
_history_dirty = false;
_next_history_flush_ms = 0;
_threads_dirty = false;
_next_threads_flush_ms = 0;
_msgs_dirty = false;
_next_msgs_flush_ms = 0;
_last_input_ms = 0;
_screen_timeout_ms = 20000; // overridden by NVS in begin()
_screen_off = false;
@@ -255,6 +264,7 @@ public:
int getUnreadTotal() const;
int getUnreadMentionCount() const; // # of threads with an unread @mention of me
void markThreadRead(int idx); // clear one thread's unread count (persisted)
void markActiveThreadRead(); // clear the currently-open thread's unread (viewing == read)
void markAllThreadsRead(); // clear every thread's unread count
bool threadHasMention(int idx) const; // unread @mention of me in this thread
int getThreadCount(bool channel_mode, int out_indexes[], int max_out) const;
+27 -4
View File
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 16 px
* Bpp: 4
* Opts: --no-compress --bpp 4 --size 16 --font /tmp/fa/fa-solid-900.ttf -r 0xF007 -r 0xF519 --format lvgl -o src/ui-touch/person_font.c
* Opts: --no-compress --bpp 4 --size 16 --font /tmp/fa/fa-solid-900.ttf -r 0xF007 -r 0xF519 -r 0xF0C0 --format lvgl -o src/ui-touch/person_font.c
******************************************************************************/
#include "lvgl.h"
@@ -34,6 +34,28 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0xff, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xfe, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
/* U+F0C0 "" */
0x0, 0x1c, 0xfc, 0x10, 0x0, 0x0, 0x0, 0x7e,
0xe7, 0x0, 0x0, 0xbf, 0xff, 0xb0, 0x0, 0x0,
0x3, 0xff, 0xff, 0x30, 0x0, 0xff, 0xff, 0xf0,
0x0, 0x0, 0x7, 0xff, 0xff, 0x70, 0x0, 0xbf,
0xff, 0xb0, 0x0, 0x0, 0x3, 0xff, 0xff, 0x30,
0x0, 0x1c, 0xfc, 0x10, 0x8e, 0xe8, 0x0, 0x7e,
0xe7, 0x0, 0x0, 0x0, 0x0, 0x8, 0xff, 0xff,
0x80, 0x0, 0x0, 0x0, 0x6, 0xdf, 0xfd, 0xe,
0xff, 0xff, 0xe0, 0xdf, 0xfd, 0x60, 0x6f, 0xff,
0xff, 0xe, 0xff, 0xff, 0xe1, 0xff, 0xff, 0xf6,
0xdf, 0xff, 0xff, 0x58, 0xff, 0xff, 0x85, 0xff,
0xff, 0xfd, 0xdf, 0xff, 0xff, 0xd1, 0x8e, 0xe8,
0x1d, 0xff, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x1, 0xae, 0xff, 0xff, 0xea, 0x10, 0x0, 0x0,
0x0, 0x0, 0x1e, 0xff, 0xff, 0xff, 0xff, 0xe1,
0x0, 0x0, 0x0, 0x0, 0xaf, 0xff, 0xff, 0xff,
0xff, 0xfa, 0x0, 0x0, 0x0, 0x0, 0xef, 0xff,
0xff, 0xff, 0xff, 0xfe, 0x0, 0x0, 0x0, 0x0,
0xcf, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0, 0x0,
/* U+F519 "" */
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x3e, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0,
@@ -65,7 +87,8 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,
{.bitmap_index = 0, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 112, .adv_w = 288, .box_w = 18, .box_h = 17, .ofs_x = 0, .ofs_y = -2}
{.bitmap_index = 112, .adv_w = 320, .box_w = 20, .box_h = 16, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 272, .adv_w = 288, .box_w = 18, .box_h = 17, .ofs_x = 0, .ofs_y = -2}
};
/*---------------------
@@ -73,7 +96,7 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
*--------------------*/
static const uint16_t unicode_list_0[] = {
0x0, 0x512
0x0, 0xb9, 0x512
};
/*Collect the unicode lists and glyph_id offsets*/
@@ -81,7 +104,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
{
.range_start = 61447, .range_length = 1299, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 2, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 3, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
}
};
+165
View File
@@ -0,0 +1,165 @@
/*******************************************************************************
* Size: 14 px
* Bpp: 4
* Opts: --no-compress --bpp 4 --size 14 --font /tmp/fa/fa-solid-900.ttf -r 0xF007 -r 0xF519 -r 0xF0C0 --format lvgl -o src/ui-touch/person_font14.c
******************************************************************************/
#include "lvgl.h"
#ifndef PERSON_FONT14
#define PERSON_FONT14 1
#endif
#if PERSON_FONT14
/*-----------------
* BITMAPS
*----------------*/
/*Store the image of the glyphs*/
static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
/* U+F007 "" */
0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0,
0x1b, 0xff, 0xd3, 0x0, 0x0, 0x0, 0xc, 0xff,
0xff, 0xe1, 0x0, 0x0, 0x3, 0xff, 0xff, 0xff,
0x70, 0x0, 0x0, 0x5f, 0xff, 0xff, 0xf9, 0x0,
0x0, 0x2, 0xff, 0xff, 0xff, 0x60, 0x0, 0x0,
0x9, 0xff, 0xff, 0xc0, 0x0, 0x0, 0x0, 0x6,
0xcc, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x5, 0xcf, 0xff, 0xfd, 0x70,
0x0, 0x9, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x5,
0xff, 0xff, 0xff, 0xff, 0xff, 0x90, 0xcf, 0xff,
0xff, 0xff, 0xff, 0xff, 0xf, 0xff, 0xff, 0xff,
0xff, 0xff, 0xf3, 0x8b, 0xbb, 0xbb, 0xbb, 0xbb,
0xba, 0x0,
/* U+F0C0 "" */
0x0, 0x2, 0x20, 0x0, 0x0, 0x0, 0x2, 0x20,
0x0, 0x0, 0xaf, 0xf8, 0x0, 0x0, 0x0, 0x9f,
0xf9, 0x0, 0x2, 0xff, 0xff, 0x0, 0x0, 0x1,
0xff, 0xff, 0x10, 0x2, 0xff, 0xff, 0x0, 0x0,
0x1, 0xff, 0xff, 0x10, 0x0, 0x8f, 0xf6, 0x6,
0xb9, 0x20, 0x7f, 0xf7, 0x0, 0x0, 0x0, 0x0,
0x6f, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x9, 0xef,
0xf3, 0xdf, 0xff, 0xf5, 0xaf, 0xfc, 0x40, 0x9f,
0xff, 0xf5, 0xcf, 0xff, 0xf4, 0xdf, 0xff, 0xf1,
0xef, 0xff, 0xfa, 0x3f, 0xff, 0xb3, 0xff, 0xff,
0xf6, 0x68, 0x88, 0x87, 0x12, 0x75, 0x5, 0x88,
0x88, 0x72, 0x0, 0x0, 0x0, 0x46, 0x66, 0x62,
0x0, 0x0, 0x0, 0x0, 0x0, 0x4e, 0xff, 0xff,
0xff, 0xa0, 0x0, 0x0, 0x0, 0x1, 0xef, 0xff,
0xff, 0xff, 0xf8, 0x0, 0x0, 0x0, 0x5, 0xff,
0xff, 0xff, 0xff, 0xfd, 0x0, 0x0, 0x0, 0x5,
0xff, 0xff, 0xff, 0xff, 0xfc, 0x0, 0x0,
/* U+F519 "" */
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20,
0x7f, 0x23, 0x10, 0x0, 0x0, 0x2, 0x16, 0xf3,
0xcf, 0x1f, 0xa0, 0x4, 0x30, 0xe, 0xc4, 0xf8,
0xed, 0x4f, 0x70, 0x9f, 0xf5, 0xb, 0xf1, 0xfa,
0xfc, 0x5f, 0x60, 0xcf, 0xf8, 0xa, 0xf1, 0xfb,
0xee, 0x3f, 0x90, 0x5f, 0xe2, 0xd, 0xf2, 0xf9,
0xaf, 0x2a, 0x70, 0xf, 0xc0, 0xa, 0x76, 0xf6,
0x2a, 0x0, 0x0, 0xf, 0xc0, 0x0, 0x3, 0xa0,
0x0, 0x0, 0x0, 0xf, 0xc0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0xf, 0xc0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0xf, 0xc0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0xf, 0xc0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0xf, 0xc0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0xf, 0xb0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0xa, 0x60, 0x0, 0x0, 0x0
};
/*---------------------
* GLYPH DESCRIPTION
*--------------------*/
static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,
{.bitmap_index = 0, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 98, .adv_w = 280, .box_w = 18, .box_h = 15, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 233, .adv_w = 252, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = -2}
};
/*---------------------
* CHARACTER MAPPING
*--------------------*/
static const uint16_t unicode_list_0[] = {
0x0, 0xb9, 0x512
};
/*Collect the unicode lists and glyph_id offsets*/
static const lv_font_fmt_txt_cmap_t cmaps[] =
{
{
.range_start = 61447, .range_length = 1299, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 3, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
}
};
/*--------------------
* ALL CUSTOM DATA
*--------------------*/
#if LVGL_VERSION_MAJOR == 8
/*Store all the custom data of the font*/
static lv_font_fmt_txt_glyph_cache_t cache;
#endif
#if LVGL_VERSION_MAJOR >= 8
static const lv_font_fmt_txt_dsc_t font_dsc = {
#else
static lv_font_fmt_txt_dsc_t font_dsc = {
#endif
.glyph_bitmap = glyph_bitmap,
.glyph_dsc = glyph_dsc,
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
.cmap_num = 1,
.bpp = 4,
.kern_classes = 0,
.bitmap_format = 0,
#if LVGL_VERSION_MAJOR == 8
.cache = &cache
#endif
};
/*-----------------
* PUBLIC FONT
*----------------*/
/*Initialize a public general font descriptor*/
#if LVGL_VERSION_MAJOR >= 8
const lv_font_t person_font14 = {
#else
lv_font_t person_font14 = {
#endif
.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
.line_height = 15, /*The maximum line height required by the font*/
.base_line = 2, /*Baseline measured from the bottom of the line*/
#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0)
.subpx = LV_FONT_SUBPX_NONE,
#endif
#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8
.underline_position = -1,
.underline_thickness = 1,
#endif
.dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
.fallback = NULL,
#endif
.user_data = NULL,
};
#endif /*#if PERSON_FONT14*/
+128
View File
@@ -0,0 +1,128 @@
/*******************************************************************************
* Size: 16 px
* Bpp: 4
* Opts: --no-compress --bpp 4 --size 16 --font /Users/dan.vybiral/.claude/jobs/dadc5d4b/tmp/fa-solid-900.ttf -r 0xF002 --format lvgl --lv-font-name zoom_font -o src/ui-touch/zoom_font.c
******************************************************************************/
#include "lvgl.h"
#ifndef ZOOM_FONT
#define ZOOM_FONT 1
#endif
#if ZOOM_FONT
/*-----------------
* BITMAPS
*----------------*/
/*Store the image of the glyphs*/
static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
/* U+F002 "" */
0x0, 0x3, 0xae, 0xfe, 0xa3, 0x0, 0x0, 0x0,
0x0, 0x9f, 0xff, 0xff, 0xff, 0x90, 0x0, 0x0,
0x9, 0xff, 0x82, 0x2, 0x8f, 0xf9, 0x0, 0x0,
0x3f, 0xf3, 0x0, 0x0, 0x3, 0xff, 0x30, 0x0,
0xaf, 0x80, 0x0, 0x0, 0x0, 0x8f, 0xa0, 0x0,
0xef, 0x20, 0x0, 0x0, 0x0, 0x2f, 0xe0, 0x0,
0xff, 0x0, 0x0, 0x0, 0x0, 0xf, 0xf0, 0x0,
0xef, 0x20, 0x0, 0x0, 0x0, 0x2f, 0xe0, 0x0,
0xaf, 0x80, 0x0, 0x0, 0x0, 0x8f, 0xa0, 0x0,
0x3f, 0xf3, 0x0, 0x0, 0x3, 0xff, 0x40, 0x0,
0x9, 0xff, 0x82, 0x2, 0x8f, 0xfe, 0x10, 0x0,
0x0, 0x9f, 0xff, 0xff, 0xff, 0xef, 0xd1, 0x0,
0x0, 0x3, 0xae, 0xfe, 0xa4, 0x1d, 0xfd, 0x10,
0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xdf, 0xd1,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1d, 0xfc,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xcc
};
/*---------------------
* GLYPH DESCRIPTION
*--------------------*/
static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */,
{.bitmap_index = 0, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}
};
/*---------------------
* CHARACTER MAPPING
*--------------------*/
/*Collect the unicode lists and glyph_id offsets*/
static const lv_font_fmt_txt_cmap_t cmaps[] =
{
{
.range_start = 61442, .range_length = 1, .glyph_id_start = 1,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
/*--------------------
* ALL CUSTOM DATA
*--------------------*/
#if LVGL_VERSION_MAJOR == 8
/*Store all the custom data of the font*/
static lv_font_fmt_txt_glyph_cache_t cache;
#endif
#if LVGL_VERSION_MAJOR >= 8
static const lv_font_fmt_txt_dsc_t font_dsc = {
#else
static lv_font_fmt_txt_dsc_t font_dsc = {
#endif
.glyph_bitmap = glyph_bitmap,
.glyph_dsc = glyph_dsc,
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
.cmap_num = 1,
.bpp = 4,
.kern_classes = 0,
.bitmap_format = 0,
#if LVGL_VERSION_MAJOR == 8
.cache = &cache
#endif
};
/*-----------------
* PUBLIC FONT
*----------------*/
/*Initialize a public general font descriptor*/
#if LVGL_VERSION_MAJOR >= 8
const lv_font_t zoom_font = {
#else
lv_font_t zoom_font = {
#endif
.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
.line_height = 16, /*The maximum line height required by the font*/
.base_line = 2, /*Baseline measured from the bottom of the line*/
#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0)
.subpx = LV_FONT_SUBPX_NONE,
#endif
#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8
.underline_position = -1,
.underline_thickness = 1,
#endif
.dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
.fallback = NULL,
#endif
.user_data = NULL,
};
#endif /*#if ZOOM_FONT*/