Compare commits
No files matched your search
@@ -44,9 +44,13 @@ if os.path.exists(nf):
|
||||
|
||||
# Both touch boards are ESP32-S3, single touch-UI ("gui") role. ota_0 sits at
|
||||
# 0x10000 on both (partitions_t*_touch.csv), so flash-update@0x10000 is correct.
|
||||
# `name` MUST match the official MeshCore device name character-for-character, or Mesh America lists it
|
||||
# as a NEW device instead of folding our firmware into the existing tile (TJ Downes, 2026-06). Canonical
|
||||
# names verified from apps.meshamerica.com/proxy/flasher/config.json. The V4 entry is the Expansion Kit
|
||||
# (Touch) variant only, per Kaj. `slug` still maps to our bin filenames on firmware.wadamesh.com.
|
||||
BOARDS = [
|
||||
{"name": "wadamesh — Heltec V4 TFT", "slug": "heltec-v4-tft"},
|
||||
{"name": "wadamesh — LilyGo T-Deck", "slug": "tdeck"},
|
||||
{"name": "Heltec v4 + Expansion Kit (Touch)", "slug": "heltec-v4-tft"},
|
||||
{"name": "LilyGo T-Deck", "slug": "tdeck"},
|
||||
]
|
||||
|
||||
def device(b):
|
||||
@@ -74,7 +78,16 @@ def device(b):
|
||||
}],
|
||||
}
|
||||
|
||||
# Optional "what's this?" blurb Mesh America shows on a hover tooltip next to our provider badge
|
||||
# (spec section 3, added by TJ 2026-06-30). Plain text only — rendered escaped, so no markup/links.
|
||||
DESCRIPTION = (
|
||||
"WADAMESH is a full touch-screen interface for MeshCore: on-device chat, contacts, a live map "
|
||||
"with offline tiles, and complete radio settings. Same MeshCore protocol and phone-app companion "
|
||||
"as stock MeshCore, just a richer on-device front-end."
|
||||
)
|
||||
|
||||
catalog = {
|
||||
"description": DESCRIPTION,
|
||||
"maker": {"wadamesh": {"name": "wadamesh"}},
|
||||
"device": [device(b) for b in BOARDS],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"description": "WADAMESH is a full touch-screen interface for MeshCore: on-device chat, contacts, a live map with offline tiles, and complete radio settings. Same MeshCore protocol and phone-app companion as stock MeshCore, just a richer on-device front-end.",
|
||||
"maker": {
|
||||
"wadamesh": {
|
||||
"name": "wadamesh"
|
||||
@@ -8,7 +9,7 @@
|
||||
{
|
||||
"maker": "wadamesh",
|
||||
"class": "wadamesh",
|
||||
"name": "wadamesh — Heltec V4 TFT",
|
||||
"name": "Heltec v4 + Expansion Kit (Touch)",
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='https://wadamesh.com/wadamesh-icon-512.png'>",
|
||||
"firmware": [
|
||||
@@ -16,19 +17,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_21": {
|
||||
"notes": "Stable and test update channels. beta_21 is the first stable build. On devices that update over Wi-Fi there is now a \"Get test builds (beta)\" switch on the About screen: leave it off to stay on stable releases, or turn it on to receive test builds earlier — it switches both the \"update available\" check and the firmware you install to the test channel. You can go back to stable at any time.\nMQTT bridge (highly experimental, off by default). An opt-in bridge that forwards the messages your node receives to an MQTT broker of your choice, so you can log your mesh or wire it into other tools. It is gated behind a privacy consent you have to tick, direct messages are excluded unless you deliberately enable them, and the payload can be sealed with an encryption key so only you can read it. Use a broker you control, never a public one. This is experimental and may change.\nPortugal (Narrow) region preset, so Portuguese users can pick the local defaults in one tap. (Issue #74, requested by Salgueiron.)\nSaved-contacts counter. The Contacts overflow menu now shows how many contacts you have saved out of the maximum it can hold (for example 42 / 2000). (Issue #72, asked by Buko84.)\nThe signal probe never travels past the first node now. The \"auto-discover\" signal pulse, and the probe behind the Signal popup, are sent strictly zero-hop, so a repeater can no longer re-flood them across the mesh. The probe only measures the link to whatever hears it first, which is what it was always meant to do.\nYour most relevant contacts now show when you have a lot of them. The on-screen contact list is capped for performance, but it was trimming the list before sorting it, so some contacts could drop out of view. It now sorts first, shows the closest matches, and adds a \"+N more — search to narrow the list\" line at the bottom. (Issue #73, reported by 1nerdherder.)\nThe blocked-users list opened from Contacts now shows the proper two-line title bar with a back button, instead of an unlabelled screen.\nThe Contacts overflow popup fits the screen again after the saved-contacts counter was added.\nEverything from beta_20 is included.",
|
||||
"beta_27": {
|
||||
"notes": "Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.\nDevices installed under the Launcher no longer show the on-device \"Install update\" button. That update path only works on a standalone flash; under the Launcher it failed with \"won't fit: Bad Size Given\". The \"update available\" note stays, but you update through the Launcher instead. Thanks to Keith (KM1USR) for the report.\nKeyboard backlight brightness control on the T-Deck: a live slider plus Off / 25 / 50 / 75 / Max presets in Settings > Keyboard. It dims smoothly (fine-grained at the low end), applies to the on and auto modes and the new-message flash, and survives a reboot. Thanks to Yazutsu (PR #85).\nOn devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). Your existing contacts move over automatically on first boot; your identity and settings stay in internal flash, so the node is unchanged. Keep the card inserted afterwards - the contact list lives on it, and an older firmware won't look there if you ever downgrade.\nThis is the new stable build, promoted from the beta_27 test channel. It includes everything from the beta_26 and beta_27 test builds.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_21/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_27/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_21/wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_27/wadamesh-heltec-v4-tft.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
@@ -40,7 +41,7 @@
|
||||
{
|
||||
"maker": "wadamesh",
|
||||
"class": "wadamesh",
|
||||
"name": "wadamesh — LilyGo T-Deck",
|
||||
"name": "LilyGo T-Deck",
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='https://wadamesh.com/wadamesh-icon-512.png'>",
|
||||
"firmware": [
|
||||
@@ -48,19 +49,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_21": {
|
||||
"notes": "Stable and test update channels. beta_21 is the first stable build. On devices that update over Wi-Fi there is now a \"Get test builds (beta)\" switch on the About screen: leave it off to stay on stable releases, or turn it on to receive test builds earlier — it switches both the \"update available\" check and the firmware you install to the test channel. You can go back to stable at any time.\nMQTT bridge (highly experimental, off by default). An opt-in bridge that forwards the messages your node receives to an MQTT broker of your choice, so you can log your mesh or wire it into other tools. It is gated behind a privacy consent you have to tick, direct messages are excluded unless you deliberately enable them, and the payload can be sealed with an encryption key so only you can read it. Use a broker you control, never a public one. This is experimental and may change.\nPortugal (Narrow) region preset, so Portuguese users can pick the local defaults in one tap. (Issue #74, requested by Salgueiron.)\nSaved-contacts counter. The Contacts overflow menu now shows how many contacts you have saved out of the maximum it can hold (for example 42 / 2000). (Issue #72, asked by Buko84.)\nThe signal probe never travels past the first node now. The \"auto-discover\" signal pulse, and the probe behind the Signal popup, are sent strictly zero-hop, so a repeater can no longer re-flood them across the mesh. The probe only measures the link to whatever hears it first, which is what it was always meant to do.\nYour most relevant contacts now show when you have a lot of them. The on-screen contact list is capped for performance, but it was trimming the list before sorting it, so some contacts could drop out of view. It now sorts first, shows the closest matches, and adds a \"+N more — search to narrow the list\" line at the bottom. (Issue #73, reported by 1nerdherder.)\nThe blocked-users list opened from Contacts now shows the proper two-line title bar with a back button, instead of an unlabelled screen.\nThe Contacts overflow popup fits the screen again after the saved-contacts counter was added.\nEverything from beta_20 is included.",
|
||||
"beta_27": {
|
||||
"notes": "Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.\nDevices installed under the Launcher no longer show the on-device \"Install update\" button. That update path only works on a standalone flash; under the Launcher it failed with \"won't fit: Bad Size Given\". The \"update available\" note stays, but you update through the Launcher instead. Thanks to Keith (KM1USR) for the report.\nKeyboard backlight brightness control on the T-Deck: a live slider plus Off / 25 / 50 / 75 / Max presets in Settings > Keyboard. It dims smoothly (fine-grained at the low end), applies to the on and auto modes and the new-message flash, and survives a reboot. Thanks to Yazutsu (PR #85).\nOn devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). Your existing contacts move over automatically on first boot; your identity and settings stay in internal flash, so the node is unchanged. Keep the card inserted afterwards - the contact list lives on it, and an older firmware won't look there if you ever downgrade.\nThis is the new stable build, promoted from the beta_27 test channel. It includes everything from the beta_26 and beta_27 test builds.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_21/wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_27/wadamesh-tdeck-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_21/wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_27/wadamesh-tdeck.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
<div class="grp">With your phone</div>
|
||||
<a href="#companion">Companion mode</a>
|
||||
<div class="grp">Reference</div>
|
||||
<a href="#updates">Updates & test builds</a>
|
||||
<a href="#settings-pages">Settings pages</a>
|
||||
<a href="#apps">Apps</a>
|
||||
</nav>
|
||||
@@ -140,6 +141,13 @@
|
||||
<span class="chip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14 0"/><path d="M8.5 16.4a5 5 0 0 1 7 0"/><line x1="12" y1="20" x2="12" y2="20"/></svg> LoRa + Wi-Fi + Bluetooth</span>
|
||||
<span class="chip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg> Keyboard + touch</span>
|
||||
</div>
|
||||
<div class="callout">
|
||||
<span class="cico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h11l5 5v11a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4z"/><path d="M9 4v4M13 4v4"/></svg></span>
|
||||
<div>
|
||||
<b>Got a microSD slot? Keep a card inserted.</b>
|
||||
<p>On devices with a microSD slot (the LilyGo T-Deck and Tanmatsu), wadamesh is best used with a card in place. It keeps map tiles, your contacts and your chat history on the card, which is roomier and far more reliable than the small internal flash, so a card prevents memory issues and stops that data being lost on reboot. The Heltec V4 has no card slot and manages fine on its internal storage.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="doc" id="home">
|
||||
@@ -197,6 +205,25 @@
|
||||
</div>
|
||||
<div class="shot"><div class="device"><img class="screen" src="docs-img/map.png" alt="Map"></div><div class="cap">Map</div></div>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<h3>Bring your own tiles (offline maps)</h3>
|
||||
<p>You don't need Wi-Fi to see the map. Copy a standard OpenStreetMap tile pack onto the T-Deck's microSD card and the map reads straight off the card, fully offline.</p>
|
||||
<p><b>Where they go.</b> Use the usual slippy-map <code>z/x/y</code> folder layout, in either of these locations on the card:</p>
|
||||
<ul>
|
||||
<li><code>/maps/osm/<z>/<x>/<y>.png</code> — the Meshtastic / MeshCore standard layout (recommended, so packs are interchangeable between apps).</li>
|
||||
<li><code>/tiles/<z>/<x>/<y>.png</code> — also read. This is the same folder the Wi-Fi download cache uses, so your own tiles and downloaded ones live side by side.</li>
|
||||
</ul>
|
||||
<p><b>Format.</b> 256×256 <code>.png</code> tiles (<code>.jpg</code> also works). <code>z</code> is the zoom level and <code>x</code>/<code>y</code> the tile column/row at that zoom — the same scheme OSM's own tile server and every XYZ tile downloader use, so most ready-made packs drop in unchanged.</p>
|
||||
<p><b>Turn it on.</b> Open the map's options (the gear button on the map) and switch on <b>Tiles from SD card</b>; the status line confirms <code>Map tiles: microSD</code>. Turn it back off to fetch live tiles over Wi-Fi again.</p>
|
||||
<p><b>Making a pack.</b> Any XYZ / slippy-map tile downloader works — point it at OpenStreetMap for your area and a sensible zoom range, then copy the resulting <code>z/x/y</code> folders onto the card. Each extra zoom level roughly quadruples the tile count, so grab only the levels you'll actually use.</p>
|
||||
<div class="callout">
|
||||
<span class="cico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
|
||||
<div>
|
||||
<b>Board differences</b>
|
||||
<p>The "Tiles from SD card" toggle is a T-Deck feature. The Heltec V4 has no microSD slot, so it caches downloaded tiles to a small internal partition (no offline packs there). On the Tanmatsu, tiles — your own or downloaded — live at <code>/tiles/<z>/<x>/<y>.jpg</code> on the microSD card; drop JPEG tiles there and they show alongside the downloaded ones.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="settings">
|
||||
@@ -239,6 +266,24 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="updates">
|
||||
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></span> Updates & test builds</h2>
|
||||
<div class="row">
|
||||
<div class="prose">
|
||||
<p>On the boards that update over Wi-Fi (the Heltec V4 and the standalone T-Deck), WADAMESH checks for new firmware on its own. When one is available, <b>Settings → About</b> shows it and you can flash it straight from the device with <b>Install update</b>, no computer needed. (Under Launcher and on the Tanmatsu you update through Launcher or the app store instead.)</p>
|
||||
<h3>Two channels: Stable and Test</h3>
|
||||
<ul>
|
||||
<li><b>Stable</b> is the default. It is the build that has been tested and is recommended for everyday use. Leave your device on this unless you want to help test.</li>
|
||||
<li><b>Test (beta)</b> gets new features and fixes earlier, before they are proven. It can be rough, so it is meant for trying things early and reporting bugs, not for a device you rely on.</li>
|
||||
</ul>
|
||||
<h3>Turn on test builds</h3>
|
||||
<p>Flip <b>Settings → About → "Get test builds (beta)"</b> on. From then on, both the update check and <b>Install update</b> follow the test channel, so the device offers the newest test build over Wi-Fi. Turn the switch back off to return to stable. You can switch either way at any time.</p>
|
||||
<p>Prefer to start on a test build from scratch? Pick <b>Beta · testing</b> when you flash from <a href="https://flasher.wadamesh.com" target="_blank" rel="noopener">flasher.wadamesh.com</a>.</p>
|
||||
</div>
|
||||
<div class="shot"><div class="device"><img class="screen" src="docs-img/set_about.png" alt="About page"></div><div class="cap">The About page</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="settings-pages">
|
||||
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg></span> Settings pages</h2>
|
||||
<div class="prose" style="max-width:68ch"><p>WADAMESH splits its settings into eighteen pages. Here is what each one does and the controls you will find on it. Open any page from the Settings tab; a back chevron in the title bar returns you to the grid.</p></div>
|
||||
@@ -438,6 +483,7 @@
|
||||
<div class="lede">Version, updates and diagnostics.</div>
|
||||
<ul>
|
||||
<li><b>Firmware version</b> plus update check / install</li>
|
||||
<li><b>Get test builds (beta)</b> switch: opt into the <a href="#updates">test channel</a></li>
|
||||
<li>Full <b>system diagnostics</b> — uptime, chip, memory, flash, storage</li>
|
||||
<li><b>Device ID</b> and crash-report export</li>
|
||||
</ul>
|
||||
|
||||
@@ -321,6 +321,7 @@
|
||||
<button type="button" data-wm-ch="stable" style="font:inherit;font-size:.85rem;font-weight:600;padding:7px 16px;border:0;background:#fff;color:var(--mut);cursor:pointer">Stable</button>
|
||||
<button type="button" data-wm-ch="beta" style="font:inherit;font-size:.85rem;font-weight:600;padding:7px 16px;border:0;border-left:1px solid var(--line);background:#fff;color:var(--mut);cursor:pointer">Beta · testing</button>
|
||||
</div>
|
||||
<p style="margin:12px auto 0;max-width:600px;text-align:left;font-size:.88rem;line-height:1.6"><b>Stable</b> is the tested default. Pick it unless you want to help test. <b>Beta · testing</b> gets new features and fixes earlier, before they are proven, so it can be rough. It is for trying things early and reporting bugs.<br>Already running WADAMESH? You do not have to reflash to switch channels: turn on <b>Settings → About → "Get test builds (beta)"</b> on the device and it updates to test builds over Wi-Fi. Switch it off to go back to stable.</p>
|
||||
<p data-wm-betahint hidden style="margin:8px auto 0;font-size:.84rem;color:#b4631a;max-width:560px">Beta builds are newer and less tested — for trying things early and reporting bugs. On a T-Deck under Launcher you download the .bin and side-load it.</p>
|
||||
<div data-whatsnew hidden style="margin:18px auto 0;max-width:560px;background:rgba(21,182,166,.07);border:1px solid rgba(21,182,166,.28);border-radius:12px;padding:13px 16px;text-align:left">
|
||||
<div style="font-weight:600">What's new in <span data-wn-tag style="color:var(--teal)">the latest build</span></div>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# beta_23 (TEST build — one user-facing note per non-blank, non-# line; # lines are section comments)
|
||||
|
||||
# --- Fixed ---
|
||||
Bluetooth companion reliability, especially messages and repeater admin. When a phone or Mac was connected over Bluetooth, the device was sending it a log entry for every LoRa packet it heard — on a busy mesh that swamped the Bluetooth link and starved the frames that actually matter. The device no longer sends that packet log over Bluetooth (USB and Wi-Fi companions still get it), so incoming messages reach the app and repeater / room-server admin (Manage Regions, saving parameters) stops timing out and dropping the connection. (Issues #46 and #54.)
|
||||
|
||||
# --- Also in this test line (carried from beta_22) ---
|
||||
The signal probe reads a real signal again: it sends a directed trace "ping" to your nearest repeater, which replies, instead of a zero-hop advert that nothing answers. Thanks to Tarmo for spotting it.
|
||||
Contacts discovered while your phone is connected now also show on the device's own screen, not just in the phone app. (Issue #73.)
|
||||
|
||||
# --- Also ---
|
||||
This is a test-channel build (opt in with "Get test builds" on the About screen). Everything from the stable beta_21 is included.
|
||||
@@ -0,0 +1,7 @@
|
||||
# beta_24 (TEST build — one user-facing note per non-blank, non-# line; # lines are section comments)
|
||||
|
||||
# --- Fixed ---
|
||||
The signal probe now reads a real signal to your nearest repeater. It sends the standard mesh node-discovery request that repeaters actually answer, instead of the trace packet from the last test build that they ignored, so the signal reading updates again. This is the same request the Ultra and KiekR apps use. Thanks to Tarmo for decoding the exact packet.
|
||||
|
||||
# --- Also ---
|
||||
This is a test-channel build (opt in with "Get test builds" on the About screen). Everything from the stable beta_21 and the earlier test builds is included.
|
||||
@@ -0,0 +1,21 @@
|
||||
# beta_25 (STABLE — rolls up the beta_22..25 test builds on top of the previous stable, beta_21.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.)
|
||||
|
||||
# --- Fixed ---
|
||||
The signal probe reads a live signal to your nearest repeater again. It sends the standard mesh node-discovery request that repeaters actually answer (the same one the Ultra and KiekR apps use), so the signal reading updates. Thanks to Tarmo for decoding the exact packet.
|
||||
Bluetooth companion reliability, especially messages and repeater / room-server admin. The device no longer floods the Bluetooth link with a per-packet log (USB and Wi-Fi companions still get it), so incoming messages reach the app and admin actions like Manage Regions stop timing out. (Issues #46 and #54.)
|
||||
Contacts show up right away. A node you discover, even while you're on another screen or one the phone app adds over Bluetooth, now appears in your contact list immediately instead of only after you change the sort. Thanks to 1nerdherder for the report. (Issue #73.)
|
||||
Manual telemetry and status requests now work on the first tap instead of needing a second. Thanks to Dan Vybiral (@Vybo) for the fix. (Issue #81.)
|
||||
On the emoji picker and quick-chat replies, pressing Esc or the red X now closes just the popup instead of the whole chat screen.
|
||||
Lock screen: you can now unlock the device after the screen wakes from an incoming message.
|
||||
|
||||
# --- New ---
|
||||
New "Advertise on boot" option in the Advertise app (off by default): turn it on to send one advert when the device powers on, so nearby nodes refresh your details after a reflash. (Issue #76.)
|
||||
|
||||
# --- Tanmatsu ---
|
||||
Tanmatsu: Wi-Fi map tiles now download and display, cached to the microSD card.
|
||||
Tanmatsu: contacts and message history now survive a reboot (moved to the microSD card, so keep a card inserted).
|
||||
Tanmatsu: fixed the crash that stopped the app from booting; popups now fit the screen and scroll, and keyboard navigation stays inside an open popup.
|
||||
|
||||
# --- Also ---
|
||||
This is the new stable build, promoted from the beta_25 test channel. It includes everything from the beta_22, beta_23, beta_24 and beta_25 test builds.
|
||||
@@ -0,0 +1,8 @@
|
||||
# beta_26 (TEST) — storage-reliability fix on top of the current stable (beta_25).
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Fixed ---
|
||||
Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.
|
||||
|
||||
# --- Improved ---
|
||||
On devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). That frees the small internal flash and puts the frequently-rewritten files on the more reliable card. Your existing contacts move to the card automatically the first time; your identity and settings stay in internal flash, so the node is unchanged.
|
||||
@@ -0,0 +1,15 @@
|
||||
# beta_27 (STABLE — rolls up the beta_26 + beta_27 test builds on top of the previous stable, beta_25.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.)
|
||||
|
||||
# --- Fixed ---
|
||||
Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.
|
||||
Devices installed under the Launcher no longer show the on-device "Install update" button. That update path only works on a standalone flash; under the Launcher it failed with "won't fit: Bad Size Given". The "update available" note stays, but you update through the Launcher instead. Thanks to Keith (KM1USR) for the report.
|
||||
|
||||
# --- New ---
|
||||
Keyboard backlight brightness control on the T-Deck: a live slider plus Off / 25 / 50 / 75 / Max presets in Settings > Keyboard. It dims smoothly (fine-grained at the low end), applies to the on and auto modes and the new-message flash, and survives a reboot. Thanks to Yazutsu (PR #85).
|
||||
|
||||
# --- Improved ---
|
||||
On devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). Your existing contacts move over automatically on first boot; your identity and settings stay in internal flash, so the node is unchanged. Keep the card inserted afterwards - the contact list lives on it, and an older firmware won't look there if you ever downgrade.
|
||||
|
||||
# --- Also ---
|
||||
This is the new stable build, promoted from the beta_27 test channel. It includes everything from the beta_26 and beta_27 test builds.
|
||||
@@ -0,0 +1,13 @@
|
||||
# beta_28 (TEST) — room-server session fix + the wyvern.red feedback batch, on top of stable beta_27.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Fixed ---
|
||||
Room servers no longer go one-way "frozen". The device now keeps its room login session alive in the background and automatically logs back in when the session drops, so posts keep being delivered and new room messages keep arriving. There is also a new "Log in again" option in the room chat's menu, and a clear warning when the room only granted you read-only access (posts would previously just vanish). Thanks to EricMW61 for the report (issue #89).
|
||||
The screen now truly switches off in standby. Before, only the backlight went dark while the panel kept displaying the last image - the actual cause of the LCD ghosting / burn-in some of you noticed. The panel itself now sleeps, and on wake your screen is back instantly. Also: a lit lock screen now always dims on every board, a screen woken by a message re-darkens after 10 seconds, and the lock-screen clock drifts a few pixels to avoid ghosting. Thanks to wyvern.red for pinning this down.
|
||||
The on-device "Install update" now also hides on mixed or older partition layouts where an update could crash the device halfway through (found via a user crash report - thanks!).
|
||||
|
||||
# --- New ---
|
||||
Compact messages (IRC style): a new toggle in chat settings shows one dense line per message instead of bubbles, fitting two to three times more history on screen. Off by default. (wyvern.red's suggestion.)
|
||||
Clearer delivery ticks: an up-arrow means sent but not yet confirmed, a double check means the other side confirmed it, and a failed message now says "tap to resend" - one tap resends it. (wyvern.red)
|
||||
Keyboard navigation: the scroll keys (F/C) now zoom the map, and backspace in an empty message box exits typing mode so the navigation keys work again. (wyvern.red)
|
||||
"Reset path" is now also in the chat's own menu next to Blocked users, so you can force a fresh route without going through Contacts.
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "DataStore.h"
|
||||
#if defined(ESP32)
|
||||
#include <SD.h>
|
||||
#include "helpers/esp32/WdtHeavyGuard.h" // suspend core-0 idle WDT during the (SPIFFS-GC-prone) contact write
|
||||
#endif
|
||||
|
||||
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
||||
@@ -70,6 +71,38 @@ void DataStore::begin() {
|
||||
#else
|
||||
// init 'blob store' support
|
||||
_fs->mkdir("/bl");
|
||||
#if defined(ESP32)
|
||||
// One-time migration: when a secondary FS is active (an SD card is present)
|
||||
// but contacts/channels still live on the primary FS from an earlier build,
|
||||
// copy them onto the card ONCE and delete the primary copies. This preserves
|
||||
// the user's contact list while getting the frequently-rewritten files off a
|
||||
// small/near-full SPIFFS — whose garbage collection can otherwise stall the
|
||||
// loop task for seconds during saveContacts and trip the task watchdog
|
||||
// (the beta_25 reboot loop). Identity + prefs stay on _fs and never move.
|
||||
// No-op once the card already holds the files, and a safe no-op on boards
|
||||
// whose primary-FS metadata can't confirm the source exists (e.g. P4 FFat).
|
||||
if (_fsExtra) {
|
||||
static const char* const kMoveFiles[] = { "/contacts3", "/channels2" };
|
||||
for (const char* nm : kMoveFiles) {
|
||||
if (_fsExtra->exists(nm)) continue; // already on the card
|
||||
if (!_fs->exists(nm)) continue; // nothing to move
|
||||
File in = openRead(_fs, nm);
|
||||
File out = openWrite(_fsExtra, nm);
|
||||
bool ok = in && out;
|
||||
if (ok) {
|
||||
uint8_t buf[256];
|
||||
int n;
|
||||
while ((n = in.read(buf, sizeof(buf))) > 0) {
|
||||
if (out.write(buf, (size_t)n) != (size_t)n) { ok = false; break; }
|
||||
}
|
||||
}
|
||||
if (in) in.close();
|
||||
if (out) out.close();
|
||||
if (ok) _fs->remove(nm); // reclaim SPIFFS space
|
||||
else if (_fsExtra->exists(nm)) _fsExtra->remove(nm); // don't leave a partial copy
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -382,6 +415,17 @@ File file = openRead(_getContactsChannelsFS(), "/contacts3");
|
||||
}
|
||||
|
||||
void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
|
||||
#if defined(ESP32)
|
||||
// A full/fragmenting contacts write on SPIFFS can trigger a multi-second GC
|
||||
// pass that disables the flash cache and starves core 0's idle task, tripping
|
||||
// the task watchdog (the beta_25 reboot loop, confirmed by coredump: loopTask
|
||||
// stuck in spiffs_gc_clean under this exact call). Suspend that watchdog for
|
||||
// the write so a bounded-but-slow pass stalls the UI instead of rebooting. The
|
||||
// ref-count is shared with the UI history/backup guards, so a backup import
|
||||
// that calls saveContacts stays balanced. On SD-routed devices this is cheap
|
||||
// (FAT has no such GC); it matters most for card-less SPIFFS devices.
|
||||
WdtHeavyGuard _wdt;
|
||||
#endif
|
||||
File file = openWrite(_getContactsChannelsFS(), "/contacts3");
|
||||
if (file) {
|
||||
uint32_t idx = 0;
|
||||
|
||||
@@ -45,6 +45,10 @@ public:
|
||||
#endif
|
||||
FILESYSTEM* getPrimaryFS() const { return _fs; }
|
||||
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
|
||||
// Route contacts + channels (see _getContactsChannelsFS) to a different FS than identity/prefs.
|
||||
// Used on the Tanmatsu to keep identity/prefs on the (working) internal FFat while putting the
|
||||
// frequently-rewritten contacts/channels on the reliable SD card. Call before begin().
|
||||
void setSecondaryFS(FILESYSTEM* fs) { _fsExtra = fs; }
|
||||
bool loadMainIdentity(mesh::LocalIdentity &identity);
|
||||
bool saveMainIdentity(const mesh::LocalIdentity &identity);
|
||||
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
|
||||
|
||||
+67
-12
@@ -135,6 +135,15 @@ static int s_companion_ota_pinned_reply_target = -1;
|
||||
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
|
||||
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
|
||||
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
||||
// Our self-chosen room-session keep-alive interval (secs). Room servers zero the
|
||||
// legacy suggested-interval field in LOGIN_OK, so the client picks. 128 is the
|
||||
// value upstream itself recommended while the field was live (CLIENT_KEEP_ALIVE_SECS
|
||||
// 128, disabled 2025-06 rather than tuned) — matching it stays friendly to the
|
||||
// server's TODO'd "throttle keep-alives, evict fast pingers" heuristic. The core
|
||||
// expires the connection at 2.5x (320 s) without an ACK; every received room push
|
||||
// also counts as activity (markConnectionActive), so a busy room barely pings at
|
||||
// all. One 9-byte direct REQ + 5-byte ACK per interval — negligible airtime.
|
||||
#define ROOM_KEEPALIVE_SECS 128
|
||||
|
||||
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
|
||||
|
||||
@@ -2200,6 +2209,26 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Deferred guest-login-then-request (uiSendRequestAfterGuestLogin): the touch
|
||||
// UI sent only a blank-password LOGIN and is waiting on the LOGIN-OK before
|
||||
// issuing the STATUS/TELEMETRY REQ. While armed we have sent no REQ to this
|
||||
// contact, so any RESPONSE from it here is the login reply. On OK, fire the
|
||||
// deferred REQ now — we're in the repeater's ACL and a direct path has been
|
||||
// learned, so it lands first try. On a non-OK (fail) reply, just disarm and
|
||||
// let the UI's reply deadline flip the window to "failed".
|
||||
if (_ui_login_then && len > 4 && memcmp(&_ui_login_then, contact.id.pub_key, 4) == 0) {
|
||||
const bool login_ok = (data[4] == RESP_SERVER_LOGIN_OK)
|
||||
|| (len > 5 && memcmp(&data[4], "OK", 2) == 0);
|
||||
const UiReqKind k = _ui_login_then_kind;
|
||||
cancelUIDeferredLogin();
|
||||
if (login_ok) {
|
||||
ContactInfo& rc = const_cast<ContactInfo&>(contact);
|
||||
if (k == UiReqKind::Telemetry) sendTelemetryRequestForUI(rc);
|
||||
else sendStatusPingForUI(rc);
|
||||
}
|
||||
return; // login frame consumed (OK fired the REQ; fail disarmed)
|
||||
}
|
||||
#endif
|
||||
|
||||
if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response
|
||||
@@ -2219,6 +2248,16 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16;
|
||||
if (keep_alive_secs > 0) {
|
||||
startConnection(contact, keep_alive_secs);
|
||||
} else if (contact.type == ADV_TYPE_ROOM) {
|
||||
// Modern room servers zero the legacy keep-alive field (simple_room_server:
|
||||
// `reply_data[5] = 0; // Legacy`), so no connection was ever armed and we
|
||||
// never pinged. The server still NEEDS to hear from us: its push loop
|
||||
// abandons a client after 3 unACKed pushes (`push_failures < 3`) and only a
|
||||
// received transmission — a post, or exactly this REQ_TYPE_KEEP_ALIVE —
|
||||
// resets the counter (issue #89: rooms went one-way-dead). Arm our own
|
||||
// interval; checkConnections() in loop() does the rest (9-byte direct REQ,
|
||||
// its ACK also refreshes the server's last_activity for us).
|
||||
startConnection(contact, ROOM_KEEPALIVE_SECS);
|
||||
}
|
||||
out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS;
|
||||
out_frame[i++] = data[6]; // permissions (eg. is_admin)
|
||||
@@ -2322,6 +2361,21 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
|
||||
}
|
||||
|
||||
void MyMesh::onControlDataRecv(mesh::Packet *packet) {
|
||||
// Signal probe: a neighbouring repeater answered our zero-hop NODE_DISCOVER_REQ with a
|
||||
// NODE_DISCOVER_RESP. Capture OUR reception of that reply (SNR + RSSI) as the live
|
||||
// signal, matched by tag. This is the standard MeshCore node-discovery, the same packet
|
||||
// the Ultra / KiekR GUIs use (a TRACE gets no reply). Additive: the control data is still
|
||||
// relayed to a connected companion app below. The tag is kept, not cleared, so a slower
|
||||
// repeater's reply still registers (a random 32-bit tag makes a stale match negligible).
|
||||
if (packet->payload_len >= 6 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP
|
||||
&& _ui_sig_probe_tag != 0) {
|
||||
uint32_t rtag; memcpy(&rtag, &packet->payload[2], 4);
|
||||
if (rtag == _ui_sig_probe_tag) {
|
||||
_ui_sig_snr_q4 = (int8_t)(packet->getSNR() * 4.0f);
|
||||
_ui_sig_rssi = (int8_t)_radio->getLastRSSI();
|
||||
_ui_sig_ms = millis();
|
||||
}
|
||||
}
|
||||
if (packet->payload_len + 4 > sizeof(out_frame)) {
|
||||
MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len);
|
||||
return;
|
||||
@@ -2364,18 +2418,8 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) {
|
||||
void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
|
||||
const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) {
|
||||
uint8_t path_sz = flags & 0x03; // NEW v1.11+
|
||||
// Silent SIGNAL PROBE result: a directed trace we sent to a neighbouring repeater
|
||||
// has come back (it retransmitted; we heard it). Capture OUR reception of that
|
||||
// reply — SNR + RSSI — as the live signal, and swallow it (no popup). This is the
|
||||
// non-flooding replacement for the old flood-advert probe, and must run before the
|
||||
// visible-ping path below.
|
||||
if (tag != 0 && tag == _ui_sig_probe_tag) {
|
||||
_ui_sig_probe_tag = 0;
|
||||
_ui_sig_snr_q4 = (int8_t)(packet->getSNR() * 4.0f);
|
||||
_ui_sig_rssi = (int8_t)_radio->getLastRSSI();
|
||||
_ui_sig_ms = millis();
|
||||
return;
|
||||
}
|
||||
// (The signal probe used to be captured here as a directed trace; it is now the
|
||||
// standard node-discovery control packet, captured in onControlDataRecv instead.)
|
||||
#if defined(DISPLAY_CLASS)
|
||||
// If this trace's tag matches the most recent UI-initiated ping, surface
|
||||
// the bidirectional SNRs directly to the touch UI instead of (or in
|
||||
@@ -4343,6 +4387,17 @@ void MyMesh::checkSerialInterface() {
|
||||
void MyMesh::loop() {
|
||||
BaseChatMesh::loop();
|
||||
|
||||
// Session keep-alives for logged-in servers (rooms). The core pinger sends the
|
||||
// 9-byte REQ_TYPE_KEEP_ALIVE (+ our sync_since) a room server expects; the ACK
|
||||
// back refreshes the server's last_activity for us AND resets its push_failures
|
||||
// counter — without it the server abandons a client after any 3 unacknowledged
|
||||
// post pushes and never pushes again (simple_room_server: `push_failures < 3`
|
||||
// gate in its round-robin push loop), which reads as a one-way-frozen room
|
||||
// (issue #89). Upstream left this call commented out ("deprecate the
|
||||
// Connections stuff"); the on-device room UI needs it. Cheap: scans 16 slots,
|
||||
// transmits only when a connection is armed and due (see the room login branch).
|
||||
checkConnections();
|
||||
|
||||
if (_cli_rescue) {
|
||||
checkCLIRescueCmd();
|
||||
} else {
|
||||
|
||||
+87
-39
@@ -6,6 +6,12 @@
|
||||
#include <helpers/MeshTouchTxTrace.h>
|
||||
#include "AbstractUITask.h"
|
||||
|
||||
// Node-discovery control packet (standard MeshCore — matches examples/simple_repeater
|
||||
// + simple_sensor): a zero-hop CTL_TYPE_NODE_DISCOVER_REQ that neighbouring repeaters
|
||||
// answer DIRECTLY with a NODE_DISCOVER_RESP. Used by the touch signal probe.
|
||||
#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 // upper nibble = type; low bit = prefix_only
|
||||
#define CTL_TYPE_NODE_DISCOVER_RESP 0x90
|
||||
|
||||
// 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
|
||||
@@ -327,6 +333,31 @@ public:
|
||||
return sendTelemetryRequestForUI(recipient);
|
||||
}
|
||||
|
||||
/** Touch-UI manual STATUS/TELEMETRY request that DEFERS the REQ until the
|
||||
* guest LOGIN is acknowledged. The chained helpers above fire LOGIN and REQ
|
||||
* back-to-back, but a repeater drops a PAYLOAD_TYPE_REQ from a sender it
|
||||
* hasn't ACL'd yet — and on first contact the ACL entry isn't committed by
|
||||
* the time the REQ is processed, so the first request usually gets no reply
|
||||
* (the user had to tap twice). This sends ONLY the blank-password LOGIN now
|
||||
* and arms _ui_login_then; onContactResponse issues the REQ once the
|
||||
* LOGIN-OK lands, by which point we're in the ACL and a direct out_path has
|
||||
* been learned, so the REQ decrypts on the first try.
|
||||
* Returns the LOGIN's MSG_SEND_* result — the UI shows "requesting…" on a
|
||||
* successful send and arms its own reply deadline. Manual paths only;
|
||||
* auto-poll keeps the immediate chained send above (a single arm slot can't
|
||||
* serve its multi-node loop, and a missed poll just retries next interval). */
|
||||
int uiSendRequestAfterGuestLogin(ContactInfo& recipient, UiReqKind kind) {
|
||||
uint32_t login_est = 0;
|
||||
int r = sendLogin(recipient, "", login_est);
|
||||
if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) {
|
||||
memcpy(&_ui_login_then, recipient.id.pub_key, 4);
|
||||
_ui_login_then_kind = kind;
|
||||
} else {
|
||||
cancelUIDeferredLogin();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Admin login for the touch UI repeater admin console.
|
||||
* Sends a sendLogin with the given password (empty = guest), records
|
||||
* pending_login so onContactResponse's existing login branch can route
|
||||
@@ -349,6 +380,29 @@ public:
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Is a keep-alive session to this server currently armed and alive?
|
||||
* (Room chat UI: the core frees the slot 2.5x the interval after the last
|
||||
* ACK/activity, so false = the session needs a re-login.) */
|
||||
bool uiHasConnectionTo(const uint8_t pub_key[32]) { return hasConnectionTo(pub_key); }
|
||||
|
||||
/** Re-login to a room server WITHOUT a password. For a client already in the
|
||||
* room's ACL a blank login replies LOGIN_OK with the stored permissions and
|
||||
* skips the login replay guard (simple_room_server onAnonDataRecv, blank-
|
||||
* password branch) — and when it arrives flooded the server also resets its
|
||||
* stale out_path to us. The blank login itself resets NOTHING else server-side
|
||||
* (not push_failures, not last_activity); the actual heal is the chain it
|
||||
* starts: LOGIN_OK -> onContactResponse arms the room keep-alive -> the first
|
||||
* REQ_TYPE_KEEP_ALIVE refreshes the server's last_activity and clears its
|
||||
* push-abandon counter, so pushes resume (issue #89: self-heal + the chat
|
||||
* sheet's "Log in again"). NOTE it cannot recover a server that REBOOTED
|
||||
* (non-admin ACL entries aren't persisted there) — that needs a passworded
|
||||
* Join from the Contacts sheet. */
|
||||
int uiRoomRelogin(const uint8_t pub_key[32]) {
|
||||
ContactInfo* c = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||
if (!c || c->type != ADV_TYPE_ROOM) return MSG_SEND_FAILED;
|
||||
return uiSendAdminLogin(*c, "");
|
||||
}
|
||||
|
||||
/** Send a CLI command line to a previously-logged-in repeater / room
|
||||
* server. The reply lands in AbstractUITask::onAdminCommandReply via
|
||||
* queueMessage when the server returns a TXT_TYPE_CLI_DATA frame.
|
||||
@@ -365,6 +419,13 @@ public:
|
||||
_ui_pending_kind = UiReqKind::None;
|
||||
_ui_pending_tag = 0;
|
||||
}
|
||||
/** Disarm a deferred guest-login-then-request (see uiSendRequestAfterGuestLogin)
|
||||
* so a late LOGIN-OK can't fire a REQ after the UI gave up. Kept separate from
|
||||
* cancelUIPingPending() so a ping timeout never disarms a telemetry request. */
|
||||
void cancelUIDeferredLogin() {
|
||||
_ui_login_then = 0;
|
||||
_ui_login_then_kind = UiReqKind::None;
|
||||
}
|
||||
/** True if a UI ping is still waiting on a reply. */
|
||||
bool hasUIPingPending() const { return _ui_pending_status != 0; }
|
||||
|
||||
@@ -623,46 +684,27 @@ public:
|
||||
return tag;
|
||||
}
|
||||
|
||||
/** SIGNAL PROBE (the non-flooding way): trace-ping the best-reachable repeater so
|
||||
* it retransmits — a real reply we can measure — WITHOUT flooding the mesh. A
|
||||
* plain zero-hop advert gets no reply (nothing repeats it), so the signal never
|
||||
* updates; a directed trace to a neighbour repeater does (it appends its RX-SNR
|
||||
* and retransmits, which we hear). Picks the repeater with the shortest known
|
||||
* path (0 = direct neighbour), breaking ties by most-recently-heard. The reply
|
||||
* is captured silently in onTraceRecv via _ui_sig_probe_tag → _ui_sig_*. Returns
|
||||
* the tag, or 0 if no repeater with a known path is available (the caller can
|
||||
* fall back to a zero-hop advert). */
|
||||
/** SIGNAL PROBE (the standard MeshCore node-discovery): broadcast a zero-hop
|
||||
* NODE_DISCOVER_REQ control packet asking repeaters to answer. Each neighbouring
|
||||
* repeater replies DIRECTLY with a NODE_DISCOVER_RESP — this is the exact packet the
|
||||
* Ultra / KiekR GUIs use, and what repeater firmware actually answers. (A TRACE or a
|
||||
* bare zero-hop advert gets no reply: repeaters don't retransmit an unpathed trace,
|
||||
* so the old trace-probe read nothing — thanks to Tarmo for decoding the real packet.)
|
||||
* onControlDataRecv matches _ui_sig_probe_tag on the reply and captures its SNR/RSSI
|
||||
* into _ui_sig_*. Never floods (zero-hop, not forwarded). Returns the tag. */
|
||||
uint32_t uiSendSignalProbe() {
|
||||
ContactInfo best; bool have = false; ContactInfo c;
|
||||
const int n = getNumContacts();
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (!getContactByIdx((uint32_t)i, c)) continue;
|
||||
if (c.type != ADV_TYPE_REPEATER) continue; // only repeaters retransmit a trace
|
||||
if (c.out_path_len == OUT_PATH_UNKNOWN) continue; // no known path → would need a flood
|
||||
if (c.out_path_len > MAX_PATH_SIZE) continue; // guard a corrupt length
|
||||
if (!have) { best = c; have = true; continue; }
|
||||
if (c.out_path_len < best.out_path_len ||
|
||||
(c.out_path_len == best.out_path_len &&
|
||||
c.last_advert_timestamp > best.last_advert_timestamp)) {
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
if (!have) return 0;
|
||||
uint32_t tag = 0;
|
||||
getRNG()->random((uint8_t*)&tag, sizeof(tag));
|
||||
if (tag == 0) tag = 1;
|
||||
uint8_t flags = (uint8_t)(_prefs.path_hash_mode & 0x03);
|
||||
mesh::Packet* pkt = createTrace(tag, 0, flags);
|
||||
if (!pkt) return 0;
|
||||
_ui_sig_probe_tag = tag;
|
||||
if (best.out_path_len > 0 && best.out_path_len <= MAX_PATH_SIZE) {
|
||||
sendDirect(pkt, best.out_path, best.out_path_len); // directed along the known path
|
||||
} else {
|
||||
uint8_t hash_sz = (uint8_t)(_prefs.path_hash_mode + 1);
|
||||
if (hash_sz == 0 || hash_sz > 4) hash_sz = 1;
|
||||
sendDirect(pkt, best.id.pub_key, hash_sz); // direct neighbour → single hop
|
||||
}
|
||||
return tag;
|
||||
uint8_t data[10];
|
||||
data[0] = CTL_TYPE_NODE_DISCOVER_REQ; // 0x80; low bit (prefix_only) stays 0
|
||||
data[1] = (uint8_t)(1 << ADV_TYPE_REPEATER); // filter: ask repeaters to answer
|
||||
getRNG()->random(&data[2], 4); // random tag, to match the responses to
|
||||
memcpy(&_ui_sig_probe_tag, &data[2], 4);
|
||||
if (_ui_sig_probe_tag == 0) { _ui_sig_probe_tag = 1; memcpy(&data[2], &_ui_sig_probe_tag, 4); }
|
||||
uint32_t since = 0; // 0 = answer regardless of freshness
|
||||
memcpy(&data[6], &since, 4);
|
||||
mesh::Packet* pkt = createControlData(data, sizeof(data));
|
||||
if (!pkt) { _ui_sig_probe_tag = 0; return 0; }
|
||||
sendZeroHop(pkt); // repeaters reply directly; never floods
|
||||
return _ui_sig_probe_tag;
|
||||
}
|
||||
|
||||
/** Request CayenneLPP telemetry from a remote contact. Reply is delivered
|
||||
@@ -909,6 +951,12 @@ private:
|
||||
* matching by pubkey alone misroutes the login OK as the REQ reply.
|
||||
* Compare tag in onContactResponse to keep the two streams separate. */
|
||||
uint32_t _ui_pending_tag = 0;
|
||||
/** Deferred guest-login-then-request arm: holds the first 4 bytes of the
|
||||
* recipient pub_key (0 = none) plus which REQ to send, set by
|
||||
* uiSendRequestAfterGuestLogin(). onContactResponse fires the REQ when the
|
||||
* matching LOGIN-OK arrives, removing the ACL-commit race on the first try. */
|
||||
uint32_t _ui_login_then = 0;
|
||||
UiReqKind _ui_login_then_kind = UiReqKind::None;
|
||||
BaseSerialInterface *_serial;
|
||||
AbstractUITask* _ui;
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
#include "MqttBridge.h"
|
||||
|
||||
// The single global bridge instance (extern in the header). Defined for every board: the header
|
||||
// makes MqttBridge a no-op stub on the Tanmatsu, and the real bridge below on all other boards.
|
||||
MqttBridge mqtt_bridge;
|
||||
|
||||
#if !defined(HAS_TANMATSU) // ---- real PubSubClient/WiFiClient implementation; NOT built on the P4 ----
|
||||
#include <Preferences.h>
|
||||
#include <WiFi.h>
|
||||
#include <stdio.h>
|
||||
@@ -9,8 +15,6 @@
|
||||
#include "mbedtls/md.h"
|
||||
#include "mbedtls/base64.h"
|
||||
|
||||
MqttBridge mqtt_bridge;
|
||||
|
||||
// Read every field from the "mqtt" NVS namespace into the members. isKey() guards
|
||||
// avoid the [E] NOT_FOUND log spam that getString emits for absent keys on the
|
||||
// USB-CDC companion stream (see TouchPrefsStore prefsGetStr note).
|
||||
@@ -212,4 +216,5 @@ void MqttBridge::reloadConfig() {
|
||||
_host, _port, (int)_enabled, (int)_pubDm, (int)_pubChannel, (int)_encOn);
|
||||
}
|
||||
|
||||
#endif // !HAS_TANMATSU (real implementation)
|
||||
#endif // ESP32 && MULTI_TRANSPORT_COMPANION
|
||||
@@ -2,6 +2,25 @@
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#if defined(HAS_TANMATSU)
|
||||
// ---- MQTT DISABLED on the Tanmatsu (ESP32-P4) ----
|
||||
// The P4's Wi-Fi runs through the esp-hosted C6 co-processor; the Arduino WiFiClient / PubSubClient
|
||||
// path this bridge uses is not wired for that and crashed at boot. Provide a no-op bridge with the
|
||||
// SAME public API so the shared companion + settings code links without pulling PubSubClient into
|
||||
// the P4 build. Re-enable once esp-hosted TCP is proven on the Tanmatsu.
|
||||
class MqttBridge {
|
||||
public:
|
||||
void begin(const char* /*nodeHex*/) {}
|
||||
void loop() {}
|
||||
void publishDM(const char*, const uint8_t*, float, uint8_t, uint32_t, const char*) {}
|
||||
void publishChannel(int, const char*, float, uint8_t, uint32_t, const char*) {}
|
||||
bool enabled() const { return false; }
|
||||
static void saveConfig(const char*, uint16_t, const char*, const char*,
|
||||
bool, bool, const char*, bool) {}
|
||||
void reloadConfig() {}
|
||||
};
|
||||
#else
|
||||
#include <PubSubClient.h>
|
||||
#include <WiFiClient.h>
|
||||
|
||||
@@ -78,6 +97,7 @@ private:
|
||||
bool sealToB64(const char* plain, char* out, size_t outCap);
|
||||
static void escapeJson(const char* src, char* dst, size_t dstLen);
|
||||
};
|
||||
#endif // HAS_TANMATSU (no-op stub) vs real MqttBridge
|
||||
|
||||
extern MqttBridge mqtt_bridge;
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#include "WifiRuntimeStore.h" // persist BLE on/off (ble_en) across reboots
|
||||
#include <string.h>
|
||||
|
||||
// Companion push code for the per-packet RX log (matches MyMesh.cpp). It is kept OFF
|
||||
// the BLE transport in writeFrameToAll — see the note there (issues #46, #54).
|
||||
#define PUSH_CODE_LOG_RX_DATA 0x88
|
||||
|
||||
MultiTransportCompanionInterface::MultiTransportCompanionInterface()
|
||||
: _tcp_port(0), _ws_port(0), _tcp_started(false), _ws_started(false), _tcp_enabled(true), _isEnabled(false), _broadcast(false), _last_reply_target(REPLY_TARGET_USB), _ota_tcp_suspended(false), _ota_ws_suspended(false), _ota_ws_listen_paused(false)
|
||||
#ifdef BLE_PIN_CODE
|
||||
@@ -329,7 +333,12 @@ size_t MultiTransportCompanionInterface::writeFrameToAll(const uint8_t src[], si
|
||||
if (_usb.isConnected() && _usb.writeFrame(src, len) != len)
|
||||
all_ok = false;
|
||||
#ifdef BLE_PIN_CODE
|
||||
if (_ble_begun && _ble_enabled && _ble.isConnected() && _ble.writeFrame(src, len) != len)
|
||||
// The per-packet RX log floods BLE's ~16 frames/sec budget on a busy mesh, starving
|
||||
// the frames that matter — chat messages + admin responses (issues #46, #54). The
|
||||
// MeshCore app doesn't consume the RX log, so keep it OFF BLE; USB/TCP/WS (far more
|
||||
// bandwidth) still receive it for the on-device Monitor app / diagnostics.
|
||||
const bool skip_ble = (len > 0 && src[0] == PUSH_CODE_LOG_RX_DATA);
|
||||
if (!skip_ble && _ble_begun && _ble_enabled && _ble.isConnected() && _ble.writeFrame(src, len) != len)
|
||||
all_ok = false;
|
||||
#endif
|
||||
if (_tcp_started && _tcp.connectedCount() > 0 && _tcp.writeToAllClients(src, len) != len)
|
||||
|
||||
@@ -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 = 28; // 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); v22 tb_nav; v23 scope_direct (opt-in: scope direct/login floods to the region); v24 tb_nav default OFF (experimental); v25 fem_lna (Heltec V4.3 high-gain FEM LNA, opt-in); v26 msg_flash (flash keyboard backlight + wake screen on a new message, opt-in); v27 flood_adv_hrs + local_adv_min (periodic self-advert intervals, the standard MeshCore flood/local advert on a timer); v28 beta_updates (opt-in to test/beta firmware on the OTA update check + install)
|
||||
static const uint8_t TOUCH_CFG_VER = 31; // 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); v22 tb_nav; v23 scope_direct (opt-in: scope direct/login floods to the region); v24 tb_nav default OFF (experimental); v25 fem_lna (Heltec V4.3 high-gain FEM LNA, opt-in); v26 msg_flash (flash keyboard backlight + wake screen on a new message, opt-in); v27 flood_adv_hrs + local_adv_min (periodic self-advert intervals, the standard MeshCore flood/local advert on a timer); v28 beta_updates (opt-in to test/beta firmware on the OTA update check + install); v29 ui_scale default -> Large/150% (Tanmatsu; bumps the old 100% default, leaves an explicit Large/Huge choice); v30 boot_advert (opt-in one-shot flood self-advert ~6s after boot, all boards, #76); v31 compact_chat (opt-in IRC-style dense chat rows instead of bubbles)
|
||||
|
||||
// Defaults (kept identical to the historical per-key defaults).
|
||||
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
|
||||
@@ -99,6 +99,8 @@ struct __attribute__((packed)) TouchCfg {
|
||||
uint8_t flood_adv_hrs; // periodic flood self-advert interval in hours (0 = off) — v27 (trailing)
|
||||
uint16_t local_adv_min; // periodic zero-hop self-advert interval in minutes (0 = off) — v27 (trailing)
|
||||
uint8_t beta_updates; // opt-in to test/beta firmware on the OTA check + install (bool) — v28 (trailing)
|
||||
uint8_t boot_advert; // one-shot flood self-advert ~6s after boot (bool, 0=off) — v30 (trailing) — #76
|
||||
uint8_t compact_chat; // IRC-style dense chat rows instead of bubbles (bool, 0=off) — v31 (trailing)
|
||||
};
|
||||
|
||||
static TouchCfg s_cfg;
|
||||
@@ -155,7 +157,7 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
c.map_show_tilexyz = 1;
|
||||
c.map_show_contacts = 1;
|
||||
c.app_grid_large = 0; // default: compact app grid (T-Deck 4 cols / V4 3 cols)
|
||||
c.ui_scale = 0; // default: 100% UI scale (native resolution)
|
||||
c.ui_scale = 1; // default: 150% "Large" UI scale (Tanmatsu; S3 boards ignore this)
|
||||
#if defined(HAS_TANMATSU)
|
||||
c.kbd_nav = 1; // Tanmatsu: no touchscreen — keyboard nav is the only input, always on
|
||||
#else
|
||||
@@ -168,6 +170,8 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
c.flood_adv_hrs = 0; // OFF: no periodic flood self-advert (advertise manually)
|
||||
c.local_adv_min = 0; // OFF: no periodic zero-hop self-advert
|
||||
c.beta_updates = 0; // OFF: stable update channel (opt-in to beta/test firmware)
|
||||
c.boot_advert = 0; // OFF: no automatic advert on boot — opt-in (#76)
|
||||
c.compact_chat = 0; // OFF: bubble chat layout (opt-in IRC-style dense rows)
|
||||
c.sleep_idle = 0; // default: idle light-sleep OFF
|
||||
{ const char* d = "ertui"; for (int i = 0; i < 5; i++) c.nav_keys[i] = (uint8_t)d[i]; } // default tab hotkeys E/R/T/U/I
|
||||
c.map_zoom_buttons = 0; // default: map zoom = slider
|
||||
@@ -246,6 +250,9 @@ static void cfgLoadOrMigrate() {
|
||||
if (s_cfg.ver < 26) s_cfg.msg_flash = 0;
|
||||
if (s_cfg.ver < 27) { s_cfg.flood_adv_hrs = 0; s_cfg.local_adv_min = 0; }
|
||||
if (s_cfg.ver < 28) s_cfg.beta_updates = 0;
|
||||
if (s_cfg.ver < 29 && s_cfg.ui_scale == 0) s_cfg.ui_scale = 1; // bump old 100% default -> Large (150%)
|
||||
if (s_cfg.ver < 30) s_cfg.boot_advert = 0; // #76 new trailing field: advert-on-boot off by default
|
||||
if (s_cfg.ver < 31) s_cfg.compact_chat = 0; // new trailing field: compact chat rows off by default
|
||||
s_cfg.ver = TOUCH_CFG_VER;
|
||||
s_cfg.magic = TOUCH_CFG_MAGIC;
|
||||
cfgFlush(); // rewrite with new fields defaulted-in
|
||||
@@ -977,6 +984,26 @@ bool touchPrefsSetMsgFlash(bool on) {
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
bool touchPrefsGetBootAdvert() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.boot_advert != 0;
|
||||
}
|
||||
bool touchPrefsSetBootAdvert(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.boot_advert = on ? 1 : 0;
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
bool touchPrefsGetCompactChat() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.compact_chat != 0;
|
||||
}
|
||||
bool touchPrefsSetCompactChat(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.compact_chat = on ? 1 : 0;
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
// Periodic self-advert intervals (0 = off). Validation mirrors MeshCore: flood in hours (cap 168);
|
||||
// local zero-hop in minutes, 0 or 60-240 (MeshCore's MIN_LOCAL_ADVERT_INTERVAL is 60).
|
||||
uint16_t touchPrefsGetFloodAdvHrs() {
|
||||
|
||||
@@ -148,6 +148,16 @@ bool touchPrefsSetFemLna(bool on);
|
||||
bool touchPrefsGetMsgFlash();
|
||||
bool touchPrefsSetMsgFlash(bool on);
|
||||
|
||||
/* Advertise on boot (#76): fire one flood self-advert ~6s after boot so peers with auto-add on
|
||||
* refresh our pubkey (useful after a reflash wiped storage). Opt-in, default off. All boards. */
|
||||
bool touchPrefsGetBootAdvert();
|
||||
bool touchPrefsSetBootAdvert(bool on);
|
||||
|
||||
/* Compact chat (IRC-style): one dense "HH:MM name: text" row per message instead of
|
||||
* bubbles, so far more history fits on screen. Opt-in, default off (bubbles). */
|
||||
bool touchPrefsGetCompactChat();
|
||||
bool touchPrefsSetCompactChat(bool on);
|
||||
|
||||
/* Periodic self-advert intervals (the standard MeshCore flood/local advert, on a timer). 0 = off.
|
||||
* Flood in hours; local zero-hop in minutes (0 or 60-240). Scheduled in UITask::loop via sendAdvert. */
|
||||
uint16_t touchPrefsGetFloodAdvHrs();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
// -----------------------------------------------------------------------------
|
||||
// WdtHeavyGuard — suspend core 0's IDLE-task watchdog around a bounded-but-slow
|
||||
// flash burst.
|
||||
//
|
||||
// On ESP32 a fragmenting SPIFFS write can trigger garbage collection: a
|
||||
// multi-second flash operation that disables the CPU cache and starves the idle
|
||||
// task long enough to trip the TASK watchdog. Seen in a coredump as an abort
|
||||
// from task_wdt_isr while the loop task was inside spiffs_gc_clean during
|
||||
// DataStore::saveContacts. Bracket such writes with a WdtHeavyGuard so a
|
||||
// bounded-but-slow GC pass stalls the UI briefly instead of rebooting.
|
||||
//
|
||||
// Ref-counted and SHARED across translation units — the touch-UI history/backup
|
||||
// saves (UITask.cpp) AND the core contact save (DataStore::saveContacts) use the
|
||||
// same count. That matters because a backup import runs under a guard and itself
|
||||
// calls saveContacts: one balanced count means the inner guard never re-arms the
|
||||
// watchdog while the outer import is still writing. The counter lives in an
|
||||
// inline function's static, so there is a single instance program-wide without a
|
||||
// dedicated .cpp (and no ordering/link surprises).
|
||||
//
|
||||
// IMPORTANT: touch ONLY core 0's IDLE-task WDT. In the Arduino S3 build only core
|
||||
// 0's idle task is subscribed to the task watchdog (CHECK_IDLE_TASK_CPU1 is off),
|
||||
// so disableCore1WDT() merely fails while enableCore1WDT() *newly subscribes*
|
||||
// IDLE1 — arming a core-1 watchdog that did not exist before, which a later slow
|
||||
// flash burst then trips. Leaving core 1 alone keeps the Arduino default
|
||||
// (loopTask not WDT-watched), so a slow write just stalls the UI briefly.
|
||||
// -----------------------------------------------------------------------------
|
||||
#if defined(ESP32)
|
||||
#include <Arduino.h> // disableCore0WDT / enableCore0WDT
|
||||
|
||||
inline int& _wdtHeavyDepth() { static int d = 0; return d; }
|
||||
inline void wdtHeavyBegin() { int& d = _wdtHeavyDepth(); if (d++ == 0) disableCore0WDT(); }
|
||||
inline void wdtHeavyEnd() { int& d = _wdtHeavyDepth(); if (d > 0 && --d == 0) enableCore0WDT(); }
|
||||
#else
|
||||
inline void wdtHeavyBegin() {}
|
||||
inline void wdtHeavyEnd() {}
|
||||
#endif
|
||||
|
||||
struct WdtHeavyGuard { WdtHeavyGuard() { wdtHeavyBegin(); } ~WdtHeavyGuard() { wdtHeavyEnd(); } };
|
||||
@@ -34,9 +34,9 @@ void tdeckKeyboardSetBacklight(uint8_t level) {
|
||||
if (level != s_bl_desired || !forced) { s_bl_desired = level; s_bl_dirty = true; forced = true; }
|
||||
}
|
||||
|
||||
void tdeckKeyboardPoll() {
|
||||
if (!s_inited) return;
|
||||
if (s_bl_dirty) {
|
||||
void tdeckKeyboardFlushBacklight() {
|
||||
if (!s_inited || !s_bl_dirty) return;
|
||||
{
|
||||
s_bl_dirty = false;
|
||||
// LilyGo T-Keyboard backlight: 2-byte command [0x01, brightness] (0 = off).
|
||||
Wire.beginTransmission(PIN_KB_ADDR);
|
||||
@@ -44,6 +44,11 @@ void tdeckKeyboardPoll() {
|
||||
Wire.write(s_bl_desired); // 0 = off, 1-255 = brightness
|
||||
Wire.endTransmission();
|
||||
}
|
||||
}
|
||||
|
||||
void tdeckKeyboardPoll() {
|
||||
if (!s_inited) return;
|
||||
tdeckKeyboardFlushBacklight();
|
||||
if (Wire.requestFrom((int)PIN_KB_ADDR, 1) != 1) return;
|
||||
uint8_t key = Wire.read();
|
||||
if (key == 0) return; // no key this scan
|
||||
|
||||
@@ -28,4 +28,10 @@ int tdeckKeyboardReadKey();
|
||||
* owns the bus. Only re-sent when the value changes. */
|
||||
void tdeckKeyboardSetBacklight(uint8_t level);
|
||||
|
||||
/** Flush a pending backlight request over I2C NOW. CORE-0 ONLY (touch task).
|
||||
* Cheap when nothing is pending (one flag check); called every poll tick so a
|
||||
* brightness change lands within ~one touch-poll period (~8 ms) instead of
|
||||
* waiting for the next full keyboard scan (~32 ms). */
|
||||
void tdeckKeyboardFlushBacklight();
|
||||
|
||||
#endif
|
||||
@@ -356,6 +356,7 @@ static void touchPollTask(void* arg) {
|
||||
#if defined(HAS_TDECK_KEYBOARD)
|
||||
// Poll the keyboard from THIS task too (same I2C bus) so the two devices
|
||||
// never get hit from two cores at once. ~every 4th tick (~32 ms) is plenty.
|
||||
tdeckKeyboardFlushBacklight(); // backlight requests land within ~one tick (~8 ms); key scan stays /4
|
||||
if (++kb_div >= 4) { kb_div = 0; tdeckKeyboardPoll(); }
|
||||
#endif
|
||||
vTaskDelay(pdMS_TO_TICKS(s_period_ms));
|
||||
|
||||
+35
-13
@@ -362,23 +362,45 @@ void setup() {
|
||||
SPIFFS.exists("/identity/_main.id"));
|
||||
bool fresh_install = !use_sd_pref && !setup_done && !spiffs_has_data;
|
||||
|
||||
bool want_sd = !spiffs_ok // no usable SPIFFS partition -> must use SD
|
||||
|| use_sd_pref // user opted in
|
||||
|| fresh_install; // brand-new device: try SD first
|
||||
// Move the WHOLE store (identity/prefs/contacts) to SD:/meshcomod when the
|
||||
// device has no usable SPIFFS, the user opted in, or it's a brand-new device.
|
||||
bool want_full_sd = !spiffs_ok || use_sd_pref || fresh_install;
|
||||
|
||||
SPIClass* _spi = tdeckSharedSPI();
|
||||
if (want_sd && _spi) {
|
||||
for (int a = 0; a < 4 && !sd_storage; ++a) { // short mount ladder (cold cards)
|
||||
bool sd_mounted = false;
|
||||
if (_spi) {
|
||||
// Try to mount the card on EVERY boot now (not just the full-adoption case):
|
||||
// even a device that keeps identity on SPIFFS wants its churn-heavy
|
||||
// contacts/channels on the card. Full 4-try ladder for the cold-card
|
||||
// first-run adoption; a shorter 2-try for the contacts-overflow case so a
|
||||
// card-less device only pays ~260 ms.
|
||||
int tries = want_full_sd ? 4 : 2;
|
||||
for (int a = 0; a < tries && !sd_mounted; ++a) { // short mount ladder (cold cards)
|
||||
SD.end();
|
||||
delay(a == 0 ? 40 : 220);
|
||||
if (SD.begin(PIN_SD_CS, *_spi, 4000000, "/sd", 3) && SD.cardType() != CARD_NONE) {
|
||||
sd_storage = store.useSdStorage();
|
||||
}
|
||||
if (SD.begin(PIN_SD_CS, *_spi, 4000000, "/sd", 3) && SD.cardType() != CARD_NONE)
|
||||
sd_mounted = true;
|
||||
}
|
||||
// On a genuine first run, persist the auto-pick so the "Store data on SD"
|
||||
// toggle reflects it and the choice sticks on every later boot.
|
||||
if (fresh_install && sd_storage && !use_sd_pref) {
|
||||
Preferences _p; if (_p.begin("touch", false)) { _p.putBool("use_sd", true); _p.end(); }
|
||||
Serial.println("[BOOT] first run + SD card present -> data defaults to SD");
|
||||
}
|
||||
if (sd_mounted) {
|
||||
if (want_full_sd) {
|
||||
sd_storage = store.useSdStorage();
|
||||
// On a genuine first run, persist the auto-pick so the "Store data on SD"
|
||||
// toggle reflects it and the choice sticks on every later boot.
|
||||
if (fresh_install && sd_storage && !use_sd_pref) {
|
||||
Preferences _p; if (_p.begin("touch", false)) { _p.putBool("use_sd", true); _p.end(); }
|
||||
Serial.println("[BOOT] first run + SD card present -> data defaults to SD");
|
||||
}
|
||||
} else {
|
||||
// Upgraded device: identity + prefs stay on SPIFFS (no node-identity
|
||||
// change, safe if the card is later pulled), but route the frequently
|
||||
// rewritten contacts + channels to the card. On a near-full 3.375 MB
|
||||
// SPIFFS every 5-second saveContacts triggers a multi-second GC pass
|
||||
// that starves the loop task and trips the task watchdog (the beta_25
|
||||
// reboot loop). DataStore::begin() migrates the existing SPIFFS copies
|
||||
// to the card once, so the contact list is preserved.
|
||||
store.setSecondaryFS(&SD);
|
||||
Serial.println("[BOOT] contacts/channels -> SD card (identity/prefs stay on SPIFFS)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+758
-172
File diff suppressed because it is too large.
Load diff
@@ -315,6 +315,16 @@ public:
|
||||
return _ui_threads[_active_thread_idx].mesh_channel_slot;
|
||||
}
|
||||
int activeThreadIdx() const { return _active_thread_idx; }
|
||||
/** DM thread's full contact pubkey (32 B) — for the chat sheet's "Reset path".
|
||||
* False for channels, unused slots, or a thread with no pubkey mapping yet. */
|
||||
bool getThreadContactPub(int idx, uint8_t out[32]) const {
|
||||
if (idx < 0 || idx >= MAX_UI_THREADS) return false;
|
||||
if (!_ui_threads[idx].used || _ui_threads[idx].channel) return false;
|
||||
const uint8_t* p = _ui_threads[idx].mesh_contact_pub;
|
||||
uint8_t any = 0;
|
||||
for (int i = 0; i < 32; i++) { out[i] = p[i]; any |= p[i]; }
|
||||
return any != 0;
|
||||
}
|
||||
int threadScroll() const { return _thread_scroll; }
|
||||
void setThreadScroll(int v) { _thread_scroll = v; }
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
|
||||
@@ -51,6 +51,9 @@ vend "MicroNMEA" # GPS NMEA parsing
|
||||
vend "AsyncTCP" # the core's ESP32Board HTTP-OTA + companion-over-IP transport
|
||||
vend "ESPAsyncWebServer" # WebSocket/HTTP companion server (needs the task WDT on — sdkconfigs/wadamesh)
|
||||
vend "AsyncElegantOTA" # OTA web UI (the core's ESP32Board HTTP-OTA)
|
||||
# NOTE: PubSubClient is deliberately NOT vended — the MQTT bridge is compiled out on the Tanmatsu
|
||||
# (HAS_TANMATSU no-op stub in MqttBridge.h); the P4's esp-hosted Wi-Fi isn't wired for it and it
|
||||
# crashed the app at boot. Re-add this vend if MQTT is ever brought up on the P4.
|
||||
# Bump the component CMakeLists so the next build re-globs (IDF component scripts can't use
|
||||
# file(GLOB CONFIGURE_DEPENDS) — they run in script mode during requirement expansion).
|
||||
touch components/meshcore/CMakeLists.txt components/ardlibs/CMakeLists.txt 2>/dev/null || true
|
||||
|
||||
@@ -41,7 +41,7 @@ set(WADA_DEFS
|
||||
MULTI_TRANSPORT_COMPANION TCP_PORT=5000 WS_PORT=8765
|
||||
BLE_PIN_CODE=123456 # enables the app's NimBLE companion (BLE over the C6)
|
||||
LORA_FREQ=869.618 LORA_BW=62.5 LORA_SF=8
|
||||
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
|
||||
MAX_CONTACTS=2000 MAX_GROUP_CHANNELS=40 # P4 has ample RAM; contacts array is PSRAM-resident (the_mesh placement-new)
|
||||
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
|
||||
@@ -53,5 +53,12 @@ set(WADA_DEFS
|
||||
foreach(_d ${WADA_DEFS})
|
||||
idf_build_set_property(COMPILE_OPTIONS "-D${_d}" APPEND)
|
||||
endforeach()
|
||||
|
||||
# Firmware version shown on the About page. The Tanmatsu builds outside the S3 release.sh flow (which
|
||||
# injects these), so set them here or About shows only a generic "development build" with no version.
|
||||
# Quotes are escaped so the compiler sees string literals. TODO: have build.sh pass the real tag/date
|
||||
# per release (e.g. from git describe) instead of hardcoding; bump this each beta for now.
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_RELEASE_TAG=\"beta_25\"" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_BUILD_DATE=\"1 Jul 2026\"" APPEND)
|
||||
# (the adcAttachPin compat shim is force-included per-component in meshcore + wadamesh_app —
|
||||
# NOT globally, which would feed the C++ header to FreeRTOS's .S assembly files and break them.)
|
||||
+14
-2
@@ -14,6 +14,7 @@
|
||||
#include "target.h" // board, radio_driver, rtc_clock, display, sensors, radio_init()
|
||||
#include <SPIFFS.h>
|
||||
#include <FFat.h> // DataStore persists on the internal 'locfd' FAT partition
|
||||
#include <SD_MMC.h> // microSD (slot 0): reliable store for contacts/channels/chat (internal FFat loses them on this P4)
|
||||
#include "esp_partition.h" // enumerate partitions (AppFS apps boot fresh — verify locfd is visible)
|
||||
#include <WiFi.h>
|
||||
#include <helpers/esp32/MultiTransportCompanionInterface.h>
|
||||
@@ -54,6 +55,7 @@ void halt() { while (1) { delay(1000); } }
|
||||
// DataStore takes — FILESYSTEM == fs::FS on ESP32). See the storage block in setup().
|
||||
DataStore store(FFat, rtc_clock);
|
||||
bool g_fs_ok = false; // true once FFat(locfd) is mounted; UITask's file browser checks this (extern)
|
||||
bool g_sd_ok = false; // true once the microSD (SD_MMC) is mounted; contacts/channels/chat persist there (extern)
|
||||
MultiTransportCompanionInterface serial_interface;
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
@@ -298,9 +300,12 @@ static void hostedConnectC6() {
|
||||
}
|
||||
|
||||
static void wadameshSetup() {
|
||||
Serial.begin(115200);
|
||||
// Do NOT call Serial.begin() on the Tanmatsu (P4). The launcher already owns the USB-Serial-JTAG
|
||||
// console; re-initialising it here crashed the app during boot (before the splash could even draw
|
||||
// — the beta_21+ symptom of "blue screen, no boot log"). Serial output is swallowed on this board's
|
||||
// IDF console anyway, so printf (not Serial) carries the [BOOT] logs. See tanmatsu-serial-begin memory.
|
||||
delay(150);
|
||||
Serial.println("[BOOT] wadamesh / tanmatsu");
|
||||
printf("[BOOT] wadamesh / tanmatsu\n");
|
||||
|
||||
// badge-bsp: panel + input + power. The panel must be up before LVGL flushes to it; ask for
|
||||
// RGB565 to match LVGL's 16-bit color depth (lvglFlush -> writePixelsRGB565 -> bsp_display_blit).
|
||||
@@ -369,6 +374,13 @@ static void wadameshSetup() {
|
||||
} else {
|
||||
printf("[storage] persistence disabled this boot (DataStore opens fail gracefully)\n");
|
||||
}
|
||||
// The internal FFat 'locfd' on this P4 has a broken FAT metadata layer (see the tile-cache notes):
|
||||
// it keeps rarely-written data (identity/prefs) but LOSES the frequently-rewritten contacts + chat on
|
||||
// reboot. So route contacts/channels (and chat history, see uiDataFsReady) to the microSD card, which
|
||||
// is reliable FAT. Identity/NodePrefs stay on FFat — they persist there and survive a card-less boot.
|
||||
g_sd_ok = SD_MMC.begin("/sdcard", false /*1-bit*/) && SD_MMC.cardType() != CARD_NONE;
|
||||
printf("[storage] SD_MMC.begin = %s\n", g_sd_ok ? "OK" : "no card");
|
||||
if (g_sd_ok) store.setSecondaryFS(&SD_MMC); // contacts + channels -> SD; identity/prefs stay on FFat
|
||||
store.begin();
|
||||
|
||||
bootLog("Mesh stack");
|
||||
|
||||
Reference in new issue
Block a user