@alteriom/painlessmesh 1.8.15 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/BRIDGE_TO_INTERNET.md +229 -0
  2. package/CHANGELOG.md +101 -1
  3. package/CONTRIBUTING.md +79 -0
  4. package/README.md +69 -144
  5. package/docs/README.md +1 -0
  6. package/docs/api/shared-gateway.md +1207 -0
  7. package/examples/bridge_failover/README.md +113 -0
  8. package/examples/bridge_failover/bridge_failover.ino +38 -4
  9. package/examples/sharedGateway/README.md +235 -0
  10. package/examples/{meshCommandNode → sharedGateway}/platformio.ini +3 -2
  11. package/examples/sharedGateway/sharedGateway.ino +303 -0
  12. package/library.json +3 -22
  13. package/library.properties +1 -1
  14. package/package.json +3 -6
  15. package/src/arduino/wifi.hpp +504 -4
  16. package/src/painlessmesh/gateway.hpp +2120 -0
  17. package/src/painlessmesh/mesh.hpp +1034 -6
  18. package/src/painlessmesh/message_tracker.hpp +311 -0
  19. package/src/painlessmesh/protocol.hpp +6 -0
  20. package/DOCUMENTATION_INDEX.md +0 -146
  21. package/RELEASE_NOTES_1.8.15.md +0 -160
  22. package/RELEASE_READINESS_PLAN.md +0 -323
  23. package/TESTING_WITH_SIMULATOR.md +0 -259
  24. package/docs/API_DESIGN_GUIDELINES.md +0 -414
  25. package/docs/ARDUINO_LIBRARY_MANAGER_SUBMISSION.md +0 -331
  26. package/docs/BOOLEAN_NAMING_CONVENTION.md +0 -235
  27. package/docs/BRIDGE_FAILOVER.md +0 -512
  28. package/docs/BRIDGE_HEALTH_MONITORING.md +0 -293
  29. package/docs/BRIDGE_INITIALIZATION_FALLBACK.md +0 -357
  30. package/docs/CHANNEL_SYNCHRONIZATION.md +0 -209
  31. package/docs/CREATE_MISSING_RELEASES.md +0 -321
  32. package/docs/DOCUMENTATION_MIGRATION_PLAN.md +0 -176
  33. package/docs/FAQ_VERSION_NUMBERS.md +0 -152
  34. package/docs/IMPLEMENTATION_PLAN_MESH_TOPOLOGY.md +0 -1062
  35. package/docs/MESH_TOPOLOGY_GUIDE.md +0 -992
  36. package/docs/MESH_TOPOLOGY_PROGRESS.md +0 -422
  37. package/docs/MQTT_BRIDGE_COMMANDS.md +0 -894
  38. package/docs/MQTT_BRIDGE_IMPLEMENTATION_SUMMARY.md +0 -324
  39. package/docs/MQTT_COMMAND_SCHEMA_PROPOSAL.md +0 -576
  40. package/docs/MQTT_SCHEMA_COMPLIANCE.md +0 -340
  41. package/docs/MQTT_SCHEMA_PROPOSALS.md +0 -446
  42. package/docs/MQTT_SCHEMA_REVIEW.md +0 -690
  43. package/docs/OTA_COMMANDS_REFERENCE.md +0 -554
  44. package/docs/PHASE1_GUIDE.md +0 -349
  45. package/docs/PHASE2_GUIDE.md +0 -543
  46. package/docs/QUICK_REFERENCE_VERSIONING.md +0 -127
  47. package/docs/RELEASE_AGENT_SUMMARY.md +0 -386
  48. package/docs/SCHEMA_VALIDATION_CHECKLIST.md +0 -222
  49. package/docs/SIMULATOR_TESTING.md +0 -408
  50. package/docs/VERSION_MANAGEMENT.md +0 -213
  51. package/docs/archive/DOCUSAURUS_DEPLOYMENT.md +0 -166
  52. package/docs/archive/FEATURE_PROPOSALS.md +0 -337
  53. package/docs/archive/LIBRARY_JSON_FIX.md +0 -98
  54. package/docs/archive/LIBRARY_STRUCTURE_FIX.md +0 -215
  55. package/docs/archive/PHASE1_IMPLEMENTATION.md +0 -325
  56. package/docs/archive/PHASE2_IMPLEMENTATION.md +0 -567
  57. package/docs/archive/RELEASE_SUMMARY.md +0 -173
  58. package/docs/archive/SCONS_BUILD_FIX.md +0 -313
  59. package/docs/archive/TRIGGER_RELEASE.md +0 -280
  60. package/docs/archive/VECTOR_INCLUDE_FIX.md +0 -129
  61. package/docs/archive/ota-and-status-enhancements.md +0 -911
  62. package/docs/archive/ota-status-architecture-diagrams.md +0 -658
  63. package/docs/archive/ota-status-quick-reference.md +0 -284
  64. package/docs/design/.gitkeep +0 -1
  65. package/docs/design/STATION_CREDENTIALS_DESIGN.md +0 -182
  66. package/docs/development/ARDUINO_COMPLIANCE_SUMMARY.md +0 -71
  67. package/docs/development/CODE_REFACTORING_RECOMMENDATIONS.md +0 -1011
  68. package/docs/development/DOCKER_TESTING.md +0 -196
  69. package/docs/development/PLATFORMIO_USAGE.md +0 -180
  70. package/docs/development/TESTING_SUMMARY.md +0 -126
  71. package/docs/development/contributing.md +0 -301
  72. package/docs/development/documentation.md +0 -583
  73. package/docs/features/DIAGNOSTICS_API.md +0 -534
  74. package/docs/implementation/BRIDGE_ARCHITECTURE_IMPLEMENTATION.md +0 -340
  75. package/docs/implementation/BRIDGE_HEALTH_MONITORING_IMPLEMENTATION.md +0 -213
  76. package/docs/implementation/BRIDGE_STATUS_FEATURE.md +0 -635
  77. package/docs/implementation/DIAGNOSTICS_API_IMPLEMENTATION.md +0 -232
  78. package/docs/implementation/IMPLEMENTATION_COMPLETE.md +0 -228
  79. package/docs/implementation/IMPLEMENTATION_NTP_TIME_SYNC.md +0 -325
  80. package/docs/implementation/IMPLEMENTATION_SUMMARY.md +0 -316
  81. package/docs/implementation/MESSAGE_QUEUE_IMPLEMENTATION.md +0 -405
  82. package/docs/implementation/MULTI_BRIDGE_IMPLEMENTATION.md +0 -520
  83. package/docs/implementation/NTP_TIME_SYNC_FEATURE.md +0 -392
  84. package/docs/improvements/FUTURE_PROPOSALS.md +0 -1016
  85. package/docs/improvements/IMPLEMENTATION_HISTORY.md +0 -1091
  86. package/docs/improvements/OTA_STATUS_ENHANCEMENTS.md +0 -709
  87. package/docs/improvements/README.md +0 -212
  88. package/docs/internal/CUSTOM_AGENT_ANALYSIS.md +0 -391
  89. package/docs/internal/ISSUE_65_VERIFICATION.md +0 -947
  90. package/docs/internal/ISSUE_66_CLOSURE.md +0 -249
  91. package/docs/internal/ISSUE_66_STATUS.md +0 -316
  92. package/docs/internal/PR_SUMMARY.md +0 -315
  93. package/docs/internal/REVIEW_SUMMARY.md +0 -332
  94. package/docs/multi-bridge-setup.md +0 -1025
  95. package/docs/platformio-publishing.md +0 -255
  96. package/docs/platformio-setup-summary.md +0 -121
  97. package/docs/releases/ANNOUNCEMENT_v1.8.6.md +0 -63
  98. package/docs/releases/ANNOUNCEMENT_v1.8.7.md +0 -113
  99. package/docs/releases/BRIDGE_STATUS_SELF_REGISTRATION_FIX.md +0 -221
  100. package/docs/releases/FEATURE_HISTORY.md +0 -543
  101. package/docs/releases/GITHUB_RELEASE_v1.8.6.md +0 -71
  102. package/docs/releases/GITHUB_RELEASE_v1.8.7.md +0 -90
  103. package/docs/releases/PATCH_v1.7.2.md +0 -262
  104. package/docs/releases/PATCH_v1.7.3.md +0 -262
  105. package/docs/releases/PATCH_v1.7.4.md +0 -219
  106. package/docs/releases/PHASE1_SUMMARY.md +0 -246
  107. package/docs/releases/PHASE2_SUMMARY.md +0 -499
  108. package/docs/releases/PUBLISH_v1.8.0_INSTRUCTIONS.md +0 -163
  109. package/docs/releases/QUICK_START_RELEASES.md +0 -113
  110. package/docs/releases/RELEASE_CHECKLIST_1.8.10.md +0 -331
  111. package/docs/releases/RELEASE_CHECKLIST_1.8.9.md +0 -207
  112. package/docs/releases/RELEASE_CHECKLIST_v1.7.4.md +0 -253
  113. package/docs/releases/RELEASE_CHECKLIST_v1.7.5.md +0 -315
  114. package/docs/releases/RELEASE_CHECKLIST_v1.7.6.md +0 -389
  115. package/docs/releases/RELEASE_CHECKLIST_v1.8.0.md +0 -331
  116. package/docs/releases/RELEASE_CHECKLIST_v1.8.2.md +0 -309
  117. package/docs/releases/RELEASE_NOTES_1.7.0.md +0 -539
  118. package/docs/releases/RELEASE_NOTES_1.8.10.md +0 -239
  119. package/docs/releases/RELEASE_NOTES_1.8.9.md +0 -213
  120. package/docs/releases/RELEASE_NOTES_v1.8.0.md +0 -685
  121. package/docs/releases/RELEASE_NOTES_v1.8.1.md +0 -221
  122. package/docs/releases/RELEASE_NOTES_v1.8.2.md +0 -421
  123. package/docs/releases/RELEASE_NOTES_v1.8.3.md +0 -292
  124. package/docs/releases/RELEASE_NOTES_v1.8.4.md +0 -277
  125. package/docs/releases/RELEASE_NOTES_v1.8.6.md +0 -205
  126. package/docs/releases/RELEASE_NOTES_v1.8.7.md +0 -184
  127. package/docs/releases/RELEASE_PLAN_v1.7.6.md +0 -816
  128. package/docs/releases/RELEASE_SUMMARY_1.8.10.md +0 -193
  129. package/docs/releases/RELEASE_SUMMARY_v1.7.4.md +0 -276
  130. package/docs/releases/RELEASE_SUMMARY_v1.7.5.md +0 -322
  131. package/docs/releases/RELEASE_SUMMARY_v1.7.6.md +0 -436
  132. package/docs/releases/RELEASE_SUMMARY_v1.7.7.md +0 -391
  133. package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +0 -523
  134. package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +0 -542
  135. package/docs/troubleshooting/ARDUINO_IDE_VERSION_FIX_SUMMARY.md +0 -229
  136. package/docs/troubleshooting/ARDUINO_LIBRARY_NAME_FIX.md +0 -197
  137. package/docs/troubleshooting/CRASH_QUICK_REF.md +0 -93
  138. package/docs/troubleshooting/ESP32_C6_COMPATIBILITY.md +0 -157
  139. package/docs/troubleshooting/FREERTOS_ASSERTION_FAILURE.md +0 -288
  140. package/docs/troubleshooting/FREERTOS_FIX_IMPLEMENTATION.md +0 -267
  141. package/docs/troubleshooting/NPM_PUBLISHING_ISSUE_SUMMARY.md +0 -110
  142. package/docs/troubleshooting/PAINLESSMESH_V1.7.4_COMPILATION_ISSUES.md +0 -547
  143. package/docs/troubleshooting/QUICK_FIX_FREERTOS.md +0 -164
  144. package/docs/troubleshooting/SENSOR_NODE_CONNECTION_CRASH.md +0 -264
  145. package/docs/troubleshooting/common-architecture-mistakes.md +0 -438
  146. package/docs/troubleshooting/internet-access-faq.md +0 -299
  147. package/docs/troubleshooting/station-reconnection-issues.md +0 -172
  148. package/docs/v1.7.7_MQTT_IMPROVEMENTS.md +0 -794
  149. package/docs/wiki/API-Reference.md +0 -246
  150. package/docs/wiki/Complete-Documentation.md +0 -123
  151. package/examples/alteriomImproved/alteriom_sensor_package.hpp +0 -224
  152. package/examples/alteriomImproved/improved_sensor_node.ino +0 -248
  153. package/examples/alteriomImproved/platformio.ini +0 -32
  154. package/examples/alteriomMetricsHealth/alteriom_sensor_package.hpp +0 -796
  155. package/examples/alteriomMetricsHealth/metrics_health_node.ino +0 -429
  156. package/examples/alteriomMetricsHealth/platformio.ini +0 -26
  157. package/examples/alteriomPhase1/alteriom_sensor_package.hpp +0 -224
  158. package/examples/alteriomPhase1/phase1_features.ino +0 -242
  159. package/examples/alteriomPhase1/platformio.ini +0 -26
  160. package/examples/alteriomPhase2/alteriom_sensor_package.hpp +0 -224
  161. package/examples/alteriomPhase2/phase2_features.ino +0 -186
  162. package/examples/alteriomPhase2/platformio.ini +0 -26
  163. package/examples/alteriomSensorNode/alteriom_sensor_node.ino +0 -186
  164. package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +0 -1227
  165. package/examples/alteriomSensorNode/platformio.ini +0 -26
  166. package/examples/bridge/alteriom_sensor_package.hpp +0 -1170
  167. package/examples/bridge/bridge_health_monitoring_example.ino +0 -188
  168. package/examples/bridge/enhanced_mqtt_bridge.hpp +0 -610
  169. package/examples/bridge/enhanced_mqtt_bridge_example.ino +0 -226
  170. package/examples/bridge/mesh_event_publisher.hpp +0 -253
  171. package/examples/bridge/mesh_topology_reporter.hpp +0 -303
  172. package/examples/bridge/mqtt_command_bridge.hpp +0 -459
  173. package/examples/bridge/mqtt_status_bridge.hpp +0 -519
  174. package/examples/bridgeAwareSensorNode/alteriom_sensor_package.hpp +0 -1227
  175. package/examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino +0 -342
  176. package/examples/bridgeAwareSensorNode/platformio.ini +0 -26
  177. package/examples/diagnosticsExample/diagnosticsExample.ino +0 -171
  178. package/examples/diagnosticsExample/platformio.ini +0 -26
  179. package/examples/echoNode/echoNode.ino +0 -33
  180. package/examples/echoNode/platformio.ini +0 -26
  181. package/examples/meshCommandNode/alteriom_sensor_package.hpp +0 -235
  182. package/examples/meshCommandNode/meshCommandNode.ino +0 -265
  183. package/examples/mqttCommandBridge/alteriom_sensor_package.hpp +0 -235
  184. package/examples/mqttCommandBridge/mesh_event_publisher.hpp +0 -253
  185. package/examples/mqttCommandBridge/mesh_topology_reporter.hpp +0 -303
  186. package/examples/mqttCommandBridge/mqttCommandBridge.ino +0 -254
  187. package/examples/mqttCommandBridge/mqtt_command_bridge.hpp +0 -462
  188. package/examples/mqttCommandBridge/platformio.ini +0 -27
  189. package/examples/mqttStatusBridge/mqttStatusBridge.ino +0 -218
  190. package/examples/mqttStatusBridge/mqtt_status_bridge.hpp +0 -522
  191. package/examples/mqttStatusBridge/platformio.ini +0 -27
  192. package/examples/mqttTopologyTest/README.md +0 -467
  193. package/examples/mqttTopologyTest/mqttTopologyTest.ino +0 -754
  194. package/examples/mqttTopologyTest/platformio.ini +0 -27
  195. package/examples/multi_bridge/README.md +0 -346
  196. package/examples/multi_bridge/primary_bridge.ino +0 -108
  197. package/examples/multi_bridge/regular_node.ino +0 -141
  198. package/examples/multi_bridge/secondary_bridge.ino +0 -123
  199. package/examples/ntpTimeSyncBridge/alteriom_sensor_package.hpp +0 -1383
  200. package/examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino +0 -86
  201. package/examples/ntpTimeSyncNode/alteriom_sensor_package.hpp +0 -1383
  202. package/examples/ntpTimeSyncNode/ntpTimeSyncNode.ino +0 -109
  203. package/examples/queued_alarms/README.md +0 -390
  204. package/examples/queued_alarms/queued_alarms.ino +0 -265
  205. package/examples/routing_demo/README.md +0 -172
  206. package/examples/routing_demo/routing_demo.ino +0 -102
  207. package/examples/rtcIntegration/README.md +0 -294
  208. package/examples/rtcIntegration/rtcIntegration.ino +0 -210
