@alteriom/painlessmesh 1.9.19 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +168 -0
- package/README.md +102 -63
- package/RELEASE_GUIDE.md +147 -8
- package/examples/alteriom/README.md +4 -4
- package/examples/alteriom/alteriom_custom_package_template.hpp +320 -0
- package/examples/alteriom/alteriom_sensor_package.hpp +1 -1
- package/examples/alteriom/mppt_example/alteriom_mppt_example.ino +208 -0
- package/examples/bridge_failover/bridge_failover.ino +17 -0
- package/examples/sendToInternet/CMakeLists.txt +54 -0
- package/examples/sendToInternet/PC_NODE_README.md +517 -0
- package/examples/sendToInternet/README.md +39 -1
- package/examples/sendToInternet/build.sh +153 -0
- package/examples/sendToInternet/mock_server_test.ino +361 -0
- package/examples/sendToInternet/pc_mesh_node.cpp +361 -0
- package/examples/tcpRetryConfig/README.md +110 -0
- package/examples/tcpRetryConfig/platformio.ini +26 -0
- package/examples/tcpRetryConfig/tcpRetryConfig.ino +154 -0
- package/keywords.txt +3 -0
- package/library.json +4 -1
- package/library.properties +1 -1
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +6 -14
- package/src/arduino/wifi.hpp +352 -114
- package/src/connection.cpp +10 -0
- package/src/painlessMesh.h +2 -15
- package/src/painlessTaskOptions.h +9 -0
- package/src/painlessmesh/buffer.hpp +4 -1
- package/src/painlessmesh/configuration.hpp +13 -2
- package/src/painlessmesh/connection.hpp +36 -21
- package/src/painlessmesh/gateway.hpp +0 -1061
- package/src/painlessmesh/mesh.hpp +102 -107
- package/src/painlessmesh/message_queue.hpp +25 -15
- package/src/painlessmesh/metrics.hpp +2 -262
- package/src/painlessmesh/plugin.hpp +27 -5
- package/src/painlessmesh/tcp.hpp +158 -29
- package/src/painlessmesh/validation.hpp +0 -143
- package/docs/README.md +0 -132
- package/docs/alteriom/overview.md +0 -531
- package/docs/api/core-api.md +0 -607
- package/docs/api/shared-gateway.md +0 -1207
- package/docs/architecture/mesh-architecture.md +0 -399
- package/docs/architecture/plugin-system.md +0 -517
- package/docs/getting-started/arduino-manual-install.md +0 -313
- package/docs/getting-started/first-mesh.md +0 -410
- package/docs/getting-started/installation.md +0 -275
- package/docs/getting-started/quickstart.md +0 -158
- package/docs/troubleshooting/common-issues.md +0 -679
- package/docs/troubleshooting/debugging.md +0 -455
- package/docs/troubleshooting/external-device-connection.md +0 -283
- package/docs/troubleshooting/faq.md +0 -574
- package/docs/tutorials/basic-examples.md +0 -718
package/src/arduino/wifi.hpp
CHANGED
|
@@ -410,7 +410,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
410
410
|
// Wait for connection (with timeout)
|
|
411
411
|
int timeout = 30; // 30 seconds timeout
|
|
412
412
|
while (WiFi.status() != WL_CONNECTED && timeout > 0) {
|
|
413
|
-
|
|
413
|
+
// Allow event loop processing during hardware settling
|
|
414
|
+
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
414
415
|
timeout--;
|
|
415
416
|
Log(STARTUP, ".");
|
|
416
417
|
}
|
|
@@ -423,20 +424,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
423
424
|
// Validate channel is in valid range (1-13 for 2.4GHz)
|
|
424
425
|
if (detectedChannel < 1 || detectedChannel > 13) {
|
|
425
426
|
Log(ERROR,
|
|
426
|
-
"\n
|
|
427
|
+
"\n[FAIL] Invalid channel detected: %d, falling back to channel 1\n",
|
|
427
428
|
detectedChannel);
|
|
428
429
|
detectedChannel = 1;
|
|
429
430
|
} else {
|
|
430
|
-
Log(STARTUP, "\n
|
|
431
|
-
Log(STARTUP, "
|
|
431
|
+
Log(STARTUP, "\n[OK] Router connected on channel %d\n", detectedChannel);
|
|
432
|
+
Log(STARTUP, "[OK] Router IP: %s\n", WiFi.localIP().toString().c_str());
|
|
432
433
|
routerConnected = true;
|
|
433
434
|
}
|
|
434
435
|
} else {
|
|
435
|
-
Log(STARTUP, "\n
|
|
436
|
+
Log(STARTUP, "\n[WARN] Router connection unavailable during initialization\n");
|
|
436
437
|
|
|
437
438
|
// Scan for router to detect its channel even though we can't connect
|
|
438
439
|
// This minimizes channel mismatch when router becomes available later
|
|
439
|
-
Log(STARTUP, "
|
|
440
|
+
Log(STARTUP, "[WARN] Scanning for router '%s' to detect channel...\n", routerSSID.c_str());
|
|
440
441
|
|
|
441
442
|
// ESP32 and ESP8266 have different scanNetworks signatures
|
|
442
443
|
#ifdef ESP32
|
|
@@ -451,7 +452,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
451
452
|
uint8_t scannedChannel = WiFi.channel(i);
|
|
452
453
|
if (scannedChannel >= 1 && scannedChannel <= 13) {
|
|
453
454
|
detectedChannel = scannedChannel;
|
|
454
|
-
Log(STARTUP, "
|
|
455
|
+
Log(STARTUP, "[OK] Router found on channel %d (not connected, will retry)\n",
|
|
455
456
|
detectedChannel);
|
|
456
457
|
break;
|
|
457
458
|
}
|
|
@@ -461,11 +462,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
461
462
|
}
|
|
462
463
|
|
|
463
464
|
if (detectedChannel == 1) {
|
|
464
|
-
Log(STARTUP, "
|
|
465
|
+
Log(STARTUP, "[WARN] Router not found in scan, using default channel %d\n", detectedChannel);
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
Log(STARTUP, "
|
|
468
|
-
Log(STARTUP, "
|
|
468
|
+
Log(STARTUP, "[WARN] Proceeding with bridge setup on channel %d\n", detectedChannel);
|
|
469
|
+
Log(STARTUP, "[WARN] Bridge will retry router connection in background\n");
|
|
469
470
|
}
|
|
470
471
|
|
|
471
472
|
Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
|
|
@@ -512,9 +513,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
512
513
|
Log(STARTUP, "INFO: Router connection will be established automatically when available\n");
|
|
513
514
|
}
|
|
514
515
|
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
// Router connection will be retried automatically via stationManual
|
|
516
|
+
// Always returns true: bridge mesh functionality is active regardless of
|
|
517
|
+
// router connection status. The router connection is opportunistic.
|
|
518
518
|
return true;
|
|
519
519
|
}
|
|
520
520
|
|
|
@@ -641,7 +641,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
641
641
|
// Wait for connection with timeout (using constant for configurability)
|
|
642
642
|
int timeout = ROUTER_CONNECTION_TIMEOUT_SECONDS;
|
|
643
643
|
while (WiFi.status() != WL_CONNECTED && timeout > 0) {
|
|
644
|
-
|
|
644
|
+
// Allow event loop processing during hardware settling
|
|
645
|
+
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
645
646
|
timeout--;
|
|
646
647
|
Log(STARTUP, ".");
|
|
647
648
|
}
|
|
@@ -654,15 +655,15 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
654
655
|
if (detectedChannel < MIN_WIFI_CHANNEL ||
|
|
655
656
|
detectedChannel > MAX_WIFI_CHANNEL) {
|
|
656
657
|
Log(ERROR,
|
|
657
|
-
"\n
|
|
658
|
+
"\n[FAIL] Invalid channel detected: %d, falling back to channel 1\n",
|
|
658
659
|
detectedChannel);
|
|
659
660
|
detectedChannel = 1;
|
|
660
661
|
} else {
|
|
661
|
-
Log(STARTUP, "\n
|
|
662
|
-
Log(STARTUP, "
|
|
662
|
+
Log(STARTUP, "\n[OK] Router connected on channel %d\n", detectedChannel);
|
|
663
|
+
Log(STARTUP, "[OK] Router IP: %s\n", WiFi.localIP().toString().c_str());
|
|
663
664
|
}
|
|
664
665
|
} else {
|
|
665
|
-
Log(ERROR, "\n
|
|
666
|
+
Log(ERROR, "\n[FAIL] Failed to connect to router during channel detection\n");
|
|
666
667
|
Log(ERROR,
|
|
667
668
|
"Continuing with default channel 1, will retry router connection "
|
|
668
669
|
"later\n");
|
|
@@ -816,8 +817,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
816
817
|
* TCP Connection Retry:
|
|
817
818
|
* The TCP connection now includes automatic retry with exponential backoff.
|
|
818
819
|
* If the initial connection fails (error -14 ERR_CONN or other errors),
|
|
819
|
-
* the system will retry up to
|
|
820
|
-
* triggering a full WiFi reconnection cycle
|
|
820
|
+
* the system will retry up to the configured maxRetries times before
|
|
821
|
+
* triggering a full WiFi reconnection cycle (see setTcpRetryConfig()).
|
|
822
|
+
* This helps handle:
|
|
821
823
|
* - Timing issues where TCP server is not ready immediately
|
|
822
824
|
* - Network stack stabilization after IP acquisition
|
|
823
825
|
* - Transient network conditions
|
|
@@ -841,8 +843,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
841
843
|
// This helps prevent error -14 (ERR_CONN) by allowing the network stack
|
|
842
844
|
// and TCP server to be fully ready. The delay is added via task scheduler
|
|
843
845
|
// to avoid blocking the event loop.
|
|
846
|
+
// The delay is read at schedule time, so calling setTcpRetryConfig()
|
|
847
|
+
// while a connect is already pending affects the next attempt, not the
|
|
848
|
+
// in-flight one.
|
|
844
849
|
this->addTask(
|
|
845
|
-
|
|
850
|
+
this->getTcpRetryConfig().stabilizationDelayMs, TASK_ONCE,
|
|
846
851
|
[this, targetIP, targetPort]() {
|
|
847
852
|
// Verify WiFi is still connected after the delay
|
|
848
853
|
if (WiFi.status() != WL_CONNECTED || !WiFi.localIP()) {
|
|
@@ -998,6 +1003,123 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
998
1003
|
bridgeRoleChangedCallback = callback;
|
|
999
1004
|
}
|
|
1000
1005
|
|
|
1006
|
+
/**
|
|
1007
|
+
* Set callback for bridge coordination messages
|
|
1008
|
+
*
|
|
1009
|
+
* Called every time a coordination message (Type 613) is received from
|
|
1010
|
+
* another bridge. Useful for monitoring bridge health and status.
|
|
1011
|
+
*
|
|
1012
|
+
* For non-bridge nodes, this also registers a Type 613 handler so that
|
|
1013
|
+
* regular mesh nodes can monitor bridge coordination traffic.
|
|
1014
|
+
*
|
|
1015
|
+
* @param callback Function called with the coordination package and sender
|
|
1016
|
+
* node ID
|
|
1017
|
+
*/
|
|
1018
|
+
void onBridgeCoordination(
|
|
1019
|
+
std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t)>
|
|
1020
|
+
callback) {
|
|
1021
|
+
using namespace logger;
|
|
1022
|
+
bridgeCoordinationCallback = callback;
|
|
1023
|
+
|
|
1024
|
+
// Non-bridge nodes don't run initBridgeCoordination(), so register
|
|
1025
|
+
// a Type 613 handler here so they can still receive coordination traffic.
|
|
1026
|
+
if (!this->isBridge()) {
|
|
1027
|
+
this->callbackList.onPackage(
|
|
1028
|
+
613,
|
|
1029
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>,
|
|
1030
|
+
uint32_t) {
|
|
1031
|
+
JsonDocument doc;
|
|
1032
|
+
TSTRING str;
|
|
1033
|
+
variant.printTo(str);
|
|
1034
|
+
deserializeJson(doc, str);
|
|
1035
|
+
JsonObject obj = doc.as<JsonObject>();
|
|
1036
|
+
|
|
1037
|
+
if (obj["priority"].is<unsigned int>()) {
|
|
1038
|
+
uint32_t fromNode = obj["from"];
|
|
1039
|
+
plugin::BridgeCoordinationPackage pkg(obj);
|
|
1040
|
+
|
|
1041
|
+
if (this->bridgeCoordinationCallback) {
|
|
1042
|
+
this->bridgeCoordinationCallback(pkg, fromNode);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Change detection for non-bridge nodes
|
|
1046
|
+
if (this->bridgeCoordinationChangedCallback) {
|
|
1047
|
+
auto it = this->lastBridgeCoordinationState.find(fromNode);
|
|
1048
|
+
if (it == this->lastBridgeCoordinationState.end()) {
|
|
1049
|
+
this->lastBridgeCoordinationState[fromNode] = {
|
|
1050
|
+
pkg.priority, pkg.role, pkg.load, (uint32_t)millis()};
|
|
1051
|
+
this->bridgeCoordinationChangedCallback(pkg, fromNode,
|
|
1052
|
+
"new");
|
|
1053
|
+
} else {
|
|
1054
|
+
auto& prev = it->second;
|
|
1055
|
+
bool changed = (prev.priority != pkg.priority ||
|
|
1056
|
+
prev.role != pkg.role ||
|
|
1057
|
+
prev.load != pkg.load);
|
|
1058
|
+
prev.priority = pkg.priority;
|
|
1059
|
+
prev.role = pkg.role;
|
|
1060
|
+
prev.load = pkg.load;
|
|
1061
|
+
prev.lastSeen = (uint32_t)millis();
|
|
1062
|
+
if (changed) {
|
|
1063
|
+
this->bridgeCoordinationChangedCallback(pkg, fromNode,
|
|
1064
|
+
"updated");
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return false;
|
|
1070
|
+
});
|
|
1071
|
+
Log(GENERAL,
|
|
1072
|
+
"onBridgeCoordination(): Registered Type 613 handler for non-bridge "
|
|
1073
|
+
"node\n");
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Set callback for bridge coordination state changes
|
|
1079
|
+
*
|
|
1080
|
+
* Called when a new bridge is discovered, an existing bridge changes its
|
|
1081
|
+
* priority/role/load, or a bridge is lost (no coordination message for 60s).
|
|
1082
|
+
*
|
|
1083
|
+
* @param callback Function called with the coordination package, sender node
|
|
1084
|
+
* ID, and change type ("new", "updated", or "lost")
|
|
1085
|
+
*/
|
|
1086
|
+
void onBridgeCoordinationChanged(
|
|
1087
|
+
std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t,
|
|
1088
|
+
TSTRING)>
|
|
1089
|
+
callback) {
|
|
1090
|
+
using namespace logger;
|
|
1091
|
+
bridgeCoordinationChangedCallback = callback;
|
|
1092
|
+
|
|
1093
|
+
// Start periodic lost-detection task (every 30 seconds, check for
|
|
1094
|
+
// bridges that haven't sent a coordination message in 60 seconds)
|
|
1095
|
+
bridgeLostDetectionTask = this->addTask(
|
|
1096
|
+
30000, TASK_FOREVER, [this]() {
|
|
1097
|
+
uint32_t now = (uint32_t)millis();
|
|
1098
|
+
for (auto it = this->lastBridgeCoordinationState.begin();
|
|
1099
|
+
it != this->lastBridgeCoordinationState.end();) {
|
|
1100
|
+
if ((now - it->second.lastSeen) > 60000) {
|
|
1101
|
+
uint32_t lostNode = it->first;
|
|
1102
|
+
// Create a package with last known state for the callback
|
|
1103
|
+
plugin::BridgeCoordinationPackage pkg;
|
|
1104
|
+
pkg.from = lostNode;
|
|
1105
|
+
pkg.priority = it->second.priority;
|
|
1106
|
+
pkg.role = it->second.role;
|
|
1107
|
+
pkg.load = it->second.load;
|
|
1108
|
+
it = this->lastBridgeCoordinationState.erase(it);
|
|
1109
|
+
if (this->bridgeCoordinationChangedCallback) {
|
|
1110
|
+
this->bridgeCoordinationChangedCallback(pkg, lostNode, "lost");
|
|
1111
|
+
}
|
|
1112
|
+
} else {
|
|
1113
|
+
++it;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
Log(GENERAL,
|
|
1119
|
+
"onBridgeCoordinationChanged(): Lost detection task started "
|
|
1120
|
+
"(interval: 30s, timeout: 60s)\n");
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1001
1123
|
/**
|
|
1002
1124
|
* Enable or disable multi-bridge coordination mode
|
|
1003
1125
|
*
|
|
@@ -1175,17 +1297,6 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1175
1297
|
}
|
|
1176
1298
|
}
|
|
1177
1299
|
|
|
1178
|
-
/**
|
|
1179
|
-
* Select a specific bridge for next transmission
|
|
1180
|
-
*
|
|
1181
|
-
* This overrides the automatic bridge selection for one message.
|
|
1182
|
-
*
|
|
1183
|
-
* @param bridgeNodeId Node ID of bridge to use
|
|
1184
|
-
*/
|
|
1185
|
-
void selectBridge(uint32_t bridgeNodeId) {
|
|
1186
|
-
selectedBridgeOverride = bridgeNodeId;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
1300
|
/**
|
|
1190
1301
|
* Check if multi-bridge mode is enabled
|
|
1191
1302
|
*
|
|
@@ -1432,6 +1543,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1432
1543
|
if (peerId != this->nodeId &&
|
|
1433
1544
|
std::find(knownBridgePeers.begin(), knownBridgePeers.end(),
|
|
1434
1545
|
peerId) == knownBridgePeers.end()) {
|
|
1546
|
+
if (knownBridgePeers.size() >= 32) {
|
|
1547
|
+
knownBridgePeers.erase(knownBridgePeers.begin());
|
|
1548
|
+
}
|
|
1435
1549
|
knownBridgePeers.push_back(peerId);
|
|
1436
1550
|
}
|
|
1437
1551
|
}
|
|
@@ -1441,6 +1555,36 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1441
1555
|
"Bridge coordination from %u: priority=%d, role=%s, "
|
|
1442
1556
|
"load=%d%%\n",
|
|
1443
1557
|
fromNode, priority, role.c_str(), load);
|
|
1558
|
+
|
|
1559
|
+
// Invoke bridge coordination callback if set
|
|
1560
|
+
if (this->bridgeCoordinationCallback) {
|
|
1561
|
+
plugin::BridgeCoordinationPackage pkg(obj);
|
|
1562
|
+
this->bridgeCoordinationCallback(pkg, fromNode);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Change detection and bridgeCoordinationChangedCallback
|
|
1566
|
+
if (this->bridgeCoordinationChangedCallback) {
|
|
1567
|
+
plugin::BridgeCoordinationPackage pkg(obj);
|
|
1568
|
+
auto it = this->lastBridgeCoordinationState.find(fromNode);
|
|
1569
|
+
if (it == this->lastBridgeCoordinationState.end()) {
|
|
1570
|
+
this->lastBridgeCoordinationState[fromNode] = {
|
|
1571
|
+
priority, role, load, (uint32_t)millis()};
|
|
1572
|
+
this->bridgeCoordinationChangedCallback(pkg, fromNode, "new");
|
|
1573
|
+
} else {
|
|
1574
|
+
auto& prev = it->second;
|
|
1575
|
+
bool changed = (prev.priority != priority ||
|
|
1576
|
+
prev.role != role ||
|
|
1577
|
+
prev.load != load);
|
|
1578
|
+
prev.priority = priority;
|
|
1579
|
+
prev.role = role;
|
|
1580
|
+
prev.load = load;
|
|
1581
|
+
prev.lastSeen = (uint32_t)millis();
|
|
1582
|
+
if (changed) {
|
|
1583
|
+
this->bridgeCoordinationChangedCallback(pkg, fromNode,
|
|
1584
|
+
"updated");
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1444
1588
|
}
|
|
1445
1589
|
return false; // Don't consume the package
|
|
1446
1590
|
});
|
|
@@ -1757,7 +1901,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1757
1901
|
}
|
|
1758
1902
|
|
|
1759
1903
|
if (winner->nodeId == this->nodeId) {
|
|
1760
|
-
Log(CONNECTION, "
|
|
1904
|
+
Log(CONNECTION, "[TARGET] I WON! Promoting to bridge...\n");
|
|
1761
1905
|
promoteToBridge();
|
|
1762
1906
|
} else {
|
|
1763
1907
|
Log(CONNECTION, "Winner is node %u, remaining as regular node\n",
|
|
@@ -1818,60 +1962,57 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1818
1962
|
router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
|
|
1819
1963
|
|
|
1820
1964
|
// Give time for announcement to propagate before channel switch
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1965
|
+
// Allow event loop processing during hardware settling
|
|
1966
|
+
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
1967
|
+
Log(STARTUP, "[OK] Takeover announcement sent on channel %d\n", _meshChannel);
|
|
1968
|
+
|
|
1969
|
+
// Save current mesh configuration to restore if bridge init fails.
|
|
1970
|
+
// The copies below are captured by value into the deferred lambda so the
|
|
1971
|
+
// stop() call inside it cannot clear/mutate these members before the
|
|
1972
|
+
// reinit reads them. (The Mesh object itself outlives the lambda; this
|
|
1973
|
+
// protects against member mutation, not object lifetime.)
|
|
1974
|
+
// TODO(#373 follow-up): if the mesh owns its scheduler
|
|
1975
|
+
// (!isExternalScheduler), stop() deletes mScheduler and savedScheduler
|
|
1976
|
+
// dangles before initAsBridge() uses it. Pre-existing limitation —
|
|
1977
|
+
// bridge promotion requires a user-supplied scheduler.
|
|
1825
1978
|
uint8_t savedChannel = _meshChannel;
|
|
1979
|
+
TSTRING savedMeshSSID = _meshSSID;
|
|
1980
|
+
TSTRING savedMeshPassword = _meshPassword;
|
|
1981
|
+
TSTRING savedRouterSSID = routerSSID;
|
|
1982
|
+
TSTRING savedRouterPassword = routerPassword;
|
|
1983
|
+
Scheduler *savedScheduler = mScheduler;
|
|
1984
|
+
auto savedBridgeRoleChangedCallback = bridgeRoleChangedCallback;
|
|
1826
1985
|
|
|
1827
1986
|
// CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
|
|
1828
1987
|
// This prevents use-after-free crash when stop() clears taskList while
|
|
1829
1988
|
// evaluateElection() task is still executing
|
|
1830
1989
|
// Use minimal delay to allow current task to complete first
|
|
1831
|
-
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
1990
|
+
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
1991
|
+
[this, savedChannel, savedMeshSSID, savedMeshPassword,
|
|
1992
|
+
savedRouterSSID, savedRouterPassword, savedScheduler,
|
|
1993
|
+
savedBridgeRoleChangedCallback]() {
|
|
1832
1994
|
using namespace logger;
|
|
1833
1995
|
|
|
1834
1996
|
Log(STARTUP, "Executing bridge promotion (stop/reinit cycle)\n");
|
|
1835
1997
|
|
|
1836
1998
|
// Now reconfigure as bridge (this will switch to router's channel)
|
|
1837
1999
|
this->stop();
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
bool bridgeInitSuccess =
|
|
1841
|
-
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1842
|
-
mScheduler, _meshPort);
|
|
1843
|
-
|
|
1844
|
-
if (!bridgeInitSuccess) {
|
|
1845
|
-
Log(ERROR, "✗ Bridge promotion failed - router unreachable\n");
|
|
1846
|
-
Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
|
|
2000
|
+
// Allow event loop processing during hardware settling
|
|
2001
|
+
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
1847
2002
|
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
// Reset election state and clear candidates (consistent with normal
|
|
1853
|
-
// election completion)
|
|
1854
|
-
electionState = ELECTION_IDLE;
|
|
1855
|
-
electionCandidates.clear();
|
|
1856
|
-
|
|
1857
|
-
// Notify via callback
|
|
1858
|
-
if (bridgeRoleChangedCallback) {
|
|
1859
|
-
bridgeRoleChangedCallback(
|
|
1860
|
-
false, "Bridge promotion failed - router unreachable");
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
return;
|
|
1864
|
-
}
|
|
2003
|
+
// initAsBridge always returns true: bridge mesh functionality is active
|
|
2004
|
+
// regardless of router connection status (router connection is opportunistic)
|
|
2005
|
+
this->initAsBridge(savedMeshSSID, savedMeshPassword, savedRouterSSID,
|
|
2006
|
+
savedRouterPassword, savedScheduler, _meshPort);
|
|
1865
2007
|
|
|
1866
2008
|
lastRoleChangeTime = millis();
|
|
1867
2009
|
|
|
1868
|
-
Log(STARTUP, "
|
|
2010
|
+
Log(STARTUP, "[OK] Bridge promotion complete on channel %d\n", _meshChannel);
|
|
1869
2011
|
|
|
1870
2012
|
// Notify via callback
|
|
1871
|
-
|
|
1872
|
-
if (bridgeRoleChangedCallback) {
|
|
2013
|
+
if (savedBridgeRoleChangedCallback) {
|
|
1873
2014
|
static const TSTRING reason = "Election winner - best router signal";
|
|
1874
|
-
|
|
2015
|
+
savedBridgeRoleChangedCallback(true, reason);
|
|
1875
2016
|
}
|
|
1876
2017
|
|
|
1877
2018
|
// Note: The initial takeover announcement was already sent earlier
|
|
@@ -1944,64 +2085,52 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1944
2085
|
Log(CONNECTION,
|
|
1945
2086
|
"Scheduling stop/reinit (async to avoid task corruption)\n");
|
|
1946
2087
|
|
|
1947
|
-
// Save current mesh configuration
|
|
2088
|
+
// Save current mesh configuration. Captured by value into the deferred
|
|
2089
|
+
// lambda so stop() cannot clear/mutate these members before the reinit
|
|
2090
|
+
// reads them (see the matching comment in promoteToBridge(); same
|
|
2091
|
+
// TODO(#373 follow-up) about savedScheduler and internal schedulers
|
|
2092
|
+
// applies here).
|
|
1948
2093
|
uint8_t savedChannel = _meshChannel;
|
|
2094
|
+
TSTRING savedMeshSSID = _meshSSID;
|
|
2095
|
+
TSTRING savedMeshPassword = _meshPassword;
|
|
2096
|
+
TSTRING savedRouterSSID = routerSSID;
|
|
2097
|
+
TSTRING savedRouterPassword = routerPassword;
|
|
2098
|
+
Scheduler *savedScheduler = mScheduler;
|
|
2099
|
+
auto savedBridgeRoleChangedCallback = bridgeRoleChangedCallback;
|
|
1949
2100
|
|
|
1950
2101
|
// CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
|
|
1951
2102
|
// This prevents use-after-free crash when stop() clears taskList while
|
|
1952
2103
|
// the retry task is still executing
|
|
1953
2104
|
// Use minimal delay to allow current task to complete first
|
|
1954
|
-
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
2105
|
+
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
2106
|
+
[this, savedChannel, savedMeshSSID, savedMeshPassword,
|
|
2107
|
+
savedRouterSSID, savedRouterPassword, savedScheduler,
|
|
2108
|
+
savedBridgeRoleChangedCallback]() {
|
|
1955
2109
|
using namespace logger;
|
|
1956
2110
|
|
|
1957
2111
|
Log(CONNECTION, "Executing isolated bridge promotion (stop/reinit cycle)\n");
|
|
1958
2112
|
|
|
1959
2113
|
// Stop current mesh operations
|
|
1960
2114
|
this->stop();
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
// Attempt to initialize as bridge
|
|
1964
|
-
bool bridgeInitSuccess =
|
|
1965
|
-
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1966
|
-
mScheduler, _meshPort);
|
|
1967
|
-
|
|
1968
|
-
if (!bridgeInitSuccess) {
|
|
1969
|
-
Log(ERROR, "✗ Isolated bridge promotion failed - router unreachable\n");
|
|
1970
|
-
Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
|
|
1971
|
-
|
|
1972
|
-
// Re-initialize as regular node on the original channel
|
|
1973
|
-
this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
|
|
1974
|
-
savedChannel, _meshHidden, MAX_CONN);
|
|
1975
|
-
|
|
1976
|
-
// Re-configure router credentials for future retry attempts
|
|
1977
|
-
this->setRouterCredentials(routerSSID, routerPassword);
|
|
1978
|
-
this->enableBridgeFailover(true);
|
|
1979
|
-
|
|
1980
|
-
// Set flag to skip empty scan check on next retry attempt
|
|
1981
|
-
// since we already confirmed isolation before this failed attempt
|
|
1982
|
-
_isolatedRetryPending = true;
|
|
1983
|
-
|
|
1984
|
-
// Notify via callback
|
|
1985
|
-
if (bridgeRoleChangedCallback) {
|
|
1986
|
-
bridgeRoleChangedCallback(
|
|
1987
|
-
false, "Isolated bridge promotion failed - router unreachable");
|
|
1988
|
-
}
|
|
2115
|
+
// Allow event loop processing during hardware settling
|
|
2116
|
+
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
1989
2117
|
|
|
1990
|
-
|
|
1991
|
-
|
|
2118
|
+
// initAsBridge always returns true: bridge mesh functionality is active
|
|
2119
|
+
// regardless of router connection status (router connection is opportunistic)
|
|
2120
|
+
this->initAsBridge(savedMeshSSID, savedMeshPassword, savedRouterSSID,
|
|
2121
|
+
savedRouterPassword, savedScheduler, _meshPort);
|
|
1992
2122
|
|
|
1993
|
-
//
|
|
2123
|
+
// Reset retry counter
|
|
1994
2124
|
_isolatedBridgeRetryAttempts = 0;
|
|
1995
2125
|
lastRoleChangeTime = millis();
|
|
1996
2126
|
|
|
1997
|
-
Log(STARTUP, "
|
|
2127
|
+
Log(STARTUP, "[OK] Isolated bridge promotion complete on channel %d\n",
|
|
1998
2128
|
_meshChannel);
|
|
1999
2129
|
|
|
2000
2130
|
// Notify via callback
|
|
2001
|
-
|
|
2002
|
-
if (bridgeRoleChangedCallback) {
|
|
2131
|
+
if (savedBridgeRoleChangedCallback) {
|
|
2003
2132
|
static const TSTRING reason = "Isolated node promoted to bridge";
|
|
2004
|
-
|
|
2133
|
+
savedBridgeRoleChangedCallback(true, reason);
|
|
2005
2134
|
}
|
|
2006
2135
|
|
|
2007
2136
|
// Note: Bridge status announcement will be sent automatically by
|
|
@@ -2131,50 +2260,141 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2131
2260
|
*/
|
|
2132
2261
|
bool hasActualInternetAccess() {
|
|
2133
2262
|
using namespace logger;
|
|
2134
|
-
|
|
2263
|
+
|
|
2264
|
+
// Cache result to avoid blocking DNS/HTTP on every call
|
|
2265
|
+
static uint32_t lastCheckTime = 0;
|
|
2266
|
+
static bool lastResult = false;
|
|
2267
|
+
uint32_t now = millis();
|
|
2268
|
+
if (lastCheckTime > 0 && (now - lastCheckTime) < 60000) {
|
|
2269
|
+
return lastResult;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2135
2272
|
// First check WiFi connection
|
|
2136
2273
|
if (WiFi.status() != WL_CONNECTED) {
|
|
2274
|
+
lastCheckTime = millis();
|
|
2275
|
+
lastResult = false;
|
|
2137
2276
|
return false;
|
|
2138
2277
|
}
|
|
2139
|
-
|
|
2278
|
+
|
|
2140
2279
|
// Check if we have a valid local IP
|
|
2141
2280
|
if (WiFi.localIP() == IPAddress(0, 0, 0, 0)) {
|
|
2281
|
+
lastCheckTime = millis();
|
|
2282
|
+
lastResult = false;
|
|
2142
2283
|
return false;
|
|
2143
2284
|
}
|
|
2144
|
-
|
|
2285
|
+
|
|
2145
2286
|
// Try to resolve a well-known DNS name
|
|
2146
2287
|
// Using Google's servers as they have high availability globally
|
|
2147
2288
|
IPAddress result;
|
|
2148
|
-
|
|
2289
|
+
|
|
2149
2290
|
#if defined(ESP32) || defined(ESP8266)
|
|
2150
2291
|
// Both ESP32 and ESP8266 support WiFi.hostByName()
|
|
2151
2292
|
int dnsResult = WiFi.hostByName("www.google.com", result);
|
|
2152
|
-
|
|
2293
|
+
|
|
2153
2294
|
// Check if DNS resolution succeeded
|
|
2154
2295
|
if (dnsResult != 1) {
|
|
2155
2296
|
Log(COMMUNICATION, "hasActualInternetAccess(): DNS resolution failed (code=%d)\n", dnsResult);
|
|
2297
|
+
lastCheckTime = millis();
|
|
2298
|
+
lastResult = false;
|
|
2156
2299
|
return false;
|
|
2157
2300
|
}
|
|
2158
|
-
|
|
2301
|
+
|
|
2159
2302
|
// Additional validation: Check if resolved IP is valid
|
|
2160
2303
|
// Some ESP8266 versions may return success but set IP to 255.255.255.255 on error
|
|
2161
2304
|
if (result == IPAddress(0, 0, 0, 0) || result == IPAddress(255, 255, 255, 255)) {
|
|
2162
2305
|
TSTRING resultStr = result.toString();
|
|
2163
2306
|
Log(COMMUNICATION, "hasActualInternetAccess(): Invalid DNS result IP: %s\n", resultStr.c_str());
|
|
2307
|
+
lastCheckTime = millis();
|
|
2308
|
+
lastResult = false;
|
|
2164
2309
|
return false;
|
|
2165
2310
|
}
|
|
2166
2311
|
#else
|
|
2167
2312
|
// Other platforms: assume internet is available if WiFi connected
|
|
2168
2313
|
// (no reliable way to test without platform-specific APIs)
|
|
2314
|
+
lastCheckTime = millis();
|
|
2315
|
+
lastResult = true;
|
|
2169
2316
|
return true;
|
|
2170
2317
|
#endif
|
|
2171
|
-
|
|
2318
|
+
|
|
2172
2319
|
TSTRING resultStr = result.toString();
|
|
2173
|
-
Log(COMMUNICATION, "hasActualInternetAccess(): Internet connectivity verified (resolved to %s)\n",
|
|
2320
|
+
Log(COMMUNICATION, "hasActualInternetAccess(): Internet connectivity verified (resolved to %s)\n",
|
|
2174
2321
|
resultStr.c_str());
|
|
2322
|
+
lastCheckTime = millis();
|
|
2323
|
+
lastResult = true;
|
|
2175
2324
|
return true;
|
|
2176
2325
|
}
|
|
2177
2326
|
|
|
2327
|
+
/**
|
|
2328
|
+
* Detect captive portal by making a lightweight HTTP request
|
|
2329
|
+
*
|
|
2330
|
+
* Captive portals often allow DNS resolution but intercept HTTP requests,
|
|
2331
|
+
* returning redirects, cached responses (HTTP 203), or their own HTML.
|
|
2332
|
+
* This function makes a simple HTTP GET request to a known endpoint and
|
|
2333
|
+
* verifies the response to detect such interference.
|
|
2334
|
+
*
|
|
2335
|
+
* Test endpoint used:
|
|
2336
|
+
* - http://captive.apple.com/hotspot-detect.html - Returns "Success" (Apple standard)
|
|
2337
|
+
*
|
|
2338
|
+
* @return true if no captive portal detected, false if portal found or check fails
|
|
2339
|
+
*/
|
|
2340
|
+
bool detectCaptivePortal() {
|
|
2341
|
+
using namespace logger;
|
|
2342
|
+
|
|
2343
|
+
#if defined(ESP32) || defined(ESP8266)
|
|
2344
|
+
HTTPClient http;
|
|
2345
|
+
http.setTimeout(5000); // 5 second timeout for quick check
|
|
2346
|
+
|
|
2347
|
+
WiFiClient client;
|
|
2348
|
+
|
|
2349
|
+
// Use Apple's captive portal detection endpoint
|
|
2350
|
+
// This is a well-maintained, reliable endpoint used by iOS devices
|
|
2351
|
+
const char* testUrl = "http://captive.apple.com/hotspot-detect.html";
|
|
2352
|
+
|
|
2353
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Testing %s\n", testUrl);
|
|
2354
|
+
|
|
2355
|
+
#ifdef ESP8266
|
|
2356
|
+
if (!http.begin(client, testUrl)) {
|
|
2357
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Failed to begin HTTP client - treating as potential network restriction\n");
|
|
2358
|
+
return false; // Conservative approach: treat initialization failure as potential captive portal or network restriction
|
|
2359
|
+
}
|
|
2360
|
+
#else
|
|
2361
|
+
// ESP32
|
|
2362
|
+
if (!http.begin(testUrl)) {
|
|
2363
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Failed to begin HTTP client - treating as potential network restriction\n");
|
|
2364
|
+
return false;
|
|
2365
|
+
}
|
|
2366
|
+
#endif
|
|
2367
|
+
|
|
2368
|
+
int httpCode = http.GET();
|
|
2369
|
+
|
|
2370
|
+
if (httpCode != 200) {
|
|
2371
|
+
// Any response other than HTTP 200 indicates captive portal or network issue
|
|
2372
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Unexpected HTTP code %d (expected 200)\n", httpCode);
|
|
2373
|
+
http.end();
|
|
2374
|
+
return false;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// Check response content
|
|
2378
|
+
String response = http.getString();
|
|
2379
|
+
http.end();
|
|
2380
|
+
|
|
2381
|
+
// Verify the response contains "Success" - this is Apple's standard response
|
|
2382
|
+
// Full response: "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>"
|
|
2383
|
+
// We check for presence of "Success" to be robust against minor format variations
|
|
2384
|
+
if (response.indexOf("Success") == -1) {
|
|
2385
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Response doesn't contain 'Success', likely captive portal\n");
|
|
2386
|
+
return false;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
Log(COMMUNICATION, "detectCaptivePortal(): No captive portal detected\n");
|
|
2390
|
+
return true;
|
|
2391
|
+
|
|
2392
|
+
#else
|
|
2393
|
+
// Non-ESP platforms: can't reliably test, assume no captive portal
|
|
2394
|
+
return true;
|
|
2395
|
+
#endif
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2178
2398
|
/**
|
|
2179
2399
|
* Helper method to send gateway acknowledgment
|
|
2180
2400
|
*/
|
|
@@ -2263,6 +2483,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2263
2483
|
sendGatewayAck(pkg, false, 0, "Router has no internet access - check WAN connection");
|
|
2264
2484
|
return true; // Consume package - we handled it (with error)
|
|
2265
2485
|
}
|
|
2486
|
+
|
|
2487
|
+
// Finally, check for captive portal interference
|
|
2488
|
+
// This detects when DNS works but HTTP requests are intercepted
|
|
2489
|
+
if (!detectCaptivePortal()) {
|
|
2490
|
+
sendGatewayAck(pkg, false, 0, "Captive portal detected - requires web authentication. Check router/WiFi settings");
|
|
2491
|
+
return true; // Consume package - we handled it (with error)
|
|
2492
|
+
}
|
|
2266
2493
|
|
|
2267
2494
|
#if defined(ESP32) || defined(ESP8266)
|
|
2268
2495
|
// Make HTTP/HTTPS request
|
|
@@ -2538,9 +2765,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2538
2765
|
std::shared_ptr<Task> bridgeCoordinationTask;
|
|
2539
2766
|
std::map<uint32_t, uint8_t> bridgePriorities; // nodeId -> priority mapping
|
|
2540
2767
|
std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
|
|
2541
|
-
uint32_t selectedBridgeOverride = 0; // Manual bridge selection override
|
|
2542
2768
|
size_t lastSelectedBridgeIndex = 0; // For round-robin selection
|
|
2543
2769
|
|
|
2770
|
+
// Bridge coordination monitoring callbacks and state
|
|
2771
|
+
struct BridgeCoordinationState {
|
|
2772
|
+
uint8_t priority;
|
|
2773
|
+
TSTRING role;
|
|
2774
|
+
uint8_t load;
|
|
2775
|
+
uint32_t lastSeen;
|
|
2776
|
+
};
|
|
2777
|
+
std::map<uint32_t, BridgeCoordinationState> lastBridgeCoordinationState;
|
|
2778
|
+
std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t)> bridgeCoordinationCallback;
|
|
2779
|
+
std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t, TSTRING)> bridgeCoordinationChangedCallback;
|
|
2780
|
+
std::shared_ptr<Task> bridgeLostDetectionTask;
|
|
2781
|
+
|
|
2544
2782
|
// Shared gateway mode state and configuration
|
|
2545
2783
|
bool _sharedGatewayMode = false;
|
|
2546
2784
|
gateway::SharedGatewayConfig _sharedGatewayConfig;
|