AlteriomPainlessMesh v2.1.1
Easy ESP32/ESP8266 mesh networking with Alteriom IoT extensions
Loading...
Searching...
No Matches
router.hpp
Go to the documentation of this file.
1#ifndef _PAINLESS_MESH_ROUTER_HPP_
2#define _PAINLESS_MESH_ROUTER_HPP_
3
4#include <algorithm>
5#include <memory>
6
11
13
14namespace painlessmesh {
15
19namespace router {
20// Layouts are taken by const reference throughout this header: Layout<T>::subs
21// is a std::list of shared_ptrs, so passing by value used to copy the whole
22// connection list (one heap allocation per connection) on every packet sent,
23// broadcast or forwarded (issue #387).
24template <class T>
25std::shared_ptr<T> findRoute(const layout::Layout<T>& tree,
26 std::function<bool(std::shared_ptr<T>)> func) {
27 auto route = std::find_if(tree.subs.begin(), tree.subs.end(), func);
28 if (route == tree.subs.end()) return NULL;
29 return (*route);
30}
31
43template <class T>
44std::shared_ptr<T> findRoute(const layout::Layout<T>& tree, uint32_t nodeId) {
45 return findRoute<T>(tree, [nodeId](std::shared_ptr<T> s) {
46 return s->connected() && layout::contains((*s), nodeId);
47 });
48}
49
56template <class T>
57std::shared_ptr<T> findLiveRoute(const layout::Layout<T>& tree, uint32_t nodeId,
58 std::shared_ptr<T> exclude = nullptr) {
59 return findRoute<T>(tree, [nodeId, exclude](std::shared_ptr<T> s) {
60 return s != exclude && s->connected() && layout::contains((*s), nodeId);
61 });
62}
63
64template <class T, class U>
65bool send(T& package, std::shared_ptr<U> conn, bool priority = false) {
66 painlessmesh::protocol::Variant variant(package);
67 TSTRING msg;
68 variant.printTo(msg);
69 return conn->addMessage(msg, priority);
70}
71
72template <class T, class U>
73bool send(T&& package, std::shared_ptr<U> conn, bool priority = false) {
74 painlessmesh::protocol::Variant variant(package);
75 TSTRING msg;
76 variant.printTo(msg);
77 return conn->addMessage(msg, priority);
78}
79
80template <class U>
81bool send(protocol::Variant& variant, std::shared_ptr<U> conn,
82 bool priority = false) {
83 TSTRING msg;
84 variant.printTo(msg);
85 return conn->addMessage(msg, priority);
86}
87
88template <class U>
89bool send(protocol::Variant&& variant, std::shared_ptr<U> conn,
90 bool priority = false) {
91 TSTRING msg;
92 variant.printTo(msg);
93 return conn->addMessage(msg, priority);
94}
95
96// New priority-level send functions (0-3 priority levels)
97template <class T, class U>
98bool sendWithPriority(T& package, std::shared_ptr<U> conn, uint8_t priorityLevel) {
99 painlessmesh::protocol::Variant variant(&package);
100 TSTRING msg;
101 variant.printTo(msg);
102 return conn->addMessageWithPriority(msg, priorityLevel);
103}
104
105template <class T, class U>
106bool sendWithPriority(T&& package, std::shared_ptr<U> conn, uint8_t priorityLevel) {
107 painlessmesh::protocol::Variant variant(&package);
108 TSTRING msg;
109 variant.printTo(msg);
110 return conn->addMessageWithPriority(msg, priorityLevel);
111}
112
113template <class U>
114bool sendWithPriority(protocol::Variant& variant, std::shared_ptr<U> conn, uint8_t priorityLevel) {
115 TSTRING msg;
116 variant.printTo(msg);
117 return conn->addMessageWithPriority(msg, priorityLevel);
118}
119
120template <class U>
121bool sendWithPriority(protocol::Variant&& variant, std::shared_ptr<U> conn, uint8_t priorityLevel) {
122 TSTRING msg;
123 variant.printTo(msg);
124 return conn->addMessageWithPriority(msg, priorityLevel);
125}
126
127// The layout-level send and broadcast functions below all funnel into the
128// protocol::Variant& core overloads, which enqueue at the priority carried in
129// the package's "prio" field (PRIORITY_NORMAL when absent). This is what keeps
130// a sender's priority attached to a package across intermediate hops instead
131// of silently dropping it to NORMAL after the first hop (issue #384): the
132// forwarding path in routePackage() re-reads the field from the wire.
133
134template <class U>
135bool send(protocol::Variant& variant, const layout::Layout<U>& layout) {
136 TSTRING msg;
137 variant.printTo(msg);
138 auto conn = findRoute<U>(layout, variant.dest());
139 if (conn) return conn->addMessageWithPriority(msg, variant.priority());
140 return false;
141}
142
143template <class T, class U>
144bool send(T& package, const layout::Layout<U>& layout) {
145 painlessmesh::protocol::Variant variant(package);
146 return send<U>(variant, layout);
147}
148
149template <class T, class U>
150bool send(T&& package, const layout::Layout<U>& layout) {
151 painlessmesh::protocol::Variant variant(package);
152 return send<U>(variant, layout);
153}
154
155template <class U>
156bool send(protocol::Variant&& variant, const layout::Layout<U>& layout) {
157 return send<U>(variant, layout);
158}
159
160template <class T>
161size_t broadcast(protocol::Variant& variant, const layout::Layout<T>& layout,
162 uint32_t exclude) {
163 TSTRING msg;
164 variant.printTo(msg);
165 const auto priority = variant.priority();
166 size_t i = 0;
167 for (auto&& conn : layout.subs) {
168 if (conn->nodeId != 0 && conn->nodeId != exclude) {
169 auto sent = conn->addMessageWithPriority(msg, priority);
170 if (sent) ++i;
171 }
172 }
173 return i;
174}
175
176template <class T, class U>
177size_t broadcast(T& package, const layout::Layout<U>& layout,
178 uint32_t exclude) {
179 painlessmesh::protocol::Variant variant(package);
180 return broadcast<U>(variant, layout, exclude);
181}
182
183template <class T, class U>
184size_t broadcast(T&& package, const layout::Layout<U>& layout,
185 uint32_t exclude) {
186 painlessmesh::protocol::Variant variant(package);
187 return broadcast<U>(variant, layout, exclude);
188}
189
190template <class T>
191size_t broadcast(protocol::Variant&& variant, const layout::Layout<T>& layout,
192 uint32_t exclude) {
193 return broadcast<T>(variant, layout, exclude);
194}
195
196template <class T>
198 std::shared_ptr<T> connection, const TSTRING& pkg,
200 uint32_t receivedAt) {
201 using namespace logger;
202 Log(COMMUNICATION, "routePackage(): Recvd from %u: %s\n", connection->nodeId,
203 pkg.c_str());
204#if ARDUINOJSON_VERSION_MAJOR == 7
205 protocol::Variant variant(pkg);
206 if (variant.error) {
207 // The error is a DeserializationError object, not an integer: passing it
208 // through varargs is undefined behaviour. size_t is unsigned long on the
209 // 64-bit desktop build and unsigned int on ESP32/ESP8266, so it is cast
210 // to one type and printed with the matching specifier.
211 Log(ERROR,
212 "routePackage(): parsing failed. err=%s, total_length=%lu, data=%s<--\n",
213 variant.error.c_str(), static_cast<unsigned long>(pkg.length()),
214 pkg.c_str());
215 return;
216 }
217
218 if (variant.routing() == SINGLE && variant.dest() != layout.getNodeId()) {
219 // Send on without further processing
220 send<T>(variant, layout);
221 return;
222 } else if (variant.routing() == BROADCAST) {
223 broadcast<T>(variant, layout, connection->nodeId);
224 }
225 auto calls = cbl.execute(variant.type(), variant, connection, receivedAt);
226 if (calls == 0)
227 Log(DEBUG, "routePackage(): No callbacks executed; %u, %s\n",
228 variant.type(), pkg.c_str());
229#else
230 // Calculate required capacity based on message size and nesting depth
231 // Fixed capacity approach to avoid segmentation fault issues with
232 // dynamic reallocation (see issue #521 and CODE_REFACTORING_RECOMMENDATIONS.md)
233 size_t nestingDepth = std::count(pkg.begin(), pkg.end(), '{') +
234 std::count(pkg.begin(), pkg.end(), '[');
235
236#if ARDUINOJSON_VERSION_MAJOR >= 7
237 // ArduinoJson v7: automatic capacity management, use generous buffer
238 size_t calculatedCapacity = pkg.length() + 1024;
239#else
240 // ArduinoJson v6: manual capacity calculation required
241 // Base capacity: message length + overhead for JSON structure
242 // Each nesting level adds overhead for pointers and metadata
243 size_t calculatedCapacity = pkg.length() +
244 JSON_OBJECT_SIZE(10) * (std::max)(nestingDepth, size_t(1)) +
245 512; // Additional buffer for strings and padding
246#endif
247
248 // Cap at 8KB for safety on ESP8266 (which has ~80KB total heap)
249 // Messages larger than this should be rejected
250 constexpr size_t MAX_MESSAGE_CAPACITY = 8192;
251 size_t capacity = (std::min)(calculatedCapacity, MAX_MESSAGE_CAPACITY);
252
253 auto variant = std::make_shared<protocol::Variant>(pkg, capacity);
254
255 if (variant->error) {
256 // Same two defects as the ArduinoJson 7 branch above (CodeQL
257 // cpp/wrong-type-format-argument, alerts #6-#9).
258 if (variant->error == DeserializationError::NoMemory) {
259 Log(ERROR,
260 "routePackage(): Message too large. length=%lu, calculated_capacity=%lu, "
261 "nesting_depth=%lu. Consider increasing MAX_MESSAGE_CAPACITY if needed.\n",
262 static_cast<unsigned long>(pkg.length()),
263 static_cast<unsigned long>(calculatedCapacity),
264 static_cast<unsigned long>(nestingDepth));
265 } else {
266 Log(ERROR,
267 "routePackage(): parsing failed. err=%s, length=%lu, data=%s<--\n",
268 variant->error.c_str(), static_cast<unsigned long>(pkg.length()),
269 pkg.c_str());
270 }
271 return;
272 }
273
274 if (variant->routing() == SINGLE && variant->dest() != layout.getNodeId()) {
275 // Send on without further processing
276 send<T>((*variant), layout);
277 return;
278 } else if (variant->routing() == BROADCAST) {
279 broadcast<T>((*variant), layout, connection->nodeId);
280 }
281 auto calls = cbl.execute(variant->type(), (*variant), connection, receivedAt);
282 if (calls == 0)
283 Log(DEBUG, "routePackage(): No callbacks executed; %u, %s\n",
284 variant->type(), pkg.c_str());
285#endif
286}
287
288template <class T, class U>
289void handleNodeSync(T& mesh, protocol::NodeTree newTree,
290 std::shared_ptr<U> conn) {
291 Log(logger::SYNC, "handleNodeSync(): with %u\n", conn->nodeId);
292
293 if (!conn->validSubs(newTree)) {
294 Log(logger::SYNC, "handleNodeSync(): invalid new connection\n");
295 Log.remote("Invalid connection to %u\n", conn->nodeId);
296 conn->close();
297 return;
298 }
299
300 if (conn->newConnection) {
301 // The loop check is the tree the new node presents: if this node is
302 // anywhere in it, the new connection would close a cycle. That is the
303 // only thing a second route to the same node can legitimately mean —
304 // a station has exactly one uplink, so a node that arrives on a fresh
305 // direct connection with a tree that does not contain us has left
306 // wherever else we remember it. Refusing it as "already connected" on
307 // the authority of that memory used to hold a rebooted node out of
308 // the mesh until the neighbour whose tree still carried it timed the
309 // old link out: every AP in turn dropped it a second after the
310 // association, for 30 to 100 s per reboot, measured on the Alteriom
311 // HIL rig on every restart a suite performs.
312 if (layout::contains(newTree, mesh.getNodeId())) {
313 // This node in the presented tree is a cycle only if the presenter is
314 // also reachable from here through some other live connection — the
315 // two ends of the loop. Without that route it is the presenter's
316 // memory of where this node used to be, held in a branch its owner
317 // has not timed out yet: a newly promoted bridge listed the node that
318 // came to join it at the place it held before the promotion, and was
319 // refused as a loop on every attempt for the whole promotion window.
320 // Stale, the mention is dropped before the tree is taken.
321 auto otherRoute = router::findLiveRoute<U>(mesh, newTree.nodeId, conn);
322 if (otherRoute) {
324 "handleNodeSync(): %u's tree contains this node and %u is already "
325 "reachable through %u: a loop. Closing the new connection\n",
326 newTree.nodeId, newTree.nodeId, otherRoute->nodeId);
327 Log.remote("Loop through %u\n", newTree.nodeId);
328 conn->close();
329 return;
330 }
332 "handleNodeSync(): %u's tree lists this node where it used to be; "
333 "stale, not a loop\n",
334 newTree.nodeId);
335 layout::forget(newTree, mesh.getNodeId());
336 }
337 // Whatever else still routes to this node is stale. A direct link to
338 // it is the one it had before it went away — TCP has not noticed yet —
339 // and closes now instead of at its timeout. A route through a
340 // neighbour is that neighbour's memory of the node's old place; the
341 // node is taken out of it here so packets go down the live link, and
342 // the neighbour's next sync brings its own tree up to date.
343 auto oldConnection = router::findLiveRoute<U>(mesh, newTree.nodeId, conn);
344 if (oldConnection) {
345 if (oldConnection->nodeId == newTree.nodeId) {
347 "handleNodeSync(): %u connected again while its old link is "
348 "still open; closing the old one\n",
349 newTree.nodeId);
350 oldConnection->close();
351 } else {
353 "handleNodeSync(): %u was reachable through %u; that place is "
354 "stale, the direct connection wins\n",
355 newTree.nodeId, oldConnection->nodeId);
356 layout::forget(*oldConnection, newTree.nodeId);
357 }
358 }
359 auto remoteNodeId = newTree.nodeId;
360 mesh.addTask([&mesh, remoteNodeId]() {
361 Log(logger::CONNECTION, "newConnectionTask():\n");
362 Log(logger::CONNECTION, "newConnectionTask(): adding %u now= %u\n",
363 remoteNodeId, mesh.getNodeTime());
364 mesh.newConnectionCallbacks.execute(remoteNodeId);
365 });
366
367 // Initially interval is every 10 seconds,
368 // this will slow down to TIME_SYNC_INTERVAL
369 // after first succesfull sync
370 // TODO move it to a new connection callback and use initTimeSync from
371 // ntp.hpp
372 conn->timeSyncTask.set(10 * TASK_SECOND, TASK_FOREVER, [conn, &mesh]() {
373 Log(logger::S_TIME, "timeSyncTask(): %u\n", conn->nodeId);
374 mesh.startTimeSync(conn);
375 });
376 mesh.mScheduler->addTask(conn->timeSyncTask);
377 if (conn->station)
378 // We are STA, request time immediately
379 conn->timeSyncTask.enable();
380 else
381 // We are the AP, give STA the change to initiate time sync
382 conn->timeSyncTask.enableDelayed();
383 conn->newConnection = false;
384 }
385
386 // What a neighbour presents is news only when it differs from what it
387 // presented last time. The cached tree can differ from a restated claim
388 // because a fresher neighbour has since taken a node out of it (below),
389 // and taking the restatement as news put the node back, marked the
390 // connection changed, forced the fresher neighbour's sync, which took it
391 // out again: a sync every 30 to 80 ms between the two for the 10 s it
392 // took the restating neighbour to time out the dead link behind its
393 // claim, on every board that heard both.
394 //
395 // (A neighbour listing a node that is on a direct link of ours is not
396 // pruned here, tempting as that is: it is also the shape a loop takes
397 // while the trees grow round it, and the loop checks above need to see
398 // it. The ring of five in the desktop integration suite never broke up
399 // with that pruning in place.)
400 auto fingerprint = layout::fingerprint(newTree);
401 bool restated = conn->presented == fingerprint;
402 conn->presented = fingerprint;
403
404 if (restated) {
405 conn->nodeSyncTask.delay();
406 mesh.stability += (std::min)(1000 - mesh.stability, (size_t)25);
407 return;
408 }
409
410 // A node is in one place, and a changed sync is the freshest word on
411 // every node below conn. Any other neighbour whose cached tree still
412 // lists one of them lists it where it used to be: on the rig every board
413 // carried a node twice — under the neighbour it had moved to and under
414 // the one it had left — and a message routed by the older copy never
415 // arrived, nor did the Internet request that went the same way. The
416 // older copies go now; their owners' next changed syncs agree.
417 for (auto&& other : mesh.subs) {
418 if (other == conn || other->nodeId == 0) continue;
419 size_t removed = layout::forgetAll(*other, newTree);
420 if (removed) {
422 "handleNodeSync(): %u nodes now under %u were still listed under "
423 "%u; forgotten there\n",
424 (unsigned)removed, conn->nodeId, other->nodeId);
425 }
426 }
427
428 // A node taken out of another neighbour's cache above can only come back
429 // through that neighbour's own sync, and a restatement is skipped. So
430 // when this neighbour stops presenting a node it used to — the node has
431 // moved on, or the claim was the stale one — the others' restatements
432 // are taken in full again: the change marks the connection changed,
433 // which forces their syncs, and whichever of them still lists the node
434 // has it back. Only a removal does this; an addition re-adopted this way
435 // would prune the presenter here, mark a change, force a sync there, and
436 // start the ping-pong over. Without the repair, the desktop time-sync
437 // scenario failed two runs in three: ntp::adopt() weighs the cached
438 // subtrees to choose which side keeps its clock, and a node missing from
439 // both sides' trees had the two ends disagree.
440 if (conn->nodeId != 0) {
441 bool dropped = false;
442 for (auto&& id : layout::asList(*conn, false)) {
443 if (!layout::contains(newTree, id)) {
444 dropped = true;
445 break;
446 }
447 }
448 if (dropped) {
449 for (auto&& other : mesh.subs) {
450 if (other != conn) other->presented = 0;
451 }
452 }
453 }
454 if (conn->updateSubs(newTree)) {
455 auto nodeId = newTree.nodeId;
456 mesh.addTask(
457 [&mesh, nodeId]() { mesh.changedConnectionCallbacks.execute(nodeId); });
458 } else {
459 conn->nodeSyncTask.delay();
460 mesh.stability += (std::min)(1000 - mesh.stability, (size_t)25);
461 }
462}
463
464template <class T, typename U>
466 T& mesh) {
467 // REQUEST type,
468 callbackList.onPackage(
470 [&mesh](protocol::Variant& variant, std::shared_ptr<U> connection,
471 uint32_t receivedAt) {
472 auto newTree = variant.to<protocol::NodeSyncRequest>();
473 handleNodeSync<T, U>(mesh, newTree, connection);
474 send<protocol::NodeSyncReply>(
475 connection->reply(std::move(mesh.asNodeTree())), connection, true);
476 return false;
477 });
478
479 // Reply type just handle it
480 callbackList.onPackage(
482 [&mesh](protocol::Variant& variant, std::shared_ptr<U> connection,
483 uint32_t receivedAt) {
484 auto newTree = variant.to<protocol::NodeSyncReply>();
485 handleNodeSync<T, U>(mesh, newTree, connection);
486 connection->timeOutTask.disable();
487 return false;
488 });
489}
490
491} // namespace router
492} // namespace painlessmesh
493
494#endif
int execute(int id, Args... args)
Definition callback.hpp:98
void onPackage(int id, std::function< void(Args...)> func)
Definition callback.hpp:62
Definition layout.hpp:98
uint32_t getNodeId() const
Definition layout.hpp:108
uint32_t nodeId
Definition layout.hpp:125
std::list< std::shared_ptr< T > > subs
Definition layout.hpp:101
Definition logger.hpp:46
void remote(const char *format...)
Definition logger.hpp:160
Definition protocol.hpp:332
Definition protocol.hpp:277
Definition protocol.hpp:167
uint32_t nodeId
Definition protocol.hpp:169
Definition protocol.hpp:481
int type()
Definition protocol.hpp:750
router::Type routing()
Definition protocol.hpp:780
uint32_t dest()
Definition protocol.hpp:800
DeserializationError error
Definition protocol.hpp:837
T to()
Definition protocol.hpp:743
uint8_t priority()
Definition protocol.hpp:775
String TSTRING
Definition configuration.hpp:132
bool forget(protocol::NodeTree &tree, uint32_t nodeId)
Definition layout.hpp:37
uint32_t fingerprint(const protocol::NodeTree &tree, uint32_t hash=2166136261u)
Definition layout.hpp:72
bool contains(protocol::NodeTree nodeTree, uint32_t nodeId)
Definition layout.hpp:16
std::list< uint32_t > asList(protocol::NodeTree nodeTree, bool includeSelf=true)
Definition layout.hpp:262
size_t forgetAll(protocol::NodeTree &tree, const protocol::NodeTree &elsewhere)
Definition layout.hpp:58
@ S_TIME
Definition logger.hpp:24
@ SYNC
Definition logger.hpp:23
@ CONNECTION
Definition logger.hpp:22
@ NODE_SYNC_REPLY
Definition protocol.hpp:39
@ NODE_SYNC_REQUEST
Definition protocol.hpp:38
void addPackageCallback(callback::MeshPackageCallbackList< U > &callbackList, T &mesh)
Definition router.hpp:465
@ SINGLE
Definition protocol.hpp:30
@ BROADCAST
Definition protocol.hpp:30
bool send(T &package, std::shared_ptr< U > conn, bool priority=false)
Definition router.hpp:65
size_t broadcast(protocol::Variant &variant, const layout::Layout< T > &layout, uint32_t exclude)
Definition router.hpp:161
void handleNodeSync(T &mesh, protocol::NodeTree newTree, std::shared_ptr< U > conn)
Definition router.hpp:289
bool sendWithPriority(T &package, std::shared_ptr< U > conn, uint8_t priorityLevel)
Definition router.hpp:98
std::shared_ptr< T > findRoute(const layout::Layout< T > &tree, std::function< bool(std::shared_ptr< T >)> func)
Definition router.hpp:25
std::shared_ptr< T > findLiveRoute(const layout::Layout< T > &tree, uint32_t nodeId, std::shared_ptr< T > exclude=nullptr)
Definition router.hpp:57
void routePackage(const layout::Layout< T > &layout, std::shared_ptr< T > connection, const TSTRING &pkg, callback::MeshPackageCallbackList< T > &cbl, uint32_t receivedAt)
Definition router.hpp:197
Definition wifi.hpp:34
painlessmesh::logger::LogClass Log
Definition wifi.cpp:2
painlessmesh::logger::LogClass Log
Definition wifi.cpp:2