Fix storage recovery and deferred runtime state

Recover usable ESP32 preferences and channels before permitting replacement; keep identity and ACL persistence transactional and complete.

Preserve MQTT commit boundaries, restore OTA identity and policy after deferred allocation, retain radio gain retries, and reject invalid flood limits. Add production-path fault regressions and CI coverage.
This commit is contained in:
mikecarper committed 2026-09-15 16:46:17 -07:00
1 parent 86defc840c
commit e2a1828987
32 files changed
+1546 -189

No files matched your search

+63 -20
View File
@@ -4,6 +4,8 @@
#include "ClientLoginPersistence.h"
#include "ClientPathPersistence.h"
#include "FileRead.h"
#include "FilePresence.h"
#include <initializer_list>
#if defined(NRF52_PLATFORM)
#include "AtomicFileWriter.h"
#endif
@@ -570,7 +572,14 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
_fs = fs;
num_clients = 0;
login_replay_store_available = false;
acl_load_complete = false;
if (_fs == NULL) return;
bool primary_exists = false, backup_exists = false;
if (!mesh::filePresence(_fs, mesh::CLIENT_ACL_PRIMARY_PATH, primary_exists)
|| !mesh::filePresence(_fs, mesh::CLIENT_ACL_BACKUP_PATH, backup_exists)) {
return;
}
const bool durable_acl_expected = primary_exists || backup_exists;
#if defined(NRF52_PLATFORM)
// AtomicFileWriter may leave only a harmless temp image when reset before
// rename. The live image remains authoritative.
@@ -595,11 +604,22 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
return;
}
#endif
if (_fs->exists("/s_contacts")) {
if (!mesh::filePresence(_fs, mesh::CLIENT_ACL_PRIMARY_PATH, primary_exists)
|| (durable_acl_expected && !primary_exists)) return;
if (primary_exists) {
File file = openRead(_fs, "/s_contacts");
if (file) {
bool full = false;
while (!full) {
size_t remaining = file.size();
#if !defined(NRF52_PLATFORM)
// Versioned SPIFFS images finish with a verified CRC trailer, not a
// partially readable extra contact. The data pass must consume every
// byte before it, even if the earlier integrity pass succeeded.
if (remaining >= 8 && (remaining - 8) % CONTACT_RECORD_SIZE == 0) {
remaining -= 8;
}
#endif
bool complete = true;
while (remaining != 0) {
ClientInfo c;
uint8_t pub_key[32];
uint8_t unused[2];
@@ -608,6 +628,11 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
c.alt_path_len = OUT_PATH_UNKNOWN;
c.observed_path_len = OUT_PATH_UNKNOWN;
static const size_t base_size = 32 + 1 + 4 + 2 + 1 + 64 + PUB_KEY_SIZE;
if (remaining < base_size || num_clients >= capacity) {
complete = false;
break;
}
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
success = success && (file.read((uint8_t *) &c.extra.room.sync_since, 4) == 4);
@@ -615,12 +640,20 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below
if (success && unused[0] >= CONTACT_RECORD_VERSION_ALT_PATH) {
size_t record_size = base_size;
if (success && unused[0] > CONTACT_RECORD_VERSION_ALT_PATH) success = false;
if (success && unused[0] == CONTACT_RECORD_VERSION_ALT_PATH) {
record_size += 1 + 64;
success = remaining >= record_size;
success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1);
success = success && (file.read(c.alt_path, 64) == 64);
}
if (!success) break; // EOF
if (!success) {
complete = false;
break;
}
remaining -= record_size;
c.id = mesh::Identity(pub_key);
c.out_path_is_persistable = true;
@@ -640,15 +673,21 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
c.last_timestamp = UINT32_MAX;
}
self_id.calcSharedSecret(c.shared_secret, pub_key); // recalculate shared secrets in case our private key changed
if (num_clients < capacity) {
clients[num_clients++] = c;
} else {
full = true;
}
clients[num_clients++] = c;
}
file.close();
if (!complete) {
// A prefix is not an authoritative ACL. Keep flash untouched and
// refuse admission/mutations until an explicit reload or reboot can
// read the complete image; never save the prefix over its source.
num_clients = 0;
return;
}
} else {
return;
}
}
acl_load_complete = true;
}
bool ClientACL::authorizeLoginTimestamp(
@@ -656,7 +695,8 @@ bool ClientACL::authorizeLoginTimestamp(
uint32_t sender_timestamp,
uint32_t runtime_last_timestamp,
uint8_t login_permissions) {
if (_fs == NULL || pubkey == NULL || !login_replay_store_available) {
if (_fs == NULL || pubkey == NULL || !login_replay_store_available
|| !acl_load_complete) {
return false;
}
@@ -692,7 +732,7 @@ bool ClientACL::authorizeLoginTimestamp(
bool ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
// A failed allocation is not an empty ACL. Preserve the stored managers.
if (capacity == 0 || fs == NULL) return false;
if (capacity == 0 || fs == NULL || !acl_load_complete) return false;
_fs = fs;
#if defined(NRF52_PLATFORM)
mesh::AtomicFileWriter file(_fs, "/s_contacts");
@@ -797,17 +837,18 @@ bool ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
bool ClientACL::clear() {
if (!_fs) return false; // no filesystem, nothing to clear
if (_fs->exists("/s_contacts")) {
_fs->remove("/s_contacts");
for (const char* path : {mesh::CLIENT_ACL_PRIMARY_PATH,
mesh::CLIENT_ACL_TEMP_PATH,
mesh::CLIENT_ACL_BACKUP_PATH}) {
bool present = false;
if (!mesh::filePresence(_fs, path, present)) return false;
if (present && !_fs->remove(path)) return false;
if (!mesh::filePresence(_fs, path, present) || present) return false;
}
if (_fs->exists("/s_contacts.tmp")) _fs->remove("/s_contacts.tmp");
if (_fs->exists("/s_contacts.bak")) _fs->remove("/s_contacts.bak");
const bool files_cleared = !_fs->exists("/s_contacts")
&& !_fs->exists("/s_contacts.tmp")
&& !_fs->exists("/s_contacts.bak");
if (clients) memset(clients, 0, sizeof(ClientInfo) * (size_t)capacity);
num_clients = 0;
return files_cleared;
acl_load_complete = true;
return true;
}
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
@@ -819,6 +860,7 @@ ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
}
ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
if (!acl_load_complete) return NULL;
uint32_t min_time = 0xFFFFFFFF;
ClientInfo* oldest = NULL;
for (int i = 0; i < num_clients; i++) {
@@ -847,6 +889,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
}
bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) {
if (!acl_load_complete) return false;
if (pubkey == NULL || key_len <= 0 || key_len > PUB_KEY_SIZE) return false;
ClientInfo* c;
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
+2
View File
@@ -68,6 +68,7 @@ class ClientACL {
int capacity; // 0 when the table could not be allocated
int num_clients;
bool login_replay_store_available;
bool acl_load_complete;
public:
// MAX_CLIENTS entries run to several kilobytes. Classic ESP32's link-time
@@ -83,6 +84,7 @@ public:
if (clients) memset(clients, 0, sizeof(ClientInfo) * (size_t)capacity);
num_clients = 0;
login_replay_store_available = false;
acl_load_complete = false;
}
~ClientACL() { delete[] clients; }
ClientACL(const ClientACL&) = delete;
+47 -45
View File
@@ -48,6 +48,7 @@
#ifdef WITH_MQTT_BRIDGE
#include "bridges/MQTTBridge.h"
#include "CommonPrefsRecovery.h"
#include "FilePresence.h"
#include "MQTTDefaults.h"
#include "MQTTPrefsAtomicStore.h"
#include "MQTTPrefsCodec.h"
@@ -875,18 +876,24 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
}
#if defined(ENABLE_OTA)
// Push the persisted OTA policy + signer allowlist into the running OtaContext (called after load).
// Register the existing preferences as the source of OTA policy. Registration
// does not need a workspace; a later allocation retry must still restore keys.
void CommonCLI::syncOtaConfigFromPrefs() {
if (!mesh::ota::ota_acquire_context(nullptr, 0)) return;
mesh::ota::OtaContext& c = mesh::ota::ota_ctx();
c.manager.set_autofetch(_prefs->ota_autofetch);
c.manager.set_checkpoint_blocks(_prefs->ota_checkpoint_blocks);
c.manager.set_advert_mins(_prefs->ota_advert_interval);
c.manager.set_max_hops(_prefs->ota_max_hops);
c.autoinstall = _prefs->ota_autoinstall;
c.allow.clear();
for (uint8_t i = 0; i < _prefs->ota_signer_count && i < MAX_OTA_SIGNERS; i++)
c.allow.add(_prefs->ota_signers[i]);
// Mesh owns one process-wide OTA context. Its role's NodePrefs outlives that
// context and already holds the durable policy, so retain only its pointer.
static const NodePrefs* ota_prefs = nullptr;
ota_prefs = _prefs;
mesh::ota::ota_set_context_config_loader([](mesh::ota::OtaConfigState& state) {
state.autofetch = ota_prefs->ota_autofetch;
state.checkpoint = ota_prefs->ota_checkpoint_blocks;
state.advert = ota_prefs->ota_advert_interval;
state.hops = ota_prefs->ota_max_hops;
state.autoinstall = ota_prefs->ota_autoinstall;
state.allow.clear();
for (uint8_t i = 0; i < ota_prefs->ota_signer_count && i < MAX_OTA_SIGNERS; i++)
state.allow.add(ota_prefs->ota_signers[i]);
return true;
});
}
#endif
@@ -1810,7 +1817,9 @@ static const char* mqttJsonImportResultName(MQTTPrefsJsonImport::Result result)
}
static MQTTPrefsRecovery::FileState mqttPrefsFileState(FILESYSTEM* fs, const char* path) {
if (!fs->exists(path)) return MQTTPrefsRecovery::FileState::Missing;
bool present = false;
if (!mesh::filePresence(fs, path, present)) return MQTTPrefsRecovery::FileState::Preserve;
if (!present) return MQTTPrefsRecovery::FileState::Missing;
File file = openMqttPrefsRead(fs, path);
if (!file) return MQTTPrefsRecovery::FileState::Preserve;
const size_t file_size = file.size();
@@ -1843,21 +1852,7 @@ static bool recoverMqttPrefsFiles(FILESYSTEM* fs) {
if (temp != MQTTPrefsRecovery::FileState::Missing) fs->remove("/mqtt_prefs.tmp");
if (backup != MQTTPrefsRecovery::FileState::Missing) fs->remove("/mqtt_prefs.bak");
}
return false;
}
if (action == MQTTPrefsRecovery::Action::PromoteTemp) {
if (fs->rename("/mqtt_prefs.tmp", "/mqtt_prefs")) {
// A usable temp is now the committed primary. Its backup is necessarily
// a stale transaction artifact, even if this firmware cannot decode it.
if (temp == MQTTPrefsRecovery::FileState::Usable &&
backup != MQTTPrefsRecovery::FileState::Missing) {
fs->remove("/mqtt_prefs.bak");
}
MESH_DEBUG_PRINTLN("MQTT: recovered /mqtt_prefs from transaction temp");
return false;
}
MESH_DEBUG_PRINTLN("MQTT: could not recover /mqtt_prefs temp; files preserved");
return true;
return primary == MQTTPrefsRecovery::FileState::Preserve;
}
if (action == MQTTPrefsRecovery::Action::PromoteBackup) {
if (fs->rename("/mqtt_prefs.bak", "/mqtt_prefs")) {
@@ -1868,11 +1863,16 @@ static bool recoverMqttPrefsFiles(FILESYSTEM* fs) {
fs->remove("/mqtt_prefs.tmp");
}
MESH_DEBUG_PRINTLN("MQTT: recovered /mqtt_prefs from transaction backup");
return false;
return backup == MQTTPrefsRecovery::FileState::Preserve;
}
MESH_DEBUG_PRINTLN("MQTT: could not recover /mqtt_prefs backup; files preserved");
return true;
}
if (action == MQTTPrefsRecovery::Action::DiscardTemp) {
// No published image exists. A first-save candidate is still uncommitted,
// even when fully written, and must not become active after a rejected save.
return !fs->remove("/mqtt_prefs.tmp");
}
return false;
}
@@ -1894,10 +1894,9 @@ public:
_open = false;
_owns_temp = false;
_bytes_written = 0;
// Recovery owns stale artifacts. Do not delete them here: a failed commit
// may have moved the old primary to .bak and left a verified temp that the
// next boot must choose between. Refusing the save is safer than erasing an
// image this firmware cannot decode.
// Retry interrupted cleanup/rollback without requiring a reboot. Recovery
// never publishes a rejected candidate or replaces an opaque primary.
if (recoverMqttPrefsFiles(_fs)) return false;
if (_fs->exists("/mqtt_prefs.tmp") || _fs->exists("/mqtt_prefs.bak")) return false;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_file = _fs->open("/mqtt_prefs.tmp", FILE_O_WRITE);
@@ -1939,13 +1938,17 @@ public:
if (!_finished) return false;
// SPIFFS refuses rename(tmp, existing_dest). Move the existing image to a
// recoverable backup first, then publish temp into the now-empty primary.
// Never remove either image after a failed boundary; boot recovery selects
// the completed temp or restores the backup.
// A rejected publication must keep the previous image authoritative.
if (_fs->exists("/mqtt_prefs.bak")) return false;
if (_fs->exists("/mqtt_prefs") && !_fs->rename("/mqtt_prefs", "/mqtt_prefs.bak")) {
return false;
}
if (!_fs->rename("/mqtt_prefs.tmp", "/mqtt_prefs")) return false;
if (!_fs->rename("/mqtt_prefs.tmp", "/mqtt_prefs")) {
if (_fs->exists("/mqtt_prefs.bak")) {
_fs->rename("/mqtt_prefs.bak", "/mqtt_prefs");
}
return false;
}
// Cleanup failure is non-fatal: the new primary is published and recovery
// will remove a known-good stale backup on a later boot.
if (_fs->exists("/mqtt_prefs.bak")) _fs->remove("/mqtt_prefs.bak");
@@ -1955,10 +1958,9 @@ public:
void abort() {
if (_open) _file.close();
_open = false;
// Once finish() has verified the temp, commit may already have moved the
// primary to .bak. Keep the temp on a commit failure so recovery can
// publish it (or fall back to .bak) after reset.
if (_owns_temp && !_finished && _fs->exists("/mqtt_prefs.tmp")) {
// Explicit rejection is not a successful commit. If cleanup or rollback
// also fails, boot recovery still chooses the old backup, never this temp.
if (_owns_temp && _fs->exists("/mqtt_prefs.tmp")) {
_fs->remove("/mqtt_prefs.tmp");
}
_finished = false;
@@ -3807,8 +3809,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error, must be 0-2");
}
} else if (memcmp(config, "flood.max.unscoped ", 19) == 0) {
uint8_t m = atoi(&config[19]);
if (m <= 64) {
uint32_t m;
if (mesh::cli::parseUnsignedIntegerStrict(&config[19], m) && m <= 64) {
_prefs->flood_max_unscoped = m;
savePrefs();
strcpy(reply, "OK");
@@ -3816,8 +3818,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error, max 64");
}
} else if (memcmp(config, "flood.max.advert ", 17) == 0) {
uint8_t m = atoi(&config[17]);
if (m <= 64) {
uint32_t m;
if (mesh::cli::parseUnsignedIntegerStrict(&config[17], m) && m <= 64) {
_prefs->flood_max_advert = m;
savePrefs();
strcpy(reply, "OK");
@@ -3825,8 +3827,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error, max 64");
}
} else if (memcmp(config, "flood.max ", 10) == 0) {
uint8_t m = atoi(&config[10]);
if (m <= 64) {
uint32_t m;
if (mesh::cli::parseUnsignedIntegerStrict(&config[10], m) && m <= 64) {
_prefs->flood_max = m;
savePrefs();
strcpy(reply, "OK");
+1 -1
View File
@@ -775,7 +775,7 @@ class CommonCLI {
bool saveMQTTPrefs(FILESYSTEM* fs);
#endif
#if defined(ENABLE_OTA)
void syncOtaConfigFromPrefs(); // persisted OTA policy + signer allowlist -> running OtaContext
void syncOtaConfigFromPrefs(); // persisted OTA policy + signer allowlist -> idle/active OtaContext
#endif
void handleRegionCmd(char* command, char* reply);
+26 -7
View File
@@ -10,6 +10,9 @@
namespace mesh {
class ContactFileTransaction {
public:
using PresenceProbe = bool (*)(FILESYSTEM*, const char*, bool&);
private:
FILESYSTEM* _fs;
const char* _target;
char _temp[48];
@@ -19,17 +22,30 @@ class ContactFileTransaction {
uint32_t _crc = 0xffffffff;
bool _ok = false;
bool _finished = false;
PresenceProbe _presence;
static bool probe(FILESYSTEM* fs, const char* path, bool& present,
PresenceProbe presence) {
if (presence) return presence(fs, path, present);
present = fs->exists(path);
return true;
}
public:
static bool recover(FILESYSTEM* fs, const char* target) {
static bool recover(FILESYSTEM* fs, const char* target,
PresenceProbe presence = nullptr) {
char backup[48];
snprintf(backup, sizeof(backup), "%s.bak", target);
if (fs->exists(target)) return true;
return !fs->exists(backup) || fs->rename(backup, target);
bool target_exists = false, backup_exists = false;
if (!probe(fs, target, target_exists, presence)
|| !probe(fs, backup, backup_exists, presence)) return false;
if (target_exists) return true;
return !backup_exists || fs->rename(backup, target);
}
ContactFileTransaction(FILESYSTEM* fs, const char* target) : _fs(fs), _target(target) {
ContactFileTransaction(FILESYSTEM* fs, const char* target,
PresenceProbe presence = nullptr)
: _fs(fs), _target(target), _presence(presence) {
snprintf(_temp, sizeof(_temp), "%s.tmp", target);
snprintf(_backup, sizeof(_backup), "%s.bak", target);
if (!recover(fs, target)) return;
if (!recover(fs, target, presence)) return;
if (fs->exists(_temp) && !fs->remove(_temp)) return;
#if defined(RP2040_PLATFORM)
_file = fs->open(_temp, "w");
@@ -67,9 +83,12 @@ public:
}
if (verify) verify.close();
ok = ok && crc == _crc;
if (ok && _fs->exists(_backup)) ok = _fs->remove(_backup);
bool backup_exists = false, target_exists = false;
if (ok) ok = probe(_fs, _backup, backup_exists, _presence)
&& probe(_fs, _target, target_exists, _presence);
if (ok && backup_exists) ok = _fs->remove(_backup);
bool backed_up = false;
if (ok && _fs->exists(_target)) {
if (ok && target_exists) {
ok = _fs->rename(_target, _backup);
backed_up = ok;
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <stdio.h>
#if defined(ESP32_PLATFORM)
#include <errno.h>
#include <sys/stat.h>
#endif
namespace mesh {
// Unlike ESP32 FS::exists(), this probe does not open the file. A failed read
// must not turn a previously committed image into an apparently fresh store.
template <typename Filesystem>
bool filePresence(Filesystem* fs, const char* path, bool& present) {
if (fs == nullptr || path == nullptr) return false;
#if defined(ESP32_PLATFORM)
char absolute[128];
const int length = snprintf(absolute, sizeof(absolute), "/spiffs%s", path);
if (length < 0 || static_cast<size_t>(length) >= sizeof(absolute)) return false;
struct stat info;
const int result = ::stat(absolute, &info);
if (result != 0 && errno != ENOENT) return false;
present = result == 0;
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
struct lfs_info info;
fs->_lockFS();
const int result = lfs_stat(fs->_getFS(), path, &info);
fs->_unlockFS();
if (result != 0 && result != LFS_ERR_NOENT) return false;
present = result == 0;
#else
// Arduino-Pico exposes only boolean metadata status. Its exists() does not
// open the file, but cannot distinguish absent files from metadata errors.
present = fs->exists(path);
#endif
return true;
}
} // namespace mesh
+32 -45
View File
@@ -1,10 +1,30 @@
#include "IdentityStore.h"
#if defined(NRF52_PLATFORM)
#include "FilePresence.h"
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include "AtomicFileWriter.h"
#else
#include "ContactFileTransaction.h"
#endif
bool IdentityStore::recover(const char* name) {
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
char filename[40], backup[48];
if (snprintf(filename, sizeof(filename), "%s/%s.id", _dir, name)
>= (int)sizeof(filename)) return false;
snprintf(backup, sizeof(backup), "%s.bak", filename);
bool primary_exists = false, backup_exists = false;
if (!mesh::filePresence(_fs, filename, primary_exists)
|| !mesh::filePresence(_fs, backup, backup_exists)) return false;
if (!primary_exists && backup_exists) return _fs->rename(backup, filename);
#else
(void)name;
#endif
return true;
}
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id) {
if (!recover(name)) return false;
bool loaded = false;
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
@@ -23,6 +43,7 @@ bool IdentityStore::load(const char *name, mesh::LocalIdentity& id) {
}
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz) {
if (!recover(name)) return false;
bool loaded = false;
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
@@ -50,44 +71,30 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) {
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
#if defined(NRF52_PLATFORM)
if (!recover(name)) return false;
uint8_t key_data[PRV_KEY_SIZE + PUB_KEY_SIZE];
if (id.writeTo(key_data, sizeof(key_data)) != sizeof(key_data)) return false;
// LocalIdentity's byte-buffer export is private-key then public-key, while
// the historical file format is public-key then private-key.
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
mesh::AtomicFileWriter writer(_fs, filename);
#else
mesh::ContactFileTransaction writer(_fs, filename, mesh::filePresence<FILESYSTEM>);
#endif
const bool wrote = writer
&& writer.write(&key_data[PRV_KEY_SIZE], PUB_KEY_SIZE) == PUB_KEY_SIZE
&& writer.write(key_data, PRV_KEY_SIZE) == PRV_KEY_SIZE;
const bool success = writer.commit(wrote);
MESH_DEBUG_PRINTLN("IdentityStore::save() atomic write - %s", success ? "OK" : "Err");
return success;
#elif defined(STM32_PLATFORM)
_fs->remove(filename);
File file = _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
File file = _fs->open(filename, "w");
#else
File file = _fs->open(filename, "w", true);
#endif
#if !defined(NRF52_PLATFORM)
if (file) {
bool success = id.writeTo(file);
file.close();
MESH_DEBUG_PRINTLN("IdentityStore::save() write - %s", success ? "OK" : "Err");
return success;
}
#endif
MESH_DEBUG_PRINTLN("IdentityStore::save() failed");
return false;
}
bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const char display_name[]) {
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
#if defined(NRF52_PLATFORM)
if (!recover(name)) return false;
uint8_t key_data[PRV_KEY_SIZE + PUB_KEY_SIZE];
if (id.writeTo(key_data, sizeof(key_data)) != sizeof(key_data)) return false;
uint8_t display_data[32];
@@ -96,34 +103,14 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const
if (display_len > sizeof(display_data) - 1) display_len = sizeof(display_data) - 1;
memcpy(display_data, display_name, display_len);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
mesh::AtomicFileWriter writer(_fs, filename);
#else
mesh::ContactFileTransaction writer(_fs, filename, mesh::filePresence<FILESYSTEM>);
#endif
const bool wrote = writer
&& writer.write(&key_data[PRV_KEY_SIZE], PUB_KEY_SIZE) == PUB_KEY_SIZE
&& writer.write(key_data, PRV_KEY_SIZE) == PRV_KEY_SIZE
&& writer.write(display_data, sizeof(display_data)) == sizeof(display_data);
return writer.commit(wrote);
#elif defined(STM32_PLATFORM)
_fs->remove(filename);
File file = _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
File file = _fs->open(filename, "w");
#else
File file = _fs->open(filename, "w", true);
#endif
#if !defined(NRF52_PLATFORM)
if (file) {
bool success = id.writeTo(file);
uint8_t tmp[32];
memset(tmp, 0, sizeof(tmp));
int n = strlen(display_name);
if (n > sizeof(tmp)-1) n = sizeof(tmp)-1;
memcpy(tmp, display_name, n);
success = success && file.write(tmp, sizeof(tmp)) == sizeof(tmp);
file.close();
return success;
}
#endif
return false;
}
+3
View File
@@ -19,6 +19,9 @@ public:
void begin() {
if (_dir && _dir[0] == '/') { _fs->mkdir(_dir); } }
// Recover an interrupted non-replacing rename before deciding this is a
// fresh identity. False means the previous image must remain protected.
bool recover(const char* name);
bool load(const char *name, mesh::LocalIdentity& id);
bool load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz);
bool save(const char *name, const mesh::LocalIdentity& id);
+5 -14
View File
@@ -19,8 +19,8 @@ enum class FileState : uint8_t {
enum class Action : uint8_t {
None,
KeepPrimary,
PromoteTemp,
PromoteBackup,
DiscardTemp,
};
inline Action select(FileState primary, FileState temp, FileState backup) {
@@ -29,20 +29,11 @@ inline Action select(FileState primary, FileState temp, FileState backup) {
// by this firmware.
if (primary != FileState::Missing) return Action::KeepPrimary;
// A completed temp is the new image and wins over the old backup.
if (temp == FileState::Usable) return Action::PromoteTemp;
// If temp is opaque but a known-good backup exists, boot from the backup.
// The caller may discard the opaque temp once that usable backup has become
// primary. Otherwise, rename the opaque temp into the empty primary name so
// the normal loader can hold it.
if (temp == FileState::Preserve) {
return backup == FileState::Usable ? Action::PromoteBackup : Action::PromoteTemp;
}
// No temp survived. The backup is the only recoverable image, even when it
// is a newer layout that this firmware must preserve rather than decode.
// Until temp has its final name, it is not committed. A failed publication
// may have been reported to the caller, so never activate it during recovery.
// The backup remains authoritative, including an opaque newer layout.
if (backup != FileState::Missing) return Action::PromoteBackup;
if (temp != FileState::Missing) return Action::DiscardTemp;
return Action::None;
}
+32 -9
View File
@@ -11,14 +11,6 @@ namespace {
bool (*context_config_loader)(OtaConfigState&) = nullptr;
}
void ota_set_context_config_loader(bool (*load)(OtaConfigState&)) {
context_config_loader = load;
if (auto* context = ota_context_if_active()) {
OtaConfigState restored;
if (load && load(restored)) restored.apply(*context);
}
}
#if OTA_DYNAMIC_CONTEXT
namespace {
OtaContext* active_context = nullptr;
@@ -79,7 +71,13 @@ void ota_begin_context(uint32_t target, OtaSend send, void* ctx,
saved_send_ctx = ctx;
strncpy(saved_hw, hw ? hw : "", sizeof(saved_hw) - 1);
saved_hw[sizeof(saved_hw) - 1] = 0;
if (seeder_id) memcpy(saved_seeder_id, seeder_id, sizeof(saved_seeder_id));
ota_refresh_seeder_identity(seeder_id);
}
void ota_refresh_seeder_identity(const uint8_t* seeder_id) {
if (!seeder_id) return;
memcpy(saved_seeder_id, seeder_id, sizeof(saved_seeder_id));
if (active_context) active_context->manager.set_seeder_id(seeder_id);
}
bool ota_acquire_context(char* reply, size_t cap) {
@@ -159,10 +157,35 @@ bool ota_acquire_context(char*, size_t) { return true; }
void ota_begin_context(uint32_t target, OtaSend send, void* ctx,
const char* hw, const uint8_t* seeder_id) {
ota_ctx().begin(target, send, ctx, hw);
ota_refresh_seeder_identity(seeder_id);
}
void ota_refresh_seeder_identity(const uint8_t* seeder_id) {
ota_ctx().manager.set_seeder_id(seeder_id);
}
uint8_t ota_hop_limit() { return ota_ctx().manager.max_hops(); }
#endif
void ota_set_context_config_loader(bool (*load)(OtaConfigState&)) {
context_config_loader = load;
OtaConfigState restored;
if (!load || !load(restored)) return;
#if OTA_DYNAMIC_CONTEXT
// The mesh also consults the hop limit while no OTA workspace is allocated.
// Seed the existing idle policy cache, without claiming heap/queue storage.
#if defined(OTA_SEEDER_ONLY)
saved_autofetch = OtaManager::AUTOFETCH_OFF;
saved_autoinstall = OtaContext::AUTOINSTALL_OFF;
#else
saved_autofetch = restored.autofetch;
saved_autoinstall = restored.autoinstall;
#endif
saved_checkpoint = restored.checkpoint;
saved_advert = restored.advert;
saved_hops = restored.hops;
saved_allow = restored.allow;
#endif
if (auto* context = ota_context_if_active()) restored.apply(*context);
}
} // namespace ota
} // namespace mesh
+5 -1
View File
@@ -706,11 +706,15 @@ OtaContext& ota_ctx(); // process-wide context
// Dynamic builds return null outside an acquired workspace. Static builds
// always return their process-wide context.
OtaContext* ota_context_if_active();
// Optional role-specific policy reload; registration never allocates a context.
// Optional role-specific policy reload; registration updates idle/active policy
// without allocating a context. A failed load leaves the prior policy intact.
void ota_set_context_config_loader(bool (*load)(OtaConfigState&));
bool ota_acquire_context(char* reply, size_t cap);
void ota_begin_context(uint32_t target, OtaSend send, void* ctx,
const char* hw, const uint8_t* seeder_id);
// Identity can become available after Mesh::begin or change through key import.
// Refresh both active and future OTA sessions without resetting transfer state.
void ota_refresh_seeder_identity(const uint8_t* seeder_id);
uint8_t ota_hop_limit();
#if defined(OTA_SHARED_COMPANION_QUEUE)