@alteriom/painlessmesh 1.10.0 → 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 (54) hide show
  1. package/BRIDGE_TO_INTERNET.md +167 -29
  2. package/CHANGELOG.md +483 -0
  3. package/CONTRIBUTING.md +56 -53
  4. package/README.md +100 -95
  5. package/RELEASE_GUIDE.md +81 -780
  6. package/examples/alteriom/README.md +8 -10
  7. package/examples/alteriom/alteriom.ino +2 -2
  8. package/examples/alteriom/alteriom_sensor_package.hpp +17 -11
  9. package/examples/alteriom/mppt_example/alteriom_custom_package_template.hpp +320 -0
  10. package/examples/alteriom/mppt_example/alteriom_sensor_package.hpp +1389 -0
  11. package/examples/alteriom/mppt_example/{alteriom_mppt_example.ino → mppt_example.ino} +4 -0
  12. package/examples/basic/test/simulator/README.md +3 -3
  13. package/examples/bridge_failover/README.md +51 -14
  14. package/examples/commandControl/commandControl.ino +86 -0
  15. package/examples/commandControl/platformio.ini +26 -0
  16. package/examples/mqttBridge/mqttBridge.ino +4 -0
  17. package/examples/mqttBridge/platformio.ini +1 -1
  18. package/examples/otaSender/otaSender.ino +5 -1
  19. package/examples/priority/README.md +1 -1
  20. package/examples/priority/{priority_basic_example.ino → priority_basic_example/priority_basic_example.ino} +4 -4
  21. package/examples/priority/{priority_with_queue.ino → priority_with_queue/priority_with_queue.ino} +20 -2
  22. package/examples/reliableSensorLogging/platformio.ini +26 -0
  23. package/examples/reliableSensorLogging/reliableSensorLogging.ino +151 -0
  24. package/examples/sendToInternet/README.md +12 -5
  25. package/examples/sendToInternet/{CMakeLists.txt → pc_node/CMakeLists.txt} +7 -7
  26. package/examples/sendToInternet/{PC_NODE_README.md → pc_node/PC_NODE_README.md} +15 -15
  27. package/examples/sendToInternet/{build.sh → pc_node/build.sh} +5 -5
  28. package/examples/sendToInternet/{pc_mesh_node.cpp → pc_node/pc_mesh_node.cpp} +12 -1
  29. package/examples/sharedGateway/README.md +1 -2
  30. package/keywords.txt +50 -1
  31. package/library.json +8 -6
  32. package/library.properties +2 -2
  33. package/package.json +3 -3
  34. package/src/AlteriomPainlessMesh.h +3 -3
  35. package/src/arduino/wifi.hpp +556 -126
  36. package/src/painlessMesh.h +2 -2
  37. package/src/painlessMeshSTA.cpp +607 -87
  38. package/src/painlessMeshSTA.h +135 -3
  39. package/src/painlessmesh/ack.hpp +283 -0
  40. package/src/painlessmesh/buffer.hpp +70 -8
  41. package/src/painlessmesh/callback.hpp +38 -5
  42. package/src/painlessmesh/configuration.hpp +69 -1
  43. package/src/painlessmesh/connection.hpp +12 -5
  44. package/src/painlessmesh/gateway.hpp +270 -5
  45. package/src/painlessmesh/layout.hpp +70 -2
  46. package/src/painlessmesh/logger.hpp +15 -0
  47. package/src/painlessmesh/mesh.hpp +552 -48
  48. package/src/painlessmesh/ntp.hpp +2 -4
  49. package/src/painlessmesh/plugin.hpp +30 -6
  50. package/src/painlessmesh/protocol.hpp +55 -2
  51. package/src/painlessmesh/router.hpp +192 -77
  52. package/src/painlessmesh/tcp.hpp +10 -0
  53. package/src/painlessmesh/message_tracker.hpp +0 -311
  54. /package/examples/sendToInternet/{mock_server_test.ino → mock_server_test/mock_server_test.ino} +0 -0
@@ -33,16 +33,64 @@ class StationScan {
33
33
  uint16_t port, uint8_t channel, bool hidden);
34
34
  void stationScan();
35
35
  void scanComplete();
36
+ // The scan-done event has arrived (ESP32: on the core's network-event
37
+ // task). The result is read by scanComplete() from the station task,
38
+ // in the loop, where the radio may be reconfigured.
39
+ void scanDone();
36
40
  void filterAPs();
37
41
  void connectToAP();
