@alteriom/painlessmesh 1.8.14 → 1.9.0

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 (208) hide show
  1. package/BRIDGE_TO_INTERNET.md +229 -0
  2. package/CHANGELOG.md +89 -1
  3. package/CONTRIBUTING.md +79 -0
  4. package/README.md +70 -143
  5. package/docs/README.md +1 -0
  6. package/docs/api/shared-gateway.md +1207 -0
  7. package/docs/troubleshooting/common-issues.md +28 -0
  8. package/docs/troubleshooting/faq.md +113 -12
  9. package/examples/basic/test/simulator/CMakeLists.txt +40 -0
  10. package/examples/basic/test/simulator/README.md +149 -0
  11. package/examples/basic/test/simulator/firmware/basic_firmware.hpp +117 -0
  12. package/examples/basic/test/simulator/scenarios/basic_mesh_test.yaml +81 -0
  13. package/examples/bridge/bridge.ino +17 -4
  14. package/examples/bridge_failover/README.md +81 -0
  15. package/examples/bridge_failover/bridge_failover.ino +51 -6
  16. package/examples/sharedGateway/README.md +235 -0
  17. package/examples/{meshCommandNode → sharedGateway}/platformio.ini +3 -2
  18. package/examples/sharedGateway/sharedGateway.ino +303 -0
  19. package/library.json +3 -22
  20. package/library.properties +1 -1
  21. package/package.json +3 -3
  22. package/src/arduino/wifi.hpp +380 -13
  23. package/src/painlessmesh/gateway.hpp +2120 -0
  24. package/src/painlessmesh/mesh.hpp +1034 -6
  25. package/src/painlessmesh/message_tracker.hpp +311 -0
  26. package/src/painlessmesh/protocol.hpp +6 -0
  27. package/DOCUMENTATION_INDEX.md +0 -146
  28. package/docs/API_DESIGN_GUIDELINES.md +0 -414
  29. package/docs/ARDUINO_LIBRARY_MANAGER_SUBMISSION.md +0 -331
  30. package/docs/BOOLEAN_NAMING_CONVENTION.md +0 -235
  31. package/docs/BRIDGE_FAILOVER.md +0 -512
  32. package/docs/BRIDGE_HEALTH_MONITORING.md +0 -293
  33. package/docs/CHANNEL_SYNCHRONIZATION.md +0 -209
  34. package/docs/CREATE_MISSING_RELEASES.md +0 -321
  35. package/docs/DOCUMENTATION_MIGRATION_PLAN.md +0 -176
  36. package/docs/FAQ_VERSION_NUMBERS.md +0 -152
  37. package/docs/IMPLEMENTATION_PLAN_MESH_TOPOLOGY.md +0 -1062
  38. package/docs/MESH_TOPOLOGY_GUIDE.md +0 -992
  39. package/docs/MESH_TOPOLOGY_PROGRESS.md +0 -422
  40. package/docs/MQTT_BRIDGE_COMMANDS.md +0 -894
  41. package/docs/MQTT_BRIDGE_IMPLEMENTATION_SUMMARY.md +0 -324
  42. package/docs/MQTT_COMMAND_SCHEMA_PROPOSAL.md +0 -576
  43. package/docs/MQTT_SCHEMA_COMPLIANCE.md +0 -340
  44. package/docs/MQTT_SCHEMA_PROPOSALS.md +0 -446
  45. package/docs/MQTT_SCHEMA_REVIEW.md +0 -690
  46. package/docs/OTA_COMMANDS_REFERENCE.md +0 -554
  47. package/docs/PHASE1_GUIDE.md +0 -349
  48. package/docs/PHASE2_GUIDE.md +0 -543
  49. package/docs/QUICK_REFERENCE_VERSIONING.md +0 -127
  50. package/docs/RELEASE_AGENT_SUMMARY.md +0 -386
  51. package/docs/SCHEMA_VALIDATION_CHECKLIST.md +0 -222
  52. package/docs/VERSION_MANAGEMENT.md +0 -213
  53. package/docs/archive/DOCUSAURUS_DEPLOYMENT.md +0 -166
  54. package/docs/archive/FEATURE_PROPOSALS.md +0 -337
  55. package/docs/archive/LIBRARY_JSON_FIX.md +0 -98
  56. package/docs/archive/LIBRARY_STRUCTURE_FIX.md +0 -215
  57. package/docs/archive/PHASE1_IMPLEMENTATION.md +0 -325
  58. package/docs/archive/PHASE2_IMPLEMENTATION.md +0 -567
  59. package/docs/archive/RELEASE_SUMMARY.md +0 -173
  60. package/docs/archive/SCONS_BUILD_FIX.md +0 -313
  61. package/docs/archive/TRIGGER_RELEASE.md +0 -280
  62. package/docs/archive/VECTOR_INCLUDE_FIX.md +0 -129
  63. package/docs/archive/ota-and-status-enhancements.md +0 -911
  64. package/docs/archive/ota-status-architecture-diagrams.md +0 -658
  65. package/docs/archive/ota-status-quick-reference.md +0 -284
  66. package/docs/design/.gitkeep +0 -1
  67. package/docs/design/STATION_CREDENTIALS_DESIGN.md +0 -182
  68. package/docs/development/ARDUINO_COMPLIANCE_SUMMARY.md +0 -71
  69. package/docs/development/CODE_REFACTORING_RECOMMENDATIONS.md +0 -1011
  70. package/docs/development/DOCKER_TESTING.md +0 -196
  71. package/docs/development/PLATFORMIO_USAGE.md +0 -180
  72. package/docs/development/TESTING_SUMMARY.md +0 -126
  73. package/docs/development/contributing.md +0 -301
  74. package/docs/development/documentation.md +0 -583
  75. package/docs/features/DIAGNOSTICS_API.md +0 -534
  76. package/docs/implementation/BRIDGE_ARCHITECTURE_IMPLEMENTATION.md +0 -340
  77. package/docs/implementation/BRIDGE_HEALTH_MONITORING_IMPLEMENTATION.md +0 -213
  78. package/docs/implementation/BRIDGE_STATUS_FEATURE.md +0 -635
  79. package/docs/implementation/DIAGNOSTICS_API_IMPLEMENTATION.md +0 -232
  80. package/docs/implementation/IMPLEMENTATION_COMPLETE.md +0 -228
  81. package/docs/implementation/IMPLEMENTATION_NTP_TIME_SYNC.md +0 -325
  82. package/docs/implementation/IMPLEMENTATION_SUMMARY.md +0 -316
  83. package/docs/implementation/MESSAGE_QUEUE_IMPLEMENTATION.md +0 -405
  84. package/docs/implementation/MULTI_BRIDGE_IMPLEMENTATION.md +0 -520
  85. package/docs/implementation/NTP_TIME_SYNC_FEATURE.md +0 -392
  86. package/docs/improvements/FUTURE_PROPOSALS.md +0 -1016
  87. package/docs/improvements/IMPLEMENTATION_HISTORY.md +0 -1091
  88. package/docs/improvements/OTA_STATUS_ENHANCEMENTS.md +0 -709
  89. package/docs/improvements/README.md +0 -212
  90. package/docs/internal/CUSTOM_AGENT_ANALYSIS.md +0 -391
  91. package/docs/internal/ISSUE_65_VERIFICATION.md +0 -947
  92. package/docs/internal/ISSUE_66_CLOSURE.md +0 -249
  93. package/docs/internal/ISSUE_66_STATUS.md +0 -316
  94. package/docs/internal/PR_SUMMARY.md +0 -315
  95. package/docs/internal/REVIEW_SUMMARY.md +0 -332
  96. package/docs/multi-bridge-setup.md +0 -1025
  97. package/docs/platformio-publishing.md +0 -255
  98. package/docs/platformio-setup-summary.md +0 -121
  99. package/docs/releases/ANNOUNCEMENT_v1.8.6.md +0 -63
  100. package/docs/releases/ANNOUNCEMENT_v1.8.7.md +0 -113
  101. package/docs/releases/BRIDGE_STATUS_SELF_REGISTRATION_FIX.md +0 -221
  102. package/docs/releases/FEATURE_HISTORY.md +0 -543
  103. package/docs/releases/GITHUB_RELEASE_v1.8.6.md +0 -71
  104. package/docs/releases/GITHUB_RELEASE_v1.8.7.md +0 -90
  105. package/docs/releases/PATCH_v1.7.2.md +0 -262
  106. package/docs/releases/PATCH_v1.7.3.md +0 -262
  107. package/docs/releases/PATCH_v1.7.4.md +0 -219
  108. package/docs/releases/PHASE1_SUMMARY.md +0 -246
  109. package/docs/releases/PHASE2_SUMMARY.md +0 -499
  110. package/docs/releases/PUBLISH_v1.8.0_INSTRUCTIONS.md +0 -163
  111. package/docs/releases/QUICK_START_RELEASES.md +0 -113
  112. package/docs/releases/RELEASE_CHECKLIST_1.8.10.md +0 -331
  113. package/docs/releases/RELEASE_CHECKLIST_1.8.9.md +0 -207
  114. package/docs/releases/RELEASE_CHECKLIST_v1.7.4.md +0 -253
  115. package/docs/releases/RELEASE_CHECKLIST_v1.7.5.md +0 -315
  116. package/docs/releases/RELEASE_CHECKLIST_v1.7.6.md +0 -389
  117. package/docs/releases/RELEASE_CHECKLIST_v1.8.0.md +0 -331
  118. package/docs/releases/RELEASE_CHECKLIST_v1.8.2.md +0 -309
  119. package/docs/releases/RELEASE_NOTES_1.7.0.md +0 -539
  120. package/docs/releases/RELEASE_NOTES_1.8.10.md +0 -239
  121. package/docs/releases/RELEASE_NOTES_1.8.9.md +0 -213
  122. package/docs/releases/RELEASE_NOTES_v1.8.0.md +0 -685
  123. package/docs/releases/RELEASE_NOTES_v1.8.1.md +0 -221
  124. package/docs/releases/RELEASE_NOTES_v1.8.2.md +0 -421
  125. package/docs/releases/RELEASE_NOTES_v1.8.3.md +0 -292
  126. package/docs/releases/RELEASE_NOTES_v1.8.4.md +0 -277
  127. package/docs/releases/RELEASE_NOTES_v1.8.6.md +0 -205
  128. package/docs/releases/RELEASE_NOTES_v1.8.7.md +0 -184
  129. package/docs/releases/RELEASE_PLAN_v1.7.6.md +0 -816
  130. package/docs/releases/RELEASE_SUMMARY_1.8.10.md +0 -193
  131. package/docs/releases/RELEASE_SUMMARY_v1.7.4.md +0 -276
  132. package/docs/releases/RELEASE_SUMMARY_v1.7.5.md +0 -322
  133. package/docs/releases/RELEASE_SUMMARY_v1.7.6.md +0 -436
  134. package/docs/releases/RELEASE_SUMMARY_v1.7.7.md +0 -391
  135. package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +0 -523
  136. package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +0 -542
  137. package/docs/troubleshooting/ARDUINO_IDE_VERSION_FIX_SUMMARY.md +0 -229
  138. package/docs/troubleshooting/ARDUINO_LIBRARY_NAME_FIX.md +0 -197
  139. package/docs/troubleshooting/CRASH_QUICK_REF.md +0 -93
  140. package/docs/troubleshooting/ESP32_C6_COMPATIBILITY.md +0 -157
  141. package/docs/troubleshooting/FREERTOS_ASSERTION_FAILURE.md +0 -288
  142. package/docs/troubleshooting/FREERTOS_FIX_IMPLEMENTATION.md +0 -267
  143. package/docs/troubleshooting/NPM_PUBLISHING_ISSUE_SUMMARY.md +0 -110
  144. package/docs/troubleshooting/PAINLESSMESH_V1.7.4_COMPILATION_ISSUES.md +0 -547
  145. package/docs/troubleshooting/QUICK_FIX_FREERTOS.md +0 -164
  146. package/docs/troubleshooting/SENSOR_NODE_CONNECTION_CRASH.md +0 -264
  147. package/docs/troubleshooting/station-reconnection-issues.md +0 -172
  148. package/docs/v1.7.7_MQTT_IMPROVEMENTS.md +0 -794
  149. package/docs/wiki/API-Reference.md +0 -246
  150. package/docs/wiki/Complete-Documentation.md +0 -123
  151. package/examples/alteriomImproved/alteriom_sensor_package.hpp +0 -224
  152. package/examples/alteriomImproved/improved_sensor_node.ino +0 -248
  153. package/examples/alteriomImproved/platformio.ini +0 -32
  154. package/examples/alteriomMetricsHealth/alteriom_sensor_package.hpp +0 -796
  155. package/examples/alteriomMetricsHealth/metrics_health_node.ino +0 -429
  156. package/examples/alteriomMetricsHealth/platformio.ini +0 -26
  157. package/examples/alteriomPhase1/alteriom_sensor_package.hpp +0 -224
  158. package/examples/alteriomPhase1/phase1_features.ino +0 -242
  159. package/examples/alteriomPhase1/platformio.ini +0 -26
  160. package/examples/alteriomPhase2/alteriom_sensor_package.hpp +0 -224
  161. package/examples/alteriomPhase2/phase2_features.ino +0 -186
  162. package/examples/alteriomPhase2/platformio.ini +0 -26
  163. package/examples/alteriomSensorNode/alteriom_sensor_node.ino +0 -186
  164. package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +0 -1227
  165. package/examples/alteriomSensorNode/platformio.ini +0 -26
  166. package/examples/bridge/alteriom_sensor_package.hpp +0 -1170
  167. package/examples/bridge/bridge_health_monitoring_example.ino +0 -188
  168. package/examples/bridge/enhanced_mqtt_bridge.hpp +0 -610
  169. package/examples/bridge/enhanced_mqtt_bridge_example.ino +0 -226
  170. package/examples/bridge/mesh_event_publisher.hpp +0 -253
  171. package/examples/bridge/mesh_topology_reporter.hpp +0 -303
  172. package/examples/bridge/mqtt_command_bridge.hpp +0 -459
  173. package/examples/bridge/mqtt_status_bridge.hpp +0 -519
  174. package/examples/bridgeAwareSensorNode/alteriom_sensor_package.hpp +0 -1227
  175. package/examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino +0 -342
  176. package/examples/bridgeAwareSensorNode/platformio.ini +0 -26
  177. package/examples/diagnosticsExample/diagnosticsExample.ino +0 -171
  178. package/examples/diagnosticsExample/platformio.ini +0 -26
  179. package/examples/echoNode/echoNode.ino +0 -33
  180. package/examples/echoNode/platformio.ini +0 -26
  181. package/examples/meshCommandNode/alteriom_sensor_package.hpp +0 -235
  182. package/examples/meshCommandNode/meshCommandNode.ino +0 -265
  183. package/examples/mqttCommandBridge/alteriom_sensor_package.hpp +0 -235
  184. package/examples/mqttCommandBridge/mesh_event_publisher.hpp +0 -253
  185. package/examples/mqttCommandBridge/mesh_topology_reporter.hpp +0 -303
  186. package/examples/mqttCommandBridge/mqttCommandBridge.ino +0 -254
  187. package/examples/mqttCommandBridge/mqtt_command_bridge.hpp +0 -462
  188. package/examples/mqttCommandBridge/platformio.ini +0 -27
  189. package/examples/mqttStatusBridge/mqttStatusBridge.ino +0 -218
  190. package/examples/mqttStatusBridge/mqtt_status_bridge.hpp +0 -522
  191. package/examples/mqttStatusBridge/platformio.ini +0 -27
  192. package/examples/mqttTopologyTest/README.md +0 -467
  193. package/examples/mqttTopologyTest/mqttTopologyTest.ino +0 -754
  194. package/examples/mqttTopologyTest/platformio.ini +0 -27
  195. package/examples/multi_bridge/README.md +0 -346
  196. package/examples/multi_bridge/primary_bridge.ino +0 -96
  197. package/examples/multi_bridge/regular_node.ino +0 -141
  198. package/examples/multi_bridge/secondary_bridge.ino +0 -111
  199. package/examples/ntpTimeSyncBridge/alteriom_sensor_package.hpp +0 -1383
  200. package/examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino +0 -86
  201. package/examples/ntpTimeSyncNode/alteriom_sensor_package.hpp +0 -1383
  202. package/examples/ntpTimeSyncNode/ntpTimeSyncNode.ino +0 -109
  203. package/examples/queued_alarms/README.md +0 -390
  204. package/examples/queued_alarms/queued_alarms.ino +0 -265
  205. package/examples/routing_demo/README.md +0 -172
  206. package/examples/routing_demo/routing_demo.ino +0 -102
  207. package/examples/rtcIntegration/README.md +0 -294
  208. package/examples/rtcIntegration/rtcIntegration.ino +0 -210
