@alteriom/painlessmesh 1.7.6 → 1.7.7

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 (36) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +81 -6
  3. package/RELEASE_GUIDE.md +36 -0
  4. package/docs/MQTT_BRIDGE_COMMANDS.md +10 -10
  5. package/docs/MQTT_BRIDGE_IMPLEMENTATION_SUMMARY.md +1 -1
  6. package/docs/MQTT_SCHEMA_COMPLIANCE.md +57 -2
  7. package/docs/PHASE1_GUIDE.md +1 -1
  8. package/docs/alteriom/overview.md +2 -2
  9. package/docs/architecture/plugin-system.md +1 -1
  10. package/docs/archive/RELEASE_SUMMARY.md +1 -1
  11. package/docs/releases/RELEASE_CHECKLIST_v1.7.6.md +389 -0
  12. package/docs/releases/RELEASE_SUMMARY_v1.7.7.md +391 -0
  13. package/docs/v1.7.7_MQTT_IMPROVEMENTS.md +776 -0
  14. package/docs/wiki/API-Reference.md +2 -2
  15. package/docs/wiki/Complete-Documentation.md +1 -1
  16. package/examples/alteriom/README.md +137 -3
  17. package/examples/alteriom/alteriom.ino +1 -1
  18. package/examples/alteriom/alteriom_sensor_package.hpp +557 -2
  19. package/examples/alteriomImproved/alteriom_sensor_package.hpp +1 -1
  20. package/examples/alteriomImproved/improved_sensor_node.ino +1 -1
  21. package/examples/alteriomMetricsHealth/alteriom_sensor_package.hpp +796 -0
  22. package/examples/alteriomMetricsHealth/metrics_health_node.ino +418 -0
  23. package/examples/alteriomMetricsHealth/platformio.ini +26 -0
  24. package/examples/alteriomPhase1/alteriom_sensor_package.hpp +1 -1
  25. package/examples/alteriomPhase1/phase1_features.ino +2 -2
  26. package/examples/alteriomPhase2/alteriom_sensor_package.hpp +1 -1
  27. package/examples/alteriomSensorNode/alteriom_sensor_node.ino +1 -1
  28. package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +1 -1
  29. package/examples/bridge/enhanced_mqtt_bridge.hpp +610 -0
  30. package/examples/bridge/enhanced_mqtt_bridge_example.ino +226 -0
  31. package/examples/meshCommandNode/alteriom_sensor_package.hpp +1 -1
  32. package/examples/mqttCommandBridge/alteriom_sensor_package.hpp +1 -1
  33. package/examples/mqttTopologyTest/mqttTopologyTest.ino +5 -1
  34. package/library.json +1 -1
  35. package/library.properties +1 -1
  36. package/package.json +2 -2
