@alteriom/painlessmesh 1.8.14 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/BRIDGE_TO_INTERNET.md +229 -0
  2. package/CHANGELOG.md +89 -1
  3. package/CONTRIBUTING.md +79 -0
  4. package/README.md +70 -143
  5. package/docs/README.md +1 -0
  6. package/docs/api/shared-gateway.md +1207 -0
  7. package/docs/troubleshooting/common-issues.md +28 -0
  8. package/docs/troubleshooting/faq.md +113 -12
  9. package/examples/basic/test/simulator/CMakeLists.txt +40 -0
  10. package/examples/basic/test/simulator/README.md +149 -0
  11. package/examples/basic/test/simulator/firmware/basic_firmware.hpp +117 -0
  12. package/examples/basic/test/simulator/scenarios/basic_mesh_test.yaml +81 -0
  13. package/examples/bridge/bridge.ino +17 -4
  14. package/examples/bridge_failover/README.md +81 -0
  15. package/examples/bridge_failover/bridge_failover.ino +51 -6
  16. package/examples/sharedGateway/README.md +235 -0
  17. package/examples/{meshCommandNode → sharedGateway}/platformio.ini +3 -2
  18. package/examples/sharedGateway/sharedGateway.ino +303 -0
  19. package/library.json +3 -22
  20. package/library.properties +1 -1
  21. package/package.json +3 -3
  22. package/src/arduino/wifi.hpp +380 -13
  23. package/src/painlessmesh/gateway.hpp +2120 -0
  24. package/src/painlessmesh/mesh.hpp +1034 -6
  25. package/src/painlessmesh/message_tracker.hpp +311 -0
  26. package/src/painlessmesh/protocol.hpp +6 -0
  27. package/DOCUMENTATION_INDEX.md +0 -146
  28. package/docs/API_DESIGN_GUIDELINES.md +0 -414
  29. package/docs/ARDUINO_LIBRARY_MANAGER_SUBMISSION.md +0 -331
  30. package/docs/BOOLEAN_NAMING_CONVENTION.md +0 -235
  31. package/docs/BRIDGE_FAILOVER.md +0 -512
  32. package/docs/BRIDGE_HEALTH_MONITORING.md +0 -293
  33. package/docs/CHANNEL_SYNCHRONIZATION.md +0 -209
  34. package/docs/CREATE_MISSING_RELEASES.md +0 -321
  35. package/docs/DOCUMENTATION_MIGRATION_PLAN.md +0 -176
  36. package/docs/FAQ_VERSION_NUMBERS.md +0 -152
  37. package/docs/IMPLEMENTATION_PLAN_MESH_TOPOLOGY.md +0 -1062
  38. package/docs/MESH_TOPOLOGY_GUIDE.md +0 -992
  39. package/docs/MESH_TOPOLOGY_PROGRESS.md +0 -422
  40. package/docs/MQTT_BRIDGE_COMMANDS.md +0 -894
  41. package/docs/MQTT_BRIDGE_IMPLEMENTATION_SUMMARY.md +0 -324
  42. package/docs/MQTT_COMMAND_SCHEMA_PROPOSAL.md +0 -576
  43. package/docs/MQTT_SCHEMA_COMPLIANCE.md +0 -340
  44. package/docs/MQTT_SCHEMA_PROPOSALS.md +0 -446
  45. package/docs/MQTT_SCHEMA_REVIEW.md +0 -690
  46. package/docs/OTA_COMMANDS_REFERENCE.md +0 -554
  47. package/docs/PHASE1_GUIDE.md +0 -349
  48. package/docs/PHASE2_GUIDE.md +0 -543
  49. package/docs/QUICK_REFERENCE_VERSIONING.md +0 -127
  50. package/docs/RELEASE_AGENT_SUMMARY.md +0 -386
  51. package/docs/SCHEMA_VALIDATION_CHECKLIST.md +0 -222
  52. package/docs/VERSION_MANAGEMENT.md +0 -213
  53. package/docs/archive/DOCUSAURUS_DEPLOYMENT.md +0 -166
  54. package/docs/archive/FEATURE_PROPOSALS.md +0 -337
  55. package/docs/archive/LIBRARY_JSON_FIX.md +0 -98
  56. package/docs/archive/LIBRARY_STRUCTURE_FIX.md +0 -215
  57. package/docs/archive/PHASE1_IMPLEMENTATION.md +0 -325
  58. package/docs/archive/PHASE2_IMPLEMENTATION.md +0 -567
  59. package/docs/archive/RELEASE_SUMMARY.md +0 -173
  60. package/docs/archive/SCONS_BUILD_FIX.md +0 -313
  61. package/docs/archive/TRIGGER_RELEASE.md +0 -280
  62. package/docs/archive/VECTOR_INCLUDE_FIX.md +0 -129
  63. package/docs/archive/ota-and-status-enhancements.md +0 -911
  64. package/docs/archive/ota-status-architecture-diagrams.md +0 -658
  65. package/docs/archive/ota-status-quick-reference.md +0 -284
  66. package/docs/design/.gitkeep +0 -1
  67. package/docs/design/STATION_CREDENTIALS_DESIGN.md +0 -182
  68. package/docs/development/ARDUINO_COMPLIANCE_SUMMARY.md +0 -71
  69. package/docs/development/CODE_REFACTORING_RECOMMENDATIONS.md +0 -1011
  70. package/docs/development/DOCKER_TESTING.md +0 -196
  71. package/docs/development/PLATFORMIO_USAGE.md +0 -180
  72. package/docs/development/TESTING_SUMMARY.md +0 -126
  73. package/docs/development/contributing.md +0 -301
  74. package/docs/development/documentation.md +0 -583
  75. package/docs/features/DIAGNOSTICS_API.md +0 -534
  76. package/docs/implementation/BRIDGE_ARCHITECTURE_IMPLEMENTATION.md +0 -340
  77. package/docs/implementation/BRIDGE_HEALTH_MONITORING_IMPLEMENTATION.md +0 -213
  78. package/docs/implementation/BRIDGE_STATUS_FEATURE.md +0 -635
  79. package/docs/implementation/DIAGNOSTICS_API_IMPLEMENTATION.md +0 -232
  80. package/docs/implementation/IMPLEMENTATION_COMPLETE.md +0 -228
  81. package/docs/implementation/IMPLEMENTATION_NTP_TIME_SYNC.md +0 -325
  82. package/docs/implementation/IMPLEMENTATION_SUMMARY.md +0 -316
  83. package/docs/implementation/MESSAGE_QUEUE_IMPLEMENTATION.md +0 -405
  84. package/docs/implementation/MULTI_BRIDGE_IMPLEMENTATION.md +0 -520
  85. package/docs/implementation/NTP_TIME_SYNC_FEATURE.md +0 -392
  86. package/docs/improvements/FUTURE_PROPOSALS.md +0 -1016
  87. package/docs/improvements/IMPLEMENTATION_HISTORY.md +0 -1091
  88. package/docs/improvements/OTA_STATUS_ENHANCEMENTS.md +0 -709
  89. package/docs/improvements/README.md +0 -212
  90. package/docs/internal/CUSTOM_AGENT_ANALYSIS.md +0 -391
  91. package/docs/internal/ISSUE_65_VERIFICATION.md +0 -947
  92. package/docs/internal/ISSUE_66_CLOSURE.md +0 -249
  93. package/docs/internal/ISSUE_66_STATUS.md +0 -316
  94. package/docs/internal/PR_SUMMARY.md +0 -315
  95. package/docs/internal/REVIEW_SUMMARY.md +0 -332
  96. package/docs/multi-bridge-setup.md +0 -1025
  97. package/docs/platformio-publishing.md +0 -255
  98. package/docs/platformio-setup-summary.md +0 -121
  99. package/docs/releases/ANNOUNCEMENT_v1.8.6.md +0 -63
  100. package/docs/releases/ANNOUNCEMENT_v1.8.7.md +0 -113
  101. package/docs/releases/BRIDGE_STATUS_SELF_REGISTRATION_FIX.md +0 -221
  102. package/docs/releases/FEATURE_HISTORY.md +0 -543
  103. package/docs/releases/GITHUB_RELEASE_v1.8.6.md +0 -71
  104. package/docs/releases/GITHUB_RELEASE_v1.8.7.md +0 -90
  105. package/docs/releases/PATCH_v1.7.2.md +0 -262
  106. package/docs/releases/PATCH_v1.7.3.md +0 -262
  107. package/docs/releases/PATCH_v1.7.4.md +0 -219
  108. package/docs/releases/PHASE1_SUMMARY.md +0 -246
  109. package/docs/releases/PHASE2_SUMMARY.md +0 -499
  110. package/docs/releases/PUBLISH_v1.8.0_INSTRUCTIONS.md +0 -163
  111. package/docs/releases/QUICK_START_RELEASES.md +0 -113
  112. package/docs/releases/RELEASE_CHECKLIST_1.8.10.md +0 -331
  113. package/docs/releases/RELEASE_CHECKLIST_1.8.9.md +0 -207
  114. package/docs/releases/RELEASE_CHECKLIST_v1.7.4.md +0 -253
  115. package/docs/releases/RELEASE_CHECKLIST_v1.7.5.md +0 -315
  116. package/docs/releases/RELEASE_CHECKLIST_v1.7.6.md +0 -389
  117. package/docs/releases/RELEASE_CHECKLIST_v1.8.0.md +0 -331
  118. package/docs/releases/RELEASE_CHECKLIST_v1.8.2.md +0 -309
  119. package/docs/releases/RELEASE_NOTES_1.7.0.md +0 -539
  120. package/docs/releases/RELEASE_NOTES_1.8.10.md +0 -239
  121. package/docs/releases/RELEASE_NOTES_1.8.9.md +0 -213
  122. package/docs/releases/RELEASE_NOTES_v1.8.0.md +0 -685
  123. package/docs/releases/RELEASE_NOTES_v1.8.1.md +0 -221
  124. package/docs/releases/RELEASE_NOTES_v1.8.2.md +0 -421
  125. package/docs/releases/RELEASE_NOTES_v1.8.3.md +0 -292
  126. package/docs/releases/RELEASE_NOTES_v1.8.4.md +0 -277
  127. package/docs/releases/RELEASE_NOTES_v1.8.6.md +0 -205
  128. package/docs/releases/RELEASE_NOTES_v1.8.7.md +0 -184
  129. package/docs/releases/RELEASE_PLAN_v1.7.6.md +0 -816
  130. package/docs/releases/RELEASE_SUMMARY_1.8.10.md +0 -193
  131. package/docs/releases/RELEASE_SUMMARY_v1.7.4.md +0 -276
  132. package/docs/releases/RELEASE_SUMMARY_v1.7.5.md +0 -322
  133. package/docs/releases/RELEASE_SUMMARY_v1.7.6.md +0 -436
  134. package/docs/releases/RELEASE_SUMMARY_v1.7.7.md +0 -391
  135. package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +0 -523
  136. package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +0 -542
  137. package/docs/troubleshooting/ARDUINO_IDE_VERSION_FIX_SUMMARY.md +0 -229
  138. package/docs/troubleshooting/ARDUINO_LIBRARY_NAME_FIX.md +0 -197
  139. package/docs/troubleshooting/CRASH_QUICK_REF.md +0 -93
  140. package/docs/troubleshooting/ESP32_C6_COMPATIBILITY.md +0 -157
  141. package/docs/troubleshooting/FREERTOS_ASSERTION_FAILURE.md +0 -288
  142. package/docs/troubleshooting/FREERTOS_FIX_IMPLEMENTATION.md +0 -267
  143. package/docs/troubleshooting/NPM_PUBLISHING_ISSUE_SUMMARY.md +0 -110
  144. package/docs/troubleshooting/PAINLESSMESH_V1.7.4_COMPILATION_ISSUES.md +0 -547
  145. package/docs/troubleshooting/QUICK_FIX_FREERTOS.md +0 -164
  146. package/docs/troubleshooting/SENSOR_NODE_CONNECTION_CRASH.md +0 -264
  147. package/docs/troubleshooting/station-reconnection-issues.md +0 -172
  148. package/docs/v1.7.7_MQTT_IMPROVEMENTS.md +0 -794
  149. package/docs/wiki/API-Reference.md +0 -246
  150. package/docs/wiki/Complete-Documentation.md +0 -123
  151. package/examples/alteriomImproved/alteriom_sensor_package.hpp +0 -224
  152. package/examples/alteriomImproved/improved_sensor_node.ino +0 -248
  153. package/examples/alteriomImproved/platformio.ini +0 -32
  154. package/examples/alteriomMetricsHealth/alteriom_sensor_package.hpp +0 -796
  155. package/examples/alteriomMetricsHealth/metrics_health_node.ino +0 -429
  156. package/examples/alteriomMetricsHealth/platformio.ini +0 -26
  157. package/examples/alteriomPhase1/alteriom_sensor_package.hpp +0 -224
  158. package/examples/alteriomPhase1/phase1_features.ino +0 -242
  159. package/examples/alteriomPhase1/platformio.ini +0 -26
  160. package/examples/alteriomPhase2/alteriom_sensor_package.hpp +0 -224
  161. package/examples/alteriomPhase2/phase2_features.ino +0 -186
  162. package/examples/alteriomPhase2/platformio.ini +0 -26
  163. package/examples/alteriomSensorNode/alteriom_sensor_node.ino +0 -186
  164. package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +0 -1227
  165. package/examples/alteriomSensorNode/platformio.ini +0 -26
  166. package/examples/bridge/alteriom_sensor_package.hpp +0 -1170
  167. package/examples/bridge/bridge_health_monitoring_example.ino +0 -188
  168. package/examples/bridge/enhanced_mqtt_bridge.hpp +0 -610
  169. package/examples/bridge/enhanced_mqtt_bridge_example.ino +0 -226
  170. package/examples/bridge/mesh_event_publisher.hpp +0 -253
  171. package/examples/bridge/mesh_topology_reporter.hpp +0 -303
  172. package/examples/bridge/mqtt_command_bridge.hpp +0 -459
  173. package/examples/bridge/mqtt_status_bridge.hpp +0 -519
  174. package/examples/bridgeAwareSensorNode/alteriom_sensor_package.hpp +0 -1227
  175. package/examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino +0 -342
  176. package/examples/bridgeAwareSensorNode/platformio.ini +0 -26
  177. package/examples/diagnosticsExample/diagnosticsExample.ino +0 -171
  178. package/examples/diagnosticsExample/platformio.ini +0 -26
  179. package/examples/echoNode/echoNode.ino +0 -33
  180. package/examples/echoNode/platformio.ini +0 -26
  181. package/examples/meshCommandNode/alteriom_sensor_package.hpp +0 -235
  182. package/examples/meshCommandNode/meshCommandNode.ino +0 -265
  183. package/examples/mqttCommandBridge/alteriom_sensor_package.hpp +0 -235
  184. package/examples/mqttCommandBridge/mesh_event_publisher.hpp +0 -253
  185. package/examples/mqttCommandBridge/mesh_topology_reporter.hpp +0 -303
  186. package/examples/mqttCommandBridge/mqttCommandBridge.ino +0 -254
  187. package/examples/mqttCommandBridge/mqtt_command_bridge.hpp +0 -462
  188. package/examples/mqttCommandBridge/platformio.ini +0 -27
  189. package/examples/mqttStatusBridge/mqttStatusBridge.ino +0 -218
  190. package/examples/mqttStatusBridge/mqtt_status_bridge.hpp +0 -522
  191. package/examples/mqttStatusBridge/platformio.ini +0 -27
  192. package/examples/mqttTopologyTest/README.md +0 -467
  193. package/examples/mqttTopologyTest/mqttTopologyTest.ino +0 -754
  194. package/examples/mqttTopologyTest/platformio.ini +0 -27
  195. package/examples/multi_bridge/README.md +0 -346
  196. package/examples/multi_bridge/primary_bridge.ino +0 -96
  197. package/examples/multi_bridge/regular_node.ino +0 -141
  198. package/examples/multi_bridge/secondary_bridge.ino +0 -111
  199. package/examples/ntpTimeSyncBridge/alteriom_sensor_package.hpp +0 -1383
  200. package/examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino +0 -86
  201. package/examples/ntpTimeSyncNode/alteriom_sensor_package.hpp +0 -1383
  202. package/examples/ntpTimeSyncNode/ntpTimeSyncNode.ino +0 -109
  203. package/examples/queued_alarms/README.md +0 -390
  204. package/examples/queued_alarms/queued_alarms.ino +0 -265
  205. package/examples/routing_demo/README.md +0 -172
  206. package/examples/routing_demo/routing_demo.ino +0 -102
  207. package/examples/rtcIntegration/README.md +0 -294
  208. package/examples/rtcIntegration/rtcIntegration.ino +0 -210
@@ -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,8 +196,9 @@ 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
  });
@@ -260,7 +272,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
260
272
  * @param baseScheduler Task scheduler for mesh operations
261
273
  * @param port TCP port for mesh communication (default: 5555)
262
274
  */
263
- void initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
275
+ bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
264
276
  TSTRING routerSSID, TSTRING routerPassword,
265
277
  Scheduler *baseScheduler, uint16_t port = 5555) {
266
278
  using namespace logger;
@@ -298,14 +310,17 @@ class Mesh : public painlessmesh::Mesh<Connection> {
298
310
  detectedChannel = WiFi.channel();
299
311
  // Validate channel is in valid range (1-13 for 2.4GHz)
300
312
  if (detectedChannel < 1 || detectedChannel > 13) {
301
- Log(ERROR, "\n✗ Invalid channel detected: %d, using default channel 1\n", detectedChannel);
313
+ Log(ERROR, "\n✗ Invalid channel detected: %d, falling back to channel 1\n", detectedChannel);
302
314
  detectedChannel = 1;
303
315
  } else {
304
316
  Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
305
317
  Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
306
318
  }
307
319
  } else {
308
- Log(ERROR, "\n✗ Failed to connect to router, using default channel 1\n");
320
+ Log(ERROR, "\n✗ Failed to connect to router\n");
321
+ Log(ERROR, "Cannot become bridge without router connection\n");
322
+ Log(ERROR, "Bridge initialization aborted - remaining as regular node\n");
323
+ return false;
309
324
  }
310
325
 
311
326
  Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n", detectedChannel);
@@ -331,6 +346,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
331
346
  Log(STARTUP, " Mesh Channel: %d (matches router)\n", detectedChannel);
332
347
  Log(STARTUP, " Router: %s\n", routerSSID.c_str());
333
348
  Log(STARTUP, " Port: %d\n", port);
349
+ return true;
334
350
  }
