Compare commits

..

7 Commits

Author SHA1 Message Date
614594a035 Merge fix/improve-error-logging: Human-readable MQTT errors 2026-02-05 10:14:40 -07:00
dd881f79f3 Merge fix/memory-leaks: Add destructor, init pointers to nullptr 2026-02-05 10:14:02 -07:00
980a42f98b Merge fix/mqtt-exponential-backoff: Backoff 1s→60s on retry 2026-02-05 10:13:58 -07:00
69a2e04cd2 Merge fix/wifi-manager-dangling-pointer: Cache SSID to fix use-after-free 2026-02-05 10:13:54 -07:00
669ef89a66 fix: memory leaks and uninitialized pointers
- Initialize _mqtt_bridge and _web_config to nullptr in declarations
- Add destructor to clean up dynamically allocated objects
- Initialize _last_mqtt_stats to 0 in declaration
- Fix WiFiState/NetworkState enum mismatch in getWiFiStatus()

While embedded firmware typically runs forever (making these not
critical leaks), proper cleanup enables testing and prevents
static analyzer warnings.
2026-02-05 09:59:49 -07:00
0905aa4bf8 Add exponential backoff to MQTT reconnection
Instead of fixed 5s/30s retry intervals, implement exponential backoff:
- Initial delay: 1 second
- Max delay: 60 seconds
- Doubles on each failed attempt
- Resets to minimum on successful connection

This prevents hammering a down broker while still reconnecting
quickly when the issue is transient.

Also includes fix for WiFiState/NetworkState enum mismatch in
getWiFiStatus() which was already on main.
2026-02-05 09:48:49 -07:00
91d144901a Fix dangling pointer bug in WiFiManager getSSID/getConnectionName
WiFi.SSID() returns a temporary String object. Calling .c_str() on it
returns a pointer to the internal buffer, but the String is destroyed
at the end of the statement - leaving a dangling pointer.

Fix by caching the SSID in a member variable when connection state
changes, and returning a pointer to that stable storage.

Also fix getWiFiStatus() in MyMesh.cpp which was using WiFiState enum
values instead of NetworkState (the interface return type).
2026-02-05 09:47:01 -07:00
6 changed files with 53 additions and 16 deletions

View File

