@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
@@ -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
- // Increased from 250ms to 500ms (v1.9.14) then to 1000ms (v1.9.15) to support ESP32-C6
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
- sentBuffer.push(data, priority);
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
- sentBuffer.pushWithPriority(data, priorityLevel);
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
- #include "painlessmesh/message_tracker.hpp"
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
- // Arduino/ESP environment - actual TCP check
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;