fixed stupid persistent contacts saved bug in datastore; prelim contacts discovery function

This commit is contained in:
pelgraine
2026-03-04 07:16:07 +11:00
parent d92fdc9ffe
commit fe949235d9
8 changed files with 379 additions and 7 deletions

View File

@@ -405,11 +405,15 @@ void DataStore::saveContacts(DataStoreHost* host) {
idx++;
}
size_t bytesWritten = file.size();
file.close();
// --- Step 2: Verify the write completed ---
// Reopen read-only to get true on-disk size (SPIFFS file.size() is unreliable before close)
size_t expectedBytes = recordsWritten * 152; // 152 bytes per contact record
File verify = openRead(fs, tmpPath);
size_t bytesWritten = verify ? verify.size() : 0;
if (verify) verify.close();
if (!writeOk || bytesWritten != expectedBytes) {
Serial.printf("DataStore: saveContacts ABORTED — wrote %d bytes, expected %d (%d records)\n",
(int)bytesWritten, (int)expectedBytes, recordsWritten);
@@ -493,10 +497,13 @@ void DataStore::saveChannels(DataStoreHost* host) {
channel_idx++;
}
size_t bytesWritten = file.size();
file.close();
// Reopen read-only to get true on-disk size (SPIFFS file.size() is unreliable before close)
size_t expectedBytes = channel_idx * 68; // 4 + 32 + 32 = 68 bytes per channel
File verify = openRead(fs, tmpPath);
size_t bytesWritten = verify ? verify.size() : 0;
if (verify) verify.close();
if (!writeOk || bytesWritten != expectedBytes) {
Serial.printf("DataStore: saveChannels ABORTED — wrote %d bytes, expected %d\n",
(int)bytesWritten, (int)expectedBytes);

View File

@@ -357,6 +357,30 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
memcpy(p->path, path, p->path_len);
}
// Buffer for on-device discovery UI
if (_discoveryActive && _discoveredCount < MAX_DISCOVERED_NODES) {
bool dup = false;
for (int i = 0; i < _discoveredCount; i++) {
if (contact.id.matches(_discovered[i].contact.id)) {
// Update existing entry with fresher data
_discovered[i].contact = contact;
_discovered[i].path_len = path_len;
_discovered[i].already_in_contacts = !is_new;
dup = true;
Serial.printf("[Discovery] Updated: %s (hops=%d)\n", contact.name, path_len);
break;
}
}
if (!dup) {
_discovered[_discoveredCount].contact = contact;
_discovered[_discoveredCount].path_len = path_len;
_discovered[_discoveredCount].already_in_contacts = !is_new;
_discoveredCount++;
Serial.printf("[Discovery] Found: %s (hops=%d, is_new=%d, total=%d)\n",
contact.name, path_len, is_new, _discoveredCount);
}
}
if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
}
@@ -998,6 +1022,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
memset(_sent_track, 0, sizeof(_sent_track));
_sent_track_idx = 0;
_admin_contact_idx = -1;
_discoveredCount = 0;
_discoveryActive = false;
_discoveryTimeout = 0;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
@@ -2201,6 +2228,12 @@ void MyMesh::loop() {
dirty_contacts_expiry = 0;
}
// Discovery scan timeout
if (_discoveryActive && millisHasNowPassed(_discoveryTimeout)) {
_discoveryActive = false;
Serial.printf("[Discovery] Scan complete: %d nodes found\n", _discoveredCount);
}
#ifdef DISPLAY_CLASS
if (_ui) _ui->setHasConnection(_serial->isConnected());
#endif
@@ -2219,4 +2252,65 @@ bool MyMesh::advert() {
} else {
return false;
}
}
void MyMesh::startDiscovery(uint32_t duration_ms) {
_discoveredCount = 0;
_discoveryActive = true;
_discoveryTimeout = futureMillis(duration_ms);
Serial.printf("[Discovery] Scan started (%lu ms)\n", duration_ms);
// Pre-seed from advert_paths cache (nodes heard recently, before scan started)
for (int i = 0; i < ADVERT_PATH_TABLE_SIZE && _discoveredCount < MAX_DISCOVERED_NODES; i++) {
if (advert_paths[i].recv_timestamp == 0) continue; // empty slot
// Look up full contact info by pubkey prefix
ContactInfo* c = lookupContactByPubKey(advert_paths[i].pubkey_prefix, sizeof(advert_paths[i].pubkey_prefix));
if (c) {
_discovered[_discoveredCount].contact = *c;
_discovered[_discoveredCount].path_len = advert_paths[i].path_len;
_discovered[_discoveredCount].already_in_contacts = true;
_discoveredCount++;
}
}
Serial.printf("[Discovery] Pre-seeded %d nodes from cache\n", _discoveredCount);
// Flood self-advert through mesh (not zero-hop) so repeaters
// multiple hops away hear it and respond with their own adverts
mesh::Packet* pkt;
if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) {
pkt = createSelfAdvert(_prefs.node_name);
} else {
pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon);
}
if (pkt) {
sendFlood(pkt);
Serial.println("[Discovery] Self-advert flooded");
} else {
Serial.println("[Discovery] ERROR: createSelfAdvert returned NULL (packet pool full?)");
}
}
void MyMesh::stopDiscovery() {
_discoveryActive = false;
}
bool MyMesh::addDiscoveredToContacts(int idx) {
if (idx < 0 || idx >= _discoveredCount) return false;
if (_discovered[idx].already_in_contacts) return true; // already there
// Retrieve cached raw advert packet and import it
uint8_t buf[256];
int plen = getBlobByKey(_discovered[idx].contact.id.pub_key, PUB_KEY_SIZE, buf);
if (plen > 0) {
bool ok = importContact(buf, (uint8_t)plen);
if (ok) {
_discovered[idx].already_in_contacts = true;
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
MESH_DEBUG_PRINTLN("Discovery: added contact '%s'", _discovered[idx].contact.name);
}
return ok;
}
MESH_DEBUG_PRINTLN("Discovery: no cached advert blob for contact '%s'", _discovered[idx].contact.name);
return false;
}

View File

@@ -84,6 +84,15 @@ struct AdvertPath {
uint8_t path[MAX_PATH_SIZE];
};
// Discovery scan — transient buffer for on-device node discovery
#define MAX_DISCOVERED_NODES 20
struct DiscoveredNode {
ContactInfo contact;
uint8_t path_len;
bool already_in_contacts; // true if contact was auto-added or already known
};
class MyMesh : public BaseChatMesh, public DataStoreHost {
public:
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
@@ -101,6 +110,14 @@ public:
void enterCLIRescue();
int getRecentlyHeard(AdvertPath dest[], int max_num);
// Discovery scan — on-device node discovery
void startDiscovery(uint32_t duration_ms = 30000);
void stopDiscovery();
bool isDiscoveryActive() const { return _discoveryActive; }
int getDiscoveredCount() const { return _discoveredCount; }
const DiscoveredNode& getDiscovered(int idx) const { return _discovered[idx]; }
bool addDiscoveredToContacts(int idx); // promote a discovered node into contacts
// Queue a sent channel message for BLE app sync
void queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* sender, const char* text);
@@ -257,6 +274,12 @@ private:
SentMsgTrack _sent_track[SENT_TRACK_SIZE];
int _sent_track_idx; // next slot in circular buffer
int _admin_contact_idx; // contact index for active admin session (-1 if none)
// Discovery scan state
DiscoveredNode _discovered[MAX_DISCOVERED_NODES];
int _discoveredCount;
bool _discoveryActive;
unsigned long _discoveryTimeout;
};
extern MyMesh the_mesh;

View File

