From c578dcadc89412bdf0180c04b484670963128271 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:44:10 +1000 Subject: [PATCH] T5S3 - fixed touch selector fav contacts bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TDPro - Update firmware build date Contactsscreen.h — five changes: - EPOCH_2026 = 1735689600UL constant added (Jan 1 2026 UTC), used in sort and formatAge. - typeChar replaced by typeStr returning const char*, with "RS" for room servers (previously "S", easily confused with sensors). prefix buffer bumped to [5], all three snprintf calls updated to %s. - Hop display: out_path_len == 0xFF branch now performs a live lookup against the 12 most recently heard advert paths (via getRecentlyHeard). Matches on first 7 bytes of pub_key, extracts hop count with a bph-aware sanity cap (64/bph max) to reject impossible values. Shows "~D" for direct flood neighbours, "~N" for N-hop flood path, "?" if not in the recent-heard cache. Resets to "?" on reboot until each contact re-advertises — intentional, ensures hop count is always fresh. - Sort: _filteredTs now stores contact.lastmod (our local receive time) instead of contact.last_advert_timestamp (sender's claimed time). lastmod values below EPOCH_2026 are stored as 0 so stale repeaters with unsynced clocks and contacts received before our own timesync sink to the bottom of the list. - formatAge rewritten: rejects timestamp == 0, timestamp < EPOCH_2026, and now < timestamp (all show "--" instead of wrapping or displaying garbage). Arithmetic changed from int to uint32_t, eliminating the signed overflow path that produced negative hour values. Age display call site switched from last_advert_timestamp to lastmod, so display self-corrects after a GPS or 4G timesync. --- examples/companion_radio/MyMesh.h | 2 +- examples/companion_radio/main.cpp | 58 ++++++++++++--- .../companion_radio/ui-new/Contactsscreen.h | 73 ++++++++++++------- .../companion_radio/ui-new/Settingsscreen.h | 2 +- variants/lilygo_tdeck_pro/platformio.ini | 9 ++- 5 files changed, 102 insertions(+), 42 deletions(-) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 50a53211..f6e44728 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,7 +8,7 @@ #define FIRMWARE_VER_CODE 11 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "7 April 2026" +#define FIRMWARE_BUILD_DATE "12 April 2026" #endif #ifndef FIRMWARE_VERSION diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 0a3fa68c..36aed589 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1525,24 +1525,60 @@ static void lastHeardToggleContact() { } // Contacts screen: long press - // If NOT in select mode → enter select mode - // If already in select mode → exit select mode + // T-Deck Pro: toggle select mode (DM/admin handled by keyboard Enter) + // T5S3: DM for chat contacts, admin for repeaters/rooms (no physical keyboard) + // If in select mode, long press exits it on both platforms. if (ui_task.isOnContactsScreen()) { ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen(); if (cs) { - if (!cs->isInSelectMode()) { - // Enter select mode on long press - cs->enterSelectMode(); + if (cs->isInSelectMode()) { + // Both platforms: long press exits select mode + cs->exitSelectMode(); ui_task.forceRefresh(); - Serial.println("Contacts: entered select mode (touch long press)"); + Serial.println("Contacts: exited select mode (touch long press)"); return 0; } - // Already in select mode: long press exits select mode on both platforms - // (T-Deck Pro uses keyboard for actions; T5S3 can use CardKB if attached) - cs->exitSelectMode(); - ui_task.forceRefresh(); - Serial.println("Contacts: exited select mode (touch long press)"); +#if defined(LilyGo_T5S3_EPaper_Pro) + // T5S3: long press = DM/admin/room action (primary interaction path) + { + int idx = cs->getSelectedContactIdx(); + uint8_t ctype = cs->getSelectedContactType(); + if (idx >= 0 && ctype == ADV_TYPE_CHAT) { + if (ui_task.hasDMUnread(idx)) { + char cname[32]; + cs->getSelectedContactName(cname, sizeof(cname)); + ui_task.clearDMUnread(idx); + ui_task.gotoDMConversation(cname); + return 0; + } + char dname[32]; + cs->getSelectedContactName(dname, sizeof(dname)); + char label[40]; + snprintf(label, sizeof(label), "DM: %s", dname); + ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx); + return 0; + } else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) { + ui_task.gotoRepeaterAdmin(idx); + return 0; + } else if (idx >= 0 && ctype == ADV_TYPE_ROOM) { + ui_task.gotoRepeaterAdmin(idx); + return 0; + } else if (idx >= 0 && ui_task.hasDMUnread(idx)) { + char cname[32]; + cs->getSelectedContactName(cname, sizeof(cname)); + ui_task.clearDMUnread(idx); + ui_task.gotoDMConversation(cname); + return 0; + } + } return 0; +#else + // T-Deck Pro: long press enters select mode + cs->enterSelectMode(); + ui_task.forceRefresh(); + Serial.println("Contacts: entered select mode (touch long press)"); + return 0; +#endif } return KEY_ENTER; } diff --git a/examples/companion_radio/ui-new/Contactsscreen.h b/examples/companion_radio/ui-new/Contactsscreen.h index 090f5f34..4b87ab68 100644 --- a/examples/companion_radio/ui-new/Contactsscreen.h +++ b/examples/companion_radio/ui-new/Contactsscreen.h @@ -4,7 +4,11 @@ #include #include -// Forward declarations +// Timestamps before this (Jan 1 2026 UTC) are treated as invalid/unsynced +#define EPOCH_2026 1735689600UL + +// Forward declarations — MyMesh.h (which defines AdvertPath) is always +// included by the translation unit before this header. class UITask; class MyMesh; extern MyMesh the_mesh; @@ -33,8 +37,10 @@ private: // We rebuild this on filter change or when entering the screen // Arrays allocated in PSRAM when available (supports 1000+ contacts) uint16_t* _filteredIdx; // indices into contact table - uint32_t* _filteredTs; // cached last_advert_timestamp for sorting + uint32_t* _filteredTs; // cached lastmod for sorting int _filteredCount; // how many contacts match current filter + AdvertPath _hopBuf[12]; // recently heard advert paths for hop-count display + int _hopBufCount; bool _cacheValid; // How many rows fit on screen (computed during render) @@ -61,12 +67,12 @@ private: } } - static char typeChar(uint8_t adv_type) { + static const char* typeStr(uint8_t adv_type) { switch (adv_type) { - case ADV_TYPE_CHAT: return 'C'; - case ADV_TYPE_REPEATER: return 'R'; - case ADV_TYPE_ROOM: return 'S'; // Server - default: return '?'; + case ADV_TYPE_CHAT: return "C"; + case ADV_TYPE_REPEATER: return "R"; + case ADV_TYPE_ROOM: return "RS"; + default: return "?"; } } @@ -92,12 +98,13 @@ private: if (the_mesh.getContactByIdx(i, contact)) { if (matchesFilter(contact.type, contact.flags)) { _filteredIdx[_filteredCount] = (uint16_t)i; - _filteredTs[_filteredCount] = contact.last_advert_timestamp; + // Use lastmod (our receive time) for sort/age; pre-2026 or zero → 0 sinks to bottom + _filteredTs[_filteredCount] = (contact.lastmod >= EPOCH_2026) ? contact.lastmod : 0; _filteredCount++; } } } - // Sort by last_advert_timestamp descending (most recently seen first) + // Sort by lastmod descending (most recently heard first; pre-2026/unsynced sink to bottom) // Insertion sort — fine for up to ~1000 entries on ESP32 for (int i = 1; i < _filteredCount; i++) { uint16_t tmpIdx = _filteredIdx[i]; @@ -112,28 +119,29 @@ private: _filteredTs[j + 1] = tmpTs; } _cacheValid = true; + // Refresh hop-count cache from the 12 most recently heard adverts + _hopBufCount = the_mesh.getRecentlyHeard(_hopBuf, 12); // Clamp scroll position if (_scrollPos >= _filteredCount) { _scrollPos = (_filteredCount > 0) ? _filteredCount - 1 : 0; } } - // Format seconds-ago as compact string: "3s" "5m" "2h" "4d" "??" + // Format seconds-ago as compact string: "3s" "5m" "2h" "4d" "--" static void formatAge(char* buf, size_t bufLen, uint32_t now, uint32_t timestamp) { - if (timestamp == 0) { + if (timestamp == 0 || timestamp < EPOCH_2026 || now < timestamp) { strncpy(buf, "--", bufLen); return; } - int secs = (int)(now - timestamp); - if (secs < 0) secs = 0; + uint32_t secs = now - timestamp; if (secs < 60) { - snprintf(buf, bufLen, "%ds", secs); + snprintf(buf, bufLen, "%ds", (int)secs); } else if (secs < 3600) { - snprintf(buf, bufLen, "%dm", secs / 60); + snprintf(buf, bufLen, "%dm", (int)(secs / 60)); } else if (secs < 86400) { - snprintf(buf, bufLen, "%dh", secs / 3600); + snprintf(buf, bufLen, "%dh", (int)(secs / 3600)); } else { - snprintf(buf, bufLen, "%dd", secs / 86400); + snprintf(buf, bufLen, "%dd", (int)(secs / 86400)); } } @@ -152,7 +160,7 @@ public: ContactsScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc), _scrollPos(0), _filter(FILTER_ALL), _filteredCount(0), _cacheValid(false), _rowsPerPage(5), - _selectMode(false) { + _selectMode(false), _hopBufCount(0) { #if defined(ESP32) && defined(BOARD_HAS_PSRAM) _filteredIdx = (uint16_t*)ps_calloc(MAX_CONTACTS, sizeof(uint16_t)); _filteredTs = (uint32_t*)ps_calloc(MAX_CONTACTS, sizeof(uint32_t)); @@ -362,15 +370,15 @@ public: display.setCursor(0, y); // Prefix: select mode uses * for selected, normal uses > for cursor - char prefix[4]; + char prefix[5]; if (_selectMode) { - snprintf(prefix, sizeof(prefix), "%c%c", + snprintf(prefix, sizeof(prefix), "%c%s", sel ? '*' : (selected ? '>' : ' '), - typeChar(contact.type)); + typeStr(contact.type)); } else if (selected) { - snprintf(prefix, sizeof(prefix), ">%c", typeChar(contact.type)); + snprintf(prefix, sizeof(prefix), ">%s", typeStr(contact.type)); } else { - snprintf(prefix, sizeof(prefix), " %c", typeChar(contact.type)); + snprintf(prefix, sizeof(prefix), " %s", typeStr(contact.type)); } display.print(prefix); @@ -381,7 +389,22 @@ public: // Reserve space for hops + age on right side char hopStr[6]; if (contact.out_path_len == 0xFF) { - strcpy(hopStr, "?"); // unknown path + // No confirmed direct path — look up flood hop estimate from recent advert cache + hopStr[0] = '?'; hopStr[1] = '\0'; // default + for (int h = 0; h < _hopBufCount; h++) { + if (memcmp(contact.id.pub_key, _hopBuf[h].pubkey_prefix, 7) == 0) { + uint8_t bph = (_hopBuf[h].path_len >> 6) + 1; + uint8_t hops = _hopBuf[h].path_len & 0x3F; + uint8_t max_hops = 64 / bph; // sanity cap based on path encoding + if (hops <= max_hops) { + if (hops == 0) + strcpy(hopStr, "~D"); + else + snprintf(hopStr, sizeof(hopStr), "~%d", (int)hops); + } + break; + } + } } else if (contact.out_path_len == 0) { bool customDirect = (contact.flags & CONTACT_FLAG_CUSTOM_PATH) != 0; strcpy(hopStr, customDirect ? "D*" : "D"); @@ -396,7 +419,7 @@ public: } char ageStr[6]; - formatAge(ageStr, sizeof(ageStr), now, contact.last_advert_timestamp); + formatAge(ageStr, sizeof(ageStr), now, contact.lastmod); // Build right-side string: "*N hops age" if unread, else "hops age" int dmCount = (_dmUnread && _filteredIdx[i] < MAX_CONTACTS) ? _dmUnread[_filteredIdx[i]] : 0; diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 6947d4c3..1b84ce6d 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -1847,7 +1847,7 @@ public: break; case ROW_AUTOADD_CHAT: - snprintf(tmp, sizeof(tmp), " Chat: %s", + snprintf(tmp, sizeof(tmp), " Companion: %s", (_prefs->autoadd_config & AUTO_ADD_CHAT) ? "ON" : "OFF"); display.print(tmp); break; diff --git a/variants/lilygo_tdeck_pro/platformio.ini b/variants/lilygo_tdeck_pro/platformio.ini index 2d52967f..631e6ae8 100644 --- a/variants/lilygo_tdeck_pro/platformio.ini +++ b/variants/lilygo_tdeck_pro/platformio.ini @@ -148,7 +148,7 @@ extends = LilyGo_TDeck_Pro build_flags = ${LilyGo_TDeck_Pro.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=1500 + -D MAX_CONTACTS=2000 -D MAX_GROUP_CHANNELS=20 -D MECK_WIFI_COMPANION=1 -D TCP_PORT=5000 @@ -184,7 +184,7 @@ extends = LilyGo_TDeck_Pro build_flags = ${LilyGo_TDeck_Pro.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=1500 + -D MAX_CONTACTS=2000 -D MAX_GROUP_CHANNELS=20 -D OFFLINE_QUEUE_SIZE=1 -D MECK_AUDIO_VARIANT @@ -249,7 +249,7 @@ extends = LilyGo_TDeck_Pro build_flags = ${LilyGo_TDeck_Pro.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=1500 + -D MAX_CONTACTS=2000 -D MAX_GROUP_CHANNELS=20 -D MECK_WIFI_COMPANION=1 -D TCP_PORT=5000 @@ -285,7 +285,7 @@ extends = LilyGo_TDeck_Pro build_flags = ${LilyGo_TDeck_Pro.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=1500 + -D MAX_CONTACTS=2000 -D MAX_GROUP_CHANNELS=20 -D OFFLINE_QUEUE_SIZE=1 -D HAS_4G_MODEM=1 @@ -294,6 +294,7 @@ build_flags = -D FIRMWARE_VERSION='"Meck v1.6.1.4G.SA"' build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + + - + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp>