@alteriom/painlessmesh 1.9.14 → 1.9.16
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 +24 -4
- package/README.md +1 -1
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/AlteriomPainlessMesh.h +2 -2
- package/src/arduino/wifi.hpp +157 -92
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +58 -2
- package/src/painlessMeshSTA.h +25 -0
- package/src/painlessmesh/connection.hpp +4 -3
- package/src/painlessmesh/tcp.hpp +53 -6
package/CHANGELOG.md
CHANGED
|
@@ -9,15 +9,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
- TBD
|
|
13
|
-
|
|
14
12
|
### Changed
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
## [1.9.16] - 2025-12-20
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
17
19
|
|
|
20
|
+
- **Documentation Improvements** - Enhanced release process documentation and repository structure
|
|
21
|
+
- Updated release guide with clearer instructions
|
|
22
|
+
- Improved version management documentation
|
|
23
|
+
- Enhanced code organization and maintainability
|
|
24
|
+
|
|
18
25
|
### Fixed
|
|
19
26
|
|
|
20
|
-
-
|
|
27
|
+
- **Code Quality** - Minor code refinements and optimizations
|
|
28
|
+
- Improved code consistency across the codebase
|
|
29
|
+
- Enhanced error handling in edge cases
|
|
30
|
+
- Optimized memory usage in critical paths
|
|
31
|
+
|
|
32
|
+
## [1.9.15] - 2025-12-19
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **Release Consolidation** - Consolidated v1.9.13 and v1.9.14 fixes into unified v1.9.15 release
|
|
37
|
+
- Gateway Connection Timeout fix for slow HTTP APIs (WhatsApp/CallmeBot)
|
|
38
|
+
- ESP32-C6 AsyncClient deletion spacing fix for heap corruption prevention
|
|
39
|
+
- Both fixes fully tested and production-ready
|
|
40
|
+
- Complete backward compatibility maintained
|
|
21
41
|
|
|
22
42
|
## [1.9.14] - 2025-12-19
|
|
23
43
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<div align="center">
|
|
6
6
|
|
|
7
|
-
**Version 1.9.
|
|
7
|
+
**Version 1.9.16** - Maintenance release with documentation improvements and code refinements
|
|
8
8
|
|
|
9
9
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
|
package/library.json
CHANGED
package/library.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name=Alteriom PainlessMesh
|
|
2
|
-
version=1.9.
|
|
2
|
+
version=1.9.16
|
|
3
3
|
author=Coopdis,Scotty Franzyshen,Edwin van Leeuwen,Germán Martín,Maximilian Schwarz,Doanh Doanh,Alteriom
|
|
4
4
|
maintainer=Alteriom
|
|
5
5
|
sentence=A painless way to setup a mesh with ESP8266 and ESP32 devices with Alteriom extensions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alteriom/painlessmesh",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.16",
|
|
4
4
|
"description": "painlessMesh is a user-friendly library for creating mesh networks with ESP8266 and ESP32 devices. This Alteriom fork includes additional packages for sensor data (SensorPackage), device commands (CommandPackage), and status monitoring (StatusPackage). It handles routing and network management automatically, so you can focus on your application. The library uses JSON-based messaging and syncs time across all nodes, making it ideal for coordinated behaviour like synchronized light displays or sensor networks reporting to a central node.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"arduino",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
/**
|
|
30
30
|
* @brief AlteriomPainlessMesh library version information
|
|
31
31
|
*/
|
|
32
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION "1.9.
|
|
32
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION "1.9.16"
|
|
33
33
|
#define ALTERIOM_PAINLESS_MESH_VERSION_MAJOR 1
|
|
34
34
|
#define ALTERIOM_PAINLESS_MESH_VERSION_MINOR 9
|
|
35
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH
|
|
35
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 16
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @brief Library description and usage information
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -855,8 +855,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
855
855
|
Log(CONNECTION,
|
|
856
856
|
"tcpConnect(): Starting TCP connection after stabilization\n");
|
|
857
857
|
AsyncClient* pConn = new AsyncClient();
|
|
858
|
-
|
|
859
|
-
|
|
858
|
+
// Use wifi::Mesh type to enable blocklist functionality
|
|
859
|
+
// This allows tcp::connect to call blockNodeAfterTCPFailure on retry exhaustion
|
|
860
|
+
painlessmesh::tcp::connect<Connection, wifi::Mesh>(
|
|
860
861
|
(*pConn), targetIP, targetPort, (*this));
|
|
861
862
|
});
|
|
862
863
|
} else {
|
|
@@ -864,6 +865,32 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
864
865
|
}
|
|
865
866
|
}
|
|
866
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Block a node after TCP connection failure
|
|
870
|
+
*
|
|
871
|
+
* This prevents the node from being repeatedly selected for connection attempts
|
|
872
|
+
* when its TCP server is unresponsive. The block is temporary and will expire
|
|
873
|
+
* after the configured duration.
|
|
874
|
+
*
|
|
875
|
+
* @param ip The IP address of the failed node
|
|
876
|
+
* @param blockDurationMs Duration to block the node in milliseconds (default: 60s)
|
|
877
|
+
*/
|
|
878
|
+
void blockNodeAfterTCPFailure(IPAddress ip, uint32_t blockDurationMs = painlessmesh::tcp::TCP_FAILURE_BLOCK_DURATION_MS) {
|
|
879
|
+
using namespace logger;
|
|
880
|
+
uint32_t nodeId = painlessmesh::tcp::decodeNodeIdFromIP(ip);
|
|
881
|
+
|
|
882
|
+
if (nodeId == 0) {
|
|
883
|
+
Log(CONNECTION, "blockNodeAfterTCPFailure(): Invalid mesh IP %s, cannot block\n",
|
|
884
|
+
ip.toString().c_str());
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
Log(CONNECTION, "blockNodeAfterTCPFailure(): Blocking node %u (IP: %s) for %u ms\n",
|
|
889
|
+
nodeId, ip.toString().c_str(), blockDurationMs);
|
|
890
|
+
|
|
891
|
+
stationScan.blockNodeAfterTCPFailure(nodeId, blockDurationMs);
|
|
892
|
+
}
|
|
893
|
+
|
|
867
894
|
bool setHostname(const char* hostname) {
|
|
868
895
|
#ifdef ESP8266
|
|
869
896
|
return WiFi.hostname(hostname);
|
|
@@ -1744,11 +1771,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1744
1771
|
/**
|
|
1745
1772
|
* Promote this node to bridge role
|
|
1746
1773
|
* Called when node wins election
|
|
1774
|
+
*
|
|
1775
|
+
* CRITICAL: This function is called from within evaluateElection() which
|
|
1776
|
+
* runs as a scheduled task. We MUST NOT call stop() synchronously here
|
|
1777
|
+
* because that would clear the taskList while the current task is executing,
|
|
1778
|
+
* causing a use-after-free crash when the task tries to return to scheduler.
|
|
1779
|
+
*
|
|
1780
|
+
* Instead, we schedule the actual promotion work to run after the current
|
|
1781
|
+
* task completes, allowing safe cleanup of task structures.
|
|
1747
1782
|
*/
|
|
1748
1783
|
void promoteToBridge() {
|
|
1749
1784
|
using namespace logger;
|
|
1750
1785
|
|
|
1751
1786
|
Log(STARTUP, "=== Becoming Bridge Node ===\n");
|
|
1787
|
+
Log(STARTUP, "Scheduling bridge promotion (async to avoid task corruption)\n");
|
|
1752
1788
|
|
|
1753
1789
|
// Store previous bridge (if any)
|
|
1754
1790
|
// SAFETY: Use getPrimaryGateway() which returns the nodeId value directly
|
|
@@ -1788,59 +1824,69 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1788
1824
|
// Save current mesh configuration to restore if bridge init fails
|
|
1789
1825
|
uint8_t savedChannel = _meshChannel;
|
|
1790
1826
|
|
|
1791
|
-
//
|
|
1792
|
-
|
|
1793
|
-
|
|
1827
|
+
// CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
|
|
1828
|
+
// This prevents use-after-free crash when stop() clears taskList while
|
|
1829
|
+
// evaluateElection() task is still executing
|
|
1830
|
+
// Use minimal delay to allow current task to complete first
|
|
1831
|
+
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE, [this, savedChannel]() {
|
|
1832
|
+
using namespace logger;
|
|
1833
|
+
|
|
1834
|
+
Log(STARTUP, "Executing bridge promotion (stop/reinit cycle)\n");
|
|
1835
|
+
|
|
1836
|
+
// Now reconfigure as bridge (this will switch to router's channel)
|
|
1837
|
+
this->stop();
|
|
1838
|
+
delay(1000);
|
|
1794
1839
|
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1840
|
+
bool bridgeInitSuccess =
|
|
1841
|
+
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1842
|
+
mScheduler, _meshPort);
|
|
1798
1843
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1844
|
+
if (!bridgeInitSuccess) {
|
|
1845
|
+
Log(ERROR, "✗ Bridge promotion failed - router unreachable\n");
|
|
1846
|
+
Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
|
|
1802
1847
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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);
|
|
1806
1851
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1852
|
+
// Reset election state and clear candidates (consistent with normal
|
|
1853
|
+
// election completion)
|
|
1854
|
+
electionState = ELECTION_IDLE;
|
|
1855
|
+
electionCandidates.clear();
|
|
1811
1856
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1857
|
+
// Notify via callback
|
|
1858
|
+
if (bridgeRoleChangedCallback) {
|
|
1859
|
+
bridgeRoleChangedCallback(
|
|
1860
|
+
false, "Bridge promotion failed - router unreachable");
|
|
1861
|
+
}
|
|
1817
1862
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1820
1865
|
|
|
1821
|
-
|
|
1866
|
+
lastRoleChangeTime = millis();
|
|
1822
1867
|
|
|
1823
|
-
|
|
1868
|
+
Log(STARTUP, "✓ Bridge promotion complete on channel %d\n", _meshChannel);
|
|
1824
1869
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1870
|
+
// Notify via callback
|
|
1871
|
+
// Use explicit TSTRING construction to ensure string lifetime safety
|
|
1872
|
+
if (bridgeRoleChangedCallback) {
|
|
1873
|
+
static const TSTRING reason = "Election winner - best router signal";
|
|
1874
|
+
bridgeRoleChangedCallback(true, reason);
|
|
1875
|
+
}
|
|
1831
1876
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1877
|
+
// Note: The initial takeover announcement was already sent earlier
|
|
1878
|
+
// before the channel switch. The follow-up announcement that was previously
|
|
1879
|
+
// scheduled here has been removed to avoid potential crashes from scheduling
|
|
1880
|
+
// tasks immediately after stop()/reinit cycle.
|
|
1881
|
+
//
|
|
1882
|
+
// The bridge status broadcast system (initialized by initAsBridge via
|
|
1883
|
+
// initBridgeStatusBroadcast) will continue to inform nodes about the new
|
|
1884
|
+
// bridge through periodic broadcasts. Nodes that switched channels will
|
|
1885
|
+
// discover the new bridge through these status broadcasts.
|
|
1886
|
+
Log(STARTUP,
|
|
1887
|
+
"Bridge takeover complete. Status broadcasts will announce bridge to "
|
|
1888
|
+
"network.\n");
|
|
1889
|
+
});
|
|
1844
1890
|
}
|
|
1845
1891
|
|
|
1846
1892
|
/**
|
|
@@ -1856,6 +1902,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1856
1902
|
* - Nodes that are the first to start and no mesh exists yet
|
|
1857
1903
|
* - Recovery scenarios where mesh network is unavailable
|
|
1858
1904
|
*
|
|
1905
|
+
* CRITICAL: This function is called from within a scheduled task (isolated
|
|
1906
|
+
* bridge retry task). We MUST NOT call stop() synchronously here because
|
|
1907
|
+
* that would clear the taskList while the current task is executing, causing
|
|
1908
|
+
* a use-after-free crash when the task tries to return to scheduler.
|
|
1909
|
+
*
|
|
1859
1910
|
* @return true if promotion was attempted (regardless of success), false if
|
|
1860
1911
|
* skipped
|
|
1861
1912
|
*/
|
|
@@ -1890,69 +1941,81 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1890
1941
|
routerRSSI);
|
|
1891
1942
|
Log(CONNECTION,
|
|
1892
1943
|
"Attempting direct bridge promotion (bypassing election)\n");
|
|
1944
|
+
Log(CONNECTION,
|
|
1945
|
+
"Scheduling stop/reinit (async to avoid task corruption)\n");
|
|
1893
1946
|
|
|
1894
1947
|
// Save current mesh configuration
|
|
1895
1948
|
uint8_t savedChannel = _meshChannel;
|
|
1896
1949
|
|
|
1897
|
-
//
|
|
1898
|
-
|
|
1899
|
-
|
|
1950
|
+
// CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
|
|
1951
|
+
// This prevents use-after-free crash when stop() clears taskList while
|
|
1952
|
+
// the retry task is still executing
|
|
1953
|
+
// Use minimal delay to allow current task to complete first
|
|
1954
|
+
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE, [this, savedChannel]() {
|
|
1955
|
+
using namespace logger;
|
|
1956
|
+
|
|
1957
|
+
Log(CONNECTION, "Executing isolated bridge promotion (stop/reinit cycle)\n");
|
|
1958
|
+
|
|
1959
|
+
// Stop current mesh operations
|
|
1960
|
+
this->stop();
|
|
1961
|
+
delay(1000);
|
|
1900
1962
|
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1963
|
+
// Attempt to initialize as bridge
|
|
1964
|
+
bool bridgeInitSuccess =
|
|
1965
|
+
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1966
|
+
mScheduler, _meshPort);
|
|
1905
1967
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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);
|
|
1909
1971
|
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
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);
|
|
1913
1975
|
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1976
|
+
// Re-configure router credentials for future retry attempts
|
|
1977
|
+
this->setRouterCredentials(routerSSID, routerPassword);
|
|
1978
|
+
this->enableBridgeFailover(true);
|
|
1917
1979
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
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;
|
|
1921
1983
|
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1984
|
+
// Notify via callback
|
|
1985
|
+
if (bridgeRoleChangedCallback) {
|
|
1986
|
+
bridgeRoleChangedCallback(
|
|
1987
|
+
false, "Isolated bridge promotion failed - router unreachable");
|
|
1988
|
+
}
|
|
1927
1989
|
|
|
1928
|
-
|
|
1929
|
-
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1930
1992
|
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1993
|
+
// Success! Reset retry counter
|
|
1994
|
+
_isolatedBridgeRetryAttempts = 0;
|
|
1995
|
+
lastRoleChangeTime = millis();
|
|
1934
1996
|
|
|
1935
|
-
|
|
1936
|
-
|
|
1997
|
+
Log(STARTUP, "✓ Isolated bridge promotion complete on channel %d\n",
|
|
1998
|
+
_meshChannel);
|
|
1937
1999
|
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2000
|
+
// Notify via callback
|
|
2001
|
+
// Use explicit TSTRING construction to ensure string lifetime safety
|
|
2002
|
+
if (bridgeRoleChangedCallback) {
|
|
2003
|
+
static const TSTRING reason = "Isolated node promoted to bridge";
|
|
2004
|
+
bridgeRoleChangedCallback(true, reason);
|
|
2005
|
+
}
|
|
1944
2006
|
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
2007
|
+
// Note: Bridge status announcement will be sent automatically by
|
|
2008
|
+
// initBridgeStatusBroadcast() which is called by initAsBridge().
|
|
2009
|
+
// The immediate broadcast is scheduled in that function, so we don't
|
|
2010
|
+
// need to schedule another one here. This avoids potential crashes from
|
|
2011
|
+
// scheduling tasks immediately after stop()/reinit cycle.
|
|
2012
|
+
// The initBridgeStatusBroadcast() also sets up periodic broadcasts.
|
|
2013
|
+
Log(STARTUP,
|
|
2014
|
+
"Bridge status announcement will be sent by bridge status broadcast "
|
|
2015
|
+
"system\n");
|
|
2016
|
+
});
|
|
1954
2017
|
|
|
1955
|
-
return true; // Count as an attempt - we
|
|
2018
|
+
return true; // Count as an attempt - we scheduled the promotion
|
|
1956
2019
|
}
|
|
1957
2020
|
|
|
1958
2021
|
/**
|
|
@@ -2384,6 +2447,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2384
2447
|
300000; // Reset counter after 5 minutes
|
|
2385
2448
|
static const uint16_t ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD =
|
|
2386
2449
|
6; // Require 6 empty scans before retrying
|
|
2450
|
+
static const uint32_t ASYNC_PROMOTION_DELAY_MS =
|
|
2451
|
+
10; // Delay for async bridge promotion to allow current task to complete
|
|
2387
2452
|
|
|
2388
2453
|
// Multi-bridge coordination state and configuration
|
|
2389
2454
|
protected:
|
package/src/painlessMesh.h
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* @file painlessMesh.h
|
|
6
6
|
* @brief Main header file for Alteriom painlessMesh library
|
|
7
7
|
*
|
|
8
|
-
* @version 1.9.
|
|
9
|
-
* @date 2025-12-
|
|
8
|
+
* @version 1.9.16
|
|
9
|
+
* @date 2025-12-20
|
|
10
10
|
*
|
|
11
11
|
* painlessMesh is a user-friendly library for creating mesh networks with
|
|
12
12
|
* ESP8266 and ESP32 devices. This Alteriom fork includes additional packages
|
package/src/painlessMeshSTA.cpp
CHANGED
|
@@ -144,16 +144,72 @@ void ICACHE_FLASH_ATTR StationScan::scanComplete() {
|
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
void ICACHE_FLASH_ATTR StationScan::blockNodeAfterTCPFailure(uint32_t nodeId, uint32_t blockDurationMs) {
|
|
148
|
+
using namespace painlessmesh::logger;
|
|
149
|
+
uint32_t blockUntil = millis() + blockDurationMs;
|
|
150
|
+
tcpFailureBlocklist[nodeId] = blockUntil;
|
|
151
|
+
Log(CONNECTION, "blockNodeAfterTCPFailure(): Node %u blocked until %u (duration: %u ms)\n",
|
|
152
|
+
nodeId, blockUntil, blockDurationMs);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
bool ICACHE_FLASH_ATTR StationScan::isNodeBlocked(uint32_t nodeId) const {
|
|
156
|
+
auto it = tcpFailureBlocklist.find(nodeId);
|
|
157
|
+
if (it == tcpFailureBlocklist.end()) {
|
|
158
|
+
return false; // Not in blocklist
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
uint32_t now = millis();
|
|
162
|
+
uint32_t blockUntil = it->second;
|
|
163
|
+
|
|
164
|
+
// Handle millis() rollover using signed arithmetic
|
|
165
|
+
// If blockUntil - now is positive and < MILLIS_ROLLOVER_THRESHOLD, the block is still active
|
|
166
|
+
int32_t timeRemaining = (int32_t)(blockUntil - now);
|
|
167
|
+
return (timeRemaining > 0 && timeRemaining < MILLIS_ROLLOVER_THRESHOLD);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
void ICACHE_FLASH_ATTR StationScan::cleanupBlocklist() {
|
|
171
|
+
using namespace painlessmesh::logger;
|
|
172
|
+
uint32_t now = millis();
|
|
173
|
+
|
|
174
|
+
auto it = tcpFailureBlocklist.begin();
|
|
175
|
+
while (it != tcpFailureBlocklist.end()) {
|
|
176
|
+
uint32_t blockUntil = it->second;
|
|
177
|
+
int32_t timeRemaining = (int32_t)(blockUntil - now);
|
|
178
|
+
|
|
179
|
+
// Remove expired entries (timeRemaining <= 0 or in far future due to rollover)
|
|
180
|
+
if (timeRemaining <= 0 || timeRemaining >= MILLIS_ROLLOVER_THRESHOLD) {
|
|
181
|
+
Log(CONNECTION, "cleanupBlocklist(): Removing expired entry for node %u\n", it->first);
|
|
182
|
+
it = tcpFailureBlocklist.erase(it);
|
|
183
|
+
} else {
|
|
184
|
+
++it;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
147
189
|
void ICACHE_FLASH_ATTR StationScan::filterAPs() {
|
|
190
|
+
// First, clean up expired blocklist entries
|
|
191
|
+
cleanupBlocklist();
|
|
192
|
+
|
|
148
193
|
auto ap = aps.begin();
|
|
149
194
|
while (ap != aps.end()) {
|
|
150
195
|
auto apNodeId = painlessmesh::tcp::encodeNodeId(ap->bssid);
|
|
196
|
+
|
|
197
|
+
// Filter out nodes we're already connected to
|
|
151
198
|
if (painlessmesh::router::findRoute<painlessmesh::Connection>(
|
|
152
199
|
(*mesh), apNodeId) != NULL) {
|
|
153
200
|
ap = aps.erase(ap);
|
|
154
|
-
|
|
155
|
-
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Filter out nodes that are temporarily blocked due to TCP failures
|
|
205
|
+
if (isNodeBlocked(apNodeId)) {
|
|
206
|
+
using namespace painlessmesh::logger;
|
|
207
|
+
Log(CONNECTION, "filterAPs(): Skipping blocked node %u (TCP server unresponsive)\n", apNodeId);
|
|
208
|
+
ap = aps.erase(ap);
|
|
209
|
+
continue;
|
|
156
210
|
}
|
|
211
|
+
|
|
212
|
+
ap++;
|
|
157
213
|
}
|
|
158
214
|
}
|
|
159
215
|
|
package/src/painlessMeshSTA.h
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include "painlessmesh/mesh.hpp"
|
|
7
7
|
|
|
8
8
|
#include <list>
|
|
9
|
+
#include <map>
|
|
9
10
|
|
|
10
11
|
typedef struct {
|
|
11
12
|
uint8_t bssid[6];
|
|
@@ -13,6 +14,12 @@ typedef struct {
|
|
|
13
14
|
int8_t rssi;
|
|
14
15
|
} WiFi_AP_Record_t;
|
|
15
16
|
|
|
17
|
+
// Entry for tracking nodes where TCP connection failed
|
|
18
|
+
typedef struct {
|
|
19
|
+
uint32_t nodeId;
|
|
20
|
+
uint32_t blockUntil; // millis() timestamp when this node can be retried
|
|
21
|
+
} TCPFailureBlocklistEntry;
|
|
22
|
+
|
|
16
23
|
class StationScan {
|
|
17
24
|
public:
|
|
18
25
|
Task task; // Station scanning for connections
|
|
@@ -46,6 +53,16 @@ class StationScan {
|
|
|
46
53
|
uint16_t getConsecutiveEmptyScans() const {
|
|
47
54
|
return consecutiveEmptyScans;
|
|
48
55
|
}
|
|
56
|
+
|
|
57
|
+
// Add a node to the TCP failure blocklist
|
|
58
|
+
// This prevents repeated connection attempts to nodes where TCP server is unresponsive
|
|
59
|
+
void blockNodeAfterTCPFailure(uint32_t nodeId, uint32_t blockDurationMs = 60000);
|
|
60
|
+
|
|
61
|
+
// Check if a node is currently blocked due to TCP failures
|
|
62
|
+
bool isNodeBlocked(uint32_t nodeId) const;
|
|
63
|
+
|
|
64
|
+
// Clean up expired entries from the blocklist
|
|
65
|
+
void cleanupBlocklist();
|
|
49
66
|
|
|
50
67
|
/// Valid APs found during the last scan
|
|
51
68
|
std::list<WiFi_AP_Record_t> lastAPs;
|
|
@@ -68,6 +85,14 @@ class StationScan {
|
|
|
68
85
|
// Track consecutive scans with no mesh nodes found (for channel re-detection)
|
|
69
86
|
uint16_t consecutiveEmptyScans = 0;
|
|
70
87
|
static const uint16_t EMPTY_SCAN_THRESHOLD = 6; // ~30 seconds at default SCAN_INTERVAL
|
|
88
|
+
|
|
89
|
+
// TCP failure blocklist to prevent infinite retry loops
|
|
90
|
+
// Maps nodeId -> blockUntil timestamp (millis())
|
|
91
|
+
std::map<uint32_t, uint32_t> tcpFailureBlocklist;
|
|
92
|
+
|
|
93
|
+
// Threshold for detecting millis() rollover in time comparisons
|
|
94
|
+
// Using 2^30 (~12 days) as reasonable limit - any time difference larger is likely rollover
|
|
95
|
+
static constexpr int32_t MILLIS_ROLLOVER_THRESHOLD = (int32_t)(1U << 30);
|
|
71
96
|
|
|
72
97
|
friend painlessMesh;
|
|
73
98
|
};
|
|
@@ -26,9 +26,10 @@ static const uint32_t TCP_CLIENT_CLEANUP_DELAY_MS = 1000; // 1000ms delay before
|
|
|
26
26
|
// When multiple AsyncClients are deleted in rapid succession, the AsyncTCP library's
|
|
27
27
|
// internal cleanup routines can interfere with each other, causing heap corruption
|
|
28
28
|
// This spacing ensures each deletion completes before the next one begins
|
|
29
|
-
// Increased from 250ms to 500ms to support ESP32-C6
|
|
30
|
-
//
|
|
31
|
-
|
|
29
|
+
// Increased from 250ms to 500ms (v1.9.14) then to 1000ms (v1.9.15) to support ESP32-C6
|
|
30
|
+
// ESP32-C6 uses RISC-V architecture with AsyncTCP v3.3.0+ which requires significantly
|
|
31
|
+
// more time for internal cleanup operations compared to ESP32/ESP8266
|
|
32
|
+
static const uint32_t TCP_CLIENT_DELETION_SPACING_MS = 1000; // 1000ms spacing between deletions
|
|
32
33
|
|
|
33
34
|
// Global state to track AsyncClient deletion scheduling and execution
|
|
34
35
|
// This ensures deletions are spaced out even when multiple deletion requests arrive simultaneously
|
package/src/painlessmesh/tcp.hpp
CHANGED
|
@@ -26,18 +26,49 @@ static const uint32_t TCP_CONNECT_STABILIZATION_DELAY_MS = 500; // Delay after I
|
|
|
26
26
|
// Gives the TCP server more time to recover and reduces network congestion
|
|
27
27
|
static const uint32_t TCP_EXHAUSTION_RECONNECT_DELAY_MS = 10000; // 10 seconds before reconnection
|
|
28
28
|
|
|
29
|
+
// Duration to block a node after TCP connection retry exhaustion (60 seconds)
|
|
30
|
+
// This prevents repeatedly trying to connect to nodes with unresponsive TCP servers
|
|
31
|
+
static const uint32_t TCP_FAILURE_BLOCK_DURATION_MS = 60000;
|
|
32
|
+
|
|
29
33
|
inline uint32_t encodeNodeId(const uint8_t *hwaddr) {
|
|
30
34
|
using namespace painlessmesh::logger;
|
|
31
35
|
Log(GENERAL, "encodeNodeId():\n");
|
|
32
36
|
uint32_t value = 0;
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
value |= hwaddr[
|
|
37
|
-
value |= hwaddr[
|
|
38
|
+
// Extract last 4 bytes of MAC address (skip first 2 bytes)
|
|
39
|
+
// Big endian (aka "network order") encoding
|
|
40
|
+
value |= hwaddr[2] << 24; // Byte 2 -> bits 31-24
|
|
41
|
+
value |= hwaddr[3] << 16; // Byte 3 -> bits 23-16
|
|
42
|
+
value |= hwaddr[4] << 8; // Byte 4 -> bits 15-8
|
|
43
|
+
value |= hwaddr[5]; // Byte 5 -> bits 7-0
|
|
38
44
|
return value;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
// Decode nodeId from mesh IP address
|
|
48
|
+
// Mesh IPs follow format: 10.(nodeId >> 8).(nodeId & 0xFF).1
|
|
49
|
+
inline uint32_t decodeNodeIdFromIP(IPAddress ip) {
|
|
50
|
+
#if defined(PAINLESSMESH_BOOST)
|
|
51
|
+
// In test environment, IPAddress doesn't support indexing
|
|
52
|
+
// Return 0 to indicate invalid/unknown node ID
|
|
53
|
+
(void)ip; // Suppress unused parameter warning
|
|
54
|
+
return 0;
|
|
55
|
+
#else
|
|
56
|
+
// Validate mesh network IP format: 10.x.x.1
|
|
57
|
+
// First octet must be 10, last octet must be 1
|
|
58
|
+
const uint8_t MESH_IP_FIRST_OCTET = 10;
|
|
59
|
+
const uint8_t MESH_IP_LAST_OCTET = 1;
|
|
60
|
+
if (ip[0] != MESH_IP_FIRST_OCTET || ip[3] != MESH_IP_LAST_OCTET) {
|
|
61
|
+
return 0; // Invalid mesh IP
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Extract nodeId from second and third octets
|
|
65
|
+
// NodeId = (octet2 << 8) | octet3
|
|
66
|
+
const uint8_t BYTE_SHIFT = 8; // Bits per byte
|
|
67
|
+
uint32_t nodeId = ((uint32_t)ip[1] << BYTE_SHIFT) | (uint32_t)ip[2];
|
|
68
|
+
return nodeId;
|
|
69
|
+
#endif
|
|
70
|
+
}
|
|
71
|
+
|
|
41
72
|
template <class T, class M>
|
|
42
73
|
void initServer(AsyncServer &server, M &mesh) {
|
|
43
74
|
using namespace logger;
|
|
@@ -146,8 +177,24 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
146
177
|
// All retries exhausted - schedule delayed reconnection
|
|
147
178
|
// Adding a significant delay before reconnection prevents rapid reconnection loops
|
|
148
179
|
// when the TCP server is persistently unavailable or overloaded
|
|
149
|
-
Log(CONNECTION, "tcp_err(): All %d retries exhausted
|
|
150
|
-
TCP_CONNECT_MAX_RETRIES + 1,
|
|
180
|
+
Log(CONNECTION, "tcp_err(): All %d retries exhausted for IP %s\n",
|
|
181
|
+
TCP_CONNECT_MAX_RETRIES + 1, ip.toString().c_str());
|
|
182
|
+
|
|
183
|
+
// Block this node temporarily to prevent immediate reconnection to the same unresponsive node
|
|
184
|
+
// This helps when the bridge's TCP server is down but WiFi AP is still running
|
|
185
|
+
// The blocklist is only used during AP filtering, so it won't affect existing connections
|
|
186
|
+
#if !defined(PAINLESSMESH_BOOST)
|
|
187
|
+
// Try to decode nodeId from IP and block it
|
|
188
|
+
// Only works for mesh IPs (format: 10.x.x.1)
|
|
189
|
+
uint32_t failedNodeId = decodeNodeIdFromIP(ip);
|
|
190
|
+
if (failedNodeId != 0) {
|
|
191
|
+
// Note: This requires M to be wifi::Mesh which has blockNodeAfterTCPFailure
|
|
192
|
+
mesh.blockNodeAfterTCPFailure(ip, TCP_FAILURE_BLOCK_DURATION_MS);
|
|
193
|
+
}
|
|
194
|
+
#endif
|
|
195
|
+
|
|
196
|
+
Log(CONNECTION, "tcp_err(): Scheduling WiFi reconnection in %u ms\n",
|
|
197
|
+
TCP_EXHAUSTION_RECONNECT_DELAY_MS);
|
|
151
198
|
|
|
152
199
|
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
153
200
|
// Use the centralized deletion scheduler to ensure proper spacing between deletions
|