@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +82 -63
  3. package/examples/alteriom/README.md +4 -4
  4. package/examples/alteriom/alteriom_custom_package_template.hpp +320 -0
  5. package/examples/alteriom/alteriom_sensor_package.hpp +1 -1
  6. package/examples/alteriom/mppt_example/alteriom_mppt_example.ino +208 -0
  7. package/examples/bridge_failover/bridge_failover.ino +17 -0
  8. package/examples/sendToInternet/CMakeLists.txt +54 -0
  9. package/examples/sendToInternet/PC_NODE_README.md +517 -0
  10. package/examples/sendToInternet/README.md +39 -1
  11. package/examples/sendToInternet/build.sh +153 -0
  12. package/examples/sendToInternet/mock_server_test.ino +361 -0
  13. package/examples/sendToInternet/pc_mesh_node.cpp +361 -0
  14. package/library.json +4 -1
  15. package/library.properties +1 -1
  16. package/package.json +3 -3
  17. package/src/AlteriomPainlessMesh.h +5 -13
  18. package/src/arduino/wifi.hpp +306 -100
  19. package/src/connection.cpp +10 -0
  20. package/src/painlessMesh.h +1 -14
  21. package/src/painlessmesh/connection.hpp +11 -16
  22. package/src/painlessmesh/gateway.hpp +0 -1061
  23. package/src/painlessmesh/mesh.hpp +58 -86
  24. package/src/painlessmesh/message_queue.hpp +1 -2
  25. package/src/painlessmesh/metrics.hpp +2 -262
  26. package/src/painlessmesh/validation.hpp +0 -143
  27. package/docs/README.md +0 -132
  28. package/docs/alteriom/overview.md +0 -531
  29. package/docs/api/core-api.md +0 -607
  30. package/docs/api/shared-gateway.md +0 -1207
  31. package/docs/architecture/mesh-architecture.md +0 -399
  32. package/docs/architecture/plugin-system.md +0 -517
  33. package/docs/getting-started/arduino-manual-install.md +0 -313
  34. package/docs/getting-started/first-mesh.md +0 -410
  35. package/docs/getting-started/installation.md +0 -275
  36. package/docs/getting-started/quickstart.md +0 -158
  37. package/docs/troubleshooting/common-issues.md +0 -679
  38. package/docs/troubleshooting/debugging.md +0 -455
  39. package/docs/troubleshooting/external-device-connection.md +0 -283
  40. package/docs/troubleshooting/faq.md +0 -574
  41. 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_