@@ -18,6 +18,7 @@
#include "ChannelScreen.h"
#include "SettingsScreen.h"
#include "RepeaterAdminScreen.h"
#include "DiscoveryScreen.h"
#ifdef MECK_WEB_READER
#include "WebReaderScreen.h"
#endif
@@ -1848,8 +1849,9 @@ void handleKeyboardInput() {
break;
case 's':
// Open settings (from home), or navigate down on channel/contacts/admin/web/map
// Open settings (from home), or navigate down on channel/contacts/admin/web/map/discovery
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
|| ui_task.isOnDiscoveryScreen()
#ifdef MECK_WEB_READER
|| ui_task.isOnWebReader()
#endif
@@ -1865,6 +1867,7 @@ void handleKeyboardInput() {
case 'w':
// Navigate up/previous (scroll on channel screen)
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()
|| ui_task.isOnDiscoveryScreen()
#ifdef MECK_WEB_READER
|| ui_task.isOnWebReader()
#endif
@@ -1972,6 +1975,23 @@ void handleKeyboardInput() {
}
drawComposeScreen();
lastComposeRefresh = millis();
} else if (ui_task.isOnDiscoveryScreen()) {
// Discovery screen: Enter adds selected node to contacts
DiscoveryScreen* ds = (DiscoveryScreen*)ui_task.getDiscoveryScreen();
int didx = ds->getSelectedIdx();
if (didx >= 0 && didx < the_mesh.getDiscoveredCount()) {
const DiscoveredNode& node = the_mesh.getDiscovered(didx);
if (node.already_in_contacts) {
ui_task.showAlert("Already in contacts", 800);
} else if (the_mesh.addDiscoveredToContacts(didx)) {
char alertBuf[48];
snprintf(alertBuf, sizeof(alertBuf), "Added: %s", node.contact.name);
ui_task.showAlert(alertBuf, 1500);
ui_task.notify(UIEventType::ack);
} else {
ui_task.showAlert("Add failed", 1000);
}
}
} else {
// Other screens: pass Enter as generic select
ui_task.injectKey(13);
@@ -2025,6 +2045,17 @@ void handleKeyboardInput() {
}
break;
case 'f':
// Start discovery scan from contacts screen, or rescan on discovery screen
if (ui_task.isOnContactsScreen()) {
Serial.println("Contacts: Starting discovery scan...");
the_mesh.startDiscovery();
ui_task.gotoDiscoveryScreen();
} else if (ui_task.isOnDiscoveryScreen()) {
ui_task.injectKey('f'); // pass through for rescan
}
break;
case 'q':
case '\b':
// If channel screen reply select or path overlay is showing, dismiss it
@@ -2050,6 +2081,13 @@ void handleKeyboardInput() {
}
}
#endif
// Discovery screen: Q goes back to contacts (not home)
if (ui_task.isOnDiscoveryScreen()) {
the_mesh.stopDiscovery();
Serial.println("Nav: Discovery -> Contacts");
ui_task.gotoContactsScreen();
break;
}
// Go back to home screen (admin mode handled above)
Serial.println("Nav: Back to home");
ui_task.gotoHomeScreen();

View File

@@ -297,17 +297,17 @@ public:
display.drawRect(0, footerY - 2, display.width(), 1);
display.setColor(DisplayDriver::YELLOW);
// Left: Q:Back
// Left: Q:Bk
display.setCursor(0, footerY);
display.print("Q:Back");
display.print("Q:Bk");
// Center: A/D:Filter
const char* mid = "A/D:Filtr";
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
display.print(mid);
// Right: W/S:Scroll
const char* right = "W/S:Scrll";
// Right: F:Dscvr
const char* right = "F:Dscvr";
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
display.print(right);

View File

@@ -0,0 +1,194 @@
#pragma once
#include <helpers/ui/UIScreen.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/AdvertDataHelpers.h>
#include <MeshCore.h>
// Forward declarations
class UITask;
class MyMesh;
extern MyMesh the_mesh;
class DiscoveryScreen : public UIScreen {
UITask* _task;
mesh::RTCClock* _rtc;
int _scrollPos;
int _rowsPerPage;
static char typeChar(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';
case ADV_TYPE_SENSOR: return 'N';
default: return '?';
}
}
static const char* typeLabel(uint8_t adv_type) {
switch (adv_type) {
case ADV_TYPE_CHAT: return "Chat";
case ADV_TYPE_REPEATER: return "Rptr";
case ADV_TYPE_ROOM: return "Room";
case ADV_TYPE_SENSOR: return "Sens";
default: return "?";
}
}
public:
DiscoveryScreen(UITask* task, mesh::RTCClock* rtc)
: _task(task), _rtc(rtc), _scrollPos(0), _rowsPerPage(5) {}
void resetScroll() { _scrollPos = 0; }
int getSelectedIdx() const { return _scrollPos; }
int render(DisplayDriver& display) override {
int count = the_mesh.getDiscoveredCount();
bool active = the_mesh.isDiscoveryActive();
// === Header ===
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, 0);
char hdr[32];
if (active) {
snprintf(hdr, sizeof(hdr), "Scanning... %d found", count);
} else {
snprintf(hdr, sizeof(hdr), "Scan done: %d found", count);
}
display.print(hdr);
// Divider
display.drawRect(0, 11, display.width(), 1);
// === Body — discovered node rows ===
display.setTextSize(0); // tiny font for compact rows
int lineHeight = 9;
int headerHeight = 14;
int footerHeight = 14;
int maxY = display.height() - footerHeight;
int y = headerHeight;
int rowsDrawn = 0;
if (count == 0) {
display.setColor(DisplayDriver::LIGHT);
display.setCursor(4, 28);
display.print(active ? "Listening for adverts..." : "No nodes found");
if (!active) {
display.setCursor(4, 38);
display.print("F: Scan again Q: Back");
}
} else {
// Center visible window around selected item
int maxVisible = (maxY - headerHeight) / lineHeight;
if (maxVisible < 3) maxVisible = 3;
int startIdx = max(0, min(_scrollPos - maxVisible / 2,
count - maxVisible));
int endIdx = min(count, startIdx + maxVisible);
for (int i = startIdx; i < endIdx && y + lineHeight <= maxY; i++) {
const DiscoveredNode& node = the_mesh.getDiscovered(i);
bool selected = (i == _scrollPos);
// Highlight selected row
if (selected) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y + 5, display.width(), lineHeight);
display.setColor(DisplayDriver::DARK);
} else {
display.setColor(DisplayDriver::LIGHT);
}
display.setCursor(0, y);
// Prefix: cursor + type
char prefix[4];
if (selected) {
snprintf(prefix, sizeof(prefix), ">%c", typeChar(node.contact.type));
} else {
snprintf(prefix, sizeof(prefix), " %c", typeChar(node.contact.type));
}
display.print(prefix);
// Build right-side info: hop count + status
char rightStr[12];
if (node.already_in_contacts) {
snprintf(rightStr, sizeof(rightStr), "%dh [+]", node.path_len);
} else {
snprintf(rightStr, sizeof(rightStr), "%dh", node.path_len);
}
int rightWidth = display.getTextWidth(rightStr) + 2;
// Name (truncated with ellipsis)
char filteredName[32];
display.translateUTF8ToBlocks(filteredName, node.contact.name, sizeof(filteredName));
int nameX = display.getTextWidth(prefix) + 2;
int nameMaxW = display.width() - nameX - rightWidth - 2;
display.drawTextEllipsized(nameX, y, nameMaxW, filteredName);
// Right-aligned info
display.setCursor(display.width() - rightWidth, y);
display.print(rightStr);
y += lineHeight;
rowsDrawn++;
}
_rowsPerPage = (rowsDrawn > 0) ? rowsDrawn : 1;
}
display.setTextSize(1); // restore for footer
// === Footer ===
int footerY = display.height() - 12;
display.drawRect(0, footerY - 2, display.width(), 1);
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, footerY);
display.print("Q:Back");
const char* mid = "Ent:Add";
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
display.print(mid);
const char* right = "F:Rescan";
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
display.print(right);
// Faster refresh while actively scanning
return active ? 1000 : 5000;
}
bool handleInput(char c) override {
int count = the_mesh.getDiscoveredCount();
// W - scroll up
if (c == 'w' || c == 'W' || c == 0xF2) {
if (_scrollPos > 0) {
_scrollPos--;
return true;
}
}
// S - scroll down
if (c == 's' || c == 'S' || c == 0xF1) {
if (_scrollPos < count - 1) {
_scrollPos++;
return true;
}
}
// F - rescan (handled here as well as in main.cpp for consistency)
if (c == 'f') {
the_mesh.startDiscovery();
_scrollPos = 0;
return true;
}
// Enter - handled by main.cpp for alert feedback
return false; // Q/back and Enter handled by main.cpp
}
};

