@alteriom/painlessmesh 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -1
- package/README.md +167 -12
- package/docs/architecture/mesh-architecture.md +20 -0
- package/docs/multi-bridge-setup.md +1025 -0
- package/examples/multi_bridge/README.md +346 -0
- package/examples/multi_bridge/primary_bridge.ino +96 -0
- package/examples/multi_bridge/regular_node.ino +141 -0
- package/examples/multi_bridge/secondary_bridge.ino +111 -0
- package/examples/queued_alarms/README.md +390 -0
- package/examples/queued_alarms/queued_alarms.ino +265 -0
- package/library.json +22 -3
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/arduino/wifi.hpp +319 -3
- package/src/painlessmesh/mesh.hpp +200 -0
- package/src/painlessmesh/message_queue.hpp +368 -0
- package/src/painlessmesh/plugin.hpp +69 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#ifndef _PAINLESS_MESH_MESSAGE_QUEUE_HPP_
|
|
2
|
+
#define _PAINLESS_MESH_MESSAGE_QUEUE_HPP_
|
|
3
|
+
|
|
4
|
+
#include <vector>
|
|
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
|
+
* Message priority levels for queue management
|
|
16
|
+
* Higher priority messages are preserved when queue is full
|
|
17
|
+
*/
|
|
18
|
+
enum MessagePriority {
|
|
19
|
+
PRIORITY_CRITICAL = 0, // Life/safety critical (O2 alarms, fire alarms)
|
|
20
|
+
PRIORITY_HIGH = 1, // Important but not life-threatening
|
|
21
|
+
PRIORITY_NORMAL = 2, // Regular sensor data
|
|
22
|
+
PRIORITY_LOW = 3 // Non-essential telemetry
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Queue state for monitoring and callbacks
|
|
27
|
+
*/
|
|
28
|
+
enum QueueState {
|
|
29
|
+
QUEUE_EMPTY, // Queue is empty
|
|
30
|
+
QUEUE_NORMAL, // Queue has space available
|
|
31
|
+
QUEUE_75_PERCENT, // Queue is 75% full - warning threshold
|
|
32
|
+
QUEUE_FULL // Queue is full - dropping low priority messages
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Queued message structure
|
|
37
|
+
* Contains all metadata needed for reliable message delivery
|
|
38
|
+
*/
|
|
39
|
+
struct QueuedMessage {
|
|
40
|
+
uint32_t id; // Unique message ID
|
|
41
|
+
MessagePriority priority; // Message priority level
|
|
42
|
+
uint32_t timestamp; // When message was queued (millis)
|
|
43
|
+
uint32_t attempts; // Number of send attempts
|
|
44
|
+
TSTRING payload; // Message content
|
|
45
|
+
TSTRING destination; // Cloud endpoint/topic (optional metadata)
|
|
46
|
+
|
|
47
|
+
QueuedMessage() : id(0), priority(PRIORITY_NORMAL), timestamp(0), attempts(0) {}
|
|
48
|
+
|
|
49
|
+
QueuedMessage(uint32_t msgId, MessagePriority prio, uint32_t ts,
|
|
50
|
+
const TSTRING& data, const TSTRING& dest = "")
|
|
51
|
+
: id(msgId), priority(prio), timestamp(ts), attempts(0),
|
|
52
|
+
payload(data), destination(dest) {}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Message queue statistics
|
|
57
|
+
*/
|
|
58
|
+
struct QueueStats {
|
|
59
|
+
uint32_t totalQueued = 0; // Total messages ever queued
|
|
60
|
+
uint32_t totalSent = 0; // Total messages successfully sent
|
|
61
|
+
uint32_t totalDropped = 0; // Total messages dropped (queue full)
|
|
62
|
+
uint32_t currentSize = 0; // Current queue size
|
|
63
|
+
uint32_t maxSize = 0; // Configured max size
|
|
64
|
+
|
|
65
|
+
// Priority-specific counts
|
|
66
|
+
uint32_t criticalQueued = 0;
|
|
67
|
+
uint32_t highQueued = 0;
|
|
68
|
+
uint32_t normalQueued = 0;
|
|
69
|
+
uint32_t lowQueued = 0;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Queue state change callback type
|
|
73
|
+
typedef std::function<void(QueueState state, uint32_t messageCount)> queueStateChangedCallback_t;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Message Queue for offline/Internet-unavailable mode
|
|
77
|
+
*
|
|
78
|
+
* Provides priority-based message queueing with support for:
|
|
79
|
+
* - Priority levels (CRITICAL messages never dropped)
|
|
80
|
+
* - Queue size limits with intelligent eviction
|
|
81
|
+
* - Statistics tracking
|
|
82
|
+
* - State change callbacks
|
|
83
|
+
*
|
|
84
|
+
* Example usage:
|
|
85
|
+
* \code
|
|
86
|
+
* MessageQueue queue(1000); // Max 1000 messages
|
|
87
|
+
*
|
|
88
|
+
* // Queue a critical message
|
|
89
|
+
* uint32_t msgId = queue.enqueue(PRIORITY_CRITICAL, "alarm_data", "mqtt://...");
|
|
90
|
+
*
|
|
91
|
+
* // Get queue size
|
|
92
|
+
* uint32_t size = queue.size();
|
|
93
|
+
*
|
|
94
|
+
* // Get all messages (for sending)
|
|
95
|
+
* std::vector<QueuedMessage> messages = queue.getMessages();
|
|
96
|
+
*
|
|
97
|
+
* // Remove sent message
|
|
98
|
+
* queue.remove(msgId);
|
|
99
|
+
* \endcode
|
|
100
|
+
*/
|
|
101
|
+
class MessageQueue {
|
|
102
|
+
public:
|
|
103
|
+
/**
|
|
104
|
+
* Constructor
|
|
105
|
+
* @param maxSize Maximum number of messages in queue
|
|
106
|
+
*/
|
|
107
|
+
explicit MessageQueue(uint32_t maxSize = 1000)
|
|
108
|
+
: maxQueueSize(maxSize), nextMessageId(1) {
|
|
109
|
+
messages.reserve(maxSize);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Enqueue a message with priority
|
|
114
|
+
*
|
|
115
|
+
* @param priority Message priority level
|
|
116
|
+
* @param payload Message content
|
|
117
|
+
* @param destination Optional destination metadata
|
|
118
|
+
* @return Message ID if queued, 0 if dropped
|
|
119
|
+
*/
|
|
120
|
+
uint32_t enqueue(MessagePriority priority, const TSTRING& payload,
|
|
121
|
+
const TSTRING& destination = "") {
|
|
122
|
+
// Check if queue is full
|
|
123
|
+
if (messages.size() >= maxQueueSize) {
|
|
124
|
+
// Try to make space by removing low priority messages
|
|
125
|
+
if (!makeSpace(priority)) {
|
|
126
|
+
stats.totalDropped++;
|
|
127
|
+
Log(logger::ERROR, "MessageQueue: Failed to queue message (queue full, priority too low)\n");
|
|
128
|
+
return 0; // Could not queue
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
uint32_t msgId = nextMessageId++;
|
|
133
|
+
uint32_t timestamp = millis();
|
|
134
|
+
|
|
135
|
+
QueuedMessage msg(msgId, priority, timestamp, payload, destination);
|
|
136
|
+
messages.push_back(msg);
|
|
137
|
+
|
|
138
|
+
// Update statistics
|
|
139
|
+
stats.totalQueued++;
|
|
140
|
+
stats.currentSize = messages.size();
|
|
141
|
+
updatePriorityStats();
|
|
142
|
+
|
|
143
|
+
// Check for state change
|
|
144
|
+
checkStateChange();
|
|
145
|
+
|
|
146
|
+
Log(logger::GENERAL, "MessageQueue: Enqueued message #%u (priority=%d, size=%u/%u)\n",
|
|
147
|
+
msgId, priority, messages.size(), maxQueueSize);
|
|
148
|
+
|
|
149
|
+
return msgId;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Remove a message from the queue
|
|
154
|
+
* @param messageId ID of message to remove
|
|
155
|
+
* @return true if message was found and removed
|
|
156
|
+
*/
|
|
157
|
+
bool remove(uint32_t messageId) {
|
|
158
|
+
auto it = std::find_if(messages.begin(), messages.end(),
|
|
159
|
+
[messageId](const QueuedMessage& msg) {
|
|
160
|
+
return msg.id == messageId;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (it != messages.end()) {
|
|
164
|
+
messages.erase(it);
|
|
165
|
+
stats.totalSent++;
|
|
166
|
+
stats.currentSize = messages.size();
|
|
167
|
+
updatePriorityStats();
|
|
168
|
+
checkStateChange();
|
|
169
|
+
|
|
170
|
+
Log(logger::GENERAL, "MessageQueue: Removed message #%u (size=%u)\n",
|
|
171
|
+
messageId, messages.size());
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get all queued messages
|
|
180
|
+
* @return Vector of queued messages (ordered by timestamp)
|
|
181
|
+
*/
|
|
182
|
+
std::vector<QueuedMessage> getMessages() const {
|
|
183
|
+
return messages;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get current queue size
|
|
188
|
+
* @return Number of messages in queue
|
|
189
|
+
*/
|
|
190
|
+
uint32_t size() const {
|
|
191
|
+
return messages.size();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get count of messages with specific priority
|
|
196
|
+
* @param priority Priority level to count
|
|
197
|
+
* @return Number of messages with that priority
|
|
198
|
+
*/
|
|
199
|
+
uint32_t size(MessagePriority priority) const {
|
|
200
|
+
return std::count_if(messages.begin(), messages.end(),
|
|
201
|
+
[priority](const QueuedMessage& msg) {
|
|
202
|
+
return msg.priority == priority;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if queue is empty
|
|
208
|
+
* @return true if no messages queued
|
|
209
|
+
*/
|
|
210
|
+
bool empty() const {
|
|
211
|
+
return messages.empty();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Clear all messages from queue
|
|
216
|
+
*/
|
|
217
|
+
void clear() {
|
|
218
|
+
messages.clear();
|
|
219
|
+
stats.currentSize = 0;
|
|
220
|
+
updatePriorityStats();
|
|
221
|
+
checkStateChange();
|
|
222
|
+
Log(logger::GENERAL, "MessageQueue: Cleared all messages\n");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get queue statistics
|
|
227
|
+
* @return QueueStats structure
|
|
228
|
+
*/
|
|
229
|
+
QueueStats getStats() const {
|
|
230
|
+
return stats;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Set queue state change callback
|
|
235
|
+
* @param callback Function to call when queue state changes
|
|
236
|
+
*/
|
|
237
|
+
void onStateChanged(queueStateChangedCallback_t callback) {
|
|
238
|
+
stateChangedCallback = callback;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Increment send attempt counter for a message
|
|
243
|
+
* @param messageId ID of message
|
|
244
|
+
* @return New attempt count, or 0 if message not found
|
|
245
|
+
*/
|
|
246
|
+
uint32_t incrementAttempts(uint32_t messageId) {
|
|
247
|
+
auto it = std::find_if(messages.begin(), messages.end(),
|
|
248
|
+
[messageId](const QueuedMessage& msg) {
|
|
249
|
+
return msg.id == messageId;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (it != messages.end()) {
|
|
253
|
+
it->attempts++;
|
|
254
|
+
return it->attempts;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Remove old messages (for queue pruning)
|
|
262
|
+
* @param maxAgeMs Maximum age in milliseconds
|
|
263
|
+
* @return Number of messages removed
|
|
264
|
+
*/
|
|
265
|
+
uint32_t pruneOldMessages(uint32_t maxAgeMs) {
|
|
266
|
+
uint32_t currentTime = millis();
|
|
267
|
+
uint32_t removedCount = 0;
|
|
268
|
+
|
|
269
|
+
auto it = messages.begin();
|
|
270
|
+
while (it != messages.end()) {
|
|
271
|
+
if (currentTime - it->timestamp > maxAgeMs) {
|
|
272
|
+
it = messages.erase(it);
|
|
273
|
+
removedCount++;
|
|
274
|
+
} else {
|
|
275
|
+
++it;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (removedCount > 0) {
|
|
280
|
+
stats.currentSize = messages.size();
|
|
281
|
+
updatePriorityStats();
|
|
282
|
+
checkStateChange();
|
|
283
|
+
Log(logger::GENERAL, "MessageQueue: Pruned %u old messages\n", removedCount);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return removedCount;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private:
|
|
290
|
+
std::vector<QueuedMessage> messages;
|
|
291
|
+
uint32_t maxQueueSize;
|
|
292
|
+
uint32_t nextMessageId;
|
|
293
|
+
QueueStats stats;
|
|
294
|
+
QueueState currentState = QUEUE_EMPTY;
|
|
295
|
+
queueStateChangedCallback_t stateChangedCallback;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Try to make space in queue by removing low priority messages
|
|
299
|
+
* @param newPriority Priority of message trying to be added
|
|
300
|
+
* @return true if space was made
|
|
301
|
+
*/
|
|
302
|
+
bool makeSpace(MessagePriority newPriority) {
|
|
303
|
+
// CRITICAL messages can always evict lower priority
|
|
304
|
+
// HIGH messages can evict NORMAL and LOW
|
|
305
|
+
// NORMAL messages can only evict LOW
|
|
306
|
+
// LOW messages cannot evict anything
|
|
307
|
+
|
|
308
|
+
if (newPriority == PRIORITY_LOW) {
|
|
309
|
+
return false; // LOW priority can't evict anything
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Try to remove lowest priority messages first
|
|
313
|
+
for (int targetPriority = PRIORITY_LOW; targetPriority > newPriority; targetPriority--) {
|
|
314
|
+
auto it = std::find_if(messages.begin(), messages.end(),
|
|
315
|
+
[targetPriority](const QueuedMessage& msg) {
|
|
316
|
+
return msg.priority == static_cast<MessagePriority>(targetPriority);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (it != messages.end()) {
|
|
320
|
+
Log(logger::GENERAL, "MessageQueue: Evicting message #%u (priority=%d) to make space\n",
|
|
321
|
+
it->id, it->priority);
|
|
322
|
+
messages.erase(it);
|
|
323
|
+
stats.totalDropped++;
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Update priority-specific statistics
|
|
333
|
+
*/
|
|
334
|
+
void updatePriorityStats() {
|
|
335
|
+
stats.criticalQueued = size(PRIORITY_CRITICAL);
|
|
336
|
+
stats.highQueued = size(PRIORITY_HIGH);
|
|
337
|
+
stats.normalQueued = size(PRIORITY_NORMAL);
|
|
338
|
+
stats.lowQueued = size(PRIORITY_LOW);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Check if queue state has changed and fire callback
|
|
343
|
+
*/
|
|
344
|
+
void checkStateChange() {
|
|
345
|
+
QueueState newState;
|
|
346
|
+
|
|
347
|
+
if (messages.empty()) {
|
|
348
|
+
newState = QUEUE_EMPTY;
|
|
349
|
+
} else if (messages.size() >= maxQueueSize) {
|
|
350
|
+
newState = QUEUE_FULL;
|
|
351
|
+
} else if (messages.size() >= maxQueueSize * 3 / 4) {
|
|
352
|
+
newState = QUEUE_75_PERCENT;
|
|
353
|
+
} else {
|
|
354
|
+
newState = QUEUE_NORMAL;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (newState != currentState) {
|
|
358
|
+
currentState = newState;
|
|
359
|
+
if (stateChangedCallback) {
|
|
360
|
+
stateChangedCallback(currentState, messages.size());
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
} // namespace painlessmesh
|
|
367
|
+
|
|
368
|
+
#endif // _PAINLESS_MESH_MESSAGE_QUEUE_HPP_
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#include "painlessmesh/configuration.hpp"
|
|
6
6
|
|
|
7
7
|
#include "painlessmesh/router.hpp"
|
|
8
|
+
#include <vector>
|
|
8
9
|
|
|
9
10
|
namespace painlessmesh {
|
|
10
11
|
|
|
@@ -93,6 +94,74 @@ class NeighbourPackage : public plugin::SinglePackage {
|
|
|
93
94
|
NeighbourPackage(JsonObject jsonObj) : SinglePackage(jsonObj) {}
|
|
94
95
|
};
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Bridge Coordination Package (Type 613)
|
|
99
|
+
*
|
|
100
|
+
* Used for multi-bridge coordination in advanced deployments.
|
|
101
|
+
* Bridges use this to:
|
|
102
|
+
* - Announce their presence and role (primary/secondary)
|
|
103
|
+
* - Exchange peer bridge lists
|
|
104
|
+
* - Report current load for load balancing decisions
|
|
105
|
+
* - Coordinate conflict resolution
|
|
106
|
+
*
|
|
107
|
+
* This enables features like:
|
|
108
|
+
* - Multiple simultaneous bridges (hot standby)
|
|
109
|
+
* - Load balancing across bridges
|
|
110
|
+
* - Geographic distribution
|
|
111
|
+
* - Traffic shaping
|
|
112
|
+
*/
|
|
113
|
+
class BridgeCoordinationPackage : public plugin::BroadcastPackage {
|
|
114
|
+
public:
|
|
115
|
+
uint8_t priority = 5; // Bridge priority (10=highest, 1=lowest)
|
|
116
|
+
TSTRING role = "secondary"; // Role: "primary", "secondary", "standby"
|
|
117
|
+
std::vector<uint32_t> peerBridges; // List of known bridge node IDs
|
|
118
|
+
uint8_t load = 0; // Current load percentage (0-100)
|
|
119
|
+
uint32_t timestamp = 0; // Coordination timestamp
|
|
120
|
+
int noJsonFields = 8; // Base fields (3) + new fields (5)
|
|
121
|
+
|
|
122
|
+
BridgeCoordinationPackage() : BroadcastPackage(613) {}
|
|
123
|
+
|
|
124
|
+
BridgeCoordinationPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
|
|
125
|
+
priority = jsonObj["priority"] | 5;
|
|
126
|
+
role = jsonObj["role"].as<TSTRING>();
|
|
127
|
+
load = jsonObj["load"] | 0;
|
|
128
|
+
timestamp = jsonObj["timestamp"] | 0;
|
|
129
|
+
|
|
130
|
+
// Parse peer bridge array
|
|
131
|
+
if (jsonObj["peerBridges"].is<JsonArray>()) {
|
|
132
|
+
JsonArray peers = jsonObj["peerBridges"];
|
|
133
|
+
for (JsonVariant peer : peers) {
|
|
134
|
+
peerBridges.push_back(peer.as<uint32_t>());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
JsonObject addTo(JsonObject&& jsonObj) const {
|
|
140
|
+
jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
|
|
141
|
+
jsonObj["priority"] = priority;
|
|
142
|
+
jsonObj["role"] = role;
|
|
143
|
+
jsonObj["load"] = load;
|
|
144
|
+
jsonObj["timestamp"] = timestamp;
|
|
145
|
+
|
|
146
|
+
// Add peer bridge array
|
|
147
|
+
JsonArray peers = jsonObj["peerBridges"].to<JsonArray>();
|
|
148
|
+
for (uint32_t peerId : peerBridges) {
|
|
149
|
+
peers.add(peerId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return jsonObj;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#if ARDUINOJSON_VERSION_MAJOR < 7
|
|
156
|
+
size_t jsonObjectSize() const {
|
|
157
|
+
// Base fields + string length + array overhead
|
|
158
|
+
size_t peerArraySize = JSON_ARRAY_SIZE(peerBridges.size()) +
|
|
159
|
+
(peerBridges.size() * sizeof(uint32_t));
|
|
160
|
+
return JSON_OBJECT_SIZE(noJsonFields) + role.length() + peerArraySize;
|
|
161
|
+
}
|
|
162
|
+
#endif
|
|
163
|
+
};
|
|
164
|
+
|
|
96
165
|
/**
|
|
97
166
|
* Handle different plugins
|
|
98
167
|
*
|