@alteriom/painlessmesh 1.9.5 → 1.9.7

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.
@@ -5,8 +5,8 @@
5
5
  * @file painlessMesh.h
6
6
  * @brief Main header file for Alteriom painlessMesh library
7
7
  *
8
- * @version 1.8.13
9
- * @date 2025-11-20
8
+ * @version 1.9.7
9
+ * @date 2025-12-13
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
@@ -233,10 +233,19 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
233
233
  Log(CONNECTION,
234
234
  "connectToAP(): Restarting AP from channel %d to channel %d\n",
235
235
  oldChannel, detectedChannel);
236
- WiFi.softAPdisconnect(false);
237
- delay(100);
236
+
237
+ // Disconnect AP and allow WiFi stack to fully reset
238
+ // Using true parameter ensures DHCP server is properly stopped
239
+ WiFi.softAPdisconnect(true);
240
+ delay(200); // Increased delay to ensure complete WiFi stack reset
241
+
238
242
  // Call apInit via friend class access (StationScan is friend of wifi::Mesh)
239
243
  mesh->apInit(mesh->getNodeId());
244
+
245
+ // Additional stabilization delay after AP restart
246
+ // This ensures DHCP server is fully initialized before clients connect
247
+ delay(100);
248
+
240
249
  Log(CONNECTION, "connectToAP(): AP restarted on channel %d\n", detectedChannel);
241
250
  }
242
251
  // Reset counter only when mesh is found on a new channel
@@ -3134,7 +3134,11 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
3134
3134
  using namespace logger;
3135
3135
  Log(CONNECTION, "eraseClosedConnections():\n");
3136
3136
  this->subs.remove_if(
3137
- [](const std::shared_ptr<T> &conn) { return !conn->connected(); });
3137
+ [](const std::shared_ptr<T> &conn) {
3138
+ // Null check for safety - should never happen but prevents crashes
3139
+ if (!conn) return true;
3140
+ return !conn->connected();
3141
+ });
3138
3142
  }
3139
3143
 
3140
3144
  public: // Windows MSVC: TCP lambdas need access to droppedConnectionCallbacks
@@ -3238,7 +3242,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
3238
3242
  Mesh &, protocol::NodeTree, std::shared_ptr<T> conn);
3239
3243
  friend void painlessmesh::tcp::initServer<T, Mesh>(AsyncServer &, Mesh &);
3240
3244
  friend void painlessmesh::tcp::connect<T, Mesh>(AsyncClient &, IPAddress,
3241
- uint16_t, Mesh &);
3245
+ uint16_t, Mesh &, uint8_t);
3242
3246
  };
3243
3247
 
3244
3248
  class Connection : public painlessmesh::layout::Neighbour,
@@ -10,6 +10,17 @@
10
10
 
