feat(mqtt): persist observer preferences as JSON

This commit is contained in:
agessaman committed 2026-08-14 09:09:05 -07:00
1 parent 40635a53c4
commit 75f3e446e0
27 files changed
+2131 -254

No files matched your search

+360 -60
View File
@@ -6,6 +6,7 @@
#include "MQTTPrefsAtomicStore.h"
#include <RTClib.h>
#include <Utils.h>
#include <new>
#ifndef BRIDGE_MAX_BAUD
#define BRIDGE_MAX_BAUD 115200
@@ -23,6 +24,7 @@
#include "MQTTDefaults.h"
#include "MQTTPrefsCodec.h"
#include "MQTTPrefsRecovery.h"
#include "MQTTPrefsSerializer.h"
#endif
// Believe it or not, this std C function is busted on some platforms!
@@ -94,7 +96,8 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
_prefs->bridge_pkt_src = 1; // Default to RX (logRx) for new installs
}
#ifdef WITH_MQTT_BRIDGE
// Load observer preferences (MQTT/WiFi/timezone/SNMP/alert) from /mqtt_prefs.
// Load observer preferences (MQTT/WiFi/timezone/SNMP/alert) from /mqtt.json,
// migrating the old /mqtt_prefs binary when JSON does not exist yet.
// Readers (MQTTBridge, AlertReporter, observer CLI) use _mqtt_prefs directly —
// these fields no longer exist in NodePrefs, so there is nothing to sync.
MQTTPrefsAtomicStore::LegacyUpgradeGate legacy_upgrade(
@@ -119,13 +122,13 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
}
}
// mqtt_rx_enabled: new field appended to end of MQTTPrefs. On upgrade from older firmware,
// the shorter /mqtt_prefs file won't contain it, so it keeps the default value (1 = on)
// a shorter legacy /mqtt_prefs file won't contain it, so it keeps the default value (1 = on)
// set by setMQTTPrefsDefaults(). No explicit migration needed.
#endif
// Republish legacy binary prefs as /prefs.json. Old-format files also carried a
// trailing observer block, which loadPrefsInt() recovered into _legacy_tail; wait
// for loadMQTTPrefs() to commit that to /mqtt_prefs first. The legacy file is left
// for loadMQTTPrefs() to commit that to /mqtt.json first. The legacy file is left
// on flash either way, so a deferred or failed save just retries on the next boot.
#ifdef WITH_MQTT_BRIDGE
if (loaded_from_legacy || _com_prefs_needs_upgrade) {
@@ -134,7 +137,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
legacy_upgrade.recordComPrefsRewrite();
_com_prefs_needs_upgrade = false;
} else {
MESH_DEBUG_PRINTLN("Prefs: deferring /prefs.json migration until /mqtt_prefs commits");
MESH_DEBUG_PRINTLN("Prefs: deferring /prefs.json migration until /mqtt.json commits");
}
}
#else
@@ -376,13 +379,17 @@ bool CommonCLI::savePrefs(FILESYSTEM* fs, bool save_mqtt) {
#ifdef WITH_MQTT_BRIDGE
// Observer config (MQTT/WiFi/timezone/SNMP/alert) is persisted separately. The
// observer CLI writes _mqtt_prefs directly, so no NodePrefs->MQTTPrefs sync runs.
// Runs regardless of the NodePrefs result so a failed JSON write cannot strand it.
// Ordinary NodePrefs callers leave save_mqtt false; migration is the only
// workflow that may explicitly combine these independent transactions.
if (save_mqtt) saveMQTTPrefs(fs);
#endif
return success;
}
#ifdef WITH_MQTT_BRIDGE
static const uint32_t MQTT_JSON_FNV1A_OFFSET_BASIS = 2166136261u;
static const uint32_t MQTT_JSON_FNV1A_PRIME = 16777619u;
// Set default values for MQTT preferences (used when file doesn't exist or is corrupted)
static void setMQTTPrefsDefaults(MQTTPrefs* prefs) {
applyMQTTDefaults(prefs);
@@ -396,6 +403,124 @@ static File openMqttPrefsRead(FILESYSTEM* fs, const char* path = "/mqtt_prefs")
#endif
}
enum class JsonPrefsLoadResult : uint8_t {
Loaded,
LoadedWithRepairs,
UnsupportedVersion,
FutureClaimed,
Invalid,
};
static JsonPrefsLoadResult loadMqttJsonFile(FILESYSTEM* fs, const char* path,
MQTTPrefs* output) {
if (output == nullptr) return JsonPrefsLoadResult::Invalid;
applyMQTTDefaults(output);
// Probe the root version without applying the v1 schema. A future version
// may legitimately change an existing field's type, and must still be held
// opaquely rather than misclassified as corrupt and rolled back.
File version_file = openMqttPrefsRead(fs, path);
if (!version_file) return JsonPrefsLoadResult::Invalid;
MQTTPrefsVersionProbe version_probe;
const bool version_parsed = version_probe.loadSerial(version_file);
version_file.close();
if (version_probe.hasFutureVersion()) {
return version_parsed ? JsonPrefsLoadResult::UnsupportedVersion
: JsonPrefsLoadResult::FutureClaimed;
}
File file = openMqttPrefsRead(fs, path);
if (!file) return JsonPrefsLoadResult::Invalid;
MQTTPrefsSerializer serializer(output);
const bool parsed = serializer.loadSerial(file);
file.close();
if (!parsed) return JsonPrefsLoadResult::Invalid;
if (serializer.hasFutureVersion()) return JsonPrefsLoadResult::UnsupportedVersion;
bool repaired = false;
if (!serializer.apply(&repaired)) return JsonPrefsLoadResult::Invalid;
return repaired ? JsonPrefsLoadResult::LoadedWithRepairs : JsonPrefsLoadResult::Loaded;
}
static MQTTPrefsRecovery::FileState mqttJsonFileState(FILESYSTEM* fs, const char* path) {
if (!fs->exists(path)) return MQTTPrefsRecovery::FileState::Missing;
MQTTPrefs* scratch = new (std::nothrow) MQTTPrefs;
if (scratch == nullptr) return MQTTPrefsRecovery::FileState::Indeterminate;
const JsonPrefsLoadResult result = loadMqttJsonFile(fs, path, scratch);
delete scratch;
if (result == JsonPrefsLoadResult::UnsupportedVersion) {
// Syntax and the mandatory version field were valid, so this can be a
// fully verified transaction written by newer firmware. Keep it distinct
// from a torn/corrupt temp during recovery.
return MQTTPrefsRecovery::FileState::FutureUsable;
}
if (result == JsonPrefsLoadResult::FutureClaimed) {
return MQTTPrefsRecovery::FileState::FutureClaimed;
}
return result == JsonPrefsLoadResult::Loaded ||
result == JsonPrefsLoadResult::LoadedWithRepairs
? MQTTPrefsRecovery::FileState::Usable
: MQTTPrefsRecovery::FileState::Preserve;
}
static bool recoverMqttJsonFiles(FILESYSTEM* fs) {
const MQTTPrefsRecovery::FileState primary = mqttJsonFileState(fs, "/mqtt.json");
const MQTTPrefsRecovery::FileState temp = mqttJsonFileState(fs, "/mqtt.json.tmp");
const MQTTPrefsRecovery::FileState backup = mqttJsonFileState(fs, "/mqtt.json.bak");
const MQTTPrefsRecovery::Action action = MQTTPrefsRecovery::select(primary, temp, backup);
if (action == MQTTPrefsRecovery::Action::KeepPrimary) {
if (primary == MQTTPrefsRecovery::FileState::Usable) {
if (!MQTTPrefsRecovery::uncertain(temp) &&
temp != MQTTPrefsRecovery::FileState::Missing) {
fs->remove("/mqtt.json.tmp");
}
if (!MQTTPrefsRecovery::uncertain(backup) &&
backup != MQTTPrefsRecovery::FileState::Missing) {
fs->remove("/mqtt.json.bak");
}
}
return MQTTPrefsRecovery::uncertain(primary) ||
MQTTPrefsRecovery::uncertain(temp) ||
MQTTPrefsRecovery::uncertain(backup);
}
if (action == MQTTPrefsRecovery::Action::DiscardTemp) {
if (fs->remove("/mqtt.json.tmp")) {
MESH_DEBUG_PRINTLN("MQTT: discarded incomplete first-migration JSON temp");
return false;
}
MESH_DEBUG_PRINTLN("MQTT: could not discard incomplete /mqtt.json temp; source held");
return true;
}
if (action == MQTTPrefsRecovery::Action::PromoteTemp) {
if (fs->rename("/mqtt.json.tmp", "/mqtt.json")) {
if (temp == MQTTPrefsRecovery::FileState::Usable &&
backup != MQTTPrefsRecovery::FileState::Missing) {
fs->remove("/mqtt.json.bak");
}
MESH_DEBUG_PRINTLN("MQTT: recovered /mqtt.json from transaction temp");
return MQTTPrefsRecovery::uncertain(temp) ||
MQTTPrefsRecovery::uncertain(backup);
}
MESH_DEBUG_PRINTLN("MQTT: could not recover /mqtt.json temp; files preserved");
return true;
}
if (action == MQTTPrefsRecovery::Action::PromoteBackup) {
if (fs->rename("/mqtt.json.bak", "/mqtt.json")) {
if (backup == MQTTPrefsRecovery::FileState::Usable &&
!MQTTPrefsRecovery::uncertain(temp) &&
temp != MQTTPrefsRecovery::FileState::Missing) {
fs->remove("/mqtt.json.tmp");
}
MESH_DEBUG_PRINTLN("MQTT: recovered /mqtt.json from transaction backup");
return MQTTPrefsRecovery::uncertain(temp) ||
MQTTPrefsRecovery::uncertain(backup);
}
MESH_DEBUG_PRINTLN("MQTT: could not recover /mqtt.json backup; files preserved");
return true;
}
return false;
}
static MQTTPrefsRecovery::FileState mqttPrefsFileState(FILESYSTEM* fs, const char* path) {
if (!fs->exists(path)) return MQTTPrefsRecovery::FileState::Missing;
File file = openMqttPrefsRead(fs, path);
@@ -432,6 +557,14 @@ static bool recoverMqttPrefsFiles(FILESYSTEM* fs) {
}
return false;
}
if (action == MQTTPrefsRecovery::Action::DiscardTemp) {
if (fs->remove("/mqtt_prefs.tmp")) {
MESH_DEBUG_PRINTLN("MQTT: discarded incomplete legacy transaction temp");
return false;
}
MESH_DEBUG_PRINTLN("MQTT: could not discard incomplete /mqtt_prefs temp; source held");
return true;
}
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
@@ -463,30 +596,31 @@ static bool recoverMqttPrefsFiles(FILESYSTEM* fs) {
return false;
}
// Filesystem adapter for MQTTPrefsAtomicStore. It writes the new image to
// /mqtt_prefs.tmp and verifies its size. Publishing is a recoverable SPIFFS
// Filesystem adapter for the ConfigSerializer image. It writes to
// /mqtt.json.tmp and verifies its size and checksum. Publishing is a recoverable SPIFFS
// transaction: primary -> .bak, then tmp -> primary, then best-effort backup
// cleanup. A power loss at every boundary leaves at least one recoverable file.
class MQTTPrefsFileStore {
class MQTTPrefsJsonFileStore {
public:
explicit MQTTPrefsFileStore(FILESYSTEM* fs) : _fs(fs) {}
explicit MQTTPrefsJsonFileStore(FILESYSTEM* fs) : _fs(fs) {}
bool begin() {
_finished = false;
_open = false;
_owns_temp = false;
_bytes_written = 0;
_expected_crc = MQTT_JSON_FNV1A_OFFSET_BASIS;
// 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.
if (_fs->exists("/mqtt_prefs.tmp") || _fs->exists("/mqtt_prefs.bak")) return false;
if (_fs->exists("/mqtt.json.tmp") || _fs->exists("/mqtt.json.bak")) return false;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_file = _fs->open("/mqtt_prefs.tmp", FILE_O_WRITE);
_file = _fs->open("/mqtt.json.tmp", FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
_file = _fs->open("/mqtt_prefs.tmp", "w");
_file = _fs->open("/mqtt.json.tmp", "w");
#else
_file = _fs->open("/mqtt_prefs.tmp", "w", true);
_file = _fs->open("/mqtt.json.tmp", "w", true);
#endif
_open = _file;
_owns_temp = _open;
@@ -496,6 +630,9 @@ public:
size_t write(const uint8_t* bytes, size_t size) {
if (!_open) return 0;
const size_t written = _file.write(bytes, size);
for (size_t i = 0; i < written; ++i) {
_expected_crc = (_expected_crc ^ bytes[i]) * MQTT_JSON_FNV1A_PRIME;
}
_bytes_written += written;
return written;
}
@@ -505,12 +642,33 @@ public:
_file.close();
_open = false;
#if defined(RP2040_PLATFORM)
File verify = _fs->open("/mqtt_prefs.tmp", "r");
File verify = _fs->open("/mqtt.json.tmp", "r");
#else
File verify = _fs->open("/mqtt_prefs.tmp");
File verify = _fs->open("/mqtt.json.tmp");
#endif
if (!verify) return false;
const bool complete = verify.size() == _bytes_written;
uint32_t actual_crc = MQTT_JSON_FNV1A_OFFSET_BASIS;
size_t actual_size = 0;
bool read_failed = false;
uint8_t buf[64];
while (verify.available() > 0) {
// Arduino File implementations normally return a byte count, but some
// Stream implementations use -1 for a read error. Keep that sentinel
// signed so it cannot become a huge size_t and overrun this buffer.
const int count = static_cast<int>(verify.read(buf, sizeof(buf)));
if (count <= 0) {
read_failed = count < 0;
break;
}
actual_size += static_cast<size_t>(count);
for (int i = 0; i < count; ++i) {
actual_crc = (actual_crc ^ buf[i]) * MQTT_JSON_FNV1A_PRIME;
}
}
const bool complete = !read_failed &&
verify.size() == _bytes_written &&
actual_size == _bytes_written &&
actual_crc == _expected_crc;
verify.close();
if (!complete) return false;
_finished = true;
@@ -523,25 +681,33 @@ public:
// 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.
if (_fs->exists("/mqtt_prefs.bak")) return false;
if (_fs->exists("/mqtt_prefs") && !_fs->rename("/mqtt_prefs", "/mqtt_prefs.bak")) {
if (_fs->exists("/mqtt.json.bak")) return false;
if (_fs->exists("/mqtt.json") && !_fs->rename("/mqtt.json", "/mqtt.json.bak")) {
return false;
}
if (!_fs->rename("/mqtt_prefs.tmp", "/mqtt_prefs")) return false;
if (!_fs->rename("/mqtt.json.tmp", "/mqtt.json")) 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");
if (_fs->exists("/mqtt.json.bak")) _fs->remove("/mqtt.json.bak");
return true;
}
void discardFinishedTemp() {
if (_open) _file.close();
_open = false;
if (_owns_temp && _fs->exists("/mqtt.json.tmp")) _fs->remove("/mqtt.json.tmp");
_finished = false;
_owns_temp = false;
}
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")) {
_fs->remove("/mqtt_prefs.tmp");
if (_owns_temp && !_finished && _fs->exists("/mqtt.json.tmp")) {
_fs->remove("/mqtt.json.tmp");
}
_finished = false;
_owns_temp = false;
@@ -554,31 +720,90 @@ private:
bool _finished = false;
bool _owns_temp = false;
size_t _bytes_written = 0;
uint32_t _expected_crc = MQTT_JSON_FNV1A_OFFSET_BASIS;
};
class MQTTPrefsStoreStream : public Stream {
public:
explicit MQTTPrefsStoreStream(MQTTPrefsJsonFileStore* store) : _store(store) {}
size_t write(uint8_t byte) override { return write(&byte, 1); }
size_t write(const uint8_t* buffer, size_t size) override {
if (!_ok || _store == nullptr) return 0;
const size_t written = _store->write(buffer, size);
if (written != size) _ok = false;
return written;
}
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
bool ok() const { return _ok; }
private:
MQTTPrefsJsonFileStore* _store;
bool _ok = true;
};
#endif // WITH_MQTT_BRIDGE
#ifdef WITH_MQTT_BRIDGE
static const char* mqttPrefsSaveResultName(MQTTPrefsAtomicStore::Result result) {
switch (result) {
case MQTTPrefsAtomicStore::Result::BeginFailed: return "begin";
case MQTTPrefsAtomicStore::Result::HeaderWriteFailed: return "header write";
case MQTTPrefsAtomicStore::Result::PayloadWriteFailed: return "payload write";
case MQTTPrefsAtomicStore::Result::FinishFailed: return "close";
case MQTTPrefsAtomicStore::Result::CommitFailed: return "rename";
case MQTTPrefsAtomicStore::Result::Committed: return "committed";
}
return "unknown";
}
void CommonCLI::loadMQTTPrefs(
FILESYSTEM* fs, MQTTPrefsAtomicStore::LegacyUpgradeGate* legacy_upgrade) {
setMQTTPrefsDefaults(&_mqtt_prefs);
_mqtt_prefs_hold = recoverMqttJsonFiles(fs);
if (_mqtt_prefs_hold && !fs->exists("/mqtt.json") &&
(fs->exists("/mqtt.json.tmp") || fs->exists("/mqtt.json.bak"))) {
_legacy_tail.valid = false;
MESH_DEBUG_PRINTLN("MQTT: unresolved /mqtt.json recovery files; using defaults (files preserved)");
return;
}
// The JSON file is authoritative once it exists. Never fall back to the
// stale binary snapshot when JSON is corrupt, unreadable, or from a future
// schema: preserve it and run defaults until an operator resolves it.
if (fs->exists("/mqtt.json")) {
MQTTPrefs* scratch = new (std::nothrow) MQTTPrefs;
if (scratch == nullptr) {
_mqtt_prefs_hold = true;
_legacy_tail.valid = false;
MESH_DEBUG_PRINTLN("MQTT: no memory to validate /mqtt.json; source preserved");
return;
}
const JsonPrefsLoadResult json_result = loadMqttJsonFile(fs, "/mqtt.json", scratch);
if (json_result == JsonPrefsLoadResult::Loaded ||
json_result == JsonPrefsLoadResult::LoadedWithRepairs) {
memcpy(&_mqtt_prefs, scratch, sizeof(_mqtt_prefs));
delete scratch;
_legacy_tail.valid = false;
if (json_result == JsonPrefsLoadResult::LoadedWithRepairs) {
MESH_DEBUG_PRINTLN("MQTT: repaired out-of-range values in /mqtt.json");
if (!_mqtt_prefs_hold && !saveMQTTPrefs(fs)) {
_mqtt_prefs_hold = true;
MESH_DEBUG_PRINTLN("MQTT: could not persist /mqtt.json repairs; source held");
}
}
return;
}
delete scratch;
_mqtt_prefs_hold = true;
_legacy_tail.valid = false;
if (json_result == JsonPrefsLoadResult::UnsupportedVersion) {
MESH_DEBUG_PRINTLN("MQTT: /mqtt.json uses a future version; using defaults (file preserved)");
} else if (json_result == JsonPrefsLoadResult::FutureClaimed) {
MESH_DEBUG_PRINTLN(
"MQTT: /mqtt.json claims a future version but uses unknown grammar; "
"using defaults (file preserved)");
} else {
MESH_DEBUG_PRINTLN("MQTT: /mqtt.json is invalid or unreadable; using defaults (file preserved)");
}
return;
}
// Complete or preserve an interrupted SPIFFS transaction before decoding.
// A failed recovery leaves the artifacts untouched and blocks this boot from
// replacing them with defaults through a later CLI save.
_mqtt_prefs_hold = recoverMqttPrefsFiles(fs);
_mqtt_prefs_hold = _mqtt_prefs_hold || recoverMqttPrefsFiles(fs);
bool has_observer_fields = false;
bool mqtt_rewrite_pending = false;
bool migrated_legacy_mqtt = false;
@@ -600,24 +825,34 @@ void CommonCLI::loadMQTTPrefs(
} else if (plan.source == MQTTPrefsCodec::Source::Current) {
file = openMqttPrefsRead(fs);
MQTTPrefsHeader header;
if (!file || file.read((uint8_t *)&header, sizeof(header)) != sizeof(header) ||
file.read((uint8_t *)&_mqtt_prefs, plan.payload_len) != plan.payload_len) {
LegacyV1MQTTPrefs* old_prefs = new (std::nothrow) LegacyV1MQTTPrefs;
if (old_prefs) memset(old_prefs, 0, sizeof(*old_prefs));
if (!old_prefs || !file || file.read((uint8_t *)&header, sizeof(header)) != sizeof(header) ||
file.read((uint8_t *)old_prefs, plan.payload_len) != plan.payload_len) {
setMQTTPrefsDefaults(&_mqtt_prefs);
_mqtt_prefs_hold = true;
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs read failed, using defaults (file preserved)");
} else if (!MQTTPrefsCodec::isPlausibleV1(*old_prefs, plan.payload_len)) {
setMQTTPrefsDefaults(&_mqtt_prefs);
_mqtt_prefs_hold = true;
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs v1 content failed plausibility checks; source preserved");
} else {
MQTTPrefsCodec::migrateV1(*old_prefs, plan.payload_len, &_mqtt_prefs);
has_observer_fields = plan.observer_fields_present;
mqtt_rewrite_pending = true;
migrated_legacy_mqtt = true;
// Written by a later build with appended fields. Everything this
// binary knows loaded normally; say so, because the next `set` will
// rewrite the file at this length and drop the newer settings.
if (file_size - sizeof(MQTTPrefsHeader) > plan.payload_len) {
MESH_DEBUG_PRINTLN(
"MQTT: /mqtt_prefs written by newer firmware (%u > %u bytes); "
"config loaded, newer settings ignored and dropped on next save",
"known settings loaded; newer binary fields remain in the rollback snapshot",
(unsigned)(file_size - sizeof(MQTTPrefsHeader)),
(unsigned)plan.payload_len);
}
}
delete old_prefs;
if (file) file.close();
} else if (plan.rewrite_legacy) {
bool migrated = false;
@@ -674,16 +909,18 @@ void CommonCLI::loadMQTTPrefs(
case MQTTPrefsCodec::Source::LegacySixSlotAudience:
case MQTTPrefsCodec::Source::LegacySixSlotAudienceRx:
case MQTTPrefsCodec::Source::LegacySixSlot: {
Legacy6SlotMQTTPrefs old_prefs = {};
if (file.read((uint8_t *)&old_prefs, plan.payload_len) == plan.payload_len) {
Legacy6SlotMQTTPrefs* old_prefs = new (std::nothrow) Legacy6SlotMQTTPrefs;
if (old_prefs) memset(old_prefs, 0, sizeof(*old_prefs));
if (old_prefs && file.read((uint8_t *)old_prefs, plan.payload_len) == plan.payload_len) {
if (MQTTPrefsCodec::isPlausibleLegacy(plan.source,
(const uint8_t *)&old_prefs, plan.payload_len)) {
MQTTPrefsCodec::migrateLegacySixSlot(old_prefs, plan.source, &_mqtt_prefs);
(const uint8_t *)old_prefs, plan.payload_len)) {
MQTTPrefsCodec::migrateLegacySixSlot(*old_prefs, plan.source, &_mqtt_prefs);
migrated = true;
} else {
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs legacy content failed plausibility checks");
}
}
delete old_prefs;
break;
}
default:
@@ -742,9 +979,9 @@ void CommonCLI::loadMQTTPrefs(
if (mqtt_rewrite_pending) {
legacy_upgrade->requireMqttRewrite();
if (migrated_legacy_mqtt) {
MESH_DEBUG_PRINTLN("MQTT: Migrating headerless /mqtt_prefs to versioned layout");
MESH_DEBUG_PRINTLN("MQTT: Migrating binary /mqtt_prefs to /mqtt.json");
} else {
MESH_DEBUG_PRINTLN("MQTT: Persisting observer tail into /mqtt_prefs before /com_prefs compaction");
MESH_DEBUG_PRINTLN("MQTT: Persisting observer tail into /mqtt.json before /com_prefs compaction");
}
if (saveMQTTPrefs(fs)) {
legacy_upgrade->recordMqttSave(true);
@@ -754,7 +991,7 @@ void CommonCLI::loadMQTTPrefs(
// untouched; the next boot can recover the tail and retry the transaction.
_mqtt_prefs_hold = true;
legacy_upgrade->recordMqttSave(false);
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs migration save failed; legacy files preserved and held");
MESH_DEBUG_PRINTLN("MQTT: /mqtt.json migration save failed; legacy files preserved and held");
}
}
}
@@ -763,26 +1000,71 @@ bool CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) {
if (_mqtt_prefs_hold) {
// Loading deliberately preserved the source file. Do not replace it with this
// boot's defaults after an unsupported, corrupt, or temporarily failed read.
MESH_DEBUG_PRINTLN("MQTT: /mqtt_prefs held, not overwriting");
MESH_DEBUG_PRINTLN("MQTT: observer preference source held, not overwriting /mqtt.json");
return false;
}
// Write header and payload sequentially so the transaction needs no second
// full-size (2.8 KiB) staging buffer on constrained targets. The length is
// the shortest that still round-trips this config, so a node with default
// packet filters keeps writing a payload older firmware can read.
const size_t payload_len = MQTTPrefsCodec::payloadLenFor(_mqtt_prefs);
const MQTTPrefsHeader header = MQTTPrefsCodec::makeHeader(payload_len);
MQTTPrefsFileStore store(fs);
const MQTTPrefsAtomicStore::Result result = MQTTPrefsAtomicStore::write(
store, (const uint8_t *)&header, sizeof(header),
(const uint8_t *)&_mqtt_prefs, payload_len);
if (!MQTTPrefsAtomicStore::committed(result)) {
MESH_DEBUG_PRINTLN("MQTT: atomic /mqtt_prefs save failed at %s; source preserved",
mqttPrefsSaveResultName(result));
MQTTPrefs* repair_defaults = new (std::nothrow) MQTTPrefs;
if (repair_defaults == nullptr) {
MESH_DEBUG_PRINTLN("MQTT: no memory to normalize observer settings before save");
return false;
}
return true;
setMQTTPrefsDefaults(repair_defaults);
MQTTPrefsSerializer serializer(&_mqtt_prefs, repair_defaults);
// The serializer hierarchy copies the default values it needs. Release this
// large temporary before file I/O and the independent verification scratch.
delete repair_defaults;
bool repaired = false;
if (!serializer.normalize(&repaired)) return false;
if (repaired) MESH_DEBUG_PRINTLN("MQTT: normalized out-of-range observer settings before save");
MQTTPrefsJsonFileStore store(fs);
bool verify_oom = false;
const MQTTPrefsAtomicStore::VerifiedImageResult result =
MQTTPrefsAtomicStore::writeVerifiedImage(
store,
[&]() -> bool {
MQTTPrefsStoreStream stream(&store);
return serializer.saveSerial(stream) && stream.ok();
},
[&]() -> bool {
MQTTPrefs* verify = new (std::nothrow) MQTTPrefs;
if (verify == nullptr) {
verify_oom = true;
return false;
}
const JsonPrefsLoadResult verify_result =
loadMqttJsonFile(fs, "/mqtt.json.tmp", verify);
delete verify;
// saveSerial() emits already-normalized values. Needing another
// repair here means save/load is not idempotent.
return verify_result == JsonPrefsLoadResult::Loaded;
});
switch (result) {
case MQTTPrefsAtomicStore::VerifiedImageResult::Committed:
return true;
case MQTTPrefsAtomicStore::VerifiedImageResult::BeginFailed:
MESH_DEBUG_PRINTLN("MQTT: atomic /mqtt.json save failed at begin; source preserved");
break;
case MQTTPrefsAtomicStore::VerifiedImageResult::WriteFailed:
MESH_DEBUG_PRINTLN("MQTT: atomic /mqtt.json save failed during write; source preserved");
break;
case MQTTPrefsAtomicStore::VerifiedImageResult::FinishFailed:
MESH_DEBUG_PRINTLN("MQTT: atomic /mqtt.json save failed during checksum verification; source preserved");
break;
case MQTTPrefsAtomicStore::VerifiedImageResult::VerifyFailed:
if (verify_oom) {
MESH_DEBUG_PRINTLN("MQTT: no memory to validate /mqtt.json temp; source preserved");
} else {
MESH_DEBUG_PRINTLN("MQTT: generated /mqtt.json temp failed schema validation; source preserved");
}
break;
case MQTTPrefsAtomicStore::VerifiedImageResult::CommitFailed:
MESH_DEBUG_PRINTLN("MQTT: atomic /mqtt.json save failed during rename; recovery files preserved");
break;
}
return false;
}
#endif
@@ -1419,6 +1701,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error: delay must be between 0-10000 ms");
}
} else if (memcmp(config, "bridge.source ", 14) == 0) {
#ifdef WITH_MQTT_BRIDGE
MQTTPrefs* observer_rollback = new (std::nothrow) MQTTPrefs;
if (observer_rollback == nullptr) {
strcpy(reply, "Error: insufficient memory to update observer setting");
return;
}
memcpy(observer_rollback, &_mqtt_prefs, sizeof(*observer_rollback));
const uint8_t old_bridge_pkt_src = _prefs->bridge_pkt_src;
#endif
_prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0;
#ifdef WITH_MQTT_BRIDGE
if (_prefs->bridge_pkt_src == 1) {
@@ -1428,6 +1719,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_mqtt_prefs.mqtt_rx_enabled = 0;
_mqtt_prefs.mqtt_tx_enabled = 1;
}
_observer_prefs_rollback = observer_rollback;
if (!persistObserverPrefs(reply)) {
_prefs->bridge_pkt_src = old_bridge_pkt_src;
_observer_prefs_rollback = nullptr;
delete observer_rollback;
return;
}
_observer_prefs_rollback = nullptr;
delete observer_rollback;
#endif
savePrefs();
strcpy(reply, "OK");
+18 -4
View File
@@ -73,7 +73,7 @@ public:
uint8_t extra_sf[4];
// NOTE: observer settings (MQTT/WiFi/timezone/SNMP/alert) are not in NodePrefs.
// They live in MQTTPrefs, persisted separately to /mqtt_prefs, so this struct
// They live in MQTTPrefs, persisted separately to /mqtt.json, so this struct
// stays aligned with upstream. See struct MQTTPrefs below.
private:
@@ -228,6 +228,13 @@ struct LegacyObserverTail {
class CommonCLICallbacks {
public:
virtual void savePrefs() = 0;
#ifdef WITH_MQTT_BRIDGE
virtual bool saveObserverPrefs() = 0;
#else
virtual bool saveObserverPrefs() {
return false;
}
#endif
virtual const char* getFirmwareVer() = 0;
virtual const char* getBuildDate() = 0;
virtual const char* getRole() = 0;
@@ -359,8 +366,11 @@ class CommonCLI {
char tmp[PRV_KEY_SIZE*2 + 4];
#ifdef WITH_MQTT_BRIDGE
MQTTPrefs _mqtt_prefs;
// Points at a per-command snapshot only while an observer setter is running.
// persistObserverPrefs() uses it to undo RAM mutations when flash commit fails.
const MQTTPrefs* _observer_prefs_rollback = nullptr;
LegacyObserverTail _legacy_tail;
// /mqtt_prefs is newer, corrupt, or temporarily unreadable. The in-memory prefs
// /mqtt.json is newer, corrupt, or temporarily unreadable. The in-memory prefs
// run on defaults and saveMQTTPrefs() must not overwrite the source file.
bool _mqtt_prefs_hold = false;
#endif
@@ -384,6 +394,7 @@ class CommonCLI {
// false to fall through to the base get/set parsing.
bool handleObserverSetCmd(uint32_t sender_timestamp, const char* config, char* reply);
bool handleObserverGetCmd(uint32_t sender_timestamp, const char* config, char* reply);
bool persistObserverPrefs(char* reply);
// Observer-only top-level commands (ota check/update, tls.bundletest, alert test)
// also live in CommonCLI_Observer.cpp; returns true if it handled the command.
bool handleObserverCommand(uint32_t sender_timestamp, char* command, char* reply);
@@ -393,14 +404,17 @@ public:
: _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(&region_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
void loadPrefs(FILESYSTEM* _fs);
bool savePrefs(FILESYSTEM* _fs, bool save_mqtt = true);
// Node preferences and observer preferences are separate transactions.
// Callers must explicitly request an observer save when they changed it.
bool savePrefs(FILESYSTEM* _fs, bool save_mqtt = false);
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
mesh::MainBoard* getBoard() { return _board; }
uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
#ifdef WITH_MQTT_BRIDGE
// Observer config (MQTT/WiFi/timezone/SNMP/alert), persisted to /mqtt_prefs.
// Observer config (MQTT/WiFi/timezone/SNMP/alert), persisted to /mqtt.json.
// Exposed so the app can hand it to MQTTBridge/AlertReporter, which read these
// fields directly (they no longer live in NodePrefs).
MQTTPrefs* getObserverPrefs() const { return const_cast<MQTTPrefs*>(&_mqtt_prefs); }
bool saveObserverPrefs(FILESYSTEM* fs) { return saveMQTTPrefs(fs); }
#endif
};
+129 -61
View File
@@ -16,6 +16,7 @@
#include "AlertReporter.h" // for alertReporterBannedChannelMatch[Hex]()
#include "MQTTObserverValidation.h" // pure input validators (host-testable)
#include <Utils.h>
#include <new>
#ifdef ESP_PLATFORM
#include <WiFi.h>
#include <WiFiClientSecure.h>
@@ -109,6 +110,46 @@ static bool valueTooLong(const char* val, size_t bufsize, char* reply, const cha
return false;
}
static bool isObserverPrefsSetCommand(const char* config) {
return strncmp(config, "snmp", 4) == 0 ||
strncmp(config, "radio.watchdog", 14) == 0 ||
strncmp(config, "mqtt", 4) == 0 ||
strncmp(config, "wifi.", 5) == 0 ||
strncmp(config, "timezone", 8) == 0 ||
strncmp(config, "alert", 5) == 0;
}
// Keep observer setters atomic from the caller's perspective. The live object
// is restored if its JSON transaction fails, and the snapshot pointer is valid
// only for this synchronous command dispatch.
class ObserverPrefsRollbackScope {
public:
ObserverPrefsRollbackScope(const MQTTPrefs** active_snapshot,
const MQTTPrefs& live,
bool capture)
: _active_snapshot(active_snapshot) {
*_active_snapshot = nullptr;
if (capture) {
_snapshot = new (std::nothrow) MQTTPrefs;
if (_snapshot != nullptr) {
memcpy(_snapshot, &live, sizeof(*_snapshot));
*_active_snapshot = _snapshot;
}
}
}
~ObserverPrefsRollbackScope() {
*_active_snapshot = nullptr;
delete _snapshot;
}
bool available() const { return _snapshot != nullptr; }
private:
const MQTTPrefs** _active_snapshot;
MQTTPrefs* _snapshot = nullptr;
};
static const char* getMQTTPresetNameByIndex(int index) {
if (index < MQTT_PRESET_COUNT) return MQTT_PRESETS[index].name;
if (index == MQTT_PRESET_COUNT) return MQTT_PRESET_CUSTOM;
@@ -161,17 +202,38 @@ static void formatMQTTPresetListReply(char* reply, size_t reply_size, int start)
}
#endif
bool CommonCLI::persistObserverPrefs(char* reply) {
#ifdef WITH_MQTT_BRIDGE
if (_callbacks->saveObserverPrefs()) return true;
if (_observer_prefs_rollback != nullptr) {
memcpy(&_mqtt_prefs, _observer_prefs_rollback, sizeof(_mqtt_prefs));
}
strcpy(reply, "Error: setting not persisted; change rolled back");
return false;
#else
(void)reply;
return false;
#endif
}
bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* config, char* reply) {
#ifdef WITH_MQTT_BRIDGE
const bool needs_snapshot = isObserverPrefsSetCommand(config);
ObserverPrefsRollbackScope rollback_scope(
&_observer_prefs_rollback, _mqtt_prefs, needs_snapshot);
if (needs_snapshot && !rollback_scope.available()) {
strcpy(reply, "Error: insufficient memory to update observer setting");
return true;
}
bool handled = true;
if (memcmp(config, "snmp.community ", 15) == 0) {
if (valueTooLong(&config[15], sizeof(_mqtt_prefs.snmp_community), reply, "snmp.community")) return true;
StrHelper::strncpy(_mqtt_prefs.snmp_community, &config[15], sizeof(_mqtt_prefs.snmp_community));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK - restart to apply");
} else if (memcmp(config, "snmp ", 5) == 0) {
_mqtt_prefs.snmp_enabled = memcmp(&config[5], "on", 2) == 0;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK - restart to apply");
} else if (memcmp(config, "radio.watchdog ", 15) == 0) {
const char* val = &config[15];
@@ -189,7 +251,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: radio.watchdog must be 0-120 minutes");
} else {
_mqtt_prefs.radio_watchdog_minutes = (uint8_t)mins;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
if (mins == 0) {
strcpy(reply, "OK - radio watchdog disabled");
} else {
@@ -200,13 +262,13 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
#ifdef WITH_MQTT_BRIDGE
} else if (strcmp(config, "mqtt.origin") == 0) {
_mqtt_prefs.mqtt_origin[0] = '\0';
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.origin ", 12) == 0) {
if (valueTooLong(&config[12], sizeof(_mqtt_prefs.mqtt_origin), reply, "origin")) return true;
StrHelper::strncpy(_mqtt_prefs.mqtt_origin, &config[12], sizeof(_mqtt_prefs.mqtt_origin));
StrHelper::stripSurroundingQuotes(_mqtt_prefs.mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.iata ", 10) == 0) {
const char* iata = &config[10];
@@ -215,7 +277,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
// Empty clears the region code (meshcore-topic publishing stays disabled
// until one is set). This keeps the pre-existing "clear IATA" capability.
_mqtt_prefs.mqtt_iata[0] = '\0';
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridge();
strcpy(reply, "OK - IATA cleared");
} else {
@@ -228,22 +290,22 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
for (int i = 0; _mqtt_prefs.mqtt_iata[i]; i++) {
_mqtt_prefs.mqtt_iata[i] = toupper(_mqtt_prefs.mqtt_iata[i]);
}
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridge();
strcpy(reply, "OK");
}
}
} else if (memcmp(config, "mqtt.status ", 12) == 0) {
_mqtt_prefs.mqtt_status_enabled = memcmp(&config[12], "on", 2) == 0;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.packets ", 13) == 0) {
_mqtt_prefs.mqtt_packets_enabled = memcmp(&config[13], "on", 2) == 0;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.raw ", 9) == 0) {
_mqtt_prefs.mqtt_raw_enabled = memcmp(&config[9], "on", 2) == 0;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.tx ", 8) == 0) {
if (memcmp(&config[8], "advert", 6) == 0) {
@@ -251,17 +313,17 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else {
_mqtt_prefs.mqtt_tx_enabled = memcmp(&config[8], "on", 2) == 0 ? 1 : 0;
}
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.rx ", 8) == 0) {
_mqtt_prefs.mqtt_rx_enabled = memcmp(&config[8], "on", 2) == 0 ? 1 : 0;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.interval ", 14) == 0) {
uint32_t minutes = _atoi(&config[14]);
if (minutes >= 1 && minutes <= 60) {
_mqtt_prefs.mqtt_status_interval = minutes * 60000;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridge();
sprintf(reply, "OK - interval set to %u minutes (%lu ms), bridge restarted", minutes, (unsigned long)_mqtt_prefs.mqtt_status_interval);
} else {
@@ -274,7 +336,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
uint32_t hours = _atoi(&config[24]);
if (hours >= MQTT_NEIGHBORS_MIN_INTERVAL_HOURS && hours <= MQTT_NEIGHBORS_MAX_INTERVAL_HOURS) {
_mqtt_prefs.mqtt_neighbors_interval = hours * 3600000UL;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
sprintf(reply, "OK - neighbors interval set to %u hours (%lu ms)", (unsigned)hours,
(unsigned long)_mqtt_prefs.mqtt_neighbors_interval);
} else {
@@ -284,7 +346,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
// The mesh loop reads this live, so no bridge restart is needed; enabling it
// triggers a discovery on the next eligible loop pass.
_mqtt_prefs.mqtt_neighbors_enabled = memcmp(&config[15], "on", 2) == 0;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
#elif defined(WITH_MQTT_BRIDGE)
} else if (memcmp(config, "mqtt.neighbors.interval ", 24) == 0 ||
@@ -303,7 +365,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else {
StrHelper::strncpy(_mqtt_prefs.mqtt_ntp_server, host, sizeof(_mqtt_prefs.mqtt_ntp_server));
}
savePrefs();
if (!persistObserverPrefs(reply)) return true;
#ifdef ESP_PLATFORM
// Queue a sync on the MQTT task (Core 0) but do NOT block: this handler
// runs on the Arduino loop task, shared with mesh/radio processing and the
@@ -325,12 +387,12 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else if (memcmp(config, "wifi.ssid ", 10) == 0) {
if (valueTooLong(&config[10], sizeof(_mqtt_prefs.wifi_ssid), reply, "wifi.ssid")) return true;
StrHelper::strncpy(_mqtt_prefs.wifi_ssid, &config[10], sizeof(_mqtt_prefs.wifi_ssid));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "wifi.pwd ", 9) == 0) {
if (valueTooLong(&config[9], sizeof(_mqtt_prefs.wifi_password), reply, "wifi.pwd")) return true;
StrHelper::strncpy(_mqtt_prefs.wifi_password, &config[9], sizeof(_mqtt_prefs.wifi_password));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "wifi.powersave ", 15) == 0) {
const char* value = &config[15];
@@ -350,7 +412,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: must be none, min, or max");
} else {
_mqtt_prefs.wifi_power_save = ps_value;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
#ifdef ESP_PLATFORM
if (WiFi.status() == WL_CONNECTED) {
wifi_ps_type_t ps_mode = (ps_value == 1) ? WIFI_PS_NONE :
@@ -374,13 +436,13 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else if (memcmp(config, "timezone ", 9) == 0) {
if (valueTooLong(&config[9], sizeof(_mqtt_prefs.timezone_string), reply, "timezone")) return true;
StrHelper::strncpy(_mqtt_prefs.timezone_string, &config[9], sizeof(_mqtt_prefs.timezone_string));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "timezone.offset ", 16) == 0) {
int8_t offset = _atoi(&config[16]);
if (offset >= -12 && offset <= 14) {
_mqtt_prefs.timezone_offset = offset;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: timezone offset must be between -12 and +14");
@@ -397,20 +459,14 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcmp(preset_name, MQTT_PRESET_CUSTOM) == 0 ||
strcmp(preset_name, MQTT_PRESET_NONE) == 0) {
// Reject duplicate presets (except "none" and "custom")
int dup_slot = -1;
if (findMQTTPreset(preset_name) != nullptr) {
for (int s = 0; s < MAX_MQTT_SLOTS; s++) {
if (s != slot && strcmp(_mqtt_prefs.mqtt_slot_preset[s], preset_name) == 0) {
dup_slot = s;
break;
}
}
}
const int dup_slot = findMQTTPreset(preset_name) == nullptr ? -1 :
mqttAssignedPresetSlot(_mqtt_prefs.mqtt_slot_preset,
preset_name, slot);
if (dup_slot >= 0) {
sprintf(reply, "Error: preset '%s' is already assigned to slot %d", preset_name, dup_slot + 1);
} else {
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], preset_name, sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
// Check if the slot has everything it needs to connect
const MQTTPresetDef* p = findMQTTPreset(preset_name);
@@ -469,7 +525,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else if (memcmp(subcmd, "server ", 7) == 0) {
if (valueTooLong(&subcmd[7], sizeof(_mqtt_prefs.mqtt_slot_host[slot]), reply, "server")) return true;
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_host[slot], &subcmd[7], sizeof(_mqtt_prefs.mqtt_slot_host[slot]));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
// Reconfigure the slot so the new host reaches the live connection (other
// custom-slot setters do the same; without it the change only applies on
// the next reboot/bridge restart).
@@ -479,7 +535,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
int port = atoi(&subcmd[5]);
if (port > 0 && port <= 65535) {
_mqtt_prefs.mqtt_slot_port[slot] = port;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else {
@@ -488,19 +544,19 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else if (memcmp(subcmd, "username ", 9) == 0) {
if (valueTooLong(&subcmd[9], sizeof(_mqtt_prefs.mqtt_slot_username[slot]), reply, "username")) return true;
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_username[slot], &subcmd[9], sizeof(_mqtt_prefs.mqtt_slot_username[slot]));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (memcmp(subcmd, "password ", 9) == 0) {
if (valueTooLong(&subcmd[9], sizeof(_mqtt_prefs.mqtt_slot_password[slot]), reply, "password")) return true;
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_password[slot], &subcmd[9], sizeof(_mqtt_prefs.mqtt_slot_password[slot]));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (memcmp(subcmd, "token ", 6) == 0) {
if (valueTooLong(&subcmd[6], sizeof(_mqtt_prefs.mqtt_slot_token[slot]), reply, "token")) return true;
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_token[slot], &subcmd[6], sizeof(_mqtt_prefs.mqtt_slot_token[slot]));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
sprintf(reply, "OK - slot %d token set", slot + 1);
} else if (memcmp(subcmd, "topic ", 6) == 0) {
@@ -510,14 +566,14 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
return true;
} else {
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_topic[slot], &subcmd[6], sizeof(_mqtt_prefs.mqtt_slot_topic[slot]));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
sprintf(reply, "OK - slot %d topic: %s", slot + 1, _mqtt_prefs.mqtt_slot_topic[slot]);
}
} else if (memcmp(subcmd, "audience ", 9) == 0) {
if (valueTooLong(&subcmd[9], sizeof(_mqtt_prefs.mqtt_slot_audience[slot]), reply, "audience")) return true;
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_audience[slot], &subcmd[9], sizeof(_mqtt_prefs.mqtt_slot_audience[slot]));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
if (_mqtt_prefs.mqtt_slot_audience[slot][0] != '\0') {
sprintf(reply, "OK - slot %d JWT audience: %s", slot + 1, _mqtt_prefs.mqtt_slot_audience[slot]);
@@ -527,7 +583,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else if (memcmp(subcmd, "audience", 8) == 0 && subcmd[8] == '\0') {
// "set mqttN.audience" with no value — clear the audience
_mqtt_prefs.mqtt_slot_audience[slot][0] = '\0';
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
sprintf(reply, "OK - slot %d JWT audience cleared (using username/password auth)", slot + 1);
} else if (strcmp(subcmd, "filter") == 0 ||
@@ -539,20 +595,18 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: filter must be all, none, or a CSV of types 0-15 / names (advert,txt_msg,...)");
} else {
_mqtt_prefs.mqtt_slot_packet_filter[slot] = filter_mask;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
char filter_text[MQTTPacketFilter::kFilterTextSize];
MQTTPacketFilter::format(filter_mask, filter_text, sizeof(filter_text));
snprintf(reply, 160, "OK - slot %d packet types: %s", slot + 1, filter_text);
// A non-default filter extends /mqtt_prefs past what pre-filter
// firmware can read (see MQTTPrefsCodec::payloadLenFor), so say when
// that cost buys nothing: slots beyond the runtime array are never
// published to on this board, the same warning `preset` gives.
// Slots beyond the runtime array are never published to on this board,
// so retain the same inactive-hardware warning `preset` gives.
if (slot >= RUNTIME_MQTT_SLOTS &&
filter_mask != MQTTPacketFilter::kAllPacketTypes) {
size_t used = strlen(reply);
if (used < 158) {
snprintf(reply + used, 160 - used,
" (slot inactive on this hardware; blocks firmware rollback)");
" (slot inactive on this hardware)");
}
}
}
@@ -562,21 +616,35 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else if (memcmp(config, "mqtt.analyzer.us ", 17) == 0) {
const int slot = 0;
if (memcmp(&config[17], "on", 2) == 0) {
const int dup_slot = mqttAssignedPresetSlot(
_mqtt_prefs.mqtt_slot_preset, "analyzer-us", slot);
if (dup_slot >= 0) {
sprintf(reply, "Error: preset 'analyzer-us' is already assigned to slot %d",
dup_slot + 1);
return true;
}
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], "analyzer-us", sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
} else {
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
}
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.analyzer.eu ", 17) == 0) {
const int slot = 1;
if (memcmp(&config[17], "on", 2) == 0) {
const int dup_slot = mqttAssignedPresetSlot(
_mqtt_prefs.mqtt_slot_preset, "analyzer-eu", slot);
if (dup_slot >= 0) {
sprintf(reply, "Error: preset 'analyzer-eu' is already assigned to slot %d",
dup_slot + 1);
return true;
}
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], "analyzer-eu", sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
} else {
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
}
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.owner ", 11) == 0) {
@@ -585,11 +653,11 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
// Owner key is optional — empty clears it (previously this errored, so a
// set key could never be removed via the portal/CLI).
_mqtt_prefs.mqtt_owner_public_key[0] = '\0';
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK - owner key cleared");
} else if (mqttOwnerKeyValid(owner_key)) {
StrHelper::strncpy(_mqtt_prefs.mqtt_owner_public_key, owner_key, sizeof(_mqtt_prefs.mqtt_owner_public_key));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: public key must be 64 hex characters (32 bytes)");
@@ -597,7 +665,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else if (memcmp(config, "mqtt.email ", 11) == 0) {
if (valueTooLong(&config[11], sizeof(_mqtt_prefs.mqtt_email), reply, "email")) return true;
StrHelper::strncpy(_mqtt_prefs.mqtt_email, &config[11], sizeof(_mqtt_prefs.mqtt_email));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
#endif
} else if (memcmp(config, "alert ", 6) == 0) {
@@ -605,12 +673,12 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
const char* val = &config[6];
if (memcmp(val, "on", 2) == 0 && (val[2] == 0 || val[2] == ' ')) {
_mqtt_prefs.alert_enabled = 1;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alerts on");
} else if (memcmp(val, "off", 3) == 0 && (val[3] == 0 || val[3] == ' ')) {
_mqtt_prefs.alert_enabled = 0;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alerts off");
} else {
@@ -625,7 +693,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
if (len == 0) {
_mqtt_prefs.alert_psk_hex[0] = '\0';
_mqtt_prefs.alert_hashtag[0] = '\0';
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.psk cleared (alerts disabled until configured)");
} else if (val[0] == '#') {
@@ -659,7 +727,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
// The new PSK is operator-supplied, so any previously-derived
// hashtag name is no longer accurate provenance — drop it.
_mqtt_prefs.alert_hashtag[0] = '\0';
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.psk updated");
}
@@ -672,7 +740,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
if (in_len == 0) {
_mqtt_prefs.alert_psk_hex[0] = '\0';
_mqtt_prefs.alert_hashtag[0] = '\0';
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.hashtag cleared (alerts disabled until configured)");
} else {
@@ -708,7 +776,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
mesh::Utils::toHex(hex, digest, 16);
StrHelper::strncpy(_mqtt_prefs.alert_hashtag, hashtag, sizeof(_mqtt_prefs.alert_hashtag));
StrHelper::strncpy(_mqtt_prefs.alert_psk_hex, hex, sizeof(_mqtt_prefs.alert_psk_hex));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->onAlertConfigChanged();
sprintf(reply, "OK - alert.hashtag: %s", _mqtt_prefs.alert_hashtag);
}
@@ -726,7 +794,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
size_t len = strlen(val);
if (len == 0) {
_mqtt_prefs.alert_region[0] = '\0';
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.region cleared (using default scope)");
} else if (len >= sizeof(_mqtt_prefs.alert_region)) {
@@ -734,7 +802,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else {
StrHelper::strncpy(_mqtt_prefs.alert_region, val, sizeof(_mqtt_prefs.alert_region));
StrHelper::stripSurroundingQuotes(_mqtt_prefs.alert_region, sizeof(_mqtt_prefs.alert_region));
savePrefs();
if (!persistObserverPrefs(reply)) return true;
_callbacks->onAlertConfigChanged();
sprintf(reply, "OK - alert.region: %s", _mqtt_prefs.alert_region);
}
@@ -744,7 +812,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: alert.wifi must be 0-1440 minutes (0=off)");
} else {
_mqtt_prefs.alert_wifi_minutes = (uint16_t)mins;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
sprintf(reply, "OK - alert.wifi %d min%s", mins, mins == 0 ? " (disabled)" : "");
}
} else if (memcmp(config, "alert.mqtt ", 11) == 0) {
@@ -753,7 +821,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: alert.mqtt must be 0-10080 minutes (0=off)");
} else {
_mqtt_prefs.alert_mqtt_minutes = (uint16_t)mins;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
sprintf(reply, "OK - alert.mqtt %d min%s", mins, mins == 0 ? " (disabled)" : "");
}
} else if (memcmp(config, "alert.interval ", 15) == 0) {
@@ -764,7 +832,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: alert.interval must be 60-10080 minutes");
} else {
_mqtt_prefs.alert_min_interval_min = (uint16_t)mins;
savePrefs();
if (!persistObserverPrefs(reply)) return true;
sprintf(reply, "OK - alert.interval %d min", mins);
}
} else {
+132 -4
View File
@@ -1,5 +1,9 @@
#include "ConfigSerializer.h"
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
bool ConfigSerializer::saveSerial(Stream& s) {
Context context(&s, OP::WRITE);
_context = &context; // set the context for structure() call
@@ -22,9 +26,12 @@ bool ConfigSerializer::saveSerial(Stream& s) {
static bool is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
static bool is_key_char(char c) {
static bool is_key_start_char(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
}
static bool is_key_char(char c) {
return is_key_start_char(c) || (c >= '0' && c <= '9');
}
static bool is_value_char(char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.';
}
@@ -64,14 +71,26 @@ int ConfigSerializer::Context::readNext() {
case EXPECT_KEY:
if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; }
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE;
if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
if (rd_len < CONFIG_MAX_KEYLEN-1 &&
((rd_len == 0 && is_key_start_char(c)) || (rd_len > 0 && is_key_char(c)))) {
rd_buf[rd_len++] = c;
return TOK_WHITESPACE;
}
return TOK_ERROR;
case EXPECT_VAL_OR_OBJ:
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE;
if (rd_len == 0 && c == '"') { rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; }
if (rd_len == 0 && c == '"') {
rd_token_quoted = true;
rd_mode = EXPECT_STRING_VAL;
return TOK_WHITESPACE;
}
if (rd_len == 0 && c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; }
if (is_value_char(c) && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
if (is_value_char(c) && rd_len < CONFIG_MAX_TOKEN_LEN-1) {
if (rd_len == 0) rd_token_quoted = false;
rd_buf[rd_len++] = c;
return TOK_WHITESPACE;
}
if (rd_len > 0 && (c == ',' || c == '}' || is_whitespace(c))) { pending = c; rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; }
return TOK_ERROR;
@@ -107,9 +126,19 @@ bool ConfigSerializer::loadSerial(Stream& s) {
if (next_tok == TOK_KEY) {
context.setKey(sp, context.getToken());
} else if (next_tok == TOK_VALUE) {
context.setValueEvent(sp, false);
_depth = 1; // re-run the structure() hierarchy again (looking for specific key, at specific depth)
structure();
} else if (next_tok == TOK_START_OBJ) {
// The root has no key. For every nested object, rerun the schema at the
// parent depth so strict scalar fields can reject an object value and
// object fields can confirm that their value has the expected shape.
if (sp > 0) {
context.setValueEvent(sp, true);
_depth = 1;
structure();
if (!context.success) break;
}
if (sp < CONFIG_MAX_DEPTH - 1) {
sp++;
} else {
@@ -153,6 +182,10 @@ void ConfigSerializer::def(const char* key, void* value, size_t len) {
_context->file()->print("\"");
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
memset(value, 0, len);
mesh::Utils::fromHex((uint8_t *)value, len, _context->getToken());
}
@@ -181,6 +214,10 @@ void ConfigSerializer::def(const char* key, char* value, size_t max_len) {
_context->file()->print("\"");
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
strncpy(value, _context->getToken(), max_len - 1);
value[max_len - 1] = 0;
}
@@ -195,6 +232,10 @@ void ConfigSerializer::def(const char* key, int32_t& value) {
_context->file()->print(value);
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
value = atol(_context->getToken());
}
}
@@ -208,6 +249,10 @@ void ConfigSerializer::def(const char* key, uint32_t& value) {
_context->file()->print(value);
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
value = atol(_context->getToken());
}
}
@@ -221,6 +266,10 @@ void ConfigSerializer::def(const char* key, int16_t& value) {
_context->file()->print((int32_t) value, 10);
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
value = atol(_context->getToken());
}
}
@@ -234,6 +283,10 @@ void ConfigSerializer::def(const char* key, uint16_t& value) {
_context->file()->print((uint32_t) value, 10);
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
value = atoi(_context->getToken());
}
}
@@ -247,6 +300,10 @@ void ConfigSerializer::def(const char* key, uint8_t& value) {
_context->file()->print((uint32_t) value, 10);
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
value = atoi(_context->getToken());
}
}
@@ -260,6 +317,10 @@ void ConfigSerializer::def(const char* key, int8_t& value) {
_context->file()->print((int32_t) value, 10);
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
value = atoi(_context->getToken());
}
}
@@ -273,6 +334,10 @@ void ConfigSerializer::def(const char* key, bool& value) {
_context->file()->print(value ? "true" : "false");
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
value = strcmp(_context->getToken(), "true") == 0 || atoi(_context->getToken()) != 0; // 'true' or a non-zero number
}
}
@@ -290,6 +355,10 @@ void ConfigSerializer::def(const char* key, double& value) {
}
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
value = atof(_context->getToken());
}
}
@@ -307,6 +376,10 @@ void ConfigSerializer::def(const char* key, float& value) {
}
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() || _context->valueDepth() != _depth) {
_context->success = false;
return;
}
value = (float) atof(_context->getToken());
}
}
@@ -323,9 +396,64 @@ void ConfigSerializer::def(const char* key, ConfigSerializer& sub_obj) {
if (_context->file()->print("}") != 1) _context->success = false; // failure detect
} else {
if (_context->keyMatch(_depth, key)) {
if (_context->objectStart() && _context->valueDepth() == _depth) {
return; // the object itself; descendant values recurse below
}
if (_context->valueDepth() <= _depth) {
_context->success = false; // known object supplied as a scalar
return;
}
sub_obj._context = _context; // inherit the Context
sub_obj._depth = _depth + 1;
sub_obj.structure(); // recurse into sub object
}
}
}
bool ConfigSerializer::defStrict(const char* key, char* value, size_t max_len, bool& seen) {
if (_context->op() == OP::WRITE) {
def(key, value, max_len);
return true;
}
if (!_context->keyMatch(_depth, key)) return false;
if (seen || max_len == 0 || _context->objectStart() ||
_context->valueDepth() != _depth || !_context->tokenQuoted()) {
_context->success = false;
return false;
}
seen = true;
const char* token = _context->getToken();
const size_t len = strlen(token);
if (len >= max_len) {
_context->success = false;
return false;
}
memcpy(value, token, len + 1);
return true;
}
bool ConfigSerializer::defStrict(const char* key, int32_t& value, bool& seen) {
if (_context->op() == OP::WRITE) {
def(key, value);
return true;
}
if (!_context->keyMatch(_depth, key)) return false;
if (seen || _context->objectStart() || _context->valueDepth() != _depth ||
_context->tokenQuoted()) {
_context->success = false;
return false;
}
seen = true;
const char* token = _context->getToken();
char* end = nullptr;
errno = 0;
const long long parsed = strtoll(token, &end, 10);
if (token[0] == '\0' || end == token || *end != '\0' || errno == ERANGE ||
parsed < INT32_MIN || parsed > INT32_MAX) {
_context->success = false;
return false;
}
value = static_cast<int32_t>(parsed);
return true;
}
+80 -1
View File
@@ -25,17 +25,35 @@ class ConfigSerializer {
OP _op;
uint8_t rd_len;
uint8_t rd_mode;
uint8_t rd_value_depth;
char pending;
bool rd_token_quoted;
bool rd_object_start;
char rd_buf[CONFIG_MAX_TOKEN_LEN];
char _keys[CONFIG_MAX_DEPTH][CONFIG_MAX_KEYLEN];
public:
bool success = true;
Context(Stream* f, OP op) : _f(f), _op(op) { rd_buf[rd_len = 0] = 0; rd_mode = 0; pending = 0; }
Context(Stream* f, OP op) : _f(f), _op(op) {
rd_buf[rd_len = 0] = 0;
rd_mode = 0;
rd_value_depth = 0;
pending = 0;
rd_token_quoted = false;
rd_object_start = false;
memset(_keys, 0, sizeof(_keys));
}
OP op() const { return _op; }
Stream* file() const { return _f; }
int readNext();
const char* getToken() const { return rd_buf; }
bool tokenQuoted() const { return rd_token_quoted; }
uint8_t valueDepth() const { return rd_value_depth; }
bool objectStart() const { return rd_object_start; }
void setValueEvent(uint8_t depth, bool object_start) {
rd_value_depth = depth;
rd_object_start = object_start;
}
bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; }
void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); }
};
@@ -60,6 +78,67 @@ protected:
void def(const char* key, bool& value);
void def(const char* key, ConfigSerializer& sub_obj);
// Strict read helpers for schemas whose values may be edited outside the
// firmware. Unlike the legacy def() overloads, these reject duplicate keys,
// truncated strings, malformed integers, and integer overflow. `seen` must
// be a field owned by the schema object and initialized false before load.
bool defStrict(const char* key, char* value, size_t max_len, bool& seen);
bool defStrict(const char* key, int32_t& value, bool& seen);
// Literal keys are checked where a schema defines them, so adding a field
// that an older ConfigSerializer cannot tokenize is a build failure rather
// than a silent downgrade trap. Dynamic keys retain the pointer overloads.
template <size_t N> static void checkKey(const char (&)[N]) {
static_assert(N <= CONFIG_MAX_KEYLEN,
"ConfigSerializer key exceeds the visible-key limit");
}
template <size_t N> void def(const char (&key)[N], char* value, size_t max_len) {
checkKey(key); def(static_cast<const char*>(key), value, max_len);
}
template <size_t N> void def(const char (&key)[N], void* value, size_t len) {
checkKey(key); def(static_cast<const char*>(key), value, len);
}
template <size_t N> void def(const char (&key)[N], int32_t& value) {
checkKey(key); def(static_cast<const char*>(key), value);
}
template <size_t N> void def(const char (&key)[N], int16_t& value) {
checkKey(key); def(static_cast<const char*>(key), value);
}
template <size_t N> void def(const char (&key)[N], int8_t& value) {
checkKey(key); def(static_cast<const char*>(key), value);
}
template <size_t N> void def(const char (&key)[N], uint32_t& value) {
checkKey(key); def(static_cast<const char*>(key), value);
}
template <size_t N> void def(const char (&key)[N], uint16_t& value) {
checkKey(key); def(static_cast<const char*>(key), value);
}
template <size_t N> void def(const char (&key)[N], uint8_t& value) {
checkKey(key); def(static_cast<const char*>(key), value);
}
template <size_t N> void def(const char (&key)[N], float& value) {
checkKey(key); def(static_cast<const char*>(key), value);
}
template <size_t N> void def(const char (&key)[N], double& value) {
checkKey(key); def(static_cast<const char*>(key), value);
}
template <size_t N> void def(const char (&key)[N], bool& value) {
checkKey(key); def(static_cast<const char*>(key), value);
}
template <size_t N> void def(const char (&key)[N], ConfigSerializer& sub_obj) {
checkKey(key); def(static_cast<const char*>(key), sub_obj);
}
template <size_t N>
bool defStrict(const char (&key)[N], char* value, size_t max_len, bool& seen) {
checkKey(key);
return defStrict(static_cast<const char*>(key), value, max_len, seen);
}
template <size_t N>
bool defStrict(const char (&key)[N], int32_t& value, bool& seen) {
checkKey(key);
return defStrict(static_cast<const char*>(key), value, seen);
}
virtual void structure() = 0;
public:
+1 -1
View File
@@ -8,7 +8,7 @@
#include "MQTTPacketFilter.h"
#include "MQTTPresets.h"
// Compile-time defaults for fresh /mqtt_prefs (override via platformio build_flags).
// Compile-time defaults for fresh /mqtt.json (override via platformio build_flags).
// Example:
// -D MQTT_DEFAULT_SLOT1_PRESET='"meshcore-ca-1"'
// -D MQTT_DEFAULT_IATA='"YYZ"'
+17
View File
@@ -58,3 +58,20 @@ static inline bool mqttNtpHostnameValid(const char* host) {
static inline bool mqttValueFits(const char* s, size_t bufsize) {
return s != NULL && bufsize > 0 && strlen(s) < bufsize;
}
// Find a preset already assigned to another slot. The caller decides which
// names are singleton presets ("none" and "custom" intentionally are not).
template <size_t SlotCount, size_t PresetSize>
static inline int mqttAssignedPresetSlot(
const char (&presets)[SlotCount][PresetSize],
const char* preset_name,
int ignored_slot) {
if (preset_name == NULL) return -1;
for (size_t slot = 0; slot < SlotCount; ++slot) {
if (static_cast<int>(slot) != ignored_slot &&
strcmp(presets[slot], preset_name) == 0) {
return static_cast<int>(slot);
}
}
return -1;
}
+4 -4
View File
@@ -71,8 +71,8 @@ inline bool resolveToken(const char* begin, size_t len, uint8_t* type_out) {
}
// Parse an allowlist value. Empty input means "all" so WebConfig can clear a
// field and retain the same backwards-compatible default as an older
// /mqtt_prefs file. Keywords and type names are deliberately lowercase; "all"
// field and retain the same all-packet-types default as an older preference
// file. Keywords and type names are deliberately lowercase; "all"
// and "none" cannot be mixed into a list. Entries may be decimal (0..15) or
// named, may carry surrounding ASCII whitespace, and may be repeated.
inline bool parse(const char* input, uint16_t* mask_out) {
@@ -186,8 +186,8 @@ inline uint16_t enabledUnion(const uint16_t* masks, const bool* enabled, size_t
return combined;
}
// True when every slot still carries the default all-types mask, i.e. nothing
// depends on the packet-filter tail of /mqtt_prefs being written.
// True when every slot still carries the default all-types mask. The legacy
// binary encoder uses this while producing migration fixtures.
inline bool allMasksDefault(const uint16_t* masks, size_t count) {
if (masks == nullptr) return true;
for (size_t i = 0; i < count; ++i) {
+44 -5
View File
@@ -3,12 +3,51 @@
#include <stddef.h>
#include <stdint.h>
// Transactional writer for /mqtt_prefs. The Store interface is intentionally
// narrow so host tests can exercise every failure boundary without an Arduino
// filesystem: begin(), write(), finish(), commit(), and abort(). The caller
// supplies header and payload separately, avoiding a second full-size buffer.
// Transactional writer policy for preferences. The Store interface is
// intentionally narrow so host tests can exercise every failure boundary
// without an Arduino filesystem.
namespace MQTTPrefsAtomicStore {
// Production /mqtt.json flow. finish() verifies the exact bytes written,
// verify_image reparses the finished temp through the schema, and only then is
// commit() allowed to publish it. A schema-verification failure discards the
// finished temp; a commit failure preserves it for boot recovery.
enum class VerifiedImageResult : uint8_t {
Committed,
BeginFailed,
WriteFailed,
FinishFailed,
VerifyFailed,
CommitFailed,
};
template <typename Store, typename ImageWriter, typename ImageVerifier>
inline VerifiedImageResult writeVerifiedImage(Store& store,
ImageWriter write_image,
ImageVerifier verify_image) {
if (!store.begin()) {
store.abort();
return VerifiedImageResult::BeginFailed;
}
if (!write_image()) {
store.abort();
return VerifiedImageResult::WriteFailed;
}
if (!store.finish()) {
store.abort();
return VerifiedImageResult::FinishFailed;
}
if (!verify_image()) {
store.discardFinishedTemp();
return VerifiedImageResult::VerifyFailed;
}
if (!store.commit()) {
store.abort();
return VerifiedImageResult::CommitFailed;
}
return VerifiedImageResult::Committed;
}
enum class Result : uint8_t {
Committed,
BeginFailed,
@@ -59,7 +98,7 @@ inline ImageResult writeImage(Store& store, ImageWriter write_image) {
}
// Coordinates a two-file legacy upgrade. /com_prefs must not be compacted
// until the observer tail it carries has been published into /mqtt_prefs.
// until the observer tail it carries has been published into /mqtt.json.
// Keeping this state in a tiny pure helper lets host tests cover power-cut
// boundaries without an Arduino filesystem.
class LegacyUpgradeGate {
+165 -1
View File
@@ -53,6 +53,9 @@ static const size_t kEncodedSize = sizeof(MQTTPrefsHeader) + kV1BaselinePayloadS
// refuses to overwrite the file, so the node cannot be recovered over the air.
// Touching any filter opts that node into the longer payload — a deliberate,
// operator-initiated trade rather than a side effect of upgrading.
// The encoder below is retained for host migration fixtures and downgrade
// compatibility tests. Production firmware reads legacy binary files but only
// writes the JSON schema.
inline size_t payloadLenFor(const MQTTPrefs& prefs) {
return MQTTPacketFilter::allMasksDefault(prefs.mqtt_slot_packet_filter,
MQTT_PREFS_SLOT_COUNT)
@@ -68,13 +71,60 @@ inline MQTTPrefsHeader makeHeader(size_t payload_len) {
return header;
}
inline void freezeV1(const MQTTPrefs& prefs, LegacyV1MQTTPrefs* frozen) {
if (frozen == nullptr) return;
memset(frozen, 0, sizeof(*frozen));
memcpy(frozen->mqtt_origin, prefs.mqtt_origin, sizeof(frozen->mqtt_origin));
memcpy(frozen->mqtt_iata, prefs.mqtt_iata, sizeof(frozen->mqtt_iata));
frozen->mqtt_status_enabled = prefs.mqtt_status_enabled;
frozen->mqtt_packets_enabled = prefs.mqtt_packets_enabled;
frozen->mqtt_raw_enabled = prefs.mqtt_raw_enabled;
frozen->mqtt_tx_enabled = prefs.mqtt_tx_enabled;
frozen->mqtt_status_interval = prefs.mqtt_status_interval;
memcpy(frozen->wifi_ssid, prefs.wifi_ssid, sizeof(frozen->wifi_ssid));
memcpy(frozen->wifi_password, prefs.wifi_password, sizeof(frozen->wifi_password));
frozen->wifi_power_save = prefs.wifi_power_save;
memcpy(frozen->timezone_string, prefs.timezone_string, sizeof(frozen->timezone_string));
frozen->timezone_offset = prefs.timezone_offset;
memcpy(frozen->mqtt_slot_preset, prefs.mqtt_slot_preset, sizeof(frozen->mqtt_slot_preset));
memcpy(frozen->mqtt_slot_host, prefs.mqtt_slot_host, sizeof(frozen->mqtt_slot_host));
memcpy(frozen->mqtt_slot_port, prefs.mqtt_slot_port, sizeof(frozen->mqtt_slot_port));
memcpy(frozen->mqtt_slot_username, prefs.mqtt_slot_username, sizeof(frozen->mqtt_slot_username));
memcpy(frozen->mqtt_slot_password, prefs.mqtt_slot_password, sizeof(frozen->mqtt_slot_password));
memcpy(frozen->mqtt_owner_public_key, prefs.mqtt_owner_public_key,
sizeof(frozen->mqtt_owner_public_key));
memcpy(frozen->mqtt_email, prefs.mqtt_email, sizeof(frozen->mqtt_email));
memcpy(frozen->mqtt_slot_token, prefs.mqtt_slot_token, sizeof(frozen->mqtt_slot_token));
memcpy(frozen->mqtt_slot_topic, prefs.mqtt_slot_topic, sizeof(frozen->mqtt_slot_topic));
memcpy(frozen->mqtt_slot_audience, prefs.mqtt_slot_audience,
sizeof(frozen->mqtt_slot_audience));
frozen->mqtt_rx_enabled = prefs.mqtt_rx_enabled;
memcpy(frozen->mqtt_ntp_server, prefs.mqtt_ntp_server, sizeof(frozen->mqtt_ntp_server));
frozen->snmp_enabled = prefs.snmp_enabled;
memcpy(frozen->snmp_community, prefs.snmp_community, sizeof(frozen->snmp_community));
frozen->radio_watchdog_minutes = prefs.radio_watchdog_minutes;
frozen->alert_enabled = prefs.alert_enabled;
memcpy(frozen->alert_psk_hex, prefs.alert_psk_hex, sizeof(frozen->alert_psk_hex));
frozen->alert_wifi_minutes = prefs.alert_wifi_minutes;
frozen->alert_mqtt_minutes = prefs.alert_mqtt_minutes;
frozen->alert_min_interval_min = prefs.alert_min_interval_min;
memcpy(frozen->alert_hashtag, prefs.alert_hashtag, sizeof(frozen->alert_hashtag));
memcpy(frozen->alert_region, prefs.alert_region, sizeof(frozen->alert_region));
frozen->mqtt_neighbors_enabled = prefs.mqtt_neighbors_enabled;
frozen->mqtt_neighbors_interval = prefs.mqtt_neighbors_interval;
memcpy(frozen->mqtt_slot_packet_filter, prefs.mqtt_slot_packet_filter,
sizeof(frozen->mqtt_slot_packet_filter));
}
inline size_t encode(const MQTTPrefs& prefs, uint8_t* output, size_t output_size) {
const size_t payload_len = payloadLenFor(prefs);
const size_t encoded_size = sizeof(MQTTPrefsHeader) + payload_len;
if (output == nullptr || output_size < encoded_size) return 0;
const MQTTPrefsHeader header = makeHeader(payload_len);
LegacyV1MQTTPrefs frozen;
freezeV1(prefs, &frozen);
memcpy(output, &header, sizeof(header));
memcpy(output + sizeof(header), &prefs, payload_len);
memcpy(output + sizeof(header), &frozen, payload_len);
return encoded_size;
}
@@ -346,6 +396,120 @@ inline bool isPlausibleLegacy(Source source, const uint8_t* input, size_t size)
}
}
// A valid v1 header proves the intended layout, not that a torn or corrupted
// payload still contains bounded C strings. Validate every field present at a
// shipped v1 boundary before copying it into the runtime object; otherwise the
// later semantic validators and JSON writer could scan beyond a fixed array.
inline bool isPlausibleV1(const LegacyV1MQTTPrefs& prefs, size_t payload_len) {
if (payload_len != kV1PreObserverPayloadSize &&
payload_len != kV1PreNeighborsPayloadSize &&
payload_len != kV1PreFilterPayloadSize &&
payload_len != kV1BaselinePayloadSize) {
return false;
}
const uint8_t* input = reinterpret_cast<const uint8_t*>(&prefs);
// A v1 header already identifies the layout. At this stage only reject
// fields that could make later C-string validation/serialization read past a
// fixed array; numeric values are safely repaired by MQTTPrefsSerializer.
if (!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, mqtt_origin), sizeof(prefs.mqtt_origin)) ||
!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, mqtt_iata), sizeof(prefs.mqtt_iata)) ||
!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, wifi_ssid), sizeof(prefs.wifi_ssid)) ||
!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, wifi_password), sizeof(prefs.wifi_password)) ||
!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, timezone_string), sizeof(prefs.timezone_string)) ||
!hasPlausibleSharedAuth(input, payload_len,
offsetof(LegacyV1MQTTPrefs, mqtt_owner_public_key),
offsetof(LegacyV1MQTTPrefs, mqtt_email)) ||
!hasPlausibleSlotText(input, payload_len, MQTT_PREFS_SLOT_COUNT,
offsetof(LegacyV1MQTTPrefs, mqtt_slot_preset),
offsetof(LegacyV1MQTTPrefs, mqtt_slot_host),
offsetof(LegacyV1MQTTPrefs, mqtt_slot_username),
offsetof(LegacyV1MQTTPrefs, mqtt_slot_password),
offsetof(LegacyV1MQTTPrefs, mqtt_slot_token),
offsetof(LegacyV1MQTTPrefs, mqtt_slot_topic),
offsetof(LegacyV1MQTTPrefs, mqtt_slot_audience)) ||
!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, mqtt_ntp_server),
sizeof(prefs.mqtt_ntp_server))) {
return false;
}
if (payload_len >= kV1PreNeighborsPayloadSize) {
if (!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, snmp_community),
sizeof(prefs.snmp_community)) ||
!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, alert_psk_hex),
sizeof(prefs.alert_psk_hex)) ||
!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, alert_hashtag),
sizeof(prefs.alert_hashtag)) ||
!hasTerminatedText(input, payload_len,
offsetof(LegacyV1MQTTPrefs, alert_region),
sizeof(prefs.alert_region))) {
return false;
}
}
return true;
}
inline void migrateV1(const LegacyV1MQTTPrefs& old_prefs, size_t payload_len,
MQTTPrefs* prefs) {
if (prefs == nullptr || payload_len < kV1PreObserverPayloadSize) return;
memcpy(prefs->mqtt_origin, old_prefs.mqtt_origin, sizeof(prefs->mqtt_origin));
memcpy(prefs->mqtt_iata, old_prefs.mqtt_iata, sizeof(prefs->mqtt_iata));
prefs->mqtt_status_enabled = old_prefs.mqtt_status_enabled;
prefs->mqtt_packets_enabled = old_prefs.mqtt_packets_enabled;
prefs->mqtt_raw_enabled = old_prefs.mqtt_raw_enabled;
prefs->mqtt_tx_enabled = old_prefs.mqtt_tx_enabled;
prefs->mqtt_status_interval = old_prefs.mqtt_status_interval;
memcpy(prefs->wifi_ssid, old_prefs.wifi_ssid, sizeof(prefs->wifi_ssid));
memcpy(prefs->wifi_password, old_prefs.wifi_password, sizeof(prefs->wifi_password));
prefs->wifi_power_save = old_prefs.wifi_power_save;
memcpy(prefs->timezone_string, old_prefs.timezone_string, sizeof(prefs->timezone_string));
prefs->timezone_offset = old_prefs.timezone_offset;
memcpy(prefs->mqtt_slot_preset, old_prefs.mqtt_slot_preset, sizeof(prefs->mqtt_slot_preset));
memcpy(prefs->mqtt_slot_host, old_prefs.mqtt_slot_host, sizeof(prefs->mqtt_slot_host));
memcpy(prefs->mqtt_slot_port, old_prefs.mqtt_slot_port, sizeof(prefs->mqtt_slot_port));
memcpy(prefs->mqtt_slot_username, old_prefs.mqtt_slot_username, sizeof(prefs->mqtt_slot_username));
memcpy(prefs->mqtt_slot_password, old_prefs.mqtt_slot_password, sizeof(prefs->mqtt_slot_password));
memcpy(prefs->mqtt_owner_public_key, old_prefs.mqtt_owner_public_key,
sizeof(prefs->mqtt_owner_public_key));
memcpy(prefs->mqtt_email, old_prefs.mqtt_email, sizeof(prefs->mqtt_email));
memcpy(prefs->mqtt_slot_token, old_prefs.mqtt_slot_token, sizeof(prefs->mqtt_slot_token));
memcpy(prefs->mqtt_slot_topic, old_prefs.mqtt_slot_topic, sizeof(prefs->mqtt_slot_topic));
memcpy(prefs->mqtt_slot_audience, old_prefs.mqtt_slot_audience,
sizeof(prefs->mqtt_slot_audience));
prefs->mqtt_rx_enabled = old_prefs.mqtt_rx_enabled;
memcpy(prefs->mqtt_ntp_server, old_prefs.mqtt_ntp_server, sizeof(prefs->mqtt_ntp_server));
if (payload_len >= kV1PreNeighborsPayloadSize) {
prefs->snmp_enabled = old_prefs.snmp_enabled;
memcpy(prefs->snmp_community, old_prefs.snmp_community, sizeof(prefs->snmp_community));
prefs->radio_watchdog_minutes = old_prefs.radio_watchdog_minutes;
prefs->alert_enabled = old_prefs.alert_enabled;
memcpy(prefs->alert_psk_hex, old_prefs.alert_psk_hex, sizeof(prefs->alert_psk_hex));
prefs->alert_wifi_minutes = old_prefs.alert_wifi_minutes;
prefs->alert_mqtt_minutes = old_prefs.alert_mqtt_minutes;
prefs->alert_min_interval_min = old_prefs.alert_min_interval_min;
memcpy(prefs->alert_hashtag, old_prefs.alert_hashtag, sizeof(prefs->alert_hashtag));
memcpy(prefs->alert_region, old_prefs.alert_region, sizeof(prefs->alert_region));
// The pre-neighbors payload includes the old zero padding byte at this
// offset; copying it preserves the deployed format's disabled default.
prefs->mqtt_neighbors_enabled = old_prefs.mqtt_neighbors_enabled;
}
if (payload_len >= kV1PreFilterPayloadSize) {
prefs->mqtt_neighbors_interval = old_prefs.mqtt_neighbors_interval;
}
if (payload_len >= kV1BaselinePayloadSize) {
memcpy(prefs->mqtt_slot_packet_filter, old_prefs.mqtt_slot_packet_filter,
sizeof(prefs->mqtt_slot_packet_filter));
}
}
inline void migratePreSlot(const OldMQTTPrefs& old_prefs, MQTTPrefs* prefs) {
memcpy(prefs->mqtt_origin, old_prefs.mqtt_origin, sizeof(prefs->mqtt_origin));
memcpy(prefs->mqtt_iata, old_prefs.mqtt_iata, sizeof(prefs->mqtt_iata));
+30 -10
View File
@@ -2,23 +2,29 @@
#include <stdint.h>
// Pure recovery policy for the three MQTT preference transaction files. The
// Pure recovery policy for the three MQTT preference transaction files. The
// writer first moves the old primary to .bak, then moves the verified .tmp to
// the primary name. On a reset, the loader uses this policy before decoding
// /mqtt_prefs. "Preserve" is deliberately distinct from "Usable": it covers
// an unsupported newer layout, corruption, or an unreadable file and must
// never be replaced by an older image.
// the primary name. On a reset, the loader uses this policy before decoding
// the primary. FutureUsable is syntactically valid but belongs to newer
// firmware. FutureClaimed and Indeterminate cannot be safely classified by
// this firmware, so recovery may use a known-good backup but must retain the
// uncertain image and hold further writes. Preserve is definitively invalid or
// unsupported and may be discarded only where the transaction policy permits.
namespace MQTTPrefsRecovery {
enum class FileState : uint8_t {
Missing,
Usable,
FutureUsable,
FutureClaimed,
Indeterminate,
Preserve,
};
enum class Action : uint8_t {
None,
KeepPrimary,
DiscardTemp,
PromoteTemp,
PromoteBackup,
};
@@ -30,13 +36,23 @@ inline Action select(FileState primary, FileState temp, FileState backup) {
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 == FileState::Usable || temp == FileState::FutureUsable) {
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.
// Preserve is definitively invalid, so it was never a committed image. If
// no backup exists, discard the interrupted temp and leave the primary name
// absent; this is what lets a first JSON migration retry from /mqtt_prefs.
// If a backup exists, it is the prior committed image and wins regardless
// of whether this firmware understands its schema.
if (temp == FileState::Preserve) {
return backup == FileState::Missing ? Action::DiscardTemp : Action::PromoteBackup;
}
// FutureClaimed and Indeterminate may be a completed image this firmware
// cannot classify. A known-good backup can run this boot, but without one
// preserve the only candidate under the authoritative name and hold writes.
if (temp == FileState::FutureClaimed || temp == FileState::Indeterminate) {
return backup == FileState::Usable ? Action::PromoteBackup : Action::PromoteTemp;
}
@@ -46,4 +62,8 @@ inline Action select(FileState primary, FileState temp, FileState backup) {
return Action::None;
}
inline bool uncertain(FileState state) {
return state == FileState::FutureClaimed || state == FileState::Indeterminate;
}
} // namespace MQTTPrefsRecovery
+412
View File
@@ -0,0 +1,412 @@
#pragma once
#include <helpers/ConfigSerializer.h>
#include <helpers/MQTTObserverValidation.h>
#include <helpers/MQTTPresets.h>
#include <helpers/MQTTPrefsStorage.h>
#ifdef WITH_MQTT_BRIDGE
static const int32_t MQTT_PREFS_JSON_FORMAT_VERSION = 1;
// Reads only the mandatory root version while ignoring every other schema
// field. Recovery uses this before the v1 decoder so a syntactically valid
// future file remains opaque even when that schema changes a v1 field's type.
class MQTTPrefsVersionProbe : public ConfigSerializer {
int32_t _version = 0;
bool _seen_version = false;
protected:
void structure() override { defStrict("version", _version, _seen_version); }
public:
bool hasFutureVersion() const {
return _seen_version && _version > MQTT_PREFS_JSON_FORMAT_VERSION;
}
};
// ConfigSerializer adapter for the observer preference POD. Keeping the
// serializer separate avoids making the runtime object layout part of the JSON
// format and adds no permanent per-slot serializer state to CommonCLI.
class MQTTPrefsSerializer : public ConfigSerializer {
class WifiPrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
int32_t _power_save;
bool _seen_ssid = false, _seen_password = false, _seen_power_save = false;
protected:
void structure() override {
defStrict("ssid", _prefs->wifi_ssid, sizeof(_prefs->wifi_ssid), _seen_ssid);
defStrict("password", _prefs->wifi_password, sizeof(_prefs->wifi_password), _seen_password);
defStrict("power_save", _power_save, _seen_power_save);
}
public:
explicit WifiPrefs(MQTTPrefs* prefs) : _prefs(prefs), _power_save(prefs->wifi_power_save) {}
bool apply(bool* repaired) {
if (_power_save < 0 || _power_save > 2) {
_power_save = 1;
*repaired = true;
}
_prefs->wifi_power_save = static_cast<uint8_t>(_power_save);
return true;
}
};
class TimePrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
int32_t _utc_offset, _default_utc_offset;
char _default_ntp_server[sizeof(MQTTPrefs::mqtt_ntp_server)];
bool _seen_timezone = false, _seen_utc_offset = false, _seen_ntp_server = false;
protected:
void structure() override {
defStrict("timezone", _prefs->timezone_string, sizeof(_prefs->timezone_string), _seen_timezone);
defStrict("utc_offset", _utc_offset, _seen_utc_offset);
defStrict("ntp_server", _prefs->mqtt_ntp_server, sizeof(_prefs->mqtt_ntp_server), _seen_ntp_server);
}
public:
TimePrefs(MQTTPrefs* prefs, const MQTTPrefs* defaults)
: _prefs(prefs), _utc_offset(prefs->timezone_offset),
_default_utc_offset(defaults->timezone_offset) {
if (_default_utc_offset < -12 || _default_utc_offset > 14) {
_default_utc_offset = 0;
}
memcpy(_default_ntp_server, defaults->mqtt_ntp_server, sizeof(_default_ntp_server));
if (_default_ntp_server[0] != '\0' &&
!mqttNtpHostnameValid(_default_ntp_server)) {
_default_ntp_server[0] = '\0';
}
}
bool apply(bool* repaired) {
if (_utc_offset < -12 || _utc_offset > 14) {
_utc_offset = _default_utc_offset;
*repaired = true;
}
if (_prefs->mqtt_ntp_server[0] != '\0' &&
!mqttNtpHostnameValid(_prefs->mqtt_ntp_server)) {
memcpy(_prefs->mqtt_ntp_server, _default_ntp_server, sizeof(_default_ntp_server));
*repaired = true;
}
_prefs->timezone_offset = static_cast<int8_t>(_utc_offset);
return true;
}
};
class StatusPrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
int32_t _enabled, _interval_ms;
bool _seen_enabled = false, _seen_interval = false;
protected:
void structure() override {
defStrict("enabled", _enabled, _seen_enabled);
defStrict("interval_ms", _interval_ms, _seen_interval);
}
public:
explicit StatusPrefs(MQTTPrefs* prefs)
: _prefs(prefs), _enabled(prefs->mqtt_status_enabled),
_interval_ms(static_cast<int32_t>(prefs->mqtt_status_interval)) {}
void apply(bool* repaired) {
if (_enabled < 0 || _enabled > 1) { _enabled = 1; *repaired = true; }
if (_interval_ms < 60000 || _interval_ms > 3600000) {
_interval_ms = 300000;
*repaired = true;
}
_prefs->mqtt_status_enabled = static_cast<uint8_t>(_enabled);
_prefs->mqtt_status_interval = static_cast<uint32_t>(_interval_ms);
}
};
class NeighborPrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
int32_t _enabled, _interval_ms;
bool _seen_enabled = false, _seen_interval = false;
protected:
void structure() override {
defStrict("enabled", _enabled, _seen_enabled);
defStrict("interval_ms", _interval_ms, _seen_interval);
}
public:
explicit NeighborPrefs(MQTTPrefs* prefs)
: _prefs(prefs), _enabled(prefs->mqtt_neighbors_enabled),
_interval_ms(static_cast<int32_t>(prefs->mqtt_neighbors_interval)) {}
void apply(bool* repaired) {
if (_enabled < 0 || _enabled > 1) { _enabled = 0; *repaired = true; }
if (_interval_ms < static_cast<int32_t>(MQTT_NEIGHBORS_MIN_INTERVAL_MS) ||
_interval_ms > static_cast<int32_t>(MQTT_NEIGHBORS_MAX_INTERVAL_MS)) {
_interval_ms = static_cast<int32_t>(MQTT_NEIGHBORS_DEFAULT_INTERVAL_MS);
*repaired = true;
}
_prefs->mqtt_neighbors_enabled = static_cast<uint8_t>(_enabled);
_prefs->mqtt_neighbors_interval = static_cast<uint32_t>(_interval_ms);
}
};
class OwnerPrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
bool _seen_public_key = false, _seen_email = false;
protected:
void structure() override {
defStrict("public_key", _prefs->mqtt_owner_public_key,
sizeof(_prefs->mqtt_owner_public_key), _seen_public_key);
defStrict("email", _prefs->mqtt_email, sizeof(_prefs->mqtt_email), _seen_email);
}
public:
explicit OwnerPrefs(MQTTPrefs* prefs) : _prefs(prefs) {}
void apply(bool* repaired) {
if (_prefs->mqtt_owner_public_key[0] != '\0' &&
!mqttOwnerKeyValid(_prefs->mqtt_owner_public_key)) {
_prefs->mqtt_owner_public_key[0] = '\0';
*repaired = true;
}
}
};
class SlotPrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
int _index;
int32_t _port, _filter;
bool _seen_preset = false, _seen_host = false, _seen_port = false;
bool _seen_username = false, _seen_password = false, _seen_token = false;
bool _seen_topic = false, _seen_audience = false, _seen_filter = false;
protected:
void structure() override {
defStrict("preset", _prefs->mqtt_slot_preset[_index],
sizeof(_prefs->mqtt_slot_preset[_index]), _seen_preset);
defStrict("host", _prefs->mqtt_slot_host[_index],
sizeof(_prefs->mqtt_slot_host[_index]), _seen_host);
defStrict("port", _port, _seen_port);
defStrict("username", _prefs->mqtt_slot_username[_index],
sizeof(_prefs->mqtt_slot_username[_index]), _seen_username);
defStrict("password", _prefs->mqtt_slot_password[_index],
sizeof(_prefs->mqtt_slot_password[_index]), _seen_password);
defStrict("token", _prefs->mqtt_slot_token[_index],
sizeof(_prefs->mqtt_slot_token[_index]), _seen_token);
defStrict("topic", _prefs->mqtt_slot_topic[_index],
sizeof(_prefs->mqtt_slot_topic[_index]), _seen_topic);
defStrict("audience", _prefs->mqtt_slot_audience[_index],
sizeof(_prefs->mqtt_slot_audience[_index]), _seen_audience);
defStrict("packet_filter", _filter, _seen_filter);
}
public:
SlotPrefs(MQTTPrefs* prefs, int index)
: _prefs(prefs), _index(index), _port(prefs->mqtt_slot_port[index]),
_filter(prefs->mqtt_slot_packet_filter[index]) {}
void apply(bool* repaired) {
if (_port < 0 || _port > 65535) { _port = 0; *repaired = true; }
if (_filter < 0 || _filter > 65535) { _filter = 0xffff; *repaired = true; }
const char* preset = _prefs->mqtt_slot_preset[_index];
if (strcmp(preset, MQTT_PRESET_NONE) != 0 &&
strcmp(preset, MQTT_PRESET_CUSTOM) != 0 && findMQTTPreset(preset) == nullptr) {
strcpy(_prefs->mqtt_slot_preset[_index], MQTT_PRESET_NONE);
*repaired = true;
}
_prefs->mqtt_slot_port[_index] = static_cast<uint16_t>(_port);
_prefs->mqtt_slot_packet_filter[_index] = static_cast<uint16_t>(_filter);
}
};
class MqttPrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
int32_t _packets_enabled, _raw_enabled, _tx_enabled, _rx_enabled;
char _default_iata[sizeof(MQTTPrefs::mqtt_iata)];
bool _seen_origin = false, _seen_iata = false, _seen_packets = false;
bool _seen_raw = false, _seen_tx = false, _seen_rx = false;
StatusPrefs _status;
NeighborPrefs _neighbors;
OwnerPrefs _owner;
static_assert(MQTT_PREFS_SLOT_COUNT == 6,
"MQTTPrefsSerializer slot members and keys must be updated");
SlotPrefs _slot1, _slot2, _slot3, _slot4, _slot5, _slot6;
protected:
void structure() override {
defStrict("origin", _prefs->mqtt_origin, sizeof(_prefs->mqtt_origin), _seen_origin);
defStrict("iata", _prefs->mqtt_iata, sizeof(_prefs->mqtt_iata), _seen_iata);
defStrict("packets_enabled", _packets_enabled, _seen_packets);
defStrict("raw_enabled", _raw_enabled, _seen_raw);
defStrict("tx_enabled", _tx_enabled, _seen_tx);
defStrict("rx_enabled", _rx_enabled, _seen_rx);
def("status", _status);
def("neighbors", _neighbors);
def("owner", _owner);
def("slot1", _slot1);
def("slot2", _slot2);
def("slot3", _slot3);
def("slot4", _slot4);
def("slot5", _slot5);
def("slot6", _slot6);
}
public:
MqttPrefs(MQTTPrefs* prefs, const MQTTPrefs* defaults)
: _prefs(prefs), _packets_enabled(prefs->mqtt_packets_enabled),
_raw_enabled(prefs->mqtt_raw_enabled), _tx_enabled(prefs->mqtt_tx_enabled),
_rx_enabled(prefs->mqtt_rx_enabled), _status(prefs), _neighbors(prefs),
_owner(prefs), _slot1(prefs, 0), _slot2(prefs, 1),
_slot3(prefs, 2), _slot4(prefs, 3),
_slot5(prefs, 4), _slot6(prefs, 5) {
memcpy(_default_iata, defaults->mqtt_iata, sizeof(_default_iata));
if (_default_iata[0] != '\0' && !mqttIataValid(_default_iata)) {
_default_iata[0] = '\0';
}
for (char* p = _default_iata; *p; ++p) {
if (*p >= 'a' && *p <= 'z') *p = static_cast<char>(*p - ('a' - 'A'));
}
}
void apply(bool* repaired) {
if (_packets_enabled < 0 || _packets_enabled > 1) { _packets_enabled = 1; *repaired = true; }
if (_raw_enabled < 0 || _raw_enabled > 1) { _raw_enabled = 0; *repaired = true; }
if (_tx_enabled < 0 || _tx_enabled > 2) { _tx_enabled = 2; *repaired = true; }
if (_rx_enabled < 0 || _rx_enabled > 1) { _rx_enabled = 1; *repaired = true; }
_prefs->mqtt_packets_enabled = static_cast<uint8_t>(_packets_enabled);
_prefs->mqtt_raw_enabled = static_cast<uint8_t>(_raw_enabled);
_prefs->mqtt_tx_enabled = static_cast<uint8_t>(_tx_enabled);
_prefs->mqtt_rx_enabled = static_cast<uint8_t>(_rx_enabled);
if (_prefs->mqtt_iata[0] != '\0') {
if (!mqttIataValid(_prefs->mqtt_iata)) {
memcpy(_prefs->mqtt_iata, _default_iata, sizeof(_default_iata));
*repaired = true;
} else {
for (char* p = _prefs->mqtt_iata; *p; ++p) {
if (*p >= 'a' && *p <= 'z') {
*p = static_cast<char>(*p - ('a' - 'A'));
*repaired = true;
}
}
}
}
_status.apply(repaired);
_neighbors.apply(repaired);
_owner.apply(repaired);
_slot1.apply(repaired); _slot2.apply(repaired); _slot3.apply(repaired);
_slot4.apply(repaired); _slot5.apply(repaired); _slot6.apply(repaired);
}
};
class SnmpPrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
int32_t _enabled;
bool _seen_enabled = false, _seen_community = false;
protected:
void structure() override {
defStrict("enabled", _enabled, _seen_enabled);
defStrict("community", _prefs->snmp_community,
sizeof(_prefs->snmp_community), _seen_community);
}
public:
explicit SnmpPrefs(MQTTPrefs* prefs) : _prefs(prefs), _enabled(prefs->snmp_enabled) {}
void apply(bool* repaired) {
if (_enabled < 0 || _enabled > 1) { _enabled = 0; *repaired = true; }
_prefs->snmp_enabled = static_cast<uint8_t>(_enabled);
}
};
class RadioPrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
int32_t _watchdog_min;
bool _seen_watchdog = false;
protected:
void structure() override { defStrict("watchdog_min", _watchdog_min, _seen_watchdog); }
public:
explicit RadioPrefs(MQTTPrefs* prefs)
: _prefs(prefs), _watchdog_min(prefs->radio_watchdog_minutes) {}
void apply(bool* repaired) {
if (_watchdog_min < 0 || _watchdog_min > 120) { _watchdog_min = 5; *repaired = true; }
_prefs->radio_watchdog_minutes = static_cast<uint8_t>(_watchdog_min);
}
};
class AlertPrefs : public ConfigSerializer {
MQTTPrefs* _prefs;
int32_t _enabled, _wifi_minutes, _mqtt_minutes, _rate_limit_min;
bool _seen_enabled = false, _seen_psk = false, _seen_wifi = false;
bool _seen_mqtt = false, _seen_rate = false, _seen_hashtag = false, _seen_region = false;
protected:
void structure() override {
defStrict("enabled", _enabled, _seen_enabled);
defStrict("psk_hex", _prefs->alert_psk_hex, sizeof(_prefs->alert_psk_hex), _seen_psk);
defStrict("wifi_minutes", _wifi_minutes, _seen_wifi);
defStrict("mqtt_minutes", _mqtt_minutes, _seen_mqtt);
defStrict("rate_limit_min", _rate_limit_min, _seen_rate);
defStrict("hashtag", _prefs->alert_hashtag, sizeof(_prefs->alert_hashtag), _seen_hashtag);
defStrict("region", _prefs->alert_region, sizeof(_prefs->alert_region), _seen_region);
}
public:
explicit AlertPrefs(MQTTPrefs* prefs)
: _prefs(prefs), _enabled(prefs->alert_enabled),
_wifi_minutes(prefs->alert_wifi_minutes), _mqtt_minutes(prefs->alert_mqtt_minutes),
_rate_limit_min(prefs->alert_min_interval_min) {}
void apply(bool* repaired) {
if (_enabled < 0 || _enabled > 1) { _enabled = 0; *repaired = true; }
if (_wifi_minutes < 0 || _wifi_minutes > 1440) { _wifi_minutes = 30; *repaired = true; }
if (_mqtt_minutes < 0 || _mqtt_minutes > 10080) { _mqtt_minutes = 240; *repaired = true; }
if (_rate_limit_min < 60 || _rate_limit_min > 10080) { _rate_limit_min = 60; *repaired = true; }
if (_prefs->alert_psk_hex[0] != '\0') {
const size_t len = strlen(_prefs->alert_psk_hex);
bool valid_hex = len == 32;
for (size_t i = 0; valid_hex && i < len; ++i) {
const char c = _prefs->alert_psk_hex[i];
valid_hex = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f');
}
if (!valid_hex) {
_prefs->alert_psk_hex[0] = '\0';
_prefs->alert_hashtag[0] = '\0';
*repaired = true;
}
}
_prefs->alert_enabled = static_cast<uint8_t>(_enabled);
_prefs->alert_wifi_minutes = static_cast<uint16_t>(_wifi_minutes);
_prefs->alert_mqtt_minutes = static_cast<uint16_t>(_mqtt_minutes);
_prefs->alert_min_interval_min = static_cast<uint16_t>(_rate_limit_min);
}
};
MQTTPrefs* _prefs;
int32_t _version = MQTT_PREFS_JSON_FORMAT_VERSION;
bool _seen_version = false;
WifiPrefs _wifi;
TimePrefs _time;
MqttPrefs _mqtt;
SnmpPrefs _snmp;
RadioPrefs _radio;
AlertPrefs _alert;
protected:
void structure() override {
defStrict("version", _version, _seen_version);
def("wifi", _wifi);
def("time", _time);
def("mqtt", _mqtt);
def("snmp", _snmp);
def("radio", _radio);
def("alert", _alert);
}
public:
explicit MQTTPrefsSerializer(MQTTPrefs* prefs, const MQTTPrefs* repair_defaults = nullptr)
: _prefs(prefs), _wifi(prefs),
_time(prefs, repair_defaults ? repair_defaults : prefs),
_mqtt(prefs, repair_defaults ? repair_defaults : prefs), _snmp(prefs),
_radio(prefs), _alert(prefs) {}
bool hasSupportedVersion() const {
return _seen_version && _version == MQTT_PREFS_JSON_FORMAT_VERSION;
}
bool hasFutureVersion() const {
return _seen_version && _version > MQTT_PREFS_JSON_FORMAT_VERSION;
}
bool normalize(bool* repaired) {
if (repaired == nullptr) return false;
*repaired = false;
_wifi.apply(repaired);
_time.apply(repaired);
_mqtt.apply(repaired);
_snmp.apply(repaired);
_radio.apply(repaired);
_alert.apply(repaired);
return true;
}
bool apply(bool* repaired) {
return hasSupportedVersion() && normalize(repaired);
}
};
#endif // WITH_MQTT_BRIDGE
+55 -16
View File
@@ -62,10 +62,9 @@ struct PreWifiPowerOldMQTTPrefs {
char mqtt_email[64];
};
// MQTT preferences stored separately from NodePrefs to avoid upstream layout
// conflicts. The full layout is the frozen v1 payload baseline. The prefix
// before observer settings is also an explicitly supported v1 payload: it was
// used before the observer fields were appended.
// Runtime MQTT preferences, stored separately from NodePrefs to avoid upstream
// layout conflicts. JSON persistence is field-based; this type is deliberately
// free to evolve independently of the frozen binary migration layouts below.
struct MQTTPrefs {
char mqtt_origin[32];
char mqtt_iata[8];
@@ -110,17 +109,57 @@ struct MQTTPrefs {
char alert_region[31];
// Neighbors publishing (PSRAM boards only). Appended at the end of the
// observer tail so a shorter (pre-neighbors) /mqtt_prefs payload from earlier
// firmware still loads with these defaulting off/24h; keeps the format at
// VERSION 1. Field order and sizes are kept byte-identical to the flex
// neighbors build so a /mqtt_prefs written by either firmware is
// interchangeable (see the offsetof static_asserts below).
// observer tail in the former binary format. Binary compatibility is now
// represented only by LegacyV1MQTTPrefs below.
uint8_t mqtt_neighbors_enabled;
uint32_t mqtt_neighbors_interval;
// Per-slot payload-type allow masks. Bit N controls MeshCore packet type N
// for both packets and raw MQTT topics. Appended so older v1 payloads load
// with the default all-types masks intact.
// for both packets and raw MQTT topics.
uint16_t mqtt_slot_packet_filter[MQTT_PREFS_SLOT_COUNT];
};
// Frozen payload written by the version-1 binary format. Keep this distinct
// from the runtime MQTTPrefs type: JSON persistence must not make the runtime
// object's padding or member order an on-flash ABI again. Legacy decoding reads
// into this type and field-copies into a defaulted runtime object.
struct LegacyV1MQTTPrefs {
char mqtt_origin[32];
char mqtt_iata[8];
uint8_t mqtt_status_enabled;
uint8_t mqtt_packets_enabled;
uint8_t mqtt_raw_enabled;
uint8_t mqtt_tx_enabled;
uint32_t mqtt_status_interval;
char wifi_ssid[32];
char wifi_password[64];
uint8_t wifi_power_save;
char timezone_string[32];
int8_t timezone_offset;
char mqtt_slot_preset[MQTT_PREFS_SLOT_COUNT][24];
char mqtt_slot_host[MQTT_PREFS_SLOT_COUNT][64];
uint16_t mqtt_slot_port[MQTT_PREFS_SLOT_COUNT];
char mqtt_slot_username[MQTT_PREFS_SLOT_COUNT][32];
char mqtt_slot_password[MQTT_PREFS_SLOT_COUNT][64];
char mqtt_owner_public_key[65];
char mqtt_email[64];
char mqtt_slot_token[MQTT_PREFS_SLOT_COUNT][48];
char mqtt_slot_topic[MQTT_PREFS_SLOT_COUNT][96];
char mqtt_slot_audience[MQTT_PREFS_SLOT_COUNT][64];
uint8_t mqtt_rx_enabled;
char mqtt_ntp_server[64];
uint8_t snmp_enabled;
char snmp_community[24];
uint8_t radio_watchdog_minutes;
uint8_t alert_enabled;
char alert_psk_hex[33];
uint16_t alert_wifi_minutes;
uint16_t alert_mqtt_minutes;
uint16_t alert_min_interval_min;
char alert_hashtag[24];
char alert_region[31];
uint8_t mqtt_neighbors_enabled;
uint32_t mqtt_neighbors_interval;
uint16_t mqtt_slot_packet_filter[MQTT_PREFS_SLOT_COUNT];
};
@@ -272,20 +311,20 @@ static const size_t LEGACY6_AUDIENCE_RX_SIZE = 2840;
// Frozen on-flash layouts; every firmware and native fixture build checks them.
static_assert(sizeof(MQTTPrefsHeader) == 8, "versioned /mqtt_prefs header must stay 8 bytes");
static_assert(offsetof(MQTTPrefs, snmp_enabled) == MQTT_PREFS_V1_PRE_OBSERVER_PAYLOAD_SIZE,
static_assert(offsetof(LegacyV1MQTTPrefs, snmp_enabled) == MQTT_PREFS_V1_PRE_OBSERVER_PAYLOAD_SIZE,
"v1 pre-observer /mqtt_prefs boundary changed");
static_assert(sizeof(MQTTPrefs) == MQTT_PREFS_V1_FULL_PAYLOAD_SIZE,
static_assert(sizeof(LegacyV1MQTTPrefs) == MQTT_PREFS_V1_FULL_PAYLOAD_SIZE,
"v1 /mqtt_prefs payload layout changed");
// Lock the neighbors tail to the flex neighbors build's layout so a /mqtt_prefs
// written by either firmware is byte-for-byte interchangeable. The enable flag
// lands in the old struct's zeroed trailing padding (offset 2857), and the
// interval begins exactly at the pre-neighbors payload size (2860) so a
// pre-neighbors read stops right before it and the interval keeps its default.
static_assert(offsetof(MQTTPrefs, mqtt_neighbors_enabled) == 2857,
static_assert(offsetof(LegacyV1MQTTPrefs, mqtt_neighbors_enabled) == 2857,
"neighbors enable flag must sit at the flex-compatible offset");
static_assert(offsetof(MQTTPrefs, mqtt_neighbors_interval) == MQTT_PREFS_V1_PRE_NEIGHBORS_PAYLOAD_SIZE,
static_assert(offsetof(LegacyV1MQTTPrefs, mqtt_neighbors_interval) == MQTT_PREFS_V1_PRE_NEIGHBORS_PAYLOAD_SIZE,
"neighbors interval offset must equal the pre-neighbors payload size");
static_assert(offsetof(MQTTPrefs, mqtt_slot_packet_filter) == MQTT_PREFS_V1_PRE_FILTER_PAYLOAD_SIZE,
static_assert(offsetof(LegacyV1MQTTPrefs, mqtt_slot_packet_filter) == MQTT_PREFS_V1_PRE_FILTER_PAYLOAD_SIZE,
"packet filters must begin at the pre-filter payload boundary");
static_assert(sizeof(OldMQTTPrefs) == 472, "frozen pre-slot /mqtt_prefs layout changed");
static_assert(sizeof(PreWifiPowerOldMQTTPrefs) == 472, "frozen pre-WiFi-power /mqtt_prefs layout changed");
+1 -1
View File
@@ -509,7 +509,7 @@ private:
LifecycleOps _lifecycle_ops;
MQTTLifecycle::Coordinator _lifecycle;
// Observer config (MQTT/WiFi/timezone/SNMP/alert), persisted to /mqtt_prefs.
// Observer config (MQTT/WiFi/timezone/SNMP/alert), persisted to /mqtt.json.
// _prefs (held by BridgeBase) still provides upstream fields (freq/sf/node_name…).
MQTTPrefs* _obs = nullptr;