42
+ // Move both Wi-Fi interfaces to a bridge-announced channel immediately.
43
+ bool followBridgeChannel(uint8_t targetChannel);
44
+ // A bridge has spoken, from this channel: its status message carries the
45
+ // channel it is on, which is its router's, and a message just received
46
+ // is live — unlike a cached tree, which can carry a root that has gone.
47
+ // That channel is home.
48
+ void noteRooted(uint8_t channel) {
49
+ everRooted = true;
50
+ rootedChannel = channel;
51
+ homeStays = 0;
52
+ }
53
+ // The tree contains a root. Enough for the rules that ask whether this
54
+ // mesh ever had one; not enough to say which channel is home.
55
+ void noteEverRooted() { everRooted = true; }
56
+ // The station attempt requestIP() started is over: it got an address, or
57
+ // it was disconnected. The half-open guard judges only an attempt still
58
+ // in progress. Without this it judged a fresh association by the clock of
59
+ // an attempt made 109 s earlier — the node had followed the bridge to its
60
+ // channel in between — and dropped it, costing a scan interval.
61
+ void stationAttemptOver() { connectAttemptStarted = 0; }
62
+ // The station got an address: its link is up. Cleared once the drop
63
+ // callbacks have judged a disconnect, so they can tell a link that was
64
+ // up and went away from an attempt that never got that far.
65
+ void stationUp() { stationLinkUp = true; }
66
+ void stationDown() { stationLinkUp = false; }
67
+ bool stationLinkUp = false;
68
+ // The station link was closed by this node's own channel move a moment
69
+ // ago (followBridgeChannel() closes it and scans next). Not a loss.
70
+ bool droppedByMove() const {
71
+ return channelMovedAt != 0 && millis() - channelMovedAt < 5000;
72
+ }
73
+ // The next scan covers every channel. For a node whose uplink just went
74
+ // away in a mesh that should have a root: the AP it was on left for the
75
+ // bridge's channel, and so will whatever is still here.
76
+ void redetectOnNextScan() { redetectRequested = true; }
77
+ // On the channel the bridge's status named: the bridge's AP is here.
78
+ bool atHome() const { return rootedChannel != 0 && channel == rootedChannel; }
38
79
  // This one will call the connectToAP next in the task and should be used
39
80
  // instead of connectToAP
40
81
  void yieldConnectToAP() {
41
82
  task.yield([this]() { connectToAP(); });
42
83
  }
43
84
 
44
- // Helper to scan all channels for a specific mesh SSID
45
- static uint8_t scanForMeshChannel(TSTRING meshSSID, bool meshHidden);
85
+ // Scan every channel for the mesh SSID. With avoidChannel set, prefer a
86
+ // channel other than it: re-detection runs when the node's own partition
87
+ // has gone quiet, so the mesh on its current channel is the partition it
88
+ // is stranded in, not the one it is looking for.
89
+ // With routerSSID set, the mesh on the router's channel is preferred over
90
+ // every other: that is where a bridge, and so the mesh, lives.
91
+ static uint8_t scanForMeshChannel(TSTRING meshSSID, bool meshHidden,
92
+ uint8_t avoidChannel = 0,
93
+ TSTRING routerSSID = "");
46
94
 
47
95
  // Check if channel re-synchronization is needed or in progress
