@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.
@@ -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,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.6** - Latest release with TCP connection improvements and documentation updates
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.7",
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.7
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.7",
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;
@@ -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
- * When connected to mesh, requires recent bridge status (within timeout).
626
- * When disconnected from mesh, returns true if any bridge previously
627
- * reported Internet connectivity - allowing operations to be queued for
628
- * when mesh connectivity is restored.
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
- bool hasConnections = hasActiveMeshConnections();
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
- // When connected: require fresh status
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) - unless node is disconnected
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
- * When this node is disconnected from the mesh (no active connections),
706
- * returns the last known bridge even if lastSeen is stale. This allows
707
- * nodes to attempt routing through the last known bridge once mesh
708
- * connectivity is restored.
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
- // Check if we have active mesh connections
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
- // When connected to mesh: require healthy (recent) bridge status
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) {
@@ -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
- // Delete the current failed client to prevent memory leak
133
- // The AsyncClient is no longer needed after connection failure
134
- delete client;
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 - clean up the failed client and trigger full reconnection
141
- Log(CONNECTION, "tcp_err(): All %d retries exhausted, triggering WiFi reconnection\n",
142
- TCP_CONNECT_MAX_RETRIES + 1);
143
- delete client;
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
  });