@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
@@ -7,9 +7,11 @@
7
7
 
8
8
  #include "painlessmesh/connection.hpp"
9
9
  #include "painlessmesh/logger.hpp"
10
+ #include "painlessmesh/message_queue.hpp"
10
11
  #include "painlessmesh/ntp.hpp"
11
12
  #include "painlessmesh/plugin.hpp"
12
13
  #include "painlessmesh/protocol.hpp"
14
+ #include "painlessmesh/rtc.hpp"
13
15
  #include "painlessmesh/tcp.hpp"
14
16
 
15
17
  #ifdef PAINLESSMESH_ENABLE_OTA
@@ -23,6 +25,119 @@ typedef std::function<void(uint32_t from, TSTRING &msg)> receivedCallback_t;
23
25
  typedef std::function<void()> changedConnectionsCallback_t;
24
26
  typedef std::function<void(int32_t offset)> nodeTimeAdjustedCallback_t;
25
27
  typedef std::function<void(uint32_t nodeId, int32_t delay)> nodeDelayCallback_t;
28
+ typedef std::function<void(uint32_t bridgeNodeId, bool internetAvailable)> bridgeStatusChangedCallback_t;
29
+ typedef std::function<void(uint32_t timestamp)> rtcSyncCompleteCallback_t;
30
+
31
+ /**
32
+ * Bridge information structure
33
+ *
34
+ * Tracks the status and health of bridge nodes in the mesh network
35
+ */
36
+ class BridgeInfo {
37
+ public:
38
+ uint32_t nodeId = 0; // Bridge node ID
39
+ bool internetConnected = false; // Is bridge connected to Internet?
40
+ int8_t routerRSSI = 0; // Router WiFi signal strength in dBm
41
+ uint8_t routerChannel = 0; // Router WiFi channel
42
+ uint32_t lastSeen = 0; // Timestamp when last status received (millis)
43
+ uint32_t uptime = 0; // Bridge uptime in milliseconds
44
+ TSTRING gatewayIP = ""; // Router gateway IP address
45
+ uint32_t timestamp = 0; // Timestamp from bridge status message
46
+
47
+ /**
48
+ * Check if this bridge is considered healthy
49
+ * A bridge is healthy if we've received a status update within the timeout period
50
+ */
51
+ bool isHealthy(uint32_t timeoutMs = 60000) const {
52
+ return (millis() - lastSeen) < timeoutMs;
53
+ }
54
+ };
55
+
56
+ /**
57
+ * Bridge Health Metrics structure
58
+ *
59
+ * Comprehensive metrics for monitoring bridge health, connectivity quality,
60
+ * and performance. Useful for troubleshooting and capacity planning.
61
+ */
62
+ struct BridgeHealthMetrics {
63
+ // Connectivity
64
+ uint32_t uptimeSeconds = 0;
65
+ uint32_t internetUptimeSeconds = 0;
66
+ uint32_t totalDisconnects = 0;
67
+ uint32_t currentUptime = 0;
68
+
69
+ // Signal Quality
70
+ int8_t currentRSSI = 0;
71
+ int8_t avgRSSI = 0;
72
+ int8_t minRSSI = 0;
73
+ int8_t maxRSSI = -127;
74
+
75
+ // Traffic
76
+ uint64_t bytesRx = 0;
77
+ uint64_t bytesTx = 0;
78
+ uint32_t messagesRx = 0;
79
+ uint32_t messagesTx = 0;
80
+ uint32_t messagesQueued = 0;
81
+ uint32_t messagesDropped = 0;
82
+
83
+ // Performance
84
+ uint32_t avgLatencyMs = 0;
85
+ uint8_t packetLossPercent = 0;
86
+ uint32_t meshNodeCount = 0;
87
+ };
88
+
89
+ /**
90
+ * Bridge Status structure
91
+ *
92
+ * Current status of this node's bridge role and connectivity
93
+ */
94
+ struct BridgeStatus {
95
+ bool isBridge = false; // Is this node acting as a bridge?
96
+ bool internetConnected = false; // Is Internet connection available?
97
+ TSTRING role = "regular"; // Role: "regular", "bridge", "root"
98
+ uint32_t bridgeNodeId = 0; // Current bridge node ID (0 if none)
99
+ int8_t bridgeRSSI = 0; // Signal strength to bridge/router (dBm)
100
+ uint32_t timeSinceBridgeChange = 0; // Time since last bridge change (ms)
101
+ };
102
+
103
+ /**
104
+ * Election Record structure
105
+ *
106
+ * Records information about bridge election events
107
+ */
108
+ struct ElectionRecord {
109
+ uint32_t timestamp = 0; // When election occurred (millis)
110
+ uint32_t winnerNodeId = 0; // Node that won election
111
+ int8_t winnerRSSI = 0; // Winner's router RSSI
112
+ uint32_t candidateCount = 0; // Number of candidates
113
+ TSTRING reason = ""; // Why election was triggered
114
+ };
115
+
116
+ /**
117
+ * Bridge Change Event structure
118
+ *
119
+ * Records the last bridge change event
120
+ */
121
+ struct BridgeChangeEvent {
122
+ uint32_t timestamp = 0; // When change occurred (millis)
123
+ uint32_t oldBridgeId = 0; // Previous bridge node ID
124
+ uint32_t newBridgeId = 0; // New bridge node ID
125
+ TSTRING reason = ""; // Reason for change
126
+ bool internetAvailable = false; // Internet available after change
127
+ };
128
+
129
+ /**
130
+ * Bridge Connectivity Test Result
131
+ *
132
+ * Results from bridge connectivity testing
133
+ */
134
+ struct BridgeTestResult {
135
+ bool success = false; // Overall test success
136
+ bool bridgeReachable = false; // Can reach bridge node
137
+ bool internetReachable = false; // Can reach Internet (if bridge available)
138
+ uint32_t latencyMs = 0; // Round-trip latency to bridge
139
+ TSTRING message = ""; // Detailed test message
140
+ };
26
141
 
