Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f47d71a05b | ||
|
|
8eb63a7edd | ||
|
|
5623237951 | ||
|
|
b90b8bac14 | ||
|
|
3585b3a1e0 | ||
|
|
a4d3715529 | ||
|
|
ffc501d09d | ||
|
|
61666ac033 |
No files matched your search
@@ -17,19 +17,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_25": {
|
||||
"notes": "The signal probe reads a live signal to your nearest repeater again. It sends the standard mesh node-discovery request that repeaters actually answer (the same one the Ultra and KiekR apps use), so the signal reading updates. Thanks to Tarmo for decoding the exact packet.\nBluetooth companion reliability, especially messages and repeater / room-server admin. The device no longer floods the Bluetooth link with a per-packet log (USB and Wi-Fi companions still get it), so incoming messages reach the app and admin actions like Manage Regions stop timing out. (Issues #46 and #54.)\nContacts show up right away. A node you discover, even while you're on another screen or one the phone app adds over Bluetooth, now appears in your contact list immediately instead of only after you change the sort. Thanks to 1nerdherder for the report. (Issue #73.)\nManual telemetry and status requests now work on the first tap instead of needing a second. Thanks to Dan Vybiral (@Vybo) for the fix. (Issue #81.)\nOn the emoji picker and quick-chat replies, pressing Esc or the red X now closes just the popup instead of the whole chat screen.\nLock screen: you can now unlock the device after the screen wakes from an incoming message.\nNew \"Advertise on boot\" option in the Advertise app (off by default): turn it on to send one advert when the device powers on, so nearby nodes refresh your details after a reflash. (Issue #76.)\nTanmatsu: Wi-Fi map tiles now download and display, cached to the microSD card.\nTanmatsu: contacts and message history now survive a reboot (moved to the microSD card, so keep a card inserted).\nTanmatsu: fixed the crash that stopped the app from booting; popups now fit the screen and scroll, and keyboard navigation stays inside an open popup.\nThis is the new stable build, promoted from the beta_25 test channel. It includes everything from the beta_22, beta_23, beta_24 and beta_25 test builds.",
|
||||
"beta_27": {
|
||||
"notes": "Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.\nDevices installed under the Launcher no longer show the on-device \"Install update\" button. That update path only works on a standalone flash; under the Launcher it failed with \"won't fit: Bad Size Given\". The \"update available\" note stays, but you update through the Launcher instead. Thanks to Keith (KM1USR) for the report.\nKeyboard backlight brightness control on the T-Deck: a live slider plus Off / 25 / 50 / 75 / Max presets in Settings > Keyboard. It dims smoothly (fine-grained at the low end), applies to the on and auto modes and the new-message flash, and survives a reboot. Thanks to Yazutsu (PR #85).\nOn devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). Your existing contacts move over automatically on first boot; your identity and settings stay in internal flash, so the node is unchanged. Keep the card inserted afterwards - the contact list lives on it, and an older firmware won't look there if you ever downgrade.\nThis is the new stable build, promoted from the beta_27 test channel. It includes everything from the beta_26 and beta_27 test builds.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_25/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_27/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_25/wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_27/wadamesh-heltec-v4-tft.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
@@ -49,19 +49,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_25": {
|
||||
"notes": "The signal probe reads a live signal to your nearest repeater again. It sends the standard mesh node-discovery request that repeaters actually answer (the same one the Ultra and KiekR apps use), so the signal reading updates. Thanks to Tarmo for decoding the exact packet.\nBluetooth companion reliability, especially messages and repeater / room-server admin. The device no longer floods the Bluetooth link with a per-packet log (USB and Wi-Fi companions still get it), so incoming messages reach the app and admin actions like Manage Regions stop timing out. (Issues #46 and #54.)\nContacts show up right away. A node you discover, even while you're on another screen or one the phone app adds over Bluetooth, now appears in your contact list immediately instead of only after you change the sort. Thanks to 1nerdherder for the report. (Issue #73.)\nManual telemetry and status requests now work on the first tap instead of needing a second. Thanks to Dan Vybiral (@Vybo) for the fix. (Issue #81.)\nOn the emoji picker and quick-chat replies, pressing Esc or the red X now closes just the popup instead of the whole chat screen.\nLock screen: you can now unlock the device after the screen wakes from an incoming message.\nNew \"Advertise on boot\" option in the Advertise app (off by default): turn it on to send one advert when the device powers on, so nearby nodes refresh your details after a reflash. (Issue #76.)\nTanmatsu: Wi-Fi map tiles now download and display, cached to the microSD card.\nTanmatsu: contacts and message history now survive a reboot (moved to the microSD card, so keep a card inserted).\nTanmatsu: fixed the crash that stopped the app from booting; popups now fit the screen and scroll, and keyboard navigation stays inside an open popup.\nThis is the new stable build, promoted from the beta_25 test channel. It includes everything from the beta_22, beta_23, beta_24 and beta_25 test builds.",
|
||||
"beta_27": {
|
||||
"notes": "Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.\nDevices installed under the Launcher no longer show the on-device \"Install update\" button. That update path only works on a standalone flash; under the Launcher it failed with \"won't fit: Bad Size Given\". The \"update available\" note stays, but you update through the Launcher instead. Thanks to Keith (KM1USR) for the report.\nKeyboard backlight brightness control on the T-Deck: a live slider plus Off / 25 / 50 / 75 / Max presets in Settings > Keyboard. It dims smoothly (fine-grained at the low end), applies to the on and auto modes and the new-message flash, and survives a reboot. Thanks to Yazutsu (PR #85).\nOn devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). Your existing contacts move over automatically on first boot; your identity and settings stay in internal flash, so the node is unchanged. Keep the card inserted afterwards - the contact list lives on it, and an older firmware won't look there if you ever downgrade.\nThis is the new stable build, promoted from the beta_27 test channel. It includes everything from the beta_26 and beta_27 test builds.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_25/wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_27/wadamesh-tdeck-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_25/wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_27/wadamesh-tdeck.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
|
||||
+24
-1
@@ -42,7 +42,15 @@
|
||||
font-family:'JetBrains Mono',monospace; background:#fff; transition:border-color .15s, color .15s}
|
||||
.kicker:hover{border-color:var(--teal); color:var(--ink)}
|
||||
.freeline{margin:0 auto 24px; color:var(--mut); font-size:.92rem}
|
||||
.fab-rail{position:fixed; top:16px; z-index:45; display:flex; flex-direction:column; gap:12px}
|
||||
.fab-rail{position:fixed; top:var(--fab-top,16px); z-index:45; display:flex; flex-direction:column; gap:12px}
|
||||
/* dismissible donate banner */
|
||||
.donate-bar{position:sticky; top:0; z-index:50; display:flex; align-items:center; justify-content:center; gap:7px 16px; flex-wrap:wrap;
|
||||
padding:9px 44px; background:var(--ink); color:#fff; font-size:.9rem; line-height:1.4; text-align:center}
|
||||
.donate-bar a{color:#fff; text-decoration:underline; text-underline-offset:2px}
|
||||
.donate-bar .donate-cta{background:var(--teal); color:#fff; text-decoration:none; padding:5px 14px; border-radius:100px; font-weight:650; white-space:nowrap}
|
||||
.donate-bar .donate-cta:hover{background:var(--cyan)}
|
||||
.donate-bar .x{position:absolute; right:12px; top:50%; transform:translateY(-50%); background:none; border:0; color:#fff; font-size:1.5rem; line-height:1; cursor:pointer; opacity:.75; padding:0 4px}
|
||||
.donate-bar .x:hover{opacity:1}
|
||||
.fab-rail.left{left:16px} .fab-rail.right{right:16px}
|
||||
.fab{width:44px; height:44px; border-radius:50%; display:grid; place-items:center; cursor:pointer;
|
||||
background:#fff; border:1px solid var(--line); color:#46525c; box-shadow:0 10px 26px -14px rgba(21,24,30,.45);
|
||||
@@ -210,6 +218,11 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="donate-bar" id="donateBar">
|
||||
<span>Like the project and want to donate? Help out the <b>MeshCore</b> founders instead. <a href="https://blog.meshcore.io/2026/07/04/help-us-save-meshcore" target="_blank" rel="noopener">Learn more</a></span>
|
||||
<a class="donate-cta" href="https://givealittle.co.nz/cause/help-us-save-meshcore" target="_blank" rel="noopener">Donate</a>
|
||||
<button class="x" id="donateX" type="button" aria-label="Dismiss">×</button>
|
||||
</div>
|
||||
<div id="vanta-bg"></div>
|
||||
|
||||
<!-- floating quick links -->
|
||||
@@ -480,6 +493,16 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
|
||||
try { if (!localStorage.getItem('wm-cookie-ack')) cbar.classList.add('show'); } catch(_) {}
|
||||
document.getElementById('cookieok').addEventListener('click', () => { cbar.classList.remove('show'); try { localStorage.setItem('wm-cookie-ack','1'); } catch(_) {} });
|
||||
|
||||
// ---- donate banner: dismissible; shifts the corner FABs down while shown ----
|
||||
(function(){
|
||||
const bar = document.getElementById('donateBar'); if (!bar) return;
|
||||
function setFabTop(){ document.documentElement.style.setProperty('--fab-top', (getComputedStyle(bar).display === 'none' ? 16 : bar.offsetHeight + 12) + 'px'); }
|
||||
try { if (localStorage.getItem('wm-donate-dismiss') === '1') bar.style.display = 'none'; } catch(_) {}
|
||||
setFabTop();
|
||||
addEventListener('resize', setFabTop);
|
||||
document.getElementById('donateX').addEventListener('click', () => { bar.style.display = 'none'; setFabTop(); try { localStorage.setItem('wm-donate-dismiss','1'); } catch(_) {} });
|
||||
})();
|
||||
|
||||
// Live release version + "what's new" — read from the firmware host so the
|
||||
// flasher always reflects the current build (the release pipeline updates
|
||||
// version.json; no site redeploy needed per release).
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# beta_27 (STABLE — rolls up the beta_26 + beta_27 test builds on top of the previous stable, beta_25.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.)
|
||||
|
||||
# --- Fixed ---
|
||||
Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.
|
||||
Devices installed under the Launcher no longer show the on-device "Install update" button. That update path only works on a standalone flash; under the Launcher it failed with "won't fit: Bad Size Given". The "update available" note stays, but you update through the Launcher instead. Thanks to Keith (KM1USR) for the report.
|
||||
|
||||
# --- New ---
|
||||
Keyboard backlight brightness control on the T-Deck: a live slider plus Off / 25 / 50 / 75 / Max presets in Settings > Keyboard. It dims smoothly (fine-grained at the low end), applies to the on and auto modes and the new-message flash, and survives a reboot. Thanks to Yazutsu (PR #85).
|
||||
|
||||
# --- Improved ---
|
||||
On devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). Your existing contacts move over automatically on first boot; your identity and settings stay in internal flash, so the node is unchanged. Keep the card inserted afterwards - the contact list lives on it, and an older firmware won't look there if you ever downgrade.
|
||||
|
||||
# --- Also ---
|
||||
This is the new stable build, promoted from the beta_27 test channel. It includes everything from the beta_26 and beta_27 test builds.
|
||||
@@ -0,0 +1,13 @@
|
||||
# beta_28 (TEST) — room-server session fix + the wyvern.red feedback batch, on top of stable beta_27.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Fixed ---
|
||||
Room servers no longer go one-way "frozen". The device now keeps its room login session alive in the background and automatically logs back in when the session drops, so posts keep being delivered and new room messages keep arriving. There is also a new "Log in again" option in the room chat's menu, and a clear warning when the room only granted you read-only access (posts would previously just vanish). Thanks to EricMW61 for the report (issue #89).
|
||||
The screen now truly switches off in standby. Before, only the backlight went dark while the panel kept displaying the last image - the actual cause of the LCD ghosting / burn-in some of you noticed. The panel itself now sleeps, and on wake your screen is back instantly. Also: a lit lock screen now always dims on every board, a screen woken by a message re-darkens after 10 seconds, and the lock-screen clock drifts a few pixels to avoid ghosting. Thanks to wyvern.red for pinning this down.
|
||||
The on-device "Install update" now also hides on mixed or older partition layouts where an update could crash the device halfway through (found via a user crash report - thanks!).
|
||||
|
||||
# --- New ---
|
||||
Compact messages (IRC style): a new toggle in chat settings shows one dense line per message instead of bubbles, fitting two to three times more history on screen. Off by default. (wyvern.red's suggestion.)
|
||||
Clearer delivery ticks: an up-arrow means sent but not yet confirmed, a double check means the other side confirmed it, and a failed message now says "tap to resend" - one tap resends it. (wyvern.red)
|
||||
Keyboard navigation: the scroll keys (F/C) now zoom the map, and backspace in an empty message box exits typing mode so the navigation keys work again. (wyvern.red)
|
||||
"Reset path" is now also in the chat's own menu next to Blocked users, so you can force a fresh route without going through Contacts.
|
||||
@@ -0,0 +1,17 @@
|
||||
# beta_29 (TEST) — clock hardening for room servers, BLE repeats-heard fix, deeper SD chat history, one-screen popups. On top of beta_28.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Fixed ---
|
||||
Room logins can no longer be silently locked out by the device clock. Room servers quietly reject anything older than the newest timestamp they have seen from you, so a clock that stepped backwards (a reboot without GPS or phone around, or a sync correcting a fast clock) black-holed every Join with zero feedback. The device now remembers the newest timestamp it has used across reboots so it can never fall behind itself, a Join that gets no reply shows a clear error after 20 seconds (including what the device thinks the time is) instead of spinning forever, and the device warns you when its clock and the room server's clock disagree. Thanks to EricMW61 for the follow-up testing on issue #89.
|
||||
"Repeats heard" works again over Bluetooth. The companion app counts repeats from the per-packet RX log, which beta_23 stopped sending over BLE to fix congestion. The firmware now recognises packets that are repeats of your own messages and lets exactly those through to BLE, so the counter works and the congestion fix stays. Thanks to daryldakine for the clean bisect (issue #94).
|
||||
Messages sent from the phone app through the device now also appear on the device itself, as your own outgoing message in that channel. This worked for direct messages but never for channels.
|
||||
Deleting a chat or channel now sticks. Deleted messages could survive in storage and resurrect the chat later, and removing a channel could miss the underlying channel entry so the thread came right back.
|
||||
Settings pages no longer jump down when opened on keyboard-navigation devices, and the red update dot can no longer steal the selection from the About button.
|
||||
The update check no longer shows "couldn't reach the update server" for six hours after one failed attempt. It now waits for Wi-Fi to actually have an address and retries within a minute.
|
||||
The Tanmatsu About page now shows the real firmware version instead of a stale hardcoded one.
|
||||
|
||||
# --- New ---
|
||||
Much deeper chat history on devices with an SD card: up to 5000 messages instead of 500, with date separators (Today, Yesterday, day names) between messages from different days. Note for testers: if you later downgrade below beta_29, the older build will not read the larger history file and will start with an empty history.
|
||||
Chat and channel rows in the messages tab now use the same compact style as the Contacts tab, fitting more conversations on screen.
|
||||
The chat or channel settings sheet (long-press or the gear) now fits on one screen, laid out like the contact menu, instead of scrolling.
|
||||
Compact chat mode is easier to read: messages alternate between two background tones.
|
||||
@@ -0,0 +1,11 @@
|
||||
# beta_30 (TEST) — the big-mesh sluggishness fix + a much faster Spectrum analyzer. On top of beta_29.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Fixed ---
|
||||
The periodic multi-second freezes on busy meshes are gone. Saving the contact list to storage took up to 3 seconds at ~570 contacts and ran on the UI thread every couple of minutes, which is exactly the trackball and touch hesitation people felt since beta_25. The save now writes the same data in a few large chunks instead of thousands of tiny ones, cutting it to a fraction. Thanks to guinnessbear for the report and Yazutsu for the precise measurements that pinned this down (issue #82).
|
||||
The Contacts tab no longer rebuilds itself every 60 seconds just to refresh the "heard 2h ago" labels - they update in place now. That rebuild cost over a second on large contact lists and could yank the list while you were scrolling.
|
||||
A burst of adverts can no longer queue several back-to-back Contacts rebuilds; they are now coalesced.
|
||||
|
||||
# --- Improved ---
|
||||
The Spectrum analyzer feels live now: the waterfall gets a new line about four times per second instead of once per sweep, the trace advances across the band as it sweeps (like a bench analyzer), and a full sweep is about 20 percent faster.
|
||||
The Spectrum trace also scales itself to the signal now: the chart tracks the live minimum and maximum, jumping out instantly for a strong signal (no more clipping flat at the top) and easing back once it is gone.
|
||||
@@ -136,6 +136,14 @@ public:
|
||||
virtual void onAdminLoginResult(const ContactInfo& contact, bool success, uint8_t perms) {
|
||||
(void)contact; (void)success; (void)perms;
|
||||
}
|
||||
/** The server's own clock, from the timestamp prefix every server RESPONSE
|
||||
* carries (first 4 bytes). Fired on LOGIN_OK so the UI can warn when the
|
||||
* device clock and the server clock disagree — a skewed device clock makes
|
||||
* the server's replay guard silently drop our logins/posts/keep-alives
|
||||
* (issue #89). Default no-op. */
|
||||
virtual void onServerClock(const ContactInfo& contact, uint32_t server_epoch) {
|
||||
(void)contact; (void)server_epoch;
|
||||
}
|
||||
/** A CLI command reply from a repeater / room server / sensor (the
|
||||
* TXT_TYPE_CLI_DATA path). `text` is the decrypted command output text
|
||||
* the server returned. Default no-op; the touch UI overrides to append
|
||||
|
||||
+57
-17
@@ -432,27 +432,67 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
ContactInfo c;
|
||||
uint8_t unused = 0;
|
||||
|
||||
while (host->getContactForSave(idx, c)) {
|
||||
if (filter && !filter(c)) {
|
||||
// Field-by-field writes cost ~12 FS calls per contact — ~7000 tiny writes at
|
||||
// ~570 contacts measured 2.6-3.0 s on SD, freezing the loop/UI for the whole
|
||||
// save (issue #82). Pack the SAME 152-byte records into a heap chunk and
|
||||
// flush in ~5 KB slabs instead: identical file bytes, ~20 large writes, tens
|
||||
// of ms. Falls back to the original per-field path if the alloc fails.
|
||||
static const size_t REC = 152; // 32+32+1+1+1+4+1+4+64+4+4+4
|
||||
static const size_t CHUNK_RECS = 32;
|
||||
uint8_t* buf = (uint8_t*)malloc(REC * CHUNK_RECS);
|
||||
if (buf) {
|
||||
size_t fill = 0;
|
||||
bool ok = true;
|
||||
while (ok && host->getContactForSave(idx, c)) {
|
||||
if (filter && !filter(c)) {
|
||||
idx++; // advance to next contact
|
||||
continue;
|
||||
}
|
||||
uint8_t* p = buf + fill;
|
||||
memcpy(p, c.id.pub_key, 32); p += 32;
|
||||
memcpy(p, (uint8_t *)&c.name, 32); p += 32;
|
||||
*p++ = c.type;
|
||||
*p++ = c.flags;
|
||||
*p++ = unused;
|
||||
memcpy(p, (uint8_t *)&c.sync_since, 4); p += 4;
|
||||
memcpy(p, (uint8_t *)&c.out_path_len, 1); p += 1;
|
||||
memcpy(p, (uint8_t *)&c.last_advert_timestamp, 4); p += 4;
|
||||
memcpy(p, c.out_path, 64); p += 64;
|
||||
memcpy(p, (uint8_t *)&c.lastmod, 4); p += 4;
|
||||
memcpy(p, (uint8_t *)&c.gps_lat, 4); p += 4;
|
||||
memcpy(p, (uint8_t *)&c.gps_lon, 4); p += 4;
|
||||
fill += REC;
|
||||
if (fill == REC * CHUNK_RECS) {
|
||||
ok = (file.write(buf, fill) == fill);
|
||||
fill = 0;
|
||||
}
|
||||
idx++; // advance to next contact
|
||||
continue;
|
||||
}
|
||||
bool success = (file.write(c.id.pub_key, 32) == 32);
|
||||
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
|
||||
success = success && (file.write(&c.type, 1) == 1);
|
||||
success = success && (file.write(&c.flags, 1) == 1);
|
||||
success = success && (file.write(&unused, 1) == 1);
|
||||
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
|
||||
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
|
||||
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
|
||||
success = success && (file.write(c.out_path, 64) == 64);
|
||||
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
|
||||
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
|
||||
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
|
||||
if (ok && fill > 0) file.write(buf, fill);
|
||||
free(buf);
|
||||
} else {
|
||||
while (host->getContactForSave(idx, c)) {
|
||||
if (filter && !filter(c)) {
|
||||
idx++; // advance to next contact
|
||||
continue;
|
||||
}
|
||||
bool success = (file.write(c.id.pub_key, 32) == 32);
|
||||
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
|
||||
success = success && (file.write(&c.type, 1) == 1);
|
||||
success = success && (file.write(&c.flags, 1) == 1);
|
||||
success = success && (file.write(&unused, 1) == 1);
|
||||
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
|
||||
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
|
||||
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
|
||||
success = success && (file.write(c.out_path, 64) == 64);
|
||||
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
|
||||
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
|
||||
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
|
||||
|
||||
if (!success) break; // write failed
|
||||
if (!success) break; // write failed
|
||||
|
||||
idx++; // advance to next contact
|
||||
idx++; // advance to next contact
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
+48
-4
@@ -21,7 +21,9 @@
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#ifdef MULTI_TRANSPORT_COMPANION
|
||||
#include <helpers/esp32/MultiTransportCompanionInterface.h>
|
||||
// QUOTED on purpose: the vendored core lib ships a STALE copy of this header in its
|
||||
// include path (no bleAllowNextRxLog); quotes force the local src/ copy we compile.
|
||||
#include "helpers/esp32/MultiTransportCompanionInterface.h"
|
||||
#include "helpers/esp32/MqttBridge.h"
|
||||
#endif
|
||||
#endif
|
||||
@@ -135,6 +137,15 @@ static int s_companion_ota_pinned_reply_target = -1;
|
||||
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
|
||||
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
|
||||
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
||||
// Our self-chosen room-session keep-alive interval (secs). Room servers zero the
|
||||
// legacy suggested-interval field in LOGIN_OK, so the client picks. 128 is the
|
||||
// value upstream itself recommended while the field was live (CLIENT_KEEP_ALIVE_SECS
|
||||
// 128, disabled 2025-06 rather than tuned) — matching it stays friendly to the
|
||||
// server's TODO'd "throttle keep-alives, evict fast pingers" heuristic. The core
|
||||
// expires the connection at 2.5x (320 s) without an ACK; every received room push
|
||||
// also counts as activity (markConnectionActive), so a busy room barely pings at
|
||||
// all. One 9-byte direct REQ + 5-byte ACK per interval — negligible airtime.
|
||||
#define ROOM_KEEPALIVE_SECS 128
|
||||
|
||||
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
|
||||
|
||||
@@ -1475,7 +1486,14 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
if (poff < len) {
|
||||
// last path hop = the repeater whose re-flood our radio just heard
|
||||
const uint8_t* lasthop = (cnt_p >= 1) ? (raw + poff - hsz_p) : nullptr;
|
||||
uiCountEcho(fnv1a32(raw + poff, len - poff), lasthop, lasthop ? hsz_p : 0);
|
||||
if (uiCountEcho(fnv1a32(raw + poff, len - poff), lasthop, lasthop ? hsz_p : 0)) {
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
// Echo of OUR OWN send: let this one RX-log frame through to BLE too.
|
||||
// The app's "Repeats heard" is computed exactly from these (issue #94)
|
||||
// and a few frames per send can't re-create the #46/#54 BLE flood.
|
||||
MultiTransportCompanionInterface::bleAllowNextRxLog();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2239,6 +2257,16 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16;
|
||||
if (keep_alive_secs > 0) {
|
||||
startConnection(contact, keep_alive_secs);
|
||||
} else if (contact.type == ADV_TYPE_ROOM) {
|
||||
// Modern room servers zero the legacy keep-alive field (simple_room_server:
|
||||
// `reply_data[5] = 0; // Legacy`), so no connection was ever armed and we
|
||||
// never pinged. The server still NEEDS to hear from us: its push loop
|
||||
// abandons a client after 3 unACKed pushes (`push_failures < 3`) and only a
|
||||
// received transmission — a post, or exactly this REQ_TYPE_KEEP_ALIVE —
|
||||
// resets the counter (issue #89: rooms went one-way-dead). Arm our own
|
||||
// interval; checkConnections() in loop() does the rest (9-byte direct REQ,
|
||||
// its ACK also refreshes the server's last_activity for us).
|
||||
startConnection(contact, ROOM_KEEPALIVE_SECS);
|
||||
}
|
||||
out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS;
|
||||
out_frame[i++] = data[6]; // permissions (eg. is_admin)
|
||||
@@ -2266,6 +2294,10 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
// Notify the touch UI so the admin console can flip from "logging in…"
|
||||
// to the prompt (success) or show "wrong password" (fail). Same data
|
||||
// the companion app sees via PUSH_CODE_LOGIN_SUCCESS/_FAIL.
|
||||
// On LOGIN_OK also hand it the server's clock (`tag` = the timestamp prefix
|
||||
// of every server response) so it can warn about device-vs-server skew —
|
||||
// skew is what makes the server's replay guard silently eat us (#89).
|
||||
if (_ui && ok) _ui->onServerClock(contact, tag);
|
||||
if (_ui) _ui->onAdminLoginResult(contact, ok, perms);
|
||||
#endif
|
||||
} else if (len > 4 && // check for status response
|
||||
@@ -2956,12 +2988,13 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
bool success = getChannel(channel_idx, channel);
|
||||
if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, len - i)) {
|
||||
writeOKFrame();
|
||||
#if defined(HAS_TANMATSU)
|
||||
// Mirror the app-sent channel message into the on-device touch UI — the channel-send path
|
||||
// otherwise never shows companion-originated channel sends on screen (the DM path does, via
|
||||
// appSentMsgToContact). NUL-terminate the text in-place first, exactly as the DM path does.
|
||||
// ALL boards — this sat behind HAS_TANMATSU from its beta_17 birth, so T-Deck/V4 users
|
||||
// never saw their own app-sent channel posts on the device. Repeater echoes of our own
|
||||
// flood can't double the bubble: the dispatcher's seen-packet table drops them pre-ingest.
|
||||
if (_ui) { cmd_frame[len] = 0; _ui->appSentMsgToChannel(channel.name, text); }
|
||||
#endif
|
||||
// Sent channel messages are not added to shared history / broadcast: channel_idx and
|
||||
// frame format are device-specific and clients (e.g. HA) without channel support can
|
||||
// misparse or show "text as sender"; also avoids failed-to-sync when versions differ.
|
||||
@@ -4368,6 +4401,17 @@ void MyMesh::checkSerialInterface() {
|
||||
void MyMesh::loop() {
|
||||
BaseChatMesh::loop();
|
||||
|
||||
// Session keep-alives for logged-in servers (rooms). The core pinger sends the
|
||||
// 9-byte REQ_TYPE_KEEP_ALIVE (+ our sync_since) a room server expects; the ACK
|
||||
// back refreshes the server's last_activity for us AND resets its push_failures
|
||||
// counter — without it the server abandons a client after any 3 unacknowledged
|
||||
// post pushes and never pushes again (simple_room_server: `push_failures < 3`
|
||||
// gate in its round-robin push loop), which reads as a one-way-frozen room
|
||||
// (issue #89). Upstream left this call commented out ("deprecate the
|
||||
// Connections stuff"); the on-device room UI needs it. Cheap: scans 16 slots,
|
||||
// transmits only when a connection is armed and due (see the room login branch).
|
||||
checkConnections();
|
||||
|
||||
if (_cli_rescue) {
|
||||
checkCLIRescueCmd();
|
||||
} else {
|
||||
|
||||
+29
-2
@@ -380,6 +380,29 @@ public:
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Is a keep-alive session to this server currently armed and alive?
|
||||
* (Room chat UI: the core frees the slot 2.5x the interval after the last
|
||||
* ACK/activity, so false = the session needs a re-login.) */
|
||||
bool uiHasConnectionTo(const uint8_t pub_key[32]) { return hasConnectionTo(pub_key); }
|
||||
|
||||
/** Re-login to a room server WITHOUT a password. For a client already in the
|
||||
* room's ACL a blank login replies LOGIN_OK with the stored permissions and
|
||||
* skips the login replay guard (simple_room_server onAnonDataRecv, blank-
|
||||
* password branch) — and when it arrives flooded the server also resets its
|
||||
* stale out_path to us. The blank login itself resets NOTHING else server-side
|
||||
* (not push_failures, not last_activity); the actual heal is the chain it
|
||||
* starts: LOGIN_OK -> onContactResponse arms the room keep-alive -> the first
|
||||
* REQ_TYPE_KEEP_ALIVE refreshes the server's last_activity and clears its
|
||||
* push-abandon counter, so pushes resume (issue #89: self-heal + the chat
|
||||
* sheet's "Log in again"). NOTE it cannot recover a server that REBOOTED
|
||||
* (non-admin ACL entries aren't persisted there) — that needs a passworded
|
||||
* Join from the Contacts sheet. */
|
||||
int uiRoomRelogin(const uint8_t pub_key[32]) {
|
||||
ContactInfo* c = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||
if (!c || c->type != ADV_TYPE_ROOM) return MSG_SEND_FAILED;
|
||||
return uiSendAdminLogin(*c, "");
|
||||
}
|
||||
|
||||
/** Send a CLI command line to a previously-logged-in repeater / room
|
||||
* server. The reply lands in AbstractUITask::onAdminCommandReply via
|
||||
* queueMessage when the server returns a TXT_TYPE_CLI_DATA frame.
|
||||
@@ -564,7 +587,10 @@ public:
|
||||
/** Count one echo of fingerprint `fp` (called from logRxRaw on a match). `hop`
|
||||
* (optional) is the re-flooding repeater's hash — the echo's last path hop —
|
||||
* recorded deduped + bounded so the sent-message Info can name the repeaters. */
|
||||
void uiCountEcho(uint32_t fp, const uint8_t* hop = nullptr, uint8_t hop_sz = 0) {
|
||||
/** Returns true when `fp` matched one of OUR recent sends (i.e. this RX is an
|
||||
* echo of our own flood) — logRxRaw uses that to let the frame through to BLE
|
||||
* for the app's "Repeats heard" (issue #94). */
|
||||
bool uiCountEcho(uint32_t fp, const uint8_t* hop = nullptr, uint8_t hop_sz = 0) {
|
||||
for (int i = 0; i < UI_ECHO_SLOTS; i++)
|
||||
if (_echo_fp[i] == fp) {
|
||||
if (_echo_rep[i] < 255) _echo_rep[i]++;
|
||||
@@ -579,8 +605,9 @@ public:
|
||||
_echo_hop_n[i]++;
|
||||
}
|
||||
}
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** True once if a repeat was counted since the last call — the UI uses this
|
||||
* to refresh the chat so the bubble's repeat tag updates live. */
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
|
||||
// Monotonic clock floor for outgoing protocol timestamps (issue #89 follow-up).
|
||||
//
|
||||
// MeshCore servers keep a per-client "newest timestamp seen" high-water mark and
|
||||
// silently drop anything at-or-below it (replay guard) — and since beta_28 our
|
||||
// room keep-alives refresh that mark every ~2 minutes. A device clock that steps
|
||||
// BACKWARD (reboot without GPS/phone around, or a sync correcting a fast clock)
|
||||
// therefore black-holes every login/post/keep-alive with zero feedback. None of
|
||||
// our boards has a battery-backed RTC chip, so a power cycle without a time
|
||||
// source is exactly such a step.
|
||||
//
|
||||
// getCurrentTime() ratchets: it never returns less than the highest value it has
|
||||
// ever returned — and getCurrentTimeUnique() (which stamps every login, post and
|
||||
// keep-alive in the core) builds directly on it. The ratchet is seeded from a
|
||||
// persisted copy at boot and written back rate-capped (see the UITask wiring +
|
||||
// touchPrefsGet/SetClockFloor), so it survives power loss.
|
||||
//
|
||||
// Two escape hatches keep a WRONG-future clock from sticking forever:
|
||||
// • setCurrentTime() rejects anything below MIN_VALID_EPOCH, whatever the
|
||||
// source — this centrally kills the 1902/1970-class garbage sets (GPS date
|
||||
// bugs, unset system clocks) for every path: GPS NMEA, phone CMD, NTP.
|
||||
// • a set that lands more than TRUSTED_BACK_CAP below the floor pulls the
|
||||
// floor down to it: a >10 min backward correction means the floor itself was
|
||||
// built on a bad clock, and freezing time for hours to bridge it would be
|
||||
// worse than the one-time server re-lock it avoids (which the login skew
|
||||
// warning surfaces to the user anyway).
|
||||
class ClockFloorRTC : public AutoDiscoverRTCClock {
|
||||
uint32_t _floor = 0;
|
||||
public:
|
||||
ClockFloorRTC(mesh::RTCClock& fallback) : AutoDiscoverRTCClock(fallback) {}
|
||||
|
||||
static constexpr uint32_t MIN_VALID_EPOCH = 1715770351UL; // 15 May 2024 — the core's own unset-clock seed
|
||||
static constexpr uint32_t TRUSTED_BACK_CAP = 10UL * 60UL;
|
||||
|
||||
uint32_t getCurrentTime() override {
|
||||
uint32_t t = AutoDiscoverRTCClock::getCurrentTime();
|
||||
if (t < _floor) return _floor;
|
||||
_floor = t;
|
||||
return t;
|
||||
}
|
||||
|
||||
void setCurrentTime(uint32_t time) override {
|
||||
if (time < MIN_VALID_EPOCH) return; // garbage set — ignore, whatever the source
|
||||
AutoDiscoverRTCClock::setCurrentTime(time);
|
||||
if (_floor > time + TRUSTED_BACK_CAP) _floor = time;
|
||||
}
|
||||
|
||||
// Boot-time seed from the persisted floor (only ever raises), and the getter
|
||||
// the persister reads back. Benign u32 races: both cores may touch _floor.
|
||||
void seedFloor(uint32_t persisted) { if (persisted > _floor) _floor = persisted; }
|
||||
uint32_t getFloor() const { return _floor; }
|
||||
};
|
||||
@@ -4,9 +4,12 @@
|
||||
#include <string.h>
|
||||
|
||||
// Companion push code for the per-packet RX log (matches MyMesh.cpp). It is kept OFF
|
||||
// the BLE transport in writeFrameToAll — see the note there (issues #46, #54).
|
||||
// the BLE transport in writeFrameToAll — see the note there (issues #46, #54) — EXCEPT
|
||||
// for one-shot passes armed via bleAllowNextRxLog() (echoes of our own sends, #94).
|
||||
#define PUSH_CODE_LOG_RX_DATA 0x88
|
||||
|
||||
bool MultiTransportCompanionInterface::s_ble_rxlog_once = false;
|
||||
|
||||
MultiTransportCompanionInterface::MultiTransportCompanionInterface()
|
||||
: _tcp_port(0), _ws_port(0), _tcp_started(false), _ws_started(false), _tcp_enabled(true), _isEnabled(false), _broadcast(false), _last_reply_target(REPLY_TARGET_USB), _ota_tcp_suspended(false), _ota_ws_suspended(false), _ota_ws_listen_paused(false)
|
||||
#ifdef BLE_PIN_CODE
|
||||
@@ -334,10 +337,16 @@ size_t MultiTransportCompanionInterface::writeFrameToAll(const uint8_t src[], si
|
||||
all_ok = false;
|
||||
#ifdef BLE_PIN_CODE
|
||||
// The per-packet RX log floods BLE's ~16 frames/sec budget on a busy mesh, starving
|
||||
// the frames that matter — chat messages + admin responses (issues #46, #54). The
|
||||
// MeshCore app doesn't consume the RX log, so keep it OFF BLE; USB/TCP/WS (far more
|
||||
// bandwidth) still receive it for the on-device Monitor app / diagnostics.
|
||||
const bool skip_ble = (len > 0 && src[0] == PUSH_CODE_LOG_RX_DATA);
|
||||
// the frames that matter — chat messages + admin responses (issues #46, #54). So it
|
||||
// is kept OFF BLE (USB/TCP/WS have the bandwidth) — EXCEPT when MyMesh::logRxRaw
|
||||
// armed a one-shot pass because this frame is an echo of OUR OWN flood send: the
|
||||
// app's "Repeats heard" is computed exactly from those (it broke on BLE when the
|
||||
// blanket skip landed in beta_23 — issue #94), and a few echoes per send are
|
||||
// nowhere near the flood that caused #46/#54.
|
||||
const bool rxlog = (len > 0 && src[0] == PUSH_CODE_LOG_RX_DATA);
|
||||
const bool ble_pass = rxlog && s_ble_rxlog_once;
|
||||
if (rxlog) s_ble_rxlog_once = false; // consume the one-shot either way
|
||||
const bool skip_ble = rxlog && !ble_pass;
|
||||
if (!skip_ble && _ble_begun && _ble_enabled && _ble.isConnected() && _ble.writeFrame(src, len) != len)
|
||||
all_ok = false;
|
||||
#endif
|
||||
|
||||
@@ -64,6 +64,13 @@ public:
|
||||
bool isWriteBusy() const override;
|
||||
size_t writeFrame(const uint8_t src[], size_t len) override;
|
||||
size_t writeFrameToAll(const uint8_t src[], size_t len) override;
|
||||
/** One-shot: let the NEXT RX-log frame (PUSH_CODE_LOG_RX_DATA) through to BLE.
|
||||
* RX logs are normally kept OFF BLE (#46/#54 congestion), but the app's
|
||||
* "Repeats heard" is computed from them — MyMesh::logRxRaw calls this right
|
||||
* before writeFrameToAll when the frame is an echo of OUR OWN send, so BLE
|
||||
* gets exactly the few frames the app needs and none of the flood (#94).
|
||||
* Same-thread set-then-consume (both in the mesh loop), so no atomics. */
|
||||
static void bleAllowNextRxLog() { s_ble_rxlog_once = true; }
|
||||
bool companionUnsolicitedPushesBroadcastToAll() const override { return _broadcast; }
|
||||
size_t checkRecvFrame(uint8_t dest[]) override;
|
||||
|
||||
@@ -73,6 +80,10 @@ public:
|
||||
int getReplyTarget() const override { return _last_reply_target; }
|
||||
void setReplyTarget(int target) override { _last_reply_target = target; }
|
||||
|
||||
private:
|
||||
static bool s_ble_rxlog_once; // one-shot BLE pass for the next RX-log frame (#94)
|
||||
public:
|
||||
|
||||
private:
|
||||
int _clientIdSlot() const;
|
||||
static const size_t _max_client_id_len = 32;
|
||||
|
||||
@@ -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 = 30; // v2 sig_probe/poll; v3 tz_zone; v4 hide_node_name; v5 map_night/map_zoom; v6 map text/marker visibility; v7 app_grid_large; v8 ui_scale; v9 tb_keypad; v10 sleep_idle; v11 nav_keys; v12 map_zoom_buttons; v13 nav_dir_keys; v14 home_is_drawer; v15 kbd_nav default ON (one-time migrate); v16 nav_scroll_keys; v17 notify_new_contact; v18 kbd_nav OFF by default (reverses v15; T-Deck/V4 only, Tanmatsu stays on); v19 show_sensors_tab; v20 map_show_links; v21 map_style (0=OSM default, 1=OpenTopoMap); v22 tb_nav; v23 scope_direct (opt-in: scope direct/login floods to the region); v24 tb_nav default OFF (experimental); v25 fem_lna (Heltec V4.3 high-gain FEM LNA, opt-in); v26 msg_flash (flash keyboard backlight + wake screen on a new message, opt-in); v27 flood_adv_hrs + local_adv_min (periodic self-advert intervals, the standard MeshCore flood/local advert on a timer); v28 beta_updates (opt-in to test/beta firmware on the OTA update check + install); v29 ui_scale default -> Large/150% (Tanmatsu; bumps the old 100% default, leaves an explicit Large/Huge choice); v30 boot_advert (opt-in one-shot flood self-advert ~6s after boot, all boards, #76)
|
||||
static const uint8_t TOUCH_CFG_VER = 32; // v2 sig_probe/poll; v3 tz_zone; v4 hide_node_name; v5 map_night/map_zoom; v6 map text/marker visibility; v7 app_grid_large; v8 ui_scale; v9 tb_keypad; v10 sleep_idle; v11 nav_keys; v12 map_zoom_buttons; v13 nav_dir_keys; v14 home_is_drawer; v15 kbd_nav default ON (one-time migrate); v16 nav_scroll_keys; v17 notify_new_contact; v18 kbd_nav OFF by default (reverses v15; T-Deck/V4 only, Tanmatsu stays on); v19 show_sensors_tab; v20 map_show_links; v21 map_style (0=OSM default, 1=OpenTopoMap); v22 tb_nav; v23 scope_direct (opt-in: scope direct/login floods to the region); v24 tb_nav default OFF (experimental); v25 fem_lna (Heltec V4.3 high-gain FEM LNA, opt-in); v26 msg_flash (flash keyboard backlight + wake screen on a new message, opt-in); v27 flood_adv_hrs + local_adv_min (periodic self-advert intervals, the standard MeshCore flood/local advert on a timer); v28 beta_updates (opt-in to test/beta firmware on the OTA update check + install); v29 ui_scale default -> Large/150% (Tanmatsu; bumps the old 100% default, leaves an explicit Large/Huge choice); v30 boot_advert (opt-in one-shot flood self-advert ~6s after boot, all boards, #76); v31 compact_chat (opt-in IRC-style dense chat rows instead of bubbles); v32 clock_floor (highest epoch handed out — monotonic send-timestamp floor across reboots, #89)
|
||||
|
||||
// Defaults (kept identical to the historical per-key defaults).
|
||||
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
|
||||
@@ -100,6 +100,8 @@ struct __attribute__((packed)) TouchCfg {
|
||||
uint16_t local_adv_min; // periodic zero-hop self-advert interval in minutes (0 = off) — v27 (trailing)
|
||||
uint8_t beta_updates; // opt-in to test/beta firmware on the OTA check + install (bool) — v28 (trailing)
|
||||
uint8_t boot_advert; // one-shot flood self-advert ~6s after boot (bool, 0=off) — v30 (trailing) — #76
|
||||
uint8_t compact_chat; // IRC-style dense chat rows instead of bubbles (bool, 0=off) — v31 (trailing)
|
||||
uint32_t clock_floor; // highest epoch this device handed out (ClockFloorRTC persistence) — v32 (trailing)
|
||||
};
|
||||
|
||||
static TouchCfg s_cfg;
|
||||
@@ -170,6 +172,8 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
c.local_adv_min = 0; // OFF: no periodic zero-hop self-advert
|
||||
c.beta_updates = 0; // OFF: stable update channel (opt-in to beta/test firmware)
|
||||
c.boot_advert = 0; // OFF: no automatic advert on boot — opt-in (#76)
|
||||
c.compact_chat = 0; // OFF: bubble chat layout (opt-in IRC-style dense rows)
|
||||
c.clock_floor = 0; // no persisted send-timestamp floor yet
|
||||
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
|
||||
@@ -250,6 +254,8 @@ static void cfgLoadOrMigrate() {
|
||||
if (s_cfg.ver < 28) s_cfg.beta_updates = 0;
|
||||
if (s_cfg.ver < 29 && s_cfg.ui_scale == 0) s_cfg.ui_scale = 1; // bump old 100% default -> Large (150%)
|
||||
if (s_cfg.ver < 30) s_cfg.boot_advert = 0; // #76 new trailing field: advert-on-boot off by default
|
||||
if (s_cfg.ver < 31) s_cfg.compact_chat = 0; // new trailing field: compact chat rows off by default
|
||||
if (s_cfg.ver < 32) s_cfg.clock_floor = 0; // new trailing field: no send-timestamp floor persisted yet (#89)
|
||||
s_cfg.ver = TOUCH_CFG_VER;
|
||||
s_cfg.magic = TOUCH_CFG_MAGIC;
|
||||
cfgFlush(); // rewrite with new fields defaulted-in
|
||||
@@ -991,6 +997,29 @@ bool touchPrefsSetBootAdvert(bool on) {
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
bool touchPrefsGetCompactChat() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.compact_chat != 0;
|
||||
}
|
||||
bool touchPrefsSetCompactChat(bool on) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
s_cfg.compact_chat = on ? 1 : 0;
|
||||
return cfgFlush();
|
||||
}
|
||||
|
||||
// Monotonic send-timestamp floor (ClockFloorRTC, issue #89). Written rate-capped
|
||||
// from UITask::loop + on shutdown; only ever grows between resets.
|
||||
uint32_t touchPrefsGetClockFloor() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
return s_cfg.clock_floor;
|
||||
}
|
||||
bool touchPrefsSetClockFloor(uint32_t epoch) {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
if (epoch <= s_cfg.clock_floor) return true; // never regress the persisted floor
|
||||
s_cfg.clock_floor = epoch;
|
||||
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() {
|
||||
|
||||
@@ -153,6 +153,13 @@ bool touchPrefsSetMsgFlash(bool on);
|
||||
bool touchPrefsGetBootAdvert();
|
||||
bool touchPrefsSetBootAdvert(bool on);
|
||||
|
||||
/* Compact chat (IRC-style): one dense "HH:MM name: text" row per message instead of
|
||||
* bubbles, so far more history fits on screen. Opt-in, default off (bubbles). */
|
||||
bool touchPrefsGetCompactChat();
|
||||
bool touchPrefsSetCompactChat(bool on);
|
||||
uint32_t touchPrefsGetClockFloor(); // monotonic send-timestamp floor (#89)
|
||||
bool touchPrefsSetClockFloor(uint32_t epoch); // only ever grows; no-op below current
|
||||
|
||||
/* 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();
|
||||
|
||||
@@ -34,9 +34,9 @@ void tdeckKeyboardSetBacklight(uint8_t level) {
|
||||
if (level != s_bl_desired || !forced) { s_bl_desired = level; s_bl_dirty = true; forced = true; }
|
||||
}
|
||||
|
||||
void tdeckKeyboardPoll() {
|
||||
if (!s_inited) return;
|
||||
if (s_bl_dirty) {
|
||||
void tdeckKeyboardFlushBacklight() {
|
||||
if (!s_inited || !s_bl_dirty) return;
|
||||
{
|
||||
s_bl_dirty = false;
|
||||
// LilyGo T-Keyboard backlight: 2-byte command [0x01, brightness] (0 = off).
|
||||
Wire.beginTransmission(PIN_KB_ADDR);
|
||||
@@ -44,6 +44,11 @@ void tdeckKeyboardPoll() {
|
||||
Wire.write(s_bl_desired); // 0 = off, 1-255 = brightness
|
||||
Wire.endTransmission();
|
||||
}
|
||||
}
|
||||
|
||||
void tdeckKeyboardPoll() {
|
||||
if (!s_inited) return;
|
||||
tdeckKeyboardFlushBacklight();
|
||||
if (Wire.requestFrom((int)PIN_KB_ADDR, 1) != 1) return;
|
||||
uint8_t key = Wire.read();
|
||||
if (key == 0) return; // no key this scan
|
||||
|
||||
@@ -28,4 +28,10 @@ int tdeckKeyboardReadKey();
|
||||
* owns the bus. Only re-sent when the value changes. */
|
||||
void tdeckKeyboardSetBacklight(uint8_t level);
|
||||
|
||||
/** Flush a pending backlight request over I2C NOW. CORE-0 ONLY (touch task).
|
||||
* Cheap when nothing is pending (one flag check); called every poll tick so a
|
||||
* brightness change lands within ~one touch-poll period (~8 ms) instead of
|
||||
* waiting for the next full keyboard scan (~32 ms). */
|
||||
void tdeckKeyboardFlushBacklight();
|
||||
|
||||
#endif
|
||||
@@ -356,6 +356,7 @@ static void touchPollTask(void* arg) {
|
||||
#if defined(HAS_TDECK_KEYBOARD)
|
||||
// Poll the keyboard from THIS task too (same I2C bus) so the two devices
|
||||
// never get hit from two cores at once. ~every 4th tick (~32 ms) is plenty.
|
||||
tdeckKeyboardFlushBacklight(); // backlight requests land within ~one tick (~8 ms); key scan stays /4
|
||||
if (++kb_div >= 4) { kb_div = 0; tdeckKeyboardPoll(); }
|
||||
#endif
|
||||
vTaskDelay(pdMS_TO_TICKS(s_period_ms));
|
||||
|
||||
+1182
-344
File diff suppressed because it is too large.
Load diff
@@ -30,6 +30,10 @@ enum class TouchUiScreen : uint8_t { Home = 0, ChatInbox = 1, Contacts = 2, Sett
|
||||
class UITask : public AbstractUITask {
|
||||
public:
|
||||
static const int MAX_UI_MESSAGES = 500;
|
||||
/** Deep ring for devices whose chat history lives on an SD card (T-Deck with a
|
||||
* card, Tanmatsu SD_MMC): 10x the internal-flash ring. Chosen at begin() into
|
||||
* _ui_msg_cap; PSRAM cost ~5000 * sizeof(UIMessage) ≈ 1.3 MB (of 8 MB). */
|
||||
static const int MAX_UI_MESSAGES_SD = 5000;
|
||||
static const int MAX_UI_THREADS = 48;
|
||||
static const int MAX_THREAD_NAME = 32;
|
||||
static const int MAX_SENDER_NAME = 24;
|
||||
@@ -130,6 +134,10 @@ private:
|
||||
int _msgcount;
|
||||
int _ui_msg_count;
|
||||
int _ui_msg_head;
|
||||
/** Runtime ring capacity: MAX_UI_MESSAGES_SD when history lives on an SD card,
|
||||
* else MAX_UI_MESSAGES. Fixed for the whole boot (chosen before the PSRAM
|
||||
* alloc in begin()); the loader linearizes files written under another cap. */
|
||||
int _ui_msg_cap = MAX_UI_MESSAGES;
|
||||
unsigned long _next_thread_seed;
|
||||
UIMessage* _ui_msgs = nullptr; // ring of recent messages — PSRAM-allocated in begin()
|
||||
UIThread* _ui_threads = nullptr; // thread table — PSRAM-allocated in begin()
|
||||
@@ -315,6 +323,26 @@ public:
|
||||
return _ui_threads[_active_thread_idx].mesh_channel_slot;
|
||||
}
|
||||
int activeThreadIdx() const { return _active_thread_idx; }
|
||||
/** Any thread's pinned mesh-channel slot (-1 when not a channel / out of range).
|
||||
* chatDeleteApply prefers this (name-validated) over a pure name scan so
|
||||
* deleting a channel actually drops its mesh-table entry — otherwise
|
||||
* refreshThreadsFromMesh() recreates the thread from the surviving channel. */
|
||||
int16_t threadMeshChannelSlot(int idx) const {
|
||||
if (idx < 0 || idx >= MAX_UI_THREADS || !_ui_threads[idx].used || !_ui_threads[idx].channel) return -1;
|
||||
return _ui_threads[idx].mesh_channel_slot;
|
||||
}
|
||||
/** Message-ring capacity this boot (500, or 5000 with SD-backed history). */
|
||||
int msgCap() const { return _ui_msg_cap; }
|
||||
/** DM thread's full contact pubkey (32 B) — for the chat sheet's "Reset path".
|
||||
* False for channels, unused slots, or a thread with no pubkey mapping yet. */
|
||||
bool getThreadContactPub(int idx, uint8_t out[32]) const {
|
||||
if (idx < 0 || idx >= MAX_UI_THREADS) return false;
|
||||
if (!_ui_threads[idx].used || _ui_threads[idx].channel) return false;
|
||||
const uint8_t* p = _ui_threads[idx].mesh_contact_pub;
|
||||
uint8_t any = 0;
|
||||
for (int i = 0; i < 32; i++) { out[i] = p[i]; any |= p[i]; }
|
||||
return any != 0;
|
||||
}
|
||||
int threadScroll() const { return _thread_scroll; }
|
||||
void setThreadScroll(int v) { _thread_scroll = v; }
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
@@ -456,6 +484,7 @@ public:
|
||||
void onPingReply(const ContactInfo& contact, const uint8_t* data, size_t len) override;
|
||||
void onTelemetryReply(const ContactInfo& contact, const uint8_t* data, size_t len) override;
|
||||
void onAdminLoginResult(const ContactInfo& contact, bool success, uint8_t perms) override;
|
||||
void onServerClock(const ContactInfo& contact, uint32_t server_epoch) override;
|
||||
void onAdminCommandReply(const ContactInfo& contact, const char* text) override;
|
||||
void onThreadsChanged() override;
|
||||
void loop() override;
|
||||
|
||||
@@ -36,7 +36,16 @@ if [ -f "$BLEDEV_CPP" ] && grep -q 'int rc = ble_gap_read_local_irk(irk);' "$BLE
|
||||
echo "[build.sh] patched arduino BLEDevice.cpp (stub renamed ble_gap_read_local_irk)"
|
||||
fi
|
||||
|
||||
# Firmware tag shown on About + used by the update check. Derived from git so dev
|
||||
# builds are honest ("beta_28-14-gabc123" = 14 commits past beta_28) and a release
|
||||
# build on a fresh beta_N tag bakes exactly "beta_N" — no more hand-bumping the
|
||||
# old hardcoded CMake define (which left every dev flash claiming the last beta).
|
||||
# Passed as a CMake cache var: idf.py reconfigures automatically when it changes.
|
||||
WADA_FW_TAG="$(git describe --tags --match 'beta_*' --always 2>/dev/null || echo dev)"
|
||||
WADA_FW_DATE="$(date '+%-d %b %Y')"
|
||||
|
||||
exec idf.py -B build/tanmatsu \
|
||||
-DDEVICE=tanmatsu \
|
||||
-DSDKCONFIG_DEFAULTS="sdkconfigs/general;sdkconfigs/tanmatsu;sdkconfigs/wadamesh" \
|
||||
-DWADA_FW_TAG="$WADA_FW_TAG" -DWADA_FW_DATE="$WADA_FW_DATE" \
|
||||
-DIDF_TARGET=esp32p4 "$@"
|
||||
@@ -54,11 +54,19 @@ foreach(_d ${WADA_DEFS})
|
||||
idf_build_set_property(COMPILE_OPTIONS "-D${_d}" APPEND)
|
||||
endforeach()
|
||||
|
||||
# Firmware version shown on the About page. The Tanmatsu builds outside the S3 release.sh flow (which
|
||||
# injects these), so set them here or About shows only a generic "development build" with no version.
|
||||
# Quotes are escaped so the compiler sees string literals. TODO: have build.sh pass the real tag/date
|
||||
# per release (e.g. from git describe) instead of hardcoding; bump this each beta for now.
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_RELEASE_TAG=\"beta_25\"" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_BUILD_DATE=\"1 Jul 2026\"" APPEND)
|
||||
# Firmware version shown on the About page + parsed by the update check. build.sh passes
|
||||
# WADA_FW_TAG/-DATE from git describe + date, so dev builds show the real state
|
||||
# ("beta_28-14-gabc123") and a release build on the beta_N tag bakes exactly "beta_N" —
|
||||
# the old hardcoded define left every dev flash claiming the last hand-bumped beta.
|
||||
# firmwareReleaseN() parses the leading "beta_%d", so the -dev suffix keeps the update
|
||||
# check on the right baseline. Quotes escaped so the compiler sees string literals.
|
||||
if(NOT WADA_FW_TAG)
|
||||
set(WADA_FW_TAG "dev")
|
||||
endif()
|
||||
if(NOT WADA_FW_DATE)
|
||||
set(WADA_FW_DATE "")
|
||||
endif()
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_RELEASE_TAG=\"${WADA_FW_TAG}\"" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_BUILD_DATE=\"${WADA_FW_DATE}\"" APPEND)
|
||||
# (the adcAttachPin compat shim is force-included per-component in meshcore + wadamesh_app —
|
||||
# NOT globally, which would feed the C++ header to FreeRTOS's .S assembly files and break them.)
|
||||
@@ -20,7 +20,7 @@ HeltecV4Board board;
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
ClockFloorRTC rtc_clock(fallback_clock);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
|
||||
@@ -10,6 +10,7 @@ extern "C" void set_boot_phase(int phase);
|
||||
#include <HeltecV4Board.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include "../../src/helpers/ClockFloorRTC.h" // monotonic send-timestamp floor (issue #89)
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
@@ -24,7 +25,7 @@ extern "C" void set_boot_phase(int phase);
|
||||
extern HeltecV4Board board;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
extern RADIO_CLASS radio; // raw SX1262 — driven directly by the Spectrum analyzer sweep
|
||||
extern AutoDiscoverRTCClock rtc_clock;
|
||||
extern ClockFloorRTC rtc_clock;
|
||||
extern EnvironmentSensorManager sensors;
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
|
||||
@@ -21,7 +21,7 @@ SPIClass* tdeckSharedSPI() {
|
||||
}
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
ClockFloorRTC rtc_clock(fallback_clock);
|
||||
MicroNMEALocationProvider gps(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors(gps);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <TDeckBoard.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include "../../src/helpers/ClockFloorRTC.h" // monotonic send-timestamp floor (issue #89)
|
||||
#include <helpers/SensorManager.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include <helpers/ui/ST7789LCDDisplay.h>
|
||||
@@ -17,7 +18,7 @@
|
||||
extern TDeckBoard board;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
extern RADIO_CLASS radio; // raw SX1262 — driven directly by the Spectrum analyzer sweep
|
||||
extern AutoDiscoverRTCClock rtc_clock;
|
||||
extern ClockFloorRTC rtc_clock;
|
||||
extern EnvironmentSensorManager sensors;
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
|
||||
@@ -10,7 +10,7 @@ DISPLAY_CLASS display; // global LVGL flush target + boot/shutdown
|
||||
SensorManager sensors; // base (no telemetry yet); EnvironmentSensorManager later
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
ClockFloorRTC rtc_clock(fallback_clock);
|
||||
|
||||
bool radio_init() {
|
||||
fallback_clock.begin();
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
#include <TanmatsuBoard.h>
|
||||
#include "TanmatsuLoraRadio.h"
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include "../../src/helpers/ClockFloorRTC.h" // monotonic send-timestamp floor (issue #89)
|
||||
|
||||
extern TanmatsuBoard board;
|
||||
extern TanmatsuLoraRadio radio_driver;
|
||||
extern AutoDiscoverRTCClock rtc_clock;
|
||||
extern ClockFloorRTC rtc_clock;
|
||||
|
||||
#include <TanmatsuDisplay.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
|
||||
Reference in new issue
Block a user