@alteriom/painlessmesh 1.9.7 → 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.
package/CHANGELOG.md CHANGED
@@ -19,6 +19,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
19
 
20
20
  - TBD
21
21
 
22
+ ## [1.9.8] - 2025-12-14
23
+
24
+ ### Fixed
25
+
26
+ - **Heap Corruption on TCP Connection Errors** (#254) - Fixed ESP32 heap corruption crashes during AsyncClient deletion
27
+ - **Root Cause**: AsyncClient objects were being deleted synchronously from within their own error callback handlers, causing heap corruption and use-after-free crashes
28
+ - **Symptom**: ESP32 devices crash with "CORRUPT HEAP: Bad head at 0x4083a398. Expected 0xabba1234 got 0xfefefefe" during TCP connection error handling
29
+ - **Solution**: Deferred AsyncClient deletion using task scheduler to execute after error handler completes
30
+ - Changed from synchronous `delete client` to deferred deletion via `mesh.addTask([client]() { delete client; }, 0)`
31
+ - Deletion now occurs microseconds after error handler returns, preventing use-after-free
32
+ - Added logging for cleanup operations to aid debugging
33
+ - **Impact**: Eliminates heap corruption crashes on ESP32 during TCP connection retries and error conditions
34
+ - **Files Modified**: `src/painlessmesh/tcp.hpp` (lines 138, 149)
35
+
36
+ ### Changed
37
+
38
+ - **README Version Reference** - Updated version banner to 1.9.7 for consistency with release history
39
+
22
40
  ## [1.9.7] - 2025-12-13
23
41
 
24
42
  ### Fixed
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div align="center">
6
6
 
7
- **Version 1.9.6** - Latest release with TCP connection improvements and documentation updates
7
+ **Version 1.9.8** - Latest release with ESP32 heap corruption fix and improved TCP error handling
8
8
 
9
9
  [![CI/CD Pipeline](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml/badge.svg)](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
10
10
  [![Documentation](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml/badge.svg)](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
package/library.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/Alteriom/painlessMesh"
8
8
  },
9
- "version": "1.9.7",
9
+ "version": "1.9.8",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=Alteriom PainlessMesh
2
- version=1.9.7
2
+ version=1.9.8
3
3
  author=Coopdis,Scotty Franzyshen,Edwin van Leeuwen,Germán Martín,Maximilian Schwarz,Doanh Doanh,Alteriom
4
4
  maintainer=Alteriom
5
5
  sentence=A painless way to setup a mesh with ESP8266 and ESP32 devices with Alteriom extensions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alteriom/painlessmesh",
3
- "version": "1.9.7",
3
+ "version": "1.9.8",
4
4
  "description": "painlessMesh is a user-friendly library for creating mesh networks with ESP8266 and ESP32 devices. This Alteriom fork includes additional packages for sensor data (SensorPackage), device commands (CommandPackage), and status monitoring (StatusPackage). It handles routing and network management automatically, so you can focus on your application. The library uses JSON-based messaging and syncs time across all nodes, making it ideal for coordinated behaviour like synchronized light displays or sensor networks reporting to a central node.",
5
5
  "keywords": [
6
6
  "arduino",
@@ -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) {
@@ -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,24 +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
164
  // Defer callback execution to avoid crashes in error handler context
146
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
147
168
  mesh.addTask([&mesh]() {
169
+ Log(CONNECTION, "tcp_err(): Executing delayed WiFi reconnection after retry exhaustion\n");
148
170
  mesh.droppedConnectionCallbacks.execute(0, true);
149
- });
171
+ }, TCP_EXHAUSTION_RECONNECT_DELAY_MS);
150
172
  mesh.semaphoreGive();
151
173
  }
152
174
  });