@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 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 `master` 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 `master` branch.
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 GitLab then click on the button provided by that pop up to create a pull request.
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 `master`.
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.2** - Consolidated release across all distribution channels
7
+ **Version 1.9.6** - Latest release with TCP connection improvements and documentation updates
8
8
 
9
9
  [![CI/CD Pipeline](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml/badge.svg)](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
10
10
  [![Documentation](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml/badge.svg)](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
- DynamicJsonDocument doc(1024);
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.0 (November 30, 2025)
556
+ ## Latest Release: v1.9.6 (December 10, 2025)
557
557
 
558
- **Major Update: Improved Bridge Detection, Consolidated Examples & Documentation**
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
- - ⚡ **Election Guard** - Skip election trigger when node is disconnected from mesh
563
- - 📦 **Consolidated Examples** - Reduced from 32 to 14 essential examples
564
- - 📚 **Cleaned Documentation** - Removed obsolete docs, kept only essentials
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](http://painlessmesh.gitlab.io/painlessMesh/index.html)** - Generated API docs
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 6.x, TaskScheduler 3.x
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 `master` 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 `master` branch. For more details see the [CONTRIBUTING](https://gitlab.com/painlessMesh/painlessMesh/blob/master/CONTRIBUTING.md) file.
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
- [`connectMode`](https://gitlab.com/painlessMesh/painlessMesh/wikis/connect-mode:-WIFI_AP,-WIFI_STA,-WIFI_AP_STA-mode) = switch between WIFI_AP, WIFI_STA and WIFI_AP_STA (default) mode
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 [an SNTP based protocol](https://gitlab.com/painlessMesh/painlessMesh/wikis/mesh-protocol#time-sync)
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](http://painlessmesh.gitlab.io/painlessMesh/index.html)**
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
- // - At least one node must be a bridge/gateway with Internet access
28
- // - OR use initAsSharedGateway() so all nodes have Internet
29
- // - Enable sendToInternet() after mesh.init(): mesh.enableSendToInternet()
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
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/Alteriom/painlessMesh"
8
8
  },
9
- "version": "1.9.4",
9
+ "version": "1.9.6",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=Alteriom PainlessMesh
2
- version=1.9.4
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.4",
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",
@@ -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
- AsyncClient *pConn = new AsyncClient();
719
- painlessmesh::tcp::connect<Connection, painlessmesh::Mesh<Connection>>(
720
- (*pConn), targetIP, targetPort, (*this));
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 new node using raw message to preserve type BRIDGE_STATUS
1158
- // Using sendSingle() would wrap it in type 1 (SINGLE) and hide type BRIDGE_STATUS
1159
- protocol::Variant variant(msg);
1160
- router::send<Connection>(variant, (*this));
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
@@ -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,
@@ -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
- client.onError([&mesh](void *, AsyncClient *client, int8_t err) {
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", err);
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
  }