@alteriom/painlessmesh 1.10.0 → 2.0.0

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.
Files changed (54) hide show
  1. package/BRIDGE_TO_INTERNET.md +167 -29
  2. package/CHANGELOG.md +483 -0
  3. package/CONTRIBUTING.md +56 -53
  4. package/README.md +100 -95
  5. package/RELEASE_GUIDE.md +81 -780
  6. package/examples/alteriom/README.md +8 -10
  7. package/examples/alteriom/alteriom.ino +2 -2
  8. package/examples/alteriom/alteriom_sensor_package.hpp +17 -11
  9. package/examples/alteriom/mppt_example/alteriom_custom_package_template.hpp +320 -0
  10. package/examples/alteriom/mppt_example/alteriom_sensor_package.hpp +1389 -0
  11. package/examples/alteriom/mppt_example/{alteriom_mppt_example.ino → mppt_example.ino} +4 -0
  12. package/examples/basic/test/simulator/README.md +3 -3
  13. package/examples/bridge_failover/README.md +51 -14
  14. package/examples/commandControl/commandControl.ino +86 -0
  15. package/examples/commandControl/platformio.ini +26 -0
  16. package/examples/mqttBridge/mqttBridge.ino +4 -0
  17. package/examples/mqttBridge/platformio.ini +1 -1
  18. package/examples/otaSender/otaSender.ino +5 -1
  19. package/examples/priority/README.md +1 -1
  20. package/examples/priority/{priority_basic_example.ino → priority_basic_example/priority_basic_example.ino} +4 -4
  21. package/examples/priority/{priority_with_queue.ino → priority_with_queue/priority_with_queue.ino} +20 -2
  22. package/examples/reliableSensorLogging/platformio.ini +26 -0
  23. package/examples/reliableSensorLogging/reliableSensorLogging.ino +151 -0
  24. package/examples/sendToInternet/README.md +12 -5
  25. package/examples/sendToInternet/{CMakeLists.txt → pc_node/CMakeLists.txt} +7 -7
  26. package/examples/sendToInternet/{PC_NODE_README.md → pc_node/PC_NODE_README.md} +15 -15
  27. package/examples/sendToInternet/{build.sh → pc_node/build.sh} +5 -5
  28. package/examples/sendToInternet/{pc_mesh_node.cpp → pc_node/pc_mesh_node.cpp} +12 -1
  29. package/examples/sharedGateway/README.md +1 -2
  30. package/keywords.txt +50 -1
  31. package/library.json +8 -6
  32. package/library.properties +2 -2
  33. package/package.json +3 -3
  34. package/src/AlteriomPainlessMesh.h +3 -3
  35. package/src/arduino/wifi.hpp +556 -126
  36. package/src/painlessMesh.h +2 -2
  37. package/src/painlessMeshSTA.cpp +607 -87
  38. package/src/painlessMeshSTA.h +135 -3
  39. package/src/painlessmesh/ack.hpp +283 -0
  40. package/src/painlessmesh/buffer.hpp +70 -8
  41. package/src/painlessmesh/callback.hpp +38 -5
  42. package/src/painlessmesh/configuration.hpp +69 -1
  43. package/src/painlessmesh/connection.hpp +12 -5
  44. package/src/painlessmesh/gateway.hpp +270 -5
  45. package/src/painlessmesh/layout.hpp +70 -2
  46. package/src/painlessmesh/logger.hpp +15 -0
  47. package/src/painlessmesh/mesh.hpp +552 -48
  48. package/src/painlessmesh/ntp.hpp +2 -4
  49. package/src/painlessmesh/plugin.hpp +30 -6
  50. package/src/painlessmesh/protocol.hpp +55 -2
  51. package/src/painlessmesh/router.hpp +192 -77
  52. package/src/painlessmesh/tcp.hpp +10 -0
  53. package/src/painlessmesh/message_tracker.hpp +0 -311
  54. /package/examples/sendToInternet/{mock_server_test.ino → mock_server_test/mock_server_test.ino} +0 -0
@@ -7,19 +7,21 @@
7
7
  #include <map>
8
8
  #include <set>
9
9
  #include <queue>
10
+ #include <type_traits>
10
11
 
11
12
  #include "painlessmesh/configuration.hpp"
12
13
 
14
+ #include "painlessmesh/ack.hpp"
13
15
  #include "painlessmesh/connection.hpp"
14
16
  #include "painlessmesh/gateway.hpp"
15
17
  #include "painlessmesh/logger.hpp"
16
18
  #include "painlessmesh/message_queue.hpp"
17
- #include "painlessmesh/message_tracker.hpp"
18
19
  #include "painlessmesh/ntp.hpp"
19
20
  #include "painlessmesh/plugin.hpp"
20
21
  #include "painlessmesh/protocol.hpp"
21
22
  #include "painlessmesh/rtc.hpp"
22
23
  #include "painlessmesh/tcp.hpp"
24
+ #include "painlessmesh/validation.hpp"
23
25
 
24
26
  #ifdef PAINLESSMESH_ENABLE_OTA
25
27
  #include "painlessmesh/ota.hpp"
@@ -37,6 +39,38 @@ typedef std::function<void(uint32_t timestamp)> rtcSyncCompleteCallback_t;
37
39
  typedef std::function<void(bool available)> localInternetChangedCallback_t;
