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..ca958f39a7 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -158,6 +158,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +## Prefs save errors + +Prefs, ACL, regions, and companion contact/channel blobs save with atomic write (temp file + rename). When a `set …` or `password …` save fails, replies are specific instead of a generic write failure: + +| Condition | Reply | +|-----------|--------| +| Partition critically full (≤2 free blocks on InternalFS) | `ERR no space left on device` | +| LittleFS returned NOSPC | Same as above | +| Other LFS error | `ERR prefs failed lfs=-NN` | +| JSON serialize failure | `ERR prefs serialize failed` | + +Stages: `open`, `write`, `rename`, `serialize`, `nospc`. + +--- + ## 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.h b/examples/simple_repeater/MyMesh.h index cac6c4a281..b17315c5b8 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -197,6 +197,7 @@ 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; } void sendSelfAdvertisement(int delay_millis, bool flood) override; void updateAdvertTimer() override; void updateFloodAdvertTimer() override; diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5cf949c6bd..0e456164f4 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -197,6 +197,7 @@ 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; } void sendSelfAdvertisement(int delay_millis, bool flood) override; void updateAdvertTimer() override; void updateFloodAdvertTimer() override; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index b5e96d5cc7..dcf51dfd01 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -61,6 +61,7 @@ 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; } 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..c3dbf9429e 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1,5 +1,6 @@ #include #include "CommonCLI.h" +#include "FsLastErr.h" #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include "TxtDataHelpers.h" @@ -140,30 +141,41 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy } } +static char s_last_prefs_save_stage[12]; + bool CommonCLI::savePrefs(FILESYSTEM* fs) { -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - fs->remove("/prefs.json"); - File file = fs->open("/prefs.json", FILE_O_WRITE); -#elif defined(RP2040_PLATFORM) - File file = fs->open("/prefs.json", "w"); -#else - File file = fs->open("/prefs.json", "w", true); -#endif - if (file) { - bool success = _prefs->saveSerial(file); - file.close(); - return success; - } - return false; + 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::savePrefs() { +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); +} + +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; } uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { @@ -256,9 +268,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)"); @@ -453,36 +468,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 +506,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 +515,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 +536,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 +559,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 +579,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 +606,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 +628,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 +636,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 +644,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 +652,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 +660,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 +672,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 +699,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 +735,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 +746,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 +784,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 +1060,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..d93a3c80f2 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -201,6 +201,7 @@ class CommonCLICallbacks { virtual const char* getBuildDate() = 0; virtual const char* getRole() = 0; virtual bool formatFileSystem() = 0; + virtual FILESYSTEM* getFileSystem() { return nullptr; } virtual void sendSelfAdvertisement(int delay_millis, bool flood) = 0; virtual void updateAdvertTimer() = 0; virtual void updateFloodAdvertTimer() = 0; @@ -260,12 +261,14 @@ 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 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..f066405b9c --- /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"); + return; + } + + if (strcmp(stage, "serialize") == 0) { + if (err != 0) { + snprintf(reply, reply_len, "ERR prefs serialize failed lfs=%d", err); + } else { + snprintf(reply, reply_len, "ERR prefs serialize failed"); + } + return; + } + + if (err != 0) { + snprintf(reply, reply_len, "ERR prefs %s failed lfs=%d", stage, err); + return; + } + + snprintf(reply, reply_len, "ERR prefs %s failed", 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"); + 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: