@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
@@ -249,8 +249,8 @@ void handleTimeDelay(T& mesh, painlessmesh::protocol::TimeDelay timeDelay,
249
249
  }
250
250
 
251
251
  template <class T, typename U>
252
- callback::MeshPackageCallbackList<U> addPackageCallback(
253
- callback::MeshPackageCallbackList<U>&& callbackList, T& mesh) {
252
+ void addPackageCallback(callback::MeshPackageCallbackList<U>& callbackList,
253
+ T& mesh) {
254
254
  // TimeSync
255
255
  callbackList.onPackage(
256
256
  protocol::TIME_SYNC,
@@ -270,8 +270,6 @@ callback::MeshPackageCallbackList<U> addPackageCallback(
270
270
  handleTimeDelay<T, U>(mesh, timeDelay, connection, receivedAt);
271
271
  return false;
272
272
  });
273
-
274
- return callbackList;
275
273
  }
276
274
 
277
275
  } // namespace ntp
@@ -5,6 +5,7 @@
5
5
  #include "painlessmesh/configuration.hpp"
6
6
 
7
7
  #include "painlessmesh/router.hpp"
8
+ #include <utility>
8
9
  #include <vector>
9
10
 
10
11
  namespace painlessmesh {
@@ -187,14 +188,19 @@ class PackageHandler : public layout::Layout<T> {
187
188
  // in the middle of its own execution - causing a use-after-free crash
188
189
  // (same bug family as upstream issue #373, but triggered by the
189
190
  // shared_ptr refcount instead of a raw delete in onDisable).
190
- // We simply leave it in the list: addTask() will recognise it as
191
- // "disabled with a single reference" and reuse it on the next call,
192
- // exactly like it already does for disabled anonymous tasks (see the
193
- // comment on addTask() above).
191
+ // Move it out of the reusable list while retaining ownership for the
192
+ // handler's lifetime. This also protects a stop()/init() sequence inside
193
+ // the callback: a new scheduler must receive a new task rather than
194
+ // mutating the task that is still executing on the old scheduler.
194
195
  Task* current = scheduler ? scheduler->getCurrentTask() : nullptr;
195
196
  for (auto it = taskList.begin(); it != taskList.end();) {
196
197
  if (current != nullptr && it->get() == current) {
197
- ++it;
198
+ // Keep the executing task alive, but remove it from the reusable
199
+ // pool. A callback may stop() and immediately init() with a new
200
+ // scheduler; reusing this still-running task would rewrite its
201
+ // closure and would not attach it to that new scheduler.
202
+ quarantinedTasks.push_back({scheduler, *it});
203
+ it = taskList.erase(it);
198
204
  continue;
199
205
  }
200
206
  (*it)->disable();
@@ -247,6 +253,7 @@ class PackageHandler : public layout::Layout<T> {
247
253
  long aIterations,
248
254
  std::function<void()> aCallback) {
249
255
  using namespace painlessmesh::logger;
256
+ reclaimQuarantinedTasks();
250
257
  for (auto&& task : taskList) {
251
258
  if (task.use_count() == 1 && !task->isEnabled()) {
252
259
  task->set(aInterval, aIterations, aCallback, NULL, NULL);
@@ -281,11 +288,28 @@ class PackageHandler : public layout::Layout<T> {
281
288
  }
282
289
 
283
290
  protected:
291
+ void reclaimQuarantinedTasks() {
292
+ for (auto it = quarantinedTasks.begin(); it != quarantinedTasks.end();) {
293
+ if (it->first != nullptr &&
294
+ it->first->getCurrentTask() == it->second.get()) {
295
+ ++it;
296
+ continue;
297
+ }
298
+ it->second->disable();
299
+ it->second->setCallback(NULL);
300
+ it = quarantinedTasks.erase(it);
301
+ }
302
+ }
303
+
284
304
  callback::MeshPackageCallbackList<T> callbackList;
285
305
  std::list<std::shared_ptr<Task> > taskList = {};
306
+ // Tasks detached while their own callback is executing, paired with the
307
+ // scheduler whose stack still references them. They become reclaimable
308
+ // after that scheduler no longer reports the task as current.
309
+ std::list<std::pair<Scheduler*, std::shared_ptr<Task> > > quarantinedTasks =
310
+ {};
286
311
  };
287
312
 
288
313
  } // namespace plugin
289
314
  } // namespace painlessmesh
290
315
  #endif
291
-
@@ -62,6 +62,17 @@ constexpr int GATEWAY_DATA = 620; // Gateway data package for Internet rou
62
62
  constexpr int GATEWAY_ACK = 621; // Gateway acknowledgment package
63
63
  constexpr int GATEWAY_HEARTBEAT = 622; // Gateway heartbeat for health monitoring
64
64
 
65
+ // Message delivery confirmation types
66
+ constexpr int MESSAGE_ACK = 630; // Per-message delivery acknowledgment (issue #379)
67
+
68
+ // Outbound queue priority levels, in SentBuffer scheduling order (lower value
69
+ // drains first). PRIORITY_NORMAL is the wire default: a package whose priority
70
+ // equals it omits the "prio" key entirely.
71
+ constexpr uint8_t PRIORITY_CRITICAL = 0;
72
+ constexpr uint8_t PRIORITY_HIGH = 1;
73
+ constexpr uint8_t PRIORITY_NORMAL = 2;
74
+ constexpr uint8_t PRIORITY_LOW = 3;
75
+
65
76
  class PackageInterface {
66
77
  public:
67
78
  virtual JsonObject addTo(JsonObject&& jsonObj) const = 0;
@@ -81,6 +92,15 @@ class Single : public PackageInterface {
81
92
  uint32_t from;
82
93
  uint32_t dest;
83
94
  TSTRING msg = "";
95
+ // Non-zero when the sender requested a delivery acknowledgment. Only
96
+ // serialized when set, so plain sends carry zero overhead.
97
+ uint32_t msgId = 0;
98
+ // Outbound queue priority: 0=CRITICAL, 1=HIGH, 2=NORMAL, 3=LOW. Carried on
99
+ // the wire (as "prio") so intermediate hops re-enqueue a forwarded package
100
+ // at the sender's priority instead of silently dropping it to NORMAL after
101
+ // the first hop (issue #384). Only serialized when it deviates from NORMAL,
102
+ // so default sends carry zero overhead. Pre-2.0 receivers ignore the key.
103
+ uint8_t priority = PRIORITY_NORMAL;
84
104
 
85
105
  Single() {}
86
106
  Single(uint32_t fromID, uint32_t destID, TSTRING& message) {
@@ -93,6 +113,8 @@ class Single : public PackageInterface {
93
113
  dest = jsonObj["dest"].as<uint32_t>();
94
114
  from = jsonObj["from"].as<uint32_t>();
95
115
  msg = jsonObj["msg"].as<TSTRING>();
116
+ msgId = jsonObj["msgId"] | (uint32_t)0;
117
+ priority = jsonObj["prio"] | PRIORITY_NORMAL;
96
118
  }
97
119
 
98
120
  JsonObject addTo(JsonObject&& jsonObj) const {
@@ -100,12 +122,16 @@ class Single : public PackageInterface {
100
122
  jsonObj["dest"] = dest;
101
123
  jsonObj["from"] = from;
102
124
  jsonObj["msg"] = msg;
125
+ if (msgId != 0) jsonObj["msgId"] = msgId;
126
+ if (priority != PRIORITY_NORMAL) jsonObj["prio"] = priority;
103
127
  return jsonObj;
104
128
  }
105
129
 
106
130
  #if ARDUINOJSON_VERSION_MAJOR < 7
107
131
  size_t jsonObjectSize() const {
108
- return JSON_OBJECT_SIZE(4) + ceil(1.1 * msg.length());
132
+ return JSON_OBJECT_SIZE((msgId != 0 ? 5 : 4) +
133
+ (priority != PRIORITY_NORMAL ? 1 : 0)) +
134
+ ceil(1.1 * msg.length());
109
135
  }
110
136
  #endif
111
137
  };
@@ -127,7 +153,9 @@ class Broadcast : public Single {
127
153
 
128
154
  #if ARDUINOJSON_VERSION_MAJOR < 7
129
155
  size_t jsonObjectSize() const {
130
- return JSON_OBJECT_SIZE(4) + ceil(1.1 * msg.length());
156
+ return JSON_OBJECT_SIZE((msgId != 0 ? 5 : 4) +
157
+ (priority != PRIORITY_NORMAL ? 1 : 0)) +
158
+ ceil(1.1 * msg.length());
131
159
  }
132
160
  #endif
133
161
  };
@@ -717,6 +745,31 @@ class Variant {
717
745
  */
718
746
  int type() { return jsonObj["type"].as<int>(); }
719
747
 
748
+ /**
749
+ * Origin node of the package (0 if not present)
750
+ *
751
+ * Lightweight field peek — unlike to<T>() this does not materialize a
752
+ * package object or copy the message payload.
753
+ */
754
+ uint32_t from() { return jsonObj["from"] | (uint32_t)0; }
755
+
756
+ /**
757
+ * Delivery-confirmation id of the package (0 = no ack requested)
758
+ *
759
+ * Lightweight field peek — unlike to<T>() this does not materialize a
760
+ * package object or copy the message payload.
761
+ */
762
+ uint32_t msgId() { return jsonObj["msgId"] | (uint32_t)0; }
763
+
764
+ /**
765
+ * Outbound queue priority of the package (PRIORITY_NORMAL if not present)
766
+ *
767
+ * Lightweight field peek — unlike to<T>() this does not materialize a
768
+ * package object or copy the message payload. Used by forwarding hops to
769
+ * re-enqueue a routed package at the sender's priority (issue #384).
770
+ */
771
+ uint8_t priority() { return jsonObj["prio"] | PRIORITY_NORMAL; }
772
+
720
773
  /**
721
774
  * Package routing method
722
775
  */
@@ -17,18 +17,47 @@ namespace painlessmesh {
17
17
  * Helper functions to route messages
18
18
  */
19
19
  namespace router {
20
+ // Layouts are taken by const reference throughout this header: Layout<T>::subs
21
+ // is a std::list of shared_ptrs, so passing by value used to copy the whole
22
+ // connection list (one heap allocation per connection) on every packet sent,
23
+ // broadcast or forwarded (issue #387).
20
24
  template <class T>
21
- std::shared_ptr<T> findRoute(layout::Layout<T> tree,
25
+ std::shared_ptr<T> findRoute(const layout::Layout<T>& tree,
22
26
  std::function<bool(std::shared_ptr<T>)> func) {
23
27
  auto route = std::find_if(tree.subs.begin(), tree.subs.end(), func);
24
28
  if (route == tree.subs.end()) return NULL;
25
29
  return (*route);
26
30
  }
27
31
 
32
+ /** The live connection through which nodeId is reachable, or NULL.
33
+ *
34
+ * A closed connection stays in subs until eraseClosedConnections() next
35
+ * runs. Routing a packet to it is a silent loss: the write is queued into
36
+ * a buffer nothing will ever drain, and the sender is told it succeeded.
37
+ * On the Alteriom HIL rig, correlating every unacknowledged delivery with
38
+ * the receiver's log showed the message had usually never arrived at all —
39
+ * 27 of 33 across three suites — which is this. A dead link is not a
40
+ * route, for any purpose; the liveness test is the one
41
+ * layout::syncLayout() already applies.
42
+ */
28
43
  template <class T>
29
- std::shared_ptr<T> findRoute(layout::Layout<T> tree, uint32_t nodeId) {
44
+ std::shared_ptr<T> findRoute(const layout::Layout<T>& tree, uint32_t nodeId) {
30
45
  return findRoute<T>(tree, [nodeId](std::shared_ptr<T> s) {
31
- return layout::contains((*s), nodeId);
46
+ return s->connected() && layout::contains((*s), nodeId);
47
+ });
48
+ }
49
+
50
+ /** findRoute() for the duplicate-connection check in handleNodeSync().
51
+ *
52
+ * `exclude` drops the connection being judged, which cannot duplicate
53
+ * itself. Refusing a live direct connection on the authority of a dead
54
+ * route left a node with neither once the dead one was erased.
55
+ */
56
+ template <class T>
57
+ std::shared_ptr<T> findLiveRoute(const layout::Layout<T>& tree, uint32_t nodeId,
58
+ std::shared_ptr<T> exclude = nullptr) {
59
+ return findRoute<T>(tree, [nodeId, exclude](std::shared_ptr<T> s) {
60
+ return s != exclude && s->connected() && layout::contains((*s), nodeId);
32
61
  });
33
62
  }
34
63
 
@@ -95,53 +124,49 @@ bool sendWithPriority(protocol::Variant&& variant, std::shared_ptr<U> conn, uint
95
124
  return conn->addMessageWithPriority(msg, priorityLevel);
96
125
  }
97
126
 
98
- template <class T, class U>
99
- bool send(T& package, layout::Layout<U> layout) {
100
- painlessmesh::protocol::Variant variant(package);
101
- TSTRING msg;
102
- variant.printTo(msg);
103
- auto conn = findRoute<U>(layout, variant.dest());
104
- if (conn) return conn->addMessage(msg);
105
- return false;
106
- }
127
+ // The layout-level send and broadcast functions below all funnel into the
128
+ // protocol::Variant& core overloads, which enqueue at the priority carried in
129
+ // the package's "prio" field (PRIORITY_NORMAL when absent). This is what keeps
130
+ // a sender's priority attached to a package across intermediate hops instead
131
+ // of silently dropping it to NORMAL after the first hop (issue #384): the
132
+ // forwarding path in routePackage() re-reads the field from the wire.
107
133
 
108
134
  template <class U>
109
- bool send(protocol::Variant& variant, layout::Layout<U> layout) {
135
+ bool send(protocol::Variant& variant, const layout::Layout<U>& layout) {
110
136
  TSTRING msg;
111
137
  variant.printTo(msg);
112
138
  auto conn = findRoute<U>(layout, variant.dest());
113
- if (conn) return conn->addMessage(msg);
139
+ if (conn) return conn->addMessageWithPriority(msg, variant.priority());
114
140
  return false;
115
141
  }
116
142
 
117
143
  template <class T, class U>
118
- bool send(T&& package, layout::Layout<U> layout) {
144
+ bool send(T& package, const layout::Layout<U>& layout) {
119
145
  painlessmesh::protocol::Variant variant(package);
120
- TSTRING msg;
121
- variant.printTo(msg);
122
- auto conn = findRoute<U>(layout, variant.dest());
123
- if (conn) return conn->addMessage(msg);
124
- return false;
146
+ return send<U>(variant, layout);
147
+ }
148
+
149
+ template <class T, class U>
150
+ bool send(T&& package, const layout::Layout<U>& layout) {
151
+ painlessmesh::protocol::Variant variant(package);
152
+ return send<U>(variant, layout);
125
153
  }
126
154
 
127
155
  template <class U>
128
- bool send(protocol::Variant&& variant, layout::Layout<U> layout) {
129
- TSTRING msg;
130
- variant.printTo(msg);
131
- auto conn = findRoute<U>(layout, variant.dest());
132
- if (conn) return conn->addMessage(msg);
133
- return false;
156
+ bool send(protocol::Variant&& variant, const layout::Layout<U>& layout) {
157
+ return send<U>(variant, layout);
134
158
  }
135
159
 
136
- template <class T, class U>
137
- size_t broadcast(T& package, layout::Layout<U> layout, uint32_t exclude) {
138
- painlessmesh::protocol::Variant variant(package);
160
+ template <class T>
161
+ size_t broadcast(protocol::Variant& variant, const layout::Layout<T>& layout,
162
+ uint32_t exclude) {
139
163
  TSTRING msg;
140
164
  variant.printTo(msg);
165
+ const auto priority = variant.priority();
141
166
  size_t i = 0;
142
167
  for (auto&& conn : layout.subs) {
143
168
  if (conn->nodeId != 0 && conn->nodeId != exclude) {
144
- auto sent = conn->addMessage(msg);
169
+ auto sent = conn->addMessageWithPriority(msg, priority);
145
170
  if (sent) ++i;
146
171
  }
147
172
  }
@@ -149,53 +174,29 @@ size_t broadcast(T& package, layout::Layout<U> layout, uint32_t exclude) {
149
174
  }
150
175
 
151
176
  template <class T, class U>
152
- size_t broadcast(T&& package, layout::Layout<U> layout, uint32_t exclude) {
177
+ size_t broadcast(T& package, const layout::Layout<U>& layout,
178
+ uint32_t exclude) {
153
179
  painlessmesh::protocol::Variant variant(package);
154
- TSTRING msg;
155
- variant.printTo(msg);
156
- size_t i = 0;
157
- for (auto&& conn : layout.subs) {
158
- if (conn->nodeId != 0 && conn->nodeId != exclude) {
159
- auto sent = conn->addMessage(msg);
160
- if (sent) ++i;
161
- }
162
- }
163
- return i;
180
+ return broadcast<U>(variant, layout, exclude);
164
181
  }
165
182
 
166
- template <class T>
167
- size_t broadcast(protocol::Variant& variant, layout::Layout<T> layout,
183
+ template <class T, class U>
184
+ size_t broadcast(T&& package, const layout::Layout<U>& layout,
168
185
  uint32_t exclude) {
169
- TSTRING msg;
170
- variant.printTo(msg);
171
- size_t i = 0;
172
- for (auto&& conn : layout.subs) {
173
- if (conn->nodeId != 0 && conn->nodeId != exclude) {
174
- auto sent = conn->addMessage(msg);
175
- if (sent) ++i;
176
- }
177
- }
178
- return i;
186
+ painlessmesh::protocol::Variant variant(package);
187
+ return broadcast<U>(variant, layout, exclude);
179
188
  }
180
189
 
181
190
  template <class T>
182
- size_t broadcast(protocol::Variant&& variant, layout::Layout<T> layout,
191
+ size_t broadcast(protocol::Variant&& variant, const layout::Layout<T>& layout,
183
192
  uint32_t exclude) {
184
- TSTRING msg;
185
- variant.printTo(msg);
186
- size_t i = 0;
187
- for (auto&& conn : layout.subs) {
188
- if (conn->nodeId != 0 && conn->nodeId != exclude) {
189
- auto sent = conn->addMessage(msg);
190
- if (sent) ++i;
191
- }
192
- }
193
- return i;
193
+ return broadcast<T>(variant, layout, exclude);
194
194
  }
195
195
 
196
196
  template <class T>
197
- void routePackage(layout::Layout<T> layout, std::shared_ptr<T> connection,
198
- const TSTRING& pkg, callback::MeshPackageCallbackList<T> cbl,
197
+ void routePackage(const layout::Layout<T>& layout,
198
+ std::shared_ptr<T> connection, const TSTRING& pkg,
199
+ callback::MeshPackageCallbackList<T>& cbl,
199
200
  uint32_t receivedAt) {
200
201
  using namespace logger;
201
202
  Log(COMMUNICATION, "routePackage(): Recvd from %u: %s\n", connection->nodeId,
@@ -287,15 +288,63 @@ void handleNodeSync(T& mesh, protocol::NodeTree newTree,
287
288
  }
288
289
 
289
290
  if (conn->newConnection) {
290
- auto oldConnection = router::findRoute<U>(mesh, newTree.nodeId);
291
- if (oldConnection) {
291
+ // The loop check is the tree the new node presents: if this node is
292
+ // anywhere in it, the new connection would close a cycle. That is the
293
+ // only thing a second route to the same node can legitimately mean —
294
+ // a station has exactly one uplink, so a node that arrives on a fresh
295
+ // direct connection with a tree that does not contain us has left
296
+ // wherever else we remember it. Refusing it as "already connected" on
297
+ // the authority of that memory used to hold a rebooted node out of
298
+ // the mesh until the neighbour whose tree still carried it timed the
299
+ // old link out: every AP in turn dropped it a second after the
300
+ // association, for 30 to 100 s per reboot, measured on the Alteriom
301
+ // HIL rig on every restart a suite performs.
302
+ if (layout::contains(newTree, mesh.getNodeId())) {
303
+ // This node in the presented tree is a cycle only if the presenter is
304
+ // also reachable from here through some other live connection — the
305
+ // two ends of the loop. Without that route it is the presenter's
306
+ // memory of where this node used to be, held in a branch its owner
307
+ // has not timed out yet: a newly promoted bridge listed the node that
308
+ // came to join it at the place it held before the promotion, and was
309
+ // refused as a loop on every attempt for the whole promotion window.
310
+ // Stale, the mention is dropped before the tree is taken.
311
+ auto otherRoute = router::findLiveRoute<U>(mesh, newTree.nodeId, conn);
312
+ if (otherRoute) {
313
+ Log(logger::SYNC,
314
+ "handleNodeSync(): %u's tree contains this node and %u is already "
315
+ "reachable through %u: a loop. Closing the new connection\n",
316
+ newTree.nodeId, newTree.nodeId, otherRoute->nodeId);
317
+ Log.remote("Loop through %u\n", newTree.nodeId);
318
+ conn->close();
319
+ return;
320
+ }
292
321
  Log(logger::SYNC,
293
- "handleNodeSync(): already connected to %u. Closing the new "
294
- "connection \n",
322
+ "handleNodeSync(): %u's tree lists this node where it used to be; "
323
+ "stale, not a loop\n",
295
324
  newTree.nodeId);
296
- Log.remote("Already connected to %u\n", newTree.nodeId);
297
- conn->close();
298
- return;
325
+ layout::forget(newTree, mesh.getNodeId());
326
+ }
327
+ // Whatever else still routes to this node is stale. A direct link to
328
+ // it is the one it had before it went away — TCP has not noticed yet —
329
+ // and closes now instead of at its timeout. A route through a
330
+ // neighbour is that neighbour's memory of the node's old place; the
331
+ // node is taken out of it here so packets go down the live link, and
332
+ // the neighbour's next sync brings its own tree up to date.
333
+ auto oldConnection = router::findLiveRoute<U>(mesh, newTree.nodeId, conn);
334
+ if (oldConnection) {
335
+ if (oldConnection->nodeId == newTree.nodeId) {
336
+ Log(logger::SYNC,
337
+ "handleNodeSync(): %u connected again while its old link is "
338
+ "still open; closing the old one\n",
339
+ newTree.nodeId);
340
+ oldConnection->close();
341
+ } else {
342
+ Log(logger::SYNC,
343
+ "handleNodeSync(): %u was reachable through %u; that place is "
344
+ "stale, the direct connection wins\n",
345
+ newTree.nodeId, oldConnection->nodeId);
346
+ layout::forget(*oldConnection, newTree.nodeId);
347
+ }
299
348
  }
300
349
  auto remoteNodeId = newTree.nodeId;
301
350
  mesh.addTask([&mesh, remoteNodeId]() {
@@ -324,6 +373,74 @@ void handleNodeSync(T& mesh, protocol::NodeTree newTree,
324
373
  conn->newConnection = false;
325
374
  }
326
375
 
376
+ // What a neighbour presents is news only when it differs from what it
377
+ // presented last time. The cached tree can differ from a restated claim
378
+ // because a fresher neighbour has since taken a node out of it (below),
379
+ // and taking the restatement as news put the node back, marked the
380
+ // connection changed, forced the fresher neighbour's sync, which took it
381
+ // out again: a sync every 30 to 80 ms between the two for the 10 s it
382
+ // took the restating neighbour to time out the dead link behind its
383
+ // claim, on every board that heard both.
384
+ //
385
+ // (A neighbour listing a node that is on a direct link of ours is not
386
+ // pruned here, tempting as that is: it is also the shape a loop takes
387
+ // while the trees grow round it, and the loop checks above need to see
388
+ // it. The ring of five in the desktop integration suite never broke up
389
+ // with that pruning in place.)
390
+ auto fingerprint = layout::fingerprint(newTree);
391
+ bool restated = conn->presented == fingerprint;
392
+ conn->presented = fingerprint;
393
+
394
+ if (restated) {
395
+ conn->nodeSyncTask.delay();
396
+ mesh.stability += (std::min)(1000 - mesh.stability, (size_t)25);
397
+ return;
398
+ }
399
+
400
+ // A node is in one place, and a changed sync is the freshest word on
401
+ // every node below conn. Any other neighbour whose cached tree still
402
+ // lists one of them lists it where it used to be: on the rig every board
403
+ // carried a node twice — under the neighbour it had moved to and under
404
+ // the one it had left — and a message routed by the older copy never
405
+ // arrived, nor did the Internet request that went the same way. The
406
+ // older copies go now; their owners' next changed syncs agree.
407
+ for (auto&& other : mesh.subs) {
408
+ if (other == conn || other->nodeId == 0) continue;
409
+ size_t removed = layout::forgetAll(*other, newTree);
410
+ if (removed) {
411
+ Log(logger::SYNC,
412
+ "handleNodeSync(): %u nodes now under %u were still listed under "
413
+ "%u; forgotten there\n",
414
+ (unsigned)removed, conn->nodeId, other->nodeId);
415
+ }
416
+ }
417
+
418
+ // A node taken out of another neighbour's cache above can only come back
419
+ // through that neighbour's own sync, and a restatement is skipped. So
420
+ // when this neighbour stops presenting a node it used to — the node has
421
+ // moved on, or the claim was the stale one — the others' restatements
422
+ // are taken in full again: the change marks the connection changed,
423
+ // which forces their syncs, and whichever of them still lists the node
424
+ // has it back. Only a removal does this; an addition re-adopted this way
425
+ // would prune the presenter here, mark a change, force a sync there, and
426
+ // start the ping-pong over. Without the repair, the desktop time-sync
427
+ // scenario failed two runs in three: ntp::adopt() weighs the cached
428
+ // subtrees to choose which side keeps its clock, and a node missing from
429
+ // both sides' trees had the two ends disagree.
430
+ if (conn->nodeId != 0) {
431
+ bool dropped = false;
432
+ for (auto&& id : layout::asList(*conn, false)) {
433
+ if (!layout::contains(newTree, id)) {
434
+ dropped = true;
435
+ break;
436
+ }
437
+ }
438
+ if (dropped) {
439
+ for (auto&& other : mesh.subs) {
440
+ if (other != conn) other->presented = 0;
441
+ }
442
+ }
443
+ }
327
444
  if (conn->updateSubs(newTree)) {
328
445
  auto nodeId = newTree.nodeId;
329
446
  mesh.addTask(
@@ -335,8 +452,8 @@ void handleNodeSync(T& mesh, protocol::NodeTree newTree,
335
452
  }
336
453
 
337
454
  template <class T, typename U>
338
- callback::MeshPackageCallbackList<U> addPackageCallback(
339
- callback::MeshPackageCallbackList<U>&& callbackList, T& mesh) {
455
+ void addPackageCallback(callback::MeshPackageCallbackList<U>& callbackList,
456
+ T& mesh) {
340
457
  // REQUEST type,
341
458
  callbackList.onPackage(
342
459
  protocol::NODE_SYNC_REQUEST,
@@ -359,8 +476,6 @@ callback::MeshPackageCallbackList<U> addPackageCallback(
359
476
  connection->timeOutTask.disable();
360
477
  return false;
361
478
  });
362
-
363
- return callbackList;
364
479
  }
365
480
 
366
481
  } // namespace router
@@ -160,7 +160,17 @@ void initServer(AsyncServer &server, M &mesh) {
160
160
  conn->initTasks();
161
161
  mesh.subs.push_back(conn);
162
162
  mesh.semaphoreGive();
163
+ return;
163
164
  }
165
+ // The mesh was busy for the whole second the semaphore allows. The
166
+ // stack has accepted the client already; left like this it is
167
+ // neither the mesh's nor closed, and the peer waits on a link that
168
+ // will never carry a node sync. Say so, and close it, so the peer
169
+ // tries again at once rather than after its own timeout.
170
+ Log(ERROR,
171
+ "New AP connection refused: mesh busy, closing it so the peer "
172
+ "retries\n");
173
+ client->close(true);
164
174
  },
165
175
  NULL);
166
176
  server.begin();