refactor(mqtt): migrate observer settings to MQTTPrefs structure

Refactored the handling of observer-related settings by moving them from
NodePrefs to a new MQTTPrefs structure. This change centralizes MQTT,
WiFi, timezone, SNMP, and alert configurations, improving code organization
and maintainability. The new structure allows for better separation of
concerns and prepares the codebase for future enhancements.
This commit is contained in:
agessaman
2026-06-26 08:59:48 -07:00
parent 9535f243af
commit 2eb41baeda
14 changed files with 372 additions and 561 deletions
+14 -31
View File
@@ -951,21 +951,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
#ifdef WITH_MQTT_BRIDGE
_prefs.agc_reset_interval = 7; // 28 seconds (secs/4) — prevents AGC drift on long-running observers
#endif
_prefs.radio_watchdog_minutes = 5; // 5 minutes default
// Alert channel defaults — disabled by default, and the channel is left
// unconfigured so a freshly-flashed observer never broadcasts on the
// well-known Public hashtag. Operators must explicitly pick a private
// key (`set alert.psk`) or a hashtag (`set alert.hashtag`) before alerts
// can fire. The sender prefix on outgoing alert messages is always the
// node name (`set name ...`), so there's no separate `alert.name`.
_prefs.alert_enabled = 0;
_prefs.alert_psk_hex[0] = '\0';
_prefs.alert_hashtag[0] = '\0';
_prefs.alert_region[0] = '\0'; // empty = use default_scope
_prefs.alert_wifi_minutes = 30; // 30 minutes
_prefs.alert_mqtt_minutes = 240; // 4 hours
_prefs.alert_min_interval_min = 60; // re-arm window: 1 hour
// Observer defaults (radio_watchdog, alert.*, snmp.*) moved to applyMQTTDefaults()
// in MQTTDefaults.h — they live in /mqtt_prefs now, not NodePrefs.
// bridge defaults
_prefs.bridge_enabled = 1; // enabled
@@ -976,21 +963,12 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
StrHelper::strncpy(_prefs.bridge_secret, "LVSITANOS", sizeof(_prefs.bridge_secret));
// SNMP defaults
_prefs.snmp_enabled = 0;
StrHelper::strncpy(_prefs.snmp_community, "public", sizeof(_prefs.snmp_community));
// GPS defaults
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
// MQTT slot/IATA/timezone defaults come from /mqtt_prefs via loadPrefs (see MQTTDefaults.h)
_prefs.mqtt_origin[0] = '\0';
// WiFi defaults (user-configured via CLI; placeholders until set)
StrHelper::strncpy(_prefs.wifi_ssid, "ssid_here", sizeof(_prefs.wifi_ssid));
StrHelper::strncpy(_prefs.wifi_password, "password_here", sizeof(_prefs.wifi_password));
// MQTT/WiFi/timezone/radio_watchdog defaults live in /mqtt_prefs now (see applyMQTTDefaults).
_prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier
@@ -1042,7 +1020,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
if (_prefs.bridge_enabled) {
#ifdef WITH_MQTT_BRIDGE
// Defer construction to avoid static init crashes on ESP32 classic
bridge = new MQTTBridge(&_prefs, _mgr, getRTCClock(), &self_id);
bridge = new MQTTBridge(&_prefs, _cli.getObserverPrefs(), _mgr, getRTCClock(), &self_id);
#endif
if (bridge) {
// Set device public key for MQTT topics
@@ -1065,7 +1043,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
// Set stats sources for automatic stats collection
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#ifdef WITH_SNMP
if (_prefs.snmp_enabled) {
if (_cli.getObserverPrefs()->snmp_enabled) {
_snmp_agent.setNodeName(_prefs.node_name);
_snmp_agent.setFirmwareVersion(getFirmwareVer());
bridge->setSNMPAgent(&_snmp_agent);
@@ -1082,8 +1060,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
// Passing `this` as the callbacks lets the reporter resolve a TransportKey
// scope (alert.region override, falling back to default_scope) so alert
// floods ride the same scope as adverts/channel messages.
_alerter.begin(&_prefs, this, this);
#if defined(WITH_MQTT_BRIDGE)
#ifdef WITH_MQTT_BRIDGE
_alerter.begin(&_prefs, _cli.getObserverPrefs(), this, this);
_alerter.setBridge(bridge);
#endif
@@ -1118,12 +1096,15 @@ bool MyMesh::resolveAlertScope(TransportKey& dest) {
// RegionMap so the operator can name a region that doesn't exist yet
// without polluting region_map state — we just silently fall through
// to default_scope on miss.
if (_prefs.alert_region[0]) {
auto r = region_map.findByNamePrefix(_prefs.alert_region);
#ifdef WITH_MQTT_BRIDGE
const char* alert_region = _cli.getObserverPrefs()->alert_region;
if (alert_region[0]) {
auto r = region_map.findByNamePrefix(alert_region);
if (r && region_map.getTransportKeysFor(*r, &dest, 1) > 0 && !dest.isNull()) {
return true;
}
}
#endif
if (!default_scope.isNull()) {
dest = default_scope;
return true;
@@ -1473,7 +1454,9 @@ void MyMesh::loop() {
uptime_millis += now - last_millis;
last_millis = now;
#ifdef WITH_MQTT_BRIDGE
_alerter.onLoop(now);
#endif
#ifdef WITH_SNMP
// Push radio stats to SNMP agent every 2 seconds
+13 -3
View File
@@ -132,7 +132,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
#ifdef WITH_SNMP
MeshSNMPAgent _snmp_agent;
#endif
#ifdef WITH_MQTT_BRIDGE
AlertReporter _alerter;
#endif
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
@@ -168,9 +170,11 @@ protected:
int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
#ifdef WITH_MQTT_BRIDGE
uint32_t getRadioWatchdogMillis() const override {
return ((uint32_t)_prefs.radio_watchdog_minutes) * 60000UL;
return ((uint32_t)_cli.getObserverPrefs()->radio_watchdog_minutes) * 60000UL;
}
#endif
uint8_t getExtraAckTransmitCount() const override {
return _prefs.multi_acks;
}
@@ -215,8 +219,10 @@ public:
// CommonCLICallbacks
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
#ifdef WITH_MQTT_BRIDGE
void onAlertConfigChanged() override { _alerter.onConfigChanged(); }
bool sendAlertText(const char* text) override { return _alerter.sendText(text); }
#endif
bool resolveAlertScope(TransportKey& dest) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
@@ -253,7 +259,7 @@ public:
void setBridgeState(bool enable) override {
if (!bridge) {
#ifdef WITH_MQTT_BRIDGE
bridge = new MQTTBridge(&_prefs, _mgr, getRTCClock(), &self_id);
bridge = new MQTTBridge(&_prefs, _cli.getObserverPrefs(), _mgr, getRTCClock(), &self_id);
#endif
if (!bridge) return;
}
@@ -303,8 +309,12 @@ public:
}
void restartBridgeSlot(int slot) override {
#ifdef WITH_MQTT_BRIDGE
if (!bridge || !bridge->isRunning()) return;
bridge->setSlotPreset(slot, _prefs.mqtt_slot_preset[slot]);
bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
#else
(void)slot;
#endif
}
// Schedule the pull-OTA flash to run from loop() in ~2.5 s, leaving time for the
+13 -20
View File
@@ -681,15 +681,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
// Alert channel defaults (same as repeater; off by default and unconfigured).
// Operator must pick `set alert.psk` or `set alert.hashtag` before alerts fire.
_prefs.alert_enabled = 0;
_prefs.alert_psk_hex[0] = '\0';
_prefs.alert_hashtag[0] = '\0';
_prefs.alert_region[0] = '\0';
_prefs.alert_wifi_minutes = 30;
_prefs.alert_mqtt_minutes = 240;
_prefs.alert_min_interval_min = 60;
// Observer defaults (alert.*, etc.) moved to applyMQTTDefaults() — they live
// in /mqtt_prefs now, not NodePrefs.
// bridge defaults (same as repeater)
_prefs.bridge_enabled = 1; // enabled
@@ -698,12 +691,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.bridge_baud = 115200; // baud rate
_prefs.bridge_channel = 1; // channel 1
// MQTT slot/IATA/timezone defaults come from /mqtt_prefs via loadPrefs (see MQTTDefaults.h)
_prefs.mqtt_origin[0] = '\0';
// WiFi defaults (user-configured via CLI; placeholders until set)
StrHelper::strncpy(_prefs.wifi_ssid, "ssid_here", sizeof(_prefs.wifi_ssid));
StrHelper::strncpy(_prefs.wifi_password, "password_here", sizeof(_prefs.wifi_password));
// MQTT/WiFi/timezone defaults live in /mqtt_prefs now (see applyMQTTDefaults).
next_post_idx = 0;
next_client_idx = 0;
@@ -757,7 +745,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
#ifdef WITH_MQTT_BRIDGE
if (_prefs.bridge_enabled) {
// Defer construction to avoid static init crashes on ESP32 classic
bridge = new MQTTBridge(&_prefs, _mgr, getRTCClock(), &self_id);
bridge = new MQTTBridge(&_prefs, _cli.getObserverPrefs(), _mgr, getRTCClock(), &self_id);
if (bridge) {
// Set device public key for MQTT topics
char device_id[65];
@@ -787,8 +775,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
// Passing `this` as the callbacks lets the reporter resolve a TransportKey
// scope (alert.region override, falling back to default_scope) so alert
// floods ride the same scope as adverts/channel messages.
_alerter.begin(&_prefs, this, this);
#if defined(WITH_MQTT_BRIDGE)
#ifdef WITH_MQTT_BRIDGE
_alerter.begin(&_prefs, _cli.getObserverPrefs(), this, this);
_alerter.setBridge(bridge);
#endif
}
@@ -806,12 +794,15 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3
bool MyMesh::resolveAlertScope(TransportKey& dest) {
// Same resolution policy as simple_repeater: alert.region > default_scope.
if (_prefs.alert_region[0]) {
auto r = region_map.findByNamePrefix(_prefs.alert_region);
#ifdef WITH_MQTT_BRIDGE
const char* alert_region = _cli.getObserverPrefs()->alert_region;
if (alert_region[0]) {
auto r = region_map.findByNamePrefix(alert_region);
if (r && region_map.getTransportKeysFor(*r, &dest, 1) > 0 && !dest.isNull()) {
return true;
}
}
#endif
if (!default_scope.isNull()) {
dest = default_scope;
return true;
@@ -1131,5 +1122,7 @@ void MyMesh::loop() {
uptime_millis += now - last_millis;
last_millis = now;
#ifdef WITH_MQTT_BRIDGE
_alerter.onLoop(now);
#endif
}
+10 -2
View File
@@ -126,7 +126,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
#ifdef WITH_MQTT_BRIDGE
MQTTBridge* bridge;
#endif
#ifdef WITH_MQTT_BRIDGE
AlertReporter _alerter;
#endif
void addPost(ClientInfo* client, const char* postData);
void pushPostToClient(ClientInfo* client, PostInfo& post);
@@ -201,8 +203,10 @@ public:
// CommonCLICallbacks
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
#ifdef WITH_MQTT_BRIDGE
void onAlertConfigChanged() override { _alerter.onConfigChanged(); }
bool sendAlertText(const char* text) override { return _alerter.sendText(text); }
#endif
bool resolveAlertScope(TransportKey& dest) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
@@ -241,7 +245,7 @@ public:
void setBridgeState(bool enable) override {
if (!bridge) {
#ifdef WITH_MQTT_BRIDGE
bridge = new MQTTBridge(&_prefs, _mgr, getRTCClock(), &self_id);
bridge = new MQTTBridge(&_prefs, _cli.getObserverPrefs(), _mgr, getRTCClock(), &self_id);
#endif
if (!bridge) return;
}
@@ -289,8 +293,12 @@ public:
}
void restartBridgeSlot(int slot) override {
#ifdef WITH_MQTT_BRIDGE
if (!bridge || !bridge->isRunning()) return;
bridge->setSlotPreset(slot, _prefs.mqtt_slot_preset[slot]);
bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
#else
(void)slot;
#endif
}
int getQueueSize() override {
+6 -4
View File
@@ -39,9 +39,11 @@ uint32_t Dispatcher::getCADFailMaxDuration() const {
return 4000; // 4 seconds
}
#ifdef WITH_MQTT_BRIDGE
uint32_t Dispatcher::getRadioWatchdogMillis() const {
return RADIO_WATCHDOG_MS;
}
#endif
void Dispatcher::loop() {
if (millisHasNowPassed(next_floor_calib_time)) {
@@ -63,10 +65,9 @@ void Dispatcher::loop() {
}
// Radio watchdog: detect radio stuck in RX mode but not receiving any packets.
// Use a composite "last radio activity" timestamp: the most recent of any valid RX,
// any ISR event (even CRC errors), or any successful TX. This prevents false firings
// in quiet mesh environments where packets may be more than RADIO_WATCHDOG_MS apart,
// while still catching a truly stuck radio (PSRAM starvation → missed ISR → no activity).
// Observer-only feature (gated behind WITH_MQTT_BRIDGE); configured via the
// MQTTPrefs radio_watchdog_minutes setting.
#ifdef WITH_MQTT_BRIDGE
{
const uint32_t watchdog_ms = getRadioWatchdogMillis();
if (watchdog_ms > 0) {
@@ -87,6 +88,7 @@ void Dispatcher::loop() {
}
}
}
#endif // WITH_MQTT_BRIDGE (radio watchdog)
if (outbound) { // waiting for outbound send to be completed
if (_radio->isSendComplete()) {
+3 -1
View File
@@ -180,7 +180,9 @@ protected:
virtual uint32_t getCADFailMaxDuration() const;
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
virtual int getAGCResetInterval() const { return 0; } // disabled by default
virtual uint32_t getRadioWatchdogMillis() const;
#ifdef WITH_MQTT_BRIDGE
virtual uint32_t getRadioWatchdogMillis() const; // observer-only radio recovery
#endif
public:
void begin();
+14 -11
View File
@@ -26,8 +26,9 @@
#define ALERT_DEBUG_PRINTLN(...) do {} while (0)
#endif
#ifdef WITH_MQTT_BRIDGE
AlertReporter::AlertReporter()
: _prefs(nullptr), _mesh(nullptr), _callbacks(nullptr),
: _prefs(nullptr), _obs(nullptr), _mesh(nullptr), _callbacks(nullptr),
#ifdef WITH_MQTT_BRIDGE
_bridge(nullptr),
#endif
@@ -38,18 +39,18 @@ AlertReporter::AlertReporter()
#endif
}
void AlertReporter::begin(NodePrefs* prefs, mesh::Mesh* mesh, CommonCLICallbacks* callbacks) {
void AlertReporter::begin(NodePrefs* prefs, MQTTPrefs* obs, mesh::Mesh* mesh, CommonCLICallbacks* callbacks) {
_prefs = prefs;
_obs = obs;
_mesh = mesh;
_callbacks = callbacks;
onConfigChanged();
}
#ifdef WITH_MQTT_BRIDGE
void AlertReporter::setBridge(MQTTBridge* bridge) {
_bridge = bridge;
}
#endif
#endif // WITH_MQTT_BRIDGE (AlertReporter methods, part 1)
// Channels banned as fault-alert destinations. Fault alerts are noisy
// operator-infrastructure messages; routing them to community channels would
@@ -97,6 +98,7 @@ const char* alertReporterBannedChannelMatchHex(const char* psk_hex) {
return alertReporterBannedChannelMatch(secret);
}
#ifdef WITH_MQTT_BRIDGE
bool AlertReporter::resolveChannel(mesh::GroupChannel& out) const {
if (!_prefs) return false;
@@ -105,7 +107,7 @@ bool AlertReporter::resolveChannel(mesh::GroupChannel& out) const {
// Only 16-byte secrets (32 hex chars) are supported; 32-byte channel keys
// are not used anywhere in MeshCore practice and not represented in the
// banned table either.
const char* psk = _prefs->alert_psk_hex;
const char* psk = _obs->alert_psk_hex;
if (strlen(psk) != 32) return false;
memset(out.secret, 0, sizeof(out.secret));
@@ -204,7 +206,7 @@ void AlertReporter::formatAge(unsigned long age_ms, char* out, size_t out_size)
}
void AlertReporter::onLoop(unsigned long now_ms) {
if (!_prefs || !_prefs->alert_enabled) return;
if (!_prefs || !_obs->alert_enabled) return;
if (!_mesh) return;
// Throttle: ~5 s cadence. The thresholds are minutes-scale so this is fine.
@@ -216,17 +218,17 @@ void AlertReporter::onLoop(unsigned long now_ms) {
// already enforces this on set, but a stale prefs file or future field
// tweak shouldn't be able to drag the floor below 1 hour and let a
// flapping link spam the mesh.
uint16_t cfg_min = _prefs->alert_min_interval_min;
uint16_t cfg_min = _obs->alert_min_interval_min;
if (cfg_min < 60) cfg_min = 60;
unsigned long min_interval_ms = (unsigned long)cfg_min * 60000UL;
// -------- WiFi fault --------
if (_prefs->alert_wifi_minutes > 0) {
if (_obs->alert_wifi_minutes > 0) {
unsigned long wifi_disc_ms = MQTTBridge::getLastWifiDisconnectTime();
unsigned long wifi_conn_ms = MQTTBridge::getWifiConnectedAtMillis();
bool wifi_down = (wifi_disc_ms != 0 && wifi_conn_ms == 0);
unsigned long down_ms = wifi_down ? (now_ms - wifi_disc_ms) : 0;
unsigned long thresh_ms = (unsigned long)_prefs->alert_wifi_minutes * 60000UL;
unsigned long thresh_ms = (unsigned long)_obs->alert_wifi_minutes * 60000UL;
if (_wifi.state == OK) {
if (wifi_down && down_ms >= thresh_ms &&
@@ -263,10 +265,10 @@ void AlertReporter::onLoop(unsigned long now_ms) {
}
// -------- MQTT slot faults --------
if (_prefs->alert_mqtt_minutes > 0 && _bridge != nullptr) {
if (_obs->alert_mqtt_minutes > 0 && _bridge != nullptr) {
int n = MQTTBridge::getRuntimeSlotCount();
if (n > (int)(sizeof(_mqtt) / sizeof(_mqtt[0]))) n = (int)(sizeof(_mqtt) / sizeof(_mqtt[0]));
unsigned long thresh_ms = (unsigned long)_prefs->alert_mqtt_minutes * 60000UL;
unsigned long thresh_ms = (unsigned long)_obs->alert_mqtt_minutes * 60000UL;
for (int i = 0; i < n; i++) {
Fault& f = _mqtt[i];
@@ -311,3 +313,4 @@ void AlertReporter::onLoop(unsigned long now_ms) {
(void)now_ms;
#endif
}
#endif // WITH_MQTT_BRIDGE (AlertReporter methods, part 2)
+4 -1
View File
@@ -49,6 +49,7 @@ const char* alertReporterBannedChannelMatchHex(const char* psk_hex);
* - WiFi/MQTT polling is #ifdef WITH_MQTT_BRIDGE-gated; without it, the
* reporter still supports manual `alert test` sends.
*/
#ifdef WITH_MQTT_BRIDGE
class AlertReporter {
public:
AlertReporter();
@@ -59,7 +60,7 @@ public:
* it to resolve a TransportKey scope for outgoing alert floods (so the
* packet rides the repeater's default scope or an `alert.region` override).
*/
void begin(NodePrefs* prefs, mesh::Mesh* mesh, CommonCLICallbacks* callbacks = nullptr);
void begin(NodePrefs* prefs, MQTTPrefs* obs, mesh::Mesh* mesh, CommonCLICallbacks* callbacks = nullptr);
#ifdef WITH_MQTT_BRIDGE
/** Bridge can be (re)created lazily; pass nullptr to detach. */
@@ -98,6 +99,7 @@ private:
};
NodePrefs* _prefs;
MQTTPrefs* _obs;
mesh::Mesh* _mesh;
CommonCLICallbacks* _callbacks;
#ifdef WITH_MQTT_BRIDGE
@@ -107,3 +109,4 @@ private:
#endif
unsigned long _next_check_ms;
};
#endif // WITH_MQTT_BRIDGE
+26 -231
View File
@@ -18,20 +18,6 @@
#ifdef WITH_MQTT_BRIDGE
#include "bridges/MQTTBridge.h"
#include "MQTTDefaults.h"
// Helper function to calculate total size of MQTT fields for file format compatibility
// Uses NodePrefs struct to get accurate field sizes
static size_t getMQTTFieldsSize(const NodePrefs* prefs) {
return sizeof(prefs->mqtt_origin) + sizeof(prefs->mqtt_iata) +
sizeof(prefs->mqtt_status_enabled) + sizeof(prefs->mqtt_packets_enabled) +
sizeof(prefs->mqtt_raw_enabled) + sizeof(prefs->mqtt_tx_enabled) +
sizeof(prefs->mqtt_status_interval) + sizeof(prefs->wifi_ssid) +
sizeof(prefs->wifi_password) + sizeof(prefs->timezone_string) +
sizeof(prefs->timezone_offset) + sizeof(prefs->mqtt_slot_preset) +
sizeof(prefs->mqtt_slot_host) + sizeof(prefs->mqtt_slot_port) +
sizeof(prefs->mqtt_slot_username) + sizeof(prefs->mqtt_slot_password) +
sizeof(prefs->mqtt_owner_public_key) + sizeof(prefs->mqtt_email);
}
#endif
// Believe it or not, this std C function is busted on some platforms!
@@ -71,11 +57,11 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
_prefs->bridge_pkt_src = 1; // Default to RX (logRx) for new installs
}
#ifdef WITH_MQTT_BRIDGE
// Load MQTT preferences from separate file
// Load observer preferences (MQTT/WiFi/timezone/SNMP/alert) from /mqtt_prefs.
// Readers (MQTTBridge, AlertReporter, observer CLI) use _mqtt_prefs directly —
// these fields no longer exist in NodePrefs, so there is nothing to sync.
loadMQTTPrefs(fs);
// Sync MQTT prefs to NodePrefs so existing code (like MQTTBridge) can access them
syncMQTTPrefsToNodePrefs();
// For MQTT bridge, migrate bridge.source to RX (logRx) only on fresh installs or upgrades
// so legacy "tx" is not the default. mqtt.rx / mqtt.tx are separate (fresh default: advert for TX)
if ((is_fresh_install || is_upgrade) && _prefs->bridge_pkt_src == 0) {
@@ -140,84 +126,20 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// MQTT settings - skip reading from main prefs file (now stored separately)
// For backward compatibility, we'll skip these bytes if they exist in old files
// The actual MQTT prefs will be loaded from /mqtt_prefs in loadMQTTPrefs()
// Skip MQTT fields for file format compatibility (whether MQTT bridge is enabled or not)
#ifdef WITH_MQTT_BRIDGE
size_t mqtt_fields_size = getMQTTFieldsSize(_prefs);
#else
// If MQTT bridge not enabled, still skip these fields for file format compatibility
size_t mqtt_fields_size =
sizeof(_prefs->mqtt_origin) + sizeof(_prefs->mqtt_iata) +
sizeof(_prefs->mqtt_status_enabled) + sizeof(_prefs->mqtt_packets_enabled) +
sizeof(_prefs->mqtt_raw_enabled) + sizeof(_prefs->mqtt_tx_enabled) +
sizeof(_prefs->mqtt_status_interval) + sizeof(_prefs->wifi_ssid) +
sizeof(_prefs->wifi_password) + sizeof(_prefs->timezone_string) +
sizeof(_prefs->timezone_offset) + sizeof(_prefs->mqtt_slot_preset) +
sizeof(_prefs->mqtt_slot_host) + sizeof(_prefs->mqtt_slot_port) +
sizeof(_prefs->mqtt_slot_username) + sizeof(_prefs->mqtt_slot_password) +
sizeof(_prefs->mqtt_owner_public_key) + sizeof(_prefs->mqtt_email);
#endif
uint8_t skip_buffer[512]; // Large enough buffer
size_t remaining = mqtt_fields_size;
while (remaining > 0) {
size_t to_read = remaining > sizeof(skip_buffer) ? sizeof(skip_buffer) : remaining;
file.read(skip_buffer, to_read);
remaining -= to_read;
// MQTT/observer settings are no longer stored in /com_prefs — they live in
// /mqtt_prefs (loaded by loadMQTTPrefs). The legacy zero-gap and trailing
// snmp/alert block that older firmware wrote here are gone; on upgrade those
// bytes are simply not read, so rx_boosted_gain/flood_max_* (which followed the
// gap) reset once and self-heal on the next save.
if (file.available() >= (int)sizeof(_prefs->rx_boosted_gain)) {
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain));
}
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
{
// Tail layout (current): 291-292 flood_max_*, 293+ snmp/alert fields.
// Legacy flex-branch files stored snmp at 291 without flood_max_* fields.
uint8_t byte291, byte292;
file.read(&byte291, 1);
file.read(&byte292, 1);
if (byte291 <= 1 && byte292 > 64) {
_prefs->snmp_enabled = byte291;
_prefs->snmp_community[0] = (char)byte292;
file.read((uint8_t *)&_prefs->snmp_community[1], sizeof(_prefs->snmp_community) - 1);
} else {
_prefs->flood_max_unscoped = byte291;
_prefs->flood_max_advert = byte292;
if (file.available() >= (int)sizeof(_prefs->snmp_enabled)) {
file.read((uint8_t *)&_prefs->snmp_enabled, sizeof(_prefs->snmp_enabled)); // 293
}
if (file.available() >= (int)sizeof(_prefs->snmp_community)) {
file.read((uint8_t *)&_prefs->snmp_community, sizeof(_prefs->snmp_community)); // 294
}
}
if (file.available() >= (int)sizeof(_prefs->flood_max_unscoped)) {
file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped));
}
if (file.available() >= (int)sizeof(_prefs->radio_watchdog_minutes)) {
file.read((uint8_t *)&_prefs->radio_watchdog_minutes, sizeof(_prefs->radio_watchdog_minutes)); // 318
if (file.available() >= (int)sizeof(_prefs->flood_max_advert)) {
file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert));
}
// Alert channel fields (appended; older files won't have them — defaults from MyMesh ctor remain)
if (file.available() >= (int)sizeof(_prefs->alert_enabled)) {
file.read((uint8_t *)&_prefs->alert_enabled, sizeof(_prefs->alert_enabled));
}
if (file.available() >= (int)sizeof(_prefs->alert_psk_hex)) {
file.read((uint8_t *)&_prefs->alert_psk_hex, sizeof(_prefs->alert_psk_hex));
}
if (file.available() >= (int)sizeof(_prefs->alert_wifi_minutes)) {
file.read((uint8_t *)&_prefs->alert_wifi_minutes, sizeof(_prefs->alert_wifi_minutes));
}
if (file.available() >= (int)sizeof(_prefs->alert_mqtt_minutes)) {
file.read((uint8_t *)&_prefs->alert_mqtt_minutes, sizeof(_prefs->alert_mqtt_minutes));
}
if (file.available() >= (int)sizeof(_prefs->alert_min_interval_min)) {
file.read((uint8_t *)&_prefs->alert_min_interval_min, sizeof(_prefs->alert_min_interval_min));
}
if (file.available() >= (int)sizeof(_prefs->alert_hashtag)) {
file.read((uint8_t *)&_prefs->alert_hashtag, sizeof(_prefs->alert_hashtag));
}
if (file.available() >= (int)sizeof(_prefs->alert_region)) {
file.read((uint8_t *)&_prefs->alert_region, sizeof(_prefs->alert_region));
}
// ensure null termination after raw read
_prefs->snmp_community[sizeof(_prefs->snmp_community) - 1] = '\0';
_prefs->alert_psk_hex[sizeof(_prefs->alert_psk_hex) - 1] = '\0';
_prefs->alert_hashtag[sizeof(_prefs->alert_hashtag) - 1] = '\0';
_prefs->alert_region[sizeof(_prefs->alert_region) - 1] = '\0';
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -247,11 +169,6 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
_prefs->snmp_enabled = constrain(_prefs->snmp_enabled, 0, 1);
_prefs->snmp_community[sizeof(_prefs->snmp_community) - 1] = '\0'; // ensure null terminated
if (_prefs->radio_watchdog_minutes > 120) {
_prefs->radio_watchdog_minutes = 5;
}
file.close();
}
@@ -312,50 +229,17 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// MQTT settings - no longer saved here (stored in separate /mqtt_prefs file)
// Write zeros/padding to maintain file format compatibility
#ifdef WITH_MQTT_BRIDGE
size_t mqtt_fields_size = getMQTTFieldsSize(_prefs);
#else
// If MQTT bridge not enabled, still write zeros for file format compatibility
size_t mqtt_fields_size =
sizeof(_prefs->mqtt_origin) + sizeof(_prefs->mqtt_iata) +
sizeof(_prefs->mqtt_status_enabled) + sizeof(_prefs->mqtt_packets_enabled) +
sizeof(_prefs->mqtt_raw_enabled) + sizeof(_prefs->mqtt_tx_enabled) +
sizeof(_prefs->mqtt_status_interval) + sizeof(_prefs->wifi_ssid) +
sizeof(_prefs->wifi_password) + sizeof(_prefs->timezone_string) +
sizeof(_prefs->timezone_offset) + sizeof(_prefs->mqtt_slot_preset) +
sizeof(_prefs->mqtt_slot_host) + sizeof(_prefs->mqtt_slot_port) +
sizeof(_prefs->mqtt_slot_username) + sizeof(_prefs->mqtt_slot_password) +
sizeof(_prefs->mqtt_owner_public_key) + sizeof(_prefs->mqtt_email);
#endif
memset(pad, 0, sizeof(pad));
size_t remaining = mqtt_fields_size;
while (remaining > 0) {
size_t to_write = remaining > sizeof(pad) ? sizeof(pad) : remaining;
file.write(pad, to_write);
remaining -= to_write;
}
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
file.write((uint8_t *)&_prefs->snmp_enabled, sizeof(_prefs->snmp_enabled)); // 293
file.write((uint8_t *)&_prefs->snmp_community, sizeof(_prefs->snmp_community)); // 294
file.write((uint8_t *)&_prefs->radio_watchdog_minutes, sizeof(_prefs->radio_watchdog_minutes)); // 318
// Alert channel fields (appended)
file.write((uint8_t *)&_prefs->alert_enabled, sizeof(_prefs->alert_enabled));
file.write((uint8_t *)&_prefs->alert_psk_hex, sizeof(_prefs->alert_psk_hex));
file.write((uint8_t *)&_prefs->alert_wifi_minutes, sizeof(_prefs->alert_wifi_minutes));
file.write((uint8_t *)&_prefs->alert_mqtt_minutes, sizeof(_prefs->alert_mqtt_minutes));
file.write((uint8_t *)&_prefs->alert_min_interval_min, sizeof(_prefs->alert_min_interval_min));
file.write((uint8_t *)&_prefs->alert_hashtag, sizeof(_prefs->alert_hashtag));
file.write((uint8_t *)&_prefs->alert_region, sizeof(_prefs->alert_region));
// MQTT/observer settings are stored in /mqtt_prefs, not here. No zero-gap is
// written anymore — /com_prefs holds only the (non-observer) fields below.
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain));
file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped));
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert));
file.close();
}
#ifdef WITH_MQTT_BRIDGE
// Save MQTT preferences to separate file
syncNodePrefsToMQTTPrefs(); // Sync any changes from NodePrefs to MQTTPrefs
// Observer config (MQTT/WiFi/timezone/SNMP/alert) is persisted separately. The
// observer CLI writes _mqtt_prefs directly, so no NodePrefs->MQTTPrefs sync runs.
saveMQTTPrefs(fs);
#endif
}
@@ -519,69 +403,6 @@ void CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) {
}
}
void CommonCLI::syncMQTTPrefsToNodePrefs() {
// Copy MQTT prefs to NodePrefs so existing code can access them
// Use StrHelper::strncpy to ensure proper null termination
StrHelper::strncpy(_prefs->mqtt_origin, _mqtt_prefs.mqtt_origin, sizeof(_prefs->mqtt_origin));
StrHelper::strncpy(_prefs->mqtt_iata, _mqtt_prefs.mqtt_iata, sizeof(_prefs->mqtt_iata));
_prefs->mqtt_status_enabled = _mqtt_prefs.mqtt_status_enabled;
_prefs->mqtt_packets_enabled = _mqtt_prefs.mqtt_packets_enabled;
_prefs->mqtt_raw_enabled = _mqtt_prefs.mqtt_raw_enabled;
_prefs->mqtt_tx_enabled = _mqtt_prefs.mqtt_tx_enabled;
_prefs->mqtt_rx_enabled = _mqtt_prefs.mqtt_rx_enabled;
_prefs->mqtt_status_interval = _mqtt_prefs.mqtt_status_interval;
StrHelper::strncpy(_prefs->wifi_ssid, _mqtt_prefs.wifi_ssid, sizeof(_prefs->wifi_ssid));
StrHelper::strncpy(_prefs->wifi_password, _mqtt_prefs.wifi_password, sizeof(_prefs->wifi_password));
_prefs->wifi_power_save = _mqtt_prefs.wifi_power_save;
StrHelper::strncpy(_prefs->timezone_string, _mqtt_prefs.timezone_string, sizeof(_prefs->timezone_string));
_prefs->timezone_offset = _mqtt_prefs.timezone_offset;
// Slot-based fields
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
StrHelper::strncpy(_prefs->mqtt_slot_preset[i], _mqtt_prefs.mqtt_slot_preset[i], sizeof(_prefs->mqtt_slot_preset[i]));
StrHelper::strncpy(_prefs->mqtt_slot_host[i], _mqtt_prefs.mqtt_slot_host[i], sizeof(_prefs->mqtt_slot_host[i]));
_prefs->mqtt_slot_port[i] = _mqtt_prefs.mqtt_slot_port[i];
StrHelper::strncpy(_prefs->mqtt_slot_username[i], _mqtt_prefs.mqtt_slot_username[i], sizeof(_prefs->mqtt_slot_username[i]));
StrHelper::strncpy(_prefs->mqtt_slot_password[i], _mqtt_prefs.mqtt_slot_password[i], sizeof(_prefs->mqtt_slot_password[i]));
StrHelper::strncpy(_prefs->mqtt_slot_token[i], _mqtt_prefs.mqtt_slot_token[i], sizeof(_prefs->mqtt_slot_token[i]));
StrHelper::strncpy(_prefs->mqtt_slot_topic[i], _mqtt_prefs.mqtt_slot_topic[i], sizeof(_prefs->mqtt_slot_topic[i]));
StrHelper::strncpy(_prefs->mqtt_slot_audience[i], _mqtt_prefs.mqtt_slot_audience[i], sizeof(_prefs->mqtt_slot_audience[i]));
}
StrHelper::strncpy(_prefs->mqtt_owner_public_key, _mqtt_prefs.mqtt_owner_public_key, sizeof(_prefs->mqtt_owner_public_key));
StrHelper::strncpy(_prefs->mqtt_email, _mqtt_prefs.mqtt_email, sizeof(_prefs->mqtt_email));
StrHelper::strncpy(_prefs->mqtt_ntp_server, _mqtt_prefs.mqtt_ntp_server, sizeof(_prefs->mqtt_ntp_server));
}
void CommonCLI::syncNodePrefsToMQTTPrefs() {
// Copy NodePrefs to MQTT prefs (used when saving after changes via CLI)
// Use StrHelper::strncpy to ensure proper null termination
StrHelper::strncpy(_mqtt_prefs.mqtt_origin, _prefs->mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin));
StrHelper::strncpy(_mqtt_prefs.mqtt_iata, _prefs->mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata));
_mqtt_prefs.mqtt_status_enabled = _prefs->mqtt_status_enabled;
_mqtt_prefs.mqtt_packets_enabled = _prefs->mqtt_packets_enabled;
_mqtt_prefs.mqtt_raw_enabled = _prefs->mqtt_raw_enabled;
_mqtt_prefs.mqtt_tx_enabled = _prefs->mqtt_tx_enabled;
_mqtt_prefs.mqtt_rx_enabled = _prefs->mqtt_rx_enabled;
_mqtt_prefs.mqtt_status_interval = _prefs->mqtt_status_interval;
StrHelper::strncpy(_mqtt_prefs.wifi_ssid, _prefs->wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid));
StrHelper::strncpy(_mqtt_prefs.wifi_password, _prefs->wifi_password, sizeof(_mqtt_prefs.wifi_password));
_mqtt_prefs.wifi_power_save = _prefs->wifi_power_save;
StrHelper::strncpy(_mqtt_prefs.timezone_string, _prefs->timezone_string, sizeof(_mqtt_prefs.timezone_string));
_mqtt_prefs.timezone_offset = _prefs->timezone_offset;
// Slot-based fields
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[i], _prefs->mqtt_slot_preset[i], sizeof(_mqtt_prefs.mqtt_slot_preset[i]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_host[i], _prefs->mqtt_slot_host[i], sizeof(_mqtt_prefs.mqtt_slot_host[i]));
_mqtt_prefs.mqtt_slot_port[i] = _prefs->mqtt_slot_port[i];
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_username[i], _prefs->mqtt_slot_username[i], sizeof(_mqtt_prefs.mqtt_slot_username[i]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_password[i], _prefs->mqtt_slot_password[i], sizeof(_mqtt_prefs.mqtt_slot_password[i]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_token[i], _prefs->mqtt_slot_token[i], sizeof(_mqtt_prefs.mqtt_slot_token[i]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_topic[i], _prefs->mqtt_slot_topic[i], sizeof(_mqtt_prefs.mqtt_slot_topic[i]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_audience[i], _prefs->mqtt_slot_audience[i], sizeof(_mqtt_prefs.mqtt_slot_audience[i]));
}
StrHelper::strncpy(_mqtt_prefs.mqtt_owner_public_key, _prefs->mqtt_owner_public_key, sizeof(_mqtt_prefs.mqtt_owner_public_key));
StrHelper::strncpy(_mqtt_prefs.mqtt_email, _prefs->mqtt_email, sizeof(_mqtt_prefs.mqtt_email));
StrHelper::strncpy(_mqtt_prefs.mqtt_ntp_server, _prefs->mqtt_ntp_server, sizeof(_mqtt_prefs.mqtt_ntp_server));
}
#endif
#define MIN_LOCAL_ADVERT_INTERVAL 60
@@ -918,30 +739,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs();
sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "radio.watchdog ", 15) == 0) {
const char* val = &config[15];
if (*val == 0) {
strcpy(reply, "Error: missing radio.watchdog minutes");
return;
}
for (const char* sp = val; *sp; sp++) {
if (*sp < '0' || *sp > '9') {
strcpy(reply, "Error: radio.watchdog must be an integer 0-120");
return;
}
}
int mins = atoi(val);
if (mins > 120) {
strcpy(reply, "Error: radio.watchdog must be 0-120 minutes");
} else {
_prefs->radio_watchdog_minutes = (uint8_t)mins;
savePrefs();
if (mins == 0) {
strcpy(reply, "OK - radio watchdog disabled");
} else {
sprintf(reply, "OK - radio watchdog %d min", mins);
}
}
} else if (memcmp(config, "multi.acks ", 11) == 0) {
_prefs->multi_acks = atoi(&config[11]);
savePrefs();
@@ -1154,11 +951,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0;
#ifdef WITH_MQTT_BRIDGE
if (_prefs->bridge_pkt_src == 1) {
_prefs->mqtt_rx_enabled = 1;
_prefs->mqtt_tx_enabled = 0;
_mqtt_prefs.mqtt_rx_enabled = 1;
_mqtt_prefs.mqtt_tx_enabled = 0;
} else {
_prefs->mqtt_rx_enabled = 0;
_prefs->mqtt_tx_enabled = 1;
_mqtt_prefs.mqtt_rx_enabled = 0;
_mqtt_prefs.mqtt_tx_enabled = 1;
}
#endif
savePrefs();
@@ -1226,8 +1023,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "radio.watchdog", 14) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->radio_watchdog_minutes);
} else if (memcmp(config, "multi.acks", 10) == 0) {
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
} else if (memcmp(config, "allow.read.only", 15) == 0) {
+23 -64
View File
@@ -63,71 +63,12 @@ struct NodePrefs { // persisted to file
uint32_t discovery_mod_timestamp;
float adc_multiplier;
char owner_info[120];
// MQTT settings (stored separately in /mqtt_prefs, but kept here for backward compatibility)
char mqtt_origin[32]; // Device name for MQTT topics
char mqtt_iata[8]; // IATA code for MQTT topics
uint8_t mqtt_status_enabled; // Enable status messages
uint8_t mqtt_packets_enabled; // Enable packet messages
uint8_t mqtt_raw_enabled; // Enable raw messages
uint8_t mqtt_tx_enabled; // TX packet uplinking: 0=off, 1=all, 2=advert (self-originated only)
uint32_t mqtt_status_interval; // Status publish interval (ms)
uint8_t mqtt_rx_enabled; // Enable RX packet uplinking (default: on)
// WiFi settings
char wifi_ssid[32]; // WiFi SSID
char wifi_password[64]; // WiFi password
uint8_t wifi_power_save; // WiFi power save mode: 0=min, 1=none, 2=max (default: 1=none)
// Timezone settings
char timezone_string[32]; // Timezone string (e.g., "America/Los_Angeles")
int8_t timezone_offset; // Timezone offset in hours (-12 to +14) - fallback
// MQTT slot presets (up to MAX_MQTT_SLOTS, each can be a preset name or "custom"/"none")
char mqtt_slot_preset[MAX_MQTT_SLOTS][24]; // e.g. "analyzer-us", "meshmapper", "custom", "none"
// Per-slot custom broker settings (only used when slot preset is "custom")
char mqtt_slot_host[MAX_MQTT_SLOTS][64];
uint16_t mqtt_slot_port[MAX_MQTT_SLOTS];
char mqtt_slot_username[MAX_MQTT_SLOTS][32];
char mqtt_slot_password[MAX_MQTT_SLOTS][64];
// Shared MQTT authentication
char mqtt_owner_public_key[65]; // Owner public key (hex string, same length as repeater public key)
char mqtt_email[64]; // Owner email address for matching nodes with owners
// Per-slot extended fields
char mqtt_slot_token[MAX_MQTT_SLOTS][48]; // Per-slot token (e.g., MeshRank account token)
char mqtt_slot_topic[MAX_MQTT_SLOTS][96]; // Per-slot custom topic template (custom preset only)
char mqtt_slot_audience[MAX_MQTT_SLOTS][64]; // JWT audience (non-empty enables JWT auth for custom slots)
uint8_t loop_detect;
// SNMP settings (optional, only used when WITH_SNMP is defined)
uint8_t snmp_enabled; // boolean: 0=off, 1=on
char snmp_community[24]; // community string (default "public")
uint8_t radio_watchdog_minutes; // 0=disabled, 1-120 minutes
// Fault alert channel (LoRa group-channel "observer status" message on prolonged WiFi/MQTT outage).
// Sent over the radio (NOT over MQTT) so the alert still works while the MQTT path is broken.
// All fields are appended at the end of NodePrefs for binary-compatible upgrades.
uint8_t alert_enabled; // 0 = off (default), 1 = on
char alert_psk_hex[33]; // 32 lowercase hex chars (16-byte channel secret) + null; empty = alerts disabled. Banned keys (Public/#test/#bot) are rejected.
uint16_t alert_wifi_minutes; // WiFi-down threshold in minutes (0 = disabled), default 30
uint16_t alert_mqtt_minutes; // MQTT-down threshold in minutes (0 = disabled), default 240 (4 h)
uint16_t alert_min_interval_min; // min minutes between alerts for the same fault, default 60, floor 60
// When the operator configures via `set alert.hashtag <name>`, we derive
// alert_psk_hex from sha256("#name")[0..15] once and remember the hashtag
// text here purely for `get alert.hashtag` readback. A subsequent
// `set alert.psk` clears this field so it doesn't lie about provenance.
char alert_hashtag[24];
// Optional region name (e.g. "us", "eu"); empty = use the repeater's
// default_scope. Looked up lazily via RegionMap::findByNamePrefix at send
// time, so the operator can name a region that doesn't exist yet without
// polluting region_map state. Falls back to default_scope on miss.
char alert_region[31];
// Custom NTP server (MQTT observer); empty = built-in default primary (pool.ntp.org)
char mqtt_ntp_server[64];
// NOTE: observer settings (MQTT/WiFi/timezone/SNMP/alert) were moved out of
// NodePrefs into MQTTPrefs (persisted to /mqtt_prefs) so this struct stays
// aligned with upstream. See struct MQTTPrefs below.
};
#ifdef WITH_MQTT_BRIDGE
@@ -206,6 +147,20 @@ struct MQTTPrefs {
// --- Appended fields (added after initial 6-slot migration) ---
uint8_t mqtt_rx_enabled; // Enable RX packet uplinking (default: on)
char mqtt_ntp_server[64]; // Custom NTP server; empty = pool.ntp.org
// --- Observer non-MQTT settings, moved out of NodePrefs (Phase 2). Appended at
// the end so existing /mqtt_prefs files (which lack them) still load; values are
// migrated one-time from the old /com_prefs trailing block on first boot. ---
uint8_t snmp_enabled; // boolean
char snmp_community[24]; // community string (default "public")
uint8_t radio_watchdog_minutes; // 0=disabled, 1-120 minutes (observer-only radio recovery)
uint8_t alert_enabled; // 0 = off (default)
char alert_psk_hex[33]; // 32 hex chars + null; empty = alerts disabled
uint16_t alert_wifi_minutes; // WiFi-down threshold (0 = disabled), default 30
uint16_t alert_mqtt_minutes; // MQTT-down threshold (0 = disabled), default 240
uint16_t alert_min_interval_min; // min minutes between same-fault alerts, default 60
char alert_hashtag[24]; // readback for `get alert.hashtag`
char alert_region[31]; // optional region override; empty = default_scope
};
// 3-slot MQTTPrefs layout — used for migrating from 3-slot to 6-slot format.
@@ -360,8 +315,6 @@ class CommonCLI {
#ifdef WITH_MQTT_BRIDGE
void loadMQTTPrefs(FILESYSTEM* fs);
void saveMQTTPrefs(FILESYSTEM* fs);
void syncMQTTPrefsToNodePrefs();
void syncNodePrefsToMQTTPrefs();
#endif
void handleRegionCmd(char* command, char* reply);
@@ -387,4 +340,10 @@ public:
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.
// 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); }
#endif
};
+153 -112
View File
@@ -160,64 +160,89 @@ static void formatMQTTPresetListReply(char* reply, size_t reply_size, int start)
#endif
bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* config, char* reply) {
#ifdef WITH_MQTT_BRIDGE
bool handled = true;
if (memcmp(config, "snmp.community ", 15) == 0) {
StrHelper::strncpy(_prefs->snmp_community, &config[15], sizeof(_prefs->snmp_community));
StrHelper::strncpy(_mqtt_prefs.snmp_community, &config[15], sizeof(_mqtt_prefs.snmp_community));
savePrefs();
strcpy(reply, "OK - restart to apply");
} else if (memcmp(config, "snmp ", 5) == 0) {
_prefs->snmp_enabled = memcmp(&config[5], "on", 2) == 0;
_mqtt_prefs.snmp_enabled = memcmp(&config[5], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK - restart to apply");
} else if (memcmp(config, "radio.watchdog ", 15) == 0) {
const char* val = &config[15];
bool all_digits = (*val != '\0');
for (const char* sp = val; *sp; sp++) {
if (*sp < '0' || *sp > '9') { all_digits = false; break; }
}
if (*val == '\0') {
strcpy(reply, "Error: missing radio.watchdog minutes");
} else if (!all_digits) {
strcpy(reply, "Error: radio.watchdog must be an integer 0-120");
} else {
int mins = atoi(val);
if (mins > 120) {
strcpy(reply, "Error: radio.watchdog must be 0-120 minutes");
} else {
_mqtt_prefs.radio_watchdog_minutes = (uint8_t)mins;
savePrefs();
if (mins == 0) {
strcpy(reply, "OK - radio watchdog disabled");
} else {
sprintf(reply, "OK - radio watchdog %d min", mins);
}
}
}
#ifdef WITH_MQTT_BRIDGE
} else if (strcmp(config, "mqtt.origin") == 0) {
_prefs->mqtt_origin[0] = '\0';
_mqtt_prefs.mqtt_origin[0] = '\0';
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.origin ", 12) == 0) {
StrHelper::strncpy(_prefs->mqtt_origin, &config[12], sizeof(_prefs->mqtt_origin));
StrHelper::stripSurroundingQuotes(_prefs->mqtt_origin, sizeof(_prefs->mqtt_origin));
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();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.iata ", 10) == 0) {
StrHelper::strncpy(_prefs->mqtt_iata, &config[10], sizeof(_prefs->mqtt_iata));
for (int i = 0; _prefs->mqtt_iata[i]; i++) {
_prefs->mqtt_iata[i] = toupper(_prefs->mqtt_iata[i]);
StrHelper::strncpy(_mqtt_prefs.mqtt_iata, &config[10], sizeof(_mqtt_prefs.mqtt_iata));
for (int i = 0; _mqtt_prefs.mqtt_iata[i]; i++) {
_mqtt_prefs.mqtt_iata[i] = toupper(_mqtt_prefs.mqtt_iata[i]);
}
savePrefs();
_callbacks->restartBridge();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.status ", 12) == 0) {
_prefs->mqtt_status_enabled = memcmp(&config[12], "on", 2) == 0;
_mqtt_prefs.mqtt_status_enabled = memcmp(&config[12], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.packets ", 13) == 0) {
_prefs->mqtt_packets_enabled = memcmp(&config[13], "on", 2) == 0;
_mqtt_prefs.mqtt_packets_enabled = memcmp(&config[13], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.raw ", 9) == 0) {
_prefs->mqtt_raw_enabled = memcmp(&config[9], "on", 2) == 0;
_mqtt_prefs.mqtt_raw_enabled = memcmp(&config[9], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.tx ", 8) == 0) {
if (memcmp(&config[8], "advert", 6) == 0) {
_prefs->mqtt_tx_enabled = 2;
_mqtt_prefs.mqtt_tx_enabled = 2;
} else {
_prefs->mqtt_tx_enabled = memcmp(&config[8], "on", 2) == 0 ? 1 : 0;
_mqtt_prefs.mqtt_tx_enabled = memcmp(&config[8], "on", 2) == 0 ? 1 : 0;
}
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.rx ", 8) == 0) {
_prefs->mqtt_rx_enabled = memcmp(&config[8], "on", 2) == 0 ? 1 : 0;
_mqtt_prefs.mqtt_rx_enabled = memcmp(&config[8], "on", 2) == 0 ? 1 : 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "mqtt.interval ", 14) == 0) {
uint32_t minutes = _atoi(&config[14]);
if (minutes >= 1 && minutes <= 60) {
_prefs->mqtt_status_interval = minutes * 60000;
_mqtt_prefs.mqtt_status_interval = minutes * 60000;
savePrefs();
_callbacks->restartBridge();
sprintf(reply, "OK - interval set to %u minutes (%lu ms), bridge restarted", minutes, (unsigned long)_prefs->mqtt_status_interval);
sprintf(reply, "OK - interval set to %u minutes (%lu ms), bridge restarted", minutes, (unsigned long)_mqtt_prefs.mqtt_status_interval);
} else {
strcpy(reply, "Error: interval must be between 1-60 minutes");
}
@@ -229,9 +254,9 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: invalid NTP hostname");
} else {
if (clearing) {
_prefs->mqtt_ntp_server[0] = '\0';
_mqtt_prefs.mqtt_ntp_server[0] = '\0';
} else {
StrHelper::strncpy(_prefs->mqtt_ntp_server, host, sizeof(_prefs->mqtt_ntp_server));
StrHelper::strncpy(_mqtt_prefs.mqtt_ntp_server, host, sizeof(_mqtt_prefs.mqtt_ntp_server));
}
savePrefs();
#ifdef ESP_PLATFORM
@@ -251,11 +276,11 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
#endif
}
} else if (memcmp(config, "wifi.ssid ", 10) == 0) {
StrHelper::strncpy(_prefs->wifi_ssid, &config[10], sizeof(_prefs->wifi_ssid));
StrHelper::strncpy(_mqtt_prefs.wifi_ssid, &config[10], sizeof(_mqtt_prefs.wifi_ssid));
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "wifi.pwd ", 9) == 0) {
StrHelper::strncpy(_prefs->wifi_password, &config[9], sizeof(_prefs->wifi_password));
StrHelper::strncpy(_mqtt_prefs.wifi_password, &config[9], sizeof(_mqtt_prefs.wifi_password));
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "wifi.powersave ", 15) == 0) {
@@ -275,7 +300,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
if (!valid) {
strcpy(reply, "Error: must be none, min, or max");
} else {
_prefs->wifi_power_save = ps_value;
_mqtt_prefs.wifi_power_save = ps_value;
savePrefs();
#ifdef ESP_PLATFORM
if (WiFi.status() == WL_CONNECTED) {
@@ -298,13 +323,13 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
#endif
}
} else if (memcmp(config, "timezone ", 9) == 0) {
StrHelper::strncpy(_prefs->timezone_string, &config[9], sizeof(_prefs->timezone_string));
StrHelper::strncpy(_mqtt_prefs.timezone_string, &config[9], sizeof(_mqtt_prefs.timezone_string));
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "timezone.offset ", 16) == 0) {
int8_t offset = _atoi(&config[16]);
if (offset >= -12 && offset <= 14) {
_prefs->timezone_offset = offset;
_mqtt_prefs.timezone_offset = offset;
savePrefs();
strcpy(reply, "OK");
} else {
@@ -325,7 +350,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
int dup_slot = -1;
if (findMQTTPreset(preset_name) != nullptr) {
for (int s = 0; s < MAX_MQTT_SLOTS; s++) {
if (s != slot && strcmp(_prefs->mqtt_slot_preset[s], preset_name) == 0) {
if (s != slot && strcmp(_mqtt_prefs.mqtt_slot_preset[s], preset_name) == 0) {
dup_slot = s;
break;
}
@@ -334,19 +359,19 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
if (dup_slot >= 0) {
sprintf(reply, "Error: preset '%s' is already assigned to slot %d", preset_name, dup_slot + 1);
} else {
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], preset_name, sizeof(_prefs->mqtt_slot_preset[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], preset_name, sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
savePrefs();
_callbacks->restartBridgeSlot(slot);
// Check if the slot has everything it needs to connect
const MQTTPresetDef* p = findMQTTPreset(preset_name);
if (p && p->topic_style == MQTT_TOPIC_MESHRANK && _prefs->mqtt_slot_token[slot][0] == '\0') {
if (p && p->topic_style == MQTT_TOPIC_MESHRANK && _mqtt_prefs.mqtt_slot_token[slot][0] == '\0') {
sprintf(reply, "OK - slot %d preset: %s (run 'set mqtt%d.token <your_token>' to connect)", slot + 1, preset_name, slot + 1);
} else if (p && p->topic_style == MQTT_TOPIC_MESHCORE &&
(strlen(_prefs->mqtt_iata) == 0 || strcmp(_prefs->mqtt_iata, "XXX") == 0)) {
(strlen(_mqtt_prefs.mqtt_iata) == 0 || strcmp(_mqtt_prefs.mqtt_iata, "XXX") == 0)) {
sprintf(reply, "OK - slot %d preset: %s (run 'set mqtt.iata <airport_code>' to publish)", slot + 1, preset_name);
} else if (p && mqttPresetNeedsSlotCredentials(p) &&
(_prefs->mqtt_slot_username[slot][0] == '\0' ||
_prefs->mqtt_slot_password[slot][0] == '\0')) {
(_mqtt_prefs.mqtt_slot_username[slot][0] == '\0' ||
_mqtt_prefs.mqtt_slot_password[slot][0] == '\0')) {
sprintf(reply,
"OK - slot %d preset: %s (run 'set mqtt%d.username <user>' and 'set mqtt%d.password <pass>' to connect)",
slot + 1, preset_name, slot + 1, slot + 1);
@@ -358,54 +383,54 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: unknown preset. Use 'get mqtt.presets'");
}
} else if (memcmp(subcmd, "server ", 7) == 0) {
StrHelper::strncpy(_prefs->mqtt_slot_host[slot], &subcmd[7], sizeof(_prefs->mqtt_slot_host[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_host[slot], &subcmd[7], sizeof(_mqtt_prefs.mqtt_slot_host[slot]));
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(subcmd, "port ", 5) == 0) {
int port = atoi(&subcmd[5]);
if (port > 0 && port <= 65535) {
_prefs->mqtt_slot_port[slot] = port;
_mqtt_prefs.mqtt_slot_port[slot] = port;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: port must be between 1 and 65535");
}
} else if (memcmp(subcmd, "username ", 9) == 0) {
StrHelper::strncpy(_prefs->mqtt_slot_username[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_username[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_username[slot], &subcmd[9], sizeof(_mqtt_prefs.mqtt_slot_username[slot]));
savePrefs();
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (memcmp(subcmd, "password ", 9) == 0) {
StrHelper::strncpy(_prefs->mqtt_slot_password[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_password[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_password[slot], &subcmd[9], sizeof(_mqtt_prefs.mqtt_slot_password[slot]));
savePrefs();
_callbacks->restartBridgeSlot(slot);
strcpy(reply, "OK");
} else if (memcmp(subcmd, "token ", 6) == 0) {
StrHelper::strncpy(_prefs->mqtt_slot_token[slot], &subcmd[6], sizeof(_prefs->mqtt_slot_token[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_token[slot], &subcmd[6], sizeof(_mqtt_prefs.mqtt_slot_token[slot]));
savePrefs();
_callbacks->restartBridgeSlot(slot);
sprintf(reply, "OK - slot %d token set", slot + 1);
} else if (memcmp(subcmd, "topic ", 6) == 0) {
if (strcmp(_prefs->mqtt_slot_preset[slot], "custom") != 0) {
if (strcmp(_mqtt_prefs.mqtt_slot_preset[slot], "custom") != 0) {
sprintf(reply, "Error: topic template only applies to custom preset slots");
} else {
StrHelper::strncpy(_prefs->mqtt_slot_topic[slot], &subcmd[6], sizeof(_prefs->mqtt_slot_topic[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_topic[slot], &subcmd[6], sizeof(_mqtt_prefs.mqtt_slot_topic[slot]));
savePrefs();
_callbacks->restartBridgeSlot(slot);
sprintf(reply, "OK - slot %d topic: %s", slot + 1, _prefs->mqtt_slot_topic[slot]);
sprintf(reply, "OK - slot %d topic: %s", slot + 1, _mqtt_prefs.mqtt_slot_topic[slot]);
}
} else if (memcmp(subcmd, "audience ", 9) == 0) {
StrHelper::strncpy(_prefs->mqtt_slot_audience[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_audience[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_audience[slot], &subcmd[9], sizeof(_mqtt_prefs.mqtt_slot_audience[slot]));
savePrefs();
_callbacks->restartBridgeSlot(slot);
if (_prefs->mqtt_slot_audience[slot][0] != '\0') {
sprintf(reply, "OK - slot %d JWT audience: %s", slot + 1, _prefs->mqtt_slot_audience[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]);
} else {
sprintf(reply, "OK - slot %d JWT audience cleared (using username/password auth)", slot + 1);
}
} else if (memcmp(subcmd, "audience", 8) == 0 && subcmd[8] == '\0') {
// "set mqttN.audience" with no value — clear the audience
_prefs->mqtt_slot_audience[slot][0] = '\0';
_mqtt_prefs.mqtt_slot_audience[slot][0] = '\0';
savePrefs();
_callbacks->restartBridgeSlot(slot);
sprintf(reply, "OK - slot %d JWT audience cleared (using username/password auth)", slot + 1);
@@ -415,9 +440,9 @@ 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) {
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], "analyzer-us", sizeof(_prefs->mqtt_slot_preset[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], "analyzer-us", sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
} else {
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_prefs->mqtt_slot_preset[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
}
savePrefs();
_callbacks->restartBridgeSlot(slot);
@@ -425,9 +450,9 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else if (memcmp(config, "mqtt.analyzer.eu ", 17) == 0) {
const int slot = 1;
if (memcmp(&config[17], "on", 2) == 0) {
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], "analyzer-eu", sizeof(_prefs->mqtt_slot_preset[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], "analyzer-eu", sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
} else {
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_prefs->mqtt_slot_preset[slot]));
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[slot], MQTT_PRESET_NONE, sizeof(_mqtt_prefs.mqtt_slot_preset[slot]));
}
savePrefs();
_callbacks->restartBridgeSlot(slot);
@@ -446,7 +471,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
}
}
if (valid_key) {
StrHelper::strncpy(_prefs->mqtt_owner_public_key, owner_key, sizeof(_prefs->mqtt_owner_public_key));
StrHelper::strncpy(_mqtt_prefs.mqtt_owner_public_key, owner_key, sizeof(_mqtt_prefs.mqtt_owner_public_key));
savePrefs();
strcpy(reply, "OK");
} else {
@@ -456,7 +481,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: public key must be 64 hex characters (32 bytes)");
}
} else if (memcmp(config, "mqtt.email ", 11) == 0) {
StrHelper::strncpy(_prefs->mqtt_email, &config[11], sizeof(_prefs->mqtt_email));
StrHelper::strncpy(_mqtt_prefs.mqtt_email, &config[11], sizeof(_mqtt_prefs.mqtt_email));
savePrefs();
strcpy(reply, "OK");
#endif
@@ -464,12 +489,12 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
// set alert on|off
const char* val = &config[6];
if (memcmp(val, "on", 2) == 0 && (val[2] == 0 || val[2] == ' ')) {
_prefs->alert_enabled = 1;
_mqtt_prefs.alert_enabled = 1;
savePrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alerts on");
} else if (memcmp(val, "off", 3) == 0 && (val[3] == 0 || val[3] == ' ')) {
_prefs->alert_enabled = 0;
_mqtt_prefs.alert_enabled = 0;
savePrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alerts off");
@@ -483,8 +508,8 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
while (*val == ' ') val++;
size_t len = strlen(val);
if (len == 0) {
_prefs->alert_psk_hex[0] = '\0';
_prefs->alert_hashtag[0] = '\0';
_mqtt_prefs.alert_psk_hex[0] = '\0';
_mqtt_prefs.alert_hashtag[0] = '\0';
savePrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.psk cleared (alerts disabled until configured)");
@@ -515,10 +540,10 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
// those channels would spam every node in the area.
sprintf(reply, "Error: refusing banned channel '%s'; pick a private key or hashtag", banned);
} else {
StrHelper::strncpy(_prefs->alert_psk_hex, normalized, sizeof(_prefs->alert_psk_hex));
StrHelper::strncpy(_mqtt_prefs.alert_psk_hex, normalized, sizeof(_mqtt_prefs.alert_psk_hex));
// The new PSK is operator-supplied, so any previously-derived
// hashtag name is no longer accurate provenance — drop it.
_prefs->alert_hashtag[0] = '\0';
_mqtt_prefs.alert_hashtag[0] = '\0';
savePrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.psk updated");
@@ -530,8 +555,8 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
while (*val == ' ') val++;
size_t in_len = strlen(val);
if (in_len == 0) {
_prefs->alert_psk_hex[0] = '\0';
_prefs->alert_hashtag[0] = '\0';
_mqtt_prefs.alert_psk_hex[0] = '\0';
_mqtt_prefs.alert_hashtag[0] = '\0';
savePrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.hashtag cleared (alerts disabled until configured)");
@@ -540,7 +565,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
// the sha256 input (matching the companion-app hashtag-channel
// derivation in docs/companion_protocol.md). Accept the user typing
// either "alerts" or "#alerts".
char hashtag[sizeof(_prefs->alert_hashtag)];
char hashtag[sizeof(_mqtt_prefs.alert_hashtag)];
size_t need = (val[0] == '#') ? in_len : in_len + 1;
if (need >= sizeof(hashtag)) {
strcpy(reply, "Error: hashtag too long");
@@ -566,11 +591,11 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
} else {
char hex[33];
mesh::Utils::toHex(hex, digest, 16);
StrHelper::strncpy(_prefs->alert_hashtag, hashtag, sizeof(_prefs->alert_hashtag));
StrHelper::strncpy(_prefs->alert_psk_hex, hex, sizeof(_prefs->alert_psk_hex));
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();
_callbacks->onAlertConfigChanged();
sprintf(reply, "OK - alert.hashtag: %s", _prefs->alert_hashtag);
sprintf(reply, "OK - alert.hashtag: %s", _mqtt_prefs.alert_hashtag);
}
}
}
@@ -585,25 +610,25 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
while (*val == ' ') val++;
size_t len = strlen(val);
if (len == 0) {
_prefs->alert_region[0] = '\0';
_mqtt_prefs.alert_region[0] = '\0';
savePrefs();
_callbacks->onAlertConfigChanged();
strcpy(reply, "OK - alert.region cleared (using default scope)");
} else if (len >= sizeof(_prefs->alert_region)) {
} else if (len >= sizeof(_mqtt_prefs.alert_region)) {
strcpy(reply, "Error: alert.region too long");
} else {
StrHelper::strncpy(_prefs->alert_region, val, sizeof(_prefs->alert_region));
StrHelper::stripSurroundingQuotes(_prefs->alert_region, sizeof(_prefs->alert_region));
StrHelper::strncpy(_mqtt_prefs.alert_region, val, sizeof(_mqtt_prefs.alert_region));
StrHelper::stripSurroundingQuotes(_mqtt_prefs.alert_region, sizeof(_mqtt_prefs.alert_region));
savePrefs();
_callbacks->onAlertConfigChanged();
sprintf(reply, "OK - alert.region: %s", _prefs->alert_region);
sprintf(reply, "OK - alert.region: %s", _mqtt_prefs.alert_region);
}
} else if (memcmp(config, "alert.wifi ", 11) == 0) {
int mins = (int)_atoi(&config[11]);
if (mins < 0 || mins > 1440) {
strcpy(reply, "Error: alert.wifi must be 0-1440 minutes (0=off)");
} else {
_prefs->alert_wifi_minutes = (uint16_t)mins;
_mqtt_prefs.alert_wifi_minutes = (uint16_t)mins;
savePrefs();
sprintf(reply, "OK - alert.wifi %d min%s", mins, mins == 0 ? " (disabled)" : "");
}
@@ -612,7 +637,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
if (mins < 0 || mins > 10080) {
strcpy(reply, "Error: alert.mqtt must be 0-10080 minutes (0=off)");
} else {
_prefs->alert_mqtt_minutes = (uint16_t)mins;
_mqtt_prefs.alert_mqtt_minutes = (uint16_t)mins;
savePrefs();
sprintf(reply, "OK - alert.mqtt %d min%s", mins, mins == 0 ? " (disabled)" : "");
}
@@ -623,7 +648,7 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
if (mins < 60 || mins > 10080) {
strcpy(reply, "Error: alert.interval must be 60-10080 minutes");
} else {
_prefs->alert_min_interval_min = (uint16_t)mins;
_mqtt_prefs.alert_min_interval_min = (uint16_t)mins;
savePrefs();
sprintf(reply, "OK - alert.interval %d min", mins);
}
@@ -631,21 +656,28 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
handled = false;
}
return handled;
#else
(void)sender_timestamp; (void)config; (void)reply;
return false;
#endif
}
bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* config, char* reply) {
#ifdef WITH_MQTT_BRIDGE
bool handled = true;
if (memcmp(config, "snmp.community", 14) == 0) {
sprintf(reply, "> %s", _prefs->snmp_community);
sprintf(reply, "> %s", _mqtt_prefs.snmp_community);
} else if (memcmp(config, "snmp", 4) == 0 && (config[4] == '\0' || config[4] == '\n' || config[4] == '\r')) {
strcpy(reply, _prefs->snmp_enabled ? "> on" : "> off");
strcpy(reply, _mqtt_prefs.snmp_enabled ? "> on" : "> off");
} else if (memcmp(config, "radio.watchdog", 14) == 0) {
sprintf(reply, "> %d", (uint32_t)_mqtt_prefs.radio_watchdog_minutes);
#ifdef WITH_MQTT_BRIDGE
} else if (memcmp(config, "mqtt.origin", 11) == 0) {
char effective_origin[32];
MQTTBridge::getEffectiveMqttOrigin(_prefs, effective_origin, sizeof(effective_origin));
MQTTBridge::getEffectiveMqttOrigin(_prefs, &_mqtt_prefs, effective_origin, sizeof(effective_origin));
sprintf(reply, "> %s", effective_origin);
} else if (memcmp(config, "mqtt.iata", 9) == 0) {
sprintf(reply, "> %s", _prefs->mqtt_iata);
sprintf(reply, "> %s", _mqtt_prefs.mqtt_iata);
} else if (memcmp(config, "mqtt.presets", 12) == 0 && (config[12] == '\0' || config[12] == ' ')) {
int start = 0;
if (config[12] == ' ') {
@@ -664,19 +696,19 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
}
formatMQTTPresetListReply(reply, 160, start);
} else if (memcmp(config, "mqtt.status", 11) == 0) {
MQTTBridge::formatMqttStatusReply(reply, 160, _prefs);
MQTTBridge::formatMqttStatusReply(reply, 160, &_mqtt_prefs);
} else if (memcmp(config, "mqtt.packets", 12) == 0) {
sprintf(reply, "> %s", _prefs->mqtt_packets_enabled ? "on" : "off");
sprintf(reply, "> %s", _mqtt_prefs.mqtt_packets_enabled ? "on" : "off");
} else if (memcmp(config, "mqtt.raw", 8) == 0) {
sprintf(reply, "> %s", _prefs->mqtt_raw_enabled ? "on" : "off");
sprintf(reply, "> %s", _mqtt_prefs.mqtt_raw_enabled ? "on" : "off");
} else if (memcmp(config, "mqtt.tx", 7) == 0) {
const char* tx_str = _prefs->mqtt_tx_enabled == 2 ? "advert" : (_prefs->mqtt_tx_enabled ? "on" : "off");
const char* tx_str = _mqtt_prefs.mqtt_tx_enabled == 2 ? "advert" : (_mqtt_prefs.mqtt_tx_enabled ? "on" : "off");
sprintf(reply, "> %s", tx_str);
} else if (memcmp(config, "mqtt.rx", 7) == 0) {
sprintf(reply, "> %s", _prefs->mqtt_rx_enabled ? "on" : "off");
sprintf(reply, "> %s", _mqtt_prefs.mqtt_rx_enabled ? "on" : "off");
} else if (memcmp(config, "mqtt.interval", 13) == 0) {
uint32_t minutes = (_prefs->mqtt_status_interval + 29999) / 60000;
sprintf(reply, "> %u minutes (%lu ms)", minutes, (unsigned long)_prefs->mqtt_status_interval);
uint32_t minutes = (_mqtt_prefs.mqtt_status_interval + 29999) / 60000;
sprintf(reply, "> %u minutes (%lu ms)", minutes, (unsigned long)_mqtt_prefs.mqtt_status_interval);
} else if (memcmp(config, "mqtt.ntp.diag", 13) == 0 && (config[13] == '\0' || config[13] == ' ')) {
#ifdef ESP_PLATFORM
// Connectivity probe across all configured NTP servers; never updates the clock.
@@ -692,37 +724,37 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, "Error: not supported on this platform");
#endif
} else if (memcmp(config, "mqtt.ntp", 8) == 0 && (config[8] == '\0' || config[8] == ' ')) {
sprintf(reply, "> %s", MQTTBridge::effectiveNtpPrimary(_prefs));
sprintf(reply, "> %s", MQTTBridge::effectiveNtpPrimary(&_mqtt_prefs));
} else if (config[0] == 'm' && config[1] == 'q' && config[2] == 't' && config[3] == 't' &&
config[4] >= '1' && config[4] <= ('0' + MAX_MQTT_SLOTS) && config[5] == '.') {
// Slot-based commands: get mqtt1.preset, get mqtt1.server, etc.
int slot = config[4] - '1'; // 0-5
const char* subcmd = &config[6];
if (memcmp(subcmd, "preset", 6) == 0) {
sprintf(reply, "> %s", _prefs->mqtt_slot_preset[slot]);
sprintf(reply, "> %s", _mqtt_prefs.mqtt_slot_preset[slot]);
} else if (memcmp(subcmd, "server", 6) == 0) {
sprintf(reply, "> %s", _prefs->mqtt_slot_host[slot]);
sprintf(reply, "> %s", _mqtt_prefs.mqtt_slot_host[slot]);
} else if (memcmp(subcmd, "port", 4) == 0) {
sprintf(reply, "> %d", _prefs->mqtt_slot_port[slot]);
sprintf(reply, "> %d", _mqtt_prefs.mqtt_slot_port[slot]);
} else if (memcmp(subcmd, "username", 8) == 0) {
sprintf(reply, "> %s", _prefs->mqtt_slot_username[slot]);
sprintf(reply, "> %s", _mqtt_prefs.mqtt_slot_username[slot]);
} else if (memcmp(subcmd, "password", 8) == 0) {
sprintf(reply, "> %s", _prefs->mqtt_slot_password[slot]);
sprintf(reply, "> %s", _mqtt_prefs.mqtt_slot_password[slot]);
} else if (memcmp(subcmd, "token", 5) == 0) {
if (_prefs->mqtt_slot_token[slot][0] != '\0') {
sprintf(reply, "> %s", _prefs->mqtt_slot_token[slot]);
if (_mqtt_prefs.mqtt_slot_token[slot][0] != '\0') {
sprintf(reply, "> %s", _mqtt_prefs.mqtt_slot_token[slot]);
} else {
strcpy(reply, "> (not set)");
}
} else if (memcmp(subcmd, "topic", 5) == 0) {
if (_prefs->mqtt_slot_topic[slot][0] != '\0') {
sprintf(reply, "> %s", _prefs->mqtt_slot_topic[slot]);
if (_mqtt_prefs.mqtt_slot_topic[slot][0] != '\0') {
sprintf(reply, "> %s", _mqtt_prefs.mqtt_slot_topic[slot]);
} else {
strcpy(reply, "> (default: meshcore/{iata}/{device}/{type})");
}
} else if (memcmp(subcmd, "audience", 8) == 0) {
if (_prefs->mqtt_slot_audience[slot][0] != '\0') {
sprintf(reply, "> %s", _prefs->mqtt_slot_audience[slot]);
if (_mqtt_prefs.mqtt_slot_audience[slot][0] != '\0') {
sprintf(reply, "> %s", _mqtt_prefs.mqtt_slot_audience[slot]);
} else {
strcpy(reply, "> (not set — custom slots use username/password auth)");
}
@@ -732,9 +764,9 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
sprintf(reply, "??: %s", config);
}
} else if (memcmp(config, "wifi.ssid", 9) == 0) {
sprintf(reply, "> %s", _prefs->wifi_ssid);
sprintf(reply, "> %s", _mqtt_prefs.wifi_ssid);
} else if (memcmp(config, "wifi.pwd", 8) == 0) {
sprintf(reply, "> %s", _prefs->wifi_password);
sprintf(reply, "> %s", _mqtt_prefs.wifi_password);
} else if (memcmp(config, "wifi.status", 11) == 0) {
wl_status_t status = WiFi.status();
const char* status_str;
@@ -789,56 +821,61 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
#endif
}
} else if (memcmp(config, "wifi.powersave", 14) == 0) {
uint8_t ps = _prefs->wifi_power_save;
uint8_t ps = _mqtt_prefs.wifi_power_save;
const char* ps_name = (ps == 1) ? "none" : (ps == 2) ? "max" : "min";
sprintf(reply, "> %s", ps_name);
} else if (memcmp(config, "timezone", 8) == 0) {
sprintf(reply, "> %s", _prefs->timezone_string);
sprintf(reply, "> %s", _mqtt_prefs.timezone_string);
} else if (memcmp(config, "timezone.offset", 15) == 0) {
sprintf(reply, "> %d", _prefs->timezone_offset);
sprintf(reply, "> %d", _mqtt_prefs.timezone_offset);
} else if (memcmp(config, "mqtt.analyzer.us", 17) == 0) {
sprintf(reply, "> %s", strcmp(_prefs->mqtt_slot_preset[0], "analyzer-us") == 0 ? "on" : "off");
sprintf(reply, "> %s", strcmp(_mqtt_prefs.mqtt_slot_preset[0], "analyzer-us") == 0 ? "on" : "off");
} else if (memcmp(config, "mqtt.analyzer.eu", 17) == 0) {
sprintf(reply, "> %s", strcmp(_prefs->mqtt_slot_preset[1], "analyzer-eu") == 0 ? "on" : "off");
sprintf(reply, "> %s", strcmp(_mqtt_prefs.mqtt_slot_preset[1], "analyzer-eu") == 0 ? "on" : "off");
} else if (sender_timestamp == 0 && memcmp(config, "mqtt.owner", 10) == 0) {
if (_prefs->mqtt_owner_public_key[0] != '\0') {
sprintf(reply, "> %s", _prefs->mqtt_owner_public_key);
if (_mqtt_prefs.mqtt_owner_public_key[0] != '\0') {
sprintf(reply, "> %s", _mqtt_prefs.mqtt_owner_public_key);
} else {
strcpy(reply, "> (not set)");
}
} else if (sender_timestamp == 0 && memcmp(config, "mqtt.email", 10) == 0) {
if (_prefs->mqtt_email[0] != '\0') {
sprintf(reply, "> %s", _prefs->mqtt_email);
if (_mqtt_prefs.mqtt_email[0] != '\0') {
sprintf(reply, "> %s", _mqtt_prefs.mqtt_email);
} else {
strcpy(reply, "> (not set)");
}
} else if (memcmp(config, "mqtt.config.valid", 17) == 0) {
bool valid = MQTTBridge::isConfigValid(_prefs);
bool valid = MQTTBridge::isConfigValid(&_mqtt_prefs);
sprintf(reply, "> %s", valid ? "valid" : "invalid");
#endif
} else if (memcmp(config, "alert.hashtag", 13) == 0) {
sprintf(reply, "> %s", _prefs->alert_hashtag[0] ? _prefs->alert_hashtag : "(unset)");
sprintf(reply, "> %s", _mqtt_prefs.alert_hashtag[0] ? _mqtt_prefs.alert_hashtag : "(unset)");
} else if (sender_timestamp == 0 && memcmp(config, "alert.psk", 9) == 0) { // from serial command line only
sprintf(reply, "> %s", _prefs->alert_psk_hex[0] ? _prefs->alert_psk_hex : "(unset)");
sprintf(reply, "> %s", _mqtt_prefs.alert_psk_hex[0] ? _mqtt_prefs.alert_psk_hex : "(unset)");
} else if (memcmp(config, "alert.region", 12) == 0) {
sprintf(reply, "> %s", _prefs->alert_region[0] ? _prefs->alert_region : "(unset, using default scope)");
sprintf(reply, "> %s", _mqtt_prefs.alert_region[0] ? _mqtt_prefs.alert_region : "(unset, using default scope)");
} else if (memcmp(config, "alert.wifi", 10) == 0) {
sprintf(reply, "> %u min%s", (unsigned)_prefs->alert_wifi_minutes,
_prefs->alert_wifi_minutes == 0 ? " (disabled)" : "");
sprintf(reply, "> %u min%s", (unsigned)_mqtt_prefs.alert_wifi_minutes,
_mqtt_prefs.alert_wifi_minutes == 0 ? " (disabled)" : "");
} else if (memcmp(config, "alert.mqtt", 10) == 0) {
sprintf(reply, "> %u min%s", (unsigned)_prefs->alert_mqtt_minutes,
_prefs->alert_mqtt_minutes == 0 ? " (disabled)" : "");
sprintf(reply, "> %u min%s", (unsigned)_mqtt_prefs.alert_mqtt_minutes,
_mqtt_prefs.alert_mqtt_minutes == 0 ? " (disabled)" : "");
} else if (memcmp(config, "alert.interval", 14) == 0) {
sprintf(reply, "> %u min", (unsigned)_prefs->alert_min_interval_min);
sprintf(reply, "> %u min", (unsigned)_mqtt_prefs.alert_min_interval_min);
} else if (memcmp(config, "alert", 5) == 0 && (config[5] == 0 || config[5] == '\n' || config[5] == '\r')) {
sprintf(reply, "> %s", _prefs->alert_enabled ? "on" : "off");
sprintf(reply, "> %s", _mqtt_prefs.alert_enabled ? "on" : "off");
} else {
handled = false;
}
return handled;
#else
(void)sender_timestamp; (void)config; (void)reply;
return false;
#endif
}
bool CommonCLI::handleObserverCommand(uint32_t sender_timestamp, char* command, char* reply) {
#ifdef WITH_MQTT_BRIDGE
if (memcmp(command, "tls.bundletest ", 15) == 0) {
#ifdef ESP_PLATFORM
if (WiFi.status() != WL_CONNECTED) {
@@ -946,7 +983,7 @@ bool CommonCLI::handleObserverCommand(uint32_t sender_timestamp, char* command,
} else {
strcpy(text, "[test] alert channel ok");
}
if (!_prefs->alert_psk_hex[0]) {
if (!_mqtt_prefs.alert_psk_hex[0]) {
strcpy(reply, "Error: alert channel not configured (set alert.psk or set alert.hashtag)");
} else {
bool ok = _callbacks->sendAlertText(text);
@@ -955,4 +992,8 @@ bool CommonCLI::handleObserverCommand(uint32_t sender_timestamp, char* command,
return true;
}
return false;
#else
(void)sender_timestamp; (void)command; (void)reply;
return false;
#endif
}
+7
View File
@@ -93,6 +93,13 @@ static inline void applyMQTTDefaults(MQTTPrefs* prefs) {
prefs->timezone_string[sizeof(prefs->timezone_string) - 1] = '\0';
}
prefs->timezone_offset = MQTT_DEFAULT_TIMEZONE_OFFSET;
// Observer non-MQTT defaults (moved out of NodePrefs/MyMesh ctor in Phase 2).
strncpy(prefs->snmp_community, "public", sizeof(prefs->snmp_community) - 1);
prefs->radio_watchdog_minutes = 5;
prefs->alert_wifi_minutes = 30;
prefs->alert_mqtt_minutes = 240;
prefs->alert_min_interval_min = 60;
}
#endif // WITH_MQTT_BRIDGE
+77 -76
View File
@@ -24,12 +24,12 @@
#endif
// Effective MQTT origin: empty mqtt_origin follows node_name; otherwise mqtt_origin override (quotes stripped).
static void applyEffectiveOrigin(const NodePrefs* prefs, char* dest, size_t dest_size) {
if (!prefs || !dest || dest_size == 0) return;
if (prefs->mqtt_origin[0] == '\0') {
strncpy(dest, prefs->node_name, dest_size - 1);
static void applyEffectiveOrigin(const NodePrefs* np, const MQTTPrefs* obs, char* dest, size_t dest_size) {
if (!np || !obs || !dest || dest_size == 0) return;
if (obs->mqtt_origin[0] == '\0') {
strncpy(dest, np->node_name, dest_size - 1);
} else {
strncpy(dest, prefs->mqtt_origin, dest_size - 1);
strncpy(dest, obs->mqtt_origin, dest_size - 1);
}
dest[dest_size - 1] = '\0';
StrHelper::stripSurroundingQuotes(dest, dest_size);
@@ -52,7 +52,7 @@ static bool ntpHostnameEquals(const char* a, const char* b) {
return strcasecmp(a, b) == 0;
}
static void fillNtpServerList(const NodePrefs* prefs, const char* servers[], int& count) {
static void fillNtpServerList(const MQTTPrefs* prefs, const char* servers[], int& count) {
count = 0;
if (prefs && prefs->mqtt_ntp_server[0] != '\0') {
servers[count++] = prefs->mqtt_ntp_server;
@@ -72,31 +72,31 @@ static void fillNtpServerList(const NodePrefs* prefs, const char* servers[], int
}
}
const char* MQTTBridge::effectiveNtpPrimary(const NodePrefs* prefs) {
if (prefs && prefs->mqtt_ntp_server[0] != '\0') {
return prefs->mqtt_ntp_server;
const char* MQTTBridge::effectiveNtpPrimary(const MQTTPrefs* obs) {
if (obs && obs->mqtt_ntp_server[0] != '\0') {
return obs->mqtt_ntp_server;
}
return kNtpBuiltinFallbacks[0];
}
void MQTTBridge::refreshOriginFromPrefs() {
if (!_prefs) return;
applyEffectiveOrigin(_prefs, _origin, sizeof(_origin));
applyEffectiveOrigin(_prefs, _obs, _origin, sizeof(_origin));
}
void MQTTBridge::getEffectiveMqttOrigin(const NodePrefs* prefs, char* buf, size_t buf_size) {
void MQTTBridge::getEffectiveMqttOrigin(const NodePrefs* np, const MQTTPrefs* obs, char* buf, size_t buf_size) {
if (!buf || buf_size == 0) return;
if (!prefs) {
if (!np || !obs) {
buf[0] = '\0';
return;
}
applyEffectiveOrigin(prefs, buf, buf_size);
applyEffectiveOrigin(np, obs, buf, buf_size);
}
// Helper function to check if WiFi credentials are valid
static bool isWiFiConfigValid(const NodePrefs* prefs) {
static bool isWiFiConfigValid(const MQTTPrefs* obs) {
// Check if WiFi SSID is configured (not empty)
if (strlen(prefs->wifi_ssid) == 0) {
if (!obs || strlen(obs->wifi_ssid) == 0) {
return false;
}
@@ -107,13 +107,13 @@ static bool isWiFiConfigValid(const NodePrefs* prefs) {
#ifdef WITH_MQTT_BRIDGE
bool MQTTBridge::isConfigValid(const NodePrefs* prefs) {
if (!prefs || !isWiFiConfigValid(prefs)) return false;
bool MQTTBridge::isConfigValid(const MQTTPrefs* obs) {
if (!obs || !isWiFiConfigValid(obs)) return false;
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
const char* preset_name = prefs->mqtt_slot_preset[i];
const char* preset_name = obs->mqtt_slot_preset[i];
if (preset_name[0] == '\0' || strcmp(preset_name, MQTT_PRESET_NONE) == 0) continue;
if (strcmp(preset_name, MQTT_PRESET_CUSTOM) == 0) {
if (prefs->mqtt_slot_host[i][0] != '\0' && prefs->mqtt_slot_port[i] != 0) return true;
if (obs->mqtt_slot_host[i][0] != '\0' && obs->mqtt_slot_port[i] != 0) return true;
} else if (findMQTTPreset(preset_name) != nullptr) {
return true;
}
@@ -194,9 +194,9 @@ unsigned long MQTTBridge::getWifiConnectedAtMillis() {
return s_wifi_connected_at;
}
void MQTTBridge::formatMqttStatusReply(char* buf, size_t bufsize, const NodePrefs* prefs) {
void MQTTBridge::formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPrefs* obs) {
if (buf == nullptr || bufsize == 0) return;
const char* msgs = (prefs->mqtt_status_enabled) ? "on" : "off";
const char* msgs = (obs && obs->mqtt_status_enabled) ? "on" : "off";
if (s_mqtt_bridge_instance == nullptr || !s_mqtt_bridge_instance->_initialized) {
snprintf(buf, bufsize, "> msgs: %s (bridge not running)", msgs);
return;
@@ -384,11 +384,12 @@ void MQTTBridge::formatSlotDiagReply(char* buf, size_t bufsize, int slot_index)
// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------
MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, mesh::LocalIdentity *identity)
MQTTBridge::MQTTBridge(NodePrefs *prefs, MQTTPrefs *obs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, mesh::LocalIdentity *identity)
: BridgeBase(prefs, mgr, rtc),
_obs(obs),
_queue_count(0),
_last_status_publish(0), _last_status_retry(0), _status_interval(300000),
_ntp_client(_ntp_udp, effectiveNtpPrimary(prefs), 0, 60000), _last_ntp_sync(0), _ntp_synced(false), _ntp_sync_pending(false), _slots_setup_done(false), _max_active_slots(RUNTIME_MQTT_SLOTS),
_ntp_client(_ntp_udp, effectiveNtpPrimary(obs), 0, 60000), _last_ntp_sync(0), _ntp_synced(false), _ntp_sync_pending(false), _slots_setup_done(false), _max_active_slots(RUNTIME_MQTT_SLOTS),
_ntp_force_requested(false), _ntp_force_done(false), _ntp_force_result(false),
_ntp_diag_requested(false), _ntp_diag_done(false), _ntp_diag_count(0),
// Default to UTC; setRules() will be called from syncTimeWithNTP when a
@@ -539,14 +540,14 @@ void MQTTBridge::begin() {
MQTT_DEBUG_PRINTLN("Max active slots: %d", _max_active_slots);
// Check if WiFi credentials are configured first
if (!isWiFiConfigValid(_prefs)) {
if (!isWiFiConfigValid(_obs)) {
MQTT_DEBUG_PRINTLN("MQTT Bridge initialization skipped - WiFi credentials not configured");
return;
}
refreshOriginFromPrefs();
strncpy(_iata, _prefs->mqtt_iata, sizeof(_iata) - 1);
strncpy(_iata, _obs->mqtt_iata, sizeof(_iata) - 1);
_iata[sizeof(_iata) - 1] = '\0';
StrHelper::stripSurroundingQuotes(_iata, sizeof(_iata));
@@ -557,17 +558,17 @@ void MQTTBridge::begin() {
}
// Update enabled flags from preferences
_status_enabled = _prefs->mqtt_status_enabled;
_packets_enabled = _prefs->mqtt_packets_enabled;
_raw_enabled = _prefs->mqtt_raw_enabled;
_rx_enabled = _prefs->mqtt_rx_enabled;
_tx_mode = _prefs->mqtt_tx_enabled; // 0=off, 1=all, 2=advert
_status_enabled = _obs->mqtt_status_enabled;
_packets_enabled = _obs->mqtt_packets_enabled;
_raw_enabled = _obs->mqtt_raw_enabled;
_rx_enabled = _obs->mqtt_rx_enabled;
_tx_mode = _obs->mqtt_tx_enabled; // 0=off, 1=all, 2=advert
// Set status interval to 5 minutes (300000 ms), or use preference if set and valid
if (_prefs->mqtt_status_interval >= 1000 && _prefs->mqtt_status_interval <= 3600000) {
_status_interval = _prefs->mqtt_status_interval;
if (_obs->mqtt_status_interval >= 1000 && _obs->mqtt_status_interval <= 3600000) {
_status_interval = _obs->mqtt_status_interval;
} else {
// Invalid or uninitialized value - fix it in preferences and use default
_prefs->mqtt_status_interval = 300000; // Fix the preference value
_obs->mqtt_status_interval = 300000; // Fix the preference value
_status_interval = 300000; // 5 minutes default
}
@@ -578,12 +579,12 @@ void MQTTBridge::begin() {
// Apply slot presets from preferences
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
const char* preset_name = _prefs->mqtt_slot_preset[i];
const char* preset_name = _obs->mqtt_slot_preset[i];
if (preset_name[0] != '\0' && strcmp(preset_name, MQTT_PRESET_NONE) != 0) {
if (strcmp(preset_name, MQTT_PRESET_CUSTOM) == 0) {
// Custom broker: copy host/port/username/password from prefs
_slots[i].preset = nullptr;
strncpy(_slots[i].host, _prefs->mqtt_slot_host[i], sizeof(_slots[i].host) - 1);
strncpy(_slots[i].host, _obs->mqtt_slot_host[i], sizeof(_slots[i].host) - 1);
_slots[i].host[sizeof(_slots[i].host) - 1] = '\0';
if (strlen(_slots[i].host) == 0) {
MQTT_DEBUG_PRINTLN("MQTT%d: custom preset has no server configured, disabling", i + 1);
@@ -591,12 +592,12 @@ void MQTTBridge::begin() {
continue;
}
_slots[i].enabled = true;
_slots[i].port = _prefs->mqtt_slot_port[i];
strncpy(_slots[i].username, _prefs->mqtt_slot_username[i], sizeof(_slots[i].username) - 1);
_slots[i].port = _obs->mqtt_slot_port[i];
strncpy(_slots[i].username, _obs->mqtt_slot_username[i], sizeof(_slots[i].username) - 1);
_slots[i].username[sizeof(_slots[i].username) - 1] = '\0';
strncpy(_slots[i].password, _prefs->mqtt_slot_password[i], sizeof(_slots[i].password) - 1);
strncpy(_slots[i].password, _obs->mqtt_slot_password[i], sizeof(_slots[i].password) - 1);
_slots[i].password[sizeof(_slots[i].password) - 1] = '\0';
strncpy(_slots[i].audience, _prefs->mqtt_slot_audience[i], sizeof(_slots[i].audience) - 1);
strncpy(_slots[i].audience, _obs->mqtt_slot_audience[i], sizeof(_slots[i].audience) - 1);
_slots[i].audience[sizeof(_slots[i].audience) - 1] = '\0';
} else {
const MQTTPresetDef* preset = findMQTTPreset(preset_name);
@@ -604,9 +605,9 @@ void MQTTBridge::begin() {
_slots[i].enabled = true;
_slots[i].preset = preset;
if (mqttPresetNeedsSlotCredentials(preset)) {
strncpy(_slots[i].username, _prefs->mqtt_slot_username[i], sizeof(_slots[i].username) - 1);
strncpy(_slots[i].username, _obs->mqtt_slot_username[i], sizeof(_slots[i].username) - 1);
_slots[i].username[sizeof(_slots[i].username) - 1] = '\0';
strncpy(_slots[i].password, _prefs->mqtt_slot_password[i], sizeof(_slots[i].password) - 1);
strncpy(_slots[i].password, _obs->mqtt_slot_password[i], sizeof(_slots[i].password) - 1);
_slots[i].password[sizeof(_slots[i].password) - 1] = '\0';
}
} else {
@@ -702,7 +703,7 @@ void MQTTBridge::begin() {
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.setAutoConnect(true);
WiFi.begin(_prefs->wifi_ssid, _prefs->wifi_password);
WiFi.begin(_obs->wifi_ssid, _obs->wifi_password);
// NOTE: Slot setup deferred until after NTP sync in loop()
#endif
@@ -845,7 +846,7 @@ void MQTTBridge::initializeWiFiInTask() {
// When already connected, the deferred slot setup still fires in mqttTaskLoop()
// because _ntp_synced persists across end() (only _slots_setup_done is reset).
if (WiFi.status() != WL_CONNECTED) {
WiFi.begin(_prefs->wifi_ssid, _prefs->wifi_password);
WiFi.begin(_obs->wifi_ssid, _obs->wifi_password);
} else if (!_ntp_synced && !_ntp_sync_pending) {
_ntp_sync_pending = true; // already connected but never synced — kick NTP now
}
@@ -976,8 +977,8 @@ void MQTTBridge::mqttTaskLoop() {
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_slot_reconfigure_pending[i]) {
_slot_reconfigure_pending[i] = false;
MQTT_DEBUG_PRINTLN("Applying deferred reconfigure for MQTT%d (preset: %s)", i + 1, _prefs->mqtt_slot_preset[i]);
applySlotPreset(i, _prefs->mqtt_slot_preset[i]);
MQTT_DEBUG_PRINTLN("Applying deferred reconfigure for MQTT%d (preset: %s)", i + 1, _obs->mqtt_slot_preset[i]);
applySlotPreset(i, _obs->mqtt_slot_preset[i]);
}
}
@@ -990,9 +991,9 @@ void MQTTBridge::mqttTaskLoop() {
#ifdef WITH_SNMP
// SNMP agent loop — process incoming UDP requests
if (_snmp_agent) {
if (!_snmp_agent->isRunning() && WiFi.isConnected() && _prefs->snmp_enabled) {
_snmp_agent->begin(_prefs->snmp_community);
MQTT_DEBUG_PRINTLN("SNMP agent started on port 161 (community: %s)", _prefs->snmp_community);
if (!_snmp_agent->isRunning() && WiFi.isConnected() && _obs->snmp_enabled) {
_snmp_agent->begin(_obs->snmp_community);
MQTT_DEBUG_PRINTLN("SNMP agent started on port 161 (community: %s)", _obs->snmp_community);
}
if (_snmp_agent->isRunning()) {
// Update MQTT stats from this core
@@ -1626,8 +1627,8 @@ bool MQTTBridge::createSlotAuthToken(int index) {
// Prepare owner key
const char* owner_key = nullptr;
char owner_key_uppercase[65];
if (_prefs->mqtt_owner_public_key[0] != '\0') {
strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1);
if (_obs->mqtt_owner_public_key[0] != '\0') {
strncpy(owner_key_uppercase, _obs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1);
owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0';
for (int i = 0; owner_key_uppercase[i]; i++) {
owner_key_uppercase[i] = toupper(owner_key_uppercase[i]);
@@ -1637,7 +1638,7 @@ bool MQTTBridge::createSlotAuthToken(int index) {
char client_version[64];
getClientVersion(client_version, sizeof(client_version));
const char* email = (_prefs->mqtt_email[0] != '\0') ? _prefs->mqtt_email : nullptr;
const char* email = (_obs->mqtt_email[0] != '\0') ? _obs->mqtt_email : nullptr;
unsigned long current_time = time(nullptr);
// Stagger token expiry per slot to avoid simultaneous renewal/reconnect
@@ -1712,7 +1713,7 @@ bool MQTTBridge::publishToAllSlots(const char* topic, const char* payload, bool
// ---------------------------------------------------------------------------
bool MQTTBridge::substituteTopicTemplate(const char* tmpl, MQTTMessageType type, int slot_index, char* buf, size_t buf_size) {
const char* type_str = (type == MSG_STATUS) ? "status" : (type == MSG_PACKETS) ? "packets" : "raw";
const char* token = _prefs->mqtt_slot_token[slot_index];
const char* token = _obs->mqtt_slot_token[slot_index];
size_t out = 0;
const char* p = tmpl;
@@ -1762,7 +1763,7 @@ bool MQTTBridge::buildTopicForSlot(int index, MQTTMessageType type, char* topic_
if (slot.preset->topic_style == MQTT_TOPIC_MESHRANK) {
// MeshRank: packets only, uses per-slot token in topic path
if (type != MSG_PACKETS) return false;
const char* token = _prefs->mqtt_slot_token[index];
const char* token = _obs->mqtt_slot_token[index];
if (!token || token[0] == '\0') return false;
snprintf(topic_buf, buf_size, "meshrank/uplink/%s/%s/packets", token, _device_id);
return true;
@@ -1775,8 +1776,8 @@ bool MQTTBridge::buildTopicForSlot(int index, MQTTMessageType type, char* topic_
}
// Custom slots: use topic template if set, otherwise default meshcore format
if (_prefs->mqtt_slot_topic[index][0] != '\0') {
return substituteTopicTemplate(_prefs->mqtt_slot_topic[index], type, index, topic_buf, buf_size);
if (_obs->mqtt_slot_topic[index][0] != '\0') {
return substituteTopicTemplate(_obs->mqtt_slot_topic[index], type, index, topic_buf, buf_size);
}
// Default: meshcore format
if (!isIATAValid()) return false;
@@ -1936,9 +1937,9 @@ void MQTTBridge::applySlotPreset(int slot_index, const char* preset_name) {
slot.enabled = true;
slot.preset = preset;
if (mqttPresetNeedsSlotCredentials(preset)) {
strncpy(slot.username, _prefs->mqtt_slot_username[slot_index], sizeof(slot.username) - 1);
strncpy(slot.username, _obs->mqtt_slot_username[slot_index], sizeof(slot.username) - 1);
slot.username[sizeof(slot.username) - 1] = '\0';
strncpy(slot.password, _prefs->mqtt_slot_password[slot_index], sizeof(slot.password) - 1);
strncpy(slot.password, _obs->mqtt_slot_password[slot_index], sizeof(slot.password) - 1);
slot.password[sizeof(slot.password) - 1] = '\0';
}
if (_initialized) {
@@ -1972,7 +1973,7 @@ void MQTTBridge::setSlotCustomBroker(int slot_index, const char* host, uint16_t
void MQTTBridge::checkConfigurationMismatch() {
// Warn if packets are enabled but both rx and tx are off — nothing will be published
if (_prefs->mqtt_packets_enabled && !_prefs->mqtt_rx_enabled && _prefs->mqtt_tx_enabled == 0) {
if (_obs->mqtt_packets_enabled && !_obs->mqtt_rx_enabled && _obs->mqtt_tx_enabled == 0) {
unsigned long now = millis();
if (_last_config_warning == 0 || (now - _last_config_warning > CONFIG_WARNING_INTERVAL)) {
MQTT_DEBUG_PRINTLN("MQTT: Both mqtt.rx and mqtt.tx are off — no packets will be published. Run 'set mqtt.rx on' or 'set mqtt.tx on' to fix.");
@@ -2010,7 +2011,7 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
_wifi_reconnect_backoff_attempt = 0;
#ifdef ESP_PLATFORM
wifi_ps_type_t ps_mode;
uint8_t ps_pref = _prefs->wifi_power_save;
uint8_t ps_pref = _obs->wifi_power_save;
if (ps_pref == 1) {
ps_mode = WIFI_PS_NONE;
} else if (ps_pref == 2) {
@@ -2054,7 +2055,7 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
_wifi_reconnect_backoff_attempt++;
}
WiFi.disconnect();
WiFi.begin(_prefs->wifi_ssid, _prefs->wifi_password);
WiFi.begin(_obs->wifi_ssid, _obs->wifi_password);
}
}
_last_wifi_status = current_wifi_status;
@@ -2063,7 +2064,7 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
}
bool MQTTBridge::isReady() const {
return _initialized && isWiFiConfigValid(_prefs);
return _initialized && isWiFiConfigValid(_obs);
}
bool MQTTBridge::isIATAValid() const {
@@ -2081,7 +2082,7 @@ bool MQTTBridge::isSlotReady(int index, char* reason_buf, size_t reason_size) co
if (slot.preset) {
if (slot.preset->topic_style == MQTT_TOPIC_MESHRANK) {
if (_prefs->mqtt_slot_token[index][0] == '\0') {
if (_obs->mqtt_slot_token[index][0] == '\0') {
if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.token <your_token>", index + 1);
return false;
}
@@ -2092,18 +2093,18 @@ bool MQTTBridge::isSlotReady(int index, char* reason_buf, size_t reason_size) co
}
}
if (mqttPresetNeedsSlotCredentials(slot.preset)) {
if (_prefs->mqtt_slot_username[index][0] == '\0') {
if (_obs->mqtt_slot_username[index][0] == '\0') {
if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.username <user>", index + 1);
return false;
}
if (_prefs->mqtt_slot_password[index][0] == '\0') {
if (_obs->mqtt_slot_password[index][0] == '\0') {
if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt%d.password <pass>", index + 1);
return false;
}
}
} else {
// Custom slot without a topic template uses meshcore format, needs IATA
if (_prefs->mqtt_slot_topic[index][0] == '\0' && !isIATAValid()) {
if (_obs->mqtt_slot_topic[index][0] == '\0' && !isIATAValid()) {
if (reason_buf) snprintf(reason_buf, reason_size, "set mqtt.iata <airport_code> or set mqtt%d.topic <template>", index + 1);
return false;
}
@@ -2154,7 +2155,7 @@ void MQTTBridge::loop() {
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_slot_reconfigure_pending[i]) {
_slot_reconfigure_pending[i] = false;
applySlotPreset(i, _prefs->mqtt_slot_preset[i]);
applySlotPreset(i, _obs->mqtt_slot_preset[i]);
}
}
@@ -2237,7 +2238,7 @@ void MQTTBridge::loop() {
// ---------------------------------------------------------------------------
void MQTTBridge::onPacketReceived(mesh::Packet *packet) {
if (!_initialized || !_prefs->mqtt_packets_enabled || !_prefs->mqtt_rx_enabled) return;
if (!_initialized || !_obs->mqtt_packets_enabled || !_obs->mqtt_rx_enabled) return;
// Check if we have any enabled slots to send to
bool has_valid_slots = false;
@@ -2254,8 +2255,8 @@ void MQTTBridge::onPacketReceived(mesh::Packet *packet) {
}
void MQTTBridge::sendPacket(mesh::Packet *packet) {
uint8_t tx_mode = _prefs->mqtt_tx_enabled; // Read live from prefs (no restart needed)
if (!_initialized || !_prefs->mqtt_packets_enabled || tx_mode == 0) return;
uint8_t tx_mode = _obs->mqtt_tx_enabled; // Read live from prefs (no restart needed)
if (!_initialized || !_obs->mqtt_packets_enabled || tx_mode == 0) return;
// Advert mode: only queue self-originated advert packets
if (tx_mode == 2) {
@@ -2910,7 +2911,7 @@ void MQTTBridge::refreshNTP() {
// Lightweight periodic refresh: just restart SNTP which runs async in the background.
// No blocking DNS, no UDP sockets, no retry loops on the MQTT task loop.
// The heavy syncTimeWithNTP() is only used for initial sync and WiFi reconnect recovery.
configTime(0, 0, effectiveNtpPrimary(_prefs));
configTime(0, 0, effectiveNtpPrimary(_obs));
_last_ntp_sync = millis();
MQTT_DEBUG_PRINTLN("NTP refresh triggered (async SNTP)");
}
@@ -2939,10 +2940,10 @@ bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) {
if (primary_only) {
// Validation path (e.g. set mqtt.ntp): test only the configured primary so a
// typo fails fast instead of walking the entire fallback list.
servers[0] = effectiveNtpPrimary(_prefs);
servers[0] = effectiveNtpPrimary(_obs);
server_count = 1;
} else {
fillNtpServerList(_prefs, servers, server_count);
fillNtpServerList(_obs, servers, server_count);
}
bool ntp_ok = false;
@@ -3041,15 +3042,15 @@ bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) {
// Reuses the inline _timezone_storage via setRules() instead of
// deleting/newing a Timezone, which was a per-change heap alloc pair.
static char last_timezone[64] = "";
if (strcmp(_prefs->timezone_string, last_timezone) != 0) {
if (strcmp(_obs->timezone_string, last_timezone) != 0) {
TimeChangeRule dst_rule, std_rule;
if (!timezoneRulesFromString(_prefs->timezone_string, dst_rule, std_rule)) {
if (!timezoneRulesFromString(_obs->timezone_string, dst_rule, std_rule)) {
TimeChangeRule utc = {"UTC", Last, Sun, Mar, 0, 0};
dst_rule = utc;
std_rule = utc;
}
_timezone_storage.setRules(dst_rule, std_rule);
strncpy(last_timezone, _prefs->timezone_string, sizeof(last_timezone) - 1);
strncpy(last_timezone, _obs->timezone_string, sizeof(last_timezone) - 1);
last_timezone[sizeof(last_timezone) - 1] = '\0';
}
@@ -3089,7 +3090,7 @@ bool MQTTBridge::requestForcedNtpSync(uint32_t timeout_ms) {
void MQTTBridge::runNtpDiagProbe() {
const char* servers[kMaxNtpServers];
int count = 0;
fillNtpServerList(_prefs, servers, count);
fillNtpServerList(_obs, servers, count);
_ntp_client.begin();
for (int i = 0; i < count; i++) {
+9 -5
View File
@@ -375,8 +375,12 @@ private:
void logMemoryStatus();
void refreshOriginFromPrefs();
// Observer config (MQTT/WiFi/timezone/SNMP/alert), persisted to /mqtt_prefs.
// _prefs (held by BridgeBase) still provides upstream fields (freq/sf/node_name…).
MQTTPrefs* _obs = nullptr;
public:
MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, mesh::LocalIdentity *identity);
MQTTBridge(NodePrefs *prefs, MQTTPrefs *obs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, mesh::LocalIdentity *identity);
void begin() override;
void end() override;
@@ -439,8 +443,8 @@ public:
const char* getSlotPresetName(int slot_index) const;
static int getRuntimeSlotCount() { return RUNTIME_MQTT_SLOTS; }
/** Resolved origin for MQTT JSON: node_name when mqtt_origin is empty, else mqtt_origin (with quote stripping). */
static void getEffectiveMqttOrigin(const NodePrefs* prefs, char* buf, size_t buf_size);
static const char* effectiveNtpPrimary(const NodePrefs* prefs);
static void getEffectiveMqttOrigin(const NodePrefs* np, const MQTTPrefs* obs, char* buf, size_t buf_size);
static const char* effectiveNtpPrimary(const MQTTPrefs* obs);
/** Sync system clock via NTP. force=true bypasses the 5s post-sync rate limit.
* primary_only=true tests just the effective primary server (no fallback walk) so a
* mistyped hostname fails fast instead of blocking through the whole fallback list.
@@ -459,9 +463,9 @@ public:
* summary in reply; verbose=false fills reply with a compact "<server> ok|fail" list
* (for LoRa). Returns false if the bridge is not running. */
bool ntpDiag(char* reply, size_t reply_size, bool verbose);
static void formatMqttStatusReply(char* buf, size_t bufsize, const NodePrefs* prefs);
static void formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPrefs* obs);
/** True when WiFi is set and at least one MQTT slot can run (preset + custom host if needed). */
static bool isConfigValid(const NodePrefs* prefs);
static bool isConfigValid(const MQTTPrefs* obs);
static void formatSlotDiagReply(char* buf, size_t bufsize, int slot_index);
static uint8_t getLastWifiDisconnectReason();
static unsigned long getLastWifiDisconnectTime();