@alteriom/painlessmesh 1.9.18 → 1.9.20
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 +62 -0
- package/README.md +82 -63
- package/examples/alteriom/README.md +4 -4
- package/examples/alteriom/alteriom_custom_package_template.hpp +320 -0
- package/examples/alteriom/alteriom_sensor_package.hpp +1 -1
- package/examples/alteriom/mppt_example/alteriom_mppt_example.ino +208 -0
- package/examples/bridge_failover/bridge_failover.ino +17 -0
- package/examples/sendToInternet/CMakeLists.txt +54 -0
- package/examples/sendToInternet/PC_NODE_README.md +517 -0
- package/examples/sendToInternet/README.md +39 -1
- package/examples/sendToInternet/build.sh +153 -0
- package/examples/sendToInternet/mock_server_test.ino +361 -0
- package/examples/sendToInternet/pc_mesh_node.cpp +361 -0
- package/library.json +4 -1
- package/library.properties +1 -1
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +5 -13
- package/src/arduino/wifi.hpp +306 -100
- package/src/connection.cpp +10 -0
- package/src/painlessMesh.h +1 -14
- package/src/painlessmesh/connection.hpp +11 -16
- package/src/painlessmesh/gateway.hpp +0 -1061
- package/src/painlessmesh/mesh.hpp +58 -86
- package/src/painlessmesh/message_queue.hpp +1 -2
- package/src/painlessmesh/metrics.hpp +2 -262
- package/src/painlessmesh/validation.hpp +0 -143
- package/docs/README.md +0 -132
- package/docs/alteriom/overview.md +0 -531
- package/docs/api/core-api.md +0 -607
- package/docs/api/shared-gateway.md +0 -1207
- package/docs/architecture/mesh-architecture.md +0 -399
- package/docs/architecture/plugin-system.md +0 -517
- package/docs/getting-started/arduino-manual-install.md +0 -313
- package/docs/getting-started/first-mesh.md +0 -410
- package/docs/getting-started/installation.md +0 -275
- package/docs/getting-started/quickstart.md +0 -158
- package/docs/troubleshooting/common-issues.md +0 -679
- package/docs/troubleshooting/debugging.md +0 -455
- package/docs/troubleshooting/external-device-connection.md +0 -283
- package/docs/troubleshooting/faq.md +0 -574
- package/docs/tutorials/basic-examples.md +0 -718
|
@@ -352,6 +352,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
352
352
|
|
|
353
353
|
if (!isExternalScheduler) {
|
|
354
354
|
delete mScheduler;
|
|
355
|
+
mScheduler = nullptr;
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
|
|
@@ -876,7 +877,11 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
876
877
|
* @return Number of available gateways
|
|
877
878
|
*/
|
|
878
879
|
size_t getGatewayCount() {
|
|
879
|
-
|
|
880
|
+
size_t count = 0;
|
|
881
|
+
for (const auto& bridge : knownBridges) {
|
|
882
|
+
if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) count++;
|
|
883
|
+
}
|
|
884
|
+
return count;
|
|
880
885
|
}
|
|
881
886
|
|
|
882
887
|
/**
|
|
@@ -1005,6 +1010,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1005
1010
|
using namespace logger;
|
|
1006
1011
|
if (bridgeCleanupTask != nullptr) {
|
|
1007
1012
|
bridgeCleanupTask->disable();
|
|
1013
|
+
// Task remains in scheduler (disabled) — negligible overhead.
|
|
1014
|
+
// The scheduler will skip it on each cycle.
|
|
1008
1015
|
bridgeCleanupTask = nullptr;
|
|
1009
1016
|
Log(GENERAL, "disableBridgeCleanup(): Cleanup task disabled\n");
|
|
1010
1017
|
}
|
|
@@ -1405,10 +1412,13 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1405
1412
|
bool cancelInternetRequest(uint32_t messageId) {
|
|
1406
1413
|
auto it = pendingInternetRequests.find(messageId);
|
|
1407
1414
|
if (it != pendingInternetRequests.end()) {
|
|
1408
|
-
|
|
1409
|
-
it->second.callback(false, 0, "Request cancelled");
|
|
1410
|
-
}
|
|
1415
|
+
auto callback = it->second.callback;
|
|
1411
1416
|
pendingInternetRequests.erase(it);
|
|
1417
|
+
if (callback) {
|
|
1418
|
+
this->addTask([callback]() {
|
|
1419
|
+
callback(false, 0, "Request cancelled");
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1412
1422
|
Log(logger::GENERAL, "cancelInternetRequest(): Cancelled msgId=%u\n", messageId);
|
|
1413
1423
|
return true;
|
|
1414
1424
|
}
|
|
@@ -1532,10 +1542,41 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1532
1542
|
isRetryable = true;
|
|
1533
1543
|
Log(COMMUNICATION, "handleGatewayAck(): HTTP 429 rate limit, marking as retryable\n");
|
|
1534
1544
|
}
|
|
1535
|
-
// Network errors (httpStatus == 0)
|
|
1545
|
+
// Network errors (httpStatus == 0) EXCEPT gateway connectivity errors
|
|
1546
|
+
// Gateway connectivity errors are infrastructure issues (not transient)
|
|
1536
1547
|
else if (ack.httpStatus == 0) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1548
|
+
// Check if this is a gateway-level connectivity error (non-retryable)
|
|
1549
|
+
bool isGatewayConnectivityError = false;
|
|
1550
|
+
|
|
1551
|
+
// These errors indicate infrastructure issues that won't be fixed by retrying:
|
|
1552
|
+
// - "Router has no internet access" - WAN connection down
|
|
1553
|
+
// - "Gateway WiFi not connected" - ESP not associated with WiFi
|
|
1554
|
+
// - "Captive portal detected" - Router requires web authentication
|
|
1555
|
+
// Use find() for std::string (test env) or indexOf() for Arduino String
|
|
1556
|
+
bool routerError = false, wifiError = false, captivePortalError = false;
|
|
1557
|
+
#if defined(PAINLESSMESH_BOOST)
|
|
1558
|
+
// Test environment: TSTRING is std::string, use find()
|
|
1559
|
+
routerError = (ack.error.find("Router has no internet") != std::string::npos);
|
|
1560
|
+
wifiError = (ack.error.find("Gateway WiFi not connected") != std::string::npos);
|
|
1561
|
+
captivePortalError = (ack.error.find("Captive portal detected") != std::string::npos);
|
|
1562
|
+
#else
|
|
1563
|
+
// Arduino environment: TSTRING is String, use indexOf()
|
|
1564
|
+
routerError = (ack.error.indexOf("Router has no internet") >= 0);
|
|
1565
|
+
wifiError = (ack.error.indexOf("Gateway WiFi not connected") >= 0);
|
|
1566
|
+
captivePortalError = (ack.error.indexOf("Captive portal detected") >= 0);
|
|
1567
|
+
#endif
|
|
1568
|
+
|
|
1569
|
+
if (routerError || wifiError || captivePortalError) {
|
|
1570
|
+
isGatewayConnectivityError = true;
|
|
1571
|
+
Log(COMMUNICATION, "handleGatewayAck(): Gateway connectivity error detected (non-retryable): %s\n",
|
|
1572
|
+
ack.error.c_str());
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Only mark as retryable if it's NOT a gateway connectivity error
|
|
1576
|
+
if (!isGatewayConnectivityError) {
|
|
1577
|
+
isRetryable = true;
|
|
1578
|
+
Log(COMMUNICATION, "handleGatewayAck(): Network error, marking as retryable\n");
|
|
1579
|
+
}
|
|
1539
1580
|
}
|
|
1540
1581
|
// HTTP 4xx client errors (except 429) are NOT retryable
|
|
1541
1582
|
// HTTP 3xx redirects are NOT retryable (should be followed by HTTPClient)
|
|
@@ -1923,8 +1964,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1923
1964
|
return rtcTime;
|
|
1924
1965
|
}
|
|
1925
1966
|
}
|
|
1926
|
-
// Fallback to mesh time (microseconds)
|
|
1927
|
-
return
|
|
1967
|
+
// Fallback to mesh time (microseconds), converted to seconds for consistency
|
|
1968
|
+
return getNodeTime() / 1000000;
|
|
1928
1969
|
}
|
|
1929
1970
|
|
|
1930
1971
|
/**
|
|
@@ -2295,7 +2336,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2295
2336
|
auto tree = this->asNodeTree();
|
|
2296
2337
|
|
|
2297
2338
|
// BFS to find hop count
|
|
2298
|
-
std::queue<std::pair<uint32_t,
|
|
2339
|
+
std::queue<std::pair<uint32_t, uint16_t>> queue; // (nodeId, hops)
|
|
2299
2340
|
std::set<uint32_t> visited;
|
|
2300
2341
|
|
|
2301
2342
|
// Start from this node
|
|
@@ -2307,7 +2348,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2307
2348
|
queue.pop();
|
|
2308
2349
|
|
|
2309
2350
|
uint32_t currentNode = current.first;
|
|
2310
|
-
|
|
2351
|
+
uint16_t hops = current.second;
|
|
2311
2352
|
|
|
2312
2353
|
// Found the target
|
|
2313
2354
|
if (currentNode == nodeId) {
|
|
@@ -2321,7 +2362,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2321
2362
|
for (auto neighbor : neighbors) {
|
|
2322
2363
|
if (visited.find(neighbor) == visited.end()) {
|
|
2323
2364
|
visited.insert(neighbor);
|
|
2324
|
-
queue.push({neighbor, static_cast<
|
|
2365
|
+
queue.push({neighbor, static_cast<uint16_t>(hops + 1)});
|
|
2325
2366
|
}
|
|
2326
2367
|
}
|
|
2327
2368
|
}
|
|
@@ -2450,7 +2491,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2450
2491
|
// Reconstruct path from parent map
|
|
2451
2492
|
if (found) {
|
|
2452
2493
|
uint32_t current = nodeId;
|
|
2453
|
-
|
|
2494
|
+
size_t maxIter = parent.size() + 1;
|
|
2495
|
+
while (current != 0 && maxIter-- > 0) {
|
|
2454
2496
|
path.insert(path.begin(), current);
|
|
2455
2497
|
current = parent[current];
|
|
2456
2498
|
}
|
|
@@ -2495,7 +2537,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2495
2537
|
uint32_t totalMessagesDropped = 0;
|
|
2496
2538
|
uint32_t totalLatency = 0;
|
|
2497
2539
|
uint32_t latencySampleCount = 0;
|
|
2498
|
-
|
|
2540
|
+
int32_t sumRSSI = 0;
|
|
2499
2541
|
int8_t minRSSI = 0;
|
|
2500
2542
|
int8_t maxRSSI = -127;
|
|
2501
2543
|
uint32_t rssiCount = 0;
|
|
@@ -2792,76 +2834,6 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2792
2834
|
return lastBridgeChange;
|
|
2793
2835
|
}
|
|
2794
2836
|
|
|
2795
|
-
/**
|
|
2796
|
-
* Get Internet path to specific node
|
|
2797
|
-
*
|
|
2798
|
-
* Returns the routing path from the specified node to the Internet bridge.
|
|
2799
|
-
* Path includes all intermediate nodes from source to bridge.
|
|
2800
|
-
*
|
|
2801
|
-
* \code
|
|
2802
|
-
* auto path = mesh.getInternetPath(targetNodeId);
|
|
2803
|
-
* Serial.print("Path to Internet: ");
|
|
2804
|
-
* for (auto nodeId : path) {
|
|
2805
|
-
* Serial.printf("%u -> ", nodeId);
|
|
2806
|
-
* }
|
|
2807
|
-
* Serial.println("Internet");
|
|
2808
|
-
* \endcode
|
|
2809
|
-
*
|
|
2810
|
-
* @param nodeId Node to find path from
|
|
2811
|
-
* @return Vector of node IDs representing the path (empty if no path)
|
|
2812
|
-
*/
|
|
2813
|
-
std::vector<uint32_t> getInternetPath(uint32_t nodeId) {
|
|
2814
|
-
std::vector<uint32_t> path;
|
|
2815
|
-
|
|
2816
|
-
// Get primary bridge
|
|
2817
|
-
auto primaryBridge = this->getPrimaryBridge();
|
|
2818
|
-
if (primaryBridge == nullptr) {
|
|
2819
|
-
Log(logger::GENERAL, "getInternetPath(): No bridge available\n");
|
|
2820
|
-
return path;
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
// If requesting path for the bridge itself
|
|
2824
|
-
if (nodeId == primaryBridge->nodeId) {
|
|
2825
|
-
path.push_back(nodeId);
|
|
2826
|
-
return path;
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
// Start with the target node
|
|
2830
|
-
path.push_back(nodeId);
|
|
2831
|
-
|
|
2832
|
-
// For now, simplified routing: if direct connection, add bridge
|
|
2833
|
-
// TODO: Implement proper multi-hop path discovery
|
|
2834
|
-
if (this->isConnected(primaryBridge->nodeId)) {
|
|
2835
|
-
path.push_back(primaryBridge->nodeId);
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
return path;
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
/**
|
|
2842
|
-
* Get bridge node ID for specific node
|
|
2843
|
-
*
|
|
2844
|
-
* Returns the bridge node ID that the specified node should use to reach Internet.
|
|
2845
|
-
* For most cases, this is the primary bridge.
|
|
2846
|
-
*
|
|
2847
|
-
* \code
|
|
2848
|
-
* uint32_t bridgeId = mesh.getBridgeForNodeId(targetNodeId);
|
|
2849
|
-
* if (bridgeId != 0) {
|
|
2850
|
-
* Serial.printf("Node %u uses bridge %u\n", targetNodeId, bridgeId);
|
|
2851
|
-
* }
|
|
2852
|
-
* \endcode
|
|
2853
|
-
*
|
|
2854
|
-
* @param nodeId Node to find bridge for
|
|
2855
|
-
* @return Bridge node ID, or 0 if no bridge available
|
|
2856
|
-
*/
|
|
2857
|
-
uint32_t getBridgeForNodeId(uint32_t nodeId) {
|
|
2858
|
-
auto primaryBridge = this->getPrimaryBridge();
|
|
2859
|
-
if (primaryBridge != nullptr) {
|
|
2860
|
-
return primaryBridge->nodeId;
|
|
2861
|
-
}
|
|
2862
|
-
return 0;
|
|
2863
|
-
}
|
|
2864
|
-
|
|
2865
2837
|
/**
|
|
2866
2838
|
* Export topology as DOT format (Graphviz)
|
|
2867
2839
|
*
|
|
@@ -3222,7 +3194,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
3222
3194
|
bool isExternalScheduler = false;
|
|
3223
3195
|
|
|
3224
3196
|
/// Is the node a root node
|
|
3225
|
-
bool shouldContainRoot;
|
|
3197
|
+
bool shouldContainRoot = false;
|
|
3226
3198
|
|
|
3227
3199
|
Scheduler *mScheduler;
|
|
3228
3200
|
|
|
@@ -3237,7 +3209,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
3237
3209
|
*/
|
|
3238
3210
|
bool semaphoreTake() {
|
|
3239
3211
|
#ifdef ESP32
|
|
3240
|
-
return xSemaphoreTake(xSemaphore, (TickType_t)
|
|
3212
|
+
return xSemaphoreTake(xSemaphore, (TickType_t)1000) == pdTRUE;
|
|
3241
3213
|
#else
|
|
3242
3214
|
return true;
|
|
3243
3215
|
#endif
|
|
@@ -104,9 +104,8 @@ public:
|
|
|
104
104
|
* Constructor
|
|
105
105
|
* @param maxSize Maximum number of messages in queue
|
|
106
106
|
*/
|
|
107
|
-
explicit MessageQueue(uint32_t maxSize = 1000)
|
|
107
|
+
explicit MessageQueue(uint32_t maxSize = 1000)
|
|
108
108
|
: maxQueueSize(maxSize), nextMessageId(1) {
|
|
109
|
-
messages.reserve(maxSize);
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
/**
|
|
@@ -33,8 +33,7 @@ class Timer {
|
|
|
33
33
|
|
|
34
34
|
uint32_t elapsed_us() const {
|
|
35
35
|
#ifdef ESP32
|
|
36
|
-
return static_cast<uint32_t>(
|
|
37
|
-
1000);
|
|
36
|
+
return static_cast<uint32_t>(esp_timer_get_time() - start_time_us_);
|
|
38
37
|
#else
|
|
39
38
|
return elapsed_ms() * 1000; // Fallback to millisecond precision
|
|
40
39
|
#endif
|
|
@@ -58,266 +57,7 @@ class Timer {
|
|
|
58
57
|
#endif
|
|
59
58
|
};
|
|
60
59
|
|
|
61
|
-
/**
|
|
62
|
-
* Message statistics tracking
|
|
63
|
-
*/
|
|
64
|
-
struct MessageStats {
|
|
65
|
-
uint32_t messages_sent = 0;
|
|
66
|
-
uint32_t messages_received = 0;
|
|
67
|
-
uint32_t messages_dropped = 0;
|
|
68
|
-
uint32_t messages_retransmitted = 0;
|
|
69
|
-
uint32_t bytes_sent = 0;
|
|
70
|
-
uint32_t bytes_received = 0;
|
|
71
|
-
uint32_t parse_errors = 0;
|
|
72
|
-
uint32_t validation_errors = 0;
|
|
73
|
-
|
|
74
|
-
// Latency tracking
|
|
75
|
-
uint32_t min_latency_ms = UINT32_MAX;
|
|
76
|
-
uint32_t max_latency_ms = 0;
|
|
77
|
-
uint32_t total_latency_ms = 0;
|
|
78
|
-
uint32_t latency_samples = 0;
|
|
79
|
-
|
|
80
|
-
void record_sent_message(size_t bytes) {
|
|
81
|
-
messages_sent++;
|
|
82
|
-
bytes_sent += bytes;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
void record_received_message(size_t bytes) {
|
|
86
|
-
messages_received++;
|
|
87
|
-
bytes_received += bytes;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
void record_dropped_message() { messages_dropped++; }
|
|
91
|
-
|
|
92
|
-
void record_retransmission() { messages_retransmitted++; }
|
|
93
|
-
|
|
94
|
-
void record_parse_error() { parse_errors++; }
|
|
95
|
-
|
|
96
|
-
void record_validation_error() { validation_errors++; }
|
|
97
|
-
|
|
98
|
-
void record_latency(uint32_t latency_ms) {
|
|
99
|
-
min_latency_ms = std::min(min_latency_ms, latency_ms);
|
|
100
|
-
max_latency_ms = std::max(max_latency_ms, latency_ms);
|
|
101
|
-
total_latency_ms += latency_ms;
|
|
102
|
-
latency_samples++;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
uint32_t average_latency_ms() const {
|
|
106
|
-
return latency_samples > 0 ? total_latency_ms / latency_samples : 0;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
double throughput_bps() const {
|
|
110
|
-
#ifdef ARDUINO
|
|
111
|
-
uint32_t uptime_s = millis() / 1000;
|
|
112
|
-
#else
|
|
113
|
-
// For non-Arduino builds, use a simpler approach or disable this feature
|
|
114
|
-
uint32_t uptime_s = 1; // Avoid division by zero
|
|
115
|
-
#endif
|
|
116
|
-
return uptime_s > 0 ? (bytes_sent + bytes_received) * 8.0 / uptime_s : 0.0;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
double packet_loss_rate() const {
|
|
120
|
-
uint32_t total_attempted = messages_sent + messages_dropped;
|
|
121
|
-
return total_attempted > 0
|
|
122
|
-
? static_cast<double>(messages_dropped) / total_attempted
|
|
123
|
-
: 0.0;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
void reset() { *this = MessageStats{}; }
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Memory usage tracking
|
|
131
|
-
*/
|
|
132
|
-
struct MemoryStats {
|
|
133
|
-
uint32_t heap_free = 0;
|
|
134
|
-
uint32_t heap_max_alloc = 0;
|
|
135
|
-
uint32_t heap_min_free = UINT32_MAX;
|
|
136
|
-
uint32_t psram_free = 0;
|
|
137
|
-
uint32_t stack_high_water = 0;
|
|
138
|
-
|
|
139
|
-
void update() {
|
|
140
|
-
#ifdef ESP32
|
|
141
|
-
heap_free = ESP.getFreeHeap();
|
|
142
|
-
heap_max_alloc = ESP.getMaxAllocHeap();
|
|
143
|
-
heap_min_free = std::min(heap_min_free, heap_free);
|
|
144
|
-
|
|
145
|
-
#ifdef BOARD_HAS_PSRAM
|
|
146
|
-
psram_free = ESP.getFreePsram();
|
|
147
|
-
#endif
|
|
148
|
-
|
|
149
|
-
stack_high_water = uxTaskGetStackHighWaterMark(NULL);
|
|
150
|
-
#elif defined(ESP8266)
|
|
151
|
-
heap_free = ESP.getFreeHeap();
|
|
152
|
-
heap_max_alloc = ESP.getMaxFreeBlockSize();
|
|
153
|
-
heap_min_free = std::min(heap_min_free, heap_free);
|
|
154
|
-
#endif
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
bool is_memory_critical() const {
|
|
158
|
-
return heap_free < 10000; // Less than 10KB free is critical
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
bool is_memory_low() const {
|
|
162
|
-
return heap_free < 20000; // Less than 20KB free is low
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Network topology metrics
|
|
168
|
-
*/
|
|
169
|
-
struct TopologyStats {
|
|
170
|
-
uint32_t node_count = 0;
|
|
171
|
-
uint32_t connection_count = 0;
|
|
172
|
-
uint32_t max_hops = 0;
|
|
173
|
-
uint32_t connection_changes = 0;
|
|
174
|
-
uint32_t failed_connections = 0;
|
|
175
|
-
|
|
176
|
-
void record_topology_change(uint32_t nodes, uint32_t connections,
|
|
177
|
-
uint32_t hops) {
|
|
178
|
-
node_count = nodes;
|
|
179
|
-
connection_count = connections;
|
|
180
|
-
max_hops = hops;
|
|
181
|
-
connection_changes++;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
void record_failed_connection() { failed_connections++; }
|
|
185
|
-
|
|
186
|
-
double connection_stability() const {
|
|
187
|
-
return connection_changes > 0
|
|
188
|
-
? 1.0 - static_cast<double>(failed_connections) /
|
|
189
|
-
connection_changes
|
|
190
|
-
: 1.0;
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Comprehensive metrics collector
|
|
196
|
-
*/
|
|
197
|
-
class MetricsCollector {
|
|
198
|
-
public:
|
|
199
|
-
MetricsCollector() : start_time_(get_current_time()) {}
|
|
200
|
-
|
|
201
|
-
MessageStats& message_stats() { return message_stats_; }
|
|
202
|
-
MemoryStats& memory_stats() { return memory_stats_; }
|
|
203
|
-
TopologyStats& topology_stats() { return topology_stats_; }
|
|
204
|
-
|
|
205
|
-
const MessageStats& message_stats() const { return message_stats_; }
|
|
206
|
-
const MemoryStats& memory_stats() const { return memory_stats_; }
|
|
207
|
-
const TopologyStats& topology_stats() const { return topology_stats_; }
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Update all metrics (call periodically)
|
|
211
|
-
*/
|
|
212
|
-
void update() {
|
|
213
|
-
memory_stats_.update();
|
|
214
|
-
last_update_ = get_current_time();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Get uptime in seconds
|
|
219
|
-
*/
|
|
220
|
-
uint32_t uptime_seconds() const {
|
|
221
|
-
return (get_current_time() - start_time_) / 1000;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Generate JSON status report
|
|
226
|
-
*/
|
|
227
|
-
TSTRING generate_status_json() const {
|
|
228
|
-
TSTRING json = "{";
|
|
229
|
-
json += "\"uptime\":";
|
|
230
|
-
json += std::to_string(uptime_seconds());
|
|
231
|
-
json += ",";
|
|
232
|
-
json += "\"messages\":{";
|
|
233
|
-
json += "\"sent\":";
|
|
234
|
-
json += std::to_string(message_stats_.messages_sent);
|
|
235
|
-
json += ",";
|
|
236
|
-
json += "\"received\":";
|
|
237
|
-
json += std::to_string(message_stats_.messages_received);
|
|
238
|
-
json += ",";
|
|
239
|
-
json += "\"dropped\":";
|
|
240
|
-
json += std::to_string(message_stats_.messages_dropped);
|
|
241
|
-
json += ",";
|
|
242
|
-
json += "\"avg_latency\":";
|
|
243
|
-
json += std::to_string(message_stats_.average_latency_ms());
|
|
244
|
-
json += ",";
|
|
245
|
-
json += "\"throughput\":";
|
|
246
|
-
json += std::to_string(message_stats_.throughput_bps());
|
|
247
|
-
json += ",";
|
|
248
|
-
json += "\"packet_loss\":";
|
|
249
|
-
json += std::to_string(message_stats_.packet_loss_rate());
|
|
250
|
-
json += "},";
|
|
251
|
-
json += "\"memory\":{";
|
|
252
|
-
json += "\"heap_free\":";
|
|
253
|
-
json += std::to_string(memory_stats_.heap_free);
|
|
254
|
-
json += ",";
|
|
255
|
-
json += "\"heap_min\":";
|
|
256
|
-
json += std::to_string(memory_stats_.heap_min_free);
|
|
257
|
-
json += ",";
|
|
258
|
-
json += "\"critical\":";
|
|
259
|
-
json += (memory_stats_.is_memory_critical() ? "true" : "false");
|
|
260
|
-
json += "},";
|
|
261
|
-
json += "\"topology\":{";
|
|
262
|
-
json += "\"nodes\":";
|
|
263
|
-
json += std::to_string(topology_stats_.node_count);
|
|
264
|
-
json += ",";
|
|
265
|
-
json += "\"connections\":";
|
|
266
|
-
json += std::to_string(topology_stats_.connection_count);
|
|
267
|
-
json += ",";
|
|
268
|
-
json += "\"max_hops\":";
|
|
269
|
-
json += std::to_string(topology_stats_.max_hops);
|
|
270
|
-
json += ",";
|
|
271
|
-
json += "\"stability\":";
|
|
272
|
-
json += std::to_string(topology_stats_.connection_stability());
|
|
273
|
-
json += "}";
|
|
274
|
-
json += "}";
|
|
275
|
-
return json;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Reset all metrics
|
|
280
|
-
*/
|
|
281
|
-
void reset() {
|
|
282
|
-
message_stats_.reset();
|
|
283
|
-
topology_stats_ = TopologyStats{};
|
|
284
|
-
start_time_ = get_current_time();
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Check if metrics indicate performance issues
|
|
289
|
-
*/
|
|
290
|
-
bool has_performance_issues() const {
|
|
291
|
-
return memory_stats_.is_memory_critical() ||
|
|
292
|
-
message_stats_.packet_loss_rate() > 0.1 || // > 10% packet loss
|
|
293
|
-
topology_stats_.connection_stability() < 0.8; // < 80% stability
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
private:
|
|
297
|
-
uint32_t get_current_time() const {
|
|
298
|
-
#ifdef ARDUINO
|
|
299
|
-
return millis();
|
|
300
|
-
#else
|
|
301
|
-
auto now = std::chrono::steady_clock::now();
|
|
302
|
-
auto duration = now.time_since_epoch();
|
|
303
|
-
return std::chrono::duration_cast<std::chrono::milliseconds>(duration)
|
|
304
|
-
.count();
|
|
305
|
-
#endif
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
MessageStats message_stats_;
|
|
309
|
-
MemoryStats memory_stats_;
|
|
310
|
-
TopologyStats topology_stats_;
|
|
311
|
-
uint32_t start_time_;
|
|
312
|
-
uint32_t last_update_ = 0;
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
// Global metrics instance (optional, can be disabled)
|
|
316
|
-
#ifdef PAINLESS_MESH_ENABLE_METRICS
|
|
317
|
-
extern MetricsCollector global_metrics;
|
|
318
|
-
#endif
|
|
319
|
-
|
|
320
60
|
} // namespace metrics
|
|
321
61
|
} // namespace painlessmesh
|
|
322
62
|
|
|
323
|
-
#endif // _PAINLESS_MESH_METRICS_HPP_
|
|
63
|
+
#endif // _PAINLESS_MESH_METRICS_HPP_
|
|
@@ -8,12 +8,9 @@
|
|
|
8
8
|
* security and robustness of the mesh network.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
#include <list>
|
|
12
|
-
#include <map>
|
|
13
11
|
#ifndef ARDUINO
|
|
14
12
|
#include <chrono>
|
|
15
13
|
#endif
|
|
16
|
-
#include "ArduinoJson.h"
|
|
17
14
|
#include "painlessmesh/configuration.hpp"
|
|
18
15
|
|
|
19
16
|
namespace painlessmesh {
|
|
@@ -45,146 +42,6 @@ struct ValidationConfig {
|
|
|
45
42
|
bool strict_type_checking = true; // Enable strict type validation
|
|
46
43
|
};
|
|
47
44
|
|
|
48
|
-
/**
|
|
49
|
-
* Rate limiting for preventing message spam
|
|
50
|
-
*/
|
|
51
|
-
class RateLimiter {
|
|
52
|
-
public:
|
|
53
|
-
RateLimiter(size_t max_messages_per_second = 10, size_t window_size_ms = 1000)
|
|
54
|
-
: max_messages_(max_messages_per_second), window_size_(window_size_ms) {}
|
|
55
|
-
|
|
56
|
-
bool allow_message(uint32_t node_id) {
|
|
57
|
-
uint32_t current_time = get_current_time();
|
|
58
|
-
auto& history = node_history_[node_id];
|
|
59
|
-
|
|
60
|
-
// Remove old entries outside the window
|
|
61
|
-
while (!history.empty() &&
|
|
62
|
-
(current_time - history.front()) > window_size_) {
|
|
63
|
-
history.pop_front();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check if limit is exceeded
|
|
67
|
-
if (history.size() >= max_messages_) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Record this message
|
|
72
|
-
history.push_back(current_time);
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
void clear_node_history(uint32_t node_id) { node_history_.erase(node_id); }
|
|
77
|
-
|
|
78
|
-
void clear_all_history() { node_history_.clear(); }
|
|
79
|
-
|
|
80
|
-
private:
|
|
81
|
-
uint32_t get_current_time() const {
|
|
82
|
-
#ifdef ARDUINO
|
|
83
|
-
return millis();
|
|
84
|
-
#else
|
|
85
|
-
auto now = std::chrono::steady_clock::now();
|
|
86
|
-
auto duration = now.time_since_epoch();
|
|
87
|
-
return std::chrono::duration_cast<std::chrono::milliseconds>(duration)
|
|
88
|
-
.count();
|
|
89
|
-
#endif
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
size_t max_messages_;
|
|
93
|
-
size_t window_size_;
|
|
94
|
-
std::map<uint32_t, std::list<uint32_t>> node_history_;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* JSON message validator
|
|
99
|
-
*/
|
|
100
|
-
class MessageValidator {
|
|
101
|
-
public:
|
|
102
|
-
explicit MessageValidator(const ValidationConfig& config = ValidationConfig{})
|
|
103
|
-
: config_(config) {}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Validate a JSON message for basic structure and security
|
|
107
|
-
*/
|
|
108
|
-
ValidationResult validate_message(const JsonObject& obj,
|
|
109
|
-
size_t message_size = 0) const {
|
|
110
|
-
// Check message size
|
|
111
|
-
if (message_size > config_.max_message_size) {
|
|
112
|
-
return ValidationResult::MESSAGE_TOO_LARGE;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Check required fields based on message type
|
|
116
|
-
if (!obj["type"].is<int>()) {
|
|
117
|
-
return ValidationResult::MISSING_REQUIRED_FIELD;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Validate node IDs if present
|
|
121
|
-
if (obj["from"].is<uint32_t>()) {
|
|
122
|
-
uint32_t from_id = obj["from"].as<uint32_t>();
|
|
123
|
-
if (!is_valid_node_id(from_id)) {
|
|
124
|
-
return ValidationResult::INVALID_NODE_ID;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (obj["dest"].is<uint32_t>()) {
|
|
129
|
-
uint32_t dest_id = obj["dest"].as<uint32_t>();
|
|
130
|
-
if (dest_id != 0 && !is_valid_node_id(dest_id)) { // 0 is broadcast
|
|
131
|
-
return ValidationResult::INVALID_NODE_ID;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Validate string fields
|
|
136
|
-
for (JsonPair pair : obj) {
|
|
137
|
-
if (pair.value().is<const char*>()) {
|
|
138
|
-
const char* str_value = pair.value().as<const char*>();
|
|
139
|
-
if (strlen(str_value) > config_.max_string_length) {
|
|
140
|
-
return ValidationResult::INVALID_FIELD_VALUE;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return ValidationResult::VALID;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Validate node ID range
|
|
150
|
-
*/
|
|
151
|
-
bool is_valid_node_id(uint32_t node_id) const {
|
|
152
|
-
return node_id >= config_.min_node_id && node_id <= config_.max_node_id;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Get validation error message
|
|
157
|
-
*/
|
|
158
|
-
const char* get_error_message(ValidationResult result) const {
|
|
159
|
-
switch (result) {
|
|
160
|
-
case ValidationResult::VALID:
|
|
161
|
-
return "Valid";
|
|
162
|
-
case ValidationResult::INVALID_JSON:
|
|
163
|
-
return "Invalid JSON format";
|
|
164
|
-
case ValidationResult::MISSING_REQUIRED_FIELD:
|
|
165
|
-
return "Missing required field";
|
|
166
|
-
case ValidationResult::INVALID_FIELD_TYPE:
|
|
167
|
-
return "Invalid field type";
|
|
168
|
-
case ValidationResult::INVALID_FIELD_VALUE:
|
|
169
|
-
return "Invalid field value";
|
|
170
|
-
case ValidationResult::MESSAGE_TOO_LARGE:
|
|
171
|
-
return "Message too large";
|
|
172
|
-
case ValidationResult::INVALID_NODE_ID:
|
|
173
|
-
return "Invalid node ID";
|
|
174
|
-
case ValidationResult::RATE_LIMIT_EXCEEDED:
|
|
175
|
-
return "Rate limit exceeded";
|
|
176
|
-
default:
|
|
177
|
-
return "Unknown error";
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const ValidationConfig& get_config() const { return config_; }
|
|
182
|
-
void set_config(const ValidationConfig& config) { config_ = config; }
|
|
183
|
-
|
|
184
|
-
private:
|
|
185
|
-
ValidationConfig config_;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
45
|
/**
|
|
189
46
|
* Secure random number generation for mesh operations
|
|
190
47
|
*/
|