@@ -8,6 +8,7 @@
8
8
  #include "painlessMeshSTA.h"
9
9
 
10
10
  #include "painlessmesh/callback.hpp"
11
+ #include "painlessmesh/gateway.hpp"
11
12
  #include "painlessmesh/mesh.hpp"
12
13
  #include "painlessmesh/router.hpp"
13
14
  #include "painlessmesh/tcp.hpp"
@@ -168,8 +169,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
168
169
  return;
169
170
  }
170
171
 
171
- // Skip check during startup period (60 seconds) to allow initial bridge discovery
172
- if (millis() < 60000) {
172
+ // Skip check during startup period to allow initial bridge discovery
173
+ if (millis() < electionStartupDelayMs) {
174
+ return;
175
+ }
176
+
177
+ // IMPORTANT: Don't trigger election if we're disconnected from the mesh
178
+ // When isolated, we can't receive bridge status broadcasts, so lack of
179
+ // healthy bridge could simply mean WE are disconnected, not that the bridge
180
+ // is unavailable. Wait until mesh connectivity is restored before considering
181
+ // an election.
182
+ if (!this->hasActiveMeshConnections()) {
183
+ Log(CONNECTION, "Bridge monitor: Skipping - no active mesh connections\n");
173
184
  return;
174
185
  }
175
186
 
@@ -185,14 +196,80 @@ class Mesh : public painlessmesh::Mesh<Connection> {
185
196
  // If no healthy bridge exists, trigger an election
186
197
  if (!hasHealthyBridge) {
187
198
  Log(CONNECTION, "Bridge monitor: No healthy bridge detected, triggering election\n");
188
- // Small delay to randomize election start across nodes
189
- uint32_t randomDelay = random(1000, 3000);
199
+ // Random delay to prevent simultaneous elections when multiple nodes start together
200
+ uint32_t randomDelay = random(electionRandomDelayMinMs, electionRandomDelayMaxMs);
201
+ Log(CONNECTION, "Bridge monitor: Scheduling election in %u ms\n", randomDelay);
190
202
  this->addTask(randomDelay, TASK_ONCE, [this]() {
191
203
  this->startBridgeElection();
192
204
  });
193
205
  }
194
206
  });
