Scope preference saves to changed settings

This commit is contained in:
mikecarper
2026-08-06 15:33:40 -07:00
parent 63e7b2ce8e
commit df2dd41512
11 changed files with 1491 additions and 1390 deletions
+3 -2
View File
@@ -728,8 +728,9 @@ public:
return &_prefs;
}
void savePrefs() override {
_cli.savePrefs(_fs);
void savePrefs(
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common) override {
_cli.savePrefs(_fs, scope);
}
void onManualClockSet() override;
+3 -2
View File
@@ -299,8 +299,9 @@ public:
return &_prefs;
}
void savePrefs() override {
_cli.savePrefs(_fs);
void savePrefs(
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common) override {
_cli.savePrefs(_fs, scope);
}
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
+4 -1
View File
@@ -64,7 +64,10 @@ public:
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() { return &_prefs; }
void savePrefs() override { _cli.savePrefs(_fs); }
void savePrefs(
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common) override {
_cli.savePrefs(_fs, scope);
}
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
+19 -8
View File
@@ -936,7 +936,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
if (legacy_upgrade.mayRewriteComPrefs()) {
// loadMQTTPrefs has already committed the full MQTT payload (including
// any recovered observer tail), so compact only /com_prefs now.
savePrefs(fs, false);
savePrefs(fs, PrefsSaveRouting::Scope::Common);
legacy_upgrade.recordComPrefsRewrite();
_com_prefs_needs_upgrade = false;
} else {
@@ -1607,14 +1607,17 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
}
#endif
void CommonCLI::savePrefs(FILESYSTEM* fs, bool save_mqtt) {
void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
const PrefsSaveRouting::Plan plan = PrefsSaveRouting::planFor(scope);
#ifdef WITH_MQTT_BRIDGE
// Observer builds use a verified temp/backup transaction for common prefs.
// Radio and bridge changes must never leave a truncated boot-time image.
saveCommonPrefsImageAtomically(fs);
if (save_mqtt) saveMQTTPrefs(fs);
if (plan.common) saveCommonPrefsImageAtomically(fs);
if (plan.observer) saveMQTTPrefs(fs);
return;
#else
// Observer-only saves are a no-op on roles with no observer preference image.
if (!plan.common) return;
#if defined(NRF52_PLATFORM)
mesh::AtomicFileWriter file(fs, "/com_prefs");
#elif defined(STM32_PLATFORM)
@@ -2321,7 +2324,7 @@ bool CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) {
#define MIN_LOCAL_ADVERT_INTERVAL 60
void CommonCLI::savePrefs() {
void CommonCLI::savePrefs(PrefsSaveRouting::Scope scope) {
uint8_t old_advert_interval = _prefs->advert_interval;
if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) {
_prefs->advert_interval = 0; // turn it off, now that device has been manually configured
@@ -2330,7 +2333,11 @@ void CommonCLI::savePrefs() {
if (old_advert_interval != _prefs->advert_interval) {
_callbacks->updateAdvertTimer();
}
_callbacks->savePrefs();
_callbacks->savePrefs(scope);
}
void CommonCLI::saveObserverPrefs() {
_callbacks->savePrefs(PrefsSaveRouting::Scope::Observer);
}
uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
@@ -3424,8 +3431,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
#ifdef WITH_MQTT_BRIDGE
_mqtt_prefs.mqtt_rx_enabled = _prefs->bridge_pkt_src;
_mqtt_prefs.mqtt_tx_enabled = !_prefs->bridge_pkt_src;
#endif
savePrefs(PrefsSaveRouting::Scope::Both);
#else
savePrefs();
#endif
strcpy(reply, "OK");
}
#endif
@@ -4253,8 +4262,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_mqtt_prefs.mqtt_rx_enabled = 0;
_mqtt_prefs.mqtt_tx_enabled = 1;
}
#endif
savePrefs(PrefsSaveRouting::Scope::Both);
#else
savePrefs();
#endif
strcpy(reply, "OK");
#endif
#ifdef WITH_RS232_BRIDGE
+12 -3
View File
@@ -6,6 +6,7 @@
#include <helpers/ClientACL.h>
#include <helpers/MQTTPresets.h> // For MAX_MQTT_SLOTS (used in NodePrefs struct layout)
#include <helpers/MQTTPrefs.h>
#include <helpers/PrefsSaveRouting.h>
#include <helpers/RegionMap.h>
#ifndef DEFAULT_CAD_ENABLED
@@ -247,7 +248,11 @@ struct LegacyObserverTail {
class CommonCLICallbacks {
public:
virtual void savePrefs() = 0;
// Ordinary CommonCLI setters mutate NodePrefs and therefore save only the
// common image. Observer setters explicitly request Observer; only
// cross-file migration and mixed-owner setters request Both.
virtual void savePrefs(
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common) = 0;
// Called only after a CLI command successfully changes the RTC. Roles that
// derive time from untrusted radio traffic can use this to prefer the manual
// value for the remainder of the boot.
@@ -531,7 +536,9 @@ class CommonCLI {
bool _com_prefs_needs_upgrade = false; // old-format /com_prefs detected; rewrite once after load
mesh::RTCClock* getRTCClock() { return _rtc; }
void savePrefs();
void savePrefs(
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common);
void saveObserverPrefs();
void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
#ifdef WITH_MQTT_BRIDGE
bool recoverCommonPrefsFiles(FILESYSTEM* fs);
@@ -575,7 +582,9 @@ public:
_sensors(&sensors), _region_map(&region_map), _acl(&acl) { }
void loadPrefs(FILESYSTEM* _fs);
void savePrefs(FILESYSTEM* _fs, bool save_mqtt = true);
void savePrefs(
FILESYSTEM* _fs,
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Both);
void loop();
bool hasActiveUserGpioTimer() const;
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
+49 -48
View File
@@ -1,7 +1,8 @@
// CommonCLI_Observer.cpp - fork-owned observer/MQTT/WiFi/timezone/alert/SNMP CLI
// command handling, split out of CommonCLI.cpp so the upstream-tracked file carries
// only two small delegation hooks. These are CommonCLI member functions, so they
// retain full access to _prefs/_callbacks/_board/savePrefs() with no re-plumbing.
// retain full access to _prefs/_callbacks/_board/saveObserverPrefs() with no
// re-plumbing.
//
// Behavior is intentionally identical to the previously-inlined branches. NOTE:
// the entire body of each set/get handler here is compiled under WITH_MQTT_BRIDGE
@@ -171,11 +172,11 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
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();
saveObserverPrefs();
strcpy(reply, "OK - restart to apply");
} else if (memcmp(config, "snmp ", 5) == 0) {
_mqtt_prefs.snmp_enabled = memcmp(&config[5], "on", 2) == 0;
savePrefs();
saveObserverPrefs();
strcpy(reply, "OK - restart to apply");
} else if (memcmp(config, "radio.watchdog ", 15) == 0) {
const char* val = &config[15];
@@ -193,7 +194,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();
saveObserverPrefs();
if (mins == 0) {
strcpy(reply, "OK - radio watchdog disabled");
} else {
@@ -204,13 +205,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();
saveObserverPrefs();
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();
saveObserverPrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.iata ", 10) == 0) {
const char* iata = &config[10];
@@ -219,7 +220,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();
saveObserverPrefs();
_callbacks->restartBridge();
strcpy(reply, "OK - IATA cleared");
} else {
@@ -232,22 +233,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();
saveObserverPrefs();
_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();
saveObserverPrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.packets ", 13) == 0) {
_mqtt_prefs.mqtt_packets_enabled = memcmp(&config[13], "on", 2) == 0;
savePrefs();
saveObserverPrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.raw ", 9) == 0) {
_mqtt_prefs.mqtt_raw_enabled = memcmp(&config[9], "on", 2) == 0;
savePrefs();
saveObserverPrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.tx ", 8) == 0) {
if (memcmp(&config[8], "advert", 6) == 0) {
@@ -255,17 +256,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();
saveObserverPrefs();
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();
saveObserverPrefs();
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();
saveObserverPrefs();
_callbacks->restartBridge();
sprintf(reply, "OK - interval set to %u minutes (%lu ms), bridge restarted", minutes, (unsigned long)_mqtt_prefs.mqtt_status_interval);
} else {
@@ -278,7 +279,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();
saveObserverPrefs();
sprintf(reply, "OK - neighbors interval set to %u hours (%lu ms)", (unsigned)hours,
(unsigned long)_mqtt_prefs.mqtt_neighbors_interval);
} else {
@@ -288,7 +289,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();
saveObserverPrefs();
strcpy(reply, "OK");
#elif defined(WITH_MQTT_BRIDGE)
} else if (memcmp(config, "mqtt.neighbors.interval ", 24) == 0 ||
@@ -307,7 +308,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();
saveObserverPrefs();
#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
@@ -331,14 +332,14 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
const char* value = config[9] == 0 ? config + 9 : config + 10;
if (valueTooLong(value, sizeof(_mqtt_prefs.wifi_ssid), reply, "wifi.ssid")) return true;
StrHelper::strncpy(_mqtt_prefs.wifi_ssid, value, sizeof(_mqtt_prefs.wifi_ssid));
savePrefs();
saveObserverPrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "wifi.pwd ", 9) == 0
|| strcmp(config, "wifi.pwd") == 0) {
const char* value = config[8] == 0 ? config + 8 : config + 9;
if (valueTooLong(value, sizeof(_mqtt_prefs.wifi_password), reply, "wifi.pwd")) return true;
StrHelper::strncpy(_mqtt_prefs.wifi_password, value, sizeof(_mqtt_prefs.wifi_password));
savePrefs();
saveObserverPrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "wifi.powersave ", 15) == 0) {
const char* value = &config[15];
@@ -358,7 +359,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();
saveObserverPrefs();
#ifdef ESP_PLATFORM
if (WiFi.status() == WL_CONNECTED) {
wifi_ps_type_t ps_mode = (ps_value == 1) ? WIFI_PS_NONE :
@@ -382,13 +383,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();
saveObserverPrefs();
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();
saveObserverPrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: timezone offset must be between -12 and +14");
@@ -418,7 +419,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
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();
saveObserverPrefs();
_callbacks->restartBridgeSlot(slot);
// Check if the slot has everything it needs to connect
const MQTTPresetDef* p = findMQTTPreset(preset_name);
@@ -477,7 +478,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();
saveObserverPrefs();
// 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).
@@ -487,7 +488,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();
saveObserverPrefs();
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else {
@@ -496,19 +497,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();
saveObserverPrefs();
_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();
saveObserverPrefs();
_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();
saveObserverPrefs();
_callbacks->restartBridgeSlot(slot);
sprintf(reply, "OK - slot %d token set", slot + 1);
} else if (memcmp(subcmd, "topic ", 6) == 0) {
@@ -518,14 +519,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();
saveObserverPrefs();
_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();
saveObserverPrefs();
_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]);
@@ -535,7 +536,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();
saveObserverPrefs();
_callbacks->restartBridgeSlot(slot);
sprintf(reply, "OK - slot %d JWT audience cleared (using username/password auth)", slot + 1);
} else {
@@ -548,7 +549,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else {
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
}
savePrefs();
saveObserverPrefs();
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.analyzer.eu ", 17) == 0) {
@@ -558,12 +559,12 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else {
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
}
savePrefs();
saveObserverPrefs();
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (strcmp(config, "mqtt.owner") == 0 || strcmp(config, "mqtt.owner ") == 0) {
_mqtt_prefs.mqtt_owner_public_key[0] = '\0';
savePrefs();
saveObserverPrefs();
strcpy(reply, "OK - owner cleared");
} else if (memcmp(config, "mqtt.owner ", 11) == 0) {
const char* owner_key = &config[11];
@@ -571,11 +572,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();
saveObserverPrefs();
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();
saveObserverPrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: public key must be 64 hex characters (32 bytes)");
@@ -583,7 +584,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();
saveObserverPrefs();
strcpy(reply, "OK");
#endif
} else if (memcmp(config, "alert ", 6) == 0) {
@@ -591,12 +592,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();
saveObserverPrefs();
_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();
saveObserverPrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alerts off");
} else {
@@ -611,7 +612,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();
saveObserverPrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.psk cleared (alerts disabled until configured)");
} else if (val[0] == '#') {
@@ -645,7 +646,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();
saveObserverPrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.psk updated");
}
@@ -658,7 +659,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();
saveObserverPrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.hashtag cleared (alerts disabled until configured)");
} else {
@@ -694,7 +695,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();
saveObserverPrefs();
_callbacks->onAlertConfigChanged();
sprintf(reply, "OK - alert.hashtag: %s", _mqtt_prefs.alert_hashtag);
}
@@ -712,7 +713,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();
saveObserverPrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.region cleared (using default scope)");
} else if (len >= sizeof(_mqtt_prefs.alert_region)) {
@@ -720,7 +721,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();
saveObserverPrefs();
_callbacks->onAlertConfigChanged();
sprintf(reply, "OK - alert.region: %s", _mqtt_prefs.alert_region);
}
@@ -730,7 +731,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();
saveObserverPrefs();
sprintf(reply, "OK - alert.wifi %d min%s", mins, mins == 0 ? " (disabled)" : "");
}
} else if (memcmp(config, "alert.mqtt ", 11) == 0) {
@@ -739,7 +740,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();
saveObserverPrefs();
sprintf(reply, "OK - alert.mqtt %d min%s", mins, mins == 0 ? " (disabled)" : "");
}
} else if (memcmp(config, "alert.interval ", 15) == 0) {
@@ -750,7 +751,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();
saveObserverPrefs();
sprintf(reply, "OK - alert.interval %d min", mins);
}
} else {
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <stdint.h>
// Keep runtime preference writes scoped to the file that owns the changed
// value. Observer builds have two independently durable images:
//
// Common -> /com_prefs (radio, identity-facing settings, admin password)
// Observer -> /mqtt_prefs (WiFi, MQTT, timezone, SNMP, alerts)
//
// Cross-file migration and the mixed-owner bridge.source setter need Both.
// Rewriting both images for every other CLI setter adds a second verified
// write/rename transaction and can exceed config.meshcore.io's five-second
// serial command deadline.
namespace PrefsSaveRouting {
enum class Scope : uint8_t {
Common = 0,
Observer,
Both,
};
struct Plan {
bool common;
bool observer;
};
constexpr Plan planFor(Scope scope) {
return scope == Scope::Common ? Plan{true, false}
: scope == Scope::Observer ? Plan{false, true}
: Plan{true, true};
}
} // namespace PrefsSaveRouting
File diff suppressed because it is too large Load Diff
+1
View File
@@ -36,6 +36,7 @@ does not reflect the GoogleTest count -- run the built binary directly
| `test_mqtt_runtime_buffer_lifecycle` | `src/helpers/MQTTRuntimeBufferLifecycle.h` | idempotent allocation/release; partial-allocation degradation; retry of only missing buffers |
| `test_mqtt_prefs_codec` | `src/helpers/MQTTPrefsStorage.h`, `src/helpers/MQTTPrefsCodec.h` | binary pre-slot/3-slot/6-slot migration fixtures; v1 header integrity; downgrade preservation |
| `test_mqtt_prefs_atomic_store` | `src/helpers/MQTTPrefsAtomicStore.h` | transactional MQTT writes and legacy `/node_prefs` handoff; exact short-write detection; begin/finish/rename failure cleanup; original-file preservation |
| `test_prefs_save_routing` | `src/helpers/PrefsSaveRouting.h` | runtime common/observer setters write only their owning preference image; mixed-owner setters and migrations can deliberately write both |
| `test_mqtt_payload_builder` | `src/helpers/MQTTPayloadBuilder.cpp` | status/packet/raw JSON contracts; optional fields; escaping; RX metrics and path; score handling; exact buffer bounds; maximum representative payloads |
| `test_telemetry_history` | `src/helpers/TelemetryHistory.h` | 30-minute rings; seven-day temperature/voltage and dynamically sized GPS retention; exact 1 C temperature/status encoding; separate Base64 series payloads; 14-bit GPS differentials; resize preservation, heap budgets, and 1-based paging bounds |
| `test_flood_filter_policy` | `src/helpers/FloodFilterPolicy.h` | unordered 3-byte and 2-byte-prefix path matching; match thresholds; repeated path-entry semantics; blacklist and bridge-bucket channel-scope selection; `require=region` and per-channel scope-gate truth tables; fast/slow scope timing; adding, replacing, and preserving packet scope |
@@ -0,0 +1,31 @@
#include <gtest/gtest.h>
#include "helpers/PrefsSaveRouting.h"
namespace Routing = PrefsSaveRouting;
TEST(PrefsSaveRouting, CommonSetterWritesOnlyCommonImage) {
constexpr Routing::Plan plan = Routing::planFor(Routing::Scope::Common);
EXPECT_TRUE(plan.common);
EXPECT_FALSE(plan.observer);
}
TEST(PrefsSaveRouting, ObserverSetterWritesOnlyObserverImage) {
constexpr Routing::Plan plan = Routing::planFor(Routing::Scope::Observer);
EXPECT_FALSE(plan.common);
EXPECT_TRUE(plan.observer);
}
TEST(PrefsSaveRouting, CrossImageOperationCanWriteBothImages) {
constexpr Routing::Plan plan = Routing::planFor(Routing::Scope::Both);
EXPECT_TRUE(plan.common);
EXPECT_TRUE(plan.observer);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+1
View File
@@ -157,6 +157,7 @@ canvas{width:100%;height:56px;display:block}
<div class="card">
<h2>Step 1 &middot; WiFi</h2>
<p style="font-size:13.5px;color:var(--mut);margin-bottom:12px" id="wz-wifi-copy">Connect this node to your WiFi network so it can reach the MQTT servers.</p>
<div class="note"><b>Preparing for remote deployment?</b> This wizard tests the WiFi before reboot. If the deployment network is not available here, use USB serial at <b>config.meshcore.io</b> and run <b>set wifi.ssid ...</b> and <b>set wifi.pwd ...</b>. The node saves them without connecting and retries after deployment.</div>
<div class="f"><label>Network name (SSID)</label>
<div class="row" style="gap:8px">
<input type="text" data-k="wifi.ssid" id="wz-ssid" autocapitalize="off" autocorrect="off" style="flex:1">