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