@alteriom/painlessmesh 1.7.9 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +118 -2
  2. package/README.md +159 -12
  3. package/docs/BRIDGE_FAILOVER.md +512 -0
  4. package/docs/BRIDGE_HEALTH_MONITORING.md +293 -0
  5. package/docs/CREATE_MISSING_RELEASES.md +321 -0
  6. package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +523 -0
  7. package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +542 -0
  8. package/examples/alteriom/alteriom_sensor_package.hpp +213 -0
  9. package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +1014 -11
  10. package/examples/basic/basic.ino +6 -2
  11. package/examples/bridge/bridge.ino +44 -23
  12. package/examples/bridge/bridge_health_monitoring_example.ino +188 -0
  13. package/examples/bridgeAwareSensorNode/alteriom_sensor_package.hpp +1227 -0
  14. package/examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino +343 -0
  15. package/examples/bridgeAwareSensorNode/platformio.ini +26 -0
  16. package/examples/bridge_failover/README.md +358 -0
  17. package/examples/bridge_failover/bridge_failover.ino +180 -0
  18. package/examples/bridge_failover/platformio.ini +27 -0
  19. package/examples/diagnosticsExample/diagnosticsExample.ino +171 -0
  20. package/examples/diagnosticsExample/platformio.ini +26 -0
  21. package/examples/multi_bridge/README.md +346 -0
  22. package/examples/multi_bridge/primary_bridge.ino +96 -0
  23. package/examples/multi_bridge/regular_node.ino +141 -0
  24. package/examples/multi_bridge/secondary_bridge.ino +111 -0
  25. package/examples/ntpTimeSyncBridge/alteriom_sensor_package.hpp +1383 -0
  26. package/examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino +81 -0
  27. package/examples/ntpTimeSyncNode/alteriom_sensor_package.hpp +1383 -0
  28. package/examples/ntpTimeSyncNode/ntpTimeSyncNode.ino +109 -0
  29. package/examples/queued_alarms/README.md +390 -0
  30. package/examples/queued_alarms/queued_alarms.ino +265 -0
  31. package/examples/rtcIntegration/README.md +235 -0
  32. package/examples/rtcIntegration/rtcIntegration.ino +196 -0
  33. package/library.json +1 -1
  34. package/library.properties +1 -1
  35. package/package.json +1 -1
  36. package/src/arduino/wifi.hpp +888 -0
  37. package/src/painlessMeshSTA.cpp +63 -0
  38. package/src/painlessMeshSTA.h +3 -0
  39. package/src/painlessmesh/mesh.hpp +1327 -4
  40. package/src/painlessmesh/message_queue.hpp +368 -0
  41. package/src/painlessmesh/plugin.hpp +69 -0
  42. package/src/painlessmesh/rtc.hpp +203 -0
