mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
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:
@@ -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();
|
||||
|
||||
|
||||
@@ -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;};
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user