@@ -0,0 +1,610 @@
1
+ #ifndef _ENHANCED_MQTT_BRIDGE_HPP_
2
+ #define _ENHANCED_MQTT_BRIDGE_HPP_
3
+
4
+ /**
5
+ * Enhanced MQTT Bridge for painlessMesh v1.7.7+
6
+ *
7
+ * Extends the basic MQTT bridge with:
8
+ * - Command handlers for requesting specific metrics
9
+ * - Aggregated network statistics
10
+ * - Support for MetricsPackage (Type 204) and HealthCheckPackage (Type 605)
11
+ * - Real-time metric requests and responses
12
+ * - Network-wide health aggregation
13
+ *
14
+ * MQTT Topics:
15
+ * Subscribe:
16
+ * - mesh/command/request_metrics - Request metrics from specific node or all nodes
17
+ * - mesh/command/request_health - Request health check from specific node or all nodes
18
+ * - mesh/command/get_aggregated - Get aggregated statistics
19
+ *
20
+ * Publish:
21
+ * - mesh/metrics/{node_id} - Individual node metrics
22
+ * - mesh/health/{node_id} - Individual node health
23
+ * - mesh/aggregated/metrics - Aggregated mesh-wide metrics
24
+ * - mesh/aggregated/health - Aggregated mesh-wide health summary
25
+ * - mesh/response/metrics - Metrics response to command
26
+ * - mesh/response/health - Health response to command
27
+ */
28
+
29
+ #include <Arduino.h>
30
+ #include <painlessMesh.h>
31
+ #include <PubSubClient.h>
32
+ #include "alteriom/alteriom_sensor_package.hpp"
33
+
34
+ using namespace alteriom;
35
+
36
+ // Command types for metric requests
37
+ #define CMD_REQUEST_METRICS 210
38
+ #define CMD_REQUEST_HEALTH 211
39
+ #define CMD_METRICS_RESPONSE 212
40
+ #define CMD_HEALTH_RESPONSE 213
41
+
42
+ class EnhancedMqttBridge {
43
+ private:
44
+ painlessMesh& mesh;
45
+ PubSubClient& mqttClient;
46
+
47
+ // Configuration
48
+ String topicPrefix = "mesh/";
49
+ String deviceId = "";
50
+ String firmwareVersion = "1.7.7";
51
+
52
+ // Aggregation settings
53
+ bool enableAggregation = true;
54
+ uint32_t aggregationInterval = 60000; // 60 seconds
55
+ unsigned long lastAggregationTime = 0;
56
+
57
+ // Metric storage for aggregation
58
+ struct NodeMetrics {
59
+ uint32_t nodeId;
60
+ uint32_t timestamp;
61
+ uint8_t cpuUsage;
62
+ uint32_t freeHeap;
63
+ uint16_t currentThroughput;
64
+ uint8_t connectionQuality;
65
+ int8_t wifiRSSI;
66
+ };
67
+
68
+ struct NodeHealth {
69
+ uint32_t nodeId;
70
+ uint32_t timestamp;
71
+ uint8_t healthStatus;
72
+ uint16_t problemFlags;
73
+ uint8_t memoryHealth;
74
+ uint8_t networkHealth;
75
+ uint8_t performanceHealth;
76
+ };
77
+
78
+ // Storage (limited to prevent memory issues)
79
+ #define MAX_STORED_NODES 20
80
+ NodeMetrics metricsCache[MAX_STORED_NODES];
81
+ NodeHealth healthCache[MAX_STORED_NODES];
82
+ uint8_t metricsCacheSize = 0;
83
+ uint8_t healthCacheSize = 0;
84
+
85
+ // Callback for processing received mesh messages
86
+ std::function<void(uint32_t, String&)> originalReceiveCallback = nullptr;
87
+
88
+ public:
89
+ /**
90
+ * Constructor
91
+ */
92
+ EnhancedMqttBridge(painlessMesh& mesh, PubSubClient& mqttClient)
93
+ : mesh(mesh), mqttClient(mqttClient) {
94
+ }
95
+
96
+ /**
97
+ * Initialize the enhanced bridge
98
+ */
99
+ void begin() {
100
+ // Set device ID if not already set
101
+ if (deviceId.length() == 0) {
102
+ deviceId = "ALT-" + String(mesh.getNodeId(), HEX);
103
+ deviceId.toUpperCase();
104
+ }
105
+
106
+ // Subscribe to command topics
107
+ subscribeMQTTCommands();
108
+
109
+ // Set up mesh message handler
110
+ setupMeshCallbacks();
111
+
112
+ Serial.println("Enhanced MQTT Bridge v1.7.7 started");
113
+ Serial.printf("Device ID: %s\n", deviceId.c_str());
114
+ Serial.println("Command topics subscribed:");
115
+ Serial.printf(" - %scommand/request_metrics\n", topicPrefix.c_str());
116
+ Serial.printf(" - %scommand/request_health\n", topicPrefix.c_str());
117
+ Serial.printf(" - %scommand/get_aggregated\n", topicPrefix.c_str());
118
+ }
119
+
120
+ /**
121
+ * Update function - call in loop()
122
+ */
123
+ void update() {
124
+ // Check if it's time to publish aggregated statistics
125
+ if (enableAggregation && (millis() - lastAggregationTime > aggregationInterval)) {
126
+ publishAggregatedMetrics();
127
+ publishAggregatedHealth();
128
+ lastAggregationTime = millis();
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Set topic prefix
134
+ */
135
+ void setTopicPrefix(const String& prefix) {
136
+ topicPrefix = prefix;
137
+ }
138
+
139
+ /**
140
+ * Set device ID
141
+ */
142
+ void setDeviceId(const String& id) {
143
+ deviceId = id;
144
+ }
145
+
146
+ /**
147
+ * Enable/disable aggregation
148
+ */
149
+ void enableAggregation(bool enable) {
150
+ enableAggregation = enable;
151
+ }
152
+
153
+ /**
154
+ * Set aggregation interval
155
+ */
156
+ void setAggregationInterval(uint32_t interval) {
157
+ aggregationInterval = interval;
158
+ }
159
+
160
+ /**
161
+ * Handle incoming MQTT messages
162
+ * Call this from your MQTT callback
163
+ */
164
+ void handleMQTTMessage(String& topic, String& payload) {
165
+ Serial.printf("MQTT message received on topic: %s\n", topic.c_str());
166
+
167
+ if (topic == topicPrefix + "command/request_metrics") {
168
+ handleRequestMetrics(payload);
169
+ } else if (topic == topicPrefix + "command/request_health") {
170
+ handleRequestHealth(payload);
171
+ } else if (topic == topicPrefix + "command/get_aggregated") {
172
+ handleGetAggregated(payload);
173
+ }
174
+ }
175
+
176
+ private:
177
+ /**
178
+ * Subscribe to MQTT command topics
179
+ */
180
+ void subscribeMQTTCommands() {
181
+ String topic = topicPrefix + "command/request_metrics";
182
+ mqttClient.subscribe(topic.c_str());
183
+
184
+ topic = topicPrefix + "command/request_health";
185
+ mqttClient.subscribe(topic.c_str());
186
+
187
+ topic = topicPrefix + "command/get_aggregated";
188
+ mqttClient.subscribe(topic.c_str());
189
+ }
190
+
191
+ /**
192
+ * Set up mesh callbacks to intercept packages
193
+ */
194
+ void setupMeshCallbacks() {
195
+ mesh.onReceive([this](uint32_t from, String& msg) {
196
+ this->handleMeshMessage(from, msg);
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Handle mesh messages and extract metrics/health packages
202
+ */
203
+ void handleMeshMessage(uint32_t from, String& msg) {
204
+ // Parse message to determine type
205
+ DynamicJsonDocument doc(2048);
206
+ DeserializationError error = deserializeJson(doc, msg);
207
+
208
+ if (error) {
209
+ Serial.printf("JSON parse error from node %u: %s\n", from, error.c_str());
210
+ return;
211
+ }
212
+
213
+ JsonObject obj = doc.as<JsonObject>();
214
+ uint8_t msgType = obj["type"];
215
+
216
+ // Handle different package types
217
+ switch(msgType) {
218
+ case 204: // MetricsPackage (SENSOR_METRICS per schema v0.7.2+)
219
+ handleMetricsPackage(from, obj);
220
+ break;
221
+ case 605: // HealthCheckPackage (MESH_METRICS per schema v0.7.2+)
222
+ handleHealthPackage(from, obj);
223
+ break;
224
+ case CMD_METRICS_RESPONSE:
225
+ handleMetricsResponse(from, obj);
226
+ break;
227
+ case CMD_HEALTH_RESPONSE:
228
+ handleHealthResponse(from, obj);
229
+ break;
230
+ }
231
+
232
+ // Call original callback if set
233
+ if (originalReceiveCallback) {
234
+ originalReceiveCallback(from, msg);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Handle MetricsPackage received from mesh
240
+ */
241
+ void handleMetricsPackage(uint32_t from, JsonObject& obj) {
242
+ Serial.printf("Received MetricsPackage from node %u\n", from);
243
+
244
+ // Store in cache for aggregation
245
+ storeMetrics(from, obj);
246
+
247
+ // Publish to MQTT
248
+ publishMetricsToMQTT(from, obj);
249
+ }
250
+
251
+ /**
252
+ * Handle HealthCheckPackage received from mesh
253
+ */
254
+ void handleHealthPackage(uint32_t from, JsonObject& obj) {
255
+ Serial.printf("Received HealthCheckPackage from node %u\n", from);
256
+
257
+ // Store in cache for aggregation
258
+ storeHealth(from, obj);
259
+
260
+ // Publish to MQTT
261
+ publishHealthToMQTT(from, obj);
262
+
263
+ // Check for critical health and alert
264
+ uint8_t healthStatus = obj["health"];
265
+ if (healthStatus == 0) {
266
+ publishCriticalAlert(from, obj);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Store metrics in cache for aggregation
272
+ */
273
+ void storeMetrics(uint32_t nodeId, JsonObject& obj) {
274
+ // Find existing entry or add new
275
+ int index = findMetricsCacheIndex(nodeId);
276
+ if (index == -1) {
277
+ if (metricsCacheSize < MAX_STORED_NODES) {
278
+ index = metricsCacheSize++;
279
+ } else {
280
+ // Cache full, replace oldest
281
+ index = 0;
282
+ }
283
+ }
284
+
285
+ metricsCache[index].nodeId = nodeId;
286
+ metricsCache[index].timestamp = obj["ts"];
287
+ metricsCache[index].cpuUsage = obj["cpu"];
288
+ metricsCache[index].freeHeap = obj["heap"];
289
+ metricsCache[index].currentThroughput = obj["throughput"];
290
+ metricsCache[index].connectionQuality = obj["connQual"];
291
+ metricsCache[index].wifiRSSI = obj["rssi"];
292
+ }
293
+
294
+ /**
295
+ * Store health in cache for aggregation
296
+ */
297
+ void storeHealth(uint32_t nodeId, JsonObject& obj) {
298
+ // Find existing entry or add new
299
+ int index = findHealthCacheIndex(nodeId);
300
+ if (index == -1) {
301
+ if (healthCacheSize < MAX_STORED_NODES) {
302
+ index = healthCacheSize++;
303
+ } else {
304
+ // Cache full, replace oldest
305
+ index = 0;
306
+ }
307
+ }
308
+
309
+ healthCache[index].nodeId = nodeId;
310
+ healthCache[index].timestamp = obj["ts"];
311
+ healthCache[index].healthStatus = obj["health"];
312
+ healthCache[index].problemFlags = obj["problems"];
313
+ healthCache[index].memoryHealth = obj["memHealth"];
314
+ healthCache[index].networkHealth = obj["netHealth"];
315
+ healthCache[index].performanceHealth = obj["perfHealth"];
316
+ }
317
+
318
+ /**
319
+ * Find metrics cache index for node
320
+ */
321
+ int findMetricsCacheIndex(uint32_t nodeId) {
322
+ for (int i = 0; i < metricsCacheSize; i++) {
323
+ if (metricsCache[i].nodeId == nodeId) {
324
+ return i;
325
+ }
326
+ }
327
+ return -1;
328
+ }
329
+
330
+ /**
331
+ * Find health cache index for node
332
+ */
333
+ int findHealthCacheIndex(uint32_t nodeId) {
334
+ for (int i = 0; i < healthCacheSize; i++) {
335
+ if (healthCache[i].nodeId == nodeId) {
336
+ return i;
337
+ }
338
+ }
339
+ return -1;
340
+ }
341
+
342
+ /**
343
+ * Publish metrics to MQTT
344
+ */
345
+ void publishMetricsToMQTT(uint32_t from, JsonObject& obj) {
346
+ String topic = topicPrefix + "metrics/" + String(from);
347
+
348
+ // Build payload
349
+ String payload;
350
+ serializeJson(obj, payload);
351
+
352
+ mqttClient.publish(topic.c_str(), payload.c_str());
353
+ }
354
+
355
+ /**
356
+ * Publish health to MQTT
357
+ */
358
+ void publishHealthToMQTT(uint32_t from, JsonObject& obj) {
359
+ String topic = topicPrefix + "health/" + String(from);
360
+
361
+ // Build payload
362
+ String payload;
363
+ serializeJson(obj, payload);
364
+
365
+ mqttClient.publish(topic.c_str(), payload.c_str());
366
+ }
367
+
368
+ /**
369
+ * Publish critical health alert
370
+ */
371
+ void publishCriticalAlert(uint32_t from, JsonObject& obj) {
372
+ String topic = topicPrefix + "alerts/critical";
373
+
374
+ String payload = "{";
375
+ payload += "\"alert_type\":\"critical_health\",";
376
+ payload += "\"node_id\":" + String(from) + ",";
377
+ payload += "\"health_status\":" + String((uint8_t)obj["health"]) + ",";
378
+ payload += "\"problem_flags\":" + String((uint16_t)obj["problems"]) + ",";
379
+ payload += "\"recommendations\":\"" + obj["recommend"].as<String>() + "\",";
380
+ payload += "\"timestamp\":\"" + buildISO8601Timestamp() + "\"";
381
+ payload += "}";
382
+
383
+ mqttClient.publish(topic.c_str(), payload.c_str());
384
+ Serial.printf("CRITICAL ALERT: Node %u health is critical!\n", from);
385
+ }
386
+
387
+ /**
388
+ * Handle request metrics command from MQTT
389
+ */
390
+ void handleRequestMetrics(String& payload) {
391
+ Serial.println("Handling request_metrics command");
392
+
393
+ // Parse payload to get target node (0 = all nodes)
394
+ DynamicJsonDocument doc(256);
395
+ deserializeJson(doc, payload);
396
+ uint32_t targetNode = doc["node_id"] | 0;
397
+
398
+ // Create command package
399
+ CommandPackage cmd;
400
+ cmd.from = mesh.getNodeId();
401
+ cmd.dest = targetNode;
402
+ cmd.command = CMD_REQUEST_METRICS;
403
+ cmd.commandId = millis();
404
+
405
+ // Send to mesh
406
+ String msg = cmd.toJsonString();
407
+ if (targetNode == 0) {
408
+ mesh.sendBroadcast(msg);
409
+ Serial.println("Metrics requested from all nodes");
410
+ } else {
411
+ mesh.sendSingle(targetNode, msg);
412
+ Serial.printf("Metrics requested from node %u\n", targetNode);
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Handle request health command from MQTT
418
+ */
419
+ void handleRequestHealth(String& payload) {
420
+ Serial.println("Handling request_health command");
421
+
422
+ // Parse payload to get target node (0 = all nodes)
423
+ DynamicJsonDocument doc(256);
424
+ deserializeJson(doc, payload);
425
+ uint32_t targetNode = doc["node_id"] | 0;
426
+
427
+ // Create command package
428
+ CommandPackage cmd;
429
+ cmd.from = mesh.getNodeId();
430
+ cmd.dest = targetNode;
431
+ cmd.command = CMD_REQUEST_HEALTH;
432
+ cmd.commandId = millis();
433
+
434
+ // Send to mesh
435
+ String msg = cmd.toJsonString();
436
+ if (targetNode == 0) {
437
+ mesh.sendBroadcast(msg);
438
+ Serial.println("Health check requested from all nodes");
439
+ } else {
440
+ mesh.sendSingle(targetNode, msg);
441
+ Serial.printf("Health check requested from node %u\n", targetNode);
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Handle get aggregated command from MQTT
447
+ */
448
+ void handleGetAggregated(String& payload) {
449
+ Serial.println("Handling get_aggregated command");
450
+
451
+ // Immediately publish current aggregated stats
452
+ publishAggregatedMetrics();
453
+ publishAggregatedHealth();
454
+ }
455
+
456
+ /**
457
+ * Handle metrics response from node
458
+ */
459
+ void handleMetricsResponse(uint32_t from, JsonObject& obj) {
460
+ Serial.printf("Received metrics response from node %u\n", from);
461
+
462
+ // Publish to response topic
463
+ String topic = topicPrefix + "response/metrics";
464
+ String payload;
465
+ serializeJson(obj, payload);
466
+ mqttClient.publish(topic.c_str(), payload.c_str());
467
+ }
468
+
469
+ /**
470
+ * Handle health response from node
471
+ */
472
+ void handleHealthResponse(uint32_t from, JsonObject& obj) {
473
+ Serial.printf("Received health response from node %u\n", from);
474
+
475
+ // Publish to response topic
476
+ String topic = topicPrefix + "response/health";
477
+ String payload;
478
+ serializeJson(obj, payload);
479
+ mqttClient.publish(topic.c_str(), payload.c_str());
480
+ }
481
+
482
+ /**
483
+ * Publish aggregated metrics
484
+ */
485
+ void publishAggregatedMetrics() {
486
+ if (metricsCacheSize == 0) {
487
+ Serial.println("No metrics to aggregate");
488
+ return;
489
+ }
490
+
491
+ // Calculate aggregated statistics
492
+ uint32_t totalCPU = 0;
493
+ uint32_t totalHeap = 0;
494
+ uint32_t totalThroughput = 0;
495
+ uint32_t totalQuality = 0;
496
+ int32_t totalRSSI = 0;
497
+ uint32_t minHeap = 0xFFFFFFFF;
498
+ uint32_t maxHeap = 0;
499
+ uint8_t minQuality = 100;
500
+ uint8_t maxQuality = 0;
501
+
502
+ for (int i = 0; i < metricsCacheSize; i++) {
503
+ totalCPU += metricsCache[i].cpuUsage;
504
+ totalHeap += metricsCache[i].freeHeap;
505
+ totalThroughput += metricsCache[i].currentThroughput;
506
+ totalQuality += metricsCache[i].connectionQuality;
507
+ totalRSSI += metricsCache[i].wifiRSSI;
508
+
509
+ if (metricsCache[i].freeHeap < minHeap) minHeap = metricsCache[i].freeHeap;
510
+ if (metricsCache[i].freeHeap > maxHeap) maxHeap = metricsCache[i].freeHeap;
511
+ if (metricsCache[i].connectionQuality < minQuality) minQuality = metricsCache[i].connectionQuality;
512
+ if (metricsCache[i].connectionQuality > maxQuality) maxQuality = metricsCache[i].connectionQuality;
513
+ }
514
+
515
+ // Build aggregated payload
516
+ String topic = topicPrefix + "aggregated/metrics";
517
+ String payload = "{";
518
+ payload += "\"node_count\":" + String(metricsCacheSize) + ",";
519
+ payload += "\"avg_cpu\":" + String(totalCPU / metricsCacheSize) + ",";
520
+ payload += "\"avg_heap\":" + String(totalHeap / metricsCacheSize) + ",";
521
+ payload += "\"min_heap\":" + String(minHeap) + ",";
522
+ payload += "\"max_heap\":" + String(maxHeap) + ",";
523
+ payload += "\"total_throughput\":" + String(totalThroughput) + ",";
524
+ payload += "\"avg_quality\":" + String(totalQuality / metricsCacheSize) + ",";
525
+ payload += "\"min_quality\":" + String(minQuality) + ",";
526
+ payload += "\"max_quality\":" + String(maxQuality) + ",";
527
+ payload += "\"avg_rssi\":" + String(totalRSSI / metricsCacheSize) + ",";
528
+ payload += "\"timestamp\":\"" + buildISO8601Timestamp() + "\"";
529
+ payload += "}";
530
+
531
+ mqttClient.publish(topic.c_str(), payload.c_str());
532
+ Serial.printf("Published aggregated metrics for %d nodes\n", metricsCacheSize);
533
+ }
534
+
535
+ /**
536
+ * Publish aggregated health
537
+ */
538
+ void publishAggregatedHealth() {
539
+ if (healthCacheSize == 0) {
540
+ Serial.println("No health data to aggregate");
541
+ return;
542
+ }
543
+
544
+ // Count nodes by health status
545
+ uint8_t healthyCount = 0;
546
+ uint8_t warningCount = 0;
547
+ uint8_t criticalCount = 0;
548
+
549
+ // Aggregate problem flags
550
+ uint16_t aggregatedProblems = 0;
551
+
552
+ // Average health scores
553
+ uint32_t totalMemHealth = 0;
554
+ uint32_t totalNetHealth = 0;
555
+ uint32_t totalPerfHealth = 0;
556
+
557
+ for (int i = 0; i < healthCacheSize; i++) {
558
+ // Count by status
559
+ if (healthCache[i].healthStatus == 2) healthyCount++;
560
+ else if (healthCache[i].healthStatus == 1) warningCount++;
561
+ else criticalCount++;
562
+
563
+ // Aggregate problems
564
+ aggregatedProblems |= healthCache[i].problemFlags;
565
+
566
+ // Sum health scores
567
+ totalMemHealth += healthCache[i].memoryHealth;
568
+ totalNetHealth += healthCache[i].networkHealth;
569
+ totalPerfHealth += healthCache[i].performanceHealth;
570
+ }
571
+
572
+ // Determine overall mesh health
573
+ uint8_t meshHealth = 2; // Healthy
574
+ if (criticalCount > 0) meshHealth = 0; // Critical
575
+ else if (warningCount > 0) meshHealth = 1; // Warning
576
+
577
+ // Build aggregated payload
578
+ String topic = topicPrefix + "aggregated/health";
579
+ String payload = "{";
580
+ payload += "\"node_count\":" + String(healthCacheSize) + ",";
581
+ payload += "\"mesh_health\":" + String(meshHealth) + ",";
582
+ payload += "\"healthy_nodes\":" + String(healthyCount) + ",";
583
+ payload += "\"warning_nodes\":" + String(warningCount) + ",";
584
+ payload += "\"critical_nodes\":" + String(criticalCount) + ",";
585
+ payload += "\"aggregated_problems\":" + String(aggregatedProblems) + ",";
586
+ payload += "\"avg_memory_health\":" + String(totalMemHealth / healthCacheSize) + ",";
587
+ payload += "\"avg_network_health\":" + String(totalNetHealth / healthCacheSize) + ",";
588
+ payload += "\"avg_performance_health\":" + String(totalPerfHealth / healthCacheSize) + ",";
589
+ payload += "\"timestamp\":\"" + buildISO8601Timestamp() + "\"";
590
+ payload += "}";
591
+
592
+ mqttClient.publish(topic.c_str(), payload.c_str());
593
+ Serial.printf("Published aggregated health: %d healthy, %d warning, %d critical\n",
594
+ healthyCount, warningCount, criticalCount);
595
+ }
596
+
597
+ /**
598
+ * Build ISO 8601 timestamp
599
+ */
600
+ String buildISO8601Timestamp() {
601
+ // Simplified - in production use NTP time
602
+ uint32_t uptime = millis() / 1000;
603
+ char buffer[32];
604
+ snprintf(buffer, sizeof(buffer), "1970-01-01T00:%02d:%02dZ",
605
+ (uptime / 60) % 60, uptime % 60);
606
+ return String(buffer);
607
+ }
608
+ };
609
+
610
+ #endif // _ENHANCED_MQTT_BRIDGE_HPP_