Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c931df6054 | ||
|
|
11b31e1cdd | ||
|
|
c16b5230a6 | ||
|
|
f5362b1cb3 | ||
|
|
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 |
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/
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the Mesh America Configurator catalog JSON for wadamesh.
|
||||
|
||||
Mesh America (apps.meshamerica.com — the flasher Cascadia Mesh uses) merges a
|
||||
provider "catalog" JSON into its device list. Spec: third-party hosts ONE static
|
||||
JSON (this file's output) at a stable HTTPS+CORS URL, plus the firmware bins over
|
||||
HTTPS+CORS; their app reads it live. See provider-api-spec.md from TJ Downes.
|
||||
|
||||
We host:
|
||||
- the bins on firmware.wadamesh.com/releases/TOUCH/<TAG>/ (Cloudflare HTTPS +
|
||||
`Access-Control-Allow-Origin: *`, immutable per-tag dir — see deploy/nginx/
|
||||
firmware.wadamesh.com.conf), and
|
||||
- this catalog at a stable HTTPS+CORS URL (deployed alongside the firmware).
|
||||
|
||||
Usage: python3 deploy/gen-meshamerica-catalog.py <TAG> > meshamerica-catalog.json
|
||||
(TAG defaults to the newest beta_N dir; notes come from release-notes/<TAG>.txt)
|
||||
|
||||
Re-run it each release (or wire it into scripts/release.sh) so the catalog always
|
||||
points at the newest immutable bins — the spec wants firmware URLs treated as
|
||||
immutable, so each release adds a fresh versioned URL.
|
||||
"""
|
||||
import sys, json, os, glob, re
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def newest_tag():
|
||||
tags = []
|
||||
for p in glob.glob(os.path.join(REPO, "release-notes", "beta_*.txt")):
|
||||
m = re.search(r"beta_(\d+)", os.path.basename(p))
|
||||
if m:
|
||||
tags.append(int(m.group(1)))
|
||||
return "beta_%d" % max(tags) if tags else "beta_9"
|
||||
|
||||
TAG = sys.argv[1] if len(sys.argv) > 1 else newest_tag()
|
||||
BASE = "https://firmware.wadamesh.com/releases/TOUCH/%s" % TAG
|
||||
ICON = "https://wadamesh.com/wadamesh-icon-512.png"
|
||||
|
||||
# "What's new" — one user-facing line per non-blank, non-# line of the notes file.
|
||||
notes = ""
|
||||
nf = os.path.join(REPO, "release-notes", "%s.txt" % TAG)
|
||||
if os.path.exists(nf):
|
||||
notes = "\n".join(l.strip() for l in open(nf, encoding="utf-8")
|
||||
if l.strip() and not l.strip().startswith("#"))
|
||||
|
||||
# Both touch boards are ESP32-S3, single touch-UI ("gui") role. ota_0 sits at
|
||||
# 0x10000 on both (partitions_t*_touch.csv), so flash-update@0x10000 is correct.
|
||||
BOARDS = [
|
||||
{"name": "wadamesh — Heltec V4 TFT", "slug": "heltec-v4-tft"},
|
||||
{"name": "wadamesh — LilyGo T-Deck", "slug": "tdeck"},
|
||||
]
|
||||
|
||||
def device(b):
|
||||
merged, appbin = "wadamesh-%s-merged.bin" % b["slug"], "wadamesh-%s.bin" % b["slug"]
|
||||
return {
|
||||
"maker": "wadamesh",
|
||||
"class": "wadamesh",
|
||||
"name": b["name"],
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='%s'>" % ICON,
|
||||
"firmware": [{
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {TAG: {
|
||||
"notes": notes,
|
||||
"files": [
|
||||
{"type": "flash-wipe", "name": merged,
|
||||
"url": "%s/%s" % (BASE, merged),
|
||||
"title": "Full install (bootloader + firmware)"},
|
||||
{"type": "flash-update", "name": appbin,
|
||||
"url": "%s/%s" % (BASE, appbin),
|
||||
"title": "Update (app only)"},
|
||||
],
|
||||
}},
|
||||
}],
|
||||
}
|
||||
|
||||
catalog = {
|
||||
"maker": {"wadamesh": {"name": "wadamesh"}},
|
||||
"device": [device(b) for b in BOARDS],
|
||||
}
|
||||
print(json.dumps(catalog, indent=2, ensure_ascii=False))
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"maker": {
|
||||
"wadamesh": {
|
||||
"name": "wadamesh"
|
||||
}
|
||||
},
|
||||
"device": [
|
||||
{
|
||||
"maker": "wadamesh",
|
||||
"class": "wadamesh",
|
||||
"name": "wadamesh — Heltec V4 TFT",
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='https://wadamesh.com/wadamesh-icon-512.png'>",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_10": {
|
||||
"notes": "",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_10/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_10/wadamesh-heltec-v4-tft.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"maker": "wadamesh",
|
||||
"class": "wadamesh",
|
||||
"name": "wadamesh — LilyGo T-Deck",
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='https://wadamesh.com/wadamesh-icon-512.png'>",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_10": {
|
||||
"notes": "",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_10/wadamesh-tdeck-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_10/wadamesh-tdeck.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+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.3
|
||||
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.3
|
||||
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.4
|
||||
SPI
|
||||
Wire
|
||||
jgromes/RadioLib @ ^7.6.0
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# beta_10 — what's new (one user-facing note per non-blank, non-# line)
|
||||
|
||||
# --- T-Deck input ---
|
||||
New trackball mode — switch the T-Deck trackball between a mouse cursor (the default) and D-pad navigation (roll to move the selection, click to choose), under Settings → Keyboard. Handy if you prefer button-style navigation.
|
||||
|
||||
# --- Map ---
|
||||
microSD map tiles now fill in as you explore: with "Use microSD for tiles" on, the device also downloads any missing tiles over Wi-Fi and saves them straight onto the SD card, so your offline library grows instead of showing blank areas past its edge (thanks 1nerdherder, #20)
|
||||
|
||||
# --- Power ---
|
||||
Experimental battery saver (idle power-save): throttles the CPU when the device is parked — screen off, on battery, no app connected and the mesh quiet — to trim idle power use, without ever missing a received packet. Off by default; enable it under Settings → System → Battery. (Dan Vybiral / @Vybo, #19)
|
||||
|
||||
# --- Settings ---
|
||||
New "Battery" section under Settings → System — opens the battery & power-history screen (the same one you get by tapping the battery icon up top) and is where the experimental battery saver now lives (it moved out of the Lock settings).
|
||||
@@ -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)
|
||||
@@ -54,6 +54,28 @@ echo "listing: $(python3 -c 'import json,sys;print(", ".join(x["name"] for x in
|
||||
# frozen one. Notes come from release-notes/<tag>.txt (one per line; optional).
|
||||
python3 "$ROOT/scripts/build/gen-flasher-meta.py" "$TAG" "$LATEST" "$ROOT/release-notes/$TAG.txt"
|
||||
|
||||
# 3c. Mesh America Configurator provider catalog (apps.meshamerica.com — the web
|
||||
# flasher Cascadia Mesh uses). It is served LIVE from raw.githubusercontent
|
||||
# main, so each release must regenerate it for $TAG and push that ONE file to
|
||||
# main; otherwise the configurator keeps offering the previous beta. It points
|
||||
# at the immutable /releases/TOUCH/$TAG/ bins published below.
|
||||
# (Generator: deploy/gen-meshamerica-catalog.py — reads release-notes/$TAG.txt.)
|
||||
CATALOG="$ROOT/deploy/meshamerica-catalog.json"
|
||||
python3 "$ROOT/deploy/gen-meshamerica-catalog.py" "$TAG" > "$CATALOG.tmp" && mv "$CATALOG.tmp" "$CATALOG"
|
||||
mkdir -p "$OUT/meshamerica" && cp "$CATALOG" "$OUT/meshamerica/catalog.json" # also mirror to the VPS (enables the branded URL once its nginx location is deployed)
|
||||
if git -C "$ROOT" diff --quiet -- "$CATALOG"; then
|
||||
echo "Mesh America catalog already current for $TAG"
|
||||
elif [ "$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null)" = "main" ]; then
|
||||
if git -C "$ROOT" commit -q -m "chore: refresh Mesh America catalog for $TAG" -- "$CATALOG" \
|
||||
&& git -C "$ROOT" push origin HEAD:main; then
|
||||
echo "Mesh America catalog refreshed + pushed to main ($TAG)"
|
||||
else
|
||||
echo "WARN: Mesh America catalog commit/push failed — push deploy/meshamerica-catalog.json to main by hand"
|
||||
fi
|
||||
else
|
||||
echo "NOTE: regenerated $CATALOG for $TAG but not on 'main' — commit + push it to main so apps.meshamerica.com updates."
|
||||
fi
|
||||
|
||||
# 4. Publish to the VPS (Cloudflare caches at the edge).
|
||||
if [ -n "$DEST" ]; then
|
||||
rsync -av "$OUT/" "$DEST:$DEST_PATH/"
|
||||
|
||||
@@ -1880,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);
|
||||
}
|
||||
@@ -2553,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) ----
|
||||
@@ -480,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) {
|
||||
@@ -774,6 +780,16 @@ public:
|
||||
// To check if there is pending work
|
||||
bool hasPendingWork() const;
|
||||
|
||||
// Number of companion clients currently connected on any transport.
|
||||
// Used by the idle light-sleep gate (TouchSleep) to confirm no one is
|
||||
// actively talking to us before the node parks in light sleep.
|
||||
int getProtoNumClients() const { return proto_num_clients; }
|
||||
|
||||
// True if the radio is currently mid-receive of a packet (preamble→RxDone).
|
||||
// _radio is the protected Dispatcher::Radio pointer; isReceiving() returns false
|
||||
// by default and is overridden by RadioLibWrapper with a real preamble-detect check.
|
||||
bool isRadioReceiving() const { return _radio && _radio->isReceiving(); }
|
||||
|
||||
private:
|
||||
void writeOKFrame();
|
||||
void writeErrFrame(uint8_t err_code);
|
||||
|
||||
@@ -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 = 3; // v2 adds sig_probe_en + sig_poll_min; v3 adds tz_zone
|
||||
static const uint8_t TOUCH_CFG_VER = 10; // 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; v8 ui_scale; v9 tb_keypad; v10 sleep_idle
|
||||
|
||||
// Defaults (kept identical to the historical per-key defaults).
|
||||
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
|
||||
@@ -73,6 +73,16 @@ struct __attribute__((packed)) TouchCfg {
|
||||
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
|
||||
uint8_t ui_scale; // UI resolution scale: 0=100% 1=150% 2=200% (Tanmatsu, applied at boot) — v8
|
||||
uint8_t tb_keypad; // T-Deck trackball: 0=mouse cursor (default), 1=D-pad focus navigation — v9
|
||||
uint8_t sleep_idle; // idle light-sleep feature on/off (bool) — v10 (trailing so existing blobs default it OFF)
|
||||
};
|
||||
|
||||
static TouchCfg s_cfg;
|
||||
@@ -122,6 +132,16 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
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)
|
||||
c.ui_scale = 0; // default: 100% UI scale (native resolution)
|
||||
c.tb_keypad = 0; // default: trackball = mouse cursor
|
||||
c.sleep_idle = 0; // default: idle light-sleep OFF
|
||||
}
|
||||
|
||||
// Persist the whole blob using the same end()/begin(RW)/put/end()/begin(RO)
|
||||
@@ -682,6 +702,102 @@ 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 touchPrefsGetSleepIdle() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.sleep_idle != 0;
|
||||
}
|
||||
bool touchPrefsSetSleepIdle(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.sleep_idle = on ? 1 : 0;
|
||||
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();
|
||||
}
|
||||
|
||||
uint8_t touchPrefsGetUiScale() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.ui_scale > 2 ? 0 : s_cfg.ui_scale; // 0=100% 1=150% 2=200%
|
||||
}
|
||||
bool touchPrefsSetUiScale(uint8_t scale) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.ui_scale = scale > 2 ? 0 : scale;
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
bool touchPrefsGetTbKeypad() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.tb_keypad != 0;
|
||||
}
|
||||
bool touchPrefsSetTbKeypad(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.tb_keypad = 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"
|
||||
|
||||
@@ -80,6 +80,50 @@ 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);
|
||||
/** Idle light-sleep feature: when ON, the device enters light sleep while the
|
||||
* screen is off and all activity gates pass. Default OFF. */
|
||||
bool touchPrefsGetSleepIdle();
|
||||
bool touchPrefsSetSleepIdle(bool on);
|
||||
|
||||
/* UI resolution scale (Tanmatsu): 0=100% (native 800x480), 1=150%, 2=200%. Applied at boot —
|
||||
* LVGL renders at a lower resolution and the flush upscales to the panel. Reboot to apply. */
|
||||
uint8_t touchPrefsGetUiScale();
|
||||
bool touchPrefsSetUiScale(uint8_t scale);
|
||||
|
||||
/* T-Deck trackball interaction mode: false (default) = the trackball drives a soft
|
||||
* mouse cursor; true = D-pad focus navigation (↑↓←→ move focus, ball click = enter),
|
||||
* like the Tanmatsu. Applied live (no reboot). */
|
||||
bool touchPrefsGetTbKeypad();
|
||||
bool touchPrefsSetTbKeypad(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). */
|
||||
|
||||
@@ -177,6 +177,11 @@ bool wifiConfigConsumeApplyRequest() {
|
||||
}
|
||||
|
||||
void wifiConfigApply() {
|
||||
#if defined(HAS_TANMATSU)
|
||||
printf("[WIFI] apply radio_en=%d hasRuntime=%d mode=%d status=%d\n",
|
||||
(int)wifiConfigGetRadioEnabled(), (int)wifiConfigHasRuntime(),
|
||||
(int)WiFi.getMode(), (int)WiFi.status());
|
||||
#endif
|
||||
if (!wifiConfigGetRadioEnabled()) {
|
||||
WiFi.disconnect(true);
|
||||
delay(50);
|
||||
@@ -188,9 +193,13 @@ void wifiConfigApply() {
|
||||
char pwd[WIFI_CONFIG_PWD_MAX];
|
||||
wifiConfigGetSsid(ssid, sizeof(ssid));
|
||||
wifiConfigGetPwd(pwd, sizeof(pwd));
|
||||
WiFi.mode(WIFI_STA); // esp_wifi_remote needs an explicit STA mode before begin()
|
||||
WiFi.disconnect();
|
||||
delay(100);
|
||||
WiFi.begin(ssid, pwd[0] ? pwd : nullptr);
|
||||
#if defined(HAS_TANMATSU)
|
||||
printf("[WIFI] begin ssid='%s' pwlen=%d\n", ssid, (int)strlen(pwd));
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
+15
-1
@@ -12,6 +12,7 @@
|
||||
#include "helpers/esp32/SdNvsPrefs.h" // route prefs to file storage (SD/SPIFFS), off NVS
|
||||
// (quoted: use wadamesh's src/ copy, not the lib's stale one)
|
||||
#include "wadamesh_mark_rgb.h" // anti-aliased mesh-mark (RGB565) for the pre-LVGL boot screen
|
||||
#include "ui-touch/TouchSleep.h" // idle light-sleep controller (loopEnd called at end of loop())
|
||||
#endif
|
||||
|
||||
// Believe it or not, this std C function is busted on some platforms!
|
||||
@@ -399,7 +400,9 @@ 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
|
||||
@@ -557,6 +560,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);
|
||||
}
|
||||
}
|
||||
@@ -666,4 +673,11 @@ void loop() {
|
||||
#if defined(ESP32_PLATFORM)
|
||||
board.pollHttpOtaReboot();
|
||||
#endif
|
||||
|
||||
#if defined(HAS_TOUCH_UI)
|
||||
// Idle light-sleep gate: evaluated every loop tick. g_enabled is false by
|
||||
// default (Task 1 is inert); Task 2 sets it from the NVS pref and wires
|
||||
// the real predicates so the gate can actually pass and arm light sleep.
|
||||
touchSleep::loopEnd(millis());
|
||||
#endif
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "TouchSleep.h"
|
||||
#include <esp_timer.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
namespace touchSleep {
|
||||
namespace {
|
||||
// When the gate passes (device parked: screen off, no companion client, WiFi+BLE
|
||||
// off, on battery, mesh idle) we THROTTLE the main loop with vTaskDelay instead of
|
||||
// entering esp_light_sleep.
|
||||
//
|
||||
// Why not real light sleep: manual esp_light_sleep_start() from the Arduino loop
|
||||
// task trips the Interrupt Watchdog in this build (dual-core, no PM framework,
|
||||
// INT_WDT on both cores @ 300 ms, always-on companion/LVGL/USB tasks) — reliably,
|
||||
// regardless of duration or wake-source handling. vTaskDelay is a standard scheduler
|
||||
// yield: the FreeRTOS idle task halts the CPU (WFI) between 1 ms ticks, trimming the
|
||||
// idle busy-spin draw, with zero watchdog/hang risk. The radio stays in RX; an
|
||||
// incoming packet is serviced on the next loop iteration (<= THROTTLE_MS late).
|
||||
constexpr uint32_t THROTTLE_MS = 50;
|
||||
|
||||
uint64_t g_acc_idle_us = 0; // cumulative time spent parked/throttled
|
||||
|
||||
Hooks g_hooks = {};
|
||||
TransitionCb g_transition = nullptr;
|
||||
bool g_enabled = false; // default OFF; driven from NVS via touchPrefsGetSleepIdle() at init
|
||||
bool g_asleep_regime = false;
|
||||
uint32_t g_cycle_count = 0; // throttle cycles while parked (liveness counter)
|
||||
WakeReason g_last_reason = WakeReason::None;
|
||||
|
||||
bool gatePasses() {
|
||||
if (!g_enabled) return false;
|
||||
if (!g_hooks.screenOff || !g_hooks.screenOff()) return false;
|
||||
if (!g_hooks.noClient || !g_hooks.noClient()) return false;
|
||||
if (!g_hooks.wifiOff || !g_hooks.wifiOff()) return false;
|
||||
if (!g_hooks.bleOff || !g_hooks.bleOff()) return false;
|
||||
if (!g_hooks.onBattery || !g_hooks.onBattery()) return false;
|
||||
if (!g_hooks.meshIdle || !g_hooks.meshIdle()) return false;
|
||||
return true;
|
||||
}
|
||||
void emitTransition(bool entering) {
|
||||
if (g_transition && g_hooks.epochNow) g_transition(g_hooks.epochNow(), entering);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void begin(const Hooks& hooks) { g_hooks = hooks; }
|
||||
void onTransition(TransitionCb cb) { g_transition = cb; }
|
||||
void setEnabled(bool on) { g_enabled = on; }
|
||||
bool enabled() { return g_enabled; }
|
||||
|
||||
void loopEnd(uint32_t /*now_ms*/) {
|
||||
const bool pass = gatePasses();
|
||||
if (!pass) {
|
||||
if (g_asleep_regime) { g_asleep_regime = false; emitTransition(false); } // sun — resumed activity
|
||||
return;
|
||||
}
|
||||
if (!g_asleep_regime) { g_asleep_regime = true; emitTransition(true); } // moon — parked
|
||||
|
||||
// Yield the CPU to the idle task for THROTTLE_MS (replaces esp_light_sleep_start —
|
||||
// see the note above). Standard FreeRTOS call: cannot starve a watchdog or hang.
|
||||
const uint64_t t0 = esp_timer_get_time();
|
||||
vTaskDelay(pdMS_TO_TICKS(THROTTLE_MS));
|
||||
g_acc_idle_us += (uint64_t)(esp_timer_get_time() - t0);
|
||||
g_cycle_count++;
|
||||
g_last_reason = WakeReason::Timer; // a throttle is a timed yield
|
||||
}
|
||||
|
||||
bool isSleeping() { return g_asleep_regime; } // true while parked/throttling
|
||||
uint32_t wakeCount() { return g_cycle_count; } // throttle cycles while parked
|
||||
WakeReason lastWakeReason() { return g_last_reason; }
|
||||
|
||||
uint8_t pctAsleep() {
|
||||
const uint64_t up = (uint64_t)esp_timer_get_time();
|
||||
if (up == 0) return 0;
|
||||
uint64_t pct = (g_acc_idle_us * 100ULL) / up;
|
||||
return pct > 100 ? 100 : (uint8_t)pct;
|
||||
}
|
||||
|
||||
} // namespace touchSleep
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// Idle power-save controller for the T-Deck (ESP32-S3). When the device is parked
|
||||
// (screen off, on battery, standalone) it throttles the main loop with vTaskDelay
|
||||
// so the FreeRTOS idle task can halt the CPU between ticks. (Real esp_light_sleep
|
||||
// trips the Interrupt Watchdog in this dual-core / no-PM build — see TouchSleep.cpp
|
||||
// and .notes/2026-06-18-light-sleep-rx-design.md for the full story.)
|
||||
//
|
||||
// Decoupled from UI/mesh by predicate hooks supplied by the integration layer
|
||||
// (UITask.cpp), so this module pulls in no UI/mesh/LVGL headers.
|
||||
namespace touchSleep {
|
||||
|
||||
enum class WakeReason : uint8_t { None, Timer, Packet, Touch, Button, Other };
|
||||
|
||||
struct Hooks {
|
||||
bool (*screenOff)(); // display is off
|
||||
bool (*noClient)(); // no companion client on any transport
|
||||
bool (*wifiOff)(); // WiFi fully off
|
||||
bool (*bleOff)(); // BLE fully off
|
||||
bool (*onBattery)(); // running on battery (no USB / charge source)
|
||||
bool (*meshIdle)(); // radio not mid-RX AND send queue empty
|
||||
// ms until the soonest wake-forcing deadline (advert / clock alarm),
|
||||
// or UINT32_MAX when nothing must wake us:
|
||||
uint32_t (*nextWakeForcingDueMs)(uint32_t now_ms);
|
||||
uint32_t (*epochNow)(); // wall-clock epoch seconds (for the event log)
|
||||
};
|
||||
|
||||
// regime transition: entering==true -> went to sleep (moon)
|
||||
// entering==false -> woke to activity (sun)
|
||||
using TransitionCb = void (*)(uint32_t epoch, bool entering);
|
||||
|
||||
void begin(const Hooks& hooks); // install hooks; arm static GPIO wake config once
|
||||
void onTransition(TransitionCb cb);
|
||||
void setEnabled(bool on); // feature toggle (driven by the NVS pref)
|
||||
bool enabled();
|
||||
|
||||
void loopEnd(uint32_t now_ms); // call at the very END of the main loop()
|
||||
|
||||
// instrumentation (read by the settings / diag UI)
|
||||
bool isSleeping(); // currently in the asleep regime
|
||||
uint32_t wakeCount(); // cumulative meaningful wakes
|
||||
WakeReason lastWakeReason();
|
||||
uint8_t pctAsleep(); // % wall-time asleep since boot (0..100)
|
||||
|
||||
} // namespace touchSleep
|
||||
+4418
-527
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;
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*******************************************************************************
|
||||
* Size: 16 px
|
||||
* Bpp: 4
|
||||
* Opts: --no-compress --bpp 4 --size 16 --font /Applications/MeshCore.app/Wrapper/Runner.app/Frameworks/App.framework/flutter_assets/packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf -r 0xF185 -r 0xF186 --format lvgl --lv-font-name sleepicons_font -o src/ui-touch/sleepicons_font.c
|
||||
******************************************************************************/
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
#ifndef SLEEPICONS_FONT
|
||||
#define SLEEPICONS_FONT 1
|
||||
#endif
|
||||
|
||||
#if SLEEPICONS_FONT
|
||||
|
||||
/*-----------------
|
||||
* BITMAPS
|
||||
*----------------*/
|
||||
|
||||
/*Store the image of the glyphs*/
|
||||
static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
|
||||
/* U+F185 "" */
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x8, 0xa1, 0x0, 0x1a, 0x80,
|
||||
0x0, 0x0, 0x0, 0x0, 0xc, 0xfe, 0x55, 0xef,
|
||||
0xc0, 0x0, 0x0, 0x0, 0x0, 0xf, 0xff, 0xff,
|
||||
0xff, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x4f, 0xff,
|
||||
0xff, 0xff, 0xf4, 0x0, 0x0, 0x8, 0xcf, 0xff,
|
||||
0xd5, 0x11, 0x5d, 0xff, 0xfc, 0x80, 0xa, 0xff,
|
||||
0xfd, 0x18, 0xee, 0x81, 0xdf, 0xff, 0xa0, 0x1,
|
||||
0xef, 0xf5, 0x8f, 0xff, 0xf8, 0x5f, 0xfe, 0x10,
|
||||
0x0, 0x5f, 0xf1, 0xef, 0xff, 0xfe, 0x1f, 0xf5,
|
||||
0x0, 0x0, 0x5f, 0xf1, 0xef, 0xff, 0xfe, 0x1f,
|
||||
0xf5, 0x0, 0x1, 0xef, 0xf5, 0x8f, 0xff, 0xf8,
|
||||
0x5f, 0xfe, 0x10, 0xa, 0xff, 0xfd, 0x18, 0xee,
|
||||
0x81, 0xdf, 0xff, 0xa0, 0x8, 0xcf, 0xff, 0xd5,
|
||||
0x11, 0x5d, 0xff, 0xfc, 0x80, 0x0, 0x0, 0x4f,
|
||||
0xff, 0xff, 0xff, 0xf4, 0x0, 0x0, 0x0, 0x0,
|
||||
0xf, 0xff, 0xff, 0xff, 0xf0, 0x0, 0x0, 0x0,
|
||||
0x0, 0xc, 0xfe, 0x55, 0xef, 0xc0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x8, 0xa1, 0x0, 0x1a, 0x80, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
/* U+F186 "" */
|
||||
0x0, 0x1, 0x7c, 0xee, 0x0, 0x0, 0x0, 0x4,
|
||||
0xef, 0xfe, 0x30, 0x0, 0x0, 0x5, 0xff, 0xff,
|
||||
0x20, 0x0, 0x0, 0x1, 0xff, 0xff, 0x80, 0x0,
|
||||
0x0, 0x0, 0x7f, 0xff, 0xf2, 0x0, 0x0, 0x0,
|
||||
0xc, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0xef,
|
||||
0xff, 0xf1, 0x0, 0x0, 0x0, 0xe, 0xff, 0xff,
|
||||
0x40, 0x0, 0x0, 0x0, 0xcf, 0xff, 0xfc, 0x0,
|
||||
0x0, 0x0, 0x7, 0xff, 0xff, 0xf8, 0x0, 0x0,
|
||||
0x0, 0x1e, 0xff, 0xff, 0xfa, 0x20, 0x0, 0x0,
|
||||
0x4f, 0xff, 0xff, 0xff, 0xdc, 0xb0, 0x0, 0x4e,
|
||||
0xff, 0xff, 0xff, 0xf5, 0x0, 0x0, 0x17, 0xce,
|
||||
0xfc, 0x81, 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 = 256, .box_w = 18, .box_h = 18, .ofs_x = -1, .ofs_y = -3},
|
||||
{.bitmap_index = 162, .adv_w = 192, .box_w = 13, .box_h = 14, .ofs_x = 0, .ofs_y = -1}
|
||||
};
|
||||
|
||||
/*---------------------
|
||||
* CHARACTER MAPPING
|
||||
*--------------------*/
|
||||
|
||||
|
||||
|
||||
/*Collect the unicode lists and glyph_id offsets*/
|
||||
static const lv_font_fmt_txt_cmap_t cmaps[] =
|
||||
{
|
||||
{
|
||||
.range_start = 61829, .range_length = 2, .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 sleepicons_font = {
|
||||
#else
|
||||
lv_font_t sleepicons_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 = 18, /*The maximum line height required by the font*/
|
||||
.base_line = 3, /*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 SLEEPICONS_FONT*/
|
||||
|
||||
@@ -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