@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.
- package/BRIDGE_TO_INTERNET.md +167 -29
- package/CHANGELOG.md +483 -0
- package/CONTRIBUTING.md +56 -53
- package/README.md +100 -95
- package/RELEASE_GUIDE.md +81 -780
- package/examples/alteriom/README.md +8 -10
- package/examples/alteriom/alteriom.ino +2 -2
- 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} +4 -0
- package/examples/basic/test/simulator/README.md +3 -3
- package/examples/bridge_failover/README.md +51 -14
- 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/keywords.txt +50 -1
- package/library.json +8 -6
- package/library.properties +2 -2
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +3 -3
- package/src/arduino/wifi.hpp +556 -126
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +607 -87
- package/src/painlessMeshSTA.h +135 -3
- package/src/painlessmesh/ack.hpp +283 -0
- package/src/painlessmesh/buffer.hpp +70 -8
- package/src/painlessmesh/callback.hpp +38 -5
- package/src/painlessmesh/configuration.hpp +69 -1
- package/src/painlessmesh/connection.hpp +12 -5
- 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 +552 -48
- package/src/painlessmesh/ntp.hpp +2 -4
- package/src/painlessmesh/plugin.hpp +30 -6
- package/src/painlessmesh/protocol.hpp +55 -2
- package/src/painlessmesh/router.hpp +192 -77
- package/src/painlessmesh/tcp.hpp +10 -0
- package/src/painlessmesh/message_tracker.hpp +0 -311
- /package/examples/sendToInternet/{mock_server_test.ino → mock_server_test/mock_server_test.ino} +0 -0
|
@@ -23,8 +23,60 @@
|
|
|
23
23
|
// Enable (arduino) wifi support
|
|
24
24
|
#define PAINLESSMESH_ENABLE_ARDUINO_WIFI
|
|
25
25
|
|
|
26
|
-
// Enable OTA support
|
|
26
|
+
// Enable OTA support. Define PAINLESSMESH_DISABLE_OTA to compile the OTA
|
|
27
|
+
// plugin out entirely (SECURITY.md, "OTA: integrity checked, not
|
|
28
|
+
// authenticated").
|
|
29
|
+
//
|
|
30
|
+
// It must be a *build flag* -- `-DPAINLESSMESH_DISABLE_OTA`, or platformio.ini
|
|
31
|
+
// `build_flags` -- so that every translation unit sees it. A `#define` above
|
|
32
|
+
// the sketch's `#include <painlessMesh.h>` is not a narrower version of the
|
|
33
|
+
// same thing, it is a bug: src/wifi.cpp and src/painlessMeshSTA.cpp compile
|
|
34
|
+
// separately without it and would reach painlessmesh::Mesh with the OTA
|
|
35
|
+
// members present, while the sketch sees the same class without them. That is
|
|
36
|
+
// an ODR violation, and the standard requires no diagnostic for it.
|
|
37
|
+
//
|
|
38
|
+
// `#undef PAINLESSMESH_ENABLE_OTA` does not work either: it runs before
|
|
39
|
+
// painlessMesh.h includes this header, which then defines the macro straight
|
|
40
|
+
// back.
|
|
41
|
+
#ifdef PAINLESSMESH_DISABLE_OTA
|
|
42
|
+
// Opting out is not the same as declining to define. If the build already
|
|
43
|
+
// supplies PAINLESSMESH_ENABLE_OTA -- redundantly, since it is the default --
|
|
44
|
+
// then merely skipping the definition below leaves that one standing and every
|
|
45
|
+
// #ifdef downstream still compiles OTA in, on a build that looks like it opted
|
|
46
|
+
// out. That is a security control failing open and saying nothing, which is
|
|
47
|
+
// the failure mode this opt-out exists to remove. Refuse the contradiction
|
|
48
|
+
// rather than guessing which flag was meant.
|
|
49
|
+
#ifdef PAINLESSMESH_ENABLE_OTA
|
|
50
|
+
#error \
|
|
51
|
+
"PAINLESSMESH_DISABLE_OTA and PAINLESSMESH_ENABLE_OTA are both defined. Remove -DPAINLESSMESH_ENABLE_OTA from your build flags: OTA is on by default, so the disable flag alone is what you want."
|
|
52
|
+
#endif
|
|
53
|
+
#else
|
|
27
54
|
#define PAINLESSMESH_ENABLE_OTA
|
|
55
|
+
#endif
|
|
56
|
+
|
|
57
|
+
// NOTE: there is deliberately no PAINLESSMESH_OTA_REQUIRE_SIGNATURE flag.
|
|
58
|
+
// OTA acceptance (painlessmesh/ota.hpp, OTA_OP_CODES::DATA) authenticates
|
|
59
|
+
// nothing -- it matches the sender-supplied md5/role/hardware tuple -- so a
|
|
60
|
+
// flag that did not gate that path would read as a security control while
|
|
61
|
+
// enforcing nothing. Defining it fails the build rather than passing
|
|
62
|
+
// silently, so no fleet can ship believing its OTA images are verified.
|
|
63
|
+
// Compiling OTA out is the only mitigation the library offers today.
|
|
64
|
+
#ifdef PAINLESSMESH_OTA_REQUIRE_SIGNATURE
|
|
65
|
+
#error \
|
|
66
|
+
"PAINLESSMESH_OTA_REQUIRE_SIGNATURE is not implemented: painlessMesh does not verify OTA image signatures. Use -DPAINLESSMESH_DISABLE_OTA to compile OTA out, or drop this define."
|
|
67
|
+
#endif
|
|
68
|
+
|
|
69
|
+
// NOTE: there is likewise no PAINLESSMESH_DISABLE_ACK flag. The one this
|
|
70
|
+
// branch briefly carried gated only the (since-deleted, #386) dead
|
|
71
|
+
// painlessmesh/message_tracker.hpp include, so the flag reclaimed nothing
|
|
72
|
+
// and its advertised RAM saving was not real.
|
|
73
|
+
//
|
|
74
|
+
// That is NOT a claim that acknowledgment is free. The delivery-ack subsystem
|
|
75
|
+
// (painlessmesh/ack.hpp) is a different thing and is genuinely allocated:
|
|
76
|
+
// Mesh holds an ack::AckTracker member whose std::map grows with each
|
|
77
|
+
// in-flight tracked message. Nothing compiles that out today. If a
|
|
78
|
+
// memory-constrained target needs it gone, that is a design question for the
|
|
79
|
+
// ack feature (#379), not a flag to reinstate here.
|
|
28
80
|
|
|
29
81
|
// NOTE: `MIN_FREE_MEMORY` and `MAX_MESSAGE_QUEUE` are kept as deprecated
|
|
30
82
|
// no-op compatibility macros. The library does not read either macro:
|
|
@@ -42,9 +94,25 @@
|
|
|
42
94
|
#define MAX_MESSAGE_QUEUE 50
|
|
43
95
|
#endif
|
|
44
96
|
|
|
97
|
+
// The mesh watchdog. Overridable, because the documented remedy for an endpoint
|
|
98
|
+
// that needs longer than the derived gateway budget is to raise this and
|
|
99
|
+
// GATEWAY_HTTP_TIMEOUT_MS together (CHANGELOG, SECURITY.md). Unguarded, a
|
|
100
|
+
// -DNODE_TIMEOUT on the build line was either silently overwritten here or a
|
|
101
|
+
// macro-redefinition error under -Werror, so that remedy could not be followed
|
|
102
|
+
// -- the same shape as the `#undef PAINLESSMESH_ENABLE_OTA` advice this
|
|
103
|
+
// release also had to fix. Pinned by test/catch/catch_node_timeout_override.cpp.
|
|
104
|
+
#ifndef NODE_TIMEOUT
|
|
45
105
|
#define NODE_TIMEOUT 10 * TASK_SECOND
|
|
106
|
+
#endif
|
|
46
107
|
#define SCAN_INTERVAL 30 * TASK_SECOND // AP scan period in ms
|
|
47
108
|
|
|
109
|
+
// A gateway relays to the Internet with blocking HTTPClient calls, from inside
|
|
110
|
+
// the cooperative scheduler. Those calls must finish well inside NODE_TIMEOUT
|
|
111
|
+
// or the gateway's peers reap connections to a node that is perfectly healthy
|
|
112
|
+
// (issues #318, #332). The socket-timeout budget that enforces this is derived
|
|
113
|
+
// from NODE_TIMEOUT in painlessmesh/gateway.hpp -- see GATEWAY_HTTP_TIMEOUT_MS
|
|
114
|
+
// there. Raising NODE_TIMEOUT raises that budget with it.
|
|
115
|
+
|
|
48
116
|
#ifdef ESP32
|
|
49
117
|
#include <AsyncTCP.h>
|
|
50
118
|
#include <WiFi.h>
|
|
@@ -26,7 +26,7 @@ static const uint32_t TCP_CLIENT_CLEANUP_DELAY_MS = 1000; // 1000ms delay before
|
|
|
26
26
|
// When multiple AsyncClients are deleted in rapid succession, the AsyncTCP library's
|
|
27
27
|
// internal cleanup routines can interfere with each other, causing heap corruption
|
|
28
28
|
// This spacing ensures each deletion completes before the next one begins
|
|
29
|
-
//
|
|
29
|
+
// ESP32-C6 needs 1000ms spacing to avoid overlapping AsyncTCP cleanup.
|
|
30
30
|
// ESP32-C6 uses RISC-V architecture with AsyncTCP v3.3.0+ which requires significantly
|
|
31
31
|
// more time for internal cleanup operations compared to ESP32/ESP8266
|
|
32
32
|
static const uint32_t TCP_CLIENT_DELETION_SPACING_MS = 1000; // 1000ms spacing between deletions
|
|
@@ -275,19 +275,26 @@ class BufferedConnection
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
bool write(const TSTRING &data, bool priority = false) {
|
|
278
|
-
|
|
278
|
+
// A closed connection has no one to drain its buffer: queueing here and
|
|
279
|
+
// reporting success is a silent loss, and the sender then waits out an
|
|
280
|
+
// acknowledgement that can never come. Say no, so the caller can.
|
|
281
|
+
if (!mConnected) return false;
|
|
282
|
+
// false = outbound buffer at PAINLESSMESH_MAX_SENT_BUFFER_MESSAGES and
|
|
283
|
+
// nothing lower-priority to evict (issue #388)
|
|
284
|
+
if (!sentBuffer.push(data, priority)) return false;
|
|
279
285
|
sentBufferTask.forceNextIteration();
|
|
280
286
|
return true;
|
|
281
287
|
}
|
|
282
|
-
|
|
288
|
+
|
|
283
289
|
/**
|
|
284
290
|
* Write data with explicit priority level (0-3)
|
|
285
|
-
*
|
|
291
|
+
*
|
|
286
292
|
* \param data The data to send
|
|
287
293
|
* \param priorityLevel Priority level: 0=CRITICAL, 1=HIGH, 2=NORMAL, 3=LOW
|
|
288
294
|
*/
|
|
289
295
|
bool writeWithPriority(const TSTRING &data, uint8_t priorityLevel) {
|
|
290
|
-
|
|
296
|
+
if (!mConnected) return false;
|
|
297
|
+
if (!sentBuffer.pushWithPriority(data, priorityLevel)) return false;
|
|
291
298
|
sentBufferTask.forceNextIteration();
|
|
292
299
|
return true;
|
|
293
300
|
}
|
|
@@ -33,16 +33,85 @@
|
|
|
33
33
|
#include "Arduino.h"
|
|
34
34
|
#include "painlessmesh/configuration.hpp"
|
|
35
35
|
#include "painlessmesh/logger.hpp"
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
#if defined(ESP32) || defined(ESP8266)
|
|
38
|
+
#include <WiFiClient.h>
|
|
39
|
+
#endif
|
|
37
40
|
#include "painlessmesh/plugin.hpp"
|
|
38
41
|
#include "painlessmesh/protocol.hpp"
|
|
39
42
|
|
|
40
43
|
#include <functional>
|
|
41
44
|
#include <map>
|
|
45
|
+
#include <vector>
|
|
42
46
|
|
|
43
47
|
namespace painlessmesh {
|
|
44
48
|
namespace gateway {
|
|
45
49
|
|
|
50
|
+
/** Return whether a channel can be announced for a 2.4 GHz mesh takeover. */
|
|
51
|
+
constexpr bool isValidMeshChannel(uint8_t channel) {
|
|
52
|
+
return channel >= 1 && channel <= 13;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Decide whether a peer must follow an elected bridge to another channel. */
|
|
56
|
+
constexpr bool shouldFollowBridgeChannel(uint32_t localNodeId,
|
|
57
|
+
uint32_t electedBridgeId,
|
|
58
|
+
uint8_t currentChannel,
|
|
59
|
+
uint8_t announcedChannel) {
|
|
60
|
+
return electedBridgeId != localNodeId &&
|
|
61
|
+
isValidMeshChannel(announcedChannel) &&
|
|
62
|
+
announcedChannel != currentChannel;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @brief One occurrence of the mesh SSID seen by an all-channel scan.
|
|
67
|
+
*/
|
|
68
|
+
struct MeshChannelCandidate {
|
|
69
|
+
uint8_t channel;
|
|
70
|
+
int32_t rssi;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @brief Where a node that has lost the mesh should go.
|
|
75
|
+
*
|
|
76
|
+
* Channel re-detection runs only after a node's own partition has produced
|
|
77
|
+
* nothing new for a while, so the mesh it can still see on its *current*
|
|
78
|
+
* channel is the partition it is stranded in. When the SSID is also visible
|
|
79
|
+
* on another channel, that is the rest of the network — a bridge that moved
|
|
80
|
+
* to its router's channel, most often — and the node should go there.
|
|
81
|
+
*
|
|
82
|
+
* Returning the first match, as this used to, made the outcome depend on
|
|
83
|
+
* scan order: a stranded node that happened to see its own partition first
|
|
84
|
+
* concluded nothing had changed and stayed stranded.
|
|
85
|
+
*
|
|
86
|
+
* A node that knows the router has a better signal than strength: a bridge's
|
|
87
|
+
* AP is on its router's channel, so a mesh with a bridge lives there. A
|
|
88
|
+
* failover backup that booted while the mesh was split across two channels
|
|
89
|
+
* saw one AP on each, two dB apart, and joined the one without the bridge;
|
|
90
|
+
* it had a partition of two to itself for the hundred seconds it took the
|
|
91
|
+
* re-detection rules to move it, and the election ran out of time.
|
|
92
|
+
*
|
|
93
|
+
* @param candidates Every channel the mesh SSID was seen on, with RSSI.
|
|
94
|
+
* @param avoidChannel The node's current mesh channel; 0 = no preference.
|
|
95
|
+
* @param routerChannel The channel the router was seen on; 0 = unknown.
|
|
96
|
+
* @return The candidate on routerChannel if there is one; else the strongest
|
|
97
|
+
* on a channel other than avoidChannel; failing that the strongest
|
|
98
|
+
* on avoidChannel; 0 if there are none.
|
|
99
|
+
*/
|
|
100
|
+
inline uint8_t pickMeshChannel(const std::vector<MeshChannelCandidate>& candidates,
|
|
101
|
+
uint8_t avoidChannel, uint8_t routerChannel = 0) {
|
|
102
|
+
const MeshChannelCandidate* elsewhere = nullptr;
|
|
103
|
+
const MeshChannelCandidate* here = nullptr;
|
|
104
|
+
for (const auto& c : candidates) {
|
|
105
|
+
if (!isValidMeshChannel(c.channel)) continue;
|
|
106
|
+
if (c.channel == routerChannel) return c.channel;
|
|
107
|
+
const MeshChannelCandidate*& slot = (c.channel == avoidChannel) ? here : elsewhere;
|
|
108
|
+
if (slot == nullptr || c.rssi > slot->rssi) slot = &c;
|
|
109
|
+
}
|
|
110
|
+
if (elsewhere != nullptr) return elsewhere->channel;
|
|
111
|
+
if (here != nullptr) return here->channel;
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
46
115
|
/**
|
|
47
116
|
* @brief Validation result structure for SharedGatewayConfig
|
|
48
117
|
*
|
|
@@ -597,11 +666,31 @@ class InternetHealthChecker {
|
|
|
597
666
|
}
|
|
598
667
|
status_.lastError = "Mock: No Internet in test environment";
|
|
599
668
|
return false;
|
|
669
|
+
#elif defined(ESP32) || defined(ESP8266)
|
|
670
|
+
WiFiClient client;
|
|
671
|
+
uint32_t started = millis();
|
|
672
|
+
#ifdef ESP32
|
|
673
|
+
bool connected = client.connect(checkHost_.c_str(), checkPort_, checkTimeout_);
|
|
674
|
+
#else
|
|
675
|
+
// ESP8266's WiFiClient lacks ESP32's per-connect timeout overload, so the
|
|
676
|
+
// timeout is set on the client instead. It is milliseconds, not seconds:
|
|
677
|
+
// WiFiClient inherits Stream::setTimeout ("maximum milliseconds to wait")
|
|
678
|
+
// and connect() hands _timeout straight to WiFi.hostByName(), whose
|
|
679
|
+
// parameter is named timeout_ms. Dividing by 1000 here gave DNS five
|
|
680
|
+
// milliseconds to resolve, so the check failed every time and an ESP8266
|
|
681
|
+
// shared gateway never reported local Internet.
|
|
682
|
+
client.setTimeout(checkTimeout_);
|
|
683
|
+
bool connected = client.connect(checkHost_.c_str(), checkPort_);
|
|
684
|
+
#endif
|
|
685
|
+
status_.lastLatencyMs = millis() - started;
|
|
686
|
+
if (!connected) {
|
|
687
|
+
status_.lastError = "TCP connectivity check failed";
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
client.stop();
|
|
691
|
+
return true;
|
|
600
692
|
#else
|
|
601
|
-
|
|
602
|
-
// Note: WiFiClient usage is handled in the arduino-specific code
|
|
603
|
-
// This base implementation returns false; override in wifi.hpp
|
|
604
|
-
status_.lastError = "Not implemented in base class";
|
|
693
|
+
status_.lastError = "Internet health checks are unsupported on this platform";
|
|
605
694
|
return false;
|
|
606
695
|
#endif
|
|
607
696
|
}
|
|
@@ -1053,6 +1142,182 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1053
1142
|
|
|
1054
1143
|
};
|
|
1055
1144
|
|
|
1145
|
+
// ===========================================================================
|
|
1146
|
+
// Scheduler-stall protection for blocking gateway requests
|
|
1147
|
+
// ===========================================================================
|
|
1148
|
+
|
|
1149
|
+
// The socket timeouts below are derived from NODE_TIMEOUT rather than written
|
|
1150
|
+
// as absolute numbers, so a gateway request cannot outlast the mesh watchdog in
|
|
1151
|
+
// *any* build. That is not hypothetical tidiness: the host test environment
|
|
1152
|
+
// overrides NODE_TIMEOUT to 5s (test/catch/Arduino.h shadows configuration.hpp
|
|
1153
|
+
// wholesale via its include guard), so hardcoded defaults sized for the 10s
|
|
1154
|
+
// production watchdog would blow the assertion below there.
|
|
1155
|
+
//
|
|
1156
|
+
// They live in this header, not in configuration.hpp, for the same reason --
|
|
1157
|
+
// gateway.hpp is reached through both configurations, configuration.hpp is not.
|
|
1158
|
+
//
|
|
1159
|
+
// At the production NODE_TIMEOUT of 10s this gives 5000ms for the request and
|
|
1160
|
+
// 2000ms for the captive-portal probe. Dividing by TASK_MILLISECOND first
|
|
1161
|
+
// normalises out the scheduler resolution, so the result is milliseconds under
|
|
1162
|
+
// _TASK_MICRO_RES too.
|
|
1163
|
+
|
|
1164
|
+
/** Socket timeout, in milliseconds, for a gateway Internet request.
|
|
1165
|
+
*
|
|
1166
|
+
* An HTTPClient call chain can wait on the socket twice — once sending the
|
|
1167
|
+
* request/reading the headers and once reading the body — so the blocking
|
|
1168
|
+
* budget below counts this value twice (issue #416). That is why the default
|
|
1169
|
+
* is NODE_TIMEOUT/5 rather than the pre-2.0 NODE_TIMEOUT/2: the *wall-clock*
|
|
1170
|
+
* worst case of the request, not one socket wait, has to fit inside the mesh
|
|
1171
|
+
* watchdog. Endpoints that genuinely need longer must raise NODE_TIMEOUT
|
|
1172
|
+
* along with this (the static_assert below enforces that). */
|
|
1173
|
+
#ifndef GATEWAY_HTTP_TIMEOUT_MS
|
|
1174
|
+
#define GATEWAY_HTTP_TIMEOUT_MS ((NODE_TIMEOUT) / TASK_MILLISECOND / 5)
|
|
1175
|
+
#endif
|
|
1176
|
+
|
|
1177
|
+
/** Socket timeout, in milliseconds, for the captive-portal probe. Counted
|
|
1178
|
+
* twice in the blocking budget, same as GATEWAY_HTTP_TIMEOUT_MS. */
|
|
1179
|
+
#ifndef GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS
|
|
1180
|
+
#define GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS ((NODE_TIMEOUT) / TASK_MILLISECOND / 10)
|
|
1181
|
+
#endif
|
|
1182
|
+
|
|
1183
|
+
/** Timeout, in milliseconds, for the DNS reachability probe (issue #416).
|
|
1184
|
+
*
|
|
1185
|
+
* Only the ESP8266 core exposes a hostByName() overload with a timeout
|
|
1186
|
+
* parameter; on ESP32 the probe is skipped entirely (the captive-portal
|
|
1187
|
+
* probe, which is an HTTP round trip bounded by
|
|
1188
|
+
* GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS, establishes reachability instead).
|
|
1189
|
+
* The budget counts this term unconditionally, which is conservative on
|
|
1190
|
+
* ESP32. */
|
|
1191
|
+
#ifndef GATEWAY_DNS_TIMEOUT_MS
|
|
1192
|
+
#define GATEWAY_DNS_TIMEOUT_MS ((NODE_TIMEOUT) / TASK_MILLISECOND / 10)
|
|
1193
|
+
#endif
|
|
1194
|
+
|
|
1195
|
+
/** How long, in milliseconds, a connectivity probe result stays cached.
|
|
1196
|
+
*
|
|
1197
|
+
* Both the DNS reachability check and the captive-portal probe are network
|
|
1198
|
+
* round trips. Running them per message would put an Internet round trip in
|
|
1199
|
+
* front of every single mesh->Internet send.
|
|
1200
|
+
*/
|
|
1201
|
+
#ifndef GATEWAY_CONNECTIVITY_CACHE_MS
|
|
1202
|
+
#define GATEWAY_CONNECTIVITY_CACHE_MS 60000UL
|
|
1203
|
+
#endif
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* @brief Wall-clock ceiling of the gateway's blocking calls on the
|
|
1207
|
+
* GATEWAY_DATA path, in milliseconds (issue #416).
|
|
1208
|
+
*
|
|
1209
|
+
* Each HTTPClient call chain is counted at *two* socket waits — GET()/POST()
|
|
1210
|
+
* (send + header read) and getString() (body read) — because
|
|
1211
|
+
* HTTPClient::setTimeout() bounds an individual socket wait, not the whole
|
|
1212
|
+
* call. The captive-portal probe and the destination request both have that
|
|
1213
|
+
* shape and run back to back, and the DNS reachability probe (bounded by
|
|
1214
|
+
* GATEWAY_DNS_TIMEOUT_MS on ESP8266, skipped on ESP32) runs before them
|
|
1215
|
+
* whenever the GATEWAY_CONNECTIVITY_CACHE_MS window has expired.
|
|
1216
|
+
*
|
|
1217
|
+
* @warning One residual is not in this budget: the HTTP calls resolve their
|
|
1218
|
+
* hostnames inside the platform core before their socket timeout
|
|
1219
|
+
* applies, and on ESP32 that in-request resolver wait is not
|
|
1220
|
+
* separately boundable in the cores this library targets. On a
|
|
1221
|
+
* network with blackholed DNS the request path can therefore still
|
|
1222
|
+
* exceed this ceiling on ESP32. See SECURITY.md "Gateway blocking:
|
|
1223
|
+
* the mesh partition risk".
|
|
1224
|
+
*/
|
|
1225
|
+
constexpr unsigned long gatewayBlockingBudgetMs() {
|
|
1226
|
+
return 2UL * static_cast<unsigned long>(GATEWAY_HTTP_TIMEOUT_MS) +
|
|
1227
|
+
2UL * static_cast<unsigned long>(GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS) +
|
|
1228
|
+
static_cast<unsigned long>(GATEWAY_DNS_TIMEOUT_MS);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// A gateway that can block longer than the mesh watchdog partitions the mesh
|
|
1232
|
+
// around itself (issues #318, #332). Catch what is expressible at compile time
|
|
1233
|
+
// -- both socket waits of each HTTP call plus the DNS probe; see the ESP32
|
|
1234
|
+
// resolver caveat above for the one wait this cannot cover. TASK_ constants
|
|
1235
|
+
// are scaled by the scheduler's resolution, so the comparison is written in
|
|
1236
|
+
// scheduler units to stay correct under _TASK_MICRO_RES too.
|
|
1237
|
+
static_assert(gatewayBlockingBudgetMs() * TASK_MILLISECOND < NODE_TIMEOUT,
|
|
1238
|
+
"Gateway blocking budget (2x GATEWAY_HTTP_TIMEOUT_MS + 2x "
|
|
1239
|
+
"GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS + GATEWAY_DNS_TIMEOUT_MS) "
|
|
1240
|
+
"must stay below NODE_TIMEOUT, or a gateway request stalls the "
|
|
1241
|
+
"scheduler for longer than its peers are willing to wait and "
|
|
1242
|
+
"the mesh partitions around the gateway. Raise NODE_TIMEOUT if "
|
|
1243
|
+
"you need a longer HTTP timeout.");
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Reserve the blocking HTTP budget on the requester's route watchdog.
|
|
1247
|
+
*
|
|
1248
|
+
* A gateway compensates its own peer watchdogs after a blocking request, but
|
|
1249
|
+
* it cannot modify the requester's timer. That timer may already be partly
|
|
1250
|
+
* spent, so extend its existing deadline by the bounded gateway budget before
|
|
1251
|
+
* sending. Disabled watchdogs remain disabled.
|
|
1252
|
+
*/
|
|
1253
|
+
template <typename T>
|
|
1254
|
+
bool reserveGatewayBlockingBudget(T& connection) {
|
|
1255
|
+
if (!connection.timeOutTask.isEnabled()) return false;
|
|
1256
|
+
connection.timeOutTask.adjust(static_cast<long>(
|
|
1257
|
+
gatewayBlockingBudgetMs() * TASK_MILLISECOND));
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* @brief Give every peer's running watchdog back the time a blocking call hid.
|
|
1263
|
+
*
|
|
1264
|
+
* Call this immediately after returning from a blocking Internet call, before
|
|
1265
|
+
* the scheduler next runs, passing the measured wall-clock duration of the
|
|
1266
|
+
* stall.
|
|
1267
|
+
*
|
|
1268
|
+
* Nothing executes while the gateway is inside `HTTPClient::GET()`/`POST()`,
|
|
1269
|
+
* but wall-clock time keeps passing. So a `timeOutTask` whose NODE_TIMEOUT
|
|
1270
|
+
* deadline fell during the stall is already overdue when the scheduler
|
|
1271
|
+
* resumes, and fires on the very next `execute()` -- closing a peer that never
|
|
1272
|
+
* actually went missing (issues #318, #332).
|
|
1273
|
+
*
|
|
1274
|
+
* The compensation equals the measured stall, no more (issue #417): each
|
|
1275
|
+
* enabled watchdog's existing deadline is postponed by `stalledMs` via
|
|
1276
|
+
* `Task::adjust()`, which extends `iDelay` without touching the task's
|
|
1277
|
+
* baseline. A peer's watchdog therefore measures only time the mesh was
|
|
1278
|
+
* actually able to observe the peer. The earlier implementation restarted
|
|
1279
|
+
* every watchdog from zero (`restartDelayed()`) on every exit path -- which
|
|
1280
|
+
* meant any live peer's gateway traffic granted a genuinely dead peer a
|
|
1281
|
+
* fresh full NODE_TIMEOUT, so the dead peer was never reaped.
|
|
1282
|
+
* `Task::delay()` would be wrong in the other direction: it resets the
|
|
1283
|
+
* baseline to now, shortening the deadline of a peer that still had more
|
|
1284
|
+
* than `stalledMs` remaining and disconnecting healthy peers early.
|
|
1285
|
+
*
|
|
1286
|
+
* Paths that did not block must pass 0 and compensate nothing.
|
|
1287
|
+
*
|
|
1288
|
+
* Only *enabled* watchdogs are touched, and this matters: `timeOutTask` is
|
|
1289
|
+
* armed by `nodeSyncTask` when a sync request goes out and disabled again when
|
|
1290
|
+
* the reply lands, so a disabled watchdog means the peer has nothing
|
|
1291
|
+
* outstanding. Enabling it here would invent a NODE_TIMEOUT deadline for an
|
|
1292
|
+
* idle-but-healthy link whose callback is `Connection::close()` -- turning a
|
|
1293
|
+
* reliability fix into a disconnect bug.
|
|
1294
|
+
*
|
|
1295
|
+
* This protects the gateway's own view of its peers. The peers' view of the
|
|
1296
|
+
* gateway depends on keeping the stall shorter than their watchdog, which is
|
|
1297
|
+
* what gatewayBlockingBudgetMs() is meant to bound -- though see the @warning
|
|
1298
|
+
* there for why that bound is not airtight (#416). Request-side reservation
|
|
1299
|
+
* plus gateway-side compensation are both needed.
|
|
1300
|
+
*
|
|
1301
|
+
* @tparam T Mesh type exposing `subs` (see painlessmesh::layout::Layout).
|
|
1302
|
+
* @param mesh The mesh whose direct peer connections should be refreshed.
|
|
1303
|
+
* @param stalledMs Measured wall-clock duration of the blocking section, in
|
|
1304
|
+
* milliseconds. 0 means nothing blocked and nothing is compensated.
|
|
1305
|
+
* @return Number of peers whose watchdog deadline was postponed.
|
|
1306
|
+
*/
|
|
1307
|
+
template <typename T>
|
|
1308
|
+
size_t refreshPeerWatchdogs(T& mesh, unsigned long stalledMs) {
|
|
1309
|
+
if (stalledMs == 0) return 0; // nothing blocked, nothing to give back
|
|
1310
|
+
size_t refreshed = 0;
|
|
1311
|
+
for (auto&& connection : mesh.subs) {
|
|
1312
|
+
if (!connection) continue;
|
|
1313
|
+
if (!connection->timeOutTask.isEnabled()) continue;
|
|
1314
|
+
connection->timeOutTask.adjust(
|
|
1315
|
+
static_cast<long>(stalledMs * TASK_MILLISECOND));
|
|
1316
|
+
++refreshed;
|
|
1317
|
+
}
|
|
1318
|
+
return refreshed;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1056
1321
|
} // namespace gateway
|
|
1057
1322
|
} // namespace painlessmesh
|
|
1058
1323
|
|
|
@@ -23,6 +23,67 @@ inline bool contains(protocol::NodeTree nodeTree, uint32_t nodeId) {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Remove the subtree rooted at nodeId from wherever it sits below tree.
|
|
28
|
+
*
|
|
29
|
+
* Only that node and what hangs under it go; the nodes on the way to it
|
|
30
|
+
* stay. Used when a node turns up on a fresh direct connection while a
|
|
31
|
+
* neighbour's tree still remembers its old place: a station has one
|
|
32
|
+
* uplink, so the old place is stale, and routing to it would send packets
|
|
33
|
+
* down a path that ends at a link that no longer exists.
|
|
34
|
+
*
|
|
35
|
+
* \return true if the node was found and removed.
|
|
36
|
+
*/
|
|
37
|
+
inline bool forget(protocol::NodeTree& tree, uint32_t nodeId) {
|
|
38
|
+
for (auto it = tree.subs.begin(); it != tree.subs.end(); ++it) {
|
|
39
|
+
if (it->nodeId == nodeId) {
|
|
40
|
+
tree.subs.erase(it);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (forget(*it, nodeId)) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Remove from tree every node that appears anywhere in `elsewhere`.
|
|
50
|
+
*
|
|
51
|
+
* A node is in one place. When a neighbour's sync presents the nodes
|
|
52
|
+
* below it, whatever another neighbour's cached tree still says about
|
|
53
|
+
* those nodes is older, and a packet routed by the older copy goes down a
|
|
54
|
+
* branch that ends at a link that no longer exists.
|
|
55
|
+
*
|
|
56
|
+
* \return how many nodes were removed.
|
|
57
|
+
*/
|
|
58
|
+
inline size_t forgetAll(protocol::NodeTree& tree,
|
|
59
|
+
const protocol::NodeTree& elsewhere) {
|
|
60
|
+
size_t removed = forget(tree, elsewhere.nodeId) ? 1 : 0;
|
|
61
|
+
for (auto&& s : elsewhere.subs) removed += forgetAll(tree, s);
|
|
62
|
+
return removed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A short identity for what a tree says: which nodes, in which order, which
|
|
67
|
+
* of them root or time authority. Never 0, so 0 can mean "nothing presented
|
|
68
|
+
* yet".
|
|
69
|
+
*
|
|
70
|
+
* A neighbour's sync is news only when this differs from its last one.
|
|
71
|
+
*/
|
|
72
|
+
inline uint32_t fingerprint(const protocol::NodeTree& tree,
|
|
73
|
+
uint32_t hash = 2166136261u) {
|
|
74
|
+
auto mix = [&hash](uint32_t v) {
|
|
75
|
+
for (int i = 0; i < 4; ++i) {
|
|
76
|
+
hash ^= (v >> (8 * i)) & 0xff;
|
|
77
|
+
hash *= 16777619u;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
mix(tree.nodeId);
|
|
81
|
+
mix((tree.root ? 1u : 0u) | (tree.hasTimeAuthority ? 2u : 0u));
|
|
82
|
+
for (auto&& s : tree.subs) hash = fingerprint(s, hash);
|
|
83
|
+
mix(0xffffffffu); // end of this node's subs
|
|
84
|
+
return hash == 0 ? 1 : hash;
|
|
85
|
+
}
|
|
86
|
+
|
|
26
87
|
inline protocol::NodeTree excludeRoute(protocol::NodeTree&& tree,
|
|
27
88
|
uint32_t exclude) {
|
|
28
89
|
// Make sure to exclude any subs with nodeId == 0,
|
|
@@ -44,12 +105,12 @@ class Layout {
|
|
|
44
105
|
* On the ESP hardware nodeId is uniquely calculated from the MAC address of
|
|
45
106
|
* the node.
|
|
46
107
|
*/
|
|
47
|
-
uint32_t getNodeId() { return nodeId; }
|
|
108
|
+
uint32_t getNodeId() const { return nodeId; }
|
|
48
109
|
|
|
49
110
|
/**
|
|
50
111
|
* Check whether this node is a root node.
|
|
51
112
|
*/
|
|
52
|
-
bool isRoot() { return root; }
|
|
113
|
+
bool isRoot() const { return root; }
|
|
53
114
|
|
|
54
115
|
protocol::NodeTree asNodeTree() {
|
|
55
116
|
auto nt = protocol::NodeTree(nodeId, root, hasTimeAuthority);
|
|
@@ -84,6 +145,13 @@ class Neighbour : public protocol::NodeTree {
|
|
|
84
145
|
// Inherit constructors
|
|
85
146
|
using protocol::NodeTree::NodeTree;
|
|
86
147
|
|
|
148
|
+
/**
|
|
149
|
+
* fingerprint() of the tree this neighbour presented last, 0 before its
|
|
150
|
+
* first sync. A sync that restates it carries nothing the cached tree
|
|
151
|
+
* does not already reflect, however the cache has since been pruned.
|
|
152
|
+
*/
|
|
153
|
+
uint32_t presented = 0;
|
|
154
|
+
|
|
87
155
|
/**
|
|
88
156
|
* Is the passed nodesync valid
|
|
89
157
|
*
|
|
@@ -32,6 +32,14 @@ typedef enum {
|
|
|
32
32
|
|
|
33
33
|
class LogClass {
|
|
34
34
|
public:
|
|
35
|
+
// Where messages go instead of Serial. The sketch owns framing: on a board
|
|
36
|
+
// whose serial port carries a line protocol, mesh logs written straight to
|
|
37
|
+
// Serial from a Wi-Fi event task splice into the sketch's own frames, so
|
|
38
|
+
// the sketch queues them here and writes them between its frames. Called
|
|
39
|
+
// from whichever task logged, so keep it short and re-entrant.
|
|
40
|
+
typedef void (*Sink)(LogLevel type, const char *message);
|
|
41
|
+
void setSink(Sink newSink) { sink = newSink; }
|
|
42
|
+
|
|
35
43
|
void setLogLevel(uint16_t newTypes) {
|
|
36
44
|
// set the different kinds of debug messages you want to generate.
|
|
37
45
|
types = newTypes;
|
|
@@ -82,6 +90,12 @@ class LogClass {
|
|
|
82
90
|
|
|
83
91
|
vsnprintf(str, 200, format, args);
|
|
84
92
|
|
|
93
|
+
if (sink) {
|
|
94
|
+
sink(type, str);
|
|
95
|
+
va_end(args);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
if (types) {
|
|
86
100
|
switch (type) {
|
|
87
101
|
case ERROR:
|
|
@@ -153,6 +167,7 @@ class LogClass {
|
|
|
153
167
|
|
|
154
168
|
private:
|
|
155
169
|
uint16_t types = 0;
|
|
170
|
+
Sink sink = nullptr;
|
|
156
171
|
char str[200];
|
|
157
172
|
std::list<std::pair<uint32_t, TSTRING>> remote_queue;
|
|
158
173
|
uint32_t remote_uuid;
|