@alteriom/painlessmesh 1.9.18 → 1.9.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +82 -63
- package/examples/alteriom/README.md +4 -4
- package/examples/alteriom/alteriom_custom_package_template.hpp +320 -0
- package/examples/alteriom/alteriom_sensor_package.hpp +1 -1
- package/examples/alteriom/mppt_example/alteriom_mppt_example.ino +208 -0
- package/examples/bridge_failover/bridge_failover.ino +17 -0
- package/examples/sendToInternet/CMakeLists.txt +54 -0
- package/examples/sendToInternet/PC_NODE_README.md +517 -0
- package/examples/sendToInternet/README.md +39 -1
- package/examples/sendToInternet/build.sh +153 -0
- package/examples/sendToInternet/mock_server_test.ino +361 -0
- package/examples/sendToInternet/pc_mesh_node.cpp +361 -0
- package/library.json +4 -1
- package/library.properties +1 -1
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +5 -13
- package/src/arduino/wifi.hpp +306 -100
- package/src/connection.cpp +10 -0
- package/src/painlessMesh.h +1 -14
- package/src/painlessmesh/connection.hpp +11 -16
- package/src/painlessmesh/gateway.hpp +0 -1061
- package/src/painlessmesh/mesh.hpp +58 -86
- package/src/painlessmesh/message_queue.hpp +1 -2
- package/src/painlessmesh/metrics.hpp +2 -262
- package/src/painlessmesh/validation.hpp +0 -143
- package/docs/README.md +0 -132
- package/docs/alteriom/overview.md +0 -531
- package/docs/api/core-api.md +0 -607
- package/docs/api/shared-gateway.md +0 -1207
- package/docs/architecture/mesh-architecture.md +0 -399
- package/docs/architecture/plugin-system.md +0 -517
- package/docs/getting-started/arduino-manual-install.md +0 -313
- package/docs/getting-started/first-mesh.md +0 -410
- package/docs/getting-started/installation.md +0 -275
- package/docs/getting-started/quickstart.md +0 -158
- package/docs/troubleshooting/common-issues.md +0 -679
- package/docs/troubleshooting/debugging.md +0 -455
- package/docs/troubleshooting/external-device-connection.md +0 -283
- package/docs/troubleshooting/faq.md +0 -574
- package/docs/tutorials/basic-examples.md +0 -718
|
@@ -345,22 +345,6 @@ struct SharedGatewayConfig {
|
|
|
345
345
|
*/
|
|
346
346
|
bool hasRouterCredentials() const { return routerSSID.length() > 0; }
|
|
347
347
|
|
|
348
|
-
/**
|
|
349
|
-
* @brief Get the estimated memory footprint in bytes
|
|
350
|
-
*
|
|
351
|
-
* Returns an estimate of the memory used by this configuration instance.
|
|
352
|
-
* Useful for monitoring memory usage on constrained devices.
|
|
353
|
-
*
|
|
354
|
-
* @return Estimated memory usage in bytes
|
|
355
|
-
*/
|
|
356
|
-
size_t estimatedMemoryFootprint() const {
|
|
357
|
-
size_t baseSize = sizeof(SharedGatewayConfig);
|
|
358
|
-
// Add dynamic string content (not included in sizeof)
|
|
359
|
-
baseSize += routerSSID.length();
|
|
360
|
-
baseSize += routerPassword.length();
|
|
361
|
-
baseSize += internetCheckHost.length();
|
|
362
|
-
return baseSize;
|
|
363
|
-
}
|
|
364
348
|
};
|
|
365
349
|
|
|
366
350
|
/**
|
|
@@ -890,21 +874,6 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
890
874
|
return ((nodeId & 0xFFFF) << 16) | counter;
|
|
891
875
|
}
|
|
892
876
|
|
|
893
|
-
/**
|
|
894
|
-
* @brief Get the estimated memory footprint
|
|
895
|
-
*
|
|
896
|
-
* Returns an estimate of the memory used by this package instance.
|
|
897
|
-
*
|
|
898
|
-
* @return Estimated memory usage in bytes
|
|
899
|
-
*/
|
|
900
|
-
size_t estimatedMemoryFootprint() const {
|
|
901
|
-
size_t baseSize = sizeof(GatewayDataPackage);
|
|
902
|
-
// Add dynamic string content (not included in sizeof)
|
|
903
|
-
baseSize += destination.length();
|
|
904
|
-
baseSize += payload.length();
|
|
905
|
-
baseSize += contentType.length();
|
|
906
|
-
return baseSize;
|
|
907
|
-
}
|
|
908
877
|
};
|
|
909
878
|
|
|
910
879
|
/**
|
|
@@ -1082,1039 +1051,9 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1082
1051
|
}
|
|
1083
1052
|
#endif
|
|
1084
1053
|
|
|
1085
|
-
/**
|
|
1086
|
-
* @brief Get the estimated memory footprint
|
|
1087
|
-
*
|
|
1088
|
-
* Returns an estimate of the memory used by this package instance.
|
|
1089
|
-
*
|
|
1090
|
-
* @return Estimated memory usage in bytes
|
|
1091
|
-
*/
|
|
1092
|
-
size_t estimatedMemoryFootprint() const {
|
|
1093
|
-
size_t baseSize = sizeof(GatewayAckPackage);
|
|
1094
|
-
// Add dynamic string content (not included in sizeof)
|
|
1095
|
-
baseSize += error.length();
|
|
1096
|
-
return baseSize;
|
|
1097
|
-
}
|
|
1098
|
-
};
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* @brief Gateway Heartbeat Package for primary gateway health monitoring
|
|
1102
|
-
*
|
|
1103
|
-
* This package is broadcast periodically by the primary gateway to inform all
|
|
1104
|
-
* nodes in the mesh about the gateway's health status. It provides essential
|
|
1105
|
-
* information for:
|
|
1106
|
-
* - Gateway availability monitoring
|
|
1107
|
-
* - Internet connectivity status
|
|
1108
|
-
* - Gateway election decision-making
|
|
1109
|
-
* - Failover detection
|
|
1110
|
-
*
|
|
1111
|
-
* BROADCAST INTERVAL
|
|
1112
|
-
* ==================
|
|
1113
|
-
* The primary gateway should broadcast heartbeats at the interval configured
|
|
1114
|
-
* in SharedGatewayConfig::gatewayHeartbeatInterval (default: 15 seconds).
|
|
1115
|
-
*
|
|
1116
|
-
* TIMEOUT DETECTION
|
|
1117
|
-
* =================
|
|
1118
|
-
* If no heartbeat is received within SharedGatewayConfig::gatewayFailureTimeout
|
|
1119
|
-
* (default: 45 seconds), the gateway should be considered failed and election
|
|
1120
|
-
* may begin.
|
|
1121
|
-
*
|
|
1122
|
-
* MEMORY FOOTPRINT
|
|
1123
|
-
* ================
|
|
1124
|
-
* The GatewayHeartbeatPackage structure has an estimated memory footprint of:
|
|
1125
|
-
* - Base fields (from BroadcastPackage): ~16 bytes
|
|
1126
|
-
* - Fixed fields (isPrimary, hasInternet, routerRSSI, uptime, timestamp): ~11 bytes
|
|
1127
|
-
* - Total estimated: ~27 bytes
|
|
1128
|
-
*
|
|
1129
|
-
* For ESP8266 with ~80KB RAM, this represents <0.1% of available memory.
|
|
1130
|
-
* For ESP32 with ~320KB RAM, this represents <0.01% of available memory.
|
|
1131
|
-
*
|
|
1132
|
-
* Example usage:
|
|
1133
|
-
* @code
|
|
1134
|
-
* // Primary gateway broadcasting heartbeat
|
|
1135
|
-
* GatewayHeartbeatPackage heartbeat;
|
|
1136
|
-
* heartbeat.from = mesh.getNodeId();
|
|
1137
|
-
* heartbeat.isPrimary = true;
|
|
1138
|
-
* heartbeat.hasInternet = internetChecker.hasLocalInternet();
|
|
1139
|
-
* heartbeat.routerRSSI = WiFi.RSSI();
|
|
1140
|
-
* heartbeat.uptime = millis() / 1000; // Convert to seconds
|
|
1141
|
-
* heartbeat.timestamp = mesh.getNodeTime();
|
|
1142
|
-
*
|
|
1143
|
-
* mesh.sendPackage(&heartbeat);
|
|
1144
|
-
*
|
|
1145
|
-
* // Node receiving heartbeat
|
|
1146
|
-
* void onHeartbeat(GatewayHeartbeatPackage& heartbeat) {
|
|
1147
|
-
* lastGatewayHeartbeat = millis();
|
|
1148
|
-
* primaryGatewayId = heartbeat.from;
|
|
1149
|
-
* hasGatewayInternet = heartbeat.hasInternet;
|
|
1150
|
-
* }
|
|
1151
|
-
* @endcode
|
|
1152
|
-
*
|
|
1153
|
-
* Type ID: 622 (GATEWAY_HEARTBEAT)
|
|
1154
|
-
* Base class: BroadcastPackage (sent to all nodes in mesh)
|
|
1155
|
-
*/
|
|
1156
|
-
class GatewayHeartbeatPackage : public plugin::BroadcastPackage {
|
|
1157
|
-
public:
|
|
1158
|
-
/**
|
|
1159
|
-
* @brief Indicates if this is the primary gateway
|
|
1160
|
-
*
|
|
1161
|
-
* True if the node sending this heartbeat is the currently elected
|
|
1162
|
-
* primary gateway. Secondary/standby gateways may also send heartbeats
|
|
1163
|
-
* with isPrimary = false for coordination purposes.
|
|
1164
|
-
*/
|
|
1165
|
-
bool isPrimary = false;
|
|
1166
|
-
|
|
1167
|
-
/**
|
|
1168
|
-
* @brief Indicates if this gateway has Internet connectivity
|
|
1169
|
-
*
|
|
1170
|
-
* True if the gateway can currently reach the Internet.
|
|
1171
|
-
* Used by other nodes to determine if data can be routed
|
|
1172
|
-
* to Internet services through this gateway.
|
|
1173
|
-
*/
|
|
1174
|
-
bool hasInternet = false;
|
|
1175
|
-
|
|
1176
|
-
/**
|
|
1177
|
-
* @brief Signal strength to the router in dBm
|
|
1178
|
-
*
|
|
1179
|
-
* The RSSI value indicating signal quality to the external WiFi router.
|
|
1180
|
-
* Range: typically -90 (weak) to -30 (strong) dBm.
|
|
1181
|
-
* Used in gateway election to prefer gateways with better connections.
|
|
1182
|
-
* Set to 0 if not connected to a router.
|
|
1183
|
-
*/
|
|
1184
|
-
int8_t routerRSSI = 0;
|
|
1185
|
-
|
|
1186
|
-
/**
|
|
1187
|
-
* @brief Gateway uptime in seconds
|
|
1188
|
-
*
|
|
1189
|
-
* How long this gateway has been running, in seconds.
|
|
1190
|
-
* Used in election tiebreakers - longer uptime indicates stability.
|
|
1191
|
-
*/
|
|
1192
|
-
uint32_t uptime = 0;
|
|
1193
|
-
|
|
1194
|
-
/**
|
|
1195
|
-
* @brief Heartbeat timestamp
|
|
1196
|
-
*
|
|
1197
|
-
* Mesh time when this heartbeat was generated.
|
|
1198
|
-
* Used for calculating latency and detecting stale heartbeats.
|
|
1199
|
-
*/
|
|
1200
|
-
uint32_t timestamp = 0;
|
|
1201
|
-
|
|
1202
|
-
/**
|
|
1203
|
-
* @brief Number of additional JSON fields in this package
|
|
1204
|
-
*
|
|
1205
|
-
* Used for jsonObjectSize() calculation in ArduinoJson v6.
|
|
1206
|
-
* Count: primary, internet, rssi, uptime, ts = 5 fields
|
|
1207
|
-
*/
|
|
1208
|
-
static constexpr int numPackageFields = 5;
|
|
1209
|
-
|
|
1210
|
-
/**
|
|
1211
|
-
* @brief Default constructor
|
|
1212
|
-
*
|
|
1213
|
-
* Creates a GatewayHeartbeatPackage with type ID 622 (GATEWAY_HEARTBEAT).
|
|
1214
|
-
*/
|
|
1215
|
-
GatewayHeartbeatPackage() : BroadcastPackage(protocol::GATEWAY_HEARTBEAT) {}
|
|
1216
|
-
|
|
1217
|
-
/**
|
|
1218
|
-
* @brief Construct from JSON object
|
|
1219
|
-
*
|
|
1220
|
-
* Deserializes a GatewayHeartbeatPackage from a JSON object.
|
|
1221
|
-
* Compatible with ArduinoJson v6 and v7.
|
|
1222
|
-
*
|
|
1223
|
-
* @param jsonObj JSON object containing package data
|
|
1224
|
-
*/
|
|
1225
|
-
GatewayHeartbeatPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
|
|
1226
|
-
isPrimary = jsonObj["primary"] | false;
|
|
1227
|
-
hasInternet = jsonObj["internet"] | false;
|
|
1228
|
-
routerRSSI = jsonObj["rssi"] | 0;
|
|
1229
|
-
uptime = jsonObj["uptime"] | 0;
|
|
1230
|
-
timestamp = jsonObj["ts"] | 0;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
/**
|
|
1234
|
-
* @brief Serialize to JSON object
|
|
1235
|
-
*
|
|
1236
|
-
* Adds all package fields to the provided JSON object.
|
|
1237
|
-
*
|
|
1238
|
-
* @param jsonObj JSON object to add fields to
|
|
1239
|
-
* @return The modified JSON object
|
|
1240
|
-
*/
|
|
1241
|
-
JsonObject addTo(JsonObject&& jsonObj) const {
|
|
1242
|
-
jsonObj = BroadcastPackage::addTo(std::move(jsonObj));
|
|
1243
|
-
jsonObj["primary"] = isPrimary;
|
|
1244
|
-
jsonObj["internet"] = hasInternet;
|
|
1245
|
-
jsonObj["rssi"] = routerRSSI;
|
|
1246
|
-
jsonObj["uptime"] = uptime;
|
|
1247
|
-
jsonObj["ts"] = timestamp;
|
|
1248
|
-
return jsonObj;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
#if ARDUINOJSON_VERSION_MAJOR < 7
|
|
1252
|
-
/**
|
|
1253
|
-
* @brief Calculate JSON object size for ArduinoJson v6
|
|
1254
|
-
*
|
|
1255
|
-
* Used for buffer allocation when serializing.
|
|
1256
|
-
*
|
|
1257
|
-
* @return Estimated size in bytes
|
|
1258
|
-
*/
|
|
1259
|
-
size_t jsonObjectSize() const {
|
|
1260
|
-
// noJsonFields (from base class) + numPackageFields (our fields)
|
|
1261
|
-
return JSON_OBJECT_SIZE(noJsonFields + numPackageFields);
|
|
1262
|
-
}
|
|
1263
|
-
#endif
|
|
1264
|
-
|
|
1265
|
-
/**
|
|
1266
|
-
* @brief Get the estimated memory footprint
|
|
1267
|
-
*
|
|
1268
|
-
* Returns an estimate of the memory used by this package instance.
|
|
1269
|
-
*
|
|
1270
|
-
* @return Estimated memory usage in bytes
|
|
1271
|
-
*/
|
|
1272
|
-
size_t estimatedMemoryFootprint() const {
|
|
1273
|
-
// No dynamic strings, so sizeof gives accurate footprint
|
|
1274
|
-
return sizeof(GatewayHeartbeatPackage);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
/**
|
|
1278
|
-
* @brief Check if this heartbeat indicates a healthy gateway
|
|
1279
|
-
*
|
|
1280
|
-
* A gateway is considered healthy if it is primary and has Internet access.
|
|
1281
|
-
*
|
|
1282
|
-
* @return true if the gateway is healthy
|
|
1283
|
-
*/
|
|
1284
|
-
bool isHealthy() const {
|
|
1285
|
-
return isPrimary && hasInternet;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
/**
|
|
1289
|
-
* @brief Check if the router signal strength is acceptable
|
|
1290
|
-
*
|
|
1291
|
-
* Signal strength is considered acceptable if RSSI is better than -70 dBm.
|
|
1292
|
-
* This is a typical threshold for reliable WiFi connectivity.
|
|
1293
|
-
*
|
|
1294
|
-
* @return true if signal strength is acceptable
|
|
1295
|
-
*/
|
|
1296
|
-
bool hasAcceptableSignal() const {
|
|
1297
|
-
// RSSI of 0 typically means not connected
|
|
1298
|
-
// RSSI better than -70 dBm is considered acceptable
|
|
1299
|
-
return routerRSSI != 0 && routerRSSI > -70;
|
|
1300
|
-
}
|
|
1301
|
-
};
|
|
1302
|
-
|
|
1303
|
-
/**
|
|
1304
|
-
* @brief Gateway Election Manager for coordinating primary gateway selection
|
|
1305
|
-
*
|
|
1306
|
-
* This class implements a deterministic election protocol for selecting a
|
|
1307
|
-
* primary gateway in the mesh network. It provides:
|
|
1308
|
-
* - Primary gateway failure detection via heartbeat monitoring
|
|
1309
|
-
* - Deterministic winner selection (highest RSSI, then highest node ID)
|
|
1310
|
-
* - Split-brain prevention during elections
|
|
1311
|
-
* - Cooldown period to prevent election thrashing
|
|
1312
|
-
*
|
|
1313
|
-
* ELECTION STATE MACHINE
|
|
1314
|
-
* ======================
|
|
1315
|
-
* The manager operates in three states:
|
|
1316
|
-
* 1. IDLE: Monitoring heartbeats, no election in progress
|
|
1317
|
-
* 2. ELECTION_RUNNING: Election triggered, collecting candidates
|
|
1318
|
-
* 3. COOLDOWN: Post-election cooldown to prevent rapid re-elections
|
|
1319
|
-
*
|
|
1320
|
-
* ELECTION ALGORITHM
|
|
1321
|
-
* ==================
|
|
1322
|
-
* Only nodes with Internet connectivity (hasInternet == true) can be candidates.
|
|
1323
|
-
* Winner is selected by:
|
|
1324
|
-
* 1. Highest RSSI wins
|
|
1325
|
-
* 2. If RSSI tie, highest node ID wins
|
|
1326
|
-
*
|
|
1327
|
-
* This ensures deterministic, consistent winner selection across all nodes.
|
|
1328
|
-
*
|
|
1329
|
-
* MEMORY FOOTPRINT
|
|
1330
|
-
* ================
|
|
1331
|
-
* The GatewayElectionManager has an estimated memory footprint of:
|
|
1332
|
-
* - Fixed fields: ~60 bytes
|
|
1333
|
-
* - Candidate map: ~40 bytes per candidate + map overhead
|
|
1334
|
-
* - Total estimated: ~200-500 bytes depending on candidate count
|
|
1335
|
-
*
|
|
1336
|
-
* Example usage:
|
|
1337
|
-
* @code
|
|
1338
|
-
* GatewayElectionManager election;
|
|
1339
|
-
* election.configure(gatewayConfig);
|
|
1340
|
-
* election.setNodeId(mesh.getNodeId());
|
|
1341
|
-
* election.setLocalCandidate(internetChecker.hasLocalInternet(), WiFi.RSSI());
|
|
1342
|
-
*
|
|
1343
|
-
* // In heartbeat callback
|
|
1344
|
-
* void onHeartbeat(GatewayHeartbeatPackage& heartbeat) {
|
|
1345
|
-
* election.processHeartbeat(heartbeat);
|
|
1346
|
-
* }
|
|
1347
|
-
*
|
|
1348
|
-
* // In update loop (call periodically)
|
|
1349
|
-
* if (election.update(millis())) {
|
|
1350
|
-
* // This node won the election, broadcast as primary
|
|
1351
|
-
* broadcastPrimaryHeartbeat();
|
|
1352
|
-
* }
|
|
1353
|
-
*
|
|
1354
|
-
* // Register callback for election results
|
|
1355
|
-
* election.onElectionResult([](uint32_t winnerId, bool isLocal) {
|
|
1356
|
-
* Serial.printf("Election winner: %u (local: %s)\n",
|
|
1357
|
-
* winnerId, isLocal ? "yes" : "no");
|
|
1358
|
-
* });
|
|
1359
|
-
* @endcode
|
|
1360
|
-
*/
|
|
1361
|
-
class GatewayElectionManager {
|
|
1362
|
-
public:
|
|
1363
|
-
/**
|
|
1364
|
-
* @brief Election state machine states
|
|
1365
|
-
*/
|
|
1366
|
-
enum class ElectionState {
|
|
1367
|
-
IDLE, ///< Monitoring heartbeats, no election in progress
|
|
1368
|
-
ELECTION_RUNNING, ///< Election triggered, collecting candidates
|
|
1369
|
-
COOLDOWN ///< Post-election cooldown period
|
|
1370
|
-
};
|
|
1371
|
-
|
|
1372
|
-
/**
|
|
1373
|
-
* @brief Callback type for election result notifications
|
|
1374
|
-
* @param winnerId The node ID of the election winner
|
|
1375
|
-
* @param isLocalNode True if this node is the winner
|
|
1376
|
-
*/
|
|
1377
|
-
typedef std::function<void(uint32_t winnerId, bool isLocalNode)>
|
|
1378
|
-
ElectionResultCallback_t;
|
|
1379
|
-
|
|
1380
|
-
/**
|
|
1381
|
-
* @brief Default constructor
|
|
1382
|
-
*
|
|
1383
|
-
* Creates a GatewayElectionManager with default configuration:
|
|
1384
|
-
* - gatewayFailureTimeout: 45000ms
|
|
1385
|
-
* - electionDuration: 5000ms
|
|
1386
|
-
* - electionCooldownPeriod: 60000ms
|
|
1387
|
-
*/
|
|
1388
|
-
GatewayElectionManager()
|
|
1389
|
-
: state_(ElectionState::IDLE),
|
|
1390
|
-
nodeId_(0),
|
|
1391
|
-
localHasInternet_(false),
|
|
1392
|
-
localRssi_(0),
|
|
1393
|
-
primaryGatewayId_(0),
|
|
1394
|
-
isElectedPrimary_(false),
|
|
1395
|
-
lastPrimaryHeartbeatTime_(0),
|
|
1396
|
-
electionStartTime_(0),
|
|
1397
|
-
cooldownStartTime_(0),
|
|
1398
|
-
gatewayFailureTimeout_(45000),
|
|
1399
|
-
electionDuration_(5000),
|
|
1400
|
-
electionCooldownPeriod_(60000) {}
|
|
1401
|
-
|
|
1402
|
-
/**
|
|
1403
|
-
* @brief Configure the election manager using SharedGatewayConfig
|
|
1404
|
-
*
|
|
1405
|
-
* Updates the failure timeout from the provided configuration.
|
|
1406
|
-
*
|
|
1407
|
-
* @param config SharedGatewayConfig with timeout parameters
|
|
1408
|
-
*/
|
|
1409
|
-
void configure(const SharedGatewayConfig& config) {
|
|
1410
|
-
gatewayFailureTimeout_ = config.gatewayFailureTimeout;
|
|
1411
|
-
Log(logger::GENERAL,
|
|
1412
|
-
"GatewayElectionManager: Configured with failureTimeout=%ums\n",
|
|
1413
|
-
gatewayFailureTimeout_);
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
/**
|
|
1417
|
-
* @brief Set the local node ID
|
|
1418
|
-
*
|
|
1419
|
-
* Must be called before the manager can participate in elections.
|
|
1420
|
-
*
|
|
1421
|
-
* @param nodeId The local node's unique identifier
|
|
1422
|
-
*/
|
|
1423
|
-
void setNodeId(uint32_t nodeId) {
|
|
1424
|
-
nodeId_ = nodeId;
|
|
1425
|
-
Log(logger::GENERAL, "GatewayElectionManager: Node ID set to %u\n", nodeId_);
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
/**
|
|
1429
|
-
* @brief Set the local node's election candidacy parameters
|
|
1430
|
-
*
|
|
1431
|
-
* Updates whether this node can participate in elections based on
|
|
1432
|
-
* Internet connectivity and signal strength.
|
|
1433
|
-
*
|
|
1434
|
-
* @param hasInternet True if this node has Internet connectivity
|
|
1435
|
-
* @param rssi Signal strength to the router in dBm
|
|
1436
|
-
*/
|
|
1437
|
-
void setLocalCandidate(bool hasInternet, int8_t rssi) {
|
|
1438
|
-
localHasInternet_ = hasInternet;
|
|
1439
|
-
localRssi_ = rssi;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
/**
|
|
1443
|
-
* @brief Process an incoming gateway heartbeat
|
|
1444
|
-
*
|
|
1445
|
-
* Updates internal state based on the received heartbeat:
|
|
1446
|
-
* - If from primary, updates last heartbeat time
|
|
1447
|
-
* - If during election, may defer to higher-priority primary
|
|
1448
|
-
*
|
|
1449
|
-
* @param heartbeat The received GatewayHeartbeatPackage
|
|
1450
|
-
* @param currentTime Current time in milliseconds (e.g., millis())
|
|
1451
|
-
*/
|
|
1452
|
-
void processHeartbeat(const GatewayHeartbeatPackage& heartbeat,
|
|
1453
|
-
uint32_t currentTime) {
|
|
1454
|
-
// Track this node as a potential candidate if it has Internet
|
|
1455
|
-
if (heartbeat.hasInternet) {
|
|
1456
|
-
Candidate candidate;
|
|
1457
|
-
candidate.nodeId = heartbeat.from;
|
|
1458
|
-
candidate.rssi = heartbeat.routerRSSI;
|
|
1459
|
-
candidate.hasInternet = heartbeat.hasInternet;
|
|
1460
|
-
candidate.lastSeen = currentTime;
|
|
1461
|
-
candidates_[heartbeat.from] = candidate;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
// Handle heartbeat from a node claiming to be primary
|
|
1465
|
-
if (heartbeat.isPrimary) {
|
|
1466
|
-
// During election, check for split-brain prevention
|
|
1467
|
-
if (state_ == ElectionState::ELECTION_RUNNING) {
|
|
1468
|
-
// If sender has higher priority (higher RSSI, or same RSSI and higher
|
|
1469
|
-
// nodeId), defer to them
|
|
1470
|
-
if (shouldDeferTo(heartbeat.routerRSSI, heartbeat.from)) {
|
|
1471
|
-
Log(logger::GENERAL,
|
|
1472
|
-
"GatewayElectionManager: Deferring to primary node %u "
|
|
1473
|
-
"(RSSI=%d)\n",
|
|
1474
|
-
heartbeat.from, heartbeat.routerRSSI);
|
|
1475
|
-
primaryGatewayId_ = heartbeat.from;
|
|
1476
|
-
isElectedPrimary_ = false;
|
|
1477
|
-
lastPrimaryHeartbeatTime_ = currentTime;
|
|
1478
|
-
transitionToCooldown(currentTime);
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
} else {
|
|
1482
|
-
// Not in election, accept this node as primary
|
|
1483
|
-
primaryGatewayId_ = heartbeat.from;
|
|
1484
|
-
lastPrimaryHeartbeatTime_ = currentTime;
|
|
1485
|
-
|
|
1486
|
-
// If we were primary but another node claims primary with higher
|
|
1487
|
-
// priority, step down
|
|
1488
|
-
if (isElectedPrimary_ && heartbeat.from != nodeId_) {
|
|
1489
|
-
if (shouldDeferTo(heartbeat.routerRSSI, heartbeat.from)) {
|
|
1490
|
-
Log(logger::GENERAL,
|
|
1491
|
-
"GatewayElectionManager: Stepping down, deferring to node %u\n",
|
|
1492
|
-
heartbeat.from);
|
|
1493
|
-
isElectedPrimary_ = false;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
/**
|
|
1501
|
-
* @brief Process an incoming gateway heartbeat (convenience overload)
|
|
1502
|
-
*
|
|
1503
|
-
* Calls processHeartbeat(heartbeat, millis()) for backward compatibility.
|
|
1504
|
-
* For testing, prefer processHeartbeat(heartbeat, currentTime) for
|
|
1505
|
-
* deterministic behavior.
|
|
1506
|
-
*
|
|
1507
|
-
* @param heartbeat The received GatewayHeartbeatPackage
|
|
1508
|
-
*/
|
|
1509
|
-
void processHeartbeat(const GatewayHeartbeatPackage& heartbeat) {
|
|
1510
|
-
processHeartbeat(heartbeat, millis());
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
/**
|
|
1514
|
-
* @brief Update the election state machine
|
|
1515
|
-
*
|
|
1516
|
-
* Should be called periodically (e.g., every second) to:
|
|
1517
|
-
* - Check for primary gateway timeout
|
|
1518
|
-
* - Progress election state machine
|
|
1519
|
-
* - Determine election winners
|
|
1520
|
-
*
|
|
1521
|
-
* @param currentTime Current time in milliseconds (e.g., millis())
|
|
1522
|
-
* @return true if this node should broadcast as primary (won election)
|
|
1523
|
-
*/
|
|
1524
|
-
bool update(uint32_t currentTime) {
|
|
1525
|
-
bool shouldBroadcastAsPrimary = false;
|
|
1526
|
-
|
|
1527
|
-
switch (state_) {
|
|
1528
|
-
case ElectionState::IDLE:
|
|
1529
|
-
// Check if primary gateway has timed out
|
|
1530
|
-
if (primaryGatewayId_ != 0 && lastPrimaryHeartbeatTime_ != 0) {
|
|
1531
|
-
uint32_t timeSinceLastHeartbeat =
|
|
1532
|
-
currentTime - lastPrimaryHeartbeatTime_;
|
|
1533
|
-
if (timeSinceLastHeartbeat > gatewayFailureTimeout_) {
|
|
1534
|
-
Log(logger::GENERAL,
|
|
1535
|
-
"GatewayElectionManager: Primary gateway %u timed out "
|
|
1536
|
-
"(no heartbeat for %ums)\n",
|
|
1537
|
-
primaryGatewayId_, timeSinceLastHeartbeat);
|
|
1538
|
-
// Primary failed, start election if we can participate
|
|
1539
|
-
if (canParticipateInElection()) {
|
|
1540
|
-
startElection(currentTime);
|
|
1541
|
-
} else {
|
|
1542
|
-
// Can't participate, just clear the primary
|
|
1543
|
-
primaryGatewayId_ = 0;
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
} else if (primaryGatewayId_ == 0 && canParticipateInElection()) {
|
|
1547
|
-
// No known primary and we can participate, start election
|
|
1548
|
-
startElection(currentTime);
|
|
1549
|
-
}
|
|
1550
|
-
break;
|
|
1551
|
-
|
|
1552
|
-
case ElectionState::ELECTION_RUNNING:
|
|
1553
|
-
// Check if election duration has elapsed
|
|
1554
|
-
if (currentTime - electionStartTime_ >= electionDuration_) {
|
|
1555
|
-
// Election complete, select winner
|
|
1556
|
-
uint32_t winnerId = selectWinner();
|
|
1557
|
-
if (winnerId != 0) {
|
|
1558
|
-
primaryGatewayId_ = winnerId;
|
|
1559
|
-
isElectedPrimary_ = (winnerId == nodeId_);
|
|
1560
|
-
|
|
1561
|
-
Log(logger::GENERAL,
|
|
1562
|
-
"GatewayElectionManager: Election complete, winner=%u "
|
|
1563
|
-
"(local=%s)\n",
|
|
1564
|
-
winnerId, isElectedPrimary_ ? "yes" : "no");
|
|
1565
|
-
|
|
1566
|
-
// Notify via callback
|
|
1567
|
-
if (electionResultCallback_) {
|
|
1568
|
-
electionResultCallback_(winnerId, isElectedPrimary_);
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
if (isElectedPrimary_) {
|
|
1572
|
-
shouldBroadcastAsPrimary = true;
|
|
1573
|
-
lastPrimaryHeartbeatTime_ = currentTime;
|
|
1574
|
-
}
|
|
1575
|
-
} else {
|
|
1576
|
-
Log(logger::GENERAL,
|
|
1577
|
-
"GatewayElectionManager: Election complete, no valid candidates\n");
|
|
1578
|
-
primaryGatewayId_ = 0;
|
|
1579
|
-
isElectedPrimary_ = false;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
transitionToCooldown(currentTime);
|
|
1583
|
-
}
|
|
1584
|
-
break;
|
|
1585
|
-
|
|
1586
|
-
case ElectionState::COOLDOWN:
|
|
1587
|
-
// Check if cooldown period has elapsed
|
|
1588
|
-
if (currentTime - cooldownStartTime_ >= electionCooldownPeriod_) {
|
|
1589
|
-
Log(logger::GENERAL,
|
|
1590
|
-
"GatewayElectionManager: Cooldown complete, returning to IDLE\n");
|
|
1591
|
-
state_ = ElectionState::IDLE;
|
|
1592
|
-
}
|
|
1593
|
-
break;
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
return shouldBroadcastAsPrimary;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
/**
|
|
1600
|
-
* @brief Check if this node is the elected primary gateway
|
|
1601
|
-
* @return true if this node won the last election
|
|
1602
|
-
*/
|
|
1603
|
-
bool isElectedPrimary() const { return isElectedPrimary_; }
|
|
1604
|
-
|
|
1605
|
-
/**
|
|
1606
|
-
* @brief Get the current election state
|
|
1607
|
-
* @return Current ElectionState
|
|
1608
|
-
*/
|
|
1609
|
-
ElectionState getState() const { return state_; }
|
|
1610
|
-
|
|
1611
|
-
/**
|
|
1612
|
-
* @brief Get the current primary gateway node ID
|
|
1613
|
-
* @return Primary gateway node ID, or 0 if none
|
|
1614
|
-
*/
|
|
1615
|
-
uint32_t getPrimaryGatewayId() const { return primaryGatewayId_; }
|
|
1616
|
-
|
|
1617
|
-
/**
|
|
1618
|
-
* @brief Register a callback for election results
|
|
1619
|
-
*
|
|
1620
|
-
* The callback is invoked when an election completes with the winner's
|
|
1621
|
-
* node ID and whether this node is the winner.
|
|
1622
|
-
*
|
|
1623
|
-
* @param callback Function to call on election completion
|
|
1624
|
-
*/
|
|
1625
|
-
void onElectionResult(ElectionResultCallback_t callback) {
|
|
1626
|
-
electionResultCallback_ = callback;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
/**
|
|
1630
|
-
* @brief Force start an election
|
|
1631
|
-
*
|
|
1632
|
-
* Manually triggers an election, useful for testing or when a node
|
|
1633
|
-
* needs to force re-election. Only starts if not already in election
|
|
1634
|
-
* or cooldown.
|
|
1635
|
-
*
|
|
1636
|
-
* @param currentTime Current time in milliseconds (e.g., millis())
|
|
1637
|
-
*/
|
|
1638
|
-
void startElection(uint32_t currentTime) {
|
|
1639
|
-
if (state_ != ElectionState::IDLE) {
|
|
1640
|
-
Log(logger::GENERAL,
|
|
1641
|
-
"GatewayElectionManager: Cannot start election, state=%d\n",
|
|
1642
|
-
static_cast<int>(state_));
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
Log(logger::GENERAL,
|
|
1647
|
-
"GatewayElectionManager: Starting election (nodeId=%u, "
|
|
1648
|
-
"hasInternet=%s, rssi=%d)\n",
|
|
1649
|
-
nodeId_, localHasInternet_ ? "yes" : "no", localRssi_);
|
|
1650
|
-
|
|
1651
|
-
state_ = ElectionState::ELECTION_RUNNING;
|
|
1652
|
-
electionStartTime_ = currentTime;
|
|
1653
|
-
isElectedPrimary_ = false;
|
|
1654
|
-
|
|
1655
|
-
// Clear old candidates and add self if eligible
|
|
1656
|
-
candidates_.clear();
|
|
1657
|
-
if (localHasInternet_ && nodeId_ != 0) {
|
|
1658
|
-
Candidate localCandidate;
|
|
1659
|
-
localCandidate.nodeId = nodeId_;
|
|
1660
|
-
localCandidate.rssi = localRssi_;
|
|
1661
|
-
localCandidate.hasInternet = localHasInternet_;
|
|
1662
|
-
localCandidate.lastSeen = electionStartTime_;
|
|
1663
|
-
candidates_[nodeId_] = localCandidate;
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
/**
|
|
1668
|
-
* @brief Force start an election (convenience overload)
|
|
1669
|
-
*
|
|
1670
|
-
* Calls startElection(millis()) for backward compatibility.
|
|
1671
|
-
* For testing, prefer startElection(currentTime) for deterministic behavior.
|
|
1672
|
-
*/
|
|
1673
|
-
void startElection() { startElection(millis()); }
|
|
1674
|
-
|
|
1675
|
-
/**
|
|
1676
|
-
* @brief Reset all election state
|
|
1677
|
-
*
|
|
1678
|
-
* Clears all state including primary gateway, candidates, and returns
|
|
1679
|
-
* to IDLE state. Does not clear configuration or node ID.
|
|
1680
|
-
*/
|
|
1681
|
-
void reset() {
|
|
1682
|
-
state_ = ElectionState::IDLE;
|
|
1683
|
-
primaryGatewayId_ = 0;
|
|
1684
|
-
isElectedPrimary_ = false;
|
|
1685
|
-
lastPrimaryHeartbeatTime_ = 0;
|
|
1686
|
-
electionStartTime_ = 0;
|
|
1687
|
-
cooldownStartTime_ = 0;
|
|
1688
|
-
candidates_.clear();
|
|
1689
|
-
Log(logger::GENERAL, "GatewayElectionManager: State reset\n");
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
/**
|
|
1693
|
-
* @brief Set the election duration
|
|
1694
|
-
* @param durationMs Duration in milliseconds (default: 5000)
|
|
1695
|
-
*/
|
|
1696
|
-
void setElectionDuration(uint32_t durationMs) {
|
|
1697
|
-
electionDuration_ = durationMs;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
/**
|
|
1701
|
-
* @brief Set the cooldown period
|
|
1702
|
-
* @param periodMs Cooldown period in milliseconds (default: 60000)
|
|
1703
|
-
*/
|
|
1704
|
-
void setCooldownPeriod(uint32_t periodMs) {
|
|
1705
|
-
electionCooldownPeriod_ = periodMs;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
/**
|
|
1709
|
-
* @brief Get the number of known candidates
|
|
1710
|
-
* @return Number of candidates in the current election
|
|
1711
|
-
*/
|
|
1712
|
-
size_t getCandidateCount() const { return candidates_.size(); }
|
|
1713
|
-
|
|
1714
|
-
private:
|
|
1715
|
-
/**
|
|
1716
|
-
* @brief Internal structure for tracking election candidates
|
|
1717
|
-
*/
|
|
1718
|
-
struct Candidate {
|
|
1719
|
-
uint32_t nodeId = 0;
|
|
1720
|
-
int8_t rssi = 0;
|
|
1721
|
-
bool hasInternet = false;
|
|
1722
|
-
uint32_t lastSeen = 0;
|
|
1723
|
-
};
|
|
1724
|
-
|
|
1725
|
-
/**
|
|
1726
|
-
* @brief Check if this node can participate in elections
|
|
1727
|
-
* @return true if node has Internet and valid node ID
|
|
1728
|
-
*/
|
|
1729
|
-
bool canParticipateInElection() const {
|
|
1730
|
-
return localHasInternet_ && nodeId_ != 0;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
/**
|
|
1734
|
-
* @brief Check if we should defer to another node
|
|
1735
|
-
*
|
|
1736
|
-
* Used for split-brain prevention. Defers to nodes with:
|
|
1737
|
-
* 1. Higher RSSI, or
|
|
1738
|
-
* 2. Same RSSI and higher node ID
|
|
1739
|
-
*
|
|
1740
|
-
* @param otherRssi The other node's RSSI
|
|
1741
|
-
* @param otherNodeId The other node's ID
|
|
1742
|
-
* @return true if we should defer to the other node
|
|
1743
|
-
*/
|
|
1744
|
-
bool shouldDeferTo(int8_t otherRssi, uint32_t otherNodeId) const {
|
|
1745
|
-
// Higher RSSI wins
|
|
1746
|
-
if (otherRssi > localRssi_) return true;
|
|
1747
|
-
if (otherRssi < localRssi_) return false;
|
|
1748
|
-
// Same RSSI, higher node ID wins
|
|
1749
|
-
return otherNodeId > nodeId_;
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
/**
|
|
1753
|
-
* @brief Select the winner from current candidates
|
|
1754
|
-
*
|
|
1755
|
-
* Implements deterministic winner selection:
|
|
1756
|
-
* 1. Highest RSSI wins
|
|
1757
|
-
* 2. If RSSI tie, highest node ID wins
|
|
1758
|
-
*
|
|
1759
|
-
* @return Winner's node ID, or 0 if no valid candidates
|
|
1760
|
-
*/
|
|
1761
|
-
uint32_t selectWinner() const {
|
|
1762
|
-
uint32_t winnerId = 0;
|
|
1763
|
-
int8_t winnerRssi = -128; // Minimum possible RSSI
|
|
1764
|
-
|
|
1765
|
-
for (const auto& pair : candidates_) {
|
|
1766
|
-
const Candidate& candidate = pair.second;
|
|
1767
|
-
|
|
1768
|
-
// Only consider candidates with Internet
|
|
1769
|
-
if (!candidate.hasInternet) continue;
|
|
1770
|
-
|
|
1771
|
-
// Check if this candidate beats the current winner
|
|
1772
|
-
bool isBetter = false;
|
|
1773
|
-
if (candidate.rssi > winnerRssi) {
|
|
1774
|
-
isBetter = true;
|
|
1775
|
-
} else if (candidate.rssi == winnerRssi && candidate.nodeId > winnerId) {
|
|
1776
|
-
isBetter = true;
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
if (isBetter) {
|
|
1780
|
-
winnerId = candidate.nodeId;
|
|
1781
|
-
winnerRssi = candidate.rssi;
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
return winnerId;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
/**
|
|
1789
|
-
* @brief Transition to cooldown state
|
|
1790
|
-
* @param currentTime Current time in milliseconds
|
|
1791
|
-
*/
|
|
1792
|
-
void transitionToCooldown(uint32_t currentTime) {
|
|
1793
|
-
state_ = ElectionState::COOLDOWN;
|
|
1794
|
-
cooldownStartTime_ = currentTime;
|
|
1795
|
-
candidates_.clear();
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// State
|
|
1799
|
-
ElectionState state_;
|
|
1800
|
-
uint32_t nodeId_;
|
|
1801
|
-
bool localHasInternet_;
|
|
1802
|
-
int8_t localRssi_;
|
|
1803
|
-
uint32_t primaryGatewayId_;
|
|
1804
|
-
bool isElectedPrimary_;
|
|
1805
|
-
uint32_t lastPrimaryHeartbeatTime_;
|
|
1806
|
-
uint32_t electionStartTime_;
|
|
1807
|
-
uint32_t cooldownStartTime_;
|
|
1808
|
-
|
|
1809
|
-
// Configuration
|
|
1810
|
-
uint32_t gatewayFailureTimeout_;
|
|
1811
|
-
uint32_t electionDuration_;
|
|
1812
|
-
uint32_t electionCooldownPeriod_;
|
|
1813
|
-
|
|
1814
|
-
// Candidates map: nodeId -> Candidate
|
|
1815
|
-
std::map<uint32_t, Candidate> candidates_;
|
|
1816
|
-
|
|
1817
|
-
// Callback
|
|
1818
|
-
ElectionResultCallback_t electionResultCallback_;
|
|
1819
|
-
};
|
|
1820
|
-
|
|
1821
|
-
/**
|
|
1822
|
-
* @brief Metrics for monitoring GatewayMessageHandler duplicate detection
|
|
1823
|
-
*
|
|
1824
|
-
* This structure tracks statistics about message processing and duplicate
|
|
1825
|
-
* detection in the gateway message handler. Useful for monitoring system
|
|
1826
|
-
* health and debugging duplicate-related issues.
|
|
1827
|
-
*
|
|
1828
|
-
* Example usage:
|
|
1829
|
-
* @code
|
|
1830
|
-
* GatewayMessageHandler handler;
|
|
1831
|
-
* // ... process messages ...
|
|
1832
|
-
* GatewayMetrics metrics = handler.getMetrics();
|
|
1833
|
-
* Serial.printf("Duplicates: %u, Processed: %u\n",
|
|
1834
|
-
* metrics.duplicatesDetected, metrics.messagesProcessed);
|
|
1835
|
-
* @endcode
|
|
1836
|
-
*/
|
|
1837
|
-
struct GatewayMetrics {
|
|
1838
|
-
/**
|
|
1839
|
-
* @brief Count of duplicate messages detected and dropped
|
|
1840
|
-
*
|
|
1841
|
-
* Incremented each time an incoming message is identified as a duplicate
|
|
1842
|
-
* and skipped from processing.
|
|
1843
|
-
*/
|
|
1844
|
-
uint32_t duplicatesDetected = 0;
|
|
1845
|
-
|
|
1846
|
-
/**
|
|
1847
|
-
* @brief Total count of messages successfully processed
|
|
1848
|
-
*
|
|
1849
|
-
* Incremented for each unique message that passes duplicate checking
|
|
1850
|
-
* and is processed.
|
|
1851
|
-
*/
|
|
1852
|
-
uint32_t messagesProcessed = 0;
|
|
1853
|
-
|
|
1854
|
-
/**
|
|
1855
|
-
* @brief Count of acknowledgments sent
|
|
1856
|
-
*
|
|
1857
|
-
* Incremented each time an acknowledgment is sent for a message.
|
|
1858
|
-
*/
|
|
1859
|
-
uint32_t acknowledgmentsSent = 0;
|
|
1860
|
-
|
|
1861
|
-
/**
|
|
1862
|
-
* @brief Count of duplicate acknowledgments skipped
|
|
1863
|
-
*
|
|
1864
|
-
* Incremented when an acknowledgment would have been sent, but was
|
|
1865
|
-
* skipped because one was already sent for that message.
|
|
1866
|
-
*/
|
|
1867
|
-
uint32_t duplicateAcksSkipped = 0;
|
|
1868
|
-
|
|
1869
|
-
/**
|
|
1870
|
-
* @brief Reset all metrics to zero
|
|
1871
|
-
*/
|
|
1872
|
-
void reset() {
|
|
1873
|
-
duplicatesDetected = 0;
|
|
1874
|
-
messagesProcessed = 0;
|
|
1875
|
-
acknowledgmentsSent = 0;
|
|
1876
|
-
duplicateAcksSkipped = 0;
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
/**
|
|
1880
|
-
* @brief Get the duplicate detection rate as a percentage
|
|
1881
|
-
* @return Percentage of messages that were duplicates (0-100), or 0 if no messages
|
|
1882
|
-
*/
|
|
1883
|
-
uint8_t getDuplicateRate() const {
|
|
1884
|
-
uint64_t total = static_cast<uint64_t>(messagesProcessed) +
|
|
1885
|
-
static_cast<uint64_t>(duplicatesDetected);
|
|
1886
|
-
if (total == 0) return 0;
|
|
1887
|
-
// Use uint64_t arithmetic to prevent overflow
|
|
1888
|
-
return static_cast<uint8_t>((static_cast<uint64_t>(duplicatesDetected) * 100) / total);
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
/**
|
|
1892
|
-
* @brief Get the duplicate ack rate as a percentage
|
|
1893
|
-
* @return Percentage of acks that were duplicates (0-100), or 0 if no acks
|
|
1894
|
-
*/
|
|
1895
|
-
uint8_t getDuplicateAckRate() const {
|
|
1896
|
-
uint64_t total = static_cast<uint64_t>(acknowledgmentsSent) +
|
|
1897
|
-
static_cast<uint64_t>(duplicateAcksSkipped);
|
|
1898
|
-
if (total == 0) return 0;
|
|
1899
|
-
// Use uint64_t arithmetic to prevent overflow
|
|
1900
|
-
return static_cast<uint8_t>((static_cast<uint64_t>(duplicateAcksSkipped) * 100) / total);
|
|
1901
|
-
}
|
|
1902
1054
|
};
|
|
1903
1055
|
|
|
1904
1056
|
} // namespace gateway
|
|
1905
|
-
|
|
1906
|
-
/**
|
|
1907
|
-
* @brief Gateway Message Handler with duplicate prevention
|
|
1908
|
-
*
|
|
1909
|
-
* This class integrates MessageTracker to prevent duplicate message processing
|
|
1910
|
-
* in gateway operations. It provides:
|
|
1911
|
-
* - Duplicate message detection and dropping
|
|
1912
|
-
* - Single acknowledgment per message enforcement
|
|
1913
|
-
* - Metrics for monitoring duplicate detection
|
|
1914
|
-
* - Configurable tracker limits via SharedGatewayConfig
|
|
1915
|
-
* - Logging for duplicate detection events
|
|
1916
|
-
*
|
|
1917
|
-
* MEMORY FOOTPRINT
|
|
1918
|
-
* ================
|
|
1919
|
-
* The GatewayMessageHandler uses a MessageTracker internally, which stores
|
|
1920
|
-
* message entries in a std::map. Memory usage depends on configuration:
|
|
1921
|
-
* - Default (500 messages): ~20KB estimated
|
|
1922
|
-
* - ESP8266 recommended (100 messages): ~4KB estimated
|
|
1923
|
-
* - Each tracked message: ~40 bytes (MessageKey + TrackedMessage + map overhead)
|
|
1924
|
-
*
|
|
1925
|
-
* Example usage:
|
|
1926
|
-
* @code
|
|
1927
|
-
* GatewayMessageHandler handler;
|
|
1928
|
-
*
|
|
1929
|
-
* // Configure from shared gateway config
|
|
1930
|
-
* SharedGatewayConfig config;
|
|
1931
|
-
* config.maxTrackedMessages = 500;
|
|
1932
|
-
* config.duplicateTrackingTimeout = 60000;
|
|
1933
|
-
* handler.configure(config);
|
|
1934
|
-
*
|
|
1935
|
-
* // Handle incoming message
|
|
1936
|
-
* GatewayDataPackage pkg;
|
|
1937
|
-
* // ... populate pkg ...
|
|
1938
|
-
*
|
|
1939
|
-
* if (handler.handleIncomingMessage(pkg)) {
|
|
1940
|
-
* // Process the message - it's not a duplicate
|
|
1941
|
-
* processGatewayRequest(pkg);
|
|
1942
|
-
*
|
|
1943
|
-
* // Check if we should send an ack
|
|
1944
|
-
* if (pkg.requiresAck && handler.shouldSendAcknowledgment(pkg.messageId, pkg.originNode)) {
|
|
1945
|
-
* sendAck(pkg);
|
|
1946
|
-
* handler.markAcknowledgmentSent(pkg.messageId, pkg.originNode);
|
|
1947
|
-
* }
|
|
1948
|
-
* }
|
|
1949
|
-
*
|
|
1950
|
-
* // Periodically cleanup old entries
|
|
1951
|
-
* handler.cleanup();
|
|
1952
|
-
*
|
|
1953
|
-
* // Monitor metrics
|
|
1954
|
-
* auto metrics = handler.getMetrics();
|
|
1955
|
-
* @endcode
|
|
1956
|
-
*/
|
|
1957
|
-
class GatewayMessageHandler {
|
|
1958
|
-
public:
|
|
1959
|
-
/**
|
|
1960
|
-
* @brief Default constructor
|
|
1961
|
-
*
|
|
1962
|
-
* Creates a GatewayMessageHandler with default configuration:
|
|
1963
|
-
* - maxTrackedMessages: 500
|
|
1964
|
-
* - duplicateTrackingTimeout: 60000ms (60 seconds)
|
|
1965
|
-
*/
|
|
1966
|
-
GatewayMessageHandler() : tracker_(500, 60000) {}
|
|
1967
|
-
|
|
1968
|
-
/**
|
|
1969
|
-
* @brief Configure the handler using SharedGatewayConfig parameters
|
|
1970
|
-
*
|
|
1971
|
-
* Updates the internal MessageTracker with configuration values from
|
|
1972
|
-
* the provided SharedGatewayConfig. This should be called before
|
|
1973
|
-
* processing messages to ensure proper configuration.
|
|
1974
|
-
*
|
|
1975
|
-
* @param config SharedGatewayConfig with tracker parameters
|
|
1976
|
-
*/
|
|
1977
|
-
void configure(const gateway::SharedGatewayConfig& config) {
|
|
1978
|
-
tracker_.setMaxMessages(config.maxTrackedMessages);
|
|
1979
|
-
tracker_.setTimeoutMs(config.duplicateTrackingTimeout);
|
|
1980
|
-
Log(logger::GENERAL,
|
|
1981
|
-
"GatewayMessageHandler: Configured with maxMessages=%u, timeout=%ums\n",
|
|
1982
|
-
config.maxTrackedMessages, config.duplicateTrackingTimeout);
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
/**
|
|
1986
|
-
* @brief Handle an incoming GatewayDataPackage and check for duplicates
|
|
1987
|
-
*
|
|
1988
|
-
* Checks if the message has already been processed using the MessageTracker.
|
|
1989
|
-
* If the message is a duplicate, it is dropped silently and metrics are updated.
|
|
1990
|
-
* If the message is new, it is marked as processed and should be handled.
|
|
1991
|
-
*
|
|
1992
|
-
* @param pkg The incoming GatewayDataPackage to check
|
|
1993
|
-
* @return true if the message should be processed (not a duplicate)
|
|
1994
|
-
* @return false if the message is a duplicate and should be skipped
|
|
1995
|
-
*/
|
|
1996
|
-
bool handleIncomingMessage(const gateway::GatewayDataPackage& pkg) {
|
|
1997
|
-
// Check if this message was already processed
|
|
1998
|
-
if (tracker_.isProcessed(pkg.messageId, pkg.originNode)) {
|
|
1999
|
-
metrics_.duplicatesDetected++;
|
|
2000
|
-
Log(logger::GENERAL,
|
|
2001
|
-
"GatewayMessageHandler: Duplicate message detected (msgId=%u, origin=%u)\n",
|
|
2002
|
-
pkg.messageId, pkg.originNode);
|
|
2003
|
-
return false;
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
// Mark as processed and allow handling
|
|
2007
|
-
tracker_.markProcessed(pkg.messageId, pkg.originNode);
|
|
2008
|
-
metrics_.messagesProcessed++;
|
|
2009
|
-
Log(logger::GENERAL,
|
|
2010
|
-
"GatewayMessageHandler: Processing new message (msgId=%u, origin=%u)\n",
|
|
2011
|
-
pkg.messageId, pkg.originNode);
|
|
2012
|
-
return true;
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
/**
|
|
2016
|
-
* @brief Check if an acknowledgment should be sent for a message
|
|
2017
|
-
*
|
|
2018
|
-
* Checks if an acknowledgment has already been sent for the specified
|
|
2019
|
-
* message. This prevents sending duplicate acknowledgments during
|
|
2020
|
-
* network partitions or message retries.
|
|
2021
|
-
*
|
|
2022
|
-
* @param messageId The unique message identifier
|
|
2023
|
-
* @param originNode The node that originated the message
|
|
2024
|
-
* @return true if acknowledgment should be sent (not already acked)
|
|
2025
|
-
* @return false if acknowledgment was already sent (skip sending)
|
|
2026
|
-
*/
|
|
2027
|
-
bool shouldSendAcknowledgment(uint32_t messageId, uint32_t originNode) {
|
|
2028
|
-
// Check if we already sent an ack for this message
|
|
2029
|
-
if (tracker_.isAcknowledged(messageId, originNode)) {
|
|
2030
|
-
metrics_.duplicateAcksSkipped++;
|
|
2031
|
-
Log(logger::GENERAL,
|
|
2032
|
-
"GatewayMessageHandler: Duplicate ack skipped (msgId=%u, origin=%u)\n",
|
|
2033
|
-
messageId, originNode);
|
|
2034
|
-
return false;
|
|
2035
|
-
}
|
|
2036
|
-
return true;
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
/**
|
|
2040
|
-
* @brief Mark that an acknowledgment has been sent for a message
|
|
2041
|
-
*
|
|
2042
|
-
* Records that an acknowledgment was sent for the specified message.
|
|
2043
|
-
* Subsequent calls to shouldSendAcknowledgment() for this message
|
|
2044
|
-
* will return false.
|
|
2045
|
-
*
|
|
2046
|
-
* @param messageId The unique message identifier
|
|
2047
|
-
* @param originNode The node that originated the message
|
|
2048
|
-
*/
|
|
2049
|
-
void markAcknowledgmentSent(uint32_t messageId, uint32_t originNode) {
|
|
2050
|
-
// First ensure the message is tracked (in case ack is sent before processing)
|
|
2051
|
-
if (!tracker_.isProcessed(messageId, originNode)) {
|
|
2052
|
-
tracker_.markProcessed(messageId, originNode);
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
tracker_.markAcknowledged(messageId, originNode);
|
|
2056
|
-
metrics_.acknowledgmentsSent++;
|
|
2057
|
-
Log(logger::GENERAL,
|
|
2058
|
-
"GatewayMessageHandler: Acknowledgment sent (msgId=%u, origin=%u)\n",
|
|
2059
|
-
messageId, originNode);
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
/**
|
|
2063
|
-
* @brief Cleanup old entries from the tracker
|
|
2064
|
-
*
|
|
2065
|
-
* Removes expired entries from the internal MessageTracker based on
|
|
2066
|
-
* the configured timeout. Should be called periodically to free memory.
|
|
2067
|
-
*
|
|
2068
|
-
* @return Number of entries removed
|
|
2069
|
-
*/
|
|
2070
|
-
uint32_t cleanup() {
|
|
2071
|
-
return tracker_.cleanup();
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
/**
|
|
2075
|
-
* @brief Get current metrics for monitoring
|
|
2076
|
-
*
|
|
2077
|
-
* Returns a copy of the current metrics structure containing
|
|
2078
|
-
* statistics about duplicate detection and message processing.
|
|
2079
|
-
*
|
|
2080
|
-
* @return GatewayMetrics structure with current statistics
|
|
2081
|
-
*/
|
|
2082
|
-
gateway::GatewayMetrics getMetrics() const {
|
|
2083
|
-
return metrics_;
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
/**
|
|
2087
|
-
* @brief Reset all metrics to zero
|
|
2088
|
-
*
|
|
2089
|
-
* Clears all metrics counters. Does not affect tracked messages.
|
|
2090
|
-
*/
|
|
2091
|
-
void resetMetrics() {
|
|
2092
|
-
metrics_.reset();
|
|
2093
|
-
Log(logger::GENERAL, "GatewayMessageHandler: Metrics reset\n");
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
/**
|
|
2097
|
-
* @brief Get the number of currently tracked messages
|
|
2098
|
-
* @return Number of entries in the tracker
|
|
2099
|
-
*/
|
|
2100
|
-
size_t getTrackedMessageCount() const {
|
|
2101
|
-
return tracker_.size();
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
/**
|
|
2105
|
-
* @brief Clear all tracked messages
|
|
2106
|
-
*
|
|
2107
|
-
* Removes all entries from the tracker. Does not affect metrics.
|
|
2108
|
-
*/
|
|
2109
|
-
void clearTrackedMessages() {
|
|
2110
|
-
tracker_.clear();
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
private:
|
|
2114
|
-
MessageTracker tracker_;
|
|
2115
|
-
gateway::GatewayMetrics metrics_;
|
|
2116
|
-
};
|
|
2117
|
-
|
|
2118
1057
|
} // namespace painlessmesh
|
|
2119
1058
|
|
|
2120
1059
|
#endif // _PAINLESS_MESH_GATEWAY_HPP_
|