Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
058721fdbc | ||
|
|
ff195bbaa2 | ||
|
|
77a7420c96 | ||
|
|
86c07560e5 | ||
|
|
ec3b47bbc2 | ||
|
|
9e2eed1551 | ||
|
|
58bae913e1 | ||
|
|
6088370f5d | ||
|
|
2c099aadb6 | ||
|
|
a405513d84 | ||
|
|
399b5004e0 |
No files matched your search
@@ -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"
|
||||
@@ -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
@@ -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 & 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.
|
||||
|
||||
@@ -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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)})")
|
||||
Executable
+44
@@ -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."
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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","Ваше имя","Ваше ім'я","Вашето име","Ваше име","Το όνομά σας" } },
|
||||
|
||||
Reference in new issue
Block a user