Compare commits

...
11 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 4.8 058721fdbc release-notes: beta_3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:07:41 +02:00
Kaj SchittecatandClaude Opus 4.8 ff195bbaa2 ui: rebrand remaining product-name strings Meshcomod -> WADAMESH
The first-boot setup welcome ('Welcome to Meshcomod', + its 10 translations), the
About build line ('Meshcomod TOUCH'), and the terminal banner still said Meshcomod.
Flipped to WADAMESH. Left storage/infra references untouched on purpose: the
/meshcomod data paths, the SD 'MESHCOMOD' FAT volume label, the meshcomod.com
tile/flasher proxies, and the 'meshcomod web' community-preset source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:57:26 +02:00
Kaj SchittecatandClaude Opus 4.8 77a7420c96 build: bump core to core-v1.16.2 (RTC seed-floor-on-every-boot fix)
Picks up the ESP32Board.h fix that seeds the time floor after any reset (not just
power-on), so the touch firmware no longer shows bogus 1970 timestamps after a
panic/OTA reboot until Wi-Fi/GPS sets the clock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:49:44 +02:00
Kaj SchittecatandClaude Opus 4.8 86c07560e5 perf(nvs): pack 17 scalar touch settings into one blob (shrink NVS footprint)
NVS is a tiny ~20KB partition shared with the Launcher/other apps/Wi-Fi; it was
filling up (Launcher 'Failed to store settings in NVS: 4357', boot loops). The
touch namespace spread 17 scalar settings (brightness, kb backlight, accent,
language, rotation, lock colour, ...) across 17 separate keys. Pack them into one
versioned 'cfg' blob -> ~16 NVS entries reclaimed per device.

One-time migration is crash-safe (write blob, then remove legacy keys only after a
durable write; idempotent via magic+ver), preserves every user's existing settings,
and falls back cleanly if the write fails on an already-full NVS. use_sd/setup_ok
stay standalone (main.cpp reads them at boot); strings, byte-blobs and Wi-Fi slots
are untouched. Public API unchanged; both touch envs build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:40:29 +02:00
Kaj SchittecatandClaude Opus 4.8 ec3b47bbc2 fix: restore 'repeats heard' on scoped floods + guard trace-route crash
- MyMesh.cpp logRxRaw: the self-echo parser tested (raw[0] & 0x80) for transport
  codes, but 0x80 is the top of the payload VERSION field, not a flag. Transport
  codes are present iff route_type (raw[0]&0x03) is TRANSPORT_FLOOD/DIRECT. Since
  beta_12's region scope, scoped floods carry transport codes -> the parser hashed
  the wrong payload slice -> the echo never matched -> 'repeats heard' showed 0 for
  anyone with a region set. Now route-type aware (matches uiStashRxMeta). Also fixed
  the diagnostic dst= decoder + the header-layout comment.
