@alteriom/painlessmesh 1.9.20 → 2.0.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/BRIDGE_TO_INTERNET.md +167 -29
- package/CHANGELOG.md +604 -0
- package/CONTRIBUTING.md +56 -53
- package/README.md +100 -75
- package/RELEASE_GUIDE.md +81 -641
- package/examples/alteriom/README.md +8 -10
- package/examples/alteriom/alteriom.ino +2 -2
- package/examples/alteriom/alteriom_custom_package_template.hpp +11 -11
- package/examples/alteriom/alteriom_sensor_package.hpp +17 -11
- package/examples/alteriom/mppt_example/alteriom_custom_package_template.hpp +320 -0
- package/examples/alteriom/mppt_example/alteriom_sensor_package.hpp +1389 -0
- package/examples/alteriom/mppt_example/{alteriom_mppt_example.ino → mppt_example.ino} +5 -1
- package/examples/basic/test/simulator/README.md +3 -3
- package/examples/bridge_failover/README.md +51 -14
- package/examples/bridge_failover/bridge_failover.ino +2 -2
- package/examples/commandControl/commandControl.ino +86 -0
- package/examples/commandControl/platformio.ini +26 -0
- package/examples/mqttBridge/mqttBridge.ino +4 -0
- package/examples/mqttBridge/platformio.ini +1 -1
- package/examples/otaSender/otaSender.ino +5 -1
- package/examples/priority/README.md +1 -1
- package/examples/priority/{priority_basic_example.ino → priority_basic_example/priority_basic_example.ino} +4 -4
- package/examples/priority/{priority_with_queue.ino → priority_with_queue/priority_with_queue.ino} +20 -2
- package/examples/reliableSensorLogging/platformio.ini +26 -0
- package/examples/reliableSensorLogging/reliableSensorLogging.ino +151 -0
- package/examples/sendToInternet/README.md +12 -5
- package/examples/sendToInternet/{CMakeLists.txt → pc_node/CMakeLists.txt} +7 -7
- package/examples/sendToInternet/{PC_NODE_README.md → pc_node/PC_NODE_README.md} +15 -15
- package/examples/sendToInternet/{build.sh → pc_node/build.sh} +5 -5
- package/examples/sendToInternet/{pc_mesh_node.cpp → pc_node/pc_mesh_node.cpp} +12 -1
- package/examples/sharedGateway/README.md +1 -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 +53 -1
- package/library.json +8 -6
- package/library.properties +2 -2
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +4 -4
- package/src/arduino/wifi.hpp +605 -143
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +607 -87
- package/src/painlessMeshSTA.h +135 -3
- package/src/painlessTaskOptions.h +9 -0
- package/src/painlessmesh/ack.hpp +283 -0
- package/src/painlessmesh/buffer.hpp +74 -9
- package/src/painlessmesh/callback.hpp +38 -5
- package/src/painlessmesh/configuration.hpp +82 -3
- package/src/painlessmesh/connection.hpp +43 -16
- package/src/painlessmesh/gateway.hpp +270 -5
- package/src/painlessmesh/layout.hpp +70 -2
- package/src/painlessmesh/logger.hpp +15 -0
- package/src/painlessmesh/mesh.hpp +625 -70
- package/src/painlessmesh/message_queue.hpp +24 -13
- package/src/painlessmesh/ntp.hpp +2 -4
- package/src/painlessmesh/plugin.hpp +52 -6
- package/src/painlessmesh/protocol.hpp +55 -2
- package/src/painlessmesh/router.hpp +192 -77
- package/src/painlessmesh/tcp.hpp +168 -29
- package/src/painlessmesh/message_tracker.hpp +0 -311
- /package/examples/sendToInternet/{mock_server_test.ino → mock_server_test/mock_server_test.ino} +0 -0
package/src/painlessmesh/tcp.hpp
CHANGED
|
@@ -30,6 +30,84 @@ static const uint32_t TCP_EXHAUSTION_RECONNECT_DELAY_MS = 10000; // 10 seconds b
|
|
|
30
30
|
// This prevents repeatedly trying to connect to nodes with unresponsive TCP servers
|
|
31
31
|
static const uint32_t TCP_FAILURE_BLOCK_DURATION_MS = 60000;
|
|
32
32
|
|
|
33
|
+
// Safe operating bounds applied by clampTcpRetryConfig().
|
|
34
|
+
// Each retry allocates a new AsyncClient and schedules a task, so an unbounded
|
|
35
|
+
// maxRetries is a heap and recursion-depth hazard on ESP8266. A zero retry
|
|
36
|
+
// delay would schedule retry tasks with no spacing, i.e. a hot loop allocating
|
|
37
|
+
// an AsyncClient per scheduler tick.
|
|
38
|
+
static const uint8_t TCP_RETRY_MAX_RETRIES_LIMIT = 10;
|
|
39
|
+
static const uint32_t TCP_RETRY_MIN_DELAY_MS = 50;
|
|
40
|
+
static const uint32_t TCP_RETRY_MAX_DELAY_MS = 60000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* User-tunable TCP connection retry parameters
|
|
44
|
+
*
|
|
45
|
+
* One instance is stored per mesh (painlessmesh::Mesh), configured through
|
|
46
|
+
* Mesh::setTcpRetryConfig(). The member defaults are spelled as the legacy
|
|
47
|
+
* constants above rather than as literals, so the "defaults match the previous
|
|
48
|
+
* hardcoded behaviour exactly" guarantee is enforced by the compiler and cannot
|
|
49
|
+
* silently drift.
|
|
50
|
+
*
|
|
51
|
+
* NOTE: this deliberately lives on the mesh instance rather than at namespace
|
|
52
|
+
* scope. A mutable namespace-scope instance in this header would give every
|
|
53
|
+
* translation unit its own private copy, so a sketch that configured the mesh
|
|
54
|
+
* in one TU and connected from another would silently fall back to defaults
|
|
55
|
+
* with no diagnostic.
|
|
56
|
+
*/
|
|
57
|
+
struct TcpRetryConfig {
|
|
58
|
+
/// Max TCP connect retry attempts before falling back to a WiFi reconnect
|
|
59
|
+
uint8_t maxRetries = TCP_CONNECT_MAX_RETRIES;
|
|
60
|
+
/// Base delay between retry attempts (ms), scaled by exponential backoff
|
|
61
|
+
uint32_t retryDelayMs = TCP_CONNECT_RETRY_DELAY_MS;
|
|
62
|
+
/// Delay after IP acquisition before the TCP connect is attempted (ms)
|
|
63
|
+
uint32_t stabilizationDelayMs = TCP_CONNECT_STABILIZATION_DELAY_MS;
|
|
64
|
+
/// Delay before the WiFi reconnect that follows retry exhaustion (ms)
|
|
65
|
+
uint32_t exhaustionReconnectDelayMs = TCP_EXHAUSTION_RECONNECT_DELAY_MS;
|
|
66
|
+
/// Duration a node is blocklisted after retry exhaustion (ms, 0 = never)
|
|
67
|
+
uint32_t failureBlockDurationMs = TCP_FAILURE_BLOCK_DURATION_MS;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Delay before retry attempt number `retryCount`
|
|
72
|
+
*
|
|
73
|
+
* Exponential backoff with the multiplier capped at 8, giving the default
|
|
74
|
+
* sequence 1s, 2s, 4s, 8s, 8s. Capping prevents excessive delays and keeps the
|
|
75
|
+
* product clear of uint32_t overflow.
|
|
76
|
+
*
|
|
77
|
+
* @param cfg Active retry configuration
|
|
78
|
+
* @param retryCount Zero-based retry attempt number
|
|
79
|
+
* @return Delay in milliseconds
|
|
80
|
+
*/
|
|
81
|
+
inline uint32_t retryBackoffDelay(const TcpRetryConfig &cfg,
|
|
82
|
+
uint8_t retryCount) {
|
|
83
|
+
uint8_t backoffMultiplier = (retryCount < 3) ? (1U << retryCount) : 8;
|
|
84
|
+
return cfg.retryDelayMs * backoffMultiplier;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Coerce a retry configuration into safe operating bounds
|
|
89
|
+
*
|
|
90
|
+
* Only the two values that can render a node unusable are clamped:
|
|
91
|
+
* maxRetries (heap/recursion pressure) and retryDelayMs (hot-loop floor and
|
|
92
|
+
* overflow ceiling). maxRetries == 0 is explicitly allowed - it means "fall
|
|
93
|
+
* back to a WiFi reconnect on the first TCP error", which is the whole point
|
|
94
|
+
* of the low-latency profile. stabilizationDelayMs, exhaustionReconnectDelayMs
|
|
95
|
+
* and failureBlockDurationMs are left alone because 0 is meaningful for each
|
|
96
|
+
* (skip stabilization / reconnect immediately / never blocklist).
|
|
97
|
+
*
|
|
98
|
+
* @param cfg Configuration to clamp (taken by value, returned clamped)
|
|
99
|
+
* @return The clamped configuration
|
|
100
|
+
*/
|
|
101
|
+
inline TcpRetryConfig clampTcpRetryConfig(TcpRetryConfig cfg) {
|
|
102
|
+
if (cfg.maxRetries > TCP_RETRY_MAX_RETRIES_LIMIT)
|
|
103
|
+
cfg.maxRetries = TCP_RETRY_MAX_RETRIES_LIMIT;
|
|
104
|
+
if (cfg.retryDelayMs < TCP_RETRY_MIN_DELAY_MS)
|
|
105
|
+
cfg.retryDelayMs = TCP_RETRY_MIN_DELAY_MS;
|
|
106
|
+
if (cfg.retryDelayMs > TCP_RETRY_MAX_DELAY_MS)
|
|
107
|
+
cfg.retryDelayMs = TCP_RETRY_MAX_DELAY_MS;
|
|
108
|
+
return cfg;
|
|
109
|
+
}
|
|
110
|
+
|
|
33
111
|
inline uint32_t encodeNodeId(const uint8_t *hwaddr) {
|
|
34
112
|
using namespace painlessmesh::logger;
|
|
35
113
|
Log(GENERAL, "encodeNodeId():\n");
|
|
@@ -82,7 +160,17 @@ void initServer(AsyncServer &server, M &mesh) {
|
|
|
82
160
|
conn->initTasks();
|
|
83
161
|
mesh.subs.push_back(conn);
|
|
84
162
|
mesh.semaphoreGive();
|
|
163
|
+
return;
|
|
85
164
|
}
|
|
165
|
+
// The mesh was busy for the whole second the semaphore allows. The
|
|
166
|
+
// stack has accepted the client already; left like this it is
|
|
167
|
+
// neither the mesh's nor closed, and the peer waits on a link that
|
|
168
|
+
// will never carry a node sync. Say so, and close it, so the peer
|
|
169
|
+
// tries again at once rather than after its own timeout.
|
|
170
|
+
Log(ERROR,
|
|
171
|
+
"New AP connection refused: mesh busy, closing it so the peer "
|
|
172
|
+
"retries\n");
|
|
173
|
+
client->close(true);
|
|
86
174
|
},
|
|
87
175
|
NULL);
|
|
88
176
|
server.begin();
|
|
@@ -93,8 +181,9 @@ void initServer(AsyncServer &server, M &mesh) {
|
|
|
93
181
|
*
|
|
94
182
|
* This function attempts to connect to the mesh network via TCP.
|
|
95
183
|
* If the connection fails (error -14 ERR_CONN or other errors), it will
|
|
96
|
-
* retry up to
|
|
97
|
-
* WiFi reconnection cycle.
|
|
184
|
+
* retry up to the configured maxRetries times before triggering a full
|
|
185
|
+
* WiFi reconnection cycle. See Mesh::setTcpRetryConfig() to tune the retry
|
|
186
|
+
* envelope; the defaults reproduce the historic hardcoded behaviour.
|
|
98
187
|
*
|
|
99
188
|
* The retry mechanism helps handle timing issues where:
|
|
100
189
|
* - The TCP server may not be immediately ready after AP initialization
|
|
@@ -117,16 +206,49 @@ template <class T, class M>
|
|
|
117
206
|
void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
118
207
|
uint8_t retryCount = 0) {
|
|
119
208
|
using namespace logger;
|
|
120
|
-
|
|
209
|
+
|
|
210
|
+
// Snapshot the retry configuration once, before any lambda is built. The
|
|
211
|
+
// onError lambda outlives this stack frame, so it captures this snapshot by
|
|
212
|
+
// value rather than reading mesh's member later: a user callback could
|
|
213
|
+
// otherwise mutate the config between the connect attempt and the error,
|
|
214
|
+
// tearing the backoff schedule mid-sequence. Each retry re-enters connect()
|
|
215
|
+
// and re-snapshots, so a config change still takes effect on the next
|
|
216
|
+
// attempt.
|
|
217
|
+
const TcpRetryConfig cfg = mesh.getTcpRetryConfig();
|
|
218
|
+
|
|
121
219
|
Log(CONNECTION, "tcp::connect(): Attempting connection to port %d (attempt %d/%d)\n",
|
|
122
|
-
port, retryCount + 1,
|
|
220
|
+
port, retryCount + 1, cfg.maxRetries + 1);
|
|
221
|
+
|
|
222
|
+
// Guard shared between onError and onConnect: under normal conditions a
|
|
223
|
+
// TCP connection attempt leads to only ONE of the two outcomes. But if
|
|
224
|
+
// WiFi drops right as the TCP handshake completes, AsyncTCP can queue
|
|
225
|
+
// BOTH events (connection succeeded at the TCP level + abort due to the
|
|
226
|
+
// WiFi link being lost) for the same AsyncClient, and both callbacks end
|
|
227
|
+
// up firing for the SAME object. Without this guard, the client would be
|
|
228
|
+
// "handed over" twice: once to a BufferedConnection (via onConnect, which
|
|
229
|
+
// becomes its owner and will delete it via its own destructor) and once
|
|
230
|
+
// to the retry path (via onError, which schedules it again for deletion)
|
|
231
|
+
// - two scheduleAsyncClientDeletion() calls on the same pointer, and
|
|
232
|
+
// therefore a double deferred deletion on the same object once both
|
|
233
|
+
// tasks fire.
|
|
234
|
+
auto claimed = std::make_shared<bool>(false);
|
|
123
235
|
|
|
124
236
|
// Store retry count and connection parameters for the error handler
|
|
125
237
|
// We need to capture these by value since they're used in the lambda
|
|
126
|
-
client.onError([&mesh, ip, port, retryCount](void *, AsyncClient *client, int8_t err) {
|
|
238
|
+
client.onError([&mesh, ip, port, retryCount, claimed, cfg](void *, AsyncClient *client, int8_t err) {
|
|
239
|
+
if (*claimed) {
|
|
240
|
+
// onConnect has already claimed this client (the connection actually
|
|
241
|
+
// succeeded, and it's already been wrapped in a BufferedConnection
|
|
242
|
+
// that owns it): don't touch the same object again.
|
|
243
|
+
Log(CONNECTION,
|
|
244
|
+
"tcp_err(): onError fired after onConnect had already claimed "
|
|
245
|
+
"the client - ignored to avoid double handling\n");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
*claimed = true;
|
|
127
249
|
if (mesh.semaphoreTake()) {
|
|
128
|
-
Log(CONNECTION, "tcp_err(): error trying to connect %d (attempt %d/%d)\n",
|
|
129
|
-
err, retryCount + 1,
|
|
250
|
+
Log(CONNECTION, "tcp_err(): error trying to connect %d (attempt %d/%d)\n",
|
|
251
|
+
err, retryCount + 1, cfg.maxRetries + 1);
|
|
130
252
|
|
|
131
253
|
// Check if we have retries left - retry logic only works on real hardware
|
|
132
254
|
// In test environment (PAINLESSMESH_BOOST), fall through to dropped connection
|
|
@@ -134,27 +256,21 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
134
256
|
(void)ip;
|
|
135
257
|
(void)port;
|
|
136
258
|
#if !defined(PAINLESSMESH_BOOST) && (defined(ESP32) || defined(ESP8266))
|
|
137
|
-
if (retryCount <
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// - retryCount=3: 1000ms * 8 = 8s (capped at 8)
|
|
144
|
-
// - retryCount=4: 1000ms * 8 = 8s (capped at 8)
|
|
145
|
-
// Cap multiplier at 8 to prevent excessive delays
|
|
146
|
-
uint8_t backoffMultiplier = (retryCount < 3) ? (1U << retryCount) : 8;
|
|
147
|
-
uint32_t retryDelay = TCP_CONNECT_RETRY_DELAY_MS * backoffMultiplier;
|
|
148
|
-
|
|
149
|
-
Log(CONNECTION, "tcp_err(): Scheduling retry in %u ms (backoff x%d)\n",
|
|
150
|
-
retryDelay, backoffMultiplier);
|
|
259
|
+
if (retryCount < cfg.maxRetries) {
|
|
260
|
+
// Delay grows with exponential backoff, multiplier capped at 8.
|
|
261
|
+
// With the default 1000ms base that is: 1s, 2s, 4s, 8s, 8s.
|
|
262
|
+
uint32_t retryDelay = retryBackoffDelay(cfg, retryCount);
|
|
263
|
+
|
|
264
|
+
Log(CONNECTION, "tcp_err(): Scheduling retry in %u ms\n", retryDelay);
|
|
151
265
|
|
|
152
266
|
// Schedule a retry after a delay using the mesh's task scheduler
|
|
153
267
|
// Note: &mesh is captured by reference because:
|
|
154
268
|
// 1. Mesh is a singleton that lives for the program's lifetime
|
|
155
269
|
// 2. The task scheduler belongs to the mesh, so mesh is always valid when task runs
|
|
156
270
|
// 3. Copying the mesh object is not possible/allowed
|
|
157
|
-
// Recursion depth is strictly bounded by
|
|
271
|
+
// Recursion depth is strictly bounded by cfg.maxRetries (default 5),
|
|
272
|
+
// which clampTcpRetryConfig() never lets exceed
|
|
273
|
+
// TCP_RETRY_MAX_RETRIES_LIMIT (10).
|
|
158
274
|
mesh.addTask([&mesh, ip, port, retryCount]() {
|
|
159
275
|
Log(CONNECTION, "tcp_err(): Retrying TCP connection...\n");
|
|
160
276
|
|
|
@@ -178,7 +294,7 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
178
294
|
// Adding a significant delay before reconnection prevents rapid reconnection loops
|
|
179
295
|
// when the TCP server is persistently unavailable or overloaded
|
|
180
296
|
Log(CONNECTION, "tcp_err(): All %d retries exhausted for IP %s\n",
|
|
181
|
-
|
|
297
|
+
cfg.maxRetries + 1, ip.toString().c_str());
|
|
182
298
|
|
|
183
299
|
// Block this node temporarily to prevent immediate reconnection to the same unresponsive node
|
|
184
300
|
// This helps when the bridge's TCP server is down but WiFi AP is still running
|
|
@@ -186,15 +302,19 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
186
302
|
#if !defined(PAINLESSMESH_BOOST)
|
|
187
303
|
// Try to decode nodeId from IP and block it
|
|
188
304
|
// Only works for mesh IPs (format: 10.x.x.1)
|
|
305
|
+
// A failureBlockDurationMs of 0 is an explicit "never blocklist" opt-out.
|
|
306
|
+
// Passing it through would set blockUntil = millis() + 0, which
|
|
307
|
+
// isNodeBlocked() reads as already expired - so the entry would do
|
|
308
|
+
// nothing except linger in the blocklist until cleanup sweeps it.
|
|
189
309
|
uint32_t failedNodeId = decodeNodeIdFromIP(ip);
|
|
190
|
-
if (failedNodeId != 0) {
|
|
310
|
+
if (failedNodeId != 0 && cfg.failureBlockDurationMs > 0) {
|
|
191
311
|
// Note: This requires M to be wifi::Mesh which has blockNodeAfterTCPFailure
|
|
192
|
-
mesh.blockNodeAfterTCPFailure(ip,
|
|
312
|
+
mesh.blockNodeAfterTCPFailure(ip, cfg.failureBlockDurationMs);
|
|
193
313
|
}
|
|
194
314
|
#endif
|
|
195
|
-
|
|
315
|
+
|
|
196
316
|
Log(CONNECTION, "tcp_err(): Scheduling WiFi reconnection in %u ms\n",
|
|
197
|
-
|
|
317
|
+
cfg.exhaustionReconnectDelayMs);
|
|
198
318
|
|
|
199
319
|
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
200
320
|
// Use the centralized deletion scheduler to ensure proper spacing between deletions
|
|
@@ -208,13 +328,32 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
208
328
|
mesh.addTask([&mesh]() {
|
|
209
329
|
Log(CONNECTION, "tcp_err(): Executing delayed WiFi reconnection after retry exhaustion\n");
|
|
210
330
|
mesh.droppedConnectionCallbacks.execute(0, true);
|
|
211
|
-
},
|
|
331
|
+
}, cfg.exhaustionReconnectDelayMs);
|
|
212
332
|
mesh.semaphoreGive();
|
|
213
333
|
}
|
|
214
334
|
});
|
|
215
335
|
|
|
216
336
|
client.onConnect(
|
|
217
|
-
[&mesh](void *, AsyncClient *client) {
|
|
337
|
+
[&mesh, claimed](void *, AsyncClient *client) {
|
|
338
|
+
if (*claimed) {
|
|
339
|
+
// onError has already fired for this client (e.g. an abort
|
|
340
|
+
// caused by losing WiFi right while the TCP handshake was
|
|
341
|
+
// completing): the client is already scheduled for deletion by
|
|
342
|
+
// the retry path. Wrapping it in a new BufferedConnection now
|
|
343
|
+
// would give it two owners at once. This connection did however
|
|
344
|
+
// genuinely succeed at the TCP level (that's why onConnect
|
|
345
|
+
// fired): close it explicitly here, so that when the deferred
|
|
346
|
+
// deletion already scheduled by onError fires, it finds a
|
|
347
|
+
// properly closed client instead of one still "connected" -
|
|
348
|
+
// otherwise we'd fall right back into the bug we're fixing.
|
|
349
|
+
Log(CONNECTION,
|
|
350
|
+
"tcp::connect(): onConnect fired after onError had already "
|
|
351
|
+
"claimed the client - closing the connection and discarding "
|
|
352
|
+
"it\n");
|
|
353
|
+
client->close();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
*claimed = true;
|
|
218
357
|
if (mesh.semaphoreTake()) {
|
|
219
358
|
Log(CONNECTION, "New STA connection incoming\n");
|
|
220
359
|
auto conn = std::make_shared<T>(client, &mesh, true);
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
#ifndef _PAINLESS_MESH_MESSAGE_TRACKER_HPP_
|
|
2
|
-
#define _PAINLESS_MESH_MESSAGE_TRACKER_HPP_
|
|
3
|
-
|
|
4
|
-
#include <map>
|
|
5
|
-
#include <algorithm>
|
|
6
|
-
#include "painlessmesh/configuration.hpp"
|
|
7
|
-
#include "painlessmesh/logger.hpp"
|
|
8
|
-
|
|
9
|
-
// External logger instance
|
|
10
|
-
extern painlessmesh::logger::LogClass Log;
|
|
11
|
-
|
|
12
|
-
namespace painlessmesh {
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Key for tracking messages by their unique combination of ID and origin node
|
|
16
|
-
*/
|
|
17
|
-
struct MessageKey {
|
|
18
|
-
uint32_t messageId;
|
|
19
|
-
uint32_t originNode;
|
|
20
|
-
|
|
21
|
-
bool operator<(const MessageKey& other) const {
|
|
22
|
-
if (messageId != other.messageId) {
|
|
23
|
-
return messageId < other.messageId;
|
|
24
|
-
}
|
|
25
|
-
return originNode < other.originNode;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
bool operator==(const MessageKey& other) const {
|
|
29
|
-
return messageId == other.messageId && originNode == other.originNode;
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Tracked message entry with timestamp and acknowledgment status
|
|
35
|
-
*/
|
|
36
|
-
struct TrackedMessage {
|
|
37
|
-
uint32_t timestamp; // When the message was first processed (millis)
|
|
38
|
-
bool acknowledged; // Whether the message has been acknowledged
|
|
39
|
-
|
|
40
|
-
TrackedMessage() : timestamp(0), acknowledged(false) {}
|
|
41
|
-
TrackedMessage(uint32_t ts) : timestamp(ts), acknowledged(false) {}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* MessageTracker - Prevents duplicate message processing and tracks acknowledgments
|
|
46
|
-
*
|
|
47
|
-
* This class provides efficient tracking of processed messages to prevent
|
|
48
|
-
* duplicate processing in mesh networks. It supports:
|
|
49
|
-
* - Configurable maximum tracked messages (memory-efficient for ESP8266)
|
|
50
|
-
* - Automatic cleanup of expired entries
|
|
51
|
-
* - Acknowledgment tracking for reliable delivery
|
|
52
|
-
* - Thread-safe consideration for ESP32
|
|
53
|
-
*
|
|
54
|
-
* Example usage:
|
|
55
|
-
* \code
|
|
56
|
-
* MessageTracker tracker(500, 60000); // Max 500 messages, 60s timeout
|
|
57
|
-
*
|
|
58
|
-
* uint32_t msgId = 12345;
|
|
59
|
-
* uint32_t originNode = 67890;
|
|
60
|
-
*
|
|
61
|
-
* // Check if message was already processed
|
|
62
|
-
* if (!tracker.isProcessed(msgId, originNode)) {
|
|
63
|
-
* // Process the message
|
|
64
|
-
* processMessage(msg);
|
|
65
|
-
*
|
|
66
|
-
* // Mark as processed
|
|
67
|
-
* tracker.markProcessed(msgId, originNode);
|
|
68
|
-
* }
|
|
69
|
-
*
|
|
70
|
-
* // Later, mark as acknowledged
|
|
71
|
-
* tracker.markAcknowledged(msgId, originNode);
|
|
72
|
-
*
|
|
73
|
-
* // Periodically cleanup old entries
|
|
74
|
-
* tracker.cleanup();
|
|
75
|
-
* \endcode
|
|
76
|
-
*/
|
|
77
|
-
class MessageTracker {
|
|
78
|
-
public:
|
|
79
|
-
/**
|
|
80
|
-
* Constructor
|
|
81
|
-
* @param maxMessages Maximum number of messages to track (default: 500)
|
|
82
|
-
* @param timeoutMs Timeout in milliseconds for automatic cleanup (default: 60000)
|
|
83
|
-
*/
|
|
84
|
-
MessageTracker(uint16_t maxMessages = 500, uint32_t timeoutMs = 60000)
|
|
85
|
-
: maxTrackedMessages(maxMessages), messageTimeoutMs(timeoutMs) {}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Check if a message has already been processed
|
|
89
|
-
* @param messageId The unique message identifier
|
|
90
|
-
* @param originNode The node that originated the message
|
|
91
|
-
* @return true if the message was already processed, false otherwise
|
|
92
|
-
*/
|
|
93
|
-
bool isProcessed(uint32_t messageId, uint32_t originNode) {
|
|
94
|
-
MessageKey key = {messageId, originNode};
|
|
95
|
-
auto it = trackedMessages.find(key);
|
|
96
|
-
return it != trackedMessages.end();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Mark a message as processed
|
|
101
|
-
* If the tracker is at capacity, oldest entries will be removed first.
|
|
102
|
-
* Note: If maxMessages is 0, this method does nothing and returns false.
|
|
103
|
-
* @param messageId The unique message identifier
|
|
104
|
-
* @param originNode The node that originated the message
|
|
105
|
-
* @return true if message was tracked, false if capacity is 0
|
|
106
|
-
*/
|
|
107
|
-
bool markProcessed(uint32_t messageId, uint32_t originNode) {
|
|
108
|
-
MessageKey key = {messageId, originNode};
|
|
109
|
-
uint32_t currentTime = millis();
|
|
110
|
-
|
|
111
|
-
// Check if already exists
|
|
112
|
-
auto it = trackedMessages.find(key);
|
|
113
|
-
if (it != trackedMessages.end()) {
|
|
114
|
-
// Update timestamp but preserve acknowledged status
|
|
115
|
-
it->second.timestamp = currentTime;
|
|
116
|
-
Log(logger::GENERAL, "MessageTracker: Updated message %u from node %u\n",
|
|
117
|
-
messageId, originNode);
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Handle capacity 0 case - don't add new entries
|
|
122
|
-
if (maxTrackedMessages == 0) {
|
|
123
|
-
Log(logger::GENERAL, "MessageTracker: Cannot track message %u (capacity=0)\n",
|
|
124
|
-
messageId);
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Enforce memory limits before adding new entry
|
|
129
|
-
if (trackedMessages.size() >= maxTrackedMessages) {
|
|
130
|
-
enforceMemoryLimit();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Add new entry
|
|
134
|
-
trackedMessages[key] = TrackedMessage(currentTime);
|
|
135
|
-
Log(logger::GENERAL, "MessageTracker: Tracked message %u from node %u (size=%zu)\n",
|
|
136
|
-
messageId, originNode, trackedMessages.size());
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Mark a message as acknowledged
|
|
142
|
-
* @param messageId The unique message identifier
|
|
143
|
-
* @param originNode The node that originated the message
|
|
144
|
-
* @return true if the message was found and marked, false otherwise
|
|
145
|
-
*/
|
|
146
|
-
bool markAcknowledged(uint32_t messageId, uint32_t originNode) {
|
|
147
|
-
MessageKey key = {messageId, originNode};
|
|
148
|
-
auto it = trackedMessages.find(key);
|
|
149
|
-
|
|
150
|
-
if (it != trackedMessages.end()) {
|
|
151
|
-
it->second.acknowledged = true;
|
|
152
|
-
Log(logger::GENERAL, "MessageTracker: Acknowledged message %u from node %u\n",
|
|
153
|
-
messageId, originNode);
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
Log(logger::GENERAL, "MessageTracker: Message %u from node %u not found for acknowledgment\n",
|
|
158
|
-
messageId, originNode);
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Check if a message has been acknowledged
|
|
164
|
-
* @param messageId The unique message identifier
|
|
165
|
-
* @param originNode The node that originated the message
|
|
166
|
-
* @return true if acknowledged, false if not found or not acknowledged
|
|
167
|
-
*/
|
|
168
|
-
bool isAcknowledged(uint32_t messageId, uint32_t originNode) {
|
|
169
|
-
MessageKey key = {messageId, originNode};
|
|
170
|
-
auto it = trackedMessages.find(key);
|
|
171
|
-
|
|
172
|
-
if (it != trackedMessages.end()) {
|
|
173
|
-
return it->second.acknowledged;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Cleanup expired entries based on the configured timeout
|
|
181
|
-
* @return Number of entries removed
|
|
182
|
-
*/
|
|
183
|
-
uint32_t cleanup() {
|
|
184
|
-
uint32_t currentTime = millis();
|
|
185
|
-
uint32_t removedCount = 0;
|
|
186
|
-
|
|
187
|
-
auto it = trackedMessages.begin();
|
|
188
|
-
while (it != trackedMessages.end()) {
|
|
189
|
-
// Handle millis() overflow - if currentTime < timestamp, assume overflow
|
|
190
|
-
uint32_t age;
|
|
191
|
-
if (currentTime >= it->second.timestamp) {
|
|
192
|
-
age = currentTime - it->second.timestamp;
|
|
193
|
-
} else {
|
|
194
|
-
// Overflow occurred
|
|
195
|
-
age = (0xFFFFFFFF - it->second.timestamp) + currentTime + 1;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (age > messageTimeoutMs) {
|
|
199
|
-
it = trackedMessages.erase(it);
|
|
200
|
-
removedCount++;
|
|
201
|
-
} else {
|
|
202
|
-
++it;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (removedCount > 0) {
|
|
207
|
-
Log(logger::GENERAL, "MessageTracker: Cleaned up %u expired entries (size=%zu)\n",
|
|
208
|
-
removedCount, trackedMessages.size());
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return removedCount;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Get the current number of tracked messages
|
|
216
|
-
* @return Number of entries in the tracker
|
|
217
|
-
*/
|
|
218
|
-
size_t size() const {
|
|
219
|
-
return trackedMessages.size();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Check if the tracker is empty
|
|
224
|
-
* @return true if no messages are being tracked
|
|
225
|
-
*/
|
|
226
|
-
bool empty() const {
|
|
227
|
-
return trackedMessages.empty();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Clear all tracked messages
|
|
232
|
-
*/
|
|
233
|
-
void clear() {
|
|
234
|
-
trackedMessages.clear();
|
|
235
|
-
Log(logger::GENERAL, "MessageTracker: Cleared all entries\n");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Get the maximum number of tracked messages
|
|
240
|
-
* @return Configured maximum
|
|
241
|
-
*/
|
|
242
|
-
uint16_t getMaxMessages() const {
|
|
243
|
-
return maxTrackedMessages;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Get the timeout value in milliseconds
|
|
248
|
-
* @return Configured timeout
|
|
249
|
-
*/
|
|
250
|
-
uint32_t getTimeoutMs() const {
|
|
251
|
-
return messageTimeoutMs;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Set a new maximum for tracked messages
|
|
256
|
-
* If current size exceeds new max, oldest entries will be removed
|
|
257
|
-
* @param maxMessages New maximum
|
|
258
|
-
*/
|
|
259
|
-
void setMaxMessages(uint16_t maxMessages) {
|
|
260
|
-
maxTrackedMessages = maxMessages;
|
|
261
|
-
|
|
262
|
-
// Enforce new limit if needed
|
|
263
|
-
while (trackedMessages.size() > maxTrackedMessages) {
|
|
264
|
-
enforceMemoryLimit();
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Set a new timeout value
|
|
270
|
-
* @param timeoutMs New timeout in milliseconds
|
|
271
|
-
*/
|
|
272
|
-
void setTimeoutMs(uint32_t timeoutMs) {
|
|
273
|
-
messageTimeoutMs = timeoutMs;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private:
|
|
277
|
-
std::map<MessageKey, TrackedMessage> trackedMessages;
|
|
278
|
-
uint16_t maxTrackedMessages;
|
|
279
|
-
uint32_t messageTimeoutMs;
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Enforce memory limit by removing the oldest entry
|
|
283
|
-
* @return true if an entry was removed, false if tracker was empty
|
|
284
|
-
*/
|
|
285
|
-
bool enforceMemoryLimit() {
|
|
286
|
-
if (trackedMessages.empty()) {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Find the oldest entry
|
|
291
|
-
auto oldest = trackedMessages.begin();
|
|
292
|
-
uint32_t oldestTimestamp = oldest->second.timestamp;
|
|
293
|
-
|
|
294
|
-
for (auto it = trackedMessages.begin(); it != trackedMessages.end(); ++it) {
|
|
295
|
-
if (it->second.timestamp < oldestTimestamp) {
|
|
296
|
-
oldest = it;
|
|
297
|
-
oldestTimestamp = it->second.timestamp;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
Log(logger::GENERAL, "MessageTracker: Evicting oldest entry (msg=%u, node=%u) to enforce limit\n",
|
|
302
|
-
oldest->first.messageId, oldest->first.originNode);
|
|
303
|
-
|
|
304
|
-
trackedMessages.erase(oldest);
|
|
305
|
-
return true;
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
} // namespace painlessmesh
|
|
310
|
-
|
|
311
|
-
#endif // _PAINLESS_MESH_MESSAGE_TRACKER_HPP_
|
/package/examples/sendToInternet/{mock_server_test.ino → mock_server_test/mock_server_test.ino}
RENAMED
|
File without changes
|