@alteriom/painlessmesh 1.9.19 → 1.9.20
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 +47 -0
- package/README.md +82 -63
- 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/library.json +4 -1
- package/library.properties +1 -1
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +5 -13
- package/src/arduino/wifi.hpp +306 -100
- package/src/connection.cpp +10 -0
- package/src/painlessMesh.h +1 -14
- package/src/painlessmesh/connection.hpp +11 -16
- package/src/painlessmesh/gateway.hpp +0 -1061
- package/src/painlessmesh/mesh.hpp +29 -85
- package/src/painlessmesh/message_queue.hpp +1 -2
- package/src/painlessmesh/metrics.hpp +2 -262
- 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");
|
|
@@ -998,6 +999,123 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
998
999
|
bridgeRoleChangedCallback = callback;
|
|
999
1000
|
}
|
|
1000
1001
|
|
|
1002
|
+
/**
|
|
1003
|
+
* Set callback for bridge coordination messages
|
|
1004
|
+
*
|
|
1005
|
+
* Called every time a coordination message (Type 613) is received from
|
|
1006
|
+
* another bridge. Useful for monitoring bridge health and status.
|
|
1007
|
+
*
|
|
1008
|
+
* For non-bridge nodes, this also registers a Type 613 handler so that
|
|
1009
|
+
* regular mesh nodes can monitor bridge coordination traffic.
|
|
1010
|
+
*
|
|
1011
|
+
* @param callback Function called with the coordination package and sender
|
|
1012
|
+
* node ID
|
|
1013
|
+
*/
|
|
1014
|
+
void onBridgeCoordination(
|
|
1015
|
+
std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t)>
|
|
1016
|
+
callback) {
|
|
1017
|
+
using namespace logger;
|
|
1018
|
+
bridgeCoordinationCallback = callback;
|
|
1019
|
+
|
|
1020
|
+
// Non-bridge nodes don't run initBridgeCoordination(), so register
|
|
1021
|
+
// a Type 613 handler here so they can still receive coordination traffic.
|
|
1022
|
+
if (!this->isBridge()) {
|
|
1023
|
+
this->callbackList.onPackage(
|
|
1024
|
+
613,
|
|
1025
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>,
|
|
1026
|
+
uint32_t) {
|
|
1027
|
+
JsonDocument doc;
|
|
1028
|
+
TSTRING str;
|
|
1029
|
+
variant.printTo(str);
|
|
1030
|
+
deserializeJson(doc, str);
|
|
1031
|
+
JsonObject obj = doc.as<JsonObject>();
|
|
1032
|
+
|
|
1033
|
+
if (obj["priority"].is<unsigned int>()) {
|
|
1034
|
+
uint32_t fromNode = obj["from"];
|
|
1035
|
+
plugin::BridgeCoordinationPackage pkg(obj);
|
|
1036
|
+
|
|
1037
|
+
if (this->bridgeCoordinationCallback) {
|
|
1038
|
+
this->bridgeCoordinationCallback(pkg, fromNode);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Change detection for non-bridge nodes
|
|
1042
|
+
if (this->bridgeCoordinationChangedCallback) {
|
|
1043
|
+
auto it = this->lastBridgeCoordinationState.find(fromNode);
|
|
1044
|
+
if (it == this->lastBridgeCoordinationState.end()) {
|
|
1045
|
+
this->lastBridgeCoordinationState[fromNode] = {
|
|
1046
|
+
pkg.priority, pkg.role, pkg.load, (uint32_t)millis()};
|
|
1047
|
+
this->bridgeCoordinationChangedCallback(pkg, fromNode,
|
|
1048
|
+
"new");
|
|
1049
|
+
} else {
|
|
1050
|
+
auto& prev = it->second;
|
|
1051
|
+
bool changed = (prev.priority != pkg.priority ||
|
|
1052
|
+
prev.role != pkg.role ||
|
|
1053
|
+
prev.load != pkg.load);
|
|
1054
|
+
prev.priority = pkg.priority;
|
|
1055
|
+
prev.role = pkg.role;
|
|
1056
|
+
prev.load = pkg.load;
|
|
1057
|
+
prev.lastSeen = (uint32_t)millis();
|
|
1058
|
+
if (changed) {
|
|
1059
|
+
this->bridgeCoordinationChangedCallback(pkg, fromNode,
|
|
1060
|
+
"updated");
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return false;
|
|
1066
|
+
});
|
|
1067
|
+
Log(GENERAL,
|
|
1068
|
+
"onBridgeCoordination(): Registered Type 613 handler for non-bridge "
|
|
1069
|
+
"node\n");
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Set callback for bridge coordination state changes
|
|
1075
|
+
*
|
|
1076
|
+
* Called when a new bridge is discovered, an existing bridge changes its
|
|
1077
|
+
* priority/role/load, or a bridge is lost (no coordination message for 60s).
|
|
1078
|
+
*
|
|
1079
|
+
* @param callback Function called with the coordination package, sender node
|
|
1080
|
+
* ID, and change type ("new", "updated", or "lost")
|
|
1081
|
+
*/
|
|
1082
|
+
void onBridgeCoordinationChanged(
|
|
1083
|
+
std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t,
|
|
1084
|
+
TSTRING)>
|
|
1085
|
+
callback) {
|
|
1086
|
+
using namespace logger;
|
|
1087
|
+
bridgeCoordinationChangedCallback = callback;
|
|
1088
|
+
|
|
1089
|
+
// Start periodic lost-detection task (every 30 seconds, check for
|
|
1090
|
+
// bridges that haven't sent a coordination message in 60 seconds)
|
|
1091
|
+
bridgeLostDetectionTask = this->addTask(
|
|
1092
|
+
30000, TASK_FOREVER, [this]() {
|
|
1093
|
+
uint32_t now = (uint32_t)millis();
|
|
1094
|
+
for (auto it = this->lastBridgeCoordinationState.begin();
|
|
1095
|
+
it != this->lastBridgeCoordinationState.end();) {
|
|
1096
|
+
if ((now - it->second.lastSeen) > 60000) {
|
|
1097
|
+
uint32_t lostNode = it->first;
|
|
1098
|
+
// Create a package with last known state for the callback
|
|
1099
|
+
plugin::BridgeCoordinationPackage pkg;
|
|
1100
|
+
pkg.from = lostNode;
|
|
1101
|
+
pkg.priority = it->second.priority;
|
|
1102
|
+
pkg.role = it->second.role;
|
|
1103
|
+
pkg.load = it->second.load;
|
|
1104
|
+
it = this->lastBridgeCoordinationState.erase(it);
|
|
1105
|
+
if (this->bridgeCoordinationChangedCallback) {
|
|
1106
|
+
this->bridgeCoordinationChangedCallback(pkg, lostNode, "lost");
|
|
1107
|
+
}
|
|
1108
|
+
} else {
|
|
1109
|
+
++it;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
Log(GENERAL,
|
|
1115
|
+
"onBridgeCoordinationChanged(): Lost detection task started "
|
|
1116
|
+
"(interval: 30s, timeout: 60s)\n");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1001
1119
|
/**
|
|
1002
1120
|
* Enable or disable multi-bridge coordination mode
|
|
1003
1121
|
*
|
|
@@ -1175,17 +1293,6 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1175
1293
|
}
|
|
1176
1294
|
}
|
|
1177
1295
|
|
|
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
1296
|
/**
|
|
1190
1297
|
* Check if multi-bridge mode is enabled
|
|
1191
1298
|
*
|
|
@@ -1432,6 +1539,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1432
1539
|
if (peerId != this->nodeId &&
|
|
1433
1540
|
std::find(knownBridgePeers.begin(), knownBridgePeers.end(),
|
|
1434
1541
|
peerId) == knownBridgePeers.end()) {
|
|
1542
|
+
if (knownBridgePeers.size() >= 32) {
|
|
1543
|
+
knownBridgePeers.erase(knownBridgePeers.begin());
|
|
1544
|
+
}
|
|
1435
1545
|
knownBridgePeers.push_back(peerId);
|
|
1436
1546
|
}
|
|
1437
1547
|
}
|
|
@@ -1441,6 +1551,36 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1441
1551
|
"Bridge coordination from %u: priority=%d, role=%s, "
|
|
1442
1552
|
"load=%d%%\n",
|
|
1443
1553
|
fromNode, priority, role.c_str(), load);
|
|
1554
|
+
|
|
1555
|
+
// Invoke bridge coordination callback if set
|
|
1556
|
+
if (this->bridgeCoordinationCallback) {
|
|
1557
|
+
plugin::BridgeCoordinationPackage pkg(obj);
|
|
1558
|
+
this->bridgeCoordinationCallback(pkg, fromNode);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Change detection and bridgeCoordinationChangedCallback
|
|
1562
|
+
if (this->bridgeCoordinationChangedCallback) {
|
|
1563
|
+
plugin::BridgeCoordinationPackage pkg(obj);
|
|
1564
|
+
auto it = this->lastBridgeCoordinationState.find(fromNode);
|
|
1565
|
+
if (it == this->lastBridgeCoordinationState.end()) {
|
|
1566
|
+
this->lastBridgeCoordinationState[fromNode] = {
|
|
1567
|
+
priority, role, load, (uint32_t)millis()};
|
|
1568
|
+
this->bridgeCoordinationChangedCallback(pkg, fromNode, "new");
|
|
1569
|
+
} else {
|
|
1570
|
+
auto& prev = it->second;
|
|
1571
|
+
bool changed = (prev.priority != priority ||
|
|
1572
|
+
prev.role != role ||
|
|
1573
|
+
prev.load != load);
|
|
1574
|
+
prev.priority = priority;
|
|
1575
|
+
prev.role = role;
|
|
1576
|
+
prev.load = load;
|
|
1577
|
+
prev.lastSeen = (uint32_t)millis();
|
|
1578
|
+
if (changed) {
|
|
1579
|
+
this->bridgeCoordinationChangedCallback(pkg, fromNode,
|
|
1580
|
+
"updated");
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1444
1584
|
}
|
|
1445
1585
|
return false; // Don't consume the package
|
|
1446
1586
|
});
|
|
@@ -1757,7 +1897,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1757
1897
|
}
|
|
1758
1898
|
|
|
1759
1899
|
if (winner->nodeId == this->nodeId) {
|
|
1760
|
-
Log(CONNECTION, "
|
|
1900
|
+
Log(CONNECTION, "[TARGET] I WON! Promoting to bridge...\n");
|
|
1761
1901
|
promoteToBridge();
|
|
1762
1902
|
} else {
|
|
1763
1903
|
Log(CONNECTION, "Winner is node %u, remaining as regular node\n",
|
|
@@ -1818,8 +1958,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1818
1958
|
router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
|
|
1819
1959
|
|
|
1820
1960
|
// Give time for announcement to propagate before channel switch
|
|
1821
|
-
|
|
1822
|
-
|
|
1961
|
+
// Allow event loop processing during hardware settling
|
|
1962
|
+
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
1963
|
+
Log(STARTUP, "[OK] Takeover announcement sent on channel %d\n", _meshChannel);
|
|
1823
1964
|
|
|
1824
1965
|
// Save current mesh configuration to restore if bridge init fails
|
|
1825
1966
|
uint8_t savedChannel = _meshChannel;
|
|
@@ -1835,37 +1976,17 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1835
1976
|
|
|
1836
1977
|
// Now reconfigure as bridge (this will switch to router's channel)
|
|
1837
1978
|
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);
|
|
1979
|
+
// Allow event loop processing during hardware settling
|
|
1980
|
+
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
1847
1981
|
|
|
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
|
-
}
|
|
1982
|
+
// initAsBridge always returns true: bridge mesh functionality is active
|
|
1983
|
+
// regardless of router connection status (router connection is opportunistic)
|
|
1984
|
+
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1985
|
+
mScheduler, _meshPort);
|
|
1865
1986
|
|
|
1866
1987
|
lastRoleChangeTime = millis();
|
|
1867
1988
|
|
|
1868
|
-
Log(STARTUP, "
|
|
1989
|
+
Log(STARTUP, "[OK] Bridge promotion complete on channel %d\n", _meshChannel);
|
|
1869
1990
|
|
|
1870
1991
|
// Notify via callback
|
|
1871
1992
|
// Use explicit TSTRING construction to ensure string lifetime safety
|
|
@@ -1958,43 +2079,19 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1958
2079
|
|
|
1959
2080
|
// Stop current mesh operations
|
|
1960
2081
|
this->stop();
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
// Attempt to initialize as bridge
|
|
1964
|
-
bool bridgeInitSuccess =
|
|
1965
|
-
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1966
|
-
mScheduler, _meshPort);
|
|
2082
|
+
// Allow event loop processing during hardware settling
|
|
2083
|
+
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
1967
2084
|
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2085
|
+
// initAsBridge always returns true: bridge mesh functionality is active
|
|
2086
|
+
// regardless of router connection status (router connection is opportunistic)
|
|
2087
|
+
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
2088
|
+
mScheduler, _meshPort);
|
|
1971
2089
|
|
|
1972
|
-
|
|
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
|
-
}
|
|
1989
|
-
|
|
1990
|
-
return;
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
// Success! Reset retry counter
|
|
2090
|
+
// Reset retry counter
|
|
1994
2091
|
_isolatedBridgeRetryAttempts = 0;
|
|
1995
2092
|
lastRoleChangeTime = millis();
|
|
1996
2093
|
|
|
1997
|
-
Log(STARTUP, "
|
|
2094
|
+
Log(STARTUP, "[OK] Isolated bridge promotion complete on channel %d\n",
|
|
1998
2095
|
_meshChannel);
|
|
1999
2096
|
|
|
2000
2097
|
// Notify via callback
|
|
@@ -2131,50 +2228,141 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2131
2228
|
*/
|
|
2132
2229
|
bool hasActualInternetAccess() {
|
|
2133
2230
|
using namespace logger;
|
|
2134
|
-
|
|
2231
|
+
|
|
2232
|
+
// Cache result to avoid blocking DNS/HTTP on every call
|
|
2233
|
+
static uint32_t lastCheckTime = 0;
|
|
2234
|
+
static bool lastResult = false;
|
|
2235
|
+
uint32_t now = millis();
|
|
2236
|
+
if (lastCheckTime > 0 && (now - lastCheckTime) < 60000) {
|
|
2237
|
+
return lastResult;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2135
2240
|
// First check WiFi connection
|
|
2136
2241
|
if (WiFi.status() != WL_CONNECTED) {
|
|
2242
|
+
lastCheckTime = millis();
|
|
2243
|
+
lastResult = false;
|
|
2137
2244
|
return false;
|
|
2138
2245
|
}
|
|
2139
|
-
|
|
2246
|
+
|
|
2140
2247
|
// Check if we have a valid local IP
|
|
2141
2248
|
if (WiFi.localIP() == IPAddress(0, 0, 0, 0)) {
|
|
2249
|
+
lastCheckTime = millis();
|
|
2250
|
+
lastResult = false;
|
|
2142
2251
|
return false;
|
|
2143
2252
|
}
|
|
2144
|
-
|
|
2253
|
+
|
|
2145
2254
|
// Try to resolve a well-known DNS name
|
|
2146
2255
|
// Using Google's servers as they have high availability globally
|
|
2147
2256
|
IPAddress result;
|
|
2148
|
-
|
|
2257
|
+
|
|
2149
2258
|
#if defined(ESP32) || defined(ESP8266)
|
|
2150
2259
|
// Both ESP32 and ESP8266 support WiFi.hostByName()
|
|
2151
2260
|
int dnsResult = WiFi.hostByName("www.google.com", result);
|
|
2152
|
-
|
|
2261
|
+
|
|
2153
2262
|
// Check if DNS resolution succeeded
|
|
2154
2263
|
if (dnsResult != 1) {
|
|
2155
2264
|
Log(COMMUNICATION, "hasActualInternetAccess(): DNS resolution failed (code=%d)\n", dnsResult);
|
|
2265
|
+
lastCheckTime = millis();
|
|
2266
|
+
lastResult = false;
|
|
2156
2267
|
return false;
|
|
2157
2268
|
}
|
|
2158
|
-
|
|
2269
|
+
|
|
2159
2270
|
// Additional validation: Check if resolved IP is valid
|
|
2160
2271
|
// Some ESP8266 versions may return success but set IP to 255.255.255.255 on error
|
|
2161
2272
|
if (result == IPAddress(0, 0, 0, 0) || result == IPAddress(255, 255, 255, 255)) {
|
|
2162
2273
|
TSTRING resultStr = result.toString();
|
|
2163
2274
|
Log(COMMUNICATION, "hasActualInternetAccess(): Invalid DNS result IP: %s\n", resultStr.c_str());
|
|
2275
|
+
lastCheckTime = millis();
|
|
2276
|
+
lastResult = false;
|
|
2164
2277
|
return false;
|
|
2165
2278
|
}
|
|
2166
2279
|
#else
|
|
2167
2280
|
// Other platforms: assume internet is available if WiFi connected
|
|
2168
2281
|
// (no reliable way to test without platform-specific APIs)
|
|
2282
|
+
lastCheckTime = millis();
|
|
2283
|
+
lastResult = true;
|
|
2169
2284
|
return true;
|
|
2170
2285
|
#endif
|
|
2171
|
-
|
|
2286
|
+
|
|
2172
2287
|
TSTRING resultStr = result.toString();
|
|
2173
|
-
Log(COMMUNICATION, "hasActualInternetAccess(): Internet connectivity verified (resolved to %s)\n",
|
|
2288
|
+
Log(COMMUNICATION, "hasActualInternetAccess(): Internet connectivity verified (resolved to %s)\n",
|
|
2174
2289
|
resultStr.c_str());
|
|
2290
|
+
lastCheckTime = millis();
|
|
2291
|
+
lastResult = true;
|
|
2175
2292
|
return true;
|
|
2176
2293
|
}
|
|
2177
2294
|
|
|
2295
|
+
/**
|
|
2296
|
+
* Detect captive portal by making a lightweight HTTP request
|
|
2297
|
+
*
|
|
2298
|
+
* Captive portals often allow DNS resolution but intercept HTTP requests,
|
|
2299
|
+
* returning redirects, cached responses (HTTP 203), or their own HTML.
|
|
2300
|
+
* This function makes a simple HTTP GET request to a known endpoint and
|
|
2301
|
+
* verifies the response to detect such interference.
|
|
2302
|
+
*
|
|
2303
|
+
* Test endpoint used:
|
|
2304
|
+
* - http://captive.apple.com/hotspot-detect.html - Returns "Success" (Apple standard)
|
|
2305
|
+
*
|
|
2306
|
+
* @return true if no captive portal detected, false if portal found or check fails
|
|
2307
|
+
*/
|
|
2308
|
+
bool detectCaptivePortal() {
|
|
2309
|
+
using namespace logger;
|
|
2310
|
+
|
|
2311
|
+
#if defined(ESP32) || defined(ESP8266)
|
|
2312
|
+
HTTPClient http;
|
|
2313
|
+
http.setTimeout(5000); // 5 second timeout for quick check
|
|
2314
|
+
|
|
2315
|
+
WiFiClient client;
|
|
2316
|
+
|
|
2317
|
+
// Use Apple's captive portal detection endpoint
|
|
2318
|
+
// This is a well-maintained, reliable endpoint used by iOS devices
|
|
2319
|
+
const char* testUrl = "http://captive.apple.com/hotspot-detect.html";
|
|
2320
|
+
|
|
2321
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Testing %s\n", testUrl);
|
|
2322
|
+
|
|
2323
|
+
#ifdef ESP8266
|
|
2324
|
+
if (!http.begin(client, testUrl)) {
|
|
2325
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Failed to begin HTTP client - treating as potential network restriction\n");
|
|
2326
|
+
return false; // Conservative approach: treat initialization failure as potential captive portal or network restriction
|
|
2327
|
+
}
|
|
2328
|
+
#else
|
|
2329
|
+
// ESP32
|
|
2330
|
+
if (!http.begin(testUrl)) {
|
|
2331
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Failed to begin HTTP client - treating as potential network restriction\n");
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
2334
|
+
#endif
|
|
2335
|
+
|
|
2336
|
+
int httpCode = http.GET();
|
|
2337
|
+
|
|
2338
|
+
if (httpCode != 200) {
|
|
2339
|
+
// Any response other than HTTP 200 indicates captive portal or network issue
|
|
2340
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Unexpected HTTP code %d (expected 200)\n", httpCode);
|
|
2341
|
+
http.end();
|
|
2342
|
+
return false;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// Check response content
|
|
2346
|
+
String response = http.getString();
|
|
2347
|
+
http.end();
|
|
2348
|
+
|
|
2349
|
+
// Verify the response contains "Success" - this is Apple's standard response
|
|
2350
|
+
// Full response: "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>"
|
|
2351
|
+
// We check for presence of "Success" to be robust against minor format variations
|
|
2352
|
+
if (response.indexOf("Success") == -1) {
|
|
2353
|
+
Log(COMMUNICATION, "detectCaptivePortal(): Response doesn't contain 'Success', likely captive portal\n");
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
Log(COMMUNICATION, "detectCaptivePortal(): No captive portal detected\n");
|
|
2358
|
+
return true;
|
|
2359
|
+
|
|
2360
|
+
#else
|
|
2361
|
+
// Non-ESP platforms: can't reliably test, assume no captive portal
|
|
2362
|
+
return true;
|
|
2363
|
+
#endif
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2178
2366
|
/**
|
|
2179
2367
|
* Helper method to send gateway acknowledgment
|
|
2180
2368
|
*/
|
|
@@ -2263,6 +2451,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2263
2451
|
sendGatewayAck(pkg, false, 0, "Router has no internet access - check WAN connection");
|
|
2264
2452
|
return true; // Consume package - we handled it (with error)
|
|
2265
2453
|
}
|
|
2454
|
+
|
|
2455
|
+
// Finally, check for captive portal interference
|
|
2456
|
+
// This detects when DNS works but HTTP requests are intercepted
|
|
2457
|
+
if (!detectCaptivePortal()) {
|
|
2458
|
+
sendGatewayAck(pkg, false, 0, "Captive portal detected - requires web authentication. Check router/WiFi settings");
|
|
2459
|
+
return true; // Consume package - we handled it (with error)
|
|
2460
|
+
}
|
|
2266
2461
|
|
|
2267
2462
|
#if defined(ESP32) || defined(ESP8266)
|
|
2268
2463
|
// Make HTTP/HTTPS request
|
|
@@ -2538,9 +2733,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2538
2733
|
std::shared_ptr<Task> bridgeCoordinationTask;
|
|
2539
2734
|
std::map<uint32_t, uint8_t> bridgePriorities; // nodeId -> priority mapping
|
|
2540
2735
|
std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
|
|
2541
|
-
uint32_t selectedBridgeOverride = 0; // Manual bridge selection override
|
|
2542
2736
|
size_t lastSelectedBridgeIndex = 0; // For round-robin selection
|
|
2543
2737
|
|
|
2738
|
+
// Bridge coordination monitoring callbacks and state
|
|
2739
|
+
struct BridgeCoordinationState {
|
|
2740
|
+
uint8_t priority;
|
|
2741
|
+
TSTRING role;
|
|
2742
|
+
uint8_t load;
|
|
2743
|
+
uint32_t lastSeen;
|
|
2744
|
+
};
|
|
2745
|
+
std::map<uint32_t, BridgeCoordinationState> lastBridgeCoordinationState;
|
|
2746
|
+
std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t)> bridgeCoordinationCallback;
|
|
2747
|
+
std::function<void(const plugin::BridgeCoordinationPackage&, uint32_t, TSTRING)> bridgeCoordinationChangedCallback;
|
|
2748
|
+
std::shared_ptr<Task> bridgeLostDetectionTask;
|
|
2749
|
+
|
|
2544
2750
|
// Shared gateway mode state and configuration
|
|
2545
2751
|
bool _sharedGatewayMode = false;
|
|
2546
2752
|
gateway::SharedGatewayConfig _sharedGatewayConfig;
|
package/src/painlessMesh.h
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @file painlessMesh.h
|
|
6
6
|
* @brief Main header file for Alteriom painlessMesh library
|
|
7
7
|
*
|
|
8
|
-
* @version 1.9.
|
|
8
|
+
* @version 1.9.20
|
|
9
9
|
* @date 2025-12-21
|
|
10
10
|
*
|
|
11
11
|
* painlessMesh is a user-friendly library for creating mesh networks with
|
|
@@ -44,19 +44,6 @@
|
|
|
44
44
|
#include "painlessmesh/ota.hpp"
|
|
45
45
|
#endif
|
|
46
46
|
|
|
47
|
-
// Include improvement modules when enabled
|
|
48
|
-
#ifdef PAINLESSMESH_ENABLE_VALIDATION
|
|
49
|
-
#include "painlessmesh/validation.hpp"
|
|
50
|
-
#endif
|
|
51
|
-
|
|
52
|
-
#ifdef PAINLESSMESH_ENABLE_METRICS
|
|
53
|
-
#include "painlessmesh/metrics.hpp"
|
|
54
|
-
#endif
|
|
55
|
-
|
|
56
|
-
#ifdef PAINLESSMESH_ENABLE_MEMORY_OPTIMIZATION
|
|
57
|
-
#include "painlessmesh/memory.hpp"
|
|
58
|
-
#endif
|
|
59
|
-
|
|
60
47
|
#include "painlessmesh/buffer.hpp"
|
|
61
48
|
#include "painlessmesh/layout.hpp"
|
|
62
49
|
#include "painlessmesh/logger.hpp"
|
|
@@ -42,10 +42,10 @@ static const uint32_t TCP_CLIENT_DELETION_SPACING_MS = 1000; // 1000ms spacing b
|
|
|
42
42
|
// - The scheduler never runs tasks concurrently within the same mesh instance
|
|
43
43
|
// - All mesh operations (including deletion callbacks) execute in the same thread
|
|
44
44
|
// - Even when multiple tasks are ready, they execute one-at-a-time via scheduler->execute()
|
|
45
|
-
|
|
45
|
+
extern uint32_t lastScheduledDeletionTime; // Timestamp of last deletion scheduled/executed (milliseconds)
|
|
46
46
|
|
|
47
47
|
// Shared buffer for reading/writing to the buffer
|
|
48
|
-
|
|
48
|
+
extern painlessmesh::buffer::temp_buffer_t shared_buffer;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Schedule deletion of an AsyncClient with proper spacing to prevent concurrent cleanups
|
|
@@ -116,25 +116,20 @@ inline void scheduleAsyncClientDeletion(Scheduler* scheduler, AsyncClient* clien
|
|
|
116
116
|
Log(CONNECTION, "%s: Scheduling AsyncClient deletion in %u ms (spaced from previous deletions)\n",
|
|
117
117
|
logPrefix, actualDelay);
|
|
118
118
|
|
|
119
|
-
// Schedule the deletion task
|
|
120
|
-
// Note: Task object is intentionally leaked to keep implementation simple
|
|
121
|
-
// This is acceptable because:
|
|
122
|
-
// 1. Connections are long-lived, destructor calls are infrequent
|
|
123
|
-
// 2. Task object is small (~32-64 bytes) vs preventing critical heap corruption
|
|
124
|
-
// 3. In typical deployments, memory impact is negligible (few KB over months)
|
|
125
|
-
// 4. Alternative cleanup patterns would add significant complexity
|
|
119
|
+
// Schedule the deletion task with self-cleanup
|
|
126
120
|
Task* cleanupTask = new Task(actualDelay * TASK_MILLISECOND, TASK_ONCE, [client, logPrefix]() {
|
|
127
121
|
using namespace logger;
|
|
128
122
|
Log(CONNECTION, "%s: Deferred cleanup of AsyncClient executing now\n", logPrefix);
|
|
129
|
-
|
|
130
|
-
// Note: lastScheduledDeletionTime is updated at scheduling time (before this task runs), not here
|
|
131
|
-
// This ensures consistent spacing based on when deletions were scheduled, preventing
|
|
132
|
-
// the race condition where execution-time updates could "rewind" the timestamp
|
|
133
|
-
// and cause subsequent deletions to be scheduled too close together
|
|
134
|
-
|
|
135
123
|
delete client;
|
|
136
124
|
});
|
|
137
|
-
|
|
125
|
+
|
|
126
|
+
// Self-cleanup: after execution, remove from scheduler and delete the Task.
|
|
127
|
+
// OnDisable fires after a TASK_ONCE task completes its single iteration.
|
|
128
|
+
cleanupTask->setOnDisable([cleanupTask, scheduler]() {
|
|
129
|
+
scheduler->deleteTask(*cleanupTask);
|
|
130
|
+
delete cleanupTask;
|
|
131
|
+
});
|
|
132
|
+
|
|
138
133
|
scheduler->addTask(*cleanupTask);
|
|
139
134
|
cleanupTask->enableDelayed();
|
|
140
135
|
}
|