@alteriom/painlessmesh 1.9.20 → 1.10.0
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 +121 -0
- package/README.md +23 -3
- package/RELEASE_GUIDE.md +147 -8
- package/examples/alteriom/alteriom_custom_package_template.hpp +11 -11
- package/examples/alteriom/mppt_example/alteriom_mppt_example.ino +1 -1
- package/examples/bridge_failover/bridge_failover.ino +2 -2
- package/examples/tcpRetryConfig/README.md +110 -0
- package/examples/tcpRetryConfig/platformio.ini +26 -0
- package/examples/tcpRetryConfig/tcpRetryConfig.ino +154 -0
- package/keywords.txt +3 -0
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/AlteriomPainlessMesh.h +3 -3
- package/src/arduino/wifi.hpp +49 -17
- package/src/painlessMesh.h +2 -2
- package/src/painlessTaskOptions.h +9 -0
- package/src/painlessmesh/buffer.hpp +4 -1
- package/src/painlessmesh/configuration.hpp +13 -2
- package/src/painlessmesh/connection.hpp +31 -11
- package/src/painlessmesh/mesh.hpp +73 -22
- package/src/painlessmesh/message_queue.hpp +24 -13
- package/src/painlessmesh/plugin.hpp +27 -5
- package/src/painlessmesh/tcp.hpp +158 -29
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//************************************************************
|
|
2
|
+
// Tuning the TCP connection retry behaviour (issue #378)
|
|
3
|
+
//
|
|
4
|
+
// painlessMesh retries a failed TCP connection with exponential backoff
|
|
5
|
+
// before giving up and falling back to a full WiFi reconnect. The defaults
|
|
6
|
+
// (5 retries, 1s base delay -> 1s, 2s, 4s, 8s, 8s) are tuned for general
|
|
7
|
+
// purpose meshes and deliberately favour reliability over speed.
|
|
8
|
+
//
|
|
9
|
+
// setTcpRetryConfig() lets you pick a different trade-off. Three ready-made
|
|
10
|
+
// profiles are shown below; switch between them with ACTIVE_PROFILE.
|
|
11
|
+
//
|
|
12
|
+
// Read examples/tcpRetryConfig/README.md before changing these values - the
|
|
13
|
+
// defaults exist because 1.9.x raised them to fix real mesh instability.
|
|
14
|
+
//************************************************************
|
|
15
|
+
#include <painlessMesh.h>
|
|
16
|
+
|
|
17
|
+
#define MESH_SSID "whateverYouLike"
|
|
18
|
+
#define MESH_PASSWORD "somethingSneaky"
|
|
19
|
+
#define MESH_PORT 5555
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Profiles
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
#define PROFILE_REALTIME 1 // low latency: fail fast, reconnect fast
|
|
25
|
+
#define PROFILE_RELIABLE 2 // industrial: many retries, long backoff
|
|
26
|
+
#define PROFILE_BATTERY 3 // conserve power: few retries, long block
|
|
27
|
+
|
|
28
|
+
// >>> Change this line to try a different profile <<<
|
|
29
|
+
#define ACTIVE_PROFILE PROFILE_REALTIME
|
|
30
|
+
|
|
31
|
+
// Prototypes
|
|
32
|
+
void sendMessage();
|
|
33
|
+
void receivedCallback(uint32_t from, String &msg);
|
|
34
|
+
void newConnectionCallback(uint32_t nodeId);
|
|
35
|
+
void droppedConnectionCallback(uint32_t nodeId);
|
|
36
|
+
|
|
37
|
+
Scheduler userScheduler;
|
|
38
|
+
painlessMesh mesh;
|
|
39
|
+
|
|
40
|
+
Task taskSendMessage(TASK_SECOND * 5, TASK_FOREVER, &sendMessage);
|
|
41
|
+
|
|
42
|
+
// Build the retry configuration for the selected profile.
|
|
43
|
+
painlessmesh::tcp::TcpRetryConfig buildRetryConfig() {
|
|
44
|
+
painlessmesh::tcp::TcpRetryConfig cfg; // starts at the library defaults
|
|
45
|
+
|
|
46
|
+
#if ACTIVE_PROFILE == PROFILE_REALTIME
|
|
47
|
+
// Real-time sensor / LED meshes: a stalled node is worse than a dropped
|
|
48
|
+
// one. Give up on the TCP handshake almost immediately and go straight
|
|
49
|
+
// back to scanning for another parent.
|
|
50
|
+
cfg.maxRetries = 1; // default 5
|
|
51
|
+
cfg.retryDelayMs = 200; // default 1000
|
|
52
|
+
cfg.stabilizationDelayMs = 100; // default 500
|
|
53
|
+
cfg.exhaustionReconnectDelayMs = 1000; // default 10000
|
|
54
|
+
cfg.failureBlockDurationMs = 5000; // default 60000
|
|
55
|
+
|
|
56
|
+
#elif ACTIVE_PROFILE == PROFILE_RELIABLE
|
|
57
|
+
// Industrial / high-reliability meshes: connectivity matters more than
|
|
58
|
+
// how long it takes to get there. Retry patiently and keep a failed peer
|
|
59
|
+
// out of the running for a good while.
|
|
60
|
+
cfg.maxRetries = 10; // default 5 (this is the maximum)
|
|
61
|
+
cfg.retryDelayMs = 2000; // default 1000
|
|
62
|
+
cfg.stabilizationDelayMs = 1000; // default 500
|
|
63
|
+
cfg.exhaustionReconnectDelayMs = 30000; // default 10000
|
|
64
|
+
// Must exceed one full failure cycle (126s of retries + 30s reconnect),
|
|
65
|
+
// otherwise a dead peer leaves the blocklist before we finished failing
|
|
66
|
+
// over and gets re-selected immediately.
|
|
67
|
+
cfg.failureBlockDurationMs = 180000; // default 60000
|
|
68
|
+
|
|
69
|
+
#elif ACTIVE_PROFILE == PROFILE_BATTERY
|
|
70
|
+
// Battery-powered nodes: every retry is radio time. Few attempts, spaced
|
|
71
|
+
// widely, and a long blocklist so we do not keep waking up for a peer
|
|
72
|
+
// that is known to be down.
|
|
73
|
+
cfg.maxRetries = 2; // default 5
|
|
74
|
+
cfg.retryDelayMs = 3000; // default 1000
|
|
75
|
+
cfg.stabilizationDelayMs = 500; // default 500
|
|
76
|
+
cfg.exhaustionReconnectDelayMs = 60000; // default 10000
|
|
77
|
+
cfg.failureBlockDurationMs = 300000; // default 60000
|
|
78
|
+
|
|
79
|
+
#else
|
|
80
|
+
#error "ACTIVE_PROFILE must be one of PROFILE_REALTIME, PROFILE_RELIABLE, PROFILE_BATTERY"
|
|
81
|
+
#endif
|
|
82
|
+
|
|
83
|
+
return cfg;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
void setup() {
|
|
87
|
+
Serial.begin(115200);
|
|
88
|
+
|
|
89
|
+
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
|
|
90
|
+
|
|
91
|
+
// Apply the retry configuration BEFORE init() so the very first connection
|
|
92
|
+
// attempt already uses it.
|
|
93
|
+
mesh.setTcpRetryConfig(buildRetryConfig());
|
|
94
|
+
|
|
95
|
+
mesh.init(MESH_SSID, MESH_PASSWORD, &userScheduler, MESH_PORT);
|
|
96
|
+
|
|
97
|
+
// Read the configuration back. Values outside safe operating bounds are
|
|
98
|
+
// clamped by the setter, so this prints what is actually in effect - not
|
|
99
|
+
// necessarily what was requested.
|
|
100
|
+
painlessmesh::tcp::TcpRetryConfig active = mesh.getTcpRetryConfig();
|
|
101
|
+
Serial.println();
|
|
102
|
+
Serial.println(F("Effective TCP retry configuration:"));
|
|
103
|
+
Serial.printf(" maxRetries = %u\n",
|
|
104
|
+
(unsigned)active.maxRetries);
|
|
105
|
+
Serial.printf(" retryDelayMs = %u\n",
|
|
106
|
+
(unsigned)active.retryDelayMs);
|
|
107
|
+
Serial.printf(" stabilizationDelayMs = %u\n",
|
|
108
|
+
(unsigned)active.stabilizationDelayMs);
|
|
109
|
+
Serial.printf(" exhaustionReconnectDelayMs = %u\n",
|
|
110
|
+
(unsigned)active.exhaustionReconnectDelayMs);
|
|
111
|
+
Serial.printf(" failureBlockDurationMs = %u\n",
|
|
112
|
+
(unsigned)active.failureBlockDurationMs);
|
|
113
|
+
|
|
114
|
+
// Worst-case time spent retrying before falling back to a WiFi reconnect.
|
|
115
|
+
uint32_t worstCase = 0;
|
|
116
|
+
for (uint8_t i = 0; i < active.maxRetries; ++i) {
|
|
117
|
+
worstCase += painlessmesh::tcp::retryBackoffDelay(active, i);
|
|
118
|
+
}
|
|
119
|
+
Serial.printf(" -> worst-case retry time = %u ms\n", (unsigned)worstCase);
|
|
120
|
+
Serial.printf(" -> plus reconnect delay = %u ms\n",
|
|
121
|
+
(unsigned)(worstCase + active.exhaustionReconnectDelayMs));
|
|
122
|
+
Serial.println();
|
|
123
|
+
|
|
124
|
+
mesh.onReceive(&receivedCallback);
|
|
125
|
+
mesh.onNewConnection(&newConnectionCallback);
|
|
126
|
+
mesh.onDroppedConnection(&droppedConnectionCallback);
|
|
127
|
+
|
|
128
|
+
userScheduler.addTask(taskSendMessage);
|
|
129
|
+
taskSendMessage.enable();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
void loop() {
|
|
133
|
+
mesh.update();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
void sendMessage() {
|
|
137
|
+
String msg = "Hello from node ";
|
|
138
|
+
msg += mesh.getNodeId();
|
|
139
|
+
mesh.sendBroadcast(msg);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
void receivedCallback(uint32_t from, String &msg) {
|
|
143
|
+
Serial.printf("tcpRetryConfig: Received from %u msg=%s\n", from, msg.c_str());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
void newConnectionCallback(uint32_t nodeId) {
|
|
147
|
+
Serial.printf("--> Connected to node %u\n", nodeId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
void droppedConnectionCallback(uint32_t nodeId) {
|
|
151
|
+
// With an aggressive profile you should expect to see this more often -
|
|
152
|
+
// and to see the reconnect that follows it happen much sooner.
|
|
153
|
+
Serial.printf("--> Dropped connection to node %u\n", nodeId);
|
|
154
|
+
}
|
package/keywords.txt
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
painlessMesh KEYWORD1
|
|
5
5
|
Scheduler KEYWORD1
|
|
6
6
|
Task KEYWORD1
|
|
7
|
+
TcpRetryConfig KEYWORD1
|
|
7
8
|
|
|
8
9
|
# Methods and Functions (KEYWORD2)
|
|
9
10
|
init KEYWORD2
|
|
@@ -18,6 +19,8 @@ getNodeList KEYWORD2
|
|
|
18
19
|
getNodeId KEYWORD2
|
|
19
20
|
getNodeTime KEYWORD2
|
|
20
21
|
setDebugMsgTypes KEYWORD2
|
|
22
|
+
setTcpRetryConfig KEYWORD2
|
|
23
|
+
getTcpRetryConfig KEYWORD2
|
|
21
24
|
subConnectionJson KEYWORD2
|
|
22
25
|
asNodeTree KEYWORD2
|
|
23
26
|
|
package/library.json
CHANGED
package/library.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name=Alteriom PainlessMesh
|
|
2
|
-
version=1.
|
|
2
|
+
version=1.10.0
|
|
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.
|
|
3
|
+
"version": "1.10.0",
|
|
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.
|
|
32
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION "1.10.0"
|
|
33
33
|
#define ALTERIOM_PAINLESS_MESH_VERSION_MAJOR 1
|
|
34
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION_MINOR
|
|
35
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH
|
|
34
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION_MINOR 10
|
|
35
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 0
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @brief Library description and usage information
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -817,8 +817,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
817
817
|
* TCP Connection Retry:
|
|
818
818
|
* The TCP connection now includes automatic retry with exponential backoff.
|
|
819
819
|
* If the initial connection fails (error -14 ERR_CONN or other errors),
|
|
820
|
-
* the system will retry up to
|
|
821
|
-
* triggering a full WiFi reconnection cycle
|
|
820
|
+
* the system will retry up to the configured maxRetries times before
|
|
821
|
+
* triggering a full WiFi reconnection cycle (see setTcpRetryConfig()).
|
|
822
|
+
* This helps handle:
|
|
822
823
|
* - Timing issues where TCP server is not ready immediately
|
|
823
824
|
* - Network stack stabilization after IP acquisition
|
|
824
825
|
* - Transient network conditions
|
|
@@ -842,8 +843,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
842
843
|
// This helps prevent error -14 (ERR_CONN) by allowing the network stack
|
|
843
844
|
// and TCP server to be fully ready. The delay is added via task scheduler
|
|
844
845
|
// to avoid blocking the event loop.
|
|
846
|
+
// The delay is read at schedule time, so calling setTcpRetryConfig()
|
|
847
|
+
// while a connect is already pending affects the next attempt, not the
|
|
848
|
+
// in-flight one.
|
|
845
849
|
this->addTask(
|
|
846
|
-
|
|
850
|
+
this->getTcpRetryConfig().stabilizationDelayMs, TASK_ONCE,
|
|
847
851
|
[this, targetIP, targetPort]() {
|
|
848
852
|
// Verify WiFi is still connected after the delay
|
|
849
853
|
if (WiFi.status() != WL_CONNECTED || !WiFi.localIP()) {
|
|
@@ -1962,14 +1966,31 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1962
1966
|
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
1963
1967
|
Log(STARTUP, "[OK] Takeover announcement sent on channel %d\n", _meshChannel);
|
|
1964
1968
|
|
|
1965
|
-
// Save current mesh configuration to restore if bridge init fails
|
|
1969
|
+
// Save current mesh configuration to restore if bridge init fails.
|
|
1970
|
+
// The copies below are captured by value into the deferred lambda so the
|
|
1971
|
+
// stop() call inside it cannot clear/mutate these members before the
|
|
1972
|
+
// reinit reads them. (The Mesh object itself outlives the lambda; this
|
|
1973
|
+
// protects against member mutation, not object lifetime.)
|
|
1974
|
+
// TODO(#373 follow-up): if the mesh owns its scheduler
|
|
1975
|
+
// (!isExternalScheduler), stop() deletes mScheduler and savedScheduler
|
|
1976
|
+
// dangles before initAsBridge() uses it. Pre-existing limitation —
|
|
1977
|
+
// bridge promotion requires a user-supplied scheduler.
|
|
1966
1978
|
uint8_t savedChannel = _meshChannel;
|
|
1979
|
+
TSTRING savedMeshSSID = _meshSSID;
|
|
1980
|
+
TSTRING savedMeshPassword = _meshPassword;
|
|
1981
|
+
TSTRING savedRouterSSID = routerSSID;
|
|
1982
|
+
TSTRING savedRouterPassword = routerPassword;
|
|
1983
|
+
Scheduler *savedScheduler = mScheduler;
|
|
1984
|
+
auto savedBridgeRoleChangedCallback = bridgeRoleChangedCallback;
|
|
1967
1985
|
|
|
1968
1986
|
// CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
|
|
1969
1987
|
// This prevents use-after-free crash when stop() clears taskList while
|
|
1970
1988
|
// evaluateElection() task is still executing
|
|
1971
1989
|
// Use minimal delay to allow current task to complete first
|
|
1972
|
-
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
1990
|
+
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
1991
|
+
[this, savedChannel, savedMeshSSID, savedMeshPassword,
|
|
1992
|
+
savedRouterSSID, savedRouterPassword, savedScheduler,
|
|
1993
|
+
savedBridgeRoleChangedCallback]() {
|
|
1973
1994
|
using namespace logger;
|
|
1974
1995
|
|
|
1975
1996
|
Log(STARTUP, "Executing bridge promotion (stop/reinit cycle)\n");
|
|
@@ -1981,18 +2002,17 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1981
2002
|
|
|
1982
2003
|
// initAsBridge always returns true: bridge mesh functionality is active
|
|
1983
2004
|
// regardless of router connection status (router connection is opportunistic)
|
|
1984
|
-
this->initAsBridge(
|
|
1985
|
-
|
|
2005
|
+
this->initAsBridge(savedMeshSSID, savedMeshPassword, savedRouterSSID,
|
|
2006
|
+
savedRouterPassword, savedScheduler, _meshPort);
|
|
1986
2007
|
|
|
1987
2008
|
lastRoleChangeTime = millis();
|
|
1988
2009
|
|
|
1989
2010
|
Log(STARTUP, "[OK] Bridge promotion complete on channel %d\n", _meshChannel);
|
|
1990
2011
|
|
|
1991
2012
|
// Notify via callback
|
|
1992
|
-
|
|
1993
|
-
if (bridgeRoleChangedCallback) {
|
|
2013
|
+
if (savedBridgeRoleChangedCallback) {
|
|
1994
2014
|
static const TSTRING reason = "Election winner - best router signal";
|
|
1995
|
-
|
|
2015
|
+
savedBridgeRoleChangedCallback(true, reason);
|
|
1996
2016
|
}
|
|
1997
2017
|
|
|
1998
2018
|
// Note: The initial takeover announcement was already sent earlier
|
|
@@ -2065,14 +2085,27 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2065
2085
|
Log(CONNECTION,
|
|
2066
2086
|
"Scheduling stop/reinit (async to avoid task corruption)\n");
|
|
2067
2087
|
|
|
2068
|
-
// Save current mesh configuration
|
|
2088
|
+
// Save current mesh configuration. Captured by value into the deferred
|
|
2089
|
+
// lambda so stop() cannot clear/mutate these members before the reinit
|
|
2090
|
+
// reads them (see the matching comment in promoteToBridge(); same
|
|
2091
|
+
// TODO(#373 follow-up) about savedScheduler and internal schedulers
|
|
2092
|
+
// applies here).
|
|
2069
2093
|
uint8_t savedChannel = _meshChannel;
|
|
2094
|
+
TSTRING savedMeshSSID = _meshSSID;
|
|
2095
|
+
TSTRING savedMeshPassword = _meshPassword;
|
|
2096
|
+
TSTRING savedRouterSSID = routerSSID;
|
|
2097
|
+
TSTRING savedRouterPassword = routerPassword;
|
|
2098
|
+
Scheduler *savedScheduler = mScheduler;
|
|
2099
|
+
auto savedBridgeRoleChangedCallback = bridgeRoleChangedCallback;
|
|
2070
2100
|
|
|
2071
2101
|
// CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
|
|
2072
2102
|
// This prevents use-after-free crash when stop() clears taskList while
|
|
2073
2103
|
// the retry task is still executing
|
|
2074
2104
|
// Use minimal delay to allow current task to complete first
|
|
2075
|
-
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
2105
|
+
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
2106
|
+
[this, savedChannel, savedMeshSSID, savedMeshPassword,
|
|
2107
|
+
savedRouterSSID, savedRouterPassword, savedScheduler,
|
|
2108
|
+
savedBridgeRoleChangedCallback]() {
|
|
2076
2109
|
using namespace logger;
|
|
2077
2110
|
|
|
2078
2111
|
Log(CONNECTION, "Executing isolated bridge promotion (stop/reinit cycle)\n");
|
|
@@ -2084,8 +2117,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2084
2117
|
|
|
2085
2118
|
// initAsBridge always returns true: bridge mesh functionality is active
|
|
2086
2119
|
// regardless of router connection status (router connection is opportunistic)
|
|
2087
|
-
this->initAsBridge(
|
|
2088
|
-
|
|
2120
|
+
this->initAsBridge(savedMeshSSID, savedMeshPassword, savedRouterSSID,
|
|
2121
|
+
savedRouterPassword, savedScheduler, _meshPort);
|
|
2089
2122
|
|
|
2090
2123
|
// Reset retry counter
|
|
2091
2124
|
_isolatedBridgeRetryAttempts = 0;
|
|
@@ -2095,10 +2128,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2095
2128
|
_meshChannel);
|
|
2096
2129
|
|
|
2097
2130
|
// Notify via callback
|
|
2098
|
-
|
|
2099
|
-
if (bridgeRoleChangedCallback) {
|
|
2131
|
+
if (savedBridgeRoleChangedCallback) {
|
|
2100
2132
|
static const TSTRING reason = "Isolated node promoted to bridge";
|
|
2101
|
-
|
|
2133
|
+
savedBridgeRoleChangedCallback(true, reason);
|
|
2102
2134
|
}
|
|
2103
2135
|
|
|
2104
2136
|
// Note: Bridge status announcement will be sent automatically by
|
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
|
-
* @date
|
|
8
|
+
* @version 1.10.0
|
|
9
|
+
* @date 2026-08-12
|
|
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
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
#define _TASK_PRIORITY // Support for layered scheduling priority
|
|
3
3
|
#define _TASK_STD_FUNCTION // Support for std::function (ESP8266 and ESP32)
|
|
4
4
|
// Required for painlessMesh lambda callbacks
|
|
5
|
+
#define _TASK_SELF_DESTRUCT // Scheduler-managed deletion of heap-allocated
|
|
6
|
+
// one-shot tasks. The Scheduler deletes the Task
|
|
7
|
+
// in execute(), after disable() has returned, so
|
|
8
|
+
// a task never has to delete itself from inside
|
|
9
|
+
// its own onDisable callback (which is a
|
|
10
|
+
// use-after-free: Task::disable() writes to the
|
|
11
|
+
// task object after onDisable returns). Used by
|
|
12
|
+
// scheduleAsyncClientDeletion() in
|
|
13
|
+
// connection.hpp. See issue #373.
|
|
5
14
|
|
|
6
15
|
// NOTE: _TASK_THREAD_SAFE is currently DISABLED due to incompatibility
|
|
7
16
|
// with _TASK_STD_FUNCTION in TaskScheduler v4.0.x
|
|
@@ -37,7 +37,10 @@ class ReceiveBuffer {
|
|
|
37
37
|
do {
|
|
38
38
|
auto len = strnlen(data_ptr, length);
|
|
39
39
|
do {
|
|
40
|
-
|
|
40
|
+
// Reserve one byte for the '\0' terminator below: read_len may be at
|
|
41
|
+
// most buf.length - 1, otherwise buf.buffer[read_len] writes one
|
|
42
|
+
// byte past the end of the buffer (caught by the ASan CI job).
|
|
43
|
+
auto read_len = (std::min)(len, buf.length - 1);
|
|
41
44
|
memcpy(buf.buffer, data_ptr, read_len);
|
|
42
45
|
buf.buffer[read_len] = '\0';
|
|
43
46
|
auto newBuffer = T(buf.buffer);
|
|
@@ -26,10 +26,21 @@
|
|
|
26
26
|
// Enable OTA support
|
|
27
27
|
#define PAINLESSMESH_ENABLE_OTA
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// NOTE: `MIN_FREE_MEMORY` and `MAX_MESSAGE_QUEUE` are kept as deprecated
|
|
30
|
+
// no-op compatibility macros. The library does not read either macro:
|
|
31
|
+
// the auto-flushing message queue they were meant to tune never landed
|
|
32
|
+
// (see #385, PR #383 review). `MessageQueue`
|
|
33
|
+
// (`painlessmesh/message_queue.hpp`) is a manual priority buffer with
|
|
34
|
+
// its own per-instance `maxSize` argument. Their historical default
|
|
35
|
+
// values are preserved so downstream code that referenced them keeps
|
|
36
|
+
// its prior behavior.
|
|
37
|
+
#ifndef MIN_FREE_MEMORY
|
|
30
38
|
#define MIN_FREE_MEMORY 4000
|
|
31
|
-
|
|
39
|
+
#endif
|
|
40
|
+
|
|
41
|
+
#ifndef MAX_MESSAGE_QUEUE
|
|
32
42
|
#define MAX_MESSAGE_QUEUE 50
|
|
43
|
+
#endif
|
|
33
44
|
|
|
34
45
|
#define NODE_TIMEOUT 10 * TASK_SECOND
|
|
35
46
|
#define SCAN_INTERVAL 30 * TASK_SECOND // AP scan period in ms
|
|
@@ -123,12 +123,17 @@ inline void scheduleAsyncClientDeletion(Scheduler* scheduler, AsyncClient* clien
|
|
|
123
123
|
delete client;
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
// Task cleanup (issue #373): the task must NOT delete itself from inside
|
|
127
|
+
// its own onDisable callback. TaskScheduler's Task::disable() keeps
|
|
128
|
+
// writing to the task object (iScheduler->iCurrent) after onDisable
|
|
129
|
+
// returns, so a self-delete there is a guaranteed use-after-free — this
|
|
130
|
+
// was crashing nodes on every connection teardown.
|
|
131
|
+
// Instead we mark the task self-destructing (_TASK_SELF_DESTRUCT, enabled
|
|
132
|
+
// in painlessTaskOptions.h): after the single iteration completes, the
|
|
133
|
+
// Scheduler itself disables the task, unlinks it from its chain, and
|
|
134
|
+
// deletes it — all from within Scheduler::execute(), safely outside the
|
|
135
|
+
// disable() call stack.
|
|
136
|
+
cleanupTask->setSelfDestruct(true);
|
|
132
137
|
|
|
133
138
|
scheduler->addTask(*cleanupTask);
|
|
134
139
|
cleanupTask->enableDelayed();
|
|
@@ -160,13 +165,28 @@ class BufferedConnection
|
|
|
160
165
|
using namespace logger;
|
|
161
166
|
Log.remote("~BufferedConnection");
|
|
162
167
|
this->close();
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
// Always call client->close() here, unconditionally - do NOT guard this
|
|
169
|
+
// behind client->freeable(). freeable() can return true while _pcb is
|
|
170
|
+
// still a valid, non-null pointer (e.g. pcb->state == CLOSED but not yet
|
|
171
|
+
// reclaimed by lwIP). If we skip close() in that case, _pcb stays
|
|
172
|
+
// non-null for the entire TCP_CLIENT_CLEANUP_DELAY_MS+ deferred-deletion
|
|
173
|
+
// window below - during which lwIP's own internal timers (e.g. TIME_WAIT
|
|
174
|
+
// expiry) can silently free/recycle that pcb without ever notifying
|
|
175
|
+
// AsyncClient (the tcp_err callback only fires on abnormal termination,
|
|
176
|
+
// not on routine timer-driven pcb reclamation). The deferred delete then
|
|
177
|
+
// finds a stale-but-non-null _pcb and tries to close/free it a second
|
|
178
|
+
// time, corrupting the heap (observed as heap_caps_free/memp_free
|
|
179
|
+
// assertion failures and wild-pointer crashes inside tcp_arg(), all
|
|
180
|
+
// several seconds after the connection actually died).
|
|
181
|
+
// close() internally handles "nothing to do" safely (AsyncTCP checks
|
|
182
|
+
// _pcb/*pcb before touching lwIP state), and reliably nulls _pcb via its
|
|
183
|
+
// synchronous tcpip_api_call round-trip - so calling it unconditionally
|
|
184
|
+
// here, right at destruction time, closes this exposure window entirely.
|
|
185
|
+
client->close();
|
|
166
186
|
// Note: client->abort() removed - calling it before deferred deletion
|
|
167
187
|
// can leave the client in an inconsistent state where AsyncTCP is still
|
|
168
|
-
// trying to clean up the aborted connection. The close()
|
|
169
|
-
//
|
|
188
|
+
// trying to clean up the aborted connection. The close() call above is
|
|
189
|
+
// sufficient for connection termination.
|
|
170
190
|
// See: AsyncTCP best practices - abort() should only be called immediately
|
|
171
191
|
// before delete, not before a deferred deletion.
|
|
172
192
|
|
|
@@ -344,7 +344,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
344
344
|
(*conn)->close();
|
|
345
345
|
this->eraseClosedConnections();
|
|
346
346
|
}
|
|
347
|
-
plugin::PackageHandler<T>::stop();
|
|
347
|
+
plugin::PackageHandler<T>::stop(mScheduler);
|
|
348
348
|
|
|
349
349
|
newConnectionCallbacks.clear();
|
|
350
350
|
droppedConnectionCallbacks.clear();
|
|
@@ -1394,6 +1394,32 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1394
1394
|
internetRetryDelay = delayMs;
|
|
1395
1395
|
}
|
|
1396
1396
|
|
|
1397
|
+
/**
|
|
1398
|
+
* Configure TCP connection retry behaviour
|
|
1399
|
+
*
|
|
1400
|
+
* Lets latency-sensitive, high-reliability or battery-powered deployments
|
|
1401
|
+
* pick their own retry envelope. Values outside safe operating bounds are
|
|
1402
|
+
* clamped - see painlessmesh::tcp::clampTcpRetryConfig(). Read the applied
|
|
1403
|
+
* (post-clamp) values back with getTcpRetryConfig().
|
|
1404
|
+
*
|
|
1405
|
+
* The defaults reproduce the previous hardcoded behaviour exactly, so
|
|
1406
|
+
* existing sketches that never call this see no change.
|
|
1407
|
+
*
|
|
1408
|
+
* @param config Retry parameters to apply
|
|
1409
|
+
*/
|
|
1410
|
+
void setTcpRetryConfig(const painlessmesh::tcp::TcpRetryConfig &config) {
|
|
1411
|
+
tcpRetryConfig = painlessmesh::tcp::clampTcpRetryConfig(config);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Get the active TCP retry configuration
|
|
1416
|
+
*
|
|
1417
|
+
* @return The configuration currently in effect, after clamping
|
|
1418
|
+
*/
|
|
1419
|
+
painlessmesh::tcp::TcpRetryConfig getTcpRetryConfig() const {
|
|
1420
|
+
return tcpRetryConfig;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1397
1423
|
/**
|
|
1398
1424
|
* Get number of pending Internet requests
|
|
1399
1425
|
*
|
|
@@ -2060,16 +2086,34 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2060
2086
|
//
|
|
2061
2087
|
|
|
2062
2088
|
/**
|
|
2063
|
-
* Enable or disable message
|
|
2064
|
-
*
|
|
2065
|
-
* When enabled,
|
|
2066
|
-
*
|
|
2067
|
-
*
|
|
2089
|
+
* Enable or disable the manual message queue for offline mode
|
|
2090
|
+
*
|
|
2091
|
+
* When enabled, allocates a `MessageQueue` that your application can
|
|
2092
|
+
* push into while an upstream (MQTT/HTTP/etc.) is unreachable. The
|
|
2093
|
+
* mesh does NOT flush the queue automatically and does NOT observe
|
|
2094
|
+
* connectivity changes — the app is responsible for detecting when
|
|
2095
|
+
* to drain it via `flushMessageQueue()` + `removeQueuedMessage()`.
|
|
2096
|
+
*
|
|
2097
|
+
* Wire it to your own connectivity signal, for example
|
|
2098
|
+
* `onLocalInternetChanged` on the bridge node or
|
|
2099
|
+
* `onBridgeStatusChanged` on downstream nodes.
|
|
2100
|
+
*
|
|
2068
2101
|
* @param enabled True to enable queueing, false to disable
|
|
2069
2102
|
* @param maxSize Maximum number of messages in queue (default 1000)
|
|
2070
|
-
*
|
|
2103
|
+
*
|
|
2071
2104
|
* \code
|
|
2072
2105
|
* mesh.enableMessageQueue(true, 500); // Enable with 500 message capacity
|
|
2106
|
+
*
|
|
2107
|
+
* mesh.onLocalInternetChanged([&mesh](bool available) {
|
|
2108
|
+
* if (available) {
|
|
2109
|
+
* auto messages = mesh.flushMessageQueue();
|
|
2110
|
+
* for (auto& msg : messages) {
|
|
2111
|
+
* if (sendToCloud(msg.payload, msg.destination)) {
|
|
2112
|
+
* mesh.removeQueuedMessage(msg.id);
|
|
2113
|
+
* }
|
|
2114
|
+
* }
|
|
2115
|
+
* }
|
|
2116
|
+
* });
|
|
2073
2117
|
* \endcode
|
|
2074
2118
|
*/
|
|
2075
2119
|
void enableMessageQueue(bool enabled, uint32_t maxSize = 1000) {
|
|
@@ -2084,11 +2128,13 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2084
2128
|
}
|
|
2085
2129
|
|
|
2086
2130
|
/**
|
|
2087
|
-
* Queue a message with priority for later delivery
|
|
2088
|
-
*
|
|
2089
|
-
* Use this to
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2131
|
+
* Queue a message with priority for later manual delivery
|
|
2132
|
+
*
|
|
2133
|
+
* Use this to buffer critical messages when your upstream
|
|
2134
|
+
* (MQTT/HTTP/etc.) is unavailable. The library does NOT deliver these
|
|
2135
|
+
* on its own — your app must call `flushMessageQueue()` and
|
|
2136
|
+
* `removeQueuedMessage()` once its own connectivity check clears.
|
|
2137
|
+
*
|
|
2092
2138
|
* @param payload Message content to queue
|
|
2093
2139
|
* @param destination Optional destination metadata (e.g., MQTT topic, HTTP endpoint)
|
|
2094
2140
|
* @param priority Message priority (default: PRIORITY_NORMAL)
|
|
@@ -2115,16 +2161,14 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2115
2161
|
}
|
|
2116
2162
|
|
|
2117
2163
|
/**
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2121
|
-
*
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2124
|
-
*
|
|
2125
|
-
*
|
|
2126
|
-
* and calling removeQueuedMessage() when successful.
|
|
2127
|
-
*
|
|
2164
|
+
* Return a snapshot of all queued messages for manual delivery
|
|
2165
|
+
*
|
|
2166
|
+
* This does NOT transmit anything and does NOT clear the queue. The
|
|
2167
|
+
* application is responsible for actually sending each message and
|
|
2168
|
+
* calling `removeQueuedMessage(id)` for each one that succeeded.
|
|
2169
|
+
* Messages left in the queue remain available for retry on the next
|
|
2170
|
+
* flush.
|
|
2171
|
+
*
|
|
2128
2172
|
* @return Vector of queued messages to send
|
|
2129
2173
|
*
|
|
2130
2174
|
* \code
|
|
@@ -3262,6 +3306,13 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
3262
3306
|
uint32_t internetRetryDelay = 1000; // Default 1 second base delay
|
|
3263
3307
|
bool sendToInternetEnabled = false;
|
|
3264
3308
|
|
|
3309
|
+
// TCP connect retry parameters, see setTcpRetryConfig().
|
|
3310
|
+
// Public because tcp::connect<Connection, wifi::Mesh> reads it but is NOT a
|
|
3311
|
+
// friend: the friend declaration below names connect<T, Mesh<T>>, whereas
|
|
3312
|
+
// wifi::Mesh::tcpConnect() instantiates connect<Connection, wifi::Mesh> - a
|
|
3313
|
+
// different specialisation. Same reason as the public: sections above.
|
|
3314
|
+
painlessmesh::tcp::TcpRetryConfig tcpRetryConfig;
|
|
3315
|
+
|
|
3265
3316
|
friend T;
|
|
3266
3317
|
friend void onDataCb(void *, AsyncClient *, void *, size_t);
|
|
3267
3318
|
friend void tcpSentCb(void *, AsyncClient *, size_t, uint32_t);
|