@alteriom/painlessmesh 1.9.8 → 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.
@@ -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 **new bridge-centric approach** automatically detects your router's channel and configures the mesh accordingly. No manual channel configuration required!
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. Connects to router and detects its channel
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: Connecting to router YourRouterSSID...
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,33 @@ 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
+
22
49
  ## [1.9.8] - 2025-12-14
23
50
 
24
51
  ### Fixed
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div align="center">
6
6
 
7
- **Version 1.9.8** - Latest release with ESP32 heap corruption fix and improved TCP error handling
7
+ **Version 1.9.9** - Latest release with AsyncClient cleanup fixes for improved stability
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)
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 (Recommended)
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 new bridge-centric architecture that
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
- // - Connects to router FIRST and auto-detects its channel
10
- // - Creates mesh network on the same channel as router
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
- // NEW: Single call to initialize as bridge with auto channel detection
75
+ // Single call to initialize as bridge with auto channel detection
67
76
  // This will:
68
- // 1. Connect to router and detect its channel
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
- bool bridgeSuccess = mesh.initAsBridge(MESH_PREFIX, MESH_PASSWORD,
74
- ROUTER_SSID, ROUTER_PASSWORD,
75
- &userScheduler, MESH_PORT);
76
-
77
- if (!bridgeSuccess) {
78
- Serial.println("✗ Failed to initialize as bridge!");
79
- Serial.println("Router unreachable - falling back to regular mesh node");
80
- Serial.println("The node will join the mesh without bridge functionality");
81
-
82
- // Fallback: Initialize as regular mesh node
83
- // This allows the device to still participate in the mesh
84
- mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
85
-
86
- Serial.println("✓ Initialized as regular mesh node");
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
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/Alteriom/painlessMesh"
8
8
  },
9
- "version": "1.9.8",
9
+ "version": "1.9.9",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=Alteriom PainlessMesh
2
- version=1.9.8
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.8",
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.7"
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 7
35
+ #define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 9
36
36
 
37
37
  /**
38
38
  * @brief Library description and usage information
@@ -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: Connecting to router %s...\n", routerSSID.c_str());
389
+ Log(STARTUP, "Step 1: Attempting to connect to router %s...\n", routerSSID.c_str());
390
390
 
391
- // Step 1: Connect to router first to detect its channel
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(ERROR, "\n Failed to connect to router\n");
431
- Log(ERROR, "Cannot become bridge without router connection\n");
432
- Log(ERROR, "Bridge initialization aborted - remaining as regular node\n");
433
- return false;
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: Re-establish router connection using stationManual
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 (matches router)\n", detectedChannel);
465
- Log(STARTUP, " Router: %s\n", routerSSID.c_str());
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
 
@@ -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.7
9
- * @date 2025-12-13
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
- delete client;
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;
@@ -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
- }, 0);
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
- }, 0);
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