@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.
- package/CHANGELOG.md +139 -3
- package/README.md +114 -4
- package/RELEASE_GUIDE.md +57 -8
- package/docs/BRIDGE_FAILOVER.md +512 -0
- package/docs/BRIDGE_HEALTH_MONITORING.md +293 -0
- package/docs/CREATE_MISSING_RELEASES.md +321 -0
- package/docs/README.md +2 -1
- package/docs/RELEASE_AGENT_SUMMARY.md +386 -0
- package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +523 -0
- package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +542 -0
- package/docs/troubleshooting/ESP32_C6_COMPATIBILITY.md +157 -0
- package/docs/troubleshooting/common-issues.md +28 -0
- package/examples/alteriom/alteriom_sensor_package.hpp +233 -1
- package/examples/alteriom/platformio.ini +1 -1
- package/examples/alteriomImproved/platformio.ini +1 -1
- package/examples/alteriomMetricsHealth/metrics_health_node.ino +18 -7
- package/examples/alteriomMetricsHealth/platformio.ini +1 -1
- package/examples/alteriomPhase1/platformio.ini +1 -1
- package/examples/alteriomPhase2/platformio.ini +1 -1
- package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +1014 -11
- package/examples/alteriomSensorNode/platformio.ini +1 -1
- package/examples/basic/basic.ino +6 -2
- package/examples/basic/platformio.ini +1 -1
- package/examples/bridge/alteriom_sensor_package.hpp +1170 -0
- package/examples/bridge/bridge.ino +44 -23
- package/examples/bridge/bridge_health_monitoring_example.ino +188 -0
- package/examples/bridge/enhanced_mqtt_bridge.hpp +1 -1
- package/examples/bridge/mqtt_command_bridge.hpp +2 -2
- package/examples/bridge/platformio.ini +2 -1
- package/examples/bridgeAwareSensorNode/alteriom_sensor_package.hpp +1227 -0
- package/examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino +343 -0
- package/examples/bridgeAwareSensorNode/platformio.ini +26 -0
- package/examples/bridge_failover/README.md +358 -0
- package/examples/bridge_failover/bridge_failover.ino +180 -0
- package/examples/bridge_failover/platformio.ini +27 -0
- package/examples/diagnosticsExample/diagnosticsExample.ino +171 -0
- package/examples/diagnosticsExample/platformio.ini +26 -0
- package/examples/echoNode/platformio.ini +1 -1
- package/examples/logClient/platformio.ini +1 -1
- package/examples/logServer/platformio.ini +1 -1
- package/examples/mqttStatusBridge/platformio.ini +1 -1
- package/examples/namedMesh/platformio.ini +1 -1
- package/examples/ntpTimeSyncBridge/alteriom_sensor_package.hpp +1383 -0
- package/examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino +81 -0
- package/examples/ntpTimeSyncNode/alteriom_sensor_package.hpp +1383 -0
- package/examples/ntpTimeSyncNode/ntpTimeSyncNode.ino +109 -0
- package/examples/otaReceiver/platformio.ini +1 -1
- package/examples/rtcIntegration/README.md +235 -0
- package/examples/rtcIntegration/rtcIntegration.ino +196 -0
- package/examples/startHere/platformio.ini +1 -1
- package/examples/webServer/platformio.ini +1 -1
- package/library.json +93 -53
- package/library.properties +1 -1
- package/package.json +2 -2
- package/src/arduino/wifi.hpp +581 -0
- package/src/painlessMeshSTA.cpp +68 -0
- package/src/painlessMeshSTA.h +3 -0
- package/src/painlessmesh/mesh.hpp +1127 -4
- 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
|
}
|