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

This commit is contained in:
pelgraine
2026-03-20 05:27:20 +11:00
parent 8e1f2a3a87
commit 5bed26cb72
6 changed files with 157 additions and 10 deletions

View File

@@ -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();

View File

@@ -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;};
};
};

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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
{