38
40
  typedef std::function<void(uint32_t oldPrimary, uint32_t newPrimary)> gatewayChangedCallback_t;
39
41
 
42
+ /**
43
+ * Options for sendSingle() / sendBroadcast() (issue #384)
44
+ *
45
+ * Bundles the outbound queue priority and the optional delivery confirmation
46
+ * into one composable struct, so a message can be both prioritized and
47
+ * acknowledgment-tracked in a single call — something the separate priority
48
+ * and ack overloads cannot express:
49
+ *
50
+ * \code
51
+ * painlessmesh::SendOptions options;
52
+ * options.priority = painlessmesh::protocol::PRIORITY_HIGH;
53
+ * options.ackCallback = [](uint32_t nodeId, bool delivered, uint32_t ms) {};
54
+ * mesh.sendSingle(dest, msg, options);
55
+ * \endcode
56
+ *
57
+ * The priority is carried on the wire (serialized as "prio" only when it
58
+ * deviates from PRIORITY_NORMAL), so intermediate nodes re-enqueue a
59
+ * forwarded package at the sender's priority instead of dropping it to
60
+ * NORMAL after the first hop.
61
+ */
62
+ struct SendOptions {
63
+ /** Outbound queue priority: PRIORITY_CRITICAL (0), PRIORITY_HIGH (1),
64
+ * PRIORITY_NORMAL (2, default) or PRIORITY_LOW (3). */
65
+ uint8_t priority = protocol::PRIORITY_NORMAL;
66
+ /** Optional delivery confirmation callback; fires once per expected node
67
+ * with delivered = true and the round-trip latency, or delivered = false
68
+ * on timeout. nullptr (default) disables tracking. */
69
+ ack::deliveryCallback_t ackCallback = nullptr;
70
+ /** How long to wait for acknowledgments before reporting failure. */
71
+ uint32_t ackTimeoutMs = 5000;
72
+ };
73
+
40
74
  /**
41
75
  * Callback type for Internet request results
42
76
  *
@@ -206,10 +240,56 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
206
240
  #endif
207
241
 
208
242
  // Add package handlers
209
- this->callbackList = painlessmesh::ntp::addPackageCallback(
210
- std::move(this->callbackList), (*this));
211
- this->callbackList = painlessmesh::router::addPackageCallback(
212
- std::move(this->callbackList), (*this));
243
+ painlessmesh::ntp::addPackageCallback(this->callbackList, (*this));
244
+ painlessmesh::router::addPackageCallback(this->callbackList, (*this));
245
+
246
+ // Seed delivery-confirmation ids with a random value so they do not
247
+ // restart at 1 after a reboot — a delayed ACK for a pre-reboot
248
+ // message could otherwise match a fresh message's id and report a
249
+ // false delivered = true.
250
+ ackTracker.seed(validation::SecureRandom::generate());
251
+
252
+ // Per-message delivery confirmation (issue #379): reply with an ACK
253
+ // when a received application message carries a msgId. Uses the
254
+ // Variant field peeks — the common no-ack case must not pay for a
255
+ // full package materialization on every received message.
256
+ auto autoAck = [this](protocol::Variant& variant, std::shared_ptr<T>,
257
+ uint32_t) {
258
+ auto msgId = variant.msgId();
259
+ if (msgId == 0) return false;
260
+ auto origin = variant.from();
261
+ if (origin == 0 || origin == this->nodeId) return false;
262
+ if (variant.type() == protocol::BROADCAST) {
263
+ // Stagger broadcast ACKs by nodeId so N nodes do not fire N
264
+ // unicast ACKs at the sender in the same instant
265
+ this->queueBroadcastAck(origin, msgId);
266
+ } else {
267
+ auto ackPkg = ack::MessageAckPackage(this->nodeId, origin, msgId);
268
+ this->sendPackage(&ackPkg);
269
+ }
270
+ return false;
271
+ };
272
+ this->callbackList.onPackage(protocol::SINGLE, autoAck);
273
+ this->callbackList.onPackage(protocol::BROADCAST, autoAck);
274
+
275
+ // Match incoming ACKs to messages awaiting delivery confirmation
276
+ this->callbackList.onPackage(
277
+ protocol::MESSAGE_ACK,
278
+ [this](protocol::Variant& variant, std::shared_ptr<T>, uint32_t) {
279
+ std::list<ack::DeliveryResult> results;
280
+ this->ackTracker.collectAck(variant.msgId(), variant.from(),
281
+ static_cast<uint32_t>(millis()),
282
+ results);
283
+ if (!results.empty()) {
284
+ this->addTask([results]() {
285
+ for (const auto& result : results) {
286
+ result.callback(result.nodeId, result.delivered,
287
+ result.latencyMs);
288
+ }
289
+ });
290
+ }
291
+ return false;
292
+ });
213
293
 
214
294
  // Add bridge status package handler (Type BRIDGE_STATUS)
215
295
  // This will be called when any node receives a bridge status broadcast
@@ -226,6 +306,14 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
226
306
 
227
307
  if (obj["internetConnected"].is<bool>()) {
228
308
  uint32_t bridgeNodeId = obj["from"];
309
+ if (obj["leaving"] | false) {
310
+ // A bridge stepping down says so, and is forgotten at once
311
+ // rather than when its last status ages out — a minute and
312
+ // more during which every candidate held it healthy.
313
+ Log(GENERAL, "Bridge %u is stepping down\n", bridgeNodeId);
314
+ this->forgetBridge(bridgeNodeId);
315
+ return false;
316
+ }
229
317
  bool internetConnected = obj["internetConnected"];
230
318
  int8_t routerRSSI = obj["routerRSSI"] | 0;
231
319
  uint8_t routerChannel = obj["routerChannel"] | 0;
@@ -344,6 +432,17 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
344
432
  (*conn)->close();
345
433
  this->eraseClosedConnections();
346
434
  }
435
+ // Disable the ack task BEFORE nulling the member: if stop() was
436
+ // reached from inside the task's own callback, the lambda's
437
+ // this->ackCheckTask guard is useless once the member is null and
438
+ // the task would keep firing forever on an external scheduler.
439
+ if (ackCheckTask) ackCheckTask->disable();
440
+ if (broadcastAckTask) broadcastAckTask->disable();
441
+ ackTracker.clear();
442
+ pendingBroadcastAcks.clear();
443
+ ackCheckTask = nullptr;
444
+ broadcastAckTask = nullptr;
445
+
347
446
  plugin::PackageHandler<T>::stop(mScheduler);
348
447
 
349
448
  newConnectionCallbacks.clear();
@@ -351,8 +450,18 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
351
450
  changedConnectionCallbacks.clear();
352
451
 
353
452
  if (!isExternalScheduler) {
354
- delete mScheduler;
355
- mScheduler = nullptr;
453
+ if (mScheduler && mScheduler->getCurrentTask() != nullptr) {
454
+ // stop() was reached from inside a scheduler callback (e.g. a
455
+ // delivery/timeout callback). Deleting the scheduler here would
456
+ // free the object whose execute() is still on the call stack —
457
+ // the same bug class as issue #373. Retire it until execute()
458
+ // unwinds, then reclaim it during the next update.
459
+ retiredSchedulers.push_back(mScheduler);
460
+ mScheduler = nullptr;
461
+ } else {
462
+ delete mScheduler;
463
+ mScheduler = nullptr;
464
+ }
356
465
  }
357
466
  }
358
467
 
@@ -363,9 +472,16 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
363
472
  */