- MyMesh.h uiSendTraceRoute: walking c.out_path with an unchecked out_path_len
  (uint8_t, can be 65..254 if a contact's path is corrupt) overran sendDirect's
  payload memcpy -> reboot when tapping 'Trace route' on a sent DM. Clamp to
  <= MAX_PATH_SIZE, else fall back to the single-hop trace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:11:17 +02:00
Kaj SchittecatandClaude Opus 4.8 9e2eed1551 fix: bound advert-name copy (corrupt contacts) + log NVS headroom at boot
- MyMesh.cpp: strcpy(p->name, contact.name) could overrun the 32-byte advert_paths
  name field when an over-the-air advert carries a 32-char (unterminated) name,
  corrupting the recently-heard/Found table. Bounded with strncpy + NUL-terminate.
- main.cpp: log nvs_free_entries at boot so a filling NVS (the boot-loop trigger
  under Launcher) is visible in serial before it goes catastrophic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:45:08 +02:00
Kaj SchittecatandClaude Opus 4.8 58bae913e1 scripts: cut-core-tag.sh — regenerate the lean meshcomod core tag
One command to cut a source-only (no prebuilt/) tag on the monorepo for wadamesh
to pin, keeping the shallow lib clone ~12 MB instead of 1.4 GB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:30:32 +02:00
Kaj SchittecatandClaude Opus 4.8 6088370f5d build: pin core to the meshcomod monorepo (lean core-v1.16.1), drop meshcomod-core
wadamesh now builds its MeshCore core from the ALLFATHER-BV/meshcomod monorepo
(the same repo as the non-touch firmware) via a lean, source-only core-* tag,
instead of the separate meshcomod-core fork. The 8 touch-app files this repo
vendors are excluded from the lib with -DMC_VENDORED_TOUCH_APP. Build is
byte-identical (Heltec flash 2625757, unchanged). One core, one repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:28:33 +02:00
Kaj SchittecatandClaude Opus 4.8 2c099aadb6 wifi: make the Wi-Fi settings-page radio toggle live
The switch on the Wi-Fi settings page had no event handler — it was only read
when you pressed Save, so flipping it appeared to do nothing. Add a
VALUE_CHANGED handler that calls wifiConfigSetRadioEnabled() (persists + flags an
apply; the main loop brings esp_wifi up/down), mirroring the control-center
toggle. Toggling now turns Wi-Fi on/off on the spot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:59:46 +02:00
Kaj SchittecatandClaude Opus 4.8 a405513d84 flasher: show current release version + notes (pipeline-driven)
The esp-web-tools manifest carried a frozen version:beta_1 while the rolling
/latest/ bins moved on, so the web flasher always said beta_1. Fix:
- scripts/build/gen-flasher-meta.py emits version.json (tag + notes) + per-release
  manifests (version=tag) into /latest/; release.sh + the CI workflow run it, so
  the metadata rolls with every release.
- the site points the install buttons at the VPS manifests and renders a live
  'What's new in <tag>' block from version.json.
- drop the frozen static site manifests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:08:10 +02:00
Kaj SchittecatandClaude Opus 4.8 399b5004e0 ci: add tag-triggered release workflow (build -> GitHub Release -> VPS)
Push a beta_* tag (or run via workflow_dispatch) to build both touch boards in
CI with the tag embedded, cut a GitHub Release with the 4 bins, and publish to
wadamesh.com over SSH. VPS host + deploy key are Actions secrets (WADAMESH_VPS,
WADAMESH_VPS_PATH, WADAMESH_VPS_SSH_KEY); the publish step auto-skips until they
exist, so scripts/release.sh remains the manual path. Nothing sensitive in-repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:53:27 +02:00
17 changed files with 574 additions and 225 deletions

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
name: release
# Slice a version by pushing a tag:
# git tag beta_13 && git push origin beta_13
# or run it manually from the Actions tab (provide the tag as input).
#
# What it does:
# 1. builds both touch boards in the cloud, with the tag embedded
# (FIRMWARE_RELEASE_TAG) so the on-device beta number matches the tag;
# 2. creates / updates the GitHub Release for the tag with the 4 bins;
# 3. publishes the bins to wadamesh.com over SSH **iff** the VPS secrets are
# set — so the on-device update check + web flasher pick them up.
#
# Safety: nothing sensitive lives in this file. The VPS host + deploy key are
# GitHub Actions *secrets* (Settings -> Secrets and variables -> Actions):
# WADAMESH_VPS user@host of the firmware VPS
# WADAMESH_VPS_PATH e.g. /srv/wadamesh/firmware
# WADAMESH_VPS_SSH_KEY a deploy private key authorized on that host
# Until those exist, step 3 is skipped and you publish with scripts/release.sh.
on:
push:
tags: ['beta_*']
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build (e.g. beta_13)'
required: true
permissions:
contents: write # create the Release + upload assets (nothing more)
jobs:
release:
runs-on: ubuntu-latest
env:
ENVS: "heltec_v4_tft_companion_radio_usb_tcp_touch LilyGo_TDeck_companion_radio_touch"
# true only when the VPS deploy key secret is configured (no secret value is exposed)
HAS_VPS: ${{ secrets.WADAMESH_VPS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: |
~/.platformio
.pio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
- name: Install PlatformIO
run: pip install --upgrade platformio
- name: Resolve tag
id: v
run: |
TAG="${{ github.event.inputs.tag }}"
[ -z "$TAG" ] && TAG="${GITHUB_REF_NAME}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Building release: $TAG"
- name: Build both touch boards (tag embedded; Wi-Fi placeholder, no baked creds)
env:
TAG: ${{ steps.v.outputs.tag }}
run: |
unset WIFI_SSID WIFI_PWD
export PLATFORMIO_BUILD_FLAGS="-DFIRMWARE_RELEASE_TAG='\"${TAG}\"' -DFIRMWARE_VERSION='\"wadamesh ${TAG}\"'"
for e in $ENVS; do pio run -t mergebin -e "$e"; done
- name: Collect bins
env:
TAG: ${{ steps.v.outputs.tag }}
run: |
mkdir -p dist
declare -A NAME=(
[heltec_v4_tft_companion_radio_usb_tcp_touch]=wadamesh-heltec-v4-tft
[LilyGo_TDeck_companion_radio_touch]=wadamesh-tdeck
)
for e in $ENVS; do
n=${NAME[$e]}
cp ".pio/build/$e/firmware.bin" "dist/$n.bin"
cp ".pio/build/$e/firmware-merged.bin" "dist/$n-merged.bin"
done
ls -la dist
- name: Web-flasher metadata (version.json + manifests, roll with release)
env:
TAG: ${{ steps.v.outputs.tag }}
run: python3 scripts/build/gen-flasher-meta.py "$TAG" dist "release-notes/$TAG.txt"
- name: Create / update the GitHub Release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.v.outputs.tag }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" dist/* --clobber
else
gh release create "$TAG" dist/* --title "wadamesh $TAG" \
--notes "Touch firmware **$TAG** for the LilyGo T-Deck and Heltec V4 + TFT. Built by CI from \`$GITHUB_SHA\`."
fi
- name: Publish to wadamesh.com (VPS) — skipped until VPS secrets are set
if: env.HAS_VPS == 'true'
env:
VPS: ${{ secrets.WADAMESH_VPS }}
VPS_PATH: ${{ secrets.WADAMESH_VPS_PATH }}
SSH_KEY: ${{ secrets.WADAMESH_VPS_SSH_KEY }}
TAG: ${{ steps.v.outputs.tag }}
run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/id_deploy && chmod 600 ~/.ssh/id_deploy
ssh-keyscan -H "${VPS#*@}" >> ~/.ssh/known_hosts 2>/dev/null || true
SSH="ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=accept-new"
REL="$VPS_PATH/releases/TOUCH/$TAG"
$SSH "$VPS" "mkdir -p '$REL' '$VPS_PATH/latest'"
for n in wadamesh-heltec-v4-tft wadamesh-tdeck; do
scp -i ~/.ssh/id_deploy "dist/$n.bin" "dist/$n-merged.bin" "$VPS:$REL/"
scp -i ~/.ssh/id_deploy "dist/$n-merged.bin" "dist/$n.bin" "$VPS:$VPS_PATH/latest/"
done
scp -i ~/.ssh/id_deploy dist/version.json dist/manifest-tdeck.json dist/manifest-heltec-v4-tft.json "$VPS:$VPS_PATH/latest/"
$SSH "$VPS" "cd '$VPS_PATH/releases/TOUCH' && python3 -c \"import os,json; b=sorted(d for d in os.listdir('.') if d.startswith('beta_') and os.path.isdir(d)); open('index.json','w').write(json.dumps([{'name':x,'type':'dir'} for x in b], indent=2))\""
rm -f ~/.ssh/id_deploy
echo "Published $TAG to wadamesh.com"
+6 -3
View File
@@ -25,9 +25,12 @@ MeshCore fork via PlatformIO `lib_deps`.
This repo holds only the **app**: the `companion_radio` glue, the `ui-touch`
LVGL UI, the two boards' glue/variants, and `platformio.ini`. The **MeshCore
core is not vendored here** — it's pulled as a library via `lib_deps` from the
fork [`ALLFATHER-BV/meshcomod-core`](https://github.com/ALLFATHER-BV/meshcomod-core) (a clean
fork of meshcore-dev/MeshCore), pinned by git tag. The build is byte-identical to
the original in-tree meshcomod firmware.
[`ALLFATHER-BV/meshcomod`](https://github.com/ALLFATHER-BV/meshcomod) monorepo
(the same repo as the non-touch firmware), pinned by a lean source-only `core-*`
git tag. The touch-app files this repo owns (TouchPrefsStore, WifiRuntimeStore,
the transports, …) are dropped from the lib via `-DMC_VENDORED_TOUCH_APP` so they
aren't compiled twice. The build is byte-identical to the original in-tree
meshcomod firmware.
## Build
+22 -2
View File
@@ -245,6 +245,10 @@
<button class="back" data-back>← choose a different path</button>
<div class="guide-head"><h3>Standalone install</h3></div>
<p class="p">Plug the device in over USB and flash it right here. Works in <b>Chrome</b> or <b>Edge</b> on desktop.</p>
<div data-whatsnew hidden style="margin:18px auto 0;max-width:560px;background:rgba(21,182,166,.07);border:1px solid rgba(21,182,166,.28);border-radius:12px;padding:13px 16px;text-align:left">
<div style="font-weight:600">What's new in <span data-wn-tag style="color:var(--teal)">the latest build</span></div>
<ul data-wn-notes style="margin:7px 0 0;padding-left:18px;font-size:.9rem;color:var(--mut);line-height:1.55"></ul>
</div>
<div class="steps" style="margin-top:22px">
<div class="step"><div class="num">1</div><div><h4>Connect over USB</h4><p>Plug your T-Deck or Heltec V4 into your computer with a data cable.</p></div></div>
<div class="step"><div class="num">2</div><div><h4>Pick your board &amp; install</h4><p>Hit <b>Install</b> below, choose the serial port, and let it flash. It does a clean full install.</p></div></div>
@@ -252,9 +256,9 @@
</div>
<div class="boards">
<div class="board"><h4>Heltec V4 + TFT</h4><p>ESP32-S3 with CHSC6x touch.</p>
<esp-web-install-button manifest="./manifest-heltec-v4-tft.json"></esp-web-install-button></div>
<esp-web-install-button manifest="https://firmware.wadamesh.com/latest/manifest-heltec-v4-tft.json"></esp-web-install-button></div>
<div class="board"><h4>LilyGo T-Deck / Plus</h4><p>ESP32-S3 keyboard handheld.</p>
<esp-web-install-button manifest="./manifest-tdeck.json"></esp-web-install-button></div>
<esp-web-install-button manifest="https://firmware.wadamesh.com/latest/manifest-tdeck.json"></esp-web-install-button></div>
</div>
<div class="noserial">Your browser can't flash here — use <b>Chrome</b> or <b>Edge</b> on a desktop.</div>
<p class="hint">T-Deck not detected? Hold the trackball while tapping reset to force download mode.</p>
@@ -379,6 +383,22 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
try { if (!localStorage.getItem('wm-cookie-ack')) cbar.classList.add('show'); } catch(_) {}
document.getElementById('cookieok').addEventListener('click', () => { cbar.classList.remove('show'); try { localStorage.setItem('wm-cookie-ack','1'); } catch(_) {} });
// Live release version + "what's new" — read from the firmware host so the
// flasher always reflects the current build (the release pipeline updates
// version.json; no site redeploy needed per release).
fetch('https://firmware.wadamesh.com/latest/version.json', {cache:'no-store'})
.then(r => r.ok ? r.json() : null)
.then(v => {
if (!v || !v.tag) return;
document.querySelectorAll('[data-wn-tag]').forEach(e => e.textContent = v.tag);
const notes = Array.isArray(v.notes) ? v.notes : [];
document.querySelectorAll('[data-wn-notes]').forEach(ul => {
ul.innerHTML = '';
notes.forEach(n => { const li = document.createElement('li'); li.textContent = n; ul.appendChild(li); });
});
if (notes.length) document.querySelectorAll('[data-whatsnew]').forEach(b => b.hidden = false);
}).catch(()=>{});
</script>
<!-- animated mesh backdrop — Vanta NET (3D dot-and-line network) in wadamesh teal on white.
-13
View File
@@ -1,13 +0,0 @@
{
"name": "wadamesh — Heltec V4 TFT",
"version": "beta_1",
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32-S3",
"parts": [
{ "path": "https://firmware.wadamesh.com/latest/wadamesh-heltec-v4-tft-merged.bin", "offset": 0 }
]
}
]
}
-13
View File
@@ -1,13 +0,0 @@
{
"name": "wadamesh — LilyGo T-Deck",
"version": "beta_1",
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32-S3",
"parts": [
{ "path": "https://firmware.wadamesh.com/latest/wadamesh-tdeck-merged.bin", "offset": 0 }
]
}
]
}
+7 -3
View File
@@ -18,6 +18,7 @@ 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
-D LORA_FREQ=869.618
-D LORA_BW=62.5
-D LORA_SF=8
@@ -162,8 +163,10 @@ build_src_filter =
+<../variants/heltec_v4/*.cpp>
lib_deps =
; MeshCore core — the upstreamable fork, pinned by tag (no vendored src/ here)
https://github.com/ALLFATHER-BV/meshcomod-core.git#v1.16.0-wada.1
; 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
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -210,6 +213,7 @@ 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
-D LORA_FREQ=869.618
-D LORA_BW=62.5
-D LORA_SF=8
@@ -320,7 +324,7 @@ build_src_filter =
+<../variants/lilygo_tdeck/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod-core.git#v1.16.0-wada.1
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.2
SPI
Wire
jgromes/RadioLib @ ^7.6.0
+4
View File
@@ -0,0 +1,4 @@
New WADAMESH boot logo + teal default theme
Wi-Fi scan no longer crashes when no network is connected
Wi-Fi settings apply live — no more reboot prompt
Factory reset now fully wipes the device (SD-storage fix)
+6
View File
@@ -0,0 +1,6 @@
"Heard repeats" indicator works again on networks with a region set
Fixed a reboot when opening Trace route on one of your sent messages
Fixed garbled "recently heard" / contacts from malformed adverts
Lighter settings storage — helps avoid the shared-NVS "storage full" issue
Clock no longer stuck near 1970 after a reboot until Wi-Fi/GPS syncs
Setup wizard and About now show the WADAMESH name
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
# Generate the web-flasher metadata that must roll with every release:
# - version.json : {tag, notes[]} — the site shows this ("what's new")
# - manifest-tdeck.json : esp-web-tools manifest (its "version" drives the
# - manifest-heltec-v4-tft.json install dialog, so it must equal the tag)
#
# These live next to the rolling bins at firmware.wadamesh.com/latest/, so the
# flasher always reflects the current release without a site redeploy.
#
# Usage: gen-flasher-meta.py <tag> <outdir> [notes_file]
# notes_file: one note per non-empty line (plain text); optional.
import sys, json, os
tag = sys.argv[1]
outdir = sys.argv[2]
notes_file = sys.argv[3] if len(sys.argv) > 3 else None
notes = []
if notes_file and os.path.exists(notes_file):
notes = [ln.strip() for ln in open(notes_file) if ln.strip() and not ln.startswith("#")]
os.makedirs(outdir, exist_ok=True)
with open(os.path.join(outdir, "version.json"), "w") as f:
json.dump({"tag": tag, "notes": notes}, f, indent=2)
BOARDS = {
"manifest-tdeck.json": ("wadamesh — LilyGo T-Deck", "wadamesh-tdeck-merged.bin"),
"manifest-heltec-v4-tft.json": ("wadamesh — Heltec V4 TFT", "wadamesh-heltec-v4-tft-merged.bin"),
}
for fn, (name, binf) in BOARDS.items():
manifest = {
"name": name,
"version": tag,
"new_install_prompt_erase": True,
"builds": [{
"chipFamily": "ESP32-S3",
"parts": [{"path": f"https://firmware.wadamesh.com/latest/{binf}", "offset": 0}],
}],
}
with open(os.path.join(outdir, fn), "w") as f:
json.dump(manifest, f, indent=2)
print(f"wrote version.json + 2 manifests for {tag} -> {outdir} (notes: {len(notes)})")
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Cut a lean, source-only MeshCore tag on the meshcomod monorepo for wadamesh to
# pin as its core lib_dep.
#
# Why: the monorepo (ALLFATHER-BV/meshcomod) is the single source of truth for the
# MeshCore core AND the non-touch firmware, but it also carries ~1.2 GB of
# prebuilt/ release bins. PlatformIO SHALLOW-clones a git lib_dep, so pinning main
# (or a normal tag) would pull that 1.2 GB into wadamesh's lib clone every CI run.
# This script creates a tag whose TREE drops prebuilt/ + bootstrap-backup/, so the
# shallow clone is ~12 MB. main is untouched (keeps prebuilt/ for the flasher/OTA);
# the lean commit is reachable only via the tag.
#
# Usage:
# scripts/cut-core-tag.sh core-v1.16.2 [/path/to/meshcomod]
# then bump lib_deps in platformio.ini to '...meshcomod.git#core-v1.16.2' + rebuild.
set -euo pipefail
TAG="${1:?usage: cut-core-tag.sh <tag> e.g. core-v1.16.2 [monorepo-path]}"
MONO="${2:-/Users/kaj/Meshcomod_Touch}"
DROP="prebuilt bootstrap-backup" # heavy, non-core dirs wadamesh never builds
cd "$MONO"
git fetch origin --quiet
WT="$(mktemp -d)/core-tag"
git worktree add --no-checkout -d "$WT" origin/main >/dev/null
cleanup() { git worktree remove "$WT" --force 2>/dev/null || true; git worktree prune; }
trap cleanup EXIT
cd "$WT"
git reset -q --mixed origin/main # populate index from main (no 1.2 GB checkout)
git rm -r --cached --quiet $DROP
git commit -q -m "build(core): source-only snapshot for the wadamesh core pin ($TAG)
Drops $DROP from the tree wadamesh shallow-clones as its MeshCore lib_dep.
main keeps them for the flasher/OTA; this commit is reachable only via the tag."
# sanity: prebuilt gone, core intact
[ "$(git ls-tree -r --name-only HEAD | grep -c '^prebuilt/')" = 0 ] || { echo "!! prebuilt still present"; exit 1; }
[ "$(git ls-tree -r --name-only HEAD | grep -c '^src/')" -gt 100 ] || { echo "!! src/ missing — aborting"; exit 1; }
git tag -a "$TAG" -m "MeshCore core lib — lean source-only snapshot for wadamesh"
git push origin "$TAG"
echo "pushed lean tag $TAG -> $(git rev-parse --short HEAD)"
echo "next: set lib_deps in wadamesh/platformio.ini to '...meshcomod.git#$TAG' and rebuild."
+5
View File
@@ -49,6 +49,11 @@ print(json.dumps([{"name": b, "type": "dir"} for b in betas], indent=2))
PY
echo "listing: $(python3 -c 'import json,sys;print(", ".join(x["name"] for x in json.load(open(sys.argv[1]))))' "$REL/index.json")"
# 3b. Web-flasher metadata (version.json + esp-web-tools manifests) — must roll
# with the release so the flasher shows the current version + notes, not a
# 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"
# 4. Publish to the VPS (Cloudflare caches at the edge).
if [ -n "$DEST" ]; then
rsync -av "$OUT/" "$DEST:$DEST_PATH/"
+14 -5
View File
@@ -1359,8 +1359,10 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
#if defined(DISPLAY_CLASS)
// Diagnostic: log EVERY received frame so we can prove what reaches the
// radio. Header byte layout is
// [hasXportCodes:1][reserved:1][payload_type:4][route_type:2]
// so payload_type = (raw[0]>>2)&0x0F and route_type = raw[0]&0x03.
// [version:2][payload_type:4][route_type:2] (bits 7..0)
// so payload_type = (raw[0]>>2)&0x0F and route_type = raw[0]&0x03. The 4
// transport-code bytes follow the header iff route_type is a TRANSPORT_* one
// (there is NO "hasXportCodes" bit at 0x80 — that's the top of the version field).
if (_ui && len > 0) {
const uint8_t ptype = (raw[0] >> 2) & 0x0F;
const uint8_t route = raw[0] & 0x03;
@@ -1390,7 +1392,8 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
// Header is byte 0; transport codes (if present) are 1-4; path_len is
// next; then path bytes; then payload. We just want the first byte of
// the payload as dest_hash for TXT/RSP/ACK style packets.
const bool has_xport = (raw[0] & 0x80) != 0;
const uint8_t rt0 = raw[0] & 0x03;
const bool has_xport = (rt0 == ROUTE_TYPE_TRANSPORT_FLOOD || rt0 == ROUTE_TYPE_TRANSPORT_DIRECT);
int payload_start = 1 + (has_xport ? 4 : 0);
if (payload_start < len) {
uint8_t path_byte = raw[payload_start];
@@ -1421,7 +1424,9 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
// and bump the matching sent message's count. The fp ring only holds our own
// recent sends, so other nodes' traffic can't false-match.
if (len > 0 && isMsgFloodType((raw[0] >> 2) & 0x0F)) {
const bool has_xp = (raw[0] & 0x80) != 0;
const uint8_t rt = raw[0] & 0x03; // route_type (PH_ROUTE_MASK)
const bool has_xp = (rt == ROUTE_TYPE_TRANSPORT_FLOOD || // 4 transport-code bytes follow the
rt == ROUTE_TYPE_TRANSPORT_DIRECT); // header iff route is a TRANSPORT_* one
const int ps = 1 + (has_xp ? 4 : 0);
if (ps < len) {
const uint8_t pb = raw[ps];
@@ -1724,7 +1729,11 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix));
strcpy(p->name, contact.name);
// contact.name is from an over-the-air advert and may fill its 32-byte field
// with no NUL terminator; an unbounded strcpy would then run past p->name and
// corrupt the advert_paths table (garbled "Found"/recently-heard entries).
strncpy(p->name, contact.name, sizeof(p->name) - 1);
p->name[sizeof(p->name) - 1] = '\0';
p->recv_timestamp = getRTCClock()->getCurrentTime();
p->path_len = path_len;
memcpy(p->path, path, p->path_len);
+3 -2
View File
@@ -480,8 +480,9 @@ public:
_ui_trace_ping_tag = tag;
mesh::Packet* pkt = createTrace(tag, 0, flags);
if (!pkt) { _ui_trace_ping_tag = 0; return 0; }
if (c.out_path_len != OUT_PATH_UNKNOWN && c.out_path_len > 0) {
sendDirect(pkt, c.out_path, c.out_path_len); // walk the full route
if (c.out_path_len != OUT_PATH_UNKNOWN && c.out_path_len > 0 &&
c.out_path_len <= MAX_PATH_SIZE) { // clamp: a corrupt out_path_len (>64)
sendDirect(pkt, c.out_path, c.out_path_len); // would overrun sendDirect's payload memcpy -> reboot
} else {
uint8_t hash_sz = (uint8_t)(_prefs.path_hash_mode + 1);
if (hash_sz == 0 || hash_sz > 4) hash_sz = 1;
+269 -178
View File
@@ -6,17 +6,218 @@
#include "SdNvsPrefs.h" // NVS, or SD /meshcomod fallback when NVS is unusable (Launcher)
#include <stddef.h> // offsetof
#include <string.h> // memcpy
static const char* TOUCH_NS = "touch";
static const char* KEY_SCR_TO = "scr_to_s";
static const char* KEY_DC_SHOW = "dc_show";
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
static const bool DEFAULT_DC_SHOW = true;
static SdNvsPrefs s_prefs;
static bool s_begun = false;
// ---------------------------------------------------------------------------
// Packed scalar config blob ("cfg")
// ---------------------------------------------------------------------------
//
// NVS is a tiny (~20 KB) partition shared across firmwares; every distinct key
// costs an entry and the namespace was filling up. The many per-key SCALAR
// settings (brightness, kb backlight, accent colour, language, …) are therefore
// packed into ONE versioned blob keyed "cfg". Strings (tile_srv, rgn_scope,
// lk_wall, channel-scopes, quick-replies), the byte blobs (fav / ign / rpw) and
// the Wi-Fi slots keep their own keys — and "use_sd" / "setup_ok" stay standalone
// keys too, because main.cpp reads (and for use_sd, writes) them directly via a
// raw Preferences open at boot, BEFORE the touch prefs load.
//
// On first run with the blob absent we read every legacy per-key into s_cfg, write
// "cfg" ONCE, and only after that durable write do we remove() the legacy keys to
// reclaim their entries. The write-before-remove ordering makes the migration
// crash-safe and idempotent (a power-cut before the write just re-migrates next
// boot; one after it finds "cfg" present and skips). `magic` rejects a garbage /
// 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 = 1;
// Defaults (kept identical to the historical per-key defaults).
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
static const uint8_t DEFAULT_BRIGHTNESS = 100;
static const uint8_t DEFAULT_KB_BL = 2; // auto
static const uint8_t DEFAULT_KB_LAYOUT = 0; // English
static const uint8_t DEFAULT_KB_SECONDARY = 0; // None
static const uint32_t DEFAULT_LOCK_COLOR = 0xE6F2FFu; // soft white
static const uint32_t DEFAULT_ACCENT = 0x15B6A6u; // brand teal
static const bool DEFAULT_DC_SHOW = true;
struct __attribute__((packed)) TouchCfg {
uint16_t magic; // TOUCH_CFG_MAGIC — rejects a garbage/short read
uint8_t ver; // TOUCH_CFG_VER — schema version
uint8_t bright; // "bright" 1..100 (clamped 5..100 on get/set)
uint8_t kb_bl; // "kb_bl" 0..2
uint8_t kb_layout; // "kblang"
uint8_t kb_secondary; // "kbsec"
uint8_t ui_lang; // "ui_lang"
uint8_t ui_rotation; // "uirot" 0..3
uint8_t dc_show; // "dc_show" bool
uint8_t use_miles; // "use_miles" bool
uint8_t tiles_from_sd; // "tiles_sd" bool
uint8_t clr_bubbles; // "clr_bub" bool
uint8_t kb_accent; // "kb_accent" bool
int8_t time_offs; // "time_offs" -23..23
uint16_t scr_to_s; // "scr_to_s"
uint16_t kb_enabled; // "kbenab" bitmask
uint16_t batt_full_mv; // "battfull"
uint32_t lock_color; // "lk_col" 0xRRGGBB
uint32_t accent; // "accent" 0xRRGGBB
uint32_t gps_baud; // "gps_baud" 0 = unset -> caller fallback
};
static TouchCfg s_cfg;
static bool s_cfg_loaded = false;
// Legacy per-key names — only referenced by the one-time migration below.
static const char* LK_SCR_TO = "scr_to_s";
static const char* LK_DC_SHOW = "dc_show";
static const char* LK_BRIGHTNESS = "bright";
static const char* LK_KB_BL = "kb_bl";
static const char* LK_KB_LAYOUT = "kblang";
static const char* LK_KB_SECONDARY = "kbsec";
static const char* LK_KB_ENABLED = "kbenab";
static const char* LK_LOCK_COLOR = "lk_col";
static const char* LK_CLR_BUBBLES = "clr_bub";
static const char* LK_KB_ACCENT = "kb_accent";
static const char* LK_ACCENT = "accent";
static const char* LK_TIME_OFFS = "time_offs";
static const char* LK_USE_MILES = "use_miles";
static const char* LK_TILES_FROM_SD= "tiles_sd";
static const char* LK_UI_LANG = "ui_lang";
static const char* LK_UI_ROTATION = "uirot";
static const char* LK_BATT_FULL = "battfull";
static const char* LK_GPS_BAUD = "gps_baud";
static void cfgSetDefaults(TouchCfg& c) {
c.magic = TOUCH_CFG_MAGIC;
c.ver = TOUCH_CFG_VER;
c.bright = DEFAULT_BRIGHTNESS;
c.kb_bl = DEFAULT_KB_BL;
c.kb_layout = DEFAULT_KB_LAYOUT;
c.kb_secondary = DEFAULT_KB_SECONDARY;
c.ui_lang = 0;
c.ui_rotation = 0;
c.dc_show = DEFAULT_DC_SHOW ? 1 : 0;
c.use_miles = 0;
c.tiles_from_sd = 0;
c.clr_bubbles = 1; // default ON
c.kb_accent = 1; // default ON
c.time_offs = 0;
c.scr_to_s = DEFAULT_SCREEN_TIMEOUT_S;
c.kb_enabled = 0;
c.batt_full_mv = 0;
c.lock_color = DEFAULT_LOCK_COLOR;
c.accent = DEFAULT_ACCENT;
c.gps_baud = 0; // 0 sentinel -> getter returns caller fallback
}
// Persist the whole blob using the same end()/begin(RW)/put/end()/begin(RO)
// discipline every setter in this file uses. Returns true on a durable write.
static bool cfgFlush() {
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) { s_begun = false; return false; }
bool ok = s_prefs.putBytes(KEY_CFG, &s_cfg, sizeof(s_cfg)) == sizeof(s_cfg);
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
}
// Load "cfg" once; on absence run the one-time legacy migration. Must be called
// with the namespace already open (RO is fine for the load + legacy reads; the
// migration write reopens RW via cfgFlush). Idempotent: a no-op after the first
// successful run because "cfg" then exists.
static void cfgLoadOrMigrate() {
if (s_cfg_loaded) return;
cfgSetDefaults(s_cfg);
// isKey() does NOT emit the [E] NOT_FOUND log that getBytes() would on a miss,
// so probe first to keep the (USB-CDC) console clean on a fresh device.
if (s_prefs.isKey(KEY_CFG)) {
TouchCfg tmp;
memset(&tmp, 0, sizeof(tmp));
size_t n = s_prefs.getBytes(KEY_CFG, &tmp, sizeof(tmp));
// Need at least magic(2)+ver(1) to trust the header; reject anything shorter
// (a half-written / garbage blob) and re-derive from legacy keys / defaults.
if (n >= offsetof(TouchCfg, bright) && tmp.magic == TOUCH_CFG_MAGIC) {
// Copy whatever was stored over the defaults (a shorter, older blob leaves
// 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) {
s_cfg.ver = TOUCH_CFG_VER;
s_cfg.magic = TOUCH_CFG_MAGIC;
cfgFlush(); // rewrite with new fields defaulted-in
}
s_cfg_loaded = true;
return;
}
// Garbage / short / wrong-magic read -> fall through and re-derive from
// legacy keys (or defaults), overwriting the bad blob.
}
// No (valid) "cfg" yet: build it from the legacy per-key values, applying the
// exact same defaults the old getters used. On a fresh device every isKey()
// is false, so this just keeps the defaults set above.
if (s_prefs.isKey(LK_SCR_TO)) s_cfg.scr_to_s = s_prefs.getUShort(LK_SCR_TO, DEFAULT_SCREEN_TIMEOUT_S);
if (s_prefs.isKey(LK_BRIGHTNESS)) s_cfg.bright = s_prefs.getUChar(LK_BRIGHTNESS, DEFAULT_BRIGHTNESS);
if (s_prefs.isKey(LK_KB_BL)) s_cfg.kb_bl = s_prefs.getUChar(LK_KB_BL, DEFAULT_KB_BL);
if (s_prefs.isKey(LK_KB_LAYOUT)) s_cfg.kb_layout = s_prefs.getUChar(LK_KB_LAYOUT, DEFAULT_KB_LAYOUT);
if (s_prefs.isKey(LK_KB_SECONDARY)) s_cfg.kb_secondary = s_prefs.getUChar(LK_KB_SECONDARY, DEFAULT_KB_SECONDARY);
if (s_prefs.isKey(LK_TIME_OFFS)) s_cfg.time_offs = s_prefs.getChar(LK_TIME_OFFS, 0);
if (s_prefs.isKey(LK_LOCK_COLOR)) s_cfg.lock_color = s_prefs.getUInt(LK_LOCK_COLOR, DEFAULT_LOCK_COLOR) & 0xFFFFFFu;
if (s_prefs.isKey(LK_ACCENT)) s_cfg.accent = s_prefs.getUInt(LK_ACCENT, DEFAULT_ACCENT) & 0xFFFFFFu;
if (s_prefs.isKey(LK_CLR_BUBBLES)) s_cfg.clr_bubbles = s_prefs.getBool(LK_CLR_BUBBLES, true) ? 1 : 0;
if (s_prefs.isKey(LK_KB_ACCENT)) s_cfg.kb_accent = s_prefs.getBool(LK_KB_ACCENT, true) ? 1 : 0;
if (s_prefs.isKey(LK_DC_SHOW)) s_cfg.dc_show = s_prefs.getBool(LK_DC_SHOW, DEFAULT_DC_SHOW) ? 1 : 0;
if (s_prefs.isKey(LK_USE_MILES)) s_cfg.use_miles = s_prefs.getBool(LK_USE_MILES, false) ? 1 : 0;
if (s_prefs.isKey(LK_TILES_FROM_SD))s_cfg.tiles_from_sd= s_prefs.getBool(LK_TILES_FROM_SD, false) ? 1 : 0;
if (s_prefs.isKey(LK_UI_LANG)) s_cfg.ui_lang = s_prefs.getUChar(LK_UI_LANG, 0);
if (s_prefs.isKey(LK_UI_ROTATION)) s_cfg.ui_rotation = s_prefs.getUChar(LK_UI_ROTATION, 0);
if (s_prefs.isKey(LK_BATT_FULL)) s_cfg.batt_full_mv = s_prefs.getUShort(LK_BATT_FULL, 0);
if (s_prefs.isKey(LK_GPS_BAUD)) s_cfg.gps_baud = s_prefs.getUInt(LK_GPS_BAUD, 0);
// Enabled-layout mask: legacy used 0xFFFF as the "never written" sentinel and
// derived a one-bit mask from the secondary layout. Reproduce that here.
{
uint16_t v = s_prefs.isKey(LK_KB_ENABLED) ? s_prefs.getUShort(LK_KB_ENABLED, 0xFFFF) : 0xFFFF;
if (v == 0xFFFF) {
uint8_t sec = s_cfg.kb_secondary;
s_cfg.kb_enabled = (sec != 0 && sec < 16) ? (uint16_t)(1u << sec) : 0;
} else {
s_cfg.kb_enabled = v;
}
}
// Write "cfg" ONCE. Only after a durable write do we reclaim the legacy keys.
// If the write fails (e.g. NVS full / SD missing) we keep the legacy keys
// intact and retry the whole migration on the next boot.
if (cfgFlush()) {
s_prefs.end();
if (s_prefs.begin(TOUCH_NS, false)) {
const char* legacy[] = {
LK_SCR_TO, LK_DC_SHOW, LK_BRIGHTNESS, LK_KB_BL, LK_KB_LAYOUT,
LK_KB_SECONDARY, LK_KB_ENABLED, LK_LOCK_COLOR, LK_CLR_BUBBLES,
LK_KB_ACCENT, LK_ACCENT, LK_TIME_OFFS, LK_USE_MILES, LK_TILES_FROM_SD,
LK_UI_LANG, LK_UI_ROTATION, LK_BATT_FULL, LK_GPS_BAUD,
};
for (const char* k : legacy) {
if (s_prefs.isKey(k)) s_prefs.remove(k);
}
s_prefs.end();
}
s_begun = s_prefs.begin(TOUCH_NS, true);
}
s_cfg_loaded = true;
}
void touchPrefsBegin() {
if (s_begun) return;
if (s_begun) {
if (!s_cfg_loaded) cfgLoadOrMigrate();
return;
}
s_begun = s_prefs.begin(TOUCH_NS, true);
if (!s_begun) {
/* Namespace may not exist yet — open RW once to create it, then reopen RO. */
@@ -25,6 +226,7 @@ void touchPrefsBegin() {
s_begun = s_prefs.begin(TOUCH_NS, true);
}
}
if (s_begun) cfgLoadOrMigrate();
}
// Arduino's Preferences::getString()/getBytes() emit an [E] nvs_get_* "NOT_FOUND"
@@ -38,25 +240,18 @@ static String prefsGetStr(const char* key, const String& def) {
uint16_t touchPrefsGetScreenTimeoutSecs() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getUShort(KEY_SCR_TO, DEFAULT_SCREEN_TIMEOUT_S);
return s_cfg.scr_to_s;
}
bool touchPrefsSetScreenTimeoutSecs(uint16_t seconds) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUShort(KEY_SCR_TO, seconds) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.scr_to_s = seconds;
return cfgFlush();
}
static const char* KEY_BRIGHTNESS = "bright";
static const uint8_t DEFAULT_BRIGHTNESS = 100;
uint8_t touchPrefsGetBrightness() {
if (!s_begun) touchPrefsBegin();
uint8_t v = s_prefs.getUChar(KEY_BRIGHTNESS, DEFAULT_BRIGHTNESS);
uint8_t v = s_cfg.bright;
if (v < 5) v = 5;
if (v > 100) v = 100;
return v;
@@ -66,94 +261,57 @@ bool touchPrefsSetBrightness(uint8_t pct) {
if (pct < 5) pct = 5;
if (pct > 100) pct = 100;
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUChar(KEY_BRIGHTNESS, pct) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.bright = pct;
return cfgFlush();
}
static const char* KEY_KB_BL = "kb_bl";
static const uint8_t DEFAULT_KB_BL = 2; // auto
uint8_t touchPrefsGetKbBacklight() {
if (!s_begun) touchPrefsBegin();
uint8_t v = s_prefs.getUChar(KEY_KB_BL, DEFAULT_KB_BL);
uint8_t v = s_cfg.kb_bl;
return v > 2 ? DEFAULT_KB_BL : v;
}
bool touchPrefsSetKbBacklight(uint8_t mode) {
if (mode > 2) mode = DEFAULT_KB_BL;
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUChar(KEY_KB_BL, mode) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.kb_bl = mode;
return cfgFlush();
}
static const char* KEY_KB_LAYOUT = "kblang";
static const uint8_t DEFAULT_KB_LAYOUT = 0; // English
uint8_t touchPrefsGetKeyboardLayout() {
if (!s_begun) touchPrefsBegin();
uint8_t v = s_prefs.getUChar(KEY_KB_LAYOUT, DEFAULT_KB_LAYOUT);
return v;
return s_cfg.kb_layout;
}
bool touchPrefsSetKeyboardLayout(uint8_t layout) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUChar(KEY_KB_LAYOUT, layout) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.kb_layout = layout;
return cfgFlush();
}
static const char* KEY_KB_SECONDARY = "kbsec";
static const uint8_t DEFAULT_KB_SECONDARY = 0; // None
uint8_t touchPrefsGetSecondaryKeyboard() {
if (!s_begun) touchPrefsBegin();
uint8_t v = s_prefs.getUChar(KEY_KB_SECONDARY, DEFAULT_KB_SECONDARY);
return v;
return s_cfg.kb_secondary;
}
bool touchPrefsSetSecondaryKeyboard(uint8_t secondary) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUChar(KEY_KB_SECONDARY, secondary) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.kb_secondary = secondary;
return cfgFlush();
}
static const char* KEY_KB_ENABLED = "kbenab";
uint16_t touchPrefsGetEnabledLayouts() {
if (!s_begun) touchPrefsBegin();
// 0xFFFF is never a valid mask (only 7 low bits are used), so use it as the
// "never written" sentinel: migrate the legacy single-secondary into a mask.
uint16_t v = s_prefs.getUShort(KEY_KB_ENABLED, 0xFFFF);
if (v == 0xFFFF) {
uint8_t sec = s_prefs.getUChar(KEY_KB_SECONDARY, 0);
return (sec != 0 && sec < 16) ? (uint16_t)(1u << sec) : 0;
}
return v;
// The legacy "never written -> derive a one-bit mask from the secondary
// layout" migration ran once at cfg-migration time (see cfgLoadOrMigrate);
// the resolved mask now lives in s_cfg.kb_enabled.
return s_cfg.kb_enabled;
}
bool touchPrefsSetEnabledLayouts(uint16_t mask) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUShort(KEY_KB_ENABLED, mask) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.kb_enabled = mask;
return cfgFlush();
}
static const char* KEY_TILE_SRV = "tile_srv";
@@ -268,11 +426,9 @@ bool touchPrefsSetLockWallpaper(const char* path) {
return ok;
}
static const char* KEY_TIME_OFFS = "time_offs";
int touchPrefsGetTimeOffsetHours() {
if (!s_begun) touchPrefsBegin();
int v = (int)s_prefs.getChar(KEY_TIME_OFFS, 0);
int v = (int)s_cfg.time_offs;
if (v < -23) v = -23;
if (v > 23) v = 23;
return v;
@@ -281,12 +437,8 @@ bool touchPrefsSetTimeOffsetHours(int hours) {
if (hours < -23) hours = -23;
if (hours > 23) hours = 23;
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putChar(KEY_TIME_OFFS, (int8_t)hours) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.time_offs = (int8_t)hours;
return cfgFlush();
}
void touchPrefsBuildLocalTz(char* out, int out_cap) {
if (!out || out_cap <= 0) return;
@@ -297,77 +449,52 @@ void touchPrefsBuildLocalTz(char* out, int out_cap) {
snprintf(out, out_cap, "CET%dCEST,M3.5.0,M10.5.0/3", -(1 + off));
}
static const char* KEY_LOCK_COLOR = "lk_col";
static const uint32_t DEFAULT_LOCK_COLOR = 0xE6F2FFu; // soft white
uint32_t touchPrefsGetLockTextColor() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getUInt(KEY_LOCK_COLOR, DEFAULT_LOCK_COLOR) & 0xFFFFFFu;
return s_cfg.lock_color & 0xFFFFFFu;
}
bool touchPrefsSetLockTextColor(uint32_t rgb) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUInt(KEY_LOCK_COLOR, rgb & 0xFFFFFFu) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.lock_color = rgb & 0xFFFFFFu;
return cfgFlush();
}
// Colourful chat bubbles: colour each bubble + sender name by a hash of the
// sender's display name (same name -> same colour). Default ON. (getBool is
// log_v on a miss, so no NOT_FOUND console spam.)
static const char* KEY_CLR_BUBBLES = "clr_bub";
// sender's display name (same name -> same colour). Default ON.
bool touchPrefsGetColorfulBubbles() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getBool(KEY_CLR_BUBBLES, true);
return s_cfg.clr_bubbles != 0;
}
bool touchPrefsSetColorfulBubbles(bool on) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putBool(KEY_CLR_BUBBLES, on) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.clr_bubbles = on ? 1 : 0;
return cfgFlush();
}
// Keyboard accent-popup picker: when a typed Latin letter has accented variants,
// a tap-to-pick box appears. Default ON. (getBool is log_v on a miss, so no
// NOT_FOUND console spam.) Key is distinct from KEY_ACCENT (the theme colour).
static const char* KEY_KB_ACCENT = "kb_accent";
// a tap-to-pick box appears. Default ON. (Distinct from the accent THEME colour.)
bool touchPrefsGetAccentPopups() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getBool(KEY_KB_ACCENT, true);
return s_cfg.kb_accent != 0;
}
bool touchPrefsSetAccentPopups(bool on) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putBool(KEY_KB_ACCENT, on) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.kb_accent = on ? 1 : 0;
return cfgFlush();
}
// UI accent colour (buttons, active tab, keyboard, highlights) as 0xRRGGBB.
// Default = the WADAMESH brand teal (the logo dots). The picker clamps it dark
// enough that the off-white button text stays readable on any hue.
static const char* KEY_ACCENT = "accent";
static const uint32_t DEFAULT_ACCENT = 0x15B6A6u;
uint32_t touchPrefsGetAccentColor() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getUInt(KEY_ACCENT, DEFAULT_ACCENT) & 0xFFFFFFu;
return s_cfg.accent & 0xFFFFFFu;
}
bool touchPrefsSetAccentColor(uint32_t rgb) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUInt(KEY_ACCENT, rgb & 0xFFFFFFu) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.accent = rgb & 0xFFFFFFu;
return cfgFlush();
}
// Quick-reply macros. Stored as NVS strings keyed "qr0".."qr5".
@@ -410,50 +537,35 @@ int touchPrefsGetQuickReply(int idx, char* out, int out_cap) {
bool touchPrefsGetDutyMeterShown() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getBool(KEY_DC_SHOW, DEFAULT_DC_SHOW);
return s_cfg.dc_show != 0;
}
bool touchPrefsSetDutyMeterShown(bool show) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putBool(KEY_DC_SHOW, show);
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.dc_show = show ? 1 : 0;
return cfgFlush();
}
static const char* KEY_USE_MILES = "use_miles";
static const char* KEY_TILES_FROM_SD = "tiles_sd";
bool touchPrefsGetUseMiles() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getBool(KEY_USE_MILES, false); // default = km
return s_cfg.use_miles != 0; // default = km
}
bool touchPrefsSetUseMiles(bool use_miles) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putBool(KEY_USE_MILES, use_miles);
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.use_miles = use_miles ? 1 : 0;
return cfgFlush();
}
bool touchPrefsGetTilesFromSd() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getBool(KEY_TILES_FROM_SD, false); // default = tile server
return s_cfg.tiles_from_sd != 0; // default = tile server
}
bool touchPrefsSetTilesFromSd(bool from_sd) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putBool(KEY_TILES_FROM_SD, from_sd);
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.tiles_from_sd = from_sd ? 1 : 0;
return cfgFlush();
}
// Store ALL device data (identity, prefs, contacts, channels) on the SD card
@@ -478,56 +590,39 @@ bool touchPrefsSetUseSdStorage(bool use_sd) {
}
// UI language index (UiLang enum in i18n.h; 0 = English). Read at boot to pick
// the active translation language. Key "ui_lang" in the "touch" namespace.
static const char* KEY_UI_LANG = "ui_lang";
// the active translation language. Packed into the "cfg" blob.
uint8_t touchPrefsGetUiLang() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getUChar(KEY_UI_LANG, 0); // default = English
return s_cfg.ui_lang; // default = English
}
bool touchPrefsSetUiLang(uint8_t lang) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUChar(KEY_UI_LANG, lang) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.ui_lang = lang;
return cfgFlush();
}
static const char* KEY_UI_ROTATION = "uirot";
uint8_t touchPrefsGetUiRotation() {
if (!s_begun) touchPrefsBegin();
uint8_t r = s_prefs.getUChar(KEY_UI_ROTATION, 0); // default = portrait
uint8_t r = s_cfg.ui_rotation; // default = portrait
return (r <= 3) ? r : 0;
}
bool touchPrefsSetUiRotation(uint8_t rot) {
if (rot > 3) rot = 0;
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUChar(KEY_UI_ROTATION, rot);
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.ui_rotation = rot;
return cfgFlush();
}
static const char* KEY_BATT_FULL = "battfull";
uint16_t touchPrefsGetBattFullMv() {
if (!s_begun) touchPrefsBegin();
return s_prefs.getUShort(KEY_BATT_FULL, 0); // 0 = not calibrated -> default 4200
return s_cfg.batt_full_mv; // 0 = not calibrated -> default 4200
}
bool touchPrefsSetBattFullMv(uint16_t mv) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUShort(KEY_BATT_FULL, mv) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.batt_full_mv = mv;
return cfgFlush();
}
// Wi-Fi profile slots ----------------------------------------------------
@@ -884,21 +979,17 @@ bool touchPrefsSetSetupDone(bool done) {
return ok;
}
static const char* KEY_GPS_BAUD = "gps_baud";
uint32_t touchPrefsGetGpsBaud(uint32_t fallback) {
if (!s_begun) touchPrefsBegin();
return s_prefs.getUInt(KEY_GPS_BAUD, fallback);
// 0 = never set -> caller's compile-time default. A real configured baud is
// always non-zero, so this preserves the old "absent key -> fallback" result.
return s_cfg.gps_baud != 0 ? s_cfg.gps_baud : fallback;
}
bool touchPrefsSetGpsBaud(uint32_t baud) {
if (!s_begun) touchPrefsBegin();
s_prefs.end();
if (!s_prefs.begin(TOUCH_NS, false)) return false;
bool ok = s_prefs.putUInt(KEY_GPS_BAUD, baud) > 0;
s_prefs.end();
s_begun = s_prefs.begin(TOUCH_NS, true);
return ok;
s_cfg.gps_baud = baud;
return cfgFlush();
}
#endif
+4 -2
View File
@@ -226,11 +226,13 @@ void setup() {
uint32_t bn = prefs.getUInt("n", 0);
++bn;
prefs.putUInt("n", bn);
size_t nvs_free = prefs.freeEntries(); // NVS partition headroom; near 0 = full (the boot-loop trigger)
prefs.end();
meshcomod_touch_set_boot_stats(bn, static_cast<uint8_t>(esp_reset_reason()));
Serial.printf("[BOOT] touch_boot_n=%lu reason=%u\n",
Serial.printf("[BOOT] touch_boot_n=%lu reason=%u nvs_free_entries=%u\n",
static_cast<unsigned long>(bn),
static_cast<unsigned>(esp_reset_reason()));
static_cast<unsigned>(esp_reset_reason()),
static_cast<unsigned>(nvs_free));
}
the_mesh.initTxtTxUniquenessFromRng();
}
+17 -3
View File
@@ -4858,7 +4858,7 @@ static void sysInfoText(char* buf, size_t cap) {
"Last reset\n %s\n\n", resetReasonString(esp_reset_reason()));
#endif
p += snprintf(buf + p, cap - p,
"Build\n %s\n %s\n Meshcomod TOUCH\n",
"Build\n %s\n %s\n WADAMESH TOUCH\n",
FIRMWARE_VERSION, FIRMWARE_BUILD_DATE);
(void)p;
}
@@ -6097,6 +6097,19 @@ static void wifiScanService() {
wifiScanFillList(); // no-op if the popup was closed
}
// Live Wi-Fi radio toggle on the Wi-Fi settings page (mirrors the control-center
// toggle). wifiConfigSetRadioEnabled persists the pref + flags an apply, so the
// main loop brings esp_wifi up / down on the spot — no Save needed, no reboot.
static void wifiRadioToggleCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED);
wifiConfigSetRadioEnabled(on);
if (g_lv.task) g_lv.task->showAlert(on ? TR("Wi-Fi on") : TR("Wi-Fi off"), 800);
refreshStatusLabels();
#endif
}
static void buildWifiSettings() {
lv_obj_t* body = createSettingsModal("Wi-Fi", SettingsModalKind::Wifi);
int y = 0;
@@ -6112,6 +6125,7 @@ static void buildWifiSettings() {
g_set_modal.wifi_sw = lv_switch_create(body);
lv_obj_align(g_set_modal.wifi_sw, LV_ALIGN_TOP_RIGHT, 0, y); // toggle next to "Status"
if (wifiConfigGetRadioEnabled()) lv_obj_add_state(g_set_modal.wifi_sw, LV_STATE_CHECKED);
lv_obj_add_event_cb(g_set_modal.wifi_sw, wifiRadioToggleCb, LV_EVENT_VALUE_CHANGED, nullptr); // live on/off — no Save needed
y += 30;
g_set_modal.wifi_sta_status_l = lv_label_create(body);
@@ -9061,7 +9075,7 @@ static struct { fs::FS* fs; char path[200]; char name[64]; bool isdir; bool is_c
s_fm_clip = {};
static const char* k_term_banner =
"Meshcomod terminal - CLI + chat on this node.\n"
"WADAMESH terminal - CLI + chat on this node.\n"
"Type a command or tap the list icon for the picker.\n"
"Chat: 'to <name>' joins a contact/channel, then just\n"
" type to send. 'exit' leaves. list / channels.\n"
@@ -18401,7 +18415,7 @@ static void setupShowStep(int step) {
const lv_coord_t btn_y = sh - kSetupBtnH - 10;
if (step == 0) {
setupHeader("Welcome to Meshcomod", nullptr, nullptr);
setupHeader("Welcome to WADAMESH", nullptr, nullptr);
lv_obj_t* m = lv_label_create(s_setup_root);
lv_label_set_text(m,
"Let's set up your device.\n\n"
+1 -1
View File
@@ -346,7 +346,7 @@ static const I18nEntry kI18n[] = {
{ "Sound off", { 0, "Geluid uit","Ton aus","Son désactivé","Sonido apagado","Suono spento","Звук выкл","Звук вимк","Звук изкл","Звук искљ","Ήχος όχι" } },
{ "TCP on", { 0, "TCP aan","TCP ein","TCP activé","TCP activado","TCP acceso","TCP вкл","TCP увімк","TCP вкл","TCP укљ","TCP ναι" } },
{ "TCP off", { 0, "TCP uit","TCP aus","TCP désactivé","TCP apagado","TCP spento","TCP выкл","TCP вимк","TCP изкл","TCP искљ","TCP όχι" } },
{ "Welcome to Meshcomod", { 0, "Welkom bij Meshcomod","Willkommen bei Meshcomod","Bienvenue sur Meshcomod","Bienvenido a Meshcomod","Benvenuto in Meshcomod","Добро пожаловать в Meshcomod","Ласкаво просимо до Meshcomod","Добре дошли в Meshcomod","Добро дошли у Meshcomod","Καλώς ήρθατε στο Meshcomod" } },
{ "Welcome to WADAMESH", { 0, "Welkom bij WADAMESH","Willkommen bei WADAMESH","Bienvenue sur WADAMESH","Bienvenido a WADAMESH","Benvenuto in WADAMESH","Добро пожаловать в WADAMESH","Ласкаво просимо до WADAMESH","Добре дошли в WADAMESH","Добро дошли у WADAMESH","Καλώς ήρθατε στο WADAMESH" } },
{ "Get Started", { 0, "Aan de slag","Loslegen","Commencer","Empezar","Inizia","Начать","Почати","Започни","Започни","Ξεκινήστε" } },
{ "Choose your name", { 0, "Kies je naam","Wählen Sie Ihren Namen","Choisissez votre nom","Elige tu nombre","Scegli il tuo nome","Выберите имя","Виберіть ім'я","Изберете името си","Изаберите своје име","Επιλέξτε το όνομά σας" } },
{ "Your name", { 0, "Je naam","Ihr Name","Votre nom","Tu nombre","Il tuo nome","Ваше имя","Ваше ім'я","Вашето име","Ваше име","Το όνομά σας" } },