mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-10 02:32:44 +02:00
T5S3 - fixed touch selector fav contacts bug
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <MeshCore.h>
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
+<helpers/esp32/*.cpp>
|
||||
-<helpers/esp32/SerialBLEInterface.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
|
||||
Reference in New Issue
Block a user