AlteriomPainlessMesh v2.1.1
Easy ESP32/ESP8266 mesh networking with Alteriom IoT extensions
Loading...
Searching...
No Matches
connection.hpp
Go to the documentation of this file.
1#ifndef _PAINLESS_MESH_CONNECTION_HPP_
2#define _PAINLESS_MESH_CONNECTION_HPP_
3
4#include <memory>
5
6#include "Arduino.h"
8
11
13
14namespace painlessmesh {
15namespace tcp {
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 several hundred milliseconds to complete its internal cleanup
20// When multiple connections are failing simultaneously (e.g., during mesh connection issues),
21// the library needs even more time to safely process multiple cleanup operations
22// Increased from 500ms to 1000ms to handle high-churn scenarios more reliably
23static const uint32_t TCP_CLIENT_CLEANUP_DELAY_MS = 1000; // 1000ms delay before deleting AsyncClient
24
25// Minimum spacing between consecutive AsyncClient deletions to prevent concurrent cleanup
26// When multiple AsyncClients are deleted in rapid succession, the AsyncTCP library's
27// internal cleanup routines can interfere with each other, causing heap corruption
28// This spacing ensures each deletion completes before the next one begins
29// ESP32-C6 needs 1000ms spacing to avoid overlapping AsyncTCP cleanup.
30// ESP32-C6 uses RISC-V architecture with AsyncTCP v3.3.0+ which requires significantly
31// more time for internal cleanup operations compared to ESP32/ESP8266
32static const uint32_t TCP_CLIENT_DELETION_SPACING_MS = 1000; // 1000ms spacing between deletions
33
34// Global state to track AsyncClient deletion scheduling and execution
35// This ensures deletions are spaced out even when multiple deletion requests arrive simultaneously
36// The timestamp is updated both when a deletion is SCHEDULED and when it EXECUTES, providing
37// double protection against concurrent cleanup operations even with scheduler jitter
38//
39// THREAD SAFETY: No synchronization needed because:
40// - ESP32/ESP8266 Arduino framework is single-threaded by design
41// - TaskScheduler executes callbacks sequentially in the main loop
42// - The scheduler never runs tasks concurrently within the same mesh instance
43// - All mesh operations (including deletion callbacks) execute in the same thread
44// - Even when multiple tasks are ready, they execute one-at-a-time via scheduler->execute()
45extern uint32_t lastScheduledDeletionTime; // Timestamp of last deletion scheduled/executed (milliseconds)
46
47// Shared buffer for reading/writing to the buffer
49
67inline void scheduleAsyncClientDeletion(Scheduler* scheduler, AsyncClient* client, const char* logPrefix) {
68 using namespace logger;
69
70 if (!scheduler) {
71 // Fallback: If scheduler not available, delete immediately (risky)
72 Log(CONNECTION, "%s: No scheduler available, deleting AsyncClient immediately (risky)\n", logPrefix);
73 delete client;
74 return;
75 }
76
77 // Get current time in milliseconds
78 uint32_t currentTime = millis();
79
80 // Calculate the earliest time this deletion should execute
81 // Base delay: TCP_CLIENT_CLEANUP_DELAY_MS (1000ms)
82 uint32_t baseDelay = TCP_CLIENT_CLEANUP_DELAY_MS;
83
84 // Calculate when this deletion should execute relative to the last scheduled deletion
85 // If the last deletion was scheduled recently, we need to add additional spacing
86 uint32_t targetDeletionTime = currentTime + baseDelay;
87
88 // If there's a recent deletion scheduled, ensure we space out from it
90 // Calculate when the next deletion slot is available
91 uint32_t nextAvailableSlot = lastScheduledDeletionTime + TCP_CLIENT_DELETION_SPACING_MS;
92
93 // If our target deletion time is before the next available slot, push it out
94 // Handle millis() rollover: Use signed arithmetic to detect if nextAvailableSlot is "in the future"
95 // relative to targetDeletionTime. This works because:
96 // - If difference is positive and < 2^31: nextAvailableSlot is ahead, we need to wait
97 // - If difference is negative or > 2^31: nextAvailableSlot is in the past (or very far future after rollover), use targetDeletionTime
98 int32_t timeUntilSlot = (int32_t)(nextAvailableSlot - targetDeletionTime);
99 if (timeUntilSlot > 0 && timeUntilSlot < (int32_t)(1U << 30)) {
100 // nextAvailableSlot is reasonably soon in the future (< ~12 days), space from it
101 targetDeletionTime = nextAvailableSlot;
102 }
103 // else: lastScheduledDeletionTime is too old (> baseDelay+spacing), or rollover occurred
104 // In this case, just use targetDeletionTime (currentTime + baseDelay) and reset spacing
105 }
106
107 // Calculate the actual delay from now
108 uint32_t actualDelay = targetDeletionTime - currentTime;
109
110 // Update the last scheduled deletion time
111 // IMPORTANT: We update this when scheduling, which provides a minimum guaranteed spacing
112 // between scheduled deletions. This ensures even with scheduler jitter, deletions won't
113 // be scheduled too close together.
114 lastScheduledDeletionTime = targetDeletionTime;
115
116 Log(CONNECTION, "%s: Scheduling AsyncClient deletion in %u ms (spaced from previous deletions)\n",
117 logPrefix, actualDelay);
118
119 // Schedule the deletion task with self-cleanup
120 Task* cleanupTask = new Task(actualDelay * TASK_MILLISECOND, TASK_ONCE, [client, logPrefix]() {
121 using namespace logger;
122 Log(CONNECTION, "%s: Deferred cleanup of AsyncClient executing now\n", logPrefix);
123 delete client;
124 });
125
126 // Task cleanup (issue #373): the task must NOT delete itself from inside
127 // its own onDisable callback. TaskScheduler's Task::disable() keeps
128 // writing to the task object (iScheduler->iCurrent) after onDisable
129 // returns, so a self-delete there is a guaranteed use-after-free — this
130 // was crashing nodes on every connection teardown.
131 // Instead we mark the task self-destructing (_TASK_SELF_DESTRUCT, enabled
132 // in painlessTaskOptions.h): after the single iteration completes, the
133 // Scheduler itself disables the task, unlinks it from its chain, and
134 // deletes it — all from within Scheduler::execute(), safely outside the
135 // disable() call stack.
136 cleanupTask->setSelfDestruct(true);
137
138 scheduler->addTask(*cleanupTask);
139 cleanupTask->enableDelayed();
140}
141
151 : public std::enable_shared_from_this<BufferedConnection> {
152 public:
163
165 using namespace logger;
166 Log.remote("~BufferedConnection");
167 this->close();
168 // Always call client->close() here, unconditionally - do NOT guard this
169 // behind client->freeable(). freeable() can return true while _pcb is
170 // still a valid, non-null pointer (e.g. pcb->state == CLOSED but not yet
171 // reclaimed by lwIP). If we skip close() in that case, _pcb stays
172 // non-null for the entire TCP_CLIENT_CLEANUP_DELAY_MS+ deferred-deletion
173 // window below - during which lwIP's own internal timers (e.g. TIME_WAIT
174 // expiry) can silently free/recycle that pcb without ever notifying
175 // AsyncClient (the tcp_err callback only fires on abnormal termination,
176 // not on routine timer-driven pcb reclamation). The deferred delete then
177 // finds a stale-but-non-null _pcb and tries to close/free it a second
178 // time, corrupting the heap (observed as heap_caps_free/memp_free
179 // assertion failures and wild-pointer crashes inside tcp_arg(), all
180 // several seconds after the connection actually died).
181 // close() internally handles "nothing to do" safely (AsyncTCP checks
182 // _pcb/*pcb before touching lwIP state), and reliably nulls _pcb via its
183 // synchronous tcpip_api_call round-trip - so calling it unconditionally
184 // here, right at destruction time, closes this exposure window entirely.
185 client->close();
186 // Note: client->abort() removed - calling it before deferred deletion
187 // can leave the client in an inconsistent state where AsyncTCP is still
188 // trying to clean up the aborted connection. The close() call above is
189 // sufficient for connection termination.
190 // See: AsyncTCP best practices - abort() should only be called immediately
191 // before delete, not before a deferred deletion.
192
193 // Defer deletion of the AsyncClient to prevent heap corruption
194 // Use the centralized deletion scheduler to ensure proper spacing between deletions
195 // See ISSUE_254_HEAP_CORRUPTION_FIX.md and ASYNCCLIENT_CLEANUP_FIX.md
196 scheduleAsyncClientDeletion(mScheduler, client, "~BufferedConnection");
197 }
198
199 void initialize(Scheduler *scheduler) {
200 // Store scheduler reference for deferred cleanup in destructor
201 mScheduler = scheduler;
202
203 auto self = this->shared_from_this();
204 sentBufferTask.set(TASK_SECOND, TASK_FOREVER, [self]() {
205 if (!self->sentBuffer.empty() && self->client->canSend()) {
206 auto ret = self->writeNext();
207 if (ret)
208 self->sentBufferTask.forceNextIteration();
209 else
210 self->sentBufferTask.delay(100 * TASK_MILLISECOND);
211 }
212 });
213 scheduler->addTask(sentBufferTask);
214 sentBufferTask.enableDelayed();
215
216 readBufferTask.set(TASK_SECOND, TASK_FOREVER, [self]() {
217 if (!self->receiveBuffer.empty()) {
218 TSTRING frnt = self->receiveBuffer.front();
219 self->receiveBuffer.pop_front();
220 if (!self->receiveBuffer.empty())
221 self->readBufferTask.forceNextIteration();
222 if (self->receiveCallback) self->receiveCallback(frnt);
223 }
224 });
225 scheduler->addTask(readBufferTask);
226 readBufferTask.enableDelayed();
227
228 client->onAck(
229 [self](void *arg, AsyncClient *client, size_t len, uint32_t time) {
230 self->sentBufferTask.forceNextIteration();
231 },
232 NULL);
233
234 client->onData(
235 [self](void *arg, AsyncClient *client, void *data, size_t len) {
236 self->receiveBuffer.push(static_cast<const char *>(data), len,
238 // Signal that we are done
239 self->client->ack(len);
240 self->readBufferTask.forceNextIteration();
241 },
242 NULL);
243
245 [self](void *arg, AsyncClient *client) { self->close(); }, NULL);
246 }
247
248 void close() {
249 if (!mConnected) return;
250
251 // Disable tasks and callbacks
252 this->sentBufferTask.setCallback(NULL);
253 this->sentBufferTask.disable();
254 this->readBufferTask.setCallback(NULL);
255 this->readBufferTask.disable();
256
257 this->client->onData(NULL, NULL);
258 this->client->onAck(NULL, NULL);
259 this->client->onDisconnect(NULL, NULL);
260 this->client->onError(NULL, NULL);
261
262 if (client->connected()) {
263 client->close();
264 }
265
268
270
271 receiveCallback = NULL;
272 disconnectCallback = NULL;
273
274 mConnected = false;
275 }
276
277 bool write(const TSTRING &data, bool priority = false) {
278 // A closed connection has no one to drain its buffer: queueing here and
279 // reporting success is a silent loss, and the sender then waits out an
280 // acknowledgement that can never come. Say no, so the caller can.
281 if (!mConnected) return false;
282 // false = outbound buffer at PAINLESSMESH_MAX_SENT_BUFFER_MESSAGES and
283 // nothing lower-priority to evict (issue #388)
284 if (!sentBuffer.push(data, priority)) return false;
285 sentBufferTask.forceNextIteration();
286 return true;
287 }
288
295 bool writeWithPriority(const TSTRING &data, uint8_t priorityLevel) {
296 if (!mConnected) return false;
297 if (!sentBuffer.pushWithPriority(data, priorityLevel)) return false;
298 sentBufferTask.forceNextIteration();
299 return true;
300 }
301
302 void onDisconnect(std::function<void()> callback) {
303 disconnectCallback = callback;
304 }
305
306 void onReceive(std::function<void(TSTRING)> callback) {
307 receiveCallback = callback;
308 }
309
310 bool connected() { return mConnected; }
311
312 protected:
313 bool mConnected = true;
314
316 Scheduler *mScheduler = nullptr; // Scheduler for deferred AsyncClient cleanup
317
318 std::function<void(TSTRING)> receiveCallback;
319 std::function<void()> disconnectCallback;
320
323
324 bool writeNext() {
325 if (sentBuffer.empty()) {
326 return false;
327 }
329 auto snd_len = client->space();
330 if (len > snd_len) len = snd_len;
331 if (len > 0) {
332 // sentBuffer.read(len, shared_buffer);
333 // auto written = client->write(shared_buffer.buffer, len, 1);
334 auto data_ptr = sentBuffer.readPtr(len);
335 auto written = client->write(data_ptr, len, 1);
336 if (written == len) {
337 // Get priority before freeing the read buffer
338 uint8_t msgPriority = sentBuffer.getLastReadPriority();
339
340 // Call send() for high priority messages (CRITICAL=0, HIGH=1)
341 // This ensures they are transmitted immediately
342 if (msgPriority <= 1) {
343 client->send();
344 }
345
347 sentBufferTask.forceNextIteration();
348 return true;
349 } else if (written == 0) {
350 return false;
351 } else {
352 return false;
353 }
354 } else {
355 return false;
356 }
357 }
358
361
362 template <typename T>
363 std::shared_ptr<T> shared_from(T *derived) {
364 return std::static_pointer_cast<T>(shared_from_this());
365 }
366}; // namespace tcp
367}; // namespace tcp
368}; // namespace painlessmesh
369
370#endif
Definition asynctcp.hpp:31
size_t space()
Definition asynctcp.hpp:129
bool connected()
Definition asynctcp.hpp:111
void onAck(AcAckHandler cb, void *arg=0)
Definition asynctcp.hpp:86
size_t write(const void *data, size_t len, size_t copy=ASYNC_WRITE_FLAG_COPY)
Definition asynctcp.hpp:50
void onData(AcDataHandler cb, void *arg=0)
Definition asynctcp.hpp:96
void onDisconnect(AcConnectHandler cb, void *arg=0)
Definition asynctcp.hpp:81
void send()
Definition asynctcp.hpp:68
void close(bool now=true)
Definition asynctcp.hpp:115
void onError(AcErrorHandler cb, void *arg=0)
Definition asynctcp.hpp:91
ReceivedBuffer handles cstrings and stores them as strings.
Definition buffer.hpp:28
void clear()
Definition buffer.hpp:85
SentBuffer stores messages (strings) and allows them to be read in any length with priority-based sch...
Definition buffer.hpp:137
bool pushWithPriority(const T &message, uint8_t priorityLevel)
Definition buffer.hpp:178
bool push(const T &message, bool priority=false)
Definition buffer.hpp:154
size_t requestLength(size_t buffer_length)
Definition buffer.hpp:206
void clear()
Definition buffer.hpp:301
const char * readPtr(size_t length)
Definition buffer.hpp:250
bool empty()
Definition buffer.hpp:299
void freeRead()
Definition buffer.hpp:265
uint8_t getLastReadPriority() const
Definition buffer.hpp:315
Definition logger.hpp:46
void remote(const char *format...)
Definition logger.hpp:160
Definition connection.hpp:151
void initialize(Scheduler *scheduler)
Definition connection.hpp:199
bool write(const TSTRING &data, bool priority=false)
Definition connection.hpp:277
std::function< void()> disconnectCallback
Definition connection.hpp:319
painlessmesh::buffer::ReceiveBuffer< TSTRING > receiveBuffer
Definition connection.hpp:321
BufferedConnection(AsyncClient *client)
Definition connection.hpp:162
~BufferedConnection()
Definition connection.hpp:164
Task readBufferTask
Definition connection.hpp:360
bool mConnected
Definition connection.hpp:313
AsyncClient * client
Definition connection.hpp:315
Scheduler * mScheduler
Definition connection.hpp:316
bool writeWithPriority(const TSTRING &data, uint8_t priorityLevel)
Definition connection.hpp:295
void close()
Definition connection.hpp:248
painlessmesh::buffer::SentBuffer< TSTRING > sentBuffer
Definition connection.hpp:322
void onReceive(std::function< void(TSTRING)> callback)
Definition connection.hpp:306
bool writeNext()
Definition connection.hpp:324
std::shared_ptr< T > shared_from(T *derived)
Definition connection.hpp:363
std::function< void(TSTRING)> receiveCallback
Definition connection.hpp:318
bool connected()
Definition connection.hpp:310
void onDisconnect(std::function< void()> callback)
Definition connection.hpp:302
Task sentBufferTask
Definition connection.hpp:359
String TSTRING
Definition configuration.hpp:132
painlessmesh::logger::LogClass Log
Definition wifi.cpp:2
uint32_t lastScheduledDeletionTime
Definition connection.cpp:6
painlessmesh::buffer::temp_buffer_t shared_buffer
Definition connection.cpp:7
void scheduleAsyncClientDeletion(Scheduler *scheduler, AsyncClient *client, const char *logPrefix)
Definition connection.hpp:67
Definition wifi.hpp:34
Definition buffer.hpp:19
size_t length
Definition buffer.hpp:20
painlessmesh::logger::LogClass Log
Definition wifi.cpp:2