@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 +18 -0
- package/README.md +1 -1
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/painlessmesh/mesh.hpp +29 -28
- package/src/painlessmesh/tcp.hpp +30 -8
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.
|
|
7
|
+
**Version 1.9.8** - Latest release with ESP32 heap corruption fix and improved TCP error handling
|
|
8
8
|
|
|
9
9
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
|
package/library.json
CHANGED
package/library.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name=Alteriom PainlessMesh
|
|
2
|
-
version=1.9.
|
|
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.
|
|
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
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
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) {
|
package/src/painlessmesh/tcp.hpp
CHANGED
|
@@ -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
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
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 -
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
});
|