From 9e25f41cddd424bb7c27a572153cb1b89eb08e35 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:19:52 -0600 Subject: [PATCH 01/14] mqtt: queue packet snapshots instead of Packet pointers --- src/helpers/bridges/MQTTBridge.cpp | 82 ++++++++++++++---------------- src/helpers/bridges/MQTTBridge.h | 3 +- 2 files changed, 41 insertions(+), 44 deletions(-) diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 287bc5f0b6..059ef385e9 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -479,13 +479,10 @@ void MQTTBridge::end() { psram_free(_mqtt_task_stack); _mqtt_task_stack = nullptr; - // Clean up queued packets from FreeRTOS queue - // NOTE: Do NOT free queued.packet - the Dispatcher owns those packets. - // We just discard our references to them. + // Clean up queued packet snapshots from FreeRTOS queue. if (_packet_queue_handle != nullptr) { QueuedPacket queued; while (xQueueReceive(_packet_queue_handle, &queued, 0) == pdTRUE) { - queued.packet = nullptr; _queue_count--; } vQueueDelete(_packet_queue_handle); @@ -522,12 +519,9 @@ void MQTTBridge::end() { _analyzer_eu_client = nullptr; } - // Clean up queued packet references - // NOTE: Do NOT free the packets - the Dispatcher owns those packets. - // We just discard our references to them. + // Clean up queued packet snapshots for (int i = 0; i < _queue_count; i++) { int index = (_queue_head + i) % MAX_QUEUE_SIZE; - _packet_queue[index].packet = nullptr; memset(&_packet_queue[index], 0, sizeof(QueuedPacket)); } @@ -1370,22 +1364,21 @@ void MQTTBridge::processPacketQueue() { break; // No more packets } - // Publish packet (use stored raw data if available) - publishPacket(queued.packet, queued.is_tx, - queued.has_raw_data ? queued.raw_data : nullptr, - queued.has_raw_data ? queued.raw_len : 0, - queued.has_raw_data ? queued.snr : 0.0f, - queued.has_raw_data ? queued.rssi : 0.0f); - - // Publish raw if enabled - if (_raw_enabled) { - publishRaw(queued.packet); + mesh::Packet snapshot; + if (queued.packet_len > 0 && snapshot.readFrom(queued.packet_data, queued.packet_len)) { + // Publish packet (use stored raw data if available) + publishPacket(&snapshot, queued.is_tx, + queued.has_raw_data ? queued.raw_data : nullptr, + queued.has_raw_data ? queued.raw_len : 0, + queued.has_raw_data ? queued.snr : 0.0f, + queued.has_raw_data ? queued.rssi : 0.0f); + + // Publish raw if enabled + if (_raw_enabled) { + publishRaw(&snapshot); + } } - // NOTE: Do NOT free the packet here - the Dispatcher owns and frees it after logRx() returns. - // The MQTT bridge only stores a pointer to read from; it does not own the packet. - queued.packet = nullptr; - _queue_count--; processed++; @@ -1426,19 +1419,19 @@ void MQTTBridge::processPacketQueue() { QueuedPacket& queued = _packet_queue[_queue_head]; - publishPacket(queued.packet, queued.is_tx, - queued.has_raw_data ? queued.raw_data : nullptr, - queued.has_raw_data ? queued.raw_len : 0, - queued.has_raw_data ? queued.snr : 0.0f, - queued.has_raw_data ? queued.rssi : 0.0f); - - if (_raw_enabled) { - publishRaw(queued.packet); + mesh::Packet snapshot; + if (queued.packet_len > 0 && snapshot.readFrom(queued.packet_data, queued.packet_len)) { + publishPacket(&snapshot, queued.is_tx, + queued.has_raw_data ? queued.raw_data : nullptr, + queued.has_raw_data ? queued.raw_len : 0, + queued.has_raw_data ? queued.snr : 0.0f, + queued.has_raw_data ? queued.rssi : 0.0f); + + if (_raw_enabled) { + publishRaw(&snapshot); + } } - // NOTE: Do NOT free the packet here - the Dispatcher owns and frees it after logRx() returns. - queued.packet = nullptr; - dequeuePacket(); processed++; } @@ -1933,6 +1926,9 @@ void MQTTBridge::publishRaw(mesh::Packet* packet) { } void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) { + if (!packet) { + return; + } #ifdef ESP_PLATFORM // Use FreeRTOS queue for thread-safe operation if (_packet_queue_handle == nullptr) { @@ -1942,7 +1938,10 @@ void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) { QueuedPacket queued; memset(&queued, 0, sizeof(QueuedPacket)); - queued.packet = packet; + queued.packet_len = packet->writeTo(queued.packet_data); + if (queued.packet_len == 0 || queued.packet_len > sizeof(queued.packet_data)) { + return; + } queued.timestamp = millis(); queued.is_tx = is_tx; queued.has_raw_data = false; @@ -1971,9 +1970,7 @@ void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) { // Queue full - try to remove oldest packet QueuedPacket oldest; if (xQueueReceive(_packet_queue_handle, &oldest, 0) == pdTRUE) { - // NOTE: Do NOT free oldest.packet - the Dispatcher owns and frees it. - // We just drop our reference to it. - MQTT_DEBUG_PRINTLN("Queue full, dropping oldest packet reference"); + MQTT_DEBUG_PRINTLN("Queue full, dropping oldest packet snapshot"); // Now try to send again if (xQueueSend(_packet_queue_handle, &queued, 0) != pdTRUE) { MQTT_DEBUG_PRINTLN("Failed to queue packet after dropping oldest"); @@ -1992,17 +1989,17 @@ void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) { // Non-ESP32: Use circular buffer if (_queue_count >= MAX_QUEUE_SIZE) { QueuedPacket& oldest = _packet_queue[_queue_head]; - // NOTE: Do NOT free oldest.packet - the Dispatcher owns and frees it. - // We just drop our reference to it. - MQTT_DEBUG_PRINTLN("Queue full, dropping oldest packet reference (queue size: %d)", _queue_count); - oldest.packet = nullptr; + MQTT_DEBUG_PRINTLN("Queue full, dropping oldest packet snapshot (queue size: %d)", _queue_count); dequeuePacket(); } QueuedPacket& queued = _packet_queue[_queue_tail]; memset(&queued, 0, sizeof(QueuedPacket)); - queued.packet = packet; + queued.packet_len = packet->writeTo(queued.packet_data); + if (queued.packet_len == 0 || queued.packet_len > sizeof(queued.packet_data)) { + return; + } queued.timestamp = millis(); queued.is_tx = is_tx; queued.has_raw_data = false; @@ -3173,4 +3170,3 @@ void MQTTBridge::logMemoryStatus() { } #endif - diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index 0afc2217b3..067eecfb9f 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -92,7 +92,8 @@ class MQTTBridge : public BridgeBase { // Packet queue for offline scenarios struct QueuedPacket { - mesh::Packet* packet; + uint8_t packet_data[MAX_TRANS_UNIT + 1]; + uint16_t packet_len; unsigned long timestamp; bool is_tx; // Store raw radio data with each packet to avoid it being overwritten From 7b8fb7cf6a91652ab665fccd84e0d645547ce245 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:20:53 -0600 Subject: [PATCH 02/14] mqtt: make begin/end lifecycle restart-safe on ESP --- src/helpers/bridges/MQTTBridge.cpp | 57 ++++++++++++++++++++++++------ src/helpers/bridges/MQTTBridge.h | 2 ++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 059ef385e9..656e20339b 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -137,16 +137,19 @@ MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCCloc _ntp_client(_ntp_udp, "pool.ntp.org", 0, 60000), _last_ntp_sync(0), _ntp_synced(false), _ntp_sync_pending(false), _timezone(nullptr), _last_raw_len(0), _last_snr(0), _last_rssi(0), _last_raw_timestamp(0), _analyzer_us_enabled(false), _analyzer_eu_enabled(false), _identity(identity), + _token_us_expires_at(0), _token_eu_expires_at(0), _analyzer_us_client(nullptr), _analyzer_eu_client(nullptr), _config_valid(false), _cached_has_brokers(false), _cached_has_analyzer_servers(false), _last_memory_check(0), _skipped_publishes(0), _last_fragmentation_recovery(0), _fragmentation_pressure_since(0), _last_critical_check_run(0), + _last_token_renewal_attempt_us(0), _last_token_renewal_attempt_eu(0), + _last_reconnect_attempt_us(0), _last_reconnect_attempt_eu(0), _last_no_broker_log(0), _last_config_warning(0), _dispatcher(nullptr), _radio(nullptr), _board(nullptr), _ms(nullptr), _last_wifi_check(0), _last_wifi_status(WL_DISCONNECTED), _wifi_status_initialized(false), _wifi_disconnected_time(0), _last_wifi_reconnect_attempt(0), _wifi_reconnect_backoff_attempt(0), _main_broker_reconnect_backoff_attempt(0), _analyzer_us_reconnect_backoff_attempt(0), _analyzer_eu_reconnect_backoff_attempt(0) #ifdef ESP_PLATFORM - , _packet_queue_handle(nullptr), _mqtt_task_handle(nullptr), _raw_data_mutex(nullptr), _mqtt_task_stack(nullptr), _packet_queue_storage(nullptr) + , _packet_queue_handle(nullptr), _mqtt_task_handle(nullptr), _raw_data_mutex(nullptr), _wifi_event_id(0), _wifi_event_registered(false), _mqtt_task_stack(nullptr), _packet_queue_storage(nullptr) #else , _queue_head(0), _queue_tail(0) #endif @@ -200,14 +203,9 @@ MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCCloc _last_analyzer_us_log = 0; _last_analyzer_eu_log = 0; - // JWT token buffers: allocate in PSRAM when available (plan §2) - _auth_token_us = (char*)psram_malloc(AUTH_TOKEN_SIZE); - _auth_token_eu = (char*)psram_malloc(AUTH_TOKEN_SIZE); - if (_auth_token_us) _auth_token_us[0] = '\0'; - if (_auth_token_eu) _auth_token_eu[0] = '\0'; - - // Raw radio buffer in PSRAM when available (plan §6) - _last_raw_data = (uint8_t*)psram_malloc(LAST_RAW_DATA_SIZE); + _auth_token_us = nullptr; + _auth_token_eu = nullptr; + _last_raw_data = nullptr; // Set default broker configuration setBrokerDefaults(); @@ -253,6 +251,26 @@ void MQTTBridge::begin() { MQTT_DEBUG_PRINTLN("MQTT Bridge initialization skipped - WiFi credentials not configured"); return; } + + // Allocate long-lived buffers on each begin() so restartBridge() is safe. + if (_auth_token_us == nullptr) { + _auth_token_us = (char*)psram_malloc(AUTH_TOKEN_SIZE); + } + if (_auth_token_eu == nullptr) { + _auth_token_eu = (char*)psram_malloc(AUTH_TOKEN_SIZE); + } + if (_last_raw_data == nullptr) { + _last_raw_data = (uint8_t*)psram_malloc(LAST_RAW_DATA_SIZE); + } + if (_auth_token_us) _auth_token_us[0] = '\0'; + if (_auth_token_eu) _auth_token_eu[0] = '\0'; + _last_raw_len = 0; + _token_us_expires_at = 0; + _token_eu_expires_at = 0; + _last_token_renewal_attempt_us = 0; + _last_token_renewal_attempt_eu = 0; + _last_reconnect_attempt_us = 0; + _last_reconnect_attempt_eu = 0; // Validate custom MQTT broker configuration (optional) _config_valid = isMQTTConfigValid(); @@ -330,6 +348,8 @@ void MQTTBridge::begin() { MQTT_DEBUG_PRINTLN("Failed to create raw data mutex!"); vQueueDelete(_packet_queue_handle); _packet_queue_handle = nullptr; + psram_free(_packet_queue_storage); + _packet_queue_storage = nullptr; return; } @@ -475,6 +495,10 @@ void MQTTBridge::end() { // Give task time to clean up vTaskDelay(pdMS_TO_TICKS(100)); } + if (_wifi_event_registered) { + WiFi.removeEvent(_wifi_event_id); + _wifi_event_registered = false; + } // Free PSRAM task stack (plan §3) psram_free(_mqtt_task_stack); _mqtt_task_stack = nullptr; @@ -496,6 +520,18 @@ void MQTTBridge::end() { vSemaphoreDelete(_raw_data_mutex); _raw_data_mutex = nullptr; } + + // Disconnect analyzer clients + if (_analyzer_us_client) { + _analyzer_us_client->disconnect(); + delete _analyzer_us_client; + _analyzer_us_client = nullptr; + } + if (_analyzer_eu_client) { + _analyzer_eu_client->disconnect(); + delete _analyzer_eu_client; + _analyzer_eu_client = nullptr; + } #else // Disconnect from all brokers (main client only exists when _config_valid) if (_mqtt_client) { @@ -576,7 +612,7 @@ void MQTTBridge::initializeWiFiInTask() { WiFi.setAutoConnect(true); // Set up WiFi event handlers for better diagnostics and immediate disconnection detection - WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { + _wifi_event_id = WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { switch(event) { case ARDUINO_EVENT_WIFI_STA_GOT_IP: MQTT_DEBUG_PRINTLN("WiFi connected: %s", IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str()); @@ -589,6 +625,7 @@ void MQTTBridge::initializeWiFiInTask() { break; } }); + _wifi_event_registered = true; WiFi.begin(_prefs->wifi_ssid, _prefs->wifi_password); diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index 067eecfb9f..18790faafc 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -111,6 +111,8 @@ class MQTTBridge : public BridgeBase { QueueHandle_t _packet_queue_handle; TaskHandle_t _mqtt_task_handle; SemaphoreHandle_t _raw_data_mutex; // Mutex for raw radio data + wifi_event_id_t _wifi_event_id; + bool _wifi_event_registered; // PSRAM-backed task stack (plan §3); TCB kept in internal RAM StackType_t* _mqtt_task_stack; // nullptr if using dynamic task creation StaticTask_t _mqtt_task_tcb; From 01f817eb1538128e6744cf4269be9ee61cf86ee8 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:21:13 -0600 Subject: [PATCH 03/14] jwt: use static JSON documents to avoid heap churn --- src/helpers/JWTHelper.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/helpers/JWTHelper.cpp b/src/helpers/JWTHelper.cpp index dc4154ba30..b3b4ed9a66 100644 --- a/src/helpers/JWTHelper.cpp +++ b/src/helpers/JWTHelper.cpp @@ -138,7 +138,7 @@ size_t JWTHelper::base64UrlEncode(const uint8_t* input, size_t inputLen, char* o size_t JWTHelper::createHeader(char* output, size_t outputSize) { // Create JWT header: {"alg":"Ed25519","typ":"JWT"} - DynamicJsonDocument doc(256); + StaticJsonDocument<128> doc; doc["alg"] = "Ed25519"; doc["typ"] = "JWT"; @@ -163,7 +163,7 @@ size_t JWTHelper::createPayload( const char* email ) { // Create JWT payload - DynamicJsonDocument doc(512); + StaticJsonDocument<384> doc; doc["publicKey"] = publicKey; doc["aud"] = audience; doc["iat"] = issuedAt; @@ -195,4 +195,3 @@ size_t JWTHelper::createPayload( return base64UrlEncode((uint8_t*)jsonBuffer, len, output, outputSize); } - From 4b1a6a55d291180ed5f54359a09fb8eb4d6554e6 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:21:44 -0600 Subject: [PATCH 04/14] mqtt: drop publish when JSON buffer alloc fails to protect stack --- src/helpers/bridges/MQTTBridge.cpp | 32 ++++++++++-------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 656e20339b..954159c2af 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -1727,18 +1727,12 @@ void MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx, // JSON buffer: prefer PSRAM to reduce stack (plan §4); fallback to stack if allocation fails static const size_t PUBLISH_JSON_BUFFER_SIZE = 2048; char* json_buffer_psram = (char*)psram_malloc(PUBLISH_JSON_BUFFER_SIZE); - char json_buffer_stack[1024]; - char json_buffer_large_stack[2048]; - int packet_size = packet->getRawLength(); - char* active_buffer; - size_t active_buffer_size; - if (json_buffer_psram != nullptr) { - active_buffer = json_buffer_psram; - active_buffer_size = PUBLISH_JSON_BUFFER_SIZE; - } else { - active_buffer = (packet_size > 200) ? json_buffer_large_stack : json_buffer_stack; - active_buffer_size = (packet_size > 200) ? 2048 : 1024; + if (json_buffer_psram == nullptr) { + _skipped_publishes++; + return; } + char* active_buffer = json_buffer_psram; + size_t active_buffer_size = PUBLISH_JSON_BUFFER_SIZE; char origin_id[65]; // Use actual device ID @@ -1869,18 +1863,12 @@ void MQTTBridge::publishRaw(mesh::Packet* packet) { // JSON buffer: prefer PSRAM (plan §4); fallback to stack if allocation fails char* json_buffer_psram = (char*)psram_malloc(2048); - char json_buffer_stack[1024]; - char json_buffer_large_stack[2048]; - int packet_size = packet->getRawLength(); - char* active_buffer; - size_t active_buffer_size; - if (json_buffer_psram != nullptr) { - active_buffer = json_buffer_psram; - active_buffer_size = 2048; - } else { - active_buffer = (packet_size > 200) ? json_buffer_large_stack : json_buffer_stack; - active_buffer_size = (packet_size > 200) ? 2048 : 1024; + if (json_buffer_psram == nullptr) { + _skipped_publishes++; + return; } + char* active_buffer = json_buffer_psram; + size_t active_buffer_size = 2048; char origin_id[65]; // Use actual device ID From b2f477ba51e15ba7476cc2217e859e77566db478 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:22:08 -0600 Subject: [PATCH 05/14] cli: fix timezone.offset parsing/order and non-ESP memory command --- src/helpers/CommonCLI.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 82b4317043..8a0b27061c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -488,9 +488,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch strcpy(reply, "ERR: clock cannot go backwards"); } } else if (memcmp(command, "memory", 6) == 0) { +#ifdef ESP_PLATFORM sprintf(reply, "Free: %d, Min: %d, Max: %d, Queue: %d", ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap(), _callbacks->getQueueSize()); +#else + sprintf(reply, "Queue: %d", _callbacks->getQueueSize()); +#endif } else if (memcmp(command, "start ota", 9) == 0) { if (!_board->startOTAUpdate(_prefs->node_name, reply)) { strcpy(reply, "Error"); @@ -712,10 +716,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch uint8_t ps = _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); } else if (memcmp(config, "timezone.offset", 15) == 0) { sprintf(reply, "> %d", _prefs->timezone_offset); + } else if (memcmp(config, "timezone", 8) == 0) { + sprintf(reply, "> %s", _prefs->timezone_string); } else if (memcmp(config, "mqtt.analyzer.us", 17) == 0) { sprintf(reply, "> %s", _prefs->mqtt_analyzer_us_enabled ? "on" : "off"); } else if (memcmp(config, "mqtt.analyzer.eu", 17) == 0) { @@ -1093,9 +1097,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "timezone.offset ", 16) == 0) { - int8_t offset = _atoi(&config[16]); + int offset = atoi(&config[16]); if (offset >= -12 && offset <= 14) { - _prefs->timezone_offset = offset; + _prefs->timezone_offset = (int8_t)offset; savePrefs(); strcpy(reply, "OK"); } else { From 6e371e6c0df002fde8eb1226f2bf43402a23010d Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:22:33 -0600 Subject: [PATCH 06/14] mqtt: preserve stored origin and analyzer settings at boot --- examples/simple_repeater/MyMesh.cpp | 11 ----------- examples/simple_room_server/MyMesh.cpp | 11 ----------- 2 files changed, 22 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 71e1a8f332..2e48a435b6 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -934,17 +934,6 @@ void MyMesh::begin(FILESYSTEM *fs) { // load persisted prefs _cli.loadPrefs(_fs); - // Ensure analyzer servers are enabled by default (in case no prefs were loaded) - if (_prefs.mqtt_analyzer_us_enabled == 0 && _prefs.mqtt_analyzer_eu_enabled == 0) { - _prefs.mqtt_analyzer_us_enabled = 1; // enabled - _prefs.mqtt_analyzer_eu_enabled = 1; // enabled - MESH_DEBUG_PRINTLN("Setting analyzer servers to enabled by default"); - } - - // Set MQTT origin to actual device name (not build-time ADVERT_NAME) - StrHelper::strncpy(_prefs.mqtt_origin, _prefs.node_name, sizeof(_prefs.mqtt_origin)); - MESH_DEBUG_PRINTLN("MQTT origin set to device name: %s", _prefs.mqtt_origin); - acl.load(_fs, self_id); // TODO: key_store.begin(); region_map.load(_fs); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 1d12a65926..b3d7e2e96a 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -706,17 +706,6 @@ void MyMesh::begin(FILESYSTEM *fs) { applyGpsPrefs(); #endif #ifdef WITH_MQTT_BRIDGE - // Ensure analyzer servers are enabled by default (in case no prefs were loaded) - same as repeater - if (_prefs.mqtt_analyzer_us_enabled == 0 && _prefs.mqtt_analyzer_eu_enabled == 0) { - _prefs.mqtt_analyzer_us_enabled = 1; // enabled - _prefs.mqtt_analyzer_eu_enabled = 1; // enabled - MESH_DEBUG_PRINTLN("Setting analyzer servers to enabled by default"); - } - - // Set MQTT origin to actual device name (not build-time ADVERT_NAME) - same as repeater - StrHelper::strncpy(_prefs.mqtt_origin, _prefs.node_name, sizeof(_prefs.mqtt_origin)); - MESH_DEBUG_PRINTLN("MQTT origin set to device name: %s", _prefs.mqtt_origin); - if (_prefs.bridge_enabled) { // Set device public key for MQTT topics (same as repeater) char device_id[65]; From a9baf8c177276bdd8a99a8ece556a862994f2a9d Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:22:53 -0600 Subject: [PATCH 07/14] mqtt: harden WiFi config checks and broker string bounds --- src/helpers/bridges/MQTTBridge.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 954159c2af..8df2e9fa11 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -38,6 +38,10 @@ static bool isWiFiConfigValid(const NodePrefs* prefs) { if (strlen(prefs->wifi_ssid) == 0) { return false; } + // Reject placeholder values used in defaults/examples + if (strcmp(prefs->wifi_ssid, "ssid_here") == 0 || strcmp(prefs->wifi_ssid, "ssid") == 0) { + return false; + } // WiFi password can be empty for open networks, so we don't check it @@ -176,15 +180,18 @@ MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCCloc // Override with build flags if defined #ifdef MQTT_SERVER strncpy(_prefs->mqtt_server, MQTT_SERVER, sizeof(_prefs->mqtt_server) - 1); + _prefs->mqtt_server[sizeof(_prefs->mqtt_server) - 1] = '\0'; #endif #ifdef MQTT_PORT _prefs->mqtt_port = MQTT_PORT; #endif #ifdef MQTT_USERNAME strncpy(_prefs->mqtt_username, MQTT_USERNAME, sizeof(_prefs->mqtt_username) - 1); + _prefs->mqtt_username[sizeof(_prefs->mqtt_username) - 1] = '\0'; #endif #ifdef MQTT_PASSWORD strncpy(_prefs->mqtt_password, MQTT_PASSWORD, sizeof(_prefs->mqtt_password) - 1); + _prefs->mqtt_password[sizeof(_prefs->mqtt_password) - 1] = '\0'; #endif // Initialize packet queue (FreeRTOS queue will be created in begin()) @@ -2089,10 +2096,16 @@ void MQTTBridge::setBroker(int broker_index, const char* host, uint16_t port, if (broker_index < 0 || broker_index >= MAX_MQTT_BROKERS_COUNT) return; MQTTBroker& broker = _brokers[broker_index]; + if (host == nullptr) host = ""; + if (username == nullptr) username = ""; + if (password == nullptr) password = ""; strncpy(broker.host, host, sizeof(broker.host) - 1); + broker.host[sizeof(broker.host) - 1] = '\0'; broker.port = port; strncpy(broker.username, username, sizeof(broker.username) - 1); + broker.username[sizeof(broker.username) - 1] = '\0'; strncpy(broker.password, password, sizeof(broker.password) - 1); + broker.password[sizeof(broker.password) - 1] = '\0'; broker.enabled = enabled; broker.connected = false; broker.reconnect_interval = 5000; From f6af2ef7e798912b6679ae4c4f081a0935ff97e1 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:34:30 -0600 Subject: [PATCH 08/14] cli: validate timezone.offset input strictly --- src/helpers/CommonCLI.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 8a0b27061c..425e3f1839 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1097,9 +1097,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "timezone.offset ", 16) == 0) { - int offset = atoi(&config[16]); - if (offset >= -12 && offset <= 14) { - _prefs->timezone_offset = (int8_t)offset; + const char* offset_str = &config[16]; + char* endptr = nullptr; + long parsed = strtol(offset_str, &endptr, 10); + if (endptr == offset_str || (endptr != nullptr && *endptr != '\0')) { + strcpy(reply, "Error: timezone offset must be an integer"); + } else if (parsed >= -12 && parsed <= 14) { + _prefs->timezone_offset = (int8_t)parsed; savePrefs(); strcpy(reply, "OK"); } else { From 1bb396e4a03cc0f7bcf45c9d693da9200b099217 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:35:45 -0600 Subject: [PATCH 09/14] mqtt: shrink queued packet metadata footprint --- src/helpers/bridges/MQTTBridge.cpp | 2 -- src/helpers/bridges/MQTTBridge.h | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 8df2e9fa11..c5106635e8 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -1974,7 +1974,6 @@ void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) { if (queued.packet_len == 0 || queued.packet_len > sizeof(queued.packet_data)) { return; } - queued.timestamp = millis(); queued.is_tx = is_tx; queued.has_raw_data = false; @@ -2032,7 +2031,6 @@ void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) { if (queued.packet_len == 0 || queued.packet_len > sizeof(queued.packet_data)) { return; } - queued.timestamp = millis(); queued.is_tx = is_tx; queued.has_raw_data = false; diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index 18790faafc..d7cf58f12e 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -93,12 +93,11 @@ class MQTTBridge : public BridgeBase { // Packet queue for offline scenarios struct QueuedPacket { uint8_t packet_data[MAX_TRANS_UNIT + 1]; - uint16_t packet_len; - unsigned long timestamp; + uint8_t packet_len; bool is_tx; // Store raw radio data with each packet to avoid it being overwritten uint8_t raw_data[256]; - int raw_len; + uint16_t raw_len; float snr; float rssi; bool has_raw_data; From fe84b2b7540d1b80b837eb5fc0a637e09ae80669 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:36:21 -0600 Subject: [PATCH 10/14] mqtt: default origin to node name when unset --- src/helpers/CommonCLI.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 425e3f1839..2152bfea50 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -65,6 +65,13 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) { loadMQTTPrefs(fs); // Sync MQTT prefs to NodePrefs so existing code (like MQTTBridge) can access them syncMQTTPrefsToNodePrefs(); + // Fresh-install fallback: default mqtt.origin to node_name when mqtt_prefs has no origin yet. + // This preserves explicit user-configured mqtt.origin values. + if (_prefs->mqtt_origin[0] == '\0' && _prefs->node_name[0] != '\0') { + StrHelper::strncpy(_prefs->mqtt_origin, _prefs->node_name, sizeof(_prefs->mqtt_origin)); + syncNodePrefsToMQTTPrefs(); + saveMQTTPrefs(fs); + } // For MQTT bridge, migrate bridge.source to RX (logRx) only on fresh installs or upgrades // This ensures new users get the correct default, but respects existing user choices From 8fc3587262200b6335ec7e7f2065b10280d755a9 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 21:36:26 -0600 Subject: [PATCH 11/14] mqtt: align publish buffer comments with behavior --- src/helpers/bridges/MQTTBridge.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index c5106635e8..467a133be4 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -1731,7 +1731,8 @@ void MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx, } #endif - // JSON buffer: prefer PSRAM to reduce stack (plan §4); fallback to stack if allocation fails + // JSON buffer: use PSRAM to avoid large stack allocations on constrained targets. + // If allocation fails, skip publish to preserve system stability. static const size_t PUBLISH_JSON_BUFFER_SIZE = 2048; char* json_buffer_psram = (char*)psram_malloc(PUBLISH_JSON_BUFFER_SIZE); if (json_buffer_psram == nullptr) { @@ -1868,7 +1869,8 @@ void MQTTBridge::publishRaw(mesh::Packet* packet) { return; } - // JSON buffer: prefer PSRAM (plan §4); fallback to stack if allocation fails + // JSON buffer: use PSRAM to avoid large stack allocations on constrained targets. + // If allocation fails, skip publish to preserve system stability. char* json_buffer_psram = (char*)psram_malloc(2048); if (json_buffer_psram == nullptr) { _skipped_publishes++; From a5b73cdd4fec070723d3ecbe43b90b633d306e0a Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 22:30:54 -0600 Subject: [PATCH 12/14] jwt: fail token build when JSON document overflows --- src/helpers/JWTHelper.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/JWTHelper.cpp b/src/helpers/JWTHelper.cpp index b3b4ed9a66..02678f5a61 100644 --- a/src/helpers/JWTHelper.cpp +++ b/src/helpers/JWTHelper.cpp @@ -141,6 +141,9 @@ size_t JWTHelper::createHeader(char* output, size_t outputSize) { StaticJsonDocument<128> doc; doc["alg"] = "Ed25519"; doc["typ"] = "JWT"; + if (doc.overflowed()) { + return 0; + } char jsonBuffer[256]; size_t len = serializeJson(doc, jsonBuffer, sizeof(jsonBuffer)); @@ -186,6 +189,9 @@ size_t JWTHelper::createPayload( if (email && strlen(email) > 0) { doc["email"] = email; } + if (doc.overflowed()) { + return 0; + } char jsonBuffer[512]; size_t len = serializeJson(doc, jsonBuffer, sizeof(jsonBuffer)); From cff9d497b89343f554db13d03185086467c2246d Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 22:31:07 -0600 Subject: [PATCH 13/14] cli: check mqtt prefs write result --- src/helpers/CommonCLI.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2152bfea50..39498ebd92 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -390,7 +390,11 @@ void CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) { File file = fs->open("/mqtt_prefs", "w", true); #endif if (file) { - file.write((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); + size_t bytes_written = file.write((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); + if (bytes_written != sizeof(_mqtt_prefs)) { + MESH_DEBUG_PRINTLN("Failed to write /mqtt_prefs completely (wrote %u/%u bytes)", + (unsigned)bytes_written, (unsigned)sizeof(_mqtt_prefs)); + } file.close(); } } From df903665f26c701affa8b46008336d418515a1ca Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 3 Mar 2026 22:34:28 -0600 Subject: [PATCH 14/14] jwt: increase payload JSON doc capacity to 512 --- src/helpers/JWTHelper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/JWTHelper.cpp b/src/helpers/JWTHelper.cpp index 02678f5a61..7284afa2da 100644 --- a/src/helpers/JWTHelper.cpp +++ b/src/helpers/JWTHelper.cpp @@ -166,7 +166,7 @@ size_t JWTHelper::createPayload( const char* email ) { // Create JWT payload - StaticJsonDocument<384> doc; + StaticJsonDocument<512> doc; doc["publicKey"] = publicKey; doc["aud"] = audience; doc["iat"] = issuedAt;