@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alteriom/painlessmesh",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "painlessMesh is a user-friendly library for creating mesh networks with ESP8266 and ESP32 devices. This Alteriom fork includes additional packages for sensor data (SensorPackage), device commands (CommandPackage), and status monitoring (StatusPackage). It handles routing and network management automatically, so you can focus on your application. The library uses JSON-based messaging and syncs time across all nodes, making it ideal for coordinated behaviour like synchronized light displays or sensor networks reporting to a central node.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"arduino",
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -18,6 +18,13 @@ namespace painlessmesh {
|
|
|
18
18
|
namespace wifi {
|
|
19
19
|
class Mesh : public painlessmesh::Mesh<Connection> {
|
|
20
20
|
public:
|
|
21
|
+
// Multi-bridge selection strategy enum (must be declared early)
|
|
22
|
+
enum BridgeSelectionStrategy {
|
|
23
|
+
PRIORITY_BASED = 0, // Use highest priority bridge (default)
|
|
24
|
+
ROUND_ROBIN = 1, // Distribute load evenly
|
|
25
|
+
BEST_SIGNAL = 2 // Use bridge with best RSSI
|
|
26
|
+
};
|
|
27
|
+
|
|
21
28
|
/** Initialize the mesh network
|
|
22
29
|
*
|
|
23
30
|
* Add this to your setup() function. This routine does the following things:
|
|
@@ -279,6 +286,51 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
279
286
|
Log(STARTUP, " Port: %d\n", port);
|
|
280
287
|
}
|
|
281
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Initialize mesh as a bridge node with priority (for multi-bridge mode)
|
|
291
|
+
*
|
|
292
|
+
* This overload adds bridge priority configuration for multi-bridge deployments.
|
|
293
|
+
* Priority determines which bridge is preferred when multiple bridges are available.
|
|
294
|
+
*
|
|
295
|
+
* @param meshSSID The name of your mesh network
|
|
296
|
+
* @param meshPassword WiFi password for the mesh
|
|
297
|
+
* @param routerSSID SSID of the router to connect to
|
|
298
|
+
* @param routerPassword Password for the router
|
|
299
|
+
* @param baseScheduler Task scheduler for mesh operations
|
|
300
|
+
* @param port TCP port for mesh communication (default: 5555)
|
|
301
|
+
* @param priority Bridge priority: 10=highest (primary), 5=medium (secondary), 1=lowest (default: 5)
|
|
302
|
+
*/
|
|
303
|
+
void initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
|
|
304
|
+
TSTRING routerSSID, TSTRING routerPassword,
|
|
305
|
+
Scheduler *baseScheduler, uint16_t port, uint8_t priority) {
|
|
306
|
+
using namespace logger;
|
|
307
|
+
|
|
308
|
+
// Validate and store priority
|
|
309
|
+
if (priority < 1) priority = 1;
|
|
310
|
+
if (priority > 10) priority = 10;
|
|
311
|
+
bridgePriority = priority;
|
|
312
|
+
|
|
313
|
+
// Store role based on priority
|
|
314
|
+
if (priority >= 8) {
|
|
315
|
+
bridgeRole = "primary";
|
|
316
|
+
} else if (priority >= 5) {
|
|
317
|
+
bridgeRole = "secondary";
|
|
318
|
+
} else {
|
|
319
|
+
bridgeRole = "standby";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
Log(STARTUP, "=== Bridge Mode Initialization (Priority: %d, Role: %s) ===\n",
|
|
323
|
+
priority, bridgeRole.c_str());
|
|
324
|
+
|
|
325
|
+
// Call the base initAsBridge method
|
|
326
|
+
initAsBridge(meshSSID, meshPassword, routerSSID, routerPassword, baseScheduler, port);
|
|
327
|
+
|
|
328
|
+
// Setup multi-bridge coordination if enabled
|
|
329
|
+
if (multiBridgeEnabled) {
|
|
330
|
+
initBridgeCoordination();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
282
334
|
/**
|
|
283
335
|
* Connect (as a station) to a specified network and ip
|
|
284
336
|
*
|
|
@@ -418,6 +470,148 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
418
470
|
bridgeRoleChangedCallback = callback;
|
|
419
471
|
}
|
|
420
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Enable or disable multi-bridge coordination mode
|
|
475
|
+
*
|
|
476
|
+
* When enabled, multiple bridges can operate simultaneously for:
|
|
477
|
+
* - Load balancing across multiple Internet connections
|
|
478
|
+
* - Geographic distribution
|
|
479
|
+
* - Hot standby redundancy without failover delays
|
|
480
|
+
*
|
|
481
|
+
* @param enabled true to enable multi-bridge mode, false for single-bridge (default)
|
|
482
|
+
*/
|
|
483
|
+
void enableMultiBridge(bool enabled) {
|
|
484
|
+
multiBridgeEnabled = enabled;
|
|
485
|
+
if (enabled) {
|
|
486
|
+
Log(logger::GENERAL, "enableMultiBridge(): Multi-bridge coordination enabled\n");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Set bridge selection strategy for multi-bridge mode
|
|
492
|
+
*
|
|
493
|
+
* @param strategy Selection strategy:
|
|
494
|
+
* - PRIORITY_BASED: Always use highest priority bridge (default)
|
|
495
|
+
* - ROUND_ROBIN: Distribute load evenly across bridges
|
|
496
|
+
* - BEST_SIGNAL: Always use bridge with best RSSI
|
|
497
|
+
*/
|
|
498
|
+
void setBridgeSelectionStrategy(BridgeSelectionStrategy strategy) {
|
|
499
|
+
bridgeSelectionStrategy = strategy;
|
|
500
|
+
Log(logger::GENERAL, "setBridgeSelectionStrategy(): Strategy set to %d\n", (int)strategy);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Set maximum number of concurrent bridges in multi-bridge mode
|
|
505
|
+
*
|
|
506
|
+
* @param maxBridges Maximum bridges to track (default: 2, max: 5)
|
|
507
|
+
*/
|
|
508
|
+
void setMaxBridges(uint8_t maxBridges) {
|
|
509
|
+
if (maxBridges < 1) maxBridges = 1;
|
|
510
|
+
if (maxBridges > 5) maxBridges = 5;
|
|
511
|
+
maxConcurrentBridges = maxBridges;
|
|
512
|
+
Log(logger::GENERAL, "setMaxBridges(): Max concurrent bridges set to %d\n", maxBridges);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Get list of all active bridges (with Internet connection)
|
|
517
|
+
*
|
|
518
|
+
* @return vector of node IDs for active bridges
|
|
519
|
+
*/
|
|
520
|
+
std::vector<uint32_t> getActiveBridges() {
|
|
521
|
+
std::vector<uint32_t> activeBridges;
|
|
522
|
+
auto bridges = this->getBridges();
|
|
523
|
+
|
|
524
|
+
for (const auto& bridge : bridges) {
|
|
525
|
+
if (bridge.internetConnected && bridge.isHealthy()) {
|
|
526
|
+
activeBridges.push_back(bridge.nodeId);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return activeBridges;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get recommended bridge for message transmission
|
|
535
|
+
*
|
|
536
|
+
* Uses the configured bridge selection strategy to pick the best bridge.
|
|
537
|
+
* Returns 0 if no suitable bridge is available.
|
|
538
|
+
*
|
|
539
|
+
* @return node ID of recommended bridge, or 0 if none available
|
|
540
|
+
*/
|
|
541
|
+
uint32_t getRecommendedBridge() {
|
|
542
|
+
auto activeBridges = getActiveBridges();
|
|
543
|
+
|
|
544
|
+
if (activeBridges.empty()) {
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Single bridge - return it
|
|
549
|
+
if (activeBridges.size() == 1) {
|
|
550
|
+
return activeBridges[0];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Multi-bridge mode: apply selection strategy
|
|
554
|
+
switch (bridgeSelectionStrategy) {
|
|
555
|
+
case ROUND_ROBIN: {
|
|
556
|
+
// Simple round-robin: cycle through bridges
|
|
557
|
+
lastSelectedBridgeIndex = (lastSelectedBridgeIndex + 1) % activeBridges.size();
|
|
558
|
+
return activeBridges[lastSelectedBridgeIndex];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
case BEST_SIGNAL: {
|
|
562
|
+
// Find bridge with best RSSI
|
|
563
|
+
uint32_t bestBridge = 0;
|
|
564
|
+
int8_t bestRSSI = -127;
|
|
565
|
+
|
|
566
|
+
for (const auto& bridge : this->getBridges()) {
|
|
567
|
+
if (bridge.internetConnected && bridge.isHealthy() && bridge.routerRSSI > bestRSSI) {
|
|
568
|
+
bestRSSI = bridge.routerRSSI;
|
|
569
|
+
bestBridge = bridge.nodeId;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return bestBridge;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
case PRIORITY_BASED:
|
|
576
|
+
default: {
|
|
577
|
+
// Use highest priority bridge (stored in bridgePriorities map)
|
|
578
|
+
uint32_t bestBridge = 0;
|
|
579
|
+
uint8_t highestPriority = 0;
|
|
580
|
+
|
|
581
|
+
for (uint32_t bridgeId : activeBridges) {
|
|
582
|
+
uint8_t priority = bridgePriorities[bridgeId];
|
|
583
|
+
if (priority > highestPriority) {
|
|
584
|
+
highestPriority = priority;
|
|
585
|
+
bestBridge = bridgeId;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// If no priority info, use first active bridge
|
|
590
|
+
return bestBridge ? bestBridge : activeBridges[0];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Select a specific bridge for next transmission
|
|
597
|
+
*
|
|
598
|
+
* This overrides the automatic bridge selection for one message.
|
|
599
|
+
*
|
|
600
|
+
* @param bridgeNodeId Node ID of bridge to use
|
|
601
|
+
*/
|
|
602
|
+
void selectBridge(uint32_t bridgeNodeId) {
|
|
603
|
+
selectedBridgeOverride = bridgeNodeId;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Check if multi-bridge mode is enabled
|
|
608
|
+
*
|
|
609
|
+
* @return true if multi-bridge coordination is enabled
|
|
610
|
+
*/
|
|
611
|
+
bool isMultiBridgeEnabled() const {
|
|
612
|
+
return multiBridgeEnabled;
|
|
613
|
+
}
|
|
614
|
+
|
|
421
615
|
void stop() {
|
|
422
616
|
// remove all WiFi events
|
|
423
617
|
#ifdef ESP32
|
|
@@ -498,6 +692,115 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
498
692
|
this->bridgeStatusIntervalMs);
|
|
499
693
|
}
|
|
500
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Initialize bridge coordination broadcasting
|
|
697
|
+
* Sets up periodic coordination messages between bridges
|
|
698
|
+
*/
|
|
699
|
+
void initBridgeCoordination() {
|
|
700
|
+
using namespace logger;
|
|
701
|
+
|
|
702
|
+
if (!this->isBridge() || !multiBridgeEnabled) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
Log(STARTUP, "initBridgeCoordination(): Setting up multi-bridge coordination\n");
|
|
707
|
+
|
|
708
|
+
// Register handler for incoming coordination messages (Type 613)
|
|
709
|
+
this->callbackList.onPackage(
|
|
710
|
+
613, // BRIDGE_COORDINATION type
|
|
711
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
|
|
712
|
+
JsonDocument doc;
|
|
713
|
+
TSTRING str;
|
|
714
|
+
variant.printTo(str);
|
|
715
|
+
deserializeJson(doc, str);
|
|
716
|
+
JsonObject obj = doc.as<JsonObject>();
|
|
717
|
+
|
|
718
|
+
if (obj["priority"].is<unsigned int>()) {
|
|
719
|
+
uint32_t fromNode = obj["from"];
|
|
720
|
+
uint8_t priority = obj["priority"];
|
|
721
|
+
TSTRING role = obj["role"].as<TSTRING>();
|
|
722
|
+
uint8_t load = obj["load"] | 0;
|
|
723
|
+
|
|
724
|
+
// Store bridge priority for selection decisions
|
|
725
|
+
bridgePriorities[fromNode] = priority;
|
|
726
|
+
|
|
727
|
+
// Update peer bridges list
|
|
728
|
+
if (obj["peerBridges"].is<JsonArray>()) {
|
|
729
|
+
JsonArray peers = obj["peerBridges"];
|
|
730
|
+
for (JsonVariant peer : peers) {
|
|
731
|
+
uint32_t peerId = peer.as<uint32_t>();
|
|
732
|
+
if (peerId != this->nodeId &&
|
|
733
|
+
std::find(knownBridgePeers.begin(), knownBridgePeers.end(), peerId) == knownBridgePeers.end()) {
|
|
734
|
+
knownBridgePeers.push_back(peerId);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
Log(CONNECTION, "Bridge coordination from %u: priority=%d, role=%s, load=%d%%\n",
|
|
740
|
+
fromNode, priority, role.c_str(), load);
|
|
741
|
+
}
|
|
742
|
+
return false; // Don't consume the package
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Create periodic task to send coordination messages
|
|
746
|
+
bridgeCoordinationTask = this->addTask(
|
|
747
|
+
30000, // 30 seconds interval
|
|
748
|
+
TASK_FOREVER,
|
|
749
|
+
[this]() {
|
|
750
|
+
this->sendBridgeCoordination();
|
|
751
|
+
}
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
Log(STARTUP, "Bridge coordination enabled (priority: %d, role: %s)\n",
|
|
755
|
+
bridgePriority, bridgeRole.c_str());
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Send bridge coordination message to other bridges
|
|
760
|
+
* Called periodically in multi-bridge mode
|
|
761
|
+
*/
|
|
762
|
+
void sendBridgeCoordination() {
|
|
763
|
+
using namespace logger;
|
|
764
|
+
|
|
765
|
+
if (!this->isBridge() || !multiBridgeEnabled) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Calculate current load (simplified: based on node count)
|
|
770
|
+
uint8_t currentLoad = 0;
|
|
771
|
+
auto nodeCount = this->getNodeList(false).size();
|
|
772
|
+
if (nodeCount > 0) {
|
|
773
|
+
currentLoad = (nodeCount * 100) / MAX_CONN;
|
|
774
|
+
if (currentLoad > 100) currentLoad = 100;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Create coordination message
|
|
778
|
+
JsonDocument doc;
|
|
779
|
+
JsonObject obj = doc.to<JsonObject>();
|
|
780
|
+
|
|
781
|
+
obj["type"] = 613; // BRIDGE_COORDINATION
|
|
782
|
+
obj["from"] = this->nodeId;
|
|
783
|
+
obj["routing"] = 2; // BROADCAST
|
|
784
|
+
obj["priority"] = bridgePriority;
|
|
785
|
+
obj["role"] = bridgeRole;
|
|
786
|
+
obj["load"] = currentLoad;
|
|
787
|
+
obj["timestamp"] = this->getNodeTime();
|
|
788
|
+
obj["message_type"] = 613;
|
|
789
|
+
|
|
790
|
+
// Add peer bridges list
|
|
791
|
+
JsonArray peers = obj["peerBridges"].to<JsonArray>();
|
|
792
|
+
for (uint32_t peerId : knownBridgePeers) {
|
|
793
|
+
peers.add(peerId);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
String msg;
|
|
797
|
+
serializeJson(doc, msg);
|
|
798
|
+
this->sendBroadcast(msg);
|
|
799
|
+
|
|
800
|
+
Log(CONNECTION, "Bridge coordination sent: priority=%d, role=%s, load=%d%%\n",
|
|
801
|
+
bridgePriority, bridgeRole.c_str(), currentLoad);
|
|
802
|
+
}
|
|
803
|
+
|
|
501
804
|
/**
|
|
502
805
|
* Scan for router and return its signal strength
|
|
503
806
|
*
|
|
@@ -578,7 +881,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
578
881
|
electionCandidates.push_back(selfCandidate);
|
|
579
882
|
|
|
580
883
|
// Broadcast candidacy using JSON directly (avoiding dependency on alteriom package)
|
|
581
|
-
|
|
884
|
+
JsonDocument doc;
|
|
582
885
|
JsonObject obj = doc.to<JsonObject>();
|
|
583
886
|
obj["type"] = 611; // BRIDGE_ELECTION
|
|
584
887
|
obj["from"] = this->nodeId;
|
|
@@ -721,7 +1024,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
721
1024
|
}
|
|
722
1025
|
|
|
723
1026
|
// Broadcast takeover announcement
|
|
724
|
-
|
|
1027
|
+
JsonDocument doc;
|
|
725
1028
|
JsonObject obj = doc.to<JsonObject>();
|
|
726
1029
|
obj["type"] = 612; // BRIDGE_TAKEOVER
|
|
727
1030
|
obj["from"] = this->nodeId;
|
|
@@ -789,7 +1092,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
789
1092
|
// Create bridge status package
|
|
790
1093
|
// We need to include the package header here since we're in wifi namespace
|
|
791
1094
|
// The package will be sent as a JSON string
|
|
792
|
-
|
|
1095
|
+
JsonDocument doc;
|
|
793
1096
|
JsonObject obj = doc.to<JsonObject>();
|
|
794
1097
|
|
|
795
1098
|
obj["type"] = 610; // BRIDGE_STATUS type
|
|
@@ -937,6 +1240,19 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
937
1240
|
uint32_t electionDeadline = 0;
|
|
938
1241
|
std::vector<BridgeCandidate> electionCandidates;
|
|
939
1242
|
std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
|
|
1243
|
+
|
|
1244
|
+
// Multi-bridge coordination state and configuration
|
|
1245
|
+
protected:
|
|
1246
|
+
bool multiBridgeEnabled = false;
|
|
1247
|
+
BridgeSelectionStrategy bridgeSelectionStrategy = PRIORITY_BASED;
|
|
1248
|
+
uint8_t maxConcurrentBridges = 2;
|
|
1249
|
+
uint8_t bridgePriority = 5; // Default medium priority
|
|
1250
|
+
TSTRING bridgeRole = "secondary"; // Default role
|
|
1251
|
+
std::shared_ptr<Task> bridgeCoordinationTask;
|
|
1252
|
+
std::map<uint32_t, uint8_t> bridgePriorities; // nodeId -> priority mapping
|
|
1253
|
+
std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
|
|
1254
|
+
uint32_t selectedBridgeOverride = 0; // Manual bridge selection override
|
|
1255
|
+
size_t lastSelectedBridgeIndex = 0; // For round-robin selection
|
|
940
1256
|
};
|
|
941
1257
|
} // namespace wifi
|
|
942
1258
|
}; // namespace painlessmesh
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
#include "painlessmesh/connection.hpp"
|
|
9
9
|
#include "painlessmesh/logger.hpp"
|
|
10
|
+
#include "painlessmesh/message_queue.hpp"
|
|
10
11
|
#include "painlessmesh/ntp.hpp"
|
|
11
12
|
#include "painlessmesh/plugin.hpp"
|
|
12
13
|
#include "painlessmesh/protocol.hpp"
|
|
@@ -810,6 +811,200 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
810
811
|
rtcSyncCompleteCallback = onRTCSyncComplete;
|
|
811
812
|
}
|
|
812
813
|
|
|
814
|
+
//
|
|
815
|
+
// Message Queue API
|
|
816
|
+
//
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Enable or disable message queueing for offline mode
|
|
820
|
+
*
|
|
821
|
+
* When enabled, messages can be queued when Internet is unavailable
|
|
822
|
+
* and automatically flushed when connection is restored.
|
|
823
|
+
*
|
|
824
|
+
* @param enabled True to enable queueing, false to disable
|
|
825
|
+
* @param maxSize Maximum number of messages in queue (default 1000)
|
|
826
|
+
*
|
|
827
|
+
* \code
|
|
828
|
+
* mesh.enableMessageQueue(true, 500); // Enable with 500 message capacity
|
|
829
|
+
* \endcode
|
|
830
|
+
*/
|
|
831
|
+
void enableMessageQueue(bool enabled, uint32_t maxSize = 1000) {
|
|
832
|
+
if (enabled && !messageQueue) {
|
|
833
|
+
messageQueue = new MessageQueue(maxSize);
|
|
834
|
+
Log(logger::GENERAL, "enableMessageQueue(): Queue enabled with capacity %u\n", maxSize);
|
|
835
|
+
} else if (!enabled && messageQueue) {
|
|
836
|
+
delete messageQueue;
|
|
837
|
+
messageQueue = nullptr;
|
|
838
|
+
Log(logger::GENERAL, "enableMessageQueue(): Queue disabled\n");
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Queue a message with priority for later delivery
|
|
844
|
+
*
|
|
845
|
+
* Use this to queue critical messages when Internet is unavailable.
|
|
846
|
+
* Messages are automatically delivered when connection is restored.
|
|
847
|
+
*
|
|
848
|
+
* @param payload Message content to queue
|
|
849
|
+
* @param destination Optional destination metadata (e.g., MQTT topic, HTTP endpoint)
|
|
850
|
+
* @param priority Message priority (default: PRIORITY_NORMAL)
|
|
851
|
+
* @return Message ID if successfully queued, 0 if failed
|
|
852
|
+
*
|
|
853
|
+
* \code
|
|
854
|
+
* // Queue critical alarm
|
|
855
|
+
* uint32_t msgId = mesh.queueMessage(
|
|
856
|
+
* alarmData.toJSON(),
|
|
857
|
+
* "mqtt://cloud.example.com/alarms",
|
|
858
|
+
* PRIORITY_CRITICAL
|
|
859
|
+
* );
|
|
860
|
+
* \endcode
|
|
861
|
+
*/
|
|
862
|
+
uint32_t queueMessage(const TSTRING& payload,
|
|
863
|
+
const TSTRING& destination = "",
|
|
864
|
+
MessagePriority priority = PRIORITY_NORMAL) {
|
|
865
|
+
if (!messageQueue) {
|
|
866
|
+
Log(logger::ERROR, "queueMessage(): Message queue not enabled\n");
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return messageQueue->enqueue(priority, payload, destination);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Flush all queued messages
|
|
875
|
+
*
|
|
876
|
+
* Attempts to send all queued messages. This is typically called
|
|
877
|
+
* automatically when Internet connection is restored, but can be
|
|
878
|
+
* called manually.
|
|
879
|
+
*
|
|
880
|
+
* Note: This returns the messages for the application to send.
|
|
881
|
+
* The application is responsible for actually transmitting them
|
|
882
|
+
* and calling removeQueuedMessage() when successful.
|
|
883
|
+
*
|
|
884
|
+
* @return Vector of queued messages to send
|
|
885
|
+
*
|
|
886
|
+
* \code
|
|
887
|
+
* auto messages = mesh.flushMessageQueue();
|
|
888
|
+
* for (auto& msg : messages) {
|
|
889
|
+
* if (sendToCloud(msg.payload, msg.destination)) {
|
|
890
|
+
* mesh.removeQueuedMessage(msg.id);
|
|
891
|
+
* }
|
|
892
|
+
* }
|
|
893
|
+
* \endcode
|
|
894
|
+
*/
|
|
895
|
+
std::vector<QueuedMessage> flushMessageQueue() {
|
|
896
|
+
if (!messageQueue) {
|
|
897
|
+
return std::vector<QueuedMessage>();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return messageQueue->getMessages();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Remove a successfully sent message from the queue
|
|
905
|
+
*
|
|
906
|
+
* @param messageId ID of message to remove
|
|
907
|
+
* @return true if message was found and removed
|
|
908
|
+
*/
|
|
909
|
+
bool removeQueuedMessage(uint32_t messageId) {
|
|
910
|
+
if (!messageQueue) {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return messageQueue->remove(messageId);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Increment send attempt counter for a message
|
|
919
|
+
*
|
|
920
|
+
* @param messageId ID of message
|
|
921
|
+
* @return New attempt count, or 0 if message not found
|
|
922
|
+
*/
|
|
923
|
+
uint32_t incrementQueuedMessageAttempts(uint32_t messageId) {
|
|
924
|
+
if (!messageQueue) {
|
|
925
|
+
return 0;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return messageQueue->incrementAttempts(messageId);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Get number of queued messages
|
|
933
|
+
*
|
|
934
|
+
* @param priority Optional priority level to count (counts all if not specified)
|
|
935
|
+
* @return Number of queued messages
|
|
936
|
+
*/
|
|
937
|
+
uint32_t getQueuedMessageCount(MessagePriority priority) {
|
|
938
|
+
if (!messageQueue) {
|
|
939
|
+
return 0;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return messageQueue->size(priority);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
uint32_t getQueuedMessageCount() {
|
|
946
|
+
if (!messageQueue) {
|
|
947
|
+
return 0;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return messageQueue->size();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Get queue statistics
|
|
955
|
+
*
|
|
956
|
+
* @return QueueStats structure with detailed statistics
|
|
957
|
+
*/
|
|
958
|
+
QueueStats getQueueStats() {
|
|
959
|
+
if (!messageQueue) {
|
|
960
|
+
return QueueStats();
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return messageQueue->getStats();
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Set callback for queue state changes
|
|
968
|
+
*
|
|
969
|
+
* Fires when queue state changes (EMPTY, NORMAL, 75%, FULL)
|
|
970
|
+
*
|
|
971
|
+
* \code
|
|
972
|
+
* mesh.onQueueStateChanged([](QueueState state, uint32_t count) {
|
|
973
|
+
* if (state == QUEUE_75_PERCENT) {
|
|
974
|
+
* Serial.printf("Warning: Queue 75%% full (%u messages)\n", count);
|
|
975
|
+
* }
|
|
976
|
+
* });
|
|
977
|
+
* \endcode
|
|
978
|
+
*/
|
|
979
|
+
void onQueueStateChanged(queueStateChangedCallback_t callback) {
|
|
980
|
+
if (messageQueue) {
|
|
981
|
+
messageQueue->onStateChanged(callback);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Prune old messages from the queue
|
|
987
|
+
*
|
|
988
|
+
* @param maxAgeMs Maximum age in milliseconds
|
|
989
|
+
* @return Number of messages removed
|
|
990
|
+
*/
|
|
991
|
+
uint32_t pruneQueue(uint32_t maxAgeMs) {
|
|
992
|
+
if (!messageQueue) {
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return messageQueue->pruneOldMessages(maxAgeMs);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Clear all messages from the queue
|
|
1001
|
+
*/
|
|
1002
|
+
void clearQueue() {
|
|
1003
|
+
if (messageQueue) {
|
|
1004
|
+
messageQueue->clear();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
813
1008
|
/**
|
|
814
1009
|
* Are we connected/know a route to the given node?
|
|
815
1010
|
*
|
|
@@ -1568,6 +1763,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1568
1763
|
~Mesh() {
|
|
1569
1764
|
this->stop();
|
|
1570
1765
|
if (!isExternalScheduler) delete mScheduler;
|
|
1766
|
+
if (messageQueue) delete messageQueue;
|
|
1571
1767
|
}
|
|
1572
1768
|
|
|
1573
1769
|
protected:
|
|
@@ -1621,6 +1817,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1621
1817
|
nodeDelayCallback_t nodeDelayReceivedCallback;
|
|
1622
1818
|
bridgeStatusChangedCallback_t bridgeStatusChangedCallback;
|
|
1623
1819
|
rtcSyncCompleteCallback_t rtcSyncCompleteCallback;
|
|
1820
|
+
|
|
1821
|
+
// Message queue for offline mode
|
|
1822
|
+
MessageQueue* messageQueue = nullptr;
|
|
1823
|
+
|
|
1624
1824
|
#ifdef ESP32
|
|
1625
1825
|
SemaphoreHandle_t xSemaphore = NULL;
|
|
1626
1826
|
#endif
|