AlteriomPainlessMesh v2.1.1
Easy ESP32/ESP8266 mesh networking with Alteriom IoT extensions
Loading...
Searching...
No Matches
wifi.hpp
Go to the documentation of this file.
1#ifndef _PAINLESS_MESH_ARDUINO_WIFI_HPP_
2#define _PAINLESS_MESH_ARDUINO_WIFI_HPP_
3
5
7#ifdef PAINLESSMESH_ENABLE_ARDUINO_WIFI
8#include "painlessMeshSTA.h"
9
12#include "painlessmesh/mesh.hpp"
14#include "painlessmesh/tcp.hpp"
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
25
32#define PAINLESSMESH_HAS_TCP_LISTENING 1
33
34namespace painlessmesh {
35namespace wifi {
36class Mesh : public painlessmesh::Mesh<Connection> {
37 public:
38 // Multi-bridge selection strategy enum (must be declared early)
40 PRIORITY_BASED = 0, // Use highest priority bridge (default)
41 ROUND_ROBIN = 1, // Distribute load evenly
42 BEST_SIGNAL = 2 // Use bridge with best RSSI
43 };
44
62 void init(TSTRING ssid, TSTRING password, uint16_t port = 5555,
63 WiFiMode_t connectMode = WIFI_AP_STA, uint8_t channel = 1,
64 uint8_t hidden = 0, uint8_t maxconn = MAX_CONN,
65 TSTRING stationSSID = "", TSTRING stationPassword = "") {
66 using namespace logger;
67 // Init random generator seed to generate delay variance
68 randomSeed(millis());
69
70 // Shut Wifi down and start with a blank slage
71 if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
72
73 // The mesh reconnects on its own terms: it scans, chooses an AP, and
74 // connects. The core must not. On core 2.x this used to call
75 // setAutoConnect(false) — the "connect at boot from stored
76 // credentials" flag, a different thing — and left the core's
77 // auto-reconnect at its default of on. After a parent's AP vanished
78 // (BEACON_TIMEOUT) the core then tried the gone BSSID again every seven
79 // seconds (NO_AP_FOUND, a full-channel search each time) for as long as
80 // it stayed gone, and every attempt kept the radio busy so the mesh's
81 // own scan "could not start": the node sat outside the mesh for the
82 // rest of the test. Measured on the rig on every core-2.x board (ESP32,
83 // C3, S3); the core-3.x boards, which got the right call, did not do it.
84 Log(STARTUP, "init(): %d\n", WiFi.setAutoReconnect(false));
85#if ESP_ARDUINO_VERSION_MAJOR < 3
86 WiFi.setAutoConnect(false);
87#endif
88 WiFi.persistent(false);
89
90 // start configuration
91 if (!WiFi.mode(connectMode)) {
92 Log(GENERAL, "WiFi.mode() false");
93 }
94#ifdef ESP32
95#if SOC_WIFI_SUPPORT_5G && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
96 // The mesh lives on 2.4 GHz. A dual-band part left in its default band
97 // mode scans the 5 GHz channels as well every time it looks for the
98 // mesh: measured on an ESP32-C5, the all-channel scan took 13–14 s
99 // against 4 s on a single-band part — at boot, where it is the whole
100 // of the C5's 15 s to its first reply, and on every channel
101 // re-detection, where it swallowed the console commands sent
102 // meanwhile. Nothing the mesh does needs the other band.
103 if (!WiFi.setBandMode(WIFI_BAND_MODE_2G_ONLY)) {
104 Log(ERROR, "init(): could not restrict the radio to 2.4 GHz\n");
105 }
106#endif
107#endif
108
109 _meshSSID = ssid;
110 _meshPassword = password;
111 _meshChannel = channel;
112 Log(STARTUP, "init(): Mesh channel set to %d\n", _meshChannel);
113 _meshHidden = hidden;
114 _meshMaxConn = maxconn;
115 _meshPort = port;
116
117 uint8_t MAC[] = {0, 0, 0, 0, 0, 0};
118#if ESP_ARDUINO_VERSION_MAJOR >= 3
119 esp_read_mac(MAC, ESP_MAC_WIFI_SOFTAP);
120#else
121 if (WiFi.softAPmacAddress(MAC) == 0) {
122 Log(ERROR, "init(): WiFi.softAPmacAddress(MAC) failed.\n");
123 }
124#endif
125 uint32_t nodeId = tcp::encodeNodeId(MAC);
126 if (nodeId == 0) Log(ERROR, "NodeId set to 0\n");
127
128 this->init(nodeId);
129
130#ifdef ESP8266
131 // The ESP8266 is specified for small meshes, or as a leaf in larger
132 // ones. Say so at runtime when a deployment has put it in the middle of
133 // one: see Mesh::capacityCheck(). Thirty seconds is often enough to
134 // catch it and rare enough to cost nothing.
135 this->addTask(30 * TASK_SECOND, TASK_FOREVER, [this]() {
136 this->capacityCheck(ESP.getFreeHeap(),
138 });
139#endif
140
141 // Add bridge election package handler (Type BRIDGE_ELECTION)
142 this->callbackList.onPackage(
144 [this](protocol::Variant& variant, std::shared_ptr<Connection>,
145 uint32_t) {
146 JsonDocument doc;
147 TSTRING str;
148 variant.printTo(str);
149 deserializeJson(doc, str);
150 JsonObject obj = doc.as<JsonObject>();
151
152 if (obj["routerRSSI"].is<int>()) {
153 uint32_t fromNode = obj["from"];
154 int8_t routerRSSI = obj["routerRSSI"];
155 uint8_t routerChannel = obj["routerChannel"] | 0;
156 uint32_t uptime = obj["uptime"] | 0;
157 uint32_t freeMemory = obj["freeMemory"] | 0;
158
159 this->handleBridgeElection(fromNode, routerRSSI, routerChannel,
160 uptime, freeMemory);
161
162 Log(CONNECTION, "Bridge election candidate from %u: RSSI %d dBm\n",
163 fromNode, routerRSSI);
164 }
165 return false; // Don't consume the package
166 });
167
168 // Add bridge takeover package handler (Type BRIDGE_TAKEOVER)
169 this->callbackList.onPackage(
171 [this](protocol::Variant& variant, std::shared_ptr<Connection>,
172 uint32_t) {
173 JsonDocument doc;
174 TSTRING str;
175 variant.printTo(str);
176 deserializeJson(doc, str);
177 JsonObject obj = doc.as<JsonObject>();
178
179 if (obj["previousBridge"].is<unsigned int>()) {
180 uint32_t newBridge = obj["from"];
181 uint32_t previousBridge = obj["previousBridge"];
182 TSTRING reason = obj["reason"].as<TSTRING>();
183 uint8_t routerChannel = obj["routerChannel"] | 0;
184
185 Log(CONNECTION,
186 "Bridge takeover: Node %u replaced %u on channel %u (%s)\n",
187 newBridge, previousBridge, routerChannel, reason.c_str());
188
189 // AP+STA radios must use one channel. Follow the elected bridge
190 // promptly instead of waiting for the slow empty-scan recovery.
192 this->nodeId, newBridge, _meshChannel, routerChannel)) {
193 this->addTask(1000, TASK_ONCE, [this, routerChannel]() {
194 stationScan.followBridgeChannel(routerChannel);
195 });
196 }
197
198 // Notify callback if this node was not the winner
199 if (newBridge != this->nodeId && bridgeRoleChangedCallback) {
200 bridgeRoleChangedCallback(false, "Another node won election");
201 }
202 }
203 return false; // Don't consume the package
204 });
205
206 // Add callback to detect bridge failures and trigger elections
207 this->onBridgeStatusChanged([this](uint32_t bridgeNodeId,
208 bool hasInternet) {
209 if (!hasInternet && bridgeFailoverEnabled &&
211 Log(CONNECTION, "Bridge %u lost Internet, considering election...\n",
212 bridgeNodeId);
213
214 // Check if we still have any healthy bridges
215 if (!this->hasInternetConnection()) {
216 Log(CONNECTION, "No healthy bridges, starting election\n");
217 // Small delay to let all nodes detect the failure
218 this->addTask(2000, TASK_ONCE,
219 [this]() { this->startBridgeElection(); });
220 }
221 }
222 });
223
224 // Add periodic monitoring task to detect when no bridge exists
225 // This handles the case where no node was initially configured as a bridge
226 this->addTask(30000, TASK_FOREVER, [this]() { this->checkForBridge(); });
227
228 // Add separate periodic task for isolated bridge retry
229 // This handles the case where a node:
230 // - Has router credentials configured
231 // - Is isolated (no mesh connections)
232 // - Should attempt to become a bridge directly
233 // This is different from the election mechanism which requires mesh
234 // connectivity
235 this->addTask(isolatedBridgeRetryIntervalMs, TASK_FOREVER, [this]() {
236 // Only retry if failover is enabled and we have credentials
238 return;
239 }
240
241 // Don't retry if we're already a bridge
242 if (this->isBridge()) {
243 return;
244 }
245
246 // Skip during startup period
247 if (millis() < electionStartupDelayMs) {
248 return;
249 }
250
251 // Only retry when isolated (no mesh connections found)
252 if (this->hasActiveMeshConnections()) {
253 // Reset retry counter and pending flag when mesh is active (no longer
254 // isolated)
256 _isolatedRetryPending = false;
257 return;
258 }
259
260 // Limit retry attempts with reset after timeout
262 // Check if enough time has passed to reset the counter
263 if (millis() > _isolatedBridgeRetryResetTime) {
264 Log(CONNECTION,
265 "Isolated bridge retry: Reset timeout reached, resetting attempt "
266 "counter\n");
268 } else {
269 Log(CONNECTION,
270 "Isolated bridge retry: Max attempts (%d) reached, reset in %u "
271 "seconds\n",
273 (_isolatedBridgeRetryResetTime - millis()) / 1000);
274 return;
275 }
276 }
277
278 // Check if mesh network exists on any channel before trying to become
279 // bridge If mesh exists but we can't connect, don't try to become bridge
280 // Skip this check if we already confirmed isolation from a previous
281 // failed attempt
282 uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
285 Log(CONNECTION,
286 "Isolated bridge retry: Only %d empty scans, waiting for more "
287 "scans\n",
288 emptyScans);
289 return;
290 }
291
292 // Clear the pending flag now that we're proceeding
293 _isolatedRetryPending = false;
294
295 Log(CONNECTION,
296 "Isolated bridge retry: Node isolated with %d empty scans, "
297 "attempting bridge promotion\n",
298 emptyScans);
299
300 // Attempt to become bridge directly (bypassing election since we're
301 // isolated) Only increment retry counter if we actually attempted
302 // promotion
303 if (this->attemptIsolatedBridgePromotion()) {
305 // Set reset time when reaching max attempts
310 }
311 }
312 });
313
315
316 _apIp = IPAddress(0, 0, 0, 0);
317
318 if (connectMode & WIFI_AP) {
319 apInit(nodeId); // setup AP
320 }
321
322 // Initialize TCP server AFTER AP is configured
323 // This ensures the network interfaces are ready when the server starts
324 // Fixes TCP connection error -14 when nodes try to connect to bridge
326
327 if (connectMode & WIFI_STA) {
328 this->initStation();
329 }
330
331 // If station credentials provided, connect to router
332 if (!stationSSID.isEmpty() && (connectMode & WIFI_STA)) {
333 Log(STARTUP, "init(): Connecting to station %s\n", stationSSID.c_str());
334 this->stationManual(stationSSID, stationPassword);
335 }
336 }
337
355 void init(TSTRING ssid, TSTRING password, Scheduler* baseScheduler,
356 uint16_t port = 5555, WiFiMode_t connectMode = WIFI_AP_STA,
357 uint8_t channel = 1, uint8_t hidden = 0, uint8_t maxconn = MAX_CONN,
358 TSTRING stationSSID = "", TSTRING stationPassword = "") {
359 this->setScheduler(baseScheduler);
360 init(ssid, password, port, connectMode, channel, hidden, maxconn,
361 stationSSID, stationPassword);
362 }
363
385 bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword, TSTRING routerSSID,
386 TSTRING routerPassword, Scheduler* baseScheduler,
387 uint16_t port = 5555) {
388 using namespace logger;
389
390 Log(STARTUP, "=== Bridge Mode Initialization ===\n");
391 Log(STARTUP, "Step 1: Attempting to connect to router %s...\n", routerSSID.c_str());
392
393 // Store router credentials for future connection attempts
395
396 // Step 1: Attempt to connect to router first to detect its channel
397 // Shut Wifi down and start with a blank slate
398 if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
399
400 // See init(): the core's auto-reconnect is off on every core version.
401 Log(STARTUP, "initAsBridge(): %d\n", WiFi.setAutoReconnect(false));
402#if ESP_ARDUINO_VERSION_MAJOR < 3
403 WiFi.setAutoConnect(false);
404#endif
405 WiFi.persistent(false);
406 WiFi.mode(WIFI_STA);
407
408 // Connect to router and wait for connection
409 WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
410
411 // Wait for connection (with timeout)
412 int timeout = 30; // 30 seconds timeout
413 while (WiFi.status() != WL_CONNECTED && timeout > 0) {
414 // Allow event loop processing during hardware settling
415 for (int i = 0; i < 100; i++) { delay(10); yield(); }
416 timeout--;
417 Log(STARTUP, ".");
418 }
419
420 uint8_t detectedChannel = 1; // Default fallback
421 bool routerConnected = false;
422
423 if (WiFi.status() == WL_CONNECTED) {
424 detectedChannel = WiFi.channel();
425 // Validate channel is in valid range (1-13 for 2.4GHz)
426 if (detectedChannel < 1 || detectedChannel > 13) {
427 Log(ERROR,
428 "\n[FAIL] Invalid channel detected: %d, falling back to channel 1\n",
429 detectedChannel);
430 detectedChannel = 1;
431 } else {
432 Log(STARTUP, "\n[OK] Router connected on channel %d\n", detectedChannel);
433 Log(STARTUP, "[OK] Router IP: %s\n", WiFi.localIP().toString().c_str());
434 routerConnected = true;
435 }
436 } else {
437 Log(STARTUP, "\n[WARN] Router connection unavailable during initialization\n");
438
439 // Scan for router to detect its channel even though we can't connect
440 // This minimizes channel mismatch when router becomes available later
441 Log(STARTUP, "[WARN] Scanning for router '%s' to detect channel...\n", routerSSID.c_str());
442
443 // ESP32 and ESP8266 have different scanNetworks signatures
444#ifdef ESP32
445 int16_t numNetworks = WiFi.scanNetworks(false, false, false, 300U, 0);
446#elif defined(ESP8266)
447 int16_t numNetworks = WiFi.scanNetworks(false, false, 0);
448#endif
449
450 if (numNetworks > 0) {
451 for (int16_t i = 0; i < numNetworks; i++) {
452 if (WiFi.SSID(i) == routerSSID) {
453 uint8_t scannedChannel = WiFi.channel(i);
454 if (scannedChannel >= 1 && scannedChannel <= 13) {
455 detectedChannel = scannedChannel;
456 Log(STARTUP, "[OK] Router found on channel %d (not connected, will retry)\n",
457 detectedChannel);
458 break;
459 }
460 }
461 }
462 WiFi.scanDelete();
463 }
464
465 if (detectedChannel == 1) {
466 Log(STARTUP, "[WARN] Router not found in scan, using default channel %d\n", detectedChannel);
467 }
468
469 Log(STARTUP, "[WARN] Proceeding with bridge setup on channel %d\n", detectedChannel);
470 Log(STARTUP, "[WARN] Bridge will retry router connection in background\n");
471 }
472
473 Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
474 detectedChannel);
475
476 // Step 2: Initialize mesh on detected/default channel
477 // This allows the bridge to establish the mesh network even without router
478 init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
479 detectedChannel, 0, MAX_CONN);
480
481 // Allow network stack to stabilize after AP and TCP server initialization
482 // This prevents TCP connection errors (-14) when nodes connect
483 delay(100);
484
485 Log(STARTUP, "Step 3: Establishing bridge connection...\n");
486
487 // Step 3: Establish/re-establish router connection using stationManual
488 // If router wasn't available initially, this will be retried automatically
490
491 // Step 4: Configure as root/bridge node
492 // Bridge role is established regardless of router connectivity
493 // This ensures mesh nodes can connect and the bridge can provide mesh services
494 this->setRoot(true);
495 this->setContainsRoot(true);
496
497 // Step 5: Setup bridge status broadcasting
499
500 // Step 6: Setup gateway Internet handler
502
503 // Step 7: Start the health checker that drives hasLocalInternet().
504 // Without it that flag stays false for the life of the node, so the
505 // local short-circuit in sendToInternet() never fires and a bridge
506 // asking its own uplink fell through to the mesh-routing path -- which
507 // fails with "No active mesh connections" whenever the bridge has no
508 // peer yet, even though the router link is up (issue #445).
509 // initAsSharedGateway() has always done this; initAsBridge() did not.
510 //
511 // No configureInternetHealthCheck() call here: a bridge has no
512 // SharedGatewayConfig, the checker's own defaults (8.8.8.8:53, 30 s,
513 // 5 s) already match that struct's, and configuring would clobber any
514 // setInternetCheckTarget() the sketch made before init.
516
517 Log(STARTUP, "=== Bridge Mode Active ===\n");
518 Log(STARTUP, " Mesh SSID: %s\n", meshSSID.c_str());
519 Log(STARTUP, " Mesh Channel: %d%s\n", detectedChannel,
520 routerConnected ? " (matches router)" : " (default, router pending)");
521 Log(STARTUP, " Router: %s%s\n", routerSSID.c_str(),
522 routerConnected ? " (connected)" : " (will retry)");
523 Log(STARTUP, " Port: %d\n", port);
524
525 if (!routerConnected) {
526 Log(STARTUP, "\nINFO: Bridge initialized without router connection\n");
527 Log(STARTUP, "INFO: Mesh network is active and accepting node connections\n");
528 Log(STARTUP, "INFO: Router connection will be established automatically when available\n");
529 }
530
531 // Always returns true: bridge mesh functionality is active regardless of
532 // router connection status. The router connection is opportunistic.
533 return true;
534 }
535
552 bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword, TSTRING routerSSID,
553 TSTRING routerPassword, Scheduler* baseScheduler,
554 uint16_t port, uint8_t priority) {
555 using namespace logger;
556
557 // Validate and store priority
558 if (priority < 1) priority = 1;
559 if (priority > 10) priority = 10;
560 bridgePriority = priority;
561
562 // Store role based on priority
563 if (priority >= 8) {
564 bridgeRole = "primary";
565 } else if (priority >= 5) {
566 bridgeRole = "secondary";
567 } else {
568 bridgeRole = "standby";
569 }
570
571 Log(STARTUP,
572 "=== Bridge Mode Initialization (Priority: %d, Role: %s) ===\n",
573 priority, bridgeRole.c_str());
574
575 // Call the base initAsBridge method
576 bool success = initAsBridge(meshSSID, meshPassword, routerSSID,
577 routerPassword, baseScheduler, port);
578
579 // Setup multi-bridge coordination if enabled and bridge init succeeded
580 if (success && multiBridgeEnabled) {
582 }
583
584 return success;
585 }
586
612 TSTRING meshPrefix, TSTRING meshPassword, TSTRING routerSSID,
613 TSTRING routerPassword, Scheduler* userScheduler, uint16_t port = 5555,
615 using namespace logger;
616
617 Log(STARTUP, "=== Shared Gateway Mode Initialization ===\n");
618
619 // Validate configuration if enabled
620 if (config.enabled) {
621 auto result = config.validate();
622 if (!result.valid) {
623 Log(ERROR, "initAsSharedGateway(): Config validation failed: %s\n",
624 result.errorMessage.c_str());
625 return false;
626 }
627 }
628
629 // Store shared gateway configuration
630 _sharedGatewayConfig = config;
634 _sharedGatewayMode = true;
635
636 Log(STARTUP, "Step 1: Scanning for router %s to detect channel...\n",
637 routerSSID.c_str());
638
639 // Step 1: Scan for router to detect its channel
640 // We need to ensure mesh and router operate on the same channel
641 if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
642
643 // See init(): the core's auto-reconnect is off on every core version.
644 WiFi.setAutoReconnect(false);
645#if ESP_ARDUINO_VERSION_MAJOR < 3
646 WiFi.setAutoConnect(false);
647#endif
648 Log(STARTUP, "initAsSharedGateway(): AutoReconnect disabled\n");
649 WiFi.persistent(false);
650 WiFi.mode(WIFI_STA);
651
652 // Connect to router to detect channel
653 WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
654
655 // Wait for connection with timeout (using constant for configurability)
657 while (WiFi.status() != WL_CONNECTED && timeout > 0) {
658 // Allow event loop processing during hardware settling
659 for (int i = 0; i < 100; i++) { delay(10); yield(); }
660 timeout--;
661 Log(STARTUP, ".");
662 }
663
664 uint8_t detectedChannel = 1; // Default fallback
665
666 if (WiFi.status() == WL_CONNECTED) {
667 detectedChannel = WiFi.channel();
668 // Validate channel is in valid range (1-14 for 2.4GHz, region-dependent)
669 if (detectedChannel < MIN_WIFI_CHANNEL ||
670 detectedChannel > MAX_WIFI_CHANNEL) {
671 Log(ERROR,
672 "\n[FAIL] Invalid channel detected: %d, falling back to channel 1\n",
673 detectedChannel);
674 detectedChannel = 1;
675 } else {
676 Log(STARTUP, "\n[OK] Router connected on channel %d\n", detectedChannel);
677 Log(STARTUP, "[OK] Router IP: %s\n", WiFi.localIP().toString().c_str());
678 }
679 } else {
680 Log(ERROR, "\n[FAIL] Failed to connect to router during channel detection\n");
681 Log(ERROR,
682 "Continuing with default channel 1, will retry router connection "
683 "later\n");
684 }
685
686 // Disconnect from router, we'll reconnect after mesh init
687 WiFi.disconnect();
688 delay(100);
689
690 Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
691 detectedChannel);
692
693 // Step 2: Initialize mesh on detected channel with AP+STA mode
694 // Set scheduler before init
695 this->setScheduler(userScheduler);
696 init(meshPrefix, meshPassword, port, WIFI_AP_STA, detectedChannel, 0,
697 MAX_CONN);
698
699 // Allow network stack to stabilize after AP and TCP server initialization
700 // This prevents TCP connection errors (-14) when nodes connect
701 delay(100);
702
703 Log(STARTUP,
704 "Step 3: Establishing router connection in shared gateway mode...\n");
705
706 // Step 3: Establish router connection using stationManual
707 // Port 0 means we don't expect TCP mesh connection to the router
709
710 // Step 4: Setup router connection monitoring and reconnection logic
712
713 // Step 5: Setup gateway Internet handler
715
716 // Step 6: Start the health checker that drives hasLocalInternet().
717 // Previously shared-gateway nodes associated and received an IP address,
718 // but the checker was never configured or scheduled, so local Internet
719 // availability remained false forever and every request was needlessly
720 // routed toward a mesh gateway.
723
724 // Store router credentials for reconnection
726
727 Log(STARTUP, "=== Shared Gateway Mode Active ===\n");
728 Log(STARTUP, " Mesh Prefix: %s\n", meshPrefix.c_str());
729 Log(STARTUP, " Mesh Channel: %d (synced with router)\n", detectedChannel);
730 Log(STARTUP, " Router: %s\n", routerSSID.c_str());
731 Log(STARTUP, " Port: %d\n", port);
732 Log(STARTUP, " Mode: AP+STA (all nodes can connect to router)\n");
733
734 return true;
735 }
736
743
752
761 void stationManual(TSTRING ssid, TSTRING password, uint16_t port = 0,
762 IPAddress remote_ip = IPAddress(0, 0, 0, 0)) {
763 using namespace logger;
764 // Set station config
765 stationScan.manualIP = remote_ip;
766
767 Log(STARTUP, "stationManual(): Connecting to %s\n", ssid.c_str());
768
769 // Start scan
770 stationScan.init(this, ssid, password, port, _meshChannel,
771 static_cast<bool>(_meshHidden));
772 stationScan.manual = true;
773
774 // A manual station is the router link of a bridge or shared gateway:
775 // one known AP, no mesh scan choosing among candidates. For that link
776 // the core's own auto-reconnect is the right mechanism, and it is what
777 // kept every bridge's upstream alive until now — by accident, on core
778 // 2.x, where init() had never really turned it off. With it off
779 // everywhere, a bridge whose second association with the router failed
780 // (init() drops the first to start the mesh) sat at WL_IDLE_STATUS for
781 // the whole run with no upstream, and no node ever learned of the
782 // Internet. On for the manual link; the mesh station in init() keeps
783 // it off.
784 WiFi.setAutoReconnect(true);
785
786 // Directly initiate connection - ESP will auto-detect router's channel
787 WiFi.begin(ssid.c_str(), password.c_str());
788
789 Log(STARTUP, "stationManual(): Connection initiated\n");
790 }
791
792 void initStation() {
794 static_cast<bool>(_meshHidden));
795 mScheduler->addTask(stationScan.task);
796 stationScan.task.enable();
797
799 [this](uint32_t nodeId, bool station) {
800 if (station) {
801 if (WiFi.status() == WL_CONNECTED) {
802 WiFi.disconnect();
803 // Schedule reconnection after disconnect completes
804 // The WiFi event handler will signal when disconnect is complete
805 _pendingStationReconnect = true;
806 } else if (_pendingStationReconnect) {
807 // A drop this node asked for: reconnect from the last scan
809 } else if (this->stationScan.droppedByMove()) {
810 // The drop this node's own channel follow caused.
811 // followBridgeChannel() closed the link and scans next;
812 // nothing to do here, and above all no re-detection.
813 using namespace logger;
814 Log(CONNECTION,
815 "Station link closed by this node's channel move\n");
816 } else if (!this->stationScan.stationLinkUp) {
817 // An attempt that never got an address — the association
818 // timed out, or the AP was gone by the time it was tried.
819 // Not a loss: scan this channel again, where the next AP is.
820 // Re-detecting here sent the sender in sweep 45 run 2, just
821 // arrived on the bridge's channel after a peer there had
822 // left, to look at the old channel and consider going back,
823 // with the bridge's AP at -55 dBm beside it.
824 using namespace logger;
825 Log(CONNECTION,
826 "Station attempt failed, scanning this channel again\n");
827 this->stationScan.task.forceNextIteration();
828 } else {
829 // A drop nobody asked for — the AP this station was on went
830 // away (its node rebooted, or a bridge moved the mesh). Scan
831 // now rather than when the current slow-scan delay runs out,
832 // which is up to four intervals away for a node that was
833 // stable a moment ago.
834 using namespace logger;
835 Log(CONNECTION,
836 "Station link lost unexpectedly, scanning now\n");
837 if (this->shouldContainRoot && this->stationScan.atHome()) {
838 // At home the bridge's AP is on this channel; the uplink
839 // that went was a relay (in sweep 46 run 2, the backup
840 // rebooting into its failover role). Scan this channel
841 // now: the all-channel hunt below took the sender twenty
842 // seconds, and its request through the bridge ran out of
843 // retries in the gap.
844 Log(CONNECTION,
845 "Station link lost at home: scanning this channel\n");
846 } else if (this->shouldContainRoot) {
847 // In a mesh that should have a root, the AP that went away
848 // most likely left for the bridge's channel, and the nodes
849 // still on this one are about to. Re-attaching here first
850 // cost the node the failover test sends from a hundred
851 // seconds at the bridge's start: it joined one remnant,
852 // lost it, joined the next, lost that, and only then looked
853 // at every channel. Look at every channel now; the rules in
854 // scanComplete() decide whether to go.
855 Log(CONNECTION,
856 "Station link lost in a rooted mesh: re-detecting the "
857 "mesh channel on this scan\n");
859 }
860 this->stationScan.task.forceNextIteration();
861 }
862 }
863 });
864 }
865
875 // Re-enable scanning if it was disabled
876 this->stationScan.task.enableIfNot();
877 }
878 }
879
881 using namespace logger;
882 Log(GENERAL, "tcpServerInit():\n");
883 // A listener that exists and listens is kept. It is bound to every
884 // address, so the AP a re-initialised node brings up is served by it,
885 // and re-creating it is not merely needless: the connections the old
886 // one accepted share its local port, and lwIP refuses to bind a new
887 // listener to a port any of them still holds — for the two minutes
888 // they sit in TIME_WAIT after stop() closes them. A node promoted to
889 // bridge on the rig logged "bind error: -8" on every attempt for
890 // exactly that long, reset every peer that came to join it, and only
891 // then had a bridge's listener. AsyncTCP keeps its pcb private, so
892 // SO_REUSEADDR cannot be set from here; not re-binding is the fix.
893 if (_tcpListener != nullptr) {
894 if (_tcpListener->status() == TCP_STATE_LISTEN) {
895 Log(CONNECTION,
896 "tcpServerInit(): listener on port %d already listening, kept\n",
897 _meshPort);
898 return;
899 }
900 delete _tcpListener;
901 _tcpListener = nullptr;
902 }
904 painlessmesh::tcp::initServer<Connection, painlessmesh::Mesh<Connection>>(
905 (*_tcpListener), (*this));
906 Log(STARTUP, "AP tcp server established on port %d\n", _meshPort);
907 // The listener's state, at the level the rig keeps: a node promoted to
908 // bridge re-creates its listener, and one such node reset every
909 // connection to its AP for its whole time as bridge while its log said
910 // nothing. LISTEN is 1 on both cores; anything else is the finding.
911 Log(CONNECTION, "tcpServerInit(): listener on port %d, state %u\n",
912 _meshPort, (unsigned)_tcpListener->status());
913 return;
914 }
915
926 return _tcpListener != nullptr && _tcpListener->status() == TCP_STATE_LISTEN;
927 }
928
936 static constexpr uint8_t TCP_STATE_LISTEN = 1;
937
961 void tcpConnect() {
962 using namespace logger;
963 Log(GENERAL, "tcpConnect():\n");
965 return; // We have been configured not to connect to the mesh
966
967 if (WiFi.status() == WL_CONNECTED && WiFi.localIP()) {
968 // Determine target IP and port for connection
969 IPAddress targetIP =
970 stationScan.manualIP ? stationScan.manualIP : WiFi.gatewayIP();
971 uint16_t targetPort = stationScan.port;
972
973 Log(CONNECTION, "tcpConnect(): Connecting to %s:%d\n",
974 targetIP.toString().c_str(), targetPort);
975
976 // Add a small stabilization delay before attempting TCP connection
977 // This helps prevent error -14 (ERR_CONN) by allowing the network stack
978 // and TCP server to be fully ready. The delay is added via task scheduler
979 // to avoid blocking the event loop.
980 // The delay is read at schedule time, so calling setTcpRetryConfig()
981 // while a connect is already pending affects the next attempt, not the
982 // in-flight one.
983 this->addTask(
984 this->getTcpRetryConfig().stabilizationDelayMs, TASK_ONCE,
985 [this, targetIP, targetPort]() {
986 // Verify WiFi is still connected after the delay
987 if (WiFi.status() != WL_CONNECTED || !WiFi.localIP()) {
988 Log(CONNECTION,
989 "tcpConnect(): WiFi disconnected during stabilization "
990 "delay\n");
991 return;
992 }
993
994 Log(CONNECTION,
995 "tcpConnect(): Starting TCP connection after stabilization\n");
996 AsyncClient* pConn = new AsyncClient();
997 // Use wifi::Mesh type to enable blocklist functionality
998 // This allows tcp::connect to call blockNodeAfterTCPFailure on retry exhaustion
999 painlessmesh::tcp::connect<Connection, wifi::Mesh>(
1000 (*pConn), targetIP, targetPort, (*this));
1001 });
1002 } else {
1003 Log(ERROR, "tcpConnect(): err Something unexpected in tcpConnect()\n");
1004 }
1005 }
1006
1017 void blockNodeAfterTCPFailure(IPAddress ip, uint32_t blockDurationMs = painlessmesh::tcp::TCP_FAILURE_BLOCK_DURATION_MS) {
1018 using namespace logger;
1020
1021 if (nodeId == 0) {
1022 Log(CONNECTION, "blockNodeAfterTCPFailure(): Invalid mesh IP %s, cannot block\n",
1023 ip.toString().c_str());
1024 return;
1025 }
1026
1027 Log(CONNECTION, "blockNodeAfterTCPFailure(): Blocking node %u (IP: %s) for %u ms\n",
1028 nodeId, ip.toString().c_str(), blockDurationMs);
1029
1031 }
1032
1033 bool setHostname(const char* hostname) {
1034#ifdef ESP8266
1035 return WiFi.hostname(hostname);
1036#elif defined(ESP32)
1037 if (strlen(hostname) > 32) {
1038 return false;
1039 }
1040 return WiFi.setHostname(hostname);
1041#endif // ESP8266
1042 }
1043
1044 IPAddress getStationIP() { return WiFi.localIP(); }
1046
1056 void enableBridgeFailover(bool enabled) { bridgeFailoverEnabled = enabled; }
1057
1068 void setRouterCredentials(TSTRING ssid, TSTRING password) {
1069 routerSSID = ssid;
1070 routerPassword = password;
1072 }
1073
1079 void setElectionTimeout(uint32_t timeoutMs) { electionTimeoutMs = timeoutMs; }
1080
1090 void setMinimumBridgeRSSI(int8_t minRSSI) {
1091 if (minRSSI < -100) minRSSI = -100;
1092 if (minRSSI > -30) minRSSI = -30;
1093 minimumBridgeRSSI = minRSSI;
1094 }
1095
1106 void setElectionStartupDelay(uint32_t delayMs) {
1107 if (delayMs < 10000) delayMs = 10000; // Minimum 10 seconds
1108 electionStartupDelayMs = delayMs;
1109 }
1110
1123 void setElectionRandomDelay(uint32_t minMs, uint32_t maxMs) {
1124 if (minMs < 100) minMs = 100; // Minimum 100ms
1125 if (maxMs < minMs) maxMs = minMs + 1000; // Ensure max > min
1128 }
1129
1136 std::function<void(bool isBridge, const TSTRING& reason)> callback) {
1137 bridgeRoleChangedCallback = callback;
1138 }
1139
1153 std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t)>
1154 callback) {
1155 using namespace logger;
1156 bridgeCoordinationCallback = callback;
1157
1158 // Non-bridge nodes don't run initBridgeCoordination(), so register
1159 // a Type 613 handler here so they can still receive coordination traffic.
1160 if (!this->isBridge()) {
1161 this->callbackList.onPackage(
1162 613,
1163 [this](protocol::Variant& variant, std::shared_ptr<Connection>,
1164 uint32_t) {
1165 JsonDocument doc;
1166 TSTRING str;
1167 variant.printTo(str);
1168 deserializeJson(doc, str);
1169 JsonObject obj = doc.as<JsonObject>();
1170
1171 if (obj["priority"].is<unsigned int>()) {
1172 uint32_t fromNode = obj["from"];
1174
1175 if (this->bridgeCoordinationCallback) {
1176 this->bridgeCoordinationCallback(pkg, fromNode);
1177 }
1178
1179 // Change detection for non-bridge nodes
1181 auto it = this->lastBridgeCoordinationState.find(fromNode);
1182 if (it == this->lastBridgeCoordinationState.end()) {
1183 this->lastBridgeCoordinationState[fromNode] = {
1184 pkg.priority, pkg.role, pkg.load, (uint32_t)millis()};
1185 this->bridgeCoordinationChangedCallback(pkg, fromNode,
1186 "new");
1187 } else {
1188 auto& prev = it->second;
1189 bool changed = (prev.priority != pkg.priority ||
1190 prev.role != pkg.role ||
1191 prev.load != pkg.load);
1192 prev.priority = pkg.priority;
1193 prev.role = pkg.role;
1194 prev.load = pkg.load;
1195 prev.lastSeen = (uint32_t)millis();
1196 if (changed) {
1197 this->bridgeCoordinationChangedCallback(pkg, fromNode,
1198 "updated");
1199 }
1200 }
1201 }
1202 }
1203 return false;
1204 });
1205 Log(GENERAL,
1206 "onBridgeCoordination(): Registered Type 613 handler for non-bridge "
1207 "node\n");
1208 }
1209 }
1210
1221 std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t,
1222 TSTRING)>
1223 callback) {
1224 using namespace logger;
1226
1227 // Start periodic lost-detection task (every 30 seconds, check for
1228 // bridges that haven't sent a coordination message in 60 seconds)
1230 30000, TASK_FOREVER, [this]() {
1231 uint32_t now = (uint32_t)millis();
1232 for (auto it = this->lastBridgeCoordinationState.begin();
1233 it != this->lastBridgeCoordinationState.end();) {
1234 if ((now - it->second.lastSeen) > 60000) {
1235 uint32_t lostNode = it->first;
1236 // Create a package with last known state for the callback
1238 pkg.from = lostNode;
1239 pkg.priority = it->second.priority;
1240 pkg.role = it->second.role;
1241 pkg.load = it->second.load;
1242 it = this->lastBridgeCoordinationState.erase(it);
1244 this->bridgeCoordinationChangedCallback(pkg, lostNode, "lost");
1245 }
1246 } else {
1247 ++it;
1248 }
1249 }
1250 });
1251
1252 Log(GENERAL,
1253 "onBridgeCoordinationChanged(): Lost detection task started "
1254 "(interval: 30s, timeout: 60s)\n");
1255 }
1256
1268 void enableMultiBridge(bool enabled) {
1269 multiBridgeEnabled = enabled;
1270 if (enabled) {
1272 "enableMultiBridge(): Multi-bridge coordination enabled\n");
1273 }
1274 }
1275
1285 bridgeSelectionStrategy = strategy;
1286 Log(logger::GENERAL, "setBridgeSelectionStrategy(): Strategy set to %d\n",
1287 (int)strategy);
1288 }
1289
1295 void setMaxBridges(uint8_t maxBridges) {
1296 if (maxBridges < 1) maxBridges = 1;
1297 if (maxBridges > 5) maxBridges = 5;
1298 maxConcurrentBridges = maxBridges;
1299 Log(logger::GENERAL, "setMaxBridges(): Max concurrent bridges set to %d\n",
1300 maxBridges);
1301 }
1302
1308 std::vector<uint32_t> getActiveBridges() {
1309 std::vector<uint32_t> activeBridges;
1310 auto bridges = this->getBridges();
1311
1312 for (const auto& bridge : bridges) {
1313 if (bridge.internetConnected && bridge.isHealthy()) {
1314 activeBridges.push_back(bridge.nodeId);
1315 }
1316 }
1317
1318 return activeBridges;
1319 }
1320
1356 // First check if THIS node is a bridge with Internet
1357 if (this->isBridge()) {
1358 // Check Internet connectivity: WiFi connected AND valid IP address
1359 bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1360 (WiFi.localIP() != IPAddress(0, 0, 0, 0));
1361 if (hasInternet) {
1362 return true;
1363 }
1364 }
1365
1366 // Then check other bridges in the mesh (call parent implementation)
1368 }
1369
1379 auto activeBridges = getActiveBridges();
1380
1381 if (activeBridges.empty()) {
1382 return 0;
1383 }
1384
1385 // Single bridge - return it
1386 if (activeBridges.size() == 1) {
1387 return activeBridges[0];
1388 }
1389
1390 // Multi-bridge mode: apply selection strategy
1391 switch (bridgeSelectionStrategy) {
1392 case ROUND_ROBIN: {
1393 // Simple round-robin: cycle through bridges
1395 (lastSelectedBridgeIndex + 1) % activeBridges.size();
1396 return activeBridges[lastSelectedBridgeIndex];
1397 }
1398
1399 case BEST_SIGNAL: {
1400 // Find bridge with best RSSI
1401 uint32_t bestBridge = 0;
1402 int8_t bestRSSI = -127;
1403
1404 for (const auto& bridge : this->getBridges()) {
1405 if (bridge.internetConnected && bridge.isHealthy() &&
1406 bridge.routerRSSI > bestRSSI) {
1407 bestRSSI = bridge.routerRSSI;
1408 bestBridge = bridge.nodeId;
1409 }
1410 }
1411 return bestBridge;
1412 }
1413
1414 case PRIORITY_BASED:
1415 default: {
1416 // Use highest priority bridge (stored in bridgePriorities map)
1417 uint32_t bestBridge = 0;
1418 uint8_t highestPriority = 0;
1419
1420 for (uint32_t bridgeId : activeBridges) {
1421 uint8_t priority = bridgePriorities[bridgeId];
1422 if (priority > highestPriority) {
1423 highestPriority = priority;
1424 bestBridge = bridgeId;
1425 }
1426 }
1427
1428 // If no priority info, use first active bridge
1429 return bestBridge ? bestBridge : activeBridges[0];
1430 }
1431 }
1432 }
1433
1440
1441 // The bridge monitor's check, one tick of the 30 s task: with no
1442 // healthy bridge known, an election is scheduled. Also run when a
1443 // bridge announces that it is stepping down, as soon as the startup
1444 // period allows.
1446 using namespace logger;
1447 // Only a failover candidate checks: one with failover enabled and the
1448 // router's credentials. Every other node scheduled an election it
1449 // could not join, every 30 s, when this guard went missing.
1451 return;
1452 }
1453
1454 // Don't check if we're already a bridge
1455 if (this->isBridge()) {
1456 return;
1457 }
1458
1459 // Skip check during startup period to allow initial bridge discovery
1460 if (millis() < electionStartupDelayMs) {
1461 return;
1462 }
1463
1464 // IMPORTANT: Don't trigger election if we're disconnected from the mesh
1465 // When isolated, we can't receive bridge status broadcasts, so lack of
1466 // healthy bridge could simply mean WE are disconnected, not that the
1467 // bridge is unavailable. Wait until mesh connectivity is restored before
1468 // considering an election.
1469 if (!this->hasActiveMeshConnections()) {
1470 Log(CONNECTION,
1471 "Bridge monitor: Skipping - no active mesh connections\n");
1472 return;
1473 }
1474
1475 // Check if there are any healthy bridges
1476 bool hasHealthyBridge = false;
1477 for (const auto& bridge : this->getBridges()) {
1478 if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
1479 hasHealthyBridge = true;
1480 break;
1481 }
1482 }
1483
1484 // If no healthy bridge exists, trigger an election
1485 if (!hasHealthyBridge) {
1486 Log(CONNECTION,
1487 "Bridge monitor: No healthy bridge detected, triggering "
1488 "election\n");
1489 // Random delay to prevent simultaneous elections when multiple nodes
1490 // start together
1491 uint32_t randomDelay =
1493 Log(CONNECTION, "Bridge monitor: Scheduling election in %u ms\n",
1494 randomDelay);
1495 this->addTask(randomDelay, TASK_ONCE,
1496 [this]() { this->startBridgeElection(); });
1497 }
1498 }
1499
1500 // Schedule checkForBridge() once, after the startup period if it is
1501 // still running, plus a second for the news to settle.
1503 if (bridgeCheckPending) return;
1504 uint32_t wait = millis() < electionStartupDelayMs
1505 ? electionStartupDelayMs - millis()
1506 : 0;
1507 bridgeCheckPending = true;
1508 this->addTask(wait + 1000, TASK_ONCE, [this]() {
1509 bridgeCheckPending = false;
1510 this->checkForBridge();
1511 });
1512 }
1513
1514 void stop() {
1515 // A bridge stepping down says so before its connections close, so the
1516 // candidates hold an election now rather than when its last status
1517 // ages out. The short wait lets the broadcast leave the buffers.
1518 if (this->isBridge() && bridgeStatusBroadcastEnabled) {
1519 sendBridgeStatus(true);
1520 delay(200);
1521 }
1522 // remove all WiFi events
1523#ifdef ESP32
1524 WiFi.removeEvent(eventScanDoneHandler);
1525 WiFi.removeEvent(eventSTAStartHandler);
1526 WiFi.removeEvent(eventSTADisconnectedHandler);
1527 WiFi.removeEvent(eventSTAGotIPHandler);
1528#elif defined(ESP8266)
1529 eventSTAConnectedHandler = WiFiEventHandler();
1530 eventSTADisconnectedHandler = WiFiEventHandler();
1531 eventSTAGotIPHandler = WiFiEventHandler();
1532
1533 stationScan.asyncTask.setCallback(NULL);
1534 mScheduler->deleteTask(stationScan.asyncTask);
1535#endif // ESP32
1536 // Stop scanning task
1537 stationScan.task.setCallback(NULL);
1538 mScheduler->deleteTask(stationScan.task);
1540
1541 // Shutdown wifi hardware
1542 if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
1543
1544 // The TCP listener stays. A node that stops to re-initialise in place
1545 // — a promotion to bridge, a return to a regular node — needs a
1546 // listener again at once, and a new one cannot bind while the
1547 // connections this one accepted are still in TIME_WAIT on the same
1548 // port (see tcpServerInit()). The connections themselves are closed
1549 // above; a client accepted in the gap before init() is closed by it.
1550 }
1551
1552 protected:
1553 friend class ::StationScan;
1554 // init() sets all of these; the defaults are init()'s own, so a node
1555 // that is asked about its AP before init() answers something true
1556 // rather than whatever the stack held.
1559 uint8_t _meshChannel = 1;
1560 uint8_t _meshHidden = 0;
1562 uint16_t _meshPort = 5555;
1563
1566
1567 void init(Scheduler* scheduler, uint32_t id) {
1569 }
1570
1572
1573 void apInit(uint32_t nodeId) {
1574 using namespace logger;
1575 _apIp = IPAddress(10, (nodeId & 0xFF00) >> 8, (nodeId & 0xFF), 1);
1576 IPAddress netmask(255, 255, 255, 0);
1577
1578 WiFi.softAPConfig(_apIp, _apIp, netmask);
1579
1580#ifdef ESP32
1581 // ESP32: Explicitly enable AP mode to ensure DHCP server starts properly
1582 // This is particularly important after channel changes or AP restarts
1583 WiFi.enableAP(true);
1584#endif
1585
1586 WiFi.softAP(_meshSSID.c_str(), _meshPassword.c_str(), _meshChannel,
1588
1589 Log(STARTUP, "apInit(): AP configured - SSID: %s, Channel: %d, IP: %s\n",
1590 _meshSSID.c_str(), _meshChannel, _apIp.toString().c_str());
1591 Log(STARTUP, "apInit(): AP active - Max connections: %d\n", _meshMaxConn);
1592 }
1593
1599 using namespace logger;
1600
1601 if (!this->isBridge() || !this->bridgeStatusBroadcastEnabled) {
1602 return;
1603 }
1604
1605 Log(STARTUP,
1606 "initBridgeStatusBroadcast(): Setting up bridge status broadcast\n");
1607
1608 // CRITICAL FIX: Schedule tasks with a small delay to avoid crashes when
1609 // called immediately after stop()/init cycle. The delay allows the scheduler
1610 // and internal task structures to stabilize before adding new tasks.
1611 // This fixes the "Load access fault" Guru Meditation error that occurred
1612 // when promoting to bridge role.
1613 const uint32_t INIT_DELAY_MS = 100;
1614
1615 // Register ourselves as a bridge in the knownBridges list
1616 // This ensures the bridge knows about itself and reports correct status
1617 this->addTask(INIT_DELAY_MS, TASK_ONCE, [this]() {
1618 // Check Internet connectivity: WiFi connected AND valid IP address
1619 bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1620 (WiFi.localIP() != IPAddress(0, 0, 0, 0));
1621
1622 this->updateBridgeStatus(this->nodeId, // bridgeNodeId
1623 hasInternet, // internetConnected
1624 WiFi.RSSI(), // routerRSSI
1625 WiFi.channel(), // routerChannel
1626 millis(), // uptime
1627 WiFi.gatewayIP().toString(), // gatewayIP
1628 this->getNodeTime() // timestamp
1629 );
1630
1631 Log(STARTUP,
1632 "initBridgeStatusBroadcast(): Registered self as bridge (nodeId: "
1633 "%u)\n",
1634 this->nodeId);
1635 });
1636
1637 // Create periodic task to broadcast bridge status
1638 // Schedule with delay to avoid crashes during stop/init cycle
1639 this->addTask(INIT_DELAY_MS, TASK_ONCE, [this]() {
1640 bridgeStatusTask = this->addTask(this->bridgeStatusIntervalMs, TASK_FOREVER,
1641 [this]() { this->sendBridgeStatus(); });
1642 });
1643
1644 // Send immediate broadcast so nodes can discover this bridge right away
1645 // This ensures bridge is discoverable before the first periodic broadcast
1646 // Use slightly larger delay to allow bridge status task to be set up first
1647 this->addTask(INIT_DELAY_MS + 50, TASK_ONCE, [this]() {
1648 Log(STARTUP, "Sending initial bridge status broadcast\n");
1649 this->sendBridgeStatus();
1650 });
1651
1652 // Also send bridge status when new nodes connect so they can discover the
1653 // bridge immediately Send directly to the new node to ensure delivery,
1654 // independent of time sync Using changedConnectionCallbacks instead of
1655 // newConnectionCallbacks ensures routing is ready
1656 this->changedConnectionCallbacks.push_back([this](uint32_t nodeId) {
1657 Log(CONNECTION,
1658 "Node %u connection changed, sending bridge status directly\n",
1659 nodeId);
1660
1661 // The direct send below reaches the neighbour whose connection
1662 // changed. A node that joined behind that neighbour is why it changed,
1663 // and it learns of this bridge only from the next periodic broadcast,
1664 // up to thirty seconds on: on the rig the node the discovery fixture
1665 // sends from joined a child of the bridge at 185 s and heard the
1666 // bridge at 214 s, after the fixture's window. So a change also
1667 // brings the broadcast forward, at most once every five seconds.
1668 if (millis() - _lastBridgeStatusBroadcast >= 5000) {
1669 this->addTask(1000, TASK_ONCE, [this]() {
1670 if (millis() - _lastBridgeStatusBroadcast >= 5000) {
1671 Log(CONNECTION,
1672 "Topology changed; broadcasting bridge status now rather "
1673 "than at the next interval\n");
1674 this->sendBridgeStatus();
1675 }
1676 });
1677 }
1678
1679 // Small delay to ensure connection is fully stable, then send directly to
1680 // the new node This avoids issues with time sync blocking broadcast
1681 // messages
1682 this->addTask(500, TASK_ONCE, [this, nodeId]() {
1683 // Check if the connection is still valid - the node may have
1684 // disconnected during the 500ms delay (e.g., due to timeout or network
1685 // issues) This prevents attempting to send messages to dropped
1686 // connections findRoute returns nullptr if node is not in the routing
1687 // table
1688 auto conn = router::findRoute<Connection>((*this), nodeId);
1689 if (!conn || !conn->connected()) {
1690 Log(CONNECTION,
1691 "Bridge status send cancelled: Node %u no longer connected\n",
1692 nodeId);
1693 return;
1694 }
1695
1696 // Create bridge status message
1697 JsonDocument doc;
1698 JsonObject obj = doc.to<JsonObject>();
1699
1700 obj["type"] = protocol::BRIDGE_STATUS;
1701 obj["from"] = this->nodeId;
1702 obj["routing"] = 1; // SINGLE routing (direct to node)
1703 obj["dest"] = nodeId;
1704 obj["timestamp"] = this->getNodeTime();
1705
1706 bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
1707 (WiFi.localIP() != IPAddress(0, 0, 0, 0));
1708 obj["internetConnected"] = hasInternet;
1709 obj["routerRSSI"] = WiFi.RSSI();
1710 obj["routerChannel"] = WiFi.channel();
1711 obj["uptime"] = millis();
1712 obj["gatewayIP"] = WiFi.gatewayIP().toString();
1713 obj["message_type"] = protocol::BRIDGE_STATUS;
1714
1715 String msg;
1716 serializeJson(doc, msg);
1717
1718 Log(CONNECTION,
1719 "Sending bridge status directly to node %u (Internet: %s)\n",
1720 nodeId, hasInternet ? "YES" : "NO");
1721
1722 // Send directly to the connection with high priority
1723 // This ensures the message is sent immediately rather than queued
1724 // The JSON message format is the same as what router::send() produces
1725 // (Variant serializes to the same JSON format we built manually)
1726 conn->addMessage(msg, true);
1727 });
1728 });
1729
1730 Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
1732 }
1733
1739 using namespace logger;
1740
1741 if (!this->isBridge() || !multiBridgeEnabled) {
1742 return;
1743 }
1744
1745 Log(STARTUP,
1746 "initBridgeCoordination(): Setting up multi-bridge coordination\n");
1747
1748 // Register our own priority in the bridgePriorities map
1749 // This ensures getRecommendedBridge() with PRIORITY_BASED strategy works
1750 // correctly
1752 Log(STARTUP,
1753 "initBridgeCoordination(): Registered self priority (nodeId: %u, "
1754 "priority: %d)\n",
1755 this->nodeId, bridgePriority);
1756
1757 // Register handler for incoming coordination messages (Type 613)
1758 this->callbackList.onPackage(
1759 613, // BRIDGE_COORDINATION type
1760 [this](protocol::Variant& variant, std::shared_ptr<Connection>,
1761 uint32_t) {
1762 JsonDocument doc;
1763 TSTRING str;
1764 variant.printTo(str);
1765 deserializeJson(doc, str);
1766 JsonObject obj = doc.as<JsonObject>();
1767
1768 if (obj["priority"].is<unsigned int>()) {
1769 uint32_t fromNode = obj["from"];
1770 uint8_t priority = obj["priority"];
1771 TSTRING role = obj["role"].as<TSTRING>();
1772 uint8_t load = obj["load"] | 0;
1773
1774 // Store bridge priority for selection decisions
1775 bridgePriorities[fromNode] = priority;
1776
1777 // Update peer bridges list
1778 if (obj["peerBridges"].is<JsonArray>()) {
1779 JsonArray peers = obj["peerBridges"];
1780 for (JsonVariant peer : peers) {
1781 uint32_t peerId = peer.as<uint32_t>();
1782 if (peerId != this->nodeId &&
1783 std::find(knownBridgePeers.begin(), knownBridgePeers.end(),
1784 peerId) == knownBridgePeers.end()) {
1785 if (knownBridgePeers.size() >= 32) {
1786 knownBridgePeers.erase(knownBridgePeers.begin());
1787 }
1788 knownBridgePeers.push_back(peerId);
1789 }
1790 }
1791 }
1792
1793 Log(CONNECTION,
1794 "Bridge coordination from %u: priority=%d, role=%s, "
1795 "load=%d%%\n",
1796 fromNode, priority, role.c_str(), load);
1797
1798 // Invoke bridge coordination callback if set
1799 if (this->bridgeCoordinationCallback) {
1801 this->bridgeCoordinationCallback(pkg, fromNode);
1802 }
1803
1804 // Change detection and bridgeCoordinationChangedCallback
1807 auto it = this->lastBridgeCoordinationState.find(fromNode);
1808 if (it == this->lastBridgeCoordinationState.end()) {
1809 this->lastBridgeCoordinationState[fromNode] = {
1810 priority, role, load, (uint32_t)millis()};
1811 this->bridgeCoordinationChangedCallback(pkg, fromNode, "new");
1812 } else {
1813 auto& prev = it->second;
1814 bool changed = (prev.priority != priority ||
1815 prev.role != role ||
1816 prev.load != load);
1817 prev.priority = priority;
1818 prev.role = role;
1819 prev.load = load;
1820 prev.lastSeen = (uint32_t)millis();
1821 if (changed) {
1822 this->bridgeCoordinationChangedCallback(pkg, fromNode,
1823 "updated");
1824 }
1825 }
1826 }
1827 }
1828 return false; // Don't consume the package
1829 });
1830
1831 // Create periodic task to send coordination messages
1833 30000, // 30 seconds interval
1834 TASK_FOREVER, [this]() { this->sendBridgeCoordination(); });
1835
1836 Log(STARTUP, "Bridge coordination enabled (priority: %d, role: %s)\n",
1837 bridgePriority, bridgeRole.c_str());
1838 }
1839
1845 using namespace logger;
1846
1847 if (!this->isBridge() || !multiBridgeEnabled) {
1848 return;
1849 }
1850
1851 // Calculate current load (simplified: based on node count)
1852 uint8_t currentLoad = 0;
1853 auto nodeCount = this->getNodeList(false).size();
1854 if (nodeCount > 0) {
1855 currentLoad = (nodeCount * 100) / MAX_CONN;
1856 if (currentLoad > 100) currentLoad = 100;
1857 }
1858
1859 // Create coordination message
1860 JsonDocument doc;
1861 JsonObject obj = doc.to<JsonObject>();
1862
1863 obj["type"] = 613; // BRIDGE_COORDINATION
1864 obj["from"] = this->nodeId;
1865 obj["routing"] = 2; // BROADCAST
1866 obj["priority"] = bridgePriority;
1867 obj["role"] = bridgeRole;
1868 obj["load"] = currentLoad;
1869 obj["timestamp"] = this->getNodeTime();
1870 obj["message_type"] = 613;
1871
1872 // Add peer bridges list
1873 JsonArray peers = obj["peerBridges"].to<JsonArray>();
1874 for (uint32_t peerId : knownBridgePeers) {
1875 peers.add(peerId);
1876 }
1877
1878 String msg;
1879 serializeJson(doc, msg);
1880
1881 // Update our own priority in bridgePriorities map
1882 // This ensures priority-based selection always has current data
1884
1885 this->sendBroadcast(msg);
1886
1887 Log(CONNECTION,
1888 "Bridge coordination sent: priority=%d, role=%s, load=%d%%\n",
1889 bridgePriority, bridgeRole.c_str(), currentLoad);
1890 }
1891
1899 uint8_t* routerChannel = nullptr) {
1900 using namespace logger;
1901 Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n",
1902 routerSSID.c_str());
1903
1904 int n = WiFi.scanNetworks(false, false);
1905 if (n == WIFI_SCAN_RUNNING) {
1906 // The station task's asynchronous scan is in flight; a synchronous
1907 // scan cannot start until it ends, and -2 is not "no networks". A
1908 // failover backup read it as the router being out of sight — twice,
1909 // thirty seconds apart — and never stood for election while the
1910 // primary was gone. Wait the scan out, bounded, then look again.
1911 Log(CONNECTION,
1912 "scanRouterSignalStrength(): a scan is already running, waiting "
1913 "for it\n");
1914 uint32_t waitedUntil = millis() + 8000;
1915#ifdef ESP32
1916 // Not WiFi.scanComplete(): on this core it gives a scan twenty
1917 // dwell times to finish and then declares it failed — 2.4 s for the
1918 // 120 ms all-channel re-detection, which takes six in AP+STA mode
1919 // as the radio keeps returning to serve the AP. Asked here at three
1920 // seconds, it dropped the scanning flag, the scan below started on
1921 // top of the one in flight, and both came back with nothing: the
1922 // backup read "router not found" and the station "mesh not found on
1923 // any channel", and lost the sighting it needed to follow the mesh.
1924 // The scanning bit is the flag the driver clears when the scan
1925 // really ends.
1926 while ((WiFiGenericClass::getStatusBits() & WIFI_SCANNING_BIT) &&
1927 (int32_t)(waitedUntil - millis()) > 0) {
1928 delay(50);
1929 }
1930 // The scan-done event hands the station its results next; let it
1931 // read them before this scan deletes them.
1932 delay(300);
1933#else
1934 while (WiFi.scanComplete() == WIFI_SCAN_RUNNING &&
1935 (int32_t)(waitedUntil - millis()) > 0) {
1936 delay(50);
1937 }
1938#endif
1939 n = WiFi.scanNetworks(false, false);
1940 }
1941 Log(CONNECTION, "scanRouterSignalStrength(): Found %d networks\n", n);
1942
1943 for (int i = 0; i < n; i++) {
1944 if (WiFi.SSID(i) == routerSSID) {
1945 int8_t rssi = WiFi.RSSI(i);
1946 uint8_t channel = WiFi.channel(i);
1947 if (routerChannel != nullptr) *routerChannel = channel;
1948 Log(CONNECTION,
1949 "scanRouterSignalStrength(): Found %s with RSSI %d dBm on "
1950 "channel %u\n",
1951 routerSSID.c_str(), rssi, channel);
1952 return rssi;
1953 }
1954 }
1955
1956 Log(CONNECTION, "scanRouterSignalStrength(): Router %s not found\n",
1957 routerSSID.c_str());
1958 return 0; // Router not found
1959 }
1960
1966 using namespace logger;
1967
1968 if (!bridgeFailoverEnabled) {
1969 Log(CONNECTION, "startBridgeElection(): Failover disabled\n");
1970 return;
1971 }
1972
1974 Log(CONNECTION,
1975 "startBridgeElection(): No router credentials, cannot participate\n");
1976 return;
1977 }
1978
1980 Log(CONNECTION, "startBridgeElection(): Election already in progress\n");
1981 return;
1982 }
1983
1984 // Prevent rapid role changes — but look again when the hold is over.
1985 // A candidate that stood down a moment ago, or that only just booted,
1986 // returned here silently and waited for the next 30 s tick to find
1987 // out that there was still no bridge.
1988 if (millis() - lastRoleChangeTime < 60000) {
1989 uint32_t wait = 60000 - (millis() - lastRoleChangeTime);
1990 Log(CONNECTION,
1991 "startBridgeElection(): Too soon after last role change, "
1992 "checking again in %u ms\n",
1993 wait);
1994 if (!bridgeCheckPending) {
1995 bridgeCheckPending = true;
1996 this->addTask(wait, TASK_ONCE, [this]() {
1997 bridgeCheckPending = false;
1998 this->checkForBridge();
1999 });
2000 }
2001 return;
2002 }
2003
2004 // CRITICAL: Check if mesh channel re-synchronization is needed first
2005 // If we haven't found any mesh nodes and are approaching the re-sync
2006 // threshold, prioritize finding the mesh over becoming a bridge. This
2007 // prevents the scenario where a node tries to become a bridge when it
2008 // should be re-syncing to find the mesh on a different channel (e.g., after
2009 // another node became bridge and switched channels to match the router).
2010 uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
2011 if (emptyScans >= 3 && WiFi.status() != WL_CONNECTED) {
2012 Log(CONNECTION,
2013 "startBridgeElection(): Mesh connectivity lost (%d empty scans), "
2014 "deferring election to allow channel re-sync\n",
2015 emptyScans);
2016
2017 // Schedule a retry after channel re-sync has had a chance to run
2018 // The channel re-sync threshold is StationScan::EMPTY_SCAN_THRESHOLD
2019 // scans (2, ~30 s). Fast scan interval is 0.5 * SCAN_INTERVAL = 15
2020 // seconds Wait for re-sync to complete plus a buffer
2021 uint32_t retryDelay =
2022 (StationScan::EMPTY_SCAN_THRESHOLD - emptyScans + 2) * 15000;
2023 Log(CONNECTION,
2024 "startBridgeElection(): Will retry election in %u seconds if still "
2025 "needed\n",
2026 retryDelay / 1000);
2027 return;
2028 }
2029
2030 Log(CONNECTION, "=== Bridge Election Started ===\n");
2032
2033 // Scan for router to get RSSI
2034 uint8_t routerChannel = 0;
2035 int8_t routerRSSI =
2036 scanRouterSignalStrength(routerSSID, &routerChannel);
2037
2038 if (routerRSSI == 0) {
2039 Log(CONNECTION,
2040 "startBridgeElection(): Router not visible, cannot participate\n");
2042 return;
2043 }
2044
2045 Log(CONNECTION, "startBridgeElection(): My router RSSI: %d dBm\n",
2046 routerRSSI);
2047
2048 // Clear previous candidates
2049 electionCandidates.clear();
2050
2051 // Add self as candidate
2052 BridgeCandidate selfCandidate;
2053 selfCandidate.nodeId = this->nodeId;
2054 selfCandidate.routerRSSI = routerRSSI;
2055 selfCandidate.routerChannel = routerChannel;
2056 selfCandidate.uptime = millis();
2057 selfCandidate.freeMemory = ESP.getFreeHeap();
2058 electionCandidates.push_back(selfCandidate);
2059
2060 // Broadcast candidacy using JSON directly (avoiding dependency on alteriom
2061 // package)
2062 JsonDocument doc;
2063 JsonObject obj = doc.to<JsonObject>();
2064 obj["type"] = protocol::BRIDGE_ELECTION;
2065 obj["from"] = this->nodeId;
2066 obj["routing"] = 2; // BROADCAST
2067 obj["routerRSSI"] = routerRSSI;
2068 obj["routerChannel"] = routerChannel;
2069 obj["uptime"] = millis();
2070 obj["freeMemory"] = ESP.getFreeHeap();
2071 obj["timestamp"] = this->getNodeTime();
2072 obj["routerSSID"] = routerSSID;
2073 obj["message_type"] = protocol::BRIDGE_ELECTION;
2074
2075 String msg;
2076 serializeJson(doc, msg);
2077
2078 // Send election message using raw broadcast to preserve type
2079 // BRIDGE_ELECTION
2080 protocol::Variant variant(msg);
2081 router::broadcast<Connection>(variant, (*this), 0);
2082
2083 Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
2084
2085 // Set election timeout
2088
2089 // Schedule election evaluation
2090 this->addTask(electionTimeoutMs + 100, TASK_ONCE,
2091 [this]() { this->evaluateElection(); });
2092 }
2093
2099 using namespace logger;
2100
2102 Log(CONNECTION, "evaluateElection(): Not in collecting state\n");
2103 return;
2104 }
2105
2106 Log(CONNECTION, "=== Evaluating Election ===\n");
2107 Log(CONNECTION, "evaluateElection(): %d candidates\n",
2108 electionCandidates.size());
2109
2110 // Find best candidate
2111 BridgeCandidate* winner = nullptr;
2112 int8_t bestRSSI = -127; // Worst possible RSSI
2113
2114 for (auto& candidate : electionCandidates) {
2115 Log(CONNECTION,
2116 "evaluateElection(): Candidate %u: RSSI=%d, uptime=%u, mem=%u\n",
2117 candidate.nodeId, candidate.routerRSSI, candidate.uptime,
2118 candidate.freeMemory);
2119
2120 if (candidate.routerRSSI > bestRSSI) {
2121 bestRSSI = candidate.routerRSSI;
2122 winner = &candidate;
2123 } else if (candidate.routerRSSI == bestRSSI && winner != nullptr) {
2124 // Tiebreaker 1: Higher uptime
2125 if (candidate.uptime > winner->uptime) {
2126 winner = &candidate;
2127 } else if (candidate.uptime == winner->uptime) {
2128 // Tiebreaker 2: More free memory
2129 if (candidate.freeMemory > winner->freeMemory) {
2130 winner = &candidate;
2131 } else if (candidate.freeMemory == winner->freeMemory) {
2132 // Tiebreaker 3: Lower node ID (deterministic)
2133 if (candidate.nodeId < winner->nodeId) {
2134 winner = &candidate;
2135 }
2136 }
2137 }
2138 }
2139 }
2140
2141 if (winner == nullptr) {
2142 Log(ERROR, "evaluateElection(): No winner found!\n");
2144 return;
2145 }
2146
2147 // Validate RSSI threshold for single-candidate elections
2148 // When only one candidate exists, it indicates the node is isolated from
2149 // the mesh. In this case, require minimum signal quality to prevent poor
2150 // connections. When multiple candidates exist, the mesh is connected and
2151 // best RSSI wins.
2152 if (electionCandidates.size() == 1 &&
2153 winner->routerRSSI < minimumBridgeRSSI) {
2154 Log(CONNECTION, "=== Election Failed: Insufficient Signal Quality ===\n");
2155 Log(CONNECTION,
2156 " Single candidate with RSSI %d dBm (minimum required: %d dBm)\n",
2157 winner->routerRSSI, minimumBridgeRSSI);
2158 Log(CONNECTION, " Node is isolated from mesh with poor router signal\n");
2159 Log(CONNECTION, " Rejecting election to prevent unreliable bridge\n");
2160 Log(CONNECTION,
2161 " Recommendation: Move closer to router or wait for mesh "
2162 "connection\n");
2163
2165 electionCandidates.clear();
2166
2167 // Notify via callback that election failed
2170 false, "Insufficient signal quality for isolated bridge");
2171 }
2172 return;
2173 }
2174
2175 Log(CONNECTION, "=== Election Winner: Node %u ===\n", winner->nodeId);
2176 Log(CONNECTION, " Router RSSI: %d dBm\n", winner->routerRSSI);
2177 Log(CONNECTION, " Uptime: %u ms\n", winner->uptime);
2178 Log(CONNECTION, " Free Memory: %u bytes\n", winner->freeMemory);
2179
2180 // Record election in diagnostics history
2181 if (this->diagnosticsEnabled) {
2182 ElectionRecord record;
2183 record.timestamp = millis();
2184 record.winnerNodeId = winner->nodeId;
2185 record.winnerRSSI = winner->routerRSSI;
2186 record.candidateCount = electionCandidates.size();
2187 record.reason = "Bridge failure detected";
2188
2189 this->electionHistory.push_back(record);
2190
2191 // Keep history limited to MAX_ELECTION_HISTORY
2192 if (this->electionHistory.size() > this->MAX_ELECTION_HISTORY) {
2193 this->electionHistory.erase(this->electionHistory.begin());
2194 }
2195
2196 Log(CONNECTION, "evaluateElection(): Election recorded in history\n");
2197 }
2198
2199 if (winner->nodeId == this->nodeId) {
2200 Log(CONNECTION, "[TARGET] I WON! Promoting to bridge...\n");
2202 } else {
2203 Log(CONNECTION, "Winner is node %u, remaining as regular node\n",
2204 winner->nodeId);
2205 }
2206
2208 electionCandidates.clear();
2209 }
2210
2223 void promoteToBridge(uint8_t routerChannel) {
2224 using namespace logger;
2225
2226 Log(STARTUP, "=== Becoming Bridge Node ===\n");
2227 Log(STARTUP, "Scheduling bridge promotion (async to avoid task corruption)\n");
2228
2229 // Store previous bridge (if any)
2230 // SAFETY: Use getPrimaryGateway() which returns the nodeId value directly
2231 // instead of getPrimaryBridge() which returns a pointer to a vector element.
2232 // This avoids crashes from dangling pointers that can occur if the
2233 // knownBridges vector is modified between pointer retrieval and use.
2234 uint32_t previousBridgeId = this->getPrimaryGateway();
2235
2236 // IMPORTANT: Send takeover announcement BEFORE switching channels
2237 // This ensures other nodes on the current channel receive the announcement
2238 Log(STARTUP,
2239 "Sending takeover announcement on current channel before "
2240 "switching...\n");
2241 JsonDocument doc;
2242 JsonObject obj = doc.to<JsonObject>();
2243 obj["type"] = protocol::BRIDGE_TAKEOVER;
2244 obj["from"] = this->nodeId;
2245 obj["routing"] = 2; // BROADCAST
2246 obj["previousBridge"] = previousBridgeId;
2247 obj["reason"] = "Election winner - best router signal";
2248 obj["routerRSSI"] = 0; // Not yet connected to router
2249 obj["routerChannel"] = routerChannel;
2250 obj["timestamp"] = this->getNodeTime();
2251 obj["message_type"] = protocol::BRIDGE_TAKEOVER;
2252
2253 String msg;
2254 serializeJson(doc, msg);
2255
2256 // Send takeover message using raw broadcast to preserve type
2257 // BRIDGE_TAKEOVER
2258 protocol::Variant variant(msg);
2259 router::broadcast<Connection>(variant, (*this), 0);
2260
2261 // Give time for announcement to propagate before channel switch
2262 // Allow event loop processing during hardware settling
2263 for (int i = 0; i < 100; i++) { delay(10); yield(); }
2264 Log(STARTUP, "[OK] Takeover announcement sent on channel %d\n", _meshChannel);
2265
2266 // Save current mesh configuration to restore if bridge init fails.
2267 // The copies below are captured by value into the deferred lambda so the
2268 // stop() call inside it cannot clear/mutate these members before the
2269 // reinit reads them. (The Mesh object itself outlives the lambda; this
2270 // protects against member mutation, not object lifetime.)
2271 // TODO(#373 follow-up): if the mesh owns its scheduler
2272 // (!isExternalScheduler), stop() deletes mScheduler and savedScheduler
2273 // dangles before initAsBridge() uses it. Pre-existing limitation —
2274 // bridge promotion requires a user-supplied scheduler.
2275 uint8_t savedChannel = _meshChannel;
2276 TSTRING savedMeshSSID = _meshSSID;
2277 TSTRING savedMeshPassword = _meshPassword;
2278 TSTRING savedRouterSSID = routerSSID;
2279 TSTRING savedRouterPassword = routerPassword;
2280 Scheduler *savedScheduler = mScheduler;
2281 auto savedBridgeRoleChangedCallback = bridgeRoleChangedCallback;
2282
2283 // CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
2284 // This prevents use-after-free crash when stop() clears taskList while
2285 // evaluateElection() task is still executing
2286 // Use minimal delay to allow current task to complete first
2287 this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
2288 [this, savedChannel, savedMeshSSID, savedMeshPassword,
2289 savedRouterSSID, savedRouterPassword, savedScheduler,
2290 savedBridgeRoleChangedCallback]() {
2291 using namespace logger;
2292
2293 Log(STARTUP, "Executing bridge promotion (stop/reinit cycle)\n");
2294
2295 // Now reconfigure as bridge (this will switch to router's channel)
2296 this->stop();
2297 // Allow event loop processing during hardware settling
2298 for (int i = 0; i < 100; i++) { delay(10); yield(); }
2299
2300 // initAsBridge always returns true: bridge mesh functionality is active
2301 // regardless of router connection status (router connection is opportunistic)
2302 this->initAsBridge(savedMeshSSID, savedMeshPassword, savedRouterSSID,
2303 savedRouterPassword, savedScheduler, _meshPort);
2304
2305 lastRoleChangeTime = millis();
2306
2307 Log(STARTUP, "[OK] Bridge promotion complete on channel %d\n", _meshChannel);
2308
2309 // Notify via callback
2310 if (savedBridgeRoleChangedCallback) {
2311 static const TSTRING reason = "Election winner - best router signal";
2312 savedBridgeRoleChangedCallback(true, reason);
2313 }
2314
2315 // Note: The initial takeover announcement was already sent earlier
2316 // before the channel switch. The follow-up announcement that was previously
2317 // scheduled here has been removed to avoid potential crashes from scheduling
2318 // tasks immediately after stop()/reinit cycle.
2319 //
2320 // The bridge status broadcast system (initialized by initAsBridge via
2321 // initBridgeStatusBroadcast) will continue to inform nodes about the new
2322 // bridge through periodic broadcasts. Nodes that switched channels will
2323 // discover the new bridge through these status broadcasts.
2324 Log(STARTUP,
2325 "Bridge takeover complete. Status broadcasts will announce bridge to "
2326 "network.\n");
2327 });
2328 }
2329
2352 using namespace logger;
2353
2354 Log(CONNECTION, "=== Isolated Bridge Promotion Attempt ===\n");
2355 Log(CONNECTION, "Attempt %d of %d\n", _isolatedBridgeRetryAttempts + 1,
2357
2358 // First, scan for router to check if it's visible
2359 int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
2360
2361 if (routerRSSI == 0) {
2362 Log(CONNECTION,
2363 "attemptIsolatedBridgePromotion(): Router %s not visible\n",
2364 routerSSID.c_str());
2365 return false; // Don't count as an attempt - router not visible
2366 }
2367
2368 // Check minimum RSSI threshold for isolated promotion
2369 if (routerRSSI < minimumBridgeRSSI) {
2370 Log(CONNECTION,
2371 "attemptIsolatedBridgePromotion(): Router RSSI %d dBm below "
2372 "threshold %d dBm\n",
2373 routerRSSI, minimumBridgeRSSI);
2374 return false; // Don't count as an attempt - signal too weak
2375 }
2376
2377 Log(CONNECTION,
2378 "attemptIsolatedBridgePromotion(): Router visible with RSSI %d dBm\n",
2379 routerRSSI);
2380 Log(CONNECTION,
2381 "Attempting direct bridge promotion (bypassing election)\n");
2382 Log(CONNECTION,
2383 "Scheduling stop/reinit (async to avoid task corruption)\n");
2384
2385 // Save current mesh configuration. Captured by value into the deferred
2386 // lambda so stop() cannot clear/mutate these members before the reinit
2387 // reads them (see the matching comment in promoteToBridge(); same
2388 // TODO(#373 follow-up) about savedScheduler and internal schedulers
2389 // applies here).
2390 uint8_t savedChannel = _meshChannel;
2391 TSTRING savedMeshSSID = _meshSSID;
2392 TSTRING savedMeshPassword = _meshPassword;
2393 TSTRING savedRouterSSID = routerSSID;
2394 TSTRING savedRouterPassword = routerPassword;
2395 Scheduler *savedScheduler = mScheduler;
2396 auto savedBridgeRoleChangedCallback = bridgeRoleChangedCallback;
2397
2398 // CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
2399 // This prevents use-after-free crash when stop() clears taskList while
2400 // the retry task is still executing
2401 // Use minimal delay to allow current task to complete first
2402 this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
2403 [this, savedChannel, savedMeshSSID, savedMeshPassword,
2404 savedRouterSSID, savedRouterPassword, savedScheduler,
2405 savedBridgeRoleChangedCallback]() {
2406 using namespace logger;
2407
2408 Log(CONNECTION, "Executing isolated bridge promotion (stop/reinit cycle)\n");
2409
2410 // Stop current mesh operations
2411 this->stop();
2412 // Allow event loop processing during hardware settling
2413 for (int i = 0; i < 100; i++) { delay(10); yield(); }
2414
2415 // initAsBridge always returns true: bridge mesh functionality is active
2416 // regardless of router connection status (router connection is opportunistic)
2417 this->initAsBridge(savedMeshSSID, savedMeshPassword, savedRouterSSID,
2418 savedRouterPassword, savedScheduler, _meshPort);
2419
2420 // Reset retry counter
2422 lastRoleChangeTime = millis();
2423
2424 Log(STARTUP, "[OK] Isolated bridge promotion complete on channel %d\n",
2425 _meshChannel);
2426
2427 // Notify via callback
2428 if (savedBridgeRoleChangedCallback) {
2429 static const TSTRING reason = "Isolated node promoted to bridge";
2430 savedBridgeRoleChangedCallback(true, reason);
2431 }
2432
2433 // Note: Bridge status announcement will be sent automatically by
2434 // initBridgeStatusBroadcast() which is called by initAsBridge().
2435 // The immediate broadcast is scheduled in that function, so we don't
2436 // need to schedule another one here. This avoids potential crashes from
2437 // scheduling tasks immediately after stop()/reinit cycle.
2438 // The initBridgeStatusBroadcast() also sets up periodic broadcasts.
2439 Log(STARTUP,
2440 "Bridge status announcement will be sent by bridge status broadcast "
2441 "system\n");
2442 });
2443
2444 return true; // Count as an attempt - we scheduled the promotion
2445 }
2446
2451 void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI,
2452 uint8_t routerChannel, uint32_t uptime,
2453 uint32_t freeMemory) {
2454 using namespace logger;
2455
2457 Log(CONNECTION,
2458 "handleBridgeElection(): Not collecting candidates, ignoring\n");
2459 return;
2460 }
2461
2462 // Check if candidate already exists
2463 for (auto& candidate : electionCandidates) {
2464 if (candidate.nodeId == fromNode) {
2465 Log(CONNECTION,
2466 "handleBridgeElection(): Duplicate candidate from %u, ignoring\n",
2467 fromNode);
2468 return;
2469 }
2470 }
2471
2472 BridgeCandidate candidate;
2473 candidate.nodeId = fromNode;
2474 candidate.routerRSSI = routerRSSI;
2475 candidate.routerChannel = routerChannel;
2476 candidate.uptime = uptime;
2477 candidate.freeMemory = freeMemory;
2478
2479 electionCandidates.push_back(candidate);
2480
2481 Log(CONNECTION,
2482 "handleBridgeElection(): Added candidate %u (RSSI: %d dBm)\n", fromNode,
2483 routerRSSI);
2484 }
2485
2490 void sendBridgeStatus(bool leaving = false) {
2491 using namespace logger;
2492
2493 if (!this->bridgeStatusBroadcastEnabled) {
2494 return;
2495 }
2496
2497 // A bridge that nobody can reach is no bridge. On the rig a node
2498 // promoted in place reset every TCP connection to its AP for its whole
2499 // time as bridge — its listener, re-created by the promotion's
2500 // stop/re-init, was not listening, and nothing looked. This task runs
2501 // every thirty seconds on a bridge: if the listener is not in LISTEN
2502 // (1 on both cores) it is re-created, and the log says so.
2503 if (_tcpListener != nullptr && _tcpListener->status() != TCP_STATE_LISTEN) {
2504 Log(ERROR,
2505 "sendBridgeStatus(): TCP listener on port %d is in state %u, not "
2506 "LISTEN; re-creating it\n",
2507 _meshPort, (unsigned)_tcpListener->status());
2508 delete _tcpListener;
2509 _tcpListener = nullptr;
2510 tcpServerInit();
2511 }
2512
2513 // Create bridge status package
2514 // We need to include the package header here since we're in wifi namespace
2515 // The package will be sent as a JSON string
2516 JsonDocument doc;
2517 JsonObject obj = doc.to<JsonObject>();
2518
2519 obj["type"] = protocol::BRIDGE_STATUS;
2520 obj["from"] = this->nodeId;
2521 obj["routing"] = 2; // BROADCAST routing
2522 obj["timestamp"] = this->getNodeTime();
2523
2524 // Check Internet connectivity: WiFi connected AND valid IP address
2525 // We check for valid local IP instead of gateway IP because:
2526 // 1. Gateway IP might not be immediately available after connection
2527 // 2. Some networks (mobile hotspots) may not provide gateway IP via DHCP
2528 // 3. Having a valid local IP + being connected is sufficient for internet
2529 // access
2530 bool hasInternet = !leaving && (WiFi.status() == WL_CONNECTED) &&
2531 (WiFi.localIP() != IPAddress(0, 0, 0, 0));
2532 obj["internetConnected"] = hasInternet;
2533 // A bridge stepping down says so. Every candidate would otherwise hold
2534 // it healthy until its last status aged out, a minute and more, before
2535 // holding an election — on the rig, past the failover contract.
2536 if (leaving) obj["leaving"] = true;
2537
2538 int8_t rssi = WiFi.RSSI();
2539 uint8_t channel = WiFi.channel();
2540 uint32_t uptime = millis();
2541 TSTRING gatewayIP = WiFi.gatewayIP().toString();
2542
2543 obj["routerRSSI"] = rssi;
2544 obj["routerChannel"] = channel;
2545 obj["uptime"] = uptime;
2546 obj["gatewayIP"] = gatewayIP;
2547 obj["message_type"] = protocol::BRIDGE_STATUS;
2548
2549 String msg;
2550 serializeJson(doc, msg);
2551
2552 Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)%s\n",
2553 hasInternet ? "Connected" : "Disconnected",
2554 leaving ? " — stepping down" : "");
2555 Log(GENERAL,
2556 "sendBridgeStatus(): WiFi status=%d, localIP=%s, gatewayIP=%s\n",
2557 WiFi.status(), WiFi.localIP().toString().c_str(),
2558 WiFi.gatewayIP().toString().c_str());
2559
2560 // Update our own bridge status in knownBridges list
2561 // This ensures the bridge reports itself correctly when queried
2562 this->updateBridgeStatus(this->nodeId, hasInternet, rssi, channel, uptime,
2563 gatewayIP, this->getNodeTime());
2564
2565 // Send bridge status using raw broadcast to preserve type BRIDGE_STATUS
2566 // Using sendBroadcast(msg) would wrap it in type 8 (BROADCAST) and hide
2567 // type BRIDGE_STATUS
2568 protocol::Variant variant(msg);
2569 router::broadcast<Connection>(variant, (*this), 0);
2570 _lastBridgeStatusBroadcast = millis();
2571 }
2572
2583 using namespace logger;
2584
2585 // Cache result to avoid blocking DNS/HTTP on every call
2586 static uint32_t lastCheckTime = 0;
2587 static bool lastResult = false;
2588 uint32_t now = millis();
2589 if (lastCheckTime > 0 && (now - lastCheckTime) < GATEWAY_CONNECTIVITY_CACHE_MS) {
2590 return lastResult;
2591 }
2592
2593 // First check WiFi connection
2594 if (WiFi.status() != WL_CONNECTED) {
2595 lastCheckTime = millis();
2596 lastResult = false;
2597 return false;
2598 }
2599
2600 // Check if we have a valid local IP
2601 if (WiFi.localIP() == IPAddress(0, 0, 0, 0)) {
2602 lastCheckTime = millis();
2603 lastResult = false;
2604 return false;
2605 }
2606
2607 // Try to resolve a well-known DNS name
2608 // Using Google's servers as they have high availability globally
2609 IPAddress result;
2610
2611#if defined(ESP8266)
2612 // ESP8266's hostByName() has a timeout overload, so the resolver cannot
2613 // stall the cooperative scheduler past GATEWAY_DNS_TIMEOUT_MS — that term
2614 // is part of gatewayBlockingBudgetMs() and covered by its static_assert
2615 // (issue #416).
2616 int dnsResult =
2617 WiFi.hostByName("www.google.com", result, GATEWAY_DNS_TIMEOUT_MS);
2618
2619 // Check if DNS resolution succeeded
2620 if (dnsResult != 1) {
2621 Log(COMMUNICATION, "hasActualInternetAccess(): DNS resolution failed (code=%d)\n", dnsResult);
2622 lastCheckTime = millis();
2623 lastResult = false;
2624 return false;
2625 }
2626
2627 // Additional validation: Check if resolved IP is valid
2628 // Some ESP8266 versions may return success but set IP to 255.255.255.255 on error
2629 if (result == IPAddress(0, 0, 0, 0) || result == IPAddress(255, 255, 255, 255)) {
2630 TSTRING resultStr = result.toString();
2631 Log(COMMUNICATION, "hasActualInternetAccess(): Invalid DNS result IP: %s\n", resultStr.c_str());
2632 lastCheckTime = millis();
2633 lastResult = false;
2634 return false;
2635 }
2636#elif defined(ESP32)
2637 // ESP32's hostByName() has no timeout parameter in the cores this library
2638 // targets, so a standalone DNS probe would be an unbounded stall on the
2639 // cooperative scheduler whenever DNS is slow or blackholed — the exact
2640 // failure mode of issues #318/#332, recurring once per
2641 // GATEWAY_CONNECTIVITY_CACHE_MS. The probe is therefore skipped on ESP32
2642 // (issue #416): actual reachability is established by the captive-portal
2643 // probe that runs right after this check on the same path — an HTTP
2644 // round trip bounded by GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS that fails on
2645 // a router without Internet just as the DNS probe would.
2646 (void)result;
2647 lastCheckTime = millis();
2648 lastResult = true;
2649 return true;
2650#else
2651 // Other platforms: assume internet is available if WiFi connected
2652 // (no reliable way to test without platform-specific APIs)
2653 lastCheckTime = millis();
2654 lastResult = true;
2655 return true;
2656#endif
2657
2658 TSTRING resultStr = result.toString();
2659 Log(COMMUNICATION, "hasActualInternetAccess(): Internet connectivity verified (resolved to %s)\n",
2660 resultStr.c_str());
2661 lastCheckTime = millis();
2662 lastResult = true;
2663 return true;
2664 }
2665
2680 using namespace logger;
2681
2682#if defined(ESP32) || defined(ESP8266)
2683 // Cache the verdict, exactly as hasActualInternetAccess() does. This probe
2684 // is a full HTTP round trip to an external host; running it per message
2685 // put that round trip in front of every single mesh->Internet send, on the
2686 // cooperative scheduler, where it stacks with the request's own timeout.
2687 static uint32_t lastCheckTime = 0;
2688 static bool lastResult = true;
2689 uint32_t now = millis();
2690 if (lastCheckTime > 0 && (now - lastCheckTime) < GATEWAY_CONNECTIVITY_CACHE_MS) {
2691 return lastResult;
2692 }
2693
2694 // Record the attempt up front so that every early return below is cached
2695 // too -- otherwise a failing probe would be retried on every message.
2696 lastCheckTime = millis();
2697 lastResult = false;
2698
2699 HTTPClient http;
2700 http.setTimeout(GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS);
2701
2702 WiFiClient client;
2703
2704 // Use Apple's captive portal detection endpoint
2705 // This is a well-maintained, reliable endpoint used by iOS devices
2706 const char* testUrl = "http://captive.apple.com/hotspot-detect.html";
2707
2708 Log(COMMUNICATION, "detectCaptivePortal(): Testing %s\n", testUrl);
2709
2710#ifdef ESP8266
2711 if (!http.begin(client, testUrl)) {
2712 Log(COMMUNICATION, "detectCaptivePortal(): Failed to begin HTTP client - treating as potential network restriction\n");
2713 return false; // Conservative approach: treat initialization failure as potential captive portal or network restriction
2714 }
2715#else
2716 // ESP32
2717 if (!http.begin(testUrl)) {
2718 Log(COMMUNICATION, "detectCaptivePortal(): Failed to begin HTTP client - treating as potential network restriction\n");
2719 return false;
2720 }
2721#endif
2722
2723 int httpCode = http.GET();
2724
2725 if (httpCode != 200) {
2726 // Any response other than HTTP 200 indicates captive portal or network issue
2727 Log(COMMUNICATION, "detectCaptivePortal(): Unexpected HTTP code %d (expected 200)\n", httpCode);
2728 http.end();
2729 return false;
2730 }
2731
2732 // Check response content
2733 String response = http.getString();
2734 http.end();
2735
2736 // Verify the response contains "Success" - this is Apple's standard response
2737 // Full response: "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>"
2738 // We check for presence of "Success" to be robust against minor format variations
2739 if (response.indexOf("Success") == -1) {
2740 Log(COMMUNICATION, "detectCaptivePortal(): Response doesn't contain 'Success', likely captive portal\n");
2741 return false;
2742 }
2743
2744 Log(COMMUNICATION, "detectCaptivePortal(): No captive portal detected\n");
2745 lastResult = true;
2746 return true;
2747
2748#else
2749 // Non-ESP platforms: can't reliably test, assume no captive portal
2750 return true;
2751#endif
2752 }
2753
2760
2761#if defined(ESP32) || defined(ESP8266)
2770 static TSTRING readResponseExcerpt(HTTPClient& http) {
2771 const int size = http.getSize();
2772 if (size >= 0 &&
2773 static_cast<size_t>(size) <=
2774 gateway::GATEWAY_RESPONSE_HEAD_BYTES + gateway::GATEWAY_RESPONSE_TAIL_BYTES) {
2775 return http.getString();
2776 }
2778 WiFiClient* stream = http.getStreamPtr();
2779 if (stream == nullptr) return excerpt.text();
2780 const bool chunked =
2781 size < 0 && gateway::transferEncodingIsChunked(http.header("Transfer-Encoding"));
2782 gateway::ChunkedBodyDecoder decoder(excerpt);
2783 const uint32_t deadline =
2784 millis() + gateway::GATEWAY_RESPONSE_HEAD_TIMEOUT_MS;
2785 bool more = true;
2786 while (more && static_cast<int32_t>(deadline - millis()) > 0) {
2787 int available = stream->available();
2788 if (available <= 0) {
2789 if (!stream->connected()) break;
2790 delay(1);
2791 continue;
2792 }
2793 while (more && available-- > 0) {
2794 const int c = stream->read();
2795 if (c < 0) break;
2796 more = chunked ? decoder.add(static_cast<char>(c))
2797 : excerpt.add(static_cast<char>(c));
2798 }
2799 }
2800 return excerpt.text();
2801 }
2802#endif
2803
2804 // sendGatewayAck() is portable and lives in painlessmesh::Mesh
2805 // (mesh.hpp): every node needs it, not only a gateway, to answer a
2806 // request it cannot serve.
2807
2839 using namespace logger;
2840 Log(STARTUP,
2841 "initGatewayInternetHandler(): Registering GATEWAY_DATA handler\n");
2842
2843 this->callbackList.onPackage(
2845 std::shared_ptr<Connection> ingress,
2846 uint32_t) {
2847 auto pkg = variant.to<gateway::GatewayDataPackage>();
2848
2849 Log(COMMUNICATION,
2850 "Gateway received Internet request: msgId=%u dest=%s\n",
2851 pkg.messageId, pkg.destination.c_str());
2852
2853 // Every exit path below goes through finish(), which re-arms the
2854 // mesh watchdog on all peers before the ack is routed.
2855 //
2856 // Everything from here on can block: the connectivity probes are
2857 // network round trips and the request itself runs to
2858 // GATEWAY_HTTP_TIMEOUT_MS. Nothing on the cooperative scheduler runs
2859 // during that, but wall-clock time keeps passing, so any peer whose
2860 // NODE_TIMEOUT deadline fell inside the stall is overdue the instant
2861 // the scheduler resumes and gets closed despite never having gone
2862 // missing (issues #318, #332).
2863 //
2864 // The previous mitigation disabled timeOutTask on the requesting
2865 // connection only. That left every other peer exposed, and it acted
2866 // before the stall rather than after it, which is the half that
2867 // cannot work: the deadline is wall-clock, so pushing it out ahead of
2868 // a stall longer than the window changes nothing.
2869 //
2870 // Compensation is by the *measured* stall (issue #417): exit paths
2871 // that never blocked measure ~0 ms and grant nothing, so a peer
2872 // that genuinely stopped answering NODE_SYNC still gets reaped even
2873 // under continuous gateway traffic from other peers.
2874 const auto blockingStartedMs = millis();
2875 // `retryable` is the gateway's word to the origin node on whether
2876 // the identical request may be sent again: 0 whenever it may have
2877 // reached the server, so a retry cannot deliver it twice.
2878 auto finish = [this, &pkg, ingress, blockingStartedMs](
2879 bool ok, uint16_t code, const TSTRING& err,
2880 int8_t retryable, const TSTRING& response = TSTRING(),
2881 uint32_t retryAfterMs = 0) {
2882 const auto stalledMs = millis() - blockingStartedMs;
2883 auto refreshed = gateway::refreshPeerWatchdogs(*this, stalledMs);
2884 if (refreshed > 0) {
2885 Log(COMMUNICATION,
2886 "Gateway postponed the mesh watchdog on %u peer(s) by %lu ms "
2887 "after a blocking Internet request\n",
2888 static_cast<unsigned>(refreshed),
2889 static_cast<unsigned long>(stalledMs));
2890 }
2891 this->sendGatewayAck(pkg, ok, code, err, ingress, response, retryable,
2892 retryAfterMs);
2893 };
2894
2895 // Check Internet connectivity
2896 // First check WiFi status for quick fail
2897 if (WiFi.status() != WL_CONNECTED) {
2898 finish(false, 0, "Gateway WiFi not connected", 0);
2899 return true; // Consume package - we handled it (with error)
2900 }
2901
2902 // Then check actual internet access (DNS resolution)
2903 // This detects when WiFi is connected but router has no internet
2904 if (!hasActualInternetAccess()) {
2905 finish(false, 0, "Router has no internet access - check WAN connection", 0);
2906 return true; // Consume package - we handled it (with error)
2907 }
2908
2909 // Finally, check for captive portal interference
2910 // This detects when DNS works but HTTP requests are intercepted
2911 if (!detectCaptivePortal()) {
2912 finish(false, 0, "Captive portal detected - requires web authentication. Check router/WiFi settings", 0);
2913 return true; // Consume package - we handled it (with error)
2914 }
2915
2916#if defined(ESP32) || defined(ESP8266)
2917#ifdef ESP32
2918 // A destination whose name failed to resolve is refused for
2919 // GATEWAY_DNS_NEGATIVE_TTL_MS without another lookup (issue #453).
2920 // HTTPClient reports a failed lookup as "connection refused" after
2921 // the resolver's own patience -- on ESP32 not boundable by this
2922 // library -- and it did so on every attempt: the origin node's
2923 // retries and, since #451, the bridge's own sends each paid it, and
2924 // the scheduler stalled long enough for relayed requests to time
2925 // out on their origin nodes. Resolving here, once, names the real
2926 // failure and caps the stall to one lookup per TTL per host.
2927 // ESP32 only: the ESP8266 core bounds HTTPClient's own lookup by the
2928 // HTTP timeout, and a second bounded wait would not fit the
2929 // blocking budget.
2930 const TSTRING host = gateway::hostFromUrl(pkg.destination);
2931 if (host.length() > 0 && !gateway::looksLikeIpLiteral(host)) {
2932 uint32_t failedAgoMs = 0;
2933 if (negativeDns.isFailing(host, millis(), &failedAgoMs)) {
2934 char dnsBuf[160];
2935 snprintf(dnsBuf, sizeof(dnsBuf),
2936 "DNS lookup for %s failed %lus ago; not retried for "
2937 "another %lus",
2938 host.c_str(),
2939 static_cast<unsigned long>(failedAgoMs / 1000),
2940 static_cast<unsigned long>(
2941 (gateway::GATEWAY_DNS_NEGATIVE_TTL_MS - failedAgoMs) /
2942 1000));
2943 Log(ERROR, "%s\n", dnsBuf);
2944 finish(false, 0, TSTRING(dnsBuf), 0);
2945 return true;
2946 }
2947 IPAddress resolved;
2948 if (WiFi.hostByName(host.c_str(), resolved) != 1) {
2949 negativeDns.remember(host, millis());
2950 char dnsBuf[160];
2951 snprintf(dnsBuf, sizeof(dnsBuf), "DNS lookup failed for %s",
2952 host.c_str());
2953 Log(ERROR, "%s\n", dnsBuf);
2954 finish(false, 0, TSTRING(dnsBuf), 0);
2955 return true;
2956 }
2957 }
2958#endif // ESP32
2959
2960 // Make HTTP/HTTPS request
2961 HTTPClient http;
2962 http.setTimeout(GATEWAY_HTTP_TIMEOUT_MS);
2963
2964 bool success = false;
2965 // HTTPClient returns an int: a positive value is an HTTP status
2966 // code, a negative one is its own transport error (HTTPC_ERROR_*,
2967 // -1 for a refused connection, -11 for a read timeout, ...). This
2968 // was a uint16_t, which wrapped -1 to 65535 -- a value that passes
2969 // `httpCode > 0`, so every transport failure was reported to the
2970 // origin node as "HTTP 65535" (issue #446), the errorToString()
2971 // branch below was unreachable, and handleGatewayAck() classified a
2972 // transient network failure as a non-retryable HTTP status.
2973 int httpCode = 0;
2974 TSTRING error = "";
2975
2976#ifdef ESP8266
2977 // ESP8266: Declare clients at function scope to ensure
2978 // they survive until after the HTTP request completes
2979 WiFiClient client;
2980 WiFiClientSecure secureClient;
2981#endif
2982
2983 if (pkg.destination.startsWith("https://")) {
2984#ifdef ESP32
2985 // ESP32: no trust anchor is configured, so the certificate is not
2986 // validated against anything this library supplies. See the
2987 // security notes on initGatewayInternetHandler() and SECURITY.md.
2988 http.begin(pkg.destination.c_str());
2989#elif defined(ESP8266)
2990 // ESP8266: Use insecure mode to reduce memory overhead
2991 // WARNING: This disables SSL certificate validation
2992 secureClient.setInsecure();
2993 http.begin(secureClient, pkg.destination.c_str());
2994#endif
2995 } else {
2996#ifdef ESP32
2997 http.begin(pkg.destination.c_str());
2998#elif defined(ESP8266)
2999 // ESP8266: begin() requires a client parameter
3000 http.begin(client, pkg.destination.c_str());
3001#endif
3002 }
3003
3004 // The same id on every attempt at this call. A service that
3005 // honours Idempotency-Key treats a retry as the request it already
3006 // has; anything recording requests can count the copies.
3007 const TSTRING requestId =
3008 gateway::requestIdFor(pkg.originNode, pkg.messageId, pkg.requestNonce);
3009 http.addHeader("X-Request-Id", requestId.c_str());
3010 http.addHeader("Idempotency-Key", requestId.c_str());
3011 // Not const: both cores declare collectHeaders(const char* keys[], ...).
3012 const char* collected[] = {"Retry-After", "Transfer-Encoding"};
3013 http.collectHeaders(collected, 2);
3014
3015 // Make request (GET if no payload, POST if payload)
3016 if (pkg.payload.length() > 0) {
3017 http.addHeader("Content-Type", pkg.contentType.c_str());
3018 httpCode = http.POST(pkg.payload.c_str());
3019 } else {
3020 httpCode = http.GET();
3021 }
3022
3023 // HTTP semantics only: 200/201/202/204 succeed, anything else fails,
3024 // and a retry is allowed only when it cannot deliver the request
3025 // twice (see gateway::classifyHttpResult). Whether a service's reply
3026 // means what the application wanted is the application's decision,
3027 // so the start of the body goes back on success as well as failure.
3028 //
3029 // 3xx redirects are not automatically followed.
3030 //
3031 // The classification lives in painlessmesh::gateway so the desktop
3032 // test suite can exercise it; this file is ESP-only and never
3033 // compiled by the Catch2 build.
3034 TSTRING responseHead;
3035 if (httpCode > 0) {
3036 responseHead =
3037 readResponseExcerpt(http);
3038 }
3039 const auto outcome =
3040 gateway::classifyHttpResult(httpCode, responseHead);
3041 success = outcome.success;
3042 const TSTRING responseSummary =
3043 httpCode > 0 ? gateway::summarizeResponseBody(responseHead) : TSTRING();
3044 uint32_t retryAfterMs = 0;
3045 if (outcome.retryable && httpCode > 0) {
3046 retryAfterMs = gateway::parseRetryAfterMs(http.header("Retry-After"));
3047 }
3048
3049 if (!outcome.transportError) {
3050 char errorBuf[320];
3051 const char* reason = outcome.reason.c_str();
3052 const char* sep = outcome.reason.length() > 0 ? ": " : "";
3053 if (success) {
3054 Log(COMMUNICATION, "HTTP request completed: code=%d\n", httpCode);
3055 } else if (outcome.unverifiedStatus) {
3056 // A 2xx outside 200/201/202/204: the server answered, but not
3057 // with a status this library can call a delivery. Never
3058 // retried. Logged at ERROR, where a sketch on the default
3059 // levels sees what the service said.
3060 snprintf(errorBuf, sizeof(errorBuf),
3061 "HTTP %d: not a delivery the gateway can confirm%s%s",
3062 httpCode, sep, reason);
3063 error = TSTRING(errorBuf);
3064 Log(ERROR, "HTTP response unverified: %s\n", errorBuf);
3065 } else if (outcome.retryable) {
3066 // 429 or 503: the server says it did not take the request.
3067 snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
3068 reason);
3069 error = TSTRING(errorBuf);
3070 Log(COMMUNICATION, "HTTP %d: the server asked to retry (after %lu ms)\n",
3071 httpCode, static_cast<unsigned long>(retryAfterMs));
3072 } else {
3073 snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
3074 reason);
3075 error = TSTRING(errorBuf);
3076 Log(ERROR, "HTTP request failed: %s\n", errorBuf);
3077 }
3078 } else {
3079 // Below HTTP. When the request may already be at the server -- a
3080 // read timeout, a connection lost after sending -- it is not
3081 // retried, and the error says why, so an application that
3082 // resends does so knowing it may duplicate.
3083 error = http.errorToString(httpCode);
3084 if (!outcome.retryable) {
3085 error += " (the request may have reached the server; not retried)";
3086 }
3087 Log(ERROR, "HTTP request failed: %s\n", error.c_str());
3088 }
3089
3090 http.end();
3091
3092 // Send acknowledgment back, with the verdict on retrying and the
3093 // start of whatever the server said.
3094 finish(success, outcome.ackStatus, error, outcome.retryable ? 1 : 0,
3095 responseSummary, retryAfterMs);
3096#else
3097 // Non-ESP platform - send error
3098 finish(false, 0, "HTTP client not available on this platform", 0);
3099#endif
3100
3101 return true; // Consume package - we have processed it and sent
3102 // acknowledgment
3103 });
3104 }
3105
3116 if (!this->isInternetHealthCheckEnabled() || !stationScan.manual) return;
3117 this->addTask([this]() { this->checkInternetNow(); });
3118 }
3119
3121 using namespace logger;
3122 // Where the station scan learns the channel the mesh is rooted on.
3123 // It used to look at its own scan's end, which a node connected to a
3124 // rooted mesh reaches once a minute: the node the failover test sends
3125 // from joined the bridge, the bridge left 24 s later, and no scan had
3126 // run in between — so it had no home to keep and followed a partition
3127 // off the router's channel. Topology changes are the moment to look.
3128 // Only "ever rooted" comes from the tree: the tree can carry a root that
3129 // is gone. On the rig a node's cached tree still showed the bridge as
3130 // its root for the seconds after the bridge had moved to its router's
3131 // channel, a topology change fired in that window, and the node
3132 // recorded the channel it was left on as home — then sat there alone,
3133 // seeing the mesh's four nodes on the router's channel and "staying".
3134 // Home is learnt from the bridge itself, below: its status message
3135 // carries the channel it is on, and a message just received is live.
3136 auto noteRoot = [this](uint32_t) {
3137 if (layout::isRooted(this->asNodeTree())) this->stationScan.noteEverRooted();
3138 };
3139 this->newConnectionCallbacks.push_back(noteRoot);
3140 this->changedConnectionCallbacks.push_back(noteRoot);
3141 this->callbackList.onPackage(
3143 [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
3144 JsonDocument doc;
3145 TSTRING str;
3146 variant.printTo(str);
3147 if (deserializeJson(doc, str)) return false;
3148 JsonObject obj = doc.as<JsonObject>();
3149 if (obj["leaving"] | false) {
3150 // The bridge is stepping down; the generic handler forgets it.
3151 // Look for another now, not at the monitor's next tick.
3152 this->checkForBridgeSoon();
3153 return false;
3154 }
3155 uint8_t routerChannel = obj["routerChannel"] | 0;
3156 if (gateway::isValidMeshChannel(routerChannel)) {
3157 this->stationScan.noteRooted(routerChannel);
3158 }
3159 return false; // the generic handler records the bridge
3160 });
3161#ifdef ESP32
3162 eventScanDoneHandler = WiFi.onEvent(
3163 [this](WiFiEvent_t event, WiFiEventInfo_t info) {
3164 if (this->semaphoreTake()) {
3165 Log(CONNECTION,
3166 "eventScanDoneHandler: ARDUINO_EVENT_WIFI_SCAN_DONE\n");
3167 // Not scanComplete() here. This callback runs on the core's
3168 // network-event task, under the lock that task dispatches
3169 // with, and scanComplete() can end in followBridgeChannel(),
3170 // which restarts the AP. On core 3.x that waits for events
3171 // only this task can deliver: on the rig every esp32-c5 and
3172 // esp32-c6 that followed the bridge's channel went silent for
3173 // the rest of the run, and stop() then blocked in
3174 // removeEvent() on the same lock. The result is consumed by
3175 // the station task instead, from update().
3176 this->stationScan.scanDone();
3177 this->semaphoreGive();
3178 }
3179 },
3180#if ESP_ARDUINO_VERSION_MAJOR >= 2
3181 WiFiEvent_t::ARDUINO_EVENT_WIFI_SCAN_DONE);
3182#else
3183 WiFiEvent_t::SYSTEM_EVENT_SCAN_DONE);
3184#endif
3185
3186 eventSTAStartHandler = WiFi.onEvent(
3187 [this](WiFiEvent_t event, WiFiEventInfo_t info) {
3188 if (this->semaphoreTake()) {
3189 Log(CONNECTION,
3190 "eventSTAStartHandler: ARDUINO_EVENT_WIFI_STA_START\n");
3191 this->semaphoreGive();
3192 }
3193 },
3194#if ESP_ARDUINO_VERSION_MAJOR >= 2
3195 WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_START);
3196#else
3197 WiFiEvent_t::SYSTEM_EVENT_STA_START);
3198#endif
3199
3200 eventSTADisconnectedHandler = WiFi.onEvent(
3201 [this](WiFiEvent_t event, WiFiEventInfo_t info) {
3202 if (this->semaphoreTake()) {
3203 Log(CONNECTION,
3204 "eventSTADisconnectedHandler: "
3205 "ARDUINO_EVENT_WIFI_STA_DISCONNECTED\n");
3206 this->stationScan.stationAttemptOver();
3207 this->droppedConnectionCallbacks.execute(0, true);
3208 this->stationScan.stationDown();
3209 // Handle station disconnect completion after callbacks
3211 this->semaphoreGive();
3212 }
3213 },
3214#if ESP_ARDUINO_VERSION_MAJOR >= 2
3215 WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
3216#else
3217 WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED);
3218#endif
3219
3220 eventSTAGotIPHandler = WiFi.onEvent(
3221 [this](WiFiEvent_t event, WiFiEventInfo_t info) {
3222 if (this->semaphoreTake()) {
3223 Log(CONNECTION,
3224 "eventSTAGotIPHandler: ARDUINO_EVENT_WIFI_STA_GOT_IP\n");
3225 this->stationScan.stationAttemptOver();
3226 this->stationScan.stationUp();
3227 this->tcpConnect(); // Connect to TCP port
3229 this->semaphoreGive();
3230 }
3231 },
3232#if ESP_ARDUINO_VERSION_MAJOR >= 2
3233 WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
3234#else
3235 WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP);
3236#endif
3237
3238#elif defined(ESP8266)
3239 eventSTAConnectedHandler = WiFi.onStationModeConnected(
3240 [&](const WiFiEventStationModeConnected& event) {
3241 // Log(CONNECTION, "Event: Station Mode Connected to \"%s\"\n",
3242 // event.ssid.c_str());
3243 Log(CONNECTION, "Event: Station Mode Connected\n");
3244 });
3245
3246 eventSTADisconnectedHandler = WiFi.onStationModeDisconnected(
3247 [&](const WiFiEventStationModeDisconnected& event) {
3248 Log(CONNECTION, "Event: Station Mode Disconnected\n");
3249 this->stationScan.stationAttemptOver();
3250 this->droppedConnectionCallbacks.execute(0, true);
3251 this->stationScan.stationDown();
3252 // Handle station disconnect completion after callbacks
3254 });
3255
3256 eventSTAGotIPHandler =
3257 WiFi.onStationModeGotIP([&](const WiFiEventStationModeGotIP& event) {
3258 Log(CONNECTION,
3259 "Event: Station Mode Got IP (IP: %s Mask: %s Gateway: %s)\n",
3260 event.ip.toString().c_str(), event.mask.toString().c_str(),
3261 event.gw.toString().c_str());
3262 this->stationScan.stationAttemptOver();
3263 this->stationScan.stationUp();
3264 this->tcpConnect(); // Connect to TCP port
3266 });
3267#endif // ESP32
3268 return;
3269 }
3270
3271#ifdef ESP32
3272 // Event ids the core hands back from onEvent(); stop() removes all four.
3273 // 0 is never issued (the core's ids start at 1 and 0 means "no
3274 // callback"), so removeEvent(0) is a no-op on a node stopped before
3275 // init() rather than a walk of the core's callback list for garbage.
3276 WiFiEventId_t eventScanDoneHandler = 0;
3277 WiFiEventId_t eventSTAStartHandler = 0;
3278 WiFiEventId_t eventSTADisconnectedHandler = 0;
3279 WiFiEventId_t eventSTAGotIPHandler = 0;
3280#elif defined(ESP8266)
3281 WiFiEventHandler eventSTAConnectedHandler;
3282 WiFiEventHandler eventSTADisconnectedHandler;
3283 WiFiEventHandler eventSTAGotIPHandler;
3284#endif // ESP8266
3285 // Null until tcpServerInit() creates it. Every reader checks for null
3286 // first, and one of them (tcpServerInit() itself, since 2.1.0) runs before
3287 // anything has assigned it -- on an uninitialised pointer that check
3288 // passed on garbage and the delete that followed crashed the node before
3289 // it served a single connection (#466).
3291 std::shared_ptr<Task> bridgeStatusTask;
3292 // millis() of the last status broadcast, periodic or brought forward by a
3293 // topology change; the latter is held to one every five seconds.
3295
3296 // Station disconnect handling state
3298
3299 // Bridge failover state and configuration
3301
3303 uint32_t nodeId = 0;
3304 int8_t routerRSSI = 0;
3305 uint8_t routerChannel = 0;
3306 uint32_t uptime = 0;
3307 uint32_t freeMemory = 0;
3308 };
3309
3314 uint32_t electionTimeoutMs = 5000; // Default 5 seconds
3316 -80; // Default -80 dBm minimum for isolated elections
3318 60000; // Default 60 seconds before first election check
3320 1000; // Default min 1 second random delay
3322 3000; // Default max 3 seconds random delay
3324 // A checkForBridge() is already scheduled (a bridge stepping down, or
3325 // the role-change hold); one at a time is enough.
3328 uint32_t electionDeadline = 0;
3329 std::vector<BridgeCandidate> electionCandidates;
3330 std::function<void(bool isBridge, const TSTRING& reason)> bridgeRoleChangedCallback;
3331
3332 // Isolated bridge retry state and configuration
3335 0; // Time when retry counter can be reset
3337 false; // Flag to skip empty scan check after failed promotion
3339 5; // Max retry attempts before waiting
3340 static const uint32_t isolatedBridgeRetryIntervalMs =
3341 60000; // Retry every 60 seconds
3343 300000; // Reset counter after 5 minutes
3345 6; // Require 6 empty scans before retrying
3346 static const uint32_t ASYNC_PROMOTION_DELAY_MS =
3347 10; // Delay for async bridge promotion to allow current task to complete
3348
3349 // Multi-bridge coordination state and configuration
3350 protected:
3354 uint8_t bridgePriority = 5; // Default medium priority
3355 TSTRING bridgeRole = "secondary"; // Default role
3356 std::shared_ptr<Task> bridgeCoordinationTask;
3357 std::map<uint32_t, uint8_t> bridgePriorities; // nodeId -> priority mapping
3358 std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
3359 size_t lastSelectedBridgeIndex = 0; // For round-robin selection
3360
3361 // Bridge coordination monitoring callbacks and state
3362 // Constructed by name, not as an aggregate: with default member
3363 // initializers this is not an aggregate under gnu++11, which the ESP32
3364 // Arduino 2.x core still builds with, and `= {priority, role, load,
3365 // millis()}` below needs a constructor to land on there.
3367 uint8_t priority = 0;
3369 uint8_t load = 0;
3370 uint32_t lastSeen = 0;
3372 BridgeCoordinationState(uint8_t priority_, const TSTRING& role_,
3373 uint8_t load_, uint32_t lastSeen_)
3374 : priority(priority_), role(role_), load(load_), lastSeen(lastSeen_) {}
3375 };
3376 std::map<uint32_t, BridgeCoordinationState> lastBridgeCoordinationState;
3379 std::shared_ptr<Task> bridgeLostDetectionTask;
3380
3381 // Shared gateway mode state and configuration
3384 std::shared_ptr<Task> _sharedGatewayMonitorTask;
3387 static const uint8_t MAX_ROUTER_RECONNECT_ATTEMPTS = 10;
3388 static const uint32_t ROUTER_RECONNECT_BASE_INTERVAL =
3389 5000; // 5 seconds base interval
3390 static const uint32_t ROUTER_RECONNECT_MAX_INTERVAL =
3391 300000; // 5 minutes max interval
3393 30; // Router connection timeout
3394 static const uint8_t MIN_WIFI_CHANNEL = 1;
3395 static const uint8_t MAX_WIFI_CHANNEL =
3396 14; // Support channels 1-14 for regions that allow it
3397
3405 using namespace logger;
3406
3407 if (!_sharedGatewayMode) {
3408 return;
3409 }
3410
3411 Log(STARTUP,
3412 "initSharedGatewayMonitoring(): Setting up router connection "
3413 "monitoring\n");
3414
3415 // Add callback for router disconnection in shared gateway mode
3417 [this](uint32_t nodeId, bool station) {
3418 if (station && _sharedGatewayMode) {
3419 Log(CONNECTION,
3420 "Router disconnected in shared gateway mode, scheduling "
3421 "reconnection\n");
3423 }
3424 });
3425
3426 // Create periodic monitoring task
3428 this->addTask(_sharedGatewayConfig.internetCheckInterval, TASK_FOREVER,
3429 [this]() { monitorRouterConnection(); });
3430
3431 Log(STARTUP, "Router connection monitoring enabled (interval: %u ms)\n",
3433 }
3434
3441 using namespace logger;
3442
3443 if (!_sharedGatewayMode) {
3444 return;
3445 }
3446
3447 bool isConnected = (WiFi.status() == WL_CONNECTED) &&
3448 (WiFi.localIP() != IPAddress(0, 0, 0, 0));
3449
3450 if (!isConnected) {
3451 Log(CONNECTION,
3452 "monitorRouterConnection(): Router connection lost, triggering "
3453 "reconnect\n");
3455 } else {
3456 // Connection is healthy, reset reconnect attempts
3458
3459 // Log periodic status
3460 Log(GENERAL,
3461 "monitorRouterConnection(): Router connected (RSSI: %d dBm, IP: "
3462 "%s)\n",
3463 WiFi.RSSI(), WiFi.localIP().toString().c_str());
3464 }
3465 }
3466
3471 using namespace logger;
3472
3473 if (!_sharedGatewayMode) {
3474 return;
3475 }
3476
3477 // Don't schedule if already connected
3478 if (WiFi.status() == WL_CONNECTED) {
3479 return;
3480 }
3481
3482 // Limit reconnection attempts
3484 Log(ERROR,
3485 "scheduleRouterReconnect(): Max reconnection attempts reached (%d)\n",
3487 Log(ERROR,
3488 "Router reconnection suspended. Manual intervention may be "
3489 "required.\n");
3490 return;
3491 }
3492
3493 // Calculate delay with exponential backoff, preventing overflow
3494 // Limit shift amount to prevent overflow (5000 * 2^6 = 320000 is safe)
3495 uint8_t shiftAmount =
3497 uint32_t delay = ROUTER_RECONNECT_BASE_INTERVAL * (1UL << shiftAmount);
3500
3501 // Don't reconnect too frequently
3502 uint32_t now = millis();
3503 if (now - _lastRouterReconnectAttempt < delay) {
3504 return;
3505 }
3506
3509
3510 Log(CONNECTION,
3511 "scheduleRouterReconnect(): Attempting reconnection (attempt %d/%d, "
3512 "delay %u ms)\n",
3514
3515 // Schedule reconnection
3516 this->addTask(delay, TASK_ONCE, [this]() { attemptRouterReconnect(); });
3517 }
3518
3523 using namespace logger;
3524
3525 if (!_sharedGatewayMode) {
3526 return;
3527 }
3528
3529 // Check if already connected
3530 if (WiFi.status() == WL_CONNECTED) {
3531 Log(CONNECTION,
3532 "attemptRouterReconnect(): Already connected to router\n");
3534 return;
3535 }
3536
3537 Log(CONNECTION, "attemptRouterReconnect(): Reconnecting to router %s...\n",
3539
3540 // Use stationManual to reconnect (port 0 means no TCP mesh connection to
3541 // router)
3544 }
3545};
3546} // namespace wifi
3547}; // namespace painlessmesh
3548
3549#endif
3550
3551#endif
boost::asio::ip::address IPAddress
Definition asynctcp.hpp:29
Definition asynctcp.hpp:31
Definition asynctcp.hpp:217
Definition painlessMeshSTA.h:23
void noteEverRooted()
Definition painlessMeshSTA.h:55
void init(painlessmesh::wifi::Mesh *pMesh, TSTRING ssid, TSTRING password, uint16_t port, uint8_t channel, bool hidden)
Definition painlessMeshSTA.cpp:25
bool droppedByMove() const
Definition painlessMeshSTA.h:77
bool followBridgeChannel(uint8_t targetChannel)
Definition painlessMeshSTA.cpp:854
bool stationLinkUp
Definition painlessMeshSTA.h:74
Task task
Definition painlessMeshSTA.h:25
IPAddress manualIP
Definition painlessMeshSTA.h:139
void scanDone()
Definition painlessMeshSTA.cpp:192
void noteRooted(uint8_t channel)
Definition painlessMeshSTA.h:48
void stationAttemptOver()
Definition painlessMeshSTA.h:61
uint16_t getConsecutiveEmptyScans() const
Definition painlessMeshSTA.h:108
void yieldConnectToAP()
Definition painlessMeshSTA.h:88
void stationUp()
Definition painlessMeshSTA.h:65
void redetectOnNextScan()
Definition painlessMeshSTA.h:83
bool atHome() const
Definition painlessMeshSTA.h:85
void stationDown()
Definition painlessMeshSTA.h:73
uint16_t port
Definition painlessMeshSTA.h:130
bool manual
Definition painlessMeshSTA.h:138
static const uint16_t EMPTY_SCAN_THRESHOLD
Definition painlessMeshSTA.h:232
void blockNodeAfterTCPFailure(uint32_t nodeId, uint32_t blockDurationMs=60000)
Definition painlessMeshSTA.cpp:483
Definition mesh.hpp:287
void stop()
Definition mesh.hpp:516
std::list< uint32_t > getNodeList(bool includeSelf=false)
Definition mesh.hpp:3094
callback::List< uint32_t > newConnectionCallbacks
Definition mesh.hpp:4094
void sendGatewayAck(const gateway::GatewayDataPackage &request, bool success, uint16_t httpStatus, const TSTRING &error, std::shared_ptr< Connection > ingressConnection=nullptr, const TSTRING &response=TSTRING(), int8_t retryable=-1, uint32_t retryAfterMs=0)
Definition mesh.hpp:2138
uint32_t bridgeStatusIntervalMs
Definition mesh.hpp:4148
bool diagnosticsEnabled
Definition mesh.hpp:4166
bool isConnected(uint32_t nodeId)
Definition mesh.hpp:3085
void configureInternetHealthCheck(const gateway::SharedGatewayConfig &config)
Definition mesh.hpp:1601
std::vector< BridgeInfo > getBridges()
Definition mesh.hpp:1087
bool sendBroadcast(TSTRING msg, bool includeSelf=false)
Definition mesh.hpp:686
std::vector< ElectionRecord > electionHistory
Definition mesh.hpp:4167
bool isInternetHealthCheckEnabled() const
Definition mesh.hpp:1705
void setRoot(bool on=true)
Definition mesh.hpp:492
uint32_t bridgeTimeoutMs
Definition mesh.hpp:4149
bool bridgeStatusBroadcastEnabled
Definition mesh.hpp:4150
Scheduler * mScheduler
Definition mesh.hpp:4116
callback::List< uint32_t > changedConnectionCallbacks
Definition mesh.hpp:4096
uint32_t getPrimaryGateway()
Definition mesh.hpp:1283
void setScheduler(Scheduler *baseScheduler)
Definition mesh.hpp:4009
void onBridgeStatusChanged(bridgeStatusChangedCallback_t onBridgeStatusChanged)
Definition mesh.hpp:1003
bool capacityCheck(uint32_t freeHeap, uint32_t floor)
Definition mesh.hpp:1221
bool hasActiveMeshConnections()
Definition mesh.hpp:1168
void updateBridgeStatus(uint32_t bridgeNodeId, bool internetConnected, int8_t routerRSSI, uint8_t routerChannel, uint32_t uptime, TSTRING gatewayIP, uint32_t timestamp)
Definition mesh.hpp:2582
void setContainsRoot(bool on=true)
Definition mesh.hpp:501
callback::List< uint32_t, bool > droppedConnectionCallbacks
Definition mesh.hpp:4095
void enableInternetHealthCheck()
Definition mesh.hpp:1661
painlessmesh::tcp::TcpRetryConfig getTcpRetryConfig() const
Definition mesh.hpp:2015
bool shouldContainRoot
Is the node a root node.
Definition mesh.hpp:4113
bool semaphoreTake()
Definition mesh.hpp:4127
void semaphoreGive()
Definition mesh.hpp:4140
bool isBridge()
Definition mesh.hpp:1183
bool checkInternetNow()
Definition mesh.hpp:1643
std::shared_ptr< Task > addTask(unsigned long aInterval, long aIterations, std::function< void()> aCallback)
Definition mesh.hpp:3890
void init(uint32_t id)
Definition mesh.hpp:289
bool hasInternetConnection()
Definition mesh.hpp:1046
int execute(Args... args)
Definition callback.hpp:20
void push_back(std::function< void(Args...)> func)
Definition callback.hpp:45
Removes HTTP/1.1 chunked transfer framing from a body read a byte at a time, and passes the content o...
Definition gateway.hpp:1282
bool add(char c)
Definition gateway.hpp:1288
Gateway Data Package for routing Internet requests through mesh.
Definition gateway.hpp:815
A few hosts that recently failed to resolve, and when.
Definition gateway.hpp:1531
bool isFailing(const TSTRING &host, uint32_t nowMs, uint32_t *ageMs=nullptr)
Definition gateway.hpp:1558
void remember(const TSTRING &host, uint32_t nowMs, uint32_t ttlMs=GATEWAY_DNS_NEGATIVE_TTL_MS)
Definition gateway.hpp:1538
The start and the end of a response body, read a byte at a time.
Definition gateway.hpp:1228
bool add(char c)
Definition gateway.hpp:1231
TSTRING text() const
Definition gateway.hpp:1252
uint32_t nodeId
Definition layout.hpp:125
protocol::NodeTree asNodeTree()
Definition layout.hpp:115
Definition logger.hpp:46
uint32_t getNodeTime()
Definition ntp.hpp:43
TSTRING role
Definition plugin.hpp:120
uint8_t priority
Definition plugin.hpp:119
uint8_t load
Definition plugin.hpp:122
uint32_t from
Definition plugin.hpp:71
callback::MeshPackageCallbackList< T > callbackList
Definition plugin.hpp:307
Definition protocol.hpp:481
T to()
Definition protocol.hpp:743
Definition wifi.hpp:36
BridgeSelectionStrategy
Definition wifi.hpp:39
@ PRIORITY_BASED
Definition wifi.hpp:40
@ BEST_SIGNAL
Definition wifi.hpp:42
@ ROUND_ROBIN
Definition wifi.hpp:41
bool _sharedGatewayMode
Definition wifi.hpp:3382
int8_t scanRouterSignalStrength(TSTRING routerSSID, uint8_t *routerChannel=nullptr)
Definition wifi.hpp:1898
bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword, TSTRING routerSSID, TSTRING routerPassword, Scheduler *baseScheduler, uint16_t port, uint8_t priority)
Definition wifi.hpp:552
TSTRING _meshPassword
Definition wifi.hpp:1558
bool isSharedGatewayMode() const
Definition wifi.hpp:742
uint32_t electionRandomDelayMinMs
Definition wifi.hpp:3319
std::shared_ptr< Task > _sharedGatewayMonitorTask
Definition wifi.hpp:3384
void init(uint32_t id)
Definition wifi.hpp:1571
void attemptRouterReconnect()
Definition wifi.hpp:3522
IPAddress _apIp
Definition wifi.hpp:1564
void initBridgeCoordination()
Definition wifi.hpp:1738
static const uint16_t ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD
Definition wifi.hpp:3344
StationScan stationScan
Definition wifi.hpp:1565
bool multiBridgeEnabled
Definition wifi.hpp:3351
size_t lastSelectedBridgeIndex
Definition wifi.hpp:3359
void evaluateElection()
Definition wifi.hpp:2098
void setMinimumBridgeRSSI(int8_t minRSSI)
Definition wifi.hpp:1090
uint8_t _meshChannel
Definition wifi.hpp:1559
uint8_t _isolatedBridgeRetryAttempts
Definition wifi.hpp:3333
static constexpr uint8_t TCP_STATE_LISTEN
Definition wifi.hpp:936
void eventHandleInit()
Definition wifi.hpp:3120
void setElectionRandomDelay(uint32_t minMs, uint32_t maxMs)
Definition wifi.hpp:1123
static const uint8_t MIN_WIFI_CHANNEL
Definition wifi.hpp:3394
void initGatewayInternetHandler()
Definition wifi.hpp:2838
static const uint32_t ROUTER_RECONNECT_MAX_INTERVAL
Definition wifi.hpp:3390
void blockNodeAfterTCPFailure(IPAddress ip, uint32_t blockDurationMs=painlessmesh::tcp::TCP_FAILURE_BLOCK_DURATION_MS)
Definition wifi.hpp:1017
void onBridgeCoordination(std::function< void(const plugin::BridgeCoordinationPackage &, uint32_t)> callback)
Definition wifi.hpp:1152
bool isMultiBridgeEnabled() const
Definition wifi.hpp:1439
std::shared_ptr< Task > bridgeLostDetectionTask
Definition wifi.hpp:3379
uint32_t electionStartupDelayMs
Definition wifi.hpp:3317
ElectionState electionState
Definition wifi.hpp:3327
uint32_t electionTimeoutMs
Definition wifi.hpp:3314
gateway::SharedGatewayConfig _sharedGatewayConfig
Definition wifi.hpp:3383
AsyncServer * _tcpListener
Definition wifi.hpp:3290
bool _pendingStationReconnect
Definition wifi.hpp:3297
void setMaxBridges(uint8_t maxBridges)
Definition wifi.hpp:1295
void sendBridgeStatus(bool leaving=false)
Definition wifi.hpp:2490
uint8_t maxConcurrentBridges
Definition wifi.hpp:3353
std::vector< uint32_t > getActiveBridges()
Definition wifi.hpp:1308
uint8_t bridgePriority
Definition wifi.hpp:3354
bool tcpListening()
Definition wifi.hpp:925
std::function< void(const plugin::BridgeCoordinationPackage &, uint32_t, TSTRING)> bridgeCoordinationChangedCallback
Definition wifi.hpp:3378
TSTRING routerPassword
Definition wifi.hpp:3313
void onBridgeCoordinationChanged(std::function< void(const plugin::BridgeCoordinationPackage &, uint32_t, TSTRING)> callback)
Definition wifi.hpp:1220
void promoteToBridge(uint8_t routerChannel)
Definition wifi.hpp:2223
void probeUplinkAfterAssociation()
Definition wifi.hpp:3115
bool routerCredentialsConfigured
Definition wifi.hpp:3311
static const uint32_t ROUTER_RECONNECT_BASE_INTERVAL
Definition wifi.hpp:3388
uint32_t _lastBridgeStatusBroadcast
Definition wifi.hpp:3294
uint32_t lastRoleChangeTime
Definition wifi.hpp:3323
void enableBridgeFailover(bool enabled)
Definition wifi.hpp:1056
uint32_t electionRandomDelayMaxMs
Definition wifi.hpp:3321
void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI, uint8_t routerChannel, uint32_t uptime, uint32_t freeMemory)
Definition wifi.hpp:2451
void startBridgeElection()
Definition wifi.hpp:1965
std::function< void(const plugin::BridgeCoordinationPackage &, uint32_t)> bridgeCoordinationCallback
Definition wifi.hpp:3377
void setBridgeSelectionStrategy(BridgeSelectionStrategy strategy)
Definition wifi.hpp:1284
void enableMultiBridge(bool enabled)
Definition wifi.hpp:1268
void initSharedGatewayMonitoring()
Definition wifi.hpp:3404
int8_t minimumBridgeRSSI
Definition wifi.hpp:3315
std::function< void(bool isBridge, const TSTRING &reason)> bridgeRoleChangedCallback
Definition wifi.hpp:3330
uint8_t _routerReconnectAttempts
Definition wifi.hpp:3386
static const uint32_t isolatedBridgeRetryResetIntervalMs
Definition wifi.hpp:3342
void initStation()
Definition wifi.hpp:792
std::shared_ptr< Task > bridgeCoordinationTask
Definition wifi.hpp:3356
IPAddress getAPIP()
Definition wifi.hpp:1045
static const uint8_t MAX_WIFI_CHANNEL
Definition wifi.hpp:3395
bool hasActualInternetAccess()
Definition wifi.hpp:2582
static const uint32_t ASYNC_PROMOTION_DELAY_MS
Definition wifi.hpp:3346
void scheduleRouterReconnect()
Definition wifi.hpp:3470
gateway::NegativeDnsCache negativeDns
Definition wifi.hpp:2759
void setElectionTimeout(uint32_t timeoutMs)
Definition wifi.hpp:1079
TSTRING _meshSSID
Definition wifi.hpp:1557
std::shared_ptr< Task > bridgeStatusTask
Definition wifi.hpp:3291
ElectionState
Definition wifi.hpp:3300
@ ELECTION_COLLECTING
Definition wifi.hpp:3300
@ ELECTION_IDLE
Definition wifi.hpp:3300
@ ELECTION_SCANNING
Definition wifi.hpp:3300
bool detectCaptivePortal()
Definition wifi.hpp:2679
void checkForBridge()
Definition wifi.hpp:1445
bool hasInternetConnection()
Definition wifi.hpp:1355
void init(TSTRING ssid, TSTRING password, Scheduler *baseScheduler, uint16_t port=5555, WiFiMode_t connectMode=WIFI_AP_STA, uint8_t channel=1, uint8_t hidden=0, uint8_t maxconn=MAX_CONN, TSTRING stationSSID="", TSTRING stationPassword="")
Definition wifi.hpp:355
bool _isolatedRetryPending
Definition wifi.hpp:3336
void init(TSTRING ssid, TSTRING password, uint16_t port=5555, WiFiMode_t connectMode=WIFI_AP_STA, uint8_t channel=1, uint8_t hidden=0, uint8_t maxconn=MAX_CONN, TSTRING stationSSID="", TSTRING stationPassword="")
Definition wifi.hpp:62
uint8_t _meshHidden
Definition wifi.hpp:1560
const gateway::SharedGatewayConfig & getSharedGatewayConfig() const
Definition wifi.hpp:749
uint32_t electionDeadline
Definition wifi.hpp:3328
void tcpServerInit()
Definition wifi.hpp:880
static const uint8_t MAX_ROUTER_RECONNECT_ATTEMPTS
Definition wifi.hpp:3387
uint16_t _meshPort
Definition wifi.hpp:1562
void initBridgeStatusBroadcast()
Definition wifi.hpp:1598
uint32_t _lastRouterReconnectAttempt
Definition wifi.hpp:3385
bool initAsSharedGateway(TSTRING meshPrefix, TSTRING meshPassword, TSTRING routerSSID, TSTRING routerPassword, Scheduler *userScheduler, uint16_t port=5555, gateway::SharedGatewayConfig config=gateway::SharedGatewayConfig())
Definition wifi.hpp:611
void stop()
Definition wifi.hpp:1514
void tcpConnect()
Definition wifi.hpp:961
void sendBridgeCoordination()
Definition wifi.hpp:1844
std::map< uint32_t, BridgeCoordinationState > lastBridgeCoordinationState
Definition wifi.hpp:3376
void setRouterCredentials(TSTRING ssid, TSTRING password)
Definition wifi.hpp:1068
static const int ROUTER_CONNECTION_TIMEOUT_SECONDS
Definition wifi.hpp:3392
void setElectionStartupDelay(uint32_t delayMs)
Definition wifi.hpp:1106
bool bridgeFailoverEnabled
Definition wifi.hpp:3310
void handleStationDisconnectComplete()
Definition wifi.hpp:871
bool attemptIsolatedBridgePromotion()
Definition wifi.hpp:2351
void checkForBridgeSoon()
Definition wifi.hpp:1502
void apInit(uint32_t nodeId)
Definition wifi.hpp:1573
uint32_t _isolatedBridgeRetryResetTime
Definition wifi.hpp:3334
IPAddress getStationIP()
Definition wifi.hpp:1044
uint32_t getRecommendedBridge()
Definition wifi.hpp:1378
void stationManual(TSTRING ssid, TSTRING password, uint16_t port=0, IPAddress remote_ip=IPAddress(0, 0, 0, 0))
Definition wifi.hpp:761
bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword, TSTRING routerSSID, TSTRING routerPassword, Scheduler *baseScheduler, uint16_t port=5555)
Definition wifi.hpp:385
static const uint32_t isolatedBridgeRetryIntervalMs
Definition wifi.hpp:3340
static const uint8_t MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS
Definition wifi.hpp:3338
std::vector< BridgeCandidate > electionCandidates
Definition wifi.hpp:3329
std::vector< uint32_t > knownBridgePeers
Definition wifi.hpp:3358
TSTRING bridgeRole
Definition wifi.hpp:3355
bool bridgeCheckPending
Definition wifi.hpp:3326
bool setHostname(const char *hostname)
Definition wifi.hpp:1033
void monitorRouterConnection()
Definition wifi.hpp:3440
BridgeSelectionStrategy bridgeSelectionStrategy
Definition wifi.hpp:3352
TSTRING routerSSID
Definition wifi.hpp:3312
uint8_t _meshMaxConn
Definition wifi.hpp:1561
void onBridgeRoleChanged(std::function< void(bool isBridge, const TSTRING &reason)> callback)
Definition wifi.hpp:1135
std::map< uint32_t, uint8_t > bridgePriorities
Definition wifi.hpp:3357
void init(Scheduler *scheduler, uint32_t id)
Definition wifi.hpp:1567
#define MAX_CONN
Definition configuration.hpp:153
String TSTRING
Definition configuration.hpp:132
Shared Gateway Mode configuration for painlessMesh.
#define GATEWAY_HTTP_TIMEOUT_MS
Definition gateway.hpp:1847
#define GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS
Definition gateway.hpp:1853
#define GATEWAY_CONNECTIVITY_CACHE_MS
Definition gateway.hpp:1875
#define GATEWAY_DNS_TIMEOUT_MS
Definition gateway.hpp:1865
uint32_t parseRetryAfterMs(const TSTRING &value)
The delay a Retry-After header asks for, in milliseconds.
Definition gateway.hpp:1388
TSTRING summarizeResponseBody(const TSTRING &body, size_t maxLen=GATEWAY_RESPONSE_REASON_MAX)
Reduce a response body to one line fit for a log or an error string.
Definition gateway.hpp:1179
constexpr bool isValidMeshChannel(uint8_t channel)
Definition gateway.hpp:52
HttpRequestOutcome classifyHttpResult(int rawCode, const TSTRING &body=TSTRING())
Classify an HTTPClient result for the gateway acknowledgment.
Definition gateway.hpp:1451
constexpr bool shouldFollowBridgeChannel(uint32_t localNodeId, uint32_t electedBridgeId, uint8_t currentChannel, uint8_t announcedChannel)
Definition gateway.hpp:57
bool looksLikeIpLiteral(const TSTRING &host)
Definition gateway.hpp:1515
TSTRING requestIdFor(uint32_t originNode, uint32_t messageId, uint32_t requestNonce=0)
The identifier the gateway sends with a request, as both X-Request-Id and Idempotency-Key.
Definition gateway.hpp:1417
bool transferEncodingIsChunked(const TSTRING &value)
Definition gateway.hpp:1371
TSTRING hostFromUrl(const TSTRING &url)
The host of an http(s) URL, without scheme, port, path or query.
Definition gateway.hpp:1497
size_t refreshPeerWatchdogs(T &mesh, unsigned long stalledMs)
Give every peer's running watchdog back the time a blocking call hid.
Definition gateway.hpp:1983
bool isRooted(protocol::NodeTree nodeTree)
Definition layout.hpp:251
@ GENERAL
Definition logger.hpp:26
constexpr int BRIDGE_ELECTION
Definition protocol.hpp:55
constexpr int BRIDGE_TAKEOVER
Definition protocol.hpp:56
constexpr int GATEWAY_DATA
Definition protocol.hpp:61
constexpr int BRIDGE_STATUS
Definition protocol.hpp:54
uint32_t decodeNodeIdFromIP(IPAddress ip)
Definition tcp.hpp:127
uint32_t encodeNodeId(const uint8_t *hwaddr)
Definition tcp.hpp:111
Definition wifi.hpp:34
Definition mesh.hpp:247
uint32_t candidateCount
Definition mesh.hpp:251
uint32_t timestamp
Definition mesh.hpp:248
uint32_t winnerNodeId
Definition mesh.hpp:249
TSTRING reason
Definition mesh.hpp:252
int8_t winnerRSSI
Definition mesh.hpp:250
Configuration structure for Shared Gateway Mode.
Definition gateway.hpp:162
uint32_t internetCheckInterval
Interval between Internet connectivity checks in milliseconds.
Definition gateway.hpp:201
TSTRING routerSSID
Router SSID to connect for Internet access.
Definition gateway.hpp:180
bool enabled
Enable or disable shared gateway functionality.
Definition gateway.hpp:172
TSTRING routerPassword
Router password for authentication.
Definition gateway.hpp:188
uint32_t nodeId
Definition wifi.hpp:3303
uint32_t freeMemory
Definition wifi.hpp:3307
uint8_t routerChannel
Definition wifi.hpp:3305
int8_t routerRSSI
Definition wifi.hpp:3304
uint32_t uptime
Definition wifi.hpp:3306
uint8_t priority
Definition wifi.hpp:3367
uint32_t lastSeen
Definition wifi.hpp:3370
BridgeCoordinationState(uint8_t priority_, const TSTRING &role_, uint8_t load_, uint32_t lastSeen_)
Definition wifi.hpp:3372
painlessmesh::logger::LogClass Log
Definition wifi.cpp:2