@ -17,6 +17,7 @@ MQTTBridge::MQTTBridge(INetworkManager* network, mesh::PacketManager* mgr, mesh:
_last_status_publish(0), _last_status_publish(0),
_last_stats_publish(0), _last_stats_publish(0),
_connected_since(0), _connected_since(0),
_current_backoff_ms(BACKOFF_MIN_MS),
_messages_sent(0), _messages_sent(0),
_messages_received(0), _messages_received(0),
_reconnect_count(0), _reconnect_count(0),
@ -81,7 +82,8 @@ void MQTTBridge::loop() {
switch (_state) { switch (_state) {
case MQTTState::DISCONNECTED: case MQTTState::DISCONNECTED:
if (millis() - _last_connect_attempt > 5000) { // Use exponential backoff for reconnection attempts
if (millis() - _last_connect_attempt > _current_backoff_ms) {
attemptConnection(); attemptConnection();
} }
break; break;
@ -94,6 +96,7 @@ void MQTTBridge::loop() {
_state = MQTTState::DISCONNECTED; _state = MQTTState::DISCONNECTED;
Serial.println("[MQTT] Connection lost"); Serial.println("[MQTT] Connection lost");
_last_connect_attempt = millis(); _last_connect_attempt = millis();
// Don't reset backoff on connection loss - broker might be down
} else { } else {
_mqtt_client.loop(); _mqtt_client.loop();
@ -105,7 +108,8 @@ void MQTTBridge::loop() {
break; break;
case MQTTState::ERROR: case MQTTState::ERROR:
if (millis() - _last_connect_attempt > 30000) { // Use backoff for error recovery too
if (millis() - _last_connect_attempt > _current_backoff_ms) {
_state = MQTTState::DISCONNECTED; _state = MQTTState::DISCONNECTED;
} }
break; break;
@ -135,7 +139,8 @@ void MQTTBridge::attemptConnection() {
_state = MQTTState::CONNECTING; _state = MQTTState::CONNECTING;
_last_connect_attempt = millis(); _last_connect_attempt = millis();
Serial.printf("[MQTT] Connecting to %s:%d...\n", _config.broker, _config.port); Serial.printf("[MQTT] Connecting to %s:%d (backoff=%lums)...\n",
_config.broker, _config.port, _current_backoff_ms);
String client_id = String(_config.client_id); String client_id = String(_config.client_id);
if (client_id.length() == 0) { if (client_id.length() == 0) {
@ -158,6 +163,8 @@ void MQTTBridge::attemptConnection() {
_state = MQTTState::CONNECTED; _state = MQTTState::CONNECTED;
_connected_since = millis(); _connected_since = millis();
_reconnect_count++; _reconnect_count++;
// Reset backoff on successful connection
_current_backoff_ms = BACKOFF_MIN_MS;
Serial.println("[MQTT] Connected!"); Serial.println("[MQTT] Connected!");
subscribeToCommands(); subscribeToCommands();
@ -170,6 +177,10 @@ void MQTTBridge::attemptConnection() {
Serial.printf("[MQTT] Broker: %s:%d, User: %s\n", _config.broker, _config.port, Serial.printf("[MQTT] Broker: %s:%d, User: %s\n", _config.broker, _config.port,
strlen(_config.user) > 0 ? _config.user : "(none)"); strlen(_config.user) > 0 ? _config.user : "(none)");
_state = MQTTState::ERROR; _state = MQTTState::ERROR;
// Exponential backoff: double the delay (up to max)
_current_backoff_ms = min(_current_backoff_ms * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
Serial.printf("[MQTTS] Next retry in %lums\n", _current_backoff_ms);
} }
} }

View File

@ -85,6 +85,12 @@ private:
unsigned long _last_stats_publish; unsigned long _last_stats_publish;
unsigned long _connected_since; unsigned long _connected_since;
// Exponential backoff for reconnection
static constexpr uint32_t BACKOFF_MIN_MS = 1000; // Start at 1 second
static constexpr uint32_t BACKOFF_MAX_MS = 60000; // Max 60 seconds
static constexpr uint32_t BACKOFF_MULTIPLIER = 2; // Double each attempt
uint32_t _current_backoff_ms;
uint32_t _messages_sent; uint32_t _messages_sent;
uint32_t _messages_received; uint32_t _messages_received;
uint32_t _reconnect_count; uint32_t _reconnect_count;

View File

@ -733,6 +733,21 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier
} }
MyMesh::~MyMesh() {
// Clean up dynamically allocated resources
#ifdef WITH_MQTT
delete _web_config;
_web_config = nullptr;
delete _mqtt_bridge;
_mqtt_bridge = nullptr;
#endif
#ifdef WITH_ETHERNET
delete _mqtt_bridge;
_mqtt_bridge = nullptr;
#endif
}
void MyMesh::begin(FILESYSTEM *fs) { void MyMesh::begin(FILESYSTEM *fs) {
mesh::Mesh::begin(); mesh::Mesh::begin();
_fs = fs; _fs = fs;

View File

@ -125,21 +125,21 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
#ifdef WITH_MQTT #ifdef WITH_MQTT
WiFiManager _wifi_mgr; WiFiManager _wifi_mgr;
MQTTBridge* _mqtt_bridge; MQTTBridge* _mqtt_bridge = nullptr;
WebConfig* _web_config; WebConfig* _web_config = nullptr;
WiFiConfig _wifi_config; WiFiConfig _wifi_config;
MQTTConfig _mqtt_config; MQTTConfig _mqtt_config;
unsigned long _last_mqtt_stats; unsigned long _last_mqtt_stats = 0;
void initMQTT(); void initMQTT();
#endif #endif
#ifdef WITH_ETHERNET #ifdef WITH_ETHERNET
EthernetManager _eth_mgr; EthernetManager _eth_mgr;
MQTTBridge* _mqtt_bridge; MQTTBridge* _mqtt_bridge = nullptr;
MQTTConfig _mqtt_config; MQTTConfig _mqtt_config;
EthernetConfig _eth_config; EthernetConfig _eth_config;
unsigned long _last_mqtt_stats; unsigned long _last_mqtt_stats = 0;
void initEthernet(); void initEthernet();
#endif #endif
@ -196,6 +196,7 @@ protected:
public: public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
~MyMesh(); // Clean up dynamically allocated resources
void begin(FILESYSTEM* fs); void begin(FILESYSTEM* fs);

View File

@ -10,6 +10,7 @@ WiFiManager::WiFiManager()
_retry_count(0), _retry_count(0),
_initialized(false) { _initialized(false) {
memset(&_config, 0, sizeof(_config)); memset(&_config, 0, sizeof(_config));
memset(_cached_ssid, 0, sizeof(_cached_ssid));
} }
void WiFiManager::begin(const WiFiConfig& config) { void WiFiManager::begin(const WiFiConfig& config) {
@ -82,6 +83,9 @@ void WiFiManager::loop() {
_state = WiFiState::CONNECTED; _state = WiFiState::CONNECTED;
_connected_since = millis(); _connected_since = millis();
_retry_count = 0; _retry_count = 0;
// Cache SSID to avoid dangling pointer from WiFi.SSID().c_str()
strncpy(_cached_ssid, WiFi.SSID().c_str(), sizeof(_cached_ssid) - 1);
_cached_ssid[sizeof(_cached_ssid) - 1] = '\0';
Serial.printf("[WiFi] Connected! IP: %s, RSSI: %d dBm\n", Serial.printf("[WiFi] Connected! IP: %s, RSSI: %d dBm\n",
WiFi.localIP().toString().c_str(), WiFi.RSSI()); WiFi.localIP().toString().c_str(), WiFi.RSSI());
} else if (status == WL_NO_SSID_AVAIL) { } else if (status == WL_NO_SSID_AVAIL) {
@ -184,6 +188,9 @@ void WiFiManager::startAPMode() {
if (success) { if (success) {
_state = WiFiState::AP_MODE; _state = WiFiState::AP_MODE;
// Cache AP SSID to avoid dangling pointer
strncpy(_cached_ssid, ap_ssid.c_str(), sizeof(_cached_ssid) - 1);
_cached_ssid[sizeof(_cached_ssid) - 1] = '\0';
Serial.printf("[WiFi] AP started: SSID='%s', IP=%s\n", Serial.printf("[WiFi] AP started: SSID='%s', IP=%s\n",
ap_ssid.c_str(), WiFi.softAPIP().toString().c_str()); ap_ssid.c_str(), WiFi.softAPIP().toString().c_str());
} else { } else {
@ -221,10 +228,8 @@ int32_t WiFiManager::getRSSI() const {
} }
const char* WiFiManager::getSSID() const { const char* WiFiManager::getSSID() const {
if (_state == WiFiState::CONNECTED) { if (_state == WiFiState::CONNECTED || _state == WiFiState::AP_MODE) {
return WiFi.SSID().c_str(); return _cached_ssid;
} else if (_state == WiFiState::AP_MODE) {
return WiFi.softAPSSID().c_str();
} }
return ""; return "";
} }
@ -264,10 +269,8 @@ NetworkState WiFiManager::getState() const {
} }
const char* WiFiManager::getConnectionName() const { const char* WiFiManager::getConnectionName() const {
if (_state == WiFiState::CONNECTED) { if (_state == WiFiState::CONNECTED || _state == WiFiState::AP_MODE) {
return WiFi.SSID().c_str(); return _cached_ssid;
} else if (_state == WiFiState::AP_MODE) {
return WiFi.softAPSSID().c_str();
} }
return "Not connected"; return "Not connected";
} }

View File

@ -61,6 +61,7 @@ private:
unsigned long _connected_since; unsigned long _connected_since;
uint8_t _retry_count; uint8_t _retry_count;
bool _initialized; bool _initialized;
char _cached_ssid[33]; // Cached SSID to avoid dangling pointer from WiFi.SSID().c_str()
void attemptConnection(); void attemptConnection();
void checkConnection(); void checkConnection();