@alteriom/painlessmesh 1.9.7 → 1.9.9
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/BRIDGE_TO_INTERNET.md +45 -7
- package/CHANGELOG.md +45 -0
- package/README.md +1 -1
- package/RELEASE_GUIDE.md +45 -2
- package/examples/bridge/bridge.ino +34 -25
- 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 +61 -10
- package/src/painlessMesh.h +2 -2
- package/src/painlessmesh/connection.hpp +41 -1
- package/src/painlessmesh/mesh.hpp +29 -28
- package/src/painlessmesh/tcp.hpp +35 -8
package/BRIDGE_TO_INTERNET.md
CHANGED
|
@@ -4,7 +4,20 @@ You can bridge your mesh network to the Internet by creating a **gateway node**
|
|
|
4
4
|
|
|
5
5
|
## Quick Start (Recommended: Auto Channel Detection)
|
|
6
6
|
|
|
7
|
-
The **
|
|
7
|
+
The **bridge-centric approach** automatically detects your router's channel and configures the mesh accordingly. No manual channel configuration required!
|
|
8
|
+
|
|
9
|
+
### Resilient Initialization (v1.9.7+)
|
|
10
|
+
|
|
11
|
+
**Power-up order no longer matters!** The bridge will initialize successfully even if:
|
|
12
|
+
- Router is not yet powered on
|
|
13
|
+
- Internet connection is unavailable
|
|
14
|
+
- Router is temporarily offline
|
|
15
|
+
|
|
16
|
+
The bridge will:
|
|
17
|
+
- Establish the mesh network immediately
|
|
18
|
+
- Accept connections from mesh nodes right away
|
|
19
|
+
- Retry router connection automatically in the background
|
|
20
|
+
- Update status when router/Internet becomes available
|
|
8
21
|
|
|
9
22
|
```cpp
|
|
10
23
|
#include "painlessMesh.h"
|
|
@@ -25,10 +38,10 @@ void setup() {
|
|
|
25
38
|
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
|
|
26
39
|
|
|
27
40
|
// Single call does everything:
|
|
28
|
-
// 1.
|
|
29
|
-
// 2. Initializes mesh on detected channel
|
|
41
|
+
// 1. Attempts to connect to router and detect its channel
|
|
42
|
+
// 2. Initializes mesh on detected channel (or default if router unavailable)
|
|
30
43
|
// 3. Sets node as root/bridge
|
|
31
|
-
// 4. Maintains router connection
|
|
44
|
+
// 4. Maintains/retries router connection automatically
|
|
32
45
|
mesh.initAsBridge(MESH_PREFIX, MESH_PASSWORD,
|
|
33
46
|
ROUTER_SSID, ROUTER_PASSWORD,
|
|
34
47
|
&userScheduler, MESH_PORT);
|
|
@@ -46,10 +59,10 @@ void receivedCallback(uint32_t from, String& msg) {
|
|
|
46
59
|
}
|
|
47
60
|
```
|
|
48
61
|
|
|
49
|
-
**Expected Output:**
|
|
62
|
+
**Expected Output (Router Available):**
|
|
50
63
|
```
|
|
51
64
|
=== Bridge Mode Initialization ===
|
|
52
|
-
Step 1:
|
|
65
|
+
Step 1: Attempting to connect to router YourRouterSSID...
|
|
53
66
|
✓ Router connected on channel 6
|
|
54
67
|
✓ Router IP: 192.168.1.100
|
|
55
68
|
Step 2: Initializing mesh on channel 6...
|
|
@@ -58,10 +71,35 @@ Step 3: Establishing bridge connection...
|
|
|
58
71
|
=== Bridge Mode Active ===
|
|
59
72
|
Mesh SSID: MyMeshNetwork
|
|
60
73
|
Mesh Channel: 6 (matches router)
|
|
61
|
-
Router: YourRouterSSID
|
|
74
|
+
Router: YourRouterSSID (connected)
|
|
75
|
+
Port: 5555
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Expected Output (Router Unavailable but Visible):**
|
|
79
|
+
```
|
|
80
|
+
=== Bridge Mode Initialization ===
|
|
81
|
+
Step 1: Attempting to connect to router YourRouterSSID...
|
|
82
|
+
⚠ Router connection unavailable during initialization
|
|
83
|
+
⚠ Scanning for router 'YourRouterSSID' to detect channel...
|
|
84
|
+
✓ Router found on channel 6 (not connected, will retry)
|
|
85
|
+
⚠ Proceeding with bridge setup on channel 6
|
|
86
|
+
⚠ Bridge will retry router connection in background
|
|
87
|
+
Step 2: Initializing mesh on channel 6...
|
|
88
|
+
STARTUP: init(): Mesh channel set to 6
|
|
89
|
+
Step 3: Establishing bridge connection...
|
|
90
|
+
=== Bridge Mode Active ===
|
|
91
|
+
Mesh SSID: MyMeshNetwork
|
|
92
|
+
Mesh Channel: 6 (default, router pending)
|
|
93
|
+
Router: YourRouterSSID (will retry)
|
|
62
94
|
Port: 5555
|
|
95
|
+
|
|
96
|
+
INFO: Bridge initialized without router connection
|
|
97
|
+
INFO: Mesh network is active and accepting node connections
|
|
98
|
+
INFO: Router connection will be established automatically when available
|
|
63
99
|
```
|
|
64
100
|
|
|
101
|
+
**Note:** If the router cannot connect but is visible in a WiFi scan, the bridge detects its channel and uses it for the mesh. This minimizes channel switching when the router becomes connectable. If the router is completely invisible (powered off), channel 1 is used as default.
|
|
102
|
+
|
|
65
103
|
### Regular Nodes with Auto-Detection
|
|
66
104
|
|
|
67
105
|
Regular mesh nodes can also auto-detect the mesh channel:
|
package/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
19
|
|
|
20
20
|
- TBD
|
|
21
21
|
|
|
22
|
+
## [1.9.9] - 2025-12-14
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Hard Reset from Heap Corruption in Connection Destructor** - Fixed ESP32/ESP8266 hard resets caused by heap corruption when connections were closed
|
|
27
|
+
- **Root Cause**: AsyncClient objects were being deleted immediately in `~BufferedConnection()` destructor when `eraseClosedConnections()` removed closed connections. The AsyncTCP library was still referencing these objects internally, causing heap corruption and hard resets
|
|
28
|
+
- **Symptom**: Device crashes with "CORRUPT HEAP: Bad head at 0x408388a4. Expected 0xabba1234 got 0xfefefefe" and "assert failed: multi_heap_free multi_heap_poisoning.c:279" when connections are removed from the mesh
|
|
29
|
+
- **Solution**: Deferred AsyncClient deletion in destructor using task scheduler with 500ms delay
|
|
30
|
+
- Store scheduler reference in `BufferedConnection` for use in destructor
|
|
31
|
+
- Schedule AsyncClient deletion with 500ms delay to give AsyncTCP time to complete internal cleanup
|
|
32
|
+
- Use same deferred deletion pattern as error handler fixes (Issues #254, #269)
|
|
33
|
+
- Added fallback for test environments where scheduler may not be available
|
|
34
|
+
- **Impact**: Eliminates hard resets and heap corruption when mesh connections are closed, allows stable mesh network operation
|
|
35
|
+
- **Files Modified**: `src/painlessmesh/connection.hpp` (lines 42-81, 89-91, 192)
|
|
36
|
+
- **Documentation**: See `ISSUE_HARD_RESET_FIX.md` for detailed analysis
|
|
37
|
+
|
|
38
|
+
- **Node Crash During TCP Connection Retries** - Fixed device crashes that occurred after 2-3 TCP connection retry attempts
|
|
39
|
+
- **Root Cause**: AsyncClient objects were being deleted too quickly (0ms delay) after connection errors. The AsyncTCP library needs 200-400ms to complete internal cleanup operations, and accessing the deleted object caused crashes
|
|
40
|
+
- **Symptom**: Device crashes or hangs after 2-3 TCP retry attempts, serial log stops abruptly during retry sequence
|
|
41
|
+
- **Solution**: Increased AsyncClient cleanup delay from 0ms to 500ms
|
|
42
|
+
- Added new constant `TCP_CLIENT_CLEANUP_DELAY_MS = 500` to give AsyncTCP library time to complete internal cleanup
|
|
43
|
+
- Updated both cleanup paths (retry and exhaustion) to use this delay
|
|
44
|
+
- Provides sufficient time for AsyncTCP to finish processing before object deletion
|
|
45
|
+
- **Impact**: Eliminates crashes during TCP connection retries, allows full retry sequence to complete
|
|
46
|
+
- **Files Modified**: `src/painlessmesh/tcp.hpp` (lines 26, 149, 170)
|
|
47
|
+
- **Documentation**: See `ASYNCCLIENT_CLEANUP_FIX.md` for detailed analysis
|
|
48
|
+
|
|
49
|
+
## [1.9.8] - 2025-12-14
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **Heap Corruption on TCP Connection Errors** (#254) - Fixed ESP32 heap corruption crashes during AsyncClient deletion
|
|
54
|
+
- **Root Cause**: AsyncClient objects were being deleted synchronously from within their own error callback handlers, causing heap corruption and use-after-free crashes
|
|
55
|
+
- **Symptom**: ESP32 devices crash with "CORRUPT HEAP: Bad head at 0x4083a398. Expected 0xabba1234 got 0xfefefefe" during TCP connection error handling
|
|
56
|
+
- **Solution**: Deferred AsyncClient deletion using task scheduler to execute after error handler completes
|
|
57
|
+
- Changed from synchronous `delete client` to deferred deletion via `mesh.addTask([client]() { delete client; }, 0)`
|
|
58
|
+
- Deletion now occurs microseconds after error handler returns, preventing use-after-free
|
|
59
|
+
- Added logging for cleanup operations to aid debugging
|
|
60
|
+
- **Impact**: Eliminates heap corruption crashes on ESP32 during TCP connection retries and error conditions
|
|
61
|
+
- **Files Modified**: `src/painlessmesh/tcp.hpp` (lines 138, 149)
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- **README Version Reference** - Updated version banner to 1.9.7 for consistency with release history
|
|
66
|
+
|
|
22
67
|
## [1.9.7] - 2025-12-13
|
|
23
68
|
|
|
24
69
|
### Fixed
|
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.9** - Latest release with AsyncClient cleanup fixes for improved stability
|
|
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/RELEASE_GUIDE.md
CHANGED
|
@@ -2,9 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
This document provides comprehensive instructions for releasing new versions of the Alteriom painlessMesh library across all distribution channels.
|
|
4
4
|
|
|
5
|
+
## ⚠️ Important: Agent Requirements for Releases
|
|
6
|
+
|
|
7
|
+
**Releases MUST be performed by Alteriom AI Agent (`@alteriom-ai-agent`) with full tool access.**
|
|
8
|
+
|
|
9
|
+
### Why This Matters
|
|
10
|
+
|
|
11
|
+
A release requires updating 7 files consistently:
|
|
12
|
+
1. library.properties
|
|
13
|
+
2. library.json
|
|
14
|
+
3. package.json
|
|
15
|
+
4. src/painlessMesh.h
|
|
16
|
+
5. src/AlteriomPainlessMesh.h
|
|
17
|
+
6. README.md
|
|
18
|
+
7. CHANGELOG.md
|
|
19
|
+
|
|
20
|
+
**✅ Correct Agent:** `@alteriom-ai-agent`
|
|
21
|
+
- Has file editing tools (`replace_string_in_file`, `multi_replace_string_in_file`)
|
|
22
|
+
- Can run terminal commands (`run_in_terminal`)
|
|
23
|
+
- Can execute git operations
|
|
24
|
+
- **Use this for release preparation**
|
|
25
|
+
|
|
26
|
+
**❌ Wrong Agent:** `@painlessmesh-coordinator` or specialized agents without tools
|
|
27
|
+
- Lack file editing capabilities
|
|
28
|
+
- Can only provide documentation/checklists
|
|
29
|
+
- Cannot actually perform releases
|
|
30
|
+
- Will result in manual work
|
|
31
|
+
|
|
32
|
+
### Agent-Assisted Release (Recommended)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Ask Alteriom AI Agent to prepare release
|
|
36
|
+
@alteriom-ai-agent Prepare release v1.9.9 with these changes:
|
|
37
|
+
- Fixed ESP8266 WiFiClientSecure scope issue
|
|
38
|
+
- Fixed TCP retry crash with AsyncClient cleanup
|
|
39
|
+
|
|
40
|
+
# Agent will:
|
|
41
|
+
# ✅ Update all 7 version files
|
|
42
|
+
# ✅ Restructure CHANGELOG.md
|
|
43
|
+
# ✅ Run validation: ./scripts/release-agent.sh
|
|
44
|
+
# ✅ Commit: "release: v1.9.9 - Description"
|
|
45
|
+
# ✅ Push to trigger automation
|
|
46
|
+
```
|
|
47
|
+
|
|
5
48
|
## 🚀 Quick Release Process
|
|
6
49
|
|
|
7
|
-
### Standard Release (
|
|
50
|
+
### Standard Release (Manual)
|
|
8
51
|
|
|
9
52
|
```bash
|
|
10
53
|
# 1. Update version using the bump script
|
|
@@ -21,7 +64,7 @@ This document provides comprehensive instructions for releasing new versions of
|
|
|
21
64
|
./scripts/release-agent.sh
|
|
22
65
|
|
|
23
66
|
# 5. If all checks pass, commit and trigger release
|
|
24
|
-
git add library.properties library.json package.json CHANGELOG.md src/*.h
|
|
67
|
+
git add library.properties library.json package.json CHANGELOG.md src/*.h README.md
|
|
25
68
|
git commit -m "release: v1.7.9 - Brief description"
|
|
26
69
|
git push origin main
|
|
27
70
|
```
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
//************************************************************
|
|
2
|
-
// Bridge Node Example - Automatic Channel Detection
|
|
2
|
+
// Bridge Node Example - Automatic Channel Detection & Resilient Initialization
|
|
3
3
|
//
|
|
4
|
-
// This example demonstrates the
|
|
4
|
+
// This example demonstrates the bridge-centric architecture that
|
|
5
5
|
// automatically detects the router's WiFi channel and configures the
|
|
6
6
|
// mesh network accordingly.
|
|
7
7
|
//
|
|
8
8
|
// Features:
|
|
9
|
-
// -
|
|
10
|
-
// -
|
|
9
|
+
// - Resilient initialization: Works even if router is unavailable at boot
|
|
10
|
+
// - Automatically detects router channel when available
|
|
11
|
+
// - Creates mesh network immediately (default channel if needed)
|
|
11
12
|
// - No manual channel configuration required
|
|
12
13
|
// - Automatically sets itself as root node
|
|
14
|
+
// - Retries router connection in background if initially unavailable
|
|
13
15
|
// - Broadcasts bridge status to mesh (Type 610 - BRIDGE_STATUS)
|
|
14
16
|
// - Reports Internet connectivity status
|
|
15
17
|
// - Updates every 30 seconds by default
|
|
16
18
|
// - Enables nodes to implement failover and queueing logic
|
|
17
19
|
//
|
|
20
|
+
// POWER-UP ORDER INDEPENDENCE (v1.9.7+):
|
|
21
|
+
// The bridge now initializes successfully regardless of power-up order:
|
|
22
|
+
// - Bridge can boot before router is ready
|
|
23
|
+
// - Mesh nodes can connect immediately to bridge
|
|
24
|
+
// - Router connection is established automatically when available
|
|
25
|
+
// - No need to restart bridge when router becomes ready
|
|
26
|
+
//
|
|
18
27
|
// For more details, see BRIDGE_TO_INTERNET.md
|
|
19
28
|
//
|
|
20
29
|
// EXTERNAL DEVICE CONNECTIONS:
|
|
@@ -63,29 +72,27 @@ void setup() {
|
|
|
63
72
|
// Set debug message types before init() to see startup messages
|
|
64
73
|
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
|
|
65
74
|
|
|
66
|
-
//
|
|
75
|
+
// Single call to initialize as bridge with auto channel detection
|
|
67
76
|
// This will:
|
|
68
|
-
// 1.
|
|
69
|
-
// 2. Initialize mesh on the detected channel
|
|
70
|
-
// 3. Set this node as root
|
|
71
|
-
// 4. Maintain router connection
|
|
77
|
+
// 1. Attempt to connect to router and detect its channel
|
|
78
|
+
// 2. Initialize mesh on the detected channel (or default if router unavailable)
|
|
79
|
+
// 3. Set this node as root/bridge
|
|
80
|
+
// 4. Maintain/retry router connection automatically
|
|
72
81
|
// 5. Start broadcasting bridge status (Type 610) every 30 seconds
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Serial.println("Note: To function as a bridge, fix router connectivity and restart");
|
|
88
|
-
}
|
|
82
|
+
//
|
|
83
|
+
// RESILIENT INITIALIZATION (v1.9.7+):
|
|
84
|
+
// The bridge will successfully initialize even if the router is unavailable
|
|
85
|
+
// at boot time. It will:
|
|
86
|
+
// - Establish the mesh network immediately on a default channel
|
|
87
|
+
// - Accept connections from mesh nodes right away
|
|
88
|
+
// - Retry router connection automatically in the background
|
|
89
|
+
// - Update bridge status when router becomes available
|
|
90
|
+
//
|
|
91
|
+
// This solves the power-up order issue (Issue #268) where bridge
|
|
92
|
+
// initialization would fail if the router wasn't ready yet.
|
|
93
|
+
mesh.initAsBridge(MESH_PREFIX, MESH_PASSWORD,
|
|
94
|
+
ROUTER_SSID, ROUTER_PASSWORD,
|
|
95
|
+
&userScheduler, MESH_PORT);
|
|
89
96
|
|
|
90
97
|
// Optional: Configure bridge status broadcasting
|
|
91
98
|
// mesh.setBridgeStatusInterval(60000); // Change to 60 seconds
|
|
@@ -98,6 +105,8 @@ void setup() {
|
|
|
98
105
|
mesh.onReceive(&receivedCallback);
|
|
99
106
|
|
|
100
107
|
Serial.println("✓ Bridge node initialized and ready!");
|
|
108
|
+
Serial.println("Mesh network active - accepting node connections");
|
|
109
|
+
Serial.println("Router connection will be established automatically when available");
|
|
101
110
|
Serial.println("Broadcasting bridge status to mesh every 30 seconds");
|
|
102
111
|
}
|
|
103
112
|
|
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.9
|
|
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.9",
|
|
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.9"
|
|
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 9
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @brief Library description and usage information
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -386,9 +386,12 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
386
386
|
using namespace logger;
|
|
387
387
|
|
|
388
388
|
Log(STARTUP, "=== Bridge Mode Initialization ===\n");
|
|
389
|
-
Log(STARTUP, "Step 1:
|
|
389
|
+
Log(STARTUP, "Step 1: Attempting to connect to router %s...\n", routerSSID.c_str());
|
|
390
390
|
|
|
391
|
-
//
|
|
391
|
+
// Store router credentials for future connection attempts
|
|
392
|
+
setRouterCredentials(routerSSID, routerPassword);
|
|
393
|
+
|
|
394
|
+
// Step 1: Attempt to connect to router first to detect its channel
|
|
392
395
|
// Shut Wifi down and start with a blank slate
|
|
393
396
|
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
394
397
|
|
|
@@ -413,6 +416,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
413
416
|
}
|
|
414
417
|
|
|
415
418
|
uint8_t detectedChannel = 1; // Default fallback
|
|
419
|
+
bool routerConnected = false;
|
|
416
420
|
|
|
417
421
|
if (WiFi.status() == WL_CONNECTED) {
|
|
418
422
|
detectedChannel = WiFi.channel();
|
|
@@ -425,18 +429,50 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
425
429
|
} else {
|
|
426
430
|
Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
|
|
427
431
|
Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
|
|
432
|
+
routerConnected = true;
|
|
428
433
|
}
|
|
429
434
|
} else {
|
|
430
|
-
Log(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
435
|
+
Log(STARTUP, "\n⚠ Router connection unavailable during initialization\n");
|
|
436
|
+
|
|
437
|
+
// Scan for router to detect its channel even though we can't connect
|
|
438
|
+
// This minimizes channel mismatch when router becomes available later
|
|
439
|
+
Log(STARTUP, "⚠ Scanning for router '%s' to detect channel...\n", routerSSID.c_str());
|
|
440
|
+
|
|
441
|
+
// ESP32 and ESP8266 have different scanNetworks signatures
|
|
442
|
+
#ifdef ESP32
|
|
443
|
+
int16_t numNetworks = WiFi.scanNetworks(false, false, false, 300U, 0);
|
|
444
|
+
#elif defined(ESP8266)
|
|
445
|
+
int16_t numNetworks = WiFi.scanNetworks(false, false, 0);
|
|
446
|
+
#endif
|
|
447
|
+
|
|
448
|
+
if (numNetworks > 0) {
|
|
449
|
+
for (int16_t i = 0; i < numNetworks; i++) {
|
|
450
|
+
if (WiFi.SSID(i) == routerSSID) {
|
|
451
|
+
uint8_t scannedChannel = WiFi.channel(i);
|
|
452
|
+
if (scannedChannel >= 1 && scannedChannel <= 13) {
|
|
453
|
+
detectedChannel = scannedChannel;
|
|
454
|
+
Log(STARTUP, "✓ Router found on channel %d (not connected, will retry)\n",
|
|
455
|
+
detectedChannel);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
WiFi.scanDelete();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (detectedChannel == 1) {
|
|
464
|
+
Log(STARTUP, "⚠ Router not found in scan, using default channel %d\n", detectedChannel);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
Log(STARTUP, "⚠ Proceeding with bridge setup on channel %d\n", detectedChannel);
|
|
468
|
+
Log(STARTUP, "⚠ Bridge will retry router connection in background\n");
|
|
434
469
|
}
|
|
435
470
|
|
|
436
471
|
Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
|
|
437
472
|
detectedChannel);
|
|
438
473
|
|
|
439
|
-
// Step 2: Initialize mesh on detected channel
|
|
474
|
+
// Step 2: Initialize mesh on detected/default channel
|
|
475
|
+
// This allows the bridge to establish the mesh network even without router
|
|
440
476
|
init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
|
|
441
477
|
detectedChannel, 0, MAX_CONN);
|
|
442
478
|
|
|
@@ -446,10 +482,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
446
482
|
|
|
447
483
|
Log(STARTUP, "Step 3: Establishing bridge connection...\n");
|
|
448
484
|
|
|
449
|
-
// Step 3:
|
|
485
|
+
// Step 3: Establish/re-establish router connection using stationManual
|
|
486
|
+
// If router wasn't available initially, this will be retried automatically
|
|
450
487
|
stationManual(routerSSID, routerPassword, 0);
|
|
451
488
|
|
|
452
489
|
// Step 4: Configure as root/bridge node
|
|
490
|
+
// Bridge role is established regardless of router connectivity
|
|
491
|
+
// This ensures mesh nodes can connect and the bridge can provide mesh services
|
|
453
492
|
this->setRoot(true);
|
|
454
493
|
this->setContainsRoot(true);
|
|
455
494
|
|
|
@@ -461,9 +500,21 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
461
500
|
|
|
462
501
|
Log(STARTUP, "=== Bridge Mode Active ===\n");
|
|
463
502
|
Log(STARTUP, " Mesh SSID: %s\n", meshSSID.c_str());
|
|
464
|
-
Log(STARTUP, " Mesh Channel: %d
|
|
465
|
-
|
|
503
|
+
Log(STARTUP, " Mesh Channel: %d%s\n", detectedChannel,
|
|
504
|
+
routerConnected ? " (matches router)" : " (default, router pending)");
|
|
505
|
+
Log(STARTUP, " Router: %s%s\n", routerSSID.c_str(),
|
|
506
|
+
routerConnected ? " (connected)" : " (will retry)");
|
|
466
507
|
Log(STARTUP, " Port: %d\n", port);
|
|
508
|
+
|
|
509
|
+
if (!routerConnected) {
|
|
510
|
+
Log(STARTUP, "\nINFO: Bridge initialized without router connection\n");
|
|
511
|
+
Log(STARTUP, "INFO: Mesh network is active and accepting node connections\n");
|
|
512
|
+
Log(STARTUP, "INFO: Router connection will be established automatically when available\n");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Return true - bridge mesh functionality is active even without router
|
|
516
|
+
// The mesh network is operational and nodes can connect
|
|
517
|
+
// Router connection will be retried automatically via stationManual
|
|
467
518
|
return true;
|
|
468
519
|
}
|
|
469
520
|
|
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.9
|
|
9
|
+
* @date 2025-12-14
|
|
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
|
|
@@ -14,6 +14,11 @@ extern painlessmesh::logger::LogClass Log;
|
|
|
14
14
|
namespace painlessmesh {
|
|
15
15
|
namespace tcp {
|
|
16
16
|
|
|
17
|
+
// Delay before cleaning up failed AsyncClient after connection error or close
|
|
18
|
+
// This prevents crashes when AsyncTCP library is still accessing the client internally
|
|
19
|
+
// The AsyncTCP library may take a few hundred milliseconds to complete its internal cleanup
|
|
20
|
+
static const uint32_t TCP_CLIENT_CLEANUP_DELAY_MS = 500; // 500ms delay before deleting AsyncClient
|
|
21
|
+
|
|
17
22
|
// Shared buffer for reading/writing to the buffer
|
|
18
23
|
static painlessmesh::buffer::temp_buffer_t shared_buffer;
|
|
19
24
|
|
|
@@ -40,16 +45,50 @@ class BufferedConnection
|
|
|
40
45
|
BufferedConnection(AsyncClient *client) : client(client) {}
|
|
41
46
|
|
|
42
47
|
~BufferedConnection() {
|
|
48
|
+
using namespace logger;
|
|
43
49
|
Log.remote("~BufferedConnection");
|
|
44
50
|
this->close();
|
|
45
51
|
if (!client->freeable()) {
|
|
46
52
|
client->close(true);
|
|
47
53
|
}
|
|
48
54
|
client->abort();
|
|
49
|
-
|
|
55
|
+
|
|
56
|
+
// Defer deletion of the AsyncClient to prevent heap corruption
|
|
57
|
+
// Deleting immediately can cause use-after-free issues when the AsyncTCP
|
|
58
|
+
// library is still referencing the object internally during cleanup
|
|
59
|
+
// See ISSUE_254_HEAP_CORRUPTION_FIX.md and ASYNCCLIENT_CLEANUP_FIX.md
|
|
60
|
+
if (mScheduler) {
|
|
61
|
+
// Capture client pointer by value for safe deferred deletion
|
|
62
|
+
AsyncClient* clientToDelete = client;
|
|
63
|
+
|
|
64
|
+
// Schedule deletion task with TCP_CLIENT_CLEANUP_DELAY_MS delay
|
|
65
|
+
// This gives AsyncTCP library time to complete its internal cleanup
|
|
66
|
+
// Note: Task object is intentionally leaked to keep implementation simple
|
|
67
|
+
// This is acceptable because:
|
|
68
|
+
// 1. Connections are long-lived, destructor calls are infrequent
|
|
69
|
+
// 2. Task object is small (~32-64 bytes) vs preventing critical heap corruption
|
|
70
|
+
// 3. In typical deployments, memory impact is negligible (few KB over months)
|
|
71
|
+
// 4. Alternative cleanup patterns would add significant complexity
|
|
72
|
+
Task* cleanupTask = new Task(TCP_CLIENT_CLEANUP_DELAY_MS * TASK_MILLISECOND, TASK_ONCE, [clientToDelete]() {
|
|
73
|
+
using namespace logger;
|
|
74
|
+
Log(CONNECTION, "~BufferedConnection: Deferred cleanup of AsyncClient\n");
|
|
75
|
+
delete clientToDelete;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
mScheduler->addTask(*cleanupTask);
|
|
79
|
+
cleanupTask->enableDelayed();
|
|
80
|
+
} else {
|
|
81
|
+
// Fallback: If scheduler not available, delete immediately
|
|
82
|
+
// This should only happen in test environments or edge cases
|
|
83
|
+
Log(CONNECTION, "~BufferedConnection: No scheduler available, deleting AsyncClient immediately (risky)\n");
|
|
84
|
+
delete client;
|
|
85
|
+
}
|
|
50
86
|
}
|
|
51
87
|
|
|
52
88
|
void initialize(Scheduler *scheduler) {
|
|
89
|
+
// Store scheduler reference for deferred cleanup in destructor
|
|
90
|
+
mScheduler = scheduler;
|
|
91
|
+
|
|
53
92
|
auto self = this->shared_from_this();
|
|
54
93
|
sentBufferTask.set(TASK_SECOND, TASK_FOREVER, [self]() {
|
|
55
94
|
if (!self->sentBuffer.empty() && self->client->canSend()) {
|
|
@@ -156,6 +195,7 @@ class BufferedConnection
|
|
|
156
195
|
bool mConnected = true;
|
|
157
196
|
|
|
158
197
|
AsyncClient *client;
|
|
198
|
+
Scheduler *mScheduler = nullptr; // Scheduler for deferred AsyncClient cleanup
|
|
159
199
|
|
|
160
200
|
std::function<void(TSTRING)> receiveCallback;
|
|
161
201
|
std::function<void()> disconnectCallback;
|
|
@@ -622,10 +622,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
622
622
|
*
|
|
623
623
|
* Use hasLocalInternet() to check if THIS specific node has direct Internet access.
|
|
624
624
|
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
625
|
+
* This method now ALWAYS requires healthy (recent) bridge status to prevent
|
|
626
|
+
* false positives when mesh connectivity is lost. Without active mesh connections,
|
|
627
|
+
* stale bridge data cannot be relied upon, as the bridge may have lost Internet
|
|
628
|
+
* connectivity or become unreachable.
|
|
629
629
|
*
|
|
630
630
|
* \code
|
|
631
631
|
* if (mesh.hasInternetConnection()) {
|
|
@@ -644,16 +644,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
644
644
|
* @see initAsSharedGateway() to give all nodes direct Internet access (requires router credentials)
|
|
645
645
|
*/
|
|
646
646
|
bool hasInternetConnection() {
|
|
647
|
-
|
|
648
|
-
|
|
647
|
+
// Always require healthy bridge status to prevent false positives
|
|
648
|
+
// when mesh is disconnected. Stale bridge data is unreliable.
|
|
649
649
|
for (const auto& bridge : knownBridges) {
|
|
650
|
-
|
|
651
|
-
// When disconnected: use last known state
|
|
652
|
-
bool isUsable = hasConnections
|
|
653
|
-
? (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected)
|
|
654
|
-
: bridge.internetConnected;
|
|
655
|
-
|
|
656
|
-
if (isUsable) {
|
|
650
|
+
if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
|
|
657
651
|
return true;
|
|
658
652
|
}
|
|
659
653
|
}
|
|
@@ -698,14 +692,17 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
698
692
|
* Get the primary (best) bridge node
|
|
699
693
|
*
|
|
700
694
|
* Primary bridge is selected based on:
|
|
701
|
-
* 1. Must be healthy (seen within timeout)
|
|
695
|
+
* 1. Must be healthy (seen within timeout)
|
|
702
696
|
* 2. Must have Internet connection
|
|
703
697
|
* 3. Best WiFi RSSI to router
|
|
704
698
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
699
|
+
* This method now ALWAYS requires healthy (recent) bridge status to prevent
|
|
700
|
+
* routing messages to unreachable or outdated bridges when mesh connectivity
|
|
701
|
+
* is lost. Without active mesh connections and fresh status, we cannot reliably
|
|
702
|
+
* route messages to any bridge.
|
|
703
|
+
*
|
|
704
|
+
* If you need access to the last known bridge regardless of health status,
|
|
705
|
+
* use getLastKnownBridge() instead.
|
|
709
706
|
*
|
|
710
707
|
* @return pointer to BridgeInfo of primary bridge, or nullptr if no suitable bridge
|
|
711
708
|
*/
|
|
@@ -713,17 +710,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
713
710
|
BridgeInfo* primary = nullptr;
|
|
714
711
|
int8_t bestRSSI = -127; // Worst possible RSSI
|
|
715
712
|
|
|
716
|
-
//
|
|
717
|
-
bool hasConnections = hasActiveMeshConnections();
|
|
718
|
-
|
|
713
|
+
// Always require healthy bridge status to prevent routing to stale/unreachable bridges
|
|
719
714
|
for (auto& bridge : knownBridges) {
|
|
720
|
-
|
|
721
|
-
// When disconnected: use any bridge that reported Internet (stale info is better than none)
|
|
722
|
-
bool isUsable = hasConnections
|
|
723
|
-
? (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected)
|
|
724
|
-
: bridge.internetConnected;
|
|
725
|
-
|
|
726
|
-
if (isUsable) {
|
|
715
|
+
if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
|
|
727
716
|
if (bridge.routerRSSI > bestRSSI) {
|
|
728
717
|
bestRSSI = bridge.routerRSSI;
|
|
729
718
|
primary = &bridge;
|
|
@@ -1290,6 +1279,18 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1290
1279
|
Log(COMMUNICATION, "sendToInternet(): Local Internet available, using gateway protocol for consistency\n");
|
|
1291
1280
|
}
|
|
1292
1281
|
|
|
1282
|
+
// Validate mesh connectivity before attempting to send
|
|
1283
|
+
if (!hasActiveMeshConnections()) {
|
|
1284
|
+
Log(ERROR, "sendToInternet(): No active mesh connections\n");
|
|
1285
|
+
if (callback) {
|
|
1286
|
+
// Schedule callback to avoid blocking
|
|
1287
|
+
this->addTask([callback]() {
|
|
1288
|
+
callback(false, 0, "No mesh connections - cannot route to gateway");
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
return 0;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1293
1294
|
// Find the best gateway to route through
|
|
1294
1295
|
BridgeInfo* gateway = getPrimaryBridge();
|
|
1295
1296
|
if (gateway == nullptr) {
|
package/src/painlessmesh/tcp.hpp
CHANGED
|
@@ -20,6 +20,11 @@ namespace tcp {
|
|
|
20
20
|
static const uint8_t TCP_CONNECT_MAX_RETRIES = 5; // Max retry attempts before giving up
|
|
21
21
|
static const uint32_t TCP_CONNECT_RETRY_DELAY_MS = 1000; // Delay between retry attempts (1 second)
|
|
22
22
|
static const uint32_t TCP_CONNECT_STABILIZATION_DELAY_MS = 500; // Delay after IP acquisition (500ms)
|
|
23
|
+
// TCP_CLIENT_CLEANUP_DELAY_MS is defined in connection.hpp since it's used in the destructor
|
|
24
|
+
// Delay before WiFi reconnection after all TCP retries are exhausted
|
|
25
|
+
// This prevents rapid reconnection loops when TCP server is persistently unavailable
|
|
26
|
+
// Gives the TCP server more time to recover and reduces network congestion
|
|
27
|
+
static const uint32_t TCP_EXHAUSTION_RECONNECT_DELAY_MS = 10000; // 10 seconds before reconnection
|
|
23
28
|
|
|
24
29
|
inline uint32_t encodeNodeId(const uint8_t *hwaddr) {
|
|
25
30
|
using namespace painlessmesh::logger;
|
|
@@ -129,24 +134,46 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
129
134
|
connect<T, M>((*pRetryConn), ip, port, mesh, retryCount + 1);
|
|
130
135
|
}, retryDelay);
|
|
131
136
|
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
137
|
+
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
138
|
+
// Deleting from within the error callback can cause use-after-free issues
|
|
139
|
+
// as the AsyncTCP library may still be referencing the object
|
|
140
|
+
// Use TCP_CLIENT_CLEANUP_DELAY_MS to give AsyncTCP library time to complete
|
|
141
|
+
// its internal cleanup before we delete the object
|
|
142
|
+
// Note: client is captured by value (pointer copy) and we are the sole owner
|
|
143
|
+
mesh.addTask([client]() {
|
|
144
|
+
Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (retry path)\n");
|
|
145
|
+
delete client;
|
|
146
|
+
}, TCP_CLIENT_CLEANUP_DELAY_MS);
|
|
135
147
|
|
|
136
148
|
mesh.semaphoreGive();
|
|
137
149
|
return;
|
|
138
150
|
}
|
|
139
151
|
|
|
140
|
-
// All retries exhausted -
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
// All retries exhausted - schedule delayed reconnection
|
|
153
|
+
// Adding a significant delay before reconnection prevents rapid reconnection loops
|
|
154
|
+
// when the TCP server is persistently unavailable or overloaded
|
|
155
|
+
Log(CONNECTION, "tcp_err(): All %d retries exhausted, scheduling WiFi reconnection in %u ms\n",
|
|
156
|
+
TCP_CONNECT_MAX_RETRIES + 1, TCP_EXHAUSTION_RECONNECT_DELAY_MS);
|
|
157
|
+
|
|
158
|
+
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
159
|
+
// Deleting from within the error callback can cause use-after-free issues
|
|
160
|
+
// as the AsyncTCP library may still be referencing the object
|
|
161
|
+
// Use TCP_CLIENT_CLEANUP_DELAY_MS to give AsyncTCP library time to complete
|
|
162
|
+
// its internal cleanup before we delete the object
|
|
163
|
+
// Note: client is captured by value (pointer copy) and we are the sole owner
|
|
164
|
+
mesh.addTask([client]() {
|
|
165
|
+
Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (exhaustion path)\n");
|
|
166
|
+
delete client;
|
|
167
|
+
}, TCP_CLIENT_CLEANUP_DELAY_MS);
|
|
144
168
|
#endif
|
|
145
169
|
// Defer callback execution to avoid crashes in error handler context
|
|
146
170
|
// Execute callbacks after semaphore is released and error handler completes
|
|
171
|
+
// The delay helps prevent endless rapid reconnection loops by giving the TCP server
|
|
172
|
+
// more time to recover and reducing network congestion from multiple retrying nodes
|
|
147
173
|
mesh.addTask([&mesh]() {
|
|
174
|
+
Log(CONNECTION, "tcp_err(): Executing delayed WiFi reconnection after retry exhaustion\n");
|
|
148
175
|
mesh.droppedConnectionCallbacks.execute(0, true);
|
|
149
|
-
});
|
|
176
|
+
}, TCP_EXHAUSTION_RECONNECT_DELAY_MS);
|
|
150
177
|
mesh.semaphoreGive();
|
|
151
178
|
}
|
|
152
179
|
});
|