@alteriom/painlessmesh 1.9.5 → 1.9.7

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.
@@ -13,6 +13,14 @@
13
13
  #include "painlessmesh/router.hpp"
14
14
  #include "painlessmesh/tcp.hpp"
15
15
 
16
+ #ifdef ESP32
17
+ #include <HTTPClient.h>
18
+ #include <WiFiClientSecure.h>
19
+ #elif defined(ESP8266)
20
+ #include <ESP8266HTTPClient.h>
21
+ #include <WiFiClientSecure.h>
22
+ #endif
23
+
16
24
  extern painlessmesh::logger::LogClass Log;
17
25
 
18
26
  namespace painlessmesh {
@@ -93,21 +101,23 @@ class Mesh : public painlessmesh::Mesh<Connection> {
93
101
  // Add bridge election package handler (Type BRIDGE_ELECTION)
94
102
  this->callbackList.onPackage(
95
103
  protocol::BRIDGE_ELECTION,
96
- [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
104
+ [this](protocol::Variant& variant, std::shared_ptr<Connection>,
105
+ uint32_t) {
97
106
  JsonDocument doc;
98
107
  TSTRING str;
99
108
  variant.printTo(str);
100
109
  deserializeJson(doc, str);
101
110
  JsonObject obj = doc.as<JsonObject>();
102
-
111
+
103
112
  if (obj["routerRSSI"].is<int>()) {
104
113
  uint32_t fromNode = obj["from"];
105
114
  int8_t routerRSSI = obj["routerRSSI"];
106
115
  uint32_t uptime = obj["uptime"] | 0;
107
116
  uint32_t freeMemory = obj["freeMemory"] | 0;
108
-
109
- this->handleBridgeElection(fromNode, routerRSSI, uptime, freeMemory);
110
-
117
+
118
+ this->handleBridgeElection(fromNode, routerRSSI, uptime,
119
+ freeMemory);
120
+
111
121
  Log(CONNECTION, "Bridge election candidate from %u: RSSI %d dBm\n",
112
122
  fromNode, routerRSSI);
113
123
  }
@@ -117,21 +127,22 @@ class Mesh : public painlessmesh::Mesh<Connection> {
117
127
  // Add bridge takeover package handler (Type BRIDGE_TAKEOVER)
118
128
  this->callbackList.onPackage(
119
129
  protocol::BRIDGE_TAKEOVER,
120
- [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
130
+ [this](protocol::Variant& variant, std::shared_ptr<Connection>,
131
+ uint32_t) {
121
132
  JsonDocument doc;
122
133
  TSTRING str;
123
134
  variant.printTo(str);
124
135
  deserializeJson(doc, str);
125
136
  JsonObject obj = doc.as<JsonObject>();
126
-
137
+
127
138
  if (obj["previousBridge"].is<unsigned int>()) {
128
139
  uint32_t newBridge = obj["from"];
129
140
  uint32_t previousBridge = obj["previousBridge"];
130
141
  TSTRING reason = obj["reason"].as<TSTRING>();
131
-
142
+
132
143
  Log(CONNECTION, "Bridge takeover: Node %u replaced %u (%s)\n",
133
144
  newBridge, previousBridge, reason.c_str());
134
-
145
+
135
146
  // Notify callback if this node was not the winner
136
147
  if (newBridge != this->nodeId && bridgeRoleChangedCallback) {
137
148
  bridgeRoleChangedCallback(false, "Another node won election");
@@ -141,17 +152,19 @@ class Mesh : public painlessmesh::Mesh<Connection> {
141
152
  });
142
153
 
143
154
  // Add callback to detect bridge failures and trigger elections
144
- this->onBridgeStatusChanged([this](uint32_t bridgeNodeId, bool hasInternet) {
145
- if (!hasInternet && bridgeFailoverEnabled && routerCredentialsConfigured) {
146
- Log(CONNECTION, "Bridge %u lost Internet, considering election...\n", bridgeNodeId);
147
-
155
+ this->onBridgeStatusChanged([this](uint32_t bridgeNodeId,
156
+ bool hasInternet) {
157
+ if (!hasInternet && bridgeFailoverEnabled &&
158
+ routerCredentialsConfigured) {
159
+ Log(CONNECTION, "Bridge %u lost Internet, considering election...\n",
160
+ bridgeNodeId);
161
+
148
162
  // Check if we still have any healthy bridges
149
163
  if (!this->hasInternetConnection()) {
150
164
  Log(CONNECTION, "No healthy bridges, starting election\n");
151
165
  // Small delay to let all nodes detect the failure
152
- this->addTask(2000, TASK_ONCE, [this]() {
153
- this->startBridgeElection();
154
- });
166
+ this->addTask(2000, TASK_ONCE,
167
+ [this]() { this->startBridgeElection(); });
155
168
  }
156
169
  }
157
170
  });
@@ -163,27 +176,28 @@ class Mesh : public painlessmesh::Mesh<Connection> {
163
176
  if (!bridgeFailoverEnabled || !routerCredentialsConfigured) {
164
177
  return;
165
178
  }
166
-
179
+
167
180
  // Don't check if we're already a bridge
168
181
  if (this->isBridge()) {
169
182
  return;
170
183
  }
171
-
184
+
172
185
  // Skip check during startup period to allow initial bridge discovery
173
186
  if (millis() < electionStartupDelayMs) {
174
187
  return;
175
188
  }
176
-
189
+
177
190
  // IMPORTANT: Don't trigger election if we're disconnected from the mesh
178
191
  // 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.
192
+ // healthy bridge could simply mean WE are disconnected, not that the
193
+ // bridge is unavailable. Wait until mesh connectivity is restored before
194
+ // considering an election.
182
195
  if (!this->hasActiveMeshConnections()) {
183
- Log(CONNECTION, "Bridge monitor: Skipping - no active mesh connections\n");
196
+ Log(CONNECTION,
197
+ "Bridge monitor: Skipping - no active mesh connections\n");
184
198
  return;
185
199
  }
186
-
200
+
187
201
  // Check if there are any healthy bridges
188
202
  bool hasHealthyBridge = false;
189
203
  for (const auto& bridge : this->getBridges()) {
@@ -192,16 +206,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
192
206
  break;
193
207
  }
194
208
  }
195
-
209
+
196
210
  // If no healthy bridge exists, trigger an election
197
211
  if (!hasHealthyBridge) {
198
- Log(CONNECTION, "Bridge monitor: No healthy bridge detected, triggering election\n");
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);
202
- this->addTask(randomDelay, TASK_ONCE, [this]() {
203
- this->startBridgeElection();
204
- });
212
+ Log(CONNECTION,
213
+ "Bridge monitor: No healthy bridge detected, triggering "
214
+ "election\n");
215
+ // Random delay to prevent simultaneous elections when multiple nodes
216
+ // start together
217
+ uint32_t randomDelay =
218
+ random(electionRandomDelayMinMs, electionRandomDelayMaxMs);
219
+ Log(CONNECTION, "Bridge monitor: Scheduling election in %u ms\n",
220
+ randomDelay);
221
+ this->addTask(randomDelay, TASK_ONCE,
222
+ [this]() { this->startBridgeElection(); });
205
223
  }
206
224
  });
207
225
 
@@ -210,62 +228,83 @@ class Mesh : public painlessmesh::Mesh<Connection> {
210
228
  // - Has router credentials configured
211
229
  // - Is isolated (no mesh connections)
212
230
  // - Should attempt to become a bridge directly
213
- // This is different from the election mechanism which requires mesh connectivity
231
+ // This is different from the election mechanism which requires mesh
232
+ // connectivity
214
233
  this->addTask(isolatedBridgeRetryIntervalMs, TASK_FOREVER, [this]() {
215
234
  // Only retry if failover is enabled and we have credentials
216
235
  if (!bridgeFailoverEnabled || !routerCredentialsConfigured) {
217
236
  return;
218
237
  }
219
-
238
+
220
239
  // Don't retry if we're already a bridge
221
240
  if (this->isBridge()) {
222
241
  return;
223
242
  }
224
-
243
+
225
244
  // Skip during startup period
226
245
  if (millis() < electionStartupDelayMs) {
227
246
  return;
228
247
  }
229
-
248
+
230
249
  // Only retry when isolated (no mesh connections found)
231
250
  if (this->hasActiveMeshConnections()) {
232
- // Reset retry counter when mesh is active
251
+ // Reset retry counter and pending flag when mesh is active (no longer
252
+ // isolated)
233
253
  _isolatedBridgeRetryAttempts = 0;
254
+ _isolatedRetryPending = false;
234
255
  return;
235
256
  }
236
-
257
+
237
258
  // Limit retry attempts with reset after timeout
238
259
  if (_isolatedBridgeRetryAttempts >= MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS) {
239
260
  // Check if enough time has passed to reset the counter
240
261
  if (millis() > _isolatedBridgeRetryResetTime) {
241
- Log(CONNECTION, "Isolated bridge retry: Reset timeout reached, resetting attempt counter\n");
262
+ Log(CONNECTION,
263
+ "Isolated bridge retry: Reset timeout reached, resetting attempt "
264
+ "counter\n");
242
265
  _isolatedBridgeRetryAttempts = 0;
243
266
  } 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);
267
+ Log(CONNECTION,
268
+ "Isolated bridge retry: Max attempts (%d) reached, reset in %u "
269
+ "seconds\n",
270
+ MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS,
271
+ (_isolatedBridgeRetryResetTime - millis()) / 1000);
246
272
  return;
247
273
  }
248
274
  }
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
275
+
276
+ // Check if mesh network exists on any channel before trying to become
277
+ // bridge If mesh exists but we can't connect, don't try to become bridge
278
+ // Skip this check if we already confirmed isolation from a previous
279
+ // failed attempt
252
280
  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",
281
+ if (!_isolatedRetryPending &&
282
+ emptyScans < ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD) {
283
+ Log(CONNECTION,
284
+ "Isolated bridge retry: Only %d empty scans, waiting for more "
285
+ "scans\n",
255
286
  emptyScans);
256
287
  return;
257
288
  }
258
-
259
- Log(CONNECTION, "Isolated bridge retry: Node isolated with %d empty scans, attempting bridge promotion\n",
289
+
290
+ // Clear the pending flag now that we're proceeding
291
+ _isolatedRetryPending = false;
292
+
293
+ Log(CONNECTION,
294
+ "Isolated bridge retry: Node isolated with %d empty scans, "
295
+ "attempting bridge promotion\n",
260
296
  emptyScans);
261
-
262
- // Attempt to become bridge directly (bypassing election since we're isolated)
263
- // Only increment retry counter if we actually attempted promotion
297
+
298
+ // Attempt to become bridge directly (bypassing election since we're
299
+ // isolated) Only increment retry counter if we actually attempted
300
+ // promotion
264
301
  if (this->attemptIsolatedBridgePromotion()) {
265
302
  _isolatedBridgeRetryAttempts++;
266
303
  // Set reset time when reaching max attempts
267
- if (_isolatedBridgeRetryAttempts >= MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS) {
268
- _isolatedBridgeRetryResetTime = millis() + isolatedBridgeRetryResetIntervalMs;
304
+ if (_isolatedBridgeRetryAttempts >=
305
+ MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS) {
306
+ _isolatedBridgeRetryResetTime =
307
+ millis() + isolatedBridgeRetryResetIntervalMs;
269
308
  }
270
309
  }
271
310
  });
@@ -277,16 +316,16 @@ class Mesh : public painlessmesh::Mesh<Connection> {
277
316
  if (connectMode & WIFI_AP) {
278
317
  apInit(nodeId); // setup AP
279
318
  }
280
-
319
+
281
320
  // Initialize TCP server AFTER AP is configured
282
321
  // This ensures the network interfaces are ready when the server starts
283
322
  // Fixes TCP connection error -14 when nodes try to connect to bridge
284
323
  tcpServerInit();
285
-
324
+
286
325
  if (connectMode & WIFI_STA) {
287
326
  this->initStation();
288
327
  }
289
-
328
+
290
329
  // If station credentials provided, connect to router
291
330
  if (!stationSSID.isEmpty() && (connectMode & WIFI_STA)) {
292
331
  Log(STARTUP, "init(): Connecting to station %s\n", stationSSID.c_str());
@@ -311,30 +350,29 @@ class Mesh : public painlessmesh::Mesh<Connection> {
311
350
  * @param connectMode Switch between WIFI_AP, WIFI_STA and WIFI_AP_STA
312
351
  * (default) mode
313
352
  */
314
- void init(TSTRING ssid, TSTRING password, Scheduler *baseScheduler,
353
+ void init(TSTRING ssid, TSTRING password, Scheduler* baseScheduler,
315
354
  uint16_t port = 5555, WiFiMode_t connectMode = WIFI_AP_STA,
316
- uint8_t channel = 1, uint8_t hidden = 0,
317
- uint8_t maxconn = MAX_CONN,
355
+ uint8_t channel = 1, uint8_t hidden = 0, uint8_t maxconn = MAX_CONN,
318
356
  TSTRING stationSSID = "", TSTRING stationPassword = "") {
319
357
  this->setScheduler(baseScheduler);
320
- init(ssid, password, port, connectMode, channel, hidden, maxconn,
358
+ init(ssid, password, port, connectMode, channel, hidden, maxconn,
321
359
  stationSSID, stationPassword);
322
360
  }
323
361
 
324
362
  /**
325
363
  * Initialize mesh as a bridge node with automatic channel detection
326
- *
364
+ *
327
365
  * This method connects to a router first, detects its channel, then
328
366
  * initializes the mesh on the same channel. This ensures the bridge
329
367
  * can maintain both router and mesh connections on the same channel.
330
- *
368
+ *
331
369
  * The bridge node will automatically:
332
370
  * - Connect to the specified router in STA mode
333
371
  * - Detect the router's WiFi channel
334
372
  * - Initialize the mesh AP on the detected channel
335
373
  * - Set itself as root node
336
374
  * - Maintain the router connection
337
- *
375
+ *
338
376
  * @param meshSSID The name of your mesh network
339
377
  * @param meshPassword WiFi password for the mesh
340
378
  * @param routerSSID SSID of the router to connect to
@@ -342,18 +380,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
342
380
  * @param baseScheduler Task scheduler for mesh operations
343
381
  * @param port TCP port for mesh communication (default: 5555)
344
382
  */
345
- bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
346
- TSTRING routerSSID, TSTRING routerPassword,
347
- Scheduler *baseScheduler, uint16_t port = 5555) {
383
+ bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword, TSTRING routerSSID,
384
+ TSTRING routerPassword, Scheduler* baseScheduler,
385
+ uint16_t port = 5555) {
348
386
  using namespace logger;
349
-
387
+
350
388
  Log(STARTUP, "=== Bridge Mode Initialization ===\n");
351
389
  Log(STARTUP, "Step 1: Connecting to router %s...\n", routerSSID.c_str());
352
-
390
+
353
391
  // Step 1: Connect to router first to detect its channel
354
392
  // Shut Wifi down and start with a blank slate
355
393
  if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
356
-
394
+
357
395
  Log(STARTUP, "initAsBridge(): %d\n",
358
396
  #if ESP_ARDUINO_VERSION_MAJOR >= 3
359
397
  WiFi.setAutoReconnect(false));
@@ -362,10 +400,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
362
400
  #endif
363
401
  WiFi.persistent(false);
364
402
  WiFi.mode(WIFI_STA);
365
-
403
+
366
404
  // Connect to router and wait for connection
367
405
  WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
368
-
406
+
369
407
  // Wait for connection (with timeout)
370
408
  int timeout = 30; // 30 seconds timeout
371
409
  while (WiFi.status() != WL_CONNECTED && timeout > 0) {
@@ -373,14 +411,16 @@ class Mesh : public painlessmesh::Mesh<Connection> {
373
411
  timeout--;
374
412
  Log(STARTUP, ".");
375
413
  }
376
-
414
+
377
415
  uint8_t detectedChannel = 1; // Default fallback
378
-
416
+
379
417
  if (WiFi.status() == WL_CONNECTED) {
380
418
  detectedChannel = WiFi.channel();
381
419
  // Validate channel is in valid range (1-13 for 2.4GHz)
382
420
  if (detectedChannel < 1 || detectedChannel > 13) {
383
- Log(ERROR, "\n✗ Invalid channel detected: %d, falling back to channel 1\n", detectedChannel);
421
+ Log(ERROR,
422
+ "\n✗ Invalid channel detected: %d, falling back to channel 1\n",
423
+ detectedChannel);
384
424
  detectedChannel = 1;
385
425
  } else {
386
426
  Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
@@ -392,29 +432,33 @@ class Mesh : public painlessmesh::Mesh<Connection> {
392
432
  Log(ERROR, "Bridge initialization aborted - remaining as regular node\n");
393
433
  return false;
394
434
  }
395
-
396
- Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n", detectedChannel);
397
-
435
+
436
+ Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
437
+ detectedChannel);
438
+
398
439
  // Step 2: Initialize mesh on detected channel
399
- init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
440
+ init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
400
441
  detectedChannel, 0, MAX_CONN);
401
-
442
+
402
443
  // Allow network stack to stabilize after AP and TCP server initialization
403
444
  // This prevents TCP connection errors (-14) when nodes connect
404
445
  delay(100);
405
-
446
+
406
447
  Log(STARTUP, "Step 3: Establishing bridge connection...\n");
407
-
448
+
408
449
  // Step 3: Re-establish router connection using stationManual
409
450
  stationManual(routerSSID, routerPassword, 0);
410
-
451
+
411
452
  // Step 4: Configure as root/bridge node
412
453
  this->setRoot(true);
413
454
  this->setContainsRoot(true);
414
-
455
+
415
456
  // Step 5: Setup bridge status broadcasting
416
457
  initBridgeStatusBroadcast();
417
-
458
+
459
+ // Step 6: Setup gateway Internet handler
460
+ initGatewayInternetHandler();
461
+
418
462
  Log(STARTUP, "=== Bridge Mode Active ===\n");
419
463
  Log(STARTUP, " Mesh SSID: %s\n", meshSSID.c_str());
420
464
  Log(STARTUP, " Mesh Channel: %d (matches router)\n", detectedChannel);
@@ -425,28 +469,30 @@ class Mesh : public painlessmesh::Mesh<Connection> {
425
469
 
426
470
  /**
427
471
  * Initialize mesh as a bridge node with priority (for multi-bridge mode)
428
- *
429
- * This overload adds bridge priority configuration for multi-bridge deployments.
430
- * Priority determines which bridge is preferred when multiple bridges are available.
431
- *
472
+ *
473
+ * This overload adds bridge priority configuration for multi-bridge
474
+ * deployments. Priority determines which bridge is preferred when multiple
475
+ * bridges are available.
476
+ *
432
477
  * @param meshSSID The name of your mesh network
433
478
  * @param meshPassword WiFi password for the mesh
434
479
  * @param routerSSID SSID of the router to connect to
435
480
  * @param routerPassword Password for the router
436
481
  * @param baseScheduler Task scheduler for mesh operations
437
482
  * @param port TCP port for mesh communication (default: 5555)
438
- * @param priority Bridge priority: 10=highest (primary), 5=medium (secondary), 1=lowest (default: 5)
483
+ * @param priority Bridge priority: 10=highest (primary), 5=medium
484
+ * (secondary), 1=lowest (default: 5)
439
485
  */
440
- bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
441
- TSTRING routerSSID, TSTRING routerPassword,
442
- Scheduler *baseScheduler, uint16_t port, uint8_t priority) {
486
+ bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword, TSTRING routerSSID,
487
+ TSTRING routerPassword, Scheduler* baseScheduler,
488
+ uint16_t port, uint8_t priority) {
443
489
  using namespace logger;
444
-
490
+
445
491
  // Validate and store priority
446
492
  if (priority < 1) priority = 1;
447
493
  if (priority > 10) priority = 10;
448
494
  bridgePriority = priority;
449
-
495
+
450
496
  // Store role based on priority
451
497
  if (priority >= 8) {
452
498
  bridgeRole = "primary";
@@ -455,36 +501,38 @@ class Mesh : public painlessmesh::Mesh<Connection> {
455
501
  } else {
456
502
  bridgeRole = "standby";
457
503
  }
458
-
459
- Log(STARTUP, "=== Bridge Mode Initialization (Priority: %d, Role: %s) ===\n",
504
+
505
+ Log(STARTUP,
506
+ "=== Bridge Mode Initialization (Priority: %d, Role: %s) ===\n",
460
507
  priority, bridgeRole.c_str());
461
-
508
+
462
509
  // Call the base initAsBridge method
463
- bool success = initAsBridge(meshSSID, meshPassword, routerSSID, routerPassword, baseScheduler, port);
464
-
510
+ bool success = initAsBridge(meshSSID, meshPassword, routerSSID,
511
+ routerPassword, baseScheduler, port);
512
+
465
513
  // Setup multi-bridge coordination if enabled and bridge init succeeded
466
514
  if (success && multiBridgeEnabled) {
467
515
  initBridgeCoordination();
468
516
  }
469
-
517
+
470
518
  return success;
471
519
  }
472
520
 
473
521
  /**
474
522
  * Initialize mesh as a shared gateway node
475
- *
523
+ *
476
524
  * This method initializes all mesh nodes in AP+STA mode with router
477
525
  * connectivity. Unlike initAsBridge() which creates a single bridge node,
478
526
  * initAsSharedGateway() allows all nodes to connect to the router while
479
527
  * maintaining mesh communication.
480
- *
528
+ *
481
529
  * Key features:
482
530
  * - All nodes operate in AP+STA mode
483
531
  * - All nodes connect to the same router
484
532
  * - Mesh and router operate on the same channel for reliability
485
533
  * - Automatic router reconnection on disconnect
486
534
  * - Channel synchronization between mesh and router
487
- *
535
+ *
488
536
  * @param meshPrefix The name prefix for the mesh network
489
537
  * @param meshPassword WiFi password for the mesh network
490
538
  * @param routerSSID SSID of the router to connect to
@@ -494,14 +542,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
494
542
  * @param config SharedGatewayConfig with advanced settings (optional)
495
543
  * @return true if initialization succeeded, false otherwise
496
544
  */
497
- bool initAsSharedGateway(TSTRING meshPrefix, TSTRING meshPassword,
498
- TSTRING routerSSID, TSTRING routerPassword,
499
- Scheduler *userScheduler, uint16_t port = 5555,
500
- gateway::SharedGatewayConfig config = gateway::SharedGatewayConfig()) {
545
+ bool initAsSharedGateway(
546
+ TSTRING meshPrefix, TSTRING meshPassword, TSTRING routerSSID,
547
+ TSTRING routerPassword, Scheduler* userScheduler, uint16_t port = 5555,
548
+ gateway::SharedGatewayConfig config = gateway::SharedGatewayConfig()) {
501
549
  using namespace logger;
502
-
550
+
503
551
  Log(STARTUP, "=== Shared Gateway Mode Initialization ===\n");
504
-
552
+
505
553
  // Validate configuration if enabled
506
554
  if (config.enabled) {
507
555
  auto result = config.validate();
@@ -511,20 +559,21 @@ class Mesh : public painlessmesh::Mesh<Connection> {
511
559
  return false;
512
560
  }
513
561
  }
514
-
562
+
515
563
  // Store shared gateway configuration
516
564
  _sharedGatewayConfig = config;
517
565
  _sharedGatewayConfig.routerSSID = routerSSID;
518
566
  _sharedGatewayConfig.routerPassword = routerPassword;
519
567
  _sharedGatewayConfig.enabled = true;
520
568
  _sharedGatewayMode = true;
521
-
522
- Log(STARTUP, "Step 1: Scanning for router %s to detect channel...\n", routerSSID.c_str());
523
-
569
+
570
+ Log(STARTUP, "Step 1: Scanning for router %s to detect channel...\n",
571
+ routerSSID.c_str());
572
+
524
573
  // Step 1: Scan for router to detect its channel
525
574
  // We need to ensure mesh and router operate on the same channel
526
575
  if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
527
-
576
+
528
577
  #if ESP_ARDUINO_VERSION_MAJOR >= 3
529
578
  WiFi.setAutoReconnect(false);
530
579
  Log(STARTUP, "initAsSharedGateway(): AutoReconnect disabled\n");
@@ -534,10 +583,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
534
583
  #endif
535
584
  WiFi.persistent(false);
536
585
  WiFi.mode(WIFI_STA);
537
-
586
+
538
587
  // Connect to router to detect channel
539
588
  WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
540
-
589
+
541
590
  // Wait for connection with timeout (using constant for configurability)
542
591
  int timeout = ROUTER_CONNECTION_TIMEOUT_SECONDS;
543
592
  while (WiFi.status() != WL_CONNECTED && timeout > 0) {
@@ -545,14 +594,17 @@ class Mesh : public painlessmesh::Mesh<Connection> {
545
594
  timeout--;
546
595
  Log(STARTUP, ".");
547
596
  }
548
-
597
+
549
598
  uint8_t detectedChannel = 1; // Default fallback
550
-
599
+
551
600
  if (WiFi.status() == WL_CONNECTED) {
552
601
  detectedChannel = WiFi.channel();
553
602
  // Validate channel is in valid range (1-14 for 2.4GHz, region-dependent)
554
- if (detectedChannel < MIN_WIFI_CHANNEL || detectedChannel > MAX_WIFI_CHANNEL) {
555
- Log(ERROR, "\n✗ Invalid channel detected: %d, falling back to channel 1\n", detectedChannel);
603
+ if (detectedChannel < MIN_WIFI_CHANNEL ||
604
+ detectedChannel > MAX_WIFI_CHANNEL) {
605
+ Log(ERROR,
606
+ "\n✗ Invalid channel detected: %d, falling back to channel 1\n",
607
+ detectedChannel);
556
608
  detectedChannel = 1;
557
609
  } else {
558
610
  Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
@@ -560,58 +612,64 @@ class Mesh : public painlessmesh::Mesh<Connection> {
560
612
  }
561
613
  } else {
562
614
  Log(ERROR, "\n✗ Failed to connect to router during channel detection\n");
563
- Log(ERROR, "Continuing with default channel 1, will retry router connection later\n");
615
+ Log(ERROR,
616
+ "Continuing with default channel 1, will retry router connection "
617
+ "later\n");
564
618
  }
565
-
619
+
566
620
  // Disconnect from router, we'll reconnect after mesh init
567
621
  WiFi.disconnect();
568
622
  delay(100);
569
-
570
- Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n", detectedChannel);
571
-
623
+
624
+ Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
625
+ detectedChannel);
626
+
572
627
  // Step 2: Initialize mesh on detected channel with AP+STA mode
573
628
  // Set scheduler before init
574
629
  this->setScheduler(userScheduler);
575
- init(meshPrefix, meshPassword, port, WIFI_AP_STA, detectedChannel, 0, MAX_CONN);
576
-
630
+ init(meshPrefix, meshPassword, port, WIFI_AP_STA, detectedChannel, 0,
631
+ MAX_CONN);
632
+
577
633
  // Allow network stack to stabilize after AP and TCP server initialization
578
634
  // This prevents TCP connection errors (-14) when nodes connect
579
635
  delay(100);
580
-
581
- Log(STARTUP, "Step 3: Establishing router connection in shared gateway mode...\n");
582
-
636
+
637
+ Log(STARTUP,
638
+ "Step 3: Establishing router connection in shared gateway mode...\n");
639
+
583
640
  // Step 3: Establish router connection using stationManual
584
641
  // Port 0 means we don't expect TCP mesh connection to the router
585
642
  stationManual(routerSSID, routerPassword, 0);
586
-
643
+
587
644
  // Step 4: Setup router connection monitoring and reconnection logic
588
645
  initSharedGatewayMonitoring();
589
-
646
+
647
+ // Step 5: Setup gateway Internet handler
648
+ initGatewayInternetHandler();
649
+
590
650
  // Store router credentials for reconnection
591
651
  setRouterCredentials(routerSSID, routerPassword);
592
-
652
+
593
653
  Log(STARTUP, "=== Shared Gateway Mode Active ===\n");
594
654
  Log(STARTUP, " Mesh Prefix: %s\n", meshPrefix.c_str());
595
655
  Log(STARTUP, " Mesh Channel: %d (synced with router)\n", detectedChannel);
596
656
  Log(STARTUP, " Router: %s\n", routerSSID.c_str());
597
657
  Log(STARTUP, " Port: %d\n", port);
598
658
  Log(STARTUP, " Mode: AP+STA (all nodes can connect to router)\n");
599
-
659
+
600
660
  return true;
601
661
  }
602
662
 
603
663
  /**
604
664
  * Check if shared gateway mode is enabled
605
- *
665
+ *
606
666
  * @return true if node is operating in shared gateway mode
607
667
  */
608
- bool isSharedGatewayMode() const {
609
- return _sharedGatewayMode;
610
- }
668
+ bool isSharedGatewayMode() const { return _sharedGatewayMode; }
611
669
 
612
670
  /**
613
671
  * Get the shared gateway configuration
614
- *
672
+ *
615
673
  * @return const reference to the SharedGatewayConfig
616
674
  */
617
675
  const gateway::SharedGatewayConfig& getSharedGatewayConfig() const {
@@ -693,16 +751,25 @@ class Mesh : public painlessmesh::Mesh<Connection> {
693
751
 
694
752
  /**
695
753
  * Establish TCP connection to mesh network
696
- *
754
+ *
697
755
  * This method is called by WiFi event handlers when station gets IP address.
698
756
  * It creates a TCP client connection to the mesh network gateway.
699
- *
757
+ *
700
758
  * Architecture Note: This is intentionally kept in the Mesh class rather than
701
759
  * extracted to a separate StationConnection class because:
702
760
  * - It's tightly coupled with WiFi event lifecycle
703
761
  * - Needs access to mesh state and callbacks
704
762
  * - Moving it would increase complexity without clear benefits
705
763
  * - The existing design keeps connection logic cohesive with WiFi management
764
+ *
765
+ * TCP Connection Retry:
766
+ * The TCP connection now includes automatic retry with exponential backoff.
767
+ * If the initial connection fails (error -14 ERR_CONN or other errors),
768
+ * the system will retry up to TCP_CONNECT_MAX_RETRIES times before
769
+ * triggering a full WiFi reconnection cycle. This helps handle:
770
+ * - Timing issues where TCP server is not ready immediately
771
+ * - Network stack stabilization after IP acquisition
772
+ * - Transient network conditions
706
773
  */
707
774
  void tcpConnect() {
708
775
  using namespace logger;
@@ -712,18 +779,41 @@ class Mesh : public painlessmesh::Mesh<Connection> {
712
779
 
713
780
  if (WiFi.status() == WL_CONNECTED && WiFi.localIP()) {
714
781
  // Determine target IP and port for connection
715
- IPAddress targetIP = stationScan.manualIP ? stationScan.manualIP : WiFi.gatewayIP();
782
+ IPAddress targetIP =
783
+ stationScan.manualIP ? stationScan.manualIP : WiFi.gatewayIP();
716
784
  uint16_t targetPort = stationScan.port;
717
-
718
- AsyncClient *pConn = new AsyncClient();
719
- painlessmesh::tcp::connect<Connection, painlessmesh::Mesh<Connection>>(
720
- (*pConn), targetIP, targetPort, (*this));
785
+
786
+ Log(CONNECTION, "tcpConnect(): Connecting to %s:%d\n",
787
+ targetIP.toString().c_str(), targetPort);
788
+
789
+ // Add a small stabilization delay before attempting TCP connection
790
+ // This helps prevent error -14 (ERR_CONN) by allowing the network stack
791
+ // and TCP server to be fully ready. The delay is added via task scheduler
792
+ // to avoid blocking the event loop.
793
+ this->addTask(
794
+ painlessmesh::tcp::TCP_CONNECT_STABILIZATION_DELAY_MS, TASK_ONCE,
795
+ [this, targetIP, targetPort]() {
796
+ // Verify WiFi is still connected after the delay
797
+ if (WiFi.status() != WL_CONNECTED || !WiFi.localIP()) {
798
+ Log(CONNECTION,
799
+ "tcpConnect(): WiFi disconnected during stabilization "
800
+ "delay\n");
801
+ return;
802
+ }
803
+
804
+ Log(CONNECTION,
805
+ "tcpConnect(): Starting TCP connection after stabilization\n");
806
+ AsyncClient* pConn = new AsyncClient();
807
+ painlessmesh::tcp::connect<Connection,
808
+ painlessmesh::Mesh<Connection>>(
809
+ (*pConn), targetIP, targetPort, (*this));
810
+ });
721
811
  } else {
722
812
  Log(ERROR, "tcpConnect(): err Something unexpected in tcpConnect()\n");
723
813
  }
724
814
  }
725
815
 
726
- bool setHostname(const char *hostname) {
816
+ bool setHostname(const char* hostname) {
727
817
  #ifdef ESP8266
728
818
  return WiFi.hostname(hostname);
729
819
  #elif defined(ESP32)
@@ -739,23 +829,22 @@ class Mesh : public painlessmesh::Mesh<Connection> {
739
829
 
740
830
  /**
741
831
  * Enable or disable automatic bridge failover
742
- *
832
+ *
743
833
  * When enabled, nodes will participate in bridge elections if the primary
744
834
  * bridge goes offline and they have router credentials configured.
745
- *
746
- * @param enabled true to enable automatic failover (default), false to disable
835
+ *
836
+ * @param enabled true to enable automatic failover (default), false to
837
+ * disable
747
838
  */
748
- void enableBridgeFailover(bool enabled) {
749
- bridgeFailoverEnabled = enabled;
750
- }
839
+ void enableBridgeFailover(bool enabled) { bridgeFailoverEnabled = enabled; }
751
840
 
752
841
  /**
753
842
  * Set router credentials for bridge election participation
754
- *
843
+ *
755
844
  * Nodes must have router credentials configured to participate in bridge
756
845
  * elections. When a bridge fails, only nodes with credentials can become
757
846
  * the new bridge.
758
- *
847
+ *
759
848
  * @param ssid Router SSID
760
849
  * @param password Router password
761
850
  */
@@ -767,20 +856,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
767
856
 
768
857
  /**
769
858
  * Set the election timeout (how long to collect candidates)
770
- *
859
+ *
771
860
  * @param timeoutMs Timeout in milliseconds (default: 5000 = 5 seconds)
772
861
  */
773
- void setElectionTimeout(uint32_t timeoutMs) {
774
- electionTimeoutMs = timeoutMs;
775
- }
862
+ void setElectionTimeout(uint32_t timeoutMs) { electionTimeoutMs = timeoutMs; }
776
863
 
777
864
  /**
778
865
  * Set the minimum RSSI required for bridge election
779
- *
866
+ *
780
867
  * Prevents nodes with poor router signal from becoming bridges in isolated
781
868
  * elections. When a node is the only candidate, it must meet this threshold.
782
869
  * When multiple candidates exist, the best RSSI wins regardless of threshold.
783
- *
870
+ *
784
871
  * @param minRSSI Minimum RSSI in dBm (default: -80 dBm, range: -100 to -30)
785
872
  */
786
873
  void setMinimumBridgeRSSI(int8_t minRSSI) {
@@ -791,12 +878,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
791
878
 
792
879
  /**
793
880
  * Set the startup delay before first bridge election check
794
- *
881
+ *
795
882
  * Allows time for mesh network formation before starting bridge elections.
796
883
  * Longer delays reduce the risk of split-brain scenarios when multiple nodes
797
884
  * start simultaneously, ensuring nodes discover each other before elections.
798
- *
799
- * @param delayMs Startup delay in milliseconds (default: 60000 = 60 seconds, min: 10000)
885
+ *
886
+ * @param delayMs Startup delay in milliseconds (default: 60000 = 60 seconds,
887
+ * min: 10000)
800
888
  */
801
889
  void setElectionStartupDelay(uint32_t delayMs) {
802
890
  if (delayMs < 10000) delayMs = 10000; // Minimum 10 seconds
@@ -805,16 +893,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
805
893
 
806
894
  /**
807
895
  * Set the random delay range for bridge elections
808
- *
896
+ *
809
897
  * When multiple nodes detect missing bridge simultaneously, randomized delays
810
- * prevent all nodes from starting elections at the same instant. Longer delays
811
- * provide more time for mesh discovery and reduce split-brain risk.
812
- *
813
- * @param minMs Minimum random delay in milliseconds (default: 1000 = 1 second)
814
- * @param maxMs Maximum random delay in milliseconds (default: 3000 = 3 seconds)
898
+ * prevent all nodes from starting elections at the same instant. Longer
899
+ * delays provide more time for mesh discovery and reduce split-brain risk.
900
+ *
901
+ * @param minMs Minimum random delay in milliseconds (default: 1000 = 1
902
+ * second)
903
+ * @param maxMs Maximum random delay in milliseconds (default: 3000 = 3
904
+ * seconds)
815
905
  */
816
906
  void setElectionRandomDelay(uint32_t minMs, uint32_t maxMs) {
817
- if (minMs < 100) minMs = 100; // Minimum 100ms
907
+ if (minMs < 100) minMs = 100; // Minimum 100ms
818
908
  if (maxMs < minMs) maxMs = minMs + 1000; // Ensure max > min
819
909
  electionRandomDelayMinMs = minMs;
820
910
  electionRandomDelayMaxMs = maxMs;
@@ -822,33 +912,36 @@ class Mesh : public painlessmesh::Mesh<Connection> {
822
912
 
823
913
  /**
824
914
  * Set callback for when this node's bridge role changes
825
- *
915
+ *
826
916
  * @param callback Function to call when role changes
827
917
  */
828
- void onBridgeRoleChanged(std::function<void(bool isBridge, TSTRING reason)> callback) {
918
+ void onBridgeRoleChanged(
919
+ std::function<void(bool isBridge, TSTRING reason)> callback) {
829
920
  bridgeRoleChangedCallback = callback;
830
921
  }
831
922
 
832
923
  /**
833
924
  * Enable or disable multi-bridge coordination mode
834
- *
925
+ *
835
926
  * When enabled, multiple bridges can operate simultaneously for:
836
927
  * - Load balancing across multiple Internet connections
837
928
  * - Geographic distribution
838
929
  * - Hot standby redundancy without failover delays
839
- *
840
- * @param enabled true to enable multi-bridge mode, false for single-bridge (default)
930
+ *
931
+ * @param enabled true to enable multi-bridge mode, false for single-bridge
932
+ * (default)
841
933
  */
842
934
  void enableMultiBridge(bool enabled) {
843
935
  multiBridgeEnabled = enabled;
844
936
  if (enabled) {
845
- Log(logger::GENERAL, "enableMultiBridge(): Multi-bridge coordination enabled\n");
937
+ Log(logger::GENERAL,
938
+ "enableMultiBridge(): Multi-bridge coordination enabled\n");
846
939
  }
847
940
  }
848
941
 
849
942
  /**
850
943
  * Set bridge selection strategy for multi-bridge mode
851
- *
944
+ *
852
945
  * @param strategy Selection strategy:
853
946
  * - PRIORITY_BASED: Always use highest priority bridge (default)
854
947
  * - ROUND_ROBIN: Distribute load evenly across bridges
@@ -856,132 +949,140 @@ class Mesh : public painlessmesh::Mesh<Connection> {
856
949
  */
857
950
  void setBridgeSelectionStrategy(BridgeSelectionStrategy strategy) {
858
951
  bridgeSelectionStrategy = strategy;
859
- Log(logger::GENERAL, "setBridgeSelectionStrategy(): Strategy set to %d\n", (int)strategy);
952
+ Log(logger::GENERAL, "setBridgeSelectionStrategy(): Strategy set to %d\n",
953
+ (int)strategy);
860
954
  }
861
955
 
862
956
  /**
863
957
  * Set maximum number of concurrent bridges in multi-bridge mode
864
- *
958
+ *
865
959
  * @param maxBridges Maximum bridges to track (default: 2, max: 5)
866
960
  */
867
961
  void setMaxBridges(uint8_t maxBridges) {
868
962
  if (maxBridges < 1) maxBridges = 1;
869
963
  if (maxBridges > 5) maxBridges = 5;
870
964
  maxConcurrentBridges = maxBridges;
871
- Log(logger::GENERAL, "setMaxBridges(): Max concurrent bridges set to %d\n", maxBridges);
965
+ Log(logger::GENERAL, "setMaxBridges(): Max concurrent bridges set to %d\n",
966
+ maxBridges);
872
967
  }
873
968
 
874
969
  /**
875
970
  * Get list of all active bridges (with Internet connection)
876
- *
971
+ *
877
972
  * @return vector of node IDs for active bridges
878
973
  */
879
974
  std::vector<uint32_t> getActiveBridges() {
880
975
  std::vector<uint32_t> activeBridges;
881
976
  auto bridges = this->getBridges();
882
-
977
+
883
978
  for (const auto& bridge : bridges) {
884
979
  if (bridge.internetConnected && bridge.isHealthy()) {
885
980
  activeBridges.push_back(bridge.nodeId);
886
981
  }
887
982
  }
888
-
983
+
889
984
  return activeBridges;
890
985
  }
891
986
 
892
987
  /**
893
988
  * Check if any bridge/gateway in the mesh has Internet connectivity
894
- *
989
+ *
895
990
  * IMPORTANT: This method checks if a GATEWAY node (bridge) in the mesh has
896
- * Internet access, NOT whether THIS node can directly make HTTP/HTTPS requests.
897
- *
991
+ * Internet access, NOT whether THIS node can directly make HTTP/HTTPS
992
+ * requests.
993
+ *
898
994
  * Regular mesh nodes do NOT have direct IP routing to the Internet. They only
899
- * communicate via the painlessMesh protocol. To send data to the Internet from
900
- * a regular node, you must use sendToInternet() which routes through a gateway,
901
- * or use initAsSharedGateway(meshSSID, meshPwd, ROUTER_SSID, ROUTER_PWD, scheduler, port)
902
- * to give all nodes direct router access (requires router credentials).
903
- *
995
+ * communicate via the painlessMesh protocol. To send data to the Internet
996
+ * from a regular node, you must use sendToInternet() which routes through a
997
+ * gateway, or use initAsSharedGateway(meshSSID, meshPwd, ROUTER_SSID,
998
+ * ROUTER_PWD, scheduler, port) to give all nodes direct router access
999
+ * (requires router credentials).
1000
+ *
904
1001
  * Override of base class method to also check if THIS node is a bridge
905
1002
  * with Internet connectivity, not just other bridges in the mesh.
906
- *
1003
+ *
907
1004
  * \code
908
1005
  * if (mesh.hasInternetConnection()) {
909
1006
  * // A gateway exists - use sendToInternet() to reach Internet
910
1007
  * mesh.sendToInternet("https://api.example.com", data, callback);
911
1008
  * }
912
- *
1009
+ *
913
1010
  * // DON'T DO THIS on regular nodes - will fail with "connection refused":
914
1011
  * // HTTPClient http;
915
1012
  * // http.begin("https://api.example.com");
916
1013
  * \endcode
917
- *
1014
+ *
918
1015
  * @return true if at least one bridge (including this node) has Internet
919
1016
  * @see hasLocalInternet() to check if THIS node has direct Internet access
920
1017
  * @see sendToInternet() to send data to Internet via gateway
921
- * @see initAsSharedGateway() requires router credentials (ROUTER_SSID, ROUTER_PASSWORD)
1018
+ * @see initAsSharedGateway() requires router credentials (ROUTER_SSID,
1019
+ * ROUTER_PASSWORD)
922
1020
  */
923
1021
  bool hasInternetConnection() {
924
1022
  // First check if THIS node is a bridge with Internet
925
1023
  if (this->isBridge()) {
926
1024
  // Check Internet connectivity: WiFi connected AND valid IP address
927
- bool hasInternet = (WiFi.status() == WL_CONNECTED) && (WiFi.localIP() != IPAddress(0, 0, 0, 0));
1025
+ bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1026
+ (WiFi.localIP() != IPAddress(0, 0, 0, 0));
928
1027
  if (hasInternet) {
929
1028
  return true;
930
1029
  }
931
1030
  }
932
-
1031
+
933
1032
  // Then check other bridges in the mesh (call parent implementation)
934
1033
  return painlessmesh::Mesh<Connection>::hasInternetConnection();
935
1034
  }
936
1035
 
937
1036
  /**
938
1037
  * Get recommended bridge for message transmission
939
- *
1038
+ *
940
1039
  * Uses the configured bridge selection strategy to pick the best bridge.
941
1040
  * Returns 0 if no suitable bridge is available.
942
- *
1041
+ *
943
1042
  * @return node ID of recommended bridge, or 0 if none available
944
1043
  */
945
1044
  uint32_t getRecommendedBridge() {
946
1045
  auto activeBridges = getActiveBridges();
947
-
1046
+
948
1047
  if (activeBridges.empty()) {
949
1048
  return 0;
950
1049
  }
951
-
1050
+
952
1051
  // Single bridge - return it
953
1052
  if (activeBridges.size() == 1) {
954
1053
  return activeBridges[0];
955
1054
  }
956
-
1055
+
957
1056
  // Multi-bridge mode: apply selection strategy
958
1057
  switch (bridgeSelectionStrategy) {
959
1058
  case ROUND_ROBIN: {
960
1059
  // Simple round-robin: cycle through bridges
961
- lastSelectedBridgeIndex = (lastSelectedBridgeIndex + 1) % activeBridges.size();
1060
+ lastSelectedBridgeIndex =
1061
+ (lastSelectedBridgeIndex + 1) % activeBridges.size();
962
1062
  return activeBridges[lastSelectedBridgeIndex];
963
1063
  }
964
-
1064
+
965
1065
  case BEST_SIGNAL: {
966
1066
  // Find bridge with best RSSI
967
1067
  uint32_t bestBridge = 0;
968
1068
  int8_t bestRSSI = -127;
969
-
1069
+
970
1070
  for (const auto& bridge : this->getBridges()) {
971
- if (bridge.internetConnected && bridge.isHealthy() && bridge.routerRSSI > bestRSSI) {
1071
+ if (bridge.internetConnected && bridge.isHealthy() &&
1072
+ bridge.routerRSSI > bestRSSI) {
972
1073
  bestRSSI = bridge.routerRSSI;
973
1074
  bestBridge = bridge.nodeId;
974
1075
  }
975
1076
  }
976
1077
  return bestBridge;
977
1078
  }
978
-
1079
+
979
1080
  case PRIORITY_BASED:
980
1081
  default: {
981
1082
  // Use highest priority bridge (stored in bridgePriorities map)
982
1083
  uint32_t bestBridge = 0;
983
1084
  uint8_t highestPriority = 0;
984
-
1085
+
985
1086
  for (uint32_t bridgeId : activeBridges) {
986
1087
  uint8_t priority = bridgePriorities[bridgeId];
987
1088
  if (priority > highestPriority) {
@@ -989,7 +1090,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
989
1090
  bestBridge = bridgeId;
990
1091
  }
991
1092
  }
992
-
1093
+
993
1094
  // If no priority info, use first active bridge
994
1095
  return bestBridge ? bestBridge : activeBridges[0];
995
1096
  }
@@ -998,9 +1099,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
998
1099
 
999
1100
  /**
1000
1101
  * Select a specific bridge for next transmission
1001
- *
1102
+ *
1002
1103
  * This overrides the automatic bridge selection for one message.
1003
- *
1104
+ *
1004
1105
  * @param bridgeNodeId Node ID of bridge to use
1005
1106
  */
1006
1107
  void selectBridge(uint32_t bridgeNodeId) {
@@ -1009,12 +1110,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1009
1110
 
1010
1111
  /**
1011
1112
  * Check if multi-bridge mode is enabled
1012
- *
1113
+ *
1013
1114
  * @return true if multi-bridge coordination is enabled
1014
1115
  */
1015
- bool isMultiBridgeEnabled() const {
1016
- return multiBridgeEnabled;
1017
- }
1116
+ bool isMultiBridgeEnabled() const { return multiBridgeEnabled; }
1018
1117
 
1019
1118
  void stop() {
1020
1119
  // remove all WiFi events
@@ -1055,19 +1154,31 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1055
1154
  IPAddress _apIp;
1056
1155
  StationScan stationScan;
1057
1156
 
1058
- void init(Scheduler *scheduler, uint32_t id) {
1157
+ void init(Scheduler* scheduler, uint32_t id) {
1059
1158
  painlessmesh::Mesh<Connection>::init(scheduler, id);
1060
1159
  }
1061
1160
 
1062
1161
  void init(uint32_t id) { painlessmesh::Mesh<Connection>::init(id); }
1063
1162
 
1064
1163
  void apInit(uint32_t nodeId) {
1164
+ using namespace logger;
1065
1165
  _apIp = IPAddress(10, (nodeId & 0xFF00) >> 8, (nodeId & 0xFF), 1);
1066
1166
  IPAddress netmask(255, 255, 255, 0);
1067
1167
 
1068
1168
  WiFi.softAPConfig(_apIp, _apIp, netmask);
1169
+
1170
+ #ifdef ESP32
1171
+ // ESP32: Explicitly enable AP mode to ensure DHCP server starts properly
1172
+ // This is particularly important after channel changes or AP restarts
1173
+ WiFi.enableAP(true);
1174
+ #endif
1175
+
1069
1176
  WiFi.softAP(_meshSSID.c_str(), _meshPassword.c_str(), _meshChannel,
1070
1177
  _meshHidden, _meshMaxConn);
1178
+
1179
+ Log(STARTUP, "apInit(): AP configured - SSID: %s, Channel: %d, IP: %s\n",
1180
+ _meshSSID.c_str(), _meshChannel, _apIp.toString().c_str());
1181
+ Log(STARTUP, "apInit(): AP active - Max connections: %d\n", _meshMaxConn);
1071
1182
  }
1072
1183
 
1073
1184
  /**
@@ -1076,80 +1187,84 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1076
1187
  */
1077
1188
  void initBridgeStatusBroadcast() {
1078
1189
  using namespace logger;
1079
-
1190
+
1080
1191
  if (!this->isBridge() || !this->bridgeStatusBroadcastEnabled) {
1081
1192
  return;
1082
1193
  }
1083
-
1084
- Log(STARTUP, "initBridgeStatusBroadcast(): Setting up bridge status broadcast\n");
1085
-
1194
+
1195
+ Log(STARTUP,
1196
+ "initBridgeStatusBroadcast(): Setting up bridge status broadcast\n");
1197
+
1086
1198
  // Register ourselves as a bridge in the knownBridges list
1087
1199
  // This ensures the bridge knows about itself and reports correct status
1088
1200
  this->addTask([this]() {
1089
1201
  // Check Internet connectivity: WiFi connected AND valid IP address
1090
- bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1202
+ bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1091
1203
  (WiFi.localIP() != IPAddress(0, 0, 0, 0));
1092
-
1093
- this->updateBridgeStatus(
1094
- this->nodeId, // bridgeNodeId
1095
- hasInternet, // internetConnected
1096
- WiFi.RSSI(), // routerRSSI
1097
- WiFi.channel(), // routerChannel
1098
- millis(), // uptime
1099
- WiFi.gatewayIP().toString(),// gatewayIP
1100
- this->getNodeTime() // timestamp
1204
+
1205
+ this->updateBridgeStatus(this->nodeId, // bridgeNodeId
1206
+ hasInternet, // internetConnected
1207
+ WiFi.RSSI(), // routerRSSI
1208
+ WiFi.channel(), // routerChannel
1209
+ millis(), // uptime
1210
+ WiFi.gatewayIP().toString(), // gatewayIP
1211
+ this->getNodeTime() // timestamp
1101
1212
  );
1102
-
1103
- Log(STARTUP, "initBridgeStatusBroadcast(): Registered self as bridge (nodeId: %u)\n",
1213
+
1214
+ Log(STARTUP,
1215
+ "initBridgeStatusBroadcast(): Registered self as bridge (nodeId: "
1216
+ "%u)\n",
1104
1217
  this->nodeId);
1105
1218
  });
1106
-
1219
+
1107
1220
  // Create periodic task to broadcast bridge status
1108
- bridgeStatusTask = this->addTask(
1109
- this->bridgeStatusIntervalMs,
1110
- TASK_FOREVER,
1111
- [this]() {
1112
- this->sendBridgeStatus();
1113
- }
1114
- );
1115
-
1221
+ bridgeStatusTask = this->addTask(this->bridgeStatusIntervalMs, TASK_FOREVER,
1222
+ [this]() { this->sendBridgeStatus(); });
1223
+
1116
1224
  // Send immediate broadcast so nodes can discover this bridge right away
1117
1225
  // This ensures bridge is discoverable before the first periodic broadcast
1118
1226
  this->addTask([this]() {
1119
1227
  Log(STARTUP, "Sending initial bridge status broadcast\n");
1120
1228
  this->sendBridgeStatus();
1121
1229
  });
1122
-
1123
- // Also send bridge status when new nodes connect so they can discover the bridge immediately
1124
- // Send directly to the new node to ensure delivery, independent of time sync
1125
- // Using changedConnectionCallbacks instead of newConnectionCallbacks ensures routing is ready
1230
+
1231
+ // Also send bridge status when new nodes connect so they can discover the
1232
+ // bridge immediately Send directly to the new node to ensure delivery,
1233
+ // independent of time sync Using changedConnectionCallbacks instead of
1234
+ // newConnectionCallbacks ensures routing is ready
1126
1235
  this->changedConnectionCallbacks.push_back([this](uint32_t nodeId) {
1127
- Log(CONNECTION, "Node %u connection changed, sending bridge status directly\n", nodeId);
1128
-
1129
- // Small delay to ensure connection is fully stable, then send directly to the new node
1130
- // This avoids issues with time sync blocking broadcast messages
1236
+ Log(CONNECTION,
1237
+ "Node %u connection changed, sending bridge status directly\n",
1238
+ nodeId);
1239
+
1240
+ // Small delay to ensure connection is fully stable, then send directly to
1241
+ // the new node This avoids issues with time sync blocking broadcast
1242
+ // messages
1131
1243
  this->addTask(500, TASK_ONCE, [this, nodeId]() {
1132
- // Check if the connection is still valid - the node may have disconnected
1133
- // during the 500ms delay (e.g., due to timeout or network issues)
1134
- // This prevents attempting to send messages to dropped connections
1135
- // findRoute returns nullptr if node is not in the routing table
1244
+ // Check if the connection is still valid - the node may have
1245
+ // disconnected during the 500ms delay (e.g., due to timeout or network
1246
+ // issues) This prevents attempting to send messages to dropped
1247
+ // connections findRoute returns nullptr if node is not in the routing
1248
+ // table
1136
1249
  auto conn = router::findRoute<Connection>((*this), nodeId);
1137
1250
  if (!conn || !conn->connected()) {
1138
- Log(CONNECTION, "Bridge status send cancelled: Node %u no longer connected\n", nodeId);
1251
+ Log(CONNECTION,
1252
+ "Bridge status send cancelled: Node %u no longer connected\n",
1253
+ nodeId);
1139
1254
  return;
1140
1255
  }
1141
-
1256
+
1142
1257
  // Create bridge status message
1143
1258
  JsonDocument doc;
1144
1259
  JsonObject obj = doc.to<JsonObject>();
1145
-
1260
+
1146
1261
  obj["type"] = protocol::BRIDGE_STATUS;
1147
1262
  obj["from"] = this->nodeId;
1148
1263
  obj["routing"] = 1; // SINGLE routing (direct to node)
1149
1264
  obj["dest"] = nodeId;
1150
1265
  obj["timestamp"] = this->getNodeTime();
1151
-
1152
- bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1266
+
1267
+ bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1153
1268
  (WiFi.localIP() != IPAddress(0, 0, 0, 0));
1154
1269
  obj["internetConnected"] = hasInternet;
1155
1270
  obj["routerRSSI"] = WiFi.RSSI();
@@ -1157,13 +1272,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1157
1272
  obj["uptime"] = millis();
1158
1273
  obj["gatewayIP"] = WiFi.gatewayIP().toString();
1159
1274
  obj["message_type"] = protocol::BRIDGE_STATUS;
1160
-
1275
+
1161
1276
  String msg;
1162
1277
  serializeJson(doc, msg);
1163
-
1164
- Log(CONNECTION, "Sending bridge status directly to node %u (Internet: %s)\n",
1278
+
1279
+ Log(CONNECTION,
1280
+ "Sending bridge status directly to node %u (Internet: %s)\n",
1165
1281
  nodeId, hasInternet ? "YES" : "NO");
1166
-
1282
+
1167
1283
  // Send directly to the connection with high priority
1168
1284
  // This ensures the message is sent immediately rather than queued
1169
1285
  // The JSON message format is the same as what router::send() produces
@@ -1171,8 +1287,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1171
1287
  conn->addMessage(msg, true);
1172
1288
  });
1173
1289
  });
1174
-
1175
- Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
1290
+
1291
+ Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
1176
1292
  this->bridgeStatusIntervalMs);
1177
1293
  }
1178
1294
 
@@ -1182,66 +1298,70 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1182
1298
  */
1183
1299
  void initBridgeCoordination() {
1184
1300
  using namespace logger;
1185
-
1301
+
1186
1302
  if (!this->isBridge() || !multiBridgeEnabled) {
1187
1303
  return;
1188
1304
  }
1189
-
1190
- Log(STARTUP, "initBridgeCoordination(): Setting up multi-bridge coordination\n");
1191
-
1305
+
1306
+ Log(STARTUP,
1307
+ "initBridgeCoordination(): Setting up multi-bridge coordination\n");
1308
+
1192
1309
  // Register our own priority in the bridgePriorities map
1193
- // This ensures getRecommendedBridge() with PRIORITY_BASED strategy works correctly
1310
+ // This ensures getRecommendedBridge() with PRIORITY_BASED strategy works
1311
+ // correctly
1194
1312
  bridgePriorities[this->nodeId] = bridgePriority;
1195
- Log(STARTUP, "initBridgeCoordination(): Registered self priority (nodeId: %u, priority: %d)\n",
1313
+ Log(STARTUP,
1314
+ "initBridgeCoordination(): Registered self priority (nodeId: %u, "
1315
+ "priority: %d)\n",
1196
1316
  this->nodeId, bridgePriority);
1197
-
1317
+
1198
1318
  // Register handler for incoming coordination messages (Type 613)
1199
1319
  this->callbackList.onPackage(
1200
1320
  613, // BRIDGE_COORDINATION type
1201
- [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
1321
+ [this](protocol::Variant& variant, std::shared_ptr<Connection>,
1322
+ uint32_t) {
1202
1323
  JsonDocument doc;
1203
1324
  TSTRING str;
1204
1325
  variant.printTo(str);
1205
1326
  deserializeJson(doc, str);
1206
1327
  JsonObject obj = doc.as<JsonObject>();
1207
-
1328
+
1208
1329
  if (obj["priority"].is<unsigned int>()) {
1209
1330
  uint32_t fromNode = obj["from"];
1210
1331
  uint8_t priority = obj["priority"];
1211
1332
  TSTRING role = obj["role"].as<TSTRING>();
1212
1333
  uint8_t load = obj["load"] | 0;
1213
-
1334
+
1214
1335
  // Store bridge priority for selection decisions
1215
1336
  bridgePriorities[fromNode] = priority;
1216
-
1337
+
1217
1338
  // Update peer bridges list
1218
1339
  if (obj["peerBridges"].is<JsonArray>()) {
1219
1340
  JsonArray peers = obj["peerBridges"];
1220
1341
  for (JsonVariant peer : peers) {
1221
1342
  uint32_t peerId = peer.as<uint32_t>();
1222
- if (peerId != this->nodeId &&
1223
- std::find(knownBridgePeers.begin(), knownBridgePeers.end(), peerId) == knownBridgePeers.end()) {
1343
+ if (peerId != this->nodeId &&
1344
+ std::find(knownBridgePeers.begin(), knownBridgePeers.end(),
1345
+ peerId) == knownBridgePeers.end()) {
1224
1346
  knownBridgePeers.push_back(peerId);
1225
1347
  }
1226
1348
  }
1227
1349
  }
1228
-
1229
- Log(CONNECTION, "Bridge coordination from %u: priority=%d, role=%s, load=%d%%\n",
1350
+
1351
+ Log(CONNECTION,
1352
+ "Bridge coordination from %u: priority=%d, role=%s, "
1353
+ "load=%d%%\n",
1230
1354
  fromNode, priority, role.c_str(), load);
1231
1355
  }
1232
1356
  return false; // Don't consume the package
1233
1357
  });
1234
-
1358
+
1235
1359
  // Create periodic task to send coordination messages
1236
1360
  bridgeCoordinationTask = this->addTask(
1237
- 30000, // 30 seconds interval
1238
- TASK_FOREVER,
1239
- [this]() {
1240
- this->sendBridgeCoordination();
1241
- }
1242
- );
1243
-
1244
- Log(STARTUP, "Bridge coordination enabled (priority: %d, role: %s)\n",
1361
+ 30000, // 30 seconds interval
1362
+ TASK_FOREVER, [this]() { this->sendBridgeCoordination(); });
1363
+
1364
+ Log(STARTUP, "Bridge coordination enabled (priority: %d, role: %s)\n",
1245
1365
  bridgePriority, bridgeRole.c_str());
1246
1366
  }
1247
1367
 
@@ -1251,11 +1371,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1251
1371
  */
1252
1372
  void sendBridgeCoordination() {
1253
1373
  using namespace logger;
1254
-
1374
+
1255
1375
  if (!this->isBridge() || !multiBridgeEnabled) {
1256
1376
  return;
1257
1377
  }
1258
-
1378
+
1259
1379
  // Calculate current load (simplified: based on node count)
1260
1380
  uint8_t currentLoad = 0;
1261
1381
  auto nodeCount = this->getNodeList(false).size();
@@ -1263,11 +1383,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1263
1383
  currentLoad = (nodeCount * 100) / MAX_CONN;
1264
1384
  if (currentLoad > 100) currentLoad = 100;
1265
1385
  }
1266
-
1386
+
1267
1387
  // Create coordination message
1268
1388
  JsonDocument doc;
1269
1389
  JsonObject obj = doc.to<JsonObject>();
1270
-
1390
+
1271
1391
  obj["type"] = 613; // BRIDGE_COORDINATION
1272
1392
  obj["from"] = this->nodeId;
1273
1393
  obj["routing"] = 2; // BROADCAST
@@ -1276,49 +1396,53 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1276
1396
  obj["load"] = currentLoad;
1277
1397
  obj["timestamp"] = this->getNodeTime();
1278
1398
  obj["message_type"] = 613;
1279
-
1399
+
1280
1400
  // Add peer bridges list
1281
1401
  JsonArray peers = obj["peerBridges"].to<JsonArray>();
1282
1402
  for (uint32_t peerId : knownBridgePeers) {
1283
1403
  peers.add(peerId);
1284
1404
  }
1285
-
1405
+
1286
1406
  String msg;
1287
1407
  serializeJson(doc, msg);
1288
-
1408
+
1289
1409
  // Update our own priority in bridgePriorities map
1290
1410
  // This ensures priority-based selection always has current data
1291
1411
  bridgePriorities[this->nodeId] = bridgePriority;
1292
-
1412
+
1293
1413
  this->sendBroadcast(msg);
1294
-
1295
- Log(CONNECTION, "Bridge coordination sent: priority=%d, role=%s, load=%d%%\n",
1414
+
1415
+ Log(CONNECTION,
1416
+ "Bridge coordination sent: priority=%d, role=%s, load=%d%%\n",
1296
1417
  bridgePriority, bridgeRole.c_str(), currentLoad);
1297
1418
  }
1298
1419
 
1299
1420
  /**
1300
1421
  * Scan for router and return its signal strength
1301
- *
1422
+ *
1302
1423
  * @param routerSSID SSID of router to scan for
1303
1424
  * @return RSSI in dBm (negative number, -127 to 0), or 0 if not found
1304
1425
  */
1305
1426
  int8_t scanRouterSignalStrength(TSTRING routerSSID) {
1306
1427
  using namespace logger;
1307
- Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n", routerSSID.c_str());
1308
-
1428
+ Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n",
1429
+ routerSSID.c_str());
1430
+
1309
1431
  int n = WiFi.scanNetworks(false, false);
1310
1432
  Log(CONNECTION, "scanRouterSignalStrength(): Found %d networks\n", n);
1311
-
1433
+
1312
1434
  for (int i = 0; i < n; i++) {
1313
1435
  if (WiFi.SSID(i) == routerSSID) {
1314
1436
  int8_t rssi = WiFi.RSSI(i);
1315
- Log(CONNECTION, "scanRouterSignalStrength(): Found %s with RSSI %d dBm\n",
1437
+ Log(CONNECTION,
1438
+ "scanRouterSignalStrength(): Found %s with RSSI %d dBm\n",
1316
1439
  routerSSID.c_str(), rssi);
1317
1440
  return rssi;
1318
1441
  }
1319
1442
  }
1320
-
1321
- Log(CONNECTION, "scanRouterSignalStrength(): Router %s not found\n", routerSSID.c_str());
1443
+
1444
+ Log(CONNECTION, "scanRouterSignalStrength(): Router %s not found\n",
1445
+ routerSSID.c_str());
1322
1446
  return 0; // Router not found
1323
1447
  }
1324
1448
 
@@ -1328,68 +1452,75 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1328
1452
  */
1329
1453
  void startBridgeElection() {
1330
1454
  using namespace logger;
1331
-
1455
+
1332
1456
  if (!bridgeFailoverEnabled) {
1333
1457
  Log(CONNECTION, "startBridgeElection(): Failover disabled\n");
1334
1458
  return;
1335
1459
  }
1336
-
1460
+
1337
1461
  if (!routerCredentialsConfigured) {
1338
- Log(CONNECTION, "startBridgeElection(): No router credentials, cannot participate\n");
1462
+ Log(CONNECTION,
1463
+ "startBridgeElection(): No router credentials, cannot participate\n");
1339
1464
  return;
1340
1465
  }
1341
-
1466
+
1342
1467
  if (electionState != ELECTION_IDLE) {
1343
1468
  Log(CONNECTION, "startBridgeElection(): Election already in progress\n");
1344
1469
  return;
1345
1470
  }
1346
-
1471
+
1347
1472
  // Prevent rapid role changes
1348
1473
  if (millis() - lastRoleChangeTime < 60000) {
1349
- Log(CONNECTION, "startBridgeElection(): Too soon after last role change\n");
1474
+ Log(CONNECTION,
1475
+ "startBridgeElection(): Too soon after last role change\n");
1350
1476
  return;
1351
1477
  }
1352
-
1478
+
1353
1479
  // CRITICAL: Check if mesh channel re-synchronization is needed first
1354
- // If we haven't found any mesh nodes and are approaching the re-sync threshold,
1355
- // prioritize finding the mesh over becoming a bridge. This prevents the scenario
1356
- // where a node tries to become a bridge when it should be re-syncing to find
1357
- // the mesh on a different channel (e.g., after another node became bridge and
1358
- // switched channels to match the router).
1480
+ // If we haven't found any mesh nodes and are approaching the re-sync
1481
+ // threshold, prioritize finding the mesh over becoming a bridge. This
1482
+ // prevents the scenario where a node tries to become a bridge when it
1483
+ // should be re-syncing to find the mesh on a different channel (e.g., after
1484
+ // another node became bridge and switched channels to match the router).
1359
1485
  uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
1360
1486
  if (emptyScans >= 3 && WiFi.status() != WL_CONNECTED) {
1361
- Log(CONNECTION,
1487
+ Log(CONNECTION,
1362
1488
  "startBridgeElection(): Mesh connectivity lost (%d empty scans), "
1363
- "deferring election to allow channel re-sync\n", emptyScans);
1364
-
1489
+ "deferring election to allow channel re-sync\n",
1490
+ emptyScans);
1491
+
1365
1492
  // Schedule a retry after channel re-sync has had a chance to run
1366
- // The channel re-sync threshold is StationScan::EMPTY_SCAN_THRESHOLD scans (default 6)
1367
- // Fast scan interval is 0.5 * SCAN_INTERVAL = 15 seconds
1368
- // Wait for re-sync to complete plus a buffer
1369
- uint32_t retryDelay = (StationScan::EMPTY_SCAN_THRESHOLD - emptyScans + 2) * 15000;
1370
- Log(CONNECTION,
1371
- "startBridgeElection(): Will retry election in %u seconds if still needed\n",
1493
+ // The channel re-sync threshold is StationScan::EMPTY_SCAN_THRESHOLD
1494
+ // scans (default 6) Fast scan interval is 0.5 * SCAN_INTERVAL = 15
1495
+ // seconds Wait for re-sync to complete plus a buffer
1496
+ uint32_t retryDelay =
1497
+ (StationScan::EMPTY_SCAN_THRESHOLD - emptyScans + 2) * 15000;
1498
+ Log(CONNECTION,
1499
+ "startBridgeElection(): Will retry election in %u seconds if still "
1500
+ "needed\n",
1372
1501
  retryDelay / 1000);
1373
1502
  return;
1374
1503
  }
1375
-
1504
+
1376
1505
  Log(CONNECTION, "=== Bridge Election Started ===\n");
1377
1506
  electionState = ELECTION_SCANNING;
1378
-
1507
+
1379
1508
  // Scan for router to get RSSI
1380
1509
  int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
1381
-
1510
+
1382
1511
  if (routerRSSI == 0) {
1383
- Log(CONNECTION, "startBridgeElection(): Router not visible, cannot participate\n");
1512
+ Log(CONNECTION,
1513
+ "startBridgeElection(): Router not visible, cannot participate\n");
1384
1514
  electionState = ELECTION_IDLE;
1385
1515
  return;
1386
1516
  }
1387
-
1388
- Log(CONNECTION, "startBridgeElection(): My router RSSI: %d dBm\n", routerRSSI);
1389
-
1517
+
1518
+ Log(CONNECTION, "startBridgeElection(): My router RSSI: %d dBm\n",
1519
+ routerRSSI);
1520
+
1390
1521
  // Clear previous candidates
1391
1522
  electionCandidates.clear();
1392
-
1523
+
1393
1524
  // Add self as candidate
1394
1525
  BridgeCandidate selfCandidate;
1395
1526
  selfCandidate.nodeId = this->nodeId;
@@ -1397,8 +1528,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1397
1528
  selfCandidate.uptime = millis();
1398
1529
  selfCandidate.freeMemory = ESP.getFreeHeap();
1399
1530
  electionCandidates.push_back(selfCandidate);
1400
-
1401
- // Broadcast candidacy using JSON directly (avoiding dependency on alteriom package)
1531
+
1532
+ // Broadcast candidacy using JSON directly (avoiding dependency on alteriom
1533
+ // package)
1402
1534
  JsonDocument doc;
1403
1535
  JsonObject obj = doc.to<JsonObject>();
1404
1536
  obj["type"] = protocol::BRIDGE_ELECTION;
@@ -1410,24 +1542,24 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1410
1542
  obj["timestamp"] = this->getNodeTime();
1411
1543
  obj["routerSSID"] = routerSSID;
1412
1544
  obj["message_type"] = protocol::BRIDGE_ELECTION;
1413
-
1545
+
1414
1546
  String msg;
1415
1547
  serializeJson(doc, msg);
1416
-
1417
- // Send election message using raw broadcast to preserve type BRIDGE_ELECTION
1548
+
1549
+ // Send election message using raw broadcast to preserve type
1550
+ // BRIDGE_ELECTION
1418
1551
  protocol::Variant variant(msg);
1419
1552
  router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
1420
-
1553
+
1421
1554
  Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
1422
-
1555
+
1423
1556
  // Set election timeout
1424
1557
  electionDeadline = millis() + electionTimeoutMs;
1425
1558
  electionState = ELECTION_COLLECTING;
1426
-
1559
+
1427
1560
  // Schedule election evaluation
1428
- this->addTask(electionTimeoutMs + 100, TASK_ONCE, [this]() {
1429
- this->evaluateElection();
1430
- });
1561
+ this->addTask(electionTimeoutMs + 100, TASK_ONCE,
1562
+ [this]() { this->evaluateElection(); });
1431
1563
  }
1432
1564
 
1433
1565
  /**
@@ -1436,23 +1568,26 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1436
1568
  */
1437
1569
  void evaluateElection() {
1438
1570
  using namespace logger;
1439
-
1571
+
1440
1572
  if (electionState != ELECTION_COLLECTING) {
1441
1573
  Log(CONNECTION, "evaluateElection(): Not in collecting state\n");
1442
1574
  return;
1443
1575
  }
1444
-
1576
+
1445
1577
  Log(CONNECTION, "=== Evaluating Election ===\n");
1446
- Log(CONNECTION, "evaluateElection(): %d candidates\n", electionCandidates.size());
1447
-
1578
+ Log(CONNECTION, "evaluateElection(): %d candidates\n",
1579
+ electionCandidates.size());
1580
+
1448
1581
  // Find best candidate
1449
1582
  BridgeCandidate* winner = nullptr;
1450
1583
  int8_t bestRSSI = -127; // Worst possible RSSI
1451
-
1584
+
1452
1585
  for (auto& candidate : electionCandidates) {
1453
- Log(CONNECTION, "evaluateElection(): Candidate %u: RSSI=%d, uptime=%u, mem=%u\n",
1454
- candidate.nodeId, candidate.routerRSSI, candidate.uptime, candidate.freeMemory);
1455
-
1586
+ Log(CONNECTION,
1587
+ "evaluateElection(): Candidate %u: RSSI=%d, uptime=%u, mem=%u\n",
1588
+ candidate.nodeId, candidate.routerRSSI, candidate.uptime,
1589
+ candidate.freeMemory);
1590
+
1456
1591
  if (candidate.routerRSSI > bestRSSI) {
1457
1592
  bestRSSI = candidate.routerRSSI;
1458
1593
  winner = &candidate;
@@ -1473,40 +1608,46 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1473
1608
  }
1474
1609
  }
1475
1610
  }
1476
-
1611
+
1477
1612
  if (winner == nullptr) {
1478
1613
  Log(ERROR, "evaluateElection(): No winner found!\n");
1479
1614
  electionState = ELECTION_IDLE;
1480
1615
  return;
1481
1616
  }
1482
-
1617
+
1483
1618
  // Validate RSSI threshold for single-candidate elections
1484
- // When only one candidate exists, it indicates the node is isolated from the mesh.
1485
- // In this case, require minimum signal quality to prevent poor connections.
1486
- // When multiple candidates exist, the mesh is connected and best RSSI wins.
1487
- if (electionCandidates.size() == 1 && winner->routerRSSI < minimumBridgeRSSI) {
1619
+ // When only one candidate exists, it indicates the node is isolated from
1620
+ // the mesh. In this case, require minimum signal quality to prevent poor
1621
+ // connections. When multiple candidates exist, the mesh is connected and
1622
+ // best RSSI wins.
1623
+ if (electionCandidates.size() == 1 &&
1624
+ winner->routerRSSI < minimumBridgeRSSI) {
1488
1625
  Log(CONNECTION, "=== Election Failed: Insufficient Signal Quality ===\n");
1489
- Log(CONNECTION, " Single candidate with RSSI %d dBm (minimum required: %d dBm)\n",
1626
+ Log(CONNECTION,
1627
+ " Single candidate with RSSI %d dBm (minimum required: %d dBm)\n",
1490
1628
  winner->routerRSSI, minimumBridgeRSSI);
1491
1629
  Log(CONNECTION, " Node is isolated from mesh with poor router signal\n");
1492
1630
  Log(CONNECTION, " Rejecting election to prevent unreliable bridge\n");
1493
- Log(CONNECTION, " Recommendation: Move closer to router or wait for mesh connection\n");
1494
-
1631
+ Log(CONNECTION,
1632
+ " Recommendation: Move closer to router or wait for mesh "
1633
+ "connection\n");
1634
+
1495
1635
  electionState = ELECTION_IDLE;
1496
1636
  electionCandidates.clear();
1497
-
1637
+
1498
1638
  // Notify via callback that election failed
1499
1639
  if (bridgeRoleChangedCallback) {
1500
- bridgeRoleChangedCallback(false, "Insufficient signal quality for isolated bridge");
1640
+ bridgeRoleChangedCallback(
1641
+ false, "Insufficient signal quality for isolated bridge");
1501
1642
  }
1502
1643
  return;
1503
1644
  }
1504
-
1645
+
1505
1646
  Log(CONNECTION, "=== Election Winner: Node %u ===\n", winner->nodeId);
1506
1647
  Log(CONNECTION, " Router RSSI: %d dBm\n", winner->routerRSSI);
1507
1648
  Log(CONNECTION, " Uptime: %u ms\n", winner->uptime);
1508
1649
  Log(CONNECTION, " Free Memory: %u bytes\n", winner->freeMemory);
1509
-
1650
+
1510
1651
  // Record election in diagnostics history
1511
1652
  if (this->diagnosticsEnabled) {
1512
1653
  ElectionRecord record;
@@ -1515,24 +1656,25 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1515
1656
  record.winnerRSSI = winner->routerRSSI;
1516
1657
  record.candidateCount = electionCandidates.size();
1517
1658
  record.reason = "Bridge failure detected";
1518
-
1659
+
1519
1660
  this->electionHistory.push_back(record);
1520
-
1661
+
1521
1662
  // Keep history limited to MAX_ELECTION_HISTORY
1522
1663
  if (this->electionHistory.size() > this->MAX_ELECTION_HISTORY) {
1523
1664
  this->electionHistory.erase(this->electionHistory.begin());
1524
1665
  }
1525
-
1666
+
1526
1667
  Log(CONNECTION, "evaluateElection(): Election recorded in history\n");
1527
1668
  }
1528
-
1669
+
1529
1670
  if (winner->nodeId == this->nodeId) {
1530
1671
  Log(CONNECTION, "🎯 I WON! Promoting to bridge...\n");
1531
1672
  promoteToBridge();
1532
1673
  } else {
1533
- Log(CONNECTION, "Winner is node %u, remaining as regular node\n", winner->nodeId);
1674
+ Log(CONNECTION, "Winner is node %u, remaining as regular node\n",
1675
+ winner->nodeId);
1534
1676
  }
1535
-
1677
+
1536
1678
  electionState = ELECTION_IDLE;
1537
1679
  electionCandidates.clear();
1538
1680
  }
@@ -1543,16 +1685,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1543
1685
  */
1544
1686
  void promoteToBridge() {
1545
1687
  using namespace logger;
1546
-
1688
+
1547
1689
  Log(STARTUP, "=== Becoming Bridge Node ===\n");
1548
-
1690
+
1549
1691
  // Store previous bridge (if any)
1550
1692
  auto primaryBridge = this->getPrimaryBridge();
1551
1693
  uint32_t previousBridgeId = primaryBridge ? primaryBridge->nodeId : 0;
1552
-
1694
+
1553
1695
  // IMPORTANT: Send takeover announcement BEFORE switching channels
1554
1696
  // This ensures other nodes on the current channel receive the announcement
1555
- Log(STARTUP, "Sending takeover announcement on current channel before switching...\n");
1697
+ Log(STARTUP,
1698
+ "Sending takeover announcement on current channel before "
1699
+ "switching...\n");
1556
1700
  JsonDocument doc;
1557
1701
  JsonObject obj = doc.to<JsonObject>();
1558
1702
  obj["type"] = protocol::BRIDGE_TAKEOVER;
@@ -1563,62 +1707,68 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1563
1707
  obj["routerRSSI"] = 0; // Not yet connected to router
1564
1708
  obj["timestamp"] = this->getNodeTime();
1565
1709
  obj["message_type"] = protocol::BRIDGE_TAKEOVER;
1566
-
1710
+
1567
1711
  String msg;
1568
1712
  serializeJson(doc, msg);
1569
-
1570
- // Send takeover message using raw broadcast to preserve type BRIDGE_TAKEOVER
1713
+
1714
+ // Send takeover message using raw broadcast to preserve type
1715
+ // BRIDGE_TAKEOVER
1571
1716
  protocol::Variant variant(msg);
1572
1717
  router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
1573
-
1718
+
1574
1719
  // Give time for announcement to propagate before channel switch
1575
1720
  delay(1000);
1576
1721
  Log(STARTUP, "✓ Takeover announcement sent on channel %d\n", _meshChannel);
1577
-
1722
+
1578
1723
  // Save current mesh configuration to restore if bridge init fails
1579
1724
  uint8_t savedChannel = _meshChannel;
1580
-
1725
+
1581
1726
  // Now reconfigure as bridge (this will switch to router's channel)
1582
1727
  this->stop();
1583
1728
  delay(1000);
1584
-
1585
- bool bridgeInitSuccess = this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
1586
- mScheduler, _meshPort);
1587
-
1729
+
1730
+ bool bridgeInitSuccess =
1731
+ this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
1732
+ mScheduler, _meshPort);
1733
+
1588
1734
  if (!bridgeInitSuccess) {
1589
1735
  Log(ERROR, "✗ Bridge promotion failed - router unreachable\n");
1590
1736
  Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
1591
-
1737
+
1592
1738
  // Re-initialize as regular node on the original channel
1593
1739
  this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
1594
1740
  savedChannel, _meshHidden, MAX_CONN);
1595
-
1596
- // Reset election state and clear candidates (consistent with normal election completion)
1741
+
1742
+ // Reset election state and clear candidates (consistent with normal
1743
+ // election completion)
1597
1744
  electionState = ELECTION_IDLE;
1598
1745
  electionCandidates.clear();
1599
-
1746
+
1600
1747
  // Notify via callback
1601
1748
  if (bridgeRoleChangedCallback) {
1602
- bridgeRoleChangedCallback(false, "Bridge promotion failed - router unreachable");
1749
+ bridgeRoleChangedCallback(
1750
+ false, "Bridge promotion failed - router unreachable");
1603
1751
  }
1604
-
1752
+
1605
1753
  return;
1606
1754
  }
1607
-
1755
+
1608
1756
  lastRoleChangeTime = millis();
1609
-
1757
+
1610
1758
  Log(STARTUP, "✓ Bridge promotion complete on channel %d\n", _meshChannel);
1611
-
1759
+
1612
1760
  // Notify via callback
1613
1761
  if (bridgeRoleChangedCallback) {
1614
1762
  bridgeRoleChangedCallback(true, "Election winner - best router signal");
1615
1763
  }
1616
-
1764
+
1617
1765
  // Send a follow-up announcement on the new channel
1618
- // This helps nodes that have already switched channels to discover the new bridge
1619
- // Schedule it after a delay to ensure mesh is fully initialized
1766
+ // This helps nodes that have already switched channels to discover the new
1767
+ // bridge Schedule it after a delay to ensure mesh is fully initialized
1620
1768
  this->addTask(3000, TASK_ONCE, [this, previousBridgeId]() {
1621
- Log(STARTUP, "Sending follow-up takeover announcement on new channel %d\n", _meshChannel);
1769
+ Log(STARTUP,
1770
+ "Sending follow-up takeover announcement on new channel %d\n",
1771
+ _meshChannel);
1622
1772
  JsonDocument doc2;
1623
1773
  JsonObject obj2 = doc2.to<JsonObject>();
1624
1774
  obj2["type"] = protocol::BRIDGE_TAKEOVER;
@@ -1629,104 +1779,123 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1629
1779
  obj2["routerRSSI"] = WiFi.RSSI();
1630
1780
  obj2["timestamp"] = this->getNodeTime();
1631
1781
  obj2["message_type"] = protocol::BRIDGE_TAKEOVER;
1632
-
1782
+
1633
1783
  String msg2;
1634
1784
  serializeJson(doc2, msg2);
1635
-
1636
- // Send follow-up takeover using raw broadcast to preserve type BRIDGE_TAKEOVER
1785
+
1786
+ // Send follow-up takeover using raw broadcast to preserve type
1787
+ // BRIDGE_TAKEOVER
1637
1788
  protocol::Variant variant2(msg2);
1638
1789
  router::broadcast<protocol::Variant, Connection>(variant2, (*this), 0);
1639
-
1790
+
1640
1791
  Log(STARTUP, "✓ Follow-up takeover announcement sent\n");
1641
1792
  });
1642
1793
  }
1643
1794
 
1644
1795
  /**
1645
1796
  * Attempt to promote an isolated node to bridge
1646
- *
1797
+ *
1647
1798
  * This method handles the case where a node is isolated (no mesh connections)
1648
1799
  * but has router credentials. Unlike the election-based promotion, this
1649
- * directly attempts to connect to the router without requiring mesh connectivity.
1650
- *
1800
+ * directly attempts to connect to the router without requiring mesh
1801
+ * connectivity.
1802
+ *
1651
1803
  * This is useful for:
1652
1804
  * - Nodes that failed initial bridge setup and need to retry
1653
1805
  * - Nodes that are the first to start and no mesh exists yet
1654
1806
  * - Recovery scenarios where mesh network is unavailable
1655
1807
  *
1656
- * @return true if promotion was attempted (regardless of success), false if skipped
1808
+ * @return true if promotion was attempted (regardless of success), false if
1809
+ * skipped
1657
1810
  */
1658
1811
  bool attemptIsolatedBridgePromotion() {
1659
1812
  using namespace logger;
1660
-
1813
+
1661
1814
  Log(CONNECTION, "=== Isolated Bridge Promotion Attempt ===\n");
1662
- Log(CONNECTION, "Attempt %d of %d\n", _isolatedBridgeRetryAttempts + 1, MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS);
1663
-
1815
+ Log(CONNECTION, "Attempt %d of %d\n", _isolatedBridgeRetryAttempts + 1,
1816
+ MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS);
1817
+
1664
1818
  // First, scan for router to check if it's visible
1665
1819
  int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
1666
-
1820
+
1667
1821
  if (routerRSSI == 0) {
1668
- Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router %s not visible\n", routerSSID.c_str());
1822
+ Log(CONNECTION,
1823
+ "attemptIsolatedBridgePromotion(): Router %s not visible\n",
1824
+ routerSSID.c_str());
1669
1825
  return false; // Don't count as an attempt - router not visible
1670
1826
  }
1671
-
1827
+
1672
1828
  // Check minimum RSSI threshold for isolated promotion
1673
1829
  if (routerRSSI < minimumBridgeRSSI) {
1674
- Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router RSSI %d dBm below threshold %d dBm\n",
1830
+ Log(CONNECTION,
1831
+ "attemptIsolatedBridgePromotion(): Router RSSI %d dBm below "
1832
+ "threshold %d dBm\n",
1675
1833
  routerRSSI, minimumBridgeRSSI);
1676
1834
  return false; // Don't count as an attempt - signal too weak
1677
1835
  }
1678
-
1679
- Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router visible with RSSI %d dBm\n", routerRSSI);
1680
- Log(CONNECTION, "Attempting direct bridge promotion (bypassing election)\n");
1681
-
1836
+
1837
+ Log(CONNECTION,
1838
+ "attemptIsolatedBridgePromotion(): Router visible with RSSI %d dBm\n",
1839
+ routerRSSI);
1840
+ Log(CONNECTION,
1841
+ "Attempting direct bridge promotion (bypassing election)\n");
1842
+
1682
1843
  // Save current mesh configuration
1683
1844
  uint8_t savedChannel = _meshChannel;
1684
-
1845
+
1685
1846
  // Stop current mesh operations
1686
1847
  this->stop();
1687
1848
  delay(1000);
1688
-
1849
+
1689
1850
  // Attempt to initialize as bridge
1690
- bool bridgeInitSuccess = this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
1691
- mScheduler, _meshPort);
1692
-
1851
+ bool bridgeInitSuccess =
1852
+ this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
1853
+ mScheduler, _meshPort);
1854
+
1693
1855
  if (!bridgeInitSuccess) {
1694
1856
  Log(ERROR, "✗ Isolated bridge promotion failed - router unreachable\n");
1695
1857
  Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
1696
-
1858
+
1697
1859
  // Re-initialize as regular node on the original channel
1698
1860
  this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
1699
1861
  savedChannel, _meshHidden, MAX_CONN);
1700
-
1862
+
1701
1863
  // Re-configure router credentials for future retry attempts
1702
1864
  this->setRouterCredentials(routerSSID, routerPassword);
1703
1865
  this->enableBridgeFailover(true);
1704
-
1866
+
1867
+ // Set flag to skip empty scan check on next retry attempt
1868
+ // since we already confirmed isolation before this failed attempt
1869
+ _isolatedRetryPending = true;
1870
+
1705
1871
  // Notify via callback
1706
1872
  if (bridgeRoleChangedCallback) {
1707
- bridgeRoleChangedCallback(false, "Isolated bridge promotion failed - router unreachable");
1873
+ bridgeRoleChangedCallback(
1874
+ false, "Isolated bridge promotion failed - router unreachable");
1708
1875
  }
1709
-
1876
+
1710
1877
  return true; // Count as an attempt - we tried but failed
1711
1878
  }
1712
-
1879
+
1713
1880
  // Success! Reset retry counter
1714
1881
  _isolatedBridgeRetryAttempts = 0;
1715
1882
  lastRoleChangeTime = millis();
1716
-
1717
- Log(STARTUP, "✓ Isolated bridge promotion complete on channel %d\n", _meshChannel);
1718
-
1883
+
1884
+ Log(STARTUP, "✓ Isolated bridge promotion complete on channel %d\n",
1885
+ _meshChannel);
1886
+
1719
1887
  // Notify via callback
1720
1888
  if (bridgeRoleChangedCallback) {
1721
1889
  bridgeRoleChangedCallback(true, "Isolated node promoted to bridge");
1722
1890
  }
1723
-
1891
+
1724
1892
  // Send bridge status announcement to attract other nodes
1725
1893
  this->addTask(3000, TASK_ONCE, [this]() {
1726
- Log(STARTUP, "Sending bridge status announcement on channel %d\n", _meshChannel);
1894
+ Log(STARTUP, "Sending bridge status announcement on channel %d\n",
1895
+ _meshChannel);
1727
1896
  this->sendBridgeStatus();
1728
1897
  });
1729
-
1898
+
1730
1899
  return true; // Count as an attempt - we succeeded
1731
1900
  }
1732
1901
 
@@ -1734,33 +1903,37 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1734
1903
  * Handle received bridge election package
1735
1904
  * Called by package handler when election message arrives
1736
1905
  */
1737
- void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI, uint32_t uptime,
1738
- uint32_t freeMemory) {
1906
+ void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI,
1907
+ uint32_t uptime, uint32_t freeMemory) {
1739
1908
  using namespace logger;
1740
-
1909
+
1741
1910
  if (electionState != ELECTION_COLLECTING) {
1742
- Log(CONNECTION, "handleBridgeElection(): Not collecting candidates, ignoring\n");
1911
+ Log(CONNECTION,
1912
+ "handleBridgeElection(): Not collecting candidates, ignoring\n");
1743
1913
  return;
1744
1914
  }
1745
-
1915
+
1746
1916
  // Check if candidate already exists
1747
1917
  for (auto& candidate : electionCandidates) {
1748
1918
  if (candidate.nodeId == fromNode) {
1749
- Log(CONNECTION, "handleBridgeElection(): Duplicate candidate from %u, ignoring\n", fromNode);
1919
+ Log(CONNECTION,
1920
+ "handleBridgeElection(): Duplicate candidate from %u, ignoring\n",
1921
+ fromNode);
1750
1922
  return;
1751
1923
  }
1752
1924
  }
1753
-
1925
+
1754
1926
  BridgeCandidate candidate;
1755
1927
  candidate.nodeId = fromNode;
1756
1928
  candidate.routerRSSI = routerRSSI;
1757
1929
  candidate.uptime = uptime;
1758
1930
  candidate.freeMemory = freeMemory;
1759
-
1931
+
1760
1932
  electionCandidates.push_back(candidate);
1761
-
1762
- Log(CONNECTION, "handleBridgeElection(): Added candidate %u (RSSI: %d dBm)\n",
1763
- fromNode, routerRSSI);
1933
+
1934
+ Log(CONNECTION,
1935
+ "handleBridgeElection(): Added candidate %u (RSSI: %d dBm)\n", fromNode,
1936
+ routerRSSI);
1764
1937
  }
1765
1938
 
1766
1939
  /**
@@ -1769,60 +1942,200 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1769
1942
  */
1770
1943
  void sendBridgeStatus() {
1771
1944
  using namespace logger;
1772
-
1945
+
1773
1946
  if (!this->bridgeStatusBroadcastEnabled) {
1774
1947
  return;
1775
1948
  }
1776
-
1949
+
1777
1950
  // Create bridge status package
1778
1951
  // We need to include the package header here since we're in wifi namespace
1779
1952
  // The package will be sent as a JSON string
1780
1953
  JsonDocument doc;
1781
1954
  JsonObject obj = doc.to<JsonObject>();
1782
-
1955
+
1783
1956
  obj["type"] = protocol::BRIDGE_STATUS;
1784
1957
  obj["from"] = this->nodeId;
1785
1958
  obj["routing"] = 2; // BROADCAST routing
1786
1959
  obj["timestamp"] = this->getNodeTime();
1787
-
1960
+
1788
1961
  // Check Internet connectivity: WiFi connected AND valid IP address
1789
1962
  // We check for valid local IP instead of gateway IP because:
1790
1963
  // 1. Gateway IP might not be immediately available after connection
1791
1964
  // 2. Some networks (mobile hotspots) may not provide gateway IP via DHCP
1792
- // 3. Having a valid local IP + being connected is sufficient for internet access
1793
- bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1965
+ // 3. Having a valid local IP + being connected is sufficient for internet
1966
+ // access
1967
+ bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1794
1968
  (WiFi.localIP() != IPAddress(0, 0, 0, 0));
1795
1969
  obj["internetConnected"] = hasInternet;
1796
-
1970
+
1797
1971
  int8_t rssi = WiFi.RSSI();
1798
1972
  uint8_t channel = WiFi.channel();
1799
1973
  uint32_t uptime = millis();
1800
1974
  TSTRING gatewayIP = WiFi.gatewayIP().toString();
1801
-
1975
+
1802
1976
  obj["routerRSSI"] = rssi;
1803
1977
  obj["routerChannel"] = channel;
1804
1978
  obj["uptime"] = uptime;
1805
1979
  obj["gatewayIP"] = gatewayIP;
1806
1980
  obj["message_type"] = protocol::BRIDGE_STATUS;
1807
-
1981
+
1808
1982
  String msg;
1809
1983
  serializeJson(doc, msg);
1810
-
1984
+
1811
1985
  Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)\n",
1812
1986
  hasInternet ? "Connected" : "Disconnected");
1813
- Log(GENERAL, "sendBridgeStatus(): WiFi status=%d, localIP=%s, gatewayIP=%s\n",
1814
- WiFi.status(), WiFi.localIP().toString().c_str(), WiFi.gatewayIP().toString().c_str());
1815
-
1987
+ Log(GENERAL,
1988
+ "sendBridgeStatus(): WiFi status=%d, localIP=%s, gatewayIP=%s\n",
1989
+ WiFi.status(), WiFi.localIP().toString().c_str(),
1990
+ WiFi.gatewayIP().toString().c_str());
1991
+
1816
1992
  // Update our own bridge status in knownBridges list
1817
1993
  // This ensures the bridge reports itself correctly when queried
1818
- this->updateBridgeStatus(this->nodeId, hasInternet, rssi, channel,
1819
- uptime, gatewayIP, this->getNodeTime());
1820
-
1994
+ this->updateBridgeStatus(this->nodeId, hasInternet, rssi, channel, uptime,
1995
+ gatewayIP, this->getNodeTime());
1996
+
1821
1997
  // Send bridge status using raw broadcast to preserve type BRIDGE_STATUS
1822
- // Using sendBroadcast(msg) would wrap it in type 8 (BROADCAST) and hide type BRIDGE_STATUS
1998
+ // Using sendBroadcast(msg) would wrap it in type 8 (BROADCAST) and hide
1999
+ // type BRIDGE_STATUS
1823
2000
  protocol::Variant variant(msg);
1824
2001
  router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
1825
2002
  }
2003
+
2004
+ /**
2005
+ * Helper method to send gateway acknowledgment
2006
+ */
2007
+ void sendGatewayAck(const gateway::GatewayDataPackage& request, bool success,
2008
+ uint16_t httpStatus, const TSTRING& error) {
2009
+ using namespace logger;
2010
+
2011
+ gateway::GatewayAckPackage ack;
2012
+ ack.from = this->nodeId;
2013
+ ack.dest = request.originNode;
2014
+ ack.messageId = request.messageId;
2015
+ ack.originNode = request.originNode;
2016
+ ack.success = success;
2017
+ ack.httpStatus = httpStatus;
2018
+ ack.error = error;
2019
+ ack.timestamp = this->getNodeTime();
2020
+
2021
+ auto conn = router::findRoute<Connection>((*this), request.originNode);
2022
+ if (conn) {
2023
+ protocol::Variant variant(&ack);
2024
+ router::send(std::move(variant), conn);
2025
+ Log(COMMUNICATION, "Sent GATEWAY_ACK to node %u (success=%d, http=%d)\n",
2026
+ request.originNode, success, httpStatus);
2027
+ } else {
2028
+ Log(ERROR, "Failed to send GATEWAY_ACK: no route to node %u\n",
2029
+ request.originNode);
2030
+ }
2031
+ }
2032
+
2033
+ /**
2034
+ * Initialize gateway Internet handler
2035
+ * Registers GATEWAY_DATA package handler for bridge/gateway nodes
2036
+ *
2037
+ * This handler processes GATEWAY_DATA packages from mesh nodes requesting
2038
+ * HTTP/HTTPS requests to Internet destinations. It validates connectivity,
2039
+ * makes the request, and sends back a GATEWAY_ACK with the result.
2040
+ *
2041
+ * Security notes:
2042
+ * - HTTPS on ESP8266 uses setInsecure() which disables SSL certificate
2043
+ * validation to reduce memory overhead. This makes connections vulnerable to
2044
+ * MITM attacks.
2045
+ * - ESP32 uses default SSL settings with certificate validation.
2046
+ *
2047
+ * Limitations:
2048
+ * - HTTP redirects (3xx) are not automatically followed
2049
+ * - Only 2xx status codes are treated as success
2050
+ * - Request timeout is fixed at 30 seconds
2051
+ */
2052
+ void initGatewayInternetHandler() {
2053
+ using namespace logger;
2054
+ Log(STARTUP,
2055
+ "initGatewayInternetHandler(): Registering GATEWAY_DATA handler\n");
2056
+
2057
+ this->callbackList.onPackage(
2058
+ protocol::GATEWAY_DATA, [this](protocol::Variant& variant,
2059
+ std::shared_ptr<Connection>, uint32_t) {
2060
+ auto pkg = variant.to<gateway::GatewayDataPackage>();
2061
+
2062
+ Log(COMMUNICATION,
2063
+ "Gateway received Internet request: msgId=%u dest=%s\n",
2064
+ pkg.messageId, pkg.destination.c_str());
2065
+
2066
+ // Check Internet connectivity
2067
+ if (WiFi.status() != WL_CONNECTED) {
2068
+ sendGatewayAck(pkg, false, 0, "Gateway not connected to Internet");
2069
+ return true; // Consume package - we handled it (with error)
2070
+ }
2071
+
2072
+ #if defined(ESP32) || defined(ESP8266)
2073
+ // Make HTTP/HTTPS request
2074
+ HTTPClient http;
2075
+ http.setTimeout(GATEWAY_HTTP_TIMEOUT_MS);
2076
+
2077
+ bool success = false;
2078
+ uint16_t httpCode = 0;
2079
+ TSTRING error = "";
2080
+
2081
+ #ifdef ESP8266
2082
+ // ESP8266: Declare clients at function scope to ensure
2083
+ // they survive until after the HTTP request completes
2084
+ WiFiClient client;
2085
+ WiFiClientSecure secureClient;
2086
+ #endif
2087
+
2088
+ if (pkg.destination.startsWith("https://")) {
2089
+ #ifdef ESP32
2090
+ // ESP32: Use default SSL settings with certificate validation
2091
+ http.begin(pkg.destination.c_str());
2092
+ #elif defined(ESP8266)
2093
+ // ESP8266: Use insecure mode to reduce memory overhead
2094
+ // WARNING: This disables SSL certificate validation
2095
+ secureClient.setInsecure();
2096
+ http.begin(secureClient, pkg.destination.c_str());
2097
+ #endif
2098
+ } else {
2099
+ #ifdef ESP32
2100
+ http.begin(pkg.destination.c_str());
2101
+ #elif defined(ESP8266)
2102
+ // ESP8266: begin() requires a client parameter
2103
+ http.begin(client, pkg.destination.c_str());
2104
+ #endif
2105
+ }
2106
+
2107
+ // Make request (GET if no payload, POST if payload)
2108
+ if (pkg.payload.length() > 0) {
2109
+ http.addHeader("Content-Type", pkg.contentType.c_str());
2110
+ httpCode = http.POST(pkg.payload.c_str());
2111
+ } else {
2112
+ httpCode = http.GET();
2113
+ }
2114
+
2115
+ if (httpCode > 0) {
2116
+ // Only 2xx status codes are treated as success
2117
+ // 3xx redirects are not automatically followed
2118
+ success = (httpCode >= 200 && httpCode < 300);
2119
+ Log(COMMUNICATION, "HTTP request completed: code=%d\n", httpCode);
2120
+ } else {
2121
+ error = http.errorToString(httpCode);
2122
+ Log(ERROR, "HTTP request failed: %s\n", error.c_str());
2123
+ }
2124
+
2125
+ http.end();
2126
+
2127
+ // Send acknowledgment back
2128
+ sendGatewayAck(pkg, success, httpCode, error);
2129
+ #else
2130
+ // Non-ESP platform - send error
2131
+ sendGatewayAck(pkg, false, 0, "HTTP client not available on this platform");
2132
+ #endif
2133
+
2134
+ return true; // Consume package - we have processed it and sent
2135
+ // acknowledgment
2136
+ });
2137
+ }
2138
+
1826
2139
  void eventHandleInit() {
1827
2140
  using namespace logger;
1828
2141
  #ifdef ESP32
@@ -1890,14 +2203,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1890
2203
 
1891
2204
  #elif defined(ESP8266)
1892
2205
  eventSTAConnectedHandler = WiFi.onStationModeConnected(
1893
- [&](const WiFiEventStationModeConnected &event) {
2206
+ [&](const WiFiEventStationModeConnected& event) {
1894
2207
  // Log(CONNECTION, "Event: Station Mode Connected to \"%s\"\n",
1895
2208
  // event.ssid.c_str());
1896
2209
  Log(CONNECTION, "Event: Station Mode Connected\n");
1897
2210
  });
1898
2211
 
1899
2212
  eventSTADisconnectedHandler = WiFi.onStationModeDisconnected(
1900
- [&](const WiFiEventStationModeDisconnected &event) {
2213
+ [&](const WiFiEventStationModeDisconnected& event) {
1901
2214
  Log(CONNECTION, "Event: Station Mode Disconnected\n");
1902
2215
  this->droppedConnectionCallbacks.execute(0, true);
1903
2216
  // Handle station disconnect completion after callbacks
@@ -1905,7 +2218,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1905
2218
  });
1906
2219
 
1907
2220
  eventSTAGotIPHandler =
1908
- WiFi.onStationModeGotIP([&](const WiFiEventStationModeGotIP &event) {
2221
+ WiFi.onStationModeGotIP([&](const WiFiEventStationModeGotIP& event) {
1909
2222
  Log(CONNECTION,
1910
2223
  "Event: Station Mode Got IP (IP: %s Mask: %s Gateway: %s)\n",
1911
2224
  event.ip.toString().c_str(), event.mask.toString().c_str(),
@@ -1926,18 +2239,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1926
2239
  WiFiEventHandler eventSTADisconnectedHandler;
1927
2240
  WiFiEventHandler eventSTAGotIPHandler;
1928
2241
  #endif // ESP8266
1929
- AsyncServer *_tcpListener;
2242
+ AsyncServer* _tcpListener;
1930
2243
  std::shared_ptr<Task> bridgeStatusTask;
1931
-
2244
+
1932
2245
  // Station disconnect handling state
1933
2246
  bool _pendingStationReconnect = false;
1934
2247
 
1935
2248
  // Bridge failover state and configuration
1936
- enum ElectionState {
1937
- ELECTION_IDLE,
1938
- ELECTION_SCANNING,
1939
- ELECTION_COLLECTING
1940
- };
2249
+ enum ElectionState { ELECTION_IDLE, ELECTION_SCANNING, ELECTION_COLLECTING };
1941
2250
 
1942
2251
  struct BridgeCandidate {
1943
2252
  uint32_t nodeId;
@@ -1951,10 +2260,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1951
2260
  TSTRING routerSSID = "";
1952
2261
  TSTRING routerPassword = "";
1953
2262
  uint32_t electionTimeoutMs = 5000; // Default 5 seconds
1954
- int8_t minimumBridgeRSSI = -80; // Default -80 dBm minimum for isolated elections
1955
- uint32_t electionStartupDelayMs = 60000; // Default 60 seconds before first election check
1956
- uint32_t electionRandomDelayMinMs = 1000; // Default min 1 second random delay
1957
- uint32_t electionRandomDelayMaxMs = 3000; // Default max 3 seconds random delay
2263
+ int8_t minimumBridgeRSSI =
2264
+ -80; // Default -80 dBm minimum for isolated elections
2265
+ uint32_t electionStartupDelayMs =
2266
+ 60000; // Default 60 seconds before first election check
2267
+ uint32_t electionRandomDelayMinMs =
2268
+ 1000; // Default min 1 second random delay
2269
+ uint32_t electionRandomDelayMaxMs =
2270
+ 3000; // Default max 3 seconds random delay
1958
2271
  uint32_t lastRoleChangeTime = 0;
1959
2272
  ElectionState electionState = ELECTION_IDLE;
1960
2273
  uint32_t electionDeadline = 0;
@@ -1963,24 +2276,31 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1963
2276
 
1964
2277
  // Isolated bridge retry state and configuration
1965
2278
  uint8_t _isolatedBridgeRetryAttempts = 0;
1966
- uint32_t _isolatedBridgeRetryResetTime = 0; // Time when retry counter can be reset
1967
- static const uint8_t MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS = 5; // Max retry attempts before waiting
1968
- static const uint32_t isolatedBridgeRetryIntervalMs = 60000; // Retry every 60 seconds
1969
- static const uint32_t isolatedBridgeRetryResetIntervalMs = 300000; // Reset counter after 5 minutes
1970
- static const uint16_t ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD = 6; // Require 6 empty scans before retrying
2279
+ uint32_t _isolatedBridgeRetryResetTime =
2280
+ 0; // Time when retry counter can be reset
2281
+ bool _isolatedRetryPending =
2282
+ false; // Flag to skip empty scan check after failed promotion
2283
+ static const uint8_t MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS =
2284
+ 5; // Max retry attempts before waiting
2285
+ static const uint32_t isolatedBridgeRetryIntervalMs =
2286
+ 60000; // Retry every 60 seconds
2287
+ static const uint32_t isolatedBridgeRetryResetIntervalMs =
2288
+ 300000; // Reset counter after 5 minutes
2289
+ static const uint16_t ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD =
2290
+ 6; // Require 6 empty scans before retrying
1971
2291
 
1972
2292
  // Multi-bridge coordination state and configuration
1973
2293
  protected:
1974
2294
  bool multiBridgeEnabled = false;
1975
2295
  BridgeSelectionStrategy bridgeSelectionStrategy = PRIORITY_BASED;
1976
2296
  uint8_t maxConcurrentBridges = 2;
1977
- uint8_t bridgePriority = 5; // Default medium priority
2297
+ uint8_t bridgePriority = 5; // Default medium priority
1978
2298
  TSTRING bridgeRole = "secondary"; // Default role
1979
2299
  std::shared_ptr<Task> bridgeCoordinationTask;
1980
2300
  std::map<uint32_t, uint8_t> bridgePriorities; // nodeId -> priority mapping
1981
- std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
2301
+ std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
1982
2302
  uint32_t selectedBridgeOverride = 0; // Manual bridge selection override
1983
- size_t lastSelectedBridgeIndex = 0; // For round-robin selection
2303
+ size_t lastSelectedBridgeIndex = 0; // For round-robin selection
1984
2304
 
1985
2305
  // Shared gateway mode state and configuration
1986
2306
  bool _sharedGatewayMode = false;
@@ -1989,72 +2309,83 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1989
2309
  uint32_t _lastRouterReconnectAttempt = 0;
1990
2310
  uint8_t _routerReconnectAttempts = 0;
1991
2311
  static const uint8_t MAX_ROUTER_RECONNECT_ATTEMPTS = 10;
1992
- static const uint32_t ROUTER_RECONNECT_BASE_INTERVAL = 5000; // 5 seconds base interval
1993
- static const uint32_t ROUTER_RECONNECT_MAX_INTERVAL = 300000; // 5 minutes max interval
1994
- static const int ROUTER_CONNECTION_TIMEOUT_SECONDS = 30; // Router connection timeout
2312
+ static const uint32_t ROUTER_RECONNECT_BASE_INTERVAL =
2313
+ 5000; // 5 seconds base interval
2314
+ static const uint32_t ROUTER_RECONNECT_MAX_INTERVAL =
2315
+ 300000; // 5 minutes max interval
2316
+ static const int ROUTER_CONNECTION_TIMEOUT_SECONDS =
2317
+ 30; // Router connection timeout
1995
2318
  static const uint8_t MIN_WIFI_CHANNEL = 1;
1996
- static const uint8_t MAX_WIFI_CHANNEL = 14; // Support channels 1-14 for regions that allow it
2319
+ static const uint8_t MAX_WIFI_CHANNEL =
2320
+ 14; // Support channels 1-14 for regions that allow it
2321
+ static const uint32_t GATEWAY_HTTP_TIMEOUT_MS =
2322
+ 30000; // 30 second timeout for gateway HTTP requests
1997
2323
 
1998
2324
  /**
1999
2325
  * Initialize shared gateway monitoring
2000
- *
2326
+ *
2001
2327
  * Sets up periodic monitoring of router connection and automatic
2002
2328
  * reconnection logic for shared gateway mode.
2003
2329
  */
2004
2330
  void initSharedGatewayMonitoring() {
2005
2331
  using namespace logger;
2006
-
2332
+
2007
2333
  if (!_sharedGatewayMode) {
2008
2334
  return;
2009
2335
  }
2010
-
2011
- Log(STARTUP, "initSharedGatewayMonitoring(): Setting up router connection monitoring\n");
2012
-
2336
+
2337
+ Log(STARTUP,
2338
+ "initSharedGatewayMonitoring(): Setting up router connection "
2339
+ "monitoring\n");
2340
+
2013
2341
  // Add callback for router disconnection in shared gateway mode
2014
2342
  this->droppedConnectionCallbacks.push_back(
2015
2343
  [this](uint32_t nodeId, bool station) {
2016
2344
  if (station && _sharedGatewayMode) {
2017
- Log(CONNECTION, "Router disconnected in shared gateway mode, scheduling reconnection\n");
2345
+ Log(CONNECTION,
2346
+ "Router disconnected in shared gateway mode, scheduling "
2347
+ "reconnection\n");
2018
2348
  scheduleRouterReconnect();
2019
2349
  }
2020
2350
  });
2021
-
2351
+
2022
2352
  // Create periodic monitoring task
2023
- _sharedGatewayMonitorTask = this->addTask(
2024
- _sharedGatewayConfig.internetCheckInterval,
2025
- TASK_FOREVER,
2026
- [this]() {
2027
- monitorRouterConnection();
2028
- });
2029
-
2353
+ _sharedGatewayMonitorTask =
2354
+ this->addTask(_sharedGatewayConfig.internetCheckInterval, TASK_FOREVER,
2355
+ [this]() { monitorRouterConnection(); });
2356
+
2030
2357
  Log(STARTUP, "Router connection monitoring enabled (interval: %u ms)\n",
2031
2358
  _sharedGatewayConfig.internetCheckInterval);
2032
2359
  }
2033
2360
 
2034
2361
  /**
2035
2362
  * Monitor router connection in shared gateway mode
2036
- *
2363
+ *
2037
2364
  * Checks router connectivity and triggers reconnection if needed.
2038
2365
  */
2039
2366
  void monitorRouterConnection() {
2040
2367
  using namespace logger;
2041
-
2368
+
2042
2369
  if (!_sharedGatewayMode) {
2043
2370
  return;
2044
2371
  }
2045
-
2046
- bool isConnected = (WiFi.status() == WL_CONNECTED) &&
2372
+
2373
+ bool isConnected = (WiFi.status() == WL_CONNECTED) &&
2047
2374
  (WiFi.localIP() != IPAddress(0, 0, 0, 0));
2048
-
2375
+
2049
2376
  if (!isConnected) {
2050
- Log(CONNECTION, "monitorRouterConnection(): Router connection lost, triggering reconnect\n");
2377
+ Log(CONNECTION,
2378
+ "monitorRouterConnection(): Router connection lost, triggering "
2379
+ "reconnect\n");
2051
2380
  scheduleRouterReconnect();
2052
2381
  } else {
2053
2382
  // Connection is healthy, reset reconnect attempts
2054
2383
  _routerReconnectAttempts = 0;
2055
-
2384
+
2056
2385
  // Log periodic status
2057
- Log(GENERAL, "monitorRouterConnection(): Router connected (RSSI: %d dBm, IP: %s)\n",
2386
+ Log(GENERAL,
2387
+ "monitorRouterConnection(): Router connected (RSSI: %d dBm, IP: "
2388
+ "%s)\n",
2058
2389
  WiFi.RSSI(), WiFi.localIP().toString().c_str());
2059
2390
  }
2060
2391
  }
@@ -2064,46 +2395,51 @@ class Mesh : public painlessmesh::Mesh<Connection> {
2064
2395
  */
2065
2396
  void scheduleRouterReconnect() {
2066
2397
  using namespace logger;
2067
-
2398
+
2068
2399
  if (!_sharedGatewayMode) {
2069
2400
  return;
2070
2401
  }
2071
-
2402
+
2072
2403
  // Don't schedule if already connected
2073
2404
  if (WiFi.status() == WL_CONNECTED) {
2074
2405
  return;
2075
2406
  }
2076
-
2407
+
2077
2408
  // Limit reconnection attempts
2078
2409
  if (_routerReconnectAttempts >= MAX_ROUTER_RECONNECT_ATTEMPTS) {
2079
- Log(ERROR, "scheduleRouterReconnect(): Max reconnection attempts reached (%d)\n",
2410
+ Log(ERROR,
2411
+ "scheduleRouterReconnect(): Max reconnection attempts reached (%d)\n",
2080
2412
  MAX_ROUTER_RECONNECT_ATTEMPTS);
2081
- Log(ERROR, "Router reconnection suspended. Manual intervention may be required.\n");
2413
+ Log(ERROR,
2414
+ "Router reconnection suspended. Manual intervention may be "
2415
+ "required.\n");
2082
2416
  return;
2083
2417
  }
2084
-
2418
+
2085
2419
  // Calculate delay with exponential backoff, preventing overflow
2086
2420
  // Limit shift amount to prevent overflow (5000 * 2^6 = 320000 is safe)
2087
- uint8_t shiftAmount = (_routerReconnectAttempts > 6) ? 6 : _routerReconnectAttempts;
2421
+ uint8_t shiftAmount =
2422
+ (_routerReconnectAttempts > 6) ? 6 : _routerReconnectAttempts;
2088
2423
  uint32_t delay = ROUTER_RECONNECT_BASE_INTERVAL * (1UL << shiftAmount);
2089
- if (delay > ROUTER_RECONNECT_MAX_INTERVAL) delay = ROUTER_RECONNECT_MAX_INTERVAL;
2090
-
2424
+ if (delay > ROUTER_RECONNECT_MAX_INTERVAL)
2425
+ delay = ROUTER_RECONNECT_MAX_INTERVAL;
2426
+
2091
2427
  // Don't reconnect too frequently
2092
2428
  uint32_t now = millis();
2093
2429
  if (now - _lastRouterReconnectAttempt < delay) {
2094
2430
  return;
2095
2431
  }
2096
-
2432
+
2097
2433
  _routerReconnectAttempts++;
2098
2434
  _lastRouterReconnectAttempt = now;
2099
-
2100
- Log(CONNECTION, "scheduleRouterReconnect(): Attempting reconnection (attempt %d/%d, delay %u ms)\n",
2435
+
2436
+ Log(CONNECTION,
2437
+ "scheduleRouterReconnect(): Attempting reconnection (attempt %d/%d, "
2438
+ "delay %u ms)\n",
2101
2439
  _routerReconnectAttempts, MAX_ROUTER_RECONNECT_ATTEMPTS, delay);
2102
-
2440
+
2103
2441
  // Schedule reconnection
2104
- this->addTask(delay, TASK_ONCE, [this]() {
2105
- attemptRouterReconnect();
2106
- });
2442
+ this->addTask(delay, TASK_ONCE, [this]() { attemptRouterReconnect(); });
2107
2443
  }
2108
2444
 
2109
2445
  /**
@@ -2111,23 +2447,26 @@ class Mesh : public painlessmesh::Mesh<Connection> {
2111
2447
  */
2112
2448
  void attemptRouterReconnect() {
2113
2449
  using namespace logger;
2114
-
2450
+
2115
2451
  if (!_sharedGatewayMode) {
2116
2452
  return;
2117
2453
  }
2118
-
2454
+
2119
2455
  // Check if already connected
2120
2456
  if (WiFi.status() == WL_CONNECTED) {
2121
- Log(CONNECTION, "attemptRouterReconnect(): Already connected to router\n");
2457
+ Log(CONNECTION,
2458
+ "attemptRouterReconnect(): Already connected to router\n");
2122
2459
  _routerReconnectAttempts = 0;
2123
2460
  return;
2124
2461
  }
2125
-
2462
+
2126
2463
  Log(CONNECTION, "attemptRouterReconnect(): Reconnecting to router %s...\n",
2127
2464
  _sharedGatewayConfig.routerSSID.c_str());
2128
-
2129
- // Use stationManual to reconnect (port 0 means no TCP mesh connection to router)
2130
- stationManual(_sharedGatewayConfig.routerSSID, _sharedGatewayConfig.routerPassword, 0);
2465
+
2466
+ // Use stationManual to reconnect (port 0 means no TCP mesh connection to
2467
+ // router)
2468
+ stationManual(_sharedGatewayConfig.routerSSID,
2469
+ _sharedGatewayConfig.routerPassword, 0);
2131
2470
  }
2132
2471
  };
2133
2472
  } // namespace wifi