Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a63ffe86c | ||
|
|
2aa8f59410 | ||
|
|
c8b451f14f | ||
|
|
9e567e1789 | ||
|
|
bbcafa3088 | ||
|
|
b3af35666b | ||
|
|
eca5281f22 | ||
|
|
3a70a8ab0f | ||
|
|
2894776a11 | ||
|
|
88a9d386f1 | ||
|
|
a6647dcf55 | ||
|
|
28879f55c4 | ||
|
|
d16c833171 | ||
|
|
031cb03bd4 | ||
|
|
5ec9176906 | ||
|
|
ed9cf619ec | ||
|
|
460b82f9bc | ||
|
|
0a4d8d5b2f | ||
|
|
e72a1b1bd5 | ||
|
|
30fe6fbd6f | ||
|
|
5e8dd55847 | ||
|
|
26ae08101c | ||
|
|
aa89572631 | ||
|
|
ff5818f7f7 | ||
|
|
be7fb32f23 | ||
|
|
17981029a7 | ||
|
|
9bc9822111 | ||
|
|
799a3754d0 | ||
|
|
c9e3de15d4 | ||
|
|
be1c72fbb1 | ||
|
|
b478851a21 | ||
|
|
8ce6baf786 | ||
|
|
b82f4a2363 | ||
|
|
6e62d35328 | ||
|
|
fe8925f91d | ||
|
|
7463530f63 | ||
|
|
d7151dbbf8 | ||
|
|
dd67336fdb | ||
|
|
13b240062a | ||
|
|
9ddb08ba93 | ||
|
|
373be3e546 | ||
|
|
d2e776afe9 | ||
|
|
a5968afb70 | ||
|
|
7aeee1f456 | ||
|
|
ed72f15a81 | ||
|
|
59896b6bdb | ||
|
|
5ff7590168 | ||
|
|
00f2f70800 | ||
|
|
fdaf34575e | ||
|
|
bb95aed673 | ||
|
|
e9ece7bf6b | ||
|
|
976bf4022c | ||
|
|
b85e0a4849 | ||
|
|
e7d6c73d16 |
No files matched your search
@@ -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/
|
||||
@@ -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 T-Deck</b> and <b>Heltec 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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+17
-7
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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*/
|
||||
|
||||
@@ -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*/
|
||||
|
||||
Reference in new issue
Block a user