@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
@@ -10,6 +10,7 @@
10
10
  #include "painlessmesh/ntp.hpp"
11
11
  #include "painlessmesh/plugin.hpp"
12
12
  #include "painlessmesh/protocol.hpp"
13
+ #include "painlessmesh/rtc.hpp"
13
14
  #include "painlessmesh/tcp.hpp"
14
15
 
15
16
  #ifdef PAINLESSMESH_ENABLE_OTA
@@ -23,6 +24,119 @@ typedef std::function<void(uint32_t from, TSTRING &msg)> receivedCallback_t;
23
24
  typedef std::function<void()> changedConnectionsCallback_t;
24
25
  typedef std::function<void(int32_t offset)> nodeTimeAdjustedCallback_t;
25
26
  typedef std::function<void(uint32_t nodeId, int32_t delay)> nodeDelayCallback_t;
27
+ typedef std::function<void(uint32_t bridgeNodeId, bool internetAvailable)> bridgeStatusChangedCallback_t;
28
+ typedef std::function<void(uint32_t timestamp)> rtcSyncCompleteCallback_t;
29
+
30
+ /**
31
+ * Bridge information structure
32
+ *
33
+ * Tracks the status and health of bridge nodes in the mesh network
34
+ */
35
+ class BridgeInfo {
36
+ public:
37
+ uint32_t nodeId = 0; // Bridge node ID
38
+ bool internetConnected = false; // Is bridge connected to Internet?
39
+ int8_t routerRSSI = 0; // Router WiFi signal strength in dBm
40
+ uint8_t routerChannel = 0; // Router WiFi channel
41
+ uint32_t lastSeen = 0; // Timestamp when last status received (millis)
42
+ uint32_t uptime = 0; // Bridge uptime in milliseconds
43
+ TSTRING gatewayIP = ""; // Router gateway IP address
44
+ uint32_t timestamp = 0; // Timestamp from bridge status message
45
+
46
+ /**
47
+ * Check if this bridge is considered healthy
48
+ * A bridge is healthy if we've received a status update within the timeout period
49
+ */
50
+ bool isHealthy(uint32_t timeoutMs = 60000) const {
51
+ return (millis() - lastSeen) < timeoutMs;
52
+ }
53
+ };
54
+
55
+ /**
56
+ * Bridge Health Metrics structure
57
+ *
58
+ * Comprehensive metrics for monitoring bridge health, connectivity quality,
59
+ * and performance. Useful for troubleshooting and capacity planning.
60
+ */
61
+ struct BridgeHealthMetrics {
62
+ // Connectivity
63
+ uint32_t uptimeSeconds = 0;
64
+ uint32_t internetUptimeSeconds = 0;
65
+ uint32_t totalDisconnects = 0;
66
+ uint32_t currentUptime = 0;
67
+
68
+ // Signal Quality
69
+ int8_t currentRSSI = 0;
70
+ int8_t avgRSSI = 0;
71
+ int8_t minRSSI = 0;
72
+ int8_t maxRSSI = -127;
73
+
74
+ // Traffic
75
+ uint64_t bytesRx = 0;
76
+ uint64_t bytesTx = 0;
77
+ uint32_t messagesRx = 0;
78
+ uint32_t messagesTx = 0;
79
+ uint32_t messagesQueued = 0;
80
+ uint32_t messagesDropped = 0;
81
+
82
+ // Performance
83
+ uint32_t avgLatencyMs = 0;
84
+ uint8_t packetLossPercent = 0;
85
+ uint32_t meshNodeCount = 0;
86
+ };
87
+
88
+ /**
89
+ * Bridge Status structure
90
+ *
91
+ * Current status of this node's bridge role and connectivity
92
+ */
93
+ struct BridgeStatus {
94
+ bool isBridge = false; // Is this node acting as a bridge?
95
+ bool internetConnected = false; // Is Internet connection available?
96
+ TSTRING role = "regular"; // Role: "regular", "bridge", "root"
97
+ uint32_t bridgeNodeId = 0; // Current bridge node ID (0 if none)
98
+ int8_t bridgeRSSI = 0; // Signal strength to bridge/router (dBm)
99
+ uint32_t timeSinceBridgeChange = 0; // Time since last bridge change (ms)
100
+ };
101
+
102
+ /**
103
+ * Election Record structure
104
+ *
105
+ * Records information about bridge election events
106
+ */
107
+ struct ElectionRecord {
108
+ uint32_t timestamp = 0; // When election occurred (millis)
109
+ uint32_t winnerNodeId = 0; // Node that won election
110
+ int8_t winnerRSSI = 0; // Winner's router RSSI
111
+ uint32_t candidateCount = 0; // Number of candidates
112
+ TSTRING reason = ""; // Why election was triggered
113
+ };
114
+
115
+ /**
116
+ * Bridge Change Event structure
117
+ *
118
+ * Records the last bridge change event
119
+ */
120
+ struct BridgeChangeEvent {
121
+ uint32_t timestamp = 0; // When change occurred (millis)
122
+ uint32_t oldBridgeId = 0; // Previous bridge node ID
123
+ uint32_t newBridgeId = 0; // New bridge node ID
124
+ TSTRING reason = ""; // Reason for change
125
+ bool internetAvailable = false; // Internet available after change
126
+ };
127
+
128
+ /**
129
+ * Bridge Connectivity Test Result
130
+ *
131
+ * Results from bridge connectivity testing
132
+ */
133
+ struct BridgeTestResult {
134
+ bool success = false; // Overall test success
135
+ bool bridgeReachable = false; // Can reach bridge node
136
+ bool internetReachable = false; // Can reach Internet (if bridge available)
137
+ uint32_t latencyMs = 0; // Round-trip latency to bridge
138
+ TSTRING message = ""; // Detailed test message
139
+ };
26
140
 