@@ -1,6 +1,8 @@
1
1
  #ifndef _PAINLESS_MESH_MESH_HPP_
2
2
  #define _PAINLESS_MESH_MESH_HPP_
3
3
 
4
+ #include <algorithm>
5
+ #include <list>
4
6
  #include <vector>
5
7
  #include <map>
6
8
  #include <set>
@@ -9,8 +11,10 @@
9
11
  #include "painlessmesh/configuration.hpp"
10
12
 
11
13
  #include "painlessmesh/connection.hpp"
14
+ #include "painlessmesh/gateway.hpp"
12
15
  #include "painlessmesh/logger.hpp"
13
16
  #include "painlessmesh/message_queue.hpp"
17
+ #include "painlessmesh/message_tracker.hpp"
14
18
  #include "painlessmesh/ntp.hpp"
15
19
  #include "painlessmesh/plugin.hpp"
16
20
  #include "painlessmesh/protocol.hpp"
@@ -30,6 +34,44 @@ typedef std::function<void(int32_t offset)> nodeTimeAdjustedCallback_t;
30
34
  typedef std::function<void(uint32_t nodeId, int32_t delay)> nodeDelayCallback_t;
31
35
  typedef std::function<void(uint32_t bridgeNodeId, bool internetAvailable)> bridgeStatusChangedCallback_t;
