From 6a9bcbf1cd85a039ed447a7c0af39b03310b4486 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Fri, 6 Feb 2026 03:48:15 +0100 Subject: [PATCH 1/2] Fix orphan blob accumulation on firmware upgrade Before PR#1495, blobs were written for every advert regardless of whether the advertiser became a contact. Nodes upgrading from older firmware (e.g., v1.11.0) have thousands of orphan blobs consuming storage space. This adds a one-time cleanup on first boot that: - Iterates all blob files in /bl/ - Deletes any blob that doesn't match a current contact's pub_key - Creates a marker file to prevent running again Affects ESP32 and RP2040 platforms (which use /bl/ directory). --- examples/companion_radio/MyMesh.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 9bb747e79..a82aee65c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -864,6 +864,35 @@ void MyMesh::begin(bool has_display) { resetContacts(); _store->loadContacts(this); bootstrapRTCfromContacts(); + +#if defined(ESP32) || defined(RP2040_PLATFORM) + // One-time cleanup of orphan blobs from pre-v1.13 firmware + FILESYSTEM* fs = _store->getPrimaryFS(); + if (!fs->exists("/bl/.cleaned")) { + MESH_DEBUG_PRINTLN("Cleaning orphan blobs..."); + File root = _store->openRead("/bl"); + if (root) { + for (File f = root.openNextFile(); f; f = root.openNextFile()) { + const char* name = f.name(); + f.close(); + uint8_t key[8]; + if (name[0] != '.' && strlen(name) == 16 && mesh::Utils::fromHex(key, 8, name)) { + bool found = false; + for (int i = 0; i < num_contacts && !found; i++) + found = (memcmp(contacts[i].id.pub_key, key, 8) == 0); + if (!found) _store->deleteBlobByKey(key, 8); + } + } + root.close(); + } +#if defined(ESP32) + File m = fs->open("/bl/.cleaned", "w", true); +#else + File m = fs->open("/bl/.cleaned", "w"); +#endif + if (m) m.close(); + } +#endif addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel _store->loadChannels(this); From 8360bed7afd0dd429d365a6f5a44c4a609baae7f Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Fri, 6 Feb 2026 09:39:26 +0100 Subject: [PATCH 2/2] Move cleanup logic to DataStore --- examples/companion_radio/DataStore.cpp | 35 +++++++++++++++++++++++++- examples/companion_radio/DataStore.h | 1 + examples/companion_radio/MyMesh.cpp | 30 ++-------------------- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index c0f2c0212..ba720ff95 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -563,6 +563,7 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { return true; // this is just a stub on NRF52/STM32 platforms } +void DataStore::cleanOrphanBlobs(DataStoreHost* host) {} #else inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) { char fname[18]; @@ -606,7 +607,39 @@ bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { makeBlobPath(key, key_len, path, sizeof(path)); _fs->remove(path); - + return true; // return true even if file did not exist } + +void DataStore::cleanOrphanBlobs(DataStoreHost* host) { + if (_fs->exists("/bl/.cleaned")) return; + MESH_DEBUG_PRINTLN("Cleaning orphan blobs..."); + File root = openRead("/bl"); + if (root) { + for (File f = root.openNextFile(); f; f = root.openNextFile()) { + const char* name = f.name(); + f.close(); + if (name[0] == '.' || strlen(name) != 16) continue; + uint8_t file_key[8]; + if (!mesh::Utils::fromHex(file_key, 8, name)) continue; + bool found = false; + ContactInfo c; + for (uint32_t i = 0; host->getContactForSave(i, c) && !found; i++) { + found = (memcmp(file_key, c.id.pub_key, 8) == 0); + } + if (!found) { + char path[24]; + sprintf(path, "/bl/%s", name); + _fs->remove(path); + } + } + root.close(); + } +#if defined(ESP32) + File m = _fs->open("/bl/.cleaned", "w", true); +#else + File m = _fs->open("/bl/.cleaned", "w"); +#endif + if (m) m.close(); +} #endif diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h index 58b4d5d28..91dd329a3 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -43,6 +43,7 @@ class DataStore { uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len); bool deleteBlobByKey(const uint8_t key[], int key_len); + void cleanOrphanBlobs(DataStoreHost* host); File openRead(const char* filename); File openRead(FILESYSTEM* fs, const char* filename); bool removeFile(const char* filename); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a82aee65c..8310764fe 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -865,34 +865,8 @@ void MyMesh::begin(bool has_display) { _store->loadContacts(this); bootstrapRTCfromContacts(); -#if defined(ESP32) || defined(RP2040_PLATFORM) - // One-time cleanup of orphan blobs from pre-v1.13 firmware - FILESYSTEM* fs = _store->getPrimaryFS(); - if (!fs->exists("/bl/.cleaned")) { - MESH_DEBUG_PRINTLN("Cleaning orphan blobs..."); - File root = _store->openRead("/bl"); - if (root) { - for (File f = root.openNextFile(); f; f = root.openNextFile()) { - const char* name = f.name(); - f.close(); - uint8_t key[8]; - if (name[0] != '.' && strlen(name) == 16 && mesh::Utils::fromHex(key, 8, name)) { - bool found = false; - for (int i = 0; i < num_contacts && !found; i++) - found = (memcmp(contacts[i].id.pub_key, key, 8) == 0); - if (!found) _store->deleteBlobByKey(key, 8); - } - } - root.close(); - } -#if defined(ESP32) - File m = fs->open("/bl/.cleaned", "w", true); -#else - File m = fs->open("/bl/.cleaned", "w"); -#endif - if (m) m.close(); - } -#endif + _store->cleanOrphanBlobs(this); + addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel _store->loadChannels(this);