@alteriom/painlessmesh 1.9.20 → 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.
- package/BRIDGE_TO_INTERNET.md +167 -29
- package/CHANGELOG.md +604 -0
- package/CONTRIBUTING.md +56 -53
- package/README.md +100 -75
- package/RELEASE_GUIDE.md +81 -641
- package/examples/alteriom/README.md +8 -10
- package/examples/alteriom/alteriom.ino +2 -2
- package/examples/alteriom/alteriom_custom_package_template.hpp +11 -11
- package/examples/alteriom/alteriom_sensor_package.hpp +17 -11
- package/examples/alteriom/mppt_example/alteriom_custom_package_template.hpp +320 -0
- package/examples/alteriom/mppt_example/alteriom_sensor_package.hpp +1389 -0
- package/examples/alteriom/mppt_example/{alteriom_mppt_example.ino → mppt_example.ino} +5 -1
- package/examples/basic/test/simulator/README.md +3 -3
- package/examples/bridge_failover/README.md +51 -14
- package/examples/bridge_failover/bridge_failover.ino +2 -2
- package/examples/commandControl/commandControl.ino +86 -0
- package/examples/commandControl/platformio.ini +26 -0
- package/examples/mqttBridge/mqttBridge.ino +4 -0
- package/examples/mqttBridge/platformio.ini +1 -1
- package/examples/otaSender/otaSender.ino +5 -1
- package/examples/priority/README.md +1 -1
- package/examples/priority/{priority_basic_example.ino → priority_basic_example/priority_basic_example.ino} +4 -4
- package/examples/priority/{priority_with_queue.ino → priority_with_queue/priority_with_queue.ino} +20 -2
- package/examples/reliableSensorLogging/platformio.ini +26 -0
- package/examples/reliableSensorLogging/reliableSensorLogging.ino +151 -0
- package/examples/sendToInternet/README.md +12 -5
- package/examples/sendToInternet/{CMakeLists.txt → pc_node/CMakeLists.txt} +7 -7
- package/examples/sendToInternet/{PC_NODE_README.md → pc_node/PC_NODE_README.md} +15 -15
- package/examples/sendToInternet/{build.sh → pc_node/build.sh} +5 -5
- package/examples/sendToInternet/{pc_mesh_node.cpp → pc_node/pc_mesh_node.cpp} +12 -1
- package/examples/sharedGateway/README.md +1 -2
- package/examples/tcpRetryConfig/README.md +110 -0
- package/examples/tcpRetryConfig/platformio.ini +26 -0
- package/examples/tcpRetryConfig/tcpRetryConfig.ino +154 -0
- package/keywords.txt +53 -1
- package/library.json +8 -6
- package/library.properties +2 -2
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +4 -4
- package/src/arduino/wifi.hpp +605 -143
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +607 -87
- package/src/painlessMeshSTA.h +135 -3
- package/src/painlessTaskOptions.h +9 -0
- package/src/painlessmesh/ack.hpp +283 -0
- package/src/painlessmesh/buffer.hpp +74 -9
- package/src/painlessmesh/callback.hpp +38 -5
- package/src/painlessmesh/configuration.hpp +82 -3
- package/src/painlessmesh/connection.hpp +43 -16
- package/src/painlessmesh/gateway.hpp +270 -5
- package/src/painlessmesh/layout.hpp +70 -2
- package/src/painlessmesh/logger.hpp +15 -0
- package/src/painlessmesh/mesh.hpp +625 -70
- package/src/painlessmesh/message_queue.hpp +24 -13
- package/src/painlessmesh/ntp.hpp +2 -4
- package/src/painlessmesh/plugin.hpp +52 -6
- package/src/painlessmesh/protocol.hpp +55 -2
- package/src/painlessmesh/router.hpp +192 -77
- package/src/painlessmesh/tcp.hpp +168 -29
- package/src/painlessmesh/message_tracker.hpp +0 -311
- /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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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,15 +432,36 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
344
432
|
(*conn)->close();
|
|
345
433
|
this->eraseClosedConnections();
|
|
346
434
|
}
|
|
347
|
-
|
|
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
|
+
|
|
446
|
+
plugin::PackageHandler<T>::stop(mScheduler);
|
|
348
447
|
|
|
349
448
|
newConnectionCallbacks.clear();
|
|
350
449
|
droppedConnectionCallbacks.clear();
|
|
351
450
|
changedConnectionCallbacks.clear();
|
|
352
451
|
|
|
353
452
|
if (!isExternalScheduler) {
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
401
|
-
if (!
|
|
402
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
//
|
|
1283
|
-
//
|
|
1284
|
-
//
|
|
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
|
-
|
|
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);
|
|
@@ -1394,6 +1806,32 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1394
1806
|
internetRetryDelay = delayMs;
|
|
1395
1807
|
}
|
|
1396
1808
|
|
|
1809
|
+
/**
|
|
1810
|
+
* Configure TCP connection retry behaviour
|
|
1811
|
+
*
|
|
1812
|
+
* Lets latency-sensitive, high-reliability or battery-powered deployments
|
|
1813
|
+
* pick their own retry envelope. Values outside safe operating bounds are
|
|
1814
|
+
* clamped - see painlessmesh::tcp::clampTcpRetryConfig(). Read the applied
|
|
1815
|
+
* (post-clamp) values back with getTcpRetryConfig().
|
|
1816
|
+
*
|
|
1817
|
+
* The defaults reproduce the previous hardcoded behaviour exactly, so
|
|
1818
|
+
* existing sketches that never call this see no change.
|
|
1819
|
+
*
|
|
1820
|
+
* @param config Retry parameters to apply
|
|
1821
|
+
*/
|
|
1822
|
+
void setTcpRetryConfig(const painlessmesh::tcp::TcpRetryConfig &config) {
|
|
1823
|
+
tcpRetryConfig = painlessmesh::tcp::clampTcpRetryConfig(config);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Get the active TCP retry configuration
|
|
1828
|
+
*
|
|
1829
|
+
* @return The configuration currently in effect, after clamping
|
|
1830
|
+
*/
|
|
1831
|
+
painlessmesh::tcp::TcpRetryConfig getTcpRetryConfig() const {
|
|
1832
|
+
return tcpRetryConfig;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1397
1835
|
/**
|
|
1398
1836
|
* Get number of pending Internet requests
|
|
1399
1837
|
*
|
|
@@ -1688,6 +2126,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1688
2126
|
auto conn = painlessmesh::router::findRoute<T>((*this), gateway->nodeId);
|
|
1689
2127
|
bool sent = false;
|
|
1690
2128
|
if (conn) {
|
|
2129
|
+
painlessmesh::gateway::reserveGatewayBlockingBudget(*conn);
|
|
1691
2130
|
sent = painlessmesh::router::sendWithPriority(pkg, conn, request.priority);
|
|
1692
2131
|
}
|
|
1693
2132
|
|
|
@@ -2060,16 +2499,34 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2060
2499
|
//
|
|
2061
2500
|
|
|
2062
2501
|
/**
|
|
2063
|
-
* Enable or disable message
|
|
2064
|
-
*
|
|
2065
|
-
* When enabled,
|
|
2066
|
-
*
|
|
2067
|
-
*
|
|
2502
|
+
* Enable or disable the manual message queue for offline mode
|
|
2503
|
+
*
|
|
2504
|
+
* When enabled, allocates a `MessageQueue` that your application can
|
|
2505
|
+
* push into while an upstream (MQTT/HTTP/etc.) is unreachable. The
|
|
2506
|
+
* mesh does NOT flush the queue automatically and does NOT observe
|
|
2507
|
+
* connectivity changes — the app is responsible for detecting when
|
|
2508
|
+
* to drain it via `flushMessageQueue()` + `removeQueuedMessage()`.
|
|
2509
|
+
*
|
|
2510
|
+
* Wire it to your own connectivity signal, for example
|
|
2511
|
+
* `onLocalInternetChanged` on the bridge node or
|
|
2512
|
+
* `onBridgeStatusChanged` on downstream nodes.
|
|
2513
|
+
*
|
|
2068
2514
|
* @param enabled True to enable queueing, false to disable
|
|
2069
2515
|
* @param maxSize Maximum number of messages in queue (default 1000)
|
|
2070
|
-
*
|
|
2516
|
+
*
|
|
2071
2517
|
* \code
|
|
2072
2518
|
* mesh.enableMessageQueue(true, 500); // Enable with 500 message capacity
|
|
2519
|
+
*
|
|
2520
|
+
* mesh.onLocalInternetChanged([&mesh](bool available) {
|
|
2521
|
+
* if (available) {
|
|
2522
|
+
* auto messages = mesh.flushMessageQueue();
|
|
2523
|
+
* for (auto& msg : messages) {
|
|
2524
|
+
* if (sendToCloud(msg.payload, msg.destination)) {
|
|
2525
|
+
* mesh.removeQueuedMessage(msg.id);
|
|
2526
|
+
* }
|
|
2527
|
+
* }
|
|
2528
|
+
* }
|
|
2529
|
+
* });
|
|
2073
2530
|
* \endcode
|
|
2074
2531
|
*/
|
|
2075
2532
|
void enableMessageQueue(bool enabled, uint32_t maxSize = 1000) {
|
|
@@ -2084,11 +2541,13 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2084
2541
|
}
|
|
2085
2542
|
|
|
2086
2543
|
/**
|
|
2087
|
-
* Queue a message with priority for later delivery
|
|
2088
|
-
*
|
|
2089
|
-
* Use this to
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2544
|
+
* Queue a message with priority for later manual delivery
|
|
2545
|
+
*
|
|
2546
|
+
* Use this to buffer critical messages when your upstream
|
|
2547
|
+
* (MQTT/HTTP/etc.) is unavailable. The library does NOT deliver these
|
|
2548
|
+
* on its own — your app must call `flushMessageQueue()` and
|
|
2549
|
+
* `removeQueuedMessage()` once its own connectivity check clears.
|
|
2550
|
+
*
|
|
2092
2551
|
* @param payload Message content to queue
|
|
2093
2552
|
* @param destination Optional destination metadata (e.g., MQTT topic, HTTP endpoint)
|
|
2094
2553
|
* @param priority Message priority (default: PRIORITY_NORMAL)
|
|
@@ -2115,16 +2574,14 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2115
2574
|
}
|
|
2116
2575
|
|
|
2117
2576
|
/**
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2121
|
-
*
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2124
|
-
*
|
|
2125
|
-
*
|
|
2126
|
-
* and calling removeQueuedMessage() when successful.
|
|
2127
|
-
*
|
|
2577
|
+
* Return a snapshot of all queued messages for manual delivery
|
|
2578
|
+
*
|
|
2579
|
+
* This does NOT transmit anything and does NOT clear the queue. The
|
|
2580
|
+
* application is responsible for actually sending each message and
|
|
2581
|
+
* calling `removeQueuedMessage(id)` for each one that succeeded.
|
|
2582
|
+
* Messages left in the queue remain available for retry on the next
|
|
2583
|
+
* flush.
|
|
2584
|
+
*
|
|
2128
2585
|
* @return Vector of queued messages to send
|
|
2129
2586
|
*
|
|
2130
2587
|
* \code
|
|
@@ -3085,11 +3542,99 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
3085
3542
|
|
|
3086
3543
|
~Mesh() {
|
|
3087
3544
|
this->stop();
|
|
3545
|
+
plugin::PackageHandler<T>::reclaimQuarantinedTasks();
|
|
3546
|
+
reclaimRetiredSchedulers();
|
|
3088
3547
|
if (!isExternalScheduler) delete mScheduler;
|
|
3089
3548
|
if (messageQueue) delete messageQueue;
|
|
3090
3549
|
}
|
|
3091
3550
|
|
|
3092
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
|
+
|
|
3093
3638
|
void setScheduler(Scheduler *baseScheduler) {
|
|
3094
3639
|
this->mScheduler = baseScheduler;
|
|
3095
3640
|
isExternalScheduler = true;
|
|
@@ -3231,6 +3776,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
3231
3776
|
uint32_t bridgeStatusIntervalMs = 30000; // Default 30 seconds
|
|
3232
3777
|
uint32_t bridgeTimeoutMs = 60000; // Default 60 seconds
|
|
3233
3778
|
bool bridgeStatusBroadcastEnabled = true;
|
|
3779
|
+
// See capacityCheck(). Only ever set on ESP8266.
|
|
3780
|
+
bool _overCapacity = false;
|
|
3781
|
+
uint32_t _capacityWarnedAt = 0;
|
|
3234
3782
|
|
|
3235
3783
|
// Bridge cleanup configuration
|
|
3236
3784
|
static const size_t MAX_KNOWN_BRIDGES = 20; // Memory efficient limit for ESP8266
|
|
@@ -3262,6 +3810,13 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
3262
3810
|
uint32_t internetRetryDelay = 1000; // Default 1 second base delay
|
|
3263
3811
|
bool sendToInternetEnabled = false;
|
|
3264
3812
|
|
|
3813
|
+
// TCP connect retry parameters, see setTcpRetryConfig().
|
|
3814
|
+
// Public because tcp::connect<Connection, wifi::Mesh> reads it but is NOT a
|
|
3815
|
+
// friend: the friend declaration below names connect<T, Mesh<T>>, whereas
|
|
3816
|
+
// wifi::Mesh::tcpConnect() instantiates connect<Connection, wifi::Mesh> - a
|
|
3817
|
+
// different specialisation. Same reason as the public: sections above.
|
|
3818
|
+
painlessmesh::tcp::TcpRetryConfig tcpRetryConfig;
|
|
3819
|
+
|
|
3265
3820
|
friend T;
|
|
3266
3821
|
friend void onDataCb(void *, AsyncClient *, void *, size_t);
|
|
3267
3822
|
friend void tcpSentCb(void *, AsyncClient *, size_t, uint32_t);
|