27
141
  /**
28
142
  * Main api class for the mesh
@@ -49,6 +163,38 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
49
163
  this->callbackList = painlessmesh::router::addPackageCallback(
50
164
  std::move(this->callbackList), (*this));
51
165
 
166
+ // Add bridge status package handler (Type 610)
167
+ // This will be called when any node receives a bridge status broadcast
168
+ this->callbackList.onPackage(
169
+ 610, // BRIDGE_STATUS type
170
+ [this](protocol::Variant& variant, std::shared_ptr<T>, uint32_t) {
171
+ // We need to manually parse the JSON since BridgeStatusPackage is in alteriom namespace
172
+ // and may not be available in all contexts. We'll parse the critical fields directly.
173
+ JsonDocument doc;
174
+ TSTRING str;
175
+ variant.printTo(str);
176
+ deserializeJson(doc, str);
177
+ JsonObject obj = doc.as<JsonObject>();
178
+
179
+ if (obj["internetConnected"].is<bool>()) {
180
+ uint32_t bridgeNodeId = obj["from"];
181
+ bool internetConnected = obj["internetConnected"];
182
+ int8_t routerRSSI = obj["routerRSSI"] | 0;
183
+ uint8_t routerChannel = obj["routerChannel"] | 0;
184
+ uint32_t uptime = obj["uptime"] | 0;
185
+ TSTRING gatewayIP = obj["gatewayIP"].as<TSTRING>();
186
+ uint32_t timestamp = obj["timestamp"] | 0;
187
+
188
+ // Update bridge status
189
+ this->updateBridgeStatus(bridgeNodeId, internetConnected, routerRSSI,
190
+ routerChannel, uptime, gatewayIP, timestamp);
191
+
192
+ Log(GENERAL, "Bridge status received from %u: Internet %s\n",
193
+ bridgeNodeId, internetConnected ? "Connected" : "Disconnected");
194
+ }
195
+ return false; // Don't consume the package, allow other handlers
196
+ });
197
+
52
198
  this->changedConnectionCallbacks.push_back([this](uint32_t nodeId) {
53
199
  Log(MESH_STATUS, "Changed connections in neighbour %u\n", nodeId);
54
200
  if (nodeId != 0) layout::syncLayout<T>((*this), nodeId);
@@ -56,6 +202,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
56
202
  this->droppedConnectionCallbacks.push_back([this](uint32_t nodeId,
57
203
  bool station) {
58
204
  Log(MESH_STATUS, "Dropped connection %u, station %d\n", nodeId, station);
205
+ this->metricsDisconnectCount++;
59
206
  this->eraseClosedConnections();
60
207
  });
61
208
  this->newConnectionCallbacks.push_back([](uint32_t nodeId) {
@@ -340,6 +487,329 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
340
487
  nodeDelayReceivedCallback = onDelayReceived;
341
488
  }
342
489
 
490
+ /** Callback that gets called when bridge status changes.
491
+ *
492
+ * This fires when a bridge node reports a change in Internet connectivity status.
493
+ * Useful for implementing failover logic or message queueing.
494
+ *
495
+ * \code
496
+ * mesh.onBridgeStatusChanged([](auto bridgeNodeId, auto hasInternet) {
497
+ * if (hasInternet) {
498
+ * Serial.println("Internet available - sending queued data");
499
+ * } else {
500
+ * Serial.println("Internet offline - queueing messages");
501
+ * }
502
+ * });
503
+ * \endcode
504
+ */
505
+ void onBridgeStatusChanged(bridgeStatusChangedCallback_t onBridgeStatusChanged) {
506
+ Log(logger::GENERAL, "onBridgeStatusChanged():\n");
507
+ bridgeStatusChangedCallback = onBridgeStatusChanged;
508
+ }
509
+
510
+ /**
511
+ * Check if any bridge in the mesh has Internet connectivity
512
+ *
513
+ * @return true if at least one healthy bridge reports Internet connection
514
+ */
515
+ bool hasInternetConnection() {
516
+ for (const auto& bridge : knownBridges) {
517
+ if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
518
+ return true;
519
+ }
520
+ }
521
+ return false;
522
+ }
523
+
524
+ /**
525
+ * Get list of all known bridges in the mesh
526
+ *
527
+ * @return vector of BridgeInfo objects for all tracked bridges
528
+ */
529
+ std::vector<BridgeInfo> getBridges() {
530
+ return knownBridges;
531
+ }
532
+
533
+ /**
534
+ * Get the primary (best) bridge node
535
+ *
536
+ * Primary bridge is selected based on:
537
+ * 1. Must be healthy (seen within timeout)
538
+ * 2. Must have Internet connection
539
+ * 3. Best WiFi RSSI to router
540
+ *
541
+ * @return pointer to BridgeInfo of primary bridge, or nullptr if no suitable bridge
542
+ */
543
+ BridgeInfo* getPrimaryBridge() {
544
+ BridgeInfo* primary = nullptr;
545
+ int8_t bestRSSI = -127; // Worst possible RSSI
546
+
547
+ for (auto& bridge : knownBridges) {
548
+ if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
549
+ if (bridge.routerRSSI > bestRSSI) {
550
+ bestRSSI = bridge.routerRSSI;
551
+ primary = &bridge;
552
+ }
553
+ }
554
+ }
555
+
556
+ return primary;
557
+ }
558
+
559
+ /**
560
+ * Check if this node is acting as a bridge
561
+ *
562
+ * @return true if this node is configured as a root node (typically bridges)
563
+ */
564
+ bool isBridge() {
565
+ return this->root;
566
+ }
567
+
568
+ /**
569
+ * Set the interval for bridge status broadcasts (bridge nodes only)
570
+ *
571
+ * @param intervalMs Broadcast interval in milliseconds (default: 30000 = 30 seconds)
572
+ */
573
+ void setBridgeStatusInterval(uint32_t intervalMs) {
574
+ bridgeStatusIntervalMs = intervalMs;
575
+ }
576
+
577
+ /**
578
+ * Set the timeout for considering a bridge offline
579
+ *
580
+ * @param timeoutMs Timeout in milliseconds (default: 60000 = 60 seconds)
581
+ */
582
+ void setBridgeTimeout(uint32_t timeoutMs) {
583
+ bridgeTimeoutMs = timeoutMs;
584
+ }
585
+
586
+ /**
587
+ * Enable or disable bridge status broadcasting (bridge nodes only)
588
+ *
589
+ * @param enabled true to enable broadcasting (default), false to disable
590
+ */
591
+ void enableBridgeStatusBroadcast(bool enabled) {
592
+ bridgeStatusBroadcastEnabled = enabled;
593
+ }
594
+
595
+ /**
596
+ * Update bridge information from received status package
597
+ * Internal method called when bridge status is received
598
+ *
599
+ * @param bridgeNodeId ID of the bridge node
600
+ * @param internetConnected Internet connectivity status
601
+ * @param routerRSSI Router signal strength
602
+ * @param routerChannel Router WiFi channel
603
+ * @param uptime Bridge uptime
604
+ * @param gatewayIP Router gateway IP
605
+ * @param timestamp Status timestamp
606
+ */
607
+ void updateBridgeStatus(uint32_t bridgeNodeId, bool internetConnected,
608
+ int8_t routerRSSI, uint8_t routerChannel,
609
+ uint32_t uptime, TSTRING gatewayIP, uint32_t timestamp) {
610
+ // Find existing bridge or add new one
611
+ BridgeInfo* bridge = nullptr;
612
+ uint32_t oldPrimaryBridgeId = 0;
613
+ auto oldPrimary = this->getPrimaryBridge();
614
+ if (oldPrimary != nullptr) {
615
+ oldPrimaryBridgeId = oldPrimary->nodeId;
616
+ }
617
+
618
+ for (auto& b : knownBridges) {
619
+ if (b.nodeId == bridgeNodeId) {
620
+ bridge = &b;
621
+ break;
622
+ }
623
+ }
624
+
625
+ bool wasConnected = false;
626
+ bool isNewBridge = false;
627
+ if (bridge == nullptr) {
628
+ // New bridge - add to list
629
+ BridgeInfo newBridge;
630
+ newBridge.nodeId = bridgeNodeId;
631
+ knownBridges.push_back(newBridge);
632
+ bridge = &knownBridges.back();
633
+ isNewBridge = true;
634
+ } else {
635
+ wasConnected = bridge->internetConnected;
636
+ }
637
+
638
+ // Update bridge info
639
+ bridge->internetConnected = internetConnected;
640
+ bridge->routerRSSI = routerRSSI;
641
+ bridge->routerChannel = routerChannel;
642
+ bridge->lastSeen = millis();
643
+ bridge->uptime = uptime;
644
+ bridge->gatewayIP = gatewayIP;
645
+ bridge->timestamp = timestamp;
646
+
647
+ // Check if primary bridge changed
648
+ auto newPrimary = this->getPrimaryBridge();
649
+ uint32_t newPrimaryBridgeId = (newPrimary != nullptr) ? newPrimary->nodeId : 0;
650
+
651
+ if (diagnosticsEnabled && oldPrimaryBridgeId != newPrimaryBridgeId) {
652
+ // Record bridge change
653
+ lastBridgeChange.timestamp = millis();
654
+ lastBridgeChange.oldBridgeId = oldPrimaryBridgeId;
655
+ lastBridgeChange.newBridgeId = newPrimaryBridgeId;
656
+ lastBridgeChange.internetAvailable = internetConnected;
657
+ if (isNewBridge) {
658
+ lastBridgeChange.reason = "New bridge discovered";
659
+ } else if (internetConnected && !wasConnected) {
660
+ lastBridgeChange.reason = "Bridge Internet restored";
661
+ } else if (!internetConnected && wasConnected) {
662
+ lastBridgeChange.reason = "Bridge Internet lost";
663
+ } else {
664
+ lastBridgeChange.reason = "Primary bridge changed";
665
+ }
666
+
667
+ lastBridgeChangeTime = millis();
668
+ Log(logger::GENERAL, "updateBridgeStatus(): Bridge change recorded\n");
669
+ }
670
+
671
+ // Trigger callback if status changed
672
+ if (wasConnected != internetConnected && bridgeStatusChangedCallback) {
673
+ bridgeStatusChangedCallback(bridgeNodeId, internetConnected);
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Enable RTC integration for offline timekeeping
679
+ *
680
+ * Allows nodes to maintain accurate timestamps even when Internet/bridge
681
+ * is unavailable. User must provide an implementation of RTCInterface
682
+ * for their specific RTC hardware.
683
+ *
684
+ * \code
685
+ * // Example with DS3231 RTC
686
+ * class MyRTC : public painlessmesh::rtc::RTCInterface {
687
+ * // ... implement interface methods ...
688
+ * };
689
+ * MyRTC myRTC;
690
+ * mesh.enableRTC(&myRTC);
691
+ * \endcode
692
+ *
693
+ * @param rtcInterface Pointer to user's RTC implementation
694
+ * @return true if RTC enabled successfully, false otherwise
695
+ */
696
+ bool enableRTC(rtc::RTCInterface* rtcInterface) {
697
+ using namespace logger;
698
+ Log(GENERAL, "enableRTC(): Initializing RTC\n");
699
+ return rtcManager.enable(rtcInterface);
700
+ }
701
+
702
+ /**
703
+ * Disable RTC integration
704
+ */
705
+ void disableRTC() {
706
+ using namespace logger;
707
+ Log(GENERAL, "disableRTC(): Disabling RTC\n");
708
+ rtcManager.disable();
709
+ }
710
+
711
+ /**
712
+ * Sync RTC from NTP/Internet time source
713
+ *
714
+ * Should be called when Internet connection is available to update
715
+ * the RTC with accurate time. Typically called in onBridgeStatusChanged
716
+ * callback when Internet becomes available.
717
+ *
718
+ * \code
719
+ * mesh.onBridgeStatusChanged([](auto bridgeNodeId, auto hasInternet) {
720
+ * if (hasInternet) {
721
+ * // Get NTP time and sync RTC
722
+ * uint32_t ntpTime = getNTPTime(); // User implements this
723
+ * if (mesh.syncRTCFromNTP(ntpTime)) {
724
+ * Serial.println("RTC synced successfully");
725
+ * }
726
+ * }
727
+ * });
728
+ * \endcode
729
+ *
730
+ * @param ntpTimestamp Unix timestamp from NTP source
731
+ * @return true if sync successful, false otherwise
732
+ */
733
+ bool syncRTCFromNTP(uint32_t ntpTimestamp) {
734
+ using namespace logger;
735
+ Log(GENERAL, "syncRTCFromNTP(): Syncing RTC to timestamp %u\n", ntpTimestamp);
736
+
737
+ if (!rtcManager.isEnabled()) {
738
+ Log(ERROR, "syncRTCFromNTP(): RTC not enabled\n");
739
+ return false;
740
+ }
741
+
742
+ bool success = rtcManager.syncFromNTP(ntpTimestamp);
743
+
744
+ if (success && rtcSyncCompleteCallback) {
745
+ rtcSyncCompleteCallback(ntpTimestamp);
746
+ }
747
+
748
+ return success;
749
+ }
750
+
751
+ /**
752
+ * Get accurate time with RTC fallback
753
+ *
754
+ * Returns time from RTC if available, otherwise falls back to mesh time.
755
+ * This provides the most accurate timestamp available to the node.
756
+ *
757
+ * @return Unix timestamp in seconds, or mesh time in microseconds if RTC unavailable
758
+ */
759
+ uint32_t getAccurateTime() {
760
+ if (rtcManager.isEnabled()) {
761
+ uint32_t rtcTime = rtcManager.getTime();
762
+ if (rtcTime > 0) {
763
+ return rtcTime;
764
+ }
765
+ }
766
+ // Fallback to mesh time (microseconds)
767
+ return this->getNodeTime();
768
+ }
769
+
770
+ /**
771
+ * Check if RTC is enabled and available
772
+ *
773
+ * @return true if RTC can be used, false otherwise
774
+ */
775
+ bool hasRTC() const {
776
+ return rtcManager.isEnabled();
777
+ }
778
+
779
+ /**
780
+ * Get RTC type
781
+ *
782
+ * @return RTCType enum value, or RTC_NONE if no RTC enabled
783
+ */
784
+ rtc::RTCType getRTCType() const {
785
+ return rtcManager.getType();
786
+ }
787
+
788
+ /**
789
+ * Get time since last RTC sync
790
+ *
791
+ * @return Milliseconds since last RTC sync, or 0 if never synced
792
+ */
793
+ uint32_t getTimeSinceRTCSync() const {
794
+ return rtcManager.getTimeSinceLastSync();
795
+ }
796
+
797
+ /**
798
+ * Callback when RTC sync completes
799
+ *
800
+ * This fires when syncRTCFromNTP() successfully updates the RTC.
801
+ *
802
+ * \code
803
+ * mesh.onRTCSyncComplete([](auto timestamp) {
804
+ * Serial.printf("RTC synced to: %u\n", timestamp);
805
+ * });
806
+ * \endcode
807
+ */
808
+ void onRTCSyncComplete(rtcSyncCompleteCallback_t onRTCSyncComplete) {
809
+ Log(logger::GENERAL, "onRTCSyncComplete():\n");
810
+ rtcSyncCompleteCallback = onRTCSyncComplete;
811
+ }
812
+
343
813
  /**
344
814
  * Are we connected/know a route to the given node?
345
815
  *
@@ -456,6 +926,634 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
456
926
  return table;
457
927
  }
458
928
 
929
+ /**
930
+ * Get bridge health metrics
931
+ *
932
+ * Collects comprehensive health metrics including connectivity, signal quality,
933
+ * traffic, and performance data. Useful for monitoring and troubleshooting.
934
+ *
935
+ * \code
936
+ * auto metrics = mesh.getBridgeHealthMetrics();
937
+ * Serial.printf("Uptime: %u s, Messages RX: %u, Avg Latency: %u ms\n",
938
+ * metrics.uptimeSeconds, metrics.messagesRx, metrics.avgLatencyMs);
939
+ * \endcode
940
+ *
941
+ * @return BridgeHealthMetrics structure with current metrics
942
+ */
943
+ BridgeHealthMetrics getBridgeHealthMetrics() {
944
+ BridgeHealthMetrics metrics;
945
+
946
+ // Connectivity metrics
947
+ metrics.uptimeSeconds = millis() / 1000;
948
+ metrics.currentUptime = millis();
949
+
950
+ // Check for primary bridge with Internet connection
951
+ auto primaryBridge = getPrimaryBridge();
952
+ if (primaryBridge != nullptr) {
953
+ metrics.internetUptimeSeconds = (millis() - primaryBridge->lastSeen) / 1000;
954
+ metrics.currentRSSI = primaryBridge->routerRSSI;
955
+ }
956
+
957
+ // Aggregate traffic and performance from all connections
958
+ uint64_t totalBytesRx = 0;
959
+ uint64_t totalBytesTx = 0;
960
+ uint32_t totalMessagesRx = 0;
961
+ uint32_t totalMessagesTx = 0;
962
+ uint32_t totalMessagesDropped = 0;
963
+ uint32_t totalLatency = 0;
964
+ uint32_t latencySampleCount = 0;
965
+ int8_t sumRSSI = 0;
966
+ int8_t minRSSI = 0;
967
+ int8_t maxRSSI = -127;
968
+ uint32_t rssiCount = 0;
969
+
970
+ for (auto conn : this->subs) {
971
+ if (conn->connected()) {
972
+ totalBytesRx += conn->bytesRx;
973
+ totalBytesTx += conn->bytesTx;
974
+ totalMessagesRx += conn->messagesRx;
975
+ totalMessagesTx += conn->messagesTx;
976
+ totalMessagesDropped += conn->messagesDropped;
977
+
978
+ // Latency
979
+ int latency = conn->getLatency();
980
+ if (latency >= 0) {
981
+ totalLatency += latency;
982
+ latencySampleCount++;
983
+ }
984
+
985
+ // RSSI
986
+ int rssi = conn->getRSSI();
987
+ if (rssi != 0) {
988
+ sumRSSI += rssi;
989
+ rssiCount++;
990
+ if (rssi < minRSSI || minRSSI == 0) minRSSI = rssi;
991
+ if (rssi > maxRSSI) maxRSSI = rssi;
992
+ }
993
+ }
994
+ }
995
+
996
+ // Set traffic metrics
997
+ metrics.bytesRx = totalBytesRx;
998
+ metrics.bytesTx = totalBytesTx;
999
+ metrics.messagesRx = totalMessagesRx;
1000
+ metrics.messagesTx = totalMessagesTx;
1001
+ metrics.messagesDropped = totalMessagesDropped;
1002
+
1003
+ // Calculate averages
1004
+ if (latencySampleCount > 0) {
1005
+ metrics.avgLatencyMs = totalLatency / latencySampleCount;
1006
+ }
1007
+
1008
+ if (rssiCount > 0) {
1009
+ metrics.avgRSSI = sumRSSI / rssiCount;
1010
+ metrics.minRSSI = minRSSI;
1011
+ metrics.maxRSSI = maxRSSI;
1012
+ }
1013
+
1014
+ // Calculate packet loss percentage
1015
+ uint32_t totalAttempted = totalMessagesTx + totalMessagesDropped;
1016
+ if (totalAttempted > 0) {
1017
+ metrics.packetLossPercent = (totalMessagesDropped * 100) / totalAttempted;
1018
+ }
1019
+
1020
+ // Mesh node count
1021
+ metrics.meshNodeCount = this->getNodeList(true).size();
1022
+
1023
+ // Track disconnects (stored in protected member)
1024
+ metrics.totalDisconnects = metricsDisconnectCount;
1025
+
1026
+ return metrics;
1027
+ }
1028
+
1029
+ /**
1030
+ * Reset health metrics counters
1031
+ *
1032
+ * Resets all counters to zero but keeps current state metrics (RSSI, node count, etc.)
1033
+ * Useful for periodic monitoring windows.
1034
+ *
1035
+ * \code
1036
+ * // Reset metrics at the start of each monitoring period
1037
+ * mesh.resetHealthMetrics();
1038
+ * \endcode
1039
+ */
1040
+ void resetHealthMetrics() {
1041
+ using namespace logger;
1042
+ Log(GENERAL, "resetHealthMetrics(): Resetting all metric counters\n");
1043
+
1044
+ // Reset connection metrics
1045
+ for (auto conn : this->subs) {
1046
+ if (conn->connected()) {
1047
+ conn->messagesRx = 0;
1048
+ conn->messagesTx = 0;
1049
+ conn->messagesDropped = 0;
1050
+ conn->bytesRx = 0;
1051
+ conn->bytesTx = 0;
1052
+ conn->latencySamples.clear();
1053
+ }
1054
+ }
1055
+
1056
+ // Reset disconnect counter
1057
+ metricsDisconnectCount = 0;
1058
+ }
1059
+
1060
+ /**
1061
+ * Export health metrics as JSON string
1062
+ *
1063
+ * Generates a JSON representation of current health metrics for easy integration
1064
+ * with monitoring tools, MQTT publishing, or logging.
1065
+ *
1066
+ * \code
1067
+ * String json = mesh.getHealthMetricsJSON();
1068
+ * mqttClient.publish("bridge/metrics", json.c_str());
1069
+ * \endcode
1070
+ *
1071
+ * @return JSON string containing all health metrics
1072
+ */
1073
+ TSTRING getHealthMetricsJSON() {
1074
+ auto metrics = getBridgeHealthMetrics();
1075
+
1076
+ TSTRING json = "{";
1077
+ json += "\"connectivity\":{";
1078
+ json += "\"uptimeSeconds\":";
1079
+ json += std::to_string(metrics.uptimeSeconds);
1080
+ json += ",\"internetUptimeSeconds\":";
1081
+ json += std::to_string(metrics.internetUptimeSeconds);
1082
+ json += ",\"totalDisconnects\":";
1083
+ json += std::to_string(metrics.totalDisconnects);
1084
+ json += ",\"currentUptime\":";
1085
+ json += std::to_string(metrics.currentUptime);
1086
+ json += "},";
1087
+
1088
+ json += "\"signalQuality\":{";
1089
+ json += "\"currentRSSI\":";
1090
+ json += std::to_string(metrics.currentRSSI);
1091
+ json += ",\"avgRSSI\":";
1092
+ json += std::to_string(metrics.avgRSSI);
1093
+ json += ",\"minRSSI\":";
1094
+ json += std::to_string(metrics.minRSSI);
1095
+ json += ",\"maxRSSI\":";
1096
+ json += std::to_string(metrics.maxRSSI);
1097
+ json += "},";
1098
+
1099
+ json += "\"traffic\":{";
1100
+ json += "\"bytesRx\":";
1101
+ json += std::to_string(metrics.bytesRx);
1102
+ json += ",\"bytesTx\":";
1103
+ json += std::to_string(metrics.bytesTx);
1104
+ json += ",\"messagesRx\":";
1105
+ json += std::to_string(metrics.messagesRx);
1106
+ json += ",\"messagesTx\":";
1107
+ json += std::to_string(metrics.messagesTx);
1108
+ json += ",\"messagesQueued\":";
1109
+ json += std::to_string(metrics.messagesQueued);
1110
+ json += ",\"messagesDropped\":";
1111
+ json += std::to_string(metrics.messagesDropped);
1112
+ json += "},";
1113
+
1114
+ json += "\"performance\":{";
1115
+ json += "\"avgLatencyMs\":";
1116
+ json += std::to_string(metrics.avgLatencyMs);
1117
+ json += ",\"packetLossPercent\":";
1118
+ json += std::to_string(metrics.packetLossPercent);
1119
+ json += ",\"meshNodeCount\":";
1120
+ json += std::to_string(metrics.meshNodeCount);
1121
+ json += "}";
1122
+
1123
+ json += "}";
1124
+ return json;
1125
+ }
1126
+
1127
+ /**
1128
+ * Set periodic health metrics callback
1129
+ *
1130
+ * Registers a callback that will be invoked periodically with current health metrics.
1131
+ * Useful for automated monitoring, MQTT publishing, or Prometheus export.
1132
+ *
1133
+ * \code
1134
+ * mesh.onHealthMetricsUpdate([](BridgeHealthMetrics metrics) {
1135
+ * String json = mesh.getHealthMetricsJSON();
1136
+ * mqttClient.publish("bridge/metrics", json.c_str());
1137
+ * }, 60000); // Every 60 seconds
1138
+ * \endcode
1139
+ *
1140
+ * @param callback Function to call with metrics
1141
+ * @param intervalMs Callback interval in milliseconds (default: 60000 = 60 seconds)
1142
+ */
1143
+ void onHealthMetricsUpdate(std::function<void(BridgeHealthMetrics)> callback,
1144
+ uint32_t intervalMs = 60000) {
1145
+ using namespace logger;
1146
+ Log(GENERAL, "onHealthMetricsUpdate(): Setting up periodic callback every %u ms\n", intervalMs);
1147
+
1148
+ // Create a task that periodically collects and reports metrics
1149
+ auto metricsTask = this->addTask(intervalMs, TASK_FOREVER, [this, callback]() {
1150
+ auto metrics = this->getBridgeHealthMetrics();
1151
+ callback(metrics);
1152
+ });
1153
+
1154
+ metricsTask->enable();
1155
+ }
1156
+
1157
+ // ==================== Enhanced Diagnostics API ====================
1158
+
1159
+ /**
1160
+ * Enable or disable diagnostics collection
1161
+ *
1162
+ * When enabled, the mesh will track election history, bridge changes,
1163
+ * and other diagnostic information. This has minimal overhead.
1164
+ *
1165
+ * @param enabled true to enable diagnostics (default), false to disable
1166
+ */
1167
+ void enableDiagnostics(bool enabled = true) {
1168
+ diagnosticsEnabled = enabled;
1169
+ if (enabled) {
1170
+ Log(logger::GENERAL, "enableDiagnostics(): Diagnostics enabled\n");
1171
+ } else {
1172
+ Log(logger::GENERAL, "enableDiagnostics(): Diagnostics disabled\n");
1173
+ }
1174
+ }
1175
+
1176
+ /**
1177
+ * Get current bridge status
1178
+ *
1179
+ * Returns information about this node's bridge role and connectivity status
1180
+ *
1181
+ * \code
1182
+ * auto status = mesh.getBridgeStatus();
1183
+ * if (status.isBridge && status.internetConnected) {
1184
+ * Serial.println("I am bridge with Internet");
1185
+ * }
1186
+ * \endcode
1187
+ *
1188
+ * @return BridgeStatus structure with current status
1189
+ */
1190
+ BridgeStatus getBridgeStatus() {
1191
+ BridgeStatus status;
1192
+
1193
+ status.isBridge = this->isBridge();
1194
+ status.internetConnected = this->hasInternetConnection();
1195
+
1196
+ if (status.isBridge) {
1197
+ status.role = "bridge";
1198
+ } else if (this->root) {
1199
+ status.role = "root";
1200
+ } else {
1201
+ status.role = "regular";
1202
+ }
1203
+
1204
+ // Get primary bridge info
1205
+ auto primaryBridge = this->getPrimaryBridge();
1206
+ if (primaryBridge != nullptr) {
1207
+ status.bridgeNodeId = primaryBridge->nodeId;
1208
+ status.bridgeRSSI = primaryBridge->routerRSSI;
1209
+ }
1210
+
1211
+ // Calculate time since last bridge change
1212
+ if (lastBridgeChangeTime > 0) {
1213
+ status.timeSinceBridgeChange = millis() - lastBridgeChangeTime;
1214
+ }
1215
+
1216
+ return status;
1217
+ }
1218
+
1219
+ /**
1220
+ * Get election history
1221
+ *
1222
+ * Returns list of recent bridge elections if diagnostics are enabled.
1223
+ * History is limited to last 10 elections.
1224
+ *
1225
+ * \code
1226
+ * auto history = mesh.getElectionHistory();
1227
+ * for (const auto& record : history) {
1228
+ * Serial.printf("Election: winner=%u, RSSI=%d, candidates=%u\n",
1229
+ * record.winnerNodeId, record.winnerRSSI, record.candidateCount);
1230
+ * }
1231
+ * \endcode
1232
+ *
1233
+ * @return Vector of ElectionRecord structures
1234
+ */
1235
+ std::vector<ElectionRecord> getElectionHistory() {
1236
+ if (!diagnosticsEnabled) {
1237
+ Log(logger::GENERAL, "getElectionHistory(): Diagnostics not enabled\n");
1238
+ return std::vector<ElectionRecord>();
1239
+ }
1240
+ return electionHistory;
1241
+ }
1242
+
1243
+ /**
1244
+ * Get last bridge change event
1245
+ *
1246
+ * Returns information about the most recent bridge change
1247
+ *
1248
+ * \code
1249
+ * auto event = mesh.getLastBridgeChange();
1250
+ * if (event.timestamp > 0) {
1251
+ * Serial.printf("Bridge changed from %u to %u: %s\n",
1252
+ * event.oldBridgeId, event.newBridgeId, event.reason.c_str());
1253
+ * }
1254
+ * \endcode
1255
+ *
1256
+ * @return BridgeChangeEvent structure
1257
+ */
1258
+ BridgeChangeEvent getLastBridgeChange() {
1259
+ return lastBridgeChange;
1260
+ }
1261
+
1262
+ /**
1263
+ * Get Internet path to specific node
1264
+ *
1265
+ * Returns the routing path from the specified node to the Internet bridge.
1266
+ * Path includes all intermediate nodes from source to bridge.
1267
+ *
1268
+ * \code
1269
+ * auto path = mesh.getInternetPath(targetNodeId);
1270
+ * Serial.print("Path to Internet: ");
1271
+ * for (auto nodeId : path) {
1272
+ * Serial.printf("%u -> ", nodeId);
1273
+ * }
1274
+ * Serial.println("Internet");
1275
+ * \endcode
1276
+ *
1277
+ * @param nodeId Node to find path from
1278
+ * @return Vector of node IDs representing the path (empty if no path)
1279
+ */
1280
+ std::vector<uint32_t> getInternetPath(uint32_t nodeId) {
1281
+ std::vector<uint32_t> path;
1282
+
1283
+ // Get primary bridge
1284
+ auto primaryBridge = this->getPrimaryBridge();
1285
+ if (primaryBridge == nullptr) {
1286
+ Log(logger::GENERAL, "getInternetPath(): No bridge available\n");
1287
+ return path;
1288
+ }
1289
+
1290
+ // If requesting path for the bridge itself
1291
+ if (nodeId == primaryBridge->nodeId) {
1292
+ path.push_back(nodeId);
1293
+ return path;
1294
+ }
1295
+
1296
+ // Start with the target node
1297
+ path.push_back(nodeId);
1298
+
1299
+ // For now, simplified routing: if direct connection, add bridge
1300
+ // TODO: Implement proper multi-hop path discovery
1301
+ if (this->isConnected(primaryBridge->nodeId)) {
1302
+ path.push_back(primaryBridge->nodeId);
1303
+ }
1304
+
1305
+ return path;
1306
+ }
1307
+
1308
+ /**
1309
+ * Get bridge node ID for specific node
1310
+ *
1311
+ * Returns the bridge node ID that the specified node should use to reach Internet.
1312
+ * For most cases, this is the primary bridge.
1313
+ *
1314
+ * \code
1315
+ * uint32_t bridgeId = mesh.getBridgeForNodeId(targetNodeId);
1316
+ * if (bridgeId != 0) {
1317
+ * Serial.printf("Node %u uses bridge %u\n", targetNodeId, bridgeId);
1318
+ * }
1319
+ * \endcode
1320
+ *
1321
+ * @param nodeId Node to find bridge for
1322
+ * @return Bridge node ID, or 0 if no bridge available
1323
+ */
1324
+ uint32_t getBridgeForNodeId(uint32_t nodeId) {
1325
+ auto primaryBridge = this->getPrimaryBridge();
1326
+ if (primaryBridge != nullptr) {
1327
+ return primaryBridge->nodeId;
1328
+ }
1329
+ return 0;
1330
+ }
1331
+
1332
+ /**
1333
+ * Export topology as DOT format (Graphviz)
1334
+ *
1335
+ * Generates a GraphViz DOT format representation of the mesh topology.
1336
+ * Can be visualized using Graphviz tools.
1337
+ *
1338
+ * \code
1339
+ * String dot = mesh.exportTopologyDOT();
1340
+ * Serial.println(dot);
1341
+ * // Save to file or send to visualization tool
1342
+ * \endcode
1343
+ *
1344
+ * @return String containing DOT format graph
1345
+ */
1346
+ TSTRING exportTopologyDOT() {
1347
+ TSTRING dot = "digraph mesh {\n";
1348
+ dot += " rankdir=TB;\n";
1349
+ dot += " node [shape=box];\n\n";
1350
+
1351
+ // Add this node
1352
+ dot += " \"" + TSTRING(std::to_string(this->nodeId).c_str()) + "\" ";
1353
+ if (this->isBridge()) {
1354
+ dot += "[style=filled,fillcolor=lightblue,label=\"" + TSTRING(std::to_string(this->nodeId).c_str()) + "\\nBridge\"];\n";
1355
+ } else {
1356
+ dot += "[label=\"" + TSTRING(std::to_string(this->nodeId).c_str()) + "\"];\n";
1357
+ }
1358
+
1359
+ // Add Internet node if bridge exists
1360
+ auto primaryBridge = this->getPrimaryBridge();
1361
+ if (primaryBridge != nullptr && primaryBridge->internetConnected) {
1362
+ dot += " \"Internet\" [shape=cloud,style=filled,fillcolor=lightgreen];\n";
1363
+ dot += " \"" + TSTRING(std::to_string(primaryBridge->nodeId).c_str()) + "\" -> \"Internet\" [style=dashed,color=green];\n";
1364
+ }
1365
+
1366
+ // Add all known nodes and connections
1367
+ auto nodeList = this->getNodeList(false);
1368
+ for (auto node : nodeList) {
1369
+ dot += " \"" + TSTRING(std::to_string(node).c_str()) + "\";\n";
1370
+ }
1371
+
1372
+ // Add edges for direct connections
1373
+ for (auto conn : this->subs) {
1374
+ if (conn->connected()) {
1375
+ dot += " \"" + TSTRING(std::to_string(this->nodeId).c_str()) + "\" -> \"" + TSTRING(std::to_string(conn->nodeId).c_str()) + "\"";
1376
+
1377
+ // Add edge labels with latency
1378
+ int latency = conn->getLatency();
1379
+ if (latency >= 0) {
1380
+ dot += " [label=\"" + TSTRING(std::to_string(latency).c_str()) + "ms\"]";
1381
+ }
1382
+ dot += ";\n";
1383
+ }
1384
+ }
1385
+
1386
+ dot += "}\n";
1387
+ return dot;
1388
+ }
1389
+
1390
+ /**
1391
+ * Test bridge connectivity
1392
+ *
1393
+ * Runs a connectivity test to the primary bridge and optionally to Internet.
1394
+ * Measures latency and reachability.
1395
+ *
1396
+ * \code
1397
+ * auto result = mesh.testBridgeConnectivity();
1398
+ * if (result.success) {
1399
+ * Serial.printf("Bridge test passed: %s (latency: %u ms)\n",
1400
+ * result.message.c_str(), result.latencyMs);
1401
+ * } else {
1402
+ * Serial.printf("Bridge test failed: %s\n", result.message.c_str());
1403
+ * }
1404
+ * \endcode
1405
+ *
1406
+ * @return BridgeTestResult with test results
1407
+ */
1408
+ BridgeTestResult testBridgeConnectivity() {
1409
+ BridgeTestResult result;
1410
+
1411
+ // Check if we have a bridge
1412
+ auto primaryBridge = this->getPrimaryBridge();
1413
+ if (primaryBridge == nullptr) {
1414
+ result.success = false;
1415
+ result.message = "No bridge available";
1416
+ return result;
1417
+ }
1418
+
1419
+ // Check if bridge is reachable
1420
+ result.bridgeReachable = this->isConnected(primaryBridge->nodeId);
1421
+ if (!result.bridgeReachable) {
1422
+ result.success = false;
1423
+ result.message = "Bridge not reachable";
1424
+ return result;
1425
+ }
1426
+
1427
+ // Estimate latency from connection info
1428
+ for (auto conn : this->subs) {
1429
+ if (conn->nodeId == primaryBridge->nodeId && conn->connected()) {
1430
+ int latency = conn->getLatency();
1431
+ if (latency >= 0) {
1432
+ result.latencyMs = latency;
1433
+ }
1434
+ break;
1435
+ }
1436
+ }
1437
+
1438
+ // Check Internet connectivity through bridge
1439
+ result.internetReachable = primaryBridge->internetConnected;
1440
+
1441
+ result.success = result.bridgeReachable;
1442
+ if (result.internetReachable) {
1443
+ result.message = "Bridge reachable with Internet";
1444
+ } else {
1445
+ result.message = "Bridge reachable, no Internet";
1446
+ }
1447
+
1448
+ return result;
1449
+ }
1450
+
1451
+ /**
1452
+ * Check if specific bridge is reachable
1453
+ *
1454
+ * Tests if the specified bridge node can be reached from this node.
1455
+ *
1456
+ * \code
1457
+ * if (mesh.isBridgeReachable(bridgeNodeId)) {
1458
+ * Serial.println("Bridge is reachable");
1459
+ * }
1460
+ * \endcode
1461
+ *
1462
+ * @param bridgeNodeId Bridge node ID to test
1463
+ * @return true if bridge is reachable, false otherwise
1464
+ */
1465
+ bool isBridgeReachable(uint32_t bridgeNodeId) {
1466
+ return this->isConnected(bridgeNodeId);
1467
+ }
1468
+
1469
+ /**
1470
+ * Get comprehensive diagnostic report
1471
+ *
1472
+ * Generates a human-readable diagnostic report with mesh status, bridge info,
1473
+ * connectivity, and performance metrics. Useful for debugging and monitoring.
1474
+ *
1475
+ * \code
1476
+ * Serial.println(mesh.getDiagnosticReport());
1477
+ * // Example output:
1478
+ * // === painlessMesh Diagnostics ===
1479
+ * // Mode: Regular Node
1480
+ * // Mesh: ProductionMesh (25 nodes)
1481
+ * // Bridge: 123456789 (RSSI: -42 dBm, Internet: ✓)
1482
+ * // Queue: 3 messages (2 CRITICAL, 1 NORMAL)
1483
+ * // Uptime: 02:15:33
1484
+ * // Last Election: 00:45:12 ago (Winner: 123456789)
1485
+ * \endcode
1486
+ *
1487
+ * @return String containing formatted diagnostic report
1488
+ */
1489
+ TSTRING getDiagnosticReport() {
1490
+ TSTRING report = "=== painlessMesh Diagnostics ===\n";
1491
+
1492
+ // Node info
1493
+ auto status = this->getBridgeStatus();
1494
+ report += "Node ID: " + TSTRING(std::to_string(this->nodeId).c_str()) + "\n";
1495
+ report += "Mode: " + status.role + "\n";
1496
+
1497
+ // Mesh info
1498
+ auto nodeList = this->getNodeList(true);
1499
+ report += "Mesh Nodes: " + TSTRING(std::to_string(nodeList.size()).c_str()) + "\n";
1500
+
1501
+ // Bridge info
1502
+ if (status.isBridge) {
1503
+ report += "Bridge: " + TSTRING(std::to_string(this->nodeId).c_str()) + " (this node)\n";
1504
+ } else {
1505
+ auto primaryBridge = this->getPrimaryBridge();
1506
+ if (primaryBridge != nullptr) {
1507
+ report += "Bridge: " + TSTRING(std::to_string(primaryBridge->nodeId).c_str());
1508
+ report += " (RSSI: " + TSTRING(std::to_string(primaryBridge->routerRSSI).c_str()) + " dBm";
1509
+ report += ", Internet: " + TSTRING(primaryBridge->internetConnected ? "✓" : "✗") + ")\n";
1510
+ } else {
1511
+ report += "Bridge: None available\n";
1512
+ }
1513
+ }
1514
+
1515
+ // Connection info
1516
+ report += "Direct Connections: " + TSTRING(std::to_string(this->subs.size()).c_str()) + "\n";
1517
+
1518
+ // Health metrics
1519
+ auto metrics = this->getBridgeHealthMetrics();
1520
+ report += "Messages RX: " + TSTRING(std::to_string(metrics.messagesRx).c_str()) + "\n";
1521
+ report += "Messages TX: " + TSTRING(std::to_string(metrics.messagesTx).c_str()) + "\n";
1522
+ report += "Messages Dropped: " + TSTRING(std::to_string(metrics.messagesDropped).c_str()) + "\n";
1523
+
1524
+ if (metrics.avgLatencyMs > 0) {
1525
+ report += "Avg Latency: " + TSTRING(std::to_string(metrics.avgLatencyMs).c_str()) + " ms\n";
1526
+ }
1527
+
1528
+ // Uptime
1529
+ uint32_t uptimeSeconds = millis() / 1000;
1530
+ uint32_t hours = uptimeSeconds / 3600;
1531
+ uint32_t minutes = (uptimeSeconds % 3600) / 60;
1532
+ uint32_t seconds = uptimeSeconds % 60;
1533
+
1534
+ char uptimeStr[32];
1535
+ snprintf(uptimeStr, sizeof(uptimeStr), "%02u:%02u:%02u", hours, minutes, seconds);
1536
+ report += "Uptime: " + TSTRING(uptimeStr) + "\n";
1537
+
1538
+ // Election info
1539
+ if (diagnosticsEnabled && !electionHistory.empty()) {
1540
+ auto& lastElection = electionHistory.back();
1541
+ uint32_t timeSinceElection = millis() - lastElection.timestamp;
1542
+ uint32_t electionMinutes = (timeSinceElection / 1000) / 60;
1543
+ uint32_t electionSeconds = (timeSinceElection / 1000) % 60;
1544
+
1545
+ char electionTimeStr[32];
1546
+ snprintf(electionTimeStr, sizeof(electionTimeStr), "%02u:%02u", electionMinutes, electionSeconds);
1547
+
1548
+ report += "Last Election: " + TSTRING(electionTimeStr) + " ago";
1549
+ report += " (Winner: " + TSTRING(std::to_string(lastElection.winnerNodeId).c_str());
1550
+ report += ", " + TSTRING(std::to_string(lastElection.candidateCount).c_str()) + " candidates)\n";
1551
+ }
1552
+
1553
+ report += "================================\n";
1554
+ return report;
1555
+ }
1556
+
459
1557
  inline std::shared_ptr<Task> addTask(unsigned long aInterval,
460
1558
  long aIterations,
461
1559
  std::function<void()> aCallback) {
@@ -521,6 +1619,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
521
1619
  callback::List<uint32_t> changedConnectionCallbacks;
522
1620
  nodeTimeAdjustedCallback_t nodeTimeAdjustedCallback;
523
1621
  nodeDelayCallback_t nodeDelayReceivedCallback;
1622
+ bridgeStatusChangedCallback_t bridgeStatusChangedCallback;
1623
+ rtcSyncCompleteCallback_t rtcSyncCompleteCallback;
524
1624
  #ifdef ESP32
525
1625
  SemaphoreHandle_t xSemaphore = NULL;
526
1626
  #endif
@@ -558,6 +1658,25 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
558
1658
  #endif
559
1659
  }
560
1660
 
1661
+ // Bridge status tracking
1662
+ std::vector<BridgeInfo> knownBridges;
1663
+ uint32_t bridgeStatusIntervalMs = 30000; // Default 30 seconds
1664
+ uint32_t bridgeTimeoutMs = 60000; // Default 60 seconds
1665
+ bool bridgeStatusBroadcastEnabled = true;
1666
+
1667
+ // Health metrics tracking
1668
+ uint32_t metricsDisconnectCount = 0;
1669
+
1670
+ // RTC management
1671
+ rtc::RTCManager rtcManager;
1672
+
1673
+ // Diagnostics tracking
1674
+ bool diagnosticsEnabled = false;
1675
+ std::vector<ElectionRecord> electionHistory;
1676
+ static const size_t MAX_ELECTION_HISTORY = 10;
1677
+ BridgeChangeEvent lastBridgeChange;
1678
+ uint32_t lastBridgeChangeTime = 0;
1679
+
561
1680
  friend T;
562
1681
  friend void onDataCb(void *, AsyncClient *, void *, size_t);
563
1682
  friend void tcpSentCb(void *, AsyncClient *, size_t, uint32_t);
@@ -589,6 +1708,8 @@ class Connection : public painlessmesh::layout::Neighbour,
589
1708
  uint32_t messagesTx = 0;
590
1709
  uint32_t messagesDropped = 0;
591
1710
  uint32_t timeLastReceived = 0;
1711
+ uint64_t bytesRx = 0;
1712
+ uint64_t bytesTx = 0;
592
1713
 
593
1714
  // Latency tracking (rolling window)
594
1715
  std::vector<uint32_t> latencySamples;
@@ -660,19 +1781,21 @@ class Connection : public painlessmesh::layout::Neighbour,
660
1781
  }
661
1782
 
662
1783
  /**
663
- * Record message received timestamp
1784
+ * Record message received timestamp and bytes
664
1785
  */
665
- void onMessageReceived() {
1786
+ void onMessageReceived(size_t bytes = 0) {
666
1787
  messagesRx++;
1788
+ bytesRx += bytes;
667
1789
  timeLastReceived = millis();
668
1790
  }
669
1791
 
670
1792
  /**
671
- * Record message sent
1793
+ * Record message sent and bytes
672
1794
  */
673
- void onMessageSent(bool success) {
1795
+ void onMessageSent(bool success, size_t bytes = 0) {
674
1796
  if (success) {
675
1797
  messagesTx++;
1798
+ bytesTx += bytes;
676
1799
  } else {
677
1800
  messagesDropped++;
678
1801
  }