From 5bed26cb72e47d1ba1880c93b59ea274184dbeeb Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 20 Mar 2026 05:27:20 +1100 Subject: [PATCH] mostly t5s3 and some tdpro fixes - chunked save infrastructure, chunked save implementation, non-blocking lazy save, favourite contacts edit double confirmation added, hibernate 4g modem properly --- examples/companion_radio/DataStore.cpp | 95 +++++++++++++++++++ examples/companion_radio/DataStore.h | 18 +++- examples/companion_radio/MyMesh.cpp | 11 ++- examples/companion_radio/main.cpp | 24 ++++- .../companion_radio/ui-new/Lastheardscreen.h | 12 ++- examples/companion_radio/ui-new/UITask.cpp | 7 +- 6 files changed, 157 insertions(+), 10 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 9e7d3d4..276090a 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -443,6 +443,101 @@ void DataStore::saveContacts(DataStoreHost* host) { } } +// ========================================================================= +// Chunked contact save — non-blocking across multiple loop iterations +// ========================================================================= + +bool DataStore::beginSaveContacts(DataStoreHost* host) { + if (_saveInProgress) return false; // Already saving + + FILESYSTEM* fs = _getContactsChannelsFS(); + _saveFile = openWrite(fs, "/contacts3.tmp"); + if (!_saveFile) { + Serial.println("DataStore: chunked save FAILED — cannot open tmp file"); + return false; + } + + _saveHost = host; + _saveIdx = 0; + _saveRecordsWritten = 0; + _saveWriteOk = true; + _saveInProgress = true; + Serial.println("DataStore: chunked save started"); + return true; +} + +bool DataStore::saveContactsChunk(int batchSize) { + if (!_saveInProgress || !_saveWriteOk) return false; + + ContactInfo c; + uint8_t unused = 0; + int written = 0; + + while (written < batchSize && _saveHost->getContactForSave(_saveIdx, c)) { + bool success = (_saveFile.write(c.id.pub_key, 32) == 32); + success = success && (_saveFile.write((uint8_t *)&c.name, 32) == 32); + success = success && (_saveFile.write(&c.type, 1) == 1); + success = success && (_saveFile.write(&c.flags, 1) == 1); + success = success && (_saveFile.write(&unused, 1) == 1); + success = success && (_saveFile.write((uint8_t *)&c.sync_since, 4) == 4); + success = success && (_saveFile.write((uint8_t *)&c.out_path_len, 1) == 1); + success = success && (_saveFile.write((uint8_t *)&c.last_advert_timestamp, 4) == 4); + success = success && (_saveFile.write(c.out_path, 64) == 64); + success = success && (_saveFile.write((uint8_t *)&c.lastmod, 4) == 4); + success = success && (_saveFile.write((uint8_t *)&c.gps_lat, 4) == 4); + success = success && (_saveFile.write((uint8_t *)&c.gps_lon, 4) == 4); + + if (!success) { + _saveWriteOk = false; + Serial.printf("DataStore: chunked save write error at record %d\n", _saveIdx); + return false; // Error — finishSaveContacts will clean up + } + + _saveRecordsWritten++; + _saveIdx++; + written++; + } + + // Check if there are more contacts to write + ContactInfo peek; + if (_saveHost->getContactForSave(_saveIdx, peek)) { + return true; // More to write + } + return false; // Done +} + +void DataStore::finishSaveContacts() { + if (!_saveInProgress) return; + + _saveFile.close(); + _saveInProgress = false; + + FILESYSTEM* fs = _getContactsChannelsFS(); + const char* finalPath = "/contacts3"; + const char* tmpPath = "/contacts3.tmp"; + + // Verify + size_t expectedBytes = _saveRecordsWritten * 152; + File verify = openRead(fs, tmpPath); + size_t bytesWritten = verify ? verify.size() : 0; + if (verify) verify.close(); + + if (!_saveWriteOk || bytesWritten != expectedBytes) { + Serial.printf("DataStore: chunked save ABORTED — wrote %d bytes, expected %d (%d records)\n", + (int)bytesWritten, (int)expectedBytes, _saveRecordsWritten); + fs->remove(tmpPath); + return; + } + + fs->remove(finalPath); + if (fs->rename(tmpPath, finalPath)) { + Serial.printf("DataStore: saved %d contacts (%d bytes, chunked)\n", + _saveRecordsWritten, (int)bytesWritten); + } else { + Serial.println("DataStore: rename failed, tmp file preserved"); + } +} + void DataStore::loadChannels(DataStoreHost* host) { FILESYSTEM* fs = _getContactsChannelsFS(); diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h index 6258094..ecaca09 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -24,6 +24,14 @@ class DataStore { void checkAdvBlobFile(); #endif + // Chunked save state + File _saveFile; + DataStoreHost* _saveHost = nullptr; + uint32_t _saveIdx = 0; + uint32_t _saveRecordsWritten = 0; + bool _saveInProgress = false; + bool _saveWriteOk = true; + public: DataStore(FILESYSTEM& fs, mesh::RTCClock& clock); DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock); @@ -37,6 +45,14 @@ public: void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon); void loadContacts(DataStoreHost* host); void saveContacts(DataStoreHost* host); + // Chunked save — splits contact write across multiple loop iterations + // to prevent blocking the main loop for 500ms+ on large contact lists. + // Call beginSaveContacts(), then saveContactsChunk() each loop until it + // returns false (done), then finishSaveContacts() to verify and commit. + bool beginSaveContacts(DataStoreHost* host); + bool saveContactsChunk(int batchSize = 20); // returns true if more to write + void finishSaveContacts(); + bool isSaveInProgress() const { return _saveInProgress; } void loadChannels(DataStoreHost* host); void saveChannels(DataStoreHost* host); void migrateToSecondaryFS(); @@ -51,4 +67,4 @@ public: private: FILESYSTEM* _getContactsChannelsFS() const { if (_fsExtra) return _fsExtra; return _fs;}; -}; +}; \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index fb9f9f8..7486b0e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2999,10 +2999,19 @@ void MyMesh::loop() { // is there are pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { - saveContacts(); + if (!_store->isSaveInProgress()) { + _store->beginSaveContacts(this); + } dirty_contacts_expiry = 0; } + // Drive chunked contact save — write a batch each loop iteration + if (_store->isSaveInProgress()) { + if (!_store->saveContactsChunk(20)) { // 20 contacts per chunk (~3KB, ~30ms) + _store->finishSaveContacts(); // Done or error — verify and commit + } + } + // Discovery scan timeout if (_discoveryActive && millisHasNowPassed(_discoveryTimeout)) { _discoveryActive = false; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 0533ac8..1382cc8 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -569,8 +569,26 @@ static void lastHeardToggleContact() { const AdvertPath* entry = lh->getSelectedEntry(); if (!entry) return; - ContactInfo* existing = the_mesh.lookupContactByPubKey(entry->pubkey_prefix, 7); + ContactInfo* existing = the_mesh.lookupContactByPubKey(entry->pubkey_prefix, 8); if (existing) { + // Double-confirm for favourites (bit 0 of flags) + static unsigned long lastRemoveAttempt = 0; + static uint8_t lastRemovePrefix[8] = {0}; + bool isFav = (existing->flags & 0x01) != 0; + + if (isFav) { + if (millis() - lastRemoveAttempt < 3000 && + memcmp(lastRemovePrefix, entry->pubkey_prefix, 8) == 0) { + // Second press within 3s — confirmed + } else { + // First press on favourite — warn and wait + lastRemoveAttempt = millis(); + memcpy(lastRemovePrefix, entry->pubkey_prefix, 8); + ui_task.showAlert("Favourite! Press again", 2500); + return; + } + } + the_mesh.removeContact(*existing); the_mesh.scheduleLazyContactSave(); char alertBuf[40]; @@ -579,7 +597,7 @@ static void lastHeardToggleContact() { Serial.printf("[LastHeard] Removed: %s\n", entry->name); } else { uint8_t blob[256]; - int blobLen = the_mesh.getContactBlob(entry->pubkey_prefix, 7, blob); + int blobLen = the_mesh.getContactBlob(entry->pubkey_prefix, 8, blob); if (blobLen > 0) { the_mesh.importContact(blob, blobLen); the_mesh.scheduleLazyContactSave(); @@ -591,7 +609,7 @@ static void lastHeardToggleContact() { // Blob store is limited to 100 entries — with many contacts, blobs // from non-contact nodes get evicted quickly. User needs to wait // for the node to re-broadcast its advert. - ui_task.showAlert("Advert expired, wait for re-broadcast", 2500); + ui_task.showAlert("Advert expired, try later", 2000); Serial.printf("[LastHeard] Blob evicted for %s (store full)\n", entry->name); } } diff --git a/examples/companion_radio/ui-new/Lastheardscreen.h b/examples/companion_radio/ui-new/Lastheardscreen.h index 5b51bad..c690293 100644 --- a/examples/companion_radio/ui-new/Lastheardscreen.h +++ b/examples/companion_radio/ui-new/Lastheardscreen.h @@ -55,7 +55,7 @@ public: // Check if selected node is already in contacts bool isSelectedInContacts() const { if (_scrollPos < 0 || _scrollPos >= _count) return false; - return the_mesh.lookupContactByPubKey(_entries[_scrollPos].pubkey_prefix, 7) != nullptr; + return the_mesh.lookupContactByPubKey(_entries[_scrollPos].pubkey_prefix, 8) != nullptr; } // Get selected entry (for add/delete operations) @@ -162,13 +162,17 @@ public: selected ? '>' : ' ', typeChar(entry.type)); display.print(prefix); - // Right side: age + hops + [+] if in contacts + // Right side: age + hops + [★] for favourites, [+] for other contacts char rightStr[20]; char ageBuf[8]; formatAge(now, entry.recv_timestamp, ageBuf, sizeof(ageBuf)); - bool inContacts = the_mesh.lookupContactByPubKey(entry.pubkey_prefix, 7) != nullptr; - if (inContacts) { + ContactInfo* ci = the_mesh.lookupContactByPubKey(entry.pubkey_prefix, 8); + bool inContacts = (ci != nullptr); + bool isFav = inContacts && (ci->flags & 0x01); + if (isFav) { + snprintf(rightStr, sizeof(rightStr), "%s %dh [*]", ageBuf, entry.path_len & 63); + } else if (inContacts) { snprintf(rightStr, sizeof(rightStr), "%s %dh [+]", ageBuf, entry.path_len & 63); } else { snprintf(rightStr, sizeof(rightStr), "%s %dh", ageBuf, entry.path_len & 63); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 245eb57..19a9328 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1378,11 +1378,16 @@ void UITask::shutdown(bool restart){ } // Disable WiFi if active - #ifdef WIFI_SSID + #if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) WiFi.disconnect(true); WiFi.mode(WIFI_OFF); #endif + // Disable 4G modem if active + #ifdef HAS_4G_MODEM + modemManager.shutdown(); + #endif + // Disable GPS if active #if ENV_INCLUDE_GPS == 1 {