@alteriom/painlessmesh 1.8.0 → 1.8.1

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.
@@ -0,0 +1,265 @@
1
+ //************************************************************
2
+ // Queued Alarms Example - Message Queueing for Offline Mode
3
+ //
4
+ // Demonstrates priority-based message queueing for critical alarms
5
+ // when Internet connection is unavailable. Perfect for IoT systems
6
+ // that cannot afford to lose critical data.
7
+ //
8
+ // Use Case: Fish farm dissolved oxygen monitoring
9
+ // - CRITICAL alarms (low O2) must never be lost
10
+ // - Queue messages during Internet outages
11
+ // - Automatic delivery when connection restored
12
+ //
13
+ // Hardware: ESP32 or ESP8266
14
+ //************************************************************
15
+
16
+ #include "painlessMesh.h"
17
+
18
+ // Mesh configuration
19
+ #define MESH_PREFIX "FishFarmMesh"
20
+ #define MESH_PASSWORD "somethingSneaky"
21
+ #define MESH_PORT 5555
22
+
23
+ // Router credentials for bridge node
24
+ #define ROUTER_SSID "YourWiFiSSID"
25
+ #define ROUTER_PASSWORD "YourWiFiPassword"
26
+
27
+ // Sensor thresholds (mg/L for dissolved oxygen)
28
+ #define CRITICAL_O2_THRESHOLD 3.0
29
+ #define WARNING_O2_THRESHOLD 5.0
30
+
31
+ // Queue configuration
32
+ #define MAX_QUEUE_SIZE 500
33
+ #define QUEUE_PRUNE_AGE (24 * 60 * 60 * 1000) // 24 hours in ms
34
+
35
+ Scheduler userScheduler;
36
+ painlessMesh mesh;
37
+
38
+ bool offlineMode = false;
39
+ uint32_t lastO2Check = 0;
40
+ uint32_t lastQueuePrune = 0;
41
+
42
+ // Simulated sensor reading (replace with actual sensor code)
43
+ float readDissolvedOxygenSensor() {
44
+ // In real application, read from actual sensor
45
+ // For demo, simulate varying O2 levels
46
+ static float o2Level = 7.0;
47
+ o2Level += random(-20, 20) / 10.0; // +/- 2.0 mg/L variation
48
+ o2Level = constrain(o2Level, 2.0, 10.0);
49
+ return o2Level;
50
+ }
51
+
52
+ // Send critical O2 alarm
53
+ void sendCriticalAlarm(float o2Level) {
54
+ // Create alarm message (in real app, use JSON)
55
+ String payload = String("{\"type\":\"CRITICAL_ALARM\",\"sensor\":\"O2\",\"value\":")
56
+ + String(o2Level, 2) + ",\"threshold\":"
57
+ + String(CRITICAL_O2_THRESHOLD, 2) + ",\"tankId\":\"TANK_A\",\"nodeId\":"
58
+ + mesh.getNodeId() + ",\"timestamp\":" + mesh.getNodeTime() + "}";
59
+
60
+ if (offlineMode || !mesh.hasInternetConnection()) {
61
+ // CRITICAL: Queue for guaranteed delivery
62
+ uint32_t msgId = mesh.queueMessage(
63
+ payload,
64
+ "mqtt://cloud.farm.com/alarms/critical",
65
+ PRIORITY_CRITICAL
66
+ );
67
+
68
+ if (msgId) {
69
+ Serial.printf("🚨 CRITICAL O2 ALARM: %.2f mg/L - QUEUED #%u\n", o2Level, msgId);
70
+ } else {
71
+ Serial.printf("🚨 CRITICAL O2 ALARM: %.2f mg/L - QUEUE FAILED!\n", o2Level);
72
+ }
73
+ } else {
74
+ // Send immediately via bridge (in real app, use MQTT client)
75
+ Serial.printf("🚨 CRITICAL O2 ALARM: %.2f mg/L - SENT IMMEDIATELY\n", o2Level);
76
+ // mqttClient.publish("alarms/critical", payload.c_str());
77
+ }
78
+ }
79
+
80
+ // Send warning alarm
81
+ void sendWarningAlarm(float o2Level) {
82
+ String payload = String("{\"type\":\"WARNING\",\"sensor\":\"O2\",\"value\":")
83
+ + String(o2Level, 2) + ",\"threshold\":"
84
+ + String(WARNING_O2_THRESHOLD, 2) + ",\"nodeId\":"
85
+ + mesh.getNodeId() + "}";
86
+
87
+ if (offlineMode) {
88
+ uint32_t msgId = mesh.queueMessage(
89
+ payload,
90
+ "mqtt://cloud.farm.com/alarms/warning",
91
+ PRIORITY_HIGH
92
+ );
93
+
94
+ if (msgId) {
95
+ Serial.printf("⚠️ WARNING O2: %.2f mg/L - QUEUED #%u\n", o2Level, msgId);
96
+ } else {
97
+ Serial.printf("⚠️ WARNING O2: %.2f mg/L - QUEUE FULL\n", o2Level);
98
+ }
99
+ } else {
100
+ Serial.printf("⚠️ WARNING O2: %.2f mg/L - SENT\n", o2Level);
101
+ }
102
+ }
103
+
104
+ // Send normal telemetry
105
+ void sendNormalTelemetry(float o2Level) {
106
+ String payload = String("{\"sensor\":\"O2\",\"value\":") + String(o2Level, 2)
107
+ + ",\"nodeId\":" + mesh.getNodeId() + "}";
108
+
109
+ if (offlineMode) {
110
+ // Low priority - queue only if space available
111
+ uint32_t msgId = mesh.queueMessage(
112
+ payload,
113
+ "mqtt://cloud.farm.com/telemetry",
114
+ PRIORITY_LOW
115
+ );
116
+
117
+ if (msgId) {
118
+ Serial.printf("📊 Telemetry: %.2f mg/L - queued #%u\n", o2Level, msgId);
119
+ } else {
120
+ Serial.printf("📊 Telemetry: %.2f mg/L - dropped (queue full)\n", o2Level);
121
+ }
122
+ } else {
123
+ Serial.printf("📊 Telemetry: %.2f mg/L\n", o2Level);
124
+ }
125
+ }
126
+
127
+ // Check O2 sensor and send appropriate message
128
+ void checkO2Sensor() {
129
+ float o2Level = readDissolvedOxygenSensor();
130
+
131
+ if (o2Level < CRITICAL_O2_THRESHOLD) {
132
+ sendCriticalAlarm(o2Level);
133
+ } else if (o2Level < WARNING_O2_THRESHOLD) {
134
+ sendWarningAlarm(o2Level);
135
+ } else {
136
+ sendNormalTelemetry(o2Level);
137
+ }
138
+ }
139
+
140
+ // Bridge status callback - Internet connectivity changed
141
+ void bridgeStatusCallback(uint32_t bridgeNodeId, bool hasInternet) {
142
+ if (!hasInternet) {
143
+ offlineMode = true;
144
+ Serial.println("\n⚠️ OFFLINE MODE ACTIVATED");
145
+ Serial.printf(" Bridge node %u lost Internet\n", bridgeNodeId);
146
+ Serial.printf(" Queue size: %u messages\n", mesh.getQueuedMessageCount());
147
+
148
+ uint32_t critical = mesh.getQueuedMessageCount(PRIORITY_CRITICAL);
149
+ if (critical > 0) {
150
+ Serial.printf(" ⚠️ %u CRITICAL messages queued!\n", critical);
151
+ }
152
+ } else {
153
+ offlineMode = false;
154
+ Serial.println("\n✅ ONLINE MODE - Internet restored");
155
+ Serial.printf(" Bridge node %u has Internet\n", bridgeNodeId);
156
+
157
+ // Flush queued messages
158
+ uint32_t queuedCount = mesh.getQueuedMessageCount();
159
+ if (queuedCount > 0) {
160
+ Serial.printf(" Flushing %u queued messages...\n", queuedCount);
161
+
162
+ auto messages = mesh.flushMessageQueue();
163
+ for (auto& msg : messages) {
164
+ // In real application, send via MQTT or HTTP
165
+ Serial.printf(" Sending queued message #%u (priority=%d, attempts=%u)\n",
166
+ msg.id, msg.priority, msg.attempts);
167
+
168
+ // Simulate sending (in real app, check if send succeeded)
169
+ bool sent = true; // Replace with: mqttClient.publish(...)
170
+
171
+ if (sent) {
172
+ mesh.removeQueuedMessage(msg.id);
173
+ } else {
174
+ // Increment attempt counter
175
+ mesh.incrementQueuedMessageAttempts(msg.id);
176
+
177
+ // Remove if too many attempts
178
+ if (msg.attempts >= 3) {
179
+ Serial.printf(" ❌ Message #%u failed after 3 attempts, removing\n", msg.id);
180
+ mesh.removeQueuedMessage(msg.id);
181
+ }
182
+ }
183
+ }
184
+
185
+ Serial.printf(" ✅ Queue flushed (%u messages sent)\n", queuedCount);
186
+ }
187
+ }
188
+ }
189
+
190
+ // Queue state callback - Monitor queue health
191
+ void queueStateCallback(QueueState state, uint32_t messageCount) {
192
+ switch (state) {
193
+ case QUEUE_EMPTY:
194
+ Serial.println("ℹ️ Queue empty");
195
+ break;
196
+ case QUEUE_NORMAL:
197
+ Serial.printf("ℹ️ Queue normal (%u messages)\n", messageCount);
198
+ break;
199
+ case QUEUE_75_PERCENT:
200
+ Serial.printf("⚠️ Queue 75%% full (%u messages)\n", messageCount);
201
+ break;
202
+ case QUEUE_FULL:
203
+ Serial.printf("🚨 Queue FULL (%u messages) - dropping LOW priority\n", messageCount);
204
+ break;
205
+ }
206
+ }
207
+
208
+ void setup() {
209
+ Serial.begin(115200);
210
+ Serial.println("\n\n=== Fish Farm O2 Monitoring with Message Queue ===\n");
211
+
212
+ // Initialize mesh
213
+ mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
214
+ mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
215
+
216
+ // For bridge node: set router credentials
217
+ // Uncomment if this is the bridge node
218
+ // mesh.stationManual(ROUTER_SSID, ROUTER_PASSWORD);
219
+ // mesh.setHostname("FishFarmBridge");
220
+
221
+ // Enable message queue
222
+ mesh.enableMessageQueue(true, MAX_QUEUE_SIZE);
223
+ Serial.printf("Message queue enabled (capacity: %u)\n", MAX_QUEUE_SIZE);
224
+
225
+ // Set callbacks
226
+ mesh.onBridgeStatusChanged(&bridgeStatusCallback);
227
+ mesh.onQueueStateChanged(&queueStateCallback);
228
+
229
+ Serial.println("Setup complete. Monitoring O2 levels...\n");
230
+
231
+ // Initialize random for sensor simulation
232
+ randomSeed(analogRead(0));
233
+ }
234
+
235
+ void loop() {
236
+ mesh.update();
237
+
238
+ // Check O2 sensor every 10 seconds
239
+ if (millis() - lastO2Check > 10000) {
240
+ lastO2Check = millis();
241
+ checkO2Sensor();
242
+
243
+ // Print queue status
244
+ uint32_t queueSize = mesh.getQueuedMessageCount();
245
+ if (queueSize > 0) {
246
+ Serial.printf(" [Queue: %u messages", queueSize);
247
+
248
+ uint32_t critical = mesh.getQueuedMessageCount(PRIORITY_CRITICAL);
249
+ if (critical > 0) {
250
+ Serial.printf(" (%u CRITICAL)", critical);
251
+ }
252
+
253
+ Serial.println("]");
254
+ }
255
+ }
256
+
257
+ // Prune old messages every hour
258
+ if (millis() - lastQueuePrune > 3600000) {
259
+ lastQueuePrune = millis();
260
+ uint32_t pruned = mesh.pruneQueue(QUEUE_PRUNE_AGE);
261
+ if (pruned > 0) {
262
+ Serial.printf("ℹ️ Pruned %u old messages from queue\n", pruned);
263
+ }
264
+ }
265
+ }
package/library.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/Alteriom/painlessMesh"
8
8
  },
9
- "version": "1.8.0",
9
+ "version": "1.8.1",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=AlteriomPainlessMesh
2
- version=1.8.0
2
+ version=1.8.1
3
3
  author=Coopdis,Scotty Franzyshen,Edwin van Leeuwen,Germán Martín,Maximilian Schwarz,Doanh Doanh,Alteriom
4
4
  maintainer=Alteriom
5
5
  sentence=A painless way to setup a mesh with ESP8266 and ESP32 devices with Alteriom extensions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alteriom/painlessmesh",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
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",
@@ -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
- DynamicJsonDocument doc(256);
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
- DynamicJsonDocument doc(256);
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
- DynamicJsonDocument doc(256);
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