Compare commits
No files matched your search
+14
-1
@@ -3,12 +3,25 @@
|
||||
Distribution stack for wadamesh: a **VPS nginx origin behind Cloudflare**.
|
||||
|
||||
```
|
||||
tiles.wadamesh.com →CF (HTTP, edge-cached) → nginx → tile.openstreetmap.org
|
||||
tiles.wadamesh.com →CF (HTTP, edge-cached) → nginx → OpenStreetMap / OpenTopoMap
|
||||
firmware.wadamesh.com →CF (cache bins) → nginx → /srv/wadamesh/firmware
|
||||
flasher.wadamesh.com →CF (HTTPS) → web flasher (TODO — see below)
|
||||
wadamesh.com →CF (HTTPS) → landing page (later)
|
||||
```
|
||||
|
||||
**Map tile styles.** The default `/{z}/{x}/{y}.jpg` route serves **OpenStreetMap**
|
||||
(the firmware default). An opt-in **OpenTopoMap** topographic style is served from
|
||||
`/opentopo/{z}/{x}/{y}.jpg` (explicit OSM alias at `/osm/...`); the device requests
|
||||
it only when the user enables *Map → Options → Topographic map*. Legal: OpenTopoMap
|
||||
map tiles are **© OpenTopoMap (CC-BY-SA)** over **© OpenStreetMap contributors
|
||||
(ODbL) + SRTM** — the touch UI shows that attribution when topo is active, and the
|
||||
14-day disk cache keeps each tile hitting OpenTopoMap at most once per fortnight
|
||||
(their tile-usage policy asks for a contactable UA + caching, both of which the
|
||||
transcode service provides). Deploying the topo routes = update
|
||||
`tiles.wadamesh.com.conf` + `tile-transcode.py`, then
|
||||
`systemctl restart wadamesh-tile-transcode && nginx -t && systemctl reload nginx`
|
||||
and purge the Cloudflare cache for `tiles.wadamesh.com/opentopo/*`.
|
||||
|
||||
The firmware fetches **tiles + the update-check over plain HTTP** (on-device HTTPS
|
||||
isn't viable — mbedTLS needs ~30 KB heap, only ~5 KB is free post-Wi-Fi), so the
|
||||
tile + firmware hosts must stay reachable over HTTP. Cloudflare provides the edge
|
||||
|
||||
@@ -16,19 +16,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_15": {
|
||||
"notes": "Power-saving now shows as an amber battery icon in the status bar — like iPhone's Low Power Mode — instead of a separate moon icon (T-Deck).\nNew @-mention picker: type @ in the message box to pick a contact from a list and drop their name in (thanks robinshizuyo-dev, #42).\nClosing a settings item, menu or chat no longer snaps the list back to the top — it stays where you left it (thanks ZorkaTheDog, #45).\nMessages you send from the MeshCore companion app now also show up on the device, in the right conversation (thanks HonzaDandy, #46).\nTyping is more reliable on the T-Deck and Tanmatsu: tapping a text field — even one that's already highlighted — starts typing right away, and the accent pop-up no longer interrupts typing outside of chats.\nThe text cursor now only appears when a field is actually ready for input, so it's a dependable \"you can type now\" cue.\nSmoother map panning: tile rendering no longer trips the watchdog and reboots during heavy scrolling.\nKeyboard navigation is now OFF by default. If you prefer driving the UI with the trackball / arrow keys, switch it back on in Settings -> Keyboard (your choice sticks until you change it). The Tanmatsu keeps it on, since it has no touchscreen.\nIncludes everything from beta_14 and the earlier betas.",
|
||||
"beta_19": {
|
||||
"notes": "Radio & Mesh settings now show an airtime / duty-cycle readout: roughly how much of the airtime budget your current spreading factor, bandwidth and coding rate use, plus the time-on-air per packet — handy for staying inside regional limits like EU 868.\nA new \"Answer telemetry requests\" toggle in Radio & Mesh settings controls whether other nodes can pull your battery and environment telemetry.\nThe Radio & Mesh settings page is now split into clear RADIO / MESH / SIGNAL sections so the options are easier to find.\nFixed a reboot that could happen when you closed a scrollable popup — the emoji picker, quick replies, the Wi-Fi scan list, the app drawer and similar — while it was still coasting from a flick. Thanks to everyone who sent in crash reports.\nThe accent popup no longer lingers on screen after you send a message on the T-Deck keyboard.\nHeltec V4: the Expansion Kit environment line and mini chart on the Home screen no longer overlap the status text or run off the bottom of the display.\nEverything from beta_18 is included.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_15/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_19/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_15/wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_19/wadamesh-heltec-v4-tft.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
@@ -48,19 +48,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_15": {
|
||||
"notes": "Power-saving now shows as an amber battery icon in the status bar — like iPhone's Low Power Mode — instead of a separate moon icon (T-Deck).\nNew @-mention picker: type @ in the message box to pick a contact from a list and drop their name in (thanks robinshizuyo-dev, #42).\nClosing a settings item, menu or chat no longer snaps the list back to the top — it stays where you left it (thanks ZorkaTheDog, #45).\nMessages you send from the MeshCore companion app now also show up on the device, in the right conversation (thanks HonzaDandy, #46).\nTyping is more reliable on the T-Deck and Tanmatsu: tapping a text field — even one that's already highlighted — starts typing right away, and the accent pop-up no longer interrupts typing outside of chats.\nThe text cursor now only appears when a field is actually ready for input, so it's a dependable \"you can type now\" cue.\nSmoother map panning: tile rendering no longer trips the watchdog and reboots during heavy scrolling.\nKeyboard navigation is now OFF by default. If you prefer driving the UI with the trackball / arrow keys, switch it back on in Settings -> Keyboard (your choice sticks until you change it). The Tanmatsu keeps it on, since it has no touchscreen.\nIncludes everything from beta_14 and the earlier betas.",
|
||||
"beta_19": {
|
||||
"notes": "Radio & Mesh settings now show an airtime / duty-cycle readout: roughly how much of the airtime budget your current spreading factor, bandwidth and coding rate use, plus the time-on-air per packet — handy for staying inside regional limits like EU 868.\nA new \"Answer telemetry requests\" toggle in Radio & Mesh settings controls whether other nodes can pull your battery and environment telemetry.\nThe Radio & Mesh settings page is now split into clear RADIO / MESH / SIGNAL sections so the options are easier to find.\nFixed a reboot that could happen when you closed a scrollable popup — the emoji picker, quick replies, the Wi-Fi scan list, the app drawer and similar — while it was still coasting from a flick. Thanks to everyone who sent in crash reports.\nThe accent popup no longer lingers on screen after you send a message on the T-Deck keyboard.\nHeltec V4: the Expansion Kit environment line and mini chart on the Home screen no longer overlap the status text or run off the bottom of the display.\nEverything from beta_18 is included.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_15/wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_19/wadamesh-tdeck-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_15/wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_19/wadamesh-tdeck.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# tiles.wadamesh.com — OSM tile proxy for the wadamesh map tab.
|
||||
# tiles.wadamesh.com — map-tile proxy for the wadamesh map tab. Default style is
|
||||
# OpenStreetMap; an opt-in OpenTopoMap topographic style is served from /opentopo/.
|
||||
#
|
||||
# The firmware fetches plain HTTP (on-device HTTPS isn't workable: mbedTLS wants
|
||||
# ~30 KB heap for a handshake and only ~5 KB is free after Wi-Fi associates), so
|
||||
@@ -62,6 +63,49 @@ server {
|
||||
proxy_connect_timeout 5s;
|
||||
}
|
||||
|
||||
# OpenTopoMap (topographic) — OPT-IN alternate style. The firmware requests
|
||||
# this path only when the user enables "Topographic map"; the default
|
||||
# /{z}/{x}/{y}.jpg above stays OpenStreetMap. Legal: map © OpenTopoMap
|
||||
# (CC-BY-SA), data © OpenStreetMap contributors + SRTM — the touch UI shows
|
||||
# that attribution when topo is selected. The 14-day disk cache means each
|
||||
# tile hits OpenTopoMap at most once per fortnight (their tile policy asks it).
|
||||
location ~ "^/opentopo/(?<z>[0-9]{1,2})/(?<x>[0-9]{1,7})/(?<y>[0-9]{1,7})\.jpg$" {
|
||||
proxy_pass http://127.0.0.1:5005/opentopo/$z/$x/$y.jpg;
|
||||
proxy_set_header Host "tiles.wadamesh.com";
|
||||
|
||||
proxy_cache wadamesh_tiles;
|
||||
proxy_cache_key "opentopo-jpg:$z/$x/$y";
|
||||
proxy_cache_valid 200 14d;
|
||||
proxy_cache_valid 404 1d;
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
proxy_cache_lock on;
|
||||
add_header X-Cache-Status $upstream_cache_status always;
|
||||
add_header Cache-Control "public, max-age=2592000" always;
|
||||
|
||||
proxy_buffering on;
|
||||
proxy_read_timeout 20s;
|
||||
proxy_connect_timeout 5s;
|
||||
}
|
||||
|
||||
# Explicit OpenStreetMap alias (identical to the default root path above).
|
||||
location ~ "^/osm/(?<z>[0-9]{1,2})/(?<x>[0-9]{1,7})/(?<y>[0-9]{1,7})\.jpg$" {
|
||||
proxy_pass http://127.0.0.1:5005/osm/$z/$x/$y.jpg;
|
||||
proxy_set_header Host "tiles.wadamesh.com";
|
||||
|
||||
proxy_cache wadamesh_tiles;
|
||||
proxy_cache_key "osm-jpg:$z/$x/$y";
|
||||
proxy_cache_valid 200 14d;
|
||||
proxy_cache_valid 404 1d;
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
proxy_cache_lock on;
|
||||
add_header X-Cache-Status $upstream_cache_status always;
|
||||
add_header Cache-Control "public, max-age=2592000" always;
|
||||
|
||||
proxy_buffering on;
|
||||
proxy_read_timeout 20s;
|
||||
proxy_connect_timeout 5s;
|
||||
}
|
||||
|
||||
# Elevation profile for the line-of-sight analyzer (same local service).
|
||||
location = /elev {
|
||||
proxy_pass http://127.0.0.1:5005;
|
||||
@@ -98,7 +142,7 @@ server {
|
||||
# Tiny landing so a human hitting the bare hostname isn't met with 404.
|
||||
location = / {
|
||||
default_type text/plain;
|
||||
return 200 "wadamesh tile proxy\n\nUsage: GET /{z}/{x}/{y}.jpg (HTTP only)\nUpstream: tile.openstreetmap.org (PNG transcoded to JPEG)\n";
|
||||
return 200 "wadamesh tile proxy (HTTP only)\n\n /{z}/{x}/{y}.jpg -> OpenStreetMap (default)\n /osm/{z}/{x}/{y}.jpg -> OpenStreetMap\n /opentopo/{z}/{x}/{y}.jpg -> OpenTopoMap (CC-BY-SA)\n\nUpstream PNG transcoded to JPEG.\n";
|
||||
}
|
||||
|
||||
location / { return 404; }
|
||||
|
||||
+76
-7
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WADAMESH — a touchscreen for your mesh radio</title>
|
||||
<meta name="description" content="Open-source MeshCore touch-UI firmware for the LilyGo T-Deck and Heltec V4. Map, chat, channels, contacts — install from your browser.">
|
||||
<meta name="description" content="Open-source MeshCore touch-UI firmware for the LilyGo T-Deck, Heltec V4 and Tanmatsu. Map, chat, channels, contacts — install from your browser.">
|
||||
<link rel="icon" href="wadamesh-badge.svg">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
@@ -135,7 +135,7 @@
|
||||
.step p{color:var(--mut); font-size:.92rem}
|
||||
.step code{background:#fff; border:1px solid var(--line); border-radius:6px; padding:1px 6px; font-size:.85em; font-family:'JetBrains Mono',monospace}
|
||||
|
||||
.boards{display:grid; gap:14px; grid-template-columns:1fr 1fr; max-width:760px; margin:18px auto 0}
|
||||
.boards{display:grid; gap:14px; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); max-width:760px; margin:18px auto 0}
|
||||
@media(max-width:560px){.boards{grid-template-columns:1fr}}
|
||||
.board{border:1px solid var(--line); background:var(--card); border-radius:14px; padding:18px; display:flex;
|
||||
flex-direction:column; gap:10px; align-items:flex-start; box-shadow:0 8px 26px -22px rgba(21,24,30,.5)}
|
||||
@@ -147,6 +147,22 @@
|
||||
.dl:hover{background:var(--teal-ink)}
|
||||
.dl svg{width:15px;height:15px}
|
||||
.hint{max-width:760px;margin:18px auto 0;color:var(--mut);font-size:.86rem;text-align:center}
|
||||
/* "install a previous version" picker */
|
||||
.verpick{max-width:760px; margin:16px auto 0; border:1px solid var(--line); border-radius:12px; background:var(--bg2); text-align:left}
|
||||
.verpick summary{cursor:pointer; padding:12px 16px; font-weight:600; font-size:.92rem; color:var(--ink); list-style:none; user-select:none}
|
||||
.verpick summary::-webkit-details-marker{display:none}
|
||||
.verpick summary::before{content:"\25B8"; color:var(--teal); margin-right:9px; display:inline-block; transition:transform .15s}
|
||||
.verpick[open] summary::before{transform:rotate(90deg)}
|
||||
.verpick-body{padding:2px 16px 16px}
|
||||
.verpick-row{display:flex; align-items:center; gap:10px}
|
||||
.verpick-row label{font-size:.9rem; color:var(--mut)}
|
||||
#verSel{font:inherit; font-size:.92rem; padding:7px 11px; border:1px solid var(--line); border-radius:8px; background:#fff; color:var(--ink); cursor:pointer}
|
||||
.verpick-note{margin:9px 0 0; font-size:.86rem; color:var(--teal-ink)}
|
||||
.rn{margin-top:12px; max-height:280px; overflow:auto; font-size:.88rem; color:var(--mut); line-height:1.55}
|
||||
.rn .rn-h{font-size:.92rem; color:var(--ink); font-weight:700; margin:13px 0 5px} .rn .rn-h:first-child{margin-top:0}
|
||||
.rn ul{margin:5px 0 0; padding-left:18px} .rn li{margin:3px 0}
|
||||
.rn p{margin:6px 0} .rn code{background:#fff; border:1px solid var(--line); border-radius:5px; padding:1px 5px; font-size:.86em}
|
||||
.rn a{color:var(--teal-ink)}
|
||||
.noserial{display:none;color:#b4631a;text-align:center;margin-top:14px}
|
||||
body.no-serial .noserial{display:block}
|
||||
|
||||
@@ -198,8 +214,8 @@
|
||||
|
||||
<!-- floating quick links -->
|
||||
<div class="fab-rail left">
|
||||
<a class="fab" href="https://t.me/+fHuhG67-RFJlNmFk" target="_blank" rel="noopener" aria-label="Join us on Telegram" title="Join us on Telegram">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.04 15.47 8.7 19.9c.5 0 .72-.22.98-.47l2.36-2.22 4.89 3.53c.9.5 1.53.24 1.77-.83l3.2-14.5c.3-1.33-.5-1.85-1.36-1.53L1.9 9.46c-1.3.5-1.28 1.22-.22 1.55l5.05 1.57L18.4 5.9c.55-.36 1.05-.16.64.2L9.04 15.47z"/></svg>
|
||||
<a class="fab" href="https://discord.gg/T8X2UaQ7Sm" target="_blank" rel="noopener" aria-label="Join us on Discord" title="Join us on Discord">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.369a19.79 19.79 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.249a18.27 18.27 0 0 0-5.487 0 12.6 12.6 0 0 0-.617-1.249.077.077 0 0 0-.079-.037A19.74 19.74 0 0 0 3.677 4.369a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.2 14.2 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.1 13.1 0 0 1-1.872-.892.077.077 0 0 1-.008-.128c.126-.094.252-.192.372-.291a.074.074 0 0 1 .078-.01c3.927 1.793 8.18 1.793 12.061 0a.074.074 0 0 1 .079.009c.12.099.246.198.373.292a.077.077 0 0 1-.006.127 12.3 12.3 0 0 1-1.873.891.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.84 19.84 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.06.06 0 0 0-.031-.029zM8.02 15.331c-1.182 0-2.157-1.086-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.332-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.086-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.332-.946 2.418-2.157 2.418z"/></svg>
|
||||
</a>
|
||||
<a class="fab" href="#videos" aria-label="Community videos" title="Community videos">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||
@@ -256,7 +272,7 @@
|
||||
<span class="byline">by meshcomod</span>
|
||||
</div>
|
||||
<p class="lede">A real touchscreen UI for your mesh radio.</p>
|
||||
<p class="sub">MeshCore MESHCOMOD firmware for the <b>LilyGo T-Deck</b> and <b>Heltec V4</b> — map, chat, channels and contacts on the device itself, <b>and</b> a companion radio for the <b>MeshCore app</b> at the same time.</p>
|
||||
<p class="sub">MeshCore MESHCOMOD firmware for the <b>LilyGo T-Deck</b>, <b>Heltec V4</b> and <b>Tanmatsu</b> — map, chat, channels and contacts on the device itself, <b>and</b> a companion radio for the <b>MeshCore app</b> at the same time.</p>
|
||||
<div class="pills">
|
||||
<span class="pill" tabindex="0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 20l-6-3V4l6 3 6-3 6 3v13l-6-3-6 3z"/><path d="M9 7v13M15 4v13"/></svg>Offline map
|
||||
<span class="pop"><b>Offline map</b>A real OpenStreetMap you can pan and zoom on the device. Pre-load tile packs to an SD card for fully offline use, or fetch over Wi-Fi — and see your contacts' GPS positions plotted on it.</span></span>
|
||||
@@ -312,10 +328,21 @@
|
||||
</div>
|
||||
<div class="boards">
|
||||
<div class="board"><h4>Heltec V4 + TFT</h4><p>ESP32-S3 with CHSC6x touch.</p>
|
||||
<esp-web-install-button manifest="https://firmware.wadamesh.com/latest/manifest-heltec-v4-tft.json"></esp-web-install-button></div>
|
||||
<esp-web-install-button id="inst-heltec" 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="https://firmware.wadamesh.com/latest/manifest-tdeck.json"></esp-web-install-button></div>
|
||||
<esp-web-install-button id="inst-tdeck" manifest="https://firmware.wadamesh.com/latest/manifest-tdeck.json"></esp-web-install-button></div>
|
||||
<div class="board"><h4>Tanmatsu</h4><p>ESP32-P4 keyboard handheld.</p>
|
||||
<p style="color:var(--mut);font-size:.84rem;margin:0;line-height:1.5">Already have one? No USB flashing needed — install WADAMESH straight from the <b>Tanmatsu app store</b> on the device.</p>
|
||||
<a class="dl" href="https://shop.nicolaielectronics.nl/" target="_blank" rel="noopener"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>Get a Tanmatsu</a></div>
|
||||
</div>
|
||||
<details class="verpick">
|
||||
<summary>Install a previous version</summary>
|
||||
<div class="verpick-body">
|
||||
<div class="verpick-row"><label for="verSel">Version</label><select id="verSel" aria-label="Firmware version"></select></div>
|
||||
<p class="verpick-note" id="verActive" hidden></p>
|
||||
<div id="verNotes" class="rn"></div>
|
||||
</div>
|
||||
</details>
|
||||
<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>
|
||||
</div>
|
||||
@@ -497,6 +524,48 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
|
||||
const docsBtn = document.getElementById('docsBtn');
|
||||
if (docsBtn) docsBtn.addEventListener('click', () => toast('Documentation — coming soon!'));
|
||||
|
||||
// ---- standalone: install a previous version (latest 4 betas + release notes) ----
|
||||
(function(){
|
||||
const sel = document.getElementById('verSel'); if (!sel) return;
|
||||
const wrap = sel.closest('.verpick');
|
||||
const notesEl = document.getElementById('verNotes'), activeEl = document.getElementById('verActive');
|
||||
const btnH = document.getElementById('inst-heltec'), btnT = document.getElementById('inst-tdeck');
|
||||
const latestH = btnH && btnH.getAttribute('manifest'), latestT = btnT && btnT.getAttribute('manifest');
|
||||
const num = t => parseInt((String(t).match(/beta_(\d+)/) || [])[1] || '-1', 10);
|
||||
let notes = {};
|
||||
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
const inl = s => esc(s).replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/`([^`]+?)`/g,'<code>$1</code>')
|
||||
.replace(/\[(.+?)\]\((https?:\/\/[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
|
||||
function md(t){ let out='', ul=false; const close=()=>{ if(ul){ out+='</ul>'; ul=false; } };
|
||||
(t||'').split(/\r?\n/).forEach(line=>{ line=line.trim(); let m;
|
||||
if(!line) close();
|
||||
else if(m=line.match(/^#{1,6}\s+(.*)/)){ close(); out+='<div class="rn-h">'+inl(m[1])+'</div>'; }
|
||||
else if(m=line.match(/^[-*]\s+(.*)/)){ if(!ul){ out+='<ul>'; ul=true; } out+='<li>'+inl(m[1])+'</li>'; }
|
||||
else { close(); out+='<p>'+inl(line)+'</p>'; } });
|
||||
close(); return out; }
|
||||
function showNotes(tag){ notesEl.innerHTML = notes[tag] ? md(notes[tag])
|
||||
: '<p>Release notes on <a href="https://github.com/ALLFATHER-BV/wadamesh/releases/tag/'+tag+'" target="_blank" rel="noopener">GitHub</a>.</p>'; }
|
||||
function blobManifest(tag, board){ const m = { name:'wadamesh '+tag, version:tag, new_install_prompt_erase:true,
|
||||
builds:[{ chipFamily:'ESP32-S3', parts:[{ path:'https://firmware.wadamesh.com/releases/TOUCH/'+tag+'/wadamesh-'+board+'-merged.bin', offset:0 }] }] };
|
||||
return URL.createObjectURL(new Blob([JSON.stringify(m)], {type:'application/json'})); }
|
||||
function apply(){ const tag = sel.value, isLatest = sel.selectedIndex === 0;
|
||||
if (isLatest){ if(btnH&&latestH) btnH.setAttribute('manifest', latestH); if(btnT&&latestT) btnT.setAttribute('manifest', latestT); activeEl.hidden = true; }
|
||||
else { if(btnH) btnH.setAttribute('manifest', blobManifest(tag,'heltec-v4-tft')); if(btnT) btnT.setAttribute('manifest', blobManifest(tag,'tdeck'));
|
||||
activeEl.hidden = false; activeEl.innerHTML = 'The <b>Install</b> buttons above now flash <b>'+tag+'</b>.'; }
|
||||
showNotes(tag); }
|
||||
// GitHub API gives version list + notes in one CORS-enabled call (the firmware
|
||||
// host listing has no CORS); bins are still pulled from the firmware host.
|
||||
fetch('https://api.github.com/repos/ALLFATHER-BV/wadamesh/releases?per_page=12').then(r=>r.ok?r.json():[]).then(rs=>{
|
||||
const rels = (Array.isArray(rs)?rs:[]).filter(r=>r&&/^beta_\d+$/.test(r.tag_name||''));
|
||||
rels.forEach(r=>{ notes[r.tag_name]=r.body||''; });
|
||||
const tags = rels.map(r=>r.tag_name).sort((a,b)=>num(b)-num(a)).slice(0,4);
|
||||
if (!tags.length){ if(wrap) wrap.hidden = true; return; }
|
||||
sel.innerHTML = tags.map((t,i)=>'<option value="'+t+'">'+t+(i===0?' — latest':'')+'</option>').join('');
|
||||
showNotes(tags[0]);
|
||||
sel.addEventListener('change', apply);
|
||||
}).catch(()=>{ if(wrap) wrap.hidden = true; });
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
<!-- animated mesh backdrop — Vanta NET (3D dot-and-line network) in wadamesh teal on white.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# WadaMesh
|
||||
|
||||
A full **[MeshCore](https://github.com/meshcore-dev/MeshCore)** client for the Tanmatsu.
|
||||
It runs the LoRa mesh, Wi-Fi and Bluetooth radios **all at the same time**, so the device
|
||||
works as a complete standalone communicator *and* as a phone companion at once — no
|
||||
mode-switching, no rebooting to swap radios.
|
||||
|
||||
MeshCore is an encrypted, infrastructure-free LoRa mesh network — messages hop from device
|
||||
to device with no cell tower, Wi-Fi or internet in between. WadaMesh gives it a full
|
||||
graphical front-end on the Tanmatsu's own screen and keyboard.
|
||||
|
||||
## Why WadaMesh
|
||||
|
||||
- **Multi-radio, simultaneously.** LoRa, Wi-Fi and Bluetooth all live at once. Chat on the
|
||||
mesh while Wi-Fi streams map tiles and a phone stays paired over Bluetooth — concurrently.
|
||||
- **Standalone *and* companion at the same time.** A complete client on its own screen and
|
||||
keyboard, while a phone app can connect over Bluetooth or Wi-Fi in parallel. You don't
|
||||
have to pick a role.
|
||||
- **Live mesh map.** Plot yourself and your contacts on a real OpenStreetMap from their
|
||||
shared GPS positions (tiles fetched and cached over Wi-Fi).
|
||||
- **Built for the keyboard.** Arrow-key navigation, type-to-edit and function-key shortcuts —
|
||||
designed around the Tanmatsu's hardware, not bolted on from a touchscreen.
|
||||
- **Polished and localised.** Full-colour emoji and 11 interface languages.
|
||||
|
||||
## Features
|
||||
|
||||
- **Messaging** — direct messages and channels with unread markers, a "new messages"
|
||||
divider and quick replies.
|
||||
- **Contacts & map** — contact list with live signal and distance, plus the mesh map.
|
||||
- **Radio control** — frequency / spreading factor / bandwidth / TX power, and GPS.
|
||||
- **Connectivity** — LoRa, Wi-Fi and Bluetooth at once; use it standalone or as a Wi-Fi +
|
||||
Bluetooth companion for a phone.
|
||||
|
||||
## Links
|
||||
|
||||
- Source & releases: <https://github.com/ALLFATHER-BV/wadamesh>
|
||||
- Web flasher & project info: <https://wadamesh.com>
|
||||
|
||||
## Licence
|
||||
|
||||
WadaMesh is distributed under **GPL-3.0** — see [LICENSE](LICENSE). It incorporates
|
||||
[MeshCore](https://github.com/meshcore-dev/MeshCore) (MIT, © Scott Powell /
|
||||
rippleradios.com); MeshCore-derived files keep their MIT notices, and the combined
|
||||
work is GPL (MIT is GPL-compatible). See [NOTICE](NOTICE) for third-party licenses.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 604 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "WadaMesh",
|
||||
"description": "MeshCore client running LoRa Wi-Fi & Bluetooth at once. A standalone unit and phone companion in one.",
|
||||
"categories": [
|
||||
"tools",
|
||||
"utils",
|
||||
"hardware"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"icon": {
|
||||
"16x16": "icon16.png",
|
||||
"32x32": "icon32.png",
|
||||
"64x64": "icon64.png"
|
||||
},
|
||||
"author": "Kaj Schittecat - ALLFATHER BV",
|
||||
"license_type": "GPL-3.0",
|
||||
"license_file": "LICENSE",
|
||||
"application": [
|
||||
{
|
||||
"targets": [
|
||||
"tanmatsu",
|
||||
"konsool"
|
||||
],
|
||||
"revision": 1,
|
||||
"type": "appfs",
|
||||
"executable": "wadamesh.bin",
|
||||
"assets": []
|
||||
}
|
||||
]
|
||||
}
|
||||
+36
-11
@@ -39,8 +39,17 @@ except ImportError:
|
||||
sys.exit("ERROR: pip install flask pillow requests")
|
||||
|
||||
OSM_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
# OSM tile policy: identify yourself with a contactable UA. This is what OSM
|
||||
# sees — the device never talks to OSM directly.
|
||||
# OpenTopoMap (topographic / terrain-relief style) — an OPT-IN alternate style.
|
||||
# The firmware default stays OpenStreetMap; the device only requests /opentopo/
|
||||
# when the user enables it. Like OSM it serves PNG, so it goes through the same
|
||||
# fetch+transcode path. Legal: map style © OpenTopoMap (CC-BY-SA), underlying
|
||||
# data © OpenStreetMap contributors (ODbL) + SRTM — the touch UI shows that
|
||||
# attribution whenever topo is the selected style. OpenTopoMap's tile-usage
|
||||
# policy asks for a contactable User-Agent + caching; nginx caches results 14
|
||||
# days so each unique tile hits OpenTopoMap at most once per fortnight.
|
||||
OPENTOPO_URL = "https://tile.opentopomap.org/{z}/{x}/{y}.png"
|
||||
# Tile policy: identify yourself with a contactable UA. This is what the upstream
|
||||
# tile servers see — the device never talks to them directly.
|
||||
OSM_UA = "wadamesh-tile-proxy/1.0 (+https://wadamesh.com)"
|
||||
JPEG_QUALITY = 80 # slippy tiles compress well; 80 ≈ visually lossless
|
||||
MAX_TILE_BYTES = 256 * 1024
|
||||
@@ -57,40 +66,56 @@ _session = requests.Session()
|
||||
_session.headers.update({"User-Agent": OSM_UA, "Accept": "image/png,image/*"})
|
||||
|
||||
|
||||
@app.get("/<int:z>/<int:x>/<int:y>.jpg")
|
||||
def tile(z: int, x: int, y: int):
|
||||
# Range-check before touching OSM so we can't be turned into an open proxy.
|
||||
def _fetch_tile_png(upstream_url: str, z: int, x: int, y: int) -> bytes:
|
||||
# Range-check before touching the upstream so we can't be turned into an open proxy.
|
||||
if not (0 <= z <= 19):
|
||||
abort(404)
|
||||
n = 1 << z
|
||||
if not (0 <= x < n and 0 <= y < n):
|
||||
abort(404)
|
||||
|
||||
try:
|
||||
r = _session.get(OSM_URL.format(z=z, x=x, y=y), timeout=REQUEST_TIMEOUT,
|
||||
r = _session.get(upstream_url.format(z=z, x=x, y=y), timeout=REQUEST_TIMEOUT,
|
||||
stream=True)
|
||||
except requests.RequestException:
|
||||
abort(502)
|
||||
|
||||
if r.status_code != 200:
|
||||
# Pass OSM's status through (404 = empty/sea tile — nginx caches it).
|
||||
# Pass the upstream status through (404 = empty/sea tile — nginx caches it).
|
||||
abort(r.status_code if r.status_code in (404, 429) else 502)
|
||||
|
||||
raw = r.raw.read(MAX_TILE_BYTES + 1, decode_content=True)
|
||||
if len(raw) > MAX_TILE_BYTES:
|
||||
abort(502)
|
||||
return raw
|
||||
|
||||
|
||||
def _tile_response_from_png(raw: bytes) -> Response:
|
||||
try:
|
||||
img = Image.open(io.BytesIO(raw)).convert("RGB")
|
||||
out = io.BytesIO()
|
||||
img.save(out, "JPEG", quality=JPEG_QUALITY, optimize=True)
|
||||
except Exception:
|
||||
abort(502)
|
||||
|
||||
return Response(out.getvalue(), mimetype="image/jpeg",
|
||||
headers={"Cache-Control": "public, max-age=2592000"})
|
||||
|
||||
|
||||
# Default path the firmware uses → OpenStreetMap (the shipping default style).
|
||||
@app.get("/<int:z>/<int:x>/<int:y>.jpg")
|
||||
def tile(z: int, x: int, y: int):
|
||||
return _tile_response_from_png(_fetch_tile_png(OSM_URL, z, x, y))
|
||||
|
||||
|
||||
# Explicit OpenStreetMap alias (identical to the root path).
|
||||
@app.get("/osm/<int:z>/<int:x>/<int:y>.jpg")
|
||||
def tile_osm(z: int, x: int, y: int):
|
||||
return _tile_response_from_png(_fetch_tile_png(OSM_URL, z, x, y))
|
||||
|
||||
|
||||
# OpenTopoMap (topographic) — opt-in alternate style selected on the device.
|
||||
@app.get("/opentopo/<int:z>/<int:x>/<int:y>.jpg")
|
||||
def tile_opentopo(z: int, x: int, y: int):
|
||||
return _tile_response_from_png(_fetch_tile_png(OPENTOPO_URL, z, x, y))
|
||||
|
||||
|
||||
@app.get("/elev")
|
||||
def elev():
|
||||
"""Elevation profile for the line-of-sight analyzer.
|
||||
|
||||
@@ -127,6 +127,9 @@ build_flags =
|
||||
-I src/ui-touch
|
||||
-D UI_LVGL=1
|
||||
-D HAS_TOUCH_UI=1
|
||||
; Heltec V4 Expansion Kit: local environment sensors UI (Sensors tab, Home env
|
||||
; widget, Local-Sensors page). Board-gated — only this env compiles it.
|
||||
-D HAS_EXPANSION_KIT=1
|
||||
-D FIRMWARE_OTA_ENV='"heltec_v4_tft_companion_radio_usb_tcp_touch"'
|
||||
-D ENABLE_ADVERT_ON_BOOT=0
|
||||
-D LV_CONF_PATH=lv_conf.h
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# beta_16 (one user-facing note per non-blank, non-# line; # lines are section comments)
|
||||
|
||||
# --- New ---
|
||||
Heltec V4 Expansion Kit support: a dedicated Sensors tab (temperature, humidity, pressure, GPS and battery, with small history charts), a Home-screen environment readout, and an on-device "mesh console" for poking at the radio. It only turns up when a sensor board is actually attached, so a bare V4 looks exactly as it did before. This one was a real grind to land — it came in as a huge pull request built on an old beta, and getting it in cleanly meant re-doing most of it by hand against the current code and carefully fencing it all off so it can't bloat or disturb the T-Deck and Tanmatsu builds. Thanks to Samuel Coustet for the original work (#29).
|
||||
French, Dutch, German, Spanish and Italian keyboards, on-screen and on the T-Deck's physical keys. Choosing one of those as your interface language now auto-selects the matching layout. Also from Samuel Coustet (#29).
|
||||
Wi-Fi firmware updates are back: on a normally-flashed device (not the Launcher) you can update straight from Settings -> About, no cable needed.
|
||||
You can also roll back now — "Install a previous version" lets you reinstall one of the last few releases over Wi-Fi, with a confirmation first since it's a downgrade.
|
||||
|
||||
# --- Fixed ---
|
||||
The status-bar clock no longer overlaps the node name on the V4 (it had been crammed against it for a good while).
|
||||
Closing a dialog no longer lets a scroll carry through to the page behind it (thanks tun0, #50).
|
||||
The contact action sheet no longer overflows the screen in landscape (thanks tun0, #49).
|
||||
The per-channel region field now shows your default region as the hint, instead of a generic placeholder (thanks tun0, #48).
|
||||
|
||||
# --- Also ---
|
||||
Everything from beta_15 is included.
|
||||
@@ -0,0 +1,23 @@
|
||||
# beta_18 (one user-facing note per non-blank, non-# line; # lines are section comments)
|
||||
|
||||
# --- New ---
|
||||
A topographic map style: turn on Map -> Options -> "Topographic map" for terrain-relief tiles (OpenStreetMap stays the default). Thanks to Samuel Coustet for the proxy work (#52).
|
||||
New emoji in the picker: soccer ball, american football, moai, and the transgender flag.
|
||||
Contacts can now sort ascending or descending, and there's a new "Nearest first" sort that orders contacts by distance from you (contacts without a shared location are left out).
|
||||
The Discovered list gets the same sorting — by most-recently-heard, name, or distance — from a new sort button in its header.
|
||||
The Chats header is tidied up: the add / mark-all-read / share-contact buttons are grouped together on the left, and sharing your contact now uses a proper QR icon.
|
||||
|
||||
# --- Fixed ---
|
||||
The USB/PC companion connection no longer drops or throws "bad state" errors mid-session. The same fix also clears up repeater admin mode disconnecting and not being able to log in to member rooms.
|
||||
Map tiles along the edges and corners of the view now fill in automatically as you pan, instead of needing a manual "reload tiles".
|
||||
Reclaimed about 41 KB of memory by moving a large internal buffer to PSRAM — this fixes the Heltec V4 running out of RAM and rebooting when it loaded map tiles, and leaves more headroom on every board.
|
||||
The map's "Show link lines" toggle is now remembered across reboots. Thanks to Yazutsu (#61).
|
||||
Settings category names (Bluetooth, Radio & Mesh, Quick replies, ...) now fit on one line instead of wrapping or splitting mid-word.
|
||||
Turning keyboard navigation on or off no longer snaps the settings list back to the top.
|
||||
Heltec V4: the node name in the status bar no longer covers the clock.
|
||||
T-Deck: the keyboard backlight no longer gets stuck on after flashing when it's set to Off.
|
||||
Crash reporting is more reliable (it only prompts after an actual crash, not on every restart), and there are fewer stray reboots while saving settings.
|
||||
Tanmatsu: changing the LoRa radio settings (frequency and so on) now takes effect immediately, without needing a reboot, and the app's settings now drive the radio.
|
||||
|
||||
# --- Also ---
|
||||
Everything from beta_16 (and the Tanmatsu's beta_17) is included.
|
||||
@@ -0,0 +1,14 @@
|
||||
# beta_19 (one user-facing note per non-blank, non-# line; # lines are section comments)
|
||||
|
||||
# --- New ---
|
||||
Radio & Mesh settings now show an airtime / duty-cycle readout: roughly how much of the airtime budget your current spreading factor, bandwidth and coding rate use, plus the time-on-air per packet — handy for staying inside regional limits like EU 868.
|
||||
A new "Answer telemetry requests" toggle in Radio & Mesh settings controls whether other nodes can pull your battery and environment telemetry.
|
||||
The Radio & Mesh settings page is now split into clear RADIO / MESH / SIGNAL sections so the options are easier to find.
|
||||
|
||||
# --- Fixed ---
|
||||
Fixed a reboot that could happen when you closed a scrollable popup — the emoji picker, quick replies, the Wi-Fi scan list, the app drawer and similar — while it was still coasting from a flick. Thanks to everyone who sent in crash reports.
|
||||
The accent popup no longer lingers on screen after you send a message on the T-Deck keyboard.
|
||||
Heltec V4: the Expansion Kit environment line and mini chart on the Home screen no longer overlap the status text or run off the bottom of the display.
|
||||
|
||||
# --- Also ---
|
||||
Everything from beta_18 is included.
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
# Bake a few EXTRA Noto colour emoji into src/ui-touch/emoji_data.{c,h} WITHOUT
|
||||
# regenerating (and re-downloading) the existing 252 — those stay byte-identical,
|
||||
# so there's no Noto-`main` art drift. Splices new glyph arrays + sorted lookup
|
||||
# entries into the file in place. Re-run is idempotent (new cps replace, existing
|
||||
# untouched). PX must match the existing bake (16) and the imgfont in UITask.
|
||||
#
|
||||
# python3 scripts/build/add-emoji.py
|
||||
#
|
||||
# Source art: googlefonts/noto-emoji png/128 (OFL). RGB565+alpha, swap=0.
|
||||
import os, re, sys, urllib.request
|
||||
from PIL import Image
|
||||
|
||||
PX = 16
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
OUTDIR = os.path.join(ROOT, "src", "ui-touch")
|
||||
OUTC = os.path.join(OUTDIR, "emoji_data.c")
|
||||
OUTH = os.path.join(OUTDIR, "emoji_data.h")
|
||||
CACHE = os.path.join(ROOT, "data", "emoji-cache")
|
||||
BASE = "https://raw.githubusercontent.com/googlefonts/noto-emoji/main/png/128/emoji_u{}.png"
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
|
||||
# Single-codepoint emoji: (codepoint, noto-filename-stem)
|
||||
SINGLE = [
|
||||
(0x26BD, "26bd"), # soccer ball âš½
|
||||
(0x1F3C8, "1f3c8"), # american football ðŸ
|
||||
(0x1F5FF, "1f5ff"), # moai 🗿
|
||||
]
|
||||
# ZWJ sequences baked as ONE combined image, keyed on the lead codepoint, with the
|
||||
# trailing visible symbol(s) mapped to the zero-width glyph so the whole UTF-8
|
||||
# sequence renders as the single combined emoji. The bare lead cp (a plain waving
|
||||
# white flag U+1F3F3) is not offered anywhere in the UI, so reusing it as the key
|
||||
# is safe. (noto-emoji drops the FE0F variation selectors from png filenames.)
|
||||
SEQ = [
|
||||
# (key_cp, noto-stem, [extra cps -> zero])
|
||||
(0x1F3F3, "1f3f3_200d_26a7", [0x26A7]), # transgender flag ðŸ³ï¸â€âš§ï¸
|
||||
]
|
||||
|
||||
|
||||
# Country/region flags: a regional-indicator PAIR (e.g. LS = U+1F1F1 U+1F1F8) baked
|
||||
# as ONE image keyed on the LEAD indicator, the trailing indicator mapped to the
|
||||
# zero-width glyph (same trick as SEQ). Noto's png/128 ships no flags, so pull the
|
||||
# flat rectangle from the same Noto project's third_party/region-flags. No bare
|
||||
# regional indicator is ever offered in the UI, so reusing the lead cp as the key
|
||||
# is safe — and the picker currently has no other flag using these indicators.
|
||||
FLAGS = [
|
||||
# (lead_cp, trail_cp, ISO-3166-alpha-2)
|
||||
(0x1F1F1, 0x1F1F8, "LS"), # Lesotho
|
||||
]
|
||||
|
||||
|
||||
def fetch(stem):
|
||||
fn = os.path.join(CACHE, "u_{}.png".format(stem))
|
||||
if os.path.exists(fn) and os.path.getsize(fn) > 0:
|
||||
return fn
|
||||
for url in (BASE.format(stem), BASE.format(stem + "_fe0f")):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "emoji-gen"})
|
||||
data = urllib.request.urlopen(req, timeout=20).read()
|
||||
with open(fn, "wb") as f:
|
||||
f.write(data)
|
||||
return fn
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def fetch_flag(iso):
|
||||
fn = os.path.join(CACHE, "flag_{}.png".format(iso))
|
||||
if os.path.exists(fn) and os.path.getsize(fn) > 0:
|
||||
return fn
|
||||
url = ("https://raw.githubusercontent.com/googlefonts/noto-emoji/main"
|
||||
"/third_party/region-flags/png/{}.png".format(iso))
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "emoji-gen"})
|
||||
data = urllib.request.urlopen(req, timeout=20).read()
|
||||
with open(fn, "wb") as f:
|
||||
f.write(data)
|
||||
return fn
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def to_rgb565a8(img):
|
||||
# IDENTICAL to gen-emoji-font.py: autocrop transparent margin, bottom-align to
|
||||
# the text baseline, emit [lo, hi, alpha] per pixel (LV_COLOR_16_SWAP=0).
|
||||
PAD_BOTTOM = 0
|
||||
img = img.convert("RGBA")
|
||||
bbox = img.split()[3].getbbox()
|
||||
if bbox:
|
||||
img = img.crop(bbox)
|
||||
w, h = img.size
|
||||
avail_h = PX - PAD_BOTTOM
|
||||
scale = min(PX / w, avail_h / h)
|
||||
nw, nh = max(1, round(w * scale)), max(1, round(h * scale))
|
||||
content = img.resize((nw, nh), Image.LANCZOS)
|
||||
canvas = Image.new("RGBA", (PX, PX), (0, 0, 0, 0))
|
||||
canvas.paste(content, ((PX - nw) // 2, PX - PAD_BOTTOM - nh))
|
||||
out = bytearray()
|
||||
for (r, g, b, a) in canvas.getdata():
|
||||
v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
||||
out += bytes((v & 0xFF, (v >> 8) & 0xFF, a))
|
||||
return out
|
||||
|
||||
|
||||
# ---- bake the new glyphs ----
|
||||
new_defs = [] # (cp, ename, bytes)
|
||||
new_entries = [] # (cp, ref)
|
||||
for cp, stem in SINGLE:
|
||||
fn = fetch(stem)
|
||||
if not fn:
|
||||
sys.exit("FAILED to fetch U+{:X} ({})".format(cp, stem))
|
||||
new_defs.append((cp, "e_{:x}".format(cp), to_rgb565a8(Image.open(fn))))
|
||||
new_entries.append((cp, "d_{:x}".format(cp)))
|
||||
for key_cp, stem, zeros in SEQ:
|
||||
fn = fetch(stem)
|
||||
if not fn:
|
||||
sys.exit("FAILED to fetch combined {} for U+{:X}".format(stem, key_cp))
|
||||
new_defs.append((key_cp, "e_{:x}".format(key_cp), to_rgb565a8(Image.open(fn))))
|
||||
new_entries.append((key_cp, "d_{:x}".format(key_cp)))
|
||||
for z in zeros:
|
||||
new_entries.append((z, "d_zero"))
|
||||
|
||||
for lead_cp, trail_cp, iso in FLAGS:
|
||||
fn = fetch_flag(iso)
|
||||
if not fn:
|
||||
sys.exit("FAILED to fetch flag {} for U+{:X}".format(iso, lead_cp))
|
||||
new_defs.append((lead_cp, "e_{:x}".format(lead_cp), to_rgb565a8(Image.open(fn))))
|
||||
new_entries.append((lead_cp, "d_{:x}".format(lead_cp)))
|
||||
new_entries.append((trail_cp, "d_zero"))
|
||||
|
||||
# ---- splice into emoji_data.c ----
|
||||
src = open(OUTC).read()
|
||||
TYPE_MARK = "\ntypedef struct { uint32_t cp;"
|
||||
FUNC_MARK = "const lv_img_dsc_t* emojiGlyphLookup(uint32_t cp) {"
|
||||
head = src[:src.index(TYPE_MARK)] # header + includes + all glyph defs
|
||||
func = src[src.index(FUNC_MARK):] # the binary-search getter (verbatim)
|
||||
|
||||
# Idempotency: on a RE-RUN, `head` still holds the e_/d_ glyph defs spliced in by a
|
||||
# PRIOR run (they live before the typedef), so blindly re-appending new_defs would
|
||||
# redefine them ("redefinition of e_26bd"). Strip any def for a cp we're about to
|
||||
# re-bake; the lookup table dedups separately (bycp), so this only touches defs.
|
||||
for _cp, _ename, _data in new_defs:
|
||||
_hx = "{:x}".format(_cp)
|
||||
head = re.sub(r"static const uint8_t e_" + _hx + r"\[\] = [^;]*;\n", "", head)
|
||||
head = re.sub(r"static const lv_img_dsc_t d_" + _hx + r" = [^;]*;\n", "", head)
|
||||
|
||||
# Existing lookup entries (includes the FE0F/FE0E/200D -> d_zero rows).
|
||||
existing = [(int(cp, 16), ref) for cp, ref in
|
||||
re.findall(r"\{\s*(0x[0-9A-Fa-f]+)u,\s*&(\w+)\s*\}", src)]
|
||||
bycp = {cp: ref for cp, ref in existing}
|
||||
for cp, ref in new_entries: # new cps win; never demote a real glyph to zero
|
||||
if not (ref == "d_zero" and bycp.get(cp, "d_zero") != "d_zero"):
|
||||
bycp[cp] = ref
|
||||
entries = sorted(bycp.items())
|
||||
count = sum(1 for _, ref in entries if ref != "d_zero")
|
||||
|
||||
# New glyph array + dsc definitions (appended after the existing ones).
|
||||
defs = []
|
||||
for cp, ename, data in new_defs:
|
||||
defs.append("static const uint8_t {}[] = {{{}}};".format(ename, ",".join(str(b) for b in data)))
|
||||
defs.append("static const lv_img_dsc_t d_{:x} = {{ {{ LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, {}, {} }}, sizeof({}), {} }};"
|
||||
.format(cp, PX, PX, ename, ename))
|
||||
|
||||
out = []
|
||||
out.append(head.rstrip("\n"))
|
||||
out.append("")
|
||||
out.append("\n".join(defs))
|
||||
out.append("")
|
||||
out.append("typedef struct { uint32_t cp; const lv_img_dsc_t* dsc; } EmojiGlyph;")
|
||||
out.append("static const EmojiGlyph kGlyphs[] = {")
|
||||
for cp, ref in entries:
|
||||
out.append(" {{ 0x{:X}u, &{} }},".format(cp, ref))
|
||||
out.append("};")
|
||||
out.append("const uint16_t kEmojiGlyphCount = {};".format(count))
|
||||
out.append("")
|
||||
out.append(func.rstrip("\n"))
|
||||
out.append("")
|
||||
open(OUTC, "w").write("\n".join(out))
|
||||
|
||||
# ---- header comment count ----
|
||||
h = open(OUTH).read()
|
||||
h = re.sub(r"// \d+ Noto colour emoji", "// {} Noto colour emoji".format(count), h, count=1)
|
||||
open(OUTH, "w").write(h)
|
||||
|
||||
print("added {} glyphs -> {} total ({} lookup rows incl zero-width)".format(
|
||||
len(new_defs), count, len(entries)))
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
# Generate src/ui-touch/qr_icon.h: a small, recolour-able "QR code" glyph baked to
|
||||
# an LVGL TRUE_COLOR_ALPHA lv_img_dsc (white opaque modules, transparent ground —
|
||||
# tinted to the button text colour at runtime via img_recolor). There is no QR
|
||||
# glyph in the Montserrat/symbol font, so the Chats-header Share button draws this
|
||||
# instead of the old stand-in LV_SYMBOL_IMAGE ("picture") icon.
|
||||
#
|
||||
# python3 scripts/build/gen-qr-icon.py
|
||||
#
|
||||
# Same pixel encoding as the emoji bake: [lo, hi, alpha], LV_COLOR_16_SWAP=0.
|
||||
import os
|
||||
|
||||
SCALE = 2 # px per module -> 9 modules * 2 = 18x18 px
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
OUT = os.path.join(ROOT, "src", "ui-touch", "qr_icon.h")
|
||||
|
||||
# 9x9 module grid. Three finder patterns (ring: 111/101/111) at TL/TR/BL corners,
|
||||
# scattered data modules + a small alignment block bottom-right so it reads as a QR.
|
||||
P = [
|
||||
[1,1,1,0,1,0,1,1,1],
|
||||
[1,0,1,0,0,0,1,0,1],
|
||||
[1,1,1,0,1,0,1,1,1],
|
||||
[0,0,0,0,0,0,0,0,0],
|
||||
[1,0,1,0,1,1,0,1,0],
|
||||
[0,0,0,0,1,1,0,0,1],
|
||||
[1,1,1,0,0,0,1,0,1],
|
||||
[1,0,1,0,1,0,0,1,0],
|
||||
[1,1,1,0,0,1,1,0,1],
|
||||
]
|
||||
|
||||
MODS = len(P)
|
||||
W = H = MODS * SCALE
|
||||
data = bytearray()
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
on = P[y // SCALE][x // SCALE]
|
||||
if on:
|
||||
data += bytes((0xFF, 0xFF, 0xFF)) # white RGB565 (lo,hi) + alpha 255
|
||||
else:
|
||||
data += bytes((0x00, 0x00, 0x00)) # transparent
|
||||
|
||||
with open(OUT, "w") as f:
|
||||
f.write("// AUTO-GENERATED by scripts/build/gen-qr-icon.py — do not edit.\n")
|
||||
f.write("// %dx%d recolour-able QR glyph for the Chats-header Share button.\n#pragma once\n" % (W, H))
|
||||
f.write('#include "lvgl.h"\n\n')
|
||||
f.write("static const uint8_t qr_icon_map[] = {%s};\n" % ",".join(str(b) for b in data))
|
||||
f.write("static const lv_img_dsc_t qr_icon_dsc = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, %d, %d }, sizeof(qr_icon_map), qr_icon_map };\n"
|
||||
% (W, H))
|
||||
|
||||
print("wrote %s (%dx%d, %d bytes)" % (OUT, W, H, len(data)))
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Publish the Tanmatsu build of WadaMesh to the Nicolai-Electronics app-repository
|
||||
# (the official Tanmatsu launcher app-store). Run this as part of cutting a beta.
|
||||
#
|
||||
# scripts/tanmatsu-appstore-publish.sh beta_17 # -> version 1.0.17, revision 17
|
||||
# scripts/tanmatsu-appstore-publish.sh 1.2.0 8 # -> explicit version 1.2.0, revision 8
|
||||
# scripts/tanmatsu-appstore-publish.sh beta_17 --skip-build # use the existing build
|
||||
#
|
||||
# What it does:
|
||||
# 1. builds the Tanmatsu app (unless --skip-build) and verifies it
|
||||
# 2. syncs our fork (B0N3-GD/app-repository) to upstream main
|
||||
# 3. (re)stages deploy/tanmatsu-app/com.wadamesh.wadamesh/ + the fresh binary,
|
||||
# setting `version` and every application `revision`
|
||||
# 4. pushes an `update-wadamesh-<version>` branch and opens/updates a PR to
|
||||
# Nicolai-Electronics/app-repository
|
||||
#
|
||||
# NB:
|
||||
# - WadaMesh on the Tanmatsu is KEYBOARD-driven (no touchscreen). Keep the
|
||||
# listing wording free of "touch" — the canonical text lives in the template.
|
||||
# - For an update you MUST bump BOTH `version` AND `revision` or the launcher
|
||||
# won't offer it. This script bumps both.
|
||||
# - The CLA is signed once per GitHub user (Kaj), so follow-up PRs auto-pass.
|
||||
# - First run can't sync until PR #55 (the initial add) is merged; until then
|
||||
# the fork's main lacks the folder, but step 3 re-stages it in full anyway.
|
||||
set -euo pipefail
|
||||
|
||||
# ---- args ---------------------------------------------------------------
|
||||
ARG="${1:?usage: tanmatsu-appstore-publish.sh <beta_N | version> [revision] [--skip-build]}"
|
||||
shift || true
|
||||
SKIP_BUILD=0; REV_ARG=""
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--skip-build) SKIP_BUILD=1 ;;
|
||||
*) REV_ARG="$a" ;;
|
||||
esac
|
||||
done
|
||||
if [[ "$ARG" =~ ^beta_([0-9]+)$ ]]; then
|
||||
N="${BASH_REMATCH[1]}"; VERSION="1.0.$N"; REVISION="${REV_ARG:-$N}"
|
||||
else
|
||||
VERSION="$ARG"; REVISION="${REV_ARG:?explicit version needs a revision: tanmatsu-appstore-publish.sh <version> <revision>}"
|
||||
fi
|
||||
|
||||
# ---- paths --------------------------------------------------------------
|
||||
SLUG=com.wadamesh.wadamesh
|
||||
WADAMESH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # repo root
|
||||
TPL="$WADAMESH/deploy/tanmatsu-app/$SLUG" # versioned template (icons/README/metadata)
|
||||
BIN="$WADAMESH/tanmatsu/build/tanmatsu/application.bin" # IDF build output
|
||||
FORK_DIR="${WADAMESH_APPSTORE_FORK:-/Users/kaj/wadamesh-appstore-fork}"
|
||||
UPSTREAM=Nicolai-Electronics/app-repository
|
||||
GHUSER="$(gh api user -q .login)"
|
||||
|
||||
echo "==> WadaMesh app-store publish: version $VERSION, revision $REVISION"
|
||||
|
||||
# ---- 1. build + verify the Tanmatsu app --------------------------------
|
||||
if [ "$SKIP_BUILD" -eq 0 ]; then
|
||||
echo "==> building Tanmatsu app (./build.sh build)..."
|
||||
LOG="$(mktemp)"
|
||||
( cd "$WADAMESH/tanmatsu" && ./build.sh build ) >"$LOG" 2>&1 || true # app_check_size exit!=0 is EXPECTED
|
||||
# real compile/link errors abort; the cosmetic 'app partitions too small' does not
|
||||
if grep -E "error:|undefined reference|was not declared" "$LOG" | grep -vE "app_check_size|too small|partitions are too small"; then
|
||||
echo "!! real build error above — aborting"; rm -f "$LOG"; exit 1
|
||||
fi
|
||||
rm -f "$LOG"
|
||||
fi
|
||||
[ -f "$BIN" ] || { echo "!! no Tanmatsu binary at $BIN — build first (drop --skip-build)"; exit 1; }
|
||||
echo "==> binary present: $(wc -c <"$BIN") bytes"
|
||||
|
||||
# ---- 2. sync the fork to upstream main ---------------------------------
|
||||
[ -d "$FORK_DIR/.git" ] || { echo "!! fork clone missing at $FORK_DIR — clone $GHUSER/app-repository there first"; exit 1; }
|
||||
git -C "$FORK_DIR" remote get-url upstream >/dev/null 2>&1 || \
|
||||
git -C "$FORK_DIR" remote add upstream "https://github.com/$UPSTREAM.git"
|
||||
git -C "$FORK_DIR" fetch -q upstream
|
||||
git -C "$FORK_DIR" checkout -q main 2>/dev/null || git -C "$FORK_DIR" checkout -qb main upstream/main
|
||||
git -C "$FORK_DIR" reset -q --hard upstream/main
|
||||
|
||||
# ---- 3. stage the app folder (template + fresh binary; set version/rev) -
|
||||
APP="$FORK_DIR/$SLUG"
|
||||
mkdir -p "$APP"
|
||||
cp "$TPL/icon16.png" "$TPL/icon32.png" "$TPL/icon64.png" "$TPL/README.md" "$APP/"
|
||||
cp "$WADAMESH/LICENSE" "$APP/LICENSE"
|
||||
cp "$WADAMESH/NOTICE" "$APP/NOTICE" # MeshCore (MIT) + other third-party attribution
|
||||
cp "$BIN" "$APP/wadamesh.bin"
|
||||
python3 - "$TPL/metadata.json" "$APP/metadata.json" "$VERSION" "$REVISION" <<'PY'
|
||||
import json, sys, re
|
||||
src, dst, ver, rev = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
|
||||
m = json.load(open(src))
|
||||
# The launcher store's verify_metadata check restricts name/description to this set
|
||||
# (NO commas, NO em-dash). Fail loudly HERE instead of as a red check on the PR.
|
||||
pat = r'^[A-Za-z0-9_\- .!@#$%^&*()\/\\]+$'
|
||||
for f in ("name", "description"):
|
||||
v = m.get(f, "")
|
||||
if not re.match(pat, v) or len(v) > 256:
|
||||
sys.stderr.write(f"!! metadata '{f}' fails the store regex (no comma / em-dash, <=256 chars): {v!r}\n")
|
||||
sys.exit(1)
|
||||
m["version"] = ver
|
||||
for app in m["application"]:
|
||||
app["revision"] = rev
|
||||
json.dump(m, open(dst, "w"), indent=4); open(dst, "a").write("\n")
|
||||
PY
|
||||
echo "==> staged $SLUG (version $VERSION, revision $REVISION)"
|
||||
|
||||
# ---- 4. commit on a branch, push, open/update the PR -------------------
|
||||
BR="update-wadamesh-$VERSION"
|
||||
git -C "$FORK_DIR" checkout -qB "$BR"
|
||||
git -C "$FORK_DIR" add "$SLUG"
|
||||
GIT_COMMITTER_NAME="Kaj Schittecat" GIT_COMMITTER_EMAIL="kaj@schittecat.com" \
|
||||
git -C "$FORK_DIR" commit -q --author="Kaj Schittecat <kaj@schittecat.com>" \
|
||||
-m "Update WadaMesh to $VERSION (revision $REVISION)" || echo " (nothing new to commit)"
|
||||
git -C "$FORK_DIR" push -fq -u origin "$BR"
|
||||
if gh pr create --repo "$UPSTREAM" --base main --head "$GHUSER:$BR" \
|
||||
--title "Update WadaMesh to $VERSION" \
|
||||
--body "Updates WadaMesh ($SLUG) to v$VERSION (revision $REVISION) — new Tanmatsu build." 2>/dev/null; then
|
||||
echo "==> opened PR for $VERSION"
|
||||
else
|
||||
echo "==> PR already open for $BR (force-push updated it)"
|
||||
fi
|
||||
echo "==> done. Track it: gh pr list --repo $UPSTREAM --author $GHUSER"
|
||||
@@ -0,0 +1,487 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError:
|
||||
print("Missing dependency: pyserial", file=sys.stderr)
|
||||
print("Install with: python -m pip install pyserial", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
CMD_APP_START = 1
|
||||
CMD_SEND_TXT_MSG = 2
|
||||
CMD_SYNC_NEXT_MESSAGE = 10
|
||||
CMD_GET_BATT_AND_STORAGE = 20
|
||||
CMD_DEVICE_QUERY = 22
|
||||
CMD_SEND_TELEMETRY_REQ = 39
|
||||
CMD_GET_CUSTOM_VARS = 40
|
||||
CMD_GET_STATS = 56
|
||||
|
||||
RESP_CODE_SELF_INFO = 5
|
||||
RESP_CODE_SENT = 6
|
||||
RESP_CODE_CONTACT_MSG_RECV_V3 = 16
|
||||
RESP_CODE_NO_MORE_MESSAGES = 10
|
||||
RESP_CODE_BATT_AND_STORAGE = 12
|
||||
RESP_CODE_DEVICE_INFO = 13
|
||||
RESP_CODE_CUSTOM_VARS = 21
|
||||
RESP_CODE_STATS = 24
|
||||
|
||||
PUSH_CODE_SEND_CONFIRMED = 0x82
|
||||
PUSH_CODE_TELEMETRY_RESPONSE = 0x8B
|
||||
PUSH_CODE_BINARY_RESPONSE = 0x8C
|
||||
|
||||
TXT_TYPE_CLI_DATA = 1
|
||||
|
||||
STATS_TYPE_CORE = 0
|
||||
STATS_TYPE_RADIO = 1
|
||||
STATS_TYPE_PACKETS = 2
|
||||
|
||||
MESHCOM_PREFIX = b"MESHCM"
|
||||
|
||||
|
||||
def build_frame(payload: bytes) -> bytes:
|
||||
return b"<" + struct.pack("<H", len(payload)) + payload
|
||||
|
||||
|
||||
def read_frame(ser: serial.Serial, timeout_s: float = 2.0):
|
||||
deadline = time.time() + timeout_s
|
||||
while time.time() < deadline:
|
||||
b = ser.read(1)
|
||||
if not b:
|
||||
continue
|
||||
if b != b">":
|
||||
continue
|
||||
hdr = ser.read(2)
|
||||
if len(hdr) != 2:
|
||||
return None
|
||||
length = struct.unpack("<H", hdr)[0]
|
||||
payload = ser.read(length)
|
||||
if len(payload) != length:
|
||||
return None
|
||||
return payload
|
||||
return None
|
||||
|
||||
|
||||
def send_and_collect(ser: serial.Serial, name: str, payload: bytes, listen_s: float = 1.0):
|
||||
print(f"\n== {name} ==")
|
||||
print("TX:", payload.hex(" "))
|
||||
ser.write(build_frame(payload))
|
||||
ser.flush()
|
||||
frames = []
|
||||
deadline = time.time() + listen_s
|
||||
while time.time() < deadline:
|
||||
frame = read_frame(ser, timeout_s=min(0.35, max(0.05, deadline - time.time())))
|
||||
if frame is None:
|
||||
continue
|
||||
frames.append(frame)
|
||||
if not frames:
|
||||
print("RX: <timeout>")
|
||||
return []
|
||||
for idx, frame in enumerate(frames, start=1):
|
||||
print(f"RX[{idx}]:", frame.hex(" "))
|
||||
decode_frame(frame)
|
||||
return frames
|
||||
|
||||
|
||||
def decode_frame(frame: bytes):
|
||||
code = frame[0]
|
||||
print(f"Code: 0x{code:02X} ({code})")
|
||||
if code == RESP_CODE_DEVICE_INFO:
|
||||
decode_device_info(frame)
|
||||
elif code == RESP_CODE_BATT_AND_STORAGE:
|
||||
decode_batt_and_storage(frame)
|
||||
elif code == RESP_CODE_SELF_INFO:
|
||||
print("Self info response received")
|
||||
elif code == RESP_CODE_CUSTOM_VARS:
|
||||
decode_custom_vars(frame)
|
||||
elif code == RESP_CODE_STATS:
|
||||
decode_stats(frame)
|
||||
elif code == RESP_CODE_CONTACT_MSG_RECV_V3:
|
||||
decode_contact_msg_recv_v3(frame)
|
||||
elif code == RESP_CODE_NO_MORE_MESSAGES:
|
||||
print("No more queued messages")
|
||||
elif code == RESP_CODE_SENT:
|
||||
decode_sent(frame)
|
||||
elif code == PUSH_CODE_SEND_CONFIRMED:
|
||||
decode_send_confirmed(frame)
|
||||
elif code == PUSH_CODE_TELEMETRY_RESPONSE:
|
||||
decode_telemetry(frame)
|
||||
elif code == PUSH_CODE_BINARY_RESPONSE:
|
||||
decode_binary_response(frame)
|
||||
else:
|
||||
print("Unhandled response")
|
||||
|
||||
|
||||
def decode_c_string(raw: bytes) -> str:
|
||||
return raw.split(b"\x00", 1)[0].decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def decode_device_info(frame: bytes):
|
||||
if len(frame) < 80:
|
||||
print("Short DEVICE_INFO frame")
|
||||
return
|
||||
fw_ver_code = frame[1]
|
||||
max_contacts_x2 = frame[2]
|
||||
max_channels = frame[3]
|
||||
ble_pin = struct.unpack("<I", frame[4:8])[0]
|
||||
build_date = decode_c_string(frame[8:20])
|
||||
manufacturer = decode_c_string(frame[20:60])
|
||||
version = decode_c_string(frame[60:80]) if len(frame) >= 80 else ""
|
||||
print("Firmware code:", fw_ver_code)
|
||||
print("Max contacts:", max_contacts_x2 * 2)
|
||||
print("Max channels:", max_channels)
|
||||
print("BLE pin:", ble_pin)
|
||||
print("Build date:", build_date)
|
||||
print("Manufacturer:", manufacturer)
|
||||
if version:
|
||||
print("Version:", version)
|
||||
|
||||
|
||||
def decode_batt_and_storage(frame: bytes):
|
||||
if len(frame) < 11:
|
||||
print("Short BATT_AND_STORAGE frame")
|
||||
return
|
||||
battery_mv = struct.unpack("<H", frame[1:3])[0]
|
||||
used_kb = struct.unpack("<I", frame[3:7])[0]
|
||||
total_kb = struct.unpack("<I", frame[7:11])[0]
|
||||
print("Battery:", f"{battery_mv} mV ({battery_mv / 1000.0:.2f} V)")
|
||||
print("Storage used:", f"{used_kb} KB")
|
||||
print("Storage total:", f"{total_kb} KB")
|
||||
|
||||
|
||||
def decode_custom_vars(frame: bytes):
|
||||
raw = frame[1:].decode("utf-8", errors="replace")
|
||||
print("Custom vars:", raw if raw else "<empty>")
|
||||
|
||||
|
||||
def decode_stats(frame: bytes):
|
||||
if len(frame) < 2:
|
||||
print("Short STATS frame")
|
||||
return
|
||||
stats_type = frame[1]
|
||||
if stats_type == STATS_TYPE_CORE and len(frame) >= 11:
|
||||
battery_mv = struct.unpack("<H", frame[2:4])[0]
|
||||
uptime_secs = struct.unpack("<I", frame[4:8])[0]
|
||||
err_flags = struct.unpack("<H", frame[8:10])[0]
|
||||
queue_len = frame[10]
|
||||
print("Stats type: core")
|
||||
print("Battery:", f"{battery_mv} mV ({battery_mv / 1000.0:.2f} V)")
|
||||
print("Uptime:", f"{uptime_secs} s")
|
||||
print("Error flags:", f"0x{err_flags:04X}")
|
||||
print("Outbound queue:", queue_len)
|
||||
elif stats_type == STATS_TYPE_RADIO and len(frame) >= 14:
|
||||
noise_floor = struct.unpack("<h", frame[2:4])[0]
|
||||
last_rssi = struct.unpack("<b", frame[4:5])[0]
|
||||
last_snr_q4 = struct.unpack("<b", frame[5:6])[0]
|
||||
tx_air_secs = struct.unpack("<I", frame[6:10])[0]
|
||||
rx_air_secs = struct.unpack("<I", frame[10:14])[0]
|
||||
print("Stats type: radio")
|
||||
print("Noise floor:", f"{noise_floor} dBm")
|
||||
print("Last RSSI:", f"{last_rssi} dBm")
|
||||
print("Last SNR:", f"{last_snr_q4 / 4.0:.2f} dB")
|
||||
print("TX airtime:", f"{tx_air_secs} s")
|
||||
print("RX airtime:", f"{rx_air_secs} s")
|
||||
elif stats_type == STATS_TYPE_PACKETS and len(frame) >= 30:
|
||||
values = struct.unpack("<IIIIIII", frame[2:30])
|
||||
labels = [
|
||||
"recv",
|
||||
"sent",
|
||||
"sent_flood",
|
||||
"sent_direct",
|
||||
"recv_flood",
|
||||
"recv_direct",
|
||||
"recv_errors",
|
||||
]
|
||||
print("Stats type: packets")
|
||||
for label, value in zip(labels, values):
|
||||
print(f"{label}:", value)
|
||||
else:
|
||||
print(f"Unknown stats subtype {stats_type} or short frame")
|
||||
|
||||
|
||||
def decode_sent(frame: bytes):
|
||||
if len(frame) < 10:
|
||||
print("Short SENT frame")
|
||||
return
|
||||
flood = frame[1]
|
||||
tag = struct.unpack("<I", frame[2:6])[0]
|
||||
est_timeout = struct.unpack("<I", frame[6:10])[0]
|
||||
print("Message accepted")
|
||||
print("Flood:", flood)
|
||||
print("Tag:", tag)
|
||||
print("Estimated timeout:", est_timeout)
|
||||
|
||||
|
||||
def decode_send_confirmed(frame: bytes):
|
||||
if len(frame) < 9:
|
||||
print("Short SEND_CONFIRMED frame")
|
||||
return
|
||||
ack = struct.unpack("<I", frame[1:5])[0]
|
||||
trip_time = struct.unpack("<I", frame[5:9])[0]
|
||||
print("Send confirmed")
|
||||
print("Ack/tag:", ack)
|
||||
print("Trip time:", trip_time)
|
||||
|
||||
|
||||
def decode_binary_response(frame: bytes):
|
||||
if len(frame) < 6:
|
||||
print("Short BINARY_RESPONSE frame")
|
||||
return
|
||||
reserved = frame[1]
|
||||
tag = struct.unpack("<I", frame[2:6])[0]
|
||||
text = frame[6:].decode("utf-8", errors="replace").rstrip("\x00")
|
||||
print("Binary response reserved:", reserved)
|
||||
print("Binary response tag:", tag)
|
||||
print("Binary response text:")
|
||||
print(text if text else "<empty>")
|
||||
|
||||
|
||||
def decode_contact_msg_recv_v3(frame: bytes):
|
||||
if len(frame) < 16:
|
||||
print("Short CONTACT_MSG_RECV_V3 frame")
|
||||
return
|
||||
snr_q4 = struct.unpack("<b", frame[1:2])[0]
|
||||
reserved1 = frame[2]
|
||||
reserved2 = frame[3]
|
||||
pubkey_prefix = frame[4:10].hex()
|
||||
path_len = frame[10]
|
||||
txt_type = frame[11]
|
||||
timestamp = struct.unpack("<I", frame[12:16])[0]
|
||||
text = frame[16:].decode("utf-8", errors="replace").rstrip("\x00")
|
||||
print("Queued contact message")
|
||||
print("SNR:", f"{snr_q4 / 4.0:.2f} dB")
|
||||
print("Reserved:", reserved1, reserved2)
|
||||
print("From pubkey prefix:", pubkey_prefix)
|
||||
print("Path len:", path_len)
|
||||
print("Text type:", txt_type)
|
||||
print("Timestamp:", timestamp)
|
||||
print("Text:")
|
||||
print(text if text else "<empty>")
|
||||
|
||||
|
||||
def _read_i16_be(buf: bytes) -> int:
|
||||
return struct.unpack(">h", buf)[0]
|
||||
|
||||
|
||||
def _read_u16_be(buf: bytes) -> int:
|
||||
return struct.unpack(">H", buf)[0]
|
||||
|
||||
|
||||
def _read_u32_be(buf: bytes) -> int:
|
||||
return struct.unpack(">I", buf)[0]
|
||||
|
||||
|
||||
def _read_i24_be(buf: bytes) -> int:
|
||||
val = int.from_bytes(buf, "big", signed=False)
|
||||
if val & 0x800000:
|
||||
val -= 1 << 24
|
||||
return val
|
||||
|
||||
|
||||
def parse_lpp(payload: bytes):
|
||||
i = 0
|
||||
items = []
|
||||
while i + 2 <= len(payload):
|
||||
channel = payload[i]
|
||||
data_type = payload[i + 1]
|
||||
i += 2
|
||||
if data_type in (0, 1, 102, 120, 142):
|
||||
if i + 1 > len(payload):
|
||||
break
|
||||
value = payload[i]
|
||||
i += 1
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type in (2, 3):
|
||||
if i + 2 > len(payload):
|
||||
break
|
||||
value = _read_i16_be(payload[i:i + 2]) / 100.0
|
||||
i += 2
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type in (101, 115, 125, 128, 132):
|
||||
if i + 2 > len(payload):
|
||||
break
|
||||
value = _read_u16_be(payload[i:i + 2])
|
||||
i += 2
|
||||
if data_type == 115:
|
||||
value /= 10.0
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type == 121:
|
||||
if i + 2 > len(payload):
|
||||
break
|
||||
value = _read_i16_be(payload[i:i + 2])
|
||||
i += 2
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type == 103:
|
||||
if i + 2 > len(payload):
|
||||
break
|
||||
value = _read_i16_be(payload[i:i + 2]) / 10.0
|
||||
i += 2
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type == 104:
|
||||
if i + 1 > len(payload):
|
||||
break
|
||||
value = payload[i] / 2.0
|
||||
i += 1
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type in (116,):
|
||||
if i + 2 > len(payload):
|
||||
break
|
||||
value = _read_u16_be(payload[i:i + 2]) / 100.0
|
||||
i += 2
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type in (117,):
|
||||
if i + 2 > len(payload):
|
||||
break
|
||||
value = _read_u16_be(payload[i:i + 2]) / 1000.0
|
||||
i += 2
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type in (100, 118, 131, 133):
|
||||
if i + 4 > len(payload):
|
||||
break
|
||||
value = _read_u32_be(payload[i:i + 4])
|
||||
i += 4
|
||||
if data_type == 131:
|
||||
value /= 1000.0
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type == 130:
|
||||
if i + 4 > len(payload):
|
||||
break
|
||||
value = _read_u32_be(payload[i:i + 4]) / 1000.0
|
||||
i += 4
|
||||
items.append((channel, data_type, value))
|
||||
elif data_type in (113, 134):
|
||||
if i + 6 > len(payload):
|
||||
break
|
||||
x = _read_i16_be(payload[i:i + 2])
|
||||
y = _read_i16_be(payload[i + 2:i + 4])
|
||||
z = _read_i16_be(payload[i + 4:i + 6])
|
||||
i += 6
|
||||
scale = 1000.0 if data_type == 113 else 100.0
|
||||
items.append((channel, data_type, (x / scale, y / scale, z / scale)))
|
||||
elif data_type == 136:
|
||||
if i + 9 > len(payload):
|
||||
break
|
||||
lat = _read_i24_be(payload[i:i + 3]) / 10000.0
|
||||
lon = _read_i24_be(payload[i + 3:i + 6]) / 10000.0
|
||||
alt = _read_i24_be(payload[i + 6:i + 9]) / 100.0
|
||||
i += 9
|
||||
items.append((channel, data_type, (lat, lon, alt)))
|
||||
else:
|
||||
items.append((channel, data_type, "<unknown>"))
|
||||
break
|
||||
return items
|
||||
|
||||
|
||||
def format_lpp_item(channel: int, data_type: int, value):
|
||||
names = {
|
||||
0: "digital_input",
|
||||
1: "digital_output",
|
||||
2: "analog_input",
|
||||
3: "analog_output",
|
||||
100: "generic_sensor",
|
||||
101: "luminosity",
|
||||
102: "presence",
|
||||
103: "temperature_c",
|
||||
104: "humidity_pct",
|
||||
113: "accelerometer_g",
|
||||
115: "barometric_hpa",
|
||||
116: "voltage_v",
|
||||
117: "current_a",
|
||||
118: "frequency_hz",
|
||||
120: "percentage",
|
||||
121: "altitude_m",
|
||||
125: "concentration_ppm",
|
||||
128: "power_w",
|
||||
130: "distance_m",
|
||||
131: "energy_kwh",
|
||||
132: "direction_deg",
|
||||
133: "unixtime",
|
||||
134: "gyrometer_dps",
|
||||
136: "gps",
|
||||
142: "switch",
|
||||
}
|
||||
return f"ch{channel} {names.get(data_type, f'type_{data_type}')} = {value}"
|
||||
|
||||
|
||||
def drain_sync_messages(ser: serial.Serial, max_reads: int = 8):
|
||||
for idx in range(max_reads):
|
||||
frames = send_and_collect(
|
||||
ser,
|
||||
f"CMD_SYNC_NEXT_MESSAGE #{idx + 1}",
|
||||
bytes([CMD_SYNC_NEXT_MESSAGE]),
|
||||
listen_s=0.7,
|
||||
)
|
||||
if not frames:
|
||||
break
|
||||
if any(frame[:1] == bytes([RESP_CODE_NO_MORE_MESSAGES]) for frame in frames):
|
||||
break
|
||||
|
||||
|
||||
def decode_telemetry(frame: bytes):
|
||||
if len(frame) < 8:
|
||||
print("Short TELEMETRY_RESPONSE frame")
|
||||
return
|
||||
reserved = frame[1]
|
||||
pubkey_prefix = frame[2:8].hex()
|
||||
lpp = frame[8:]
|
||||
print("Telemetry reserved:", reserved)
|
||||
print("Telemetry pubkey prefix:", pubkey_prefix)
|
||||
print("Telemetry raw LPP:", lpp.hex(" "))
|
||||
items = parse_lpp(lpp)
|
||||
if not items:
|
||||
print("Telemetry parsed: <empty>")
|
||||
return
|
||||
print("Telemetry parsed:")
|
||||
for channel, data_type, value in items:
|
||||
print(format_lpp_item(channel, data_type, value))
|
||||
|
||||
|
||||
def build_meshcomod_cli(command: str) -> bytes:
|
||||
return (
|
||||
bytes([CMD_SEND_TXT_MSG, TXT_TYPE_CLI_DATA, 0])
|
||||
+ struct.pack("<I", 0)
|
||||
+ MESHCOM_PREFIX
|
||||
+ command.encode("utf-8")
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Probe WADAMESH companion protocol over serial")
|
||||
parser.add_argument("--port", required=True, help="Serial port, e.g. COM4")
|
||||
parser.add_argument("--baud", type=int, default=115200, help="Serial baud rate")
|
||||
args = parser.parse_args()
|
||||
|
||||
with serial.Serial(args.port, args.baud, timeout=0.25) as ser:
|
||||
time.sleep(0.2)
|
||||
try:
|
||||
ser.reset_input_buffer()
|
||||
ser.reset_output_buffer()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
device_query = bytes([CMD_DEVICE_QUERY, 3])
|
||||
app_start = bytes([CMD_APP_START]) + bytes(7) + b"wadamesh-probe"
|
||||
|
||||
send_and_collect(ser, "CMD_DEVICE_QUERY", device_query, listen_s=0.8)
|
||||
send_and_collect(ser, "CMD_APP_START", app_start, listen_s=0.8)
|
||||
drain_sync_messages(ser, max_reads=4)
|
||||
send_and_collect(ser, "CMD_GET_BATT_AND_STORAGE", bytes([CMD_GET_BATT_AND_STORAGE]), listen_s=0.8)
|
||||
send_and_collect(ser, "CMD_GET_CUSTOM_VARS", bytes([CMD_GET_CUSTOM_VARS]), listen_s=0.8)
|
||||
send_and_collect(ser, "CMD_GET_STATS core", bytes([CMD_GET_STATS, STATS_TYPE_CORE]), listen_s=0.8)
|
||||
send_and_collect(ser, "CMD_GET_STATS radio", bytes([CMD_GET_STATS, STATS_TYPE_RADIO]), listen_s=0.8)
|
||||
send_and_collect(ser, "CMD_GET_STATS packets", bytes([CMD_GET_STATS, STATS_TYPE_PACKETS]), listen_s=0.8)
|
||||
send_and_collect(ser, "CMD_SEND_TELEMETRY_REQ self", bytes([CMD_SEND_TELEMETRY_REQ, 0, 0, 0]), listen_s=1.2)
|
||||
send_and_collect(ser, "CMD_SEND_TXT_MSG meshcomod status", build_meshcomod_cli("status"), listen_s=1.5)
|
||||
drain_sync_messages(ser, max_reads=4)
|
||||
send_and_collect(ser, "CMD_SEND_TXT_MSG meshcomod wifi status", build_meshcomod_cli("wifi status"), listen_s=1.5)
|
||||
drain_sync_messages(ser, max_reads=4)
|
||||
send_and_collect(ser, "CMD_SEND_TXT_MSG meshcomod ota status", build_meshcomod_cli("ota status"), listen_s=1.5)
|
||||
drain_sync_messages(ser, max_reads=4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -94,6 +94,12 @@ public:
|
||||
uint32_t ack_hash) {
|
||||
(void)to_pub; (void)to_name; (void)text; (void)ack_hash;
|
||||
}
|
||||
/** Mirror an app-originated CHANNEL message into the on-device UI — the channel-send path, like the
|
||||
* DM path above, never shows companion-originated sends on screen otherwise. text is NUL-terminated
|
||||
* by the caller. Default no-op for non-touch UIs. */
|
||||
virtual void appSentMsgToChannel(const char* channel_name, const char* text) {
|
||||
(void)channel_name; (void)text;
|
||||
}
|
||||
virtual void notify(UIEventType t = UIEventType::none) = 0;
|
||||
virtual void appendDiag(const char* message) { (void)message; }
|
||||
/** Notify UI of an advert reception. `is_new=true` means the contact is NOT in
|
||||
|
||||
+7
-1
@@ -2201,7 +2201,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
// Diagnostic (room-server login trace): the login response we matched +
|
||||
// whether we accepted it. resp byte: 0x4F 'O' = legacy "OK"; otherwise a
|
||||
// RESP_SERVER_LOGIN_OK / fail code. Pair with "[ROOM] login send".
|
||||
Serial.printf("[ROOM] login resp '%s' resp=0x%02x ok=%d perms=%d\n",
|
||||
WIRE_DBG("[ROOM] login resp '%s' resp=0x%02x ok=%d perms=%d\n",
|
||||
contact.name, data[4], (int)ok, (int)perms);
|
||||
#ifdef DISPLAY_CLASS
|
||||
// Notify the touch UI so the admin console can flip from "logging in…"
|
||||
@@ -2880,6 +2880,12 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
bool success = getChannel(channel_idx, channel);
|
||||
if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, len - i)) {
|
||||
writeOKFrame();
|
||||
#if defined(HAS_TANMATSU)
|
||||
// Mirror the app-sent channel message into the on-device touch UI — the channel-send path
|
||||
// otherwise never shows companion-originated channel sends on screen (the DM path does, via
|
||||
// appSentMsgToContact). NUL-terminate the text in-place first, exactly as the DM path does.
|
||||
if (_ui) { cmd_frame[len] = 0; _ui->appSentMsgToChannel(channel.name, text); }
|
||||
#endif
|
||||
// Sent channel messages are not added to shared history / broadcast: channel_idx and
|
||||
// frame format are device-specific and clients (e.g. HA) without channel support can
|
||||
// misparse or show "text as sender"; also avoids failed-to-sync when versions differ.
|
||||
|
||||
+19
-1
@@ -6,6 +6,20 @@
|
||||
#include <helpers/MeshTouchTxTrace.h>
|
||||
#include "AbstractUITask.h"
|
||||
|
||||
// Operational wire-debug. The companion link is a BINARY protocol over Serial (USB-CDC); writing raw
|
||||
// text to Serial during a session interleaves into that stream and corrupts it — the MeshCore app
|
||||
// then throws "Bad state: Streamsink is bound to a stream" and the connect / room login / repeater
|
||||
// admin all fail (GH #25, #54, #23). Compiled out in release; build -DCOMPANION_WIRE_DEBUG=1 to get
|
||||
// these traces back (only safe over a non-companion debug console).
|
||||
#ifndef COMPANION_WIRE_DEBUG
|
||||
#define COMPANION_WIRE_DEBUG 0
|
||||
#endif
|
||||
#if COMPANION_WIRE_DEBUG
|
||||
#define WIRE_DBG(...) Serial.printf(__VA_ARGS__)
|
||||
#else
|
||||
#define WIRE_DBG(...) do {} while (0)
|
||||
#endif
|
||||
|
||||
/*------------ Frame Protocol --------------*/
|
||||
// Keep the fork's companion-protocol lineage (27 — a superset of upstream's 13)
|
||||
// so the existing meshcomod web/phone client still matches. 1.16's new companion
|
||||
@@ -328,7 +342,7 @@ public:
|
||||
// Diagnostic (room-server login trace): which contact/type we sent the login
|
||||
// to and the send result. Pair with the "[ROOM] login resp" line in
|
||||
// onContactResponse to see exactly where a room login breaks.
|
||||
Serial.printf("[ROOM] login send '%s' type=%u pwlen=%u sync_since=%lu -> r=%d\n",
|
||||
WIRE_DBG("[ROOM] login send '%s' type=%u pwlen=%u sync_since=%lu -> r=%d\n",
|
||||
recipient.name, (unsigned)recipient.type,
|
||||
(unsigned)(password ? strlen(password) : 0),
|
||||
(unsigned long)recipient.sync_since, r);
|
||||
@@ -937,4 +951,8 @@ private:
|
||||
uint32_t _ui_trace_ping_tag = 0;
|
||||
};
|
||||
|
||||
#if defined(ESP32_PLATFORM)
|
||||
extern MyMesh& the_mesh; // PSRAM-resident (placement-new'd in main.cpp); reference keeps call sites unchanged
|
||||
#else
|
||||
extern MyMesh the_mesh;
|
||||
#endif
|
||||
@@ -35,7 +35,7 @@ static bool s_begun = false;
|
||||
// short read (→ treat as absent → defaults); `ver` lets later builds add fields.
|
||||
static const char* KEY_CFG = "cfg";
|
||||
static const uint16_t TOUCH_CFG_MAGIC = 0x5743; // 'WC' (WadaCfg)
|
||||
static const uint8_t TOUCH_CFG_VER = 18; // v2 sig_probe/poll; v3 tz_zone; v4 hide_node_name; v5 map_night/map_zoom; v6 map text/marker visibility; v7 app_grid_large; v8 ui_scale; v9 tb_keypad; v10 sleep_idle; v11 nav_keys; v12 map_zoom_buttons; v13 nav_dir_keys; v14 home_is_drawer; v15 kbd_nav default ON (one-time migrate); v16 nav_scroll_keys; v17 notify_new_contact; v18 kbd_nav OFF by default (reverses v15; T-Deck/V4 only, Tanmatsu stays on)
|
||||
static const uint8_t TOUCH_CFG_VER = 21; // v2 sig_probe/poll; v3 tz_zone; v4 hide_node_name; v5 map_night/map_zoom; v6 map text/marker visibility; v7 app_grid_large; v8 ui_scale; v9 tb_keypad; v10 sleep_idle; v11 nav_keys; v12 map_zoom_buttons; v13 nav_dir_keys; v14 home_is_drawer; v15 kbd_nav default ON (one-time migrate); v16 nav_scroll_keys; v17 notify_new_contact; v18 kbd_nav OFF by default (reverses v15; T-Deck/V4 only, Tanmatsu stays on); v19 show_sensors_tab; v20 map_show_links; v21 map_style (0=OSM default, 1=OpenTopoMap)
|
||||
|
||||
// Defaults (kept identical to the historical per-key defaults).
|
||||
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
|
||||
@@ -89,6 +89,9 @@ struct __attribute__((packed)) TouchCfg {
|
||||
uint8_t home_is_drawer; // Home tab defaults to the app drawer (1) vs the Commander screen (0, default) — v14 (trailing)
|
||||
uint8_t nav_scroll_keys[2]; // keyboard-nav scroll keys (ASCII): scroll-up, scroll-down — v16 (trailing)
|
||||
uint8_t notify_new_contact;// toast/chip when a contact is auto-discovered (bool) — v17 (trailing)
|
||||
uint8_t show_sensors_tab; // V4 Expansion Kit: show the Sensors tab + Home env widget (bool, default 1) — v19 (trailing)
|
||||
uint8_t map_show_links; // show self->contact link lines on the map (bool, default 1) — v20 (trailing)
|
||||
uint8_t map_style; // map tile style: 0=OpenStreetMap (default), 1=OpenTopoMap — v21 (trailing)
|
||||
};
|
||||
|
||||
static TouchCfg s_cfg;
|
||||
@@ -166,6 +169,9 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
c.nav_scroll_keys[0] = 'f'; c.nav_scroll_keys[1] = 'c'; // default scroll-up F / scroll-down C
|
||||
#endif
|
||||
c.notify_new_contact = 1; // default: show the new-contact toast (preserve prior behaviour)
|
||||
c.show_sensors_tab = 1; // default: show the V4 Expansion-Kit Sensors tab + Home env widget
|
||||
c.map_show_links = 1; // default: show self->contact link lines (PR #61)
|
||||
c.map_style = 0; // default: OpenStreetMap (OpenTopoMap is opt-in)
|
||||
}
|
||||
|
||||
// Persist the whole blob using the same end()/begin(RW)/put/end()/begin(RO)
|
||||
@@ -756,6 +762,20 @@ bool touchPrefsSetNewContactToast(bool on) {
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
// Heltec V4 Expansion Kit: show the Sensors tab + the Home env widget. Default
|
||||
// ON. The UI also requires an ENVIRONMENT sensor to actually be present (checked
|
||||
// at runtime in buildUiTree), so a bare V4 hides the UI even with this on.
|
||||
bool touchPrefsGetShowSensorsTab() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.show_sensors_tab != 0; // default = show the Sensors tab
|
||||
}
|
||||
|
||||
bool touchPrefsSetShowSensorsTab(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.show_sensors_tab = on ? 1 : 0;
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
bool touchPrefsGetMapNight() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.map_night != 0;
|
||||
@@ -811,6 +831,24 @@ bool touchPrefsSetMapShowContacts(bool on) {
|
||||
s_cfg.map_show_contacts = on ? 1 : 0;
|
||||
return cfgFlush();
|
||||
}
|
||||
bool touchPrefsGetMapShowLinks() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.map_show_links != 0;
|
||||
}
|
||||
bool touchPrefsSetMapShowLinks(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.map_show_links = on ? 1 : 0;
|
||||
return cfgFlush();
|
||||
}
|
||||
uint8_t touchPrefsGetMapStyle() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.map_style;
|
||||
}
|
||||
bool touchPrefsSetMapStyle(uint8_t style) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.map_style = style;
|
||||
return cfgFlush();
|
||||
}
|
||||
bool touchPrefsGetAppGridLarge() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.app_grid_large != 0;
|
||||
@@ -1255,6 +1293,11 @@ void touchPrefsSetEdgeScroll(bool on) { if (!s_begun) touchPrefsBegin(); pr
|
||||
bool touchPrefsGetLockOnScreenOff() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("lock_off", 0) != 0; }
|
||||
void touchPrefsSetLockOnScreenOff(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("lock_off", on ? 1 : 0); }
|
||||
|
||||
#if defined(HAS_TANMATSU) // only the Tanmatsu has the message LED — keep S3 (T-Deck/V4) bins unchanged
|
||||
bool touchPrefsGetMsgLed() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("msg_led", 1) != 0; } // default ON
|
||||
void touchPrefsSetMsgLed(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("msg_led", on ? 1 : 0); }
|
||||
#endif
|
||||
|
||||
// Generic blob (used to persist the discovered-nodes list across reboots).
|
||||
size_t touchPrefsGetBlob(const char* key, uint8_t* out, size_t maxlen) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
@@ -1279,6 +1322,12 @@ uint8_t touchPrefsGetSoundVolume() {
|
||||
return v > 100 ? 100 : v;
|
||||
}
|
||||
void touchPrefsSetSoundVolume(uint8_t vol) { if (vol > 100) vol = 100; if (!s_begun) touchPrefsBegin(); prefsPutUChar("snd_vol", vol); }
|
||||
uint8_t touchPrefsGetKbdBacklight() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
uint8_t v = s_prefs.getUChar("kbd_bl", 100);
|
||||
return v > 100 ? 100 : v;
|
||||
}
|
||||
void touchPrefsSetKbdBacklight(uint8_t pct) { if (pct > 100) pct = 100; if (!s_begun) touchPrefsBegin(); prefsPutUChar("kbd_bl", pct); }
|
||||
|
||||
// Per-channel mute (name-keyed NVS blob "chm" + tiny RAM cache) --------------
|
||||
static const char* KEY_CHM = "chm";
|
||||
|
||||
@@ -98,6 +98,12 @@ bool touchPrefsGetMapShowTileXYZ();
|
||||
bool touchPrefsSetMapShowTileXYZ(bool on);
|
||||
bool touchPrefsGetMapShowContacts();
|
||||
bool touchPrefsSetMapShowContacts(bool on);
|
||||
bool touchPrefsGetMapShowLinks();
|
||||
bool touchPrefsSetMapShowLinks(bool on);
|
||||
|
||||
/** Map tile style: 0 = OpenStreetMap (default), 1 = OpenTopoMap (topographic, opt-in). */
|
||||
uint8_t touchPrefsGetMapStyle();
|
||||
bool touchPrefsSetMapStyle(uint8_t style);
|
||||
|
||||
/* App drawer: large grid (one fewer column → bigger icons + labels, for low vision).
|
||||
* Default false = the compact grid (T-Deck 4 cols / Heltec V4 3 cols). */
|
||||
@@ -153,6 +159,13 @@ bool touchPrefsSetHideNodeName(bool hide);
|
||||
bool touchPrefsGetNewContactToast();
|
||||
bool touchPrefsSetNewContactToast(bool on);
|
||||
|
||||
/** Heltec V4 Expansion Kit only: show the bottom "Sensors" tab + the Home
|
||||
* environment widget. Default true. Combined with a runtime check for any
|
||||
* ENVIRONMENT sensor actually being present, so a bare V4 hides the UI even
|
||||
* with this on. Read in buildUiTree → applies after a restart. */
|
||||
bool touchPrefsGetShowSensorsTab();
|
||||
bool touchPrefsSetShowSensorsTab(bool on);
|
||||
|
||||
/** Store all device data (identity/prefs/contacts/channels) on the SD card under
|
||||
* /meshcomod instead of internal SPIFFS. T-Deck only; read at boot before data
|
||||
* loads, so changing it requires a reboot. Default false (SPIFFS). */
|
||||
@@ -257,6 +270,11 @@ bool touchPrefsGetSoundMentions(); // default true
|
||||
void touchPrefsSetSoundMentions(bool on);
|
||||
uint8_t touchPrefsGetSoundVolume(); // 0..100, default 70
|
||||
void touchPrefsSetSoundVolume(uint8_t vol);
|
||||
|
||||
/** Tanmatsu keyboard backlight brightness, 0–100 %. Default 100. Separate from
|
||||
* screen brightness (touchPrefsGet/SetBrightness) and volume (…SoundVolume). */
|
||||
uint8_t touchPrefsGetKbdBacklight();
|
||||
void touchPrefsSetKbdBacklight(uint8_t pct);
|
||||
bool touchPrefsGetEnterSends(); // Enter key sends a chat message (default true)
|
||||
void touchPrefsSetEnterSends(bool on);
|
||||
bool touchPrefsGetClock12h(); // 12-hour clock (default false = 24h)
|
||||
@@ -339,6 +357,13 @@ bool touchPrefsSetLockTextColor(uint32_t rgb);
|
||||
bool touchPrefsGetColorfulBubbles();
|
||||
bool touchPrefsSetColorfulBubbles(bool on);
|
||||
|
||||
/** Tanmatsu message-notification LED: flash the envelope-icon LED on a new message and breathe it
|
||||
* softly while there are unread messages. Default ON. Tanmatsu-only (no such LED on T-Deck/V4). */
|
||||
#if defined(HAS_TANMATSU)
|
||||
bool touchPrefsGetMsgLed();
|
||||
void touchPrefsSetMsgLed(bool on);
|
||||
#endif
|
||||
|
||||
/** UI accent colour (buttons, active tab, keyboard, highlights) as 0xRRGGBB.
|
||||
* Default = the stock neutral gray; the picker keeps it dark enough that the
|
||||
* off-white text stays readable. */
|
||||
|
||||
@@ -26,7 +26,12 @@ void tdeckKeyboardBegin() {
|
||||
}
|
||||
|
||||
void tdeckKeyboardSetBacklight(uint8_t level) {
|
||||
if (level != s_bl_desired) { s_bl_desired = level; s_bl_dirty = true; }
|
||||
// Force the FIRST write even when the requested level matches our cached default (0). A reflash
|
||||
// resets the ESP32 but NOT the keyboard's C3 — it keeps its previously-lit backlight — so without
|
||||
// this the boot "off" request (0 == cached 0) was never sent and the backlight stayed on despite
|
||||
// the setting reading "Off" (issue #33). After the first write, change-detection resumes.
|
||||
static bool forced = false;
|
||||
if (level != s_bl_desired || !forced) { s_bl_desired = level; s_bl_dirty = true; forced = true; }
|
||||
}
|
||||
|
||||
void tdeckKeyboardPoll() {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
#include "MyMesh.h"
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <new> // placement-new for the PSRAM-resident the_mesh
|
||||
#include "esp_heap_caps.h" // heap_caps_malloc(MALLOC_CAP_SPIRAM)
|
||||
#endif
|
||||
#if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI)
|
||||
#include <Preferences.h>
|
||||
#include <esp_system.h>
|
||||
@@ -127,11 +131,31 @@ static uint32_t _atoi(const char* sp) {
|
||||
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
#if defined(ESP32_PLATFORM)
|
||||
// the_mesh is ~42 KB (dominated by the MAX_CONTACTS ContactInfo array) and was the
|
||||
// single biggest static internal-DRAM consumer. Place the whole object in PSRAM —
|
||||
// the contacts array rides along inside it — and bind a reference so every
|
||||
// `the_mesh.foo()` call site is unchanged. The constructor still runs HERE at
|
||||
// static-init (PSRAM is already up; the UITask psAlloc statics rely on the same),
|
||||
// so timing/behaviour are identical to the old direct global — only the address
|
||||
// moves off internal DRAM. heap_caps falls back to internal RAM if PSRAM is absent.
|
||||
static MyMesh& makeTheMesh() {
|
||||
void* mem = heap_caps_malloc(sizeof(MyMesh), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!mem) mem = malloc(sizeof(MyMesh)); // no-PSRAM fallback: behaves as before
|
||||
return *new (mem) MyMesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
#ifdef DISPLAY_CLASS
|
||||
, &ui_task
|
||||
#endif
|
||||
);
|
||||
}
|
||||
MyMesh& the_mesh = makeTheMesh();
|
||||
#else
|
||||
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
#ifdef DISPLAY_CLASS
|
||||
, &ui_task
|
||||
#endif
|
||||
);
|
||||
#endif
|
||||
|
||||
/* END GLOBAL OBJECTS */
|
||||
|
||||
@@ -151,11 +175,23 @@ void halt() {
|
||||
unsigned long last_wifi_reconnect_attempt = 0;
|
||||
#endif
|
||||
|
||||
#include "esp_task_wdt.h" // task-watchdog reconfigure — see setup() (GH #56)
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(200);
|
||||
Serial.println("[BOOT] setup start");
|
||||
|
||||
// Widen the task-watchdog grace period. The ~5 s default trips during a legitimate-but-slow flash
|
||||
// burst — a SPIFFS garbage-collect, or a bulk save (DataStore issues ~12 flash ops per contact,
|
||||
// thousands for a full address book). A flash op parks BOTH cores with the cache disabled, so both
|
||||
// IDLE tasks miss their reset and the watchdog panics (decoded coredumps: task_wdt CPU0=IDLE0
|
||||
// CPU1=IDLE1, core 0 in spi_flash_op_block_func). The burst can't be chunked under the limit easily
|
||||
// and the per-core WdtHeavyGuard only covers core 0, so give the dog enough headroom to ride the
|
||||
// burst out while still catching a genuine multi-second hang. (Fixes the random WDT reboots, GH #56.)
|
||||
esp_task_wdt_init(20, true); // 20 s grace (was ~5 s), keep panic. Re-init reconfigures the
|
||||
// already-running TWDT + keeps the idle-task subscriptions.
|
||||
|
||||
#if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI)
|
||||
// Record which slot we booted from so the recovery's "Boot firmware" can return
|
||||
// here. Recovery-first itself is enforced by the CUSTOM bootloader (it boots
|
||||
|
||||
@@ -221,6 +221,155 @@ static const lv_btnmatrix_ctrl_t kb_en_upper_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
/* ---------- French (AZERTY) on-screen keyboard ---------- */
|
||||
/* Keeps the proven LVGL control rows/punctuation, but swaps the alpha rows to
|
||||
* a familiar French AZERTY order. Accents still come from the existing popup. */
|
||||
static const char* kb_fr_lower[] = {
|
||||
"1#", "a", "z", "e", "r", "t", "y", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"ABC", "q", "s", "d", "f", "g", "h", "j", "k", "l", "m", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"_", "-", "w", "x", "c", "v", "b", "n", ".", ",", ":", "", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_fr_lower_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_HIDDEN,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
static const char* kb_fr_upper[] = {
|
||||
"1#", "A", "Z", "E", "R", "T", "Y", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"abc", "Q", "S", "D", "F", "G", "H", "J", "K", "L", "M", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"_", "-", "W", "X", "C", "V", "B", "N", ".", ",", ":", "", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_fr_upper_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_HIDDEN,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
/* Dutch largely stays QWERTY, but a dedicated IJ key makes the most common
|
||||
* digraph directly reachable without forcing users through the accent popup. */
|
||||
static const char* kb_nl_lower[] = {
|
||||
"1#", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"ABC", "a", "s", "d", "f", "g", "h", "j", "k", "l", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"_", "-", "z", "x", "c", "v", "b", "n", "m", ".", ",", "ij", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_nl_lower_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(2),
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
static const char* kb_nl_upper[] = {
|
||||
"1#", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"abc", "A", "S", "D", "F", "G", "H", "J", "K", "L", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"_", "-", "Z", "X", "C", "V", "B", "N", "M", ".", ",", "IJ", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_nl_upper_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(2),
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
/* German QWERTZ keeps the proven LVGL control rows and swaps the Y/Z alpha
|
||||
* positions. Umlauts / eszett still come from the accent popup. */
|
||||
static const char* kb_de_lower[] = {
|
||||
"1#", "q", "w", "e", "r", "t", "z", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"ABC", "a", "s", "d", "f", "g", "h", "j", "k", "l", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"_", "-", "y", "x", "c", "v", "b", "n", "m", ".", ",", ":", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_de_lower_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
static const char* kb_de_upper[] = {
|
||||
"1#", "Q", "W", "E", "R", "T", "Z", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"abc", "A", "S", "D", "F", "G", "H", "J", "K", "L", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"_", "-", "Y", "X", "C", "V", "B", "N", "M", ".", ",", ":", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_de_upper_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
/* Spanish keeps QWERTY but promotes n-tilde directly onto the alpha deck. */
|
||||
static const char* kb_es_lower[] = {
|
||||
"1#", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"ABC", "a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"_", "-", "z", "x", "c", "v", "b", "n", "m", ".", ",", ":", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_es_lower_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
static const char* kb_es_upper[] = {
|
||||
"1#", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"abc", "A", "S", "D", "F", "G", "H", "J", "K", "L", "Ñ", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"_", "-", "Z", "X", "C", "V", "B", "N", "M", ".", ",", ":", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_es_upper_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
/* Italian gets the common accented vowels directly on the base layer. */
|
||||
static const char* kb_it_lower[] = {
|
||||
"1#", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"ABC", "a", "s", "d", "f", "g", "h", "j", "k", "l", "à", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"è", "ì", "z", "x", "c", "v", "b", "n", "m", ".", ",", ":", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_it_lower_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
static const char* kb_it_upper[] = {
|
||||
"1#", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
|
||||
"abc", "A", "S", "D", "F", "G", "H", "J", "K", "L", "À", LV_SYMBOL_NEW_LINE, "\n",
|
||||
"È", "Ì", "Z", "X", "C", "V", "B", "N", "M", ".", ",", ":", "\n",
|
||||
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
|
||||
};
|
||||
|
||||
static const lv_btnmatrix_ctrl_t kb_it_upper_ctrl[] = {
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
|
||||
EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
|
||||
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
|
||||
};
|
||||
|
||||
struct OsKeyboardLayout {
|
||||
KeyboardLayoutId id;
|
||||
const char* name;
|
||||
@@ -252,6 +401,21 @@ static const OsKeyboardLayout k_os_layouts[] = {
|
||||
{ KeyboardLayoutId::AR, "AR",
|
||||
kb_ar_lower, kb_ar_lower_ctrl,
|
||||
kb_ar_lower, kb_ar_lower_ctrl }, // unicameral: same map for both
|
||||
{ KeyboardLayoutId::FR, "FR",
|
||||
kb_fr_lower, kb_fr_lower_ctrl,
|
||||
kb_fr_upper, kb_fr_upper_ctrl },
|
||||
{ KeyboardLayoutId::NL, "NL",
|
||||
kb_nl_lower, kb_nl_lower_ctrl,
|
||||
kb_nl_upper, kb_nl_upper_ctrl },
|
||||
{ KeyboardLayoutId::DE, "DE",
|
||||
kb_de_lower, kb_de_lower_ctrl,
|
||||
kb_de_upper, kb_de_upper_ctrl },
|
||||
{ KeyboardLayoutId::ES, "ES",
|
||||
kb_es_lower, kb_es_lower_ctrl,
|
||||
kb_es_upper, kb_es_upper_ctrl },
|
||||
{ KeyboardLayoutId::IT, "IT",
|
||||
kb_it_lower, kb_it_lower_ctrl,
|
||||
kb_it_upper, kb_it_upper_ctrl },
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
@@ -271,6 +435,11 @@ static const HwKeyboardLayout k_hw_layouts[] = {
|
||||
{ KeyboardLayoutId::SR, "SR" },
|
||||
{ KeyboardLayoutId::EL, "EL" },
|
||||
{ KeyboardLayoutId::AR, "AR" },
|
||||
{ KeyboardLayoutId::FR, "FR" },
|
||||
{ KeyboardLayoutId::NL, "NL" },
|
||||
{ KeyboardLayoutId::DE, "DE" },
|
||||
{ KeyboardLayoutId::ES, "ES" },
|
||||
{ KeyboardLayoutId::IT, "IT" },
|
||||
};
|
||||
|
||||
/* Bulgarian phonetic mapping for T-Deck.
|
||||
@@ -424,6 +593,83 @@ static const char* hw_ar_digits[10] = {
|
||||
nullptr, "ج", "د", "ذ", "ز", "ط", "ظ", "و", nullptr, nullptr
|
||||
};
|
||||
|
||||
/* French AZERTY mapping for the T-Deck's US-QWERTY physical keyboard.
|
||||
* We only remap the alpha keys so punctuation/digits remain predictable on the
|
||||
* compact hardware; accented letters are still available through accent popups. */
|
||||
static const char* hw_fr_lower[26] = {
|
||||
"q", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "a", "r", "s", "t",
|
||||
"u", "v", "z", "x", "y", "w"
|
||||
};
|
||||
|
||||
static const char* hw_fr_upper[26] = {
|
||||
"Q", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "A", "R", "S", "T",
|
||||
"U", "V", "Z", "X", "Y", "W"
|
||||
};
|
||||
|
||||
static const char* hw_fr_digits[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
|
||||
static const char* hw_fr_digits_shift[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
|
||||
|
||||
/* German QWERTZ on the T-Deck's US-QWERTY hardware: swap Y/Z and keep the rest
|
||||
* predictable. Umlauts / eszett stay available through accent popups. */
|
||||
static const char* hw_de_lower[26] = {
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
|
||||
"u", "v", "w", "x", "z", "y"
|
||||
};
|
||||
static const char* hw_de_upper[26] = {
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Z", "Y"
|
||||
};
|
||||
static const char* hw_de_digits[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
|
||||
static const char* hw_de_digits_shift[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
|
||||
|
||||
/* Dutch keeps the standard alpha matrix but exposes the common IJ digraph on a
|
||||
* digit slot so compact hardware still gets a language-specific shortcut. */
|
||||
static const char* hw_nl_lower[26] = {
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
|
||||
"u", "v", "w", "x", "y", "z"
|
||||
};
|
||||
static const char* hw_nl_upper[26] = {
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z"
|
||||
};
|
||||
static const char* hw_nl_digits[10] = { nullptr, "ij", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
static const char* hw_nl_digits_shift[10] = { nullptr, "IJ", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
|
||||
/* Spanish stays close to US QWERTY; use the number row to expose n-tilde on
|
||||
* compact hardware where the semicolon key is absent. */
|
||||
static const char* hw_es_lower[26] = {
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
|
||||
"u", "v", "w", "x", "y", "z"
|
||||
};
|
||||
static const char* hw_es_upper[26] = {
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z"
|
||||
};
|
||||
static const char* hw_es_digits[10] = { nullptr, "ñ", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
static const char* hw_es_digits_shift[10] = { nullptr, "Ñ", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
|
||||
/* Italian uses the number row to surface the common accented vowels directly. */
|
||||
static const char* hw_it_lower[26] = {
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
|
||||
"u", "v", "w", "x", "y", "z"
|
||||
};
|
||||
static const char* hw_it_upper[26] = {
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z"
|
||||
};
|
||||
static const char* hw_it_digits[10] = { nullptr, "à", "è", "ì", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
static const char* hw_it_digits_shift[10] = { nullptr, "À", "È", "Ì", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||
|
||||
/* ================================================================
|
||||
* Runtime state
|
||||
* ================================================================ */
|
||||
@@ -499,6 +745,11 @@ static const HwPhoneticMap k_hw_maps[KEYBOARD_LAYOUT_COUNT] = {
|
||||
/* SR */ { hw_sr_lower, hw_sr_upper, hw_sr_digits, hw_sr_digits_shift },
|
||||
/* EL */ { hw_el_lower, hw_el_upper, hw_el_digits, hw_el_digits_shift },
|
||||
/* AR */ { hw_ar_lower, hw_ar_lower, hw_ar_digits, hw_ar_digits }, // unicameral
|
||||
/* FR */ { hw_fr_lower, hw_fr_upper, hw_fr_digits, hw_fr_digits_shift },
|
||||
/* NL */ { hw_nl_lower, hw_nl_upper, hw_nl_digits, hw_nl_digits_shift },
|
||||
/* DE */ { hw_de_lower, hw_de_upper, hw_de_digits, hw_de_digits_shift },
|
||||
/* ES */ { hw_es_lower, hw_es_upper, hw_es_digits, hw_es_digits_shift },
|
||||
/* IT */ { hw_it_lower, hw_it_upper, hw_it_digits, hw_it_digits_shift },
|
||||
};
|
||||
|
||||
const char* keyboardLayoutMapHwKey(KeyboardLayoutId id, int key, bool shifted) {
|
||||
|
||||
@@ -19,6 +19,11 @@ enum class KeyboardLayoutId : uint8_t {
|
||||
SR = 4, // Serbian (phonetic) — best-effort, validate with a native speaker
|
||||
EL = 5, // Greek (ΕΛΟΤ-style phonetic)
|
||||
AR = 6, // Arabic (RTL, Arabic-101 positions) — EXPERIMENTAL, needs on-device validation
|
||||
FR = 7, // French (AZERTY)
|
||||
NL = 8, // Dutch (QWERTY + accent popups)
|
||||
DE = 9, // German (QWERTZ)
|
||||
ES = 10, // Spanish (QWERTY + accent popups)
|
||||
IT = 11, // Italian (QWERTY + accent popups)
|
||||
Count
|
||||
};
|
||||
|
||||
|
||||
+3515
-377
File diff suppressed because it is too large.
Load diff
@@ -113,6 +113,10 @@ private:
|
||||
* unlock from this state, only another BOOT button press. False = the
|
||||
* screen turned off from idle timeout, in which case touch wakes it. */
|
||||
bool _manual_lock;
|
||||
// Burn-in guard for the lit lock screen (#55): stamped when the lock screen goes from off->lit;
|
||||
// the loop dims a hard-locked LIT panel a bounded time after this, independent of the screen-timeout
|
||||
// setting and of held/ghost touches that keep re-revealing it. 0 = not currently lit-locked.
|
||||
uint32_t _lock_lit_ms = 0;
|
||||
NodePrefs* _node_prefs;
|
||||
// GPS auto-location: once a fix is seen, keep the node location (node_lat/lon,
|
||||
// used by the profile + adverts) synced to GPS and persist it occasionally
|
||||
@@ -332,6 +336,35 @@ public:
|
||||
void toggleBuzzer();
|
||||
bool getGPSState();
|
||||
void toggleGPS();
|
||||
#if defined(HAS_EXPANSION_KIT)
|
||||
// Heltec V4 Expansion Kit: snapshot of the locally-attached sensor rail
|
||||
// (battery, BME280, GXHTV3/SHT4X) plus the GPS/buzzer module presence.
|
||||
struct LocalEnvSnapshot {
|
||||
bool query_ok = false;
|
||||
bool have_batt = false;
|
||||
float batt_v = 0.0f;
|
||||
bool have_bme_temp = false;
|
||||
bool have_bme_hum = false;
|
||||
bool have_bme_pressure = false;
|
||||
bool have_bme_alt = false;
|
||||
float bme_temp_c = 0.0f;
|
||||
float bme_hum_pct = 0.0f;
|
||||
float bme_pressure_hpa = 0.0f;
|
||||
int16_t bme_alt_m = 0;
|
||||
bool have_gxhtv3_temp = false;
|
||||
bool have_gxhtv3_hum = false;
|
||||
float gxhtv3_temp_c = 0.0f;
|
||||
float gxhtv3_hum_pct = 0.0f;
|
||||
bool gps_present = false;
|
||||
bool gps_enabled = false;
|
||||
bool gps_fix = false;
|
||||
int gps_sats = -1;
|
||||
bool buzzer_available = false;
|
||||
bool buzzer_quiet = true;
|
||||
};
|
||||
bool getLocalEnvSnapshot(LocalEnvSnapshot& out) const;
|
||||
bool getLocalEnvSummary(char* buf, size_t cap) const;
|
||||
#endif
|
||||
/** True if the GPS currently reports a valid fix. */
|
||||
bool getGpsFix();
|
||||
/** Satellites currently in view, or -1 if unknown / no GPS hardware. */
|
||||
@@ -353,6 +386,7 @@ public:
|
||||
void setAdvertLocationPolicy(uint8_t policy);
|
||||
void setPathHashMode(uint8_t mode);
|
||||
void setExperimentalFlags(uint8_t multi_acks, uint8_t client_repeat, uint8_t rx_boosted);
|
||||
void setTelemetryAllow(bool on); // answer mesh telemetry requests (battery+env; location stays separate)
|
||||
/** Meshcomod CLI on device: `wifi on` / `wifi off`. */
|
||||
bool setWifiRadio(bool on);
|
||||
bool isTcpEnabled() const { return _serial && _serial->isTcpEnabled(); }
|
||||
@@ -381,6 +415,9 @@ public:
|
||||
void unlockScreen();
|
||||
/** True while the panel backlight is off (idle-dimmed or manually locked). */
|
||||
bool isScreenOff() const { return _screen_off; }
|
||||
bool isManualLocked() const { return _manual_lock; } // hard screen lock engaged
|
||||
void toggleScreenLock(); // Tanmatsu Vol- long-press: lock <-> unlock
|
||||
void sleepScreen(); // soft screen sleep (backlight off, not locked)
|
||||
/** True while hard-locked (manual lock engaged), whether lit or dark. */
|
||||
bool isManualLock() const { return _manual_lock; }
|
||||
bool sendAdvertNow(); // legacy: zero-hop
|
||||
@@ -411,6 +448,7 @@ public:
|
||||
int8_t snr_q4, int8_t rssi) override;
|
||||
void appSentMsgToContact(const uint8_t* to_pub, const char* to_name, const char* text,
|
||||
uint32_t ack_hash) override;
|
||||
void appSentMsgToChannel(const char* channel_name, const char* text) override;
|
||||
void notify(UIEventType t = UIEventType::none) override;
|
||||
void logRxFrame(float snr, float rssi, const uint8_t* raw, int len) override;
|
||||
void discoveredContact(const ContactInfo& contact, bool is_new, uint8_t path_len) override;
|
||||
|
||||
@@ -509,6 +509,18 @@ static const uint8_t e_1f37a[] = {0,0,0,0,0,0,255,255,2,0,0,0,46,254,22,55,255,1
|
||||
static const lv_img_dsc_t d_1f37a = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f37a), e_1f37a };
|
||||
static const uint8_t e_1f37b[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,2,0,0,0,68,249,65,100,249,52,101,249,94,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,239,251,2,115,254,5,197,249,48,100,249,86,36,249,142,64,248,61,114,255,14,89,255,119,154,255,137,186,255,126,56,255,66,0,0,0,85,253,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,32,2,249,106,102,250,63,220,255,165,221,255,233,255,255,255,255,255,255,255,255,255,155,255,237,192,252,5,0,0,0,255,255,2,255,255,2,255,255,2,89,255,39,187,255,166,252,255,158,253,255,193,187,255,219,254,255,255,255,255,255,255,255,252,255,255,249,221,255,251,123,255,215,254,255,24,255,255,19,0,0,0,255,255,5,0,0,0,88,255,115,255,255,255,255,255,255,255,255,255,255,255,255,189,255,252,156,255,254,189,255,255,121,255,254,212,254,255,216,238,224,187,214,200,186,214,215,125,239,50,0,0,0,255,255,42,91,247,117,188,255,250,154,255,249,122,255,253,255,255,253,255,255,255,255,255,255,121,255,255,49,246,254,232,228,254,76,229,255,30,215,94,121,214,105,60,231,158,158,247,100,251,222,234,252,222,221,240,237,252,101,228,255,199,228,255,122,255,255,23,255,255,122,255,253,174,237,254,100,220,255,2,220,253,165,236,255,26,239,140,255,255,10,60,231,189,60,231,185,85,181,43,191,247,165,106,245,255,192,219,252,7,229,255,214,246,255,2,220,255,142,229,239,35,220,253,67,220,255,131,228,254,224,244,255,113,238,212,127,231,143,121,214,209,93,239,174,0,0,0,156,247,209,70,245,255,2,237,253,112,246,255,48,246,255,224,219,252,115,238,237,80,246,255,225,236,255,165,245,254,96,253,254,73,254,245,187,214,211,51,165,67,218,222,202,61,223,142,90,247,238,98,253,255,131,245,254,199,245,255,68,245,255,6,237,247,213,238,228,51,255,255,195,253,255,226,253,255,160,253,255,199,237,251,220,214,183,255,255,5,84,173,70,123,206,189,142,254,250,96,253,255,193,253,255,96,253,255,64,253,255,201,237,231,95,231,136,176,246,246,229,253,251,130,245,249,138,221,246,121,214,255,28,223,203,90,239,13,0,0,0,158,247,142,181,238,250,112,238,241,78,238,245,9,246,246,226,253,254,78,246,189,255,231,54,184,222,255,53,214,255,120,214,255,188,214,251,61,231,186,93,247,36,0,0,0,121,206,5,154,214,157,57,198,240,58,198,251,90,198,251,123,206,247,185,222,255,251,230,188,255,231,10,61,231,119,62,231,158,61,231,127,124,247,62,0,0,0,0,0,0,255,255,1,0,0,0,170,82,6,181,181,79,182,189,140,214,189,176,214,189,195,248,189,197,154,214,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,2,0,0,0};
|
||||
static const lv_img_dsc_t d_1f37b = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f37b), e_1f37b };
|
||||
|
||||
static const uint8_t e_26bd[] = {0,0,0,0,0,1,0,0,0,0,0,0,77,99,65,154,214,167,28,231,228,60,231,253,219,222,253,179,148,228,73,66,172,227,24,72,0,0,0,0,0,0,239,123,2,0,0,0,0,0,1,0,0,0,44,99,10,8,58,155,48,132,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,207,123,255,53,165,255,247,189,169,146,148,19,0,0,0,239,123,2,170,82,3,170,42,6,138,74,190,8,58,255,207,115,253,158,247,252,255,255,254,255,255,255,255,255,255,56,198,254,81,140,253,255,255,253,255,255,255,219,222,211,211,156,18,239,123,2,0,0,0,138,74,139,41,66,255,207,115,249,20,165,255,174,115,255,211,156,255,52,165,255,45,99,255,41,66,255,171,82,255,251,222,255,255,255,249,255,255,255,154,214,171,0,0,0,41,66,44,41,58,250,81,140,252,223,255,254,255,255,255,255,255,255,243,156,255,167,49,255,73,66,255,106,74,255,8,58,255,44,99,255,190,247,255,255,255,252,158,247,255,44,99,69,20,157,147,239,123,255,255,255,252,255,255,255,223,255,255,255,255,255,56,198,255,40,58,255,106,74,255,105,66,255,106,74,255,41,58,255,109,107,255,52,165,252,118,173,254,41,66,171,56,198,212,146,148,254,255,255,253,255,255,255,255,255,255,255,255,255,89,206,255,8,58,255,138,74,255,106,74,255,106,74,255,8,58,255,211,156,255,215,189,254,207,123,255,41,66,227,89,206,237,113,140,255,255,255,254,255,255,255,255,255,255,255,255,255,211,156,255,41,66,255,8,58,255,41,66,255,40,58,255,77,99,255,255,255,255,255,255,255,125,239,255,106,74,251,251,222,236,16,132,255,154,206,254,255,255,255,255,255,255,81,140,255,146,148,255,60,231,255,117,173,255,175,115,255,105,66,255,219,214,255,255,255,255,255,255,255,190,247,255,44,99,254,56,198,211,236,90,255,8,58,253,240,123,255,16,124,255,20,165,255,255,255,255,255,255,255,255,255,255,255,255,255,178,148,255,121,206,255,255,255,255,255,255,254,158,247,255,203,82,237,240,123,153,138,74,255,106,74,252,199,49,255,45,99,255,255,255,255,255,255,255,255,255,255,223,255,255,255,255,255,215,189,255,52,173,255,255,255,255,255,255,253,243,156,254,170,82,191,41,66,59,106,74,255,105,66,252,105,66,255,203,82,255,158,247,255,255,255,255,223,255,255,223,255,255,255,255,255,186,214,255,174,115,255,186,214,255,150,181,252,44,99,255,121,206,95,0,0,0,73,66,172,138,74,255,73,66,249,199,49,255,150,181,255,255,255,255,255,255,255,255,255,255,28,231,255,77,99,255,40,58,255,40,58,250,203,82,255,182,181,203,255,255,6,239,123,2,73,74,21,106,66,213,203,82,255,240,123,253,81,132,252,16,132,254,182,181,255,56,198,255,203,82,254,8,58,253,106,74,252,106,74,255,236,90,237,154,214,40,0,0,0,239,123,2,0,0,0,170,82,21,150,181,172,251,222,255,125,239,255,24,198,255,145,148,255,12,91,255,41,66,255,138,74,255,138,74,255,106,74,199,232,57,45,0,0,0,239,123,2,0,0,0,239,123,2,0,0,0,0,0,0,24,198,73,24,198,168,122,214,224,219,222,250,24,198,254,171,82,235,105,66,190,106,74,101,134,49,5,0,0,0,255,255,2,0,0,0};
|
||||
static const lv_img_dsc_t d_26bd = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_26bd), e_26bd };
|
||||
static const uint8_t e_1f3c8[] = {0,0,0,0,0,0,0,0,0,0,0,0,160,170,3,0,0,0,204,252,5,228,161,85,103,162,173,83,205,231,190,247,252,153,230,254,42,163,237,1,121,191,164,129,97,0,0,0,0,0,0,0,0,0,0,248,1,0,120,2,0,0,0,4,162,78,196,145,205,196,137,255,214,221,255,149,205,255,140,163,255,27,239,255,255,255,255,111,196,255,163,153,255,196,137,117,0,0,0,0,0,1,224,123,2,0,0,0,5,162,143,164,145,255,70,146,255,219,230,253,190,255,253,92,247,254,229,129,255,228,153,255,246,221,254,255,255,251,144,204,254,34,129,217,0,248,1,160,170,3,0,0,0,37,162,164,98,145,255,10,155,252,27,247,253,93,247,255,218,238,255,38,138,255,164,137,255,196,153,255,163,153,255,149,213,254,255,255,255,43,155,232,170,170,3,0,0,0,38,162,141,66,145,255,108,163,249,218,238,255,60,247,255,251,238,255,6,138,255,131,137,255,228,153,255,229,153,255,196,153,255,163,153,254,120,230,255,173,155,232,0,0,0,70,170,66,98,145,255,42,155,251,246,221,254,222,255,255,55,230,255,196,129,255,164,137,255,228,161,255,228,153,255,228,153,255,228,153,255,163,153,254,70,162,255,229,121,221,255,255,2,196,153,201,103,154,254,116,213,252,255,255,255,84,213,255,6,138,255,131,129,255,228,161,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,253,164,145,255,34,97,194,66,153,66,66,145,255,210,188,254,255,255,255,55,230,255,168,154,255,66,121,255,5,162,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,252,164,129,255,67,97,139,14,188,155,5,162,255,104,146,252,116,205,255,140,171,255,1,121,255,5,162,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,196,145,254,67,105,253,66,105,55,255,255,217,60,247,255,75,163,253,192,120,255,98,129,255,5,154,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,252,131,129,255,67,105,187,0,0,0,120,230,242,255,255,255,255,255,255,51,213,255,196,153,255,196,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,254,131,129,251,67,105,254,99,113,51,0,0,0,164,145,253,238,171,255,157,247,255,255,255,255,214,213,255,196,153,255,196,153,255,228,153,255,228,153,255,228,153,255,228,153,254,131,129,250,99,113,255,67,105,118,0,0,0,160,82,3,164,137,253,33,121,255,42,179,255,60,239,255,255,255,255,214,221,255,196,153,255,196,153,255,228,153,254,196,153,251,131,129,255,99,113,255,67,105,137,0,0,0,160,82,3,0,0,0,131,129,244,228,145,253,130,153,253,233,170,254,60,239,254,255,255,254,181,213,253,228,153,253,196,145,255,131,121,255,67,105,245,67,113,107,0,0,0,160,82,3,0,0,0,0,0,0,99,121,167,228,161,255,5,162,255,131,153,255,42,179,255,157,247,255,251,238,255,136,146,255,34,105,240,99,105,161,99,105,40,0,0,0,0,80,3,0,0,0,0,0,0,0,0,0,227,137,16,228,153,139,228,153,210,228,145,238,99,137,240,201,146,220,165,113,178,225,96,111,131,113,36,0,0,0,0,0,0,160,82,3,0,0,0,0,0,0,0,0,0,0,0,0};
|
||||
static const lv_img_dsc_t d_1f3c8 = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f3c8), e_1f3c8 };
|
||||
static const uint8_t e_1f5ff[] = {0,0,0,0,0,0,239,123,2,0,0,0,56,206,50,154,222,169,24,206,218,150,189,232,182,197,213,215,197,154,86,181,43,0,0,0,239,123,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,1,57,214,40,89,214,237,89,214,255,247,205,255,215,197,255,215,197,255,56,214,255,211,172,224,170,122,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,117,189,156,52,181,255,206,131,248,141,123,254,48,148,254,178,164,254,52,181,250,80,148,255,170,98,177,235,90,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,107,194,7,74,254,203,98,252,207,131,255,8,74,255,7,74,255,170,98,255,12,107,252,203,106,255,170,98,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,1,8,74,172,12,107,255,215,197,252,56,214,255,235,106,255,235,98,255,170,90,255,73,82,253,203,98,252,170,98,211,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,181,159,24,206,255,56,214,251,48,148,255,174,131,255,24,206,255,215,197,255,12,107,254,170,98,255,170,98,223,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,149,189,42,247,197,245,89,214,255,52,181,254,202,98,255,174,131,255,215,197,255,85,181,255,235,106,254,170,98,255,170,98,226,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,117,189,129,121,222,255,215,197,252,178,164,255,52,181,255,142,123,255,243,172,255,52,181,255,138,90,254,170,98,255,170,98,231,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,150,189,177,207,131,254,105,90,252,203,98,255,16,140,255,76,115,255,207,139,255,48,148,255,138,90,254,170,98,255,170,98,231,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,215,197,196,85,181,255,48,148,253,142,123,255,44,115,255,77,123,255,48,148,255,109,123,255,138,90,254,170,98,254,170,98,235,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,181,204,210,164,255,113,156,253,178,164,255,48,148,255,16,148,255,81,156,255,142,123,255,138,90,254,203,98,255,170,98,251,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,181,209,142,123,255,203,98,253,44,115,255,16,148,255,48,148,255,81,148,255,142,123,254,138,90,254,170,98,232,170,98,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,215,197,207,52,181,255,207,139,253,81,156,255,215,197,255,243,172,255,48,148,255,142,131,251,203,98,255,170,98,144,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,182,197,218,215,197,254,85,181,253,117,189,254,182,197,254,52,181,253,48,148,253,142,123,248,170,98,252,170,98,171,170,82,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,181,255,247,205,255,247,205,255,182,197,255,182,197,255,85,181,255,113,156,255,109,123,255,203,106,255,170,98,157,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,148,127,146,156,223,146,156,242,113,156,241,113,156,229,48,148,204,239,139,181,203,98,170,170,98,129,138,106,22,0,0,1,0,0,0,0,0,0,0,0,0};
|
||||
static const lv_img_dsc_t d_1f5ff = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f5ff), e_1f5ff };
|
||||
static const uint8_t e_1f3f3[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,154,93,65,219,93,164,29,94,219,29,94,234,252,93,221,252,93,184,187,93,125,122,93,60,145,83,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220,85,232,255,86,255,191,86,255,191,86,255,191,86,255,223,86,255,191,86,255,62,94,255,28,94,219,219,93,162,154,93,107,89,85,67,57,93,52,57,85,61,154,93,110,187,93,195,219,101,230,60,158,252,186,189,253,185,197,254,185,189,254,251,165,253,61,134,252,127,102,255,223,86,255,255,86,255,223,94,255,127,94,255,94,94,255,126,94,255,223,94,255,94,86,255,245,220,230,54,253,255,21,253,254,21,253,255,21,253,255,21,253,255,54,253,255,119,237,255,185,197,253,251,157,251,93,126,251,126,102,253,127,102,254,126,102,253,93,126,251,120,165,252,213,228,230,249,253,255,90,246,254,155,254,255,122,246,255,25,246,255,151,245,255,54,253,255,21,253,255,22,253,255,54,253,255,87,237,255,119,229,255,119,237,255,54,253,255,212,236,255,219,230,230,255,255,255,255,255,254,255,255,255,255,255,255,255,255,255,223,255,255,93,255,255,155,254,255,249,245,255,119,245,255,54,245,255,54,253,255,54,245,255,151,245,255,216,229,255,125,239,230,223,255,255,93,255,254,61,255,255,93,255,255,191,255,255,255,255,255,255,255,255,255,255,255,255,255,255,191,255,255,125,255,255,61,255,255,93,255,255,223,255,255,158,239,255,216,229,230,184,253,255,54,245,254,54,253,255,54,245,255,119,245,255,216,245,255,123,246,255,61,255,255,223,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,238,255,180,236,230,86,253,255,87,237,254,119,229,255,87,237,255,54,245,255,22,253,255,21,253,255,54,253,255,119,245,255,249,245,255,122,246,255,187,254,255,123,246,255,249,245,255,21,229,255,88,173,228,93,142,251,126,102,251,127,102,253,126,110,252,93,126,251,251,157,252,185,197,253,119,229,255,54,253,255,21,253,255,21,253,255,21,253,255,21,253,255,22,253,255,245,228,255,28,86,239,31,95,255,127,94,255,126,94,255,159,94,255,223,94,255,255,86,255,191,86,255,127,102,255,61,134,252,251,165,253,185,197,254,185,205,255,185,197,255,251,165,253,219,109,253,186,93,192,219,93,131,121,93,72,89,85,58,89,93,72,154,93,110,219,93,168,28,94,223,94,94,255,191,86,255,191,86,255,191,86,255,159,86,255,159,86,255,223,86,255,61,86,255,239,59,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,83,76,13,121,93,66,187,93,136,220,93,200,252,93,237,252,93,253,28,94,242,252,93,196,154,93,105};
|
||||
static const lv_img_dsc_t d_1f3f3 = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f3f3), e_1f3f3 };
|
||||
static const uint8_t e_1f1f1[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,19,1,255,19,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,212,0,255,212,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,118,132,255,118,132,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,19,165,255,19,165,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,223,255,255,223,255,255,223,255,255,223,255,255,223,255,255,223,247,255,255,255,255,138,82,255,105,74,255,255,255,255,223,247,255,223,255,255,223,255,255,223,255,255,223,255,255,223,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,118,181,255,65,16,255,65,16,255,86,181,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,248,182,255,248,182,255,248,182,255,248,182,255,248,182,255,248,182,255,114,133,255,13,84,255,45,84,255,114,133,255,248,182,255,248,182,255,248,182,255,248,182,255,248,182,255,248,182,255,103,4,255,103,4,255,103,4,255,103,4,255,103,4,255,103,4,255,200,4,255,232,4,255,232,4,255,200,4,255,103,4,255,103,4,255,103,4,255,103,4,255,103,4,255,103,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,136,4,255,136,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255};
|
||||
static const lv_img_dsc_t d_1f1f1 = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f1f1), e_1f1f1 };
|
||||
|
||||
typedef struct { uint32_t cp; const lv_img_dsc_t* dsc; } EmojiGlyph;
|
||||
static const EmojiGlyph kGlyphs[] = {
|
||||
{ 0x200Du, &d_zero },
|
||||
@@ -517,6 +529,8 @@ static const EmojiGlyph kGlyphs[] = {
|
||||
{ 0x261Du, &d_261d },
|
||||
{ 0x26A0u, &d_26a0 },
|
||||
{ 0x26A1u, &d_26a1 },
|
||||
{ 0x26A7u, &d_zero },
|
||||
{ 0x26BDu, &d_26bd },
|
||||
{ 0x2705u, &d_2705 },
|
||||
{ 0x270Au, &d_270a },
|
||||
{ 0x270Bu, &d_270b },
|
||||
@@ -539,6 +553,8 @@ static const EmojiGlyph kGlyphs[] = {
|
||||
{ 0x2B55u, &d_2b55 },
|
||||
{ 0xFE0Eu, &d_zero },
|
||||
{ 0xFE0Fu, &d_zero },
|
||||
{ 0x1F1F1u, &d_1f1f1 },
|
||||
{ 0x1F1F8u, &d_zero },
|
||||
{ 0x1F308u, &d_1f308 },
|
||||
{ 0x1F30Au, &d_1f30a },
|
||||
{ 0x1F319u, &d_1f319 },
|
||||
@@ -566,6 +582,8 @@ static const EmojiGlyph kGlyphs[] = {
|
||||
{ 0x1F38Au, &d_1f38a },
|
||||
{ 0x1F3AFu, &d_1f3af },
|
||||
{ 0x1F3C6u, &d_1f3c6 },
|
||||
{ 0x1F3C8u, &d_1f3c8 },
|
||||
{ 0x1F3F3u, &d_1f3f3 },
|
||||
{ 0x1F40Cu, &d_1f40c },
|
||||
{ 0x1F414u, &d_1f414 },
|
||||
{ 0x1F417u, &d_1f417 },
|
||||
@@ -651,6 +669,7 @@ static const EmojiGlyph kGlyphs[] = {
|
||||
{ 0x1F595u, &d_1f595 },
|
||||
{ 0x1F596u, &d_1f596 },
|
||||
{ 0x1F5A4u, &d_1f5a4 },
|
||||
{ 0x1F5FFu, &d_1f5ff },
|
||||
{ 0x1F600u, &d_1f600 },
|
||||
{ 0x1F601u, &d_1f601 },
|
||||
{ 0x1F602u, &d_1f602 },
|
||||
@@ -767,7 +786,7 @@ static const EmojiGlyph kGlyphs[] = {
|
||||
{ 0x1F9D0u, &d_1f9d0 },
|
||||
{ 0x1F9E1u, &d_1f9e1 },
|
||||
};
|
||||
const uint16_t kEmojiGlyphCount = 252;
|
||||
const uint16_t kEmojiGlyphCount = 257;
|
||||
|
||||
const lv_img_dsc_t* emojiGlyphLookup(uint32_t cp) {
|
||||
int lo = 0, hi = (int)(sizeof(kGlyphs)/sizeof(kGlyphs[0])) - 1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// AUTO-GENERATED by scripts/build/gen-emoji-font.py — do not edit.
|
||||
// 252 Noto colour emoji baked to RGB565+alpha at 16px.
|
||||
// 257 Noto colour emoji baked to RGB565+alpha at 16px.
|
||||
#pragma once
|
||||
#include "lvgl.h"
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// AUTO-GENERATED by scripts/build/gen-qr-icon.py — do not edit.
|
||||
// 18x18 recolour-able QR glyph for the Chats-header Share button.
|
||||
#pragma once
|
||||
#include "lvgl.h"
|
||||
|
||||
static const uint8_t qr_icon_map[] = {255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255};
|
||||
static const lv_img_dsc_t qr_icon_dsc = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 18, 18 }, sizeof(qr_icon_map), qr_icon_map };
|
||||
@@ -44,6 +44,9 @@ set(WADA_DEFS
|
||||
MAX_CONTACTS=500 MAX_GROUP_CHANNELS=40 # 2000 was a ~400KB internal-DRAM strand; BT's SDIO RX buffers (MALLOC_CAP_INTERNAL|DMA) need that headroom
|
||||
ENABLE_PRIVATE_KEY_IMPORT=1 ENABLE_PRIVATE_KEY_EXPORT=1
|
||||
DISPLAY_CLASS=TanmatsuDisplay
|
||||
# microSD lives on the P4's SDMMC *slot 0* (IOMUX pins CLK43/CMD44/D0-3 39-42); slot 1 is the
|
||||
# C6 radio (esp-hosted SDIO), so these make Arduino SD_MMC target slot 0 only — no radio conflict.
|
||||
BOARD_HAS_SDMMC=1 BOARD_SDMMC_SLOT=0
|
||||
# the core's ESP32Board.h reads these raw pins directly; Tanmatsu uses badge-bsp, so -1 (none):
|
||||
P_LORA_DIO_1=-1 P_LORA_TX_LED=-1 P_LORA_TX_NEOPIXEL_LED=-1
|
||||
PIN_BOARD_SCL=-1 PIN_BOARD_SDA=-1 PIN_VBAT_READ=-1)
|
||||
|
||||
+34
-3
@@ -8,6 +8,8 @@
|
||||
#include <Arduino.h>
|
||||
#include <Mesh.h>
|
||||
#include "MyMesh.h"
|
||||
#include <new> // placement-new for the PSRAM-resident the_mesh
|
||||
#include "esp_heap_caps.h" // heap_caps_malloc(MALLOC_CAP_SPIRAM)
|
||||
#include "UITask.h"
|
||||
#include "target.h" // board, radio_driver, rtc_clock, display, sensors, radio_init()
|
||||
#include <SPIFFS.h>
|
||||
@@ -27,6 +29,8 @@ extern "C" {
|
||||
#include "bsp/device.h"
|
||||
#include "bsp/input.h"
|
||||
#include "bsp/display.h"
|
||||
#include "bsp/tanmatsu.h" // bsp_tanmatsu_coprocessor_get_handle (C6 radio power)
|
||||
#include "tanmatsu_coprocessor.h" // tanmatsu_coprocessor_radio_disable / _enable_application
|
||||
#include "esp_hosted.h" // C6 radio co-processor link (WiFi + LoRa ride esp-hosted)
|
||||
#include "esp_netif.h" // lwIP/tcpip bring-up (else WiFi calls assert "Invalid mbox")
|
||||
#include "esp_event.h"
|
||||
@@ -54,7 +58,17 @@ MultiTransportCompanionInterface serial_interface;
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
UITask ui_task(&board, &serial_interface);
|
||||
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store, &ui_task);
|
||||
// the_mesh (~42 KB, dominated by the MAX_CONTACTS array) lives in PSRAM instead of the
|
||||
// scarce internal DRAM — the P4 has 32 MB PSRAM. Placement-new into a heap_caps_malloc
|
||||
// block + a reference, exactly like src/main.cpp on the S3 boards: the ctor still runs
|
||||
// HERE at static-init (PSRAM is up first), so only the address moves. Internal-RAM
|
||||
// fallback if PSRAM is somehow absent. MyMesh.h declares `extern MyMesh& the_mesh`.
|
||||
static MyMesh& makeTheMesh() {
|
||||
void* mem = heap_caps_malloc(sizeof(MyMesh), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!mem) mem = malloc(sizeof(MyMesh));
|
||||
return *new (mem) MyMesh(radio_driver, fast_rng, rtc_clock, tables, store, &ui_task);
|
||||
}
|
||||
MyMesh& the_mesh = makeTheMesh();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Boot splash — a tiny on-screen console so launching the app shows progress
|
||||
@@ -304,6 +318,19 @@ static void wadameshSetup() {
|
||||
esp_netif_init();
|
||||
esp_event_loop_create_default();
|
||||
bootLog("C6 radio coproc");
|
||||
// Power-cycle the C6 through the CH32 so the app ALWAYS gets a fresh C6. Without this,
|
||||
// re-launching the app (especially right after a flash) onto a C6 still in its previous
|
||||
// state wedges esp-hosted ("Not able to connect with ESP-Hosted slave") into a P4 reboot
|
||||
// loop that previously only a manual power cycle cleared — this does the same in software.
|
||||
{
|
||||
tanmatsu_coprocessor_handle_t cph = nullptr;
|
||||
if (bsp_tanmatsu_coprocessor_get_handle(&cph) == ESP_OK && cph) {
|
||||
tanmatsu_coprocessor_radio_disable(cph);
|
||||
delay(150);
|
||||
tanmatsu_coprocessor_radio_enable_application(cph); // boot the C6 into its esp-hosted slave fw
|
||||
delay(600); // let the C6 come up before we attach
|
||||
}
|
||||
}
|
||||
hostedConnectC6(); // identify the C6 slave so WiFi + LoRa work (must precede radio_init)
|
||||
bootLog("Radio link");
|
||||
if (!radio_init()) { bootLog("radio init FAILED"); Serial.println("[BOOT] radio_init FAILED"); }
|
||||
@@ -437,6 +464,11 @@ extern "C" void app_main(void) {
|
||||
}
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
if (!modem_sleep_set) { WiFi.setSleep(true); modem_sleep_set = true; }
|
||||
// Start the TCP (+ WebSocket) companion server now the STA is associated. Deferred to here on
|
||||
// purpose: starting WiFiServer before association asserts in lwIP (the old reason this was
|
||||
// stubbed out). startTcpServer() is idempotent — it no-ops once the server is up — so calling
|
||||
// it on every connected tick is safe.
|
||||
serial_interface.startTcpServer(true);
|
||||
if (!sntp_kicked) {
|
||||
char tz[48]; touchPrefsBuildLocalTz(tz, sizeof tz);
|
||||
configTzTime(tz, "pool.ntp.org", "time.google.com");
|
||||
@@ -447,8 +479,7 @@ extern "C" void app_main(void) {
|
||||
}
|
||||
} else if (sntp_kicked && !sntp_pushed) { sntp_kicked = false; }
|
||||
}
|
||||
// (No TCP/WS companion server on the Tanmatsu — the companion app connects over USB serial.
|
||||
// Starting WiFiServer here asserted in lwIP when the STA wasn't associated yet.)
|
||||
serial_interface.tickWebSocketHandshake(); // accept WS clients/handshakes (no-op until the WS server is up)
|
||||
|
||||
the_mesh.loop(); // mesh + companion servers
|
||||
vTaskDelay(1);
|
||||
|
||||
@@ -21,6 +21,12 @@ CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_ENABLED=y
|
||||
|
||||
# Persist BLE bonds (LTK/IRK) to NVS so a paired phone survives a device reboot. Default is OFF, which
|
||||
# keeps bonds in RAM only — after every power-cycle the device has no key, so the phone's saved pairing
|
||||
# is rejected as "invalid" and the user must delete + re-pair. The nvs partition (partition_tables/16M.csv)
|
||||
# already backs Wi-Fi creds, so there's room for the (max 3) bonds.
|
||||
CONFIG_BT_NIMBLE_NVS_PERSIST=y
|
||||
|
||||
# AsyncTCP (pulled by the core's ESP32Board HTTP-OTA) references CONFIG_ESP_TASK_WDT_TIMEOUT_S,
|
||||
# which IDF 5.5 only defines when the task watchdog is initialised. Enable it (5 s timeout).
|
||||
CONFIG_ESP_TASK_WDT_INIT=y
|
||||
|
||||
@@ -54,6 +54,18 @@ int TanmatsuLoraRadio::recvRaw(uint8_t* bytes, int sz) {
|
||||
// one-shot is enough. It also gates pushConfig from here on (the remote link blocks under RX load).
|
||||
_rx_polling = true;
|
||||
lora_set_mode(&_h, LORA_PROTOCOL_MODE_RX);
|
||||
} else if (_cfg_dirty) {
|
||||
// A runtime setParams()/setTxPower()/setRxBoostedGainMode() (the wadamesh radio
|
||||
// settings save, from the UI or the companion app) changed the PHY config. Apply it
|
||||
// HERE, on the RX-poll thread, so the C6 remote link is never touched cross-thread:
|
||||
// leave RX to STANDBY (so the link isn't under active-RX load and lora_set_config
|
||||
// can't block), push the new config, then re-arm continuous RX. This is what makes a
|
||||
// live frequency / SF / BW / CR / power change actually reach the modem (it used to
|
||||
// need a reboot) AND lets wadamesh's settings override whatever the Tanmatsu OS set.
|
||||
_cfg_dirty = false;
|
||||
lora_set_mode(&_h, LORA_PROTOCOL_MODE_STANDBY_RC);
|
||||
lora_set_config(&_h, &_cfg);
|
||||
lora_set_mode(&_h, LORA_PROTOCOL_MODE_RX);
|
||||
}
|
||||
lora_protocol_lora_packet_t pkt;
|
||||
// Non-blocking poll of the RX queue (timeout 0).
|
||||
|
||||
@@ -56,16 +56,25 @@ public:
|
||||
uint32_t getPacketsSent() const { return _n_sent; }
|
||||
|
||||
private:
|
||||
// Push _cfg to the modem — but ONLY while not yet polling RX. The remote link's
|
||||
// esp_hosted_send_custom_data() blocks indefinitely under continuous-RX load, so a
|
||||
// live reconfig from the UI thread would freeze the app. Boot bring-up (before the
|
||||
// RX loop starts) pushes fine; runtime changes update _cfg and apply on next init.
|
||||
void pushConfig() { if (_started && !_rx_polling) lora_set_config(&_h, &_cfg); }
|
||||
// Push _cfg to the modem. The remote link's esp_hosted_send_custom_data() can block
|
||||
// under continuous-RX load, so a live lora_set_config() from the UI/companion thread
|
||||
// (mid-RX) risked freezing the app — which is why this used to no-op once RX started,
|
||||
// making a frequency/SF/BW change only take effect on the next reboot ("receives
|
||||
// nothing on the new frequency", and wadamesh's settings never overrode the Tanmatsu
|
||||
// OS's). Now: boot bring-up pushes directly; a runtime change marks _cfg dirty and
|
||||
// recvRaw() applies it on the RX-poll thread, between polls (standby → push → re-arm),
|
||||
// so wadamesh's settings actually reach the modem and win.
|
||||
void pushConfig() {
|
||||
if (!_started) return;
|
||||
if (_rx_polling) { _cfg_dirty = true; return; } // defer to recvRaw() (RX-poll thread)
|
||||
lora_set_config(&_h, &_cfg);
|
||||
}
|
||||
|
||||
lora_handle_t _h = {0};
|
||||
lora_protocol_config_params_t _cfg = {0};
|
||||
bool _started = false;
|
||||
bool _rx_polling = false; // set once recvRaw() starts draining — see pushConfig()
|
||||
bool _cfg_dirty = false; // runtime config change pending; applied by recvRaw() between polls
|
||||
bool _sending = false;
|
||||
bool _rx_boost = false;
|
||||
float _last_rssi = 0.0f;
|
||||
|
||||
Reference in new issue
Block a user