@alteriom/painlessmesh 1.9.6 → 1.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,8 +5,8 @@
5
5
  * @file painlessMesh.h
6
6
  * @brief Main header file for Alteriom painlessMesh library
7
7
  *
8
- * @version 1.8.13
9
- * @date 2025-11-20
8
+ * @version 1.9.7
9
+ * @date 2025-12-13
10
10
  *
11
11
  * painlessMesh is a user-friendly library for creating mesh networks with
12
12
  * ESP8266 and ESP32 devices. This Alteriom fork includes additional packages
@@ -233,10 +233,19 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
233
233
  Log(CONNECTION,
234
234
  "connectToAP(): Restarting AP from channel %d to channel %d\n",
235
235
  oldChannel, detectedChannel);
236
- WiFi.softAPdisconnect(false);
237
- delay(100);
236
+
237
+ // Disconnect AP and allow WiFi stack to fully reset
238
+ // Using true parameter ensures DHCP server is properly stopped
239
+ WiFi.softAPdisconnect(true);
240
+ delay(200); // Increased delay to ensure complete WiFi stack reset
241
+
238
242
  // Call apInit via friend class access (StationScan is friend of wifi::Mesh)
239
243
  mesh->apInit(mesh->getNodeId());
244
+
245
+ // Additional stabilization delay after AP restart
246
+ // This ensures DHCP server is fully initialized before clients connect
247
+ delay(100);
248
+
240
249
  Log(CONNECTION, "connectToAP(): AP restarted on channel %d\n", detectedChannel);
241
250
  }
242
251
  // Reset counter only when mesh is found on a new channel
@@ -622,10 +622,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
622
622
  *
623
623
  * Use hasLocalInternet() to check if THIS specific node has direct Internet access.
624
624
  *
625
- * When connected to mesh, requires recent bridge status (within timeout).
626
- * When disconnected from mesh, returns true if any bridge previously
627
- * reported Internet connectivity - allowing operations to be queued for
628
- * when mesh connectivity is restored.
625
+ * This method now ALWAYS requires healthy (recent) bridge status to prevent
626
+ * false positives when mesh connectivity is lost. Without active mesh connections,
627
+ * stale bridge data cannot be relied upon, as the bridge may have lost Internet
628
+ * connectivity or become unreachable.
629
629
  *
630
630
  * \code