32
36
  typedef std::function<void(uint32_t timestamp)> rtcSyncCompleteCallback_t;
37
+ typedef std::function<void(bool available)> localInternetChangedCallback_t;
38
+ typedef std::function<void(uint32_t oldPrimary, uint32_t newPrimary)> gatewayChangedCallback_t;
39
+
40
+ /**
41
+ * Callback type for Internet request results
42
+ *
43
+ * @param success Whether the request was delivered successfully
44
+ * @param httpStatus HTTP status code from the destination (0 if not applicable)
45
+ * @param error Human-readable error message (empty if success)
46
+ */
47
+ typedef std::function<void(bool success, uint16_t httpStatus, TSTRING error)>
48
+ internetResultCallback_t;
49
+
50
+ /**
51
+ * Pending Internet request entry
52
+ *
53
+ * Tracks a pending sendToInternet() request for acknowledgment handling
54
+ */
55
+ struct PendingInternetRequest {
56
+ uint32_t messageId = 0; ///< Unique message ID
57
+ uint32_t timestamp = 0; ///< When request was sent (millis)
58
+ uint8_t retryCount = 0; ///< Number of retries attempted
59
+ uint8_t maxRetries = 3; ///< Maximum retry attempts
60
+ uint8_t priority = 2; ///< Priority level (0=CRITICAL to 3=LOW)
61
+ uint32_t timeoutMs = 30000; ///< Timeout in milliseconds
62
+ uint32_t retryDelayMs = 1000; ///< Current retry delay (for exponential backoff)
63
+ uint32_t gatewayNodeId = 0; ///< Target gateway node ID
64
+ TSTRING destination = ""; ///< Internet destination URL
65
+ TSTRING payload = ""; ///< Request payload
66
+ internetResultCallback_t callback; ///< User callback for result
67
+
68
+ /**
69
+ * Check if this request has timed out
70
+ */
71
+ bool isTimedOut() const {
72
+ return (static_cast<uint32_t>(millis()) - timestamp) > timeoutMs;
73
+ }
74
+ };
33
75
 
34
76
  /**
35
77
  * Bridge information structure
@@ -52,7 +94,9 @@ public:
52
94
  * A bridge is healthy if we've received a status update within the timeout period
53
95
  */
54
96
  bool isHealthy(uint32_t timeoutMs = 60000) const {
55
- return (millis() - lastSeen) < timeoutMs;
97
+ // Cast millis() to uint32_t to handle overflow correctly
98
+ // This ensures consistent behavior on both embedded and test environments
99
+ return (static_cast<uint32_t>(millis()) - lastSeen) < timeoutMs;
56
100
  }
57
101
  };
58
102
 
@@ -564,17 +608,55 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
564
608
  /**
565
609
  * Check if any bridge in the mesh has Internet connectivity
566
610
  *
567
- * @return true if at least one healthy bridge reports Internet connection
611
+ * When connected to mesh, requires recent bridge status (within timeout).
612
+ * When disconnected from mesh, returns true if any bridge previously
613
+ * reported Internet connectivity - allowing operations to be queued for
614
+ * when mesh connectivity is restored.
615
+ *
616
+ * @return true if at least one bridge reports Internet connection
568
617
  */
569
618
  bool hasInternetConnection() {
619
+ bool hasConnections = hasActiveMeshConnections();
620
+
570
621
  for (const auto& bridge : knownBridges) {
571
- if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
622
+ // When connected: require fresh status
623
+ // When disconnected: use last known state
624
+ bool isUsable = hasConnections
625
+ ? (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected)
626
+ : bridge.internetConnected;
627
+
628
+ if (isUsable) {
572
629
  return true;
573
630
  }
574
631
  }
575
632
  return false;
576
633
  }
577
634
 
635
+ /**
636
+ * Get list of nodes that currently have Internet access
637
+ *
638
+ * Returns node IDs of all healthy bridges with active Internet connectivity.
639
+ * Only bridges that have reported status within the timeout period are included.
640
+ *
641
+ * \code
642
+ * auto internetNodes = mesh.getNodesWithInternet();
643
+ * for (auto nodeId : internetNodes) {
644
+ * Serial.printf("Node %u has Internet access\n", nodeId);
645
+ * }
646
+ * \endcode
647
+ *
648
+ * @return Vector of node IDs with Internet connectivity
649
+ */
650
+ std::vector<uint32_t> getNodesWithInternet() {
651
+ std::vector<uint32_t> nodesWithInternet;
652
+ for (const auto& bridge : knownBridges) {
653
+ if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
654
+ nodesWithInternet.push_back(bridge.nodeId);
655
+ }
656
+ }
657
+ return nodesWithInternet;
658
+ }
659
+
578
660
  /**
579
661
  * Get list of all known bridges in the mesh
580
662
  *
@@ -588,18 +670,32 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
588
670
  * Get the primary (best) bridge node
589
671
  *
590
672
  * Primary bridge is selected based on:
591
- * 1. Must be healthy (seen within timeout)
673
+ * 1. Must be healthy (seen within timeout) - unless node is disconnected
592
674
  * 2. Must have Internet connection
593
675
  * 3. Best WiFi RSSI to router
594
676
  *
677
+ * When this node is disconnected from the mesh (no active connections),
678
+ * returns the last known bridge even if lastSeen is stale. This allows
679
+ * nodes to attempt routing through the last known bridge once mesh
680
+ * connectivity is restored.
681
+ *
595
682
  * @return pointer to BridgeInfo of primary bridge, or nullptr if no suitable bridge
596
683
  */
