@alteriom/painlessmesh 1.9.9 → 1.9.11
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 +80 -0
- package/README.md +2 -2
- package/docs/troubleshooting/common-issues.md +5 -4
- package/examples/bridge_failover/README.md +2 -0
- package/examples/bridge_failover/bridge_failover.ino +10 -3
- package/examples/sendToInternet/sendToInternet.ino +3 -3
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +2 -2
- package/src/AlteriomPainlessMesh.h +2 -2
- package/src/arduino/wifi.hpp +34 -40
- package/src/painlessMesh.h +2 -2
- package/src/painlessmesh/connection.hpp +102 -30
- package/src/painlessmesh/mesh.hpp +9 -0
- package/src/painlessmesh/plugin.hpp +14 -2
- package/src/painlessmesh/tcp.hpp +6 -18
package/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
19
|
|
|
20
20
|
- TBD
|
|
21
21
|
|
|
22
|
+
## [1.9.11] - 2025-12-18
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Hard Reset on Bridge Promotion - Unsafe addTask After stop/reinit** - Fixed ESP32/ESP8266 hard resets (Guru Meditation Error: Load access fault) immediately after bridge promotion in both isolated and election winner paths
|
|
27
|
+
- **Root Cause**: Calling `addTask()` immediately after `stop()/initAsBridge()` cycle accessed unstable internal task scheduling structures before they were fully reinitialized in the new context
|
|
28
|
+
- **Symptom**: Device crashes with "Load access fault" at MTVAL 0xbaad59d4 (freed memory marker) immediately after "🎯 PROMOTED TO BRIDGE" message when node becomes bridge through either isolated promotion or election
|
|
29
|
+
- **Solution**: Removed redundant task scheduling calls that were unsafe after stop/reinit
|
|
30
|
+
- Removed `addTask()` call in `attemptIsolatedBridgePromotion()` (line 1947)
|
|
31
|
+
- Removed `addTask()` call in `promoteToBridge()` (line 1822)
|
|
32
|
+
- Relied on existing `initBridgeStatusBroadcast()` infrastructure which safely handles announcements
|
|
33
|
+
- Used explicit `TSTRING` construction in callback invocations for string lifetime safety
|
|
34
|
+
- **Why Safe**: The removed tasks were redundant - `initBridgeStatusBroadcast()` (called by `initAsBridge()`) already sends immediate and periodic bridge status broadcasts (lines 1277-1280), and election winner path sends initial takeover announcement before stop/reinit (line 1771)
|
|
35
|
+
- **Testing**: All test suites pass (1000+ assertions), including bridge election and promotion tests
|
|
36
|
+
- **Documentation**: Added ISSUE_HARD_RESET_BRIDGE_PROMOTION_FIX.md with detailed analysis of both affected code paths
|
|
37
|
+
- **Impact**: Eliminates critical crash during bridge promotion, allows stable bridge failover operation in both isolated and competitive election scenarios
|
|
38
|
+
|
|
39
|
+
- **Bridge Failover & sendToInternet Retry Connectivity** - Fixed heap corruption and request timeouts when using bridge_failover with sendToInternet() during connection instability
|
|
40
|
+
- **Root Cause**: `retryInternetRequest()` did not check mesh connectivity before attempting retry, causing routing attempts through unreachable gateways during bridge disconnection
|
|
41
|
+
- **Symptom**: Nodes experience timeouts, heap corruption ("CORRUPT HEAP: Bad head at 0x40831da0"), and system instability during bridge failover cycles when messages are queued via sendToInternet()
|
|
42
|
+
- **Solution**: Added `hasActiveMeshConnections()` check at start of `retryInternetRequest()`
|
|
43
|
+
- Retry only proceeds if mesh connections are active
|
|
44
|
+
- If disconnected, reschedules retry instead of attempting to route
|
|
45
|
+
- Prevents routing to unreachable gateways during temporary disconnection
|
|
46
|
+
- Maintains existing retry logic and exponential backoff
|
|
47
|
+
- **Testing**: Added comprehensive test coverage (catch_sendtointernet_retry_no_mesh.cpp) with 31 assertions validating disconnected retry scenarios
|
|
48
|
+
- **Documentation**: Added BRIDGE_FAILOVER_RETRY_FIX.md with detailed analysis and usage notes
|
|
49
|
+
- **Impact**: Fixes critical stability issue during bridge failover, enabling reliable sendToInternet() usage in production deployments with unstable connections
|
|
50
|
+
|
|
51
|
+
- **Hard Reset During sendToInternet - Serialized AsyncClient Deletion** - Fixed ESP32/ESP8266 hard resets caused by heap corruption when multiple AsyncClient cleanup operations execute concurrently
|
|
52
|
+
- **Root Cause**: When multiple connections fail in rapid succession (e.g., during sendToInternet operations, mesh topology changes, or bridge failover), all AsyncClient deletions were scheduled with the same 1000ms delay, causing them to execute concurrently. The AsyncTCP library's internal cleanup routines cannot handle concurrent operations, leading to heap corruption.
|
|
53
|
+
- **Symptom**: Device crashes with "CORRUPT HEAP: Bad head at 0x40831da0. Expected 0xabba1234 got 0x4081faa4" even with 1000ms cleanup delay. Error occurs when multiple "Deferred cleanup of AsyncClient" messages appear nearly simultaneously.
|
|
54
|
+
- **Solution**: Implemented serialized deletion with 250ms spacing between consecutive AsyncClient deletions
|
|
55
|
+
- Added `TCP_CLIENT_DELETION_SPACING_MS` constant (250ms) to ensure deletions don't overlap
|
|
56
|
+
- Added global `lastScheduledDeletionTime` tracker to coordinate deletion timing
|
|
57
|
+
- Implemented `scheduleAsyncClientDeletion()` function that calculates proper spacing
|
|
58
|
+
- Updated `BufferedConnection` destructor and tcp.hpp error handlers to use centralized scheduler
|
|
59
|
+
- Ensures each AsyncClient deletion completes before the next one starts
|
|
60
|
+
- Handles millis() rollover and multiple concurrent deletion requests
|
|
61
|
+
- **Performance Impact**:
|
|
62
|
+
- Single deletion: No change (1000ms)
|
|
63
|
+
- Multiple concurrent deletions: Spaced by 250ms each (total spread <1 second for typical scenarios)
|
|
64
|
+
- High-churn scenario (10 failures): Spread over ~3 seconds (still acceptable)
|
|
65
|
+
- **Testing**: All test suites pass (1000+ assertions), including new deletion spacing tests (47 assertions in tcp_retry)
|
|
66
|
+
- **Documentation**: Added ISSUE_HARD_RESET_SENDTOINTERNET_SERIALIZED_DELETION_FIX.md with detailed analysis
|
|
67
|
+
- **Impact**: Fixes critical stability issue in production deployments with high connection churn, particularly affecting sendToInternet and bridge failover scenarios
|
|
68
|
+
|
|
69
|
+
## [1.9.10] - 2025-12-15
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- **TCP Connection Retry Immediate Execution** - Fixed mesh connection failures where TCP retries executed immediately instead of with exponential backoff delays
|
|
74
|
+
- **Root Cause**: `PackageHandler::addTask()` was calling `task->enable()` instead of `task->enableDelayed()` for one-shot delayed tasks, causing immediate execution
|
|
75
|
+
- **Symptom**: Nodes unable to establish mesh connection - TCP error -14 (ERR_CONN) with all retry attempts happening immediately rather than with 1s, 2s, 4s, 8s, 8s delays
|
|
76
|
+
- **Solution**: Modified `PackageHandler::addTask()` to use `enableDelayed()` for `TASK_ONCE` tasks with intervals > 0
|
|
77
|
+
- Retry tasks now properly wait for their scheduled delays before executing
|
|
78
|
+
- Exponential backoff mechanism now works as designed (total ~23s before WiFi reconnection)
|
|
79
|
+
- Reduces network congestion from multiple simultaneously retrying nodes
|
|
80
|
+
- **Impact**: Enables successful mesh connection establishment with proper retry timing
|
|
81
|
+
- **Files Modified**:
|
|
82
|
+
- `src/painlessmesh/plugin.hpp` (lines 231, 239, 245-251)
|
|
83
|
+
- `docs/troubleshooting/common-issues.md` (lines 206-212)
|
|
84
|
+
- `test/catch/catch_delayed_task_execution.cpp` (new test file)
|
|
85
|
+
- `ISSUE_TCP_RETRY_FIX.md` (new documentation)
|
|
86
|
+
|
|
87
|
+
- **Hard Reset on Bridge Failover Election Winner** - Fixed ESP32 hard reset (Guru Meditation Error: Load access fault) when node becomes bridge after election
|
|
88
|
+
- **Root Cause**: The `bridgeRoleChangedCallback` signature used pass-by-value for the String parameter (`TSTRING reason`), causing temporary String object creation from const char* literals. On memory-constrained ESP32/ESP8266, this could trigger heap allocation failures and memory access faults
|
|
89
|
+
- **Symptom**: Device crashes with "Guru Meditation Error: Core 0 panic'ed (Load access fault)" immediately after logging "🎯 PROMOTED TO BRIDGE: Election winner - best router signal"
|
|
90
|
+
- **Solution**: Changed callback signature to use const reference (`const TSTRING& reason`) instead of pass-by-value
|
|
91
|
+
- Eliminates unnecessary String object copying and temporary creation
|
|
92
|
+
- Reduces memory pressure during callback invocation
|
|
93
|
+
- String literals are now directly bound to const references without heap allocation
|
|
94
|
+
- **Impact**: Eliminates hard resets during bridge promotion, allows stable failover operation
|
|
95
|
+
- **Breaking Change**: Users must update their callback function signatures from `void callback(bool, String)` to `void callback(bool, const String&)`
|
|
96
|
+
- **Files Modified**:
|
|
97
|
+
- `src/arduino/wifi.hpp` (lines 970, 2326)
|
|
98
|
+
- `examples/bridge_failover/bridge_failover.ino` (line 147)
|
|
99
|
+
- `README.md` (line 171)
|
|
100
|
+
- `USER_GUIDE.md` (line 731)
|
|
101
|
+
|
|
22
102
|
## [1.9.9] - 2025-12-14
|
|
23
103
|
|
|
24
104
|
### 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.11** - Latest release with critical stability fixes for bridge promotion and AsyncClient cleanup
|
|
8
8
|
|
|
9
9
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
|
|
@@ -168,7 +168,7 @@ mesh.setRouterCredentials(ROUTER_SSID, ROUTER_PASSWORD);
|
|
|
168
168
|
mesh.enableBridgeFailover(true);
|
|
169
169
|
mesh.onBridgeRoleChanged(&bridgeRoleCallback);
|
|
170
170
|
|
|
171
|
-
void bridgeRoleCallback(bool isBridge, String reason) {
|
|
171
|
+
void bridgeRoleCallback(bool isBridge, const String& reason) {
|
|
172
172
|
if (isBridge) {
|
|
173
173
|
Serial.printf("🎯 Promoted to bridge: %s\n", reason.c_str());
|
|
174
174
|
}
|
|
@@ -205,11 +205,12 @@ For Arduino IDE, install manually from: https://github.com/ESP32Async/AsyncTCP
|
|
|
205
205
|
|
|
206
206
|
#### 2. Built-in Retry Mechanism
|
|
207
207
|
painlessMesh now includes automatic TCP connection retry with the following behavior:
|
|
208
|
-
- Up to
|
|
209
|
-
-
|
|
210
|
-
-
|
|
208
|
+
- Up to 6 total connection attempts (initial + 5 retries)
|
|
209
|
+
- Exponential backoff delays between retries: 1s, 2s, 4s, 8s, 8s (total ~23s)
|
|
210
|
+
- 500ms stabilization delay after IP acquisition before first connection attempt
|
|
211
|
+
- Full WiFi reconnection with 10s delay only triggered after all retries are exhausted
|
|
211
212
|
|
|
212
|
-
This helps handle transient timing issues automatically.
|
|
213
|
+
This exponential backoff helps handle transient timing issues automatically while reducing network congestion from multiple retrying nodes.
|
|
213
214
|
|
|
214
215
|
#### 3. Check Node Resource Usage
|
|
215
216
|
Monitor memory and ensure nodes aren't overloaded:
|
|
@@ -11,6 +11,8 @@ Regular mesh nodes do NOT have direct IP routing to the Internet. They only comm
|
|
|
11
11
|
**To send data to the Internet from a regular mesh node, you must:**
|
|
12
12
|
|
|
13
13
|
1. **Use `sendToInternet()`** - Routes data through a gateway node
|
|
14
|
+
- Call `mesh.enableSendToInternet()` on ALL nodes after mesh.init()
|
|
15
|
+
- This enables both sending (regular nodes) AND routing (bridge nodes)
|
|
14
16
|
2. **Use `initAsSharedGateway()`** - Configures all nodes with direct router access (requires router credentials - see below)
|
|
15
17
|
3. **Send mesh messages to the bridge** - Bridge node handles Internet communication
|
|
16
18
|
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
//
|
|
40
40
|
// To send data to the Internet from a regular node:
|
|
41
41
|
// 1. Use mesh.sendToInternet() to route through a gateway
|
|
42
|
-
// - Call mesh.enableSendToInternet()
|
|
43
|
-
// - Bridge nodes
|
|
42
|
+
// - Call mesh.enableSendToInternet() AFTER mesh.init() on SENDING nodes only
|
|
43
|
+
// - Bridge nodes automatically handle routing via initAsBridge()
|
|
44
44
|
// - See examples/sendToInternet/sendToInternet.ino for complete usage
|
|
45
45
|
// 2. Use initAsSharedGateway() so all nodes have router access
|
|
46
46
|
// NOTE: initAsSharedGateway() requires ROUTER credentials:
|
|
@@ -144,7 +144,7 @@ void bridgeStatusCallback(uint32_t bridgeNodeId, bool hasInternet) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
void bridgeRoleCallback(bool isBridge, String reason) {
|
|
147
|
+
void bridgeRoleCallback(bool isBridge, const String& reason) {
|
|
148
148
|
if (isBridge) {
|
|
149
149
|
Serial.printf("🎯 PROMOTED TO BRIDGE: %s\n", reason.c_str());
|
|
150
150
|
Serial.println("This node is now the primary bridge!");
|
|
@@ -223,6 +223,13 @@ void setup() {
|
|
|
223
223
|
// mesh.setElectionRandomDelay(10000, 30000); // 10-30 seconds (default: 1-3 seconds)
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
// NOTE: Bridge nodes do NOT need to call mesh.enableSendToInternet()
|
|
227
|
+
// The initAsBridge() method already sets up gateway routing via initGatewayInternetHandler()
|
|
228
|
+
// which handles incoming sendToInternet() requests from regular nodes.
|
|
229
|
+
//
|
|
230
|
+
// Only call enableSendToInternet() on nodes that will SEND requests (regular nodes).
|
|
231
|
+
// Bridge nodes only need to ROUTE requests, which is automatically configured.
|
|
232
|
+
|
|
226
233
|
// Register callbacks
|
|
227
234
|
mesh.onReceive(&receivedCallback);
|
|
228
235
|
mesh.onNewConnection(&newConnectionCallback);
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
// - Works as-is without any modifications needed!
|
|
43
43
|
//
|
|
44
44
|
// 2. SENDING NODE SETUP:
|
|
45
|
-
// - Call mesh.enableSendToInternet() AFTER mesh.init() on nodes that
|
|
46
|
-
// - Bridge nodes
|
|
47
|
-
// - This example shows how to enable it in the setup() function below
|
|
45
|
+
// - Call mesh.enableSendToInternet() AFTER mesh.init() on nodes that SEND requests.
|
|
46
|
+
// - Bridge nodes automatically handle routing via initAsBridge().
|
|
47
|
+
// - This example shows how to enable it in the setup() function below.
|
|
48
48
|
//
|
|
49
49
|
// For Callmebot WhatsApp API:
|
|
50
50
|
// - Get your API key from https://www.callmebot.com/blog/free-api-whatsapp-messages/
|
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.11
|
|
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.11",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@alteriom/mqtt-schema": "^0.8.0",
|
|
43
|
-
"@eslint/js": "^9.39.
|
|
43
|
+
"@eslint/js": "^9.39.2",
|
|
44
44
|
"ajv": "^8.17.1",
|
|
45
45
|
"ajv-formats": "^3.0.1",
|
|
46
46
|
"prettier": "^3.7.4"
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
/**
|
|
30
30
|
* @brief AlteriomPainlessMesh library version information
|
|
31
31
|
*/
|
|
32
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION "1.9.
|
|
32
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION "1.9.11"
|
|
33
33
|
#define ALTERIOM_PAINLESS_MESH_VERSION_MAJOR 1
|
|
34
34
|
#define ALTERIOM_PAINLESS_MESH_VERSION_MINOR 9
|
|
35
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH
|
|
35
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 11
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @brief Library description and usage information
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -967,7 +967,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
967
967
|
* @param callback Function to call when role changes
|
|
968
968
|
*/
|
|
969
969
|
void onBridgeRoleChanged(
|
|
970
|
-
std::function<void(bool isBridge, TSTRING reason)> callback) {
|
|
970
|
+
std::function<void(bool isBridge, const TSTRING& reason)> callback) {
|
|
971
971
|
bridgeRoleChangedCallback = callback;
|
|
972
972
|
}
|
|
973
973
|
|
|
@@ -1740,8 +1740,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1740
1740
|
Log(STARTUP, "=== Becoming Bridge Node ===\n");
|
|
1741
1741
|
|
|
1742
1742
|
// Store previous bridge (if any)
|
|
1743
|
-
|
|
1744
|
-
|
|
1743
|
+
// SAFETY: Use getPrimaryGateway() which returns the nodeId value directly
|
|
1744
|
+
// instead of getPrimaryBridge() which returns a pointer to a vector element.
|
|
1745
|
+
// This avoids crashes from dangling pointers that can occur if the
|
|
1746
|
+
// knownBridges vector is modified between pointer retrieval and use.
|
|
1747
|
+
uint32_t previousBridgeId = this->getPrimaryGateway();
|
|
1745
1748
|
|
|
1746
1749
|
// IMPORTANT: Send takeover announcement BEFORE switching channels
|
|
1747
1750
|
// This ensures other nodes on the current channel receive the announcement
|
|
@@ -1809,38 +1812,24 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1809
1812
|
Log(STARTUP, "✓ Bridge promotion complete on channel %d\n", _meshChannel);
|
|
1810
1813
|
|
|
1811
1814
|
// Notify via callback
|
|
1815
|
+
// Use explicit TSTRING construction to ensure string lifetime safety
|
|
1812
1816
|
if (bridgeRoleChangedCallback) {
|
|
1813
|
-
|
|
1817
|
+
static const TSTRING reason = "Election winner - best router signal";
|
|
1818
|
+
bridgeRoleChangedCallback(true, reason);
|
|
1814
1819
|
}
|
|
1815
1820
|
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
obj2["previousBridge"] = previousBridgeId;
|
|
1829
|
-
obj2["reason"] = "Election winner - best router signal";
|
|
1830
|
-
obj2["routerRSSI"] = WiFi.RSSI();
|
|
1831
|
-
obj2["timestamp"] = this->getNodeTime();
|
|
1832
|
-
obj2["message_type"] = protocol::BRIDGE_TAKEOVER;
|
|
1833
|
-
|
|
1834
|
-
String msg2;
|
|
1835
|
-
serializeJson(doc2, msg2);
|
|
1836
|
-
|
|
1837
|
-
// Send follow-up takeover using raw broadcast to preserve type
|
|
1838
|
-
// BRIDGE_TAKEOVER
|
|
1839
|
-
protocol::Variant variant2(msg2);
|
|
1840
|
-
router::broadcast<protocol::Variant, Connection>(variant2, (*this), 0);
|
|
1841
|
-
|
|
1842
|
-
Log(STARTUP, "✓ Follow-up takeover announcement sent\n");
|
|
1843
|
-
});
|
|
1821
|
+
// Note: The initial takeover announcement was already sent earlier
|
|
1822
|
+
// before the channel switch. The follow-up announcement that was previously
|
|
1823
|
+
// scheduled here has been removed to avoid potential crashes from scheduling
|
|
1824
|
+
// tasks immediately after stop()/reinit cycle.
|
|
1825
|
+
//
|
|
1826
|
+
// The bridge status broadcast system (initialized by initAsBridge via
|
|
1827
|
+
// initBridgeStatusBroadcast) will continue to inform nodes about the new
|
|
1828
|
+
// bridge through periodic broadcasts. Nodes that switched channels will
|
|
1829
|
+
// discover the new bridge through these status broadcasts.
|
|
1830
|
+
Log(STARTUP,
|
|
1831
|
+
"Bridge takeover complete. Status broadcasts will announce bridge to "
|
|
1832
|
+
"network.\n");
|
|
1844
1833
|
}
|
|
1845
1834
|
|
|
1846
1835
|
/**
|
|
@@ -1936,16 +1925,21 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1936
1925
|
_meshChannel);
|
|
1937
1926
|
|
|
1938
1927
|
// Notify via callback
|
|
1928
|
+
// Use explicit TSTRING construction to ensure string lifetime safety
|
|
1939
1929
|
if (bridgeRoleChangedCallback) {
|
|
1940
|
-
|
|
1930
|
+
static const TSTRING reason = "Isolated node promoted to bridge";
|
|
1931
|
+
bridgeRoleChangedCallback(true, reason);
|
|
1941
1932
|
}
|
|
1942
1933
|
|
|
1943
|
-
//
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1934
|
+
// Note: Bridge status announcement will be sent automatically by
|
|
1935
|
+
// initBridgeStatusBroadcast() which is called by initAsBridge().
|
|
1936
|
+
// The immediate broadcast is scheduled in that function, so we don't
|
|
1937
|
+
// need to schedule another one here. This avoids potential crashes from
|
|
1938
|
+
// scheduling tasks immediately after stop()/reinit cycle.
|
|
1939
|
+
// The initBridgeStatusBroadcast() also sets up periodic broadcasts.
|
|
1940
|
+
Log(STARTUP,
|
|
1941
|
+
"Bridge status announcement will be sent by bridge status broadcast "
|
|
1942
|
+
"system\n");
|
|
1949
1943
|
|
|
1950
1944
|
return true; // Count as an attempt - we succeeded
|
|
1951
1945
|
}
|
|
@@ -2323,7 +2317,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2323
2317
|
ElectionState electionState = ELECTION_IDLE;
|
|
2324
2318
|
uint32_t electionDeadline = 0;
|
|
2325
2319
|
std::vector<BridgeCandidate> electionCandidates;
|
|
2326
|
-
std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
|
|
2320
|
+
std::function<void(bool isBridge, const TSTRING& reason)> bridgeRoleChangedCallback;
|
|
2327
2321
|
|
|
2328
2322
|
// Isolated bridge retry state and configuration
|
|
2329
2323
|
uint8_t _isolatedBridgeRetryAttempts = 0;
|
package/src/painlessMesh.h
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* @file painlessMesh.h
|
|
6
6
|
* @brief Main header file for Alteriom painlessMesh library
|
|
7
7
|
*
|
|
8
|
-
* @version 1.9.
|
|
9
|
-
* @date 2025-12-
|
|
8
|
+
* @version 1.9.11
|
|
9
|
+
* @date 2025-12-18
|
|
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
|
|
@@ -16,12 +16,110 @@ namespace tcp {
|
|
|
16
16
|
|
|
17
17
|
// Delay before cleaning up failed AsyncClient after connection error or close
|
|
18
18
|
// This prevents crashes when AsyncTCP library is still accessing the client internally
|
|
19
|
-
// The AsyncTCP library may take
|
|
20
|
-
|
|
19
|
+
// The AsyncTCP library may take several hundred milliseconds to complete its internal cleanup
|
|
20
|
+
// When multiple connections are failing simultaneously (e.g., during mesh connection issues),
|
|
21
|
+
// the library needs even more time to safely process multiple cleanup operations
|
|
22
|
+
// Increased from 500ms to 1000ms to handle high-churn scenarios more reliably
|
|
23
|
+
static const uint32_t TCP_CLIENT_CLEANUP_DELAY_MS = 1000; // 1000ms delay before deleting AsyncClient
|
|
24
|
+
|
|
25
|
+
// Minimum spacing between consecutive AsyncClient deletions to prevent concurrent cleanup
|
|
26
|
+
// When multiple AsyncClients are deleted in rapid succession, the AsyncTCP library's
|
|
27
|
+
// internal cleanup routines can interfere with each other, causing heap corruption
|
|
28
|
+
// This spacing ensures each deletion completes before the next one begins
|
|
29
|
+
static const uint32_t TCP_CLIENT_DELETION_SPACING_MS = 250; // 250ms spacing between deletions
|
|
30
|
+
|
|
31
|
+
// Global state to track AsyncClient deletion scheduling
|
|
32
|
+
// This ensures deletions are spaced out even when multiple deletion requests arrive simultaneously
|
|
33
|
+
// Note: Thread safety is not required - ESP32/ESP8266 mesh runs single-threaded in Arduino framework
|
|
34
|
+
// All mesh operations occur in the main loop or scheduler callbacks, never concurrently
|
|
35
|
+
static uint32_t lastScheduledDeletionTime = 0; // Timestamp when last deletion was scheduled (milliseconds)
|
|
21
36
|
|
|
22
37
|
// Shared buffer for reading/writing to the buffer
|
|
23
38
|
static painlessmesh::buffer::temp_buffer_t shared_buffer;
|
|
24
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Schedule deletion of an AsyncClient with proper spacing to prevent concurrent cleanups
|
|
42
|
+
*
|
|
43
|
+
* This function ensures that AsyncClient deletions are spaced out in time to prevent
|
|
44
|
+
* the AsyncTCP library's internal cleanup routines from interfering with each other.
|
|
45
|
+
*
|
|
46
|
+
* When multiple AsyncClient objects need to be deleted (e.g., during high connection churn
|
|
47
|
+
* or sendToInternet scenarios), scheduling them all with the same delay can cause them to
|
|
48
|
+
* execute concurrently, leading to heap corruption.
|
|
49
|
+
*
|
|
50
|
+
* This function maintains a global timestamp of when the last deletion was scheduled and
|
|
51
|
+
* calculates an appropriate delay for the new deletion to ensure adequate spacing.
|
|
52
|
+
*
|
|
53
|
+
* @param scheduler The task scheduler to use for scheduling the deletion
|
|
54
|
+
* @param client The AsyncClient pointer to delete
|
|
55
|
+
* @param logPrefix Prefix for the log message (e.g., "~BufferedConnection" or "tcp_err")
|
|
56
|
+
*/
|
|
57
|
+
inline void scheduleAsyncClientDeletion(Scheduler* scheduler, AsyncClient* client, const char* logPrefix) {
|
|
58
|
+
using namespace logger;
|
|
59
|
+
|
|
60
|
+
if (!scheduler) {
|
|
61
|
+
// Fallback: If scheduler not available, delete immediately (risky)
|
|
62
|
+
Log(CONNECTION, "%s: No scheduler available, deleting AsyncClient immediately (risky)\n", logPrefix);
|
|
63
|
+
delete client;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get current time in milliseconds
|
|
68
|
+
uint32_t currentTime = millis();
|
|
69
|
+
|
|
70
|
+
// Calculate the earliest time this deletion should execute
|
|
71
|
+
// Base delay: TCP_CLIENT_CLEANUP_DELAY_MS (1000ms)
|
|
72
|
+
uint32_t baseDelay = TCP_CLIENT_CLEANUP_DELAY_MS;
|
|
73
|
+
|
|
74
|
+
// Calculate when this deletion should execute relative to the last scheduled deletion
|
|
75
|
+
// If the last deletion was scheduled recently, we need to add additional spacing
|
|
76
|
+
uint32_t targetDeletionTime = currentTime + baseDelay;
|
|
77
|
+
|
|
78
|
+
// If there's a recent deletion scheduled, ensure we space out from it
|
|
79
|
+
if (lastScheduledDeletionTime > 0) {
|
|
80
|
+
// Calculate when the next deletion slot is available
|
|
81
|
+
uint32_t nextAvailableSlot = lastScheduledDeletionTime + TCP_CLIENT_DELETION_SPACING_MS;
|
|
82
|
+
|
|
83
|
+
// If our target deletion time is before the next available slot, push it out
|
|
84
|
+
// Handle millis() rollover: Use signed arithmetic to detect if nextAvailableSlot is "in the future"
|
|
85
|
+
// relative to targetDeletionTime. This works because:
|
|
86
|
+
// - If difference is positive and < 2^31: nextAvailableSlot is ahead, we need to wait
|
|
87
|
+
// - If difference is negative or > 2^31: nextAvailableSlot is in the past (or very far future after rollover), use targetDeletionTime
|
|
88
|
+
int32_t timeUntilSlot = (int32_t)(nextAvailableSlot - targetDeletionTime);
|
|
89
|
+
if (timeUntilSlot > 0 && timeUntilSlot < (int32_t)(1U << 30)) {
|
|
90
|
+
// nextAvailableSlot is reasonably soon in the future (< ~12 days), space from it
|
|
91
|
+
targetDeletionTime = nextAvailableSlot;
|
|
92
|
+
}
|
|
93
|
+
// else: lastScheduledDeletionTime is too old (> baseDelay+spacing), or rollover occurred
|
|
94
|
+
// In this case, just use targetDeletionTime (currentTime + baseDelay) and reset spacing
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate the actual delay from now
|
|
98
|
+
uint32_t actualDelay = targetDeletionTime - currentTime;
|
|
99
|
+
|
|
100
|
+
// Update the last scheduled deletion time
|
|
101
|
+
lastScheduledDeletionTime = targetDeletionTime;
|
|
102
|
+
|
|
103
|
+
Log(CONNECTION, "%s: Scheduling AsyncClient deletion in %u ms (spaced from previous deletions)\n",
|
|
104
|
+
logPrefix, actualDelay);
|
|
105
|
+
|
|
106
|
+
// Schedule the deletion task
|
|
107
|
+
// Note: Task object is intentionally leaked to keep implementation simple
|
|
108
|
+
// This is acceptable because:
|
|
109
|
+
// 1. Connections are long-lived, destructor calls are infrequent
|
|
110
|
+
// 2. Task object is small (~32-64 bytes) vs preventing critical heap corruption
|
|
111
|
+
// 3. In typical deployments, memory impact is negligible (few KB over months)
|
|
112
|
+
// 4. Alternative cleanup patterns would add significant complexity
|
|
113
|
+
Task* cleanupTask = new Task(actualDelay * TASK_MILLISECOND, TASK_ONCE, [client, logPrefix]() {
|
|
114
|
+
using namespace logger;
|
|
115
|
+
Log(CONNECTION, "%s: Deferred cleanup of AsyncClient executing now\n", logPrefix);
|
|
116
|
+
delete client;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
scheduler->addTask(*cleanupTask);
|
|
120
|
+
cleanupTask->enableDelayed();
|
|
121
|
+
}
|
|
122
|
+
|
|
25
123
|
/**
|
|
26
124
|
* Class that performs buffered read and write to the tcp connection
|
|
27
125
|
* (asyncclient)
|
|
@@ -54,35 +152,9 @@ class BufferedConnection
|
|
|
54
152
|
client->abort();
|
|
55
153
|
|
|
56
154
|
// Defer deletion of the AsyncClient to prevent heap corruption
|
|
57
|
-
//
|
|
58
|
-
// library is still referencing the object internally during cleanup
|
|
155
|
+
// Use the centralized deletion scheduler to ensure proper spacing between deletions
|
|
59
156
|
// See ISSUE_254_HEAP_CORRUPTION_FIX.md and ASYNCCLIENT_CLEANUP_FIX.md
|
|
60
|
-
|
|
61
|
-
// Capture client pointer by value for safe deferred deletion
|
|
62
|
-
AsyncClient* clientToDelete = client;
|
|
63
|
-
|
|
64
|
-
// Schedule deletion task with TCP_CLIENT_CLEANUP_DELAY_MS delay
|
|
65
|
-
// This gives AsyncTCP library time to complete its internal cleanup
|
|
66
|
-
// Note: Task object is intentionally leaked to keep implementation simple
|
|
67
|
-
// This is acceptable because:
|
|
68
|
-
// 1. Connections are long-lived, destructor calls are infrequent
|
|
69
|
-
// 2. Task object is small (~32-64 bytes) vs preventing critical heap corruption
|
|
70
|
-
// 3. In typical deployments, memory impact is negligible (few KB over months)
|
|
71
|
-
// 4. Alternative cleanup patterns would add significant complexity
|
|
72
|
-
Task* cleanupTask = new Task(TCP_CLIENT_CLEANUP_DELAY_MS * TASK_MILLISECOND, TASK_ONCE, [clientToDelete]() {
|
|
73
|
-
using namespace logger;
|
|
74
|
-
Log(CONNECTION, "~BufferedConnection: Deferred cleanup of AsyncClient\n");
|
|
75
|
-
delete clientToDelete;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
mScheduler->addTask(*cleanupTask);
|
|
79
|
-
cleanupTask->enableDelayed();
|
|
80
|
-
} else {
|
|
81
|
-
// Fallback: If scheduler not available, delete immediately
|
|
82
|
-
// This should only happen in test environments or edge cases
|
|
83
|
-
Log(CONNECTION, "~BufferedConnection: No scheduler available, deleting AsyncClient immediately (risky)\n");
|
|
84
|
-
delete client;
|
|
85
|
-
}
|
|
157
|
+
scheduleAsyncClientDeletion(mScheduler, client, "~BufferedConnection");
|
|
86
158
|
}
|
|
87
159
|
|
|
88
160
|
void initialize(Scheduler *scheduler) {
|
|
@@ -1558,6 +1558,15 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1558
1558
|
|
|
1559
1559
|
PendingInternetRequest& request = it->second;
|
|
1560
1560
|
|
|
1561
|
+
// Check mesh connectivity before attempting retry
|
|
1562
|
+
// During bridge failover, connection may be temporarily lost
|
|
1563
|
+
if (!hasActiveMeshConnections()) {
|
|
1564
|
+
Log(logger::ERROR, "retryInternetRequest(): No active mesh connections for retry msgId=%u, rescheduling\n",
|
|
1565
|
+
messageId);
|
|
1566
|
+
scheduleInternetRetry(messageId);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1561
1570
|
// Find gateway (may have changed)
|
|
1562
1571
|
BridgeInfo* gateway = getPrimaryBridge();
|
|
1563
1572
|
if (gateway == nullptr) {
|
|
@@ -228,7 +228,13 @@ class PackageHandler : public layout::Layout<T> {
|
|
|
228
228
|
for (auto&& task : taskList) {
|
|
229
229
|
if (task.use_count() == 1 && !task->isEnabled()) {
|
|
230
230
|
task->set(aInterval, aIterations, aCallback, NULL, NULL);
|
|
231
|
-
|
|
231
|
+
// Use enableDelayed() for delayed one-shot tasks to prevent immediate execution
|
|
232
|
+
// This ensures tasks with intervals execute after the delay, not immediately
|
|
233
|
+
if (aInterval > 0 && aIterations == TASK_ONCE) {
|
|
234
|
+
task->enableDelayed();
|
|
235
|
+
} else {
|
|
236
|
+
task->enable();
|
|
237
|
+
}
|
|
232
238
|
return task;
|
|
233
239
|
}
|
|
234
240
|
}
|
|
@@ -236,7 +242,13 @@ class PackageHandler : public layout::Layout<T> {
|
|
|
236
242
|
std::shared_ptr<Task> task =
|
|
237
243
|
std::make_shared<Task>(aInterval, aIterations, aCallback);
|
|
238
244
|
scheduler.addTask((*task));
|
|
239
|
-
|
|
245
|
+
// Use enableDelayed() for delayed one-shot tasks to prevent immediate execution
|
|
246
|
+
// This ensures tasks with intervals execute after the delay, not immediately
|
|
247
|
+
if (aInterval > 0 && aIterations == TASK_ONCE) {
|
|
248
|
+
task->enableDelayed();
|
|
249
|
+
} else {
|
|
250
|
+
task->enable();
|
|
251
|
+
}
|
|
240
252
|
taskList.push_front(task);
|
|
241
253
|
return task;
|
|
242
254
|
}
|
package/src/painlessmesh/tcp.hpp
CHANGED
|
@@ -135,15 +135,9 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
135
135
|
}, retryDelay);
|
|
136
136
|
|
|
137
137
|
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
// its internal cleanup before we delete the object
|
|
142
|
-
// Note: client is captured by value (pointer copy) and we are the sole owner
|
|
143
|
-
mesh.addTask([client]() {
|
|
144
|
-
Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (retry path)\n");
|
|
145
|
-
delete client;
|
|
146
|
-
}, TCP_CLIENT_CLEANUP_DELAY_MS);
|
|
138
|
+
// Use the centralized deletion scheduler to ensure proper spacing between deletions
|
|
139
|
+
// This prevents concurrent cleanup operations in the AsyncTCP library
|
|
140
|
+
scheduleAsyncClientDeletion(mesh.mScheduler, client, "tcp_err(retry)");
|
|
147
141
|
|
|
148
142
|
mesh.semaphoreGive();
|
|
149
143
|
return;
|
|
@@ -156,15 +150,9 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
156
150
|
TCP_CONNECT_MAX_RETRIES + 1, TCP_EXHAUSTION_RECONNECT_DELAY_MS);
|
|
157
151
|
|
|
158
152
|
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
// its internal cleanup before we delete the object
|
|
163
|
-
// Note: client is captured by value (pointer copy) and we are the sole owner
|
|
164
|
-
mesh.addTask([client]() {
|
|
165
|
-
Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (exhaustion path)\n");
|
|
166
|
-
delete client;
|
|
167
|
-
}, TCP_CLIENT_CLEANUP_DELAY_MS);
|
|
153
|
+
// Use the centralized deletion scheduler to ensure proper spacing between deletions
|
|
154
|
+
// This prevents concurrent cleanup operations in the AsyncTCP library
|
|
155
|
+
scheduleAsyncClientDeletion(mesh.mScheduler, client, "tcp_err(exhaustion)");
|
|
168
156
|
#endif
|
|
169
157
|
// Defer callback execution to avoid crashes in error handler context
|
|
170
158
|
// Execute callbacks after semaphore is released and error handler completes
|