631
631
  * if (mesh.hasInternetConnection()) {
@@ -644,16 +644,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
644
644
  * @see initAsSharedGateway() to give all nodes direct Internet access (requires router credentials)
645
645
  */
646
646
  bool hasInternetConnection() {
647
- bool hasConnections = hasActiveMeshConnections();
648
-
647
+ // Always require healthy bridge status to prevent false positives
648
+ // when mesh is disconnected. Stale bridge data is unreliable.
649
649
  for (const auto& bridge : knownBridges) {
650
- // When connected: require fresh status
651
- // When disconnected: use last known state
652
- bool isUsable = hasConnections
653
- ? (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected)
654
- : bridge.internetConnected;
655
-
656
- if (isUsable) {
650
+ if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
657
651
  return true;
658
652
  }
659
653
  }
@@ -698,14 +692,17 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
698
692
  * Get the primary (best) bridge node
699
693
  *
700
694
  * Primary bridge is selected based on:
701
- * 1. Must be healthy (seen within timeout) - unless node is disconnected
695
+ * 1. Must be healthy (seen within timeout)
702
696
  * 2. Must have Internet connection
703
697
  * 3. Best WiFi RSSI to router
704
698
  *
705
- * When this node is disconnected from the mesh (no active connections),
706
- * returns the last known bridge even if lastSeen is stale. This allows
707
- * nodes to attempt routing through the last known bridge once mesh
708
- * connectivity is restored.
699
+ * This method now ALWAYS requires healthy (recent) bridge status to prevent
700
+ * routing messages to unreachable or outdated bridges when mesh connectivity
701
+ * is lost. Without active mesh connections and fresh status, we cannot reliably
702
+ * route messages to any bridge.
703
+ *
704
+ * If you need access to the last known bridge regardless of health status,
705
+ * use getLastKnownBridge() instead.
709
706
  *
710
707
  * @return pointer to BridgeInfo of primary bridge, or nullptr if no suitable bridge
711
708
  */
@@ -713,17 +710,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
713
710
  BridgeInfo* primary = nullptr;
714
711
  int8_t bestRSSI = -127; // Worst possible RSSI
715
712
 
716
- // Check if we have active mesh connections
717
- bool hasConnections = hasActiveMeshConnections();
718
-
713
+ // Always require healthy bridge status to prevent routing to stale/unreachable bridges
719
714
  for (auto& bridge : knownBridges) {
720
- // When connected to mesh: require healthy (recent) bridge status
721
- // When disconnected: use any bridge that reported Internet (stale info is better than none)
722
- bool isUsable = hasConnections
723
- ? (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected)
724
- : bridge.internetConnected;
725
-
726
- if (isUsable) {
715
+ if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
727
716
  if (bridge.routerRSSI > bestRSSI) {
728
717
  bestRSSI = bridge.routerRSSI;
729
718
  primary = &bridge;
@@ -1290,6 +1279,18 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1290
1279
  Log(COMMUNICATION, "sendToInternet(): Local Internet available, using gateway protocol for consistency\n");
1291
1280
  }
1292
1281
 
1282
+ // Validate mesh connectivity before attempting to send
1283
+ if (!hasActiveMeshConnections()) {
1284
+ Log(ERROR, "sendToInternet(): No active mesh connections\n");
1285
+ if (callback) {
1286
+ // Schedule callback to avoid blocking
1287
+ this->addTask([callback]() {
1288
+ callback(false, 0, "No mesh connections - cannot route to gateway");
1289
+ });
1290
+ }
1291
+ return 0;
1292
+ }
1293
+
1293
1294
  // Find the best gateway to route through
1294
1295
  BridgeInfo* gateway = getPrimaryBridge();
1295
1296
  if (gateway == nullptr) {
@@ -3134,7 +3135,11 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
3134
3135
  using namespace logger;
3135
3136
  Log(CONNECTION, "eraseClosedConnections():\n");
3136
3137
  this->subs.remove_if(
3137
- [](const std::shared_ptr<T> &conn) { return !conn->connected(); });
3138
+ [](const std::shared_ptr<T> &conn) {
3139
+ // Null check for safety - should never happen but prevents crashes
3140
+ if (!conn) return true;
3141
+ return !conn->connected();
3142
+ });
3138
3143
  }
3139
3144
 
3140
3145
  public: // Windows MSVC: TCP lambdas need access to droppedConnectionCallbacks
@@ -20,6 +20,10 @@ namespace tcp {
20
20
  static const uint8_t TCP_CONNECT_MAX_RETRIES = 5; // Max retry attempts before giving up
21
21
  static const uint32_t TCP_CONNECT_RETRY_DELAY_MS = 1000; // Delay between retry attempts (1 second)
22
22
  static const uint32_t TCP_CONNECT_STABILIZATION_DELAY_MS = 500; // Delay after IP acquisition (500ms)
23
+ // Delay before WiFi reconnection after all TCP retries are exhausted
24
+ // This prevents rapid reconnection loops when TCP server is persistently unavailable
25
+ // Gives the TCP server more time to recover and reduces network congestion
26
+ static const uint32_t TCP_EXHAUSTION_RECONNECT_DELAY_MS = 10000; // 10 seconds before reconnection
23
27
 
24
28
  inline uint32_t encodeNodeId(const uint8_t *hwaddr) {
25
29
  using namespace painlessmesh::logger;
@@ -129,20 +133,42 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
129
133
  connect<T, M>((*pRetryConn), ip, port, mesh, retryCount + 1);
130
134
  }, retryDelay);
131
135
 
132
- // Delete the current failed client to prevent memory leak
133
- // The AsyncClient is no longer needed after connection failure
134
- delete client;
136
+ // Defer deletion of the failed AsyncClient to prevent heap corruption
137
+ // Deleting from within the error callback can cause use-after-free issues
138
+ // as the AsyncTCP library may still be referencing the object
139
+ // Note: client is captured by value (pointer copy) and we are the sole owner
140
+ mesh.addTask([client]() {
141
+ Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (retry path)\n");
142
+ delete client;
143
+ }, 0);
135
144
 
136
145
  mesh.semaphoreGive();
137
146
  return;
138
147
  }
139
148
 
140
- // All retries exhausted - clean up the failed client and trigger full reconnection
141
- Log(CONNECTION, "tcp_err(): All %d retries exhausted, triggering WiFi reconnection\n",
142
- TCP_CONNECT_MAX_RETRIES + 1);
143
- delete client;
149
+ // All retries exhausted - schedule delayed reconnection
150
+ // Adding a significant delay before reconnection prevents rapid reconnection loops
151
+ // when the TCP server is persistently unavailable or overloaded
152
+ Log(CONNECTION, "tcp_err(): All %d retries exhausted, scheduling WiFi reconnection in %u ms\n",
153
+ TCP_CONNECT_MAX_RETRIES + 1, TCP_EXHAUSTION_RECONNECT_DELAY_MS);
154
+
155
+ // Defer deletion of the failed AsyncClient to prevent heap corruption
156
+ // Deleting from within the error callback can cause use-after-free issues
157
+ // as the AsyncTCP library may still be referencing the object
158
+ // Note: client is captured by value (pointer copy) and we are the sole owner
159
+ mesh.addTask([client]() {
160
+ Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (exhaustion path)\n");
161
+ delete client;
162
+ }, 0);
144
163
  #endif
145
- mesh.droppedConnectionCallbacks.execute(0, true);
164
+ // Defer callback execution to avoid crashes in error handler context
165
+ // Execute callbacks after semaphore is released and error handler completes
166
+ // The delay helps prevent endless rapid reconnection loops by giving the TCP server
167
+ // more time to recover and reducing network congestion from multiple retrying nodes
168
+ mesh.addTask([&mesh]() {
169
+ Log(CONNECTION, "tcp_err(): Executing delayed WiFi reconnection after retry exhaustion\n");
170
+ mesh.droppedConnectionCallbacks.execute(0, true);
171
+ }, TCP_EXHAUSTION_RECONNECT_DELAY_MS);
146
172
  mesh.semaphoreGive();
147
173
  }
148
174
  });