195
207
 
208
+ // Add separate periodic task for isolated bridge retry
209
+ // This handles the case where a node:
210
+ // - Has router credentials configured
211
+ // - Is isolated (no mesh connections)
212
+ // - Should attempt to become a bridge directly
213
+ // This is different from the election mechanism which requires mesh connectivity
214
+ this->addTask(isolatedBridgeRetryIntervalMs, TASK_FOREVER, [this]() {
215
+ // Only retry if failover is enabled and we have credentials
216
+ if (!bridgeFailoverEnabled || !routerCredentialsConfigured) {
217
+ return;
218
+ }
219
+
220
+ // Don't retry if we're already a bridge
221
+ if (this->isBridge()) {
222
+ return;
223
+ }
224
+
225
+ // Skip during startup period
226
+ if (millis() < electionStartupDelayMs) {
227
+ return;
228
+ }
229
+
230
+ // Only retry when isolated (no mesh connections found)
231
+ if (this->hasActiveMeshConnections()) {
232
+ // Reset retry counter when mesh is active
233
+ _isolatedBridgeRetryAttempts = 0;
234
+ return;
235
+ }
236
+
237
+ // Limit retry attempts with reset after timeout
238
+ if (_isolatedBridgeRetryAttempts >= MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS) {
239
+ // Check if enough time has passed to reset the counter
240
+ if (millis() > _isolatedBridgeRetryResetTime) {
241
+ Log(CONNECTION, "Isolated bridge retry: Reset timeout reached, resetting attempt counter\n");
242
+ _isolatedBridgeRetryAttempts = 0;
243
+ } else {
244
+ Log(CONNECTION, "Isolated bridge retry: Max attempts (%d) reached, reset in %u seconds\n",
245
+ MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS, (_isolatedBridgeRetryResetTime - millis()) / 1000);
246
+ return;
247
+ }
248
+ }
249
+
250
+ // Check if mesh network exists on any channel before trying to become bridge
251
+ // If mesh exists but we can't connect, don't try to become bridge
252
+ uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
253
+ if (emptyScans < ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD) {
254
+ Log(CONNECTION, "Isolated bridge retry: Only %d empty scans, waiting for more scans\n",
255
+ emptyScans);
256
+ return;
257
+ }
258
+
259
+ Log(CONNECTION, "Isolated bridge retry: Node isolated with %d empty scans, attempting bridge promotion\n",
260
+ emptyScans);
261
+
262
+ // Attempt to become bridge directly (bypassing election since we're isolated)
263
+ // Only increment retry counter if we actually attempted promotion
264
+ if (this->attemptIsolatedBridgePromotion()) {
265
+ _isolatedBridgeRetryAttempts++;
266
+ // Set reset time when reaching max attempts
267
+ if (_isolatedBridgeRetryAttempts >= MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS) {
268
+ _isolatedBridgeRetryResetTime = millis() + isolatedBridgeRetryResetIntervalMs;
269
+ }
270
+ }
271
+ });
272
+
196
273
  tcpServerInit();
