@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
@@ -0,0 +1,2120 @@
1
+ #ifndef _PAINLESS_MESH_GATEWAY_HPP_
2
+ #define _PAINLESS_MESH_GATEWAY_HPP_
3
+
4
+ /**
5
+ * @file gateway.hpp
6
+ * @brief Shared Gateway Mode configuration for painlessMesh
7
+ *
8
+ * This file defines the SharedGatewayConfig structure which provides
9
+ * configuration for the Shared Gateway Mode functionality. This feature
10
+ * allows any node in the mesh to become a gateway to the Internet when
11
+ * configured with router credentials.
12
+ *
13
+ * MEMORY FOOTPRINT
14
+ * ================
15
+ * The SharedGatewayConfig structure has an estimated memory footprint of:
16
+ * - Base fields (booleans, integers): ~44 bytes
17
+ * - TSTRING fields (routerSSID, routerPassword, internetCheckHost):
18
+ * - ESP8266/ESP32 String: ~12 bytes overhead per String + content length
19
+ * - PC/Test std::string: ~32 bytes overhead per string + content length
20
+ * - Total estimated minimum: ~80 bytes (ESP) to ~140 bytes (PC/Test)
21
+ * - With typical content: ~150-250 bytes depending on SSID/password length
22
+ *
23
+ * For ESP8266 with ~80KB RAM, this represents <0.5% of available memory.
24
+ * For ESP32 with ~320KB RAM, this represents <0.1% of available memory.
25
+ *
26
+ * CONFIGURATION DEFAULTS
27
+ * ======================
28
+ * All time-based fields are stored in milliseconds for consistency with
29
+ * the Alteriom time field naming convention. Default values are chosen
30
+ * for reliable operation in typical home/office mesh deployments.
31
+ */
32
+
33
+ #include "Arduino.h"
34
+ #include "painlessmesh/configuration.hpp"
35
+ #include "painlessmesh/logger.hpp"
36
+ #include "painlessmesh/message_tracker.hpp"
37
+ #include "painlessmesh/plugin.hpp"
38
+ #include "painlessmesh/protocol.hpp"
39
+
40
+ #include <functional>
41
+ #include <map>
42
+
43
+ namespace painlessmesh {
44
+ namespace gateway {
45
+
46
+ /**
47
+ * @brief Validation result structure for SharedGatewayConfig
48
+ *
49
+ * Provides detailed validation feedback with error messages.
50
+ */
51
+ struct ValidationResult {
52
+ bool valid = true;
53
+ TSTRING errorMessage = "";
54
+
55
+ ValidationResult() = default;
56
+ ValidationResult(bool v, const TSTRING& msg) : valid(v), errorMessage(msg) {}
57
+
58
+ explicit operator bool() const { return valid; }
59
+ };
60
+
61
+ /**
62
+ * @brief Configuration structure for Shared Gateway Mode
63
+ *
64
+ * This structure holds all configuration parameters needed for a node
65
+ * to operate as a shared gateway, providing Internet connectivity to
66
+ * the mesh network.
67
+ *
68
+ * When enabled and configured with valid router credentials, a node can:
69
+ * - Connect to an external WiFi router for Internet access
70
+ * - Relay messages from the mesh to Internet services (e.g., MQTT)
71
+ * - Participate in gateway election when the current gateway fails
72
+ * - Broadcast its status to other mesh nodes
73
+ *
74
+ * Example usage:
75
+ * @code
76
+ * SharedGatewayConfig config;
77
+ * config.enabled = true;
78
+ * config.routerSSID = "MyHomeWiFi";
79
+ * config.routerPassword = "secretpassword";
80
+ *
81
+ * auto result = config.validate();
82
+ * if (!result.valid) {
83
+ * Serial.println(result.errorMessage.c_str());
84
+ * }
85
+ * @endcode
86
+ */
87
+ struct SharedGatewayConfig {
88
+ // ============================================
89
+ // Core Configuration
90
+ // ============================================
91
+
92
+ /**
93
+ * @brief Enable or disable shared gateway functionality
94
+ *
95
+ * When false, this node will not attempt to act as a gateway.
96
+ */
97
+ bool enabled = false;
98
+
99
+ /**
100
+ * @brief Router SSID to connect for Internet access
101
+ *
102
+ * The SSID of the external WiFi router that provides Internet connectivity.
103
+ * Required when enabled is true.
104
+ */
105
+ TSTRING routerSSID = "";
106
+
107
+ /**
108
+ * @brief Router password for authentication
109
+ *
110
+ * The password for the external WiFi router.
111
+ * May be empty for open networks (not recommended).
112
+ */
113
+ TSTRING routerPassword = "";
114
+
115
+ // ============================================
116
+ // Internet Connectivity Checking
117
+ // ============================================
118
+
119
+ /**
120
+ * @brief Interval between Internet connectivity checks in milliseconds
121
+ *
122
+ * How often the gateway should verify it can reach the Internet.
123
+ * Lower values provide faster failure detection but increase network usage.
124
+ * Default: 30000ms (30 seconds)
125
+ */
126
+ uint32_t internetCheckInterval = 30000;
127
+
128
+ /**
129
+ * @brief Host to ping for Internet connectivity verification
130
+ *
131
+ * A reliable external host used to verify Internet connectivity.
132
+ * Default: "8.8.8.8" (Google Public DNS)
133
+ */
134
+ TSTRING internetCheckHost = "8.8.8.8";
135
+
136
+ /**
137
+ * @brief Port for Internet connectivity check
138
+ *
139
+ * The port to use when checking connectivity to internetCheckHost.
140
+ * Default: 53 (DNS port)
141
+ */
142
+ uint16_t internetCheckPort = 53;
143
+
144
+ /**
145
+ * @brief Timeout for Internet connectivity check in milliseconds
146
+ *
147
+ * Maximum time to wait for a response from the Internet check host.
148
+ * Default: 5000ms (5 seconds)
149
+ */
150
+ uint32_t internetCheckTimeout = 5000;
151
+
152
+ // ============================================
153
+ // Message Handling
154
+ // ============================================
155
+
156
+ /**
157
+ * @brief Number of retry attempts for message delivery
158
+ *
159
+ * How many times to retry sending a message before considering it failed.
160
+ * Default: 3
161
+ */
162
+ uint8_t messageRetryCount = 3;
163
+
164
+ /**
165
+ * @brief Interval between retry attempts in milliseconds
166
+ *
167
+ * Base delay between message retry attempts.
168
+ * Actual delay may use exponential backoff.
169
+ * Default: 1000ms (1 second)
170
+ */
171
+ uint32_t retryInterval = 1000;
172
+
173
+ /**
174
+ * @brief Timeout for tracking duplicate messages in milliseconds
175
+ *
176
+ * How long to remember message IDs to prevent duplicate processing.
177
+ * Default: 60000ms (60 seconds)
178
+ */
179
+ uint32_t duplicateTrackingTimeout = 60000;
180
+
181
+ /**
182
+ * @brief Maximum number of messages to track for deduplication
183
+ *
184
+ * The maximum number of message IDs to store for duplicate detection.
185
+ * Older entries are removed when this limit is reached.
186
+ * Default: 500
187
+ */
188
+ uint16_t maxTrackedMessages = 500;
189
+
190
+ // ============================================
191
+ // Gateway Coordination
192
+ // ============================================
193
+
194
+ /**
195
+ * @brief Interval for broadcasting gateway heartbeat in milliseconds
196
+ *
197
+ * How often the gateway broadcasts its status to the mesh.
198
+ * Other nodes use this to detect gateway health.
199
+ * Default: 15000ms (15 seconds)
200
+ */
201
+ uint32_t gatewayHeartbeatInterval = 15000;
202
+
203
+ /**
204
+ * @brief Timeout for detecting gateway failure in milliseconds
205
+ *
206
+ * If no heartbeat is received within this period, the gateway is
207
+ * considered failed and election may begin.
208
+ * Should be at least 2x gatewayHeartbeatInterval.
209
+ * Default: 45000ms (45 seconds)
210
+ */
211
+ uint32_t gatewayFailureTimeout = 45000;
212
+
213
+ /**
214
+ * @brief Whether this node should participate in gateway elections
215
+ *
216
+ * When true, this node may become a gateway if the current gateway fails.
217
+ * Requires routerSSID and routerPassword to be set.
218
+ * Default: true
219
+ */
220
+ bool participateInElection = true;
221
+
222
+ // ============================================
223
+ // Advanced Configuration
224
+ // ============================================
225
+
226
+ /**
227
+ * @brief Priority for relayed messages (0 = highest/CRITICAL)
228
+ *
229
+ * The priority level assigned to messages being relayed through the gateway.
230
+ * Lower values = higher priority.
231
+ * Default: 0 (CRITICAL priority)
232
+ */
233
+ uint8_t relayedMessagePriority = 0;
234
+
235
+ /**
236
+ * @brief Whether to maintain a permanent connection to the router
237
+ *
238
+ * When true, the gateway maintains a continuous connection to the router.
239
+ * When false, the gateway may disconnect when idle to save power.
240
+ * Default: true
241
+ */
242
+ bool maintainPermanentConnection = true;
243
+
244
+ // ============================================
245
+ // Validation Methods
246
+ // ============================================
247
+
248
+ /**
249
+ * @brief Validate the configuration
250
+ *
251
+ * Performs comprehensive validation of all configuration fields.
252
+ * Returns a ValidationResult with detailed error information.
253
+ *
254
+ * @return ValidationResult indicating validity and any error messages
255
+ */
256
+ ValidationResult validate() const {
257
+ // If not enabled, configuration is valid (nothing to validate)
258
+ if (!enabled) {
259
+ return ValidationResult(true, "");
260
+ }
261
+
262
+ // Router SSID is required when enabled
263
+ if (routerSSID.length() == 0) {
264
+ return ValidationResult(false, "routerSSID is required when enabled");
265
+ }
266
+
267
+ // SSID length validation (max 32 characters per WiFi spec)
268
+ if (routerSSID.length() > 32) {
269
+ return ValidationResult(false,
270
+ "routerSSID exceeds maximum length of 32 characters");
271
+ }
272
+
273
+ // Password length validation (WPA2 max is 63 characters)
274
+ if (routerPassword.length() > 63) {
275
+ return ValidationResult(
276
+ false, "routerPassword exceeds maximum length of 63 characters");
277
+ }
278
+
279
+ // Internet check host validation
280
+ if (internetCheckHost.length() == 0) {
281
+ return ValidationResult(false, "internetCheckHost cannot be empty");
282
+ }
283
+
284
+ // Interval validations (minimum sensible values)
285
+ if (internetCheckInterval < 1000) {
286
+ return ValidationResult(
287
+ false, "internetCheckInterval must be at least 1000ms");
288
+ }
289
+
290
+ if (internetCheckTimeout < 100) {
291
+ return ValidationResult(false,
292
+ "internetCheckTimeout must be at least 100ms");
293
+ }
294
+
295
+ if (internetCheckTimeout >= internetCheckInterval) {
296
+ return ValidationResult(
297
+ false,
298
+ "internetCheckTimeout must be less than internetCheckInterval");
299
+ }
300
+
301
+ if (gatewayHeartbeatInterval < 1000) {
302
+ return ValidationResult(
303
+ false, "gatewayHeartbeatInterval must be at least 1000ms");
304
+ }
305
+
306
+ if (gatewayFailureTimeout < gatewayHeartbeatInterval * 2) {
307
+ return ValidationResult(
308
+ false,
309
+ "gatewayFailureTimeout should be at least 2x gatewayHeartbeatInterval");
310
+ }
311
+
312
+ if (duplicateTrackingTimeout < 1000) {
313
+ return ValidationResult(
314
+ false, "duplicateTrackingTimeout must be at least 1000ms");
315
+ }
316
+
317
+ if (maxTrackedMessages < 10) {
318
+ return ValidationResult(false, "maxTrackedMessages must be at least 10");
319
+ }
320
+
321
+ if (retryInterval < 100) {
322
+ return ValidationResult(false, "retryInterval must be at least 100ms");
323
+ }
324
+
325
+ return ValidationResult(true, "");
326
+ }
327
+
328
+ /**
329
+ * @brief Check if this node can participate in gateway elections
330
+ *
331
+ * A node can participate if:
332
+ * - participateInElection is true
333
+ * - routerSSID is configured
334
+ *
335
+ * @return true if eligible to become a gateway
336
+ */
337
+ bool canParticipateInElection() const {
338
+ return participateInElection && routerSSID.length() > 0;
339
+ }
340
+
341
+ /**
342
+ * @brief Check if the configuration has valid router credentials
343
+ *
344
+ * @return true if router SSID is set
345
+ */
346
+ bool hasRouterCredentials() const { return routerSSID.length() > 0; }
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
+ };
365
+
366
+ /**
367
+ * @brief Structure to hold Internet connectivity check results
368
+ *
369
+ * Contains detailed information about the last Internet connectivity check,
370
+ * including whether it succeeded, timing information, and error details.
371
+ */
372
+ struct InternetStatus {
373
+ bool available = false; ///< Whether Internet is currently available
374
+ uint32_t lastCheckTime = 0; ///< Timestamp of last check (millis)
375
+ uint32_t lastSuccessTime = 0; ///< Timestamp of last successful check (millis)
376
+ uint32_t checkCount = 0; ///< Total number of checks performed
377
+ uint32_t successCount = 0; ///< Number of successful checks
378
+ uint32_t failureCount = 0; ///< Number of failed checks
379
+ uint32_t lastLatencyMs = 0; ///< Latency of last successful check in ms
380
+ TSTRING lastError = ""; ///< Error message from last failed check
381
+ TSTRING checkHost = ""; ///< Host used for connectivity check
382
+ uint16_t checkPort = 0; ///< Port used for connectivity check
383
+
384
+ /**
385
+ * @brief Get the uptime percentage of Internet connectivity
386
+ * @return Percentage of successful checks (0-100), or 0 if no checks performed
387
+ */
388
+ uint8_t getUptimePercent() const {
389
+ if (checkCount == 0) return 0;
390
+ return static_cast<uint8_t>((successCount * 100) / checkCount);
391
+ }
392
+
393
+ /**
394
+ * @brief Get time since last successful Internet check
395
+ * @return Milliseconds since last success, or UINT32_MAX if never succeeded
396
+ */
397
+ uint32_t getTimeSinceLastSuccess() const {
398
+ if (lastSuccessTime == 0) return UINT32_MAX;
399
+ return millis() - lastSuccessTime;
400
+ }
401
+
402
+ /**
403
+ * @brief Check if Internet status is stale (no recent check)
404
+ * @param maxAgeMs Maximum age in milliseconds (default: 60000)
405
+ * @return true if last check was too long ago
406
+ */
407
+ bool isStale(uint32_t maxAgeMs = 60000) const {
408
+ if (lastCheckTime == 0) return true;
409
+ return (millis() - lastCheckTime) > maxAgeMs;
410
+ }
411
+ };
412
+
413
+ /**
414
+ * @brief Callback type for Internet connectivity change events
415
+ */
416
+ typedef std::function<void(bool available)> InternetChangedCallback_t;
417
+
418
+ /**
419
+ * @brief Internet Health Checker class for periodic connectivity monitoring
420
+ *
421
+ * This class performs periodic TCP connection tests to verify Internet
422
+ * connectivity. It is designed to be non-blocking and work with the
423
+ * TaskScheduler for asynchronous operation.
424
+ *
425
+ * PLATFORM SUPPORT:
426
+ * - ESP32/ESP8266: Uses WiFiClient for actual TCP connections
427
+ * - PC/Test: Mocked connectivity (always fails in test environment)
428
+ *
429
+ * Example usage:
430
+ * @code
431
+ * InternetHealthChecker checker;
432
+ * checker.setConfig(gatewayConfig);
433
+ * checker.onConnectivityChanged([](bool available) {
434
+ * Serial.printf("Internet %s\n", available ? "connected" : "disconnected");
435
+ * });
436
+ * checker.start(scheduler);
437
+ * @endcode
438
+ */
439
+ class InternetHealthChecker {
440
+ public:
441
+ InternetHealthChecker() = default;
442
+
443
+ /**
444
+ * @brief Configure the health checker with gateway settings
445
+ * @param config SharedGatewayConfig with check parameters
446
+ */
447
+ void setConfig(const SharedGatewayConfig& config) {
448
+ checkHost_ = config.internetCheckHost;
449
+ checkPort_ = config.internetCheckPort;
450
+ checkInterval_ = config.internetCheckInterval;
451
+ checkTimeout_ = config.internetCheckTimeout;
452
+ status_.checkHost = checkHost_;
453
+ status_.checkPort = checkPort_;
454
+ }
455
+
456
+ /**
457
+ * @brief Set custom check host and port
458
+ * @param host Host to check (IP address or hostname)
459
+ * @param port Port to connect to (default: 53 for DNS)
460
+ */
461
+ void setCheckTarget(const TSTRING& host, uint16_t port = 53) {
462
+ checkHost_ = host;
463
+ checkPort_ = port;
464
+ status_.checkHost = checkHost_;
465
+ status_.checkPort = checkPort_;
466
+ }
467
+
468
+ /**
469
+ * @brief Set check interval
470
+ * @param intervalMs Interval between checks in milliseconds
471
+ */
472
+ void setCheckInterval(uint32_t intervalMs) {
473
+ checkInterval_ = intervalMs;
474
+ }
475
+
476
+ /**
477
+ * @brief Set check timeout
478
+ * @param timeoutMs Timeout for each check in milliseconds
479
+ */
480
+ void setCheckTimeout(uint32_t timeoutMs) {
481
+ checkTimeout_ = timeoutMs;
482
+ }
483
+
484
+ /**
485
+ * @brief Register callback for connectivity changes
486
+ * @param callback Function to call when connectivity status changes
487
+ */
488
+ void onConnectivityChanged(InternetChangedCallback_t callback) {
489
+ connectivityChangedCallback_ = callback;
490
+ }
491
+
492
+ /**
493
+ * @brief Check if local Internet is currently available
494
+ * @return true if last check succeeded
495
+ */
496
+ bool hasLocalInternet() const {
497
+ return status_.available;
498
+ }
499
+
500
+ /**
501
+ * @brief Get detailed Internet status
502
+ * @return InternetStatus structure with full details
503
+ */
504
+ InternetStatus getStatus() const {
505
+ return status_;
506
+ }
507
+
508
+ /**
509
+ * @brief Perform an immediate Internet connectivity check
510
+ *
511
+ * This method performs a synchronous TCP connection test.
512
+ * On ESP32/ESP8266, it uses WiFiClient.
513
+ * In test environment, connectivity is mocked.
514
+ *
515
+ * @return true if connection succeeded
516
+ */
517
+ bool checkNow() {
518
+ status_.checkCount++;
519
+ status_.lastCheckTime = millis();
520
+
521
+ bool connected = performTcpCheck();
522
+
523
+ if (connected) {
524
+ status_.successCount++;
525
+ status_.lastSuccessTime = millis();
526
+ status_.lastError = "";
527
+ } else {
528
+ status_.failureCount++;
529
+ }
530
+
531
+ // Detect status change and fire callback
532
+ if (connected != status_.available) {
533
+ status_.available = connected;
534
+ if (connectivityChangedCallback_) {
535
+ connectivityChangedCallback_(connected);
536
+ }
537
+ }
538
+
539
+ return connected;
540
+ }
541
+
542
+ /**
543
+ * @brief Get check interval
544
+ * @return Check interval in milliseconds
545
+ */
546
+ uint32_t getCheckInterval() const {
547
+ return checkInterval_;
548
+ }
549
+
550
+ /**
551
+ * @brief Get check timeout
552
+ * @return Check timeout in milliseconds
553
+ */
554
+ uint32_t getCheckTimeout() const {
555
+ return checkTimeout_;
556
+ }
557
+
558
+ /**
559
+ * @brief Get check host
560
+ * @return Host being checked
561
+ */
562
+ TSTRING getCheckHost() const {
563
+ return checkHost_;
564
+ }
565
+
566
+ /**
567
+ * @brief Get check port
568
+ * @return Port being checked
569
+ */
570
+ uint16_t getCheckPort() const {
571
+ return checkPort_;
572
+ }
573
+
574
+ /**
575
+ * @brief Reset all statistics
576
+ */
577
+ void resetStats() {
578
+ status_.checkCount = 0;
579
+ status_.successCount = 0;
580
+ status_.failureCount = 0;
581
+ status_.lastCheckTime = 0;
582
+ status_.lastSuccessTime = 0;
583
+ status_.lastLatencyMs = 0;
584
+ status_.lastError = "";
585
+ }
586
+
587
+ #ifdef PAINLESSMESH_BOOST
588
+ /**
589
+ * @brief Set mock connectivity result (test environment only)
590
+ * @param connected Whether to simulate connected state
591
+ */
592
+ void setMockConnected(bool connected) {
593
+ mockConnected_ = connected;
594
+ }
595
+ #endif
596
+
597
+ private:
598
+ /**
599
+ * @brief Perform the actual TCP connection check
600
+ *
601
+ * Platform-specific implementation:
602
+ * - ESP32/ESP8266: Uses WiFiClient to connect
603
+ * - Test/PC: Returns mock value
604
+ *
605
+ * @return true if connection succeeded
606
+ */
607
+ bool performTcpCheck() {
608
+ #ifdef PAINLESSMESH_BOOST
609
+ // Test environment - use mock value
610
+ if (mockConnected_) {
611
+ status_.lastLatencyMs = 10; // Simulated latency
612
+ return true;
613
+ }
614
+ status_.lastError = "Mock: No Internet in test environment";
615
+ return false;
616
+ #else
617
+ // Arduino/ESP environment - actual TCP check
618
+ // Note: WiFiClient usage is handled in the arduino-specific code
619
+ // This base implementation returns false; override in wifi.hpp
620
+ status_.lastError = "Not implemented in base class";
621
+ return false;
622
+ #endif
623
+ }
624
+
625
+ // Configuration
626
+ TSTRING checkHost_ = "8.8.8.8";
627
+ uint16_t checkPort_ = 53;
628
+ uint32_t checkInterval_ = 30000;
629
+ uint32_t checkTimeout_ = 5000;
630
+
631
+ // State
632
+ InternetStatus status_;
633
+ InternetChangedCallback_t connectivityChangedCallback_;
634
+
635
+ #ifdef PAINLESSMESH_BOOST
636
+ bool mockConnected_ = false;
637
+ #endif
638
+ };
639
+
640
+ /**
641
+ * @brief Priority levels for GatewayDataPackage messages
642
+ *
643
+ * Defines the priority levels for message routing through the gateway.
644
+ * Lower values indicate higher priority.
645
+ *
646
+ * @note Uses PRIORITY_ prefix to avoid conflicts with Arduino macros
647
+ * (HIGH and LOW are defined in esp32-hal-gpio.h)
648
+ */
649
+ enum class GatewayPriority : uint8_t {
650
+ PRIORITY_CRITICAL = 0, ///< Critical messages - immediate processing
651
+ PRIORITY_HIGH = 1, ///< High priority - processed before normal
652
+ PRIORITY_NORMAL = 2, ///< Normal priority - standard processing
653
+ PRIORITY_LOW = 3 ///< Low priority - processed when idle
654
+ };
655
+
656
+ /**
657
+ * @brief Gateway Data Package for routing Internet requests through mesh
658
+ *
659
+ * This package enables mesh nodes to send data through a gateway node to
660
+ * the Internet. It provides a standardized format for:
661
+ * - HTTP requests to external APIs
662
+ * - MQTT message publishing
663
+ * - WebSocket communications
664
+ * - Any other Internet-bound data
665
+ *
666
+ * MEMORY FOOTPRINT
667
+ * ================
668
+ * The GatewayDataPackage structure has an estimated memory footprint of:
669
+ * - Base fields (from SinglePackage): ~20 bytes
670
+ * - Fixed fields (messageId, originNode, timestamp, priority, retryCount,
671
+ * requiresAck): ~18 bytes
672
+ * - TSTRING fields (destination, payload, contentType):
673
+ * - ESP8266/ESP32 String: ~12 bytes overhead per String + content length
674
+ * - PC/Test std::string: ~32 bytes overhead per string + content length
675
+ * - Total estimated minimum: ~74 bytes (ESP) to ~134 bytes (PC/Test)
676
+ * - With typical content: ~200-500 bytes depending on payload size
677
+ *
678
+ * For ESP8266 with ~80KB RAM, keep payload under 1KB for safety.
679
+ * For ESP32 with ~320KB RAM, larger payloads are acceptable.
680
+ *
681
+ * MESSAGE ID GENERATION
682
+ * =====================
683
+ * Use generateMessageId(nodeId) to create unique message IDs.
684
+ * The ID combines a per-node counter with the node ID to ensure
685
+ * uniqueness across the mesh network.
686
+ *
687
+ * Example usage:
688
+ * @code
689
+ * GatewayDataPackage pkg;
690
+ * pkg.messageId = GatewayDataPackage::generateMessageId(mesh.getNodeId());
691
+ * pkg.originNode = mesh.getNodeId();
692
+ * pkg.timestamp = mesh.getNodeTime();
693
+ * pkg.priority = static_cast<uint8_t>(GatewayPriority::PRIORITY_NORMAL);
694
+ * pkg.destination = "https://api.example.com/data";
695
+ * pkg.payload = "{\"sensor\": 42}";
696
+ * pkg.contentType = "application/json";
697
+ * pkg.requiresAck = true;
698
+ *
699
+ * mesh.sendPackage(&pkg);
700
+ * @endcode
701
+ *
702
+ * Type ID: 620 (GATEWAY_DATA)
703
+ * Base class: SinglePackage (routed to specific gateway node)
704
+ */
705
+ class GatewayDataPackage : public plugin::SinglePackage {
706
+ public:
707
+ /**
708
+ * @brief Unique message identifier
709
+ *
710
+ * Generated using generateMessageId() to ensure uniqueness across the mesh.
711
+ * Used for tracking, acknowledgment, and deduplication.
712
+ */
713
+ uint32_t messageId = 0;
714
+
715
+ /**
716
+ * @brief Node ID that originated this message
717
+ *
718
+ * The node that created the message, which may differ from the
719
+ * 'from' field during relay operations.
720
+ */
721
+ uint32_t originNode = 0;
722
+
723
+ /**
724
+ * @brief Creation timestamp
725
+ *
726
+ * Mesh time when the message was created.
727
+ * Used for TTL calculations and ordering.
728
+ */
729
+ uint32_t timestamp = 0;
730
+
731
+ /**
732
+ * @brief Message priority (0=CRITICAL, 1=HIGH, 2=NORMAL, 3=LOW)
733
+ *
734
+ * Determines processing order at the gateway.
735
+ * Use GatewayPriority enum for type-safe values.
736
+ */
737
+ uint8_t priority = static_cast<uint8_t>(GatewayPriority::PRIORITY_NORMAL);
738
+
739
+ /**
740
+ * @brief Destination URL or endpoint
741
+ *
742
+ * The Internet destination for this data. Examples:
743
+ * - "https://api.example.com/sensor"
744
+ * - "mqtt://broker.example.com/topic"
745
+ * - "wss://ws.example.com/stream"
746
+ */
747
+ TSTRING destination = "";
748
+
749
+ /**
750
+ * @brief Application payload data
751
+ *
752
+ * The actual data to send to the destination.
753
+ * Format depends on contentType (JSON, binary, etc.).
754
+ */
755
+ TSTRING payload = "";
756
+
757
+ /**
758
+ * @brief MIME content type
759
+ *
760
+ * Describes the format of the payload. Common values:
761
+ * - "application/json"
762
+ * - "text/plain"
763
+ * - "application/octet-stream"
764
+ */
765
+ TSTRING contentType = "application/json";
766
+
767
+ /**
768
+ * @brief Number of relay attempts
769
+ *
770
+ * Incremented each time the message is relayed.
771
+ * Can be used for hop counting and loop detection.
772
+ */
773
+ uint8_t retryCount = 0;
774
+
775
+ /**
776
+ * @brief Whether acknowledgment is required
777
+ *
778
+ * When true, the gateway should send a response back
779
+ * confirming successful delivery to the Internet destination.
780
+ */
781
+ bool requiresAck = false;
782
+
783
+ /**
784
+ * @brief Number of additional JSON fields in this package
785
+ *
786
+ * Used for jsonObjectSize() calculation in ArduinoJson v6.
787
+ * Count: msgId, origin, ts, prio, dest_url, payload, content, retry, ack = 9 fields
788
+ */
789
+ static constexpr int numPackageFields = 9;
790
+
791
+ /**
792
+ * @brief Default constructor
793
+ *
794
+ * Creates a GatewayDataPackage with type ID 620 (GATEWAY_DATA).
795
+ */
796
+ GatewayDataPackage() : SinglePackage(protocol::GATEWAY_DATA) {}
797
+
798
+ /**
799
+ * @brief Construct from JSON object
800
+ *
801
+ * Deserializes a GatewayDataPackage from a JSON object.
802
+ * Compatible with ArduinoJson v6 and v7.
803
+ *
804
+ * @param jsonObj JSON object containing package data
805
+ */
806
+ GatewayDataPackage(JsonObject jsonObj) : SinglePackage(jsonObj) {
807
+ messageId = jsonObj["msgId"];
808
+ originNode = jsonObj["origin"];
809
+ timestamp = jsonObj["ts"];
810
+ priority = jsonObj["prio"];
811
+ retryCount = jsonObj["retry"];
812
+ requiresAck = jsonObj["ack"] | false;
813
+
814
+ #if ARDUINOJSON_VERSION_MAJOR < 7
815
+ if (jsonObj.containsKey("dest_url"))
816
+ destination = jsonObj["dest_url"].as<TSTRING>();
817
+ if (jsonObj.containsKey("payload"))
818
+ payload = jsonObj["payload"].as<TSTRING>();
819
+ if (jsonObj.containsKey("content"))
820
+ contentType = jsonObj["content"].as<TSTRING>();
821
+ #else
822
+ if (jsonObj["dest_url"].is<TSTRING>())
823
+ destination = jsonObj["dest_url"].as<TSTRING>();
824
+ if (jsonObj["payload"].is<TSTRING>())
825
+ payload = jsonObj["payload"].as<TSTRING>();
826
+ if (jsonObj["content"].is<TSTRING>())
827
+ contentType = jsonObj["content"].as<TSTRING>();
828
+ #endif
829
+ }
830
+
831
+ /**
832
+ * @brief Serialize to JSON object
833
+ *
834
+ * Adds all package fields to the provided JSON object.
835
+ *
836
+ * @param jsonObj JSON object to add fields to
837
+ * @return The modified JSON object
838
+ */
839
+ JsonObject addTo(JsonObject&& jsonObj) const {
840
+ jsonObj = SinglePackage::addTo(std::move(jsonObj));
841
+ jsonObj["msgId"] = messageId;
842
+ jsonObj["origin"] = originNode;
843
+ jsonObj["ts"] = timestamp;
844
+ jsonObj["prio"] = priority;
845
+ jsonObj["dest_url"] = destination;
846
+ jsonObj["payload"] = payload;
847
+ jsonObj["content"] = contentType;
848
+ jsonObj["retry"] = retryCount;
849
+ jsonObj["ack"] = requiresAck;
850
+ return jsonObj;
851
+ }
852
+
853
+ #if ARDUINOJSON_VERSION_MAJOR < 7
854
+ /**
855
+ * @brief Calculate JSON object size for ArduinoJson v6
856
+ *
857
+ * Used for buffer allocation when serializing.
858
+ *
859
+ * @return Estimated size in bytes
860
+ */
861
+ size_t jsonObjectSize() const {
862
+ // noJsonFields (from base class) + numPackageFields (our fields)
863
+ return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + destination.length() +
864
+ payload.length() + contentType.length();
865
+ }
866
+ #endif
867
+
868
+ /**
869
+ * @brief Generate a unique message ID
870
+ *
871
+ * Creates a unique message ID by combining a per-node counter
872
+ * with the node ID. This ensures uniqueness across the mesh
873
+ * even if multiple nodes generate IDs simultaneously.
874
+ *
875
+ * The ID format is:
876
+ * - Upper 16 bits: Lower 16 bits of node ID
877
+ * - Lower 16 bits: Incrementing counter (wraps at 65535)
878
+ *
879
+ * @note This function is not thread-safe. On ESP8266/ESP32, this is
880
+ * acceptable as the main loop is single-threaded. For multi-threaded
881
+ * environments, consider using atomic operations.
882
+ *
883
+ * @param nodeId The ID of the node generating the message
884
+ * @return A unique message ID
885
+ */
886
+ static uint32_t generateMessageId(uint32_t nodeId) {
887
+ static uint16_t counter = 0;
888
+ ++counter;
889
+ // Combine node ID (upper 16 bits) with counter (lower 16 bits)
890
+ return ((nodeId & 0xFFFF) << 16) | counter;
891
+ }
892
+
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
+ };
909
+
910
+ /**
911
+ * @brief Gateway Acknowledgment Package for delivery confirmations
912
+ *
913
+ * This package is sent from the gateway back to the origin node to confirm
914
+ * delivery status of a GatewayDataPackage. It provides feedback on whether
915
+ * the data was successfully delivered to the Internet destination.
916
+ *
917
+ * MEMORY FOOTPRINT
918
+ * ================
919
+ * The GatewayAckPackage structure has an estimated memory footprint of:
920
+ * - Base fields (from SinglePackage): ~20 bytes
921
+ * - Fixed fields (messageId, originNode, success, httpStatus, timestamp): ~14 bytes
922
+ * - TSTRING field (error):
923
+ * - ESP8266/ESP32 String: ~12 bytes overhead + content length
924
+ * - PC/Test std::string: ~32 bytes overhead + content length
925
+ * - Total estimated minimum: ~46 bytes (ESP) to ~66 bytes (PC/Test)
926
+ * - With typical error message: ~100-200 bytes
927
+ *
928
+ * For ESP8266 with ~80KB RAM, this represents <0.3% of available memory.
929
+ * For ESP32 with ~320KB RAM, this represents <0.1% of available memory.
930
+ *
931
+ * Example usage:
932
+ * @code
933
+ * // Gateway responding to a successful delivery
934
+ * GatewayAckPackage ack;
935
+ * ack.messageId = originalPackage.messageId;
936
+ * ack.originNode = originalPackage.originNode;
937
+ * ack.dest = originalPackage.originNode; // Route back to origin
938
+ * ack.from = mesh.getNodeId();
939
+ * ack.success = true;
940
+ * ack.httpStatus = 200;
941
+ * ack.timestamp = mesh.getNodeTime();
942
+ *
943
+ * mesh.sendPackage(&ack);
944
+ *
945
+ * // Gateway responding to a failed delivery
946
+ * GatewayAckPackage ack;
947
+ * ack.messageId = originalPackage.messageId;
948
+ * ack.originNode = originalPackage.originNode;
949
+ * ack.dest = originalPackage.originNode;
950
+ * ack.from = mesh.getNodeId();
951
+ * ack.success = false;
952
+ * ack.httpStatus = 503;
953
+ * ack.error = "Service unavailable";
954
+ * ack.timestamp = mesh.getNodeTime();
955
+ *
956
+ * mesh.sendPackage(&ack);
957
+ * @endcode
958
+ *
959
+ * Type ID: 621 (GATEWAY_ACK)
960
+ * Base class: SinglePackage (routed back to origin node)
961
+ */
962
+ class GatewayAckPackage : public plugin::SinglePackage {
963
+ public:
964
+ /**
965
+ * @brief Original message ID being acknowledged
966
+ *
967
+ * The messageId from the GatewayDataPackage that this acknowledgment
968
+ * corresponds to. Used for correlation at the origin node.
969
+ */
970
+ uint32_t messageId = 0;
971
+
972
+ /**
973
+ * @brief Original sender node ID
974
+ *
975
+ * The node ID that originally sent the GatewayDataPackage.
976
+ * Used for routing and correlation.
977
+ */
978
+ uint32_t originNode = 0;
979
+
980
+ /**
981
+ * @brief Delivery success status
982
+ *
983
+ * True if the message was successfully delivered to the Internet
984
+ * destination, false otherwise.
985
+ */
986
+ bool success = false;
987
+
988
+ /**
989
+ * @brief HTTP response code (if applicable)
990
+ *
991
+ * The HTTP status code received from the Internet destination.
992
+ * Examples: 200 (OK), 404 (Not Found), 500 (Server Error).
993
+ * Set to 0 if not applicable (e.g., connection failure).
994
+ */
995
+ uint16_t httpStatus = 0;
996
+
997
+ /**
998
+ * @brief Error message (if failed)
999
+ *
1000
+ * A human-readable error message describing why delivery failed.
1001
+ * Empty string if success is true.
1002
+ */
1003
+ TSTRING error = "";
1004
+
1005
+ /**
1006
+ * @brief Acknowledgment timestamp
1007
+ *
1008
+ * Mesh time when the acknowledgment was created.
1009
+ * Can be used to calculate round-trip time.
1010
+ */
1011
+ uint32_t timestamp = 0;
1012
+
1013
+ /**
1014
+ * @brief Number of additional JSON fields in this package
1015
+ *
1016
+ * Used for jsonObjectSize() calculation in ArduinoJson v6.
1017
+ * Count: msgId, origin, success, http, err, ts = 6 fields
1018
+ */
1019
+ static constexpr int numPackageFields = 6;
1020
+
1021
+ /**
1022
+ * @brief Default constructor
1023
+ *
1024
+ * Creates a GatewayAckPackage with type ID 621 (GATEWAY_ACK).
1025
+ */
1026
+ GatewayAckPackage() : SinglePackage(protocol::GATEWAY_ACK) {}
1027
+
1028
+ /**
1029
+ * @brief Construct from JSON object
1030
+ *
1031
+ * Deserializes a GatewayAckPackage from a JSON object.
1032
+ * Compatible with ArduinoJson v6 and v7.
1033
+ *
1034
+ * @param jsonObj JSON object containing package data
1035
+ */
1036
+ GatewayAckPackage(JsonObject jsonObj) : SinglePackage(jsonObj) {
1037
+ messageId = jsonObj["msgId"];
1038
+ originNode = jsonObj["origin"];
1039
+ success = jsonObj["success"] | false;
1040
+ httpStatus = jsonObj["http"];
1041
+ timestamp = jsonObj["ts"];
1042
+
1043
+ #if ARDUINOJSON_VERSION_MAJOR < 7
1044
+ if (jsonObj.containsKey("err"))
1045
+ error = jsonObj["err"].as<TSTRING>();
1046
+ #else
1047
+ if (jsonObj["err"].is<TSTRING>())
1048
+ error = jsonObj["err"].as<TSTRING>();
1049
+ #endif
1050
+ }
1051
+
1052
+ /**
1053
+ * @brief Serialize to JSON object
1054
+ *
1055
+ * Adds all package fields to the provided JSON object.
1056
+ *
1057
+ * @param jsonObj JSON object to add fields to
1058
+ * @return The modified JSON object
1059
+ */
1060
+ JsonObject addTo(JsonObject&& jsonObj) const {
1061
+ jsonObj = SinglePackage::addTo(std::move(jsonObj));
1062
+ jsonObj["msgId"] = messageId;
1063
+ jsonObj["origin"] = originNode;
1064
+ jsonObj["success"] = success;
1065
+ jsonObj["http"] = httpStatus;
1066
+ jsonObj["err"] = error;
1067
+ jsonObj["ts"] = timestamp;
1068
+ return jsonObj;
1069
+ }
1070
+
1071
+ #if ARDUINOJSON_VERSION_MAJOR < 7
1072
+ /**
1073
+ * @brief Calculate JSON object size for ArduinoJson v6
1074
+ *
1075
+ * Used for buffer allocation when serializing.
1076
+ *
1077
+ * @return Estimated size in bytes
1078
+ */
1079
+ size_t jsonObjectSize() const {
1080
+ // noJsonFields (from base class) + numPackageFields (our fields)
1081
+ return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + error.length();
1082
+ }
1083
+ #endif
1084
+
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
+ };
1903
+
1904
+ } // 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
+ } // namespace painlessmesh
2119
+
2120
+ #endif // _PAINLESS_MESH_GATEWAY_HPP_