@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.
- package/BRIDGE_TO_INTERNET.md +167 -29
- package/CHANGELOG.md +483 -0
- package/CONTRIBUTING.md +56 -53
- package/README.md +100 -95
- package/RELEASE_GUIDE.md +81 -780
- package/examples/alteriom/README.md +8 -10
- package/examples/alteriom/alteriom.ino +2 -2
- 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} +4 -0
- package/examples/basic/test/simulator/README.md +3 -3
- package/examples/bridge_failover/README.md +51 -14
- 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/keywords.txt +50 -1
- package/library.json +8 -6
- package/library.properties +2 -2
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +3 -3
- package/src/arduino/wifi.hpp +556 -126
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +607 -87
- package/src/painlessMeshSTA.h +135 -3
- package/src/painlessmesh/ack.hpp +283 -0
- package/src/painlessmesh/buffer.hpp +70 -8
- package/src/painlessmesh/callback.hpp +38 -5
- package/src/painlessmesh/configuration.hpp +69 -1
- package/src/painlessmesh/connection.hpp +12 -5
- 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 +552 -48
- package/src/painlessmesh/ntp.hpp +2 -4
- package/src/painlessmesh/plugin.hpp +30 -6
- package/src/painlessmesh/protocol.hpp +55 -2
- package/src/painlessmesh/router.hpp +192 -77
- package/src/painlessmesh/tcp.hpp +10 -0
- 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/painlessMeshSTA.h
CHANGED
|
@@ -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
|
-
//
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|
|
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) {
|
|
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
|
|
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>
|