@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
package/CHANGELOG.md CHANGED
@@ -7,6 +7,488 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.0] - 2026-09-07
11
+
12
+ painlessMesh 2.0 is a major release. It adds per-message delivery
13
+ confirmation and a unified send path, and it is the first release whose
14
+ gateway, failover, routing and radio behaviour was validated on hardware:
15
+ every entry in the *hardware-validated series* below was found in the serial
16
+ logs of the Alteriom HIL rig — an ESP32, ESP32-C3, ESP32-C5, ESP32-C6,
17
+ ESP32-S3 and ESP8266 in one mesh, with a real router and upstream — and
18
+ confirmed there. The release candidate passed the rig's whole suite (26
19
+ scenarios: mesh formation, delivery and acknowledgement, priorities,
20
+ dedicated and shared gateways, Internet relay, gateway failover, mesh OTA,
21
+ sustained soak) three times in a row, 25 passed and 1 skipped per run; the
22
+ skip is the power-cut scenario, which needs per-port USB power the rig does
23
+ not have.
24
+
25
+ ### Before you upgrade
26
+
27
+ **Wire protocol.** 2.0 adds to the protocol; it does not change what 1.x
28
+ nodes already send. A 1.x node forwards what it does not understand and
29
+ ignores fields it does not know, so a mixed fleet keeps working, but the
30
+ new behaviour only holds end-to-end once every node on the path runs 2.0:
31
+
32
+ - `MESSAGE_ACK` (type 630) — the acknowledgement a 2.0 receiver returns for
33
+ a message sent with a delivery callback. A 1.x receiver never sends one,
34
+ so the sender's callback reports `delivered = false` for it.
35
+ - `msgId` on `SINGLE` and `BROADCAST`, only when a callback was requested.
36
+ - `prio` on `SINGLE` and `BROADCAST`, only when it differs from normal; a
37
+ 1.x forwarder ignores it and forwards at normal priority.
38
+ - `routerChannel` on `BRIDGE_ELECTION` and `BRIDGE_TAKEOVER`, so peers
39
+ follow an elected bridge to its channel at once; a peer that misses it
40
+ still recovers by scanning.
41
+ - `leaving: true` on `BRIDGE_STATUS` when a bridge stops cleanly
42
+ (`mesh.stop()`), so candidates hold their election within seconds instead
43
+ of waiting for the bridge's last status to age out.
44
+
45
+ **Upgrade receivers and forwarders before the senders that will rely on
46
+ delivery callbacks or priorities**, and only read `delivered = false` as a
47
+ loss signal once the whole mesh runs 2.0.
48
+
49
+ **Defaults that changed.**
50
+
51
+ - `GATEWAY_HTTP_TIMEOUT_MS` was a hardcoded 30 s; it is now derived from
52
+ `NODE_TIMEOUT` (2000 ms at the stock 10 s watchdog), the captive-portal
53
+ probe is bounded by `GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS` (1000 ms) and cached
54
+ for 60 s, and the DNS probe by `GATEWAY_DNS_TIMEOUT_MS`. A gateway that
55
+ needs longer must raise `NODE_TIMEOUT` with it; a `static_assert` says so.
56
+ - A node that finds no mesh re-detects the channel after 2 empty scans
57
+ (about 30 s) instead of 6 (90 s).
58
+ - The Arduino core's station auto-reconnect is off for the mesh link on
59
+ every core; the library reconnects by its own scan rules. A bridge's
60
+ router link keeps the core's auto-reconnect.
61
+ - On ESP8266 the library checks free heap every 30 s and logs an `ERROR`
62
+ below 12 KB with more than one child attached; see the ESP8266 entry.
63
+
64
+ **Arduino cores.** The ESP32 Arduino core 2.0.x and 3.x are both supported;
65
+ the ESP32-C5 and ESP32-C6 need 3.x. Core 3.x dispatches Wi-Fi events under a
66
+ lock that forbids reconfiguring the radio from inside a callback; 2.0
67
+ processes scan results from the loop for that reason (see the ESP32 entry).
68
+
69
+ ### Added (hardware-validated series)
70
+
71
+ - **`LogClass::setSink()`** (#427) — a sketch whose serial port carries a
72
+ line protocol can take the library's log lines through a callback and
73
+ frame them itself, instead of having them printed from the Wi-Fi event
74
+ task into the middle of its own output.
75
+ - **`overCapacity()` and `apChildren()`** (#432) — whether an ESP8266 is
76
+ below 12 KB free with more than one child attached, and how many children
77
+ are attached, for sketches that want to warn or shed load.
78
+ - **`setContainsRoot()`** is now what a mesh that contains a bridge should
79
+ set on every node: it lets a node that is connected to a partition the
80
+ bridge has left notice it has no root and go looking for the bridge's
81
+ channel (#427). `initAsBridge()` and the failover path set it.
82
+
83
+ ### Fixed — gateway and failover (hardware-validated series)
84
+
85
+ - **Gateway takeover left the mesh partitioned on the old radio channel
86
+ (#424)** — a failover candidate correctly moved its AP+STA radio to the
87
+ Internet router's channel after election, but peers learned only that a
88
+ takeover occurred, not which channel to follow. They remained disconnected
89
+ until the slow all-channel recovery scan, exceeding the failover deadline
90
+ and leaving `getPrimaryGateway()` at zero. Election and takeover packages
91
+ now carry the candidate's router channel. Peers validate the announcement,
92
+ discard stale scan state, move both interfaces after the takeover has
93
+ propagated, and resume discovery immediately. A missed or older takeover
94
+ message remains compatible with the scan-based recovery path.
95
+
96
+ - **A bridge that stops cleanly says so (#435)** — the backup that booted
97
+ beside a live primary took its status and held it healthy until the
98
+ status aged out (`bridgeTimeoutMs`, 60 s), then waited for the 30 s
99
+ monitor tick, and missed the rig's 120 s promotion window whenever the
100
+ primary left shortly after it came up. `mesh.stop()` on a bridge now
101
+ broadcasts a status marked `leaving` before closing its connections; every
102
+ node forgets that bridge at once (`forgetBridge()`), a candidate checks
103
+ for a bridge as soon as the startup period allows instead of at the next
104
+ tick, and the "too soon after last role change" hold schedules a retry
105
+ for when it ends rather than returning silently. A bridge that loses power
106
+ still announces nothing; that path is as slow as the status timeout.
107
+
108
+ - **A backup joins the mesh on the router's channel (#435)** — a candidate
109
+ with router credentials used to pick the strongest mesh AP across a split
110
+ mesh; it now joins on the router's channel, where a bridge would be. The
111
+ election's router scan waits on the driver's scanning bit instead of
112
+ `WiFi.scanComplete()`, whose 20-dwell timeout declared the station's own
113
+ all-channel scan failed at 2.4 s and let the two scans collide.
114
+
115
+ - **Home is the channel the bridge's status names (#435)** — a rootless
116
+ node keeps the channel it was last rooted on as home: it does not leave
117
+ home for a partition elsewhere, and away from home it goes back as soon as
118
+ the mesh is visible there, whatever the sizes, since a freshly promoted
119
+ bridge is one AP on the router's channel. Home is learned from the
120
+ bridge's status message, not from a cached tree that could still carry a
121
+ bridge that has gone, and it is forgotten after four re-detections that
122
+ found the mesh elsewhere and no root here. A topology change on the bridge
123
+ brings its status broadcast forward (at most once per 5 s), so a node
124
+ joining anywhere in the tree hears the bridge within seconds.
125
+
126
+ - **A leaf at home, or a failover candidate, does not leave a rootless
127
+ partition (#435)** — the rule that sends a leaf looking for the root had
128
+ fired on the backup itself and dropped its own link in the middle of the
129
+ election.
130
+
131
+ - **A failover candidate is the only node that runs the bridge check
132
+ (#435)** — regular nodes without router credentials no longer schedule
133
+ elections they cannot join.
134
+
135
+ - **Shared-gateway local Internet health never became operational (#422)**
136
+ — the TCP probe used by `InternetHealthChecker` was unimplemented on
137
+ ESP32/ESP8266 and the health task was never started, so
138
+ `hasLocalInternet()` stayed false for the life of a node and every request
139
+ it could have served itself went to a mesh gateway. Shared-gateway
140
+ initialisation now configures and starts the health monitor, a node with
141
+ its own healthy uplink executes Internet requests locally, and local
142
+ gateway acknowledgements complete through the pending-request path.
143
+
144
+ - **ESP8266 health check timeout was set in seconds, not milliseconds
145
+ (#426)** — the 5000 ms check became a 5 ms `WiFiClient` timeout, DNS never
146
+ resolved inside it, and an ESP8266 shared gateway never reported local
147
+ Internet.
148
+
149
+ - **A promoted backup's HTTP result could not reach the requester (#423)**
150
+ — an armed requester-route watchdog is preserved across bounded gateway
151
+ HTTP work and `GATEWAY_ACK` returns through the request's ingress while
152
+ the newly promoted route converges. Automatic channel discovery retries
153
+ when a node first sees no peer, reconfiguring the AP once the channel is
154
+ known.
155
+
156
+ - **A bridge whose TCP listener is not listening re-creates it, and a node
157
+ re-initialised in place keeps the one it has (#430, #435)** — `stop()`
158
+ deleted the listener and the re-bind hit `ERR_USE` for the 2×MSL
159
+ `TIME_WAIT` (120 s), so a node that re-initialised (a promotion, a return
160
+ to regular mode) accepted nobody for two minutes. A client accepted while
161
+ the mesh semaphore is held is closed rather than left half-open, and
162
+ `tcpServerInit()` logs the listener's state.
163
+
164
+ - **Gateway Internet requests partitioned the mesh around the gateway
165
+ (#318, #332, #416, #417)** — the gateway relays messages from inside the
166
+ cooperative TaskScheduler using blocking `HTTPClient` calls, so nothing
167
+ else ran for the duration while wall-clock time kept passing. With a 30 s
168
+ HTTP timeout against a 10 s `NODE_TIMEOUT`, every peer watchdog that fell
169
+ due mid-request fired the instant the scheduler resumed, closing
170
+ connections to nodes that had never gone missing (*"Internet available via
171
+ gateway: YES / Mesh connections active: NO"*). The blocking budget is now a
172
+ wall-clock model derived from `NODE_TIMEOUT` — both socket waits of each
173
+ HTTP call, the captive-portal probe and the DNS probe — enforced by a
174
+ `static_assert`, and after a blocking request the gateway postpones every
175
+ running peer watchdog by exactly the measured stall (`Task::adjust()`),
176
+ so a genuinely dead peer is still reaped on schedule. One residual is
177
+ documented rather than closed: ESP32's in-request hostname resolution
178
+ happens inside the core before the socket timeout applies; `SECURITY.md`
179
+ states it plainly.
180
+
181
+ - **Captive-portal probe ran on every gateway message** —
182
+ `detectCaptivePortal()` made an uncached HTTP round trip before *each*
183
+ mesh→Internet send. It is now cached for `GATEWAY_CONNECTIVITY_CACHE_MS`
184
+ (60 s) and bounded by `GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS`.
185
+
186
+ ### Fixed — channel following and the station scan (hardware-validated series)
187
+
188
+ - **Stranded followers find the bridge's channel (#427, #428)** — when a
189
+ bridge moved the mesh to its router's channel, the nodes behind its direct
190
+ children stayed connected to each other on the old channel and were gated
191
+ out of re-detection for good. Re-detection now also runs for a connected
192
+ node that should have a root and has none; the scan collects every channel
193
+ the mesh is on and prefers one other than the node's own; the move closes
194
+ the station link so an orphan leaves its old partition; an unexpected
195
+ station loss scans at once instead of sleeping out a delay of up to two
196
+ minutes; a scan that could not start is retried in half an interval
197
+ instead of five minutes; an association that never gets an address is
198
+ dropped after half an interval; a connected node that keeps finding the
199
+ mesh only on its own channel backs off instead of scanning every 15 s;
200
+ and a stale scan-done event no longer consumes the scan still in flight
201
+ (#428 restored channel auto-detection after the first fix broke it).
202
+
203
+ - **A connected node changes channel only for a strictly bigger partition;
204
+ a disconnected one follows the mesh wherever it is (#434, #435)** — a
205
+ node in a partition had followed any other channel it saw the mesh on,
206
+ including a lone node still in gateway mode during a teardown. Every
207
+ follow now waits for a second sighting one scan later (a teardown
208
+ straggler is seen once, a bridge twice); a rootless node follows a
209
+ partition that persists whatever its size; a leaf leaves a rootless
210
+ partition only if the mesh ever had a root; a re-init in place resets the
211
+ scan state.
212
+
213
+ - **Re-detection with stations under the AP is done a channel at a time
214
+ (#435)** — an all-channel scan takes the AP off its channel for two to
215
+ three seconds, and the ESP8266 station under it did not survive that. A
216
+ node with stations hunts one channel per scan (300 ms dwell, 1.5 s at home
217
+ between slices) and decides on the own-channel scan after the last slice;
218
+ a node with nothing under its AP keeps the fast all-channel scan.
219
+
220
+ - **Link loss and re-detection are judged by what was lost (#435)** — an
221
+ uplink lost in a rooted mesh re-detects the channel at once (the AP most
222
+ likely left for the bridge's channel); an uplink lost *at home* rescans
223
+ this channel, where the bridge's AP is, instead of hunting all thirteen
224
+ (the hunt cost a sender its request); the station drop a node's own
225
+ channel move causes, and an association attempt that never got an
226
+ address, are not losses (judged as losses they re-detected the channel
227
+ just left, found the remnant bigger, and moved back). The half-open guard
228
+ judges only an attempt still in progress; it had dropped a fresh
229
+ association by the clock of an attempt made 109 s earlier.
230
+
231
+ - **The core's station auto-reconnect is off on every core (#435)** — on
232
+ Arduino core 2.x as on 3.x. The core's own reconnect raced the library's
233
+ scan and re-attached nodes to APs that were leaving; the bridge's router
234
+ link is the exception and keeps it.
235
+
236
+ - **ESP32 Arduino core 3.x: the scan result is read from the loop, not on
237
+ the network-event task (#435)** — core 3.x dispatches Wi-Fi callbacks
238
+ under the lock it also takes in `removeEvent()`, and the library ran
239
+ `scanComplete()` inside the scan-done callback, where a channel follow
240
+ restarts the AP and waits for events only the blocked task can deliver.
241
+ Every ESP32-C5 and ESP32-C6 that followed the bridge's channel had gone
242
+ silent for the rest of its run — still answering the sketch, never
243
+ scanning again — and `stop()` then blocked on the same lock. The callback
244
+ now only yields the station task to `scanComplete()`.
245
+
246
+ ### Fixed — routing (hardware-validated series)
247
+
248
+ - **A connection that has already dropped no longer refuses a live one, and
249
+ is no longer a route (#429)** — a closed connection stays in the layout
250
+ until the next cleanup, and `handleNodeSync()` had turned away a working
251
+ direct connection on its authority, moments before erasing it. Only live
252
+ connections count as routes now, for the duplicate check and for every
253
+ send; `write()` on a closed connection returns `false` instead of queueing
254
+ into the void (27 of 33 unacknowledged deliveries correlated on the rig
255
+ had never arrived). A partitioned leaf that sees nodes it has no route to
256
+ on two consecutive scans reconnects toward them; interior nodes do not
257
+ jump, since dropping an interior link fragments the subtree it carries.
258
+
259
+ - **A node that comes back is not refused for where it used to be (#433)**
260
+ — after any restart, the returning node was associated by each AP in turn
261
+ and dropped a second later, for 30–100 s, because a neighbour's tree
262
+ still listed its old place. The loop check is now the tree the arriving
263
+ node presents; a stale direct link is closed at once and a stale place in
264
+ a neighbour's tree is forgotten (`layout::forget()`).
265
+
266
+ - **A node is in one place (#435)** — when a neighbour's sync presents the
267
+ nodes below it, every other neighbour's cached tree forgets them
268
+ (`layout::forgetAll()`); every board on the rig had carried one node twice
269
+ and routed by the older copy. A restated sync is not news: a neighbour's
270
+ sync is adopted only when the tree it presents differs from its last one
271
+ (a fingerprint per neighbour, covering the time-authority flag), which
272
+ ended a sync storm of one exchange every 30–80 ms between two claimants;
273
+ and a neighbour that stops presenting a node lets the others' restatements
274
+ back in, so a pruned node can return. A stale mention of this node in a
275
+ presented tree is a loop only if there is another live route to the
276
+ presenter.
277
+
278
+ ### Fixed — ESP8266 (hardware-validated series)
279
+
280
+ - **The ESP8266 is specified for small meshes, or as a leaf in larger ones
281
+ (#432)** — measured as an interior node of a seven-node mesh it runs at
282
+ 10–13 KB free, and below about 8 KB a single 8 KB package or one OTA part
283
+ fails to allocate; every ESP32 family holds within a few percent of its
284
+ starting heap in the same mesh. Configure a leaf with
285
+ `init(..., maxconn = 0)`. The library checks every 30 s on ESP8266 and logs
286
+ an `ERROR` below 12 KB free with more than one child attached.
287
+
288
+ ### Security
289
+
290
+ - **Corrected a false claim about gateway TLS.** The source comment on
291
+ `initGatewayInternetHandler()` asserted that "ESP32 uses default SSL settings
292
+ with certificate validation". Nothing in `src/` ever backed that: there is no
293
+ `setCACert`, no certificate bundle and no fingerprint API anywhere in the
294
+ library, and the ESP32 path calls bare `http.begin(url)`. Gateway HTTPS is
295
+ transport encryption **without** peer authentication on both targets.
296
+ No behaviour changed — only the claim.
297
+
298
+ - **Added a threat model to `SECURITY.md`** covering mesh membership
299
+ (one shared password, no per-node identity or revocation), OTA (MD5 is an
300
+ integrity check against an unauthenticated announcer, not a signature),
301
+ gateway TLS, and the absence of rate limiting — along with which of these are
302
+ known-and-documented rather than reportable vulnerabilities.
303
+
304
+ ### Packaging and examples
305
+
306
+ - **`mqttBridge` example fails to compile in Arduino IDE (#398)** — the
307
+ `PubSubClient` library was missing from `library.properties`'s `depends`
308
+ field, so installing this library through the Arduino IDE Library Manager
309
+ never pulled in `PubSubClient`. It is now listed, the example says so, and
310
+ `examples/mqttBridge/platformio.ini` pins the same `knolleary/PubSubClient`
311
+ package used by `examples/bridge`.
312
+ - **`examples/alteriom/mppt_example` never compiled** — its sketch was named
313
+ differently from its directory, so the CI example loop skipped it, and it
314
+ included two headers from the parent example directory by bare name, which
315
+ the Arduino build cannot resolve. The sketch is now `mppt_example.ino`,
316
+ carries the two headers it needs, and is compiled for esp32 and esp8266 on
317
+ every PR like the other twenty.
318
+ - `keywords.txt` now lists the bridge, gateway, failover, queue and capacity
319
+ API so the Arduino IDE highlights it.
320
+
321
+ ### Added (post-review series)
322
+
323
+ - **Unified send path: `SendOptions` (#384)** — `sendSingle()` and
324
+ `sendBroadcast()` gained overloads taking a
325
+ `painlessmesh::SendOptions{priority, ackCallback, ackTimeoutMs}` struct,
326
+ so a message can be both prioritized and delivery-confirmed in one call —
327
+ something the separate priority and ack overload families could not
328
+ express. All pre-existing overloads still compile and now delegate to the
329
+ unified path.
330
+ - **Priority is carried across hops (#384)** — the priority level is now
331
+ serialized on the wire (as `"prio"`, only when it deviates from NORMAL, so
332
+ default sends carry zero overhead) and every forwarding node re-enqueues
333
+ the package at the sender's priority. Previously priority only affected
334
+ the first hop's transmit queue and was silently dropped on forwarding.
335
+ Pre-2.0 nodes ignore the field and forward at normal priority. Named
336
+ constants `PRIORITY_CRITICAL/HIGH/NORMAL/LOW` are exposed in
337
+ `painlessmesh::protocol`.
338
+
339
+ ### Fixed (post-review series)
340
+
341
+ - **Gateway blocking budget is now a wall-clock model, DNS included (#416)**
342
+ — `gatewayBlockingBudgetMs()` counts **both** socket waits of each HTTP
343
+ call (request/header read plus body read — `HTTPClient::setTimeout()`
344
+ bounds one wait, not a whole call) for the destination request *and* the
345
+ captive-portal probe, plus a new `GATEWAY_DNS_TIMEOUT_MS` term for the
346
+ DNS reachability probe. On ESP8266 the probe now uses the `hostByName()`
347
+ timeout overload; on ESP32, whose core has no resolver timeout, the
348
+ standalone probe is skipped and the (timed) captive-portal probe
349
+ establishes reachability instead. **Defaults changed to keep the honest
350
+ budget inside `NODE_TIMEOUT`:** at the stock 10 s watchdog,
351
+ `GATEWAY_HTTP_TIMEOUT_MS` is now 2000 ms (was 5000) and
352
+ `GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS` 1000 ms (was 2000); raise them
353
+ together with `NODE_TIMEOUT` if your endpoint needs longer — the
354
+ `static_assert` enforces the pairing. One residual is documented rather
355
+ than closed: ESP32's in-request hostname resolution happens inside the
356
+ core before the socket timeout applies and cannot be bounded there;
357
+ SECURITY.md states it plainly.
358
+ - **Gateway watchdog compensation now equals the measured stall (#417)** —
359
+ `gateway::refreshPeerWatchdogs()` takes the measured blocking duration and
360
+ postpones each running peer watchdog by exactly that long via
361
+ `Task::adjust()`, instead of restarting every watchdog from zero on every
362
+ exit path. The full reset was strictly more generous than the time the
363
+ scheduler actually lost, and the excess starved the reaper: a genuinely
364
+ dead peer stayed connected indefinitely as long as any *live* peer
365
+ generated gateway traffic more often than `NODE_TIMEOUT`. Paths that never
366
+ blocked now compensate nothing. Regression-tested in
367
+ `catch_gateway_watchdog.cpp`, including the dead-peer-under-continuous-
368
+ traffic case.
369
+ - **Outbound send buffer is bounded (#388)** — each connection's
370
+ `SentBuffer` now holds at most `PAINLESSMESH_MAX_SENT_BUFFER_MESSAGES`
371
+ (default 64, build-time overridable) messages. Previously a peer that
372
+ stopped draining (stalled TCP connection) grew the outbound list until
373
+ allocation failed on the ESP8266 heap. At the cap, an incoming message
374
+ evicts the newest message of a strictly lower priority class (mirroring
375
+ `MessageQueue::makeSpace()`); if nothing lower-priority is queued, the
376
+ push is rejected and the send reports failure. Drops are counted in
377
+ `getStats().dropped`; a partially-transmitted message is never evicted.
378
+
379
+ ### Removed (post-review series)
380
+
381
+ - **`MessageTracker` dead code (#386)** — `message_tracker.hpp` defined a
382
+ full dedup/ack-tracking class that was `#include`d but never instantiated
383
+ or called anywhere in the tree, costing compile time and flash in every
384
+ build. Removed together with its unit test. Broadcast flood dedup, the
385
+ integration it was meant for, remains future work with its own design
386
+ pass.
387
+
388
+ - **Routing no longer copies the connection list per packet (#387)** — every
389
+ `router::` send/broadcast/forward took the mesh layout **by value**,
390
+ copying a `std::list` of `shared_ptr`s (one heap allocation per
391
+ connection) on every packet sent, broadcast, or forwarded. All routing
392
+ functions now take the layout by const reference — measurable allocation
393
+ and fragmentation relief on ESP8266.
394
+
395
+ Feature release adding per-message delivery confirmation (issue #379). The
396
+ release is a major version bump because it introduces a new wire-protocol
397
+ message type: pre-2.0 nodes forward acknowledgment packets but never send
398
+ them, so delivery confirmation only works reliably once every participating
399
+ node runs 2.0.0. All existing sketches compile and behave unchanged.
400
+
401
+ ### Mixed-fleet rollout order
402
+
403
+ During a rolling upgrade, a v1.x node never replies with a `MESSAGE_ACK`, so
404
+ a v2.0 sender's delivery callback fires `delivered = false` against every
405
+ un-upgraded peer — indistinguishable from real packet loss. This is not a
406
+ bug in the ACK feature; it is the expected behavior of a mixed fleet.
407
+ **Upgrade leaf/receiver nodes before the senders that will use the ack
408
+ callbacks**, and only rely on `delivered = false` as a loss signal once the
409
+ whole mesh runs 2.0.0. The same applies to cross-hop priority: pre-2.0
410
+ forwarders ignore the `prio` field and forward at normal priority, so
411
+ priority guarantees only hold end-to-end on an upgraded path.
412
+
413
+ ### Added
414
+
415
+ - **Per-message delivery confirmation and acknowledgment API (#379)** —
416
+ `sendSingle()` and `sendBroadcast()` gained overloads that accept a
417
+ `painlessmesh::ack::deliveryCallback_t` callback and an acknowledgment
418
+ timeout (default 5000 ms). When a callback is provided the outgoing
419
+ message is tagged with a unique `msgId`, the receiving node automatically
420
+ replies with a `MessageAckPackage` (new protocol type 630, routed as a
421
+ SINGLE package so it traverses multiple hops), and the callback fires
422
+ with `delivered = true` plus the measured round-trip latency — or
423
+ `delivered = false` when the timeout elapses. Broadcast tracking
424
+ snapshots the mesh layout at send time and fires the callback once per
425
+ expected node.
426
+ - `checkAcks()` — non-blocking poll that processes acknowledgment timeouts
427
+ and returns the number of messages still pending (timeouts are also
428
+ processed automatically inside `mesh.update()`).
429
+ - `pendingAcks()` — number of messages still awaiting acknowledgment.
430
+ - New header `painlessmesh/ack.hpp` with the platform-independent
431
+ `AckTracker` (unit-tested, uint32 wraparound safe) and
432
+ `MessageAckPackage`.
433
+ - Arduino examples `reliableSensorLogging` (buffered retries until the
434
+ gateway confirms) and `commandControl` (per-node broadcast confirmation).
435
+ - Unit tests (`catch_message_ack.cpp`) covering serialization, ack
436
+ matching, timeout, duplicate/unknown acks, broadcast fan-in and clock
437
+ wraparound, plus an end-to-end multi-node scenario in the TCP
438
+ integration suite.
439
+
440
+ ### Changed
441
+
442
+ - `protocol::Single` / `protocol::Broadcast` carry an optional `msgId`
443
+ field. It is only serialized when delivery confirmation was requested,
444
+ so plain sends have zero added wire overhead.
445
+ - `protocol::Variant` gained lightweight `from()` and `msgId()` field
446
+ peeks; the receive-path ACK handlers use them instead of materializing
447
+ a full package (no per-message payload copy on the hot path).
448
+
449
+ ### Hardening (post-review, pre-release)
450
+
451
+ A full adversarial review of the ACK feature before release led to:
452
+
453
+ - `AckTracker::expire()` now collects and erases expired entries before
454
+ firing any callback — a delivery callback that reentered the tracker
455
+ (retry `track()`, `checkAcks()`, or `clear()` via `mesh.stop()`) could
456
+ previously invalidate the live iterator (use-after-free).
457
+ - `mesh.stop()` reached from inside a scheduler callback no longer
458
+ deletes the internally-owned `Scheduler` out from under its own
459
+ `execute()` (same bug class as #373); the ack poll task is also
460
+ disabled before its handle is cleared so a reentrant stop cannot
461
+ orphan it.
462
+ - Message ids are seeded from `validation::SecureRandom` at `init()` —
463
+ previously the counter restarted at 1 every boot, so a delayed
464
+ pre-reboot ACK could confirm a fresh message (false
465
+ `delivered = true`).
466
+ - Pending acknowledgments are capped at `PAINLESSMESH_MAX_PENDING_ACKS`
467
+ (default 32, build-time overridable); sends beyond the cap are
468
+ rejected instead of growing the tracker unbounded on the ESP8266 heap.
469
+ - Broadcast ACK replies are staggered by nodeId within a 50 ms window so
470
+ an N-node broadcast does not converge N simultaneous ACK unicasts on
471
+ the sender.
472
+ - The ack timeout poll interval is build-time configurable
473
+ (`PAINLESSMESH_ACK_CHECK_INTERVAL_MS`, default 100 ms) and its
474
+ battery/light-sleep implications are documented.
475
+
476
+ ### CI / packaging fixes
477
+
478
+ - Fixed the Arduino example-compile loop in CI (`ci.yml`): a quoted glob
479
+ meant **no example sketch was ever compiled** — the job reported green
480
+ while compiling nothing. All examples now build for esp32 and esp8266
481
+ on every PR.
482
+ - Added the two new examples to `library.json`'s `examples` array so
483
+ they appear in the PlatformIO registry listing.
484
+ - `doxygen/Doxyfile` `PROJECT_NUMBER` bumped from the stale v1.6.1 to
485
+ v2.0.0; stale 1.6.1 install snippets in the wiki docs updated.
486
+ - Wiki sync now publishes `docsify-site/` documentation (it previously
487
+ copied from a `docs/` directory that does not exist) and triggers on
488
+ docsify changes.
489
+ - Removed dead links from the docsify sidebar.
490
+
491
+
10
492
  ## [1.10.0] - 2026-08-12
11
493
 
12
494
  Feature release making the TCP connect-retry envelope tunable per mesh instance
@@ -82,6 +564,7 @@ macros keep their historical values for source compatibility.
82
564
  convention used across every other example (otaSender, namedMesh,
83
565
  alteriom_*).
84
566
 
567
+
85
568
  ## [1.9.21] - 2026-08-04
86
569
 
87
570
  Crash-fix release resolving a family of use-after-free bugs in the task and
package/CONTRIBUTING.md CHANGED
@@ -1,79 +1,82 @@
1
1
  # Contributing
2
2
 
3
- We try to follow the [git flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) development model. Which means that we have a `develop` branch and `main` branch. All development is done under feature branches, which are (when finished) merged into the development branch. When a new version is released we merge the `develop` branch into the `main` branch.
3
+ ## Branches
4
4
 
5
- ## Git flow
5
+ - `main` holds released code. A push to `main` that carries a version bump,
6
+ or whose head commit message starts with `release:`, is what tags and
7
+ publishes a release (see [RELEASE_GUIDE.md](RELEASE_GUIDE.md)).
8
+ - `Feat/next-release` is the integration branch for the next version. Open
9
+ pull requests against it.
10
+ - Work happens on short-lived feature branches (`fix/…`, `feat/…`, `docs/…`)
11
+ cut from `Feat/next-release`.
6
12
 
7
- If you would like to use [git flow tools](http://danielkummer.github.io/git-flow-cheatsheet/) you are more than welcome to. We use it and it's pretty nifty. If you see a `feature\` prefix on a comment then that is git flow automating branch creation. It does need more typing than just plain git so I suggest creating shell aliases for the commands.
13
+ Maintainers merge quickly, often as a squash. Push every commit you describe
14
+ before you describe it, and cut follow-up work from the merged base rather
15
+ than from a stale branch.
8
16
 
9
- ## Submit a pull request:
17
+ ## Submit a pull request
10
18
 
11
- * If your push triggered a 'you just pushed...' message from GitHub then click on the button provided by that pop up to create a pull request.
12
- * If not, then create a pull request and point it to your branch.
13
- * Make sure that you're attempting to merge into `develop` and not `main`.
14
- * Get your code reviewed by another contributor. If there are no contributors who possess the same set of skills then get them to review it anyway but explain what the code does beforehand and why. Use it as an opportunity for discussion around the feature set, to transfer knowledge, and to possibly [rubber duck](https://en.wikipedia.org/wiki/Rubber_duck_debugging) your code.
15
- * Once the code is reviewed then have your reviewer merge your code.
19
+ - Point the pull request at `Feat/next-release`, not `main`.
20
+ - Say what was wrong, how you know (a log, a test, a measurement), and what
21
+ the change does about it. For anything that touches the radio, routing,
22
+ the gateway or OTA, the evidence is a serial log or a run on the
23
+ hardware-in-the-loop rig; a unit test alone is not enough there, because
24
+ the unit tests mock the radio.
25
+ - Get your code reviewed by another contributor, and let the reviewer merge.
16
26
 
17
- NOTE: Tests *must* pass in order for the code to be merged.
27
+ Tests must pass for the code to be merged. Add a changelog entry under
28
+ `## [Unreleased]` in [CHANGELOG.md](CHANGELOG.md) for any user-visible change.
18
29
 
19
- NOTE: Always do a `git pull` on `develop` before you start working to capture the latest changes.
30
+ ## Testing requirements
20
31
 
21
- ## Testing Requirements
32
+ ### Running the desktop tests
22
33
 
23
- ### Running Tests
24
-
25
- Before submitting a pull request, ensure all tests pass:
34
+ Before submitting a pull request, build and run the Catch2 and Boost suites:
26
35
 
27
36
  ```bash
28
- # Build and run unit/integration tests
37
+ git submodule update --init
29
38
  cmake -G Ninja .
30
39
  ninja
31
40
  run-parts --regex catch_ bin/
32
-
33
- # Run simulator tests (for examples)
34
- cd test/simulator
35
- mkdir build && cd build
36
- cmake -G Ninja .. && ninja
37
- bin/painlessmesh-simulator --config ../../../examples/basic/test/simulator/scenarios/basic_mesh_test.yaml
38
41
  ```
39
42
 
40
- ### Adding Tests for New Features
41
-
42
- When adding new features or examples:
43
-
44
- 1. **Unit Tests**: Add tests in `test/catch/` for new components
45
- 2. **Integration Tests**: Add to `test/boost/tcp_integration.cpp` for core functionality
46
- 3. **Simulator Tests**: Create test scenarios in `examples/your_example/test/simulator/` for new examples
47
- 4. **Documentation**: Update relevant test documentation
48
-
49
- ### Example Validation with Simulator
43
+ The same suites run in CI under gcc, clang and AddressSanitizer, and CI also
44
+ compiles every example for esp32 and esp8266 with `arduino-cli`, builds the
45
+ PlatformIO projects under `test/ci/`, and checks formatting and library
46
+ metadata.
50
47
 
51
- All example sketches should have simulator tests that validate behavior with multiple virtual nodes:
48
+ ### Simulator and hardware tests
52
49
 
53
- 1. Create firmware adapter in `examples/your_example/test/simulator/firmware/`
54
- 2. Create YAML test scenarios in `examples/your_example/test/simulator/scenarios/`
55
- 3. Document test setup in `examples/your_example/test/simulator/README.md`
50
+ Multi-node behaviour is tested with the external
51
+ [painlessMesh-simulator](https://github.com/Alteriom/painlessMesh-simulator);
52
+ scenarios for an example live under `examples/<example>/test/simulator/`
53
+ (see `examples/basic/test/simulator/`). Radio, routing, gateway, failover
54
+ and OTA behaviour is validated on the Alteriom hardware-in-the-loop farm; a
55
+ maintainer runs it on a pull request by adding the `run-hil` label, and the
56
+ release gate is three consecutive clean runs of the whole suite.
56
57
 
57
- See [Simulator Testing Guide](docs/SIMULATOR_TESTING.md) for complete instructions.
58
+ ### Adding tests for new features
58
59
 
59
- This ensures examples:
60
- - Work as documented with multiple nodes
61
- - Handle edge cases properly
62
- - Don't regress with library changes
63
- - Serve as validated references for users
60
+ 1. **Unit tests**: add to `test/catch/` for new components.
61
+ 2. **Integration tests**: add to `test/boost/tcp_integration.cpp` for core
62
+ behaviour that spans connections.
63
+ 3. **Simulator scenarios**: add YAML scenarios under
64
+ `examples/<example>/test/simulator/` for new examples.
65
+ 4. **Documentation**: keep `USER_GUIDE.md`, `README.md` and the example's
66
+ own README in step with the change.
64
67
 
65
68
  ## Versioning
66
69
 
67
- This project will try its best to adhere to [semver](http://semver.org/) i.e, a codified guide to versioning software. When a new feature is developed or a bug is fixed the version will need to be bumped to signify the change.
68
-
69
- The semver string is built like this:
70
-
71
- Major.Minor.Patch
72
-
73
- A major version bump means that a massive change took place and that application will probably have to be redeployed because a *backwards incompatible* version was released. Example: A library => model relationship change which requires previous configuration options to become invalid.
74
-
75
- A minor version is a *backwards compatible* addition or change to the core software. Most development activity will be this type of version bump. Example: A new feature or model.
70
+ This project follows [semver](https://semver.org/). Version 2.0 patch
71
+ releases must remain wire-compatible with the 2.0 protocol; anything that
72
+ changes what a node puts on the wire, or what an existing sketch has to do to
73
+ keep working, is a major version.
76
74
 
77
- A patch version is a *backwards compatible* bug fix or application configuration change.
75
+ - **Major**: a backwards-incompatible change (a new or changed protocol
76
+ message, a removed API).
77
+ - **Minor**: a backwards-compatible addition.
78
+ - **Patch**: a backwards-compatible fix.
78
79
 
79
- Documentation doesn't require a version bump.
80
+ Documentation does not require a version bump. The three version files
81
+ (`library.properties`, `library.json`, `package.json`) are changed together
82
+ with `./scripts/bump-version.sh`.