@alteriom/painlessmesh 1.10.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BRIDGE_TO_INTERNET.md +167 -29
- package/CHANGELOG.md +483 -0
- package/CONTRIBUTING.md +56 -53
- package/README.md +100 -95
- package/RELEASE_GUIDE.md +81 -780
- package/examples/alteriom/README.md +8 -10
- package/examples/alteriom/alteriom.ino +2 -2
- package/examples/alteriom/alteriom_sensor_package.hpp +17 -11
- package/examples/alteriom/mppt_example/alteriom_custom_package_template.hpp +320 -0
- package/examples/alteriom/mppt_example/alteriom_sensor_package.hpp +1389 -0
- package/examples/alteriom/mppt_example/{alteriom_mppt_example.ino → mppt_example.ino} +4 -0
- package/examples/basic/test/simulator/README.md +3 -3
- package/examples/bridge_failover/README.md +51 -14
- package/examples/commandControl/commandControl.ino +86 -0
- package/examples/commandControl/platformio.ini +26 -0
- package/examples/mqttBridge/mqttBridge.ino +4 -0
- package/examples/mqttBridge/platformio.ini +1 -1
- package/examples/otaSender/otaSender.ino +5 -1
- package/examples/priority/README.md +1 -1
- package/examples/priority/{priority_basic_example.ino → priority_basic_example/priority_basic_example.ino} +4 -4
- package/examples/priority/{priority_with_queue.ino → priority_with_queue/priority_with_queue.ino} +20 -2
- package/examples/reliableSensorLogging/platformio.ini +26 -0
- package/examples/reliableSensorLogging/reliableSensorLogging.ino +151 -0
- package/examples/sendToInternet/README.md +12 -5
- package/examples/sendToInternet/{CMakeLists.txt → pc_node/CMakeLists.txt} +7 -7
- package/examples/sendToInternet/{PC_NODE_README.md → pc_node/PC_NODE_README.md} +15 -15
- package/examples/sendToInternet/{build.sh → pc_node/build.sh} +5 -5
- package/examples/sendToInternet/{pc_mesh_node.cpp → pc_node/pc_mesh_node.cpp} +12 -1
- package/examples/sharedGateway/README.md +1 -2
- package/keywords.txt +50 -1
- package/library.json +8 -6
- package/library.properties +2 -2
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +3 -3
- package/src/arduino/wifi.hpp +556 -126
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +607 -87
- package/src/painlessMeshSTA.h +135 -3
- package/src/painlessmesh/ack.hpp +283 -0
- package/src/painlessmesh/buffer.hpp +70 -8
- package/src/painlessmesh/callback.hpp +38 -5
- package/src/painlessmesh/configuration.hpp +69 -1
- package/src/painlessmesh/connection.hpp +12 -5
- package/src/painlessmesh/gateway.hpp +270 -5
- package/src/painlessmesh/layout.hpp +70 -2
- package/src/painlessmesh/logger.hpp +15 -0
- package/src/painlessmesh/mesh.hpp +552 -48
- package/src/painlessmesh/ntp.hpp +2 -4
- package/src/painlessmesh/plugin.hpp +30 -6
- package/src/painlessmesh/protocol.hpp +55 -2
- package/src/painlessmesh/router.hpp +192 -77
- package/src/painlessmesh/tcp.hpp +10 -0
- package/src/painlessmesh/message_tracker.hpp +0 -311
- /package/examples/sendToInternet/{mock_server_test.ino → mock_server_test/mock_server_test.ino} +0 -0
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
|
-
|
|
3
|
+
## Branches
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
+
## Testing requirements
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
### Running the desktop tests
|
|
22
33
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
+
### Simulator and hardware tests
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
+
### Adding tests for new features
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
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`.
|