197
274
  eventHandleInit();
198
275
 
@@ -384,6 +461,150 @@ class Mesh : public painlessmesh::Mesh<Connection> {
384
461
  return success;
385
462
  }
386
463
 
464
+ /**
465
+ * Initialize mesh as a shared gateway node
466
+ *
467
+ * This method initializes all mesh nodes in AP+STA mode with router
468
+ * connectivity. Unlike initAsBridge() which creates a single bridge node,
469
+ * initAsSharedGateway() allows all nodes to connect to the router while
470
+ * maintaining mesh communication.
471
+ *
472
+ * Key features:
473
+ * - All nodes operate in AP+STA mode
474
+ * - All nodes connect to the same router
475
+ * - Mesh and router operate on the same channel for reliability
476
+ * - Automatic router reconnection on disconnect
477
+ * - Channel synchronization between mesh and router
478
+ *
479
+ * @param meshPrefix The name prefix for the mesh network
480
+ * @param meshPassword WiFi password for the mesh network
481
+ * @param routerSSID SSID of the router to connect to
482
+ * @param routerPassword Password for the router
483
+ * @param userScheduler Task scheduler for mesh operations
484
+ * @param port TCP port for mesh communication (default: 5555)
485
+ * @param config SharedGatewayConfig with advanced settings (optional)
486
+ * @return true if initialization succeeded, false otherwise
487
+ */
488
+ bool initAsSharedGateway(TSTRING meshPrefix, TSTRING meshPassword,
489
+ TSTRING routerSSID, TSTRING routerPassword,
490
+ Scheduler *userScheduler, uint16_t port = 5555,
491
+ gateway::SharedGatewayConfig config = gateway::SharedGatewayConfig()) {
492
+ using namespace logger;
493
+
494
+ Log(STARTUP, "=== Shared Gateway Mode Initialization ===\n");
495
+
496
+ // Validate configuration if enabled
497
+ if (config.enabled) {
498
+ auto result = config.validate();
499
+ if (!result.valid) {
500
+ Log(ERROR, "initAsSharedGateway(): Config validation failed: %s\n",
501
+ result.errorMessage.c_str());
502
+ return false;
503
+ }
504
+ }
505
+
506
+ // Store shared gateway configuration
507
+ _sharedGatewayConfig = config;
508
+ _sharedGatewayConfig.routerSSID = routerSSID;
509
+ _sharedGatewayConfig.routerPassword = routerPassword;
510
+ _sharedGatewayConfig.enabled = true;
511
+ _sharedGatewayMode = true;
512
+
513
+ Log(STARTUP, "Step 1: Scanning for router %s to detect channel...\n", routerSSID.c_str());
514
+
515
+ // Step 1: Scan for router to detect its channel
516
+ // We need to ensure mesh and router operate on the same channel
517
+ if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
518
+
519
+ #if ESP_ARDUINO_VERSION_MAJOR >= 3
520
+ WiFi.setAutoReconnect(false);
521
+ Log(STARTUP, "initAsSharedGateway(): AutoReconnect disabled\n");
522
+ #else
523
+ WiFi.setAutoConnect(false);
524
+ Log(STARTUP, "initAsSharedGateway(): AutoConnect disabled\n");
525
+ #endif
526
+ WiFi.persistent(false);
527
+ WiFi.mode(WIFI_STA);
528
+
529
+ // Connect to router to detect channel
530
+ WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
531
+
532
+ // Wait for connection with timeout (using constant for configurability)
533
+ int timeout = ROUTER_CONNECTION_TIMEOUT_SECONDS;
534
+ while (WiFi.status() != WL_CONNECTED && timeout > 0) {
535
+ delay(1000);
536
+ timeout--;
537
+ Log(STARTUP, ".");
538
+ }
539
+
540
+ uint8_t detectedChannel = 1; // Default fallback
541
+
542
+ if (WiFi.status() == WL_CONNECTED) {
543
+ detectedChannel = WiFi.channel();
544
+ // Validate channel is in valid range (1-14 for 2.4GHz, region-dependent)
545
+ if (detectedChannel < MIN_WIFI_CHANNEL || detectedChannel > MAX_WIFI_CHANNEL) {
546
+ Log(ERROR, "\n✗ Invalid channel detected: %d, falling back to channel 1\n", detectedChannel);
547
+ detectedChannel = 1;
548
+ } else {
549
+ Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
550
+ Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
551
+ }
552
+ } else {
553
+ Log(ERROR, "\n✗ Failed to connect to router during channel detection\n");
554
+ Log(ERROR, "Continuing with default channel 1, will retry router connection later\n");
555
+ }
556
+
557
+ // Disconnect from router, we'll reconnect after mesh init
558
+ WiFi.disconnect();
559
+ delay(100);
560
+
561
+ Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n", detectedChannel);
562
+
563
+ // Step 2: Initialize mesh on detected channel with AP+STA mode
564
+ // Set scheduler before init
565
+ this->setScheduler(userScheduler);
566
+ init(meshPrefix, meshPassword, port, WIFI_AP_STA, detectedChannel, 0, MAX_CONN);
567
+
568
+ Log(STARTUP, "Step 3: Establishing router connection in shared gateway mode...\n");
569
+
570
+ // Step 3: Establish router connection using stationManual
571
+ // Port 0 means we don't expect TCP mesh connection to the router
572
+ stationManual(routerSSID, routerPassword, 0);
573
+
574
+ // Step 4: Setup router connection monitoring and reconnection logic
575
+ initSharedGatewayMonitoring();
576
+
577
+ // Store router credentials for reconnection
578
+ setRouterCredentials(routerSSID, routerPassword);
579
+
580
+ Log(STARTUP, "=== Shared Gateway Mode Active ===\n");
581
+ Log(STARTUP, " Mesh Prefix: %s\n", meshPrefix.c_str());
582
+ Log(STARTUP, " Mesh Channel: %d (synced with router)\n", detectedChannel);
583
+ Log(STARTUP, " Router: %s\n", routerSSID.c_str());
584
+ Log(STARTUP, " Port: %d\n", port);
585
+ Log(STARTUP, " Mode: AP+STA (all nodes can connect to router)\n");
586
+
587
+ return true;
588
+ }
589
+
590
+ /**
591
+ * Check if shared gateway mode is enabled
592
+ *
593
+ * @return true if node is operating in shared gateway mode
594
+ */
595
+ bool isSharedGatewayMode() const {
596
+ return _sharedGatewayMode;
597
+ }
598
+
599
+ /**
600
+ * Get the shared gateway configuration
601
+ *
602
+ * @return const reference to the SharedGatewayConfig
603
+ */
604
+ const gateway::SharedGatewayConfig& getSharedGatewayConfig() const {
605
+ return _sharedGatewayConfig;
606
+ }
607
+
387
608
  /**
388
609
  * Connect (as a station) to a specified network and ip
389
610
  *
@@ -555,6 +776,37 @@ class Mesh : public painlessmesh::Mesh<Connection> {
555
776
  minimumBridgeRSSI = minRSSI;
556
777
  }
557
778
 
779
+ /**
780
+ * Set the startup delay before first bridge election check
781
+ *
782
+ * Allows time for mesh network formation before starting bridge elections.
783
+ * Longer delays reduce the risk of split-brain scenarios when multiple nodes
784
+ * start simultaneously, ensuring nodes discover each other before elections.
785
+ *
786
+ * @param delayMs Startup delay in milliseconds (default: 60000 = 60 seconds, min: 10000)
787
+ */
788
+ void setElectionStartupDelay(uint32_t delayMs) {
789
+ if (delayMs < 10000) delayMs = 10000; // Minimum 10 seconds
790
+ electionStartupDelayMs = delayMs;
791
+ }
792
+
793
+ /**
794
+ * Set the random delay range for bridge elections
795
+ *
796
+ * When multiple nodes detect missing bridge simultaneously, randomized delays
797
+ * prevent all nodes from starting elections at the same instant. Longer delays
798
+ * provide more time for mesh discovery and reduce split-brain risk.
799
+ *
800
+ * @param minMs Minimum random delay in milliseconds (default: 1000 = 1 second)
801
+ * @param maxMs Maximum random delay in milliseconds (default: 3000 = 3 seconds)
802
+ */
803
+ void setElectionRandomDelay(uint32_t minMs, uint32_t maxMs) {
804
+ if (minMs < 100) minMs = 100; // Minimum 100ms
805
+ if (maxMs < minMs) maxMs = minMs + 1000; // Ensure max > min
806
+ electionRandomDelayMinMs = minMs;
807
+ electionRandomDelayMaxMs = maxMs;
808
+ }
809
+
558
810
  /**
559
811
  * Set callback for when this node's bridge role changes
560
812
  *
@@ -1342,6 +1594,95 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1342
1594
  });
1343
1595
  }
1344
1596
 
1597
+ /**
1598
+ * Attempt to promote an isolated node to bridge
1599
+ *
1600
+ * This method handles the case where a node is isolated (no mesh connections)
1601
+ * but has router credentials. Unlike the election-based promotion, this
1602
+ * directly attempts to connect to the router without requiring mesh connectivity.
1603
+ *
1604
+ * This is useful for:
1605
+ * - Nodes that failed initial bridge setup and need to retry
1606
+ * - Nodes that are the first to start and no mesh exists yet
1607
+ * - Recovery scenarios where mesh network is unavailable
1608
+ *
1609
+ * @return true if promotion was attempted (regardless of success), false if skipped
1610
+ */
1611
+ bool attemptIsolatedBridgePromotion() {
1612
+ using namespace logger;
1613
+
1614
+ Log(CONNECTION, "=== Isolated Bridge Promotion Attempt ===\n");
1615
+ Log(CONNECTION, "Attempt %d of %d\n", _isolatedBridgeRetryAttempts + 1, MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS);
1616
+
1617
+ // First, scan for router to check if it's visible
1618
+ int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
1619
+
1620
+ if (routerRSSI == 0) {
1621
+ Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router %s not visible\n", routerSSID.c_str());
1622
+ return false; // Don't count as an attempt - router not visible
1623
+ }
1624
+
1625
+ // Check minimum RSSI threshold for isolated promotion
1626
+ if (routerRSSI < minimumBridgeRSSI) {
1627
+ Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router RSSI %d dBm below threshold %d dBm\n",
1628
+ routerRSSI, minimumBridgeRSSI);
1629
+ return false; // Don't count as an attempt - signal too weak
1630
+ }
1631
+
1632
+ Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router visible with RSSI %d dBm\n", routerRSSI);
1633
+ Log(CONNECTION, "Attempting direct bridge promotion (bypassing election)\n");
1634
+
1635
+ // Save current mesh configuration
1636
+ uint8_t savedChannel = _meshChannel;
1637
+
1638
+ // Stop current mesh operations
1639
+ this->stop();
1640
+ delay(1000);
1641
+
1642
+ // Attempt to initialize as bridge
1643
+ bool bridgeInitSuccess = this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
1644
+ mScheduler, _meshPort);
1645
+
1646
+ if (!bridgeInitSuccess) {
1647
+ Log(ERROR, "✗ Isolated bridge promotion failed - router unreachable\n");
1648
+ Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
1649
+
1650
+ // Re-initialize as regular node on the original channel
1651
+ this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
1652
+ savedChannel, _meshHidden, MAX_CONN);
1653
+
1654
+ // Re-configure router credentials for future retry attempts
1655
+ this->setRouterCredentials(routerSSID, routerPassword);
1656
+ this->enableBridgeFailover(true);
1657
+
1658
+ // Notify via callback
1659
+ if (bridgeRoleChangedCallback) {
1660
+ bridgeRoleChangedCallback(false, "Isolated bridge promotion failed - router unreachable");
1661
+ }
1662
+
1663
+ return true; // Count as an attempt - we tried but failed
1664
+ }
1665
+
1666
+ // Success! Reset retry counter
1667
+ _isolatedBridgeRetryAttempts = 0;
1668
+ lastRoleChangeTime = millis();
1669
+
1670
+ Log(STARTUP, "✓ Isolated bridge promotion complete on channel %d\n", _meshChannel);
1671
+
1672
+ // Notify via callback
1673
+ if (bridgeRoleChangedCallback) {
1674
+ bridgeRoleChangedCallback(true, "Isolated node promoted to bridge");
1675
+ }
1676
+
1677
+ // Send bridge status announcement to attract other nodes
1678
+ this->addTask(3000, TASK_ONCE, [this]() {
1679
+ Log(STARTUP, "Sending bridge status announcement on channel %d\n", _meshChannel);
1680
+ this->sendBridgeStatus();
1681
+ });
1682
+
1683
+ return true; // Count as an attempt - we succeeded
1684
+ }
1685
+
1345
1686
  /**
1346
1687
  * Handle received bridge election package
1347
1688
  * Called by package handler when election message arrives
@@ -1564,12 +1905,23 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1564
1905
  TSTRING routerPassword = "";
1565
1906
  uint32_t electionTimeoutMs = 5000; // Default 5 seconds
1566
1907
  int8_t minimumBridgeRSSI = -80; // Default -80 dBm minimum for isolated elections
1908
+ uint32_t electionStartupDelayMs = 60000; // Default 60 seconds before first election check
1909
+ uint32_t electionRandomDelayMinMs = 1000; // Default min 1 second random delay
1910
+ uint32_t electionRandomDelayMaxMs = 3000; // Default max 3 seconds random delay
1567
1911
  uint32_t lastRoleChangeTime = 0;
1568
1912
  ElectionState electionState = ELECTION_IDLE;
1569
1913
  uint32_t electionDeadline = 0;
1570
1914
  std::vector<BridgeCandidate> electionCandidates;
1571
1915
  std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
1572
1916
 
1917
+ // Isolated bridge retry state and configuration
1918
+ uint8_t _isolatedBridgeRetryAttempts = 0;
1919
+ uint32_t _isolatedBridgeRetryResetTime = 0; // Time when retry counter can be reset
1920
+ static const uint8_t MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS = 5; // Max retry attempts before waiting
1921
+ static const uint32_t isolatedBridgeRetryIntervalMs = 60000; // Retry every 60 seconds
1922
+ static const uint32_t isolatedBridgeRetryResetIntervalMs = 300000; // Reset counter after 5 minutes
1923
+ static const uint16_t ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD = 6; // Require 6 empty scans before retrying
1924
+
1573
1925
  // Multi-bridge coordination state and configuration
1574
1926
  protected:
1575
1927
  bool multiBridgeEnabled = false;
@@ -1582,6 +1934,154 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1582
1934
  std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
1583
1935
  uint32_t selectedBridgeOverride = 0; // Manual bridge selection override
1584
1936
  size_t lastSelectedBridgeIndex = 0; // For round-robin selection
1937
+
1938
+ // Shared gateway mode state and configuration
1939
+ bool _sharedGatewayMode = false;
1940
+ gateway::SharedGatewayConfig _sharedGatewayConfig;
1941
+ std::shared_ptr<Task> _sharedGatewayMonitorTask;
1942
+ uint32_t _lastRouterReconnectAttempt = 0;
1943
+ uint8_t _routerReconnectAttempts = 0;
1944
+ static const uint8_t MAX_ROUTER_RECONNECT_ATTEMPTS = 10;
1945
+ static const uint32_t ROUTER_RECONNECT_BASE_INTERVAL = 5000; // 5 seconds base interval
1946
+ static const uint32_t ROUTER_RECONNECT_MAX_INTERVAL = 300000; // 5 minutes max interval
1947
+ static const int ROUTER_CONNECTION_TIMEOUT_SECONDS = 30; // Router connection timeout
1948
+ static const uint8_t MIN_WIFI_CHANNEL = 1;
1949
+ static const uint8_t MAX_WIFI_CHANNEL = 14; // Support channels 1-14 for regions that allow it
1950
+
1951
+ /**
1952
+ * Initialize shared gateway monitoring
1953
+ *
1954
+ * Sets up periodic monitoring of router connection and automatic
1955
+ * reconnection logic for shared gateway mode.
1956
+ */
1957
+ void initSharedGatewayMonitoring() {
1958
+ using namespace logger;
1959
+
1960
+ if (!_sharedGatewayMode) {
1961
+ return;
1962
+ }
1963
+
1964
+ Log(STARTUP, "initSharedGatewayMonitoring(): Setting up router connection monitoring\n");
1965
+
1966
+ // Add callback for router disconnection in shared gateway mode
1967
+ this->droppedConnectionCallbacks.push_back(
1968
+ [this](uint32_t nodeId, bool station) {
1969
+ if (station && _sharedGatewayMode) {
1970
+ Log(CONNECTION, "Router disconnected in shared gateway mode, scheduling reconnection\n");
1971
+ scheduleRouterReconnect();
1972
+ }
1973
+ });
1974
+
1975
+ // Create periodic monitoring task
1976
+ _sharedGatewayMonitorTask = this->addTask(
1977
+ _sharedGatewayConfig.internetCheckInterval,
1978
+ TASK_FOREVER,
1979
+ [this]() {
1980
+ monitorRouterConnection();
1981
+ });
1982
+
1983
+ Log(STARTUP, "Router connection monitoring enabled (interval: %u ms)\n",
1984
+ _sharedGatewayConfig.internetCheckInterval);
1985
+ }
1986
+
1987
+ /**
1988
+ * Monitor router connection in shared gateway mode
1989
+ *
1990
+ * Checks router connectivity and triggers reconnection if needed.
1991
+ */
1992
+ void monitorRouterConnection() {
1993
+ using namespace logger;
1994
+
1995
+ if (!_sharedGatewayMode) {
1996
+ return;
1997
+ }
1998
+
1999
+ bool isConnected = (WiFi.status() == WL_CONNECTED) &&
2000
+ (WiFi.localIP() != IPAddress(0, 0, 0, 0));
2001
+
2002
+ if (!isConnected) {
2003
+ Log(CONNECTION, "monitorRouterConnection(): Router connection lost, triggering reconnect\n");
2004
+ scheduleRouterReconnect();
2005
+ } else {
2006
+ // Connection is healthy, reset reconnect attempts
2007
+ _routerReconnectAttempts = 0;
2008
+
2009
+ // Log periodic status
2010
+ Log(GENERAL, "monitorRouterConnection(): Router connected (RSSI: %d dBm, IP: %s)\n",
2011
+ WiFi.RSSI(), WiFi.localIP().toString().c_str());
2012
+ }
2013
+ }
2014
+
2015
+ /**
2016
+ * Schedule router reconnection with exponential backoff
2017
+ */
2018
+ void scheduleRouterReconnect() {
2019
+ using namespace logger;
2020
+
2021
+ if (!_sharedGatewayMode) {
2022
+ return;
2023
+ }
2024
+
2025
+ // Don't schedule if already connected
2026
+ if (WiFi.status() == WL_CONNECTED) {
2027
+ return;
2028
+ }
2029
+
2030
+ // Limit reconnection attempts
2031
+ if (_routerReconnectAttempts >= MAX_ROUTER_RECONNECT_ATTEMPTS) {
2032
+ Log(ERROR, "scheduleRouterReconnect(): Max reconnection attempts reached (%d)\n",
2033
+ MAX_ROUTER_RECONNECT_ATTEMPTS);
2034
+ Log(ERROR, "Router reconnection suspended. Manual intervention may be required.\n");
2035
+ return;
2036
+ }
2037
+
2038
+ // Calculate delay with exponential backoff, preventing overflow
2039
+ // Limit shift amount to prevent overflow (5000 * 2^6 = 320000 is safe)
2040
+ uint8_t shiftAmount = (_routerReconnectAttempts > 6) ? 6 : _routerReconnectAttempts;
2041
+ uint32_t delay = ROUTER_RECONNECT_BASE_INTERVAL * (1UL << shiftAmount);
2042
+ if (delay > ROUTER_RECONNECT_MAX_INTERVAL) delay = ROUTER_RECONNECT_MAX_INTERVAL;
2043
+
2044
+ // Don't reconnect too frequently
2045
+ uint32_t now = millis();
2046
+ if (now - _lastRouterReconnectAttempt < delay) {
2047
+ return;
2048
+ }
2049
+
2050
+ _routerReconnectAttempts++;
2051
+ _lastRouterReconnectAttempt = now;
2052
+
2053
+ Log(CONNECTION, "scheduleRouterReconnect(): Attempting reconnection (attempt %d/%d, delay %u ms)\n",
2054
+ _routerReconnectAttempts, MAX_ROUTER_RECONNECT_ATTEMPTS, delay);
2055
+
2056
+ // Schedule reconnection
2057
+ this->addTask(delay, TASK_ONCE, [this]() {
2058
+ attemptRouterReconnect();
2059
+ });
2060
+ }
2061
+
2062
+ /**
2063
+ * Attempt to reconnect to the router
2064
+ */
2065
+ void attemptRouterReconnect() {
2066
+ using namespace logger;
2067
+
2068
+ if (!_sharedGatewayMode) {
2069
+ return;
2070
+ }
2071
+
2072
+ // Check if already connected
2073
+ if (WiFi.status() == WL_CONNECTED) {
2074
+ Log(CONNECTION, "attemptRouterReconnect(): Already connected to router\n");
2075
+ _routerReconnectAttempts = 0;
2076
+ return;
2077
+ }
2078
+
2079
+ Log(CONNECTION, "attemptRouterReconnect(): Reconnecting to router %s...\n",
2080
+ _sharedGatewayConfig.routerSSID.c_str());
2081
+
2082
+ // Use stationManual to reconnect (port 0 means no TCP mesh connection to router)
2083
+ stationManual(_sharedGatewayConfig.routerSSID, _sharedGatewayConfig.routerPassword, 0);
2084
+ }
1585
2085
  };
1586
2086
  } // namespace wifi
1587
2087
  }; // namespace painlessmesh