From 2eb41baedaad6375e33231a8a1ac5981986bcbef Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 26 Jun 2026 08:59:48 -0700 Subject: [PATCH] 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. --- examples/simple_repeater/MyMesh.cpp | 45 ++--- examples/simple_repeater/MyMesh.h | 16 +- examples/simple_room_server/MyMesh.cpp | 33 ++- examples/simple_room_server/MyMesh.h | 12 +- src/Dispatcher.cpp | 10 +- src/Dispatcher.h | 4 +- src/helpers/AlertReporter.cpp | 25 ++- src/helpers/AlertReporter.h | 5 +- src/helpers/CommonCLI.cpp | 257 +++--------------------- src/helpers/CommonCLI.h | 87 +++----- src/helpers/CommonCLI_Observer.cpp | 265 ++++++++++++++----------- src/helpers/MQTTDefaults.h | 7 + src/helpers/bridges/MQTTBridge.cpp | 153 +++++++------- src/helpers/bridges/MQTTBridge.h | 14 +- 14 files changed, 372 insertions(+), 561 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 833c318f..3fd7ec00 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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 diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 9ef04ac8..853e8a34 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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 diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index de5507da..892008d1 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -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 } diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 72e5ff96..96186f0a 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -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 { diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index edb1ee9a..8bd8504a 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -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()) { diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 6ee20087..dc309968 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -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(); diff --git a/src/helpers/AlertReporter.cpp b/src/helpers/AlertReporter.cpp index df38d6fc..793a8d79 100644 --- a/src/helpers/AlertReporter.cpp +++ b/src/helpers/AlertReporter.cpp @@ -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) diff --git a/src/helpers/AlertReporter.h b/src/helpers/AlertReporter.h index dbbd154f..390262b6 100644 --- a/src/helpers/AlertReporter.h +++ b/src/helpers/AlertReporter.h @@ -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 diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index f4f900fa..c243c3f7 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 48096320..fb3e1a78 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -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 `, 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(&_mqtt_prefs); } +#endif }; diff --git a/src/helpers/CommonCLI_Observer.cpp b/src/helpers/CommonCLI_Observer.cpp index ad143373..413e92b4 100644 --- a/src/helpers/CommonCLI_Observer.cpp +++ b/src/helpers/CommonCLI_Observer.cpp @@ -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 ' 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 ' 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 ' and 'set mqtt%d.password ' 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 } diff --git a/src/helpers/MQTTDefaults.h b/src/helpers/MQTTDefaults.h index cf58b997..f3c117eb 100644 --- a/src/helpers/MQTTDefaults.h +++ b/src/helpers/MQTTDefaults.h @@ -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 diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 9dcf5561..20858790 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -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 ", 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 ", 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 ", 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 or set mqtt%d.topic