@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +82 -63
  3. package/examples/alteriom/README.md +4 -4
  4. package/examples/alteriom/alteriom_custom_package_template.hpp +320 -0
  5. package/examples/alteriom/alteriom_sensor_package.hpp +1 -1
  6. package/examples/alteriom/mppt_example/alteriom_mppt_example.ino +208 -0
  7. package/examples/bridge_failover/bridge_failover.ino +17 -0
  8. package/examples/sendToInternet/CMakeLists.txt +54 -0
  9. package/examples/sendToInternet/PC_NODE_README.md +517 -0
  10. package/examples/sendToInternet/README.md +39 -1
  11. package/examples/sendToInternet/build.sh +153 -0
  12. package/examples/sendToInternet/mock_server_test.ino +361 -0
  13. package/examples/sendToInternet/pc_mesh_node.cpp +361 -0
  14. package/library.json +4 -1
  15. package/library.properties +1 -1
  16. package/package.json +3 -3
  17. package/src/AlteriomPainlessMesh.h +5 -13
  18. package/src/arduino/wifi.hpp +306 -100
  19. package/src/connection.cpp +10 -0
  20. package/src/painlessMesh.h +1 -14
  21. package/src/painlessmesh/connection.hpp +11 -16
  22. package/src/painlessmesh/gateway.hpp +0 -1061
  23. package/src/painlessmesh/mesh.hpp +29 -85
  24. package/src/painlessmesh/message_queue.hpp +1 -2
  25. package/src/painlessmesh/metrics.hpp +2 -262
  26. package/src/painlessmesh/validation.hpp +0 -143
  27. package/docs/README.md +0 -132
  28. package/docs/alteriom/overview.md +0 -531
  29. package/docs/api/core-api.md +0 -607
  30. package/docs/api/shared-gateway.md +0 -1207
  31. package/docs/architecture/mesh-architecture.md +0 -399
  32. package/docs/architecture/plugin-system.md +0 -517
  33. package/docs/getting-started/arduino-manual-install.md +0 -313
  34. package/docs/getting-started/first-mesh.md +0 -410
  35. package/docs/getting-started/installation.md +0 -275
  36. package/docs/getting-started/quickstart.md +0 -158
  37. package/docs/troubleshooting/common-issues.md +0 -679
  38. package/docs/troubleshooting/debugging.md +0 -455
  39. package/docs/troubleshooting/external-device-connection.md +0 -283
  40. package/docs/troubleshooting/faq.md +0 -574
  41. package/docs/tutorials/basic-examples.md +0 -718
@@ -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
- delay(1000);
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✗ Invalid channel detected: %d, falling back to channel 1\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✓ Router connected on channel %d\n", detectedChannel);
431
- Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
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⚠ Router connection unavailable during initialization\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, "⚠ Scanning for router '%s' to detect channel...\n", routerSSID.c_str());
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, "✓ Router found on channel %d (not connected, will retry)\n",
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, "⚠ Router not found in scan, using default channel %d\n", detectedChannel);
465
+ Log(STARTUP, "[WARN] Router not found in scan, using default channel %d\n", detectedChannel);
465
466
  }
466
467
 
467
- Log(STARTUP, "⚠ Proceeding with bridge setup on channel %d\n", detectedChannel);
468
- Log(STARTUP, "⚠ Bridge will retry router connection in background\n");
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
- // Return true - bridge mesh functionality is active even without router
516
- // The mesh network is operational and nodes can connect
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
- delay(1000);
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✗ Invalid channel detected: %d, falling back to channel 1\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✓ Router connected on channel %d\n", detectedChannel);
662
- Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
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✗ Failed to connect to router during channel detection\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, "🎯 I WON! Promoting to bridge...\n");
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
- delay(1000);
1822
- Log(STARTUP, "✓ Takeover announcement sent on channel %d\n", _meshChannel);
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
- delay(1000);
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
- // Re-initialize as regular node on the original channel
1849
- this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
1850
- savedChannel, _meshHidden, MAX_CONN);
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, "✓ Bridge promotion complete on channel %d\n", _meshChannel);
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
- delay(1000);
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
- if (!bridgeInitSuccess) {
1969
- Log(ERROR, "✗ Isolated bridge promotion failed - router unreachable\n");
1970
- Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
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
- // 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
- }
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, "✓ Isolated bridge promotion complete on channel %d\n",
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;
@@ -0,0 +1,10 @@
1
+ #include "painlessmesh/connection.hpp"
2
+
3
+ namespace painlessmesh {
4
+ namespace tcp {
5
+
6
+ uint32_t lastScheduledDeletionTime = 0;
7
+ painlessmesh::buffer::temp_buffer_t shared_buffer;
8
+
9
+ } // namespace tcp
10
+ } // namespace painlessmesh
@@ -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.19
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
- static uint32_t lastScheduledDeletionTime = 0; // Timestamp of last deletion scheduled/executed (milliseconds)
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
- static painlessmesh::buffer::temp_buffer_t shared_buffer;
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
  }