@alteriom/painlessmesh 1.7.8 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +139 -3
  2. package/README.md +114 -4
  3. package/RELEASE_GUIDE.md +57 -8
  4. package/docs/BRIDGE_FAILOVER.md +512 -0
  5. package/docs/BRIDGE_HEALTH_MONITORING.md +293 -0
  6. package/docs/CREATE_MISSING_RELEASES.md +321 -0
  7. package/docs/README.md +2 -1
  8. package/docs/RELEASE_AGENT_SUMMARY.md +386 -0
  9. package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +523 -0
  10. package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +542 -0
  11. package/docs/troubleshooting/ESP32_C6_COMPATIBILITY.md +157 -0
  12. package/docs/troubleshooting/common-issues.md +28 -0
  13. package/examples/alteriom/alteriom_sensor_package.hpp +233 -1
  14. package/examples/alteriom/platformio.ini +1 -1
  15. package/examples/alteriomImproved/platformio.ini +1 -1
  16. package/examples/alteriomMetricsHealth/metrics_health_node.ino +18 -7
  17. package/examples/alteriomMetricsHealth/platformio.ini +1 -1
  18. package/examples/alteriomPhase1/platformio.ini +1 -1
  19. package/examples/alteriomPhase2/platformio.ini +1 -1
  20. package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +1014 -11
  21. package/examples/alteriomSensorNode/platformio.ini +1 -1
  22. package/examples/basic/basic.ino +6 -2
  23. package/examples/basic/platformio.ini +1 -1
  24. package/examples/bridge/alteriom_sensor_package.hpp +1170 -0
  25. package/examples/bridge/bridge.ino +44 -23
  26. package/examples/bridge/bridge_health_monitoring_example.ino +188 -0
  27. package/examples/bridge/enhanced_mqtt_bridge.hpp +1 -1
  28. package/examples/bridge/mqtt_command_bridge.hpp +2 -2
  29. package/examples/bridge/platformio.ini +2 -1
  30. package/examples/bridgeAwareSensorNode/alteriom_sensor_package.hpp +1227 -0
  31. package/examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino +343 -0
  32. package/examples/bridgeAwareSensorNode/platformio.ini +26 -0
  33. package/examples/bridge_failover/README.md +358 -0
  34. package/examples/bridge_failover/bridge_failover.ino +180 -0
  35. package/examples/bridge_failover/platformio.ini +27 -0
  36. package/examples/diagnosticsExample/diagnosticsExample.ino +171 -0
  37. package/examples/diagnosticsExample/platformio.ini +26 -0
  38. package/examples/echoNode/platformio.ini +1 -1
  39. package/examples/logClient/platformio.ini +1 -1
  40. package/examples/logServer/platformio.ini +1 -1
  41. package/examples/mqttStatusBridge/platformio.ini +1 -1
  42. package/examples/namedMesh/platformio.ini +1 -1
  43. package/examples/ntpTimeSyncBridge/alteriom_sensor_package.hpp +1383 -0
  44. package/examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino +81 -0
  45. package/examples/ntpTimeSyncNode/alteriom_sensor_package.hpp +1383 -0
  46. package/examples/ntpTimeSyncNode/ntpTimeSyncNode.ino +109 -0
  47. package/examples/otaReceiver/platformio.ini +1 -1
  48. package/examples/rtcIntegration/README.md +235 -0
  49. package/examples/rtcIntegration/rtcIntegration.ino +196 -0
  50. package/examples/startHere/platformio.ini +1 -1
  51. package/examples/webServer/platformio.ini +1 -1
  52. package/library.json +93 -53
  53. package/library.properties +1 -1
  54. package/package.json +2 -2
  55. package/src/arduino/wifi.hpp +581 -0
  56. package/src/painlessMeshSTA.cpp +68 -0
  57. package/src/painlessMeshSTA.h +3 -0
  58. package/src/painlessmesh/mesh.hpp +1127 -4
  59. package/src/painlessmesh/rtc.hpp +203 -0
@@ -63,6 +63,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
63
63
  _meshSSID = ssid;
64
64
  _meshPassword = password;
65
65
  _meshChannel = channel;
66
+ Log(STARTUP, "init(): Mesh channel set to %d\n", _meshChannel);
66
67
  _meshHidden = hidden;
67
68
  _meshMaxConn = maxconn;
68
69
  _meshPort = port;
