AlteriomPainlessMesh v2.1.1
Easy ESP32/ESP8266 mesh networking with Alteriom IoT extensions
Loading...
Searching...
No Matches
tcp.hpp
Go to the documentation of this file.
1#ifndef _PAINLESS_MESH_TCP_HPP_
2#define _PAINLESS_MESH_TCP_HPP_
3
4#include <list>
5
6#include "Arduino.h"
8
10
11namespace painlessmesh {
12namespace 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
20static const uint8_t TCP_CONNECT_MAX_RETRIES = 5; // Max retry attempts before giving up
21static const uint32_t TCP_CONNECT_RETRY_DELAY_MS = 1000; // Delay between retry attempts (1 second)
22static 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
27static const uint32_t TCP_EXHAUSTION_RECONNECT_DELAY_MS = 10000; // 10 seconds before reconnection
28
29// Duration to block a node after TCP connection retry exhaustion (60 seconds)
30// This prevents repeatedly trying to connect to nodes with unresponsive TCP servers
31static const uint32_t TCP_FAILURE_BLOCK_DURATION_MS = 60000;
32
33// Safe operating bounds applied by clampTcpRetryConfig().
34// Each retry allocates a new AsyncClient and schedules a task, so an unbounded
35// maxRetries is a heap and recursion-depth hazard on ESP8266. A zero retry
36// delay would schedule retry tasks with no spacing, i.e. a hot loop allocating
37// an AsyncClient per scheduler tick.
38static const uint8_t TCP_RETRY_MAX_RETRIES_LIMIT = 10;
39static const uint32_t TCP_RETRY_MIN_DELAY_MS = 50;
40static const uint32_t TCP_RETRY_MAX_DELAY_MS = 60000;
41
59 uint8_t maxRetries = TCP_CONNECT_MAX_RETRIES;
61 uint32_t retryDelayMs = TCP_CONNECT_RETRY_DELAY_MS;
63 uint32_t stabilizationDelayMs = TCP_CONNECT_STABILIZATION_DELAY_MS;
65 uint32_t exhaustionReconnectDelayMs = TCP_EXHAUSTION_RECONNECT_DELAY_MS;
67 uint32_t failureBlockDurationMs = TCP_FAILURE_BLOCK_DURATION_MS;
68};
69
81inline uint32_t retryBackoffDelay(const TcpRetryConfig &cfg,
82 uint8_t retryCount) {
83 uint8_t backoffMultiplier = (retryCount < 3) ? (1U << retryCount) : 8;
84 return cfg.retryDelayMs * backoffMultiplier;
85}
86
102 if (cfg.maxRetries > TCP_RETRY_MAX_RETRIES_LIMIT)
103 cfg.maxRetries = TCP_RETRY_MAX_RETRIES_LIMIT;
104 if (cfg.retryDelayMs < TCP_RETRY_MIN_DELAY_MS)
105 cfg.retryDelayMs = TCP_RETRY_MIN_DELAY_MS;
106 if (cfg.retryDelayMs > TCP_RETRY_MAX_DELAY_MS)
107 cfg.retryDelayMs = TCP_RETRY_MAX_DELAY_MS;
108 return cfg;
109}
110
111inline uint32_t encodeNodeId(const uint8_t *hwaddr) {
112 using namespace painlessmesh::logger;
113 Log(GENERAL, "encodeNodeId():\n");
114 uint32_t value = 0;
115
116 // Extract last 4 bytes of MAC address (skip first 2 bytes)
117 // Big endian (aka "network order") encoding
118 value |= hwaddr[2] << 24; // Byte 2 -> bits 31-24
119 value |= hwaddr[3] << 16; // Byte 3 -> bits 23-16
120 value |= hwaddr[4] << 8; // Byte 4 -> bits 15-8
121 value |= hwaddr[5]; // Byte 5 -> bits 7-0
122 return value;
123}
124
125// Decode nodeId from mesh IP address
126// Mesh IPs follow format: 10.(nodeId >> 8).(nodeId & 0xFF).1
127inline uint32_t decodeNodeIdFromIP(IPAddress ip) {
128#if defined(PAINLESSMESH_BOOST)
129 // In test environment, IPAddress doesn't support indexing
130 // Return 0 to indicate invalid/unknown node ID
131 (void)ip; // Suppress unused parameter warning
132 return 0;
133#else
134 // Validate mesh network IP format: 10.x.x.1
135 // First octet must be 10, last octet must be 1
136 const uint8_t MESH_IP_FIRST_OCTET = 10;
137 const uint8_t MESH_IP_LAST_OCTET = 1;
138 if (ip[0] != MESH_IP_FIRST_OCTET || ip[3] != MESH_IP_LAST_OCTET) {
139 return 0; // Invalid mesh IP
140 }
141
142 // Extract nodeId from second and third octets
143 // NodeId = (octet2 << 8) | octet3
144 const uint8_t BYTE_SHIFT = 8; // Bits per byte
145 uint32_t nodeId = ((uint32_t)ip[1] << BYTE_SHIFT) | (uint32_t)ip[2];
146 return nodeId;
147#endif
148}
149
150template <class T, class M>
151void initServer(AsyncServer &server, M &mesh) {
152 using namespace logger;
153 server.setNoDelay(true);
154
155 server.onClient(
156 [&mesh](void *arg, AsyncClient *client) {
157 if (mesh.semaphoreTake()) {
158 Log(CONNECTION, "New AP connection incoming\n");
159 auto conn = std::make_shared<T>(client, &mesh, false);
160 conn->initTasks();
161 mesh.subs.push_back(conn);
162 mesh.semaphoreGive();
163 return;
164 }
165 // The mesh was busy for the whole second the semaphore allows. The
166 // stack has accepted the client already; left like this it is
167 // neither the mesh's nor closed, and the peer waits on a link that
168 // will never carry a node sync. Say so, and close it, so the peer
169 // tries again at once rather than after its own timeout.
170 Log(ERROR,
171 "New AP connection refused: mesh busy, closing it so the peer "
172 "retries\n");
173 client->close(true);
174 },
175 NULL);
176 server.begin();
177}
178
205template <class T, class M>
206void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
207 uint8_t retryCount = 0) {
208 using namespace logger;
209
210 // Snapshot the retry configuration once, before any lambda is built. The
211 // onError lambda outlives this stack frame, so it captures this snapshot by
212 // value rather than reading mesh's member later: a user callback could
213 // otherwise mutate the config between the connect attempt and the error,
214 // tearing the backoff schedule mid-sequence. Each retry re-enters connect()
215 // and re-snapshots, so a config change still takes effect on the next
216 // attempt.
217 const TcpRetryConfig cfg = mesh.getTcpRetryConfig();
218
219 Log(CONNECTION, "tcp::connect(): Attempting connection to port %d (attempt %d/%d)\n",
220 port, retryCount + 1, cfg.maxRetries + 1);
221
222 // Guard shared between onError and onConnect: under normal conditions a
223 // TCP connection attempt leads to only ONE of the two outcomes. But if
224 // WiFi drops right as the TCP handshake completes, AsyncTCP can queue
225 // BOTH events (connection succeeded at the TCP level + abort due to the
226 // WiFi link being lost) for the same AsyncClient, and both callbacks end
227 // up firing for the SAME object. Without this guard, the client would be
228 // "handed over" twice: once to a BufferedConnection (via onConnect, which
229 // becomes its owner and will delete it via its own destructor) and once
230 // to the retry path (via onError, which schedules it again for deletion)
231 // - two scheduleAsyncClientDeletion() calls on the same pointer, and
232 // therefore a double deferred deletion on the same object once both
233 // tasks fire.
234 auto claimed = std::make_shared<bool>(false);
235
236 // Store retry count and connection parameters for the error handler
237 // We need to capture these by value since they're used in the lambda
238 client.onError([&mesh, ip, port, retryCount, claimed, cfg](void *, AsyncClient *client, int8_t err) {
239 if (*claimed) {
240 // onConnect has already claimed this client (the connection actually
241 // succeeded, and it's already been wrapped in a BufferedConnection
242 // that owns it): don't touch the same object again.
243 Log(CONNECTION,
244 "tcp_err(): onError fired after onConnect had already claimed "
245 "the client - ignored to avoid double handling\n");
246 return;
247 }
248 *claimed = true;
249 if (mesh.semaphoreTake()) {
250 Log(CONNECTION, "tcp_err(): error trying to connect %d (attempt %d/%d)\n",
251 err, retryCount + 1, cfg.maxRetries + 1);
252
253 // Check if we have retries left - retry logic only works on real hardware
254 // In test environment (PAINLESSMESH_BOOST), fall through to dropped connection
255 // Note: ip and port are used in retry logic below, suppress unused warnings for test builds
256 (void)ip;
257 (void)port;
258#if !defined(PAINLESSMESH_BOOST) && (defined(ESP32) || defined(ESP8266))
259 if (retryCount < cfg.maxRetries) {
260 // Delay grows with exponential backoff, multiplier capped at 8.
261 // With the default 1000ms base that is: 1s, 2s, 4s, 8s, 8s.
262 uint32_t retryDelay = retryBackoffDelay(cfg, retryCount);
263
264 Log(CONNECTION, "tcp_err(): Scheduling retry in %u ms\n", retryDelay);
265
266 // Schedule a retry after a delay using the mesh's task scheduler
267 // Note: &mesh is captured by reference because:
268 // 1. Mesh is a singleton that lives for the program's lifetime
269 // 2. The task scheduler belongs to the mesh, so mesh is always valid when task runs
270 // 3. Copying the mesh object is not possible/allowed
271 // Recursion depth is strictly bounded by cfg.maxRetries (default 5),
272 // which clampTcpRetryConfig() never lets exceed
273 // TCP_RETRY_MAX_RETRIES_LIMIT (10).
274 mesh.addTask([&mesh, ip, port, retryCount]() {
275 Log(CONNECTION, "tcp_err(): Retrying TCP connection...\n");
276
277 // Create a new AsyncClient for the retry
278 // On success, the client is managed by the Connection object
279 // On failure, the onError handler for the new client will handle cleanup
280 AsyncClient *pRetryConn = new AsyncClient();
281 connect<T, M>((*pRetryConn), ip, port, mesh, retryCount + 1);
282 }, retryDelay);
283
284 // Defer deletion of the failed AsyncClient to prevent heap corruption
285 // Use the centralized deletion scheduler to ensure proper spacing between deletions
286 // This prevents concurrent cleanup operations in the AsyncTCP library
287 scheduleAsyncClientDeletion(mesh.mScheduler, client, "tcp_err(retry)");
288
289 mesh.semaphoreGive();
290 return;
291 }
292
293 // All retries exhausted - schedule delayed reconnection
294 // Adding a significant delay before reconnection prevents rapid reconnection loops
295 // when the TCP server is persistently unavailable or overloaded
296 Log(CONNECTION, "tcp_err(): All %d retries exhausted for IP %s\n",
297 cfg.maxRetries + 1, ip.toString().c_str());
298
299 // Block this node temporarily to prevent immediate reconnection to the same unresponsive node
300 // This helps when the bridge's TCP server is down but WiFi AP is still running
301 // The blocklist is only used during AP filtering, so it won't affect existing connections
302 #if !defined(PAINLESSMESH_BOOST)
303 // Try to decode nodeId from IP and block it
304 // Only works for mesh IPs (format: 10.x.x.1)
305 // A failureBlockDurationMs of 0 is an explicit "never blocklist" opt-out.
306 // Passing it through would set blockUntil = millis() + 0, which
307 // isNodeBlocked() reads as already expired - so the entry would do
308 // nothing except linger in the blocklist until cleanup sweeps it.
309 uint32_t failedNodeId = decodeNodeIdFromIP(ip);
310 if (failedNodeId != 0 && cfg.failureBlockDurationMs > 0) {
311 // Note: This requires M to be wifi::Mesh which has blockNodeAfterTCPFailure
312 mesh.blockNodeAfterTCPFailure(ip, cfg.failureBlockDurationMs);
313 }
314 #endif
315
316 Log(CONNECTION, "tcp_err(): Scheduling WiFi reconnection in %u ms\n",
318
319 // Defer deletion of the failed AsyncClient to prevent heap corruption
320 // Use the centralized deletion scheduler to ensure proper spacing between deletions
321 // This prevents concurrent cleanup operations in the AsyncTCP library
322 scheduleAsyncClientDeletion(mesh.mScheduler, client, "tcp_err(exhaustion)");
323#endif
324 // Defer callback execution to avoid crashes in error handler context
325 // Execute callbacks after semaphore is released and error handler completes
326 // The delay helps prevent endless rapid reconnection loops by giving the TCP server
327 // more time to recover and reducing network congestion from multiple retrying nodes
328 mesh.addTask([&mesh]() {
329 Log(CONNECTION, "tcp_err(): Executing delayed WiFi reconnection after retry exhaustion\n");
330 mesh.droppedConnectionCallbacks.execute(0, true);
332 mesh.semaphoreGive();
333 }
334 });
335
336 client.onConnect(
337 [&mesh, claimed](void *, AsyncClient *client) {
338 if (*claimed) {
339 // onError has already fired for this client (e.g. an abort
340 // caused by losing WiFi right while the TCP handshake was
341 // completing): the client is already scheduled for deletion by
342 // the retry path. Wrapping it in a new BufferedConnection now
343 // would give it two owners at once. This connection did however
344 // genuinely succeed at the TCP level (that's why onConnect
345 // fired): close it explicitly here, so that when the deferred
346 // deletion already scheduled by onError fires, it finds a
347 // properly closed client instead of one still "connected" -
348 // otherwise we'd fall right back into the bug we're fixing.
349 Log(CONNECTION,
350 "tcp::connect(): onConnect fired after onError had already "
351 "claimed the client - closing the connection and discarding "
352 "it\n");
353 client->close();
354 return;
355 }
356 *claimed = true;
357 if (mesh.semaphoreTake()) {
358 Log(CONNECTION, "New STA connection incoming\n");
359 auto conn = std::make_shared<T>(client, &mesh, true);
360 conn->initTasks();
361 mesh.subs.push_back(conn);
362 mesh.semaphoreGive();
363 }
364 },
365 NULL);
366
367 client.connect(ip, port);
368}
369} // namespace tcp
370} // namespace painlessmesh
371#endif
boost::asio::ip::address IPAddress
Definition asynctcp.hpp:29
Definition asynctcp.hpp:31
void onConnect(AcConnectHandler cb, void *arg=0)
Definition asynctcp.hpp:76
bool connect(IPAddress ipaddress, uint16_t port)
Definition asynctcp.hpp:36
void close(bool now=true)
Definition asynctcp.hpp:115
void onError(AcErrorHandler cb, void *arg=0)
Definition asynctcp.hpp:91
Definition asynctcp.hpp:217
void setNoDelay(bool value=true)
Definition asynctcp.hpp:257
void begin()
Definition asynctcp.hpp:233
void onClient(AcConnectHandler cb, void *arg=0)
Definition asynctcp.hpp:227
Definition logger.hpp:7
uint32_t decodeNodeIdFromIP(IPAddress ip)
Definition tcp.hpp:127
void initServer(AsyncServer &server, M &mesh)
Definition tcp.hpp:151
TcpRetryConfig clampTcpRetryConfig(TcpRetryConfig cfg)
Definition tcp.hpp:101
void scheduleAsyncClientDeletion(Scheduler *scheduler, AsyncClient *client, const char *logPrefix)
Definition connection.hpp:67
void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh, uint8_t retryCount=0)
Definition tcp.hpp:206
uint32_t encodeNodeId(const uint8_t *hwaddr)
Definition tcp.hpp:111
uint32_t retryBackoffDelay(const TcpRetryConfig &cfg, uint8_t retryCount)
Definition tcp.hpp:81
Definition wifi.hpp:34
uint32_t stabilizationDelayMs
Delay after IP acquisition before the TCP connect is attempted (ms)
Definition tcp.hpp:63
uint8_t maxRetries
Max TCP connect retry attempts before falling back to a WiFi reconnect.
Definition tcp.hpp:59
uint32_t failureBlockDurationMs
Duration a node is blocklisted after retry exhaustion (ms, 0 = never)
Definition tcp.hpp:67
uint32_t retryDelayMs
Base delay between retry attempts (ms), scaled by exponential backoff.
Definition tcp.hpp:61
uint32_t exhaustionReconnectDelayMs
Delay before the WiFi reconnect that follows retry exhaustion (ms)
Definition tcp.hpp:65
painlessmesh::logger::LogClass Log
Definition wifi.cpp:2