11
11
  namespace painlessmesh {
12
12
  namespace tcp {
13
+
14
+ // TCP connection retry configuration
15
+ // These can be tuned for different network conditions
16
+ // Increased values to better handle real-world mesh network conditions where:
17
+ // - TCP server may need more time to be ready after AP initialization
18
+ // - Network stack stabilization takes longer on some hardware
19
+ // - Multiple nodes connecting simultaneously can cause temporary overload
20
+ static const uint8_t TCP_CONNECT_MAX_RETRIES = 5; // Max retry attempts before giving up
21
+ static const uint32_t TCP_CONNECT_RETRY_DELAY_MS = 1000; // Delay between retry attempts (1 second)
22
+ static const uint32_t TCP_CONNECT_STABILIZATION_DELAY_MS = 500; // Delay after IP acquisition (500ms)
23
+
13
24
  inline uint32_t encodeNodeId(const uint8_t *hwaddr) {
14
25
  using namespace painlessmesh::logger;
15
26
  Log(GENERAL, "encodeNodeId():\n");
@@ -41,13 +52,101 @@ void initServer(AsyncServer &server, M &mesh) {
41
52
  server.begin();
42
53
  }
43
54
 
55
+ /**
56
+ * Establish TCP connection with retry mechanism and exponential backoff
57
+ *
58
+ * This function attempts to connect to the mesh network via TCP.
59
+ * If the connection fails (error -14 ERR_CONN or other errors), it will
60
+ * retry up to TCP_CONNECT_MAX_RETRIES times before triggering a full
61
+ * WiFi reconnection cycle.
62
+ *
63
+ * The retry mechanism helps handle timing issues where:
64
+ * - The TCP server may not be immediately ready after AP initialization
65
+ * - Network stack may need time to stabilize after IP acquisition
66
+ * - Transient network conditions may cause temporary connection failures
67
+ * - Multiple nodes connecting simultaneously may cause temporary overload
68
+ *
69
+ * Exponential backoff is used to increase delay between retries, which:
70
+ * - Gives the TCP server more time to recover from overload
71
+ * - Reduces network contention when multiple nodes are retrying
72
+ * - Improves overall connection success rate in congested networks
73
+ *
74
+ * @param client AsyncClient to use for connection
75
+ * @param ip Target IP address
76
+ * @param port Target port
77
+ * @param mesh Reference to mesh instance for callbacks
78
+ * @param retryCount Current retry attempt (default 0, used internally for recursion)
79
+ */
44
80
  template <class T, class M>
45
- void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh) {
81
+ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
82
+ uint8_t retryCount = 0) {
46
83
  using namespace logger;
47
- client.onError([&mesh](void *, AsyncClient *client, int8_t err) {
84
+
85
+ Log(CONNECTION, "tcp::connect(): Attempting connection to port %d (attempt %d/%d)\n",
86
+ port, retryCount + 1, TCP_CONNECT_MAX_RETRIES + 1);
87
+
88
+ // Store retry count and connection parameters for the error handler
89
+ // We need to capture these by value since they're used in the lambda
90
+ client.onError([&mesh, ip, port, retryCount](void *, AsyncClient *client, int8_t err) {
48
91
  if (mesh.semaphoreTake()) {
49
- Log(CONNECTION, "tcp_err(): error trying to connect %d\n", err);
50
- mesh.droppedConnectionCallbacks.execute(0, true);
92
+ Log(CONNECTION, "tcp_err(): error trying to connect %d (attempt %d/%d)\n",
93
+ err, retryCount + 1, TCP_CONNECT_MAX_RETRIES + 1);
94
+
95
+ // Check if we have retries left - retry logic only works on real hardware
96
+ // In test environment (PAINLESSMESH_BOOST), fall through to dropped connection
97
+ // Note: ip and port are used in retry logic below, suppress unused warnings for test builds
98
+ (void)ip;
99
+ (void)port;
100
+ #if !defined(PAINLESSMESH_BOOST) && (defined(ESP32) || defined(ESP8266))
101
+ if (retryCount < TCP_CONNECT_MAX_RETRIES) {
102
+ // Calculate delay with exponential backoff: base_delay * 2^retryCount
103
+ // This gives increasing time between retries as failures accumulate:
104
+ // - retryCount=0: 1000ms * 1 = 1s
105
+ // - retryCount=1: 1000ms * 2 = 2s
106
+ // - retryCount=2: 1000ms * 4 = 4s
107
+ // - retryCount=3: 1000ms * 8 = 8s (capped at 8)
108
+ // - retryCount=4: 1000ms * 8 = 8s (capped at 8)
109
+ // Cap multiplier at 8 to prevent excessive delays
110
+ uint8_t backoffMultiplier = (retryCount < 3) ? (1U << retryCount) : 8;
111
+ uint32_t retryDelay = TCP_CONNECT_RETRY_DELAY_MS * backoffMultiplier;
112
+
113
+ Log(CONNECTION, "tcp_err(): Scheduling retry in %u ms (backoff x%d)\n",
114
+ retryDelay, backoffMultiplier);
115
+
116
+ // Schedule a retry after a delay using the mesh's task scheduler
117
+ // Note: &mesh is captured by reference because:
118
+ // 1. Mesh is a singleton that lives for the program's lifetime
119
+ // 2. The task scheduler belongs to the mesh, so mesh is always valid when task runs
120
+ // 3. Copying the mesh object is not possible/allowed
121
+ // Recursion depth is strictly bounded by TCP_CONNECT_MAX_RETRIES (default: 5)
122
+ mesh.addTask([&mesh, ip, port, retryCount]() {
123
+ Log(CONNECTION, "tcp_err(): Retrying TCP connection...\n");
124
+
125
+ // Create a new AsyncClient for the retry
126
+ // On success, the client is managed by the Connection object
127
+ // On failure, the onError handler for the new client will handle cleanup
128
+ AsyncClient *pRetryConn = new AsyncClient();
129
+ connect<T, M>((*pRetryConn), ip, port, mesh, retryCount + 1);
130
+ }, retryDelay);
131
+
132
+ // Delete the current failed client to prevent memory leak
133
+ // The AsyncClient is no longer needed after connection failure
134
+ delete client;
135
+
136
+ mesh.semaphoreGive();
137
+ return;
138
+ }
139
+
140
+ // All retries exhausted - clean up the failed client and trigger full reconnection
141
+ Log(CONNECTION, "tcp_err(): All %d retries exhausted, triggering WiFi reconnection\n",
142
+ TCP_CONNECT_MAX_RETRIES + 1);
143
+ delete client;
144
+ #endif
145
+ // Defer callback execution to avoid crashes in error handler context
146
+ // Execute callbacks after semaphore is released and error handler completes
147
+ mesh.addTask([&mesh]() {
148
+ mesh.droppedConnectionCallbacks.execute(0, true);
149
+ });
51
150
  mesh.semaphoreGive();
52
151
  }
53
152
  });