@@ -80,6 +81,72 @@ class Mesh : public painlessmesh::Mesh<Connection> {
80
81
 
81
82
  this->init(nodeId);
82
83
 
84
+ // Add bridge election package handler (Type 611)
85
+ this->callbackList.onPackage(
86
+ 611, // BRIDGE_ELECTION type
87
+ [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
88
+ JsonDocument doc;
89
+ TSTRING str;
90
+ variant.printTo(str);
91
+ deserializeJson(doc, str);
92
+ JsonObject obj = doc.as<JsonObject>();
93
+
94
+ if (obj["routerRSSI"].is<int>()) {
95
+ uint32_t fromNode = obj["from"];
96
+ int8_t routerRSSI = obj["routerRSSI"];
97
+ uint32_t uptime = obj["uptime"] | 0;
98
+ uint32_t freeMemory = obj["freeMemory"] | 0;
99
+
100
+ this->handleBridgeElection(fromNode, routerRSSI, uptime, freeMemory);
101
+
102
+ Log(CONNECTION, "Bridge election candidate from %u: RSSI %d dBm\n",
103
+ fromNode, routerRSSI);
104
+ }
105
+ return false; // Don't consume the package
106
+ });
107
+
108
+ // Add bridge takeover package handler (Type 612)
109
+ this->callbackList.onPackage(
110
+ 612, // BRIDGE_TAKEOVER type
111
+ [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
112
+ JsonDocument doc;
113
+ TSTRING str;
114
+ variant.printTo(str);
115
+ deserializeJson(doc, str);
116
+ JsonObject obj = doc.as<JsonObject>();
117
+
118
+ if (obj["previousBridge"].is<unsigned int>()) {
119
+ uint32_t newBridge = obj["from"];
120
+ uint32_t previousBridge = obj["previousBridge"];
121
+ TSTRING reason = obj["reason"].as<TSTRING>();
122
+
123
+ Log(CONNECTION, "Bridge takeover: Node %u replaced %u (%s)\n",
124
+ newBridge, previousBridge, reason.c_str());
125
+
126
+ // Notify callback if this node was not the winner
127
+ if (newBridge != this->nodeId && bridgeRoleChangedCallback) {
128
+ bridgeRoleChangedCallback(false, "Another node won election");
129
+ }
130
+ }
131
+ return false; // Don't consume the package
132
+ });
133
+
134
+ // Add callback to detect bridge failures and trigger elections
135
+ this->onBridgeStatusChanged([this](uint32_t bridgeNodeId, bool hasInternet) {
136
+ if (!hasInternet && bridgeFailoverEnabled && routerCredentialsConfigured) {
137
+ Log(CONNECTION, "Bridge %u lost Internet, considering election...\n", bridgeNodeId);
138
+
139
+ // Check if we still have any healthy bridges
140
+ if (!this->hasInternetConnection()) {
141
+ Log(CONNECTION, "No healthy bridges, starting election\n");
142
+ // Small delay to let all nodes detect the failure
143
+ this->addTask(2000, TASK_ONCE, [this]() {
144
+ this->startBridgeElection();
145
+ });
146
+ }
147
+ }
148
+ });
149
+
83
150
  tcpServerInit();
84
151
  eventHandleInit();
85
152
 
@@ -118,6 +185,100 @@ class Mesh : public painlessmesh::Mesh<Connection> {
118
185
  init(ssid, password, port, connectMode, channel, hidden, maxconn);
119
186
  }
120
187
 
188
+ /**
189
+ * Initialize mesh as a bridge node with automatic channel detection
190
+ *
191
+ * This method connects to a router first, detects its channel, then
192
+ * initializes the mesh on the same channel. This ensures the bridge
193
+ * can maintain both router and mesh connections on the same channel.
194
+ *
195
+ * The bridge node will automatically:
196
+ * - Connect to the specified router in STA mode
197
+ * - Detect the router's WiFi channel
198
+ * - Initialize the mesh AP on the detected channel
199
+ * - Set itself as root node
200
+ * - Maintain the router connection
201
+ *
202
+ * @param meshSSID The name of your mesh network
203
+ * @param meshPassword WiFi password for the mesh
204
+ * @param routerSSID SSID of the router to connect to
205
+ * @param routerPassword Password for the router
206
+ * @param baseScheduler Task scheduler for mesh operations
207
+ * @param port TCP port for mesh communication (default: 5555)
208
+ */
209
+ void initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
210
+ TSTRING routerSSID, TSTRING routerPassword,
211
+ Scheduler *baseScheduler, uint16_t port = 5555) {
212
+ using namespace logger;
213
+
214
+ Log(STARTUP, "=== Bridge Mode Initialization ===\n");
215
+ Log(STARTUP, "Step 1: Connecting to router %s...\n", routerSSID.c_str());
216
+
217
+ // Step 1: Connect to router first to detect its channel
218
+ // Shut Wifi down and start with a blank slate
219
+ if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
220
+
221
+ Log(STARTUP, "initAsBridge(): %d\n",
222
+ #if ESP_ARDUINO_VERSION_MAJOR >= 3
223
+ WiFi.setAutoReconnect(false));
224
+ #else
225
+ WiFi.setAutoConnect(false));
226
+ #endif
227
+ WiFi.persistent(false);
228
+ WiFi.mode(WIFI_STA);
229
+
230
+ // Connect to router and wait for connection
231
+ WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
232
+
233
+ // Wait for connection (with timeout)
234
+ int timeout = 30; // 30 seconds timeout
235
+ while (WiFi.status() != WL_CONNECTED && timeout > 0) {
236
+ delay(1000);
237
+ timeout--;
238
+ Log(STARTUP, ".");
239
+ }
240
+
241
+ uint8_t detectedChannel = 1; // Default fallback
242
+
243
+ if (WiFi.status() == WL_CONNECTED) {
244
+ detectedChannel = WiFi.channel();
245
+ // Validate channel is in valid range (1-13 for 2.4GHz)
246
+ if (detectedChannel < 1 || detectedChannel > 13) {
247
+ Log(ERROR, "\n✗ Invalid channel detected: %d, using default channel 1\n", detectedChannel);
248
+ detectedChannel = 1;
249
+ } else {
250
+ Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
251
+ Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
252
+ }
253
+ } else {
254
+ Log(ERROR, "\n✗ Failed to connect to router, using default channel 1\n");
255
+ }
256
+
257
+ Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n", detectedChannel);
258
+
259
+ // Step 2: Initialize mesh on detected channel
260
+ init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
261
+ detectedChannel, 0, MAX_CONN);
262
+
263
+ Log(STARTUP, "Step 3: Establishing bridge connection...\n");
264
+
265
+ // Step 3: Re-establish router connection using stationManual
266
+ stationManual(routerSSID, routerPassword, 0);
267
+
268
+ // Step 4: Configure as root/bridge node
269
+ this->setRoot(true);
270
+ this->setContainsRoot(true);
271
+
272
+ // Step 5: Setup bridge status broadcasting
273
+ initBridgeStatusBroadcast();
274
+
275
+ Log(STARTUP, "=== Bridge Mode Active ===\n");
276
+ Log(STARTUP, " Mesh SSID: %s\n", meshSSID.c_str());
277
+ Log(STARTUP, " Mesh Channel: %d (matches router)\n", detectedChannel);
278
+ Log(STARTUP, " Router: %s\n", routerSSID.c_str());
279
+ Log(STARTUP, " Port: %d\n", port);
280
+ }
281
+
121
282
  /**
122
283
  * Connect (as a station) to a specified network and ip
123
284
  *
@@ -128,13 +289,21 @@ class Mesh : public painlessmesh::Mesh<Connection> {
128
289
  */