597
684
  BridgeInfo* getPrimaryBridge() {
598
685
  BridgeInfo* primary = nullptr;
599
686
  int8_t bestRSSI = -127; // Worst possible RSSI
600
687
 
688
+ // Check if we have active mesh connections
689
+ bool hasConnections = hasActiveMeshConnections();
690
+
601
691
  for (auto& bridge : knownBridges) {
602
- if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
692
+ // When connected to mesh: require healthy (recent) bridge status
693
+ // When disconnected: use any bridge that reported Internet (stale info is better than none)
694
+ bool isUsable = hasConnections
695
+ ? (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected)
696
+ : bridge.internetConnected;
697
+
698
+ if (isUsable) {
603
699
  if (bridge.routerRSSI > bestRSSI) {
604
700
  bestRSSI = bridge.routerRSSI;
605
701
  primary = &bridge;
@@ -610,6 +706,58 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
610
706
  return primary;
611
707
  }
612
708
 
709
+ /**
710
+ * Get the last known bridge with Internet connection (regardless of lastSeen timeout)
711
+ *
712
+ * Unlike getPrimaryBridge(), this method returns the best known bridge even if
713
+ * the lastSeen timestamp is stale. This is useful when the local node is temporarily
714
+ * disconnected from the mesh and cannot receive bridge status updates.
715
+ *
716
+ * The returned bridge may not be currently reachable, but represents the last
717
+ * known good configuration.
718
+ *
719
+ * @return pointer to BridgeInfo of last known bridge, or nullptr if no bridge ever seen
720
+ */
721
+ BridgeInfo* getLastKnownBridge() {
722
+ BridgeInfo* best = nullptr;
723
+ int8_t bestRSSI = -127; // Worst possible RSSI
724
+
725
+ for (auto& bridge : knownBridges) {
726
+ // Include any bridge that reported Internet at some point
727
+ if (bridge.internetConnected) {
728
+ if (bridge.routerRSSI > bestRSSI) {
729
+ bestRSSI = bridge.routerRSSI;
730
+ best = &bridge;
731
+ }
732
+ }
733
+ }
734
+
735
+ return best;
736
+ }
737
+
738
+ /**
739
+ * Check if this node has active mesh connections
740
+ *
741
+ * Returns true if this node has at least one active connection to other
742
+ * mesh nodes. When false, the node is isolated and cannot receive any
743
+ * mesh messages including bridge status updates.
744
+ *
745
+ * This is useful for distinguishing between:
746
+ * - Bridge genuinely unavailable (mesh connected, but bridge not responding)
747
+ * - Local node temporarily disconnected (cannot receive any mesh messages)
748
+ *
749
+ * @return true if at least one mesh connection is active
750
+ */
751
+ bool hasActiveMeshConnections() {
752
+ // Check if we have any active connections in our subs list
753
+ for (auto& conn : this->subs) {
754
+ if (conn && conn->connected()) {
755
+ return true;
756
+ }
757
+ }
758
+ return false;
759
+ }
760
+
613
761
  /**
614
762
  * Check if this node is acting as a bridge
615
763
  *
@@ -619,6 +767,127 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
619
767
  return this->root;
620
768
  }
621
769
 
770
+ // ==================== Gateway Status API ====================
771
+
772
+ /**
773
+ * Check if this node is the primary gateway
774
+ *
775
+ * A node is the primary gateway if it's the bridge with the best RSSI
776
+ * to the router and has Internet connectivity.
777
+ *
778
+ * \code
779
+ * if (mesh.isPrimaryGateway()) {
780
+ * Serial.println("I am the primary gateway!");
781
+ * }
782
+ * \endcode
783
+ *
784
+ * @return true if this node is the primary gateway
785
+ */
786
+ bool isPrimaryGateway() {
787
+ BridgeInfo* primary = getPrimaryBridge();
788
+ if (primary == nullptr) {
789
+ return false;
790
+ }
791
+ return primary->nodeId == this->nodeId;
792
+ }
793
+
794
+ /**
795
+ * Get the node ID of the current primary gateway
796
+ *
797
+ * Returns the node ID of the bridge with the best RSSI to the router
798
+ * that has Internet connectivity, or 0 if no gateway is available.
799
+ *
800
+ * \code
801
+ * uint32_t gatewayId = mesh.getPrimaryGateway();
802
+ * if (gatewayId != 0) {
803
+ * Serial.printf("Primary gateway is node %u\n", gatewayId);
804
+ * } else {
805
+ * Serial.println("No gateway available");
806
+ * }
807
+ * \endcode
808
+ *
809
+ * @return Node ID of the primary gateway, or 0 if none available
810
+ */
811
+ uint32_t getPrimaryGateway() {
812
+ BridgeInfo* primary = getPrimaryBridge();
813
+ if (primary == nullptr) {
814
+ return 0;
815
+ }
816
+ return primary->nodeId;
817
+ }
818
+
819
+ /**
820
+ * Get list of all nodes with Internet access
821
+ *
822
+ * Returns node IDs of all healthy bridges with active Internet connectivity.
823
+ * Only bridges that have reported status within the timeout period are included.
824
+ *
825
+ * This method provides the same data as getNodesWithInternet() but returns
826
+ * a std::list for consistency with other node list methods in painlessMesh.
827
+ *
828
+ * \code
829
+ * auto gateways = mesh.getGateways();
830
+ * Serial.printf("Available gateways: %d\n", gateways.size());
831
+ * for (auto gatewayId : gateways) {
832
+ * Serial.printf(" Gateway: %u\n", gatewayId);
833
+ * }
834
+ * \endcode
835
+ *
836
+ * @return List of node IDs with Internet connectivity
837
+ */
838
+ std::list<uint32_t> getGateways() {
839
+ // Convert vector to list for API consistency with getNodeList()
840
+ // The copy overhead is acceptable since gateway lists are typically small
841
+ auto nodes = getNodesWithInternet();
842
+ return std::list<uint32_t>(nodes.begin(), nodes.end());
843
+ }
844
+
845
+ /**
846
+ * Get count of available gateways
847
+ *
848
+ * Returns the number of healthy bridges with active Internet connectivity.
849
+ *
850
+ * \code
851
+ * size_t count = mesh.getGatewayCount();
852
+ * if (count == 0) {
853
+ * Serial.println("Warning: No gateways available!");
854
+ * } else {
855
+ * Serial.printf("%d gateway(s) available\n", count);
856
+ * }
857
+ * \endcode
858
+ *
859
+ * @return Number of available gateways
860
+ */
861
+ size_t getGatewayCount() {
862
+ return getNodesWithInternet().size();
863
+ }
864
+
865
+ /**
866
+ * Register callback for primary gateway changes
867
+ *
868
+ * This callback fires when the primary gateway changes, either because
869
+ * a new gateway becomes available, the current gateway becomes unavailable,
870
+ * or a better gateway is discovered.
871
+ *
872
+ * \code
873
+ * mesh.onGatewayChanged([](uint32_t oldPrimary, uint32_t newPrimary) {
874
+ * if (newPrimary == 0) {
875
+ * Serial.println("Warning: Lost all gateways!");
876
+ * } else if (oldPrimary == 0) {
877
+ * Serial.printf("Gateway available: %u\n", newPrimary);
878
+ * } else {
879
+ * Serial.printf("Gateway changed: %u -> %u\n", oldPrimary, newPrimary);
880
+ * }
881
+ * });
882
+ * \endcode
883
+ *
884
+ * @param callback Function to call when primary gateway changes
885
+ */
886
+ void onGatewayChanged(gatewayChangedCallback_t callback) {
887
+ Log(logger::GENERAL, "onGatewayChanged():\n");
888
+ gatewayChangedCallback = callback;
889
+ }
890
+
622
891
  /**
623
892
  * Set the interval for bridge status broadcasts (bridge nodes only)
624
893
  *
@@ -646,6 +915,719 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
646
915
  bridgeStatusBroadcastEnabled = enabled;
647
916
  }
648
917
 
918
+ /**
919
+ * Clean up expired bridge entries that haven't reported recently
920
+ *
921
+ * Removes bridges that haven't sent a status update within the timeout period.
922
+ * This helps maintain an accurate list of available gateways and prevents
923
+ * the bridge list from growing unbounded.
924
+ *
925
+ * Called periodically when bridge cleanup is enabled, or can be called manually.
926
+ *
927
+ * \code
928
+ * // Manual cleanup
929
+ * mesh.cleanupExpiredBridges();
930
+ *
931
+ * // Or enable automatic cleanup (recommended)
932
+ * mesh.enableBridgeCleanup();
933
+ * \endcode
934
+ */
935
+ void cleanupExpiredBridges() {
936
+ using namespace logger;
937
+ size_t sizeBefore = knownBridges.size();
938
+
939
+ // Remove bridges that haven't reported within the timeout period
940
+ knownBridges.erase(
941
+ std::remove_if(knownBridges.begin(), knownBridges.end(),
942
+ [this](const BridgeInfo& bridge) {
943
+ return !bridge.isHealthy(bridgeTimeoutMs);
944
+ }),
945
+ knownBridges.end());
946
+
947
+ size_t removed = sizeBefore - knownBridges.size();
948
+ if (removed > 0) {
949
+ Log(GENERAL, "cleanupExpiredBridges(): Removed %u expired bridges\n", removed);
950
+ }
951
+ }
952
+
953
+ /**
954
+ * Enable periodic cleanup of expired bridge entries
955
+ *
956
+ * Starts a background task that periodically removes stale bridge entries.
957
+ * The cleanup runs at the same interval as the bridge timeout (default 60 seconds).
958
+ *
959
+ * This is recommended for all nodes to prevent memory growth from
960
+ * accumulating stale bridge entries.
961
+ *
962
+ * \code
963
+ * mesh.enableBridgeCleanup(); // Start automatic cleanup
964
+ * \endcode
965
+ */
966
+ void enableBridgeCleanup() {
967
+ using namespace logger;
968
+ if (bridgeCleanupTask != nullptr) {
969
+ Log(GENERAL, "enableBridgeCleanup(): Already enabled\n");
970
+ return;
971
+ }
972
+
973
+ Log(GENERAL, "enableBridgeCleanup(): Starting cleanup task (interval: %u ms)\n",
974
+ bridgeTimeoutMs);
975
+
976
+ bridgeCleanupTask = this->addTask(
977
+ bridgeTimeoutMs,
978
+ TASK_FOREVER,
979
+ [this]() {
980
+ cleanupExpiredBridges();
981
+ });
982
+ }
983
+
984
+ /**
985
+ * Disable periodic cleanup of expired bridge entries
986
+ */
987
+ void disableBridgeCleanup() {
988
+ using namespace logger;
989
+ if (bridgeCleanupTask != nullptr) {
990
+ bridgeCleanupTask->disable();
991
+ bridgeCleanupTask = nullptr;
992
+ Log(GENERAL, "disableBridgeCleanup(): Cleanup task disabled\n");
993
+ }
994
+ }
995
+
996
+ /**
997
+ * Check if bridge cleanup is enabled
998
+ *
999
+ * @return true if periodic cleanup is running
1000
+ */
1001
+ bool isBridgeCleanupEnabled() const {
1002
+ return bridgeCleanupTask != nullptr && bridgeCleanupTask->isEnabled();
1003
+ }
1004
+
1005
+ // ==================== Local Internet Health Check API ====================
1006
+
1007
+ /**
1008
+ * Check if THIS node has direct local Internet access
1009
+ *
1010
+ * This method checks whether this specific node can directly reach the Internet,
1011
+ * as opposed to hasInternetConnection() which checks if any bridge in the mesh
1012
+ * has Internet access.
1013
+ *
1014
+ * For gateway/bridge nodes, this returns the result of the local health check.
1015
+ * For regular nodes, this typically returns false unless they have their own
1016
+ * Internet connection.
1017
+ *
1018
+ * \code
1019
+ * if (mesh.hasLocalInternet()) {
1020
+ * Serial.println("This node has direct Internet access");
1021
+ * }
1022
+ * \endcode
1023
+ *
1024
+ * @return true if this node has verified local Internet connectivity
1025
+ */
1026
+ bool hasLocalInternet() {
1027
+ return internetHealthChecker.hasLocalInternet();
1028
+ }
1029
+
1030
+ /**
1031
+ * Register callback for local Internet connectivity changes
1032
+ *
1033
+ * This callback fires when THIS node's direct Internet connectivity changes,
1034
+ * as detected by the periodic health check.
1035
+ *
1036
+ * \code
1037
+ * mesh.onLocalInternetChanged([](bool available) {
1038
+ * if (available) {
1039
+ * Serial.println("Local Internet connected");
1040
+ * } else {
1041
+ * Serial.println("Local Internet disconnected");
1042
+ * }
1043
+ * });
1044
+ * \endcode
1045
+ *
1046
+ * @param callback Function to call when local Internet status changes
1047
+ */
1048
+ void onLocalInternetChanged(localInternetChangedCallback_t callback) {
1049
+ Log(logger::GENERAL, "onLocalInternetChanged():\n");
1050
+ localInternetChangedCallback = callback;
1051
+ internetHealthChecker.onConnectivityChanged(callback);
1052
+ }
1053
+
1054
+ /**
1055
+ * Get detailed status of local Internet connectivity
1056
+ *
1057
+ * Returns comprehensive information about Internet connectivity checks
1058
+ * including success/failure counts, timing, and error messages.
1059
+ *
1060
+ * \code
1061
+ * auto status = mesh.getInternetStatus();
1062
+ * Serial.printf("Internet %s, uptime: %d%%\n",
1063
+ * status.available ? "UP" : "DOWN",
1064
+ * status.getUptimePercent());
1065
+ * \endcode
1066
+ *
1067
+ * @return InternetStatus structure with detailed connectivity info
1068
+ */
1069
+ gateway::InternetStatus getInternetStatus() {
1070
+ return internetHealthChecker.getStatus();
1071
+ }
1072
+
1073
+ /**
1074
+ * Configure Internet health checker with gateway settings
1075
+ *
1076
+ * Sets up the health checker with parameters from SharedGatewayConfig.
1077
+ * This should be called before enabling gateway mode.
1078
+ *
1079
+ * @param config Gateway configuration with check parameters
1080
+ */
1081
+ void configureInternetHealthCheck(const gateway::SharedGatewayConfig& config) {
1082
+ internetHealthChecker.setConfig(config);
1083
+ }
1084
+
1085
+ /**
1086
+ * Set custom Internet check target
1087
+ *
1088
+ * Override the default check host/port (8.8.8.8:53).
1089
+ *
1090
+ * @param host Host to check (IP or hostname)
1091
+ * @param port Port to connect to (default 53 for DNS)
1092
+ */
1093
+ void setInternetCheckTarget(const TSTRING& host, uint16_t port = 53) {
1094
+ internetHealthChecker.setCheckTarget(host, port);
1095
+ }
1096
+
1097
+ /**
1098
+ * Set Internet check interval
1099
+ *
1100
+ * @param intervalMs Interval between checks in milliseconds
1101
+ */
1102
+ void setInternetCheckInterval(uint32_t intervalMs) {
1103
+ internetHealthChecker.setCheckInterval(intervalMs);
1104
+ }
1105
+
1106
+ /**
1107
+ * Set Internet check timeout
1108
+ *
1109
+ * @param timeoutMs Timeout for each check in milliseconds
1110
+ */
1111
+ void setInternetCheckTimeout(uint32_t timeoutMs) {
1112
+ internetHealthChecker.setCheckTimeout(timeoutMs);
1113
+ }
1114
+
1115
+ /**
1116
+ * Perform an immediate Internet connectivity check
1117
+ *
1118
+ * Bypasses the periodic check schedule and performs an immediate check.
1119
+ * Fires the onLocalInternetChanged callback if status changes.
1120
+ *
1121
+ * @return true if Internet is reachable
1122
+ */
1123
+ bool checkInternetNow() {
1124
+ return internetHealthChecker.checkNow();
1125
+ }
1126
+
1127
+ /**
1128
+ * Enable periodic Internet health checking
1129
+ *
1130
+ * Starts a TaskScheduler task that periodically checks Internet connectivity.
1131
+ * The check interval is configured via setInternetCheckInterval() or
1132
+ * configureInternetHealthCheck().
1133
+ *
1134
+ * \code
1135
+ * mesh.enableInternetHealthCheck(); // Use default 30 second interval
1136
+ * // or
1137
+ * mesh.setInternetCheckInterval(15000); // 15 seconds
1138
+ * mesh.enableInternetHealthCheck();
1139
+ * \endcode
1140
+ */
1141
+ void enableInternetHealthCheck() {
1142
+ using namespace logger;
1143
+ if (internetHealthCheckTask != nullptr) {
1144
+ Log(GENERAL, "enableInternetHealthCheck(): Already enabled\n");
1145
+ return;
1146
+ }
1147
+
1148
+ Log(GENERAL, "enableInternetHealthCheck(): Starting health check task (interval: %u ms)\n",
1149
+ internetHealthChecker.getCheckInterval());
1150
+
1151
+ internetHealthCheckTask = this->addTask(
1152
+ internetHealthChecker.getCheckInterval(),
1153
+ TASK_FOREVER,
1154
+ [this]() {
1155
+ this->internetHealthChecker.checkNow();
1156
+ });
1157
+ }
1158
+
1159
+ /**
1160
+ * Disable periodic Internet health checking
1161
+ */
1162
+ void disableInternetHealthCheck() {
1163
+ using namespace logger;
1164
+ if (internetHealthCheckTask != nullptr) {
1165
+ internetHealthCheckTask->disable();
1166
+ internetHealthCheckTask = nullptr;
1167
+ Log(GENERAL, "disableInternetHealthCheck(): Health check task disabled\n");
1168
+ }
1169
+ }
1170
+
1171
+ /**
1172
+ * Check if Internet health checking is enabled
1173
+ *
1174
+ * @return true if periodic health checks are running
1175
+ */
1176
+ bool isInternetHealthCheckEnabled() const {
1177
+ return internetHealthCheckTask != nullptr && internetHealthCheckTask->isEnabled();
1178
+ }
1179
+
1180
+ /**
1181
+ * Reset Internet health check statistics
1182
+ */
1183
+ void resetInternetHealthStats() {
1184
+ internetHealthChecker.resetStats();
1185
+ }
1186
+
1187
+ #ifdef PAINLESSMESH_BOOST
1188
+ /**
1189
+ * Set mock Internet connectivity (test environment only)
1190
+ *
1191
+ * @param connected Whether to simulate connected state
1192
+ */
1193
+ void setMockInternetConnected(bool connected) {
1194
+ internetHealthChecker.setMockConnected(connected);
1195
+ }
1196
+ #endif
1197
+
1198
+ // ==================== Send to Internet API ====================
1199
+
1200
+ /**
1201
+ * Send data to an Internet destination through the mesh gateway
1202
+ *
1203
+ * This method provides a high-level API for sending data to the Internet
1204
+ * through the mesh network. It automatically handles:
1205
+ * - Local Internet: If this node has direct Internet access, sends directly
1206
+ * - Gateway routing: Otherwise routes through the best available gateway
1207
+ * - Acknowledgment tracking: Tracks message delivery with callbacks
1208
+ * - Retry logic: Implements exponential backoff for failed deliveries
1209
+ * - Priority handling: Respects message priority levels
1210
+ *
1211
+ * PRIORITY LEVELS:
1212
+ * - 0 (CRITICAL): Immediate processing, no delays
1213
+ * - 1 (HIGH): High priority, processed before normal
1214
+ * - 2 (NORMAL): Standard processing (default)
1215
+ * - 3 (LOW): Background processing when idle
1216
+ *
1217
+ * CALLBACK BEHAVIOR:
1218
+ * The callback is invoked when:
1219
+ * - Success: Gateway confirms delivery to Internet destination
1220
+ * - Failure: Timeout, no gateway available, or delivery error
1221
+ *
1222
+ * \code
1223
+ * // Send sensor data to cloud API
1224
+ * uint32_t msgId = mesh.sendToInternet(
1225
+ * "https://api.example.com/sensor",
1226
+ * "{\"temperature\": 23.5}",
1227
+ * [](bool success, uint16_t httpStatus, String error) {
1228
+ * if (success) {
1229
+ * Serial.printf("Delivered! HTTP %u\n", httpStatus);
1230
+ * } else {
1231
+ * Serial.printf("Failed: %s\n", error.c_str());
1232
+ * }
1233
+ * },
1234
+ * PRIORITY_NORMAL // Optional priority
1235
+ * );
1236
+ * Serial.printf("Message ID: %u\n", msgId);
1237
+ * \endcode
1238
+ *
1239
+ * @param destination The Internet destination URL (e.g., "https://api.example.com/data")
1240
+ * @param payload The data payload to send (typically JSON)
1241
+ * @param callback Function called with result (success, httpStatus, error)
1242
+ * @param priority Message priority (0=CRITICAL to 3=LOW, default: 2=NORMAL)
1243
+ * @return Unique message ID for tracking, or 0 if immediate failure
1244
+ */
1245
+ uint32_t sendToInternet(
1246
+ TSTRING destination,
1247
+ TSTRING payload,
1248
+ internetResultCallback_t callback,
1249
+ uint8_t priority = static_cast<uint8_t>(gateway::GatewayPriority::PRIORITY_NORMAL)) {
1250
+ using namespace logger;
1251
+
1252
+ // Generate unique message ID
1253
+ uint32_t messageId = gateway::GatewayDataPackage::generateMessageId(this->nodeId);
1254
+
1255
+ Log(COMMUNICATION, "sendToInternet(): msgId=%u dest=%s priority=%u\n",
1256
+ messageId, destination.c_str(), priority);
1257
+
1258
+ // Check if we have local Internet access
1259
+ // Note: Even with local Internet, we still use the gateway protocol for consistency.
1260
+ // A future optimization could bypass the mesh for nodes with direct Internet access.
1261
+ if (hasLocalInternet()) {
1262
+ Log(COMMUNICATION, "sendToInternet(): Local Internet available, using gateway protocol for consistency\n");
1263
+ }
1264
+
1265
+ // Find the best gateway to route through
1266
+ BridgeInfo* gateway = getPrimaryBridge();
1267
+ if (gateway == nullptr) {
1268
+ Log(ERROR, "sendToInternet(): No gateway available\n");
1269
+ if (callback) {
1270
+ // Schedule callback to avoid blocking
1271
+ this->addTask([callback]() {
1272
+ callback(false, 0, "No gateway available");
1273
+ });
1274
+ }
1275
+ return 0;
1276
+ }
1277
+
1278
+ // Create and store the pending request
1279
+ PendingInternetRequest request;
1280
+ request.messageId = messageId;
1281
+ request.timestamp = millis();
1282
+ request.retryCount = 0;
1283
+ request.maxRetries = internetRetryCount;
1284
+ request.priority = priority;
1285
+ request.timeoutMs = internetRequestTimeout;
1286
+ request.retryDelayMs = internetRetryDelay;
1287
+ request.gatewayNodeId = gateway->nodeId;
1288
+ request.destination = destination;
1289
+ request.payload = payload;
1290
+ request.callback = callback;
1291
+
1292
+ // Store pending request
1293
+ pendingInternetRequests[messageId] = request;
1294
+
1295
+ // Create gateway data package
1296
+ gateway::GatewayDataPackage pkg;
1297
+ pkg.from = this->nodeId;
1298
+ pkg.dest = gateway->nodeId;
1299
+ pkg.messageId = messageId;
1300
+ pkg.originNode = this->nodeId;
1301
+ pkg.timestamp = this->getNodeTime();
1302
+ pkg.priority = priority;
1303
+ pkg.destination = destination;
1304
+ pkg.payload = payload;
1305
+ pkg.contentType = "application/json";
1306
+ pkg.retryCount = 0;
1307
+ pkg.requiresAck = true;
1308
+
1309
+ // Send the package with priority
1310
+ bool sent = false;
1311
+ auto conn = painlessmesh::router::findRoute<T>((*this), gateway->nodeId);
1312
+ if (conn) {
1313
+ sent = painlessmesh::router::sendWithPriority(pkg, conn, priority);
1314
+ if (!sent) {
1315
+ Log(ERROR, "sendToInternet(): sendWithPriority failed to gateway %u (send buffer full?)\n", gateway->nodeId);
1316
+ }
1317
+ } else {
1318
+ Log(ERROR, "sendToInternet(): No route to gateway %u\n", gateway->nodeId);
1319
+ }
1320
+
1321
+ if (!sent) {
1322
+ Log(ERROR, "sendToInternet(): Failed to send to gateway %u, scheduling retry\n", gateway->nodeId);
1323
+ // Schedule retry or failure callback
1324
+ scheduleInternetRetry(messageId);
1325
+ } else {
1326
+ Log(COMMUNICATION, "sendToInternet(): Sent to gateway %u\n", gateway->nodeId);
1327
+ // Schedule timeout check
1328
+ scheduleInternetTimeout(messageId);
1329
+ }
1330
+
1331
+ return messageId;
1332
+ }
1333
+
1334
+ /**
1335
+ * Configure Internet request timeout
1336
+ *
1337
+ * @param timeoutMs Timeout in milliseconds (default: 30000)
1338
+ */
1339
+ void setInternetRequestTimeout(uint32_t timeoutMs) {
1340
+ internetRequestTimeout = timeoutMs;
1341
+ }
1342
+
1343
+ /**
1344
+ * Configure Internet request retry count
1345
+ *
1346
+ * @param retryCount Number of retry attempts (default: 3)
1347
+ */
1348
+ void setInternetRetryCount(uint8_t retryCount) {
1349
+ internetRetryCount = retryCount;
1350
+ }
1351
+
1352
+ /**
1353
+ * Configure base retry delay for exponential backoff
1354
+ *
1355
+ * @param delayMs Base delay in milliseconds (default: 1000)
1356
+ */
1357
+ void setInternetRetryDelay(uint32_t delayMs) {
1358
+ internetRetryDelay = delayMs;
1359
+ }
1360
+
1361
+ /**
1362
+ * Get number of pending Internet requests
1363
+ *
1364
+ * @return Number of requests waiting for acknowledgment
1365
+ */
1366
+ size_t getPendingInternetRequestCount() const {
1367
+ return pendingInternetRequests.size();
1368
+ }
1369
+
1370
+ /**
1371
+ * Cancel a pending Internet request
1372
+ *
1373
+ * @param messageId Message ID to cancel
1374
+ * @return true if request was found and cancelled
1375
+ */
1376
+ bool cancelInternetRequest(uint32_t messageId) {
1377
+ auto it = pendingInternetRequests.find(messageId);
1378
+ if (it != pendingInternetRequests.end()) {
1379
+ if (it->second.callback) {
1380
+ it->second.callback(false, 0, "Request cancelled");
1381
+ }
1382
+ pendingInternetRequests.erase(it);
1383
+ Log(logger::GENERAL, "cancelInternetRequest(): Cancelled msgId=%u\n", messageId);
1384
+ return true;
1385
+ }
1386
+ return false;
1387
+ }
1388
+
1389
+ /**
1390
+ * Enable the sendToInternet() API
1391
+ *
1392
+ * This registers the handler for GatewayAckPackage (Type 621) and
1393
+ * starts the periodic cleanup of timed-out requests.
1394
+ *
1395
+ * Call this after mesh.init() to enable the sendToInternet() functionality.
1396
+ *
1397
+ * \code
1398
+ * mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
1399
+ * mesh.enableSendToInternet(); // Enable the API
1400
+ * \endcode
1401
+ */
1402
+ void enableSendToInternet() {
1403
+ using namespace logger;
1404
+ Log(GENERAL, "enableSendToInternet(): Enabling Internet routing API\n");
1405
+
1406
+ // Register handler for gateway acknowledgment packages
1407
+ this->callbackList.onPackage(
1408
+ protocol::GATEWAY_ACK,
1409
+ [this](protocol::Variant& variant, std::shared_ptr<T>, uint32_t) {
1410
+ auto ack = variant.to<gateway::GatewayAckPackage>();
1411
+ this->handleGatewayAck(ack);
1412
+ return false; // Don't consume, allow other handlers
1413
+ });
1414
+
1415
+ // Start cleanup task for timed-out requests
1416
+ if (internetCleanupTask == nullptr) {
1417
+ internetCleanupTask = this->addTask(
1418
+ 5000, // Check every 5 seconds
1419
+ TASK_FOREVER,
1420
+ [this]() {
1421
+ this->cleanupTimedOutRequests();
1422
+ });
1423
+ }
1424
+
1425
+ sendToInternetEnabled = true;
1426
+ }
1427
+
1428
+ /**
1429
+ * Disable the sendToInternet() API
1430
+ */
1431
+ void disableSendToInternet() {
1432
+ using namespace logger;
1433
+ Log(GENERAL, "disableSendToInternet(): Disabling Internet routing API\n");
1434
+
1435
+ if (internetCleanupTask != nullptr) {
1436
+ internetCleanupTask->disable();
1437
+ internetCleanupTask = nullptr;
1438
+ }
1439
+
1440
+ // Cancel all pending requests
1441
+ for (auto& pair : pendingInternetRequests) {
1442
+ if (pair.second.callback) {
1443
+ pair.second.callback(false, 0, "API disabled");
1444
+ }
1445
+ }
1446
+ pendingInternetRequests.clear();
1447
+
1448
+ sendToInternetEnabled = false;
1449
+ }
1450
+
1451
+ /**
1452
+ * Check if sendToInternet() API is enabled
1453
+ *
1454
+ * @return true if the API is enabled
1455
+ */
1456
+ bool isSendToInternetEnabled() const {
1457
+ return sendToInternetEnabled;
1458
+ }
1459
+
1460
+ private:
1461
+ /**
1462
+ * Handle incoming gateway acknowledgment package
1463
+ */
1464
+ void handleGatewayAck(const gateway::GatewayAckPackage& ack) {
1465
+ using namespace logger;
1466
+ Log(COMMUNICATION, "handleGatewayAck(): msgId=%u success=%d http=%u\n",
1467
+ ack.messageId, ack.success, ack.httpStatus);
1468
+
1469
+ auto it = pendingInternetRequests.find(ack.messageId);
1470
+ if (it == pendingInternetRequests.end()) {
1471
+ Log(GENERAL, "handleGatewayAck(): Unknown message ID %u\n", ack.messageId);
1472
+ return;
1473
+ }
1474
+
1475
+ PendingInternetRequest& request = it->second;
1476
+
1477
+ // Call user callback
1478
+ if (request.callback) {
1479
+ request.callback(ack.success, ack.httpStatus, ack.error);
1480
+ }
1481
+
1482
+ // Remove from pending
1483
+ pendingInternetRequests.erase(it);
1484
+ }
1485
+
1486
+ /**
1487
+ * Schedule retry for a failed Internet request
1488
+ */
1489
+ void scheduleInternetRetry(uint32_t messageId) {
1490
+ auto it = pendingInternetRequests.find(messageId);
1491
+ if (it == pendingInternetRequests.end()) {
1492
+ return;
1493
+ }
1494
+
1495
+ PendingInternetRequest& request = it->second;
1496
+
1497
+ if (request.retryCount >= request.maxRetries) {
1498
+ // Max retries reached - fail the request
1499
+ Log(logger::ERROR, "scheduleInternetRetry(): Max retries reached for msgId=%u\n",
1500
+ messageId);
1501
+ if (request.callback) {
1502
+ request.callback(false, 0, "Max retries exceeded");
1503
+ }
1504
+ pendingInternetRequests.erase(it);
1505
+ return;
1506
+ }
1507
+
1508
+ // Calculate exponential backoff delay
1509
+ uint32_t delay = request.retryDelayMs * (1 << request.retryCount);
1510
+ request.retryCount++;
1511
+
1512
+ Log(logger::COMMUNICATION, "scheduleInternetRetry(): Retry %u for msgId=%u in %u ms\n",
1513
+ request.retryCount, messageId, delay);
1514
+
1515
+ // Schedule retry
1516
+ this->addTask(delay, TASK_ONCE, [this, messageId]() {
1517
+ this->retryInternetRequest(messageId);
1518
+ });
1519
+ }
1520
+
1521
+ /**
1522
+ * Retry sending an Internet request
1523
+ */
1524
+ void retryInternetRequest(uint32_t messageId) {
1525
+ auto it = pendingInternetRequests.find(messageId);
1526
+ if (it == pendingInternetRequests.end()) {
1527
+ return;
1528
+ }
1529
+
1530
+ PendingInternetRequest& request = it->second;
1531
+
1532
+ // Find gateway (may have changed)
1533
+ BridgeInfo* gateway = getPrimaryBridge();
1534
+ if (gateway == nullptr) {
1535
+ Log(logger::ERROR, "retryInternetRequest(): No gateway for retry msgId=%u\n",
1536
+ messageId);
1537
+ scheduleInternetRetry(messageId);
1538
+ return;
1539
+ }
1540
+
1541
+ // Update gateway node ID (may have changed)
1542
+ request.gatewayNodeId = gateway->nodeId;
1543
+
1544
+ // Create gateway data package
1545
+ gateway::GatewayDataPackage pkg;
1546
+ pkg.from = this->nodeId;
1547
+ pkg.dest = gateway->nodeId;
1548
+ pkg.messageId = messageId;
1549
+ pkg.originNode = this->nodeId;
1550
+ pkg.timestamp = this->getNodeTime();
1551
+ pkg.priority = request.priority;
1552
+ pkg.destination = request.destination;
1553
+ pkg.payload = request.payload;
1554
+ pkg.contentType = "application/json";
1555
+ pkg.retryCount = request.retryCount;
1556
+ pkg.requiresAck = true;
1557
+
1558
+ // Send the package
1559
+ auto conn = painlessmesh::router::findRoute<T>((*this), gateway->nodeId);
1560
+ bool sent = false;
1561
+ if (conn) {
1562
+ sent = painlessmesh::router::sendWithPriority(pkg, conn, request.priority);
1563
+ }
1564
+
1565
+ if (!sent) {
1566
+ Log(logger::ERROR, "retryInternetRequest(): Retry send failed msgId=%u\n", messageId);
1567
+ scheduleInternetRetry(messageId);
1568
+ } else {
1569
+ Log(logger::COMMUNICATION, "retryInternetRequest(): Retry sent msgId=%u to gateway %u\n",
1570
+ messageId, gateway->nodeId);
1571
+ }
1572
+ }
1573
+
1574
+ /**
1575
+ * Schedule timeout check for an Internet request
1576
+ */
1577
+ void scheduleInternetTimeout(uint32_t messageId) {
1578
+ auto it = pendingInternetRequests.find(messageId);
1579
+ if (it == pendingInternetRequests.end()) {
1580
+ return;
1581
+ }
1582
+
1583
+ uint32_t timeout = it->second.timeoutMs;
1584
+
1585
+ this->addTask(timeout, TASK_ONCE, [this, messageId]() {
1586
+ this->checkInternetRequestTimeout(messageId);
1587
+ });
1588
+ }
1589
+
1590
+ /**
1591
+ * Check if a specific request has timed out
1592
+ */
1593
+ void checkInternetRequestTimeout(uint32_t messageId) {
1594
+ auto it = pendingInternetRequests.find(messageId);
1595
+ if (it == pendingInternetRequests.end()) {
1596
+ return; // Already handled
1597
+ }
1598
+
1599
+ PendingInternetRequest& request = it->second;
1600
+
1601
+ if (request.isTimedOut()) {
1602
+ Log(logger::ERROR, "checkInternetRequestTimeout(): Request timed out msgId=%u\n",
1603
+ messageId);
1604
+ if (request.callback) {
1605
+ request.callback(false, 0, "Request timed out");
1606
+ }
1607
+ pendingInternetRequests.erase(it);
1608
+ }
1609
+ }
1610
+
1611
+ /**
1612
+ * Cleanup all timed-out Internet requests
1613
+ */
1614
+ void cleanupTimedOutRequests() {
1615
+ auto it = pendingInternetRequests.begin();
1616
+ while (it != pendingInternetRequests.end()) {
1617
+ if (it->second.isTimedOut()) {
1618
+ Log(logger::GENERAL, "cleanupTimedOutRequests(): Cleaning up msgId=%u\n",
1619
+ it->first);
1620
+ if (it->second.callback) {
1621
+ it->second.callback(false, 0, "Request timed out");
1622
+ }
1623
+ it = pendingInternetRequests.erase(it);
1624
+ } else {
1625
+ ++it;
1626
+ }
1627
+ }
1628
+ }
1629
+
1630
+ public:
649
1631
  /**
650
1632
  * Update bridge information from received status package
651
1633
  * Internal method called when bridge status is received
@@ -679,7 +1661,30 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
679
1661
  bool wasConnected = false;
680
1662
  bool isNewBridge = false;
681
1663
  if (bridge == nullptr) {
682
- // New bridge - add to list
1664
+ // New bridge - enforce MAX_KNOWN_BRIDGES limit before adding
1665
+ if (knownBridges.size() >= MAX_KNOWN_BRIDGES) {
1666
+ // First try to remove expired bridges
1667
+ cleanupExpiredBridges();
1668
+
1669
+ // If still at capacity, remove the bridge with worst RSSI (lowest signal)
1670
+ if (knownBridges.size() >= MAX_KNOWN_BRIDGES && !knownBridges.empty()) {
1671
+ auto worstBridge = knownBridges.begin();
1672
+ int8_t worstRSSI = worstBridge->routerRSSI;
1673
+
1674
+ for (auto it = knownBridges.begin(); it != knownBridges.end(); ++it) {
1675
+ if (it->routerRSSI < worstRSSI) {
1676
+ worstRSSI = it->routerRSSI;
1677
+ worstBridge = it;
1678
+ }
1679
+ }
1680
+
1681
+ Log(logger::GENERAL, "updateBridgeStatus(): Removing bridge %u (RSSI: %d) to make room\n",
1682
+ worstBridge->nodeId, worstBridge->routerRSSI);
1683
+ knownBridges.erase(worstBridge);
1684
+ }
1685
+ }
1686
+
1687
+ // Add new bridge
683
1688
  BridgeInfo newBridge;
684
1689
  newBridge.nodeId = bridgeNodeId;
685
1690
  knownBridges.push_back(newBridge);
@@ -702,6 +1707,11 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
702
1707
  auto newPrimary = this->getPrimaryBridge();
703
1708
  uint32_t newPrimaryBridgeId = (newPrimary != nullptr) ? newPrimary->nodeId : 0;
704
1709
 
1710
+ // Trigger gateway changed callback if primary gateway changed
1711
+ if (oldPrimaryBridgeId != newPrimaryBridgeId && gatewayChangedCallback) {
1712
+ gatewayChangedCallback(oldPrimaryBridgeId, newPrimaryBridgeId);
1713
+ }
1714
+
705
1715
  if (diagnosticsEnabled && oldPrimaryBridgeId != newPrimaryBridgeId) {
706
1716
  // Record bridge change
707
1717
  lastBridgeChange.timestamp = millis();
@@ -2095,6 +3105,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
2095
3105
  nodeDelayCallback_t nodeDelayReceivedCallback;
2096
3106
  bridgeStatusChangedCallback_t bridgeStatusChangedCallback;
2097
3107
  rtcSyncCompleteCallback_t rtcSyncCompleteCallback;
3108
+ gatewayChangedCallback_t gatewayChangedCallback;
2098
3109
 
2099
3110
  // Message queue for offline mode
2100
3111
  MessageQueue* messageQueue = nullptr;
@@ -2143,6 +3154,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
2143
3154
  uint32_t bridgeStatusIntervalMs = 30000; // Default 30 seconds
2144
3155
  uint32_t bridgeTimeoutMs = 60000; // Default 60 seconds
2145
3156
  bool bridgeStatusBroadcastEnabled = true;
3157
+
3158
+ // Bridge cleanup configuration
3159
+ static const size_t MAX_KNOWN_BRIDGES = 20; // Memory efficient limit for ESP8266
3160
+ std::shared_ptr<Task> bridgeCleanupTask = nullptr;
2146
3161
 
2147
3162
  // Health metrics tracking
2148
3163
  uint32_t metricsDisconnectCount = 0;
@@ -2157,6 +3172,19 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
2157
3172
  BridgeChangeEvent lastBridgeChange;
2158
3173
  uint32_t lastBridgeChangeTime = 0;
2159
3174
 
3175
+ // Local Internet health check
3176
+ gateway::InternetHealthChecker internetHealthChecker;
3177
+ std::shared_ptr<Task> internetHealthCheckTask = nullptr;
3178
+ localInternetChangedCallback_t localInternetChangedCallback;
3179
+
3180
+ // sendToInternet API
3181
+ std::map<uint32_t, PendingInternetRequest> pendingInternetRequests;
3182
+ std::shared_ptr<Task> internetCleanupTask = nullptr;
3183
+ uint32_t internetRequestTimeout = 30000; // Default 30 seconds
3184
+ uint8_t internetRetryCount = 3; // Default 3 retries
3185
+ uint32_t internetRetryDelay = 1000; // Default 1 second base delay
3186
+ bool sendToInternetEnabled = false;
3187
+
2160
3188
  friend T;
2161
3189
  friend void onDataCb(void *, AsyncClient *, void *, size_t);
2162
3190
  friend void tcpSentCb(void *, AsyncClient *, size_t, uint32_t);