@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.
Files changed (61) hide show
  1. package/BRIDGE_TO_INTERNET.md +167 -29
  2. package/CHANGELOG.md +604 -0
  3. package/CONTRIBUTING.md +56 -53
  4. package/README.md +100 -75
  5. package/RELEASE_GUIDE.md +81 -641
  6. package/examples/alteriom/README.md +8 -10
  7. package/examples/alteriom/alteriom.ino +2 -2
  8. package/examples/alteriom/alteriom_custom_package_template.hpp +11 -11
  9. package/examples/alteriom/alteriom_sensor_package.hpp +17 -11
  10. package/examples/alteriom/mppt_example/alteriom_custom_package_template.hpp +320 -0
  11. package/examples/alteriom/mppt_example/alteriom_sensor_package.hpp +1389 -0
  12. package/examples/alteriom/mppt_example/{alteriom_mppt_example.ino → mppt_example.ino} +5 -1
  13. package/examples/basic/test/simulator/README.md +3 -3
  14. package/examples/bridge_failover/README.md +51 -14
  15. package/examples/bridge_failover/bridge_failover.ino +2 -2
  16. package/examples/commandControl/commandControl.ino +86 -0
  17. package/examples/commandControl/platformio.ini +26 -0
  18. package/examples/mqttBridge/mqttBridge.ino +4 -0
  19. package/examples/mqttBridge/platformio.ini +1 -1
  20. package/examples/otaSender/otaSender.ino +5 -1
  21. package/examples/priority/README.md +1 -1
  22. package/examples/priority/{priority_basic_example.ino → priority_basic_example/priority_basic_example.ino} +4 -4
  23. package/examples/priority/{priority_with_queue.ino → priority_with_queue/priority_with_queue.ino} +20 -2
  24. package/examples/reliableSensorLogging/platformio.ini +26 -0
  25. package/examples/reliableSensorLogging/reliableSensorLogging.ino +151 -0
  26. package/examples/sendToInternet/README.md +12 -5
  27. package/examples/sendToInternet/{CMakeLists.txt → pc_node/CMakeLists.txt} +7 -7
  28. package/examples/sendToInternet/{PC_NODE_README.md → pc_node/PC_NODE_README.md} +15 -15
  29. package/examples/sendToInternet/{build.sh → pc_node/build.sh} +5 -5
  30. package/examples/sendToInternet/{pc_mesh_node.cpp → pc_node/pc_mesh_node.cpp} +12 -1
  31. package/examples/sharedGateway/README.md +1 -2
  32. package/examples/tcpRetryConfig/README.md +110 -0
  33. package/examples/tcpRetryConfig/platformio.ini +26 -0
  34. package/examples/tcpRetryConfig/tcpRetryConfig.ino +154 -0
  35. package/keywords.txt +53 -1
  36. package/library.json +8 -6
  37. package/library.properties +2 -2
  38. package/package.json +3 -3
  39. package/src/AlteriomPainlessMesh.h +4 -4
  40. package/src/arduino/wifi.hpp +605 -143
  41. package/src/painlessMesh.h +2 -2
  42. package/src/painlessMeshSTA.cpp +607 -87
  43. package/src/painlessMeshSTA.h +135 -3
  44. package/src/painlessTaskOptions.h +9 -0
  45. package/src/painlessmesh/ack.hpp +283 -0
  46. package/src/painlessmesh/buffer.hpp +74 -9
  47. package/src/painlessmesh/callback.hpp +38 -5
  48. package/src/painlessmesh/configuration.hpp +82 -3
  49. package/src/painlessmesh/connection.hpp +43 -16
  50. package/src/painlessmesh/gateway.hpp +270 -5
  51. package/src/painlessmesh/layout.hpp +70 -2
  52. package/src/painlessmesh/logger.hpp +15 -0
  53. package/src/painlessmesh/mesh.hpp +625 -70
  54. package/src/painlessmesh/message_queue.hpp +24 -13
  55. package/src/painlessmesh/ntp.hpp +2 -4
  56. package/src/painlessmesh/plugin.hpp +52 -6
  57. package/src/painlessmesh/protocol.hpp +55 -2
  58. package/src/painlessmesh/router.hpp +192 -77
  59. package/src/painlessmesh/tcp.hpp +168 -29
  60. package/src/painlessmesh/message_tracker.hpp +0 -311
  61. /package/examples/sendToInternet/{mock_server_test.ino → mock_server_test/mock_server_test.ino} +0 -0
@@ -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 TCP_CONNECT_MAX_RETRIES times before triggering a full
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, TCP_CONNECT_MAX_RETRIES + 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, TCP_CONNECT_MAX_RETRIES + 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 < TCP_CONNECT_MAX_RETRIES) {
138
- // Calculate delay with exponential backoff: base_delay * 2^retryCount
139
- // This gives increasing time between retries as failures accumulate:
140
- // - retryCount=0: 1000ms * 1 = 1s
141
- // - retryCount=1: 1000ms * 2 = 2s
142
- // - retryCount=2: 1000ms * 4 = 4s
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 TCP_CONNECT_MAX_RETRIES (default: 5)
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
- TCP_CONNECT_MAX_RETRIES + 1, ip.toString().c_str());
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, TCP_FAILURE_BLOCK_DURATION_MS);
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
- TCP_EXHAUSTION_RECONNECT_DELAY_MS);
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
- }, TCP_EXHAUSTION_RECONNECT_DELAY_MS);
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_