@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.
- package/BRIDGE_TO_INTERNET.md +229 -0
- package/CHANGELOG.md +89 -1
- package/CONTRIBUTING.md +79 -0
- package/README.md +70 -143
- package/docs/README.md +1 -0
- package/docs/api/shared-gateway.md +1207 -0
- package/docs/troubleshooting/common-issues.md +28 -0
- package/docs/troubleshooting/faq.md +113 -12
- package/examples/basic/test/simulator/CMakeLists.txt +40 -0
- package/examples/basic/test/simulator/README.md +149 -0
- package/examples/basic/test/simulator/firmware/basic_firmware.hpp +117 -0
- package/examples/basic/test/simulator/scenarios/basic_mesh_test.yaml +81 -0
- package/examples/bridge/bridge.ino +17 -4
- package/examples/bridge_failover/README.md +81 -0
- package/examples/bridge_failover/bridge_failover.ino +51 -6
- package/examples/sharedGateway/README.md +235 -0
- package/examples/{meshCommandNode → sharedGateway}/platformio.ini +3 -2
- package/examples/sharedGateway/sharedGateway.ino +303 -0
- package/library.json +3 -22
- package/library.properties +1 -1
- package/package.json +3 -3
- package/src/arduino/wifi.hpp +380 -13
- package/src/painlessmesh/gateway.hpp +2120 -0
- package/src/painlessmesh/mesh.hpp +1034 -6
- package/src/painlessmesh/message_tracker.hpp +311 -0
- package/src/painlessmesh/protocol.hpp +6 -0
- package/DOCUMENTATION_INDEX.md +0 -146
- package/docs/API_DESIGN_GUIDELINES.md +0 -414
- package/docs/ARDUINO_LIBRARY_MANAGER_SUBMISSION.md +0 -331
- package/docs/BOOLEAN_NAMING_CONVENTION.md +0 -235
- package/docs/BRIDGE_FAILOVER.md +0 -512
- package/docs/BRIDGE_HEALTH_MONITORING.md +0 -293
- package/docs/CHANNEL_SYNCHRONIZATION.md +0 -209
- package/docs/CREATE_MISSING_RELEASES.md +0 -321
- package/docs/DOCUMENTATION_MIGRATION_PLAN.md +0 -176
- package/docs/FAQ_VERSION_NUMBERS.md +0 -152
- package/docs/IMPLEMENTATION_PLAN_MESH_TOPOLOGY.md +0 -1062
- package/docs/MESH_TOPOLOGY_GUIDE.md +0 -992
- package/docs/MESH_TOPOLOGY_PROGRESS.md +0 -422
- package/docs/MQTT_BRIDGE_COMMANDS.md +0 -894
- package/docs/MQTT_BRIDGE_IMPLEMENTATION_SUMMARY.md +0 -324
- package/docs/MQTT_COMMAND_SCHEMA_PROPOSAL.md +0 -576
- package/docs/MQTT_SCHEMA_COMPLIANCE.md +0 -340
- package/docs/MQTT_SCHEMA_PROPOSALS.md +0 -446
- package/docs/MQTT_SCHEMA_REVIEW.md +0 -690
- package/docs/OTA_COMMANDS_REFERENCE.md +0 -554
- package/docs/PHASE1_GUIDE.md +0 -349
- package/docs/PHASE2_GUIDE.md +0 -543
- package/docs/QUICK_REFERENCE_VERSIONING.md +0 -127
- package/docs/RELEASE_AGENT_SUMMARY.md +0 -386
- package/docs/SCHEMA_VALIDATION_CHECKLIST.md +0 -222
- package/docs/VERSION_MANAGEMENT.md +0 -213
- package/docs/archive/DOCUSAURUS_DEPLOYMENT.md +0 -166
- package/docs/archive/FEATURE_PROPOSALS.md +0 -337
- package/docs/archive/LIBRARY_JSON_FIX.md +0 -98
- package/docs/archive/LIBRARY_STRUCTURE_FIX.md +0 -215
- package/docs/archive/PHASE1_IMPLEMENTATION.md +0 -325
- package/docs/archive/PHASE2_IMPLEMENTATION.md +0 -567
- package/docs/archive/RELEASE_SUMMARY.md +0 -173
- package/docs/archive/SCONS_BUILD_FIX.md +0 -313
- package/docs/archive/TRIGGER_RELEASE.md +0 -280
- package/docs/archive/VECTOR_INCLUDE_FIX.md +0 -129
- package/docs/archive/ota-and-status-enhancements.md +0 -911
- package/docs/archive/ota-status-architecture-diagrams.md +0 -658
- package/docs/archive/ota-status-quick-reference.md +0 -284
- package/docs/design/.gitkeep +0 -1
- package/docs/design/STATION_CREDENTIALS_DESIGN.md +0 -182
- package/docs/development/ARDUINO_COMPLIANCE_SUMMARY.md +0 -71
- package/docs/development/CODE_REFACTORING_RECOMMENDATIONS.md +0 -1011
- package/docs/development/DOCKER_TESTING.md +0 -196
- package/docs/development/PLATFORMIO_USAGE.md +0 -180
- package/docs/development/TESTING_SUMMARY.md +0 -126
- package/docs/development/contributing.md +0 -301
- package/docs/development/documentation.md +0 -583
- package/docs/features/DIAGNOSTICS_API.md +0 -534
- package/docs/implementation/BRIDGE_ARCHITECTURE_IMPLEMENTATION.md +0 -340
- package/docs/implementation/BRIDGE_HEALTH_MONITORING_IMPLEMENTATION.md +0 -213
- package/docs/implementation/BRIDGE_STATUS_FEATURE.md +0 -635
- package/docs/implementation/DIAGNOSTICS_API_IMPLEMENTATION.md +0 -232
- package/docs/implementation/IMPLEMENTATION_COMPLETE.md +0 -228
- package/docs/implementation/IMPLEMENTATION_NTP_TIME_SYNC.md +0 -325
- package/docs/implementation/IMPLEMENTATION_SUMMARY.md +0 -316
- package/docs/implementation/MESSAGE_QUEUE_IMPLEMENTATION.md +0 -405
- package/docs/implementation/MULTI_BRIDGE_IMPLEMENTATION.md +0 -520
- package/docs/implementation/NTP_TIME_SYNC_FEATURE.md +0 -392
- package/docs/improvements/FUTURE_PROPOSALS.md +0 -1016
- package/docs/improvements/IMPLEMENTATION_HISTORY.md +0 -1091
- package/docs/improvements/OTA_STATUS_ENHANCEMENTS.md +0 -709
- package/docs/improvements/README.md +0 -212
- package/docs/internal/CUSTOM_AGENT_ANALYSIS.md +0 -391
- package/docs/internal/ISSUE_65_VERIFICATION.md +0 -947
- package/docs/internal/ISSUE_66_CLOSURE.md +0 -249
- package/docs/internal/ISSUE_66_STATUS.md +0 -316
- package/docs/internal/PR_SUMMARY.md +0 -315
- package/docs/internal/REVIEW_SUMMARY.md +0 -332
- package/docs/multi-bridge-setup.md +0 -1025
- package/docs/platformio-publishing.md +0 -255
- package/docs/platformio-setup-summary.md +0 -121
- package/docs/releases/ANNOUNCEMENT_v1.8.6.md +0 -63
- package/docs/releases/ANNOUNCEMENT_v1.8.7.md +0 -113
- package/docs/releases/BRIDGE_STATUS_SELF_REGISTRATION_FIX.md +0 -221
- package/docs/releases/FEATURE_HISTORY.md +0 -543
- package/docs/releases/GITHUB_RELEASE_v1.8.6.md +0 -71
- package/docs/releases/GITHUB_RELEASE_v1.8.7.md +0 -90
- package/docs/releases/PATCH_v1.7.2.md +0 -262
- package/docs/releases/PATCH_v1.7.3.md +0 -262
- package/docs/releases/PATCH_v1.7.4.md +0 -219
- package/docs/releases/PHASE1_SUMMARY.md +0 -246
- package/docs/releases/PHASE2_SUMMARY.md +0 -499
- package/docs/releases/PUBLISH_v1.8.0_INSTRUCTIONS.md +0 -163
- package/docs/releases/QUICK_START_RELEASES.md +0 -113
- package/docs/releases/RELEASE_CHECKLIST_1.8.10.md +0 -331
- package/docs/releases/RELEASE_CHECKLIST_1.8.9.md +0 -207
- package/docs/releases/RELEASE_CHECKLIST_v1.7.4.md +0 -253
- package/docs/releases/RELEASE_CHECKLIST_v1.7.5.md +0 -315
- package/docs/releases/RELEASE_CHECKLIST_v1.7.6.md +0 -389
- package/docs/releases/RELEASE_CHECKLIST_v1.8.0.md +0 -331
- package/docs/releases/RELEASE_CHECKLIST_v1.8.2.md +0 -309
- package/docs/releases/RELEASE_NOTES_1.7.0.md +0 -539
- package/docs/releases/RELEASE_NOTES_1.8.10.md +0 -239
- package/docs/releases/RELEASE_NOTES_1.8.9.md +0 -213
- package/docs/releases/RELEASE_NOTES_v1.8.0.md +0 -685
- package/docs/releases/RELEASE_NOTES_v1.8.1.md +0 -221
- package/docs/releases/RELEASE_NOTES_v1.8.2.md +0 -421
- package/docs/releases/RELEASE_NOTES_v1.8.3.md +0 -292
- package/docs/releases/RELEASE_NOTES_v1.8.4.md +0 -277
- package/docs/releases/RELEASE_NOTES_v1.8.6.md +0 -205
- package/docs/releases/RELEASE_NOTES_v1.8.7.md +0 -184
- package/docs/releases/RELEASE_PLAN_v1.7.6.md +0 -816
- package/docs/releases/RELEASE_SUMMARY_1.8.10.md +0 -193
- package/docs/releases/RELEASE_SUMMARY_v1.7.4.md +0 -276
- package/docs/releases/RELEASE_SUMMARY_v1.7.5.md +0 -322
- package/docs/releases/RELEASE_SUMMARY_v1.7.6.md +0 -436
- package/docs/releases/RELEASE_SUMMARY_v1.7.7.md +0 -391
- package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +0 -523
- package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +0 -542
- package/docs/troubleshooting/ARDUINO_IDE_VERSION_FIX_SUMMARY.md +0 -229
- package/docs/troubleshooting/ARDUINO_LIBRARY_NAME_FIX.md +0 -197
- package/docs/troubleshooting/CRASH_QUICK_REF.md +0 -93
- package/docs/troubleshooting/ESP32_C6_COMPATIBILITY.md +0 -157
- package/docs/troubleshooting/FREERTOS_ASSERTION_FAILURE.md +0 -288
- package/docs/troubleshooting/FREERTOS_FIX_IMPLEMENTATION.md +0 -267
- package/docs/troubleshooting/NPM_PUBLISHING_ISSUE_SUMMARY.md +0 -110
- package/docs/troubleshooting/PAINLESSMESH_V1.7.4_COMPILATION_ISSUES.md +0 -547
- package/docs/troubleshooting/QUICK_FIX_FREERTOS.md +0 -164
- package/docs/troubleshooting/SENSOR_NODE_CONNECTION_CRASH.md +0 -264
- package/docs/troubleshooting/station-reconnection-issues.md +0 -172
- package/docs/v1.7.7_MQTT_IMPROVEMENTS.md +0 -794
- package/docs/wiki/API-Reference.md +0 -246
- package/docs/wiki/Complete-Documentation.md +0 -123
- package/examples/alteriomImproved/alteriom_sensor_package.hpp +0 -224
- package/examples/alteriomImproved/improved_sensor_node.ino +0 -248
- package/examples/alteriomImproved/platformio.ini +0 -32
- package/examples/alteriomMetricsHealth/alteriom_sensor_package.hpp +0 -796
- package/examples/alteriomMetricsHealth/metrics_health_node.ino +0 -429
- package/examples/alteriomMetricsHealth/platformio.ini +0 -26
- package/examples/alteriomPhase1/alteriom_sensor_package.hpp +0 -224
- package/examples/alteriomPhase1/phase1_features.ino +0 -242
- package/examples/alteriomPhase1/platformio.ini +0 -26
- package/examples/alteriomPhase2/alteriom_sensor_package.hpp +0 -224
- package/examples/alteriomPhase2/phase2_features.ino +0 -186
- package/examples/alteriomPhase2/platformio.ini +0 -26
- package/examples/alteriomSensorNode/alteriom_sensor_node.ino +0 -186
- package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +0 -1227
- package/examples/alteriomSensorNode/platformio.ini +0 -26
- package/examples/bridge/alteriom_sensor_package.hpp +0 -1170
- package/examples/bridge/bridge_health_monitoring_example.ino +0 -188
- package/examples/bridge/enhanced_mqtt_bridge.hpp +0 -610
- package/examples/bridge/enhanced_mqtt_bridge_example.ino +0 -226
- package/examples/bridge/mesh_event_publisher.hpp +0 -253
- package/examples/bridge/mesh_topology_reporter.hpp +0 -303
- package/examples/bridge/mqtt_command_bridge.hpp +0 -459
- package/examples/bridge/mqtt_status_bridge.hpp +0 -519
- package/examples/bridgeAwareSensorNode/alteriom_sensor_package.hpp +0 -1227
- package/examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino +0 -342
- package/examples/bridgeAwareSensorNode/platformio.ini +0 -26
- package/examples/diagnosticsExample/diagnosticsExample.ino +0 -171
- package/examples/diagnosticsExample/platformio.ini +0 -26
- package/examples/echoNode/echoNode.ino +0 -33
- package/examples/echoNode/platformio.ini +0 -26
- package/examples/meshCommandNode/alteriom_sensor_package.hpp +0 -235
- package/examples/meshCommandNode/meshCommandNode.ino +0 -265
- package/examples/mqttCommandBridge/alteriom_sensor_package.hpp +0 -235
- package/examples/mqttCommandBridge/mesh_event_publisher.hpp +0 -253
- package/examples/mqttCommandBridge/mesh_topology_reporter.hpp +0 -303
- package/examples/mqttCommandBridge/mqttCommandBridge.ino +0 -254
- package/examples/mqttCommandBridge/mqtt_command_bridge.hpp +0 -462
- package/examples/mqttCommandBridge/platformio.ini +0 -27
- package/examples/mqttStatusBridge/mqttStatusBridge.ino +0 -218
- package/examples/mqttStatusBridge/mqtt_status_bridge.hpp +0 -522
- package/examples/mqttStatusBridge/platformio.ini +0 -27
- package/examples/mqttTopologyTest/README.md +0 -467
- package/examples/mqttTopologyTest/mqttTopologyTest.ino +0 -754
- package/examples/mqttTopologyTest/platformio.ini +0 -27
- package/examples/multi_bridge/README.md +0 -346
- package/examples/multi_bridge/primary_bridge.ino +0 -96
- package/examples/multi_bridge/regular_node.ino +0 -141
- package/examples/multi_bridge/secondary_bridge.ino +0 -111
- package/examples/ntpTimeSyncBridge/alteriom_sensor_package.hpp +0 -1383
- package/examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino +0 -86
- package/examples/ntpTimeSyncNode/alteriom_sensor_package.hpp +0 -1383
- package/examples/ntpTimeSyncNode/ntpTimeSyncNode.ino +0 -109
- package/examples/queued_alarms/README.md +0 -390
- package/examples/queued_alarms/queued_alarms.ino +0 -265
- package/examples/routing_demo/README.md +0 -172
- package/examples/routing_demo/routing_demo.ino +0 -102
- package/examples/rtcIntegration/README.md +0 -294
- 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_
|