@alteriom/painlessmesh 1.9.4 → 1.9.6
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 +58 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +21 -12
- package/docs/README.md +1 -1
- package/docs/troubleshooting/common-issues.md +75 -0
- package/examples/bridge_failover/bridge_failover.ino +3 -0
- package/examples/sendToInternet/sendToInternet.ino +21 -3
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/arduino/wifi.hpp +56 -9
- package/src/painlessMeshSTA.cpp +14 -3
- package/src/painlessmesh/mesh.hpp +1 -1
- package/src/painlessmesh/tcp.hpp +98 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,64 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.6] - 2025-12-10
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **TCP Connection Retry Improvements** (#231) - Improved TCP connection reliability with increased retries and exponential backoff
|
|
13
|
+
- **Root Cause**: Nodes experience endless loop of WiFi connect → TCP error -14 → WiFi disconnect because the TCP retry mechanism wasn't sufficient for real-world mesh conditions
|
|
14
|
+
- **Symptom**: Mesh connections never fully establish; nodes can discover and get IP from bridge but TCP connection consistently fails
|
|
15
|
+
- **Solution**: Improved TCP connection retry parameters and added exponential backoff:
|
|
16
|
+
- Increased `TCP_CONNECT_STABILIZATION_DELAY_MS` from 100ms to 500ms (more time for network stack to stabilize after IP acquisition)
|
|
17
|
+
- Increased `TCP_CONNECT_RETRY_DELAY_MS` from 500ms to 1000ms (base delay between retries)
|
|
18
|
+
- Increased `TCP_CONNECT_MAX_RETRIES` from 3 to 5 (more retry attempts before giving up)
|
|
19
|
+
- Added exponential backoff: retry delays are 1s, 2s, 4s, 8s, 8s (capped) for attempts 1-5
|
|
20
|
+
- **Impact**: More reliable mesh connection establishment, especially when bridge TCP server is temporarily busy or network is congested
|
|
21
|
+
|
|
22
|
+
### Documentation
|
|
23
|
+
|
|
24
|
+
- **README.md Comprehensive Review** - Updated main README for completeness and accuracy
|
|
25
|
+
- Updated version references from 1.9.2 to 1.9.6
|
|
26
|
+
- Verified all documentation links and references
|
|
27
|
+
- Confirmed package type documentation accuracy
|
|
28
|
+
- Validated installation instructions
|
|
29
|
+
- Updated "Latest Release" section with current features
|
|
30
|
+
- Fixed ArduinoJson v7 code examples (DynamicJsonDocument → JsonDocument)
|
|
31
|
+
- Updated dependency versions in documentation (ArduinoJson 6.x→7.x, TaskScheduler 3.x→4.x)
|
|
32
|
+
- Corrected API Documentation links (GitLab → GitHub Pages)
|
|
33
|
+
- Fixed Contributing section references (master→main, GitLab→GitHub)
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Version Consistency** - Synchronized version numbers across all distribution files
|
|
38
|
+
- Updated library.properties to v1.9.6
|
|
39
|
+
- Updated library.json to v1.9.6
|
|
40
|
+
- Updated package.json to v1.9.6
|
|
41
|
+
- Ensures consistent versioning for NPM, PlatformIO, and Arduino Library Manager
|
|
42
|
+
|
|
43
|
+
## [1.9.5] - 2025-12-03
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **Bridge Status Send Race Condition** (#224) - Fixed sendToInternet failures caused by race condition
|
|
48
|
+
- **Root Cause**: Bridge would attempt to send status messages to nodes that had already disconnected during the 500ms delay after `changedConnectionCallbacks`
|
|
49
|
+
- **Symptom**: Silent failures when the connection times out before the delayed task executes, causing sendToInternet to fail
|
|
50
|
+
- **Solution**: Added connection validation before sending bridge status:
|
|
51
|
+
- Check `findRoute()` and `conn->connected()` before attempting to send
|
|
52
|
+
- Use direct high-priority send via `conn->addMessage(msg, true)` to avoid redundant routing lookup
|
|
53
|
+
- Added debug logging for cancelled sends to aid troubleshooting
|
|
54
|
+
- **Impact**: Improved reliability of sendToInternet by ensuring bridge status is only sent to active connections
|
|
55
|
+
|
|
56
|
+
- **Isolated Bridge Retry Mechanism** (#225) - Fixed isolated bridge retry when mesh network not found
|
|
57
|
+
- Nodes that fail initial bridge setup can now retry automatically via mesh connection monitoring
|
|
58
|
+
- **Impact**: More reliable bridge establishment in challenging network conditions
|
|
59
|
+
|
|
60
|
+
- **Isolated Bridge Retry Delay After Failed Promotion** - Fixed slow retry after failed bridge promotion
|
|
61
|
+
- **Root Cause**: When bridge promotion fails, `init()` is called which resets `consecutiveEmptyScans` to 0. The isolated retry task would then wait for 6+ new empty scans (90 seconds) before retrying.
|
|
62
|
+
- **Symptom**: After a failed bridge promotion, retries only happen every ~2 minutes instead of ~60 seconds
|
|
63
|
+
- **Solution**: Added `_isolatedRetryPending` flag that is set when promotion fails. This flag allows the next retry attempt to skip the empty scan threshold check.
|
|
64
|
+
- **Impact**: Faster retry after failed promotion - retries happen at the normal 60 second interval instead of waiting for scan accumulation
|
|
65
|
+
|
|
8
66
|
## [1.9.4] - 2025-12-03
|
|
9
67
|
|
|
10
68
|
### Fixed
|
package/CONTRIBUTING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Contributing
|
|
2
2
|
|
|
3
|
-
We try to follow the [git flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) development model. Which means that we have a `develop` branch and `
|
|
3
|
+
We try to follow the [git flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) development model. Which means that we have a `develop` branch and `main` branch. All development is done under feature branches, which are (when finished) merged into the development branch. When a new version is released we merge the `develop` branch into the `main` branch.
|
|
4
4
|
|
|
5
5
|
## Git flow
|
|
6
6
|
|
|
@@ -8,9 +8,9 @@ If you would like to use [git flow tools](http://danielkummer.github.io/git-flow
|
|
|
8
8
|
|
|
9
9
|
## Submit a pull request:
|
|
10
10
|
|
|
11
|
-
* If your push triggered a 'you just pushed...' message from
|
|
11
|
+
* If your push triggered a 'you just pushed...' message from GitHub then click on the button provided by that pop up to create a pull request.
|
|
12
12
|
* If not, then create a pull request and point it to your branch.
|
|
13
|
-
* Make sure that you're attempting to merge into `develop` and not `
|
|
13
|
+
* Make sure that you're attempting to merge into `develop` and not `main`.
|
|
14
14
|
* Get your code reviewed by another contributor. If there are no contributors who possess the same set of skills then get them to review it anyway but explain what the code does beforehand and why. Use it as an opportunity for discussion around the feature set, to transfer knowledge, and to possibly [rubber duck](https://en.wikipedia.org/wiki/Rubber_duck_debugging) your code.
|
|
15
15
|
* Once the code is reviewed then have your reviewer merge your code.
|
|
16
16
|
|
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.6** - Latest release with TCP connection improvements and documentation updates
|
|
8
8
|
|
|
9
9
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
|
|
@@ -418,7 +418,7 @@ void loop() {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
void receivedCallback(uint32_t from, String& msg) {
|
|
421
|
-
|
|
421
|
+
JsonDocument doc; // ArduinoJson v7
|
|
422
422
|
deserializeJson(doc, msg);
|
|
423
423
|
|
|
424
424
|
if (doc["type"] == 200) { // SensorPackage
|
|
@@ -553,15 +553,24 @@ These are the message types used by applications built on painlessMesh:
|
|
|
553
553
|
- **Event Coordination** - Synchronized displays, distributed processing
|
|
554
554
|
- **Bridge Networks** - Connect mesh to WiFi/Internet/MQTT - [📖 Bridge Guide](BRIDGE_TO_INTERNET.md)
|
|
555
555
|
|
|
556
|
-
## Latest Release: v1.9.
|
|
556
|
+
## Latest Release: v1.9.6 (December 10, 2025)
|
|
557
557
|
|
|
558
|
-
**
|
|
558
|
+
**TCP Connection Improvements & Documentation Update**
|
|
559
|
+
|
|
560
|
+
- ⚡ **TCP Connection Retry Improvements** - Enhanced reliability with exponential backoff (#231)
|
|
561
|
+
- Increased stabilization delay (100ms → 500ms)
|
|
562
|
+
- Increased retry delay (500ms → 1000ms) with exponential backoff
|
|
563
|
+
- More retry attempts (3 → 5) for better connection establishment
|
|
564
|
+
- 📚 **Comprehensive Documentation Review** - Updated README.md for completeness and accuracy
|
|
565
|
+
- 🔄 **Version Consistency** - Aligned version numbers across all distribution channels
|
|
566
|
+
|
|
567
|
+
**Recent Key Features (v1.9.0 - v1.9.5):**
|
|
559
568
|
|
|
560
569
|
- 🔍 **Mesh Connectivity Detection** - New `hasActiveMeshConnections()` and `getLastKnownBridge()` APIs
|
|
561
570
|
- 🌉 **Improved Bridge Detection** - `getPrimaryBridge()` returns last known bridge when disconnected
|
|
562
|
-
- ⚡ **
|
|
563
|
-
-
|
|
564
|
-
-
|
|
571
|
+
- ⚡ **Enhanced TCP Reliability** - Exponential backoff and increased retries for mesh connections
|
|
572
|
+
- 🛡️ **Race Condition Fixes** - Improved bridge status and connection validation
|
|
573
|
+
- 📦 **Consolidated Examples** - Streamlined to 14 essential examples
|
|
565
574
|
- ⚙️ **Configurable Election Timing** - Prevent split-brain with `setElectionStartupDelay()` and `setElectionRandomDelay()`
|
|
566
575
|
|
|
567
576
|
**[📋 Full CHANGELOG](CHANGELOG.md)**
|
|
@@ -572,7 +581,7 @@ These are the message types used by applications built on painlessMesh:
|
|
|
572
581
|
- **[Common Issues](docs/troubleshooting/common-issues.md)** - Troubleshooting guide
|
|
573
582
|
- **[GitHub Issues](https://github.com/Alteriom/painlessMesh/issues)** - Bug reports and feature requests
|
|
574
583
|
- **[Community Forum](https://groups.google.com/forum/#!forum/painlessmesh-user)** - Community support
|
|
575
|
-
- **[API Documentation](
|
|
584
|
+
- **[API Documentation](https://alteriom.github.io/painlessMesh/#/api/doxygen)** - Generated API docs
|
|
576
585
|
|
|
577
586
|
## Development
|
|
578
587
|
|
|
@@ -590,7 +599,7 @@ run-parts --regex catch_ bin/ # Run tests
|
|
|
590
599
|
### Requirements
|
|
591
600
|
|
|
592
601
|
- **ESP32/ESP8266**: Arduino Core 2.0.0+
|
|
593
|
-
- **Dependencies**: ArduinoJson
|
|
602
|
+
- **Dependencies**: ArduinoJson 7.x, TaskScheduler 4.x
|
|
594
603
|
- **Development**: CMake, Ninja, Boost (for desktop testing)
|
|
595
604
|
|
|
596
605
|
### CI/CD Pipeline
|
|
@@ -628,7 +637,7 @@ See [RELEASE_GUIDE.md](RELEASE_GUIDE.md) for complete release documentation.
|
|
|
628
637
|
|
|
629
638
|
## Contributing
|
|
630
639
|
|
|
631
|
-
We try to follow the [git flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) development model. Which means that we have a `develop` branch and `
|
|
640
|
+
We try to follow the [git flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) development model. Which means that we have a `develop` branch and `main` branch. All development is done under feature branches, which are (when finished) merged into the development branch. When a new version is released we merge the `develop` branch into the `main` branch. For more details see the [CONTRIBUTING.md](CONTRIBUTING.md) file.
|
|
632
641
|
|
|
633
642
|
## Funding
|
|
634
643
|
|
|
@@ -726,7 +735,7 @@ Initialize the mesh network. This routine does the following things.
|
|
|
726
735
|
`ssid` = the name of your mesh. All nodes share same AP ssid. They are distinguished by BSSID.
|
|
727
736
|
`password` = wifi password to your mesh.
|
|
728
737
|
`port` = the TCP port that you want the mesh server to run on. Defaults to 5555 if not specified.
|
|
729
|
-
|
|
738
|
+
`connectMode` = switch between WIFI_AP, WIFI_STA and WIFI_AP_STA (default) mode
|
|
730
739
|
|
|
731
740
|
#### void painlessMesh::stop()
|
|
732
741
|
|
|
@@ -813,7 +822,7 @@ Return the chipId of the node that we are running on.
|
|
|
813
822
|
|
|
814
823
|
Returns the mesh timebase microsecond counter. Rolls over 71 minutes from startup of the first node.
|
|
815
824
|
|
|
816
|
-
Nodes try to keep a common time base synchronizing to each other using
|
|
825
|
+
Nodes try to keep a common time base synchronizing to each other using an SNTP based protocol
|
|
817
826
|
|
|
818
827
|
#### bool painlessMesh::startDelayMeas(uint32_t nodeId)
|
|
819
828
|
|
package/docs/README.md
CHANGED
|
@@ -115,7 +115,7 @@ Welcome to the comprehensive documentation for the Alteriom fork of painlessMesh
|
|
|
115
115
|
## Quick Links
|
|
116
116
|
|
|
117
117
|
- **[GitHub Repository](https://github.com/Alteriom/painlessMesh)**
|
|
118
|
-
- **[API Documentation](
|
|
118
|
+
- **[API Documentation](https://alteriom.github.io/painlessMesh/#/api/doxygen)**
|
|
119
119
|
- **[Community Forum](https://groups.google.com/forum/#!forum/painlessmesh-user)**
|
|
120
120
|
- **[Issue Tracker](https://github.com/Alteriom/painlessMesh/issues)**
|
|
121
121
|
|
|
@@ -149,6 +149,81 @@ void checkMemory() {
|
|
|
149
149
|
}
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
### TCP Connection Error -14 (ERR_CONN)
|
|
153
|
+
|
|
154
|
+
**Symptoms:**
|
|
155
|
+
- Serial output shows: `tcp_err(): error trying to connect -14`
|
|
156
|
+
- Nodes get an IP address but fail to establish mesh connection
|
|
157
|
+
- Connection attempts keep failing and retrying
|
|
158
|
+
|
|
159
|
+
**Cause:**
|
|
160
|
+
|
|
161
|
+
The error -14 (ERR_CONN in LwIP) indicates a TCP connection failure. This typically occurs when:
|
|
162
|
+
1. The TCP server on the target node is not ready when the connection is attempted
|
|
163
|
+
2. There's a timing issue between WiFi association and TCP readiness
|
|
164
|
+
3. Network stack hasn't fully stabilized after IP acquisition
|
|
165
|
+
4. The target node is overloaded or has resource constraints
|
|
166
|
+
|
|
167
|
+
**Solutions:**
|
|
168
|
+
|
|
169
|
+
#### 1. Update AsyncTCP Library (Most Common Fix)
|
|
170
|
+
The error often occurs with older AsyncTCP versions that don't have proper thread safety for ESP32 Arduino Core 3.x:
|
|
171
|
+
|
|
172
|
+
For PlatformIO:
|
|
173
|
+
```ini
|
|
174
|
+
lib_deps =
|
|
175
|
+
esp32async/AsyncTCP @ ^3.4.7
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
For Arduino IDE, install manually from: https://github.com/ESP32Async/AsyncTCP
|
|
179
|
+
|
|
180
|
+
#### 2. Built-in Retry Mechanism
|
|
181
|
+
painlessMesh now includes automatic TCP connection retry with the following behavior:
|
|
182
|
+
- Up to 3 retry attempts with 500ms delay between each
|
|
183
|
+
- 100ms stabilization delay after IP acquisition before first connection attempt
|
|
184
|
+
- Full WiFi reconnection only triggered after all retries are exhausted
|
|
185
|
+
|
|
186
|
+
This helps handle transient timing issues automatically.
|
|
187
|
+
|
|
188
|
+
#### 3. Check Node Resource Usage
|
|
189
|
+
Monitor memory and ensure nodes aren't overloaded:
|
|
190
|
+
|
|
191
|
+
```cpp
|
|
192
|
+
void loop() {
|
|
193
|
+
mesh.update();
|
|
194
|
+
|
|
195
|
+
// Monitor health periodically
|
|
196
|
+
static unsigned long lastCheck = 0;
|
|
197
|
+
if (millis() - lastCheck > 10000) {
|
|
198
|
+
lastCheck = millis();
|
|
199
|
+
Serial.printf("Free heap: %d, WiFi RSSI: %d\n",
|
|
200
|
+
ESP.getFreeHeap(), WiFi.RSSI());
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### 4. Ensure Proper Initialization Order
|
|
206
|
+
Make sure the mesh is properly initialized before connections are attempted:
|
|
207
|
+
|
|
208
|
+
```cpp
|
|
209
|
+
void setup() {
|
|
210
|
+
Serial.begin(115200);
|
|
211
|
+
delay(100); // Let serial initialize
|
|
212
|
+
|
|
213
|
+
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
|
|
214
|
+
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
|
|
215
|
+
// Add callbacks after init
|
|
216
|
+
mesh.onReceive(&receivedCallback);
|
|
217
|
+
mesh.onNewConnection(&newConnectionCallback);
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### 5. Check WiFi Signal Strength
|
|
222
|
+
Poor signal can cause connection timing issues:
|
|
223
|
+
- Ensure nodes are within good WiFi range
|
|
224
|
+
- Check for interference from other 2.4GHz devices
|
|
225
|
+
- Monitor RSSI values (should be above -80 dBm for reliable connections)
|
|
226
|
+
|
|
152
227
|
## Message Delivery Issues
|
|
153
228
|
|
|
154
229
|
### Messages Not Being Received
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
//
|
|
16
16
|
// To send data to the Internet from a regular node:
|
|
17
17
|
// 1. Use mesh.sendToInternet() to route through a gateway
|
|
18
|
+
// - Call mesh.enableSendToInternet() on the sending node after mesh.init()
|
|
19
|
+
// - Bridge nodes (this example) do NOT need enableSendToInternet() - they route automatically
|
|
20
|
+
// - See examples/sendToInternet/sendToInternet.ino for complete usage
|
|
18
21
|
// 2. Use initAsSharedGateway() so all nodes have router access
|
|
19
22
|
// NOTE: initAsSharedGateway() requires ROUTER credentials:
|
|
20
23
|
// mesh.initAsSharedGateway(MESH_PREFIX, MESH_PASSWORD,
|
|
@@ -24,9 +24,27 @@
|
|
|
24
24
|
// - Smart home sensors reporting to home automation servers
|
|
25
25
|
//
|
|
26
26
|
// Prerequisites:
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
//
|
|
28
|
+
// 1. GATEWAY SETUP (Choose one approach):
|
|
29
|
+
//
|
|
30
|
+
// Option A - Dedicated Bridge (Recommended):
|
|
31
|
+
// - Use initAsBridge() on ONE node (see examples/bridge/bridge.ino)
|
|
32
|
+
// - The bridge node automatically handles Internet routing
|
|
33
|
+
//
|
|
34
|
+
// Option B - Shared Gateway (All nodes have router access):
|
|
35
|
+
// - Use initAsSharedGateway() on ALL nodes (see examples/sharedGateway/sharedGateway.ino)
|
|
36
|
+
// - Requires ROUTER_SSID and ROUTER_PASSWORD on every node
|
|
37
|
+
// - All nodes connect directly to the router for Internet access
|
|
38
|
+
//
|
|
39
|
+
// Option C - Failover Bridge (High Availability):
|
|
40
|
+
// - Use bridge_failover example unchanged (see examples/bridge_failover/bridge_failover.ino)
|
|
41
|
+
// - Automatically elects backup bridges if primary fails
|
|
42
|
+
// - Works as-is without any modifications needed!
|
|
43
|
+
//
|
|
44
|
+
// 2. SENDING NODE SETUP:
|
|
45
|
+
// - Call mesh.enableSendToInternet() AFTER mesh.init() on nodes that will SEND requests
|
|
46
|
+
// - Bridge nodes do NOT need to call enableSendToInternet() - they route automatically
|
|
47
|
+
// - This example shows how to enable it in the setup() function below
|
|
30
48
|
//
|
|
31
49
|
// For Callmebot WhatsApp API:
|
|
32
50
|
// - Get your API key from https://www.callmebot.com/blog/free-api-whatsapp-messages/
|
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.6
|
|
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.6",
|
|
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",
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -229,8 +229,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
229
229
|
|
|
230
230
|
// Only retry when isolated (no mesh connections found)
|
|
231
231
|
if (this->hasActiveMeshConnections()) {
|
|
232
|
-
// Reset retry counter when mesh is active
|
|
232
|
+
// Reset retry counter and pending flag when mesh is active (no longer isolated)
|
|
233
233
|
_isolatedBridgeRetryAttempts = 0;
|
|
234
|
+
_isolatedRetryPending = false;
|
|
234
235
|
return;
|
|
235
236
|
}
|
|
236
237
|
|
|
@@ -249,13 +250,17 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
249
250
|
|
|
250
251
|
// Check if mesh network exists on any channel before trying to become bridge
|
|
251
252
|
// If mesh exists but we can't connect, don't try to become bridge
|
|
253
|
+
// Skip this check if we already confirmed isolation from a previous failed attempt
|
|
252
254
|
uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
|
|
253
|
-
if (emptyScans < ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD) {
|
|
255
|
+
if (!_isolatedRetryPending && emptyScans < ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD) {
|
|
254
256
|
Log(CONNECTION, "Isolated bridge retry: Only %d empty scans, waiting for more scans\n",
|
|
255
257
|
emptyScans);
|
|
256
258
|
return;
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
// Clear the pending flag now that we're proceeding
|
|
262
|
+
_isolatedRetryPending = false;
|
|
263
|
+
|
|
259
264
|
Log(CONNECTION, "Isolated bridge retry: Node isolated with %d empty scans, attempting bridge promotion\n",
|
|
260
265
|
emptyScans);
|
|
261
266
|
|
|
@@ -703,6 +708,15 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
703
708
|
* - Needs access to mesh state and callbacks
|
|
704
709
|
* - Moving it would increase complexity without clear benefits
|
|
705
710
|
* - The existing design keeps connection logic cohesive with WiFi management
|
|
711
|
+
*
|
|
712
|
+
* TCP Connection Retry:
|
|
713
|
+
* The TCP connection now includes automatic retry with exponential backoff.
|
|
714
|
+
* If the initial connection fails (error -14 ERR_CONN or other errors),
|
|
715
|
+
* the system will retry up to TCP_CONNECT_MAX_RETRIES times before
|
|
716
|
+
* triggering a full WiFi reconnection cycle. This helps handle:
|
|
717
|
+
* - Timing issues where TCP server is not ready immediately
|
|
718
|
+
* - Network stack stabilization after IP acquisition
|
|
719
|
+
* - Transient network conditions
|
|
706
720
|
*/
|
|
707
721
|
void tcpConnect() {
|
|
708
722
|
using namespace logger;
|
|
@@ -715,9 +729,26 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
715
729
|
IPAddress targetIP = stationScan.manualIP ? stationScan.manualIP : WiFi.gatewayIP();
|
|
716
730
|
uint16_t targetPort = stationScan.port;
|
|
717
731
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
732
|
+
Log(CONNECTION, "tcpConnect(): Connecting to %s:%d\n",
|
|
733
|
+
targetIP.toString().c_str(), targetPort);
|
|
734
|
+
|
|
735
|
+
// Add a small stabilization delay before attempting TCP connection
|
|
736
|
+
// This helps prevent error -14 (ERR_CONN) by allowing the network stack
|
|
737
|
+
// and TCP server to be fully ready. The delay is added via task scheduler
|
|
738
|
+
// to avoid blocking the event loop.
|
|
739
|
+
this->addTask(painlessmesh::tcp::TCP_CONNECT_STABILIZATION_DELAY_MS, TASK_ONCE,
|
|
740
|
+
[this, targetIP, targetPort]() {
|
|
741
|
+
// Verify WiFi is still connected after the delay
|
|
742
|
+
if (WiFi.status() != WL_CONNECTED || !WiFi.localIP()) {
|
|
743
|
+
Log(CONNECTION, "tcpConnect(): WiFi disconnected during stabilization delay\n");
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
Log(CONNECTION, "tcpConnect(): Starting TCP connection after stabilization\n");
|
|
748
|
+
AsyncClient *pConn = new AsyncClient();
|
|
749
|
+
painlessmesh::tcp::connect<Connection, painlessmesh::Mesh<Connection>>(
|
|
750
|
+
(*pConn), targetIP, targetPort, (*this));
|
|
751
|
+
});
|
|
721
752
|
} else {
|
|
722
753
|
Log(ERROR, "tcpConnect(): err Something unexpected in tcpConnect()\n");
|
|
723
754
|
}
|
|
@@ -1129,6 +1160,16 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1129
1160
|
// Small delay to ensure connection is fully stable, then send directly to the new node
|
|
1130
1161
|
// This avoids issues with time sync blocking broadcast messages
|
|
1131
1162
|
this->addTask(500, TASK_ONCE, [this, nodeId]() {
|
|
1163
|
+
// Check if the connection is still valid - the node may have disconnected
|
|
1164
|
+
// during the 500ms delay (e.g., due to timeout or network issues)
|
|
1165
|
+
// This prevents attempting to send messages to dropped connections
|
|
1166
|
+
// findRoute returns nullptr if node is not in the routing table
|
|
1167
|
+
auto conn = router::findRoute<Connection>((*this), nodeId);
|
|
1168
|
+
if (!conn || !conn->connected()) {
|
|
1169
|
+
Log(CONNECTION, "Bridge status send cancelled: Node %u no longer connected\n", nodeId);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1132
1173
|
// Create bridge status message
|
|
1133
1174
|
JsonDocument doc;
|
|
1134
1175
|
JsonObject obj = doc.to<JsonObject>();
|
|
@@ -1154,10 +1195,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1154
1195
|
Log(CONNECTION, "Sending bridge status directly to node %u (Internet: %s)\n",
|
|
1155
1196
|
nodeId, hasInternet ? "YES" : "NO");
|
|
1156
1197
|
|
|
1157
|
-
// Send directly to the
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1160
|
-
|
|
1198
|
+
// Send directly to the connection with high priority
|
|
1199
|
+
// This ensures the message is sent immediately rather than queued
|
|
1200
|
+
// The JSON message format is the same as what router::send() produces
|
|
1201
|
+
// (Variant serializes to the same JSON format we built manually)
|
|
1202
|
+
conn->addMessage(msg, true);
|
|
1161
1203
|
});
|
|
1162
1204
|
});
|
|
1163
1205
|
|
|
@@ -1691,6 +1733,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1691
1733
|
this->setRouterCredentials(routerSSID, routerPassword);
|
|
1692
1734
|
this->enableBridgeFailover(true);
|
|
1693
1735
|
|
|
1736
|
+
// Set flag to skip empty scan check on next retry attempt
|
|
1737
|
+
// since we already confirmed isolation before this failed attempt
|
|
1738
|
+
_isolatedRetryPending = true;
|
|
1739
|
+
|
|
1694
1740
|
// Notify via callback
|
|
1695
1741
|
if (bridgeRoleChangedCallback) {
|
|
1696
1742
|
bridgeRoleChangedCallback(false, "Isolated bridge promotion failed - router unreachable");
|
|
@@ -1953,6 +1999,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1953
1999
|
// Isolated bridge retry state and configuration
|
|
1954
2000
|
uint8_t _isolatedBridgeRetryAttempts = 0;
|
|
1955
2001
|
uint32_t _isolatedBridgeRetryResetTime = 0; // Time when retry counter can be reset
|
|
2002
|
+
bool _isolatedRetryPending = false; // Flag to skip empty scan check after failed promotion
|
|
1956
2003
|
static const uint8_t MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS = 5; // Max retry attempts before waiting
|
|
1957
2004
|
static const uint32_t isolatedBridgeRetryIntervalMs = 60000; // Retry every 60 seconds
|
|
1958
2005
|
static const uint32_t isolatedBridgeRetryResetIntervalMs = 300000; // Reset counter after 5 minutes
|
package/src/painlessMeshSTA.cpp
CHANGED
|
@@ -239,13 +239,24 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
|
|
|
239
239
|
mesh->apInit(mesh->getNodeId());
|
|
240
240
|
Log(CONNECTION, "connectToAP(): AP restarted on channel %d\n", detectedChannel);
|
|
241
241
|
}
|
|
242
|
+
// Reset counter only when mesh is found on a new channel
|
|
243
|
+
// This allows isolated bridge retry to continue when mesh is truly absent
|
|
244
|
+
consecutiveEmptyScans = 0;
|
|
242
245
|
} else if (detectedChannel == 0) {
|
|
243
246
|
Log(CONNECTION,
|
|
244
247
|
"connectToAP(): Mesh not found on any channel during re-scan\n");
|
|
248
|
+
// Do NOT reset consecutiveEmptyScans here - mesh is still absent
|
|
249
|
+
// This allows isolated bridge retry mechanism to trigger when
|
|
250
|
+
// the counter exceeds ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD
|
|
251
|
+
} else {
|
|
252
|
+
// detectedChannel == mesh->_meshChannel
|
|
253
|
+
// Mesh found on same channel we're already on - no channel change needed
|
|
254
|
+
// Reset counter since mesh exists, nodes may appear in subsequent scans
|
|
255
|
+
Log(CONNECTION,
|
|
256
|
+
"connectToAP(): Mesh found on current channel %d, no channel change needed\n",
|
|
257
|
+
detectedChannel);
|
|
258
|
+
consecutiveEmptyScans = 0;
|
|
245
259
|
}
|
|
246
|
-
|
|
247
|
-
// Reset counter after re-scan attempt
|
|
248
|
-
consecutiveEmptyScans = 0;
|
|
249
260
|
}
|
|
250
261
|
|
|
251
262
|
if (WiFi.status() == WL_CONNECTED &&
|
|
@@ -3238,7 +3238,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
3238
3238
|
Mesh &, protocol::NodeTree, std::shared_ptr<T> conn);
|
|
3239
3239
|
friend void painlessmesh::tcp::initServer<T, Mesh>(AsyncServer &, Mesh &);
|
|
3240
3240
|
friend void painlessmesh::tcp::connect<T, Mesh>(AsyncClient &, IPAddress,
|
|
3241
|
-
uint16_t, Mesh
|
|
3241
|
+
uint16_t, Mesh &, uint8_t);
|
|
3242
3242
|
};
|
|
3243
3243
|
|
|
3244
3244
|
class Connection : public painlessmesh::layout::Neighbour,
|
package/src/painlessmesh/tcp.hpp
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
namespace painlessmesh {
|
|
12
12
|
namespace tcp {
|
|
13
|
+
|
|
14
|
+
// TCP connection retry configuration
|
|
15
|
+
// These can be tuned for different network conditions
|
|
16
|
+
// Increased values to better handle real-world mesh network conditions where:
|
|
17
|
+
// - TCP server may need more time to be ready after AP initialization
|
|
18
|
+
// - Network stack stabilization takes longer on some hardware
|
|
19
|
+
// - Multiple nodes connecting simultaneously can cause temporary overload
|
|
20
|
+
static const uint8_t TCP_CONNECT_MAX_RETRIES = 5; // Max retry attempts before giving up
|
|
21
|
+
static const uint32_t TCP_CONNECT_RETRY_DELAY_MS = 1000; // Delay between retry attempts (1 second)
|
|
22
|
+
static const uint32_t TCP_CONNECT_STABILIZATION_DELAY_MS = 500; // Delay after IP acquisition (500ms)
|
|
23
|
+
|
|
13
24
|
inline uint32_t encodeNodeId(const uint8_t *hwaddr) {
|
|
14
25
|
using namespace painlessmesh::logger;
|
|
15
26
|
Log(GENERAL, "encodeNodeId():\n");
|
|
@@ -41,12 +52,96 @@ void initServer(AsyncServer &server, M &mesh) {
|
|
|
41
52
|
server.begin();
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Establish TCP connection with retry mechanism and exponential backoff
|
|
57
|
+
*
|
|
58
|
+
* This function attempts to connect to the mesh network via TCP.
|
|
59
|
+
* If the connection fails (error -14 ERR_CONN or other errors), it will
|
|
60
|
+
* retry up to TCP_CONNECT_MAX_RETRIES times before triggering a full
|
|
61
|
+
* WiFi reconnection cycle.
|
|
62
|
+
*
|
|
63
|
+
* The retry mechanism helps handle timing issues where:
|
|
64
|
+
* - The TCP server may not be immediately ready after AP initialization
|
|
65
|
+
* - Network stack may need time to stabilize after IP acquisition
|
|
66
|
+
* - Transient network conditions may cause temporary connection failures
|
|
67
|
+
* - Multiple nodes connecting simultaneously may cause temporary overload
|
|
68
|
+
*
|
|
69
|
+
* Exponential backoff is used to increase delay between retries, which:
|
|
70
|
+
* - Gives the TCP server more time to recover from overload
|
|
71
|
+
* - Reduces network contention when multiple nodes are retrying
|
|
72
|
+
* - Improves overall connection success rate in congested networks
|
|
73
|
+
*
|
|
74
|
+
* @param client AsyncClient to use for connection
|
|
75
|
+
* @param ip Target IP address
|
|
76
|
+
* @param port Target port
|
|
77
|
+
* @param mesh Reference to mesh instance for callbacks
|
|
78
|
+
* @param retryCount Current retry attempt (default 0, used internally for recursion)
|
|
79
|
+
*/
|
|
44
80
|
template <class T, class M>
|
|
45
|
-
void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh
|
|
81
|
+
void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
82
|
+
uint8_t retryCount = 0) {
|
|
46
83
|
using namespace logger;
|
|
47
|
-
|
|
84
|
+
|
|
85
|
+
Log(CONNECTION, "tcp::connect(): Attempting connection to port %d (attempt %d/%d)\n",
|
|
86
|
+
port, retryCount + 1, TCP_CONNECT_MAX_RETRIES + 1);
|
|
87
|
+
|
|
88
|
+
// Store retry count and connection parameters for the error handler
|
|
89
|
+
// We need to capture these by value since they're used in the lambda
|
|
90
|
+
client.onError([&mesh, ip, port, retryCount](void *, AsyncClient *client, int8_t err) {
|
|
48
91
|
if (mesh.semaphoreTake()) {
|
|
49
|
-
Log(CONNECTION, "tcp_err(): error trying to connect %d\n",
|
|
92
|
+
Log(CONNECTION, "tcp_err(): error trying to connect %d (attempt %d/%d)\n",
|
|
93
|
+
err, retryCount + 1, TCP_CONNECT_MAX_RETRIES + 1);
|
|
94
|
+
|
|
95
|
+
// Check if we have retries left - retry logic only works on real hardware
|
|
96
|
+
// In test environment (PAINLESSMESH_BOOST), fall through to dropped connection
|
|
97
|
+
// Note: ip and port are used in retry logic below, suppress unused warnings for test builds
|
|
98
|
+
(void)ip;
|
|
99
|
+
(void)port;
|
|
100
|
+
#if !defined(PAINLESSMESH_BOOST) && (defined(ESP32) || defined(ESP8266))
|
|
101
|
+
if (retryCount < TCP_CONNECT_MAX_RETRIES) {
|
|
102
|
+
// Calculate delay with exponential backoff: base_delay * 2^retryCount
|
|
103
|
+
// This gives increasing time between retries as failures accumulate:
|
|
104
|
+
// - retryCount=0: 1000ms * 1 = 1s
|
|
105
|
+
// - retryCount=1: 1000ms * 2 = 2s
|
|
106
|
+
// - retryCount=2: 1000ms * 4 = 4s
|
|
107
|
+
// - retryCount=3: 1000ms * 8 = 8s (capped at 8)
|
|
108
|
+
// - retryCount=4: 1000ms * 8 = 8s (capped at 8)
|
|
109
|
+
// Cap multiplier at 8 to prevent excessive delays
|
|
110
|
+
uint8_t backoffMultiplier = (retryCount < 3) ? (1U << retryCount) : 8;
|
|
111
|
+
uint32_t retryDelay = TCP_CONNECT_RETRY_DELAY_MS * backoffMultiplier;
|
|
112
|
+
|
|
113
|
+
Log(CONNECTION, "tcp_err(): Scheduling retry in %u ms (backoff x%d)\n",
|
|
114
|
+
retryDelay, backoffMultiplier);
|
|
115
|
+
|
|
116
|
+
// Schedule a retry after a delay using the mesh's task scheduler
|
|
117
|
+
// Note: &mesh is captured by reference because:
|
|
118
|
+
// 1. Mesh is a singleton that lives for the program's lifetime
|
|
119
|
+
// 2. The task scheduler belongs to the mesh, so mesh is always valid when task runs
|
|
120
|
+
// 3. Copying the mesh object is not possible/allowed
|
|
121
|
+
// Recursion depth is strictly bounded by TCP_CONNECT_MAX_RETRIES (default: 5)
|
|
122
|
+
mesh.addTask([&mesh, ip, port, retryCount]() {
|
|
123
|
+
Log(CONNECTION, "tcp_err(): Retrying TCP connection...\n");
|
|
124
|
+
|
|
125
|
+
// Create a new AsyncClient for the retry
|
|
126
|
+
// On success, the client is managed by the Connection object
|
|
127
|
+
// On failure, the onError handler for the new client will handle cleanup
|
|
128
|
+
AsyncClient *pRetryConn = new AsyncClient();
|
|
129
|
+
connect<T, M>((*pRetryConn), ip, port, mesh, retryCount + 1);
|
|
130
|
+
}, retryDelay);
|
|
131
|
+
|
|
132
|
+
// Delete the current failed client to prevent memory leak
|
|
133
|
+
// The AsyncClient is no longer needed after connection failure
|
|
134
|
+
delete client;
|
|
135
|
+
|
|
136
|
+
mesh.semaphoreGive();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// All retries exhausted - clean up the failed client and trigger full reconnection
|
|
141
|
+
Log(CONNECTION, "tcp_err(): All %d retries exhausted, triggering WiFi reconnection\n",
|
|
142
|
+
TCP_CONNECT_MAX_RETRIES + 1);
|
|
143
|
+
delete client;
|
|
144
|
+
#endif
|
|
50
145
|
mesh.droppedConnectionCallbacks.execute(0, true);
|
|
51
146
|
mesh.semaphoreGive();
|
|
52
147
|
}
|