View File

@@ -3,6 +3,7 @@
#include "../MyMesh.h"
#include "NotesScreen.h"
#include "RepeaterAdminScreen.h"
#include "DiscoveryScreen.h"
#include "MapScreen.h"
#include "target.h"
#if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION)
@@ -946,6 +947,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
notes_screen = new NotesScreen(this);
settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs);
repeater_admin = nullptr; // Lazy-initialized on first use to preserve heap for audio
discovery_screen = new DiscoveryScreen(this, &rtc_clock);
audiobook_screen = nullptr; // Created and assigned from main.cpp if audio hardware present
#ifdef HAS_4G_MODEM
sms_screen = new SMSScreen(this);
@@ -1606,6 +1608,16 @@ void UITask::gotoRepeaterAdmin(int contactIdx) {
_next_refresh = 100;
}
void UITask::gotoDiscoveryScreen() {
((DiscoveryScreen*)discovery_screen)->resetScroll();
setCurrScreen(discovery_screen);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
}
#ifdef MECK_WEB_READER
void UITask::gotoWebReader() {
// Lazy-initialize on first use (same pattern as audiobook player)

View File

@@ -79,6 +79,7 @@ class UITask : public AbstractUITask {
UIScreen* sms_screen; // SMS messaging screen (4G variant only)
#endif
UIScreen* repeater_admin; // Repeater admin screen
UIScreen* discovery_screen; // Node discovery scan screen
#ifdef MECK_WEB_READER
UIScreen* web_reader; // Web reader screen (lazy-init, WiFi required)
#endif
@@ -119,6 +120,7 @@ public:
void gotoOnboarding(); // Navigate to settings in onboarding mode
void gotoAudiobookPlayer(); // Navigate to audiobook player
void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin
void gotoDiscoveryScreen(); // Navigate to node discovery scan
void gotoMapScreen(); // Navigate to map tile screen
#ifdef MECK_WEB_READER
void gotoWebReader(); // Navigate to web reader (browser)
@@ -147,6 +149,7 @@ public:
bool isOnSettingsScreen() const { return curr == settings_screen; }
bool isOnAudiobookPlayer() const { return curr == audiobook_screen; }
bool isOnRepeaterAdmin() const { return curr == repeater_admin; }
bool isOnDiscoveryScreen() const { return curr == discovery_screen; }
bool isOnMapScreen() const { return curr == map_screen; }
#ifdef MECK_WEB_READER
bool isOnWebReader() const { return curr == web_reader; }
@@ -191,6 +194,7 @@ public:
UIScreen* getAudiobookScreen() const { return audiobook_screen; }
void setAudiobookScreen(UIScreen* s) { audiobook_screen = s; }
UIScreen* getRepeaterAdminScreen() const { return repeater_admin; }
UIScreen* getDiscoveryScreen() const { return discovery_screen; }
UIScreen* getMapScreen() const { return map_screen; }
#ifdef MECK_WEB_READER
UIScreen* getWebReaderScreen() const { return web_reader; }