48
96
  bool isChannelResyncNeeded() const {
@@ -84,7 +132,91 @@ class StationScan {
84
132
 
85
133
  // Track consecutive scans with no mesh nodes found (for channel re-detection)
86
134
  uint16_t consecutiveEmptyScans = 0;
87
- static const uint16_t EMPTY_SCAN_THRESHOLD = 6; // ~30 seconds at default SCAN_INTERVAL
135
+ // millis() of the last requestIP(), to tell a station that is still
136
+ // obtaining an address from one that associated and never got one.
137
+ uint32_t connectAttemptStarted = 0;
138
+ // millis() of the last followBridgeChannel(), for droppedByMove().
139
+ uint32_t channelMovedAt = 0;
140
+ // Whether the current attempt's half-open association has already been
141
+ // dropped. WiFi.disconnect() on a station that has nothing to disconnect
142
+ // leaves the status where it was, and a guard that fired on every pass
143
+ // held a node out of the mesh for the rest of a test.
144
+ bool halfOpenDropped = false;
145
+ // Set when this task starts an async scan, cleared when its result is
146
+ // consumed. The scan-done event also fires for the synchronous scans
147
+ // channel re-detection runs, and consuming those results here found them
148
+ // already deleted, reported "wifi scan failed", and rescanned at once.
149
+ bool scanRequested = false;
150
+ // Set when the next station scan must cover every channel: the empty
151
+ // scans have piled up and the node is looking for the channel the mesh
152
+ // moved to. The re-detection is this task's ordinary asynchronous scan
153
+ // with the channel left open, not a synchronous all-channel scan run
154
+ // inline. The synchronous one held the main loop and the radio for four
155
+ // to seven seconds; measured on the rig, an ACK owed through the node
156
+ // during that window came back after the sender's eight-second budget
157
+ // and counted as a loss — one in every two or three soak runs.
158
+ bool redetectRequested = false;
159
+ // The scan in flight covers every channel (the fast re-detection of a
160
+ // node with nothing under its AP), or is one slice of a sliced hunt.
161
+ bool scanAllChannels = false;
162
+ uint8_t scanSlice = 0;
163
+ // A re-detection with stations under this node's AP is done a channel at
164
+ // a time: an all-channel scan takes the AP off its channel for two to
165
+ // three seconds, and an ESP8266 station does not survive that — on the
166
+ // rig the soak's sender ran one with the ESP8266 as its child and lost
167
+ // it for the rest of the test. A slice is one channel for 120 ms, then
168
+ // 1.5 s at home; the mesh APs seen on each channel are tallied, and the
169
+ // decision is taken on the own-channel scan that follows the last slice,
170
+ // by the same rules as the all-channel scan.
171
+ uint8_t huntChannel = 0;
172
+ bool huntPending = false;
173
+ std::map<uint8_t, size_t> huntCounts;
174
+ std::map<uint8_t, int8_t> huntRssi;
175
+ // Doubles the scan interval of a connected node that keeps finding the
176
+ // mesh only on its own channel while told the mesh has a root: a mesh
177
+ // that is simply rootless would otherwise cost every node a full
178
+ // all-channel scan every half interval, for as long as it stays so.
179
+ uint8_t orphanScanBackoff = 0;
180
+ // Re-detections in a row, while connected and rootless, that found the
181
+ // mesh only on this channel and nothing new on it. Two, and a leaf
182
+ // drops its station link to look for the root with an empty tree.
183
+ uint8_t orphanRedetects = 0;
184
+ // Whether this node's tree has ever contained a root since the mesh
185
+ // started. A leaf leaves a rootless partition to look for the root only
186
+ // if there was one: a mesh that never had a bridge is rootless by
187
+ // design, and its leaves dropping their links every other minute to look
188
+ // for what does not exist cost the soak its deliveries.
189
+ bool everRooted = false;
190
+ // The channel the mesh was on the last time this node's tree had a root:
191
+ // the bridge's channel, which is its router's. A rootless node that has
192
+ // one treats it as home. It does not leave home for a partition on
193
+ // another channel, and away from home it goes back as soon as it sees
194
+ // the mesh there, whatever the sizes: a bridge that has just been
195
+ // promoted is one AP on the router's channel, and it is the one to join.
196
+ // On the rig, the node the failover test sends from followed a two-node
197
+ // partition off the router's channel while the backup was being elected
198
+ // beside it, then stayed away because its new partition was bigger than
199
+ // the lone bridge, and never heard the bridge's status.
200
+ uint8_t rootedChannel = 0;
201
+ // Re-detections in a row, at home, that declined to follow a partition
202
+ // seen elsewhere. A bridge that moves for good (its router changed
203
+ // channel) would otherwise keep the old channel rootless forever, so
204
+ // after enough of them home is forgotten and the ordinary rules apply.
205
+ uint8_t homeStays = 0;
206
+ // The other channel a disconnected node saw a smaller partition on, at
207
+ // its last re-detection: it follows only if the same channel shows the
208
+ // mesh again a scan later — a straggler is gone by then, a bridge is
209
+ // not.
210
+ uint8_t pendingElsewhere = 0;
211
+ // Consecutive scans, while connected and unrooted in a mesh that should
212
+ // have a root, that found nodes this node has no route to: a partition,
213
+ // with the root on the other side. Two of them and the node joins it.
214
+ uint8_t partitionScans = 0;
215
+ // Empty scans before re-detecting the mesh channel. A disconnected or
216
+ // orphaned node scans every 0.5 * SCAN_INTERVAL = 15 s, so 2 is ~30 s —
217
+ // what the old comment promised while the value of 6 delivered 90 s, long
218
+ // enough for a bridge's followers to miss a 120 s gateway contract.
219
+ static const uint16_t EMPTY_SCAN_THRESHOLD = 2;
88
220
 
89
221
  // TCP failure blocklist to prevent infinite retry loops
90
222
  // Maps nodeId -> blockUntil timestamp (millis())
@@ -0,0 +1,283 @@
1
+ #ifndef PAINLESS_MESH_ACK_HPP
2
+ #define PAINLESS_MESH_ACK_HPP
3
+
4
+ #include <functional>
5
+ #include <list>
6
+ #include <map>
7
+ #include <set>
8
+
9
+ #include "painlessmesh/configuration.hpp"
10
+ #include "painlessmesh/logger.hpp"
11
+ #include "painlessmesh/plugin.hpp"
12
+
13
+ extern painlessmesh::logger::LogClass Log;
14
+
15
+ // Maximum number of messages that may await acknowledgment at once.
16
+ // Bounded to protect the small ESP8266 heap; override at build time if
17
+ // your application legitimately needs more concurrent tracked sends.
18
+ #ifndef PAINLESSMESH_MAX_PENDING_ACKS
19
+ #define PAINLESSMESH_MAX_PENDING_ACKS 32
20
+ #endif
21
+
22
+ // Maximum number of delayed broadcast acknowledgments queued by a receiver.
23
+ // A single shared scheduler task drains the queue after the jitter delay.
24
+ #ifndef PAINLESSMESH_MAX_QUEUED_BROADCAST_ACKS
25
+ #define PAINLESSMESH_MAX_QUEUED_BROADCAST_ACKS 32
26
+ #endif
27
+
28
+ namespace painlessmesh {
29
+
30
+ /**
31
+ * Per-message delivery confirmation (issue #379)
32
+ *
33
+ * When a delivery callback is passed to Mesh::sendSingle() or
34
+ * Mesh::sendBroadcast(), the outgoing package is tagged with a unique
35
+ * msgId. Receiving nodes automatically reply with a MessageAckPackage
36
+ * (type 630) and the sender's AckTracker matches the reply to the pending
37
+ * message, firing the callback with the measured round-trip latency.
38
+ * Messages that are not acknowledged within their timeout fire the
39
+ * callback with delivered = false.
40
+ */
41
+ namespace ack {
42
+
43
+ /**
44
+ * Callback type for delivery confirmation
45
+ *
46
+ * @param nodeId The destination node this result refers to
47
+ * @param delivered True when the node acknowledged the message, false when
48
+ * the acknowledgment timed out
49
+ * @param latencyMs Round-trip time in ms when delivered, otherwise the
50
+ * configured timeout
51
+ */
52
+ typedef std::function<void(uint32_t nodeId, bool delivered, uint32_t latencyMs)>
53
+ deliveryCallback_t;
54
+
55
+ /**
56
+ * Acknowledgment package sent back to the original sender
57
+ *
58
+ * Routed as a SINGLE package so it traverses multiple hops using the
59
+ * normal routing logic.
60
+ *
61
+ * Type ID: 630 (MESSAGE_ACK)
62
+ */
63
+ class MessageAckPackage : public plugin::SinglePackage {
64
+ public:
65
+ /** ID of the message being acknowledged */
66
+ uint32_t messageId = 0;
67
+
68
+ MessageAckPackage() : SinglePackage(protocol::MESSAGE_ACK) {}
69
+
70
+ MessageAckPackage(uint32_t fromNode, uint32_t destNode, uint32_t msgId)
71
+ : SinglePackage(protocol::MESSAGE_ACK) {
72
+ from = fromNode;
73
+ dest = destNode;
74
+ messageId = msgId;
75
+ }
76
+
77
+ MessageAckPackage(JsonObject jsonObj) : SinglePackage(jsonObj) {
78
+ messageId = jsonObj["msgId"] | (uint32_t)0;
79
+ }
80
+
81
+ JsonObject addTo(JsonObject&& jsonObj) const {
82
+ jsonObj = SinglePackage::addTo(std::move(jsonObj));
83
+ jsonObj["msgId"] = messageId;
84
+ return jsonObj;
85
+ }
86
+
87
+ #if ARDUINOJSON_VERSION_MAJOR < 7
88
+ size_t jsonObjectSize() const { return JSON_OBJECT_SIZE(noJsonFields + 1); }
89
+ #endif
90
+ };
91
+
92
+ /**
93
+ * A message awaiting acknowledgment(s)
94
+ */
95
+ struct PendingAck {
96
+ deliveryCallback_t callback;
97
+ uint32_t sentAt = 0;
98
+ uint32_t timeoutMs = 0;
99
+ std::set<uint32_t> waitingFor;
100
+ };
101
+
102
+ struct DeliveryResult {
103
+ deliveryCallback_t callback;
104
+ uint32_t nodeId = 0;
105
+ bool delivered = false;
106
+ uint32_t latencyMs = 0;
107
+
108
+ DeliveryResult(deliveryCallback_t callback, uint32_t nodeId, bool delivered,
109
+ uint32_t latencyMs)
110
+ : callback(callback),
111
+ nodeId(nodeId),
112
+ delivered(delivered),
113
+ latencyMs(latencyMs) {}
114
+ };
115
+
116
+ /**
117
+ * AckTracker - tracks outgoing messages awaiting acknowledgment
118
+ *
119
+ * Time is passed in explicitly (millis() on device) so the tracker stays
120
+ * platform independent and unit testable. All time comparisons are
121
+ * uint32 wraparound safe.
122
+ */
123
+ class AckTracker {
124
+ public:
125
+ /**
126
+ * Seed the message-id counter
127
+ *
128
+ * Called once at mesh init with a random value so ids do not restart
129
+ * at 1 after a reboot — a delayed ACK for a pre-reboot message could
130
+ * otherwise match a fresh message's id and report a false
131
+ * delivered = true.
132
+ */
133
+ void seed(uint32_t value) { counter = value; }
134
+
135
+ /** Generate the next unique, non-zero message id */
136
+ uint32_t nextMessageId() {
137
+ if (++counter == 0) ++counter;
138
+ return counter;
139
+ }
140
+
141
+ /** Whether the tracker is at its pending-message limit */
142
+ bool full() const { return entries.size() >= PAINLESSMESH_MAX_PENDING_ACKS; }
143
+
144
+ /**
145
+ * Start tracking a message
146
+ *
147
+ * @param msgId Unique id from nextMessageId()
148
+ * @param destinations Nodes expected to acknowledge
149
+ * @param callback Fired once per destination (ack or timeout)
150
+ * @param timeoutMs Time to wait for acknowledgments
151
+ * @param now Current time (millis())
152
+ * @return false when the callback/destinations are empty or the
153
+ * tracker is full (PAINLESSMESH_MAX_PENDING_ACKS)
154
+ */
155
+ bool track(uint32_t msgId, const std::list<uint32_t>& destinations,
156
+ deliveryCallback_t callback, uint32_t timeoutMs, uint32_t now) {
157
+ if (!callback || destinations.empty()) return false;
158
+ if (full()) {
159
+ Log(logger::ERROR,
160
+ "AckTracker: pending-ack limit (%u) reached, not tracking %u\n",
161
+ (unsigned)PAINLESSMESH_MAX_PENDING_ACKS, msgId);
162
+ return false;
163
+ }
164
+ PendingAck entry;
165
+ entry.callback = callback;
166
+ entry.sentAt = now;
167
+ entry.timeoutMs = timeoutMs;
168
+ entry.waitingFor.insert(destinations.begin(), destinations.end());
169
+ entries[msgId] = std::move(entry);
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * Process an incoming acknowledgment
175
+ *
176
+ * Fires the callback with delivered = true and the measured latency.
177
+ *
178
+ * @return true when the ack matched a pending message/destination
179
+ */
180
+ bool handleAck(uint32_t msgId, uint32_t fromNode, uint32_t now) {
181
+ std::list<DeliveryResult> results;
182
+ auto matched = collectAck(msgId, fromNode, now, results);
183
+ for (auto&& result : results) {
184
+ result.callback(result.nodeId, result.delivered, result.latencyMs);
185
+ }
186
+ return matched;
187
+ }
188
+
189
+ /**
190
+ * Match an acknowledgment without invoking user code.
191
+ *
192
+ * Mesh receive dispatch uses this form so callbacks can be scheduled after
193
+ * the connection read task has unwound. Tracker state is still updated
194
+ * synchronously and exactly once.
195
+ */
196
+ bool collectAck(uint32_t msgId, uint32_t fromNode, uint32_t now,
197
+ std::list<DeliveryResult>& results) {
198
+ auto it = entries.find(msgId);
199
+ if (it == entries.end()) return false;
200
+ if (it->second.waitingFor.count(fromNode) == 0) return false;
201
+ auto latency = (uint32_t)(now - it->second.sentAt);
202
+ if (latency >= it->second.timeoutMs) {
203
+ // The polling task may not have run exactly at the deadline. Reject
204
+ // this late ACK and expire every destination still waiting on the
205
+ // same message before invoking user code (callbacks may reenter us).
206
+ auto expired = std::move(it->second);
207
+ entries.erase(it);
208
+ for (auto&& nodeId : expired.waitingFor) {
209
+ Log(logger::COMMUNICATION,
210
+ "AckTracker: timeout waiting for ack from %u\n", nodeId);
211
+ results.push_back(
212
+ DeliveryResult(expired.callback, nodeId, false, expired.timeoutMs));
213
+ }
214
+ return false;
215
+ }
216
+ it->second.waitingFor.erase(fromNode);
217
+ auto callback = it->second.callback;
218
+ if (it->second.waitingFor.empty()) entries.erase(it);
219
+ results.push_back(DeliveryResult(callback, fromNode, true, latency));
220
+ return true;
221
+ }
222
+
223
+ /**
224
+ * Fire timeout callbacks for expired messages
225
+ *
226
+ * @param now Current time (millis())
227
+ * @return Number of messages still awaiting acknowledgment
228
+ */
229
+ size_t expire(uint32_t now) {
230
+ std::list<DeliveryResult> results;
231
+ collectExpired(now, results);
232
+ for (const auto& result : results) {
233
+ result.callback(result.nodeId, result.delivered, result.latencyMs);
234
+ }
235
+ return entries.size();
236
+ }
237
+
238
+ /**
239
+ * Collect timeout results without invoking user callbacks
240
+ *
241
+ * Mesh uses this form to defer callbacks until receive dispatch has
242
+ * unwound. Direct AckTracker users retain the synchronous expire() API.
243
+ */
244
+ size_t collectExpired(uint32_t now, std::list<DeliveryResult>& results) {
245
+ // Collect expired entries and erase them from the map BEFORE exposing
246
+ // results to user callbacks: a callback may reenter this tracker (track a
247
+ // retry, call clear() via mesh.stop(), or poll checkAcks()), which
248
+ // would invalidate a live iterator into `entries`.
249
+ std::list<PendingAck> expired;
250
+ for (auto it = entries.begin(); it != entries.end();) {
251
+ if ((uint32_t)(now - it->second.sentAt) >= it->second.timeoutMs) {
252
+ expired.push_back(std::move(it->second));
253
+ it = entries.erase(it);
254
+ } else {
255
+ ++it;
256
+ }
257
+ }
258
+ for (auto&& entry : expired) {
259
+ for (auto&& nodeId : entry.waitingFor) {
260
+ Log(logger::COMMUNICATION,
261
+ "AckTracker: timeout waiting for ack from %u\n", nodeId);
262
+ results.push_back(
263
+ DeliveryResult(entry.callback, nodeId, false, entry.timeoutMs));
264
+ }
265
+ }
266
+ return entries.size();
267
+ }
268
+
269
+ /** Number of messages awaiting acknowledgment */
270
+ size_t pending() const { return entries.size(); }
271
+
272
+ /** Drop all pending messages without firing callbacks */
273
+ void clear() { entries.clear(); }
274
+
275
+ protected:
276
+ std::map<uint32_t, PendingAck> entries;
277
+ uint32_t counter = 0;
278
+ };
279
+
280
+ } // namespace ack
281
+ } // namespace painlessmesh
282
+
283
+ #endif
@@ -108,6 +108,16 @@ inline void ReceiveBuffer<std::string>::stringAppend(std::string &buffer,
108
108
  }
109
109
  #endif
110
110
 
111
+ // Maximum number of messages a single connection's SentBuffer may hold
112
+ // (issue #388). Without a cap, a peer that stops draining (stalled TCP
113
+ // connection) grows the outbound list until allocation fails on the ESP8266
114
+ // heap. When the buffer is full, an incoming message evicts the newest
115
+ // message of a strictly lower priority class; if nothing lower-priority is
116
+ // queued, the incoming message itself is rejected (push returns false).
117
+ #ifndef PAINLESSMESH_MAX_SENT_BUFFER_MESSAGES
118
+ #define PAINLESSMESH_MAX_SENT_BUFFER_MESSAGES 64
119
+ #endif
120
+
111
121
  /**
112
122
  * Structure to hold a message with its priority level
113
123
  */
@@ -135,14 +145,16 @@ class SentBuffer {
135
145
  *
136
146
  * \param message The message to queue
137
147
  * \param priority Whether this is a high priority message (legacy bool API)
148
+ * \return true if the message was queued, false if the buffer is full and
149
+ * nothing lower-priority could be evicted (issue #388)
138
150
  *
139
151
  * Legacy API: High priority messages (true) will be sent to the front of the buffer
140
152
  * This maintains backward compatibility with existing code.
141
153
  */
142
- void push(const T &message, bool priority = false) {
154
+ bool push(const T &message, bool priority = false) {
143
155
  // Legacy API: map bool to uint8_t priority (false=2 NORMAL, true=1 HIGH)
144
156
  uint8_t priorityLevel = priority ? 1 : 2;
145
- pushWithPriority(message, priorityLevel);
157
+ return pushWithPriority(message, priorityLevel);
146
158
  }
147
159
 
148
160
  /**
@@ -150,15 +162,31 @@ class SentBuffer {
150
162
  *
151
163
  * \param message The message to queue
152
164
  * \param priorityLevel Priority level: 0=CRITICAL, 1=HIGH, 2=NORMAL, 3=LOW
165
+ * \return true if the message was queued, false if the buffer is full and
166
+ * nothing lower-priority could be evicted (issue #388)
153
167
  *
154
168
  * Messages are scheduled in priority order. Within same priority, FIFO order is maintained.
169
+ *
170
+ * The buffer holds at most PAINLESSMESH_MAX_SENT_BUFFER_MESSAGES entries.
171
+ * At the cap, the newest message of a strictly lower priority class is
172
+ * evicted to make room (mirroring MessageQueue::makeSpace(): CRITICAL
173
+ * evicts HIGH/NORMAL/LOW, HIGH evicts NORMAL/LOW, NORMAL evicts LOW, LOW
174
+ * evicts nothing); when no lower-priority message exists the incoming
175
+ * message is rejected. Either way the drop is counted in
176
+ * getStats().dropped.
155
177
  */
156
- void pushWithPriority(const T &message, uint8_t priorityLevel) {
178
+ bool pushWithPriority(const T &message, uint8_t priorityLevel) {
157
179
  // Clamp priority to valid range
158
180
  if (priorityLevel > 3) priorityLevel = 3;
159
-
181
+
182
+ if (prioritizedMessages.size() >= PAINLESSMESH_MAX_SENT_BUFFER_MESSAGES &&
183
+ !evictLowerPriority(priorityLevel)) {
184
+ messagesDropped++;
185
+ return false;
186
+ }
187
+
160
188
  prioritizedMessages.push_back(PrioritizedMessage<T>(message, priorityLevel));
161
-
189
+
162
190
  // Track statistics
163
191
  totalMessagesQueued++;
164
192
  switch(priorityLevel) {
@@ -167,6 +195,7 @@ class SentBuffer {
167
195
  case 2: normalQueued++; break;
168
196
  case 3: lowQueued++; break;
169
197
  }
198
+ return true;
170
199
  }
171
200
 
172
201
  /**
@@ -264,9 +293,10 @@ class SentBuffer {
264
293
 
265
294
  bool empty() { return prioritizedMessages.empty(); }
266
295
 
267
- void clear() {
268
- prioritizedMessages.clear();
296
+ void clear() {
297
+ prioritizedMessages.clear();
269
298
  current_read_iterator = prioritizedMessages.end();
299
+ messagesDropped = 0;
270
300
  totalMessagesQueued = 0;
271
301
  criticalQueued = highQueued = normalQueued = lowQueued = 0;
272
302
  criticalSent = highSent = normalSent = lowSent = 0;
@@ -292,10 +322,14 @@ class SentBuffer {
292
322
  uint32_t highSent;
293
323
  uint32_t normalSent;
294
324
  uint32_t lowSent;
325
+ // Messages lost to the PAINLESSMESH_MAX_SENT_BUFFER_MESSAGES cap:
326
+ // evicted from the buffer or rejected on push (issue #388)
327
+ uint32_t dropped;
295
328
  };
296
-
329
+
297
330
  SendStats getStats() const {
298
331
  SendStats stats;
332
+ stats.dropped = messagesDropped;
299
333
  stats.totalQueued = totalMessagesQueued;
300
334
  stats.criticalQueued = criticalQueued;
301
335
  stats.highQueued = highQueued;
@@ -316,6 +350,7 @@ class SentBuffer {
316
350
  typename std::list<PrioritizedMessage<T>>::iterator current_read_iterator;
317
351
 
318
352
  // Statistics tracking
353
+ uint32_t messagesDropped = 0;
319
354
  uint32_t totalMessagesQueued = 0;
320
355
  uint32_t criticalQueued = 0;
321
356
  uint32_t highQueued = 0;
@@ -380,6 +415,33 @@ class SentBuffer {
380
415
  return 2; // NORMAL default
381
416
  }
382
417
 
418
+ /**
419
+ * Evict a message of a strictly lower priority class than the incoming
420
+ * one to make room (issue #388). Prefers the newest message of the lowest
421
+ * class present; never evicts the message a partial read is in progress
422
+ * on. Returns false when nothing lower-priority is queued.
423
+ */
424
+ bool evictLowerPriority(uint8_t newPriority) {
425
+ auto victim = prioritizedMessages.end();
426
+ for (auto it = prioritizedMessages.begin();
427
+ it != prioritizedMessages.end(); ++it) {
428
+ if (!clean && it == current_read_iterator) continue;
429
+ if (it->priority <= newPriority) continue;
430
+ // >= keeps walking forward within the lowest class, so the newest
431
+ // (last-queued) entry of that class is the one dropped
432
+ if (victim == prioritizedMessages.end() ||
433
+ it->priority >= victim->priority)
434
+ victim = it;
435
+ }
436
+ if (victim == prioritizedMessages.end()) return false;
437
+ messagesDropped++;
438
+ prioritizedMessages.erase(victim);
439
+ // The cached read iterator may point at the erased node; recompute on
440
+ // the next read. A partial read in progress (!clean) is never evicted.
441
+ if (clean) current_read_iterator = prioritizedMessages.end();
442
+ return true;
443
+ }
444
+
383
445
  inline void stringEraseFront(T &string, size_t length) {
384
446
  string.remove(0, length);
385
447
  };
@@ -60,26 +60,59 @@ class PackageCallbackList {
60
60
  * Add a callback for specific package id
61
61
  */
62
62
  void onPackage(int id, std::function<void(Args...)> func) {
63
- callbackMap[id].push_back(func);
63
+ if (clearPending) {
64
+ (*pendingCallbackMap)[id].push_back(func);
65
+ } else {
66
+ (*callbackMap)[id].push_back(func);
67
+ }
64
68
  }
65
69
 
66
70
  size_t size() {
67
71
  size_t size = 0;
68
- for (auto&& key_value : callbackMap) {
72
+ auto generation = clearPending ? pendingCallbackMap : callbackMap;
73
+ for (auto&& key_value : *generation) {
69
74
  size += key_value.second.size();
70
75
  }
71
76
  return size;
72
77
  }
73
78
 
74
- void clear() { callbackMap.clear(); }
79
+ void clear() {
80
+ if (dispatchDepth > 0) {
81
+ clearPending = true;
82
+ pendingCallbackMap = std::make_shared<CallbackMap>();
83
+ return;
84
+ }
85
+ callbackMap->clear();
86
+ }
75
87
 
76
88
  /**
77
89
  * Execute all the callbacks associated with a certain package
78
90
  */
79
- int execute(int id, Args... args) { return callbackMap[id].execute(args...); }
91
+ int execute(int id, Args... args) {
92
+ // Retain the selected generation for the complete call. A nested callback
93
+ // may clear and replace the pending generation without destroying the
94
+ // List/std::function currently executing on this stack.
95
+ auto generation = clearPending ? pendingCallbackMap : callbackMap;
96
+ ++dispatchDepth;
97
+ auto result = (*generation)[id].execute(args...);
98
+ --dispatchDepth;
99
+ if (dispatchDepth == 0 && clearPending) {
100
+ callbackMap = pendingCallbackMap;
101
+ pendingCallbackMap.reset();
102
+ clearPending = false;
103
+ }
104
+ return result;
105
+ }
80
106
 
81
107
  protected:
82
- std::map<int, List<Args...>> callbackMap;
108
+ using CallbackMap = std::map<int, List<Args...>>;
109
+ std::shared_ptr<CallbackMap> callbackMap = std::make_shared<CallbackMap>();
110
+ // Registrations made after clear() during an active dispatch belong to
111
+ // the next callback generation (for example stop(); init(); from a user
112
+ // callback) and must survive removal of the currently executing one.
113
+ std::shared_ptr<CallbackMap> pendingCallbackMap;
114
+ size_t dispatchDepth = 0;
115
+ bool clearPending = false;
83
116
  };
84
117
 
85
118
  template <typename T>