@alteriom/painlessmesh 1.6.1 → 1.7.2
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 +380 -143
- package/LICENSE +674 -674
- package/README.md +477 -434
- package/RELEASE_GUIDE.md +504 -418
- package/docs/DOCUMENTATION_MIGRATION_PLAN.md +175 -175
- package/docs/IMPLEMENTATION_PLAN_MESH_TOPOLOGY.md +1062 -0
- package/docs/MESH_TOPOLOGY_GUIDE.md +992 -0
- package/docs/MESH_TOPOLOGY_PROGRESS.md +422 -0
- package/docs/MQTT_BRIDGE_COMMANDS.md +894 -0
- package/docs/MQTT_BRIDGE_IMPLEMENTATION_SUMMARY.md +324 -0
- package/docs/MQTT_COMMAND_SCHEMA_PROPOSAL.md +576 -0
- package/docs/MQTT_SCHEMA_COMPLIANCE.md +285 -0
- package/docs/MQTT_SCHEMA_PROPOSALS.md +446 -0
- package/docs/MQTT_SCHEMA_REVIEW.md +690 -0
- package/docs/OTA_COMMANDS_REFERENCE.md +554 -0
- package/docs/PHASE1_GUIDE.md +349 -0
- package/docs/PHASE2_GUIDE.md +543 -0
- package/docs/README.md +77 -70
- package/docs/SCHEMA_VALIDATION_CHECKLIST.md +222 -0
- package/docs/alteriom/overview.md +507 -507
- package/docs/api/core-api.md +606 -606
- package/docs/architecture/mesh-architecture.md +378 -378
- package/docs/architecture/plugin-system.md +516 -516
- package/docs/getting-started/first-mesh.md +409 -409
- package/docs/getting-started/installation.md +274 -274
- package/docs/getting-started/quickstart.md +157 -157
- package/docs/improvements/FEATURE_PROPOSALS.md +337 -0
- package/docs/improvements/PHASE1_IMPLEMENTATION.md +325 -0
- package/docs/improvements/PHASE2_IMPLEMENTATION.md +567 -0
- package/docs/improvements/README.md +86 -68
- package/docs/improvements/ota-and-status-enhancements.md +911 -0
- package/docs/improvements/ota-status-architecture-diagrams.md +658 -0
- package/docs/improvements/ota-status-quick-reference.md +284 -0
- package/docs/platformio-publishing.md +255 -0
- package/docs/platformio-setup-summary.md +121 -0
- package/docs/troubleshooting/common-issues.md +520 -520
- package/docs/troubleshooting/faq.md +472 -472
- package/docs/tutorials/basic-examples.md +717 -717
- package/docs/wiki/API-Reference.md +245 -245
- package/docs/wiki/Complete-Documentation.md +122 -122
- package/examples/alteriom/README.md +139 -81
- package/examples/alteriom/alteriom.ino +186 -185
- package/examples/alteriom/alteriom_sensor_package.hpp +240 -127
- package/examples/alteriom/platformio.ini +24 -24
- package/examples/alteriomImproved/alteriom_sensor_package.hpp +224 -0
- package/examples/{alteriom → alteriomImproved}/improved_sensor_node.ino +245 -245
- package/examples/alteriomImproved/platformio.ini +25 -0
- package/examples/alteriomPhase1/alteriom_sensor_package.hpp +224 -0
- package/examples/alteriomPhase1/phase1_features.ino +242 -0
- package/examples/alteriomPhase1/platformio.ini +25 -0
- package/examples/alteriomPhase2/alteriom_sensor_package.hpp +224 -0
- package/examples/alteriomPhase2/phase2_features.ino +186 -0
- package/examples/alteriomPhase2/platformio.ini +25 -0
- package/examples/{alteriom → alteriomSensorNode}/alteriom_sensor_node.ino +183 -183
- package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +224 -0
- package/examples/alteriomSensorNode/platformio.ini +25 -0
- package/examples/basic/basic.ino +66 -66
- package/examples/basic/platformio.ini +25 -25
- package/examples/bridge/bridge.ino +51 -51
- package/examples/bridge/mesh_event_publisher.hpp +253 -0
- package/examples/bridge/mesh_topology_reporter.hpp +303 -0
- package/examples/bridge/mqtt_command_bridge.hpp +459 -0
- package/examples/bridge/mqtt_status_bridge.hpp +519 -0
- package/examples/bridge/platformio.ini +25 -25
- package/examples/echoNode/echoNode.ino +33 -33
- package/examples/echoNode/platformio.ini +25 -25
- package/examples/logClient/logClient.ino +109 -109
- package/examples/logClient/platformio.ini +25 -25
- package/examples/logServer/logServer.ino +81 -81
- package/examples/logServer/platformio.ini +25 -25
- package/examples/meshCommandNode/alteriom_sensor_package.hpp +235 -0
- package/examples/meshCommandNode/meshCommandNode.ino +263 -0
- package/examples/meshCommandNode/platformio.ini +25 -0
- package/examples/mqttBridge/mqttBridge.ino +118 -118
- package/examples/mqttBridge/platformio.ini +26 -26
- package/examples/mqttCommandBridge/alteriom_sensor_package.hpp +235 -0
- package/examples/mqttCommandBridge/mesh_event_publisher.hpp +253 -0
- package/examples/mqttCommandBridge/mesh_topology_reporter.hpp +303 -0
- package/examples/mqttCommandBridge/mqttCommandBridge.ino +252 -0
- package/examples/mqttCommandBridge/mqtt_command_bridge.hpp +459 -0
- package/examples/mqttCommandBridge/platformio.ini +26 -0
- package/examples/mqttStatusBridge/mqttStatusBridge.ino +216 -0
- package/examples/mqttStatusBridge/mqtt_status_bridge.hpp +522 -0
- package/examples/mqttStatusBridge/platformio.ini +26 -0
- package/examples/mqttTopologyTest/README.md +467 -0
- package/examples/mqttTopologyTest/mqttTopologyTest.ino +748 -0
- package/examples/mqttTopologyTest/platformio.ini +26 -0
- package/examples/namedMesh/namedMesh.ino +97 -97
- package/examples/namedMesh/platformio.ini +25 -25
- package/examples/otaReceiver/otaReceiver.ino +79 -79
- package/examples/otaReceiver/platformio.ini +25 -25
- package/examples/otaSender/otaSender.ino +160 -151
- package/examples/otaSender/platformio.ini +25 -25
- package/examples/startHere/platformio.ini +25 -25
- package/examples/startHere/startHere.ino +159 -159
- package/examples/webServer/platformio.ini +27 -27
- package/examples/webServer/webServer.ino +89 -89
- package/keywords.txt +48 -48
- package/library.json +55 -34
- package/library.properties +10 -10
- package/package.json +86 -78
- package/src/AlteriomPainlessMesh.h +97 -97
- package/src/arduino/wifi.hpp +365 -365
- package/src/boost/asynctcp.hpp +279 -279
- package/src/painlessMesh.h +70 -70
- package/src/painlessMeshSTA.cpp +236 -236
- package/src/painlessMeshSTA.h +58 -58
- package/src/painlessTaskOptions.h +4 -4
- package/src/painlessmesh/base64.hpp +111 -111
- package/src/painlessmesh/buffer.hpp +229 -229
- package/src/painlessmesh/callback.hpp +91 -91
- package/src/painlessmesh/configuration.hpp +77 -77
- package/src/painlessmesh/connection.hpp +192 -192
- package/src/painlessmesh/layout.hpp +188 -188
- package/src/painlessmesh/logger.hpp +158 -158
- package/src/painlessmesh/memory.hpp +119 -119
- package/src/painlessmesh/mesh.hpp +761 -560
- package/src/painlessmesh/metrics.hpp +322 -322
- package/src/painlessmesh/ntp.hpp +263 -263
- package/src/painlessmesh/ota.hpp +582 -553
- package/src/painlessmesh/plugin.hpp +188 -188
- package/src/painlessmesh/protocol.hpp +813 -813
- package/src/painlessmesh/router.hpp +322 -322
- package/src/painlessmesh/tcp.hpp +71 -71
- package/src/painlessmesh/validation.hpp +238 -238
- package/src/plugin/performance.hpp +214 -214
- package/src/plugin/remote.hpp +64 -64
- package/src/scheduler.cpp +10 -10
- package/src/wifi.cpp +2 -2
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
# painlessMesh Implementation Plan for MQTT Schema v0.5.0
|
|
2
|
+
|
|
3
|
+
**Date:** October 12, 2025
|
|
4
|
+
**Target:** Support mesh_topology.schema.json and mesh_event.schema.json
|
|
5
|
+
**Status:** 📋 IMPLEMENTATION PLAN
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Executive Summary
|
|
10
|
+
|
|
11
|
+
Assuming @alteriom/mqtt-schema v0.5.0 is released with the new mesh topology and event schemas, this document outlines **all required implementations** in the painlessMesh library to fully support these features.
|
|
12
|
+
|
|
13
|
+
### What's Already Done ✅
|
|
14
|
+
|
|
15
|
+
1. ✅ **Command Bridge** - MqttCommandBridge class (417 lines)
|
|
16
|
+
2. ✅ **Gateway Example** - mqttCommandBridge.ino with basic topology
|
|
17
|
+
3. ✅ **Node Handler** - mesh_command_node.ino with command responses
|
|
18
|
+
4. ✅ **Status Packages** - Enhanced StatusPackage with response tracking
|
|
19
|
+
5. ✅ **Basic Topology** - Simple node list publishing every 30s
|
|
20
|
+
|
|
21
|
+
### What Needs Implementation ⚠️
|
|
22
|
+
|
|
23
|
+
1. ⚠️ **Schema-Compliant Topology Reporter** - Full mesh structure with connections
|
|
24
|
+
2. ⚠️ **Connection Quality Tracking** - RSSI, latency, hop count per link
|
|
25
|
+
3. ⚠️ **Mesh Event Publisher** - Real-time node join/leave notifications
|
|
26
|
+
4. ⚠️ **Topology Command Handler** - `get_topology` command support
|
|
27
|
+
5. ⚠️ **Enhanced Gateway Bridge** - Schema v0.5.0 compliance
|
|
28
|
+
6. ⚠️ **Connection Metadata API** - Expose connection details from painlessMesh core
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Part 1: Core Library Enhancements
|
|
33
|
+
|
|
34
|
+
### 1.1 Add Connection Metadata API
|
|
35
|
+
|
|
36
|
+
**File:** `src/painlessmesh/mesh.hpp`
|
|
37
|
+
|
|
38
|
+
**Problem:** painlessMesh tracks connections internally but doesn't expose connection quality metrics (RSSI, latency, hop count).
|
|
39
|
+
|
|
40
|
+
**Solution:** Add public API to retrieve connection metadata.
|
|
41
|
+
|
|
42
|
+
#### New Public Methods
|
|
43
|
+
|
|
44
|
+
```cpp
|
|
45
|
+
/**
|
|
46
|
+
* @brief Get connection quality for a specific link
|
|
47
|
+
* @param fromNode Source node ID
|
|
48
|
+
* @param toNode Destination node ID
|
|
49
|
+
* @return Connection quality (0-100) or -1 if not connected
|
|
50
|
+
*/
|
|
51
|
+
int getConnectionQuality(uint32_t fromNode, uint32_t toNode);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @brief Get RSSI for a direct connection
|
|
55
|
+
* @param nodeId Connected node ID
|
|
56
|
+
* @return RSSI in dBm (typically -30 to -90) or 0 if not available
|
|
57
|
+
*/
|
|
58
|
+
int getConnectionRSSI(uint32_t nodeId);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @brief Get average latency to a node
|
|
62
|
+
* @param nodeId Target node ID
|
|
63
|
+
* @return Latency in milliseconds or -1 if not available
|
|
64
|
+
*/
|
|
65
|
+
int getConnectionLatency(uint32_t nodeId);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @brief Get hop count to a node
|
|
69
|
+
* @param nodeId Target node ID
|
|
70
|
+
* @return Number of hops or -1 if unreachable
|
|
71
|
+
*/
|
|
72
|
+
int getHopCount(uint32_t nodeId);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @brief Get all direct connections with metadata
|
|
76
|
+
* @return List of connection objects with quality metrics
|
|
77
|
+
*/
|
|
78
|
+
std::vector<ConnectionInfo> getConnectionDetails();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @brief Get routing table for mesh visualization
|
|
82
|
+
* @return Map of destination -> next hop mappings
|
|
83
|
+
*/
|
|
84
|
+
std::map<uint32_t, uint32_t> getRoutingTable();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### New Data Structure
|
|
88
|
+
|
|
89
|
+
```cpp
|
|
90
|
+
struct ConnectionInfo {
|
|
91
|
+
uint32_t nodeId; // Connected node ID
|
|
92
|
+
uint32_t lastSeen; // Timestamp of last message (ms)
|
|
93
|
+
int rssi; // Signal strength (dBm)
|
|
94
|
+
int avgLatency; // Average round-trip time (ms)
|
|
95
|
+
int hopCount; // Hops from current node
|
|
96
|
+
int quality; // Connection quality (0-100)
|
|
97
|
+
uint32_t messagesRx; // Messages received
|
|
98
|
+
uint32_t messagesTx; // Messages sent
|
|
99
|
+
uint32_t messagesDropped; // Failed transmissions
|
|
100
|
+
};
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Implementation Location
|
|
104
|
+
|
|
105
|
+
**File to modify:** `src/painlessmesh/mesh.hpp`
|
|
106
|
+
|
|
107
|
+
**Add after line 365 (after `subConnectionJson()`):**
|
|
108
|
+
|
|
109
|
+
```cpp
|
|
110
|
+
/**
|
|
111
|
+
* Get detailed connection information for all direct neighbors
|
|
112
|
+
*/
|
|
113
|
+
std::vector<ConnectionInfo> getConnectionDetails() {
|
|
114
|
+
std::vector<ConnectionInfo> connections;
|
|
115
|
+
|
|
116
|
+
for (auto conn : this->connections) {
|
|
117
|
+
if (conn->isConnected()) {
|
|
118
|
+
ConnectionInfo info;
|
|
119
|
+
info.nodeId = conn->nodeId;
|
|
120
|
+
info.lastSeen = conn->timeLastReceived;
|
|
121
|
+
info.rssi = conn->getRSSI(); // Need to add this to Connection class
|
|
122
|
+
info.avgLatency = conn->getLatency(); // Need to add this
|
|
123
|
+
info.hopCount = 1; // Direct connection
|
|
124
|
+
info.quality = conn->getQuality(); // Need to add this
|
|
125
|
+
info.messagesRx = conn->messagesRx; // Need to add counter
|
|
126
|
+
info.messagesTx = conn->messagesTx; // Need to add counter
|
|
127
|
+
info.messagesDropped = conn->messagesDropped; // Need to add counter
|
|
128
|
+
|
|
129
|
+
connections.push_back(info);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return connections;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get hop count to specific node using routing table
|
|
138
|
+
*/
|
|
139
|
+
int getHopCount(uint32_t nodeId) {
|
|
140
|
+
auto route = this->findRoute(nodeId);
|
|
141
|
+
if (route.empty()) return -1;
|
|
142
|
+
return route.size() - 1; // Don't count source node
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get routing table as map (destination -> next hop)
|
|
147
|
+
*/
|
|
148
|
+
std::map<uint32_t, uint32_t> getRoutingTable() {
|
|
149
|
+
std::map<uint32_t, uint32_t> table;
|
|
150
|
+
auto nodeList = this->getNodeList(false);
|
|
151
|
+
|
|
152
|
+
for (auto destNode : nodeList) {
|
|
153
|
+
auto route = this->findRoute(destNode);
|
|
154
|
+
if (!route.empty() && route.size() > 1) {
|
|
155
|
+
table[destNode] = route[1]; // Next hop after source
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return table;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Estimated Effort:** 4-6 hours (requires adding metrics tracking to Connection class)
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### 1.2 Enhance Connection Class with Metrics
|
|
168
|
+
|
|
169
|
+
**File:** `src/painlessmesh/tcp.hpp` (or wherever Connection is defined)
|
|
170
|
+
|
|
171
|
+
**Add to Connection class:**
|
|
172
|
+
|
|
173
|
+
```cpp
|
|
174
|
+
class Connection {
|
|
175
|
+
public:
|
|
176
|
+
// Existing members...
|
|
177
|
+
|
|
178
|
+
// New metrics tracking
|
|
179
|
+
uint32_t messagesRx = 0;
|
|
180
|
+
uint32_t messagesTx = 0;
|
|
181
|
+
uint32_t messagesDropped = 0;
|
|
182
|
+
uint32_t timeLastReceived = 0;
|
|
183
|
+
|
|
184
|
+
// Latency tracking
|
|
185
|
+
std::vector<uint32_t> latencySamples;
|
|
186
|
+
const size_t MAX_LATENCY_SAMPLES = 10;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Record message received timestamp
|
|
190
|
+
*/
|
|
191
|
+
void onMessageReceived() {
|
|
192
|
+
messagesRx++;
|
|
193
|
+
timeLastReceived = millis();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Record message sent
|
|
198
|
+
*/
|
|
199
|
+
void onMessageSent(bool success) {
|
|
200
|
+
if (success) {
|
|
201
|
+
messagesTx++;
|
|
202
|
+
} else {
|
|
203
|
+
messagesDropped++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Record round-trip time sample
|
|
209
|
+
*/
|
|
210
|
+
void recordLatency(uint32_t latencyMs) {
|
|
211
|
+
latencySamples.push_back(latencyMs);
|
|
212
|
+
if (latencySamples.size() > MAX_LATENCY_SAMPLES) {
|
|
213
|
+
latencySamples.erase(latencySamples.begin());
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get average latency from recent samples
|
|
219
|
+
*/
|
|
220
|
+
int getLatency() {
|
|
221
|
+
if (latencySamples.empty()) return -1;
|
|
222
|
+
|
|
223
|
+
uint32_t sum = 0;
|
|
224
|
+
for (auto sample : latencySamples) {
|
|
225
|
+
sum += sample;
|
|
226
|
+
}
|
|
227
|
+
return sum / latencySamples.size();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Calculate connection quality (0-100)
|
|
232
|
+
* Based on: latency, packet loss, RSSI
|
|
233
|
+
*/
|
|
234
|
+
int getQuality() {
|
|
235
|
+
// Simple quality calculation
|
|
236
|
+
int quality = 100;
|
|
237
|
+
|
|
238
|
+
// Penalize high latency (>100ms)
|
|
239
|
+
int latency = getLatency();
|
|
240
|
+
if (latency > 100) quality -= (latency - 100) / 5;
|
|
241
|
+
|
|
242
|
+
// Penalize packet loss
|
|
243
|
+
if (messagesTx > 0) {
|
|
244
|
+
int lossRate = (messagesDropped * 100) / messagesTx;
|
|
245
|
+
quality -= lossRate;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Penalize weak RSSI (if available)
|
|
249
|
+
int rssi = getRSSI();
|
|
250
|
+
if (rssi < -80) quality -= (80 + rssi);
|
|
251
|
+
|
|
252
|
+
return max(0, min(100, quality));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get WiFi RSSI if available
|
|
257
|
+
* Note: Requires ESP8266/ESP32 specific code
|
|
258
|
+
*/
|
|
259
|
+
int getRSSI() {
|
|
260
|
+
#if defined(ESP32) || defined(ESP8266)
|
|
261
|
+
// This needs platform-specific implementation
|
|
262
|
+
// For now, return placeholder
|
|
263
|
+
return -60; // TODO: Get actual RSSI from WiFi
|
|
264
|
+
#else
|
|
265
|
+
return 0; // Not available on non-WiFi platforms
|
|
266
|
+
#endif
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Estimated Effort:** 6-8 hours (requires testing on ESP32/ESP8266)
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Part 2: Topology Reporter Implementation
|
|
276
|
+
|
|
277
|
+
### 2.1 Create MeshTopologyReporter Class
|
|
278
|
+
|
|
279
|
+
**File:** `examples/bridge/mesh_topology_reporter.hpp` (NEW)
|
|
280
|
+
|
|
281
|
+
**Purpose:** Generate schema-compliant topology messages
|
|
282
|
+
|
|
283
|
+
```cpp
|
|
284
|
+
#ifndef MESH_TOPOLOGY_REPORTER_HPP
|
|
285
|
+
#define MESH_TOPOLOGY_REPORTER_HPP
|
|
286
|
+
|
|
287
|
+
#include "painlessMesh.h"
|
|
288
|
+
#include <ArduinoJson.h>
|
|
289
|
+
|
|
290
|
+
class MeshTopologyReporter {
|
|
291
|
+
private:
|
|
292
|
+
painlessMesh& mesh;
|
|
293
|
+
String meshId;
|
|
294
|
+
String lastTopologyHash; // Detect changes
|
|
295
|
+
|
|
296
|
+
public:
|
|
297
|
+
MeshTopologyReporter(painlessMesh& mesh, const String& meshId)
|
|
298
|
+
: mesh(mesh), meshId(meshId) {}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Generate full topology message (schema v0.5.0 compliant)
|
|
302
|
+
*/
|
|
303
|
+
String generateFullTopology() {
|
|
304
|
+
DynamicJsonDocument doc(8192); // Large buffer for full topology
|
|
305
|
+
|
|
306
|
+
// Envelope fields (from @alteriom/mqtt-schema)
|
|
307
|
+
doc["schema_version"] = 1;
|
|
308
|
+
doc["device_id"] = String(mesh.getNodeId());
|
|
309
|
+
doc["device_type"] = "gateway";
|
|
310
|
+
doc["timestamp"] = getISO8601Timestamp();
|
|
311
|
+
doc["firmware_version"] = "GW 2.3.4"; // Make configurable
|
|
312
|
+
|
|
313
|
+
// Event discriminator
|
|
314
|
+
doc["event"] = "mesh_topology";
|
|
315
|
+
|
|
316
|
+
// Mesh identification
|
|
317
|
+
doc["mesh_id"] = meshId;
|
|
318
|
+
doc["gateway_node_id"] = String(mesh.getNodeId());
|
|
319
|
+
|
|
320
|
+
// Nodes array
|
|
321
|
+
JsonArray nodes = doc.createNestedArray("nodes");
|
|
322
|
+
addNodeInfo(nodes, mesh.getNodeId(), "gateway"); // Self
|
|
323
|
+
|
|
324
|
+
auto nodeList = mesh.getNodeList(false);
|
|
325
|
+
for (auto nodeId : nodeList) {
|
|
326
|
+
addNodeInfo(nodes, nodeId, "sensor"); // Assume sensor role
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Connections array
|
|
330
|
+
JsonArray connections = doc.createNestedArray("connections");
|
|
331
|
+
addConnectionInfo(connections);
|
|
332
|
+
|
|
333
|
+
// Network metrics
|
|
334
|
+
JsonObject metrics = doc.createNestedObject("metrics");
|
|
335
|
+
metrics["total_nodes"] = nodeList.size() + 1; // +1 for gateway
|
|
336
|
+
metrics["online_nodes"] = nodeList.size() + 1;
|
|
337
|
+
metrics["network_diameter"] = calculateNetworkDiameter();
|
|
338
|
+
metrics["avg_connection_quality"] = calculateAvgQuality();
|
|
339
|
+
metrics["messages_per_second"] = calculateThroughput();
|
|
340
|
+
|
|
341
|
+
doc["update_type"] = "full";
|
|
342
|
+
|
|
343
|
+
String output;
|
|
344
|
+
serializeJson(doc, output);
|
|
345
|
+
return output;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Generate incremental topology update
|
|
350
|
+
*/
|
|
351
|
+
String generateIncrementalUpdate(uint32_t nodeId, bool joined) {
|
|
352
|
+
DynamicJsonDocument doc(2048);
|
|
353
|
+
|
|
354
|
+
// Envelope fields
|
|
355
|
+
doc["schema_version"] = 1;
|
|
356
|
+
doc["device_id"] = String(mesh.getNodeId());
|
|
357
|
+
doc["device_type"] = "gateway";
|
|
358
|
+
doc["timestamp"] = getISO8601Timestamp();
|
|
359
|
+
doc["firmware_version"] = "GW 2.3.4";
|
|
360
|
+
|
|
361
|
+
doc["event"] = "mesh_topology";
|
|
362
|
+
doc["mesh_id"] = meshId;
|
|
363
|
+
doc["gateway_node_id"] = String(mesh.getNodeId());
|
|
364
|
+
|
|
365
|
+
// Node that changed
|
|
366
|
+
JsonArray nodes = doc.createNestedArray("nodes");
|
|
367
|
+
if (joined) {
|
|
368
|
+
addNodeInfo(nodes, nodeId, "sensor");
|
|
369
|
+
} else {
|
|
370
|
+
// For node leaving, mark as offline
|
|
371
|
+
JsonObject node = nodes.createNestedObject();
|
|
372
|
+
node["node_id"] = String(nodeId);
|
|
373
|
+
node["status"] = "offline";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Update connections
|
|
377
|
+
JsonArray connections = doc.createNestedArray("connections");
|
|
378
|
+
// Add connections related to this node
|
|
379
|
+
|
|
380
|
+
doc["update_type"] = "incremental";
|
|
381
|
+
|
|
382
|
+
String output;
|
|
383
|
+
serializeJson(doc, output);
|
|
384
|
+
return output;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Check if topology has changed
|
|
389
|
+
*/
|
|
390
|
+
bool hasTopologyChanged() {
|
|
391
|
+
String currentHash = calculateTopologyHash();
|
|
392
|
+
if (currentHash != lastTopologyHash) {
|
|
393
|
+
lastTopologyHash = currentHash;
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private:
|
|
400
|
+
/**
|
|
401
|
+
* Add node information to JSON array
|
|
402
|
+
*/
|
|
403
|
+
void addNodeInfo(JsonArray& nodes, uint32_t nodeId, const char* role) {
|
|
404
|
+
JsonObject node = nodes.createNestedObject();
|
|
405
|
+
node["node_id"] = String(nodeId);
|
|
406
|
+
node["role"] = role;
|
|
407
|
+
node["status"] = "online";
|
|
408
|
+
node["last_seen"] = getISO8601Timestamp();
|
|
409
|
+
|
|
410
|
+
// If we have connection to this node, add metrics
|
|
411
|
+
// This requires connection metadata API
|
|
412
|
+
node["uptime_seconds"] = 0; // TODO: Track per-node uptime
|
|
413
|
+
node["free_memory_kb"] = 0; // TODO: Get from node status messages
|
|
414
|
+
node["connection_count"] = 0; // TODO: Calculate from mesh structure
|
|
415
|
+
|
|
416
|
+
// Gateway node has additional info
|
|
417
|
+
if (nodeId == mesh.getNodeId()) {
|
|
418
|
+
node["uptime_seconds"] = millis() / 1000;
|
|
419
|
+
node["free_memory_kb"] = ESP.getFreeHeap() / 1024;
|
|
420
|
+
node["connection_count"] = mesh.getNodeList().size();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Add connection information to JSON array
|
|
426
|
+
*/
|
|
427
|
+
void addConnectionInfo(JsonArray& connections) {
|
|
428
|
+
// Get direct connections with quality metrics
|
|
429
|
+
// This requires new API: mesh.getConnectionDetails()
|
|
430
|
+
|
|
431
|
+
auto nodeList = mesh.getNodeList(false);
|
|
432
|
+
uint32_t gatewayId = mesh.getNodeId();
|
|
433
|
+
|
|
434
|
+
// For now, create basic connection data
|
|
435
|
+
// TODO: Use actual connection quality API
|
|
436
|
+
for (auto nodeId : nodeList) {
|
|
437
|
+
JsonObject conn = connections.createNestedObject();
|
|
438
|
+
conn["from_node"] = String(gatewayId);
|
|
439
|
+
conn["to_node"] = String(nodeId);
|
|
440
|
+
conn["quality"] = 85; // TODO: Get actual quality
|
|
441
|
+
conn["latency_ms"] = 20; // TODO: Get actual latency
|
|
442
|
+
conn["rssi"] = -55; // TODO: Get actual RSSI
|
|
443
|
+
conn["hop_count"] = 1; // TODO: Calculate hop count
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Calculate network diameter (max hop count)
|
|
449
|
+
*/
|
|
450
|
+
int calculateNetworkDiameter() {
|
|
451
|
+
int maxHops = 0;
|
|
452
|
+
auto nodeList = mesh.getNodeList(false);
|
|
453
|
+
|
|
454
|
+
for (auto nodeId : nodeList) {
|
|
455
|
+
// TODO: Use mesh.getHopCount(nodeId)
|
|
456
|
+
// For now assume max 2 hops
|
|
457
|
+
maxHops = max(maxHops, 2);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return maxHops;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Calculate average connection quality
|
|
465
|
+
*/
|
|
466
|
+
float calculateAvgQuality() {
|
|
467
|
+
// TODO: Use connection quality API
|
|
468
|
+
// For now return placeholder
|
|
469
|
+
return 85.0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Calculate network throughput
|
|
474
|
+
*/
|
|
475
|
+
float calculateThroughput() {
|
|
476
|
+
// TODO: Track message rates
|
|
477
|
+
// For now return placeholder
|
|
478
|
+
return 12.4;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Calculate topology hash for change detection
|
|
483
|
+
*/
|
|
484
|
+
String calculateTopologyHash() {
|
|
485
|
+
auto nodeList = mesh.getNodeList(true);
|
|
486
|
+
String hash = String(nodeList.size());
|
|
487
|
+
for (auto nodeId : nodeList) {
|
|
488
|
+
hash += String(nodeId);
|
|
489
|
+
}
|
|
490
|
+
return hash;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Get current timestamp in ISO 8601 format
|
|
495
|
+
*/
|
|
496
|
+
String getISO8601Timestamp() {
|
|
497
|
+
// TODO: Use proper NTP time if available
|
|
498
|
+
// For now return epoch-based timestamp
|
|
499
|
+
unsigned long ms = millis();
|
|
500
|
+
char timestamp[32];
|
|
501
|
+
snprintf(timestamp, sizeof(timestamp), "2025-10-12T%02lu:%02lu:%02luZ",
|
|
502
|
+
(ms / 3600000) % 24,
|
|
503
|
+
(ms / 60000) % 60,
|
|
504
|
+
(ms / 1000) % 60);
|
|
505
|
+
return String(timestamp);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
#endif // MESH_TOPOLOGY_REPORTER_HPP
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Estimated Effort:** 8-10 hours (initial implementation + testing)
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### 2.2 Create MeshEventPublisher Class
|
|
517
|
+
|
|
518
|
+
**File:** `examples/bridge/mesh_event_publisher.hpp` (NEW)
|
|
519
|
+
|
|
520
|
+
**Purpose:** Publish real-time mesh events
|
|
521
|
+
|
|
522
|
+
```cpp
|
|
523
|
+
#ifndef MESH_EVENT_PUBLISHER_HPP
|
|
524
|
+
#define MESH_EVENT_PUBLISHER_HPP
|
|
525
|
+
|
|
526
|
+
#include "painlessMesh.h"
|
|
527
|
+
#include <PubSubClient.h>
|
|
528
|
+
#include <ArduinoJson.h>
|
|
529
|
+
|
|
530
|
+
class MeshEventPublisher {
|
|
531
|
+
private:
|
|
532
|
+
painlessMesh& mesh;
|
|
533
|
+
PubSubClient& mqttClient;
|
|
534
|
+
String meshId;
|
|
535
|
+
String eventTopic;
|
|
536
|
+
|
|
537
|
+
public:
|
|
538
|
+
MeshEventPublisher(painlessMesh& mesh, PubSubClient& mqttClient,
|
|
539
|
+
const String& meshId, const String& topic = "mesh/events")
|
|
540
|
+
: mesh(mesh), mqttClient(mqttClient), meshId(meshId), eventTopic(topic) {}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Publish node join event
|
|
544
|
+
*/
|
|
545
|
+
void publishNodeJoin(uint32_t nodeId) {
|
|
546
|
+
DynamicJsonDocument doc(1024);
|
|
547
|
+
|
|
548
|
+
// Envelope
|
|
549
|
+
doc["schema_version"] = 1;
|
|
550
|
+
doc["device_id"] = String(mesh.getNodeId());
|
|
551
|
+
doc["device_type"] = "gateway";
|
|
552
|
+
doc["timestamp"] = getISO8601Timestamp();
|
|
553
|
+
doc["firmware_version"] = "GW 2.3.4";
|
|
554
|
+
|
|
555
|
+
// Event
|
|
556
|
+
doc["event"] = "mesh_event";
|
|
557
|
+
doc["event_type"] = "node_join";
|
|
558
|
+
doc["mesh_id"] = meshId;
|
|
559
|
+
|
|
560
|
+
JsonArray affected = doc.createNestedArray("affected_nodes");
|
|
561
|
+
affected.add(String(nodeId));
|
|
562
|
+
|
|
563
|
+
JsonObject details = doc.createNestedObject("details");
|
|
564
|
+
details["total_nodes"] = mesh.getNodeList().size() + 1;
|
|
565
|
+
details["timestamp"] = getISO8601Timestamp();
|
|
566
|
+
|
|
567
|
+
publishEvent(doc);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Publish node leave event
|
|
572
|
+
*/
|
|
573
|
+
void publishNodeLeave(uint32_t nodeId) {
|
|
574
|
+
DynamicJsonDocument doc(1024);
|
|
575
|
+
|
|
576
|
+
// Envelope
|
|
577
|
+
doc["schema_version"] = 1;
|
|
578
|
+
doc["device_id"] = String(mesh.getNodeId());
|
|
579
|
+
doc["device_type"] = "gateway";
|
|
580
|
+
doc["timestamp"] = getISO8601Timestamp();
|
|
581
|
+
doc["firmware_version"] = "GW 2.3.4";
|
|
582
|
+
|
|
583
|
+
// Event
|
|
584
|
+
doc["event"] = "mesh_event";
|
|
585
|
+
doc["event_type"] = "node_leave";
|
|
586
|
+
doc["mesh_id"] = meshId;
|
|
587
|
+
|
|
588
|
+
JsonArray affected = doc.createNestedArray("affected_nodes");
|
|
589
|
+
affected.add(String(nodeId));
|
|
590
|
+
|
|
591
|
+
JsonObject details = doc.createNestedObject("details");
|
|
592
|
+
details["reason"] = "connection_lost";
|
|
593
|
+
details["last_seen"] = getISO8601Timestamp();
|
|
594
|
+
details["total_nodes"] = mesh.getNodeList().size() + 1;
|
|
595
|
+
|
|
596
|
+
publishEvent(doc);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Publish connection lost event
|
|
601
|
+
*/
|
|
602
|
+
void publishConnectionLost(uint32_t nodeId) {
|
|
603
|
+
DynamicJsonDocument doc(1024);
|
|
604
|
+
|
|
605
|
+
doc["schema_version"] = 1;
|
|
606
|
+
doc["device_id"] = String(mesh.getNodeId());
|
|
607
|
+
doc["device_type"] = "gateway";
|
|
608
|
+
doc["timestamp"] = getISO8601Timestamp();
|
|
609
|
+
doc["firmware_version"] = "GW 2.3.4";
|
|
610
|
+
|
|
611
|
+
doc["event"] = "mesh_event";
|
|
612
|
+
doc["event_type"] = "connection_lost";
|
|
613
|
+
doc["mesh_id"] = meshId;
|
|
614
|
+
|
|
615
|
+
JsonArray affected = doc.createNestedArray("affected_nodes");
|
|
616
|
+
affected.add(String(nodeId));
|
|
617
|
+
|
|
618
|
+
publishEvent(doc);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Publish connection restored event
|
|
623
|
+
*/
|
|
624
|
+
void publishConnectionRestored(uint32_t nodeId) {
|
|
625
|
+
DynamicJsonDocument doc(1024);
|
|
626
|
+
|
|
627
|
+
doc["schema_version"] = 1;
|
|
628
|
+
doc["device_id"] = String(mesh.getNodeId());
|
|
629
|
+
doc["device_type"] = "gateway";
|
|
630
|
+
doc["timestamp"] = getISO8601Timestamp();
|
|
631
|
+
doc["firmware_version"] = "GW 2.3.4";
|
|
632
|
+
|
|
633
|
+
doc["event"] = "mesh_event";
|
|
634
|
+
doc["event_type"] = "connection_restored";
|
|
635
|
+
doc["mesh_id"] = meshId;
|
|
636
|
+
|
|
637
|
+
JsonArray affected = doc.createNestedArray("affected_nodes");
|
|
638
|
+
affected.add(String(nodeId));
|
|
639
|
+
|
|
640
|
+
publishEvent(doc);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private:
|
|
644
|
+
void publishEvent(const DynamicJsonDocument& doc) {
|
|
645
|
+
if (!mqttClient.connected()) return;
|
|
646
|
+
|
|
647
|
+
String payload;
|
|
648
|
+
serializeJson(doc, payload);
|
|
649
|
+
|
|
650
|
+
mqttClient.publish(eventTopic.c_str(), payload.c_str());
|
|
651
|
+
|
|
652
|
+
Serial.printf("[Event] Published: %s\n",
|
|
653
|
+
doc["event_type"].as<const char*>());
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
String getISO8601Timestamp() {
|
|
657
|
+
unsigned long ms = millis();
|
|
658
|
+
char timestamp[32];
|
|
659
|
+
snprintf(timestamp, sizeof(timestamp), "2025-10-12T%02lu:%02lu:%02luZ",
|
|
660
|
+
(ms / 3600000) % 24,
|
|
661
|
+
(ms / 60000) % 60,
|
|
662
|
+
(ms / 1000) % 60);
|
|
663
|
+
return String(timestamp);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
#endif // MESH_EVENT_PUBLISHER_HPP
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
**Estimated Effort:** 4-6 hours
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Part 3: Enhanced Gateway Bridge
|
|
675
|
+
|
|
676
|
+
### 3.1 Update mqttCommandBridge.ino
|
|
677
|
+
|
|
678
|
+
**File:** `examples/mqttCommandBridge/mqttCommandBridge.ino`
|
|
679
|
+
|
|
680
|
+
**Changes Required:**
|
|
681
|
+
|
|
682
|
+
1. Include new reporters
|
|
683
|
+
2. Replace simple topology with schema-compliant topology
|
|
684
|
+
3. Add event publisher integration
|
|
685
|
+
4. Add topology command handler
|
|
686
|
+
|
|
687
|
+
```cpp
|
|
688
|
+
// Add includes
|
|
689
|
+
#include "examples/bridge/mesh_topology_reporter.hpp"
|
|
690
|
+
#include "examples/bridge/mesh_event_publisher.hpp"
|
|
691
|
+
|
|
692
|
+
// Add global instances
|
|
693
|
+
MeshTopologyReporter* topologyReporter;
|
|
694
|
+
MeshEventPublisher* eventPublisher;
|
|
695
|
+
|
|
696
|
+
// Update taskTopology
|
|
697
|
+
Task taskTopology(TASK_SECOND * 60, TASK_FOREVER, []() {
|
|
698
|
+
if (mqttClient.connected()) {
|
|
699
|
+
// Generate schema-compliant topology
|
|
700
|
+
String topology = topologyReporter->generateFullTopology();
|
|
701
|
+
mqttClient.publish("alteriom/mesh/MESH-001/topology", topology.c_str());
|
|
702
|
+
|
|
703
|
+
Serial.println("[Topology] Published full mesh topology");
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Add incremental topology task
|
|
708
|
+
Task taskTopologyIncremental(TASK_SECOND * 5, TASK_FOREVER, []() {
|
|
709
|
+
if (mqttClient.connected() && topologyReporter->hasTopologyChanged()) {
|
|
710
|
+
// Publish incremental update only if topology changed
|
|
711
|
+
// Note: This requires tracking what changed
|
|
712
|
+
Serial.println("[Topology] Topology changed, publishing update");
|
|
713
|
+
|
|
714
|
+
// For now, publish full topology
|
|
715
|
+
// TODO: Generate incremental updates
|
|
716
|
+
String topology = topologyReporter->generateFullTopology();
|
|
717
|
+
mqttClient.publish("alteriom/mesh/MESH-001/topology", topology.c_str());
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// Update setup()
|
|
722
|
+
void setup() {
|
|
723
|
+
// ... existing setup code ...
|
|
724
|
+
|
|
725
|
+
// Initialize topology reporter
|
|
726
|
+
topologyReporter = new MeshTopologyReporter(mesh, "MESH-001");
|
|
727
|
+
|
|
728
|
+
// Initialize event publisher
|
|
729
|
+
eventPublisher = new MeshEventPublisher(mesh, mqttClient, "MESH-001");
|
|
730
|
+
|
|
731
|
+
// Update mesh callbacks to use event publisher
|
|
732
|
+
mesh.onNewConnection([](uint32_t nodeId) {
|
|
733
|
+
Serial.printf("New node connected: %u\n", nodeId);
|
|
734
|
+
eventPublisher->publishNodeJoin(nodeId);
|
|
735
|
+
|
|
736
|
+
// Trigger topology update
|
|
737
|
+
String topology = topologyReporter->generateIncrementalUpdate(nodeId, true);
|
|
738
|
+
mqttClient.publish("alteriom/mesh/MESH-001/topology", topology.c_str());
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
mesh.onDroppedConnection([](uint32_t nodeId) {
|
|
742
|
+
Serial.printf("Node disconnected: %u\n", nodeId);
|
|
743
|
+
eventPublisher->publishNodeLeave(nodeId);
|
|
744
|
+
|
|
745
|
+
// Trigger topology update
|
|
746
|
+
String topology = topologyReporter->generateIncrementalUpdate(nodeId, false);
|
|
747
|
+
mqttClient.publish("alteriom/mesh/MESH-001/topology", topology.c_str());
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// Add incremental topology task
|
|
751
|
+
userScheduler.addTask(taskTopologyIncremental);
|
|
752
|
+
taskTopologyIncremental.enable();
|
|
753
|
+
|
|
754
|
+
// ... rest of setup ...
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**Estimated Effort:** 3-4 hours
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
### 3.2 Add Topology Command Handler
|
|
763
|
+
|
|
764
|
+
**Update:** `examples/bridge/mqtt_command_bridge.hpp`
|
|
765
|
+
|
|
766
|
+
**Add command 300 (GET_TOPOLOGY):**
|
|
767
|
+
|
|
768
|
+
```cpp
|
|
769
|
+
case 300: // GET_TOPOLOGY
|
|
770
|
+
Serial.println("Command: GET_TOPOLOGY");
|
|
771
|
+
if (cmd.targetDevice == mesh.getNodeId() || cmd.targetDevice == 0) {
|
|
772
|
+
// Gateway should respond with full topology
|
|
773
|
+
// This requires access to MeshTopologyReporter
|
|
774
|
+
// For now, acknowledge and let scheduled task handle it
|
|
775
|
+
executeCommand(cmd);
|
|
776
|
+
} else {
|
|
777
|
+
// Forward to specific node (may not make sense)
|
|
778
|
+
forwardCommandToMesh(cmd);
|
|
779
|
+
}
|
|
780
|
+
break;
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
**Estimated Effort:** 2-3 hours
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## Part 4: Testing & Validation
|
|
788
|
+
|
|
789
|
+
### 4.1 Create Test Sketch
|
|
790
|
+
|
|
791
|
+
**File:** `examples/mqttTopologyTest/mqttTopologyTest.ino` (NEW)
|
|
792
|
+
|
|
793
|
+
**Purpose:** Test topology reporting with 3-node mesh
|
|
794
|
+
|
|
795
|
+
```cpp
|
|
796
|
+
// Minimal test setup with 3 ESP32s
|
|
797
|
+
// - 1 Gateway (publishes topology)
|
|
798
|
+
// - 2 Sensor nodes
|
|
799
|
+
// Verify:
|
|
800
|
+
// - Full topology published every 60s
|
|
801
|
+
// - Incremental updates on node join/leave
|
|
802
|
+
// - Events published correctly
|
|
803
|
+
// - Schema validation passes
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
**Estimated Effort:** 4-6 hours (requires physical hardware)
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
### 4.2 Schema Validation Tests
|
|
811
|
+
|
|
812
|
+
**File:** `test/catch/catch_topology_schema.cpp` (NEW)
|
|
813
|
+
|
|
814
|
+
**Purpose:** Validate generated topology matches schema
|
|
815
|
+
|
|
816
|
+
```cpp
|
|
817
|
+
#define CATCH_CONFIG_MAIN
|
|
818
|
+
#include "catch2/catch.hpp"
|
|
819
|
+
#include "Arduino.h"
|
|
820
|
+
#include "catch_utils.hpp"
|
|
821
|
+
#include "../../examples/bridge/mesh_topology_reporter.hpp"
|
|
822
|
+
|
|
823
|
+
SCENARIO("Topology message validates against schema") {
|
|
824
|
+
GIVEN("A MeshTopologyReporter") {
|
|
825
|
+
// Test that generated JSON matches schema requirements
|
|
826
|
+
REQUIRE(topology_has_envelope_fields());
|
|
827
|
+
REQUIRE(topology_has_nodes_array());
|
|
828
|
+
REQUIRE(topology_has_connections_array());
|
|
829
|
+
REQUIRE(topology_has_metrics_object());
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
**Estimated Effort:** 6-8 hours
|
|
835
|
+
|
|
836
|
+
---
|
|
837
|
+
|
|
838
|
+
## Part 5: Documentation Updates
|
|
839
|
+
|
|
840
|
+
### 5.1 Update MQTT_BRIDGE_COMMANDS.md
|
|
841
|
+
|
|
842
|
+
Add sections:
|
|
843
|
+
|
|
844
|
+
- Mesh Topology Reporting
|
|
845
|
+
- Mesh Events
|
|
846
|
+
- Topology command (300)
|
|
847
|
+
|
|
848
|
+
**Estimated Effort:** 2-3 hours
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
### 5.2 Create MESH_TOPOLOGY_GUIDE.md
|
|
853
|
+
|
|
854
|
+
**File:** `docs/MESH_TOPOLOGY_GUIDE.md` (NEW)
|
|
855
|
+
|
|
856
|
+
**Content:**
|
|
857
|
+
|
|
858
|
+
- How topology reporting works
|
|
859
|
+
- How to visualize with D3.js
|
|
860
|
+
- Troubleshooting topology issues
|
|
861
|
+
- Performance considerations
|
|
862
|
+
|
|
863
|
+
**Estimated Effort:** 4-6 hours
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
## Implementation Summary
|
|
868
|
+
|
|
869
|
+
### Phase 1: Core Library (HIGH PRIORITY)
|
|
870
|
+
|
|
871
|
+
| Task | File | Effort | Priority |
|
|
872
|
+
|------|------|--------|----------|
|
|
873
|
+
| Add ConnectionInfo struct | mesh.hpp | 2h | 🔴 HIGH |
|
|
874
|
+
| Add getConnectionDetails() | mesh.hpp | 4h | 🔴 HIGH |
|
|
875
|
+
| Add getHopCount() | mesh.hpp | 2h | 🔴 HIGH |
|
|
876
|
+
| Add getRoutingTable() | mesh.hpp | 2h | 🔴 HIGH |
|
|
877
|
+
| Add Connection metrics | tcp.hpp | 6h | 🔴 HIGH |
|
|
878
|
+
| **Subtotal** | | **16h** | |
|
|
879
|
+
|
|
880
|
+
### Phase 2: Topology Reporter (HIGH PRIORITY)
|
|
881
|
+
|
|
882
|
+
| Task | File | Effort | Priority |
|
|
883
|
+
|------|------|--------|----------|
|
|
884
|
+
| Create MeshTopologyReporter | NEW | 8h | 🔴 HIGH |
|
|
885
|
+
| Create MeshEventPublisher | NEW | 4h | 🔴 HIGH |
|
|
886
|
+
| Update mqttCommandBridge.ino | MODIFY | 4h | 🔴 HIGH |
|
|
887
|
+
| Add topology command handler | MODIFY | 2h | 🟡 MEDIUM |
|
|
888
|
+
| **Subtotal** | | **18h** | |
|
|
889
|
+
|
|
890
|
+
### Phase 3: Testing (MEDIUM PRIORITY)
|
|
891
|
+
|
|
892
|
+
| Task | File | Effort | Priority |
|
|
893
|
+
|------|------|--------|----------|
|
|
894
|
+
| Physical hardware testing | NEW | 6h | 🟡 MEDIUM |
|
|
895
|
+
| Schema validation tests | NEW | 8h | 🟡 MEDIUM |
|
|
896
|
+
| Integration testing | VARIOUS | 4h | 🟡 MEDIUM |
|
|
897
|
+
| **Subtotal** | | **18h** | |
|
|
898
|
+
|
|
899
|
+
### Phase 4: Documentation (LOW PRIORITY)
|
|
900
|
+
|
|
901
|
+
| Task | File | Effort | Priority |
|
|
902
|
+
|------|------|--------|----------|
|
|
903
|
+
| Update MQTT_BRIDGE_COMMANDS.md | MODIFY | 3h | 🟢 LOW |
|
|
904
|
+
| Create MESH_TOPOLOGY_GUIDE.md | NEW | 6h | 🟢 LOW |
|
|
905
|
+
| Update README examples | MODIFY | 2h | 🟢 LOW |
|
|
906
|
+
| **Subtotal** | | **11h** | |
|
|
907
|
+
|
|
908
|
+
---
|
|
909
|
+
|
|
910
|
+
## Total Implementation Effort
|
|
911
|
+
|
|
912
|
+
| Phase | Hours | Priority |
|
|
913
|
+
|-------|-------|----------|
|
|
914
|
+
| Phase 1: Core Library | 16h | 🔴 HIGH |
|
|
915
|
+
| Phase 2: Topology Reporter | 18h | 🔴 HIGH |
|
|
916
|
+
| Phase 3: Testing | 18h | 🟡 MEDIUM |
|
|
917
|
+
| Phase 4: Documentation | 11h | 🟢 LOW |
|
|
918
|
+
| **TOTAL** | **63h** | |
|
|
919
|
+
|
|
920
|
+
**Estimated Calendar Time:** 2-3 weeks (assuming 1 developer, part-time)
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
924
|
+
## Dependencies
|
|
925
|
+
|
|
926
|
+
### External Dependencies
|
|
927
|
+
|
|
928
|
+
1. ✅ **@alteriom/mqtt-schema v0.5.0** - Schema definitions
|
|
929
|
+
2. ✅ **ArduinoJson** - Already used
|
|
930
|
+
3. ✅ **PubSubClient** - Already used
|
|
931
|
+
4. ✅ **TaskScheduler** - Already used
|
|
932
|
+
|
|
933
|
+
### Internal Dependencies
|
|
934
|
+
|
|
935
|
+
1. ⚠️ **painlessMesh core API changes** - Breaking changes to Connection class
|
|
936
|
+
2. ⚠️ **RSSI tracking** - Platform-specific (ESP32/ESP8266)
|
|
937
|
+
3. ⚠️ **NTP time sync** - For accurate ISO 8601 timestamps
|
|
938
|
+
|
|
939
|
+
---
|
|
940
|
+
|
|
941
|
+
## Risk Assessment
|
|
942
|
+
|
|
943
|
+
### High Risk ⚠️
|
|
944
|
+
|
|
945
|
+
1. **Connection metrics API** - Requires changes to core library
|
|
946
|
+
- Mitigation: Extensive testing, backward compatibility checks
|
|
947
|
+
|
|
948
|
+
2. **RSSI tracking** - Platform-specific implementation
|
|
949
|
+
- Mitigation: Abstract platform differences, provide defaults
|
|
950
|
+
|
|
951
|
+
3. **Memory constraints** - Large JSON documents (8KB topology)
|
|
952
|
+
- Mitigation: Use streaming JSON, reduce buffer sizes on ESP8266
|
|
953
|
+
|
|
954
|
+
### Medium Risk 🟡
|
|
955
|
+
|
|
956
|
+
1. **Performance impact** - Additional metric tracking overhead
|
|
957
|
+
- Mitigation: Make metrics collection optional, optimize data structures
|
|
958
|
+
|
|
959
|
+
2. **Breaking changes** - Connection class modifications
|
|
960
|
+
- Mitigation: Version bump to v1.7.0, update changelog
|
|
961
|
+
|
|
962
|
+
### Low Risk ✅
|
|
963
|
+
|
|
964
|
+
1. **Schema compliance** - Matching @alteriom/mqtt-schema exactly
|
|
965
|
+
- Mitigation: Automated schema validation tests
|
|
966
|
+
|
|
967
|
+
---
|
|
968
|
+
|
|
969
|
+
## Backward Compatibility
|
|
970
|
+
|
|
971
|
+
### Breaking Changes ⚠️
|
|
972
|
+
|
|
973
|
+
1. **Connection class** - New public members may break existing code that copies/serializes Connection objects
|
|
974
|
+
- **Solution:** Version bump to v1.7.0, deprecation warnings in v1.6.x
|
|
975
|
+
|
|
976
|
+
2. **Memory usage** - Increased RAM usage due to metric tracking
|
|
977
|
+
- **Solution:** Make metrics optional via compile-time flag `PAINLESSMESH_ENABLE_METRICS`
|
|
978
|
+
|
|
979
|
+
### Non-Breaking Changes ✅
|
|
980
|
+
|
|
981
|
+
1. **New API methods** - additive, doesn't affect existing code
|
|
982
|
+
2. **MQTT bridge enhancements** - in examples, not core library
|
|
983
|
+
3. **Documentation** - purely additive
|
|
984
|
+
|
|
985
|
+
---
|
|
986
|
+
|
|
987
|
+
## Recommended Implementation Order
|
|
988
|
+
|
|
989
|
+
### Week 1: Foundation
|
|
990
|
+
|
|
991
|
+
1. ✅ Review and approve schema additions
|
|
992
|
+
2. Add ConnectionInfo struct to mesh.hpp
|
|
993
|
+
3. Add basic metric tracking to Connection class
|
|
994
|
+
4. Implement getConnectionDetails() API
|
|
995
|
+
|
|
996
|
+
### Week 2: Topology Reporting
|
|
997
|
+
|
|
998
|
+
1. Create MeshTopologyReporter class
|
|
999
|
+
2. Create MeshEventPublisher class
|
|
1000
|
+
3. Update mqttCommandBridge.ino
|
|
1001
|
+
4. Basic testing with 2-node mesh
|
|
1002
|
+
|
|
1003
|
+
### Week 3: Polish & Testing
|
|
1004
|
+
|
|
1005
|
+
1. Add topology command handler
|
|
1006
|
+
2. Physical hardware testing (3-4 node mesh)
|
|
1007
|
+
3. Schema validation tests
|
|
1008
|
+
4. Documentation updates
|
|
1009
|
+
|
|
1010
|
+
---
|
|
1011
|
+
|
|
1012
|
+
## Success Criteria
|
|
1013
|
+
|
|
1014
|
+
✅ **Must Have (v1.7.0):**
|
|
1015
|
+
|
|
1016
|
+
- Schema-compliant topology messages
|
|
1017
|
+
- Real-time mesh events (join/leave)
|
|
1018
|
+
- Connection quality metrics (RSSI, latency, quality)
|
|
1019
|
+
- Full and incremental topology updates
|
|
1020
|
+
- Backward compatible with v1.6.x (no breaking changes to public API)
|
|
1021
|
+
|
|
1022
|
+
📋 **Should Have (v1.7.1):**
|
|
1023
|
+
|
|
1024
|
+
- Topology command handler (GET_TOPOLOGY)
|
|
1025
|
+
- Hop count calculation
|
|
1026
|
+
- Routing table export
|
|
1027
|
+
- Performance optimization
|
|
1028
|
+
|
|
1029
|
+
🎯 **Nice to Have (v1.8.0):**
|
|
1030
|
+
|
|
1031
|
+
- Mesh diagnostics command
|
|
1032
|
+
- Per-message route tracking
|
|
1033
|
+
- Historical topology storage
|
|
1034
|
+
- Advanced quality metrics (jitter, throughput)
|
|
1035
|
+
|
|
1036
|
+
---
|
|
1037
|
+
|
|
1038
|
+
## Next Steps
|
|
1039
|
+
|
|
1040
|
+
### Immediate Actions
|
|
1041
|
+
|
|
1042
|
+
1. 📝 **Review this plan** - Get team approval
|
|
1043
|
+
2. 🔧 **Set up development branch** - `feature/mesh-topology-v0.5`
|
|
1044
|
+
3. 📦 **Create GitHub issues** - Break down tasks
|
|
1045
|
+
4. 🧪 **Set up test hardware** - 3x ESP32 boards
|
|
1046
|
+
5. 📚 **Wait for schema release** - Monitor @alteriom/mqtt-schema repo
|
|
1047
|
+
|
|
1048
|
+
### Development Kickoff
|
|
1049
|
+
|
|
1050
|
+
Once @alteriom/mqtt-schema v0.5.0 is released:
|
|
1051
|
+
|
|
1052
|
+
1. Start with Phase 1 (Core Library APIs)
|
|
1053
|
+
2. Weekly progress check-ins
|
|
1054
|
+
3. Code reviews for each major component
|
|
1055
|
+
4. Continuous integration testing
|
|
1056
|
+
|
|
1057
|
+
---
|
|
1058
|
+
|
|
1059
|
+
**Prepared By:** Alteriom Development Team
|
|
1060
|
+
**Date:** October 12, 2025
|
|
1061
|
+
**Status:** 📋 READY FOR IMPLEMENTATION
|
|
1062
|
+
**Estimated Completion:** 3 weeks from start
|