335
351
 
336
352
  /**
@@ -347,7 +363,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
347
363
  * @param port TCP port for mesh communication (default: 5555)
348
364
  * @param priority Bridge priority: 10=highest (primary), 5=medium (secondary), 1=lowest (default: 5)
349
365
  */
350
- void initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
366
+ bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
351
367
  TSTRING routerSSID, TSTRING routerPassword,
352
368
  Scheduler *baseScheduler, uint16_t port, uint8_t priority) {
353
369
  using namespace logger;
@@ -370,12 +386,158 @@ class Mesh : public painlessmesh::Mesh<Connection> {
370
386
  priority, bridgeRole.c_str());
371
387
 
372
388
  // Call the base initAsBridge method
373
- initAsBridge(meshSSID, meshPassword, routerSSID, routerPassword, baseScheduler, port);
389
+ bool success = initAsBridge(meshSSID, meshPassword, routerSSID, routerPassword, baseScheduler, port);
374
390
 
375
- // Setup multi-bridge coordination if enabled
376
- if (multiBridgeEnabled) {
391
+ // Setup multi-bridge coordination if enabled and bridge init succeeded
392
+ if (success && multiBridgeEnabled) {
377
393
  initBridgeCoordination();
378
394
  }
395
+
396
+ return success;
397
+ }
398
+
399
+ /**
400
+ * Initialize mesh as a shared gateway node
401
+ *
402
+ * This method initializes all mesh nodes in AP+STA mode with router
403
+ * connectivity. Unlike initAsBridge() which creates a single bridge node,
404
+ * initAsSharedGateway() allows all nodes to connect to the router while
405
+ * maintaining mesh communication.
406
+ *
407
+ * Key features:
408
+ * - All nodes operate in AP+STA mode
409
+ * - All nodes connect to the same router
410
+ * - Mesh and router operate on the same channel for reliability
411
+ * - Automatic router reconnection on disconnect
412
+ * - Channel synchronization between mesh and router
413
+ *
414
+ * @param meshPrefix The name prefix for the mesh network
415
+ * @param meshPassword WiFi password for the mesh network
416
+ * @param routerSSID SSID of the router to connect to
417
+ * @param routerPassword Password for the router
418
+ * @param userScheduler Task scheduler for mesh operations
419
+ * @param port TCP port for mesh communication (default: 5555)
420
+ * @param config SharedGatewayConfig with advanced settings (optional)
421
+ * @return true if initialization succeeded, false otherwise
422
+ */
423
+ bool initAsSharedGateway(TSTRING meshPrefix, TSTRING meshPassword,
424
+ TSTRING routerSSID, TSTRING routerPassword,
425
+ Scheduler *userScheduler, uint16_t port = 5555,
426
+ gateway::SharedGatewayConfig config = gateway::SharedGatewayConfig()) {
427
+ using namespace logger;
428
+
429
+ Log(STARTUP, "=== Shared Gateway Mode Initialization ===\n");
430
+
431
+ // Validate configuration if enabled
432
+ if (config.enabled) {
433
+ auto result = config.validate();
434
+ if (!result.valid) {
435
+ Log(ERROR, "initAsSharedGateway(): Config validation failed: %s\n",
436
+ result.errorMessage.c_str());
437
+ return false;
438
+ }
439
+ }
440
+
441
+ // Store shared gateway configuration
442
+ _sharedGatewayConfig = config;
443
+ _sharedGatewayConfig.routerSSID = routerSSID;
444
+ _sharedGatewayConfig.routerPassword = routerPassword;
445
+ _sharedGatewayConfig.enabled = true;
446
+ _sharedGatewayMode = true;
447
+
448
+ Log(STARTUP, "Step 1: Scanning for router %s to detect channel...\n", routerSSID.c_str());
449
+
450
+ // Step 1: Scan for router to detect its channel
451
+ // We need to ensure mesh and router operate on the same channel
452
+ if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
453
+
454
+ #if ESP_ARDUINO_VERSION_MAJOR >= 3
455
+ WiFi.setAutoReconnect(false);
456
+ Log(STARTUP, "initAsSharedGateway(): AutoReconnect disabled\n");
457
+ #else
458
+ WiFi.setAutoConnect(false);
459
+ Log(STARTUP, "initAsSharedGateway(): AutoConnect disabled\n");
460
+ #endif
461
+ WiFi.persistent(false);
462
+ WiFi.mode(WIFI_STA);
463
+
464
+ // Connect to router to detect channel
465
+ WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
466
+
467
+ // Wait for connection with timeout (using constant for configurability)
468
+ int timeout = ROUTER_CONNECTION_TIMEOUT_SECONDS;
469
+ while (WiFi.status() != WL_CONNECTED && timeout > 0) {
470
+ delay(1000);
471
+ timeout--;
472
+ Log(STARTUP, ".");
473
+ }
474
+
475
+ uint8_t detectedChannel = 1; // Default fallback
476
+
477
+ if (WiFi.status() == WL_CONNECTED) {
478
+ detectedChannel = WiFi.channel();
479
+ // Validate channel is in valid range (1-14 for 2.4GHz, region-dependent)
480
+ if (detectedChannel < MIN_WIFI_CHANNEL || detectedChannel > MAX_WIFI_CHANNEL) {
481
+ Log(ERROR, "\n✗ Invalid channel detected: %d, falling back to channel 1\n", detectedChannel);
482
+ detectedChannel = 1;
483
+ } else {
484
+ Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
485
+ Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
486
+ }
487
+ } else {
488
+ Log(ERROR, "\n✗ Failed to connect to router during channel detection\n");
489
+ Log(ERROR, "Continuing with default channel 1, will retry router connection later\n");
490
+ }
491
+
492
+ // Disconnect from router, we'll reconnect after mesh init
493
+ WiFi.disconnect();
494
+ delay(100);
495
+
496
+ Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n", detectedChannel);
497
+
498
+ // Step 2: Initialize mesh on detected channel with AP+STA mode
499
+ // Set scheduler before init
500
+ this->setScheduler(userScheduler);
501
+ init(meshPrefix, meshPassword, port, WIFI_AP_STA, detectedChannel, 0, MAX_CONN);
502
+
503
+ Log(STARTUP, "Step 3: Establishing router connection in shared gateway mode...\n");
504
+
505
+ // Step 3: Establish router connection using stationManual
506
+ // Port 0 means we don't expect TCP mesh connection to the router
507
+ stationManual(routerSSID, routerPassword, 0);
508
+
509
+ // Step 4: Setup router connection monitoring and reconnection logic
510
+ initSharedGatewayMonitoring();
511
+
512
+ // Store router credentials for reconnection
513
+ setRouterCredentials(routerSSID, routerPassword);
514
+
515
+ Log(STARTUP, "=== Shared Gateway Mode Active ===\n");
516
+ Log(STARTUP, " Mesh Prefix: %s\n", meshPrefix.c_str());
517
+ Log(STARTUP, " Mesh Channel: %d (synced with router)\n", detectedChannel);
518
+ Log(STARTUP, " Router: %s\n", routerSSID.c_str());
519
+ Log(STARTUP, " Port: %d\n", port);
520
+ Log(STARTUP, " Mode: AP+STA (all nodes can connect to router)\n");
521
+
522
+ return true;
523
+ }
524
+
525
+ /**
526
+ * Check if shared gateway mode is enabled
527
+ *
528
+ * @return true if node is operating in shared gateway mode
529
+ */
530
+ bool isSharedGatewayMode() const {
531
+ return _sharedGatewayMode;
532
+ }
533
+
534
+ /**
535
+ * Get the shared gateway configuration
536
+ *
537
+ * @return const reference to the SharedGatewayConfig
538
+ */
539
+ const gateway::SharedGatewayConfig& getSharedGatewayConfig() const {
540
+ return _sharedGatewayConfig;
379
541
  }
380
542
 
381
543
  /**
@@ -549,6 +711,37 @@ class Mesh : public painlessmesh::Mesh<Connection> {
549
711
  minimumBridgeRSSI = minRSSI;
550
712
  }
551
713
 
714
+ /**
715
+ * Set the startup delay before first bridge election check
716
+ *
717
+ * Allows time for mesh network formation before starting bridge elections.
718
+ * Longer delays reduce the risk of split-brain scenarios when multiple nodes
719
+ * start simultaneously, ensuring nodes discover each other before elections.
720
+ *
721
+ * @param delayMs Startup delay in milliseconds (default: 60000 = 60 seconds, min: 10000)
722
+ */
723
+ void setElectionStartupDelay(uint32_t delayMs) {
724
+ if (delayMs < 10000) delayMs = 10000; // Minimum 10 seconds
725
+ electionStartupDelayMs = delayMs;
726
+ }
727
+
728
+ /**
729
+ * Set the random delay range for bridge elections
730
+ *
731
+ * When multiple nodes detect missing bridge simultaneously, randomized delays
732
+ * prevent all nodes from starting elections at the same instant. Longer delays
733
+ * provide more time for mesh discovery and reduce split-brain risk.
734
+ *
735
+ * @param minMs Minimum random delay in milliseconds (default: 1000 = 1 second)
736
+ * @param maxMs Maximum random delay in milliseconds (default: 3000 = 3 seconds)
737
+ */
738
+ void setElectionRandomDelay(uint32_t minMs, uint32_t maxMs) {
739
+ if (minMs < 100) minMs = 100; // Minimum 100ms
740
+ if (maxMs < minMs) maxMs = minMs + 1000; // Ensure max > min
741
+ electionRandomDelayMinMs = minMs;
742
+ electionRandomDelayMaxMs = maxMs;
743
+ }
744
+
552
745
  /**
553
746
  * Set callback for when this node's bridge role changes
554
747
  *
@@ -1270,12 +1463,35 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1270
1463
  delay(1000);
1271
1464
  Log(STARTUP, "✓ Takeover announcement sent on channel %d\n", _meshChannel);
1272
1465
 
1466
+ // Save current mesh configuration to restore if bridge init fails
1467
+ uint8_t savedChannel = _meshChannel;
1468
+
1273
1469
  // Now reconfigure as bridge (this will switch to router's channel)
1274
1470
  this->stop();
1275
1471
  delay(1000);
1276
1472
 
1277
- this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
1278
- mScheduler, _meshPort);
1473
+ bool bridgeInitSuccess = this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
1474
+ mScheduler, _meshPort);
1475
+
1476
+ if (!bridgeInitSuccess) {
1477
+ Log(ERROR, "✗ Bridge promotion failed - router unreachable\n");
1478
+ Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
1479
+
1480
+ // Re-initialize as regular node on the original channel
1481
+ this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
1482
+ savedChannel, _meshHidden, MAX_CONN);
1483
+
1484
+ // Reset election state and clear candidates (consistent with normal election completion)
1485
+ electionState = ELECTION_IDLE;
1486
+ electionCandidates.clear();
1487
+
1488
+ // Notify via callback
1489
+ if (bridgeRoleChangedCallback) {
1490
+ bridgeRoleChangedCallback(false, "Bridge promotion failed - router unreachable");
1491
+ }
1492
+
1493
+ return;
1494
+ }
1279
1495
 
1280
1496
  lastRoleChangeTime = millis();
1281
1497
 
@@ -1535,6 +1751,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1535
1751
  TSTRING routerPassword = "";
1536
1752
  uint32_t electionTimeoutMs = 5000; // Default 5 seconds
1537
1753
  int8_t minimumBridgeRSSI = -80; // Default -80 dBm minimum for isolated elections
1754
+ uint32_t electionStartupDelayMs = 60000; // Default 60 seconds before first election check
1755
+ uint32_t electionRandomDelayMinMs = 1000; // Default min 1 second random delay
1756
+ uint32_t electionRandomDelayMaxMs = 3000; // Default max 3 seconds random delay
1538
1757
  uint32_t lastRoleChangeTime = 0;
1539
1758
  ElectionState electionState = ELECTION_IDLE;
1540
1759
  uint32_t electionDeadline = 0;
@@ -1553,6 +1772,154 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1553
1772
  std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
1554
1773
  uint32_t selectedBridgeOverride = 0; // Manual bridge selection override
1555
1774
  size_t lastSelectedBridgeIndex = 0; // For round-robin selection
1775
+
1776
+ // Shared gateway mode state and configuration
1777
+ bool _sharedGatewayMode = false;
1778
+ gateway::SharedGatewayConfig _sharedGatewayConfig;
1779
+ std::shared_ptr<Task> _sharedGatewayMonitorTask;
1780
+ uint32_t _lastRouterReconnectAttempt = 0;
1781
+ uint8_t _routerReconnectAttempts = 0;
1782
+ static const uint8_t MAX_ROUTER_RECONNECT_ATTEMPTS = 10;
1783
+ static const uint32_t ROUTER_RECONNECT_BASE_INTERVAL = 5000; // 5 seconds base interval
1784
+ static const uint32_t ROUTER_RECONNECT_MAX_INTERVAL = 300000; // 5 minutes max interval
1785
+ static const int ROUTER_CONNECTION_TIMEOUT_SECONDS = 30; // Router connection timeout
1786
+ static const uint8_t MIN_WIFI_CHANNEL = 1;
1787
+ static const uint8_t MAX_WIFI_CHANNEL = 14; // Support channels 1-14 for regions that allow it
1788
+
1789
+ /**
1790
+ * Initialize shared gateway monitoring
1791
+ *
1792
+ * Sets up periodic monitoring of router connection and automatic
1793
+ * reconnection logic for shared gateway mode.
1794
+ */
1795
+ void initSharedGatewayMonitoring() {
1796
+ using namespace logger;
1797
+
1798
+ if (!_sharedGatewayMode) {
1799
+ return;
1800
+ }
1801
+
1802
+ Log(STARTUP, "initSharedGatewayMonitoring(): Setting up router connection monitoring\n");
1803
+
1804
+ // Add callback for router disconnection in shared gateway mode
1805
+ this->droppedConnectionCallbacks.push_back(
1806
+ [this](uint32_t nodeId, bool station) {
1807
+ if (station && _sharedGatewayMode) {
1808
+ Log(CONNECTION, "Router disconnected in shared gateway mode, scheduling reconnection\n");
1809
+ scheduleRouterReconnect();
1810
+ }
1811
+ });
1812
+
1813
+ // Create periodic monitoring task
1814
+ _sharedGatewayMonitorTask = this->addTask(
1815
+ _sharedGatewayConfig.internetCheckInterval,
1816
+ TASK_FOREVER,
1817
+ [this]() {
1818
+ monitorRouterConnection();
1819
+ });
1820
+
1821
+ Log(STARTUP, "Router connection monitoring enabled (interval: %u ms)\n",
1822
+ _sharedGatewayConfig.internetCheckInterval);
1823
+ }
1824
+
1825
+ /**
1826
+ * Monitor router connection in shared gateway mode
1827
+ *
1828
+ * Checks router connectivity and triggers reconnection if needed.
1829
+ */
1830
+ void monitorRouterConnection() {
1831
+ using namespace logger;
1832
+
1833
+ if (!_sharedGatewayMode) {
1834
+ return;
1835
+ }
1836
+
1837
+ bool isConnected = (WiFi.status() == WL_CONNECTED) &&
1838
+ (WiFi.localIP() != IPAddress(0, 0, 0, 0));
1839
+
1840
+ if (!isConnected) {
1841
+ Log(CONNECTION, "monitorRouterConnection(): Router connection lost, triggering reconnect\n");
1842
+ scheduleRouterReconnect();
1843
+ } else {
1844
+ // Connection is healthy, reset reconnect attempts
1845
+ _routerReconnectAttempts = 0;
1846
+
1847
+ // Log periodic status
1848
+ Log(GENERAL, "monitorRouterConnection(): Router connected (RSSI: %d dBm, IP: %s)\n",
1849
+ WiFi.RSSI(), WiFi.localIP().toString().c_str());
1850
+ }
1851
+ }
1852
+
1853
+ /**
1854
+ * Schedule router reconnection with exponential backoff
1855
+ */
1856
+ void scheduleRouterReconnect() {
1857
+ using namespace logger;
1858
+
1859
+ if (!_sharedGatewayMode) {
1860
+ return;
1861
+ }
1862
+
1863
+ // Don't schedule if already connected
1864
+ if (WiFi.status() == WL_CONNECTED) {
1865
+ return;
1866
+ }
1867
+
1868
+ // Limit reconnection attempts
1869
+ if (_routerReconnectAttempts >= MAX_ROUTER_RECONNECT_ATTEMPTS) {
1870
+ Log(ERROR, "scheduleRouterReconnect(): Max reconnection attempts reached (%d)\n",
1871
+ MAX_ROUTER_RECONNECT_ATTEMPTS);
1872
+ Log(ERROR, "Router reconnection suspended. Manual intervention may be required.\n");
1873
+ return;
1874
+ }
1875
+
1876
+ // Calculate delay with exponential backoff, preventing overflow
1877
+ // Limit shift amount to prevent overflow (5000 * 2^6 = 320000 is safe)
1878
+ uint8_t shiftAmount = (_routerReconnectAttempts > 6) ? 6 : _routerReconnectAttempts;
1879
+ uint32_t delay = ROUTER_RECONNECT_BASE_INTERVAL * (1UL << shiftAmount);
1880
+ if (delay > ROUTER_RECONNECT_MAX_INTERVAL) delay = ROUTER_RECONNECT_MAX_INTERVAL;
1881
+
1882
+ // Don't reconnect too frequently
1883
+ uint32_t now = millis();
1884
+ if (now - _lastRouterReconnectAttempt < delay) {
1885
+ return;
1886
+ }
1887
+
1888
+ _routerReconnectAttempts++;
1889
+ _lastRouterReconnectAttempt = now;
1890
+
1891
+ Log(CONNECTION, "scheduleRouterReconnect(): Attempting reconnection (attempt %d/%d, delay %u ms)\n",
1892
+ _routerReconnectAttempts, MAX_ROUTER_RECONNECT_ATTEMPTS, delay);
1893
+
1894
+ // Schedule reconnection
1895
+ this->addTask(delay, TASK_ONCE, [this]() {
1896
+ attemptRouterReconnect();
1897
+ });
1898
+ }
1899
+
1900
+ /**
1901
+ * Attempt to reconnect to the router
1902
+ */
1903
+ void attemptRouterReconnect() {
1904
+ using namespace logger;
1905
+
1906
+ if (!_sharedGatewayMode) {
1907
+ return;
1908
+ }
1909
+
1910
+ // Check if already connected
1911
+ if (WiFi.status() == WL_CONNECTED) {
1912
+ Log(CONNECTION, "attemptRouterReconnect(): Already connected to router\n");
1913
+ _routerReconnectAttempts = 0;
1914
+ return;
1915
+ }
1916
+
1917
+ Log(CONNECTION, "attemptRouterReconnect(): Reconnecting to router %s...\n",
1918
+ _sharedGatewayConfig.routerSSID.c_str());
1919
+
1920
+ // Use stationManual to reconnect (port 0 means no TCP mesh connection to router)
1921
+ stationManual(_sharedGatewayConfig.routerSSID, _sharedGatewayConfig.routerPassword, 0);
1922
+ }
1556
1923
  };
1557
1924
  } // namespace wifi
1558
1925
  }; // namespace painlessmesh