364
473
  void update(void) {
365
474
  if (semaphoreTake()) {
475
+ plugin::PackageHandler<T>::reclaimQuarantinedTasks();
476
+ reclaimRetiredSchedulers();
366
477
  // Check if something is executed (returns false)
367
478
  if (!mScheduler->execute())
368
- Log(logger::GENERAL, "update(): Scheduler executed a task\n");
479
+ // DEBUG, not GENERAL: this fires every time any task runs, which on a
480
+ // busy node is thousands of lines a second. At GENERAL it drowned the
481
+ // level that carries the bridge and gateway diagnostics —
482
+ // sendBridgeStatus(), "Bridge status received from %u" — so nobody
483
+ // can turn those on to investigate a failover without being flooded.
484
+ Log(logger::DEBUG, "update(): Scheduler executed a task\n");
369
485
  semaphoreGive();
370
486
  }
371
487
  return;
@@ -379,12 +495,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
379
495
  * @return true if everything works, false if not.
380
496
  */
381
497
  bool sendSingle(uint32_t destId, TSTRING msg) {
382
- Log(logger::COMMUNICATION, "sendSingle(): dest=%u msg=%s\n", destId,
383
- msg.c_str());
384
- auto single = painlessmesh::protocol::Single(this->nodeId, destId, msg);
385
- return painlessmesh::router::send<T>(single, (*this));
498
+ return sendSingle(destId, msg, SendOptions());
386
499
  }
387
-
500
+
388
501
  /** Send message to a specific node with priority
389
502
  *
390
503
  * @param destId The nodeId of the node to send it to.
@@ -394,12 +507,85 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
394
507
  * @return true if everything works, false if not.
395
508
  */
396
509
  bool sendSingle(uint32_t destId, TSTRING msg, uint8_t priorityLevel) {
397
- Log(logger::COMMUNICATION, "sendSingle(): dest=%u msg=%s priority=%u\n", destId,
398
- msg.c_str(), priorityLevel);
510
+ SendOptions options;
511
+ options.priority = priorityLevel;
512
+ return sendSingle(destId, msg, options);
513
+ }
514
+
515
+ /** Send message to a specific node — unified path (issue #384)
516
+ *
517
+ * All sendSingle() overloads funnel into this one. Priority and delivery
518
+ * confirmation compose freely; see SendOptions for the semantics of each
519
+ * field.
520
+ *
521
+ * @param destId The nodeId of the node to send it to.
522
+ * @param msg The message to send
523
+ * @param options Priority and/or delivery confirmation options.
524
+ *
525
+ * @return true if the message was queued for sending, false if not (no
526
+ * route to destination, or — when an ackCallback is set —
527
+ * PAINLESSMESH_MAX_PENDING_ACKS messages already await
528
+ * acknowledgment; in both cases nothing is sent and the callback
529
+ * will not fire).
530
+ */
531
+ bool sendSingle(uint32_t destId, TSTRING msg, const SendOptions& options) {
532
+ Log(logger::COMMUNICATION, "sendSingle(): dest=%u msg=%s priority=%u%s\n",
533
+ destId, msg.c_str(), options.priority,
534
+ options.ackCallback ? " with ack" : "");
399
535
  auto single = painlessmesh::protocol::Single(this->nodeId, destId, msg);
400
- auto conn = painlessmesh::router::findRoute<T>((*this), destId);
401
- if (!conn) return false;
402
- return painlessmesh::router::sendWithPriority<painlessmesh::protocol::Single, T>(single, conn, priorityLevel);
536
+ single.priority = options.priority;
537
+ if (!options.ackCallback)
538
+ return painlessmesh::router::send<T>(single, (*this));
539
+ if (ackTracker.full()) {
540
+ Log(logger::ERROR, "sendSingle(): pending-ack limit reached\n");
541
+ return false;
542
+ }
543
+ single.msgId = ackTracker.nextMessageId();
544
+ if (!painlessmesh::router::send<T>(single, (*this))) return false;
545
+ ackTracker.track(single.msgId, {destId}, options.ackCallback,
546
+ options.ackTimeoutMs, static_cast<uint32_t>(millis()));
547
+ this->ensureAckScheduling();
548
+ return true;
549
+ }
550
+
551
+ /** Send message to a specific node with delivery confirmation
552
+ *
553
+ * The message is tagged with a unique id and the destination node
554
+ * automatically replies with an acknowledgment. The callback fires
555
+ * exactly once: with delivered = true and the measured round-trip
556
+ * latency when the ACK arrives, or with delivered = false when
557
+ * ackTimeoutMs elapses without an ACK. ACK processing happens inside
558
+ * mesh.update() — no blocking waits.
559
+ *
560
+ * \code
561
+ * mesh.sendSingle(dest, msg,
562
+ * [](uint32_t nodeId, bool delivered, uint32_t latencyMs) {
563
+ * if (delivered)
564
+ * Serial.printf("Node %u confirmed in %u ms\n", nodeId, latencyMs);
565
+ * else
566
+ * Serial.printf("Delivery to %u timed out\n", nodeId);
567
+ * });
568
+ * \endcode
569
+ *
570
+ * @param destId The nodeId of the node to send it to.
571
+ * @param msg The message to send
572
+ * @param ackCallback Delivery confirmation callback. Passing nullptr
573
+ * behaves exactly like the plain sendSingle().
574
+ * @param ackTimeoutMs How long to wait for the acknowledgment (default
575
+ * 5000 ms).
576
+ *
577
+ * @return true if the message was queued for sending, false if not (no
578
+ * route to destination, or PAINLESSMESH_MAX_PENDING_ACKS
579
+ * messages already await acknowledgment — in both cases nothing
580
+ * is sent and the callback will not fire).
581
+ */
582
+ bool sendSingle(uint32_t destId, TSTRING msg,
583
+ ack::deliveryCallback_t ackCallback,
584
+ uint32_t ackTimeoutMs = 5000) {
585
+ SendOptions options;
586
+ options.ackCallback = ackCallback;
587
+ options.ackTimeoutMs = ackTimeoutMs;
588
+ return sendSingle(destId, msg, options);
403
589
  }
404
590
 
405
591
  /** Broadcast a message to every node on the mesh network.
@@ -409,18 +595,142 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
409
595
  * @return true if everything works, false if not
410
596
  */
411
597
  bool sendBroadcast(TSTRING msg, bool includeSelf = false) {
598
+ return sendBroadcast(msg, SendOptions(), includeSelf);
599
+ }
600
+
601
+ /** Broadcast a message to every node — unified path (issue #384)
602
+ *
603
+ * All sendBroadcast() overloads funnel into this one. Priority and
604
+ * delivery confirmation compose freely; see SendOptions for the semantics
605
+ * of each field. With an ackCallback set, the set of nodes expected to
606
+ * acknowledge is snapshotted from the current mesh layout (excluding this
607
+ * node) when the broadcast is sent, and the callback fires once per
608
+ * expected node. ackTimeoutMs values below 1000 ms are raised to 1000 ms
609
+ * so the 50 ms ACK jitter window still leaves time for routed round-trip
610
+ * delivery.
611
+ *
612
+ * @param msg The message to broadcast
613
+ * @param options Priority and/or delivery confirmation options.
614
+ * @param includeSelf Send message to myself as well. Default is false.
615
+ *
616
+ * @return Without an ackCallback: true if the message was queued to at
617
+ * least one peer. With an ackCallback: true if the message was
618
+ * queued and at least one node is expected to acknowledge, false
619
+ * otherwise — including when PAINLESSMESH_MAX_PENDING_ACKS
620
+ * messages already await acknowledgment (nothing is sent; the
621
+ * callback will not fire).
622
+ */
623
+ bool sendBroadcast(TSTRING msg, const SendOptions& options,
624
+ bool includeSelf = false) {
412
625
  using namespace logger;
413
- Log(COMMUNICATION, "sendBroadcast(): msg=%s\n", msg.c_str());
626
+ Log(COMMUNICATION, "sendBroadcast(): msg=%s priority=%u%s\n", msg.c_str(),
627
+ options.priority, options.ackCallback ? " with ack" : "");
414
628
  painlessmesh::protocol::Broadcast pkg(this->nodeId, 0, msg);
629
+ pkg.priority = options.priority;
630
+
631
+ if (!options.ackCallback) {
632
+ auto success = router::broadcast<protocol::Broadcast, T>(pkg, (*this), 0);
633
+ if (includeSelf) {
634
+ protocol::Variant var(pkg);
635
+ this->callbackList.execute(var.type(), var, NULL, 0);
636
+ }
637
+ return success > 0;
638
+ }
639
+
640
+ if (ackTracker.full()) {
641
+ Log(ERROR, "sendBroadcast(): pending-ack limit reached\n");
642
+ return false;
643
+ }
644
+ // Broadcast recipients intentionally jitter ACK replies across this
645
+ // window. A shorter deadline would deterministically time out nodes
646
+ // whose slot falls after it, even when delivery is immediate.
647
+ auto ackTimeoutMs = options.ackTimeoutMs;
648
+ if (ackTimeoutMs < ACK_BROADCAST_MIN_TIMEOUT_MS)
649
+ ackTimeoutMs = ACK_BROADCAST_MIN_TIMEOUT_MS;
650
+ auto expected = this->getNodeList(false);
651
+ pkg.msgId = ackTracker.nextMessageId();
415
652
  auto success = router::broadcast<protocol::Broadcast, T>(pkg, (*this), 0);
653
+ const bool trackingPeers = success > 0 && !expected.empty();
654
+ if (trackingPeers) {
655
+ ackTracker.track(pkg.msgId, expected, options.ackCallback, ackTimeoutMs,
656
+ static_cast<uint32_t>(millis()));
657
+ this->ensureAckScheduling();
658
+ }
416
659
  if (includeSelf) {
417
660
  protocol::Variant var(pkg);
418
661
  this->callbackList.execute(var.type(), var, NULL, 0);
419
662
  }
420
- if (success > 0) return true;
421
- return false;
663
+ // Do not access mesh state after self-delivery: an onReceive callback
664
+ // may have called stop(). Local delivery still occurs without peers,
665
+ // while the return value continues to describe peer ACK tracking.
666
+ return trackingPeers;
422
667
  }
423
668
 
669
+ /** Broadcast a message with per-node delivery confirmation
670
+ *
671
+ * The set of nodes expected to acknowledge is snapshotted from the
672
+ * current mesh layout (excluding this node) when the broadcast is sent.
673
+ * The callback fires once per expected node: with delivered = true and
674
+ * the round-trip latency when that node's ACK arrives, or with
675
+ * delivered = false when ackTimeoutMs elapses. The local node never
676
+ * acknowledges itself, even with includeSelf = true.
677
+ *
678
+ * Note: includeSelf and ackCallback are deliberately non-defaulted on
679
+ * this overload; C++ overload resolution against the priority overload
680
+ * would otherwise be ambiguous or, worse, silently convert a lambda to
681
+ * bool.
682
+ *
683
+ * @param msg The message to broadcast
684
+ * @param includeSelf Send message to myself as well.
685
+ * @param ackCallback Delivery confirmation callback, fired once per
686
+ * node. Passing nullptr behaves like the plain sendBroadcast().
687
+ * @param ackTimeoutMs How long to wait for acknowledgments (default
688
+ * 5000 ms). Values below 1000 ms are raised to 1000 ms so the
689
+ * 50 ms jitter window still leaves time for routed round-trip
690
+ * delivery.
691
+ *
692
+ * @return true if the message was queued and at least one node is
693
+ * expected to acknowledge, false otherwise — including when
694
+ * PAINLESSMESH_MAX_PENDING_ACKS messages already await
695
+ * acknowledgment (nothing is sent; the callback will not
696
+ * fire).
697
+ */
698
+ bool sendBroadcast(TSTRING msg, bool includeSelf,
699
+ ack::deliveryCallback_t ackCallback,
700
+ uint32_t ackTimeoutMs = 5000) {
701
+ SendOptions options;
702
+ options.ackCallback = ackCallback;
703
+ options.ackTimeoutMs = ackTimeoutMs;
704
+ return sendBroadcast(msg, options, includeSelf);
705
+ }
706
+
707
+ /** Process pending delivery acknowledgments (non-blocking poll)
708
+ *
709
+ * Schedules timeout callbacks for messages whose acknowledgment window has
710
+ * elapsed. Callbacks run on a later scheduler pass, after the current mesh
711
+ * dispatch has unwound. This also happens automatically inside
712
+ * mesh.update(), so calling it manually is only needed in tight loops that
713
+ * want prompt timeout detection.
714
+ *
715
+ * @return Number of messages still awaiting acknowledgment
716
+ */
717
+ size_t checkAcks() {
718
+ std::list<ack::DeliveryResult> results;
719
+ auto pending = ackTracker.collectExpired(static_cast<uint32_t>(millis()),
720
+ results);
721
+ if (!results.empty()) {
722
+ this->addTask([results]() {
723
+ for (const auto& result : results) {
724
+ result.callback(result.nodeId, result.delivered, result.latencyMs);
725
+ }
726
+ });
727
+ }
728
+ return pending;
729
+ }
730
+
731
+ /** Number of messages still awaiting delivery acknowledgment */
732
+ size_t pendingAcks() const { return ackTracker.pending(); }
733
+
424
734
  /** Broadcast a message with priority to every node on the mesh network.
425
735
  *
426
736
  * @param msg The message to broadcast
@@ -429,29 +739,20 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
429
739
  *
430
740
  * @return true if everything works, false if not
431
741
  */
432
- bool sendBroadcast(TSTRING msg, uint8_t priorityLevel, bool includeSelf = false) {
433
- using namespace logger;
434
- Log(COMMUNICATION, "sendBroadcast(): msg=%s priority=%u\n", msg.c_str(), priorityLevel);
435
- painlessmesh::protocol::Broadcast pkg(this->nodeId, 0, msg);
436
-
437
- // Broadcast to all connections with priority
438
- size_t success = 0;
439
- for (auto&& conn : this->subs) {
440
- if (conn->nodeId != 0) {
441
- painlessmesh::protocol::Variant variant(pkg);
442
- TSTRING msgStr;
443
- variant.printTo(msgStr);
444
- auto sent = conn->addMessageWithPriority(msgStr, priorityLevel);
445
- if (sent) ++success;
446
- }
447
- }
448
-
449
- if (includeSelf) {
450
- protocol::Variant var(pkg);
451
- this->callbackList.execute(var.type(), var, NULL, 0);
452
- }
453
- if (success > 0) return true;
454
- return false;
742
+ template <
743
+ typename Priority,
744
+ typename IncludeSelf = bool,
745
+ typename std::enable_if<
746
+ (std::is_integral<typename std::decay<Priority>::type>::value ||
747
+ std::is_enum<typename std::decay<Priority>::type>::value) &&
748
+ !std::is_same<typename std::decay<Priority>::type, bool>::value &&
749
+ std::is_same<typename std::decay<IncludeSelf>::type, bool>::value,
750
+ int>::type = 0>
751
+ bool sendBroadcast(TSTRING msg, Priority priorityLevel,
752
+ IncludeSelf includeSelf = false) {
753
+ SendOptions options;
754
+ options.priority = static_cast<uint8_t>(priorityLevel);
755
+ return sendBroadcast(msg, options, includeSelf);
455
756
  }
456
757
 
457
758
  /** Sends a node a packet to measure network trip delay to that node.
@@ -483,6 +784,15 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
483
784
  * Serial.println(msg);
484
785
  * });
485
786
  * \endcode
787
+ *
788
+ * \note **Accumulates — does not replace.** Calling onReceive() a second
789
+ * time does not overwrite the previous handler; both handlers are invoked
790
+ * for every subsequent message, in the order they were registered. This
791
+ * mirrors the other on...() setters on this class (onNewConnection,
792
+ * onDroppedConnection, onChangedConnections, onNodeTimeAdjusted,
793
+ * onNodeDelayReceived). There is currently no public API to unregister
794
+ * a handler; if you need replace-semantics, keep a single dispatcher
795
+ * lambda and mutate the target it dispatches to.
486
796
  */
487
797
  void onReceive(receivedCallback_t onReceive) {
488
798
  using namespace painlessmesh;
@@ -785,6 +1095,61 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
785
1095
  return this->root;
786
1096
  }
787
1097
 
1098
+ // ==================== Capacity (ESP8266) ====================
1099
+
1100
+ /** Live connections for which this node is the access point.
1101
+ *
1102
+ * Every one of them holds send and receive buffers on this node's heap.
1103
+ * A leaf has none; a node the mesh routes through has as many as attach.
1104
+ */
1105
+ size_t apChildren() {
1106
+ size_t n = 0;
1107
+ for (auto&& sub : this->subs) {
1108
+ if (sub && sub->connected() && !sub->station) ++n;
1109
+ }
1110
+ return n;
1111
+ }
1112
+
1113
+ /** Whether this node is being asked to do more than its part can carry.
1114
+ *
1115
+ * The ESP8266 is specified for small meshes, or as a **leaf** in larger
1116
+ * ones: measured as an interior node of a seven-node mesh it runs at
1117
+ * 10–13 KB free — a working set that tracks its live connections and the
1118
+ * traffic through them, not a leak — and a single 8 KB package or one
1119
+ * OTA part can then fail to allocate. This is the part's limit, not a
1120
+ * defect. It is set by capacityCheck(), which the Arduino layer runs
1121
+ * periodically on ESP8266 and which logs an ERROR when the condition is
1122
+ * first met and again every five minutes it persists. Configure the node
1123
+ * as a leaf with init(..., maxconn = 0) — or 1 to allow a single child.
1124
+ */
1125
+ bool overCapacity() { return _overCapacity; }
1126
+
1127
+ /** Evaluate the capacity rule now. Returns the new state.
1128
+ *
1129
+ * \param freeHeap the part's current free heap in bytes
1130
+ * \param floor below this, with more than one child, is over capacity
1131
+ */
1132
+ bool capacityCheck(uint32_t freeHeap, uint32_t floor) {
1133
+ using namespace logger;
1134
+ size_t children = apChildren();
1135
+ bool over = freeHeap < floor && children > 1;
1136
+ uint32_t now = millis();
1137
+ if (over && (!_overCapacity ||
1138
+ now - _capacityWarnedAt > CAPACITY_WARN_INTERVAL_MS)) {
1139
+ Log(ERROR,
1140
+ "capacity: %u B free with %u AP children on ESP8266. This part is "
1141
+ "specified as a leaf in meshes of this size; init(..., maxconn=0) "
1142
+ "or reduce the mesh.\n",
1143
+ freeHeap, children);
1144
+ _capacityWarnedAt = now;
1145
+ }
1146
+ _overCapacity = over;
1147
+ return over;
1148
+ }
1149
+
1150
+ static constexpr uint32_t ESP8266_CAPACITY_FLOOR = 12 * 1024;
1151
+ static constexpr uint32_t CAPACITY_WARN_INTERVAL_MS = 5 * 60 * 1000;
1152
+
788
1153
  // ==================== Gateway Status API ====================
789
1154
 
790
1155
  /**
@@ -954,6 +1319,22 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
954
1319
  * mesh.enableBridgeCleanup();
955
1320
  * \endcode
956
1321
  */
1322
+ /**
1323
+ * Forget one bridge now: it announced that it is stepping down.
1324
+ *
1325
+ * Returns whether it was known.
1326
+ */
1327
+ bool forgetBridge(uint32_t bridgeNodeId) {
1328
+ size_t sizeBefore = knownBridges.size();
1329
+ knownBridges.erase(
1330
+ std::remove_if(knownBridges.begin(), knownBridges.end(),
1331
+ [bridgeNodeId](const BridgeInfo& bridge) {
1332
+ return bridge.nodeId == bridgeNodeId;
1333
+ }),
1334
+ knownBridges.end());
1335
+ return knownBridges.size() != sizeBefore;
1336
+ }
1337
+
957
1338
  void cleanupExpiredBridges() {
958
1339
  using namespace logger;
959
1340
  size_t sizeBefore = knownBridges.size();
@@ -1279,11 +1660,39 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1279
1660
  Log(COMMUNICATION, "sendToInternet(): msgId=%u dest=%s priority=%u\n",
1280
1661
  messageId, destination.c_str(), priority);
1281
1662
 
1282
- // Check if we have local Internet access
1283
- // Note: Even with local Internet, we still use the gateway protocol for consistency.
1284
- // A future optimization could bypass the mesh for nodes with direct Internet access.
1663
+ // A shared gateway can execute its own gateway package locally. Keep the
1664
+ // same package handler, pending-request tracking and callback semantics,
1665
+ // but do not require a mesh peer merely to reach this node's own uplink.
1285
1666
  if (hasLocalInternet()) {
1286
- Log(COMMUNICATION, "sendToInternet(): Local Internet available, using gateway protocol for consistency\n");
1667
+ PendingInternetRequest request;
1668
+ request.messageId = messageId;
1669
+ request.timestamp = millis();
1670
+ request.retryCount = 0;
1671
+ request.maxRetries = internetRetryCount;
1672
+ request.priority = priority;
1673
+ request.timeoutMs = internetRequestTimeout;
1674
+ request.retryDelayMs = internetRetryDelay;
1675
+ request.gatewayNodeId = this->nodeId;
1676
+ request.destination = destination;
1677
+ request.payload = payload;
1678
+ request.callback = callback;
1679
+ pendingInternetRequests[messageId] = request;
1680
+
1681
+ gateway::GatewayDataPackage pkg;
1682
+ pkg.from = this->nodeId;
1683
+ pkg.dest = this->nodeId;
1684
+ pkg.messageId = messageId;
1685
+ pkg.originNode = this->nodeId;
1686
+ pkg.timestamp = this->getNodeTime();
1687
+ pkg.priority = priority;
1688
+ pkg.destination = destination;
1689
+ pkg.payload = payload;
1690
+ pkg.contentType = "application/json";
1691
+ pkg.retryCount = 0;
1692
+ pkg.requiresAck = true;
1693
+ protocol::Variant variant(&pkg);
1694
+ this->callbackList.execute(protocol::GATEWAY_DATA, variant, nullptr, 0);
1695
+ return messageId;
1287
1696
  }
1288
1697
 
1289
1698
  // Validate mesh connectivity before attempting to send
@@ -1346,6 +1755,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1346
1755
  bool sent = false;
1347
1756
  auto conn = painlessmesh::router::findRoute<T>((*this), gateway->nodeId);
1348
1757
  if (conn) {
1758
+ // HTTP runs on the gateway's cooperative scheduler. Preserve this
1759
+ // requester's route until the corresponding acknowledgment can return.
1760
+ painlessmesh::gateway::reserveGatewayBlockingBudget(*conn);
1349
1761
  sent = painlessmesh::router::sendWithPriority(pkg, conn, priority);
1350
1762
  if (!sent) {
1351
1763
  Log(ERROR, "sendToInternet(): sendWithPriority failed to gateway %u (send buffer full?)\n", gateway->nodeId);
@@ -1714,6 +2126,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1714
2126
  auto conn = painlessmesh::router::findRoute<T>((*this), gateway->nodeId);
1715
2127
  bool sent = false;
1716
2128
  if (conn) {
2129
+ painlessmesh::gateway::reserveGatewayBlockingBudget(*conn);
1717
2130
  sent = painlessmesh::router::sendWithPriority(pkg, conn, request.priority);
1718
2131
  }
1719
2132
 
@@ -3129,11 +3542,99 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
3129
3542
 
3130
3543
  ~Mesh() {
3131
3544
  this->stop();
3545
+ plugin::PackageHandler<T>::reclaimQuarantinedTasks();
3546
+ reclaimRetiredSchedulers();
3132
3547
  if (!isExternalScheduler) delete mScheduler;
3133
3548
  if (messageQueue) delete messageQueue;
3134
3549
  }
3135
3550
 
3136
3551
  protected:
3552
+ void reclaimRetiredSchedulers() {
3553
+ for (auto it = retiredSchedulers.begin();
3554
+ it != retiredSchedulers.end();) {
3555
+ if ((*it)->getCurrentTask() != nullptr) {
3556
+ ++it;
3557
+ continue;
3558
+ }
3559
+ delete *it;
3560
+ it = retiredSchedulers.erase(it);
3561
+ }
3562
+ }
3563
+
3564
+ std::list<Scheduler*> retiredSchedulers = {};
3565
+
3566
+ void queueBroadcastAck(uint32_t origin, uint32_t msgId) {
3567
+ const auto key = std::make_pair(origin, msgId);
3568
+ if (std::find(pendingBroadcastAcks.begin(), pendingBroadcastAcks.end(),
3569
+ key) != pendingBroadcastAcks.end())
3570
+ return;
3571
+ if (pendingBroadcastAcks.size() >= MAX_QUEUED_BROADCAST_ACKS) {
3572
+ Log(logger::ERROR, "Mesh: broadcast-ack queue limit (%u) reached\n",
3573
+ (unsigned)MAX_QUEUED_BROADCAST_ACKS);
3574
+ return;
3575
+ }
3576
+ pendingBroadcastAcks.push_back(key);
3577
+ if (!broadcastAckTask) {
3578
+ broadcastAckTask = this->addTask(
3579
+ [this]() {
3580
+ while (!this->pendingBroadcastAcks.empty()) {
3581
+ auto queued = std::move(this->pendingBroadcastAcks);
3582
+ this->pendingBroadcastAcks.clear();
3583
+ for (const auto& item : queued) {
3584
+ auto pkg = ack::MessageAckPackage(this->nodeId, item.first,
3585
+ item.second);
3586
+ this->sendPackage(&pkg);
3587
+ }
3588
+ }
3589
+ },
3590
+ this->nodeId % ACK_BROADCAST_JITTER_MS);
3591
+ } else if (!broadcastAckTask->isEnabled() ||
3592
+ broadcastAckTask->isLastIteration()) {
3593
+ broadcastAckTask->restartDelayed();
3594
+ }
3595
+ }
3596
+
3597
+ /**
3598
+ * Make sure the periodic ack-timeout task is running
3599
+ *
3600
+ * The task disables itself as soon as no acknowledgments are pending,
3601
+ * so there is zero scheduler overhead when the ack API is not in use.
3602
+ */
3603
+ void ensureAckScheduling() {
3604
+ if (!ackCheckTask) {
3605
+ ackCheckTask = this->addTask(ACK_CHECK_INTERVAL_MS, TASK_FOREVER,
3606
+ [this]() {
3607
+ if (this->checkAcks() == 0 &&
3608
+ this->ackCheckTask)
3609
+ this->ackCheckTask->disable();
3610
+ });
3611
+ } else if (!ackCheckTask->isEnabled()) {
3612
+ ackCheckTask->enable();
3613
+ }
3614
+ }
3615
+
3616
+ // Poll interval for ack timeouts while acks are pending. Note for
3617
+ // battery/light-sleep nodes: while any ack is outstanding the
3618
+ // scheduler wakes at this cadence for up to ackTimeoutMs; override at
3619
+ // build time (coarser is cheaper, timeout resolution degrades
3620
+ // accordingly).
3621
+ #ifndef PAINLESSMESH_ACK_CHECK_INTERVAL_MS
3622
+ #define PAINLESSMESH_ACK_CHECK_INTERVAL_MS 100
3623
+ #endif
3624
+ static constexpr unsigned long ACK_CHECK_INTERVAL_MS =
3625
+ PAINLESSMESH_ACK_CHECK_INTERVAL_MS;
3626
+ // Broadcast ACK replies are staggered by nodeId within this window so
3627
+ // N nodes do not converge N simultaneous unicasts on the sender
3628
+ static constexpr uint32_t ACK_BROADCAST_JITTER_MS = 50;
3629
+ // Allow the jittered ACK to traverse a multi-hop route after its slot.
3630
+ static constexpr uint32_t ACK_BROADCAST_MIN_TIMEOUT_MS = 1000;
3631
+ static constexpr size_t MAX_QUEUED_BROADCAST_ACKS =
3632
+ PAINLESSMESH_MAX_QUEUED_BROADCAST_ACKS;
3633
+ ack::AckTracker ackTracker;
3634
+ std::shared_ptr<Task> ackCheckTask;
3635
+ std::vector<std::pair<uint32_t, uint32_t> > pendingBroadcastAcks;
3636
+ std::shared_ptr<Task> broadcastAckTask;
3637
+
3137
3638
  void setScheduler(Scheduler *baseScheduler) {
3138
3639
  this->mScheduler = baseScheduler;
3139
3640
  isExternalScheduler = true;
@@ -3275,6 +3776,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
3275
3776
  uint32_t bridgeStatusIntervalMs = 30000; // Default 30 seconds
3276
3777
  uint32_t bridgeTimeoutMs = 60000; // Default 60 seconds
3277
3778
  bool bridgeStatusBroadcastEnabled = true;
3779
+ // See capacityCheck(). Only ever set on ESP8266.
3780
+ bool _overCapacity = false;
3781
+ uint32_t _capacityWarnedAt = 0;
3278
3782
 
3279
3783
  // Bridge cleanup configuration
3280
3784
  static const size_t MAX_KNOWN_BRIDGES = 20; // Memory efficient limit for ESP8266