129
290
  void stationManual(TSTRING ssid, TSTRING password, uint16_t port = 0,
130
291
  IPAddress remote_ip = IPAddress(0, 0, 0, 0)) {
292
+ using namespace logger;
131
293
  // Set station config
132
294
  stationScan.manualIP = remote_ip;
133
295
 
296
+ Log(STARTUP, "stationManual(): Connecting to %s\n", ssid.c_str());
297
+
134
298
  // Start scan
135
299
  stationScan.init(this, ssid, password, port, _meshChannel,
136
300
  static_cast<bool>(_meshHidden));
137
301
  stationScan.manual = true;
302
+
303
+ // Directly initiate connection - ESP will auto-detect router's channel
304
+ WiFi.begin(ssid.c_str(), password.c_str());
305
+
306
+ Log(STARTUP, "stationManual(): Connection initiated\n");
138
307
  }
139
308
 
140
309
  void initStation() {
@@ -203,6 +372,52 @@ class Mesh : public painlessmesh::Mesh<Connection> {
203
372
  IPAddress getStationIP() { return WiFi.localIP(); }
204
373
  IPAddress getAPIP() { return _apIp; }
205
374
 
375
+ /**
376
+ * Enable or disable automatic bridge failover
377
+ *
378
+ * When enabled, nodes will participate in bridge elections if the primary
379
+ * bridge goes offline and they have router credentials configured.
380
+ *
381
+ * @param enabled true to enable automatic failover (default), false to disable
382
+ */
383
+ void enableBridgeFailover(bool enabled) {
384
+ bridgeFailoverEnabled = enabled;
385
+ }
386
+
387
+ /**
388
+ * Set router credentials for bridge election participation
389
+ *
390
+ * Nodes must have router credentials configured to participate in bridge
391
+ * elections. When a bridge fails, only nodes with credentials can become
392
+ * the new bridge.
393
+ *
394
+ * @param ssid Router SSID
395
+ * @param password Router password
396
+ */
397
+ void setRouterCredentials(TSTRING ssid, TSTRING password) {
398
+ routerSSID = ssid;
399
+ routerPassword = password;
400
+ routerCredentialsConfigured = true;
401
+ }
402
+
403
+ /**
404
+ * Set the election timeout (how long to collect candidates)
405
+ *
406
+ * @param timeoutMs Timeout in milliseconds (default: 5000 = 5 seconds)
407
+ */
408
+ void setElectionTimeout(uint32_t timeoutMs) {
409
+ electionTimeoutMs = timeoutMs;
410
+ }
411
+
412
+ /**
413
+ * Set callback for when this node's bridge role changes
414
+ *
415
+ * @param callback Function to call when role changes
416
+ */
417
+ void onBridgeRoleChanged(std::function<void(bool isBridge, TSTRING reason)> callback) {
418
+ bridgeRoleChangedCallback = callback;
419
+ }
420
+
206
421
  void stop() {
207
422
  // remove all WiFi events
208
423
  #ifdef ESP32
@@ -256,6 +471,346 @@ class Mesh : public painlessmesh::Mesh<Connection> {
256
471
  WiFi.softAP(_meshSSID.c_str(), _meshPassword.c_str(), _meshChannel,
257
472
  _meshHidden, _meshMaxConn);
258
473
  }
474
+
475
+ /**
476
+ * Initialize bridge status broadcasting
477
+ * Sets up a periodic task to broadcast bridge status to the mesh
478
+ */
479
+ void initBridgeStatusBroadcast() {
480
+ using namespace logger;
481
+
482
+ if (!this->isBridge() || !this->bridgeStatusBroadcastEnabled) {
483
+ return;
484
+ }
485
+
486
+ Log(STARTUP, "initBridgeStatusBroadcast(): Setting up bridge status broadcast\n");
487
+
488
+ // Create periodic task to broadcast bridge status
489
+ bridgeStatusTask = this->addTask(
490
+ this->bridgeStatusIntervalMs,
491
+ TASK_FOREVER,
492
+ [this]() {
493
+ this->sendBridgeStatus();
494
+ }
495
+ );
496
+
497
+ Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
498
+ this->bridgeStatusIntervalMs);
499
+ }
500
+
501
+ /**
502
+ * Scan for router and return its signal strength
503
+ *
504
+ * @param routerSSID SSID of router to scan for
505
+ * @return RSSI in dBm (negative number, -127 to 0), or 0 if not found
506
+ */
507
+ int8_t scanRouterSignalStrength(TSTRING routerSSID) {
508
+ using namespace logger;
509
+ Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n", routerSSID.c_str());
510
+
511
+ int n = WiFi.scanNetworks(false, false);
512
+ Log(CONNECTION, "scanRouterSignalStrength(): Found %d networks\n", n);
513
+
514
+ for (int i = 0; i < n; i++) {
515
+ if (WiFi.SSID(i) == routerSSID) {
516
+ int8_t rssi = WiFi.RSSI(i);
517
+ Log(CONNECTION, "scanRouterSignalStrength(): Found %s with RSSI %d dBm\n",
518
+ routerSSID.c_str(), rssi);
519
+ return rssi;
520
+ }
521
+ }
522
+
523
+ Log(CONNECTION, "scanRouterSignalStrength(): Router %s not found\n", routerSSID.c_str());
524
+ return 0; // Router not found
525
+ }
526
+
527
+ /**
528
+ * Start bridge election process
529
+ * Called when primary bridge failure is detected
530
+ */
531
+ void startBridgeElection() {
532
+ using namespace logger;
533
+
534
+ if (!bridgeFailoverEnabled) {
535
+ Log(CONNECTION, "startBridgeElection(): Failover disabled\n");
536
+ return;
537
+ }
538
+
539
+ if (!routerCredentialsConfigured) {
540
+ Log(CONNECTION, "startBridgeElection(): No router credentials, cannot participate\n");
541
+ return;
542
+ }
543
+
544
+ if (electionState != ELECTION_IDLE) {
545
+ Log(CONNECTION, "startBridgeElection(): Election already in progress\n");
546
+ return;
547
+ }
548
+
549
+ // Prevent rapid role changes
550
+ if (millis() - lastRoleChangeTime < 60000) {
551
+ Log(CONNECTION, "startBridgeElection(): Too soon after last role change\n");
552
+ return;
553
+ }
554
+
555
+ Log(CONNECTION, "=== Bridge Election Started ===\n");
556
+ electionState = ELECTION_SCANNING;
557
+
558
+ // Scan for router to get RSSI
559
+ int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
560
+
561
+ if (routerRSSI == 0) {
562
+ Log(CONNECTION, "startBridgeElection(): Router not visible, cannot participate\n");
563
+ electionState = ELECTION_IDLE;
564
+ return;
565
+ }
566
+
567
+ Log(CONNECTION, "startBridgeElection(): My router RSSI: %d dBm\n", routerRSSI);
568
+
569
+ // Clear previous candidates
570
+ electionCandidates.clear();
571
+
572
+ // Add self as candidate
573
+ BridgeCandidate selfCandidate;
574
+ selfCandidate.nodeId = this->nodeId;
575
+ selfCandidate.routerRSSI = routerRSSI;
576
+ selfCandidate.uptime = millis();
577
+ selfCandidate.freeMemory = ESP.getFreeHeap();
578
+ electionCandidates.push_back(selfCandidate);
579
+
580
+ // Broadcast candidacy using JSON directly (avoiding dependency on alteriom package)
581
+ DynamicJsonDocument doc(256);
582
+ JsonObject obj = doc.to<JsonObject>();
583
+ obj["type"] = 611; // BRIDGE_ELECTION
584
+ obj["from"] = this->nodeId;
585
+ obj["routing"] = 2; // BROADCAST
586
+ obj["routerRSSI"] = routerRSSI;
587
+ obj["uptime"] = millis();
588
+ obj["freeMemory"] = ESP.getFreeHeap();
589
+ obj["timestamp"] = this->getNodeTime();
590
+ obj["routerSSID"] = routerSSID;
591
+ obj["message_type"] = 611;
592
+
593
+ String msg;
594
+ serializeJson(doc, msg);
595
+ this->sendBroadcast(msg);
596
+
597
+ Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
598
+
599
+ // Set election timeout
600
+ electionDeadline = millis() + electionTimeoutMs;
601
+ electionState = ELECTION_COLLECTING;
602
+
603
+ // Schedule election evaluation
604
+ this->addTask(electionTimeoutMs + 100, TASK_ONCE, [this]() {
605
+ this->evaluateElection();
606
+ });
607
+ }
608
+
609
+ /**
610
+ * Evaluate election and determine winner
611
+ * Called after election timeout expires
612
+ */
613
+ void evaluateElection() {
614
+ using namespace logger;
615
+
616
+ if (electionState != ELECTION_COLLECTING) {
617
+ Log(CONNECTION, "evaluateElection(): Not in collecting state\n");
618
+ return;
619
+ }
620
+
621
+ Log(CONNECTION, "=== Evaluating Election ===\n");
622
+ Log(CONNECTION, "evaluateElection(): %d candidates\n", electionCandidates.size());
623
+
624
+ // Find best candidate
625
+ BridgeCandidate* winner = nullptr;
626
+ int8_t bestRSSI = -127; // Worst possible RSSI
627
+
628
+ for (auto& candidate : electionCandidates) {
629
+ Log(CONNECTION, "evaluateElection(): Candidate %u: RSSI=%d, uptime=%u, mem=%u\n",
630
+ candidate.nodeId, candidate.routerRSSI, candidate.uptime, candidate.freeMemory);
631
+
632
+ if (candidate.routerRSSI > bestRSSI) {
633
+ bestRSSI = candidate.routerRSSI;
634
+ winner = &candidate;
635
+ } else if (candidate.routerRSSI == bestRSSI && winner != nullptr) {
636
+ // Tiebreaker 1: Higher uptime
637
+ if (candidate.uptime > winner->uptime) {
638
+ winner = &candidate;
639
+ } else if (candidate.uptime == winner->uptime) {
640
+ // Tiebreaker 2: More free memory
641
+ if (candidate.freeMemory > winner->freeMemory) {
642
+ winner = &candidate;
643
+ } else if (candidate.freeMemory == winner->freeMemory) {
644
+ // Tiebreaker 3: Lower node ID (deterministic)
645
+ if (candidate.nodeId < winner->nodeId) {
646
+ winner = &candidate;
647
+ }
648
+ }
649
+ }
650
+ }
651
+ }
652
+
653
+ if (winner == nullptr) {
654
+ Log(ERROR, "evaluateElection(): No winner found!\n");
655
+ electionState = ELECTION_IDLE;
656
+ return;
657
+ }
658
+
659
+ Log(CONNECTION, "=== Election Winner: Node %u ===\n", winner->nodeId);
660
+ Log(CONNECTION, " Router RSSI: %d dBm\n", winner->routerRSSI);
661
+ Log(CONNECTION, " Uptime: %u ms\n", winner->uptime);
662
+ Log(CONNECTION, " Free Memory: %u bytes\n", winner->freeMemory);
663
+
664
+ // Record election in diagnostics history
665
+ if (this->diagnosticsEnabled) {
666
+ ElectionRecord record;
667
+ record.timestamp = millis();
668
+ record.winnerNodeId = winner->nodeId;
669
+ record.winnerRSSI = winner->routerRSSI;
670
+ record.candidateCount = electionCandidates.size();
671
+ record.reason = "Bridge failure detected";
672
+
673
+ this->electionHistory.push_back(record);
674
+
675
+ // Keep history limited to MAX_ELECTION_HISTORY
676
+ if (this->electionHistory.size() > this->MAX_ELECTION_HISTORY) {
677
+ this->electionHistory.erase(this->electionHistory.begin());
678
+ }
679
+
680
+ Log(CONNECTION, "evaluateElection(): Election recorded in history\n");
681
+ }
682
+
683
+ if (winner->nodeId == this->nodeId) {
684
+ Log(CONNECTION, "🎯 I WON! Promoting to bridge...\n");
685
+ promoteToBridge();
686
+ } else {
687
+ Log(CONNECTION, "Winner is node %u, remaining as regular node\n", winner->nodeId);
688
+ }
689
+
690
+ electionState = ELECTION_IDLE;
691
+ electionCandidates.clear();
692
+ }
693
+
694
+ /**
695
+ * Promote this node to bridge role
696
+ * Called when node wins election
697
+ */
698
+ void promoteToBridge() {
699
+ using namespace logger;
700
+
701
+ Log(STARTUP, "=== Becoming Bridge Node ===\n");
702
+
703
+ // Store previous bridge (if any)
704
+ auto primaryBridge = this->getPrimaryBridge();
705
+ uint32_t previousBridgeId = primaryBridge ? primaryBridge->nodeId : 0;
706
+
707
+ // Reconfigure as bridge
708
+ this->stop();
709
+ delay(1000);
710
+
711
+ this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
712
+ mScheduler, _meshPort);
713
+
714
+ lastRoleChangeTime = millis();
715
+
716
+ Log(STARTUP, "✓ Bridge promotion complete\n");
717
+
718
+ // Notify via callback
719
+ if (bridgeRoleChangedCallback) {
720
+ bridgeRoleChangedCallback(true, "Election winner - best router signal");
721
+ }
722
+
723
+ // Broadcast takeover announcement
724
+ DynamicJsonDocument doc(256);
725
+ JsonObject obj = doc.to<JsonObject>();
726
+ obj["type"] = 612; // BRIDGE_TAKEOVER
727
+ obj["from"] = this->nodeId;
728
+ obj["routing"] = 2; // BROADCAST
729
+ obj["previousBridge"] = previousBridgeId;
730
+ obj["reason"] = "Election winner - best router signal";
731
+ obj["routerRSSI"] = WiFi.RSSI();
732
+ obj["timestamp"] = this->getNodeTime();
733
+ obj["message_type"] = 612;
734
+
735
+ String msg;
736
+ serializeJson(doc, msg);
737
+
738
+ // Small delay to ensure mesh is ready
739
+ delay(2000);
740
+ this->sendBroadcast(msg);
741
+
742
+ Log(STARTUP, "✓ Takeover announcement sent\n");
743
+ }
744
+
745
+ /**
746
+ * Handle received bridge election package
747
+ * Called by package handler when election message arrives
748
+ */
749
+ void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI, uint32_t uptime,
750
+ uint32_t freeMemory) {
751
+ using namespace logger;
752
+
753
+ if (electionState != ELECTION_COLLECTING) {
754
+ Log(CONNECTION, "handleBridgeElection(): Not collecting candidates, ignoring\n");
755
+ return;
756
+ }
757
+
758
+ // Check if candidate already exists
759
+ for (auto& candidate : electionCandidates) {
760
+ if (candidate.nodeId == fromNode) {
761
+ Log(CONNECTION, "handleBridgeElection(): Duplicate candidate from %u, ignoring\n", fromNode);
762
+ return;
763
+ }
764
+ }
765
+
766
+ BridgeCandidate candidate;
767
+ candidate.nodeId = fromNode;
768
+ candidate.routerRSSI = routerRSSI;
769
+ candidate.uptime = uptime;
770
+ candidate.freeMemory = freeMemory;
771
+
772
+ electionCandidates.push_back(candidate);
773
+
774
+ Log(CONNECTION, "handleBridgeElection(): Added candidate %u (RSSI: %d dBm)\n",
775
+ fromNode, routerRSSI);
776
+ }
777
+
778
+ /**
779
+ * Send bridge status broadcast
780
+ * Called periodically by bridge nodes to report connectivity status
781
+ */
782
+ void sendBridgeStatus() {
783
+ using namespace logger;
784
+
785
+ if (!this->bridgeStatusBroadcastEnabled) {
786
+ return;
787
+ }
788
+
789
+ // Create bridge status package
790
+ // We need to include the package header here since we're in wifi namespace
791
+ // The package will be sent as a JSON string
792
+ DynamicJsonDocument doc(256);
793
+ JsonObject obj = doc.to<JsonObject>();
794
+
795
+ obj["type"] = 610; // BRIDGE_STATUS type
796
+ obj["from"] = this->nodeId;
797
+ obj["routing"] = 2; // BROADCAST routing
798
+ obj["timestamp"] = this->getNodeTime();
799
+ obj["internetConnected"] = (WiFi.status() == WL_CONNECTED);
800
+ obj["routerRSSI"] = WiFi.RSSI();
801
+ obj["routerChannel"] = WiFi.channel();
802
+ obj["uptime"] = millis();
803
+ obj["gatewayIP"] = WiFi.gatewayIP().toString();
804
+ obj["message_type"] = 610;
805
+
806
+ String msg;
807
+ serializeJson(doc, msg);
808
+
809
+ Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)\n",
810
+ (WiFi.status() == WL_CONNECTED) ? "Connected" : "Disconnected");
811
+
812
+ this->sendBroadcast(msg);
813
+ }
259
814
  void eventHandleInit() {
260
815
  using namespace logger;
261
816
  #ifdef ESP32
@@ -356,6 +911,32 @@ class Mesh : public painlessmesh::Mesh<Connection> {
356
911
  WiFiEventHandler eventSTAGotIPHandler;
357
912
  #endif // ESP8266
358
913
  AsyncServer *_tcpListener;
914
+ std::shared_ptr<Task> bridgeStatusTask;
915
+
916
+ // Bridge failover state and configuration
917
+ enum ElectionState {
918
+ ELECTION_IDLE,
919
+ ELECTION_SCANNING,
920
+ ELECTION_COLLECTING
921
+ };
922
+
923
+ struct BridgeCandidate {
924
+ uint32_t nodeId;
925
+ int8_t routerRSSI;
926
+ uint32_t uptime;
927
+ uint32_t freeMemory;
928
+ };
929
+
930
+ bool bridgeFailoverEnabled = true;
931
+ bool routerCredentialsConfigured = false;
932
+ TSTRING routerSSID = "";
933
+ TSTRING routerPassword = "";
934
+ uint32_t electionTimeoutMs = 5000; // Default 5 seconds
935
+ uint32_t lastRoleChangeTime = 0;
936
+ ElectionState electionState = ELECTION_IDLE;
937
+ uint32_t electionDeadline = 0;
938
+ std::vector<BridgeCandidate> electionCandidates;
939
+ std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
359
940
  };
360
941
  } // namespace wifi
361
942
  }; // namespace painlessmesh