@alteriom/painlessmesh 1.9.10 → 1.9.12
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 +62 -0
- package/README.md +1 -1
- package/examples/bridge_failover/README.md +2 -0
- package/examples/bridge_failover/bridge_failover.ino +9 -2
- package/examples/sendToInternet/README.md +17 -0
- package/examples/sendToInternet/sendToInternet.ino +10 -3
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/AlteriomPainlessMesh.h +2 -2
- package/src/arduino/wifi.hpp +62 -41
- package/src/painlessMesh.h +2 -2
- package/src/painlessmesh/connection.hpp +124 -31
- package/src/painlessmesh/mesh.hpp +9 -0
- package/src/painlessmesh/tcp.hpp +6 -18
package/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
19
|
|
|
20
20
|
- TBD
|
|
21
21
|
|
|
22
|
+
## [1.9.12] - 2025-12-18
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Hard Reset During Bridge Operations - AsyncClient abort() Timing Issue** - Fixed ESP32/ESP8266 heap corruption crashes during TCP connection failures and bridge operations
|
|
27
|
+
- **Root Cause**: Calling `client->abort()` synchronously before scheduling deferred AsyncClient deletion (1000ms+ later) left the client in an inconsistent state where AsyncTCP's internal cleanup tried to access the aborted client
|
|
28
|
+
- **Symptom**: Device crashes with "CORRUPT HEAP: Bad head at 0x40838cdc. Expected 0xabba1234 got 0x4200822e" and "assert failed: multi_heap_free multi_heap_poisoning.c:279" during connection cleanup
|
|
29
|
+
- **Solution**: Removed synchronous `abort()` call from `~BufferedConnection()` destructor
|
|
30
|
+
- The existing `close()` and `close(true)` calls are sufficient for connection termination
|
|
31
|
+
- According to AsyncTCP best practices, `abort()` should only be called immediately before `delete`, not before a deferred deletion
|
|
32
|
+
- Eliminates the 1000ms window where AsyncTCP tries to clean up an aborted but not-yet-deleted client
|
|
33
|
+
- **Testing**: All test suites pass (1000+ assertions), including TCP retry, connection, and mesh connectivity tests
|
|
34
|
+
- **Documentation**: Added ISSUE_ABORT_TIMING_FIX.md with detailed AsyncTCP best practices analysis
|
|
35
|
+
- **Impact**: Completes the AsyncClient lifecycle management improvements, eliminating the last known heap corruption scenario in connection cleanup
|
|
36
|
+
|
|
37
|
+
## [1.9.11] - 2025-12-18
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **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
|
|
42
|
+
- **Root Cause**: Calling `addTask()` immediately after `stop()/initAsBridge()` cycle accessed unstable internal task scheduling structures before they were fully reinitialized in the new context
|
|
43
|
+
- **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
|
|
44
|
+
- **Solution**: Removed redundant task scheduling calls that were unsafe after stop/reinit
|
|
45
|
+
- Removed `addTask()` call in `attemptIsolatedBridgePromotion()` (line 1947)
|
|
46
|
+
- Removed `addTask()` call in `promoteToBridge()` (line 1822)
|
|
47
|
+
- Relied on existing `initBridgeStatusBroadcast()` infrastructure which safely handles announcements
|
|
48
|
+
- Used explicit `TSTRING` construction in callback invocations for string lifetime safety
|
|
49
|
+
- **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)
|
|
50
|
+
- **Testing**: All test suites pass (1000+ assertions), including bridge election and promotion tests
|
|
51
|
+
- **Documentation**: Added ISSUE_HARD_RESET_BRIDGE_PROMOTION_FIX.md with detailed analysis of both affected code paths
|
|
52
|
+
- **Impact**: Eliminates critical crash during bridge promotion, allows stable bridge failover operation in both isolated and competitive election scenarios
|
|
53
|
+
|
|
54
|
+
- **Bridge Failover & sendToInternet Retry Connectivity** - Fixed heap corruption and request timeouts when using bridge_failover with sendToInternet() during connection instability
|
|
55
|
+
- **Root Cause**: `retryInternetRequest()` did not check mesh connectivity before attempting retry, causing routing attempts through unreachable gateways during bridge disconnection
|
|
56
|
+
- **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()
|
|
57
|
+
- **Solution**: Added `hasActiveMeshConnections()` check at start of `retryInternetRequest()`
|
|
58
|
+
- Retry only proceeds if mesh connections are active
|
|
59
|
+
- If disconnected, reschedules retry instead of attempting to route
|
|
60
|
+
- Prevents routing to unreachable gateways during temporary disconnection
|
|
61
|
+
- Maintains existing retry logic and exponential backoff
|
|
62
|
+
- **Testing**: Added comprehensive test coverage (catch_sendtointernet_retry_no_mesh.cpp) with 31 assertions validating disconnected retry scenarios
|
|
63
|
+
- **Documentation**: Added BRIDGE_FAILOVER_RETRY_FIX.md with detailed analysis and usage notes
|
|
64
|
+
- **Impact**: Fixes critical stability issue during bridge failover, enabling reliable sendToInternet() usage in production deployments with unstable connections
|
|
65
|
+
|
|
66
|
+
- **Hard Reset During sendToInternet - Serialized AsyncClient Deletion** - Fixed ESP32/ESP8266 hard resets caused by heap corruption when multiple AsyncClient cleanup operations execute concurrently
|
|
67
|
+
- **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.
|
|
68
|
+
- **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.
|
|
69
|
+
- **Solution**: Implemented serialized deletion with 250ms spacing between consecutive AsyncClient deletions
|
|
70
|
+
- Added `TCP_CLIENT_DELETION_SPACING_MS` constant (250ms) to ensure deletions don't overlap
|
|
71
|
+
- Added global `lastScheduledDeletionTime` tracker to coordinate deletion timing
|
|
72
|
+
- Implemented `scheduleAsyncClientDeletion()` function that calculates proper spacing
|
|
73
|
+
- Updated `BufferedConnection` destructor and tcp.hpp error handlers to use centralized scheduler
|
|
74
|
+
- Ensures each AsyncClient deletion completes before the next one starts
|
|
75
|
+
- Handles millis() rollover and multiple concurrent deletion requests
|
|
76
|
+
- **Performance Impact**:
|
|
77
|
+
- Single deletion: No change (1000ms)
|
|
78
|
+
- Multiple concurrent deletions: Spaced by 250ms each (total spread <1 second for typical scenarios)
|
|
79
|
+
- High-churn scenario (10 failures): Spread over ~3 seconds (still acceptable)
|
|
80
|
+
- **Testing**: All test suites pass (1000+ assertions), including new deletion spacing tests (47 assertions in tcp_retry)
|
|
81
|
+
- **Documentation**: Added ISSUE_HARD_RESET_SENDTOINTERNET_SERIALIZED_DELETION_FIX.md with detailed analysis
|
|
82
|
+
- **Impact**: Fixes critical stability issue in production deployments with high connection churn, particularly affecting sendToInternet and bridge failover scenarios
|
|
83
|
+
|
|
22
84
|
## [1.9.10] - 2025-12-15
|
|
23
85
|
|
|
24
86
|
### 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.12** - Latest release with AsyncClient abort() timing fix for heap corruption prevention
|
|
8
8
|
|
|
9
9
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
|
|
@@ -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:
|
|
@@ -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);
|
|
@@ -146,6 +146,23 @@ mesh.sendToInternet("https://api.callmebot.com/...", "", callback);
|
|
|
146
146
|
3. Check HTTP status code in callback (200 = success)
|
|
147
147
|
4. URL-encode special characters in the message
|
|
148
148
|
|
|
149
|
+
### Understanding HTTP Status Codes
|
|
150
|
+
|
|
151
|
+
The callback provides `httpStatus` to indicate the result:
|
|
152
|
+
|
|
153
|
+
**SUCCESS (success = true):**
|
|
154
|
+
- `200 OK` - Standard success (most common for WhatsApp API)
|
|
155
|
+
- `201 Created` - Resource successfully created
|
|
156
|
+
- `202 Accepted` - Request accepted for processing
|
|
157
|
+
- `204 No Content` - Successful with no response body
|
|
158
|
+
|
|
159
|
+
**FAILURE (success = false):**
|
|
160
|
+
- `203 Non-Authoritative Information` - **Cached/proxied response, NOT actual delivery**
|
|
161
|
+
- `4xx` - Client error (bad request, unauthorized, not found, etc.)
|
|
162
|
+
- `5xx` - Server error (service unavailable, gateway timeout, etc.)
|
|
163
|
+
|
|
164
|
+
⚠️ **Important:** HTTP 203 is treated as **FAILURE** because it indicates the response came from a cache or proxy, not from the actual WhatsApp API server. If you see `HTTP Status: 203`, the message was **NOT delivered**.
|
|
165
|
+
|
|
149
166
|
## Files
|
|
150
167
|
|
|
151
168
|
- `sendToInternet.ino` - Main example sketch
|
|
@@ -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/
|
|
@@ -184,6 +184,13 @@ void sendAlertToWhatsApp(String message) {
|
|
|
184
184
|
|
|
185
185
|
// Use sendToInternet() to route the request through a gateway
|
|
186
186
|
// The callback will be invoked when we get a response (or timeout)
|
|
187
|
+
//
|
|
188
|
+
// SUCCESS CODES: Only specific HTTP codes indicate genuine delivery:
|
|
189
|
+
// - 200 OK: Standard success (most common for WhatsApp API)
|
|
190
|
+
// - 201 Created, 202 Accepted, 204 No Content
|
|
191
|
+
//
|
|
192
|
+
// FAILURE: HTTP 203 (Non-Authoritative) is treated as FAILURE because
|
|
193
|
+
// it indicates a cached/proxied response, not actual delivery to WhatsApp.
|
|
187
194
|
uint32_t msgId = mesh.sendToInternet(
|
|
188
195
|
url,
|
|
189
196
|
"", // No payload needed for GET request - params are in URL
|
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.12
|
|
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.12",
|
|
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",
|
|
@@ -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.12"
|
|
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 12
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @brief Library description and usage information
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -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
|
}
|
|
@@ -2164,10 +2158,37 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2164
2158
|
}
|
|
2165
2159
|
|
|
2166
2160
|
if (httpCode > 0) {
|
|
2167
|
-
// Only 2xx status codes
|
|
2161
|
+
// Only specific 2xx status codes indicate genuine success
|
|
2162
|
+
// 200 OK: Standard successful response
|
|
2163
|
+
// 201 Created: Resource successfully created
|
|
2164
|
+
// 202 Accepted: Request accepted for processing
|
|
2165
|
+
// 204 No Content: Successful with no response body
|
|
2166
|
+
//
|
|
2167
|
+
// Other 2xx codes like 203 (Non-Authoritative Information) often
|
|
2168
|
+
// indicate cached/proxied responses that may not represent actual
|
|
2169
|
+
// delivery to the destination service (e.g., WhatsApp API).
|
|
2170
|
+
//
|
|
2168
2171
|
// 3xx redirects are not automatically followed
|
|
2169
|
-
success = (httpCode
|
|
2170
|
-
|
|
2172
|
+
success = (httpCode == 200 || httpCode == 201 ||
|
|
2173
|
+
httpCode == 202 || httpCode == 204);
|
|
2174
|
+
|
|
2175
|
+
if (success) {
|
|
2176
|
+
Log(COMMUNICATION, "HTTP request completed: code=%d\n", httpCode);
|
|
2177
|
+
} else if (httpCode >= 200 && httpCode < 300) {
|
|
2178
|
+
// Other 2xx codes - ambiguous success
|
|
2179
|
+
char errorBuf[128];
|
|
2180
|
+
snprintf(errorBuf, sizeof(errorBuf),
|
|
2181
|
+
"Ambiguous response - HTTP %d may indicate cached/proxied response, not actual delivery",
|
|
2182
|
+
httpCode);
|
|
2183
|
+
error = TSTRING(errorBuf);
|
|
2184
|
+
Log(ERROR, "HTTP request ambiguous: code=%d (treated as failure)\n", httpCode);
|
|
2185
|
+
} else {
|
|
2186
|
+
// 1xx, 3xx, 4xx, 5xx
|
|
2187
|
+
char errorBuf[32];
|
|
2188
|
+
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d", httpCode);
|
|
2189
|
+
error = TSTRING(errorBuf);
|
|
2190
|
+
Log(ERROR, "HTTP request failed: code=%d\n", httpCode);
|
|
2191
|
+
}
|
|
2171
2192
|
} else {
|
|
2172
2193
|
error = http.errorToString(httpCode);
|
|
2173
2194
|
Log(ERROR, "HTTP request failed: %s\n", error.c_str());
|
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.12
|
|
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,126 @@ 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 and execution
|
|
32
|
+
// This ensures deletions are spaced out even when multiple deletion requests arrive simultaneously
|
|
33
|
+
// The timestamp is updated both when a deletion is SCHEDULED and when it EXECUTES, providing
|
|
34
|
+
// double protection against concurrent cleanup operations even with scheduler jitter
|
|
35
|
+
//
|
|
36
|
+
// THREAD SAFETY: No synchronization needed because:
|
|
37
|
+
// - ESP32/ESP8266 Arduino framework is single-threaded by design
|
|
38
|
+
// - TaskScheduler executes callbacks sequentially in the main loop
|
|
39
|
+
// - The scheduler never runs tasks concurrently within the same mesh instance
|
|
40
|
+
// - All mesh operations (including deletion callbacks) execute in the same thread
|
|
41
|
+
// - Even when multiple tasks are ready, they execute one-at-a-time via scheduler->execute()
|
|
42
|
+
static uint32_t lastScheduledDeletionTime = 0; // Timestamp of last deletion scheduled/executed (milliseconds)
|
|
21
43
|
|
|
22
44
|
// Shared buffer for reading/writing to the buffer
|
|
23
45
|
static painlessmesh::buffer::temp_buffer_t shared_buffer;
|
|
24
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Schedule deletion of an AsyncClient with proper spacing to prevent concurrent cleanups
|
|
49
|
+
*
|
|
50
|
+
* This function ensures that AsyncClient deletions are spaced out in time to prevent
|
|
51
|
+
* the AsyncTCP library's internal cleanup routines from interfering with each other.
|
|
52
|
+
*
|
|
53
|
+
* When multiple AsyncClient objects need to be deleted (e.g., during high connection churn
|
|
54
|
+
* or sendToInternet scenarios), scheduling them all with the same delay can cause them to
|
|
55
|
+
* execute concurrently, leading to heap corruption.
|
|
56
|
+
*
|
|
57
|
+
* This function maintains a global timestamp of when the last deletion was scheduled and
|
|
58
|
+
* calculates an appropriate delay for the new deletion to ensure adequate spacing.
|
|
59
|
+
*
|
|
60
|
+
* @param scheduler The task scheduler to use for scheduling the deletion
|
|
61
|
+
* @param client The AsyncClient pointer to delete
|
|
62
|
+
* @param logPrefix Prefix for the log message (e.g., "~BufferedConnection" or "tcp_err")
|
|
63
|
+
*/
|
|
64
|
+
inline void scheduleAsyncClientDeletion(Scheduler* scheduler, AsyncClient* client, const char* logPrefix) {
|
|
65
|
+
using namespace logger;
|
|
66
|
+
|
|
67
|
+
if (!scheduler) {
|
|
68
|
+
// Fallback: If scheduler not available, delete immediately (risky)
|
|
69
|
+
Log(CONNECTION, "%s: No scheduler available, deleting AsyncClient immediately (risky)\n", logPrefix);
|
|
70
|
+
delete client;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get current time in milliseconds
|
|
75
|
+
uint32_t currentTime = millis();
|
|
76
|
+
|
|
77
|
+
// Calculate the earliest time this deletion should execute
|
|
78
|
+
// Base delay: TCP_CLIENT_CLEANUP_DELAY_MS (1000ms)
|
|
79
|
+
uint32_t baseDelay = TCP_CLIENT_CLEANUP_DELAY_MS;
|
|
80
|
+
|
|
81
|
+
// Calculate when this deletion should execute relative to the last scheduled deletion
|
|
82
|
+
// If the last deletion was scheduled recently, we need to add additional spacing
|
|
83
|
+
uint32_t targetDeletionTime = currentTime + baseDelay;
|
|
84
|
+
|
|
85
|
+
// If there's a recent deletion scheduled, ensure we space out from it
|
|
86
|
+
if (lastScheduledDeletionTime > 0) {
|
|
87
|
+
// Calculate when the next deletion slot is available
|
|
88
|
+
uint32_t nextAvailableSlot = lastScheduledDeletionTime + TCP_CLIENT_DELETION_SPACING_MS;
|
|
89
|
+
|
|
90
|
+
// If our target deletion time is before the next available slot, push it out
|
|
91
|
+
// Handle millis() rollover: Use signed arithmetic to detect if nextAvailableSlot is "in the future"
|
|
92
|
+
// relative to targetDeletionTime. This works because:
|
|
93
|
+
// - If difference is positive and < 2^31: nextAvailableSlot is ahead, we need to wait
|
|
94
|
+
// - If difference is negative or > 2^31: nextAvailableSlot is in the past (or very far future after rollover), use targetDeletionTime
|
|
95
|
+
int32_t timeUntilSlot = (int32_t)(nextAvailableSlot - targetDeletionTime);
|
|
96
|
+
if (timeUntilSlot > 0 && timeUntilSlot < (int32_t)(1U << 30)) {
|
|
97
|
+
// nextAvailableSlot is reasonably soon in the future (< ~12 days), space from it
|
|
98
|
+
targetDeletionTime = nextAvailableSlot;
|
|
99
|
+
}
|
|
100
|
+
// else: lastScheduledDeletionTime is too old (> baseDelay+spacing), or rollover occurred
|
|
101
|
+
// In this case, just use targetDeletionTime (currentTime + baseDelay) and reset spacing
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Calculate the actual delay from now
|
|
105
|
+
uint32_t actualDelay = targetDeletionTime - currentTime;
|
|
106
|
+
|
|
107
|
+
// Update the last scheduled deletion time
|
|
108
|
+
// IMPORTANT: We update this when scheduling, which provides a minimum guaranteed spacing
|
|
109
|
+
// between scheduled deletions. This ensures even with scheduler jitter, deletions won't
|
|
110
|
+
// be scheduled too close together.
|
|
111
|
+
lastScheduledDeletionTime = targetDeletionTime;
|
|
112
|
+
|
|
113
|
+
Log(CONNECTION, "%s: Scheduling AsyncClient deletion in %u ms (spaced from previous deletions)\n",
|
|
114
|
+
logPrefix, actualDelay);
|
|
115
|
+
|
|
116
|
+
// Schedule the deletion task
|
|
117
|
+
// Note: Task object is intentionally leaked to keep implementation simple
|
|
118
|
+
// This is acceptable because:
|
|
119
|
+
// 1. Connections are long-lived, destructor calls are infrequent
|
|
120
|
+
// 2. Task object is small (~32-64 bytes) vs preventing critical heap corruption
|
|
121
|
+
// 3. In typical deployments, memory impact is negligible (few KB over months)
|
|
122
|
+
// 4. Alternative cleanup patterns would add significant complexity
|
|
123
|
+
Task* cleanupTask = new Task(actualDelay * TASK_MILLISECOND, TASK_ONCE, [client, logPrefix]() {
|
|
124
|
+
using namespace logger;
|
|
125
|
+
Log(CONNECTION, "%s: Deferred cleanup of AsyncClient executing now\n", logPrefix);
|
|
126
|
+
|
|
127
|
+
// Update the last deletion time when the deletion actually executes
|
|
128
|
+
// This ensures subsequent deletions are spaced from the actual execution time,
|
|
129
|
+
// not just the scheduled time, preventing concurrent cleanup operations
|
|
130
|
+
lastScheduledDeletionTime = millis();
|
|
131
|
+
|
|
132
|
+
delete client;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
scheduler->addTask(*cleanupTask);
|
|
136
|
+
cleanupTask->enableDelayed();
|
|
137
|
+
}
|
|
138
|
+
|
|
25
139
|
/**
|
|
26
140
|
* Class that performs buffered read and write to the tcp connection
|
|
27
141
|
* (asyncclient)
|
|
@@ -51,38 +165,17 @@ class BufferedConnection
|
|
|
51
165
|
if (!client->freeable()) {
|
|
52
166
|
client->close(true);
|
|
53
167
|
}
|
|
54
|
-
client->abort()
|
|
168
|
+
// Note: client->abort() removed - calling it before deferred deletion
|
|
169
|
+
// can leave the client in an inconsistent state where AsyncTCP is still
|
|
170
|
+
// trying to clean up the aborted connection. The close() and close(true)
|
|
171
|
+
// calls above are sufficient for connection termination.
|
|
172
|
+
// See: AsyncTCP best practices - abort() should only be called immediately
|
|
173
|
+
// before delete, not before a deferred deletion.
|
|
55
174
|
|
|
56
175
|
// Defer deletion of the AsyncClient to prevent heap corruption
|
|
57
|
-
//
|
|
58
|
-
// library is still referencing the object internally during cleanup
|
|
176
|
+
// Use the centralized deletion scheduler to ensure proper spacing between deletions
|
|
59
177
|
// 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
|
-
}
|
|
178
|
+
scheduleAsyncClientDeletion(mScheduler, client, "~BufferedConnection");
|
|
86
179
|
}
|
|
87
180
|
|
|
88
181
|
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) {
|
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
|