Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39a4db8f03 | ||
|
|
03e50b0609 | ||
|
|
d75db73fc8 |
No files matched your search
@@ -0,0 +1,94 @@
|
||||
# WadaMesh release channels — Stable + Test
|
||||
|
||||
Goal: ship **test** builds to the community fast, let them shake out, then **promote**
|
||||
a proven test build to **stable**. What ships to stable is exactly what was tested
|
||||
(promote the binary, don't rebuild).
|
||||
|
||||
## Two concepts, kept separate
|
||||
|
||||
- **Branches** = where code lives.
|
||||
- **Channels** = what a device / the flasher pulls (Stable vs Beta).
|
||||
|
||||
## Branches
|
||||
|
||||
- **`main`** — active line. All work lands here; every *test* release is cut from `main`. (Unchanged from today.)
|
||||
- **`stable`** — promoted line. Moves only when a test build is blessed. Starts at the **beta_20** commit.
|
||||
- Promote = `git branch -f stable <blessed beta commit>` + push, then run the stable-promotion publish.
|
||||
|
||||
## Versioning / tags
|
||||
|
||||
- **Test builds:** `beta_N` (immutable, incrementing), marked **GitHub pre-release**. Also mirrored to a rolling **`beta-latest`** pre-release (overwritten each time) for the Launcher beta sync.
|
||||
- **Stable:** the blessed `beta_N`, marked **GitHub Latest**. Keep the `beta_N` id for now; friendly `vX.Y` names are an optional later polish (would need a rebuild-with-tag per promotion).
|
||||
- First stable = **beta_20**.
|
||||
|
||||
## Distribution matrix
|
||||
|
||||
| Target | Stable | Test / Beta |
|
||||
|---|---|---|
|
||||
| GitHub release | blessed `beta_N`, marked **Latest** | every `beta_N`, **pre-release** + rolling `beta-latest` |
|
||||
| VPS archive | `releases/TOUCH/<beta_N>/` (only blessed lands here) | `releases/BETA/<beta_N>/` |
|
||||
| VPS channel feed | `stable/` (version.json + bins) | `beta/` (version.json + bins) |
|
||||
| Flasher (flasher.wadamesh.com) | default | "Beta / testing" toggle |
|
||||
| On-device update check | stable feed (legacy firmware reads `releases/TOUCH`) | beta feed (Phase 2 setting) |
|
||||
| LauncherHub (bmorcelli, T-Deck) | existing entry (follows GitHub Latest) | new "WadaMesh (Beta)" entry (Phase 3) |
|
||||
| Tanmatsu app-store | promote on stable only | (skipped for tests) |
|
||||
|
||||
## The linchpin: keeping field devices on stable
|
||||
|
||||
Legacy firmware's update check scans `releases/TOUCH/` for the highest `beta_N`. So:
|
||||
|
||||
- **Test builds archive under `releases/BETA/`** — invisible to legacy firmware.
|
||||
- **Promotion copies the blessed build into `releases/TOUCH/`** — legacy devices then see it and show "update available," landing everyone on stable.
|
||||
|
||||
Net effect: existing devices quietly settle on **stable (beta_20)** and stop chasing
|
||||
every beta; beta testers opt in via the flasher Beta toggle (or, later, the on-device
|
||||
Beta channel).
|
||||
|
||||
## Two workflows
|
||||
|
||||
### Cut a TEST release (fast, frequent) — from `main`
|
||||
1. `beta_N` = next number.
|
||||
2. Build both touch envs with the tag embedded.
|
||||
3. GitHub: create `beta_N` release **`--prerelease`** with the 7 assets; also refresh the rolling `beta-latest` pre-release.
|
||||
4. VPS: rsync to `releases/BETA/<beta_N>/` + refresh the `beta/` feed.
|
||||
5. Flasher Beta toggle now serves it. Skip LauncherHub / Tanmatsu / site. (~minutes)
|
||||
|
||||
### Promote to STABLE (deliberate, when proven)
|
||||
1. `git branch -f stable <beta_N commit>` + push.
|
||||
2. GitHub: mark that `beta_N` release **Latest** (clear pre-release).
|
||||
3. VPS: copy `releases/BETA/<beta_N>/` → `releases/TOUCH/<beta_N>/`; refresh `stable/` + `latest/` (flasher stable) feeds.
|
||||
4. Full fan-out: LauncherHub follows automatically; push the Tanmatsu store; bump the site "what's new."
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1 — channel split, no firmware change (do now)
|
||||
- [ ] Create + push `stable` branch at beta_20.
|
||||
- [ ] VPS: stand up `stable/`, `beta/`, `releases/BETA/`; point `latest/` (flasher stable) at beta_20; freeze `releases/TOUCH/` at beta_20.
|
||||
- [ ] GitHub: ensure beta_20 is **Latest**; future betas → `--prerelease`.
|
||||
- [ ] Flasher: Stable (default) / Beta toggle + the beta manifest.
|
||||
- [ ] `release.sh` / `gen-flasher-meta.py`: add `--channel beta|stable` producing the right feed/assets + the rolling `beta-latest`.
|
||||
- [ ] Rewrite the `wadamesh-release` skill into the two-workflow form.
|
||||
|
||||
### Phase 2 — on-device channel selector (ships in a beta, then promoted)
|
||||
- [ ] Settings → **Update channel: Stable / Beta**, default Stable (persisted).
|
||||
- [ ] `verchkFetchLatest` reads the `stable/` or `beta/` feed per the setting.
|
||||
- [ ] Beta devices get the new-beta banner automatically; stable devices never see betas.
|
||||
|
||||
### Phase 3 — LauncherHub beta listing (PR to bmorcelli, when wanted)
|
||||
- [ ] Rolling `beta-latest` already produced in Phase 1.
|
||||
- [ ] PR `bmorcelli/M5Stack-json-fw`: add a `wadamesh_beta.py` (mirror `bruce_beta.py`, track `beta-latest`) + a `t-deck.json` (Beta) entry.
|
||||
- [ ] Result: T-Deck Launcher shows **WadaMesh** + **WadaMesh (Beta)**, both auto-synced.
|
||||
- Until then: T-Deck Launcher beta testers side-load the beta `.bin` manually (it is a T-Deck-only gap; V4 uses the flasher).
|
||||
|
||||
## Where current work lands
|
||||
|
||||
- The uncommitted post-beta_20 work (auto-advert GUI, message-flash, docs, RAM pass)
|
||||
plus the hardened **MQTT-privacy** branch → commit to `main` → cut as **beta_21**,
|
||||
the first TEST release in this model. Community tests it; promote when proven.
|
||||
- This is exactly the model that handles PR #71: land the reviewed/hardened pieces on
|
||||
`main`, ship as a beta, promote later.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Locked:** `main` + `stable`; keep `beta_N` ids with a Stable/Beta channel label; Phase 1 now, Phase 2 in beta_21, Phase 3 as a fast-follow.
|
||||
- **Open (reversible):** whether stable later gets friendly `vX.Y` names (needs a rebuild-with-tag per promotion).
|
||||
@@ -16,19 +16,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_20": {
|
||||
"notes": "Spectrum analyzer: a new app in the app drawer that sweeps the LoRa band and draws a live spectrum with a waterfall under it, a colour scale on the side and a peak-frequency readout. Handy for spotting interference or finding a quiet part of the band before you settle on a frequency.\nCustom notification sounds: you can set your own notification sound from a 16-bit WAV file on the SD card, per event, instead of only the built-in chime. There is also a separate switch for the direct-message chime.\nSaved Wi-Fi networks: the device remembers more than one network now and can auto-join them. Each saved network has its own auto-join switch and can be forgotten on its own.\nOne chat settings screen: the gear on a chat or channel row, a long-press on that row, and the cog inside an open chat now all open the same screen. Everything is in one place: mark as read, mute messages, mute mentions, region and scope, share secret, blocked users, and remove or delete.\nPer-channel mute: silence the notification sound for a single channel while the rest keep chiming.\nHigh-gain receiver for the Heltec V4.3: the V4.3 front-end module has a roughly 17 dB receive amplifier that the firmware never switched on, so every V4.3 shipped with it bypassed. There is now a \"High-gain receiver (FEM LNA)\" toggle under Radio and Mesh, in the Signal section. Turning it on can noticeably improve weak-signal reception in quiet or remote spots; in a noisy area it can overload the front end, so it is off by default. V4.2 boards do not have this amplifier and do not show the toggle. Thanks to Wireless Rocks for the report and the measurements.\nApps button on the Heltec V4 home screen: the V4 home screen now has an Apps button next to Send advert, so the app drawer (Spectrum, RF Monitor, Snake and the rest) is one tap away. The T-Deck and Tanmatsu already had this.\nBlocked list is easier to reach: open it from Contacts (the overflow menu) and from the chat settings screen, not only from where it used to live.\nPortuguese (Brazil) is now a UI language, which brings the total to twelve. Community translation.\nAdverts sent from the touch screen are now relayed by region-scoped repeaters. The advert went out without a region tag, so a repeater set to a single region dropped it, while the same advert sent from the phone app got through. The device now tags its flood advert the same way the phone companion does. (Issue #68, reported by EricMW61.)\nA message could be transmitted on the wrong channel. After you added or removed channels the cached channel index could drift, so a message typed in one channel was sent on another channel's key. It looked correct on your own screen but arrived in the other channel. The send path now matches the target channel by name at the moment of sending, so this can no longer happen.\nRoom server logins could time out when the only route to the server was through a region-scoped repeater. A new opt-in toggle, \"Scope direct messages to region\" under Radio and Mesh, in the Signal section, tags your direct, login and admin messages with your region so a region-only repeater will relay them. It is off by default, so cross-region messaging is unchanged for anyone who does not need it. (Issue #64, reported by randelreiss.)\nThe Wi-Fi scan could come back empty. If a scan ran while the device was already connected, the automatic reconnect could cancel it mid-sweep and the list returned zero networks. The scan and the reconnect no longer fight over the radio, and the SSID list loads reliably again.\nFlood scoping was reworked so channel floods and direct floods are handled separately. Channel and group messages stay tagged with your region scope so public channels remain contained, while direct messages, logins and admin traffic stay unscoped by default to keep cross-region messaging working. The new region-scope toggle opts the direct traffic into the region tag (issue #64), and the advert path was moved onto the same scoping the phone companion already used (issue #68).\nThe Spectrum app and the mesh share one radio. While Spectrum is open it takes ownership of the SX1262 so the mesh loop does not re-tune or re-arm receive underneath the sweep, and leaving the app restores the mesh radio settings and resumes normal receive. The sweep also waits out the chip's RSSI settle time after each retune and takes a short peak reading per step, so the trace shows real energy instead of the reset floor.\nA shared flag now tells the main loop when a Wi-Fi scan is running, so the periodic reconnect does not disconnect and reconnect during a sweep, and auto-reconnect is paused for the length of the scan.\nThe Heltec V4.3 front-end module (KCT8103L) is now driven for receive with its low-noise amplifier behind the new setting. This is the large amplifier and it is separate from the SX1262's own internal RX-boost bit, which is only a fraction of a dB.\nThe new settings (per-channel mute, custom sounds, saved WLine truncated
|
||||
"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.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_20/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_21/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_20/wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_21/wadamesh-heltec-v4-tft.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
@@ -48,19 +48,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_20": {
|
||||
"notes": "Spectrum analyzer: a new app in the app drawer that sweeps the LoRa band and draws a live spectrum with a waterfall under it, a colour scale on the side and a peak-frequency readout. Handy for spotting interference or finding a quiet part of the band before you settle on a frequency.\nCustom notification sounds: you can set your own notification sound from a 16-bit WAV file on the SD card, per event, instead of only the built-in chime. There is also a separate switch for the direct-message chime.\nSaved Wi-Fi networks: the device remembers more than one network now and can auto-join them. Each saved network has its own auto-join switch and can be forgotten on its own.\nOne chat settings screen: the gear on a chat or channel row, a long-press on that row, and the cog inside an open chat now all open the same screen. Everything is in one place: mark as read, mute messages, mute mentions, region and scope, share secret, blocked users, and remove or delete.\nPer-channel mute: silence the notification sound for a single channel while the rest keep chiming.\nHigh-gain receiver for the Heltec V4.3: the V4.3 front-end module has a roughly 17 dB receive amplifier that the firmware never switched on, so every V4.3 shipped with it bypassed. There is now a \"High-gain receiver (FEM LNA)\" toggle under Radio and Mesh, in the Signal section. Turning it on can noticeably improve weak-signal reception in quiet or remote spots; in a noisy area it can overload the front end, so it is off by default. V4.2 boards do not have this amplifier and do not show the toggle. Thanks to Wireless Rocks for the report and the measurements.\nApps button on the Heltec V4 home screen: the V4 home screen now has an Apps button next to Send advert, so the app drawer (Spectrum, RF Monitor, Snake and the rest) is one tap away. The T-Deck and Tanmatsu already had this.\nBlocked list is easier to reach: open it from Contacts (the overflow menu) and from the chat settings screen, not only from where it used to live.\nPortuguese (Brazil) is now a UI language, which brings the total to twelve. Community translation.\nAdverts sent from the touch screen are now relayed by region-scoped repeaters. The advert went out without a region tag, so a repeater set to a single region dropped it, while the same advert sent from the phone app got through. The device now tags its flood advert the same way the phone companion does. (Issue #68, reported by EricMW61.)\nA message could be transmitted on the wrong channel. After you added or removed channels the cached channel index could drift, so a message typed in one channel was sent on another channel's key. It looked correct on your own screen but arrived in the other channel. The send path now matches the target channel by name at the moment of sending, so this can no longer happen.\nRoom server logins could time out when the only route to the server was through a region-scoped repeater. A new opt-in toggle, \"Scope direct messages to region\" under Radio and Mesh, in the Signal section, tags your direct, login and admin messages with your region so a region-only repeater will relay them. It is off by default, so cross-region messaging is unchanged for anyone who does not need it. (Issue #64, reported by randelreiss.)\nThe Wi-Fi scan could come back empty. If a scan ran while the device was already connected, the automatic reconnect could cancel it mid-sweep and the list returned zero networks. The scan and the reconnect no longer fight over the radio, and the SSID list loads reliably again.\nFlood scoping was reworked so channel floods and direct floods are handled separately. Channel and group messages stay tagged with your region scope so public channels remain contained, while direct messages, logins and admin traffic stay unscoped by default to keep cross-region messaging working. The new region-scope toggle opts the direct traffic into the region tag (issue #64), and the advert path was moved onto the same scoping the phone companion already used (issue #68).\nThe Spectrum app and the mesh share one radio. While Spectrum is open it takes ownership of the SX1262 so the mesh loop does not re-tune or re-arm receive underneath the sweep, and leaving the app restores the mesh radio settings and resumes normal receive. The sweep also waits out the chip's RSSI settle time after each retune and takes a short peak reading per step, so the trace shows real energy instead of the reset floor.\nA shared flag now tells the main loop when a Wi-Fi scan is running, so the periodic reconnect does not disconnect and reconnect during a sweep, and auto-reconnect is paused for the length of the scan.\nThe Heltec V4.3 front-end module (KCT8103L) is now driven for receive with its low-noise amplifier behind the new setting. This is the large amplifier and it is separate from the SX1262's own internal RX-boost bit, which is only a fraction of a dB.\nThe new settings (per-channel mute, custom sounds, saved WLine truncated
|
||||
"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.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_20/wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_21/wadamesh-tdeck-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_20/wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_21/wadamesh-tdeck.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,488 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WADAMESH — Documentation</title>
|
||||
<meta name="description" content="User guide for the WADAMESH touch firmware on the LilyGo T-Deck: home, chat, contacts, map, settings and the built-in apps. Real device screenshots.">
|
||||
<link rel="icon" href="wadamesh-badge.svg">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#ffffff; --bg2:#f4f6f2; --ink:#15181e; --mut:#5d6770;
|
||||
--line:#e5e8e2; --teal:#15b6a6; --teal-ink:#0e8d80;
|
||||
color-scheme:light;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html{scroll-behavior:smooth}
|
||||
body{font:16px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Inter,sans-serif; background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased}
|
||||
a{color:var(--teal-ink); text-decoration:none}
|
||||
|
||||
.top{position:sticky; top:0; z-index:30; display:flex; align-items:center; gap:14px; padding:12px 22px;
|
||||
background:rgba(255,255,255,.88); backdrop-filter:saturate(1.2) blur(8px); border-bottom:1px solid var(--line)}
|
||||
.top .word{font-weight:700; letter-spacing:2px; font-size:1.05rem; color:var(--ink)}
|
||||
.top .word .t{color:var(--teal)}
|
||||
.top .tag{font-family:'JetBrains Mono',monospace; font-size:.66rem; letter-spacing:.18em; text-transform:uppercase; color:var(--mut); border:1px solid var(--line); border-radius:100px; padding:4px 9px}
|
||||
.top .sp{flex:1}
|
||||
.top .home{display:inline-flex; align-items:center; gap:7px; color:var(--mut); font-size:.86rem; border:1px solid var(--line); border-radius:100px; padding:6px 13px; background:#fff; transition:background .15s,color .15s,border-color .15s}
|
||||
.top .home:hover{color:var(--teal-ink); background:var(--bg2); border-color:#bfe6e0}
|
||||
.top .home svg{width:16px;height:16px}
|
||||
|
||||
.shell{max-width:1200px; margin:0 auto; display:grid; grid-template-columns:220px 1fr; gap:34px; padding:0 22px}
|
||||
nav.toc{position:sticky; top:64px; align-self:start; height:calc(100vh - 64px); overflow:auto; padding:26px 0 40px}
|
||||
nav.toc .grp{font-family:'JetBrains Mono',monospace; font-size:.68rem; letter-spacing:.13em; text-transform:uppercase; color:var(--mut); margin:20px 0 7px}
|
||||
nav.toc .grp:first-child{margin-top:0}
|
||||
nav.toc a{display:block; padding:6px 12px; border-radius:9px; color:#46525c; font-size:.92rem; border-left:2px solid transparent}
|
||||
nav.toc a:hover{background:var(--bg2); color:var(--ink)}
|
||||
nav.toc a.on{color:var(--teal-ink); background:rgba(21,182,166,.09); border-left-color:var(--teal); font-weight:600}
|
||||
|
||||
main{padding:30px 0 90px; min-width:0}
|
||||
.hero h1{font-size:clamp(1.9rem,4.4vw,2.6rem); letter-spacing:-.5px; font-weight:750}
|
||||
.hero p{color:var(--mut); max-width:64ch; margin-top:10px; font-size:1.05rem}
|
||||
.hero .meta{display:flex; gap:8px; flex-wrap:wrap; margin-top:16px}
|
||||
.chip{display:inline-flex; align-items:center; gap:6px; font-size:.8rem; color:#3a444d; border:1px solid var(--line); border-radius:100px; padding:5px 11px; background:#fff}
|
||||
.chip svg{width:14px;height:14px;color:var(--teal)}
|
||||
|
||||
section.doc{padding:36px 0; border-top:1px solid var(--line); scroll-margin-top:74px}
|
||||
.doc h2{font-size:1.5rem; font-weight:730; letter-spacing:-.3px; display:flex; align-items:center; gap:10px}
|
||||
.doc h2 .ico{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;flex:none; background:rgba(21,182,166,.12);color:var(--teal)}
|
||||
.doc h2 .ico svg{width:18px;height:18px}
|
||||
.row{display:grid; grid-template-columns:minmax(0,1fr) 372px; gap:34px; align-items:start; margin-top:16px}
|
||||
@media(max-width:980px){.row{grid-template-columns:1fr}}
|
||||
.prose{color:#3c454d; min-width:0}
|
||||
.prose p{margin:0 0 12px}
|
||||
.prose h3{font-size:1.02rem; margin:18px 0 6px; color:var(--ink)}
|
||||
.prose ul{margin:4px 0 12px; padding-left:18px}
|
||||
.prose li{margin:4px 0}
|
||||
.prose b{color:var(--ink)}
|
||||
.prose code{font-family:'JetBrains Mono',monospace; font-size:.85em; background:var(--bg2); border:1px solid var(--line); border-radius:6px; padding:1px 6px}
|
||||
.shot{position:sticky; top:74px}
|
||||
.cap{text-align:center; font-family:'JetBrains Mono',monospace; font-size:.68rem; color:var(--mut); margin-top:10px; letter-spacing:.04em}
|
||||
|
||||
/* T-Deck frame: the real screenshot + a generated QWERTY keyboard (added in JS) */
|
||||
.device{width:344px; margin:0 auto; background:#16191d; border-radius:16px; padding:12px 12px 9px; border:1px solid #e9ece7; box-shadow:0 24px 60px -30px rgba(21,24,30,.5)}
|
||||
.device img.screen{display:block; width:320px; height:240px; border-radius:8px}
|
||||
.kbd{margin:9px auto 2px; display:flex; flex-direction:column; gap:4px; width:312px}
|
||||
.kr{display:flex; gap:4px; justify-content:center}
|
||||
.key{flex:1; max-width:28px; height:15px; background:#23282e; border-radius:4px; display:grid; place-items:center; font:8px/1 system-ui; color:#7d8893}
|
||||
.key.wide{max-width:140px; flex:4}
|
||||
|
||||
.gallery{display:grid; grid-template-columns:repeat(auto-fill,minmax(232px,1fr)); gap:20px; margin-top:18px}
|
||||
.mini{background:#16191d; border-radius:12px; padding:7px; border:1px solid #e9ece7}
|
||||
.mini img{display:block; width:100%; height:auto; border-radius:6px}
|
||||
.mini .cap{margin-top:6px}
|
||||
|
||||
/* Settings-page detail cards: real screenshot + what the page does + its controls */
|
||||
.sgrp{font-family:'JetBrains Mono',monospace; font-size:.7rem; letter-spacing:.14em; text-transform:uppercase; color:var(--mut); margin:30px 0 0; padding-top:4px}
|
||||
.scards{display:grid; grid-template-columns:repeat(auto-fill,minmax(290px,1fr)); gap:20px; margin-top:13px}
|
||||
.scard{border:1px solid var(--line); border-radius:14px; background:#fff; padding:13px; display:flex; flex-direction:column}
|
||||
.scard .thumb{background:#16191d; border-radius:9px; padding:6px}
|
||||
.scard .thumb img{display:block; width:100%; height:auto; border-radius:5px}
|
||||
.scard .thumb.ph{aspect-ratio:4/3; display:grid; place-items:center; border:1px dashed #39424b; padding:0}
|
||||
.scard .thumb.ph span{font-family:'JetBrains Mono',monospace; font-size:.6rem; letter-spacing:.12em; color:#7d8893; text-transform:uppercase}
|
||||
.scard h3{font-size:1.04rem; margin:12px 0 2px; color:var(--ink); display:flex; align-items:center; gap:8px; flex-wrap:wrap}
|
||||
.scard h3 .tag{font-family:'JetBrains Mono',monospace; font-size:.58rem; letter-spacing:.05em; text-transform:uppercase; color:var(--teal-ink); background:rgba(21,182,166,.1); border-radius:5px; padding:2px 6px; font-weight:600}
|
||||
.scard .lede{color:var(--mut); font-size:.9rem; margin:2px 0 9px}
|
||||
.scard ul{margin:0; padding-left:17px}
|
||||
.scard li{margin:4px 0; font-size:.89rem; color:#3c454d; line-height:1.5}
|
||||
.scard li b{color:var(--ink); font-weight:600}
|
||||
|
||||
/* Caution callout (amber) */
|
||||
.callout{display:flex; gap:11px; margin-top:18px; padding:13px 15px; border-radius:11px; background:#fff7ed; border:1px solid #f6dcb8}
|
||||
.callout .cico{flex:none; width:20px; height:20px; color:#c2740b; margin-top:1px}
|
||||
.callout .cico svg{width:20px; height:20px}
|
||||
.callout b{display:block; color:#7c4a03; margin-bottom:3px; font-size:.94rem}
|
||||
.callout p{margin:0; color:#7a5320; font-size:.9rem; line-height:1.55}
|
||||
|
||||
footer{border-top:1px solid var(--line); padding:26px 22px; text-align:center; color:var(--mut); font-size:.85rem}
|
||||
@media(max-width:980px){
|
||||
.shell{grid-template-columns:1fr; gap:0}
|
||||
nav.toc{position:static; height:auto; border-bottom:1px solid var(--line); padding:14px 0; display:flex; gap:6px; overflow:auto; white-space:nowrap}
|
||||
nav.toc .grp{display:none} nav.toc a{border-left:0; flex:none}
|
||||
.shot{position:static; margin:0 auto 6px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="top">
|
||||
<a class="word" href="index.html">WADA<span class="t">MESH</span></a>
|
||||
<span class="tag">Docs</span>
|
||||
<span class="sp"></span>
|
||||
<a class="home" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg> Back to site</a>
|
||||
</div>
|
||||
|
||||
<div class="shell">
|
||||
<nav class="toc" id="toc">
|
||||
<div class="grp">Start here</div>
|
||||
<a href="#overview">Overview</a>
|
||||
<div class="grp">The five tabs</div>
|
||||
<a href="#home">Home</a>
|
||||
<a href="#chat">Chat</a>
|
||||
<a href="#contacts">Contacts</a>
|
||||
<a href="#map">Map</a>
|
||||
<a href="#settings">Settings</a>
|
||||
<div class="grp">With your phone</div>
|
||||
<a href="#companion">Companion mode</a>
|
||||
<div class="grp">Reference</div>
|
||||
<a href="#settings-pages">Settings pages</a>
|
||||
<a href="#apps">Apps</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div class="hero" id="overview">
|
||||
<h1>WADAMESH user guide</h1>
|
||||
<p>WADAMESH turns a LilyGo T-Deck — its landscape screen, BlackBerry-style keyboard and trackball — into a complete front-end for your MeshCore radio: chat, channels, contacts, a live map and deep radio settings, all on the device. (It also runs on the Heltec V4 and Tanmatsu.) Every screen below is a real screenshot from the device.</p>
|
||||
<div class="meta">
|
||||
<span class="chip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg> No phone required</span>
|
||||
<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>
|
||||
|
||||
<section class="doc" id="home">
|
||||
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg></span> Home</h2>
|
||||
<div class="row">
|
||||
<div class="prose">
|
||||
<p>The home tab is your <b>app launcher</b> — a grid of everything on the device:</p>
|
||||
<ul>
|
||||
<li><b>Cmdr</b> — the command centre overview.</li>
|
||||
<li><b>Chats</b>, <b>Contacts</b>, <b>Map</b>, <b>Mentions</b> — jump straight to those.</li>
|
||||
<li><b>Advertise</b> — announce yourself to the mesh (manual or on a timer).</li>
|
||||
<li><b>Signal</b> and <b>Monitor</b> — live RX signal and a repeater-style RF activity view.</li>
|
||||
<li><b>Spectrum</b> — sweep the LoRa band for interference.</li>
|
||||
<li><b>Settings</b>, <b>Terminal</b> (MeshCore CLI), <b>Files</b> (microSD).</li>
|
||||
</ul>
|
||||
<p>The bar along the bottom is always there — it switches the five main tabs (Chat, Contacts, Home, Map, Settings) from anywhere. The top bar shows unread count, time, signal and battery.</p>
|
||||
</div>
|
||||
<div class="shot"><div class="device"><img class="screen" src="docs-img/home.png" alt="Home app launcher"></div><div class="cap">Home — app launcher</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="chat">
|
||||
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></span> Chat</h2>
|
||||
<div class="row">
|
||||
<div class="prose">
|
||||
<p>Every conversation in one list, newest first — both <b>channels</b> (group chats like <code>#wadamesh</code>, <code>Public</code>) and <b>direct messages</b>. An unread badge shows the count.</p>
|
||||
<h3>On each row</h3>
|
||||
<ul>
|
||||
<li><b>Tap</b> a row to open the conversation, then type on the keyboard to reply.</li>
|
||||
<li>The <b>gear</b> on the right opens that chat's settings — mute, region & scope, share secret, blocked users, remove — the same screen as the cog inside a chat.</li>
|
||||
</ul>
|
||||
<p>The top-right buttons add a contact, mark everything read, and show your QR code.</p>
|
||||
</div>
|
||||
<div class="shot"><div class="device"><img class="screen" src="docs-img/chat.png" alt="Chat channel list"></div><div class="cap">Chat — channels & DMs</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="contacts">
|
||||
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></span> Contacts</h2>
|
||||
<div class="row">
|
||||
<div class="prose">
|
||||
<p>Everyone your node knows — name, type and how recently you heard them. Tap a contact to message it or open its details: signal history, a route trace, telemetry, and share/favourite options.</p>
|
||||
<p>The <b>overflow menu</b> (top right) holds Search, the <b>Discovered</b> list (nodes heard but not yet added), Add contact, Auto-add settings, and the Blocked list. Auto-add can pull new nodes in from their adverts, or you can add them by hand.</p>
|
||||
</div>
|
||||
<div class="shot"><div class="device"><img class="screen" src="docs-img/contacts.png" alt="Contacts list"></div><div class="cap">Contacts</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="map">
|
||||
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l-6 3V6l6-3 6 3 6-3v15l-6 3-6-3z"/><path d="M9 3v15"/><path d="M15 6v15"/></svg></span> Map</h2>
|
||||
<div class="row">
|
||||
<div class="prose">
|
||||
<p>An OpenStreetMap / OpenTopoMap view with your position and your contacts plotted as markers, plus link lines from you to each node. Tiles are fetched over Wi-Fi through the wadamesh proxy and cached to flash (or microSD), so areas you've viewed work offline.</p>
|
||||
<p>It also has a line-of-sight analyzer: pick a contact and it uses SRTM elevation to show whether terrain blocks the path.</p>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="settings">
|
||||
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-2.4.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 8 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-2.4 1.65 1.65 0 0 0-1.51-1H1a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 3 8z"/></svg></span> Settings</h2>
|
||||
<div class="row">
|
||||
<div class="prose">
|
||||
<p>A tile for each category. Tap one to open its page; the title bar carries a back chevron. The full set is in <a href="#settings-pages">Settings pages</a> below.</p>
|
||||
<ul>
|
||||
<li><b>Profile</b> — your name and node identity.</li>
|
||||
<li><b>Radio & Mesh</b> — frequency, spreading factor, region scope, high-gain receiver, signal probe, auto-advert.</li>
|
||||
<li><b>Auto-add, Wi-Fi, Bluetooth, GPS</b> — discovery and connectivity.</li>
|
||||
<li><b>Clock, Battery, Sensors, Display, Keyboard, Sound, Quick replies, Lock screen</b> — the device experience.</li>
|
||||
<li><b>Language</b> (twelve UI languages), <b>Backups</b>, and <b>About</b> (firmware version, update check, diagnostics).</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="shot"><div class="device"><img class="screen" src="docs-img/settings.png" alt="Settings categories"></div><div class="cap">Settings categories</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="companion">
|
||||
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="11" y1="18" x2="13" y2="18"/></svg></span> Companion mode</h2>
|
||||
<div class="row">
|
||||
<div class="prose">
|
||||
<p>WADAMESH runs everything on its own, but it can also be the radio for the <b>official MeshCore app</b> on your phone (iOS and Android). Your phone brings the big screen and full keyboard; the device does the LoRa. Best of all it works <b>alongside</b> the on-device UI — you can keep using the touch screen while your phone is connected, and your contacts, channels and messages stay in sync between them.</p>
|
||||
<h3>Three ways to connect</h3>
|
||||
<ul>
|
||||
<li><b>USB</b> — plug the device into an Android phone or a computer; the app connects over the cable. Nothing to set up on the device.</li>
|
||||
<li><b>Bluetooth</b> — turn on Bluetooth in <b>Settings → Bluetooth</b>. The device appears in the app as <code>MeshCore-<your node name></code>; pair with the 6-digit code (default <code>123456</code>, changeable on the same page).</li>
|
||||
<li><b>Wi-Fi</b> — put the device on your network in <b>Settings → Wi-Fi</b>. On the same Wi-Fi, the app connects to the device's IP address on port <code>5000</code> (the Wi-Fi page shows the IP).</li>
|
||||
</ul>
|
||||
<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="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
|
||||
<div>
|
||||
<b>Run Bluetooth and Wi-Fi together only when you need to</b>
|
||||
<p>Bluetooth and Wi-Fi can be on at the same time, but each one reserves a good chunk of RAM. On the Heltec V4 especially — it has the least free memory of the three boards — keeping both on at once leaves little headroom and can lead to instability or odd behaviour. If you only want the phone app, pick one transport (Bluetooth or Wi-Fi) and leave the other off; if you mostly use the device on its own, keep both off.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shot"><div class="device"><img class="screen" src="docs-img/set_bluetooth.png" alt="Bluetooth pairing page"></div><div class="cap">Bluetooth — pairing code</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>
|
||||
|
||||
<div class="sgrp">Identity & radio</div>
|
||||
<div class="scards">
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_profile.png" alt="Profile settings"></div>
|
||||
<h3>Profile</h3>
|
||||
<div class="lede">Who you are on the mesh.</div>
|
||||
<ul>
|
||||
<li><b>Node name</b> — your callsign on the network</li>
|
||||
<li><b>Advert location</b> plus a share-in-advert toggle</li>
|
||||
<li><b>Identity key</b> and <b>Share QR</b> to hand out your contact</li>
|
||||
<li><b>Export / Import</b> your whole setup as a JSON backup</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_radio___mesh.png" alt="Radio and Mesh settings"></div>
|
||||
<h3>Radio & Mesh</h3>
|
||||
<div class="lede">Every LoRa and mesh parameter, split into RADIO / MESH / SIGNAL.</div>
|
||||
<ul>
|
||||
<li><b>Radio</b> — preset, frequency, bandwidth, spreading factor, coding rate, TX power, with a live duty-cycle and airtime-per-packet readout</li>
|
||||
<li><b>Mesh</b> — answer-telemetry, path-hash size, region scope tag, scope direct messages</li>
|
||||
<li><b>Signal</b> — high-gain receiver (Heltec V4.3), signal probe, and a shortcut to the Advertise app</li>
|
||||
<li><b>Experimental</b> — multi-ACKs, client repeat, RX boost, duty meter</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_auto_add.png" alt="Auto-add settings"></div>
|
||||
<h3>Auto-add</h3>
|
||||
<div class="lede">Which nodes join Contacts automatically from their adverts.</div>
|
||||
<ul>
|
||||
<li>Per-type toggles: <b>chat</b>, <b>repeater</b>, <b>room</b>, <b>sensor</b></li>
|
||||
<li><b>Overwrite oldest</b> when the contact list is full</li>
|
||||
<li><b>Notify</b> when a new contact is added</li>
|
||||
<li><b>Max hops</b> filter for how far away a node can be</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sgrp">Connectivity</div>
|
||||
<div class="scards">
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_wi_fi.png" alt="Wi-Fi settings"></div>
|
||||
<h3>Wi-Fi</h3>
|
||||
<div class="lede">For map tiles, clock sync and update checks.</div>
|
||||
<ul>
|
||||
<li>On / off with a live status line (IP, signal)</li>
|
||||
<li><b>Scan</b> and tap an SSID, or type one in by hand</li>
|
||||
<li><b>Save & reconnect</b> applies live — no reboot</li>
|
||||
<li>Saved network slots for one-tap quick-connect</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_bluetooth.png" alt="Bluetooth settings"></div>
|
||||
<h3>Bluetooth</h3>
|
||||
<div class="lede">Pair with the MeshCore phone app over BLE.</div>
|
||||
<ul>
|
||||
<li><b>Enable Bluetooth</b> with a live mode status line</li>
|
||||
<li>Set a 6-digit <b>pairing code</b> (applies after a reboot)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_gps.png" alt="GPS settings"></div>
|
||||
<h3>GPS</h3>
|
||||
<div class="lede">The on-board GPS receiver.</div>
|
||||
<ul>
|
||||
<li>On / off with a live <b>fix status</b> (acquiring, 2D/3D, age)</li>
|
||||
<li><b>Serial baud</b> rate (applies after a reboot)</li>
|
||||
<li>Reads "no GPS module" on devices without one</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sgrp">Time & power</div>
|
||||
<div class="scards">
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_clock___time.png" alt="Clock and time settings"></div>
|
||||
<h3>Clock & time</h3>
|
||||
<div class="lede">Keep the clock right.</div>
|
||||
<ul>
|
||||
<li><b>Sync clock</b> from the system / NTP</li>
|
||||
<li><b>Time zone</b> picker, or a custom UTC offset with steppers</li>
|
||||
<li><b>12-hour clock</b> toggle</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_battery.png" alt="Battery settings"></div>
|
||||
<h3>Battery</h3>
|
||||
<div class="lede">Power readouts and tuning.</div>
|
||||
<ul>
|
||||
<li><b>Battery & power history</b> chart</li>
|
||||
<li><b>Calibrate</b> — tap to set 100% at the current voltage</li>
|
||||
<li><b>Battery saver</b> throttles the CPU when idle (T-Deck)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_sensors.png" alt="Sensors settings"></div>
|
||||
<h3>Sensors <span class="tag">Heltec V4</span></h3>
|
||||
<div class="lede">For the V4 Expansion Kit.</div>
|
||||
<ul>
|
||||
<li><b>Expansion Kit</b> configuration</li>
|
||||
<li><b>Show Sensors tab</b> in the bottom bar (applies after restart)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sgrp">Experience</div>
|
||||
<div class="scards">
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_display.png" alt="Display settings"></div>
|
||||
<h3>Display</h3>
|
||||
<div class="lede">Look and feel.</div>
|
||||
<ul>
|
||||
<li><b>Screen timeout</b> before it sleeps</li>
|
||||
<li><b>UI size</b> — Normal / Large / Huge</li>
|
||||
<li><b>Colourful chat bubbles</b>, distance in miles, hide device name</li>
|
||||
<li><b>Theme colour</b> picker</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_keyboard.png" alt="Keyboard settings"></div>
|
||||
<h3>Keyboard</h3>
|
||||
<div class="lede">Typing and on-device input.</div>
|
||||
<ul>
|
||||
<li><b>Secondary layouts</b> — Cyrillic, Greek, Arabic, French, German and more</li>
|
||||
<li><b>Accent popups</b> — long-press for accented letters</li>
|
||||
<li><b>Enter sends</b> the message (T-Deck)</li>
|
||||
<li><b>Flash</b> the screen and keys on a new message (T-Deck)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_sound.png" alt="Sound settings"></div>
|
||||
<h3>Sound</h3>
|
||||
<div class="lede">Notification audio.</div>
|
||||
<ul>
|
||||
<li>Master sound plus per-type toggles: <b>message</b>, <b>DM</b>, <b>@mention</b></li>
|
||||
<li>Custom <b>sound files</b> from microSD where supported</li>
|
||||
<li><b>Volume</b> control (T-Deck / Tanmatsu)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_quick_replies.png" alt="Quick replies settings"></div>
|
||||
<h3>Quick replies</h3>
|
||||
<div class="lede">One-tap canned messages.</div>
|
||||
<ul>
|
||||
<li>Edit <b>six</b> short replies</li>
|
||||
<li>Pick them from the composer to send instantly</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_lock_screen.png" alt="Lock screen settings"></div>
|
||||
<h3>Lock screen <span class="tag">T-Deck</span></h3>
|
||||
<div class="lede">A simple lock when idle.</div>
|
||||
<ul>
|
||||
<li><b>Wallpaper</b> picker and <b>lock text colour</b></li>
|
||||
<li><b>Lock when screen off</b>, then hold to unlock</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sgrp">System</div>
|
||||
<div class="scards">
|
||||
<div class="scard">
|
||||
<div class="thumb ph"><span>Screenshot pending</span></div>
|
||||
<h3>General</h3>
|
||||
<div class="lede">Device housekeeping.</div>
|
||||
<ul>
|
||||
<li><b>Send advert now</b></li>
|
||||
<li><b>Store data on SD</b> (reboot)</li>
|
||||
<li><b>Run setup again</b> from scratch</li>
|
||||
<li><b>Reboot device</b></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_backups.png" alt="Backups settings"></div>
|
||||
<h3>Backups</h3>
|
||||
<div class="lede">Save and restore your whole config.</div>
|
||||
<ul>
|
||||
<li><b>Export</b> a new JSON backup</li>
|
||||
<li>Restore from the <b>saved list</b></li>
|
||||
<li><b>Factory reset</b> in the danger zone</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_language.png" alt="Language settings"></div>
|
||||
<h3>Language</h3>
|
||||
<div class="lede">Twelve UI languages.</div>
|
||||
<ul>
|
||||
<li>Tap a language; the device reboots to apply</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="scard">
|
||||
<div class="thumb"><img src="docs-img/set_about.png" alt="About settings"></div>
|
||||
<h3>About</h3>
|
||||
<div class="lede">Version, updates and diagnostics.</div>
|
||||
<ul>
|
||||
<li><b>Firmware version</b> plus update check / install</li>
|
||||
<li>Full <b>system diagnostics</b> — uptime, chip, memory, flash, storage</li>
|
||||
<li><b>Device ID</b> and crash-report export</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="apps">
|
||||
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg></span> Apps</h2>
|
||||
<div class="prose" style="max-width:64ch">
|
||||
<p>Tools from the home launcher. <b>RF Monitor</b> is a repeater-style view of recent packets and radio stats; <b>Spectrum</b> sweeps the band so you can spot interference or find a clear frequency.</p>
|
||||
</div>
|
||||
<div class="gallery">
|
||||
<div class="mini"><img src="docs-img/app_rfmonitor.png" alt="RF Monitor"><div class="cap">RF Monitor</div></div>
|
||||
<div class="mini"><img src="docs-img/app_spectrum.png" alt="Spectrum analyzer"><div class="cap">Spectrum analyzer</div></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>WADAMESH — open-source MeshCore touch firmware · <a href="index.html">wadamesh.com</a></footer>
|
||||
|
||||
<script>
|
||||
var KB = ['QWERTYUIOP', 'ASDFGHJKL', 'ZXCVBNM'];
|
||||
document.querySelectorAll('.device').forEach(function(dev){
|
||||
var kb = document.createElement('div'); kb.className = 'kbd';
|
||||
KB.forEach(function(rowStr){
|
||||
var r = document.createElement('div'); r.className = 'kr';
|
||||
rowStr.split('').forEach(function(ch){ var k = document.createElement('span'); k.className = 'key'; k.textContent = ch; r.appendChild(k); });
|
||||
kb.appendChild(r);
|
||||
});
|
||||
var sp = document.createElement('div'); sp.className = 'kr';
|
||||
sp.innerHTML = '<span class="key">sym</span><span class="key wide"></span><span class="key">⏎</span>';
|
||||
kb.appendChild(sp);
|
||||
dev.appendChild(kb);
|
||||
});
|
||||
var links = Array.prototype.slice.call(document.querySelectorAll('nav.toc a'));
|
||||
var obs = new IntersectionObserver(function(es){
|
||||
es.forEach(function(e){ if (e.isIntersecting) {
|
||||
links.forEach(function(l){ l.classList.remove('on'); });
|
||||
var a = document.querySelector('nav.toc a[href="#' + e.target.id + '"]');
|
||||
if (a) a.classList.add('on');
|
||||
}});
|
||||
}, { rootMargin: '-25% 0px -65% 0px' });
|
||||
['overview','home','chat','contacts','map','settings','companion','settings-pages','apps'].forEach(function(id){ var el = document.getElementById(id); if (el) obs.observe(el); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -225,9 +225,9 @@
|
||||
<a class="fab" href="https://github.com/ALLFATHER-BV/wadamesh/issues" target="_blank" rel="noopener" aria-label="Report a bug on GitHub" title="Report a bug">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="8" height="14" x="8" y="6" rx="4"/><path d="m19 7-3 2"/><path d="m5 7 3 2"/><path d="m19 19-3-2"/><path d="m5 19 3-2"/><path d="M20 13h-4"/><path d="M4 13h4"/><path d="m10 4 1 2"/><path d="m14 4-1 2"/></svg>
|
||||
</a>
|
||||
<button class="fab" id="docsBtn" type="button" aria-label="Documentation" title="Documentation">
|
||||
<a class="fab" id="docsBtn" href="docs.html" aria-label="Documentation" title="Documentation">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v5h5"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/></svg>
|
||||
</button>
|
||||
</a>
|
||||
<a class="fab" href="https://t413.com/p/projects/MapTileDl/" target="_blank" rel="noopener" aria-label="Map tile downloader" title="Map tile downloader">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l-6 3V6l6-3 6 3 6-3v15l-6 3-6-3z"/><path d="M9 3v15"/><path d="M15 6v15"/></svg>
|
||||
</a>
|
||||
@@ -317,6 +317,11 @@
|
||||
<button class="back" data-back>← choose a different path</button>
|
||||
<div class="guide-head"><h3>Standalone install</h3></div>
|
||||
<p class="p">Plug the device in over USB and flash it right here. Works in <b>Chrome</b> or <b>Edge</b> on desktop.</p>
|
||||
<div class="chtoggle" role="group" aria-label="Release channel" style="display:inline-flex;margin:14px 0 0;border:1px solid var(--line);border-radius:10px;overflow:hidden">
|
||||
<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 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>
|
||||
<ul data-wn-notes style="margin:7px 0 0;padding-left:18px;font-size:.9rem;color:var(--mut);line-height:1.55"></ul>
|
||||
@@ -359,7 +364,7 @@
|
||||
</div>
|
||||
<div class="boards" style="grid-template-columns:1fr; max-width:380px">
|
||||
<div class="board"><h4>LilyGo T-Deck / Plus</h4><p>app image (no bootloader)</p>
|
||||
<a class="dl" href="https://firmware.wadamesh.com/latest/wadamesh-tdeck.bin"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</a></div>
|
||||
<a class="dl" id="dl-tdeck" href="https://firmware.wadamesh.com/latest/wadamesh-tdeck.bin"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</a></div>
|
||||
</div>
|
||||
<p class="hint">New to Launcher? It's the easiest way to try WADAMESH without giving up your other firmware — and you can fully switch to Standalone any time.</p>
|
||||
</div>
|
||||
@@ -477,18 +482,44 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
|
||||
// Live release version + "what's new" — read from the firmware host so the
|
||||
// flasher always reflects the current build (the release pipeline updates
|
||||
// version.json; no site redeploy needed per release).
|
||||
fetch('https://firmware.wadamesh.com/latest/version.json', {cache:'no-store'})
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(v => {
|
||||
if (!v || !v.tag) return;
|
||||
document.querySelectorAll('[data-wn-tag]').forEach(e => e.textContent = v.tag);
|
||||
const notes = Array.isArray(v.notes) ? v.notes : [];
|
||||
document.querySelectorAll('[data-wn-notes]').forEach(ul => {
|
||||
ul.innerHTML = '';
|
||||
notes.forEach(n => { const li = document.createElement('li'); li.textContent = n; ul.appendChild(li); });
|
||||
});
|
||||
if (notes.length) document.querySelectorAll('[data-whatsnew]').forEach(b => b.hidden = false);
|
||||
}).catch(()=>{});
|
||||
// Release channel (Stable default / Beta testing). The toggle swaps the feed dir
|
||||
// on the firmware host — latest/ (stable) <-> latest-beta/ (beta) — and the install
|
||||
// manifests, "what's new", the Launcher .bin link and the version picker follow.
|
||||
const WM = (window.WM = { feed:'latest', arch:'TOUCH' });
|
||||
const WM_MAN = { heltec:'manifest-heltec-v4-tft.json', tdeck:'manifest-tdeck.json' };
|
||||
window.wmManifest = board => 'https://firmware.wadamesh.com/' + WM.feed + '/' + WM_MAN[board];
|
||||
function wmWhatsNew(){
|
||||
const wn = document.querySelectorAll('[data-whatsnew]');
|
||||
fetch('https://firmware.wadamesh.com/' + WM.feed + '/version.json', {cache:'no-store'})
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(v => {
|
||||
if (!v || !v.tag) { wn.forEach(b => b.hidden = true); return; }
|
||||
document.querySelectorAll('[data-wn-tag]').forEach(e => e.textContent = v.tag);
|
||||
const notes = Array.isArray(v.notes) ? v.notes : [];
|
||||
document.querySelectorAll('[data-wn-notes]').forEach(ul => {
|
||||
ul.innerHTML = '';
|
||||
notes.forEach(n => { const li = document.createElement('li'); li.textContent = n; ul.appendChild(li); });
|
||||
});
|
||||
wn.forEach(b => b.hidden = !notes.length);
|
||||
}).catch(() => { wn.forEach(b => b.hidden = true); });
|
||||
}
|
||||
window.wmSetChannel = function(ch){
|
||||
WM.feed = ch === 'beta' ? 'latest-beta' : 'latest';
|
||||
WM.arch = ch === 'beta' ? 'BETA' : 'TOUCH';
|
||||
const bH = document.getElementById('inst-heltec'), bT = document.getElementById('inst-tdeck');
|
||||
if (bH) bH.setAttribute('manifest', wmManifest('heltec'));
|
||||
if (bT) bT.setAttribute('manifest', wmManifest('tdeck'));
|
||||
const dl = document.getElementById('dl-tdeck');
|
||||
if (dl) dl.href = 'https://firmware.wadamesh.com/' + WM.feed + '/wadamesh-tdeck.bin';
|
||||
document.querySelectorAll('[data-wm-ch]').forEach(b => { const on = b.getAttribute('data-wm-ch') === ch;
|
||||
b.style.background = on ? 'var(--teal)' : '#fff'; b.style.color = on ? '#fff' : 'var(--mut)'; });
|
||||
document.querySelectorAll('[data-wm-betahint]').forEach(e => e.hidden = ch !== 'beta');
|
||||
const vs = document.getElementById('verSel');
|
||||
if (vs && vs.options.length) { vs.selectedIndex = 0; vs.dispatchEvent(new Event('change')); }
|
||||
wmWhatsNew();
|
||||
};
|
||||
document.querySelectorAll('[data-wm-ch]').forEach(b => b.addEventListener('click', () => wmSetChannel(b.getAttribute('data-wm-ch'))));
|
||||
wmSetChannel('stable');
|
||||
|
||||
// ---- community videos — add an entry here to add a video ----
|
||||
const VIDEOS = [
|
||||
@@ -521,8 +552,7 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
|
||||
|
||||
// ---- toast + docs placeholder ----
|
||||
function toast(msg){ const t = document.getElementById('toast'); if (!t) return; t.textContent = msg; t.classList.add('show'); clearTimeout(t._t); t._t = setTimeout(() => t.classList.remove('show'), 2400); }
|
||||
const docsBtn = document.getElementById('docsBtn');
|
||||
if (docsBtn) docsBtn.addEventListener('click', () => toast('Documentation — coming soon!'));
|
||||
// The documentation button is a plain link to docs.html (no handler needed).
|
||||
|
||||
// ---- standalone: install a previous version (latest 4 betas + release notes) ----
|
||||
(function(){
|
||||
@@ -530,7 +560,7 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
|
||||
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 latestH = () => window.wmManifest('heltec'), latestT = () => window.wmManifest('tdeck');
|
||||
const num = t => parseInt((String(t).match(/beta_(\d+)/) || [])[1] || '-1', 10);
|
||||
let notes = {};
|
||||
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
@@ -546,10 +576,10 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
|
||||
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 }] }] };
|
||||
builds:[{ chipFamily:'ESP32-S3', parts:[{ path:'https://firmware.wadamesh.com/releases/'+((window.WM&&window.WM.arch)||'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; }
|
||||
if (isLatest){ if(btnH) btnH.setAttribute('manifest', latestH()); if(btnT) 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); }
|
||||
|
||||
@@ -209,6 +209,7 @@ lib_deps =
|
||||
lvgl/lvgl @ ^8.3.11
|
||||
bodmer/TFT_eSPI @ ^2.5.43
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
knolleary/PubSubClient @ ^2.8.0
|
||||
|
||||
; ======================================================================
|
||||
; LilyGo T-Deck / T-Deck Plus — flattened equivalent of meshcomod's
|
||||
@@ -377,3 +378,4 @@ lib_deps =
|
||||
file://arch/esp32/AsyncElegantOTA
|
||||
lvgl/lvgl @ ^8.3.11
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
knolleary/PubSubClient @ ^2.8.0
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# --- New ---
|
||||
Spectrum analyzer: a new app in the app drawer that sweeps the LoRa band and draws a live spectrum with a waterfall under it, a colour scale on the side and a peak-frequency readout. Handy for spotting interference or finding a quiet part of the band before you settle on a frequency.
|
||||
Custom notification sounds: you can set your own notification sound from a 16-bit WAV file on the SD card, per event, instead of only the built-in chime. There is also a separate switch for the direct-message chime.
|
||||
Custom notification sounds: you can set your own notification sound from a 16-bit WAV file on the SD card, per event, instead of only the built-in chime. There is also a separate switch for the direct-message chime. Contributed by Yazutsu (#66).
|
||||
Saved Wi-Fi networks: the device remembers more than one network now and can auto-join them. Each saved network has its own auto-join switch and can be forgotten on its own.
|
||||
One chat settings screen: the gear on a chat or channel row, a long-press on that row, and the cog inside an open chat now all open the same screen. Everything is in one place: mark as read, mute messages, mute mentions, region and scope, share secret, blocked users, and remove or delete.
|
||||
Per-channel mute: silence the notification sound for a single channel while the rest keep chiming.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# beta_21 (one user-facing note per non-blank, non-# line; # lines are section comments)
|
||||
|
||||
# --- New ---
|
||||
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.
|
||||
MQTT 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.
|
||||
Portugal (Narrow) region preset, so Portuguese users can pick the local defaults in one tap. (Issue #74, requested by Salgueiron.)
|
||||
Saved-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.)
|
||||
|
||||
# --- Fixed ---
|
||||
The 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.
|
||||
Your 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.)
|
||||
The blocked-users list opened from Contacts now shows the proper two-line title bar with a back button, instead of an unlabelled screen.
|
||||
The Contacts overflow popup fits the screen again after the saved-contacts counter was added.
|
||||
|
||||
# --- Also ---
|
||||
Everything from beta_20 is included.
|
||||
@@ -1,21 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# Generate the web-flasher metadata that must roll with every release:
|
||||
# - version.json : {tag, notes[]} — the site shows this ("what's new")
|
||||
# - version.json : {tag, channel, notes[]} — the site shows this ("what's new")
|
||||
# - manifest-tdeck.json : esp-web-tools manifest (its "version" drives the
|
||||
# - manifest-heltec-v4-tft.json install dialog, so it must equal the tag)
|
||||
#
|
||||
# These live next to the rolling bins at firmware.wadamesh.com/latest/, so the
|
||||
# flasher always reflects the current release without a site redeploy. The
|
||||
# manifests themselves point at the IMMUTABLE per-tag bins (see below) to dodge
|
||||
# the latest/*.bin 4h edge cache.
|
||||
# These live next to the rolling bins in a per-channel feed dir on
|
||||
# firmware.wadamesh.com (latest/ for stable, latest-beta/ for the test channel),
|
||||
# so the flasher always reflects the current release without a site redeploy. The
|
||||
# manifests themselves point at the IMMUTABLE per-tag bins (releases/TOUCH for
|
||||
# stable, releases/BETA for beta) to dodge the latest/*.bin 4h edge cache.
|
||||
#
|
||||
# Usage: gen-flasher-meta.py <tag> <outdir> [notes_file]
|
||||
# notes_file: one note per non-empty line (plain text); optional.
|
||||
# Usage: gen-flasher-meta.py <tag> <outdir> [notes_file] [channel]
|
||||
# notes_file: one note per non-empty line (plain text); optional ("" to skip).
|
||||
# channel: "stable" (default) or "beta" — picks the immutable archive dir
|
||||
# (releases/TOUCH vs releases/BETA) the manifests point at, and is
|
||||
# recorded in version.json so the site/flasher can label the channel.
|
||||
import sys, json, os
|
||||
|
||||
tag = sys.argv[1]
|
||||
outdir = sys.argv[2]
|
||||
notes_file = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
notes_file = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else None
|
||||
channel = sys.argv[4] if len(sys.argv) > 4 else "stable"
|
||||
archive = "BETA" if channel == "beta" else "TOUCH"
|
||||
|
||||
notes = []
|
||||
if notes_file and os.path.exists(notes_file):
|
||||
@@ -24,7 +30,7 @@ if notes_file and os.path.exists(notes_file):
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
with open(os.path.join(outdir, "version.json"), "w") as f:
|
||||
json.dump({"tag": tag, "notes": notes}, f, indent=2)
|
||||
json.dump({"tag": tag, "channel": channel, "notes": notes}, f, indent=2)
|
||||
|
||||
BOARDS = {
|
||||
"manifest-tdeck.json": ("wadamesh — LilyGo T-Deck", "wadamesh-tdeck-merged.bin"),
|
||||
@@ -32,21 +38,21 @@ BOARDS = {
|
||||
}
|
||||
for fn, (name, binf) in BOARDS.items():
|
||||
manifest = {
|
||||
"name": name,
|
||||
"name": name + ("" if channel == "stable" else " (beta)"),
|
||||
"version": tag,
|
||||
"new_install_prompt_erase": True,
|
||||
"builds": [{
|
||||
"chipFamily": "ESP32-S3",
|
||||
# Point at the IMMUTABLE per-tag bin, NOT latest/. The latest/*.bin URLs
|
||||
# are stable filenames cached 4h (max-age=14400) and overwritten in place,
|
||||
# so for up to 4h after a release the flasher could hand out the PREVIOUS
|
||||
# beta's bytes while version.json already advertised the new tag. The
|
||||
# per-tag path is never overwritten, so its (24h) cache is always correct.
|
||||
# The manifest itself is max-age=300, so this new path propagates in <=5min.
|
||||
"parts": [{"path": f"https://firmware.wadamesh.com/releases/TOUCH/{tag}/{binf}", "offset": 0}],
|
||||
# Point at the IMMUTABLE per-tag bin, NOT the rolling feed bin. The feed
|
||||
# *.bin URLs are stable filenames cached 4h and overwritten in place, so
|
||||
# for up to 4h after a release the flasher could hand out the PREVIOUS
|
||||
# build's bytes while version.json already advertised the new tag. The
|
||||
# per-tag path is never overwritten, so its cache is always correct. The
|
||||
# manifest itself is max-age=300, so the new path propagates in <=5min.
|
||||
"parts": [{"path": f"https://firmware.wadamesh.com/releases/{archive}/{tag}/{binf}", "offset": 0}],
|
||||
}],
|
||||
}
|
||||
with open(os.path.join(outdir, fn), "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
|
||||
print(f"wrote version.json + 2 manifests for {tag} -> {outdir} (notes: {len(notes)})")
|
||||
print(f"wrote version.json + 2 manifests for {tag} ({channel}) -> {outdir} (notes: {len(notes)})")
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Documentation screenshot capture for the DOC_CAPTURE firmware build.
|
||||
|
||||
Build the firmware with -DDOC_CAPTURE, flash it to the T-Deck, then run:
|
||||
|
||||
python3 scripts/doc/capture.py /dev/cu.usbmodemXXXX [outdir]
|
||||
|
||||
The device waits for a 'G' byte, then walks every screen and streams each
|
||||
framebuffer over USB as a 16-bit BMP wrapped in <<<WMSHOT ...>>> / <<<WMEND>>>
|
||||
markers. This script sends 'G', reads each frame, and writes a PNG per screen
|
||||
(via macOS `sips`; falls back to leaving the .bmp if sips is unavailable).
|
||||
"""
|
||||
import sys, os, re, time, subprocess
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError:
|
||||
sys.exit("pyserial not found. Try: /Library/Developer/CommandLineTools/usr/bin/python3 -m pip install pyserial")
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit("usage: capture.py <serial-port> [outdir]")
|
||||
port = sys.argv[1]
|
||||
outdir = sys.argv[2] if len(sys.argv) > 2 else "doc-shots"
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
ser = serial.Serial(port, 115200, timeout=2)
|
||||
print(f"listening on {port} -> {outdir}/")
|
||||
|
||||
hdr_re = re.compile(rb"<<<WMSHOT name=(\S+) w=(\d+) h=(\d+) bytes=(\d+)>>>")
|
||||
|
||||
def read_exact(n):
|
||||
data = bytearray()
|
||||
while len(data) < n:
|
||||
chunk = ser.read(n - len(data))
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
return bytes(data)
|
||||
|
||||
count = 0
|
||||
started = False
|
||||
last_go = 0.0
|
||||
deadline = time.time() + 90 # give the device up to 90s to start
|
||||
while time.time() < deadline:
|
||||
if not started and time.time() - last_go > 0.4:
|
||||
ser.write(b"G") # nudge the device to begin the tour
|
||||
ser.flush()
|
||||
last_go = time.time()
|
||||
line = ser.readline()
|
||||
if not line:
|
||||
continue
|
||||
if b"WMTOUR START" in line:
|
||||
started = True
|
||||
print("tour started")
|
||||
continue
|
||||
if b"WMTOUR END" in line:
|
||||
print("tour ended")
|
||||
break
|
||||
m = hdr_re.search(line)
|
||||
if not m:
|
||||
continue
|
||||
name = m.group(1).decode()
|
||||
nbytes = int(m.group(4))
|
||||
data = read_exact(nbytes)
|
||||
if len(data) != nbytes:
|
||||
print(f" ! {name}: short read {len(data)}/{nbytes}")
|
||||
continue
|
||||
bmp = os.path.join(outdir, name + ".bmp")
|
||||
png = os.path.join(outdir, name + ".png")
|
||||
with open(bmp, "wb") as f:
|
||||
f.write(data)
|
||||
try:
|
||||
subprocess.run(["sips", "-s", "format", "png", bmp, "--out", png],
|
||||
check=True, capture_output=True)
|
||||
os.remove(bmp)
|
||||
count += 1
|
||||
print(f" [{count:2d}] {name}.png ({nbytes} bytes)")
|
||||
except Exception as e:
|
||||
count += 1
|
||||
print(f" [{count:2d}] {name}.bmp ({nbytes} bytes; sips convert failed: {e})")
|
||||
|
||||
ser.close()
|
||||
print(f"done: {count} screenshots in {outdir}/")
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reference subscriber + decryptor for the wadamesh MQTT bridge.
|
||||
|
||||
The bridge publishes received mesh messages to:
|
||||
wadamesh/{node_hex}/msg/dm direct / signed messages (only if DM publish is ON)
|
||||
wadamesh/{node_hex}/msg/ch channel messages
|
||||
wadamesh/{node_hex}/status "online" / "offline" (retained LWT)
|
||||
|
||||
Payload:
|
||||
- No encryption key set on the device -> payload is plain UTF-8 JSON.
|
||||
- Encryption key (PSK) set -> payload is
|
||||
base64( nonce[12] || ciphertext || tag[16] ), AES-256-GCM,
|
||||
key = SHA-256(psk).
|
||||
Set PSK below to the same passphrase you typed on the device.
|
||||
|
||||
Requires: pip install paho-mqtt cryptography
|
||||
"""
|
||||
import base64, hashlib, json
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
BROKER = "homeassistant.local"
|
||||
PORT = 1883
|
||||
PSK = "" # same passphrase as Settings -> MQTT -> Encryption key; "" = plaintext
|
||||
|
||||
|
||||
def decode(payload: bytes) -> dict:
|
||||
"""Return the message dict, decrypting if a PSK is configured."""
|
||||
if not PSK:
|
||||
return json.loads(payload)
|
||||
key = hashlib.sha256(PSK.encode()).digest() # 32-byte AES-256 key
|
||||
blob = base64.b64decode(payload)
|
||||
nonce, ct_and_tag = blob[:12], blob[12:] # GCM tag is the trailing 16 bytes
|
||||
plain = AESGCM(key).decrypt(nonce, ct_and_tag, None)
|
||||
return json.loads(plain)
|
||||
|
||||
|
||||
def main():
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
def on_connect(c, *_):
|
||||
c.subscribe("wadamesh/+/msg/#")
|
||||
print("subscribed to wadamesh/+/msg/#")
|
||||
|
||||
def on_message(_c, _u, msg):
|
||||
try:
|
||||
data = decode(msg.payload)
|
||||
print(f"{msg.topic}: {data}")
|
||||
except Exception as e:
|
||||
print(f"{msg.topic}: <undecodable: {e}>")
|
||||
|
||||
c = mqtt.Client()
|
||||
c.on_connect, c.on_message = on_connect, on_message
|
||||
c.connect(BROKER, PORT, 60)
|
||||
c.loop_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,85 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# wadamesh release: build tagged bins for both touch boards, refresh the
|
||||
# update-check listing, and publish to the firmware VPS (behind Cloudflare).
|
||||
# wadamesh release — two channels: TEST (beta, default) and STABLE (promote).
|
||||
#
|
||||
# Usage:
|
||||
# WADAMESH_VPS=user@your-vps WADAMESH_VPS_PATH=/srv/wadamesh/firmware \
|
||||
# scripts/release.sh beta_2
|
||||
# Cut a TEST build (builds both boards, publishes to the beta channel — fast/frequent):
|
||||
# WADAMESH_VPS=user@host scripts/release.sh beta_21
|
||||
#
|
||||
# Promote an ALREADY-BUILT beta to STABLE (no rebuild — copies the tested bins):
|
||||
# WADAMESH_VPS=user@host scripts/release.sh beta_21 --promote
|
||||
#
|
||||
# Channels on the VPS firmware root (firmware.wadamesh.com, behind Cloudflare):
|
||||
# releases/BETA/<tag>/ immutable beta archives <- test channel + new-firmware beta check
|
||||
# releases/TOUCH/<tag>/ immutable stable archives <- stable feed + legacy on-device check
|
||||
# latest-beta/ beta feed (version.json + manifests + rolling bins)
|
||||
# latest/ stable feed (version.json + manifests + rolling bins)
|
||||
#
|
||||
# The VPS target comes from the environment — never commit it (Cloudflare fronts
|
||||
# the origin, so the IP isn't needed in the firmware anyway).
|
||||
set -euo pipefail
|
||||
|
||||
TAG="${1:?usage: release.sh <tag> e.g. beta_2}"
|
||||
TAG="${1:?usage: release.sh <tag> [--promote] e.g. beta_21 / beta_21 --promote}"
|
||||
MODE="beta"
|
||||
[ "${2:-}" = "--promote" ] && MODE="stable"
|
||||
|
||||
PIO="${PIO:-$HOME/Library/Python/3.9/bin/pio}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT="$ROOT/out/firmware" # local mirror of the VPS firmware root
|
||||
REL="$OUT/releases/TOUCH"; LATEST="$OUT/latest"
|
||||
OUT="$ROOT/out/firmware" # local mirror of the VPS firmware root
|
||||
DEST="${WADAMESH_VPS:-}"; DEST_PATH="${WADAMESH_VPS_PATH:-/srv/wadamesh/firmware}"
|
||||
|
||||
# env:binname pairs — plain string form (works on macOS's bash 3.2; no associative arrays)
|
||||
ENVS="heltec_v4_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-tft LilyGo_TDeck_companion_radio_touch:wadamesh-tdeck"
|
||||
|
||||
# 1. Pull the current published tree so the listing stays complete across tags.
|
||||
# Per-channel destination paths.
|
||||
if [ "$MODE" = "stable" ]; then
|
||||
ARCH="$OUT/releases/TOUCH"; FEED="$OUT/latest"
|
||||
else
|
||||
ARCH="$OUT/releases/BETA"; FEED="$OUT/latest-beta"
|
||||
fi
|
||||
|
||||
# 1. Pull the current published tree so listings stay complete across tags.
|
||||
if [ -n "$DEST" ]; then
|
||||
mkdir -p "$OUT"
|
||||
rsync -a "$DEST:$DEST_PATH/" "$OUT/" 2>/dev/null || echo "note: first publish (nothing to pull yet)"
|
||||
fi
|
||||
mkdir -p "$REL/$TAG" "$LATEST"
|
||||
mkdir -p "$ARCH/$TAG" "$FEED"
|
||||
|
||||
# 2. Build both boards (app + merged), tag + version embedded.
|
||||
export PLATFORMIO_BUILD_FLAGS="-DFIRMWARE_RELEASE_TAG='\"${TAG}\"' -DFIRMWARE_VERSION='\"wadamesh ${TAG}\"'"
|
||||
for pair in $ENVS; do
|
||||
env="${pair%%:*}"; name="${pair##*:}"
|
||||
"$PIO" run -t mergebin -e "$env"
|
||||
cp ".pio/build/$env/firmware.bin" "$REL/$TAG/$name.bin"
|
||||
cp ".pio/build/$env/firmware-merged.bin" "$REL/$TAG/$name-merged.bin"
|
||||
cp ".pio/build/$env/firmware-merged.bin" "$LATEST/$name-merged.bin" # rolling latest -> web flasher (standalone)
|
||||
cp ".pio/build/$env/firmware.bin" "$LATEST/$name.bin" # rolling latest app image -> Launcher path
|
||||
done
|
||||
if [ "$MODE" = "beta" ]; then
|
||||
# 2b. TEST build: compile both boards (tag + version embedded) into the beta archive + feed.
|
||||
export PLATFORMIO_BUILD_FLAGS="-DFIRMWARE_RELEASE_TAG='\"${TAG}\"' -DFIRMWARE_VERSION='\"wadamesh ${TAG}\"'"
|
||||
for pair in $ENVS; do
|
||||
env="${pair%%:*}"; name="${pair##*:}"
|
||||
"$PIO" run -t mergebin -e "$env"
|
||||
cp ".pio/build/$env/firmware.bin" "$ARCH/$TAG/$name.bin"
|
||||
cp ".pio/build/$env/firmware-merged.bin" "$ARCH/$TAG/$name-merged.bin"
|
||||
cp ".pio/build/$env/firmware-merged.bin" "$FEED/$name-merged.bin" # rolling -> flasher (standalone)
|
||||
cp ".pio/build/$env/firmware.bin" "$FEED/$name.bin" # rolling app image -> Launcher path
|
||||
done
|
||||
else
|
||||
# 2s. STABLE promotion: NO rebuild — copy the already-tested beta bins so what
|
||||
# ships to stable is byte-for-byte what the community tested.
|
||||
SRC="$OUT/releases/BETA/$TAG"
|
||||
if [ ! -d "$SRC" ] || ! ls "$SRC/"*.bin >/dev/null 2>&1; then
|
||||
# beta_20 + any pre-channel build already lives under releases/TOUCH/<tag>/.
|
||||
if ls "$ARCH/$TAG/"*.bin >/dev/null 2>&1; then
|
||||
SRC="$ARCH/$TAG"; echo "note: $TAG not in releases/BETA — promoting from existing $ARCH/$TAG"
|
||||
else
|
||||
echo "ERROR: no built bins for $TAG (expected $OUT/releases/BETA/$TAG). Cut the beta first."; exit 1
|
||||
fi
|
||||
fi
|
||||
for pair in $ENVS; do
|
||||
name="${pair##*:}"
|
||||
cp "$SRC/$name.bin" "$ARCH/$TAG/$name.bin"
|
||||
cp "$SRC/$name-merged.bin" "$ARCH/$TAG/$name-merged.bin"
|
||||
cp "$SRC/$name-merged.bin" "$FEED/$name-merged.bin"
|
||||
cp "$SRC/$name.bin" "$FEED/$name.bin"
|
||||
done
|
||||
echo "promoted $TAG bins $SRC -> $ARCH/$TAG (+ $FEED)"
|
||||
fi
|
||||
|
||||
# 3. Regenerate the update-check listing (the firmware scans the body for the
|
||||
# highest beta_<N>). JSON array of dir names — GitHub-contents shape.
|
||||
python3 - "$REL" > "$REL/index.json" <<'PY'
|
||||
# 3. Regenerate this channel's update-check listing (firmware scans the body for
|
||||
# the highest beta_<N>). JSON array of dir names — GitHub-contents shape.
|
||||
python3 - "$ARCH" > "$ARCH/index.json" <<'PY'
|
||||
import os, sys, json
|
||||
rel = sys.argv[1]
|
||||
betas = sorted(d for d in os.listdir(rel)
|
||||
if d.startswith("beta_") and os.path.isdir(os.path.join(rel, d)))
|
||||
print(json.dumps([{"name": b, "type": "dir"} for b in betas], indent=2))
|
||||
PY
|
||||
echo "listing: $(python3 -c 'import json,sys;print(", ".join(x["name"] for x in json.load(open(sys.argv[1]))))' "$REL/index.json")"
|
||||
echo "listing ($MODE): $(python3 -c 'import json,sys;print(", ".join(x["name"] for x in json.load(open(sys.argv[1]))))' "$ARCH/index.json")"
|
||||
|
||||
# 3b. Web-flasher metadata (version.json + esp-web-tools manifests) — must roll
|
||||
# with the release so the flasher shows the current version + notes, not a
|
||||
# frozen one. Notes come from release-notes/<tag>.txt (one per line; optional).
|
||||
python3 "$ROOT/scripts/build/gen-flasher-meta.py" "$TAG" "$LATEST" "$ROOT/release-notes/$TAG.txt"
|
||||
# 3b. Web-flasher metadata for THIS channel (version.json + manifests -> the channel feed).
|
||||
python3 "$ROOT/scripts/build/gen-flasher-meta.py" "$TAG" "$FEED" "$ROOT/release-notes/$TAG.txt" "$MODE"
|
||||
|
||||
# 3c. Mesh America Configurator provider catalog (apps.meshamerica.com — the web
|
||||
# flasher Cascadia Mesh uses). It is served LIVE from raw.githubusercontent
|
||||
# main, so each release must regenerate it for $TAG and push that ONE file to
|
||||
# main; otherwise the configurator keeps offering the previous beta. It points
|
||||
# at the immutable /releases/TOUCH/$TAG/ bins published below.
|
||||
# (Generator: deploy/gen-meshamerica-catalog.py — reads release-notes/$TAG.txt.)
|
||||
CATALOG="$ROOT/deploy/meshamerica-catalog.json"
|
||||
python3 "$ROOT/deploy/gen-meshamerica-catalog.py" "$TAG" > "$CATALOG.tmp" && mv "$CATALOG.tmp" "$CATALOG"
|
||||
mkdir -p "$OUT/meshamerica" && cp "$CATALOG" "$OUT/meshamerica/catalog.json" # also mirror to the VPS (enables the branded URL once its nginx location is deployed)
|
||||
if git -C "$ROOT" diff --quiet -- "$CATALOG"; then
|
||||
echo "Mesh America catalog already current for $TAG"
|
||||
elif [ "$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null)" = "main" ]; then
|
||||
if git -C "$ROOT" commit -q -m "chore: refresh Mesh America catalog for $TAG" -- "$CATALOG" \
|
||||
&& git -C "$ROOT" push origin HEAD:main; then
|
||||
echo "Mesh America catalog refreshed + pushed to main ($TAG)"
|
||||
# 3c. Mesh America Configurator provider catalog — STABLE only (it is a public
|
||||
# flasher catalog served live from raw.githubusercontent main; test builds
|
||||
# must not bump it). Points at the immutable releases/TOUCH/$TAG bins.
|
||||
if [ "$MODE" = "stable" ]; then
|
||||
CATALOG="$ROOT/deploy/meshamerica-catalog.json"
|
||||
python3 "$ROOT/deploy/gen-meshamerica-catalog.py" "$TAG" > "$CATALOG.tmp" && mv "$CATALOG.tmp" "$CATALOG"
|
||||
mkdir -p "$OUT/meshamerica" && cp "$CATALOG" "$OUT/meshamerica/catalog.json"
|
||||
if git -C "$ROOT" diff --quiet -- "$CATALOG"; then
|
||||
echo "Mesh America catalog already current for $TAG"
|
||||
elif [ "$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null)" = "main" ]; then
|
||||
if git -C "$ROOT" commit -q -m "chore: refresh Mesh America catalog for $TAG" -- "$CATALOG" \
|
||||
&& git -C "$ROOT" push origin HEAD:main; then
|
||||
echo "Mesh America catalog refreshed + pushed to main ($TAG)"
|
||||
else
|
||||
echo "WARN: Mesh America catalog commit/push failed — push deploy/meshamerica-catalog.json to main by hand"
|
||||
fi
|
||||
else
|
||||
echo "WARN: Mesh America catalog commit/push failed — push deploy/meshamerica-catalog.json to main by hand"
|
||||
echo "NOTE: regenerated $CATALOG for $TAG but not on 'main' — commit + push it to main."
|
||||
fi
|
||||
else
|
||||
echo "NOTE: regenerated $CATALOG for $TAG but not on 'main' — commit + push it to main so apps.meshamerica.com updates."
|
||||
fi
|
||||
|
||||
# 4. Publish to the VPS (Cloudflare caches at the edge).
|
||||
if [ -n "$DEST" ]; then
|
||||
rsync -av "$OUT/" "$DEST:$DEST_PATH/"
|
||||
echo "published $TAG -> $DEST:$DEST_PATH"
|
||||
echo "published $TAG ($MODE) -> $DEST:$DEST_PATH"
|
||||
else
|
||||
echo "WADAMESH_VPS not set — built + staged in $REL/$TAG only (no publish)."
|
||||
echo "WADAMESH_VPS not set — built + staged in $ARCH/$TAG + $FEED only (no publish)."
|
||||
fi
|
||||
@@ -22,6 +22,7 @@
|
||||
#endif
|
||||
#ifdef MULTI_TRANSPORT_COMPANION
|
||||
#include <helpers/esp32/MultiTransportCompanionInterface.h>
|
||||
#include "helpers/esp32/MqttBridge.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1365,22 +1366,32 @@ static inline bool isMsgFloodType(uint8_t t) {
|
||||
}
|
||||
|
||||
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
// Live signal for the top-bar icon: SNR of whatever we just heard, with a
|
||||
// timestamp so the UI can dim the icon when the mesh has gone quiet.
|
||||
_ui_sig_snr_q4 = (int8_t)(snr * 4.0f);
|
||||
_ui_sig_rssi = (int8_t)rssi;
|
||||
_ui_sig_ms = millis();
|
||||
// Recent-RX ring for the Monitor app (see UiRxRec): parse the minimal header
|
||||
// (type / route / hops) and push newest-first. Header byte layout is
|
||||
const int8_t snr_q4 = (int8_t)(snr * 4.0f);
|
||||
const uint32_t now_ms = millis();
|
||||
// Parse route + path length (hop count). Header byte layout is
|
||||
// [version:2][payload_type:4][route_type:2]
|
||||
// with 4 transport-code bytes following iff route is a TRANSPORT_* one; the
|
||||
// next byte's low 6 bits are the path length (hop count).
|
||||
// next byte's low 6 bits are the path length. hops == 0 = heard DIRECTLY.
|
||||
uint8_t rt = 0, hops = 0;
|
||||
if (len > 0) {
|
||||
const uint8_t rt = raw[0] & 0x03;
|
||||
const bool xp = (rt == ROUTE_TYPE_TRANSPORT_FLOOD || rt == ROUTE_TYPE_TRANSPORT_DIRECT);
|
||||
const int ps = 1 + (xp ? 4 : 0);
|
||||
const uint8_t hops = (ps < len) ? (uint8_t)(raw[ps] & 0x3F) : 0;
|
||||
uiRxLogPush(_ui_sig_ms, (int8_t)rssi, _ui_sig_snr_q4,
|
||||
rt = raw[0] & 0x03;
|
||||
const bool xp = (rt == ROUTE_TYPE_TRANSPORT_FLOOD || rt == ROUTE_TYPE_TRANSPORT_DIRECT);
|
||||
const int ps = 1 + (xp ? 4 : 0);
|
||||
hops = (ps < len) ? (uint8_t)(raw[ps] & 0x3F) : 0;
|
||||
}
|
||||
// Live signal for the top-bar icon: ONLY from packets heard DIRECTLY (0-hop), so
|
||||
// the reading reflects a real direct-neighbour RF link, not the SNR of a repeater
|
||||
// relaying multi-hop traffic. The signal probe sends a zero-hop advert (it never
|
||||
// floods), so this is fed passively by directly-heard neighbours.
|
||||
if (len > 0 && hops == 0) {
|
||||
_ui_sig_snr_q4 = snr_q4;
|
||||
_ui_sig_rssi = (int8_t)rssi;
|
||||
_ui_sig_ms = now_ms;
|
||||
}
|
||||
// Recent-RX ring for the Monitor app (see UiRxRec): log THIS packet's actual
|
||||
// SNR/RSSI regardless of hop count, newest-first.
|
||||
if (len > 0) {
|
||||
uiRxLogPush(now_ms, (int8_t)rssi, snr_q4,
|
||||
(uint8_t)((raw[0] >> 2) & 0x0F), rt, hops,
|
||||
(uint8_t)(len > 255 ? 255 : len));
|
||||
}
|
||||
@@ -2015,6 +2026,9 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
|
||||
const char *text) {
|
||||
markConnectionActive(from); // in case this is from a server, and we have a connection
|
||||
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text);
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
mqtt_bridge.publishDM(from.name, from.id.pub_key, pkt->getSNR(), pkt->path_len, sender_timestamp, text);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
@@ -2029,6 +2043,9 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin
|
||||
// from.sync_since change needs to be persisted
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
queueMessage(from, TXT_TYPE_SIGNED_PLAIN, pkt, sender_timestamp, sender_prefix, 4, text);
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
mqtt_bridge.publishDM(from.name, from.id.pub_key, pkt->getSNR(), pkt->path_len, sender_timestamp, text);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
|
||||
@@ -2073,6 +2090,16 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
|
||||
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
|
||||
_serial->writeFrameToAll(frame, 1);
|
||||
}
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
{
|
||||
const char* _ch_name = "";
|
||||
ChannelDetails _cd{};
|
||||
if (getChannel(channel_idx, _cd)) _ch_name = _cd.name;
|
||||
mqtt_bridge.publishChannel(channel_idx, _ch_name, pkt->getSNR(),
|
||||
pkt->isRouteFlood() ? pkt->path_len : 0,
|
||||
timestamp, text);
|
||||
}
|
||||
#endif
|
||||
#ifdef DISPLAY_CLASS
|
||||
// Get the channel name from the channel index
|
||||
const char *channel_name = "Unknown";
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
#include "MqttBridge.h"
|
||||
#include <Preferences.h>
|
||||
#include <WiFi.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_random.h"
|
||||
#include "mbedtls/gcm.h"
|
||||
#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).
|
||||
void MqttBridge::loadConfig() {
|
||||
_enabled = false; _pubDm = false; _pubChannel = true;
|
||||
_host[0] = _user[0] = _pwd[0] = _psk[0] = '\0';
|
||||
_port = 1883;
|
||||
|
||||
Preferences p;
|
||||
if (p.begin("mqtt", true)) {
|
||||
_enabled = p.getBool("en", false);
|
||||
_port = (uint16_t)p.getUInt("port", 1883);
|
||||
_pubDm = p.getBool("dm", false);
|
||||
_pubChannel = p.getBool("ch", true);
|
||||
if (p.isKey("host")) p.getString("host", _host, sizeof(_host));
|
||||
if (p.isKey("user")) p.getString("user", _user, sizeof(_user));
|
||||
if (p.isKey("pwd")) p.getString("pwd", _pwd, sizeof(_pwd));
|
||||
if (p.isKey("psk")) p.getString("psk", _psk, sizeof(_psk));
|
||||
p.end();
|
||||
}
|
||||
deriveKey();
|
||||
if (!_enabled || _host[0] == '\0') _enabled = false;
|
||||
}
|
||||
|
||||
// Derive the AES key from the passphrase. mbedtls_md() (generic digest) is used
|
||||
// rather than mbedtls_sha256(), whose _ret suffix differs across mbedTLS 2.x/3.x.
|
||||
void MqttBridge::deriveKey() {
|
||||
_encOn = (_psk[0] != '\0');
|
||||
if (!_encOn) { memset(_key, 0, sizeof(_key)); return; }
|
||||
const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||
if (!info || mbedtls_md(info, (const unsigned char*)_psk, strlen(_psk), _key) != 0) {
|
||||
memset(_key, 0, sizeof(_key));
|
||||
_encOn = false; // can't derive → fail closed to plaintext-disabled publishing
|
||||
}
|
||||
}
|
||||
|
||||
void MqttBridge::begin(const char* nodeHex) {
|
||||
strncpy(_nodeHex, nodeHex, sizeof(_nodeHex) - 1);
|
||||
_nodeHex[sizeof(_nodeHex) - 1] = '\0';
|
||||
|
||||
loadConfig();
|
||||
if (!_enabled) return;
|
||||
|
||||
_mqtt.setServer(_host, _port);
|
||||
_mqtt.setKeepAlive(60);
|
||||
_mqtt.setSocketTimeout(2); // bound connect/read — a dead broker must not stall loop()
|
||||
_mqtt.setBufferSize(_encOn ? 1024 : 512); // sealed base64 needs the bigger buffer
|
||||
Serial.printf("[MQTT] configured -> %s:%u dm=%d ch=%d enc=%d\n",
|
||||
_host, _port, (int)_pubDm, (int)_pubChannel, (int)_encOn);
|
||||
}
|
||||
|
||||
bool MqttBridge::reconnect() {
|
||||
if (!_enabled || WiFi.status() != WL_CONNECTED) return false;
|
||||
|
||||
char clientId[32], lwtTopic[80];
|
||||
snprintf(clientId, sizeof(clientId), "wadamesh-%s", _nodeHex);
|
||||
snprintf(lwtTopic, sizeof(lwtTopic), "wadamesh/%s/status", _nodeHex);
|
||||
|
||||
bool ok = _user[0]
|
||||
? _mqtt.connect(clientId, _user, _pwd, lwtTopic, 0, true, "offline")
|
||||
: _mqtt.connect(clientId, nullptr, nullptr, lwtTopic, 0, true, "offline");
|
||||
|
||||
if (ok) {
|
||||
_mqtt.publish(lwtTopic, "online", true);
|
||||
Serial.printf("[MQTT] connected as %s\n", clientId);
|
||||
} else {
|
||||
Serial.printf("[MQTT] connect failed, rc=%d\n", _mqtt.state());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void MqttBridge::loop() {
|
||||
if (!_enabled) return;
|
||||
if (!_mqtt.connected()) {
|
||||
uint32_t now = millis();
|
||||
if ((uint32_t)(now - _lastReconnectMs) >= RECONNECT_INTERVAL_MS) {
|
||||
_lastReconnectMs = now;
|
||||
reconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_mqtt.loop();
|
||||
}
|
||||
|
||||
// AES-256-GCM seal: out = base64( nonce[12] || ciphertext || tag[16] ). Single-task
|
||||
// use (mesh callbacks + loop() run on the same Arduino task), so the stack buffers
|
||||
// are safe. Returns false on any crypto/size failure (caller then publishes nothing).
|
||||
bool MqttBridge::sealToB64(const char* plain, char* out, size_t outCap) {
|
||||
size_t plen = strlen(plain);
|
||||
if (plen > 480) return false; // matches the json[] producers below
|
||||
|
||||
uint8_t blob[12 + 480 + 16];
|
||||
esp_fill_random(blob, 12); // fresh nonce per message
|
||||
uint8_t tag[16];
|
||||
|
||||
mbedtls_gcm_context gcm;
|
||||
mbedtls_gcm_init(&gcm);
|
||||
int rc = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, _key, 256);
|
||||
if (rc == 0) {
|
||||
rc = mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, plen,
|
||||
blob, 12, nullptr, 0,
|
||||
(const unsigned char*)plain, blob + 12,
|
||||
16, tag);
|
||||
}
|
||||
mbedtls_gcm_free(&gcm);
|
||||
if (rc != 0) return false;
|
||||
|
||||
memcpy(blob + 12 + plen, tag, 16);
|
||||
size_t blobLen = 12 + plen + 16, olen = 0;
|
||||
return mbedtls_base64_encode((unsigned char*)out, outCap, &olen, blob, blobLen) == 0;
|
||||
}
|
||||
|
||||
void MqttBridge::pub(const char* subtopic, const char* json) {
|
||||
if (!_enabled || !_mqtt.connected()) return;
|
||||
char topic[80];
|
||||
snprintf(topic, sizeof(topic), "wadamesh/%s/%s", _nodeHex, subtopic);
|
||||
if (_encOn) {
|
||||
char b64[720]; // base64 of the ~508-byte sealed blob
|
||||
if (sealToB64(json, b64, sizeof(b64))) _mqtt.publish(topic, b64);
|
||||
} else {
|
||||
_mqtt.publish(topic, json);
|
||||
}
|
||||
}
|
||||
|
||||
void MqttBridge::escapeJson(const char* src, char* dst, size_t dstLen) {
|
||||
static const char* hexd = "0123456789abcdef";
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; src[i] && j + 7 < dstLen; ++i) { // +7: worst case is \u00XX (6) + NUL
|
||||
unsigned char c = (unsigned char)src[i];
|
||||
if (c == '"' || c == '\\') { dst[j++] = '\\'; dst[j++] = (char)c; }
|
||||
else if (c == '\n') { dst[j++] = '\\'; dst[j++] = 'n'; }
|
||||
else if (c == '\r') { dst[j++] = '\\'; dst[j++] = 'r'; }
|
||||
else if (c == '\t') { dst[j++] = '\\'; dst[j++] = 't'; }
|
||||
else if (c < 0x20) { // other control chars → \u00XX (valid JSON)
|
||||
dst[j++] = '\\'; dst[j++] = 'u'; dst[j++] = '0'; dst[j++] = '0';
|
||||
dst[j++] = hexd[(c >> 4) & 0xF]; dst[j++] = hexd[c & 0xF];
|
||||
} else { dst[j++] = (char)c; }
|
||||
}
|
||||
dst[j] = '\0';
|
||||
}
|
||||
|
||||
void MqttBridge::publishDM(const char* senderName, const uint8_t* senderKey32,
|
||||
float snr, uint8_t hops, uint32_t ts, const char* text) {
|
||||
if (!_enabled || !_pubDm || !_mqtt.connected()) return; // DMs are opt-in
|
||||
char keyHex[13] = {};
|
||||
for (int i = 0; i < 6; ++i) snprintf(keyHex + i * 2, 3, "%02x", senderKey32[i]);
|
||||
|
||||
char safeName[48], safeText[300];
|
||||
escapeJson(senderName, safeName, sizeof(safeName));
|
||||
escapeJson(text, safeText, sizeof(safeText));
|
||||
|
||||
char json[480];
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"sender\":\"%s\",\"key\":\"%s\",\"snr\":%.1f,\"hops\":%u,\"ts\":%lu,\"text\":\"%s\"}",
|
||||
safeName, keyHex, snr, (unsigned)hops, (unsigned long)ts, safeText);
|
||||
pub("msg/dm", json);
|
||||
}
|
||||
|
||||
void MqttBridge::publishChannel(int channelIdx, const char* channelName,
|
||||
float snr, uint8_t hops, uint32_t ts, const char* text) {
|
||||
if (!_enabled || !_pubChannel || !_mqtt.connected()) return; // channel publish toggle
|
||||
char safeName[48], safeText[300];
|
||||
escapeJson(channelName, safeName, sizeof(safeName));
|
||||
escapeJson(text, safeText, sizeof(safeText));
|
||||
|
||||
char json[480];
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"channel\":\"%s\",\"ch_idx\":%d,\"snr\":%.1f,\"hops\":%u,\"ts\":%lu,\"text\":\"%s\"}",
|
||||
safeName, channelIdx, snr, (unsigned)hops, (unsigned long)ts, safeText);
|
||||
pub("msg/ch", json);
|
||||
}
|
||||
|
||||
void MqttBridge::saveConfig(const char* host, uint16_t port,
|
||||
const char* user, const char* pwd,
|
||||
bool pubDm, bool pubChannel, const char* psk, bool enable) {
|
||||
Preferences p;
|
||||
if (!p.begin("mqtt", false)) return;
|
||||
p.putBool("en", enable);
|
||||
p.putString("host", host);
|
||||
p.putUInt("port", port);
|
||||
p.putString("user", user);
|
||||
p.putString("pwd", pwd);
|
||||
p.putBool("dm", pubDm);
|
||||
p.putBool("ch", pubChannel);
|
||||
p.putString("psk", psk);
|
||||
p.end();
|
||||
}
|
||||
|
||||
void MqttBridge::reloadConfig() {
|
||||
if (_mqtt.connected()) _mqtt.disconnect();
|
||||
loadConfig();
|
||||
if (!_enabled) { Serial.println("[MQTT] disabled"); return; }
|
||||
_mqtt.setServer(_host, _port);
|
||||
_mqtt.setKeepAlive(60);
|
||||
_mqtt.setSocketTimeout(2);
|
||||
_mqtt.setBufferSize(_encOn ? 1024 : 512);
|
||||
_lastReconnectMs = 0; // reconnect on next loop() tick
|
||||
Serial.printf("[MQTT] reloaded -> %s:%u en=%d dm=%d ch=%d enc=%d\n",
|
||||
_host, _port, (int)_enabled, (int)_pubDm, (int)_pubChannel, (int)_encOn);
|
||||
}
|
||||
|
||||
#endif // ESP32 && MULTI_TRANSPORT_COMPANION
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <WiFiClient.h>
|
||||
|
||||
// Publishes received mesh messages to an MQTT broker over the existing WiFi link.
|
||||
//
|
||||
// PRIVACY MODEL (mirrors how Meshtastic's MQTT module guards content):
|
||||
// - Master enable is opt-in (default off).
|
||||
// - Channel messages publish by default; DIRECT MESSAGES are off by default and
|
||||
// must be explicitly enabled — they are private 1:1 traffic, and may be from
|
||||
// someone else who never consented to being bridged.
|
||||
// - If an encryption key (PSK) is set, every JSON payload is sealed with
|
||||
// AES-256-GCM (fresh random 12-byte nonce per message) before it leaves the
|
||||
// device, so the broker only ever sees opaque base64. TLS is deliberately NOT
|
||||
// used: the mbedTLS handshake (~30 KB heap) does not fit the ESP32-S3 budget
|
||||
// alongside Wi-Fi + BLE + LVGL (same reason map tiles are HTTP-only). App-layer
|
||||
// GCM is the lightweight, hardware-accelerated equivalent.
|
||||
//
|
||||
// Topics (QoS 0, retained where noted):
|
||||
// wadamesh/{node_hex}/msg/dm — direct / signed messages (only if DM publish ON)
|
||||
// wadamesh/{node_hex}/msg/ch — channel messages (only if channel publish ON)
|
||||
// wadamesh/{node_hex}/status — LWT: "online" on connect, "offline" on drop (retained)
|
||||
//
|
||||
// ENCRYPTED PAYLOAD WIRE FORMAT (when a PSK is set):
|
||||
// base64( nonce[12] || ciphertext[n] || tag[16] ), AES-256-GCM,
|
||||
// key = SHA-256(psk). A subscriber (Home Assistant / Node-RED) decrypts with the
|
||||
// same passphrase. With no PSK, the payload is the plain JSON (use a private broker).
|
||||
//
|
||||
// Config persisted in Preferences namespace "mqtt" (file-backed via SdNvsPrefs):
|
||||
// en bool · host str · port u16 · user str · pwd str · dm bool · ch bool · psk str
|
||||
//
|
||||
// Call begin() once after the_mesh.begin() and SdNvsPrefs::useFile().
|
||||
// Call loop() every iteration of the Arduino loop().
|
||||
class MqttBridge {
|
||||
public:
|
||||
void begin(const char* nodeHex);
|
||||
void loop();
|
||||
|
||||
void publishDM(const char* senderName, const uint8_t* senderKey32,
|
||||
float snr, uint8_t hops, uint32_t ts, const char* text);
|
||||
void publishChannel(int channelIdx, const char* channelName,
|
||||
float snr, uint8_t hops, uint32_t ts, const char* text);
|
||||
|
||||
bool enabled() const { return _enabled; }
|
||||
|
||||
// Persist config (called from Settings UI save).
|
||||
static void saveConfig(const char* host, uint16_t port,
|
||||
const char* user, const char* pwd,
|
||||
bool pubDm, bool pubChannel, const char* psk, bool enable);
|
||||
// Re-read config from Preferences and reconnect (call after saveConfig).
|
||||
void reloadConfig();
|
||||
|
||||
private:
|
||||
WiFiClient _wc;
|
||||
PubSubClient _mqtt{_wc};
|
||||
char _nodeHex[13] = {}; // 6-byte key → 12 hex chars + '\0'
|
||||
bool _enabled = false;
|
||||
bool _pubDm = false; // DMs off by default (private 1:1 traffic)
|
||||
bool _pubChannel = true; // channel messages on by default
|
||||
char _host[64] = {};
|
||||
char _user[32] = {};
|
||||
char _pwd[32] = {};
|
||||
char _psk[33] = {}; // passphrase; empty = no payload encryption
|
||||
uint8_t _key[32] = {}; // SHA-256(psk), valid when _encOn
|
||||
bool _encOn = false;
|
||||
uint16_t _port = 1883;
|
||||
uint32_t _lastReconnectMs = 0;
|
||||
|
||||
static const uint32_t RECONNECT_INTERVAL_MS = 15000;
|
||||
|
||||
void loadConfig(); // shared by begin() / reloadConfig()
|
||||
void deriveKey(); // _key = SHA-256(_psk); sets _encOn
|
||||
bool reconnect();
|
||||
void pub(const char* subtopic, const char* json); // seals if _encOn
|
||||
bool sealToB64(const char* plain, char* out, size_t outCap);
|
||||
static void escapeJson(const char* src, char* dst, size_t dstLen);
|
||||
};
|
||||
|
||||
extern MqttBridge mqtt_bridge;
|
||||
|
||||
#endif // ESP32 && MULTI_TRANSPORT_COMPANION
|
||||
@@ -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 = 25; // 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)
|
||||
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)
|
||||
|
||||
// Defaults (kept identical to the historical per-key defaults).
|
||||
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
|
||||
@@ -95,6 +95,10 @@ struct __attribute__((packed)) TouchCfg {
|
||||
uint8_t tb_nav; // T-Deck trackball: 1=D-pad UI navigation (default), 0=soft cursor — v22 (trailing)
|
||||
uint8_t scope_direct; // 1=tag direct/login/admin floods with the default region scope (opt-in, default 0) — v23 (trailing)
|
||||
uint8_t fem_lna; // Heltec V4.3 high-gain FEM LNA (~17 dB): 1=on, 0=bypass (default) — v25 (trailing)
|
||||
uint8_t msg_flash; // flash keyboard backlight + wake screen on a new message (bool) — v26 (trailing)
|
||||
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)
|
||||
};
|
||||
|
||||
static TouchCfg s_cfg;
|
||||
@@ -160,6 +164,10 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
c.tb_nav = 0; // T-Deck trackball: soft-cursor by default. D-pad UI nav is EXPERIMENTAL (opt-in)
|
||||
c.scope_direct = 0; // OFF: direct/login floods stay unscoped (cross-region safe). Opt-in per issue #64.
|
||||
c.fem_lna = 0; // OFF: V4.3 FEM LNA bypassed (matches the hardware default). Opt-in high-gain RX.
|
||||
c.msg_flash = 0; // OFF: opt-in new-message keyboard/screen flash
|
||||
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.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
|
||||
@@ -235,6 +243,9 @@ static void cfgLoadOrMigrate() {
|
||||
if (s_cfg.ver < 24) s_cfg.tb_nav = 0;
|
||||
// v25: new trailing field — V4.3 FEM LNA OFF on existing installs (matches hardware default).
|
||||
if (s_cfg.ver < 25) s_cfg.fem_lna = 0;
|
||||
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;
|
||||
s_cfg.ver = TOUCH_CFG_VER;
|
||||
s_cfg.magic = TOUCH_CFG_MAGIC;
|
||||
cfgFlush(); // rewrite with new fields defaulted-in
|
||||
@@ -956,6 +967,48 @@ bool touchPrefsSetFemLna(bool on) {
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
bool touchPrefsGetMsgFlash() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.msg_flash != 0;
|
||||
}
|
||||
bool touchPrefsSetMsgFlash(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.msg_flash = 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() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.flood_adv_hrs;
|
||||
}
|
||||
bool touchPrefsSetFloodAdvHrs(uint16_t hrs) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
if (hrs > 168) hrs = 168;
|
||||
s_cfg.flood_adv_hrs = (uint8_t)hrs;
|
||||
return cfgFlush();
|
||||
}
|
||||
uint16_t touchPrefsGetLocalAdvMin() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.local_adv_min;
|
||||
}
|
||||
bool touchPrefsSetLocalAdvMin(uint16_t mins) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
if (mins != 0) { if (mins < 60) mins = 60; if (mins > 240) mins = 240; }
|
||||
s_cfg.local_adv_min = mins;
|
||||
return cfgFlush();
|
||||
}
|
||||
bool touchPrefsGetBetaUpdates() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.beta_updates != 0;
|
||||
}
|
||||
bool touchPrefsSetBetaUpdates(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.beta_updates = on ? 1 : 0;
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
uint8_t touchPrefsGetNavKey(int tab) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
if (tab < 0 || tab >= 5) return 0;
|
||||
|
||||
@@ -143,6 +143,24 @@ bool touchPrefsSetScopeDirect(bool on);
|
||||
bool touchPrefsGetFemLna();
|
||||
bool touchPrefsSetFemLna(bool on);
|
||||
|
||||
/* New-message notify flash (T-Deck): briefly light the keyboard backlight + wake the screen when a
|
||||
* message arrives, like the Tanmatsu envelope LED. Opt-in (default off). */
|
||||
bool touchPrefsGetMsgFlash();
|
||||
bool touchPrefsSetMsgFlash(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();
|
||||
bool touchPrefsSetFloodAdvHrs(uint16_t hrs);
|
||||
uint16_t touchPrefsGetLocalAdvMin();
|
||||
bool touchPrefsSetLocalAdvMin(uint16_t mins);
|
||||
|
||||
/* OTA update channel: opt-in to test/beta firmware. When on, the on-device update
|
||||
* check AND the Install-update download both point at the beta channel (releases/BETA)
|
||||
* instead of the stable channel (releases/TOUCH). Default off (stable). */
|
||||
bool touchPrefsGetBetaUpdates();
|
||||
bool touchPrefsSetBetaUpdates(bool on);
|
||||
|
||||
/* Keyboard-nav tab hotkeys: the ASCII key that jumps to each main tab while
|
||||
* keyboard navigation is on. `tab` is the tab index 0..4 = chat / contacts / home
|
||||
* / map / settings. Defaults E/R/T/U/I. Programmable in Settings → Keyboard. */
|
||||
|
||||
@@ -66,6 +66,7 @@ static uint32_t _atoi(const char* sp) {
|
||||
#ifdef ESP32
|
||||
#ifdef MULTI_TRANSPORT_COMPANION
|
||||
#include <helpers/esp32/MultiTransportCompanionInterface.h>
|
||||
#include "helpers/esp32/MqttBridge.h"
|
||||
MultiTransportCompanionInterface serial_interface;
|
||||
#ifndef TCP_PORT
|
||||
#define TCP_PORT 5000
|
||||
@@ -410,6 +411,14 @@ void setup() {
|
||||
);
|
||||
Serial.println("[BOOT] mesh ok");
|
||||
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
{
|
||||
char nodeHex[13] = {};
|
||||
for (int i = 0; i < 6; ++i) snprintf(nodeHex + i * 2, 3, "%02x", the_mesh.self_id.pub_key[i]);
|
||||
mqtt_bridge.begin(nodeHex);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION)
|
||||
wifiConfigBegin();
|
||||
Serial.println("[BOOT] wifiConfig ok");
|
||||
@@ -688,6 +697,9 @@ void loop() {
|
||||
if (!spectrumOwnsRadio())
|
||||
#endif
|
||||
the_mesh.loop();
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
mqtt_bridge.loop();
|
||||
#endif
|
||||
#if defined(GPS_BUF_DEBUG)
|
||||
// Bench diagnostic (build with -DGPS_BUF_DEBUG only; absent in releases): peak GPS UART
|
||||
// backlog accumulated between sensors.loop() drains. A peak above the old 256-byte default
|
||||
|
||||