27
142
  /**
28
143
  * Main api class for the mesh
@@ -49,6 +164,38 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
49
164
  this->callbackList = painlessmesh::router::addPackageCallback(
50
165
  std::move(this->callbackList), (*this));
51
166
 
167
+ // Add bridge status package handler (Type 610)
168
+ // This will be called when any node receives a bridge status broadcast
169
+ this->callbackList.onPackage(
170
+ 610, // BRIDGE_STATUS type
171
+ [this](protocol::Variant& variant, std::shared_ptr<T>, uint32_t) {
172
+ // We need to manually parse the JSON since BridgeStatusPackage is in alteriom namespace
173
+ // and may not be available in all contexts. We'll parse the critical fields directly.
174
+ JsonDocument doc;
175
+ TSTRING str;
176
+ variant.printTo(str);
177
+ deserializeJson(doc, str);
178
+ JsonObject obj = doc.as<JsonObject>();
179
+
180
+ if (obj["internetConnected"].is<bool>()) {
181
+ uint32_t bridgeNodeId = obj["from"];
182
+ bool internetConnected = obj["internetConnected"];
183
+ int8_t routerRSSI = obj["routerRSSI"] | 0;
184
+ uint8_t routerChannel = obj["routerChannel"] | 0;
185
+ uint32_t uptime = obj["uptime"] | 0;
186
+ TSTRING gatewayIP = obj["gatewayIP"].as<TSTRING>();
187
+ uint32_t timestamp = obj["timestamp"] | 0;
188
+
189
+ // Update bridge status
190
+ this->updateBridgeStatus(bridgeNodeId, internetConnected, routerRSSI,
191
+ routerChannel, uptime, gatewayIP, timestamp);
192
+
193
+ Log(GENERAL, "Bridge status received from %u: Internet %s\n",
194
+ bridgeNodeId, internetConnected ? "Connected" : "Disconnected");
195
+ }
196
+ return false; // Don't consume the package, allow other handlers
197
+ });
198
+
52
199
  this->changedConnectionCallbacks.push_back([this](uint32_t nodeId) {
53
200
  Log(MESH_STATUS, "Changed connections in neighbour %u\n", nodeId);
54
201
  if (nodeId != 0) layout::syncLayout<T>((*this), nodeId);
@@ -56,6 +203,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
56
203
  this->droppedConnectionCallbacks.push_back([this](uint32_t nodeId,
57
204
  bool station) {
58
205
  Log(MESH_STATUS, "Dropped connection %u, station %d\n", nodeId, station);
206
+ this->metricsDisconnectCount++;
59
207
  this->eraseClosedConnections();
60
208
  });
61
209
  this->newConnectionCallbacks.push_back([](uint32_t nodeId) {
@@ -340,6 +488,523 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
340
488
  nodeDelayReceivedCallback = onDelayReceived;
341
489
  }
342
490
 
491
+ /** Callback that gets called when bridge status changes.
492
+ *
493
+ * This fires when a bridge node reports a change in Internet connectivity status.
494
+ * Useful for implementing failover logic or message queueing.
495
+ *
496
+ * \code
497
+ * mesh.onBridgeStatusChanged([](auto bridgeNodeId, auto hasInternet) {
498
+ * if (hasInternet) {
499
+ * Serial.println("Internet available - sending queued data");
500
+ * } else {
501
+ * Serial.println("Internet offline - queueing messages");
502
+ * }
503
+ * });
504
+ * \endcode
505
+ */
506
+ void onBridgeStatusChanged(bridgeStatusChangedCallback_t onBridgeStatusChanged) {
507
+ Log(logger::GENERAL, "onBridgeStatusChanged():\n");
508
+ bridgeStatusChangedCallback = onBridgeStatusChanged;
509
+ }
510
+
511
+ /**
512
+ * Check if any bridge in the mesh has Internet connectivity
513
+ *
514
+ * @return true if at least one healthy bridge reports Internet connection
515
+ */
516
+ bool hasInternetConnection() {
517
+ for (const auto& bridge : knownBridges) {
518
+ if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
519
+ return true;
520
+ }
521
+ }
522
+ return false;
523
+ }
524
+
525
+ /**
526
+ * Get list of all known bridges in the mesh
527
+ *
528
+ * @return vector of BridgeInfo objects for all tracked bridges
529
+ */
530
+ std::vector<BridgeInfo> getBridges() {
531
+ return knownBridges;
532
+ }
533
+
534
+ /**
535
+ * Get the primary (best) bridge node
536
+ *
537
+ * Primary bridge is selected based on:
538
+ * 1. Must be healthy (seen within timeout)
539
+ * 2. Must have Internet connection
540
+ * 3. Best WiFi RSSI to router
541
+ *
542
+ * @return pointer to BridgeInfo of primary bridge, or nullptr if no suitable bridge
543
+ */
544
+ BridgeInfo* getPrimaryBridge() {
545
+ BridgeInfo* primary = nullptr;
546
+ int8_t bestRSSI = -127; // Worst possible RSSI
547
+
548
+ for (auto& bridge : knownBridges) {
549
+ if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
550
+ if (bridge.routerRSSI > bestRSSI) {
551
+ bestRSSI = bridge.routerRSSI;
552
+ primary = &bridge;
553
+ }
554
+ }
555
+ }
556
+
557
+ return primary;
558
+ }
559
+
560
+ /**
561
+ * Check if this node is acting as a bridge
562
+ *
563
+ * @return true if this node is configured as a root node (typically bridges)
564
+ */
565
+ bool isBridge() {
566
+ return this->root;
567
+ }
568
+
569
+ /**
570
+ * Set the interval for bridge status broadcasts (bridge nodes only)
571
+ *
572
+ * @param intervalMs Broadcast interval in milliseconds (default: 30000 = 30 seconds)
573
+ */
574
+ void setBridgeStatusInterval(uint32_t intervalMs) {
575
+ bridgeStatusIntervalMs = intervalMs;
576
+ }
577
+
578
+ /**
579
+ * Set the timeout for considering a bridge offline
580
+ *
581
+ * @param timeoutMs Timeout in milliseconds (default: 60000 = 60 seconds)
582
+ */
583
+ void setBridgeTimeout(uint32_t timeoutMs) {
584
+ bridgeTimeoutMs = timeoutMs;
585
+ }
586
+
587
+ /**
588
+ * Enable or disable bridge status broadcasting (bridge nodes only)
589
+ *
590
+ * @param enabled true to enable broadcasting (default), false to disable
591
+ */
592
+ void enableBridgeStatusBroadcast(bool enabled) {
593
+ bridgeStatusBroadcastEnabled = enabled;
594
+ }
595
+
596
+ /**
597
+ * Update bridge information from received status package
598
+ * Internal method called when bridge status is received
599
+ *
600
+ * @param bridgeNodeId ID of the bridge node
601
+ * @param internetConnected Internet connectivity status
602
+ * @param routerRSSI Router signal strength
603
+ * @param routerChannel Router WiFi channel
604
+ * @param uptime Bridge uptime
605
+ * @param gatewayIP Router gateway IP
606
+ * @param timestamp Status timestamp
607
+ */
608
+ void updateBridgeStatus(uint32_t bridgeNodeId, bool internetConnected,
609
+ int8_t routerRSSI, uint8_t routerChannel,
610
+ uint32_t uptime, TSTRING gatewayIP, uint32_t timestamp) {
611
+ // Find existing bridge or add new one
612
+ BridgeInfo* bridge = nullptr;
613
+ uint32_t oldPrimaryBridgeId = 0;
614
+ auto oldPrimary = this->getPrimaryBridge();
615
+ if (oldPrimary != nullptr) {
616
+ oldPrimaryBridgeId = oldPrimary->nodeId;
617
+ }
618
+
619
+ for (auto& b : knownBridges) {
620
+ if (b.nodeId == bridgeNodeId) {
621
+ bridge = &b;
622
+ break;
623
+ }
624
+ }
625
+
626
+ bool wasConnected = false;
627
+ bool isNewBridge = false;
628
+ if (bridge == nullptr) {
629
+ // New bridge - add to list
630
+ BridgeInfo newBridge;
631
+ newBridge.nodeId = bridgeNodeId;
632
+ knownBridges.push_back(newBridge);
633
+ bridge = &knownBridges.back();
634
+ isNewBridge = true;
635
+ } else {
636
+ wasConnected = bridge->internetConnected;
637
+ }
638
+
639
+ // Update bridge info
640
+ bridge->internetConnected = internetConnected;
641
+ bridge->routerRSSI = routerRSSI;
642
+ bridge->routerChannel = routerChannel;
643
+ bridge->lastSeen = millis();
644
+ bridge->uptime = uptime;
645
+ bridge->gatewayIP = gatewayIP;
646
+ bridge->timestamp = timestamp;
647
+
648
+ // Check if primary bridge changed
649
+ auto newPrimary = this->getPrimaryBridge();
650
+ uint32_t newPrimaryBridgeId = (newPrimary != nullptr) ? newPrimary->nodeId : 0;
651
+
652
+ if (diagnosticsEnabled && oldPrimaryBridgeId != newPrimaryBridgeId) {
653
+ // Record bridge change
654
+ lastBridgeChange.timestamp = millis();
655
+ lastBridgeChange.oldBridgeId = oldPrimaryBridgeId;
656
+ lastBridgeChange.newBridgeId = newPrimaryBridgeId;
657
+ lastBridgeChange.internetAvailable = internetConnected;
658
+ if (isNewBridge) {
659
+ lastBridgeChange.reason = "New bridge discovered";
660
+ } else if (internetConnected && !wasConnected) {
661
+ lastBridgeChange.reason = "Bridge Internet restored";
662
+ } else if (!internetConnected && wasConnected) {
663
+ lastBridgeChange.reason = "Bridge Internet lost";
664
+ } else {
665
+ lastBridgeChange.reason = "Primary bridge changed";
666
+ }
667
+
668
+ lastBridgeChangeTime = millis();
669
+ Log(logger::GENERAL, "updateBridgeStatus(): Bridge change recorded\n");
670
+ }
671
+
672
+ // Trigger callback if status changed
673
+ if (wasConnected != internetConnected && bridgeStatusChangedCallback) {
674
+ bridgeStatusChangedCallback(bridgeNodeId, internetConnected);
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Enable RTC integration for offline timekeeping
680
+ *
681
+ * Allows nodes to maintain accurate timestamps even when Internet/bridge
682
+ * is unavailable. User must provide an implementation of RTCInterface
683
+ * for their specific RTC hardware.
684
+ *
685
+ * \code
686
+ * // Example with DS3231 RTC
687
+ * class MyRTC : public painlessmesh::rtc::RTCInterface {
688
+ * // ... implement interface methods ...
689
+ * };
690
+ * MyRTC myRTC;
691
+ * mesh.enableRTC(&myRTC);
692
+ * \endcode
693
+ *
694
+ * @param rtcInterface Pointer to user's RTC implementation
695
+ * @return true if RTC enabled successfully, false otherwise
696
+ */
697
+ bool enableRTC(rtc::RTCInterface* rtcInterface) {
698
+ using namespace logger;
699
+ Log(GENERAL, "enableRTC(): Initializing RTC\n");
700
+ return rtcManager.enable(rtcInterface);
701
+ }
702
+
703
+ /**
704
+ * Disable RTC integration
705
+ */
706
+ void disableRTC() {
707
+ using namespace logger;
708
+ Log(GENERAL, "disableRTC(): Disabling RTC\n");
709
+ rtcManager.disable();
710
+ }
711
+
712
+ /**
713
+ * Sync RTC from NTP/Internet time source
714
+ *
715
+ * Should be called when Internet connection is available to update
716
+ * the RTC with accurate time. Typically called in onBridgeStatusChanged
717
+ * callback when Internet becomes available.
718
+ *
719
+ * \code
720
+ * mesh.onBridgeStatusChanged([](auto bridgeNodeId, auto hasInternet) {
721
+ * if (hasInternet) {
722
+ * // Get NTP time and sync RTC
723
+ * uint32_t ntpTime = getNTPTime(); // User implements this
724
+ * if (mesh.syncRTCFromNTP(ntpTime)) {
725
+ * Serial.println("RTC synced successfully");
726
+ * }
727
+ * }
728
+ * });
729
+ * \endcode
730
+ *
731
+ * @param ntpTimestamp Unix timestamp from NTP source
732
+ * @return true if sync successful, false otherwise
733
+ */
734
+ bool syncRTCFromNTP(uint32_t ntpTimestamp) {
735
+ using namespace logger;
736
+ Log(GENERAL, "syncRTCFromNTP(): Syncing RTC to timestamp %u\n", ntpTimestamp);
737
+
738
+ if (!rtcManager.isEnabled()) {
739
+ Log(ERROR, "syncRTCFromNTP(): RTC not enabled\n");
740
+ return false;
741
+ }
742
+
743
+ bool success = rtcManager.syncFromNTP(ntpTimestamp);
744
+
745
+ if (success && rtcSyncCompleteCallback) {
746
+ rtcSyncCompleteCallback(ntpTimestamp);
747
+ }
748
+
749
+ return success;
750
+ }
751
+
752
+ /**
753
+ * Get accurate time with RTC fallback
754
+ *
755
+ * Returns time from RTC if available, otherwise falls back to mesh time.
756
+ * This provides the most accurate timestamp available to the node.
757
+ *
758
+ * @return Unix timestamp in seconds, or mesh time in microseconds if RTC unavailable
759
+ */
760
+ uint32_t getAccurateTime() {
761
+ if (rtcManager.isEnabled()) {
762
+ uint32_t rtcTime = rtcManager.getTime();
763
+ if (rtcTime > 0) {
764
+ return rtcTime;
765
+ }
766
+ }
767
+ // Fallback to mesh time (microseconds)
768
+ return this->getNodeTime();
769
+ }
770
+
771
+ /**
772
+ * Check if RTC is enabled and available
773
+ *
774
+ * @return true if RTC can be used, false otherwise
775
+ */
776
+ bool hasRTC() const {
777
+ return rtcManager.isEnabled();
778
+ }
779
+
780
+ /**
781
+ * Get RTC type
782
+ *
783
+ * @return RTCType enum value, or RTC_NONE if no RTC enabled
784
+ */
785
+ rtc::RTCType getRTCType() const {
786
+ return rtcManager.getType();
787
+ }
788
+
789
+ /**
790
+ * Get time since last RTC sync
791
+ *
792
+ * @return Milliseconds since last RTC sync, or 0 if never synced
793
+ */
794
+ uint32_t getTimeSinceRTCSync() const {
795
+ return rtcManager.getTimeSinceLastSync();
796
+ }
797
+
798
+ /**
799
+ * Callback when RTC sync completes
800
+ *
801
+ * This fires when syncRTCFromNTP() successfully updates the RTC.
802
+ *
803
+ * \code
804
+ * mesh.onRTCSyncComplete([](auto timestamp) {
805
+ * Serial.printf("RTC synced to: %u\n", timestamp);
806
+ * });
807
+ * \endcode
808
+ */
809
+ void onRTCSyncComplete(rtcSyncCompleteCallback_t onRTCSyncComplete) {
810
+ Log(logger::GENERAL, "onRTCSyncComplete():\n");
811
+ rtcSyncCompleteCallback = onRTCSyncComplete;
812
+ }
813
+
814
+ //
815
+ // Message Queue API
816
+ //
817
+
818
+ /**
819
+ * Enable or disable message queueing for offline mode
820
+ *
821
+ * When enabled, messages can be queued when Internet is unavailable
822
+ * and automatically flushed when connection is restored.
823
+ *
824
+ * @param enabled True to enable queueing, false to disable
825
+ * @param maxSize Maximum number of messages in queue (default 1000)
826
+ *
827
+ * \code
828
+ * mesh.enableMessageQueue(true, 500); // Enable with 500 message capacity
829
+ * \endcode
830
+ */
831
+ void enableMessageQueue(bool enabled, uint32_t maxSize = 1000) {
832
+ if (enabled && !messageQueue) {
833
+ messageQueue = new MessageQueue(maxSize);
834
+ Log(logger::GENERAL, "enableMessageQueue(): Queue enabled with capacity %u\n", maxSize);
835
+ } else if (!enabled && messageQueue) {
836
+ delete messageQueue;
837
+ messageQueue = nullptr;
838
+ Log(logger::GENERAL, "enableMessageQueue(): Queue disabled\n");
839
+ }
840
+ }
841
+
842
+ /**
843
+ * Queue a message with priority for later delivery
844
+ *
845
+ * Use this to queue critical messages when Internet is unavailable.
846
+ * Messages are automatically delivered when connection is restored.
847
+ *
848
+ * @param payload Message content to queue
849
+ * @param destination Optional destination metadata (e.g., MQTT topic, HTTP endpoint)
850
+ * @param priority Message priority (default: PRIORITY_NORMAL)
851
+ * @return Message ID if successfully queued, 0 if failed
852
+ *
853
+ * \code
854
+ * // Queue critical alarm
855
+ * uint32_t msgId = mesh.queueMessage(
856
+ * alarmData.toJSON(),
857
+ * "mqtt://cloud.example.com/alarms",
858
+ * PRIORITY_CRITICAL
859
+ * );
860
+ * \endcode
861
+ */
862
+ uint32_t queueMessage(const TSTRING& payload,
863
+ const TSTRING& destination = "",
864
+ MessagePriority priority = PRIORITY_NORMAL) {
865
+ if (!messageQueue) {
866
+ Log(logger::ERROR, "queueMessage(): Message queue not enabled\n");
867
+ return 0;
868
+ }
869
+
870
+ return messageQueue->enqueue(priority, payload, destination);
871
+ }
872
+
873
+ /**
874
+ * Flush all queued messages
875
+ *
876
+ * Attempts to send all queued messages. This is typically called
877
+ * automatically when Internet connection is restored, but can be
878
+ * called manually.
879
+ *
880
+ * Note: This returns the messages for the application to send.
881
+ * The application is responsible for actually transmitting them
882
+ * and calling removeQueuedMessage() when successful.
883
+ *
884
+ * @return Vector of queued messages to send
885
+ *
886
+ * \code
887
+ * auto messages = mesh.flushMessageQueue();
888
+ * for (auto& msg : messages) {
889
+ * if (sendToCloud(msg.payload, msg.destination)) {
890
+ * mesh.removeQueuedMessage(msg.id);
891
+ * }
892
+ * }
893
+ * \endcode
894
+ */
895
+ std::vector<QueuedMessage> flushMessageQueue() {
896
+ if (!messageQueue) {
897
+ return std::vector<QueuedMessage>();
898
+ }
899
+
900
+ return messageQueue->getMessages();
901
+ }
902
+
903
+ /**
904
+ * Remove a successfully sent message from the queue
905
+ *
906
+ * @param messageId ID of message to remove
907
+ * @return true if message was found and removed
908
+ */
909
+ bool removeQueuedMessage(uint32_t messageId) {
910
+ if (!messageQueue) {
911
+ return false;
912
+ }
913
+
914
+ return messageQueue->remove(messageId);
915
+ }
916
+
917
+ /**
918
+ * Increment send attempt counter for a message
919
+ *
920
+ * @param messageId ID of message
921
+ * @return New attempt count, or 0 if message not found
922
+ */
923
+ uint32_t incrementQueuedMessageAttempts(uint32_t messageId) {
924
+ if (!messageQueue) {
925
+ return 0;
926
+ }
927
+
928
+ return messageQueue->incrementAttempts(messageId);
929
+ }
930
+
931
+ /**
932
+ * Get number of queued messages
933
+ *
934
+ * @param priority Optional priority level to count (counts all if not specified)
935
+ * @return Number of queued messages
936
+ */
937
+ uint32_t getQueuedMessageCount(MessagePriority priority) {
938
+ if (!messageQueue) {
939
+ return 0;
940
+ }
941
+
942
+ return messageQueue->size(priority);
943
+ }
944
+
945
+ uint32_t getQueuedMessageCount() {
946
+ if (!messageQueue) {
947
+ return 0;
948
+ }
949
+
950
+ return messageQueue->size();
951
+ }
952
+
953
+ /**
954
+ * Get queue statistics
955
+ *
956
+ * @return QueueStats structure with detailed statistics
957
+ */
958
+ QueueStats getQueueStats() {
959
+ if (!messageQueue) {
960
+ return QueueStats();
961
+ }
962
+
963
+ return messageQueue->getStats();
964
+ }
965
+
966
+ /**
967
+ * Set callback for queue state changes
968
+ *
969
+ * Fires when queue state changes (EMPTY, NORMAL, 75%, FULL)
970
+ *
971
+ * \code
972
+ * mesh.onQueueStateChanged([](QueueState state, uint32_t count) {
973
+ * if (state == QUEUE_75_PERCENT) {
974
+ * Serial.printf("Warning: Queue 75%% full (%u messages)\n", count);
975
+ * }
976
+ * });
977
+ * \endcode
978
+ */
979
+ void onQueueStateChanged(queueStateChangedCallback_t callback) {
980
+ if (messageQueue) {
981
+ messageQueue->onStateChanged(callback);
982
+ }
983
+ }
984
+
985
+ /**
986
+ * Prune old messages from the queue
987
+ *
988
+ * @param maxAgeMs Maximum age in milliseconds
989
+ * @return Number of messages removed
990
+ */
991
+ uint32_t pruneQueue(uint32_t maxAgeMs) {
992
+ if (!messageQueue) {
993
+ return 0;
994
+ }
995
+
996
+ return messageQueue->pruneOldMessages(maxAgeMs);
997
+ }
998
+
999
+ /**
1000
+ * Clear all messages from the queue
1001
+ */
1002
+ void clearQueue() {
1003
+ if (messageQueue) {
1004
+ messageQueue->clear();
1005
+ }
1006
+ }
1007
+
343
1008
  /**
344
1009
  * Are we connected/know a route to the given node?
345
1010
  *
@@ -456,6 +1121,634 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
456
1121
  return table;
457
1122
  }
458
1123
 
1124
+ /**
1125
+ * Get bridge health metrics
1126
+ *
1127
+ * Collects comprehensive health metrics including connectivity, signal quality,
1128
+ * traffic, and performance data. Useful for monitoring and troubleshooting.
1129
+ *
1130
+ * \code
1131
+ * auto metrics = mesh.getBridgeHealthMetrics();
1132
+ * Serial.printf("Uptime: %u s, Messages RX: %u, Avg Latency: %u ms\n",
1133
+ * metrics.uptimeSeconds, metrics.messagesRx, metrics.avgLatencyMs);
1134
+ * \endcode
1135
+ *
1136
+ * @return BridgeHealthMetrics structure with current metrics
1137
+ */
1138
+ BridgeHealthMetrics getBridgeHealthMetrics() {
1139
+ BridgeHealthMetrics metrics;
1140
+
1141
+ // Connectivity metrics
1142
+ metrics.uptimeSeconds = millis() / 1000;
1143
+ metrics.currentUptime = millis();
1144
+
1145
+ // Check for primary bridge with Internet connection
1146
+ auto primaryBridge = getPrimaryBridge();
1147
+ if (primaryBridge != nullptr) {
1148
+ metrics.internetUptimeSeconds = (millis() - primaryBridge->lastSeen) / 1000;
1149
+ metrics.currentRSSI = primaryBridge->routerRSSI;
1150
+ }
1151
+
1152
+ // Aggregate traffic and performance from all connections
1153
+ uint64_t totalBytesRx = 0;
1154
+ uint64_t totalBytesTx = 0;
1155
+ uint32_t totalMessagesRx = 0;
1156
+ uint32_t totalMessagesTx = 0;
1157
+ uint32_t totalMessagesDropped = 0;
1158
+ uint32_t totalLatency = 0;
1159
+ uint32_t latencySampleCount = 0;
1160
+ int8_t sumRSSI = 0;
1161
+ int8_t minRSSI = 0;
1162
+ int8_t maxRSSI = -127;
1163
+ uint32_t rssiCount = 0;
1164
+
1165
+ for (auto conn : this->subs) {
1166
+ if (conn->connected()) {
1167
+ totalBytesRx += conn->bytesRx;
1168
+ totalBytesTx += conn->bytesTx;
1169
+ totalMessagesRx += conn->messagesRx;
1170
+ totalMessagesTx += conn->messagesTx;
1171
+ totalMessagesDropped += conn->messagesDropped;
1172
+
1173
+ // Latency
1174
+ int latency = conn->getLatency();
1175
+ if (latency >= 0) {
1176
+ totalLatency += latency;
1177
+ latencySampleCount++;
1178
+ }
1179
+
1180
+ // RSSI
1181
+ int rssi = conn->getRSSI();
1182
+ if (rssi != 0) {
1183
+ sumRSSI += rssi;
1184
+ rssiCount++;
1185
+ if (rssi < minRSSI || minRSSI == 0) minRSSI = rssi;
1186
+ if (rssi > maxRSSI) maxRSSI = rssi;
1187
+ }
1188
+ }
1189
+ }
1190
+
1191
+ // Set traffic metrics
1192
+ metrics.bytesRx = totalBytesRx;
1193
+ metrics.bytesTx = totalBytesTx;
1194
+ metrics.messagesRx = totalMessagesRx;
1195
+ metrics.messagesTx = totalMessagesTx;
1196
+ metrics.messagesDropped = totalMessagesDropped;
1197
+
1198
+ // Calculate averages
1199
+ if (latencySampleCount > 0) {
1200
+ metrics.avgLatencyMs = totalLatency / latencySampleCount;
1201
+ }
1202
+
1203
+ if (rssiCount > 0) {
1204
+ metrics.avgRSSI = sumRSSI / rssiCount;
1205
+ metrics.minRSSI = minRSSI;
1206
+ metrics.maxRSSI = maxRSSI;
1207
+ }
1208
+
1209
+ // Calculate packet loss percentage
1210
+ uint32_t totalAttempted = totalMessagesTx + totalMessagesDropped;
1211
+ if (totalAttempted > 0) {
1212
+ metrics.packetLossPercent = (totalMessagesDropped * 100) / totalAttempted;
1213
+ }
1214
+
1215
+ // Mesh node count
1216
+ metrics.meshNodeCount = this->getNodeList(true).size();
1217
+
1218
+ // Track disconnects (stored in protected member)
1219
+ metrics.totalDisconnects = metricsDisconnectCount;
1220
+
1221
+ return metrics;
1222
+ }
1223
+
1224
+ /**
1225
+ * Reset health metrics counters
1226
+ *
1227
+ * Resets all counters to zero but keeps current state metrics (RSSI, node count, etc.)
1228
+ * Useful for periodic monitoring windows.
1229
+ *
1230
+ * \code
1231
+ * // Reset metrics at the start of each monitoring period
1232
+ * mesh.resetHealthMetrics();
1233
+ * \endcode
1234
+ */
1235
+ void resetHealthMetrics() {
1236
+ using namespace logger;
1237
+ Log(GENERAL, "resetHealthMetrics(): Resetting all metric counters\n");
1238
+
1239
+ // Reset connection metrics
1240
+ for (auto conn : this->subs) {
1241
+ if (conn->connected()) {
1242
+ conn->messagesRx = 0;
1243
+ conn->messagesTx = 0;
1244
+ conn->messagesDropped = 0;
1245
+ conn->bytesRx = 0;
1246
+ conn->bytesTx = 0;
1247
+ conn->latencySamples.clear();
1248
+ }
1249
+ }
1250
+
1251
+ // Reset disconnect counter
1252
+ metricsDisconnectCount = 0;
1253
+ }
1254
+
1255
+ /**
1256
+ * Export health metrics as JSON string
1257
+ *
1258
+ * Generates a JSON representation of current health metrics for easy integration
1259
+ * with monitoring tools, MQTT publishing, or logging.
1260
+ *
1261
+ * \code
1262
+ * String json = mesh.getHealthMetricsJSON();
1263
+ * mqttClient.publish("bridge/metrics", json.c_str());
1264
+ * \endcode
1265
+ *
1266
+ * @return JSON string containing all health metrics
1267
+ */
1268
+ TSTRING getHealthMetricsJSON() {
1269
+ auto metrics = getBridgeHealthMetrics();
1270
+
1271
+ TSTRING json = "{";
1272
+ json += "\"connectivity\":{";
1273
+ json += "\"uptimeSeconds\":";
1274
+ json += std::to_string(metrics.uptimeSeconds);
1275
+ json += ",\"internetUptimeSeconds\":";
1276
+ json += std::to_string(metrics.internetUptimeSeconds);
1277
+ json += ",\"totalDisconnects\":";
1278
+ json += std::to_string(metrics.totalDisconnects);
1279
+ json += ",\"currentUptime\":";
1280
+ json += std::to_string(metrics.currentUptime);
1281
+ json += "},";
1282
+
1283
+ json += "\"signalQuality\":{";
1284
+ json += "\"currentRSSI\":";
1285
+ json += std::to_string(metrics.currentRSSI);
1286
+ json += ",\"avgRSSI\":";
1287
+ json += std::to_string(metrics.avgRSSI);
1288
+ json += ",\"minRSSI\":";
1289
+ json += std::to_string(metrics.minRSSI);
1290
+ json += ",\"maxRSSI\":";
1291
+ json += std::to_string(metrics.maxRSSI);
1292
+ json += "},";
1293
+
1294
+ json += "\"traffic\":{";
1295
+ json += "\"bytesRx\":";
1296
+ json += std::to_string(metrics.bytesRx);
1297
+ json += ",\"bytesTx\":";
1298
+ json += std::to_string(metrics.bytesTx);
1299
+ json += ",\"messagesRx\":";
1300
+ json += std::to_string(metrics.messagesRx);
1301
+ json += ",\"messagesTx\":";
1302
+ json += std::to_string(metrics.messagesTx);
1303
+ json += ",\"messagesQueued\":";
1304
+ json += std::to_string(metrics.messagesQueued);
1305
+ json += ",\"messagesDropped\":";
1306
+ json += std::to_string(metrics.messagesDropped);
1307
+ json += "},";
1308
+
1309
+ json += "\"performance\":{";
1310
+ json += "\"avgLatencyMs\":";
1311
+ json += std::to_string(metrics.avgLatencyMs);
1312
+ json += ",\"packetLossPercent\":";
1313
+ json += std::to_string(metrics.packetLossPercent);
1314
+ json += ",\"meshNodeCount\":";
1315
+ json += std::to_string(metrics.meshNodeCount);
1316
+ json += "}";
1317
+
1318
+ json += "}";
1319
+ return json;
1320
+ }
1321
+
1322
+ /**
1323
+ * Set periodic health metrics callback
1324
+ *
1325
+ * Registers a callback that will be invoked periodically with current health metrics.
1326
+ * Useful for automated monitoring, MQTT publishing, or Prometheus export.
1327
+ *
1328
+ * \code
1329
+ * mesh.onHealthMetricsUpdate([](BridgeHealthMetrics metrics) {
1330
+ * String json = mesh.getHealthMetricsJSON();
1331
+ * mqttClient.publish("bridge/metrics", json.c_str());
1332
+ * }, 60000); // Every 60 seconds
1333
+ * \endcode
1334
+ *
1335
+ * @param callback Function to call with metrics
1336
+ * @param intervalMs Callback interval in milliseconds (default: 60000 = 60 seconds)
1337
+ */
1338
+ void onHealthMetricsUpdate(std::function<void(BridgeHealthMetrics)> callback,
1339
+ uint32_t intervalMs = 60000) {
1340
+ using namespace logger;
1341
+ Log(GENERAL, "onHealthMetricsUpdate(): Setting up periodic callback every %u ms\n", intervalMs);
1342
+
1343
+ // Create a task that periodically collects and reports metrics
1344
+ auto metricsTask = this->addTask(intervalMs, TASK_FOREVER, [this, callback]() {
1345
+ auto metrics = this->getBridgeHealthMetrics();
1346
+ callback(metrics);
1347
+ });
1348
+
1349
+ metricsTask->enable();
1350
+ }
1351
+
1352
+ // ==================== Enhanced Diagnostics API ====================
1353
+
1354
+ /**
1355
+ * Enable or disable diagnostics collection
1356
+ *
1357
+ * When enabled, the mesh will track election history, bridge changes,
1358
+ * and other diagnostic information. This has minimal overhead.
1359
+ *
1360
+ * @param enabled true to enable diagnostics (default), false to disable
1361
+ */
1362
+ void enableDiagnostics(bool enabled = true) {
1363
+ diagnosticsEnabled = enabled;
1364
+ if (enabled) {
1365
+ Log(logger::GENERAL, "enableDiagnostics(): Diagnostics enabled\n");
1366
+ } else {
1367
+ Log(logger::GENERAL, "enableDiagnostics(): Diagnostics disabled\n");
1368
+ }
1369
+ }
1370
+
1371
+ /**
1372
+ * Get current bridge status
1373
+ *
1374
+ * Returns information about this node's bridge role and connectivity status
1375
+ *
1376
+ * \code
1377
+ * auto status = mesh.getBridgeStatus();
1378
+ * if (status.isBridge && status.internetConnected) {
1379
+ * Serial.println("I am bridge with Internet");
1380
+ * }
1381
+ * \endcode
1382
+ *
1383
+ * @return BridgeStatus structure with current status
1384
+ */
1385
+ BridgeStatus getBridgeStatus() {
1386
+ BridgeStatus status;
1387
+
1388
+ status.isBridge = this->isBridge();
1389
+ status.internetConnected = this->hasInternetConnection();
1390
+
1391
+ if (status.isBridge) {
1392
+ status.role = "bridge";
1393
+ } else if (this->root) {
1394
+ status.role = "root";
1395
+ } else {
1396
+ status.role = "regular";
1397
+ }
1398
+
1399
+ // Get primary bridge info
1400
+ auto primaryBridge = this->getPrimaryBridge();
1401
+ if (primaryBridge != nullptr) {
1402
+ status.bridgeNodeId = primaryBridge->nodeId;
1403
+ status.bridgeRSSI = primaryBridge->routerRSSI;
1404
+ }
1405
+
1406
+ // Calculate time since last bridge change
1407
+ if (lastBridgeChangeTime > 0) {
1408
+ status.timeSinceBridgeChange = millis() - lastBridgeChangeTime;
1409
+ }
1410
+
1411
+ return status;
1412
+ }
1413
+
1414
+ /**
1415
+ * Get election history
1416
+ *
1417
+ * Returns list of recent bridge elections if diagnostics are enabled.
1418
+ * History is limited to last 10 elections.
1419
+ *
1420
+ * \code
1421
+ * auto history = mesh.getElectionHistory();
1422
+ * for (const auto& record : history) {
1423
+ * Serial.printf("Election: winner=%u, RSSI=%d, candidates=%u\n",
1424
+ * record.winnerNodeId, record.winnerRSSI, record.candidateCount);
1425
+ * }
1426
+ * \endcode
1427
+ *
1428
+ * @return Vector of ElectionRecord structures
1429
+ */
1430
+ std::vector<ElectionRecord> getElectionHistory() {
1431
+ if (!diagnosticsEnabled) {
1432
+ Log(logger::GENERAL, "getElectionHistory(): Diagnostics not enabled\n");
1433
+ return std::vector<ElectionRecord>();
1434
+ }
1435
+ return electionHistory;
1436
+ }
1437
+
1438
+ /**
1439
+ * Get last bridge change event
1440
+ *
1441
+ * Returns information about the most recent bridge change
1442
+ *
1443
+ * \code
1444
+ * auto event = mesh.getLastBridgeChange();
1445
+ * if (event.timestamp > 0) {
1446
+ * Serial.printf("Bridge changed from %u to %u: %s\n",
1447
+ * event.oldBridgeId, event.newBridgeId, event.reason.c_str());
1448
+ * }
1449
+ * \endcode
1450
+ *
1451
+ * @return BridgeChangeEvent structure
1452
+ */
1453
+ BridgeChangeEvent getLastBridgeChange() {
1454
+ return lastBridgeChange;
1455
+ }
1456
+
1457
+ /**
1458
+ * Get Internet path to specific node
1459
+ *
1460
+ * Returns the routing path from the specified node to the Internet bridge.
1461
+ * Path includes all intermediate nodes from source to bridge.
1462
+ *
1463
+ * \code
1464
+ * auto path = mesh.getInternetPath(targetNodeId);
1465
+ * Serial.print("Path to Internet: ");
1466
+ * for (auto nodeId : path) {
1467
+ * Serial.printf("%u -> ", nodeId);
1468
+ * }
1469
+ * Serial.println("Internet");
1470
+ * \endcode
1471
+ *
1472
+ * @param nodeId Node to find path from
1473
+ * @return Vector of node IDs representing the path (empty if no path)
1474
+ */
1475
+ std::vector<uint32_t> getInternetPath(uint32_t nodeId) {
1476
+ std::vector<uint32_t> path;
1477
+
1478
+ // Get primary bridge
1479
+ auto primaryBridge = this->getPrimaryBridge();
1480
+ if (primaryBridge == nullptr) {
1481
+ Log(logger::GENERAL, "getInternetPath(): No bridge available\n");
1482
+ return path;
1483
+ }
1484
+
1485
+ // If requesting path for the bridge itself
1486
+ if (nodeId == primaryBridge->nodeId) {
1487
+ path.push_back(nodeId);
1488
+ return path;
1489
+ }
1490
+
1491
+ // Start with the target node
1492
+ path.push_back(nodeId);
1493
+
1494
+ // For now, simplified routing: if direct connection, add bridge
1495
+ // TODO: Implement proper multi-hop path discovery
1496
+ if (this->isConnected(primaryBridge->nodeId)) {
1497
+ path.push_back(primaryBridge->nodeId);
1498
+ }
1499
+
1500
+ return path;
1501
+ }
1502
+
1503
+ /**
1504
+ * Get bridge node ID for specific node
1505
+ *
1506
+ * Returns the bridge node ID that the specified node should use to reach Internet.
1507
+ * For most cases, this is the primary bridge.
1508
+ *
1509
+ * \code
1510
+ * uint32_t bridgeId = mesh.getBridgeForNodeId(targetNodeId);
1511
+ * if (bridgeId != 0) {
1512
+ * Serial.printf("Node %u uses bridge %u\n", targetNodeId, bridgeId);
1513
+ * }
1514
+ * \endcode
1515
+ *
1516
+ * @param nodeId Node to find bridge for
1517
+ * @return Bridge node ID, or 0 if no bridge available
1518
+ */
1519
+ uint32_t getBridgeForNodeId(uint32_t nodeId) {
1520
+ auto primaryBridge = this->getPrimaryBridge();
1521
+ if (primaryBridge != nullptr) {
1522
+ return primaryBridge->nodeId;
1523
+ }
1524
+ return 0;
1525
+ }
1526
+
1527
+ /**
1528
+ * Export topology as DOT format (Graphviz)
1529
+ *
1530
+ * Generates a GraphViz DOT format representation of the mesh topology.
1531
+ * Can be visualized using Graphviz tools.
1532
+ *
1533
+ * \code
1534
+ * String dot = mesh.exportTopologyDOT();
1535
+ * Serial.println(dot);
1536
+ * // Save to file or send to visualization tool
1537
+ * \endcode
1538
+ *
1539
+ * @return String containing DOT format graph
1540
+ */
1541
+ TSTRING exportTopologyDOT() {
1542
+ TSTRING dot = "digraph mesh {\n";
1543
+ dot += " rankdir=TB;\n";
1544
+ dot += " node [shape=box];\n\n";
1545
+
1546
+ // Add this node
1547
+ dot += " \"" + TSTRING(std::to_string(this->nodeId).c_str()) + "\" ";
1548
+ if (this->isBridge()) {
1549
+ dot += "[style=filled,fillcolor=lightblue,label=\"" + TSTRING(std::to_string(this->nodeId).c_str()) + "\\nBridge\"];\n";
1550
+ } else {
1551
+ dot += "[label=\"" + TSTRING(std::to_string(this->nodeId).c_str()) + "\"];\n";
1552
+ }
1553
+
1554
+ // Add Internet node if bridge exists
1555
+ auto primaryBridge = this->getPrimaryBridge();
1556
+ if (primaryBridge != nullptr && primaryBridge->internetConnected) {
1557
+ dot += " \"Internet\" [shape=cloud,style=filled,fillcolor=lightgreen];\n";
1558
+ dot += " \"" + TSTRING(std::to_string(primaryBridge->nodeId).c_str()) + "\" -> \"Internet\" [style=dashed,color=green];\n";
1559
+ }
1560
+
1561
+ // Add all known nodes and connections
1562
+ auto nodeList = this->getNodeList(false);
1563
+ for (auto node : nodeList) {
1564
+ dot += " \"" + TSTRING(std::to_string(node).c_str()) + "\";\n";
1565
+ }
1566
+
1567
+ // Add edges for direct connections
1568
+ for (auto conn : this->subs) {
1569
+ if (conn->connected()) {
1570
+ dot += " \"" + TSTRING(std::to_string(this->nodeId).c_str()) + "\" -> \"" + TSTRING(std::to_string(conn->nodeId).c_str()) + "\"";
1571
+
1572
+ // Add edge labels with latency
1573
+ int latency = conn->getLatency();
1574
+ if (latency >= 0) {
1575
+ dot += " [label=\"" + TSTRING(std::to_string(latency).c_str()) + "ms\"]";
1576
+ }
1577
+ dot += ";\n";
1578
+ }
1579
+ }
1580
+
1581
+ dot += "}\n";
1582
+ return dot;
1583
+ }
1584
+
1585
+ /**
1586
+ * Test bridge connectivity
1587
+ *
1588
+ * Runs a connectivity test to the primary bridge and optionally to Internet.
1589
+ * Measures latency and reachability.
1590
+ *
1591
+ * \code
1592
+ * auto result = mesh.testBridgeConnectivity();
1593
+ * if (result.success) {
1594
+ * Serial.printf("Bridge test passed: %s (latency: %u ms)\n",
1595
+ * result.message.c_str(), result.latencyMs);
1596
+ * } else {
1597
+ * Serial.printf("Bridge test failed: %s\n", result.message.c_str());
1598
+ * }
1599
+ * \endcode
1600
+ *
1601
+ * @return BridgeTestResult with test results
1602
+ */
1603
+ BridgeTestResult testBridgeConnectivity() {
1604
+ BridgeTestResult result;
1605
+
1606
+ // Check if we have a bridge
1607
+ auto primaryBridge = this->getPrimaryBridge();
1608
+ if (primaryBridge == nullptr) {
1609
+ result.success = false;
1610
+ result.message = "No bridge available";
1611
+ return result;
1612
+ }
1613
+
1614
+ // Check if bridge is reachable
1615
+ result.bridgeReachable = this->isConnected(primaryBridge->nodeId);
1616
+ if (!result.bridgeReachable) {
1617
+ result.success = false;
1618
+ result.message = "Bridge not reachable";
1619
+ return result;
1620
+ }
1621
+
1622
+ // Estimate latency from connection info
1623
+ for (auto conn : this->subs) {
1624
+ if (conn->nodeId == primaryBridge->nodeId && conn->connected()) {
1625
+ int latency = conn->getLatency();
1626
+ if (latency >= 0) {
1627
+ result.latencyMs = latency;
1628
+ }
1629
+ break;
1630
+ }
1631
+ }
1632
+
1633
+ // Check Internet connectivity through bridge
1634
+ result.internetReachable = primaryBridge->internetConnected;
1635
+
1636
+ result.success = result.bridgeReachable;
1637
+ if (result.internetReachable) {
1638
+ result.message = "Bridge reachable with Internet";
1639
+ } else {
1640
+ result.message = "Bridge reachable, no Internet";
1641
+ }
1642
+
1643
+ return result;
1644
+ }
1645
+
1646
+ /**
1647
+ * Check if specific bridge is reachable
1648
+ *
1649
+ * Tests if the specified bridge node can be reached from this node.
1650
+ *
1651
+ * \code
1652
+ * if (mesh.isBridgeReachable(bridgeNodeId)) {
1653
+ * Serial.println("Bridge is reachable");
1654
+ * }
1655
+ * \endcode
1656
+ *
1657
+ * @param bridgeNodeId Bridge node ID to test
1658
+ * @return true if bridge is reachable, false otherwise
1659
+ */
1660
+ bool isBridgeReachable(uint32_t bridgeNodeId) {
1661
+ return this->isConnected(bridgeNodeId);
1662
+ }
1663
+
1664
+ /**
1665
+ * Get comprehensive diagnostic report
1666
+ *
1667
+ * Generates a human-readable diagnostic report with mesh status, bridge info,
1668
+ * connectivity, and performance metrics. Useful for debugging and monitoring.
1669
+ *
1670
+ * \code
1671
+ * Serial.println(mesh.getDiagnosticReport());
1672
+ * // Example output:
1673
+ * // === painlessMesh Diagnostics ===
1674
+ * // Mode: Regular Node
1675
+ * // Mesh: ProductionMesh (25 nodes)
1676
+ * // Bridge: 123456789 (RSSI: -42 dBm, Internet: ✓)
1677
+ * // Queue: 3 messages (2 CRITICAL, 1 NORMAL)
1678
+ * // Uptime: 02:15:33
1679
+ * // Last Election: 00:45:12 ago (Winner: 123456789)
1680
+ * \endcode
1681
+ *
1682
+ * @return String containing formatted diagnostic report
1683
+ */
1684
+ TSTRING getDiagnosticReport() {
1685
+ TSTRING report = "=== painlessMesh Diagnostics ===\n";
1686
+
1687
+ // Node info
1688
+ auto status = this->getBridgeStatus();
1689
+ report += "Node ID: " + TSTRING(std::to_string(this->nodeId).c_str()) + "\n";
1690
+ report += "Mode: " + status.role + "\n";
1691
+
1692
+ // Mesh info
1693
+ auto nodeList = this->getNodeList(true);
1694
+ report += "Mesh Nodes: " + TSTRING(std::to_string(nodeList.size()).c_str()) + "\n";
1695
+
1696
+ // Bridge info
1697
+ if (status.isBridge) {
1698
+ report += "Bridge: " + TSTRING(std::to_string(this->nodeId).c_str()) + " (this node)\n";
1699
+ } else {
1700
+ auto primaryBridge = this->getPrimaryBridge();
1701
+ if (primaryBridge != nullptr) {
1702
+ report += "Bridge: " + TSTRING(std::to_string(primaryBridge->nodeId).c_str());
1703
+ report += " (RSSI: " + TSTRING(std::to_string(primaryBridge->routerRSSI).c_str()) + " dBm";
1704
+ report += ", Internet: " + TSTRING(primaryBridge->internetConnected ? "✓" : "✗") + ")\n";
1705
+ } else {
1706
+ report += "Bridge: None available\n";
1707
+ }
1708
+ }
1709
+
1710
+ // Connection info
1711
+ report += "Direct Connections: " + TSTRING(std::to_string(this->subs.size()).c_str()) + "\n";
1712
+
1713
+ // Health metrics
1714
+ auto metrics = this->getBridgeHealthMetrics();
1715
+ report += "Messages RX: " + TSTRING(std::to_string(metrics.messagesRx).c_str()) + "\n";
1716
+ report += "Messages TX: " + TSTRING(std::to_string(metrics.messagesTx).c_str()) + "\n";
1717
+ report += "Messages Dropped: " + TSTRING(std::to_string(metrics.messagesDropped).c_str()) + "\n";
1718
+
1719
+ if (metrics.avgLatencyMs > 0) {
1720
+ report += "Avg Latency: " + TSTRING(std::to_string(metrics.avgLatencyMs).c_str()) + " ms\n";
1721
+ }
1722
+
1723
+ // Uptime
1724
+ uint32_t uptimeSeconds = millis() / 1000;
1725
+ uint32_t hours = uptimeSeconds / 3600;
1726
+ uint32_t minutes = (uptimeSeconds % 3600) / 60;
1727
+ uint32_t seconds = uptimeSeconds % 60;
1728
+
1729
+ char uptimeStr[32];
1730
+ snprintf(uptimeStr, sizeof(uptimeStr), "%02u:%02u:%02u", hours, minutes, seconds);
1731
+ report += "Uptime: " + TSTRING(uptimeStr) + "\n";
1732
+
1733
+ // Election info
1734
+ if (diagnosticsEnabled && !electionHistory.empty()) {
1735
+ auto& lastElection = electionHistory.back();
1736
+ uint32_t timeSinceElection = millis() - lastElection.timestamp;
1737
+ uint32_t electionMinutes = (timeSinceElection / 1000) / 60;
1738
+ uint32_t electionSeconds = (timeSinceElection / 1000) % 60;
1739
+
1740
+ char electionTimeStr[32];
1741
+ snprintf(electionTimeStr, sizeof(electionTimeStr), "%02u:%02u", electionMinutes, electionSeconds);
1742
+
1743
+ report += "Last Election: " + TSTRING(electionTimeStr) + " ago";
1744
+ report += " (Winner: " + TSTRING(std::to_string(lastElection.winnerNodeId).c_str());
1745
+ report += ", " + TSTRING(std::to_string(lastElection.candidateCount).c_str()) + " candidates)\n";
1746
+ }
1747
+
1748
+ report += "================================\n";
1749
+ return report;
1750
+ }
1751
+
459
1752
  inline std::shared_ptr<Task> addTask(unsigned long aInterval,
460
1753
  long aIterations,
461
1754
  std::function<void()> aCallback) {
@@ -470,6 +1763,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
470
1763
  ~Mesh() {
471
1764
  this->stop();
472
1765
  if (!isExternalScheduler) delete mScheduler;
1766
+ if (messageQueue) delete messageQueue;
473
1767
  }
474
1768
 
475
1769
  protected:
@@ -521,6 +1815,12 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
521
1815
  callback::List<uint32_t> changedConnectionCallbacks;
522
1816
  nodeTimeAdjustedCallback_t nodeTimeAdjustedCallback;
523
1817
  nodeDelayCallback_t nodeDelayReceivedCallback;
1818
+ bridgeStatusChangedCallback_t bridgeStatusChangedCallback;
1819
+ rtcSyncCompleteCallback_t rtcSyncCompleteCallback;
1820
+
1821
+ // Message queue for offline mode
1822
+ MessageQueue* messageQueue = nullptr;
1823
+
524
1824
  #ifdef ESP32
525
1825
  SemaphoreHandle_t xSemaphore = NULL;
526
1826
  #endif
@@ -558,6 +1858,25 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
558
1858
  #endif
559
1859
  }
560
1860
 
1861
+ // Bridge status tracking
1862
+ std::vector<BridgeInfo> knownBridges;
1863
+ uint32_t bridgeStatusIntervalMs = 30000; // Default 30 seconds
1864
+ uint32_t bridgeTimeoutMs = 60000; // Default 60 seconds
1865
+ bool bridgeStatusBroadcastEnabled = true;
1866
+
1867
+ // Health metrics tracking
1868
+ uint32_t metricsDisconnectCount = 0;
1869
+
1870
+ // RTC management
1871
+ rtc::RTCManager rtcManager;
1872
+
1873
+ // Diagnostics tracking
1874
+ bool diagnosticsEnabled = false;
1875
+ std::vector<ElectionRecord> electionHistory;
1876
+ static const size_t MAX_ELECTION_HISTORY = 10;
1877
+ BridgeChangeEvent lastBridgeChange;
1878
+ uint32_t lastBridgeChangeTime = 0;
1879
+
561
1880
  friend T;
562
1881
  friend void onDataCb(void *, AsyncClient *, void *, size_t);
563
1882
  friend void tcpSentCb(void *, AsyncClient *, size_t, uint32_t);
@@ -589,6 +1908,8 @@ class Connection : public painlessmesh::layout::Neighbour,
589
1908
  uint32_t messagesTx = 0;
590
1909
  uint32_t messagesDropped = 0;
591
1910
  uint32_t timeLastReceived = 0;
1911
+ uint64_t bytesRx = 0;
1912
+ uint64_t bytesTx = 0;
592
1913
 
593
1914
  // Latency tracking (rolling window)
594
1915
  std::vector<uint32_t> latencySamples;
@@ -660,19 +1981,21 @@ class Connection : public painlessmesh::layout::Neighbour,
660
1981
  }
661
1982
 
662
1983
  /**
663
- * Record message received timestamp
1984
+ * Record message received timestamp and bytes
664
1985
  */
665
- void onMessageReceived() {
1986
+ void onMessageReceived(size_t bytes = 0) {
666
1987
  messagesRx++;
1988
+ bytesRx += bytes;
667
1989
  timeLastReceived = millis();
668
1990
  }
669
1991
 
670
1992
  /**
671
- * Record message sent
1993
+ * Record message sent and bytes
672
1994
  */
673
- void onMessageSent(bool success) {
1995
+ void onMessageSent(bool success, size_t bytes = 0) {
674
1996
  if (success) {
675
1997
  messagesTx++;
1998
+ bytesTx += bytes;
676
1999
  } else {
677
2000
  messagesDropped++;
678
2001
  }