@alteriom/painlessmesh 1.9.8 → 1.9.10
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 +60 -0
- package/README.md +2 -2
- package/RELEASE_GUIDE.md +45 -2
- package/docs/troubleshooting/common-issues.md +5 -4
- package/examples/bridge/bridge.ino +34 -25
- package/examples/bridge_failover/bridge_failover.ino +1 -1
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +2 -2
- package/src/AlteriomPainlessMesh.h +2 -2
- package/src/arduino/wifi.hpp +63 -12
- package/src/painlessMesh.h +2 -2
- package/src/painlessmesh/connection.hpp +41 -1
- package/src/painlessmesh/plugin.hpp +14 -2
- package/src/painlessmesh/tcp.hpp +7 -2
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,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
19
|
|
|
20
20
|
- TBD
|
|
21
21
|
|
|
22
|
+
## [1.9.10] - 2025-12-15
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **TCP Connection Retry Immediate Execution** - Fixed mesh connection failures where TCP retries executed immediately instead of with exponential backoff delays
|
|
27
|
+
- **Root Cause**: `PackageHandler::addTask()` was calling `task->enable()` instead of `task->enableDelayed()` for one-shot delayed tasks, causing immediate execution
|
|
28
|
+
- **Symptom**: Nodes unable to establish mesh connection - TCP error -14 (ERR_CONN) with all retry attempts happening immediately rather than with 1s, 2s, 4s, 8s, 8s delays
|
|
29
|
+
- **Solution**: Modified `PackageHandler::addTask()` to use `enableDelayed()` for `TASK_ONCE` tasks with intervals > 0
|
|
30
|
+
- Retry tasks now properly wait for their scheduled delays before executing
|
|
31
|
+
- Exponential backoff mechanism now works as designed (total ~23s before WiFi reconnection)
|
|
32
|
+
- Reduces network congestion from multiple simultaneously retrying nodes
|
|
33
|
+
- **Impact**: Enables successful mesh connection establishment with proper retry timing
|
|
34
|
+
- **Files Modified**:
|
|
35
|
+
- `src/painlessmesh/plugin.hpp` (lines 231, 239, 245-251)
|
|
36
|
+
- `docs/troubleshooting/common-issues.md` (lines 206-212)
|
|
37
|
+
- `test/catch/catch_delayed_task_execution.cpp` (new test file)
|
|
38
|
+
- `ISSUE_TCP_RETRY_FIX.md` (new documentation)
|
|
39
|
+
|
|
40
|
+
- **Hard Reset on Bridge Failover Election Winner** - Fixed ESP32 hard reset (Guru Meditation Error: Load access fault) when node becomes bridge after election
|
|
41
|
+
- **Root Cause**: The `bridgeRoleChangedCallback` signature used pass-by-value for the String parameter (`TSTRING reason`), causing temporary String object creation from const char* literals. On memory-constrained ESP32/ESP8266, this could trigger heap allocation failures and memory access faults
|
|
42
|
+
- **Symptom**: Device crashes with "Guru Meditation Error: Core 0 panic'ed (Load access fault)" immediately after logging "🎯 PROMOTED TO BRIDGE: Election winner - best router signal"
|
|
43
|
+
- **Solution**: Changed callback signature to use const reference (`const TSTRING& reason`) instead of pass-by-value
|
|
44
|
+
- Eliminates unnecessary String object copying and temporary creation
|
|
45
|
+
- Reduces memory pressure during callback invocation
|
|
46
|
+
- String literals are now directly bound to const references without heap allocation
|
|
47
|
+
- **Impact**: Eliminates hard resets during bridge promotion, allows stable failover operation
|
|
48
|
+
- **Breaking Change**: Users must update their callback function signatures from `void callback(bool, String)` to `void callback(bool, const String&)`
|
|
49
|
+
- **Files Modified**:
|
|
50
|
+
- `src/arduino/wifi.hpp` (lines 970, 2326)
|
|
51
|
+
- `examples/bridge_failover/bridge_failover.ino` (line 147)
|
|
52
|
+
- `README.md` (line 171)
|
|
53
|
+
- `USER_GUIDE.md` (line 731)
|
|
54
|
+
|
|
55
|
+
## [1.9.9] - 2025-12-14
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **Hard Reset from Heap Corruption in Connection Destructor** - Fixed ESP32/ESP8266 hard resets caused by heap corruption when connections were closed
|
|
60
|
+
- **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
|
|
61
|
+
- **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
|
|
62
|
+
- **Solution**: Deferred AsyncClient deletion in destructor using task scheduler with 500ms delay
|
|
63
|
+
- Store scheduler reference in `BufferedConnection` for use in destructor
|
|
64
|
+
- Schedule AsyncClient deletion with 500ms delay to give AsyncTCP time to complete internal cleanup
|
|
65
|
+
- Use same deferred deletion pattern as error handler fixes (Issues #254, #269)
|
|
66
|
+
- Added fallback for test environments where scheduler may not be available
|
|
67
|
+
- **Impact**: Eliminates hard resets and heap corruption when mesh connections are closed, allows stable mesh network operation
|
|
68
|
+
- **Files Modified**: `src/painlessmesh/connection.hpp` (lines 42-81, 89-91, 192)
|
|
69
|
+
- **Documentation**: See `ISSUE_HARD_RESET_FIX.md` for detailed analysis
|
|
70
|
+
|
|
71
|
+
- **Node Crash During TCP Connection Retries** - Fixed device crashes that occurred after 2-3 TCP connection retry attempts
|
|
72
|
+
- **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
|
|
73
|
+
- **Symptom**: Device crashes or hangs after 2-3 TCP retry attempts, serial log stops abruptly during retry sequence
|
|
74
|
+
- **Solution**: Increased AsyncClient cleanup delay from 0ms to 500ms
|
|
75
|
+
- Added new constant `TCP_CLIENT_CLEANUP_DELAY_MS = 500` to give AsyncTCP library time to complete internal cleanup
|
|
76
|
+
- Updated both cleanup paths (retry and exhaustion) to use this delay
|
|
77
|
+
- Provides sufficient time for AsyncTCP to finish processing before object deletion
|
|
78
|
+
- **Impact**: Eliminates crashes during TCP connection retries, allows full retry sequence to complete
|
|
79
|
+
- **Files Modified**: `src/painlessmesh/tcp.hpp` (lines 26, 149, 170)
|
|
80
|
+
- **Documentation**: See `ASYNCCLIENT_CLEANUP_FIX.md` for detailed analysis
|
|
81
|
+
|
|
22
82
|
## [1.9.8] - 2025-12-14
|
|
23
83
|
|
|
24
84
|
### 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.10** - Latest release with critical bug fixes for TCP retry and bridge failover
|
|
8
8
|
|
|
9
9
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
|
|
@@ -168,7 +168,7 @@ mesh.setRouterCredentials(ROUTER_SSID, ROUTER_PASSWORD);
|
|
|
168
168
|
mesh.enableBridgeFailover(true);
|
|
169
169
|
mesh.onBridgeRoleChanged(&bridgeRoleCallback);
|
|
170
170
|
|
|
171
|
-
void bridgeRoleCallback(bool isBridge, String reason) {
|
|
171
|
+
void bridgeRoleCallback(bool isBridge, const String& reason) {
|
|
172
172
|
if (isBridge) {
|
|
173
173
|
Serial.printf("🎯 Promoted to bridge: %s\n", reason.c_str());
|
|
174
174
|
}
|
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
|
```
|
|
@@ -205,11 +205,12 @@ For Arduino IDE, install manually from: https://github.com/ESP32Async/AsyncTCP
|
|
|
205
205
|
|
|
206
206
|
#### 2. Built-in Retry Mechanism
|
|
207
207
|
painlessMesh now includes automatic TCP connection retry with the following behavior:
|
|
208
|
-
- Up to
|
|
209
|
-
-
|
|
210
|
-
-
|
|
208
|
+
- Up to 6 total connection attempts (initial + 5 retries)
|
|
209
|
+
- Exponential backoff delays between retries: 1s, 2s, 4s, 8s, 8s (total ~23s)
|
|
210
|
+
- 500ms stabilization delay after IP acquisition before first connection attempt
|
|
211
|
+
- Full WiFi reconnection with 10s delay only triggered after all retries are exhausted
|
|
211
212
|
|
|
212
|
-
This helps handle transient timing issues automatically.
|
|
213
|
+
This exponential backoff helps handle transient timing issues automatically while reducing network congestion from multiple retrying nodes.
|
|
213
214
|
|
|
214
215
|
#### 3. Check Node Resource Usage
|
|
215
216
|
Monitor memory and ensure nodes aren't overloaded:
|
|
@@ -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
|
|
|
@@ -144,7 +144,7 @@ void bridgeStatusCallback(uint32_t bridgeNodeId, bool hasInternet) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
void bridgeRoleCallback(bool isBridge, String reason) {
|
|
147
|
+
void bridgeRoleCallback(bool isBridge, const String& reason) {
|
|
148
148
|
if (isBridge) {
|
|
149
149
|
Serial.printf("🎯 PROMOTED TO BRIDGE: %s\n", reason.c_str());
|
|
150
150
|
Serial.println("This node is now the primary bridge!");
|
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.10
|
|
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.10",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@alteriom/mqtt-schema": "^0.8.0",
|
|
43
|
-
"@eslint/js": "^9.39.
|
|
43
|
+
"@eslint/js": "^9.39.2",
|
|
44
44
|
"ajv": "^8.17.1",
|
|
45
45
|
"ajv-formats": "^3.0.1",
|
|
46
46
|
"prettier": "^3.7.4"
|
|
@@ -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.10"
|
|
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 10
|
|
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
|
|
|
@@ -916,7 +967,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
916
967
|
* @param callback Function to call when role changes
|
|
917
968
|
*/
|
|
918
969
|
void onBridgeRoleChanged(
|
|
919
|
-
std::function<void(bool isBridge, TSTRING reason)> callback) {
|
|
970
|
+
std::function<void(bool isBridge, const TSTRING& reason)> callback) {
|
|
920
971
|
bridgeRoleChangedCallback = callback;
|
|
921
972
|
}
|
|
922
973
|
|
|
@@ -2272,7 +2323,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2272
2323
|
ElectionState electionState = ELECTION_IDLE;
|
|
2273
2324
|
uint32_t electionDeadline = 0;
|
|
2274
2325
|
std::vector<BridgeCandidate> electionCandidates;
|
|
2275
|
-
std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
|
|
2326
|
+
std::function<void(bool isBridge, const TSTRING& reason)> bridgeRoleChangedCallback;
|
|
2276
2327
|
|
|
2277
2328
|
// Isolated bridge retry state and configuration
|
|
2278
2329
|
uint8_t _isolatedBridgeRetryAttempts = 0;
|
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.10
|
|
9
|
+
* @date 2025-12-15
|
|
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;
|
|
@@ -228,7 +228,13 @@ class PackageHandler : public layout::Layout<T> {
|
|
|
228
228
|
for (auto&& task : taskList) {
|
|
229
229
|
if (task.use_count() == 1 && !task->isEnabled()) {
|
|
230
230
|
task->set(aInterval, aIterations, aCallback, NULL, NULL);
|
|
231
|
-
|
|
231
|
+
// Use enableDelayed() for delayed one-shot tasks to prevent immediate execution
|
|
232
|
+
// This ensures tasks with intervals execute after the delay, not immediately
|
|
233
|
+
if (aInterval > 0 && aIterations == TASK_ONCE) {
|
|
234
|
+
task->enableDelayed();
|
|
235
|
+
} else {
|
|
236
|
+
task->enable();
|
|
237
|
+
}
|
|
232
238
|
return task;
|
|
233
239
|
}
|
|
234
240
|
}
|
|
@@ -236,7 +242,13 @@ class PackageHandler : public layout::Layout<T> {
|
|
|
236
242
|
std::shared_ptr<Task> task =
|
|
237
243
|
std::make_shared<Task>(aInterval, aIterations, aCallback);
|
|
238
244
|
scheduler.addTask((*task));
|
|
239
|
-
|
|
245
|
+
// Use enableDelayed() for delayed one-shot tasks to prevent immediate execution
|
|
246
|
+
// This ensures tasks with intervals execute after the delay, not immediately
|
|
247
|
+
if (aInterval > 0 && aIterations == TASK_ONCE) {
|
|
248
|
+
task->enableDelayed();
|
|
249
|
+
} else {
|
|
250
|
+
task->enable();
|
|
251
|
+
}
|
|
240
252
|
taskList.push_front(task);
|
|
241
253
|
return task;
|
|
242
254
|
}
|
package/src/painlessmesh/tcp.hpp
CHANGED
|
@@ -20,6 +20,7 @@ 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
|
|
23
24
|
// Delay before WiFi reconnection after all TCP retries are exhausted
|
|
24
25
|
// This prevents rapid reconnection loops when TCP server is persistently unavailable
|
|
25
26
|
// Gives the TCP server more time to recover and reduces network congestion
|
|
@@ -136,11 +137,13 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
136
137
|
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
137
138
|
// Deleting from within the error callback can cause use-after-free issues
|
|
138
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
|
|
139
142
|
// Note: client is captured by value (pointer copy) and we are the sole owner
|
|
140
143
|
mesh.addTask([client]() {
|
|
141
144
|
Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (retry path)\n");
|
|
142
145
|
delete client;
|
|
143
|
-
},
|
|
146
|
+
}, TCP_CLIENT_CLEANUP_DELAY_MS);
|
|
144
147
|
|
|
145
148
|
mesh.semaphoreGive();
|
|
146
149
|
return;
|
|
@@ -155,11 +158,13 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
155
158
|
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
156
159
|
// Deleting from within the error callback can cause use-after-free issues
|
|
157
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
|
|
158
163
|
// Note: client is captured by value (pointer copy) and we are the sole owner
|
|
159
164
|
mesh.addTask([client]() {
|
|
160
165
|
Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (exhaustion path)\n");
|
|
161
166
|
delete client;
|
|
162
|
-
},
|
|
167
|
+
}, TCP_CLIENT_CLEANUP_DELAY_MS);
|
|
163
168
|
#endif
|
|
164
169
|
// Defer callback execution to avoid crashes in error handler context
|
|
165
170
|
// Execute callbacks after semaphore is released and error handler completes
|