@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,748 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file mqttTopologyTest.ino
|
|
3
|
+
* @brief Hardware test sketch for painlessMesh topology reporting with MQTT
|
|
4
|
+
*
|
|
5
|
+
* This sketch tests mesh topology reporting according to @alteriom/mqtt-schema v0.5.0
|
|
6
|
+
*
|
|
7
|
+
* HARDWARE REQUIREMENTS:
|
|
8
|
+
* - 3x ESP32 boards (or ESP8266, but ESP32 recommended)
|
|
9
|
+
* - MQTT broker accessible on network (e.g., Mosquitto)
|
|
10
|
+
* - USB cables for programming and Serial monitoring
|
|
11
|
+
*
|
|
12
|
+
* TEST CONFIGURATION:
|
|
13
|
+
* - Node 1: Gateway (this sketch with IS_GATEWAY = true)
|
|
14
|
+
* - Node 2: Sensor node (this sketch with IS_GATEWAY = false, role = "sensor")
|
|
15
|
+
* - Node 3: Repeater node (this sketch with IS_GATEWAY = false, role = "repeater")
|
|
16
|
+
*
|
|
17
|
+
* MQTT TOPICS TESTED:
|
|
18
|
+
* - alteriom/mesh/MESH-001/topology (full topology every 60s, incremental every 5s)
|
|
19
|
+
* - alteriom/mesh/MESH-001/events (node join/leave events)
|
|
20
|
+
* - alteriom/mesh/MESH-001/topology/response (GET_TOPOLOGY command response)
|
|
21
|
+
* - alteriom/mesh/MESH-001/command (receive GET_TOPOLOGY commands)
|
|
22
|
+
*
|
|
23
|
+
* SCHEMA COMPLIANCE:
|
|
24
|
+
* - Device ID format: ALT-XXXXXXXXXXXX (uppercase hex)
|
|
25
|
+
* - Quality metrics: 0-100 range
|
|
26
|
+
* - RSSI values: negative dBm
|
|
27
|
+
* - Timestamps: ISO 8601 format
|
|
28
|
+
* - Envelope fields: schema_version, device_id, device_type, timestamp, firmware_version
|
|
29
|
+
*
|
|
30
|
+
* TEST PROCEDURE:
|
|
31
|
+
* 1. Configure WiFi credentials (WIFI_SSID, WIFI_PASSWORD)
|
|
32
|
+
* 2. Configure MQTT broker (MQTT_BROKER, MQTT_PORT)
|
|
33
|
+
* 3. Flash Node 1 with IS_GATEWAY = true
|
|
34
|
+
* 4. Flash Node 2 with IS_GATEWAY = false, NODE_ROLE = "sensor"
|
|
35
|
+
* 5. Flash Node 3 with IS_GATEWAY = false, NODE_ROLE = "repeater"
|
|
36
|
+
* 6. Open Serial monitors for all 3 nodes
|
|
37
|
+
* 7. Wait for mesh formation (watch for "Node joined" messages)
|
|
38
|
+
* 8. Verify full topology published every 60s
|
|
39
|
+
* 9. Verify incremental updates on node join/leave
|
|
40
|
+
* 10. Send GET_TOPOLOGY command and verify response
|
|
41
|
+
*
|
|
42
|
+
* EXPECTED BEHAVIOR:
|
|
43
|
+
* - All nodes join mesh within 30 seconds
|
|
44
|
+
* - Gateway publishes full topology every 60 seconds
|
|
45
|
+
* - Gateway publishes incremental updates within 5 seconds of node join/leave
|
|
46
|
+
* - All messages conform to @alteriom/mqtt-schema v0.5.0
|
|
47
|
+
* - Connection quality metrics are calculated correctly
|
|
48
|
+
*
|
|
49
|
+
* @author Alteriom Development Team
|
|
50
|
+
* @date October 14, 2025
|
|
51
|
+
* @version 1.0.0
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
#include "painlessMesh.h"
|
|
55
|
+
#ifdef ESP8266
|
|
56
|
+
#include <ESP8266WiFi.h>
|
|
57
|
+
#else
|
|
58
|
+
#include <WiFi.h>
|
|
59
|
+
#endif
|
|
60
|
+
#include <PubSubClient.h>
|
|
61
|
+
#include <ArduinoJson.h>
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// CONFIGURATION - MODIFY THESE FOR YOUR SETUP
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
// Mesh Configuration
|
|
68
|
+
#define MESH_PREFIX "AlteriomMesh"
|
|
69
|
+
#define MESH_PASSWORD "alteriom2025"
|
|
70
|
+
#define MESH_PORT 5555
|
|
71
|
+
|
|
72
|
+
// WiFi Configuration (for gateway only)
|
|
73
|
+
#define WIFI_SSID "YourWiFiSSID"
|
|
74
|
+
#define WIFI_PASSWORD "YourWiFiPassword"
|
|
75
|
+
|
|
76
|
+
// MQTT Configuration (for gateway only)
|
|
77
|
+
#define MQTT_BROKER "192.168.1.100" // Your MQTT broker IP
|
|
78
|
+
#define MQTT_PORT 1883
|
|
79
|
+
#define MQTT_USER "" // Leave empty if no auth
|
|
80
|
+
#define MQTT_PASS "" // Leave empty if no auth
|
|
81
|
+
|
|
82
|
+
// Node Configuration - CHANGE THIS FOR EACH NODE
|
|
83
|
+
#define IS_GATEWAY true // Set to false for sensor/repeater nodes
|
|
84
|
+
#define NODE_ROLE "gateway" // "gateway", "sensor", or "repeater"
|
|
85
|
+
#define MESH_ID "MESH-001"
|
|
86
|
+
#define FIRMWARE_VERSION "TEST 1.0.0"
|
|
87
|
+
|
|
88
|
+
// Topology Publishing Configuration
|
|
89
|
+
#define FULL_TOPOLOGY_INTERVAL 60000 // Full topology every 60 seconds
|
|
90
|
+
#define INCREMENTAL_INTERVAL 5000 // Check for changes every 5 seconds
|
|
91
|
+
#define COMMAND_CHECK_INTERVAL 1000 // Check for commands every 1 second
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// GLOBAL OBJECTS
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
Scheduler userScheduler;
|
|
98
|
+
painlessMesh mesh;
|
|
99
|
+
|
|
100
|
+
#if IS_GATEWAY
|
|
101
|
+
WiFiClient wifiClient;
|
|
102
|
+
PubSubClient mqttClient(wifiClient);
|
|
103
|
+
#endif
|
|
104
|
+
|
|
105
|
+
// Task declarations
|
|
106
|
+
Task taskSendFullTopology(FULL_TOPOLOGY_INTERVAL, TASK_FOREVER, &sendFullTopology);
|
|
107
|
+
Task taskCheckIncrementalUpdate(INCREMENTAL_INTERVAL, TASK_FOREVER, &checkIncrementalUpdate);
|
|
108
|
+
Task taskCheckCommands(COMMAND_CHECK_INTERVAL, TASK_FOREVER, &checkCommands);
|
|
109
|
+
|
|
110
|
+
// Topology state tracking
|
|
111
|
+
struct NodeState {
|
|
112
|
+
uint32_t nodeId;
|
|
113
|
+
String role;
|
|
114
|
+
bool online;
|
|
115
|
+
unsigned long lastSeen;
|
|
116
|
+
int connectionCount;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
std::map<uint32_t, NodeState> lastTopology;
|
|
120
|
+
unsigned long lastTopologyUpdate = 0;
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// DEVICE ID GENERATION
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate Alteriom-compliant device ID from chip ID
|
|
128
|
+
* Format: ALT-XXXXXXXXXXXX (uppercase hex)
|
|
129
|
+
*/
|
|
130
|
+
String getDeviceId() {
|
|
131
|
+
uint64_t chipid = ESP.getEfuseMac(); // ESP32 unique ID
|
|
132
|
+
char deviceId[17];
|
|
133
|
+
snprintf(deviceId, sizeof(deviceId), "ALT-%012llX", chipid);
|
|
134
|
+
return String(deviceId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// ISO 8601 TIMESTAMP
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get current timestamp in ISO 8601 format
|
|
143
|
+
* Note: Uses mesh time, not real-world time (no NTP in this test)
|
|
144
|
+
*/
|
|
145
|
+
String getTimestamp() {
|
|
146
|
+
uint32_t meshTime = mesh.getNodeTime();
|
|
147
|
+
// Convert microseconds to seconds
|
|
148
|
+
uint32_t seconds = meshTime / 1000000;
|
|
149
|
+
|
|
150
|
+
// Simple ISO 8601 format (no actual date, just offset from boot)
|
|
151
|
+
char timestamp[30];
|
|
152
|
+
snprintf(timestamp, sizeof(timestamp), "2025-10-14T%02d:%02d:%02dZ",
|
|
153
|
+
(seconds / 3600) % 24,
|
|
154
|
+
(seconds / 60) % 60,
|
|
155
|
+
seconds % 60);
|
|
156
|
+
return String(timestamp);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// CONNECTION QUALITY CALCULATION
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Calculate connection quality (0-100) based on mesh metrics
|
|
165
|
+
* This is a simplified calculation for testing
|
|
166
|
+
*/
|
|
167
|
+
int calculateQuality(uint32_t nodeId) {
|
|
168
|
+
// In a real implementation, this would use:
|
|
169
|
+
// - RSSI measurements
|
|
170
|
+
// - Packet loss rate
|
|
171
|
+
// - Latency measurements
|
|
172
|
+
// For testing, we'll use a simulated value based on node ID
|
|
173
|
+
|
|
174
|
+
// Simulate quality degradation with distance (hop count)
|
|
175
|
+
SimpleList<uint32_t> nodeList = mesh.getNodeList();
|
|
176
|
+
int nodeCount = nodeList.size();
|
|
177
|
+
|
|
178
|
+
// Base quality: 95 for direct connections
|
|
179
|
+
int quality = 95;
|
|
180
|
+
|
|
181
|
+
// Reduce quality based on mesh size (simulate congestion)
|
|
182
|
+
if (nodeCount > 3) quality -= (nodeCount - 3) * 5;
|
|
183
|
+
|
|
184
|
+
// Add some variation based on node ID
|
|
185
|
+
quality -= (nodeId % 10);
|
|
186
|
+
|
|
187
|
+
// Clamp to 0-100 range
|
|
188
|
+
return std::max(0, std::min(100, quality));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Simulate RSSI (WiFi signal strength in dBm)
|
|
193
|
+
* Real implementation would read actual WiFi RSSI
|
|
194
|
+
*/
|
|
195
|
+
int calculateRSSI(uint32_t nodeId) {
|
|
196
|
+
// RSSI ranges from -30 (excellent) to -90 (poor)
|
|
197
|
+
int rssi = -40 - (nodeId % 50); // Simulated based on node ID
|
|
198
|
+
return std::max(-90, std::min(-30, rssi));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Simulate round-trip latency in milliseconds
|
|
203
|
+
* Real implementation would measure actual ping times
|
|
204
|
+
*/
|
|
205
|
+
int calculateLatency(uint32_t nodeId) {
|
|
206
|
+
// Latency increases with hop count
|
|
207
|
+
int baseLatency = 10;
|
|
208
|
+
int variation = nodeId % 20;
|
|
209
|
+
return baseLatency + variation;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// MQTT PUBLISHING (GATEWAY ONLY)
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
#if IS_GATEWAY
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Connect to MQTT broker
|
|
220
|
+
*/
|
|
221
|
+
void connectMQTT() {
|
|
222
|
+
while (!mqttClient.connected()) {
|
|
223
|
+
Serial.print("Connecting to MQTT broker...");
|
|
224
|
+
|
|
225
|
+
String clientId = "AlteriomGateway-" + getDeviceId();
|
|
226
|
+
|
|
227
|
+
if (mqttClient.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) {
|
|
228
|
+
Serial.println(" connected!");
|
|
229
|
+
|
|
230
|
+
// Subscribe to command topic
|
|
231
|
+
String commandTopic = "alteriom/mesh/" + String(MESH_ID) + "/command";
|
|
232
|
+
mqttClient.subscribe(commandTopic.c_str());
|
|
233
|
+
Serial.println("Subscribed to: " + commandTopic);
|
|
234
|
+
} else {
|
|
235
|
+
Serial.print(" failed, rc=");
|
|
236
|
+
Serial.print(mqttClient.state());
|
|
237
|
+
Serial.println(" retrying in 5 seconds");
|
|
238
|
+
delay(5000);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Publish message to MQTT with envelope fields
|
|
245
|
+
*/
|
|
246
|
+
bool publishMQTT(const String& topic, JsonDocument& doc) {
|
|
247
|
+
// Add envelope fields
|
|
248
|
+
doc["schema_version"] = 1;
|
|
249
|
+
doc["device_id"] = getDeviceId();
|
|
250
|
+
doc["device_type"] = NODE_ROLE;
|
|
251
|
+
doc["timestamp"] = getTimestamp();
|
|
252
|
+
doc["firmware_version"] = FIRMWARE_VERSION;
|
|
253
|
+
|
|
254
|
+
// Serialize to JSON
|
|
255
|
+
String jsonString;
|
|
256
|
+
serializeJson(doc, jsonString);
|
|
257
|
+
|
|
258
|
+
// Publish
|
|
259
|
+
bool success = mqttClient.publish(topic.c_str(), jsonString.c_str());
|
|
260
|
+
|
|
261
|
+
if (success) {
|
|
262
|
+
Serial.println("✅ Published to " + topic);
|
|
263
|
+
Serial.println(" Size: " + String(jsonString.length()) + " bytes");
|
|
264
|
+
} else {
|
|
265
|
+
Serial.println("❌ Failed to publish to " + topic);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return success;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Publish mesh event (node join/leave)
|
|
273
|
+
*/
|
|
274
|
+
void publishMeshEvent(const String& eventType, uint32_t nodeId, const String& reason = "") {
|
|
275
|
+
JsonDocument doc;
|
|
276
|
+
|
|
277
|
+
doc["event"] = "mesh_event";
|
|
278
|
+
doc["mesh_id"] = MESH_ID;
|
|
279
|
+
doc["event_type"] = eventType;
|
|
280
|
+
|
|
281
|
+
JsonArray affectedNodes = doc["affected_nodes"].to<JsonArray>();
|
|
282
|
+
char nodeIdStr[17];
|
|
283
|
+
snprintf(nodeIdStr, sizeof(nodeIdStr), "ALT-%012X", nodeId);
|
|
284
|
+
affectedNodes.add(nodeIdStr);
|
|
285
|
+
|
|
286
|
+
if (reason != "") {
|
|
287
|
+
JsonObject details = doc["details"].to<JsonObject>();
|
|
288
|
+
details["reason"] = reason;
|
|
289
|
+
details["last_seen"] = getTimestamp();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
String topic = "alteriom/mesh/" + String(MESH_ID) + "/events";
|
|
293
|
+
publishMQTT(topic, doc);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#endif
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// TOPOLOGY REPORTING
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Build and send full mesh topology
|
|
304
|
+
*/
|
|
305
|
+
void sendFullTopology() {
|
|
306
|
+
#if IS_GATEWAY
|
|
307
|
+
Serial.println("\n📊 Generating FULL topology report...");
|
|
308
|
+
|
|
309
|
+
if (!mqttClient.connected()) {
|
|
310
|
+
connectMQTT();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
JsonDocument doc;
|
|
315
|
+
|
|
316
|
+
doc["event"] = "mesh_topology";
|
|
317
|
+
doc["mesh_id"] = MESH_ID;
|
|
318
|
+
doc["gateway_node_id"] = getDeviceId();
|
|
319
|
+
doc["update_type"] = "full";
|
|
320
|
+
|
|
321
|
+
// Get node list
|
|
322
|
+
SimpleList<uint32_t> nodeList = mesh.getNodeList();
|
|
323
|
+
|
|
324
|
+
// Add nodes array
|
|
325
|
+
JsonArray nodes = doc["nodes"].to<JsonArray>();
|
|
326
|
+
|
|
327
|
+
// Add gateway node
|
|
328
|
+
JsonObject gatewayNode = nodes.add<JsonObject>();
|
|
329
|
+
gatewayNode["node_id"] = getDeviceId();
|
|
330
|
+
gatewayNode["role"] = NODE_ROLE;
|
|
331
|
+
gatewayNode["status"] = "online";
|
|
332
|
+
gatewayNode["last_seen"] = getTimestamp();
|
|
333
|
+
gatewayNode["firmware_version"] = FIRMWARE_VERSION;
|
|
334
|
+
gatewayNode["uptime_seconds"] = millis() / 1000;
|
|
335
|
+
gatewayNode["free_memory_kb"] = ESP.getFreeHeap() / 1024;
|
|
336
|
+
gatewayNode["connection_count"] = nodeList.size();
|
|
337
|
+
|
|
338
|
+
// Add other nodes
|
|
339
|
+
for (auto nodeId : nodeList) {
|
|
340
|
+
JsonObject node = nodes.add<JsonObject>();
|
|
341
|
+
|
|
342
|
+
char nodeIdStr[17];
|
|
343
|
+
snprintf(nodeIdStr, sizeof(nodeIdStr), "ALT-%012X", nodeId);
|
|
344
|
+
node["node_id"] = nodeIdStr;
|
|
345
|
+
|
|
346
|
+
// Simulate role based on node ID
|
|
347
|
+
node["role"] = (nodeId % 2 == 0) ? "sensor" : "repeater";
|
|
348
|
+
node["status"] = "online";
|
|
349
|
+
node["last_seen"] = getTimestamp();
|
|
350
|
+
node["firmware_version"] = FIRMWARE_VERSION;
|
|
351
|
+
node["uptime_seconds"] = 0; // Unknown for remote nodes
|
|
352
|
+
node["free_memory_kb"] = 0; // Unknown for remote nodes
|
|
353
|
+
node["connection_count"] = 1; // Simplified
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Add connections array
|
|
357
|
+
JsonArray connections = doc["connections"].to<JsonArray>();
|
|
358
|
+
|
|
359
|
+
for (auto nodeId : nodeList) {
|
|
360
|
+
JsonObject conn = connections.add<JsonObject>();
|
|
361
|
+
conn["from_node"] = getDeviceId();
|
|
362
|
+
|
|
363
|
+
char nodeIdStr[17];
|
|
364
|
+
snprintf(nodeIdStr, sizeof(nodeIdStr), "ALT-%012X", nodeId);
|
|
365
|
+
conn["to_node"] = nodeIdStr;
|
|
366
|
+
|
|
367
|
+
conn["quality"] = calculateQuality(nodeId);
|
|
368
|
+
conn["latency_ms"] = calculateLatency(nodeId);
|
|
369
|
+
conn["rssi"] = calculateRSSI(nodeId);
|
|
370
|
+
conn["hop_count"] = 1; // Direct connection to gateway
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Add metrics
|
|
374
|
+
JsonObject metrics = doc["metrics"].to<JsonObject>();
|
|
375
|
+
metrics["total_nodes"] = nodeList.size() + 1; // +1 for gateway
|
|
376
|
+
metrics["online_nodes"] = nodeList.size() + 1;
|
|
377
|
+
metrics["network_diameter"] = 1; // Single hop for this test
|
|
378
|
+
|
|
379
|
+
int avgQuality = 0;
|
|
380
|
+
for (auto nodeId : nodeList) {
|
|
381
|
+
avgQuality += calculateQuality(nodeId);
|
|
382
|
+
}
|
|
383
|
+
if (nodeList.size() > 0) {
|
|
384
|
+
avgQuality /= nodeList.size();
|
|
385
|
+
}
|
|
386
|
+
metrics["avg_connection_quality"] = avgQuality;
|
|
387
|
+
metrics["messages_per_second"] = 0.0; // Not tracked in this test
|
|
388
|
+
|
|
389
|
+
String topic = "alteriom/mesh/" + String(MESH_ID) + "/topology";
|
|
390
|
+
publishMQTT(topic, doc);
|
|
391
|
+
|
|
392
|
+
// Update last topology state
|
|
393
|
+
lastTopology.clear();
|
|
394
|
+
for (auto nodeId : nodeList) {
|
|
395
|
+
NodeState state;
|
|
396
|
+
state.nodeId = nodeId;
|
|
397
|
+
state.role = (nodeId % 2 == 0) ? "sensor" : "repeater";
|
|
398
|
+
state.online = true;
|
|
399
|
+
state.lastSeen = millis();
|
|
400
|
+
state.connectionCount = 1;
|
|
401
|
+
lastTopology[nodeId] = state;
|
|
402
|
+
}
|
|
403
|
+
lastTopologyUpdate = millis();
|
|
404
|
+
|
|
405
|
+
Serial.println("✅ Full topology published");
|
|
406
|
+
Serial.println(" Nodes: " + String(nodeList.size() + 1));
|
|
407
|
+
Serial.println(" Connections: " + String(nodeList.size()));
|
|
408
|
+
|
|
409
|
+
#endif
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Check for topology changes and send incremental update
|
|
414
|
+
*/
|
|
415
|
+
void checkIncrementalUpdate() {
|
|
416
|
+
#if IS_GATEWAY
|
|
417
|
+
if (!mqttClient.connected()) {
|
|
418
|
+
connectMQTT();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
SimpleList<uint32_t> currentNodes = mesh.getNodeList();
|
|
423
|
+
bool hasChanges = false;
|
|
424
|
+
|
|
425
|
+
// Check for new nodes
|
|
426
|
+
std::vector<uint32_t> newNodes;
|
|
427
|
+
for (auto nodeId : currentNodes) {
|
|
428
|
+
if (lastTopology.find(nodeId) == lastTopology.end()) {
|
|
429
|
+
newNodes.push_back(nodeId);
|
|
430
|
+
hasChanges = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check for removed nodes
|
|
435
|
+
std::vector<uint32_t> removedNodes;
|
|
436
|
+
for (auto& pair : lastTopology) {
|
|
437
|
+
uint32_t nodeId = pair.first;
|
|
438
|
+
bool found = false;
|
|
439
|
+
for (auto currentId : currentNodes) {
|
|
440
|
+
if (currentId == nodeId) {
|
|
441
|
+
found = true;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!found) {
|
|
446
|
+
removedNodes.push_back(nodeId);
|
|
447
|
+
hasChanges = true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (hasChanges) {
|
|
452
|
+
Serial.println("\n🔄 Topology changed! Generating incremental update...");
|
|
453
|
+
|
|
454
|
+
JsonDocument doc;
|
|
455
|
+
|
|
456
|
+
doc["event"] = "mesh_topology";
|
|
457
|
+
doc["mesh_id"] = MESH_ID;
|
|
458
|
+
doc["gateway_node_id"] = getDeviceId();
|
|
459
|
+
doc["update_type"] = "incremental";
|
|
460
|
+
|
|
461
|
+
JsonArray nodes = doc["nodes"].to<JsonArray>();
|
|
462
|
+
JsonArray connections = doc["connections"].to<JsonArray>();
|
|
463
|
+
|
|
464
|
+
// Add new nodes
|
|
465
|
+
for (auto nodeId : newNodes) {
|
|
466
|
+
JsonObject node = nodes.add<JsonObject>();
|
|
467
|
+
|
|
468
|
+
char nodeIdStr[17];
|
|
469
|
+
snprintf(nodeIdStr, sizeof(nodeIdStr), "ALT-%012X", nodeId);
|
|
470
|
+
node["node_id"] = nodeIdStr;
|
|
471
|
+
node["role"] = (nodeId % 2 == 0) ? "sensor" : "repeater";
|
|
472
|
+
node["status"] = "online";
|
|
473
|
+
node["last_seen"] = getTimestamp();
|
|
474
|
+
|
|
475
|
+
JsonObject conn = connections.add<JsonObject>();
|
|
476
|
+
conn["from_node"] = getDeviceId();
|
|
477
|
+
conn["to_node"] = nodeIdStr;
|
|
478
|
+
conn["quality"] = calculateQuality(nodeId);
|
|
479
|
+
conn["latency_ms"] = calculateLatency(nodeId);
|
|
480
|
+
conn["rssi"] = calculateRSSI(nodeId);
|
|
481
|
+
conn["hop_count"] = 1;
|
|
482
|
+
|
|
483
|
+
Serial.println(" + Node joined: " + String(nodeIdStr));
|
|
484
|
+
|
|
485
|
+
// Publish event
|
|
486
|
+
publishMeshEvent("node_join", nodeId);
|
|
487
|
+
|
|
488
|
+
// Update state
|
|
489
|
+
NodeState state;
|
|
490
|
+
state.nodeId = nodeId;
|
|
491
|
+
state.role = (nodeId % 2 == 0) ? "sensor" : "repeater";
|
|
492
|
+
state.online = true;
|
|
493
|
+
state.lastSeen = millis();
|
|
494
|
+
state.connectionCount = 1;
|
|
495
|
+
lastTopology[nodeId] = state;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Mark removed nodes as offline
|
|
499
|
+
for (auto nodeId : removedNodes) {
|
|
500
|
+
JsonObject node = nodes.add<JsonObject>();
|
|
501
|
+
|
|
502
|
+
char nodeIdStr[17];
|
|
503
|
+
snprintf(nodeIdStr, sizeof(nodeIdStr), "ALT-%012X", nodeId);
|
|
504
|
+
node["node_id"] = nodeIdStr;
|
|
505
|
+
node["status"] = "offline";
|
|
506
|
+
node["last_seen"] = getTimestamp();
|
|
507
|
+
|
|
508
|
+
Serial.println(" - Node left: " + String(nodeIdStr));
|
|
509
|
+
|
|
510
|
+
// Publish event
|
|
511
|
+
publishMeshEvent("node_leave", nodeId, "timeout");
|
|
512
|
+
|
|
513
|
+
// Remove from state
|
|
514
|
+
lastTopology.erase(nodeId);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
String topic = "alteriom/mesh/" + String(MESH_ID) + "/topology";
|
|
518
|
+
publishMQTT(topic, doc);
|
|
519
|
+
|
|
520
|
+
Serial.println("✅ Incremental update published");
|
|
521
|
+
}
|
|
522
|
+
#endif
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Check for incoming MQTT commands
|
|
527
|
+
*/
|
|
528
|
+
void checkCommands() {
|
|
529
|
+
#if IS_GATEWAY
|
|
530
|
+
mqttClient.loop();
|
|
531
|
+
#endif
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ============================================================================
|
|
535
|
+
// MQTT COMMAND HANDLER (GATEWAY ONLY)
|
|
536
|
+
// ============================================================================
|
|
537
|
+
|
|
538
|
+
#if IS_GATEWAY
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Handle incoming MQTT messages (commands)
|
|
542
|
+
*/
|
|
543
|
+
void mqttCallback(char* topic, byte* payload, unsigned int length) {
|
|
544
|
+
Serial.println("\n📥 MQTT message received:");
|
|
545
|
+
Serial.println(" Topic: " + String(topic));
|
|
546
|
+
|
|
547
|
+
// Parse JSON
|
|
548
|
+
JsonDocument doc;
|
|
549
|
+
DeserializationError error = deserializeJson(doc, payload, length);
|
|
550
|
+
|
|
551
|
+
if (error) {
|
|
552
|
+
Serial.println(" ❌ JSON parse error: " + String(error.c_str()));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
String event = doc["event"];
|
|
557
|
+
String command = doc["command"];
|
|
558
|
+
String correlationId = doc["correlation_id"];
|
|
559
|
+
|
|
560
|
+
Serial.println(" Event: " + event);
|
|
561
|
+
Serial.println(" Command: " + command);
|
|
562
|
+
Serial.println(" Correlation ID: " + correlationId);
|
|
563
|
+
|
|
564
|
+
// Handle GET_TOPOLOGY command (command ID 300)
|
|
565
|
+
if (event == "command" && command == "get_topology") {
|
|
566
|
+
Serial.println(" 🔍 Processing GET_TOPOLOGY command...");
|
|
567
|
+
|
|
568
|
+
unsigned long startTime = millis();
|
|
569
|
+
|
|
570
|
+
// Build topology response
|
|
571
|
+
JsonDocument responseDoc;
|
|
572
|
+
responseDoc["event"] = "command_response";
|
|
573
|
+
responseDoc["command"] = "get_topology";
|
|
574
|
+
responseDoc["correlation_id"] = correlationId;
|
|
575
|
+
responseDoc["success"] = true;
|
|
576
|
+
|
|
577
|
+
// Build topology result
|
|
578
|
+
JsonObject result = responseDoc["result"].to<JsonObject>();
|
|
579
|
+
result["event"] = "mesh_topology";
|
|
580
|
+
result["mesh_id"] = MESH_ID;
|
|
581
|
+
result["gateway_node_id"] = getDeviceId();
|
|
582
|
+
result["update_type"] = "full";
|
|
583
|
+
|
|
584
|
+
SimpleList<uint32_t> nodeList = mesh.getNodeList();
|
|
585
|
+
|
|
586
|
+
// Add nodes
|
|
587
|
+
JsonArray nodes = result["nodes"].to<JsonArray>();
|
|
588
|
+
|
|
589
|
+
JsonObject gatewayNode = nodes.add<JsonObject>();
|
|
590
|
+
gatewayNode["node_id"] = getDeviceId();
|
|
591
|
+
gatewayNode["role"] = NODE_ROLE;
|
|
592
|
+
gatewayNode["status"] = "online";
|
|
593
|
+
gatewayNode["connection_count"] = nodeList.size();
|
|
594
|
+
|
|
595
|
+
for (auto nodeId : nodeList) {
|
|
596
|
+
JsonObject node = nodes.add<JsonObject>();
|
|
597
|
+
char nodeIdStr[17];
|
|
598
|
+
snprintf(nodeIdStr, sizeof(nodeIdStr), "ALT-%012X", nodeId);
|
|
599
|
+
node["node_id"] = nodeIdStr;
|
|
600
|
+
node["role"] = (nodeId % 2 == 0) ? "sensor" : "repeater";
|
|
601
|
+
node["status"] = "online";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Add connections
|
|
605
|
+
JsonArray connections = result["connections"].to<JsonArray>();
|
|
606
|
+
for (auto nodeId : nodeList) {
|
|
607
|
+
JsonObject conn = connections.add<JsonObject>();
|
|
608
|
+
conn["from_node"] = getDeviceId();
|
|
609
|
+
char nodeIdStr[17];
|
|
610
|
+
snprintf(nodeIdStr, sizeof(nodeIdStr), "ALT-%012X", nodeId);
|
|
611
|
+
conn["to_node"] = nodeIdStr;
|
|
612
|
+
conn["quality"] = calculateQuality(nodeId);
|
|
613
|
+
conn["hop_count"] = 1;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Add metrics
|
|
617
|
+
JsonObject metrics = result["metrics"].to<JsonObject>();
|
|
618
|
+
metrics["total_nodes"] = nodeList.size() + 1;
|
|
619
|
+
metrics["online_nodes"] = nodeList.size() + 1;
|
|
620
|
+
|
|
621
|
+
// Calculate latency
|
|
622
|
+
unsigned long latency = millis() - startTime;
|
|
623
|
+
responseDoc["latency_ms"] = latency;
|
|
624
|
+
|
|
625
|
+
// Publish response
|
|
626
|
+
String responseTopic = "alteriom/mesh/" + String(MESH_ID) + "/topology/response";
|
|
627
|
+
publishMQTT(responseTopic, responseDoc);
|
|
628
|
+
|
|
629
|
+
Serial.println(" ✅ GET_TOPOLOGY response sent");
|
|
630
|
+
Serial.println(" Latency: " + String(latency) + "ms");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
#endif
|
|
635
|
+
|
|
636
|
+
// ============================================================================
|
|
637
|
+
// MESH CALLBACKS
|
|
638
|
+
// ============================================================================
|
|
639
|
+
|
|
640
|
+
void receivedCallback(uint32_t from, String &msg) {
|
|
641
|
+
Serial.printf("Received from %u: %s\n", from, msg.c_str());
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
void newConnectionCallback(uint32_t nodeId) {
|
|
645
|
+
Serial.printf("✅ New connection: %u\n", nodeId);
|
|
646
|
+
|
|
647
|
+
#if IS_GATEWAY
|
|
648
|
+
// Trigger incremental update on next check
|
|
649
|
+
lastTopologyUpdate = 0;
|
|
650
|
+
#endif
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
void changedConnectionCallback() {
|
|
654
|
+
Serial.println("🔄 Connections changed");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
void nodeTimeAdjustedCallback(int32_t offset) {
|
|
658
|
+
Serial.printf("⏰ Time adjusted: %d us\n", offset);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
void droppedConnectionCallback(uint32_t nodeId) {
|
|
662
|
+
Serial.printf("❌ Connection dropped: %u\n", nodeId);
|
|
663
|
+
|
|
664
|
+
#if IS_GATEWAY
|
|
665
|
+
// Trigger incremental update on next check
|
|
666
|
+
lastTopologyUpdate = 0;
|
|
667
|
+
#endif
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ============================================================================
|
|
671
|
+
// SETUP
|
|
672
|
+
// ============================================================================
|
|
673
|
+
|
|
674
|
+
void setup() {
|
|
675
|
+
Serial.begin(115200);
|
|
676
|
+
delay(1000);
|
|
677
|
+
|
|
678
|
+
Serial.println("\n\n");
|
|
679
|
+
Serial.println("================================");
|
|
680
|
+
Serial.println("Alteriom Mesh Topology Test");
|
|
681
|
+
Serial.println("================================");
|
|
682
|
+
Serial.println("Device ID: " + getDeviceId());
|
|
683
|
+
Serial.println("Role: " + String(NODE_ROLE));
|
|
684
|
+
Serial.println("Firmware: " + String(FIRMWARE_VERSION));
|
|
685
|
+
Serial.println("Gateway: " + String(IS_GATEWAY ? "YES" : "NO"));
|
|
686
|
+
Serial.println("================================\n");
|
|
687
|
+
|
|
688
|
+
// Initialize mesh
|
|
689
|
+
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
|
|
690
|
+
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
|
|
691
|
+
|
|
692
|
+
// Set callbacks
|
|
693
|
+
mesh.onReceive(&receivedCallback);
|
|
694
|
+
mesh.onNewConnection(&newConnectionCallback);
|
|
695
|
+
mesh.onChangedConnections(&changedConnectionCallback);
|
|
696
|
+
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
|
|
697
|
+
mesh.onDroppedConnection(&droppedConnectionCallback);
|
|
698
|
+
|
|
699
|
+
#if IS_GATEWAY
|
|
700
|
+
// Gateway: Connect to WiFi
|
|
701
|
+
Serial.println("Connecting to WiFi: " + String(WIFI_SSID));
|
|
702
|
+
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
|
703
|
+
|
|
704
|
+
while (WiFi.status() != WL_CONNECTED) {
|
|
705
|
+
delay(500);
|
|
706
|
+
Serial.print(".");
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
Serial.println("\n✅ WiFi connected!");
|
|
710
|
+
Serial.println(" IP: " + WiFi.localIP().toString());
|
|
711
|
+
|
|
712
|
+
// Connect to MQTT
|
|
713
|
+
mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
|
|
714
|
+
mqttClient.setCallback(mqttCallback);
|
|
715
|
+
mqttClient.setBufferSize(2048); // Increase buffer for large topology messages
|
|
716
|
+
connectMQTT();
|
|
717
|
+
|
|
718
|
+
// Add topology reporting tasks
|
|
719
|
+
userScheduler.addTask(taskSendFullTopology);
|
|
720
|
+
userScheduler.addTask(taskCheckIncrementalUpdate);
|
|
721
|
+
userScheduler.addTask(taskCheckCommands);
|
|
722
|
+
|
|
723
|
+
taskSendFullTopology.enable();
|
|
724
|
+
taskCheckIncrementalUpdate.enable();
|
|
725
|
+
taskCheckCommands.enable();
|
|
726
|
+
|
|
727
|
+
Serial.println("✅ Topology reporting enabled");
|
|
728
|
+
Serial.println(" Full topology: every " + String(FULL_TOPOLOGY_INTERVAL/1000) + "s");
|
|
729
|
+
Serial.println(" Incremental check: every " + String(INCREMENTAL_INTERVAL/1000) + "s");
|
|
730
|
+
#endif
|
|
731
|
+
|
|
732
|
+
Serial.println("\n🚀 Setup complete! Mesh is running...\n");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ============================================================================
|
|
736
|
+
// LOOP
|
|
737
|
+
// ============================================================================
|
|
738
|
+
|
|
739
|
+
void loop() {
|
|
740
|
+
mesh.update();
|
|
741
|
+
|
|
742
|
+
#if IS_GATEWAY
|
|
743
|
+
// Keep MQTT connected
|
|
744
|
+
if (!mqttClient.connected()) {
|
|
745
|
+
connectMQTT();
|
|
746
|
+
}
|
|
747
|
+
#endif
|
|
748
|
+
}
|