@@ -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:
@@ -81,6 +88,72 @@ class Mesh : public painlessmesh::Mesh<Connection> {
81
88
 
82
89
  this->init(nodeId);
83
90
 
91
+ // Add bridge election package handler (Type 611)
92
+ this->callbackList.onPackage(
93
+ 611, // BRIDGE_ELECTION type
94
+ [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
95
+ JsonDocument doc;
96
+ TSTRING str;
97
+ variant.printTo(str);
98
+ deserializeJson(doc, str);
99
+ JsonObject obj = doc.as<JsonObject>();
100
+
101
+ if (obj["routerRSSI"].is<int>()) {
102
+ uint32_t fromNode = obj["from"];
103
+ int8_t routerRSSI = obj["routerRSSI"];
104
+ uint32_t uptime = obj["uptime"] | 0;
105
+ uint32_t freeMemory = obj["freeMemory"] | 0;
106
+
107
+ this->handleBridgeElection(fromNode, routerRSSI, uptime, freeMemory);
108
+
109
+ Log(CONNECTION, "Bridge election candidate from %u: RSSI %d dBm\n",
110
+ fromNode, routerRSSI);
111
+ }
112
+ return false; // Don't consume the package
113
+ });
114
+
115
+ // Add bridge takeover package handler (Type 612)
116
+ this->callbackList.onPackage(
117
+ 612, // BRIDGE_TAKEOVER type
118
+ [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
119
+ JsonDocument doc;
120
+ TSTRING str;
121
+ variant.printTo(str);
122
+ deserializeJson(doc, str);
123
+ JsonObject obj = doc.as<JsonObject>();
124
+
125
+ if (obj["previousBridge"].is<unsigned int>()) {
126
+ uint32_t newBridge = obj["from"];
127
+ uint32_t previousBridge = obj["previousBridge"];
128
+ TSTRING reason = obj["reason"].as<TSTRING>();
129
+
130
+ Log(CONNECTION, "Bridge takeover: Node %u replaced %u (%s)\n",
131
+ newBridge, previousBridge, reason.c_str());
132
+
133
+ // Notify callback if this node was not the winner
134
+ if (newBridge != this->nodeId && bridgeRoleChangedCallback) {
135
+ bridgeRoleChangedCallback(false, "Another node won election");
136
+ }
137
+ }
138
+ return false; // Don't consume the package
139
+ });
140
+
141
+ // Add callback to detect bridge failures and trigger elections
142
+ this->onBridgeStatusChanged([this](uint32_t bridgeNodeId, bool hasInternet) {
143
+ if (!hasInternet && bridgeFailoverEnabled && routerCredentialsConfigured) {
144
+ Log(CONNECTION, "Bridge %u lost Internet, considering election...\n", bridgeNodeId);
145
+
146
+ // Check if we still have any healthy bridges
147
+ if (!this->hasInternetConnection()) {
148
+ Log(CONNECTION, "No healthy bridges, starting election\n");
149
+ // Small delay to let all nodes detect the failure
150
+ this->addTask(2000, TASK_ONCE, [this]() {
151
+ this->startBridgeElection();
152
+ });
153
+ }
154
+ }
155
+ });
156
+
84
157
  tcpServerInit();
85
158
  eventHandleInit();
86
159
 
@@ -119,6 +192,145 @@ class Mesh : public painlessmesh::Mesh<Connection> {
119
192
  init(ssid, password, port, connectMode, channel, hidden, maxconn);
120
193
  }
121
194
 
195
+ /**
196
+ * Initialize mesh as a bridge node with automatic channel detection
197
+ *
198
+ * This method connects to a router first, detects its channel, then
199
+ * initializes the mesh on the same channel. This ensures the bridge
200
+ * can maintain both router and mesh connections on the same channel.
201
+ *
202
+ * The bridge node will automatically:
203
+ * - Connect to the specified router in STA mode
204
+ * - Detect the router's WiFi channel
205
+ * - Initialize the mesh AP on the detected channel
206
+ * - Set itself as root node
207
+ * - Maintain the router connection
208
+ *
209
+ * @param meshSSID The name of your mesh network
210
+ * @param meshPassword WiFi password for the mesh
211
+ * @param routerSSID SSID of the router to connect to
212
+ * @param routerPassword Password for the router
213
+ * @param baseScheduler Task scheduler for mesh operations
214
+ * @param port TCP port for mesh communication (default: 5555)
215
+ */
216
+ void initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
217
+ TSTRING routerSSID, TSTRING routerPassword,
218
+ Scheduler *baseScheduler, uint16_t port = 5555) {
219
+ using namespace logger;
220
+
221
+ Log(STARTUP, "=== Bridge Mode Initialization ===\n");
222
+ Log(STARTUP, "Step 1: Connecting to router %s...\n", routerSSID.c_str());
223
+
224
+ // Step 1: Connect to router first to detect its channel
225
+ // Shut Wifi down and start with a blank slate
226
+ if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
227
+
228
+ Log(STARTUP, "initAsBridge(): %d\n",
229
+ #if ESP_ARDUINO_VERSION_MAJOR >= 3
230
+ WiFi.setAutoReconnect(false));
231
+ #else
232
+ WiFi.setAutoConnect(false));
233
+ #endif
234
+ WiFi.persistent(false);
235
+ WiFi.mode(WIFI_STA);
236
+
237
+ // Connect to router and wait for connection
238
+ WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
239
+
240
+ // Wait for connection (with timeout)
241
+ int timeout = 30; // 30 seconds timeout
242
+ while (WiFi.status() != WL_CONNECTED && timeout > 0) {
243
+ delay(1000);
244
+ timeout--;
245
+ Log(STARTUP, ".");
246
+ }
247
+
248
+ uint8_t detectedChannel = 1; // Default fallback
249
+
250
+ if (WiFi.status() == WL_CONNECTED) {
251
+ detectedChannel = WiFi.channel();
252
+ // Validate channel is in valid range (1-13 for 2.4GHz)
253
+ if (detectedChannel < 1 || detectedChannel > 13) {
254
+ Log(ERROR, "\n✗ Invalid channel detected: %d, using default channel 1\n", detectedChannel);
255
+ detectedChannel = 1;
256
+ } else {
257
+ Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
258
+ Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
259
+ }
260
+ } else {
261
+ Log(ERROR, "\n✗ Failed to connect to router, using default channel 1\n");
262
+ }
263
+
264
+ Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n", detectedChannel);
265
+
266
+ // Step 2: Initialize mesh on detected channel
267
+ init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
268
+ detectedChannel, 0, MAX_CONN);
269
+
270
+ Log(STARTUP, "Step 3: Establishing bridge connection...\n");
271
+
272
+ // Step 3: Re-establish router connection using stationManual
273
+ stationManual(routerSSID, routerPassword, 0);
274
+
275
+ // Step 4: Configure as root/bridge node
276
+ this->setRoot(true);
277
+ this->setContainsRoot(true);
278
+
279
+ // Step 5: Setup bridge status broadcasting
280
+ initBridgeStatusBroadcast();
281
+
282
+ Log(STARTUP, "=== Bridge Mode Active ===\n");
283
+ Log(STARTUP, " Mesh SSID: %s\n", meshSSID.c_str());
284
+ Log(STARTUP, " Mesh Channel: %d (matches router)\n", detectedChannel);
285
+ Log(STARTUP, " Router: %s\n", routerSSID.c_str());
286
+ Log(STARTUP, " Port: %d\n", port);
287
+ }
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
+
122
334
  /**
123
335
  * Connect (as a station) to a specified network and ip
124
336
  *
@@ -212,6 +424,194 @@ class Mesh : public painlessmesh::Mesh<Connection> {
212
424
  IPAddress getStationIP() { return WiFi.localIP(); }
213
425
  IPAddress getAPIP() { return _apIp; }
214
426
 
427
+ /**
428
+ * Enable or disable automatic bridge failover
429
+ *
430
+ * When enabled, nodes will participate in bridge elections if the primary
431
+ * bridge goes offline and they have router credentials configured.
432
+ *
433
+ * @param enabled true to enable automatic failover (default), false to disable
434
+ */
435
+ void enableBridgeFailover(bool enabled) {
436
+ bridgeFailoverEnabled = enabled;
437
+ }
438
+
439
+ /**
440
+ * Set router credentials for bridge election participation
441
+ *
442
+ * Nodes must have router credentials configured to participate in bridge
443
+ * elections. When a bridge fails, only nodes with credentials can become
444
+ * the new bridge.
445
+ *
446
+ * @param ssid Router SSID
447
+ * @param password Router password
448
+ */
449
+ void setRouterCredentials(TSTRING ssid, TSTRING password) {
450
+ routerSSID = ssid;
451
+ routerPassword = password;
452
+ routerCredentialsConfigured = true;
453
+ }
454
+
455
+ /**
456
+ * Set the election timeout (how long to collect candidates)
457
+ *
458
+ * @param timeoutMs Timeout in milliseconds (default: 5000 = 5 seconds)
459
+ */
460
+ void setElectionTimeout(uint32_t timeoutMs) {
461
+ electionTimeoutMs = timeoutMs;
462
+ }
463
+
464
+ /**
465
+ * Set callback for when this node's bridge role changes
466
+ *
467
+ * @param callback Function to call when role changes
468
+ */
469
+ void onBridgeRoleChanged(std::function<void(bool isBridge, TSTRING reason)> callback) {
470
+ bridgeRoleChangedCallback = callback;
471
+ }
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
+
215
615
  void stop() {
216
616
  // remove all WiFi events
217
617
  #ifdef ESP32
@@ -265,6 +665,455 @@ class Mesh : public painlessmesh::Mesh<Connection> {
265
665
  WiFi.softAP(_meshSSID.c_str(), _meshPassword.c_str(), _meshChannel,
266
666
  _meshHidden, _meshMaxConn);
267
667
  }
668
+
669
+ /**
670
+ * Initialize bridge status broadcasting
671
+ * Sets up a periodic task to broadcast bridge status to the mesh
672
+ */
673
+ void initBridgeStatusBroadcast() {
674
+ using namespace logger;
675
+
676
+ if (!this->isBridge() || !this->bridgeStatusBroadcastEnabled) {
677
+ return;
678
+ }
679
+
680
+ Log(STARTUP, "initBridgeStatusBroadcast(): Setting up bridge status broadcast\n");
681
+
682
+ // Create periodic task to broadcast bridge status
683
+ bridgeStatusTask = this->addTask(
684
+ this->bridgeStatusIntervalMs,
685
+ TASK_FOREVER,
686
+ [this]() {
687
+ this->sendBridgeStatus();
688
+ }
689
+ );
690
+
691
+ Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
692
+ this->bridgeStatusIntervalMs);
693
+ }
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
+
804
+ /**
805
+ * Scan for router and return its signal strength
806
+ *
807
+ * @param routerSSID SSID of router to scan for
808
+ * @return RSSI in dBm (negative number, -127 to 0), or 0 if not found
809
+ */
810
+ int8_t scanRouterSignalStrength(TSTRING routerSSID) {
811
+ using namespace logger;
812
+ Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n", routerSSID.c_str());
813
+
814
+ int n = WiFi.scanNetworks(false, false);
815
+ Log(CONNECTION, "scanRouterSignalStrength(): Found %d networks\n", n);
816
+
817
+ for (int i = 0; i < n; i++) {
818
+ if (WiFi.SSID(i) == routerSSID) {
819
+ int8_t rssi = WiFi.RSSI(i);
820
+ Log(CONNECTION, "scanRouterSignalStrength(): Found %s with RSSI %d dBm\n",
821
+ routerSSID.c_str(), rssi);
822
+ return rssi;
823
+ }
824
+ }
825
+
826
+ Log(CONNECTION, "scanRouterSignalStrength(): Router %s not found\n", routerSSID.c_str());
827
+ return 0; // Router not found
828
+ }
829
+
830
+ /**
831
+ * Start bridge election process
832
+ * Called when primary bridge failure is detected
833
+ */
834
+ void startBridgeElection() {
835
+ using namespace logger;
836
+
837
+ if (!bridgeFailoverEnabled) {
838
+ Log(CONNECTION, "startBridgeElection(): Failover disabled\n");
839
+ return;
840
+ }
841
+
842
+ if (!routerCredentialsConfigured) {
843
+ Log(CONNECTION, "startBridgeElection(): No router credentials, cannot participate\n");
844
+ return;
845
+ }
846
+
847
+ if (electionState != ELECTION_IDLE) {
848
+ Log(CONNECTION, "startBridgeElection(): Election already in progress\n");
849
+ return;
850
+ }
851
+
852
+ // Prevent rapid role changes
853
+ if (millis() - lastRoleChangeTime < 60000) {
854
+ Log(CONNECTION, "startBridgeElection(): Too soon after last role change\n");
855
+ return;
856
+ }
857
+
858
+ Log(CONNECTION, "=== Bridge Election Started ===\n");
859
+ electionState = ELECTION_SCANNING;
860
+
861
+ // Scan for router to get RSSI
862
+ int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
863
+
864
+ if (routerRSSI == 0) {
865
+ Log(CONNECTION, "startBridgeElection(): Router not visible, cannot participate\n");
866
+ electionState = ELECTION_IDLE;
867
+ return;
868
+ }
869
+
870
+ Log(CONNECTION, "startBridgeElection(): My router RSSI: %d dBm\n", routerRSSI);
871
+
872
+ // Clear previous candidates
873
+ electionCandidates.clear();
874
+
875
+ // Add self as candidate
876
+ BridgeCandidate selfCandidate;
877
+ selfCandidate.nodeId = this->nodeId;
878
+ selfCandidate.routerRSSI = routerRSSI;
879
+ selfCandidate.uptime = millis();
880
+ selfCandidate.freeMemory = ESP.getFreeHeap();
881
+ electionCandidates.push_back(selfCandidate);
882
+
883
+ // Broadcast candidacy using JSON directly (avoiding dependency on alteriom package)
884
+ JsonDocument doc;
885
+ JsonObject obj = doc.to<JsonObject>();
886
+ obj["type"] = 611; // BRIDGE_ELECTION
887
+ obj["from"] = this->nodeId;
888
+ obj["routing"] = 2; // BROADCAST
889
+ obj["routerRSSI"] = routerRSSI;
890
+ obj["uptime"] = millis();
891
+ obj["freeMemory"] = ESP.getFreeHeap();
892
+ obj["timestamp"] = this->getNodeTime();
893
+ obj["routerSSID"] = routerSSID;
894
+ obj["message_type"] = 611;
895
+
896
+ String msg;
897
+ serializeJson(doc, msg);
898
+ this->sendBroadcast(msg);
899
+
900
+ Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
901
+
902
+ // Set election timeout
903
+ electionDeadline = millis() + electionTimeoutMs;
904
+ electionState = ELECTION_COLLECTING;
905
+
906
+ // Schedule election evaluation
907
+ this->addTask(electionTimeoutMs + 100, TASK_ONCE, [this]() {
908
+ this->evaluateElection();
909
+ });
910
+ }
911
+
912
+ /**
913
+ * Evaluate election and determine winner
914
+ * Called after election timeout expires
915
+ */
916
+ void evaluateElection() {
917
+ using namespace logger;
918
+
919
+ if (electionState != ELECTION_COLLECTING) {
920
+ Log(CONNECTION, "evaluateElection(): Not in collecting state\n");
921
+ return;
922
+ }
923
+
924
+ Log(CONNECTION, "=== Evaluating Election ===\n");
925
+ Log(CONNECTION, "evaluateElection(): %d candidates\n", electionCandidates.size());
926
+
927
+ // Find best candidate
928
+ BridgeCandidate* winner = nullptr;
929
+ int8_t bestRSSI = -127; // Worst possible RSSI
930
+
931
+ for (auto& candidate : electionCandidates) {
932
+ Log(CONNECTION, "evaluateElection(): Candidate %u: RSSI=%d, uptime=%u, mem=%u\n",
933
+ candidate.nodeId, candidate.routerRSSI, candidate.uptime, candidate.freeMemory);
934
+
935
+ if (candidate.routerRSSI > bestRSSI) {
936
+ bestRSSI = candidate.routerRSSI;
937
+ winner = &candidate;
938
+ } else if (candidate.routerRSSI == bestRSSI && winner != nullptr) {
939
+ // Tiebreaker 1: Higher uptime
940
+ if (candidate.uptime > winner->uptime) {
941
+ winner = &candidate;
942
+ } else if (candidate.uptime == winner->uptime) {
943
+ // Tiebreaker 2: More free memory
944
+ if (candidate.freeMemory > winner->freeMemory) {
945
+ winner = &candidate;
946
+ } else if (candidate.freeMemory == winner->freeMemory) {
947
+ // Tiebreaker 3: Lower node ID (deterministic)
948
+ if (candidate.nodeId < winner->nodeId) {
949
+ winner = &candidate;
950
+ }
951
+ }
952
+ }
953
+ }
954
+ }
955
+
956
+ if (winner == nullptr) {
957
+ Log(ERROR, "evaluateElection(): No winner found!\n");
958
+ electionState = ELECTION_IDLE;
959
+ return;
960
+ }
961
+
962
+ Log(CONNECTION, "=== Election Winner: Node %u ===\n", winner->nodeId);
963
+ Log(CONNECTION, " Router RSSI: %d dBm\n", winner->routerRSSI);
964
+ Log(CONNECTION, " Uptime: %u ms\n", winner->uptime);
965
+ Log(CONNECTION, " Free Memory: %u bytes\n", winner->freeMemory);
966
+
967
+ // Record election in diagnostics history
968
+ if (this->diagnosticsEnabled) {
969
+ ElectionRecord record;
970
+ record.timestamp = millis();
971
+ record.winnerNodeId = winner->nodeId;
972
+ record.winnerRSSI = winner->routerRSSI;
973
+ record.candidateCount = electionCandidates.size();
974
+ record.reason = "Bridge failure detected";
975
+
976
+ this->electionHistory.push_back(record);
977
+
978
+ // Keep history limited to MAX_ELECTION_HISTORY
979
+ if (this->electionHistory.size() > this->MAX_ELECTION_HISTORY) {
980
+ this->electionHistory.erase(this->electionHistory.begin());
981
+ }
982
+
983
+ Log(CONNECTION, "evaluateElection(): Election recorded in history\n");
984
+ }
985
+
986
+ if (winner->nodeId == this->nodeId) {
987
+ Log(CONNECTION, "🎯 I WON! Promoting to bridge...\n");
988
+ promoteToBridge();
989
+ } else {
990
+ Log(CONNECTION, "Winner is node %u, remaining as regular node\n", winner->nodeId);
991
+ }
992
+
993
+ electionState = ELECTION_IDLE;
994
+ electionCandidates.clear();
995
+ }
996
+
997
+ /**
998
+ * Promote this node to bridge role
999
+ * Called when node wins election
1000
+ */
1001
+ void promoteToBridge() {
1002
+ using namespace logger;
1003
+
1004
+ Log(STARTUP, "=== Becoming Bridge Node ===\n");
1005
+
1006
+ // Store previous bridge (if any)
1007
+ auto primaryBridge = this->getPrimaryBridge();
1008
+ uint32_t previousBridgeId = primaryBridge ? primaryBridge->nodeId : 0;
1009
+
1010
+ // Reconfigure as bridge
1011
+ this->stop();
1012
+ delay(1000);
1013
+
1014
+ this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
1015
+ mScheduler, _meshPort);
1016
+
1017
+ lastRoleChangeTime = millis();
1018
+
1019
+ Log(STARTUP, "✓ Bridge promotion complete\n");
1020
+
1021
+ // Notify via callback
1022
+ if (bridgeRoleChangedCallback) {
1023
+ bridgeRoleChangedCallback(true, "Election winner - best router signal");
1024
+ }
1025
+
1026
+ // Broadcast takeover announcement
1027
+ JsonDocument doc;
1028
+ JsonObject obj = doc.to<JsonObject>();
1029
+ obj["type"] = 612; // BRIDGE_TAKEOVER
1030
+ obj["from"] = this->nodeId;
1031
+ obj["routing"] = 2; // BROADCAST
1032
+ obj["previousBridge"] = previousBridgeId;
1033
+ obj["reason"] = "Election winner - best router signal";
1034
+ obj["routerRSSI"] = WiFi.RSSI();
1035
+ obj["timestamp"] = this->getNodeTime();
1036
+ obj["message_type"] = 612;
1037
+
1038
+ String msg;
1039
+ serializeJson(doc, msg);
1040
+
1041
+ // Small delay to ensure mesh is ready
1042
+ delay(2000);
1043
+ this->sendBroadcast(msg);
1044
+
1045
+ Log(STARTUP, "✓ Takeover announcement sent\n");
1046
+ }
1047
+
1048
+ /**
1049
+ * Handle received bridge election package
1050
+ * Called by package handler when election message arrives
1051
+ */
1052
+ void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI, uint32_t uptime,
1053
+ uint32_t freeMemory) {
1054
+ using namespace logger;
1055
+
1056
+ if (electionState != ELECTION_COLLECTING) {
1057
+ Log(CONNECTION, "handleBridgeElection(): Not collecting candidates, ignoring\n");
1058
+ return;
1059
+ }
1060
+
1061
+ // Check if candidate already exists
1062
+ for (auto& candidate : electionCandidates) {
1063
+ if (candidate.nodeId == fromNode) {
1064
+ Log(CONNECTION, "handleBridgeElection(): Duplicate candidate from %u, ignoring\n", fromNode);
1065
+ return;
1066
+ }
1067
+ }
1068
+
1069
+ BridgeCandidate candidate;
1070
+ candidate.nodeId = fromNode;
1071
+ candidate.routerRSSI = routerRSSI;
1072
+ candidate.uptime = uptime;
1073
+ candidate.freeMemory = freeMemory;
1074
+
1075
+ electionCandidates.push_back(candidate);
1076
+
1077
+ Log(CONNECTION, "handleBridgeElection(): Added candidate %u (RSSI: %d dBm)\n",
1078
+ fromNode, routerRSSI);
1079
+ }
1080
+
1081
+ /**
1082
+ * Send bridge status broadcast
1083
+ * Called periodically by bridge nodes to report connectivity status
1084
+ */
1085
+ void sendBridgeStatus() {
1086
+ using namespace logger;
1087
+
1088
+ if (!this->bridgeStatusBroadcastEnabled) {
1089
+ return;
1090
+ }
1091
+
1092
+ // Create bridge status package
1093
+ // We need to include the package header here since we're in wifi namespace
1094
+ // The package will be sent as a JSON string
1095
+ JsonDocument doc;
1096
+ JsonObject obj = doc.to<JsonObject>();
1097
+
1098
+ obj["type"] = 610; // BRIDGE_STATUS type
1099
+ obj["from"] = this->nodeId;
1100
+ obj["routing"] = 2; // BROADCAST routing
1101
+ obj["timestamp"] = this->getNodeTime();
1102
+ obj["internetConnected"] = (WiFi.status() == WL_CONNECTED);
1103
+ obj["routerRSSI"] = WiFi.RSSI();
1104
+ obj["routerChannel"] = WiFi.channel();
1105
+ obj["uptime"] = millis();
1106
+ obj["gatewayIP"] = WiFi.gatewayIP().toString();
1107
+ obj["message_type"] = 610;
1108
+
1109
+ String msg;
1110
+ serializeJson(doc, msg);
1111
+
1112
+ Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)\n",
1113
+ (WiFi.status() == WL_CONNECTED) ? "Connected" : "Disconnected");
1114
+
1115
+ this->sendBroadcast(msg);
1116
+ }
268
1117
  void eventHandleInit() {
269
1118
  using namespace logger;
270
1119
  #ifdef ESP32
@@ -365,6 +1214,45 @@ class Mesh : public painlessmesh::Mesh<Connection> {
365
1214
  WiFiEventHandler eventSTAGotIPHandler;
366
1215
  #endif // ESP8266
367
1216
  AsyncServer *_tcpListener;
1217
+ std::shared_ptr<Task> bridgeStatusTask;
1218
+
1219
+ // Bridge failover state and configuration
1220
+ enum ElectionState {
1221
+ ELECTION_IDLE,
1222
+ ELECTION_SCANNING,
1223
+ ELECTION_COLLECTING
1224
+ };
1225
+
1226
+ struct BridgeCandidate {
1227
+ uint32_t nodeId;
1228
+ int8_t routerRSSI;
1229
+ uint32_t uptime;
1230
+ uint32_t freeMemory;
1231
+ };
1232
+
1233
+ bool bridgeFailoverEnabled = true;
1234
+ bool routerCredentialsConfigured = false;
1235
+ TSTRING routerSSID = "";
1236
+ TSTRING routerPassword = "";
1237
+ uint32_t electionTimeoutMs = 5000; // Default 5 seconds
1238
+ uint32_t lastRoleChangeTime = 0;
1239
+ ElectionState electionState = ELECTION_IDLE;
1240
+ uint32_t electionDeadline = 0;
1241
+ std::vector<BridgeCandidate> electionCandidates;
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
368
1256
  };
369
1257
  } // namespace wifi
370
1258
  }; // namespace painlessmesh