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