diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp index 0c9c97b5b9..d1954bd86d 100644 --- a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp @@ -25,6 +25,7 @@ #include #include #include "Adafruit_LittleFS.h" +#include //#include // for Serial @@ -193,6 +194,7 @@ bool Adafruit_LittleFS::remove (char const *filepath) _lockFS(); int err = lfs_remove(&_lfs, filepath); + if (err != LFS_ERR_OK) fsLastErrSet(err); PRINT_LFS_ERR(err); _unlockFS(); @@ -205,6 +207,7 @@ bool Adafruit_LittleFS::rename (char const *oldfilepath, char const *newfilepath _lockFS(); int err = lfs_rename(&_lfs, oldfilepath, newfilepath); + if (err != LFS_ERR_OK) fsLastErrSet(err); PRINT_LFS_ERR(err); _unlockFS(); @@ -217,6 +220,7 @@ bool Adafruit_LittleFS::rmdir (char const *filepath) _lockFS(); int err = lfs_remove(&_lfs, filepath); + if (err != LFS_ERR_OK) fsLastErrSet(err); PRINT_LFS_ERR(err); _unlockFS(); @@ -235,6 +239,7 @@ bool Adafruit_LittleFS::rmdir_r (char const *filepath) _lockFS(); int err = lfs_remove(&_lfs, filepath); + if (err != LFS_ERR_OK) fsLastErrSet(err); PRINT_LFS_ERR(err); _unlockFS(); diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.cpp b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.cpp index 41ba35706e..aadb696c6f 100644 --- a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.cpp +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.cpp @@ -25,6 +25,7 @@ #include #include "Adafruit_LittleFS.h" #include "littlefs/lfs.h" +#include //--------------------------------------------------------------------+ // MACRO TYPEDEF CONSTANT ENUM DECLARATION @@ -66,6 +67,7 @@ bool File::_open_file (char const *filepath, uint8_t mode) if ( rc ) { // failed to open + fsLastErrSet(rc); PRINT_LFS_ERR(rc); // free memory free(_file); @@ -92,6 +94,7 @@ bool File::_open_dir (char const *filepath) if ( rc ) { // failed to open + fsLastErrSet(rc); PRINT_LFS_ERR(rc); // free memory free(_dir); @@ -167,6 +170,7 @@ size_t File::write (uint8_t const *buf, size_t size) wrcount = lfs_file_write(_fs->_getFS(), _file, buf, size); if (wrcount < 0) { + fsLastErrSet((int) wrcount); wrcount = 0; } } @@ -340,7 +344,8 @@ void File::_close(void) } else { - lfs_file_close(this->_fs->_getFS(), _file); + int rc = lfs_file_close(this->_fs->_getFS(), _file); + if (rc != 0) fsLastErrSet(rc); free(_file); _file = NULL; } diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8772b929fe..1eb04efac4 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -7,6 +7,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - [Operational](#operational) - [Neighbors](#neighbors-repeater-only) - [Statistics](#statistics) +- [Doctor](#doctor) - [Logging](#logging) - [Information](#info) - [Configuration](#configuration) @@ -158,6 +159,38 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +## Doctor + +Admin diagnostics namespace. First commands target onboard filesystem health (InternalFS/SPIFFS); more subcommands may be added later. + +Typical recovery when `set` commands fail with space errors: + +``` +doctor stat +doctor gc +doctor check +``` + +| Command | Remote admin | Description | +|---------|--------------|-------------| +| `doctor stat` | No (USB) | Print partition free/block headroom (`FS_STAT` lines on serial) | +| `doctor gc` | Yes | Remove common cruft (`/packet_log`, legacy prefs paths, doctor temp files) | +| `doctor check` | Yes | Probe atomic prefs write (writes `/.doctor_prefs.json`, then removes it) | +| `doctor ls` | No (USB) | Recursive file listing (`FS_LS` on serial) | +| `doctor probe` | No (USB) | Write-size probe + prefs JSON write test (`FS_PROBE` on serial) | +| `doctor dump` | No (USB) | Hex dump raw flash region backing InternalFS | + +**Reply examples:** + +- `OK gc removed 3 item(s)` +- `OK prefs_writeable prefs=1 id=1 acl=0 regions=0` +- `ERR no space left on device (try: doctor gc)` +- `ERR prefs rename failed lfs=-28 (try: doctor gc)` + +Requires [#3253](https://github.com/meshcore-dev/MeshCore/pull/3253) (atomic prefs save + FS error replies). This PR adds `(try: doctor gc)` hints to those error messages. + +--- + ## Logging ### Begin capture of rx log to node storage diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 06c56a7a44..8c2edf7623 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -1,5 +1,6 @@ #include #include "DataStore.h" +#include #if defined(EXTRAFS) || defined(QSPIFLASH) #define MAX_BLOBRECS 100 @@ -31,7 +32,8 @@ DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) } #endif -static File openWrite(FILESYSTEM* fs, const char* filename) { +// One-time migration into an empty destination FS only. +static File migrateOpenWrite(FILESYSTEM* fs, const char* filename) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) fs->remove(filename); return fs->open(filename, FILE_O_WRITE); @@ -247,13 +249,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs) { } bool DataStore::savePrefs(NodePrefs& _prefs) { - File file = openWrite(_fs, "/prefs.json"); - if (file) { - bool success = _prefs.saveSerial(file); - file.close(); - return success; - } - return false; + return saveConfigJsonAtomic(_fs, _prefs, "/prefs.json", "/.prefs.json.new"); } void DataStore::loadContacts(DataStoreHost* host) { @@ -287,37 +283,43 @@ File file = openRead(_getContactsChannelsFS(), "/contacts3"); } } -void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) { - File file = openWrite(_getContactsChannelsFS(), "/contacts3"); - if (file) { - uint32_t idx = 0; - ContactInfo c; - uint8_t unused = 0; - - while (host->getContactForSave(idx, c)) { - if (filter && !filter(c)) { - idx++; // advance to next contact - continue; - } - bool success = (file.write(c.id.pub_key, 32) == 32); - success = success && (file.write((uint8_t *)&c.name, 32) == 32); - success = success && (file.write(&c.type, 1) == 1); - success = success && (file.write(&c.flags, 1) == 1); - success = success && (file.write(&unused, 1) == 1); - success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4); - success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1); - success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4); - success = success && (file.write(c.out_path, 64) == 64); - success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4); - success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4); - success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4); - - if (!success) break; // write failed - - idx++; // advance to next contact +struct SaveContactsCtx { + DataStoreHost* host; + bool (*filter)(const ContactInfo& c); +}; + +static bool writeContactsBody(File& file, void* ctx) { + SaveContactsCtx* c = (SaveContactsCtx*) ctx; + uint32_t idx = 0; + ContactInfo contact; + uint8_t unused = 0; + + while (c->host->getContactForSave(idx, contact)) { + if (c->filter && !c->filter(contact)) { + idx++; + continue; } - file.close(); + bool success = (file.write(contact.id.pub_key, 32) == 32); + success = success && (file.write((uint8_t*) &contact.name, 32) == 32); + success = success && (file.write(&contact.type, 1) == 1); + success = success && (file.write(&contact.flags, 1) == 1); + success = success && (file.write(&unused, 1) == 1); + success = success && (file.write((uint8_t*) &contact.sync_since, 4) == 4); + success = success && (file.write((uint8_t*) &contact.out_path_len, 1) == 1); + success = success && (file.write((uint8_t*) &contact.last_advert_timestamp, 4) == 4); + success = success && (file.write(contact.out_path, 64) == 64); + success = success && (file.write((uint8_t*) &contact.lastmod, 4) == 4); + success = success && (file.write((uint8_t*) &contact.gps_lat, 4) == 4); + success = success && (file.write((uint8_t*) &contact.gps_lon, 4) == 4); + if (!success) return false; + idx++; } + return true; +} + +void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) { + SaveContactsCtx ctx = {host, filter}; + writeFileAtomic(_getContactsChannelsFS(), "/contacts3", "/.contacts3.new", writeContactsBody, &ctx); } void DataStore::loadChannels(DataStoreHost* host) { @@ -345,24 +347,30 @@ void DataStore::loadChannels(DataStoreHost* host) { } } -void DataStore::saveChannels(DataStoreHost* host) { - File file = openWrite(_getContactsChannelsFS(), "/channels2"); - if (file) { - uint8_t channel_idx = 0; - ChannelDetails ch; - uint8_t unused[4]; - memset(unused, 0, 4); - - while (host->getChannelForSave(channel_idx, ch)) { - bool success = (file.write(unused, 4) == 4); - success = success && (file.write((uint8_t *)ch.name, 32) == 32); - success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32); - - if (!success) break; // write failed - channel_idx++; - } - file.close(); +struct SaveChannelsCtx { + DataStoreHost* host; +}; + +static bool writeChannelsBody(File& file, void* ctx) { + SaveChannelsCtx* c = (SaveChannelsCtx*) ctx; + uint8_t channel_idx = 0; + ChannelDetails ch; + uint8_t unused[4]; + memset(unused, 0, 4); + + while (c->host->getChannelForSave(channel_idx, ch)) { + bool success = (file.write(unused, 4) == 4); + success = success && (file.write((uint8_t*) ch.name, 32) == 32); + success = success && (file.write((uint8_t*) ch.channel.secret, 32) == 32); + if (!success) return false; + channel_idx++; } + return true; +} + +void DataStore::saveChannels(DataStoreHost* host) { + SaveChannelsCtx ctx = {host}; + writeFileAtomic(_getContactsChannelsFS(), "/channels2", "/.channels2.new", writeChannelsBody, &ctx); } #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) @@ -376,17 +384,24 @@ struct BlobRec { uint8_t data[MAX_ADVERT_PKT_LEN]; }; +struct InitAdvBlobsCtx { + int max_recs; +}; + +static bool writeAdvBlobsInitBody(File& file, void* ctx) { + InitAdvBlobsCtx* c = (InitAdvBlobsCtx*) ctx; + BlobRec zeroes; + memset(&zeroes, 0, sizeof(zeroes)); + for (int i = 0; i < c->max_recs; i++) { + if (file.write((uint8_t*) &zeroes, sizeof(zeroes)) != sizeof(zeroes)) return false; + } + return true; +} + void DataStore::checkAdvBlobFile() { if (!_getContactsChannelsFS()->exists("/adv_blobs")) { - File file = openWrite(_getContactsChannelsFS(), "/adv_blobs"); - if (file) { - BlobRec zeroes; - memset(&zeroes, 0, sizeof(zeroes)); - for (int i = 0; i < MAX_BLOBRECS; i++) { // pre-allocate to fixed size - file.write((uint8_t *) &zeroes, sizeof(zeroes)); - } - file.close(); - } + InitAdvBlobsCtx ctx = {MAX_BLOBRECS}; + writeFileAtomic(_getContactsChannelsFS(), "/adv_blobs", "/.adv_blobs.new", writeAdvBlobsInitBody, &ctx); } } @@ -395,7 +410,7 @@ void DataStore::migrateToSecondaryFS() { if (!_fsExtra->exists("/adv_blobs")) { if (_fs->exists("/adv_blobs")) { File oldAdvBlobs = openRead(_fs, "/adv_blobs"); - File newAdvBlobs = openWrite(_fsExtra, "/adv_blobs"); + File newAdvBlobs = migrateOpenWrite(_fsExtra, "/adv_blobs"); if (oldAdvBlobs && newAdvBlobs) { BlobRec rec; @@ -416,7 +431,7 @@ void DataStore::migrateToSecondaryFS() { if (!_fsExtra->exists("/contacts3")) { if (_fs->exists("/contacts3")) { File oldFile = openRead(_fs, "/contacts3"); - File newFile = openWrite(_fsExtra, "/contacts3"); + File newFile = migrateOpenWrite(_fsExtra, "/contacts3"); if (oldFile && newFile) { uint8_t buf[64]; @@ -433,7 +448,7 @@ void DataStore::migrateToSecondaryFS() { if (!_fsExtra->exists("/channels2")) { if (_fs->exists("/channels2")) { File oldFile = openRead(_fs, "/channels2"); - File newFile = openWrite(_fsExtra, "/channels2"); + File newFile = migrateOpenWrite(_fsExtra, "/channels2"); if (oldFile && newFile) { uint8_t buf[64]; @@ -451,7 +466,7 @@ void DataStore::migrateToSecondaryFS() { if (_fsExtra->exists("/_main.id")) { if (_fs->exists("/_main.id")) {_fs->remove("/_main.id");} File oldFile = openRead(_fsExtra, "/_main.id"); - File newFile = openWrite(_fs, "/_main.id"); + File newFile = migrateOpenWrite(_fs, "/_main.id"); if (oldFile && newFile) { uint8_t buf[64]; @@ -467,7 +482,7 @@ void DataStore::migrateToSecondaryFS() { if (_fsExtra->exists("/new_prefs")) { if (_fs->exists("/new_prefs")) {_fs->remove("/new_prefs");} File oldFile = openRead(_fsExtra, "/new_prefs"); - File newFile = openWrite(_fs, "/new_prefs"); + File newFile = migrateOpenWrite(_fs, "/new_prefs"); if (oldFile && newFile) { uint8_t buf[64]; @@ -578,19 +593,24 @@ uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_b return 0; // not found } +struct BlobWriteCtx { + const uint8_t* buf; + uint8_t len; +}; + +static bool writeBlobBody(File& file, void* ctx) { + BlobWriteCtx* c = (BlobWriteCtx*) ctx; + return file.write(c->buf, c->len) == c->len; +} + bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) { char path[64]; makeBlobPath(key, key_len, path, sizeof(path)); - File f = openWrite(_fs, path); - if (f) { - int n = f.write(src_buf, len); - f.close(); - if (n == len) return true; // success! - - _fs->remove(path); // blob was only partially written! - } - return false; // error + char tmp_path[72]; + snprintf(tmp_path, sizeof(tmp_path), "%s.new", path); + BlobWriteCtx ctx = {src_buf, len}; + return writeFileAtomic(_fs, path, tmp_path, writeBlobBody, &ctx); } bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a711ec0a51..f449403c62 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1029,6 +1029,18 @@ bool MyMesh::formatFileSystem() { #endif } +bool MyMesh::remountFileSystem() { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return InternalFS.begin(); +#elif defined(RP2040_PLATFORM) + return LittleFS.begin(); +#elif defined(ESP32) + return SPIFFS.begin(true); +#else + return true; +#endif +} + void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet *pkt = createSelfAdvert(); if (pkt) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index cac6c4a281..6c2172c198 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -197,6 +197,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { // CommonCLICallbacks void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override; bool formatFileSystem() override; + FILESYSTEM* getFileSystem() override { return _fs; } + bool remountFileSystem() override; void sendSelfAdvertisement(int delay_millis, bool flood) override; void updateAdvertTimer() override; void updateFloodAdvertTimer() override; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 546d094fc8..2d4cc091bc 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -793,6 +793,18 @@ bool MyMesh::formatFileSystem() { #endif } +bool MyMesh::remountFileSystem() { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return InternalFS.begin(); +#elif defined(RP2040_PLATFORM) + return LittleFS.begin(); +#elif defined(ESP32) + return SPIFFS.begin(true); +#else + return true; +#endif +} + void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet *pkt = createSelfAdvert(); if (pkt) { diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5cf949c6bd..88b6ab9039 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -197,6 +197,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { // CommonCLICallbacks void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override; bool formatFileSystem() override; + FILESYSTEM* getFileSystem() override { return _fs; } + bool remountFileSystem() override; void sendSelfAdvertisement(int delay_millis, bool flood) override; void updateAdvertTimer() override; void updateFloodAdvertTimer() override; diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 9bfa5ec6a0..630436f657 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -797,6 +797,18 @@ bool SensorMesh::formatFileSystem() { #endif } +bool SensorMesh::remountFileSystem() { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return InternalFS.begin(); +#elif defined(RP2040_PLATFORM) + return LittleFS.begin(); +#elif defined(ESP32) + return SPIFFS.begin(true); +#else + return true; +#endif +} + void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) IdentityStore store(*_fs, ""); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index b5e96d5cc7..5714384b1d 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -61,6 +61,8 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { NodePrefs* getNodePrefs() { return &_prefs; } void savePrefs() override { _cli.savePrefs(_fs); } bool formatFileSystem() override; + FILESYSTEM* getFileSystem() override { return _fs; } + bool remountFileSystem() override; void sendSelfAdvertisement(int delay_millis, bool flood) override; void updateAdvertTimer() override; void updateFloodAdvertTimer() override; diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 1282382737..c6a9b22e6b 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -1,14 +1,30 @@ #include "ClientACL.h" - -static File openWrite(FILESYSTEM* _fs, const char* filename) { - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - _fs->remove(filename); - return _fs->open(filename, FILE_O_WRITE); - #elif defined(RP2040_PLATFORM) - return _fs->open(filename, "w"); - #else - return _fs->open(filename, "w", true); - #endif +#include "ConfigSerializer.h" + +struct SaveAclCtx { + ClientACL* acl; + bool (*filter)(ClientInfo*); +}; + +static bool writeAclBody(File& file, void* ctx) { + SaveAclCtx* c = (SaveAclCtx*) ctx; + uint8_t unused[2]; + memset(unused, 0, sizeof(unused)); + + for (int i = 0; i < c->acl->getNumClients(); i++) { + auto client = c->acl->getClientByIdx(i); + if (client->permissions == 0 || (c->filter && !c->filter(client))) continue; + + bool success = (file.write(client->id.pub_key, 32) == 32); + success = success && (file.write((uint8_t*) &client->permissions, 1) == 1); + success = success && (file.write((uint8_t*) &client->extra.room.sync_since, 4) == 4); + success = success && (file.write(unused, 2) == 2); + success = success && (file.write((uint8_t*) &client->out_path_len, 1) == 1); + success = success && (file.write(client->out_path, 64) == 64); + success = success && (file.write(client->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); + if (!success) return false; + } + return true; } void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { @@ -54,27 +70,8 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { _fs = fs; - File file = openWrite(_fs, "/s_contacts"); - if (file) { - uint8_t unused[2]; - memset(unused, 0, sizeof(unused)); - - for (int i = 0; i < num_clients; i++) { - auto c = &clients[i]; - if (c->permissions == 0 || (filter && !filter(c))) continue; // skip deleted entries, or by filter function - - bool success = (file.write(c->id.pub_key, 32) == 32); - success = success && (file.write((uint8_t *) &c->permissions, 1) == 1); - success = success && (file.write((uint8_t *) &c->extra.room.sync_since, 4) == 4); - success = success && (file.write(unused, 2) == 2); - success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); - success = success && (file.write(c->out_path, 64) == 64); - success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); - - if (!success) break; // write failed - } - file.close(); - } + SaveAclCtx ctx = {this, filter}; + writeFileAtomic(_fs, "/s_contacts", "/.s_contacts.new", writeAclBody, &ctx); } bool ClientACL::clear() { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58e8..d8fd5015c9 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1,9 +1,22 @@ #include #include "CommonCLI.h" +#include "ConfigSerializer.h" +#include "FsLastErr.h" #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include "TxtDataHelpers.h" #include +#include +#if defined(NRF52_PLATFORM) + #include "flash/flash_nrf5x.h" +#elif defined(STM32_PLATFORM) + #include "InternalFileSystem.h" +#elif defined(ESP32) + #include +#endif + +static void repairFeedWatchdog() { } + #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 @@ -140,30 +153,618 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy } } +static char s_last_prefs_save_stage[12]; + bool CommonCLI::savePrefs(FILESYSTEM* fs) { + s_last_prefs_save_stage[0] = 0; + return saveConfigJsonAtomic(fs, *_prefs, "/prefs.json", "/.prefs.json.new", + s_last_prefs_save_stage, sizeof(s_last_prefs_save_stage)); +} + +#define MIN_LOCAL_ADVERT_INTERVAL 60 + +void CommonCLI::formatPrefsSaveErr(char* reply) { + const char* stage = s_last_prefs_save_stage[0] ? s_last_prefs_save_stage : "write"; + FILESYSTEM* fs = _callbacks->getFileSystem(); + fsLastErrReplyForFs(reply, 160, fsLastErrGet(), stage, fs); +} + +static bool fsHasIdentity(FILESYSTEM* fs) { +#if defined(ESP32) || defined(RP2040_PLATFORM) + return fs->exists("/identity/_main.id"); +#else + return fs->exists("/_main.id"); +#endif +} + +bool CommonCLI::savePrefs() { + if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) { + _prefs->advert_interval = 0; // turn it off, now that device has been manually configured + } + FILESYSTEM* fs = _callbacks->getFileSystem(); + if (fs) { + return savePrefs(fs); + } + _callbacks->savePrefs(); + return true; +} + +bool CommonCLI::persistPrefs(char* reply, const char* ok_msg) { + if (savePrefs()) { + strcpy(reply, ok_msg); + return true; + } + formatPrefsSaveErr(reply); + return false; +} + +bool CommonCLI::tryPrefsWrite(FILESYSTEM* fs, char* err_stage, size_t err_stage_len) { + static const char* path = "/.doctor_prefs.json"; + static const char* tmp = "/.doctor_prefs.json.new"; + repairFeedWatchdog(); + fs->remove(path); + bool success = saveConfigJsonAtomic(fs, *_prefs, path, tmp, err_stage, err_stage_len); + fs->remove(path); + fs->remove(tmp); + repairFeedWatchdog(); + return success; +} + +bool CommonCLI::checkFileSystem(char* reply) { + FILESYSTEM* fs = _callbacks->getFileSystem(); + if (!fs) { + strcpy(reply, "ERR unsupported"); + return false; + } + + bool prefs = fs->exists("/prefs.json"); + bool id = fsHasIdentity(fs); + bool acl = fs->exists("/s_contacts"); + bool regions = fs->exists("/regions2"); + + char stage[12]; + stage[0] = 0; + bool prefs_write_ok = tryPrefsWrite(fs, stage, sizeof(stage)); + if (prefs_write_ok) { + sprintf(reply, "OK prefs_writeable prefs=%d id=%d acl=%d regions=%d", prefs, id, acl, regions); + } else if (strcmp(stage, "nospc") == 0) { + fsLastErrReplyForFs(reply, 160, fsLastErrGet(), stage, fs); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + } else if (fsIsCriticallyFull(fs)) { + fsLastErrReplyForFs(reply, 160, fsLastErrGet(), stage, fs); +#endif + } else { + sprintf(reply, "ERR prefs %s failed prefs=%d id=%d acl=%d regions=%d (try: doctor gc)", + stage[0] ? stage : "write", prefs, id, acl, regions); + } + return prefs_write_ok; +} + +bool CommonCLI::wipeFileSystem(char* reply) { + repairFeedWatchdog(); + if (!_callbacks->formatFileSystem()) { + strcpy(reply, "ERR format failed"); + return false; + } + repairFeedWatchdog(); + if (!_callbacks->remountFileSystem()) { + strcpy(reply, "ERR remount failed"); + return false; + } + strcpy(reply, "OK wiped (reboot required)"); + return true; +} + +#if defined(NRF52_PLATFORM) +static bool doctorFsFlashRegion(uint32_t* addr, uint32_t* size) { +#ifdef NRF52840_XXAA + *addr = 0xED000; +#else + *addr = 0x6D000; +#endif + *size = 7u * FLASH_NRF52_PAGE_SIZE; + return true; +} +#elif defined(STM32_PLATFORM) +static bool doctorFsFlashRegion(uint32_t* addr, uint32_t* size) { + *addr = LFS_FLASH_ADDR_BASE; + *size = LFS_FLASH_TOTAL_SIZE; + return true; +} +#else +static bool doctorFsFlashRegion(uint32_t* addr, uint32_t* size) { + (void) addr; + (void) size; + return false; +} +#endif + +static void doctorFsLine(const char* fmt, ...) { + char buf[160]; + va_list args; + va_start(args, fmt); + int n = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + if (n <= 0) return; + if (n >= (int) sizeof(buf)) n = sizeof(buf) - 1; + Serial.write((const uint8_t*) buf, n); + Serial.print("\r\n"); + Serial.flush(); +} + +static void doctorFsPrintHexLine(uint32_t addr, const uint8_t* data, size_t len) { + char buf[80]; + int pos = snprintf(buf, sizeof(buf), "FS_DUMP %06X:", addr & 0xFFFFFF); + for (size_t i = 0; i < len && pos > 0 && pos < (int) sizeof(buf) - 4; i++) { + pos += snprintf(buf + pos, sizeof(buf) - pos, " %02X", data[i]); + } + doctorFsLine("%s", buf); +} + +bool CommonCLI::dumpFileSystem(char* reply) { + uint32_t base = 0; + uint32_t size = 0; + if (!doctorFsFlashRegion(&base, &size)) { + strcpy(reply, "ERR dump unsupported on this platform"); + return false; + } + + uint8_t buf[16]; + doctorFsLine("FS_DUMP begin addr=0x%X size=%u", base, size); + for (uint32_t off = 0; off < size; off += sizeof(buf)) { + repairFeedWatchdog(); + uint32_t chunk = size - off; + if (chunk > sizeof(buf)) chunk = sizeof(buf); +#if defined(NRF52_PLATFORM) + if (flash_nrf5x_read(buf, base + off, chunk) <= 0) { + doctorFsLine("FS_DUMP abort read failed"); + sprintf(reply, "ERR read failed at 0x%X", base + off); + return false; + } +#else + memcpy(buf, (void*) (base + off), chunk); +#endif + doctorFsPrintHexLine(base + off, buf, chunk); + } + doctorFsLine("FS_DUMP end"); + sprintf(reply, "OK dumped %u bytes", size); + return true; +} + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + +static int doctorFsCountBlock(void* p, lfs_block_t block) { + (void) block; + lfs_size_t* count = (lfs_size_t*) p; + (*count)++; + return 0; +} + +static lfs_ssize_t doctorFsUsedBlocks(lfs_t* lfs) { + lfs_size_t count = 0; + if (lfs_traverse(lfs, doctorFsCountBlock, &count) != 0) return -1; + return (lfs_ssize_t) count; +} + +static void doctorFsStatPath(lfs_t* lfs, const char* path) { + struct lfs_info info; + if (lfs_stat(lfs, path, &info) == 0) { + doctorFsLine("FS_STAT file %s %u", path, (unsigned) info.size); + } else { + doctorFsLine("FS_STAT file %s missing", path); + } +} + +static void doctorFsListLfs(lfs_t* lfs, const char* path, int depth) { + lfs_dir_t dir; + if (lfs_dir_open(lfs, &dir, path) < 0) { + doctorFsLine("FS_LS err open %s", path); + return; + } + + struct lfs_info info; + while (true) { + int res = lfs_dir_read(lfs, &dir, &info); + if (res <= 0) break; + if (info.name[0] == '.' && (info.name[1] == 0 || (info.name[1] == '.' && info.name[2] == 0))) continue; + + char indent[12]; + int spaces = depth * 2; + if (spaces > (int) sizeof(indent) - 1) spaces = sizeof(indent) - 1; + memset(indent, ' ', spaces); + indent[spaces] = 0; + + if (info.type == LFS_TYPE_DIR) { + doctorFsLine("FS_LS %s[dir] %s/", indent, info.name); + char sub[48]; + if (strcmp(path, "/") == 0) { + snprintf(sub, sizeof(sub), "/%s", info.name); + } else { + snprintf(sub, sizeof(sub), "%s/%s", path, info.name); + } + doctorFsListLfs(lfs, sub, depth + 1); + } else { + doctorFsLine("FS_LS %s[file] %s %u", indent, info.name, (unsigned) info.size); + } + repairFeedWatchdog(); + } + lfs_dir_close(lfs, &dir); +} + +static File doctorFsOpenWrite(FILESYSTEM* fs, const char* path) { + return fs->open(path, FILE_O_WRITE); +} + +static bool doctorFsProbeRaw(FILESYSTEM* fs, uint16_t size, char* stage, unsigned long* dt_ms) { + static const char* final = "/.doctor_probe"; + static const char* tmp = "/.doctor_probe.new"; + unsigned long t0 = millis(); + + fs->remove(final); + fs->remove(tmp); + + File file = doctorFsOpenWrite(fs, tmp); + if (!file) { + strcpy(stage, "open"); + *dt_ms = millis() - t0; + return false; + } + + uint8_t buf[64]; + memset(buf, 0xA5, sizeof(buf)); + uint16_t left = size; + while (left > 0) { + uint16_t chunk = left > sizeof(buf) ? sizeof(buf) : left; + if (file.write(buf, chunk) != chunk) { + file.close(); + fs->remove(tmp); + strcpy(stage, "write"); + *dt_ms = millis() - t0; + return false; + } + left -= chunk; + repairFeedWatchdog(); + } + file.close(); + + if (!fs->rename(tmp, final)) { + fs->remove(tmp); + strcpy(stage, "rename"); + *dt_ms = millis() - t0; + return false; + } + fs->remove(final); + stage[0] = 0; + *dt_ms = millis() - t0; + return true; +} + +#elif defined(ESP32) + +static File doctorFsOpenWrite(FILESYSTEM* fs, const char* path) { + return fs->open(path, "w", true); +} + +static bool doctorFsProbeRaw(FILESYSTEM* fs, uint16_t size, char* stage, unsigned long* dt_ms) { + static const char* final = "/.doctor_probe"; + static const char* tmp = "/.doctor_probe.new"; + unsigned long t0 = millis(); + + fs->remove(final); + fs->remove(tmp); + + File file = doctorFsOpenWrite(fs, tmp); + if (!file) { + strcpy(stage, "open"); + *dt_ms = millis() - t0; + return false; + } + + uint8_t buf[64]; + memset(buf, 0xA5, sizeof(buf)); + uint16_t left = size; + while (left > 0) { + uint16_t chunk = left > sizeof(buf) ? sizeof(buf) : left; + if (file.write(buf, chunk) != chunk) { + file.close(); + fs->remove(tmp); + strcpy(stage, "write"); + *dt_ms = millis() - t0; + return false; + } + left -= chunk; + } + file.close(); + + if (!fs->rename(tmp, final)) { + fs->remove(tmp); + strcpy(stage, "rename"); + *dt_ms = millis() - t0; + return false; + } + fs->remove(final); + stage[0] = 0; + *dt_ms = millis() - t0; + return true; +} + +#elif defined(RP2040_PLATFORM) + +static File doctorFsOpenWrite(FILESYSTEM* fs, const char* path) { + return fs->open(path, "w"); +} + +static bool doctorFsProbeRaw(FILESYSTEM* fs, uint16_t size, char* stage, unsigned long* dt_ms) { + static const char* final = "/.doctor_probe"; + static const char* tmp = "/.doctor_probe.new"; + unsigned long t0 = millis(); + + fs->remove(final); + fs->remove(tmp); + + File file = doctorFsOpenWrite(fs, tmp); + if (!file) { + strcpy(stage, "open"); + *dt_ms = millis() - t0; + return false; + } + + uint8_t buf[64]; + memset(buf, 0xA5, sizeof(buf)); + uint16_t left = size; + while (left > 0) { + uint16_t chunk = left > sizeof(buf) ? sizeof(buf) : left; + if (file.write(buf, chunk) != chunk) { + file.close(); + fs->remove(tmp); + strcpy(stage, "write"); + *dt_ms = millis() - t0; + return false; + } + left -= chunk; + } + file.close(); + + if (!fs->rename(tmp, final)) { + fs->remove(tmp); + strcpy(stage, "rename"); + *dt_ms = millis() - t0; + return false; + } + fs->remove(final); + stage[0] = 0; + *dt_ms = millis() - t0; + return true; +} + +#endif + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32) || defined(RP2040_PLATFORM) + +static void doctorFsListGeneric(FILESYSTEM* fs) { + File root = fs->open("/"); + if (!root) { + doctorFsLine("FS_LS err open /"); + return; + } + File file = root.openNextFile(); + while (file) { + if (file.isDirectory()) { + doctorFsLine("FS_LS [dir] %s/", file.name()); + } else { + doctorFsLine("FS_LS [file] %s %d", file.name(), file.size()); + } + repairFeedWatchdog(); + file = root.openNextFile(); + } + root.close(); +} + +bool CommonCLI::statFileSystem(char* reply) { + FILESYSTEM* fs = _callbacks->getFileSystem(); + if (!fs) { + strcpy(reply, "ERR unsupported"); + return false; + } + + doctorFsLine("FS_STAT begin"); #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - fs->remove("/prefs.json"); - File file = fs->open("/prefs.json", FILE_O_WRITE); + lfs_t* lfs = fs->_getFS(); + const lfs_config* cfg = lfs->cfg; + lfs_ssize_t used_blocks = doctorFsUsedBlocks(lfs); + uint32_t block_count = cfg->block_count; + uint32_t block_size = cfg->block_size; + uint32_t total_bytes = block_count * block_size; + uint32_t used_bytes = used_blocks >= 0 ? (uint32_t) used_blocks * block_size : 0; + uint32_t free_bytes = used_blocks >= 0 && used_bytes <= total_bytes ? total_bytes - used_bytes : 0; + + doctorFsLine("FS_STAT total %u", total_bytes); + doctorFsLine("FS_STAT used~ %u free~ %u", used_bytes, free_bytes); + doctorFsLine("FS_STAT blocks %u used %ld bsize %u", block_count, (long) used_blocks, block_size); + doctorFsStatPath(lfs, "/prefs.json"); + doctorFsStatPath(lfs, "/_main.id"); + doctorFsStatPath(lfs, "/s_contacts"); + doctorFsStatPath(lfs, "/regions2"); + sprintf(reply, "OK free~=%u/%u blk=%ld/%u", free_bytes, total_bytes, (long) used_blocks, block_count); +#elif defined(ESP32) + uint32_t total_bytes = SPIFFS.totalBytes(); + uint32_t used_bytes = SPIFFS.usedBytes(); + uint32_t free_bytes = total_bytes - used_bytes; + doctorFsLine("FS_STAT total %u used %u free %u", total_bytes, used_bytes, free_bytes); + static const char* paths[] = {"/prefs.json", "/identity/_main.id", "/s_contacts", "/regions2", NULL}; + for (int i = 0; paths[i]; i++) { + if (fs->exists(paths[i])) { + File f = fs->open(paths[i]); + doctorFsLine("FS_STAT file %s %d", paths[i], f ? (int) f.size() : -1); + if (f) f.close(); + } else { + doctorFsLine("FS_STAT file %s missing", paths[i]); + } + } + sprintf(reply, "OK free=%u/%u", free_bytes, total_bytes); #elif defined(RP2040_PLATFORM) - File file = fs->open("/prefs.json", "w"); + FSInfo info; + fs->info(info); + doctorFsLine("FS_STAT total %u used %u free %u", info.totalBytes, info.usedBytes, info.totalBytes - info.usedBytes); + sprintf(reply, "OK free=%u/%u", info.totalBytes - info.usedBytes, info.totalBytes); #else - File file = fs->open("/prefs.json", "w", true); + strcpy(reply, "OK"); #endif - if (file) { - bool success = _prefs->saveSerial(file); - file.close(); - return success; + doctorFsLine("FS_STAT end"); + return true; +} + +bool CommonCLI::listFileSystem(char* reply) { + FILESYSTEM* fs = _callbacks->getFileSystem(); + if (!fs) { + strcpy(reply, "ERR unsupported"); + return false; } - return false; + + doctorFsLine("FS_LS begin /"); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + doctorFsListLfs(fs->_getFS(), "/", 0); +#else + doctorFsListGeneric(fs); +#endif + doctorFsLine("FS_LS end"); + strcpy(reply, "OK see serial FS_LS"); + return true; } -#define MIN_LOCAL_ADVERT_INTERVAL 60 +bool CommonCLI::probeFileSystem(char* reply) { + FILESYSTEM* fs = _callbacks->getFileSystem(); + if (!fs) { + strcpy(reply, "ERR unsupported"); + return false; + } -void CommonCLI::savePrefs() { - if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) { - _prefs->advert_interval = 0; // turn it off, now that device has been manually configured + static const uint16_t sizes[] = { + 1, 2, 4, 8, 10, 16, 32, 64, 100, 128, 256, 512, 768, 1000, 1280, 1536, + 1800, 2048, 2304, 2560, 2800, 3072, 3584, 4096 + }; + + doctorFsLine("FS_PROBE begin raw"); + uint16_t max_ok = 0; + uint16_t first_fail = 0; + char fail_stage[12]; + fail_stage[0] = 0; + + for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { + char stage[12]; + unsigned long dt = 0; + bool ok = doctorFsProbeRaw(fs, sizes[i], stage, &dt); + if (ok) { + doctorFsLine("FS_PROBE raw %u ok %lu", sizes[i], dt); + max_ok = sizes[i]; + } else { + doctorFsLine("FS_PROBE raw %u fail %s %lu", sizes[i], stage, dt); + if (first_fail == 0) { + first_fail = sizes[i]; + strncpy(fail_stage, stage, sizeof(fail_stage) - 1); + fail_stage[sizeof(fail_stage) - 1] = 0; + } + } + repairFeedWatchdog(); + } + + char prefs_stage[12]; + prefs_stage[0] = 0; + unsigned long prefs_dt = millis(); + bool prefs_ok = tryPrefsWrite(fs, prefs_stage, sizeof(prefs_stage)); + prefs_dt = millis() - prefs_dt; + doctorFsLine("FS_PROBE prefs_json %s %s %lu", + prefs_ok ? "ok" : "fail", prefs_stage[0] ? prefs_stage : "-", prefs_dt); + doctorFsLine("FS_PROBE end"); + + if (first_fail == 0) { + sprintf(reply, "OK raw_max=%u prefs=%s", max_ok, prefs_ok ? "ok" : "fail"); + } else { + sprintf(reply, "OK raw_max=%u fail>=%u@%s prefs=%s", + max_ok, first_fail, fail_stage[0] ? fail_stage : "?", prefs_ok ? "ok" : "fail"); + } + return true; +} + +#endif + +bool CommonCLI::gcFileSystem(char* reply) { + FILESYSTEM* fs = _callbacks->getFileSystem(); + if (!fs) { + strcpy(reply, "ERR unsupported"); + return false; + } + + repairFeedWatchdog(); + uint32_t removed = 0; + + static const char* files[] = { + "/packet_log", + "/com_prefs", + "/.doctor_prefs.json", + "/.doctor_prefs.json.new", + "/.doctor_probe", + "/.doctor_probe.new", + NULL + }; + + for (int i = 0; files[i]; i++) { + if (fs->exists(files[i])) { + fs->remove(files[i]); + removed++; + repairFeedWatchdog(); + } + } + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + if (fs->exists("/prefs")) { + fs->rmdir_r("/prefs"); + removed++; + } +#endif + + sprintf(reply, "OK gc removed %u item(s)", removed); + return true; +} + +void CommonCLI::handleDoctor(uint32_t sender_timestamp, const char* args, char* reply) { + while (*args == ' ') args++; + + if (*args == 0) { + strcpy(reply, "usage: doctor check|stat|ls|probe|dump|gc"); + } else if (memcmp(args, "gc", 2) == 0 && (args[2] == 0 || args[2] == ' ')) { + gcFileSystem(reply); + } else if (memcmp(args, "check", 5) == 0 && (args[5] == 0 || args[5] == ' ')) { + checkFileSystem(reply); + } else if (memcmp(args, "dump", 4) == 0 && (args[4] == 0 || args[4] == ' ')) { + if (sender_timestamp != 0) { + strcpy(reply, "ERR dump requires USB"); + } else { + dumpFileSystem(reply); + } +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32) || defined(RP2040_PLATFORM) + } else if (memcmp(args, "stat", 4) == 0 && (args[4] == 0 || args[4] == ' ')) { + if (sender_timestamp != 0) { + strcpy(reply, "ERR stat requires USB"); + } else { + statFileSystem(reply); + } + } else if (memcmp(args, "ls", 2) == 0 && (args[2] == 0 || args[2] == ' ')) { + if (sender_timestamp != 0) { + strcpy(reply, "ERR ls requires USB"); + } else { + listFileSystem(reply); + } + } else if (memcmp(args, "probe", 5) == 0 && (args[5] == 0 || args[5] == ' ')) { + if (sender_timestamp != 0) { + strcpy(reply, "ERR probe requires USB"); + } else { + probeFileSystem(reply); + } +#endif + } else { + strcpy(reply, "usage: doctor check|stat|ls|probe|dump|gc"); } - _callbacks->savePrefs(); } uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { @@ -256,9 +857,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "password ", 9) == 0) { // change admin password StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password)); - savePrefs(); - sprintf(reply, "password now: "); - StrHelper::strncpy(&reply[14], _prefs->password, 160-15); // echo back just to let admin know for sure!! + if (!savePrefs()) { + formatPrefsSaveErr(reply); + } else { + sprintf(reply, "password now: "); + StrHelper::strncpy(&reply[14], _prefs->password, 160-15); // echo back just to let admin know for sure!! + } } else if (memcmp(command, "clear stats", 11) == 0) { _callbacks->clearStats(); strcpy(reply, "(OK - stats reset)"); @@ -266,9 +870,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re handleGetCmd(sender_timestamp, command, reply); } else if (memcmp(command, "set ", 4) == 0) { handleSetCmd(sender_timestamp, command, reply); + } else if (memcmp(command, "doctor", 6) == 0 && (command[6] == 0 || command[6] == ' ')) { + handleDoctor(sender_timestamp, &command[6], reply); } else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) { - bool s = _callbacks->formatFileSystem(); - sprintf(reply, "File system erase: %s", s ? "OK" : "Err"); + if (_callbacks->getFileSystem()) { + wipeFileSystem(reply); + } else { + bool s = _callbacks->formatFileSystem(); + sprintf(reply, "File system erase: %s", s ? "OK" : "Err"); + } } else if (memcmp(command, "ver", 3) == 0) { sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate()); } else if (memcmp(command, "board", 5) == 0) { @@ -453,36 +1063,37 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "ERROR: dutycycle must be 1-100"); } else { _prefs->airtime_factor = (100.0f / dc) - 1.0f; - savePrefs(); - float actual = 100.0f / (_prefs->airtime_factor + 1.0f); - int a_int = (int)actual; - int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); - sprintf(reply, "OK - %d.%d%%", a_int, a_frac); + if (!savePrefs()) { + formatPrefsSaveErr(reply); + } else { + float actual = 100.0f / (_prefs->airtime_factor + 1.0f); + int a_int = (int)actual; + int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); + sprintf(reply, "OK - %d.%d%%", a_int, a_frac); + } } } else if (memcmp(config, "af ", 3) == 0) { _prefs->airtime_factor = atof(&config[3]); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "int.thresh ", 11) == 0) { _prefs->interference_threshold = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "cad ", 4) == 0) { _prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { _prefs->agc_reset_interval = atoi(&config[19]) / 4; - savePrefs(); - sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4); + if (!savePrefs()) { + formatPrefsSaveErr(reply); + } else { + sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4); + } } else if (memcmp(config, "multi.acks ", 11) == 0) { _prefs->multi_acks = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "allow.read.only ", 16) == 0) { _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { int hours = _atoi(&config[22]); if ((hours > 0 && hours < 3) || (hours > 168)) { @@ -490,8 +1101,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { _prefs->flood_advert_interval = (uint8_t)(hours); _callbacks->updateFloodAdvertTimer(); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } } else if (memcmp(config, "advert.interval ", 16) == 0) { int mins = _atoi(&config[16]); @@ -500,13 +1110,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { _prefs->advert_interval = (uint8_t)(mins / 2); _callbacks->updateAdvertTimer(); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } } else if (memcmp(config, "guest.password ", 15) == 0) { StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password)); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "prv.key ", 8) == 0) { uint8_t prv_key[PRV_KEY_SIZE]; bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]); @@ -523,20 +1131,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(config, "name ", 5) == 0) { if (isValidName(&config[5])) { StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error, bad chars"); } } else if (memcmp(config, "repeat ", 7) == 0) { _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; - savePrefs(); - strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); + persistPrefs(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); } else if (memcmp(config, "radio.rxgain ", 13) == 0) { bool enabled = memcmp(&config[13], "on", 2) == 0; _prefs->rx_boosted_gain = enabled; - savePrefs(); - if (_callbacks->setRxBoostedGain(enabled)) { + if (!savePrefs()) { + formatPrefsSaveErr(reply); + } else if (_callbacks->setRxBoostedGain(enabled)) { strcpy(reply, "OK"); } else { strcpy(reply, "Error: unsupported"); @@ -547,16 +1154,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(&config[17], "on", 2) == 0) { if (_board->setLoRaFemLnaEnabled(true)) { _prefs->radio_fem_rxgain = 1; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain on"); + persistPrefs(reply, "OK - LoRa FEM RX gain on"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); } } else if (memcmp(&config[17], "off", 3) == 0) { if (_board->setLoRaFemLnaEnabled(false)) { _prefs->radio_fem_rxgain = 0; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain off"); + persistPrefs(reply, "OK - LoRa FEM RX gain off"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); } @@ -569,16 +1174,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(&config[17], "on", 2) == 0) { if (_board->setLoRaFemPaGainEnabled(true)) { _prefs->radio_fem_txgain = 1; - savePrefs(); - strcpy(reply, "OK - LoRa FEM TX gain on"); + persistPrefs(reply, "OK - LoRa FEM TX gain on"); } else { strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); } } else if (memcmp(&config[17], "off", 3) == 0) { if (_board->setLoRaFemPaGainEnabled(false)) { _prefs->radio_fem_txgain = 0; - savePrefs(); - strcpy(reply, "OK - LoRa FEM TX gain off"); + persistPrefs(reply, "OK - LoRa FEM TX gain off"); } else { strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); } @@ -598,25 +1201,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->cr = cr; _prefs->freq = freq; _prefs->bw = bw; - _callbacks->savePrefs(); - strcpy(reply, "OK - reboot to apply"); + persistPrefs(reply, "OK - reboot to apply"); } else { strcpy(reply, "Error, invalid radio params"); } } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "lon ", 4) == 0) { _prefs->node_lon = atof(&config[4]); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "rxdelay ", 8) == 0) { float db = atof(&config[8]); if (db >= 0 && db <= 20.0f) { _prefs->rx_delay_base = db; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error, must be 0-20"); } @@ -624,8 +1223,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep float f = atof(&config[8]); if (f >= 0 && f <= 2.0f) { _prefs->tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error, must be 0-2"); } @@ -633,8 +1231,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep uint8_t m = atoi(&config[19]); if (m <= 64) { _prefs->flood_max_unscoped = m; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error, max 64"); } @@ -642,8 +1239,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep uint8_t m = atoi(&config[17]); if (m <= 64) { _prefs->flood_max_advert = m; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error, max 64"); } @@ -651,8 +1247,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep uint8_t m = atoi(&config[10]); if (m <= 64) { _prefs->flood_max = m; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error, max 64"); } @@ -660,8 +1255,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep float f = atof(&config[15]); if (f >= 0 && f <= 2.0f) { _prefs->direct_tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error, must be 0-2"); } @@ -673,15 +1267,13 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep config++; } *dp = 0; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "path.hash.mode ", 15) == 0) { config += 15; uint8_t mode = atoi(config); if (mode < 3) { _prefs->path_hash_mode = mode; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error, must be 0,1, or 2"); } @@ -702,37 +1294,35 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } if (mode != 0xFF) { _prefs->loop_detect = mode; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } } else if (memcmp(config, "tx ", 3) == 0) { _prefs->tx_power_dbm = atoi(&config[3]); - savePrefs(); - _callbacks->setTxPower(_prefs->tx_power_dbm); - strcpy(reply, "OK"); + if (savePrefs()) { + _callbacks->setTxPower(_prefs->tx_power_dbm); + strcpy(reply, "OK"); + } else { + formatPrefsSaveErr(reply); + } } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { _prefs->freq = atof(&config[5]); - savePrefs(); - strcpy(reply, "OK - reboot to apply"); + persistPrefs(reply, "OK - reboot to apply"); #ifdef WITH_BRIDGE } else if (memcmp(config, "bridge.enabled ", 15) == 0) { _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0; _callbacks->setBridgeState(_prefs->bridge_enabled); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else if (memcmp(config, "bridge.delay ", 13) == 0) { int delay = _atoi(&config[13]); if (delay >= 0 && delay <= 10000) { _prefs->bridge_delay = (uint16_t)delay; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error: delay must be between 0-10000 ms"); } } else if (memcmp(config, "bridge.source ", 14) == 0) { _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); #endif #ifdef WITH_RS232_BRIDGE } else if (memcmp(config, "bridge.baud ", 12) == 0) { @@ -740,8 +1330,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) { _prefs->bridge_baud = (uint32_t)baud; _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); } @@ -752,22 +1341,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep if (ch > 0 && ch < 15) { _prefs->bridge_channel = (uint8_t)ch; _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); } else { strcpy(reply, "Error: channel must be between 1-14"); } } else if (memcmp(config, "bridge.secret ", 14) == 0) { StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); + persistPrefs(reply, "OK"); #endif } else if (memcmp(config, "adc.multiplier ", 15) == 0) { _prefs->adc_multiplier = atof(&config[15]); if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { - savePrefs(); - if (_prefs->adc_multiplier == 0.0f) { + if (!savePrefs()) { + formatPrefsSaveErr(reply); + } else if (_prefs->adc_multiplier == 0.0f) { strcpy(reply, "OK - using default board multiplier"); } else { sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); @@ -791,8 +1379,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep sideDetSFs[num] = 0; if (_callbacks->configSideDetectors(sideDetSFs, num, _prefs->bw)) { for (int i = 0; i <= num; i++) _prefs->extra_sf[i] = sideDetSFs[i]; - savePrefs(); - sprintf(reply, "OK - extra SFs set"); + if (savePrefs()) { + sprintf(reply, "OK - extra SFs set"); + } else { + formatPrefsSaveErr(reply); + } } else { sprintf(reply, "Invalid extra SF config"); } @@ -1064,9 +1655,12 @@ void CommonCLI::handleRegionCmd(char* command, char* reply) { _callbacks->startRegionsLoad(); } else if (n >= 2 && strcmp(parts[1], "save") == 0) { _prefs->discovery_mod_timestamp = getRTCClock()->getCurrentTime(); // this node is now 'modified' (for discovery info) - savePrefs(); - bool success = _callbacks->saveRegions(); - strcpy(reply, success ? "OK" : "Err - save failed"); + if (!savePrefs()) { + formatPrefsSaveErr(reply); + } else { + bool success = _callbacks->saveRegions(); + strcpy(reply, success ? "OK" : "Err - save failed"); + } } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) { auto region = _region_map->findByNamePrefix(parts[2]); if (region) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 237c758e9f..854c9c8220 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -201,6 +201,8 @@ class CommonCLICallbacks { virtual const char* getBuildDate() = 0; virtual const char* getRole() = 0; virtual bool formatFileSystem() = 0; + virtual FILESYSTEM* getFileSystem() { return nullptr; } + virtual bool remountFileSystem() { return true; } virtual void sendSelfAdvertisement(int delay_millis, bool flood) = 0; virtual void updateAdvertTimer() = 0; virtual void updateFloodAdvertTimer() = 0; @@ -260,12 +262,23 @@ class CommonCLI { char tmp[PRV_KEY_SIZE*2 + 4]; mesh::RTCClock* getRTCClock() { return _rtc; } - void savePrefs(); + bool savePrefs(); + bool persistPrefs(char* reply, const char* ok_msg); void loadPrefsInt(FILESYSTEM* _fs, const char* filename); void handleRegionCmd(char* command, char* reply); void handleGetCmd(uint32_t sender_timestamp, char* command, char* reply); void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply); + void handleDoctor(uint32_t sender_timestamp, const char* args, char* reply); + bool checkFileSystem(char* reply); + bool tryPrefsWrite(FILESYSTEM* fs, char* err_stage, size_t err_stage_len); + bool wipeFileSystem(char* reply); + bool dumpFileSystem(char* reply); + bool statFileSystem(char* reply); + bool listFileSystem(char* reply); + bool probeFileSystem(char* reply); + bool gcFileSystem(char* reply); + void formatPrefsSaveErr(char* reply); public: CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks) diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index adff147f47..09d38c4af4 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -1,4 +1,98 @@ #include "ConfigSerializer.h" +#include +#include +#include "FsLastErr.h" + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + #include "littlefs/lfs.h" +#endif + +static File openNewFile(FILESYSTEM* fs, const char* path) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return fs->open(path, FILE_O_WRITE); +#elif defined(RP2040_PLATFORM) + return fs->open(path, "w"); +#else + return fs->open(path, "w", true); +#endif +} + +static void setAtomicErr(char* err_stage, size_t err_stage_len, const char* msg) { + if (err_stage && err_stage_len > 0) { + strncpy(err_stage, msg, err_stage_len - 1); + err_stage[err_stage_len - 1] = 0; + } +} + +static void mapAtomicErr(char* err_stage, size_t err_stage_len, const char* fallback) { + fsLastErrStage(err_stage, err_stage_len, fsLastErrGet(), fallback); +} + +bool writeFileAtomic(FILESYSTEM* fs, const char* final_path, const char* tmp_path, FileWriteFn writer, void* ctx, + char* err_stage, size_t err_stage_len) { + if (!fs || !final_path || !tmp_path || !writer) return false; + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + if (fsIsCriticallyFull(fs)) { + setAtomicErr(err_stage, err_stage_len, "nospc"); + return false; + } +#endif + + fsLastErrClear(); + fs->remove(tmp_path); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + if (fsLastErrGet() == LFS_ERR_NOENT) fsLastErrClear(); +#endif + File file = openNewFile(fs, tmp_path); + if (!file) { + mapAtomicErr(err_stage, err_stage_len, "open"); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + if (fsLastErrGet() == 0 && fsIsCriticallyFull(fs)) setAtomicErr(err_stage, err_stage_len, "nospc"); +#endif + return false; + } + bool success = writer(file, ctx); + file.close(); + if (fsLastErrGet() != 0) success = false; + if (!success) { + fs->remove(tmp_path); + mapAtomicErr(err_stage, err_stage_len, "write"); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + if (fsLastErrGet() == 0 && fsIsCriticallyFull(fs)) setAtomicErr(err_stage, err_stage_len, "nospc"); +#endif + return false; + } + if (!fs->rename(tmp_path, final_path)) { + fs->remove(tmp_path); + mapAtomicErr(err_stage, err_stage_len, "rename"); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + if (fsLastErrGet() == 0 && fsIsCriticallyFull(fs)) setAtomicErr(err_stage, err_stage_len, "nospc"); +#endif + return false; + } + return true; +} + +struct SaveSerialCtx { + ConfigSerializer* obj; +}; + +static bool saveSerialWriter(File& file, void* ctx) { + return ((SaveSerialCtx*) ctx)->obj->saveSerial(file); +} + +bool saveConfigJsonAtomic(FILESYSTEM* fs, ConfigSerializer& obj, const char* final_path, const char* tmp_path, + char* err_stage, size_t err_stage_len) { + SaveSerialCtx ctx = {&obj}; + if (!writeFileAtomic(fs, final_path, tmp_path, saveSerialWriter, &ctx, err_stage, err_stage_len)) { + if (err_stage && err_stage_len > 0 && strcmp(err_stage, "write") == 0 && fsLastErrGet() == 0) { + setAtomicErr(err_stage, err_stage_len, "serialize"); + } + return false; + } + return true; +} bool ConfigSerializer::saveSerial(Stream& s) { Context context(&s, OP::WRITE); diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 7e6d6f2a69..830a6567e6 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -66,3 +66,15 @@ class ConfigSerializer { bool loadSerial(Stream& s); bool saveSerial(Stream& s); }; + +#include "IdentityStore.h" + +typedef bool (*FileWriteFn)(File& file, void* ctx); + +// Write to tmp_path via writer, then lfs_rename over final_path. Keeps the old file on failed writes. +bool writeFileAtomic(FILESYSTEM* fs, const char* final_path, const char* tmp_path, FileWriteFn writer, void* ctx, + char* err_stage = nullptr, size_t err_stage_len = 0); + +// Write JSON to tmp_path, then lfs_rename over final_path. Keeps the old file on failed writes. +bool saveConfigJsonAtomic(FILESYSTEM* fs, ConfigSerializer& obj, const char* final_path, const char* tmp_path, + char* err_stage = nullptr, size_t err_stage_len = 0); diff --git a/src/helpers/FsLastErr.cpp b/src/helpers/FsLastErr.cpp new file mode 100644 index 0000000000..b502608931 --- /dev/null +++ b/src/helpers/FsLastErr.cpp @@ -0,0 +1,105 @@ +#include "FsLastErr.h" +#include +#include + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + #include + #include "littlefs/lfs.h" + #ifndef LFS_ERR_NOSPC + #define LFS_ERR_NOSPC (-28) + #endif +#endif + +static int s_last_lfs_err = 0; + +void fsLastErrClear() { + s_last_lfs_err = 0; +} + +void fsLastErrSet(int err) { + if (err != 0) s_last_lfs_err = err; +} + +int fsLastErrGet() { + return s_last_lfs_err; +} + +static bool fsErrIsNospc(int err) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return err == LFS_ERR_NOSPC; +#else + (void) err; + return false; +#endif +} + +void fsLastErrStage(char* stage, size_t stage_len, int err, const char* fallback_stage) { + if (!stage || stage_len == 0) return; + if (fsErrIsNospc(err)) { + strncpy(stage, "nospc", stage_len - 1); + } else if (fallback_stage && fallback_stage[0]) { + strncpy(stage, fallback_stage, stage_len - 1); + } else { + strncpy(stage, "write", stage_len - 1); + } + stage[stage_len - 1] = 0; +} + +void fsLastErrReply(char* reply, size_t reply_len, int err, const char* fallback_stage) { + if (!reply || reply_len == 0) return; + + const char* stage = (fallback_stage && fallback_stage[0]) ? fallback_stage : "write"; + + if (fsErrIsNospc(err) || strcmp(stage, "nospc") == 0) { + snprintf(reply, reply_len, "ERR no space left on device (try: doctor gc)"); + return; + } + + if (strcmp(stage, "serialize") == 0) { + if (err != 0) { + snprintf(reply, reply_len, "ERR prefs serialize failed lfs=%d (try: doctor gc)", err); + } else { + snprintf(reply, reply_len, "ERR prefs serialize failed (try: doctor gc)"); + } + return; + } + + if (err != 0) { + snprintf(reply, reply_len, "ERR prefs %s failed lfs=%d (try: doctor gc)", stage, err); + return; + } + + snprintf(reply, reply_len, "ERR prefs %s failed (try: doctor gc)", stage); +} + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + +static int fsCountBlock(void* p, lfs_block_t block) { + (void) block; + lfs_size_t* count = (lfs_size_t*) p; + (*count)++; + return 0; +} + +bool fsIsCriticallyFull(FILESYSTEM* fs) { + if (!fs) return false; + lfs_t* lfs = fs->_getFS(); + if (!lfs || !lfs->cfg) return false; + lfs_size_t used = 0; + if (lfs_traverse(lfs, fsCountBlock, &used) != 0) return false; + return used + 2 >= lfs->cfg->block_count; +} + +#endif + +void fsLastErrReplyForFs(char* reply, size_t reply_len, int err, const char* stage, FILESYSTEM* fs) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + if (err == 0 && fs && fsIsCriticallyFull(fs)) { + snprintf(reply, reply_len, "ERR no space left on device (try: doctor gc)"); + return; + } +#else + (void) fs; +#endif + fsLastErrReply(reply, reply_len, err, stage); +} diff --git a/src/helpers/FsLastErr.h b/src/helpers/FsLastErr.h new file mode 100644 index 0000000000..892de04e5c --- /dev/null +++ b/src/helpers/FsLastErr.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32) || defined(RP2040_PLATFORM) + #include "IdentityStore.h" +#endif + +void fsLastErrClear(); +void fsLastErrSet(int err); +int fsLastErrGet(); + +void fsLastErrStage(char* stage, size_t stage_len, int err, const char* fallback_stage); + +void fsLastErrReply(char* reply, size_t reply_len, int err, const char* fallback_stage); + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +bool fsIsCriticallyFull(FILESYSTEM* fs); +#endif + +void fsLastErrReplyForFs(char* reply, size_t reply_len, int err, const char* stage, FILESYSTEM* fs); diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 4667e0038e..13508035bd 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -1,5 +1,7 @@ #include "RegionMap.h" #include +#include +#include #include // helper class for region map exporter, we emulate Stream with a safe buffer writer. @@ -58,15 +60,31 @@ static const char* skip_hash(const char* name) { return *name == '#' ? name + 1 : name; } -static File openWrite(FILESYSTEM* _fs, const char* filename) { - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - _fs->remove(filename); - return _fs->open(filename, FILE_O_WRITE); - #elif defined(RP2040_PLATFORM) - return _fs->open(filename, "w"); - #else - return _fs->open(filename, "w", true); - #endif +bool RegionMap::saveBodyWriter(File& file, void* ctx) { + return ((RegionMap*) ctx)->writeSaveBody(file); +} + +bool RegionMap::writeSaveBody(File& file) const { + uint8_t pad[128]; + memset(pad, 0, sizeof(pad)); + + bool success = file.write(pad, 3) == 3; + success = success && file.write((uint8_t*) &default_id, sizeof(default_id)) == sizeof(default_id); + success = success && file.write((uint8_t*) &home_id, sizeof(home_id)) == sizeof(home_id); + success = success && file.write((uint8_t*) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); + success = success && file.write((uint8_t*) &next_id, sizeof(next_id)) == sizeof(next_id); + if (!success) return false; + + for (int i = 0; i < num_regions; i++) { + auto r = ®ions[i]; + success = file.write((uint8_t*) &r->id, sizeof(r->id)) == sizeof(r->id); + success = success && file.write((uint8_t*) &r->parent, sizeof(r->parent)) == sizeof(r->parent); + success = success && file.write((uint8_t*) r->name, sizeof(r->name)) == sizeof(r->name); + success = success && file.write((uint8_t*) &r->flags, sizeof(r->flags)) == sizeof(r->flags); + success = success && file.write(pad, sizeof(pad)) == sizeof(pad); + if (!success) return false; + } + return true; } bool RegionMap::load(FILESYSTEM* _fs, const char* path) { @@ -117,33 +135,10 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) { } bool RegionMap::save(FILESYSTEM* _fs, const char* path) { - File file = openWrite(_fs, path ? path : "/regions2"); - if (file) { - uint8_t pad[128]; - memset(pad, 0, sizeof(pad)); - - bool success = file.write(pad, 3) == 3; // reserved header - success = success && file.write((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id); - success = success && file.write((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id); - success = success && file.write((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); - success = success && file.write((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id); - - if (success) { - for (int i = 0; i < num_regions; i++) { - auto r = ®ions[i]; - - success = file.write((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id); - success = success && file.write((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent); - success = success && file.write((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name); - success = success && file.write((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags); - success = success && file.write(pad, sizeof(pad)) == sizeof(pad); - if (!success) break; // write failed - } - } - file.close(); - return success; - } - return false; // failed + const char* final_path = path ? path : "/regions2"; + char tmp_path[32]; + snprintf(tmp_path, sizeof(tmp_path), "/.%s.new", final_path + 1); + return writeFileAtomic(_fs, final_path, tmp_path, saveBodyWriter, this); } RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t id) { diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h index 5eb1442983..11208dc8ce 100644 --- a/src/helpers/RegionMap.h +++ b/src/helpers/RegionMap.h @@ -27,6 +27,8 @@ class RegionMap { RegionEntry regions[MAX_REGION_ENTRIES]; RegionEntry wildcard; + bool writeSaveBody(File& file) const; + static bool saveBodyWriter(File& file, void* ctx); void printChildRegions(int indent, const RegionEntry* parent, Stream& out) const; public: