@alteriom/painlessmesh 1.9.14 → 1.9.15
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 +9 -5
- 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 +29 -2
- package/src/painlessMesh.h +1 -1
- package/src/painlessMeshSTA.cpp +58 -2
- package/src/painlessMeshSTA.h +25 -0
- package/src/painlessmesh/tcp.hpp +53 -6
package/CHANGELOG.md
CHANGED
|
@@ -9,15 +9,19 @@ 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
|
-
- TBD
|
|
17
|
-
|
|
18
14
|
### Fixed
|
|
19
15
|
|
|
20
|
-
-
|
|
16
|
+
## [1.9.15] - 2025-12-19
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Release Consolidation** - Consolidated v1.9.13 and v1.9.14 fixes into unified v1.9.15 release
|
|
21
|
+
- Gateway Connection Timeout fix for slow HTTP APIs (WhatsApp/CallmeBot)
|
|
22
|
+
- ESP32-C6 AsyncClient deletion spacing fix for heap corruption prevention
|
|
23
|
+
- Both fixes fully tested and production-ready
|
|
24
|
+
- Complete backward compatibility maintained
|
|
21
25
|
|
|
22
26
|
## [1.9.14] - 2025-12-19
|
|
23
27
|
|
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.15** - Consolidated release with ESP32-C6 spacing fix and gateway timeout improvements
|
|
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.15
|
|
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.15",
|
|
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.15"
|
|
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 15
|
|
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);
|
package/src/painlessMesh.h
CHANGED
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
|
};
|
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
|