@alteriom/painlessmesh 1.9.20 → 1.10.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/CHANGELOG.md +121 -0
- package/README.md +23 -3
- package/RELEASE_GUIDE.md +147 -8
- package/examples/alteriom/alteriom_custom_package_template.hpp +11 -11
- package/examples/alteriom/mppt_example/alteriom_mppt_example.ino +1 -1
- package/examples/bridge_failover/bridge_failover.ino +2 -2
- package/examples/tcpRetryConfig/README.md +110 -0
- package/examples/tcpRetryConfig/platformio.ini +26 -0
- package/examples/tcpRetryConfig/tcpRetryConfig.ino +154 -0
- package/keywords.txt +3 -0
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/AlteriomPainlessMesh.h +3 -3
- package/src/arduino/wifi.hpp +49 -17
- package/src/painlessMesh.h +2 -2
- package/src/painlessTaskOptions.h +9 -0
- package/src/painlessmesh/buffer.hpp +4 -1
- package/src/painlessmesh/configuration.hpp +13 -2
- package/src/painlessmesh/connection.hpp +31 -11
- package/src/painlessmesh/mesh.hpp +73 -22
- package/src/painlessmesh/message_queue.hpp +24 -13
- package/src/painlessmesh/plugin.hpp +27 -5
- package/src/painlessmesh/tcp.hpp +158 -29
package/CHANGELOG.md
CHANGED
|
@@ -7,12 +7,133 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.10.0] - 2026-08-12
|
|
11
|
+
|
|
12
|
+
Feature release making the TCP connect-retry envelope tunable per mesh instance
|
|
13
|
+
(#378), alongside two documentation corrections that retire long-standing claims
|
|
14
|
+
the library never actually implemented (#385) and an example build fix (#360).
|
|
15
|
+
|
|
16
|
+
**Upgrading is behaviour-neutral.** Every new setting defaults to the value that
|
|
17
|
+
was previously hardcoded, so a sketch that does not call `setTcpRetryConfig()`
|
|
18
|
+
behaves exactly as it did on 1.9.21. Nothing was removed: the deprecated queue
|
|
19
|
+
macros keep their historical values for source compatibility.
|
|
20
|
+
|
|
21
|
+
> **Note for npm users:** v1.9.21 was never published to npm — the `NPM_TOKEN`
|
|
22
|
+
> used by CI had expired (#381), which failed the npm publish job while the
|
|
23
|
+
> GitHub Release, GitHub Packages, PlatformIO and Arduino channels all succeeded.
|
|
24
|
+
> npm's previous version is therefore **1.9.20**, and upgrading from npm brings
|
|
25
|
+
> in both 1.9.21 and 1.10.0. See the 1.9.21 entry below for what that release
|
|
26
|
+
> contained — it was a crash-fix release, and npm users have been missing it.
|
|
27
|
+
|
|
10
28
|
### Added
|
|
11
29
|
|
|
30
|
+
- **User-configurable TCP retry parameters (#378)** — the five TCP connect
|
|
31
|
+
retry values that were hardcoded as `static const` in
|
|
32
|
+
`src/painlessmesh/tcp.hpp` are now tunable per mesh instance via
|
|
33
|
+
`mesh.setTcpRetryConfig()` / `mesh.getTcpRetryConfig()`, using the new
|
|
34
|
+
`painlessmesh::tcp::TcpRetryConfig` struct (`maxRetries`, `retryDelayMs`,
|
|
35
|
+
`stabilizationDelayMs`, `exhaustionReconnectDelayMs`,
|
|
36
|
+
`failureBlockDurationMs`). This lets latency-sensitive meshes (see
|
|
37
|
+
discussion #368), high-reliability industrial deployments and
|
|
38
|
+
battery-powered nodes each pick their own retry envelope without forking
|
|
39
|
+
the library.
|
|
40
|
+
|
|
41
|
+
The struct's defaults are spelled as the existing constants, so **behaviour
|
|
42
|
+
is unchanged for any sketch that does not call the new setter**, and the
|
|
43
|
+
constants themselves remain in place. `maxRetries` is clamped to 10 and
|
|
44
|
+
`retryDelayMs` to 50–60000 ms, since an unbounded retry count is a
|
|
45
|
+
heap/recursion hazard and a zero delay produces a hot reconnect loop; the
|
|
46
|
+
remaining fields accept 0 as a meaningful "disable this step" value.
|
|
47
|
+
New `examples/tcpRetryConfig/` demonstrates real-time, high-reliability and
|
|
48
|
+
battery-saver profiles.
|
|
49
|
+
|
|
12
50
|
### Changed
|
|
13
51
|
|
|
52
|
+
- **`MessageQueue` documented honestly as a manual buffer (#385)** —
|
|
53
|
+
removed the "messages are automatically delivered when connection is
|
|
54
|
+
restored" claim from `MessageQueue` and the `mesh.enableMessageQueue`
|
|
55
|
+
/ `queueMessage` / `flushMessageQueue` doc comments. Nothing in the
|
|
56
|
+
library ever transmitted queued messages or observed connectivity
|
|
57
|
+
changes; the app has always owned the send loop. The docs now say so,
|
|
58
|
+
and the `flushMessageQueue` example shows the intended pattern of
|
|
59
|
+
wiring the drain into `onLocalInternetChanged`.
|
|
60
|
+
|
|
61
|
+
### Deprecated
|
|
62
|
+
|
|
63
|
+
- **Compatibility queue macros kept as ignored no-ops (#385)** —
|
|
64
|
+
`MIN_FREE_MEMORY` and `MAX_MESSAGE_QUEUE` remain defined in
|
|
65
|
+
`painlessmesh/configuration.hpp` (and `test/boost/Arduino.h`) for
|
|
66
|
+
source compatibility, but nothing in the library reads them. They were
|
|
67
|
+
placeholders for the auto-flush behavior that never landed.
|
|
68
|
+
`MessageQueue` has always taken its own per-instance `maxSize`
|
|
69
|
+
constructor argument. Their historical default values
|
|
70
|
+
(`MIN_FREE_MEMORY 4000`, `MAX_MESSAGE_QUEUE 50`) are preserved so any
|
|
71
|
+
downstream code that referenced the macros keeps its prior behavior.
|
|
72
|
+
|
|
14
73
|
### Fixed
|
|
15
74
|
|
|
75
|
+
- **`bridge_failover` example failed to compile (#360)** — the two
|
|
76
|
+
`mesh.onBridgeCoordination*` lambdas referenced
|
|
77
|
+
`plugin::BridgeCoordinationPackage` with a bare `plugin::` prefix, but
|
|
78
|
+
`painlessMesh.h` only lifts `painlessmesh::logger` to global scope, so
|
|
79
|
+
the type did not resolve (`'plugin' does not name a type`). Both lambda
|
|
80
|
+
parameters are now fully qualified as
|
|
81
|
+
`painlessmesh::plugin::BridgeCoordinationPackage`, matching the
|
|
82
|
+
convention used across every other example (otaSender, namedMesh,
|
|
83
|
+
alteriom_*).
|
|
84
|
+
|
|
85
|
+
## [1.9.21] - 2026-08-04
|
|
86
|
+
|
|
87
|
+
Crash-fix release resolving a family of use-after-free bugs in the task and
|
|
88
|
+
TCP-connection lifecycle. Root-caused and fixed by @vaz82 (PR #376) with
|
|
89
|
+
reports and field testing from @fidla73 and @miloshev (issue #373); finalized
|
|
90
|
+
with TaskScheduler's native self-destruct mechanism and regression coverage.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- **Use-after-free in `Task::disable()` on connection teardown (#373)** —
|
|
95
|
+
`scheduleAsyncClientDeletion()`'s cleanup task deleted itself inside its
|
|
96
|
+
own `onDisable` callback, but TaskScheduler's `Task::disable()` writes to
|
|
97
|
+
the task object after `onDisable` returns. Crashed nodes (StoreProhibited,
|
|
98
|
+
`EXCVADDR 0x8`) on every peer disconnect. The cleanup task now uses
|
|
99
|
+
TaskScheduler's `_TASK_SELF_DESTRUCT` support (enabled in
|
|
100
|
+
`painlessTaskOptions.h`): the Scheduler deletes the task from within
|
|
101
|
+
`execute()`, safely outside the `disable()` call stack.
|
|
102
|
+
- **`PackageHandler::stop()` destroying the currently-executing task** —
|
|
103
|
+
when `stop()` runs from within a task's own callback (bridge promotion
|
|
104
|
+
path), it destroyed that task's closure mid-execution via
|
|
105
|
+
`setCallback(NULL)`/`shared_ptr` release. `stop()` now accepts the
|
|
106
|
+
scheduler, detects the running task via `getCurrentTask()`, and leaves it
|
|
107
|
+
in `taskList` for safe reuse by `addTask()`.
|
|
108
|
+
- **Stale `_pcb` window in `~BufferedConnection()`** — `client->close()` was
|
|
109
|
+
skipped when `freeable()` returned true, leaving a non-null-but-stale pcb
|
|
110
|
+
that lwIP's timers could recycle during the deferred-deletion window
|
|
111
|
+
(`heap_caps_free`/`memp_free` assertion failures, `tcp_arg()` wild-pointer
|
|
112
|
+
stores). `close()` is now called unconditionally at destruction.
|
|
113
|
+
- **`onError`/`onConnect` double-handling race in `tcp::connect()`** — if
|
|
114
|
+
WiFi dropped as the TCP handshake completed, AsyncTCP could fire both
|
|
115
|
+
callbacks for the same `AsyncClient`, handing it to two owners and
|
|
116
|
+
scheduling its deletion twice. A shared claim guard now ensures exactly
|
|
117
|
+
one callback processes the client.
|
|
118
|
+
- **Bridge promotion state capture** — the deferred stop/reinit lambda in
|
|
119
|
+
`promoteToBridge()` (and the isolated-node variant) now captures mesh
|
|
120
|
+
credentials, scheduler, and callback by value so `stop()` cannot mutate
|
|
121
|
+
them before the reinit reads them.
|
|
122
|
+
- **Off-by-one buffer overflow in `ReceiveBuffer::push()`** — when a
|
|
123
|
+
received chunk was ≥ `TCP_MSS`, the null terminator was written one byte
|
|
124
|
+
past the end of the shared temp buffer, corrupting adjacent memory on
|
|
125
|
+
every large read. Found by the new AddressSanitizer CI job on its first
|
|
126
|
+
run; `read_len` now reserves one byte for the terminator.
|
|
127
|
+
|
|
128
|
+
### Added
|
|
129
|
+
|
|
130
|
+
- Regression test `catch_connection_cleanup.cpp` covering the #373
|
|
131
|
+
schedule → fire → self-destruct cleanup lifecycle and
|
|
132
|
+
`~BufferedConnection` churn.
|
|
133
|
+
- AddressSanitizer job in CI (gcc + `-fsanitize=address`) so use-after-free
|
|
134
|
+
and double-free regressions in the task/connection lifecycle fail the
|
|
135
|
+
build instead of crashing devices in the field.
|
|
136
|
+
|
|
16
137
|
## [1.9.20] - 2026-03-27
|
|
17
138
|
|
|
18
139
|
### Added
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<div align="center">
|
|
6
6
|
|
|
7
|
-
**Version 1.
|
|
7
|
+
**Version 1.10.0** - Tunable TCP connect-retry behaviour via `setTcpRetryConfig()` (#378)
|
|
8
8
|
|
|
9
9
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
|
|
@@ -560,9 +560,29 @@ These are the message types used by applications built on painlessMesh:
|
|
|
560
560
|
- **Event Coordination** - Synchronized displays, distributed processing
|
|
561
561
|
- **Bridge Networks** - Connect mesh to WiFi/Internet/MQTT - [📖 Bridge Guide](BRIDGE_TO_INTERNET.md)
|
|
562
562
|
|
|
563
|
-
## Latest Release: v1.
|
|
563
|
+
## Latest Release: v1.10.0 (August 12, 2026)
|
|
564
564
|
|
|
565
|
-
**
|
|
565
|
+
**Tunable TCP Connect-Retry Behaviour (issue #378)**
|
|
566
|
+
|
|
567
|
+
- The five TCP connect-retry values that were hardcoded in `tcp.hpp` are now tunable per mesh instance via `mesh.setTcpRetryConfig()` / `mesh.getTcpRetryConfig()` (#378, PR #395)
|
|
568
|
+
- Defaults match the previous constants exactly — **no behaviour change unless you call the setter**
|
|
569
|
+
- New `examples/tcpRetryConfig/` with real-time, high-reliability and battery-saver profiles
|
|
570
|
+
- `MessageQueue` documentation corrected: it is a manual buffer, never an auto-flush queue (#385)
|
|
571
|
+
- `MIN_FREE_MEMORY` / `MAX_MESSAGE_QUEUE` deprecated as ignored no-ops, values preserved for source compatibility (#385)
|
|
572
|
+
- Fixed `bridge_failover` example failing to compile on an unqualified `plugin::` type (#360)
|
|
573
|
+
|
|
574
|
+
> **npm users:** v1.9.21 was never published to npm ([#381](https://github.com/Alteriom/painlessMesh/issues/381) — expired token). npm's previous version is 1.9.20, so upgrading from npm picks up both releases. GitHub, PlatformIO and Arduino were unaffected.
|
|
575
|
+
|
|
576
|
+
**Previous Release: v1.9.21 (August 4, 2026)**
|
|
577
|
+
|
|
578
|
+
**Crash Fixes: Task & TCP Connection Lifecycle (issue #373)**
|
|
579
|
+
|
|
580
|
+
- Fixed use-after-free in `Task::disable()` that crashed nodes on every peer disconnect (#373, PR #376 by @vaz82)
|
|
581
|
+
- Fixed `PackageHandler::stop()` destroying the currently-executing task during bridge promotion
|
|
582
|
+
- Fixed stale-pcb heap corruption window in `~BufferedConnection()` and an `onError`/`onConnect` double-handling race
|
|
583
|
+
- New AddressSanitizer CI job and regression test for the task/connection cleanup lifecycle
|
|
584
|
+
|
|
585
|
+
**Previous Release: v1.9.20 (March 27, 2026)** — Full repo cleanup, bug fixes & bridge coordination callbacks:
|
|
566
586
|
|
|
567
587
|
- New `onBridgeCoordination()` and `onBridgeCoordinationChanged()` monitoring callbacks
|
|
568
588
|
- Fixed 13 critical/high/medium bugs (double-free, RSSI overflow, memory leaks, blocking delays)
|
package/RELEASE_GUIDE.md
CHANGED
|
@@ -209,11 +209,22 @@ Each release triggers the **PlatformIO Library Publishing** workflow:
|
|
|
209
209
|
|
|
210
210
|
#### Automatic Workflow Trigger
|
|
211
211
|
|
|
212
|
-
The PlatformIO workflow
|
|
212
|
+
The PlatformIO workflow is started by:
|
|
213
213
|
|
|
214
|
-
-
|
|
214
|
+
- The `platformio-dispatch` job in **Automated Release**, which calls
|
|
215
|
+
`gh workflow run platformio-publish.yml --ref v<version> -f version=<version>`
|
|
216
|
+
right after the release is created
|
|
217
|
+
- A GitHub release published by a human (via the UI or a PAT)
|
|
215
218
|
- Manual workflow dispatch for testing
|
|
216
219
|
|
|
220
|
+
> **Why the explicit dispatch?** `platformio-publish.yml` also listens for
|
|
221
|
+
> `release: published`, but that event never fires for releases created by
|
|
222
|
+
> `release.yml`: GitHub suppresses events raised by the built-in `GITHUB_TOKEN`.
|
|
223
|
+
> `workflow_dispatch` is one of the two documented exceptions to that rule, so
|
|
224
|
+
> the release workflow dispatches the publish explicitly and then verifies a run
|
|
225
|
+
> actually appeared. Before this was added, PlatformIO publication silently did
|
|
226
|
+
> not happen and had to be dispatched by hand (v1.9.21).
|
|
227
|
+
|
|
217
228
|
### PlatformIO Package Contents
|
|
218
229
|
|
|
219
230
|
Published package includes:
|
|
@@ -454,13 +465,127 @@ npm run build
|
|
|
454
465
|
npm run test
|
|
455
466
|
```
|
|
456
467
|
|
|
457
|
-
**NPM Token Invalid**
|
|
468
|
+
**NPM Token Expired / Invalid (`E401 Unauthorized`)**
|
|
469
|
+
|
|
470
|
+
Symptom: the `npm-publish` job fails at *Verify NPM authentication* with
|
|
471
|
+
`401 Unauthorized - GET https://registry.npmjs.org/-/whoami`. npm tokens
|
|
472
|
+
expire; everything else in the release (tag, GitHub Release, zip asset,
|
|
473
|
+
GitHub Packages, PlatformIO) succeeds independently, so **the release can look
|
|
474
|
+
green-ish while npmjs.org is missing the version**. Always confirm with
|
|
475
|
+
`npm view @alteriom/painlessmesh version`.
|
|
476
|
+
|
|
477
|
+
Rotating the token is operator-only — it cannot be automated from CI:
|
|
478
|
+
|
|
458
479
|
```bash
|
|
459
|
-
#
|
|
460
|
-
|
|
461
|
-
#
|
|
480
|
+
# 1. Mint a fresh granular token, scoped to @alteriom/painlessmesh, with
|
|
481
|
+
# "Read and write" AND the "Bypass 2FA" option enabled <-- see EOTP below
|
|
482
|
+
# https://www.npmjs.com/settings/tokens
|
|
483
|
+
# 2. Verify the new token before saving it (recommended).
|
|
484
|
+
# Ask the registry directly — do NOT use a bare `npm whoami`, which answers
|
|
485
|
+
# for whatever credential your local ~/.npmrc already holds and will happily
|
|
486
|
+
# pass while the new token is bad:
|
|
487
|
+
curl -sS -H "Authorization: Bearer <new-token>" \
|
|
488
|
+
https://registry.npmjs.org/-/whoami # -> {"username":"..."} , not 401
|
|
489
|
+
|
|
490
|
+
# 3. Update the NPM_TOKEN *organisation* secret — NOT a repository secret.
|
|
491
|
+
# painlessMesh has no repo-level NPM_TOKEN and must not gain one; see
|
|
492
|
+
# "Where NPM_TOKEN actually lives" below.
|
|
493
|
+
# https://github.com/organizations/Alteriom/settings/secrets/actions
|
|
494
|
+
|
|
495
|
+
# 4a. Re-run the failed release job (keeps the original run's context)
|
|
496
|
+
gh run rerun <run-id> --failed --repo Alteriom/painlessMesh
|
|
497
|
+
|
|
498
|
+
# 4b. …or republish a missed version out-of-band. Pass the TAG as ref:
|
|
499
|
+
# without it the workflow builds the default branch, and if main has moved
|
|
500
|
+
# on since the tag it would upload today's code under the old version
|
|
501
|
+
# number. The workflow now refuses that outright — pass ref so you never
|
|
502
|
+
# have to rely on the guard catching it.
|
|
503
|
+
gh workflow run manual-publish.yml --repo Alteriom/painlessMesh \
|
|
504
|
+
-f ref=v1.9.21 -f publish_npm=true -f publish_github=false
|
|
505
|
+
|
|
506
|
+
# 5. Confirm the version actually landed
|
|
507
|
+
npm view @alteriom/painlessmesh version
|
|
462
508
|
```
|
|
463
509
|
|
|
510
|
+
#### Where NPM_TOKEN actually lives
|
|
511
|
+
|
|
512
|
+
`NPM_TOKEN` is an **organisation** secret on `Alteriom`, shared by every repo
|
|
513
|
+
that publishes to npm. painlessMesh has **no repository-level copy**, and adding
|
|
514
|
+
one is a trap rather than a tightening:
|
|
515
|
+
|
|
516
|
+
> A repository secret silently takes precedence over an organisation secret of
|
|
517
|
+
> the same name. The repo then stops seeing org-wide rotations and keeps using
|
|
518
|
+
> its own copy until that copy expires — which is invisible until a release day
|
|
519
|
+
> fails.
|
|
520
|
+
|
|
521
|
+
Two sibling repos already sit in that state, with repo-level `NPM_TOKEN` copies
|
|
522
|
+
that shadow the org secret (`webhook-client`, `repository-metadata-manager`).
|
|
523
|
+
Rotate the org secret and those two are still broken; delete the repo-level copy
|
|
524
|
+
and they inherit the fresh one. Check before assuming a rotation reached a repo:
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
# Empty output = good (inherits the org secret)
|
|
528
|
+
gh api repos/Alteriom/<repo>/actions/secrets \
|
|
529
|
+
--jq '.secrets[] | select(.name=="NPM_TOKEN") | "SHADOWED, updated \(.updated_at)"'
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
While rotating `NPM_TOKEN`, check `PLATFORMIO_AUTH_TOKEN` too — it expires the
|
|
533
|
+
same way and `platformio-publish.yml` hard-fails on an invalid one.
|
|
534
|
+
|
|
535
|
+
**NPM asks for a one-time password (`EOTP`)**
|
|
536
|
+
|
|
537
|
+
Symptom: authentication *succeeds* — `npm whoami` prints the username — and then
|
|
538
|
+
`npm publish` fails with:
|
|
539
|
+
|
|
540
|
+
```
|
|
541
|
+
npm error code EOTP
|
|
542
|
+
npm error This operation requires a one-time password from your authenticator.
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
The token is valid but is not allowed to bypass 2FA, and CI has no authenticator
|
|
546
|
+
to answer the challenge with. **A rotation that fixes `E401` lands here if the
|
|
547
|
+
replacement token is minted without the bypass option** — which is what happened
|
|
548
|
+
on the second rotation attempt for #381.
|
|
549
|
+
|
|
550
|
+
npm removed the legacy token types (`read-only` / `automation` / `publish`) in
|
|
551
|
+
**November 2025**; only granular access tokens exist now. The old *Automation*
|
|
552
|
+
token bypassed 2FA by virtue of its type, so this was never a decision anyone had
|
|
553
|
+
to make. On a granular token it is an explicit checkbox, and a token minted from
|
|
554
|
+
muscle memory does not have it:
|
|
555
|
+
|
|
556
|
+
> **Bypass 2FA** — required. Takes precedence over account-level and
|
|
557
|
+
> package-level 2FA settings for publishing.
|
|
558
|
+
|
|
559
|
+
Re-mint at <https://www.npmjs.com/settings/tokens> with *Read and write* on
|
|
560
|
+
`@alteriom/painlessmesh` **and Bypass 2FA enabled**, update the secret, re-run.
|
|
561
|
+
`npm whoami` cannot detect this ahead of time — it passes for both token kinds,
|
|
562
|
+
so the failure necessarily surfaces at the publish call.
|
|
563
|
+
|
|
564
|
+
### Trusted publishing (OIDC) — the way out of token rotation
|
|
565
|
+
|
|
566
|
+
Both failures above are symptoms of the same thing: a long-lived credential that
|
|
567
|
+
expires silently and is only exercised on release day. npm's replacement is
|
|
568
|
+
**trusted publishing** — the workflow authenticates to npm over OIDC, and
|
|
569
|
+
`NPM_TOKEN` stops existing.
|
|
570
|
+
|
|
571
|
+
This is on a clock rather than merely being nicer: as of **2026-07-31** bypass-2FA
|
|
572
|
+
tokens can no longer manage tokens, package access, or trusted-publishing config,
|
|
573
|
+
and npm has targeted **January 2027** for removing *direct publish* from them —
|
|
574
|
+
after which they can only stage a publish for a maintainer to approve with 2FA.
|
|
575
|
+
The current setup stops working at that point.
|
|
576
|
+
|
|
577
|
+
Requirements, none of which this repo blocks on today:
|
|
578
|
+
|
|
579
|
+
| Requirement | Status here |
|
|
580
|
+
|---|---|
|
|
581
|
+
| `id-token: write` permission | ✅ already set in `release.yml` and `manual-publish.yml` |
|
|
582
|
+
| npm CLI ≥ 11.5.1, Node ≥ 22.14.0 | ❌ workflows pin `node-version: '18'` — needs a bump |
|
|
583
|
+
| Trusted publisher registered on npmjs.com | ❌ operator, one-time, per workflow file |
|
|
584
|
+
|
|
585
|
+
The npmjs.com side is under *Package settings → Trusted publisher*: org
|
|
586
|
+
`Alteriom`, repository `painlessMesh`, workflow filename `release.yml` (add a
|
|
587
|
+
second entry for `manual-publish.yml` if that path should keep working).
|
|
588
|
+
|
|
464
589
|
**GitHub Packages Authentication**
|
|
465
590
|
```bash
|
|
466
591
|
# Check if GITHUB_TOKEN has packages:write permission
|
|
@@ -508,11 +633,18 @@ If this happens, you can manually publish packages:
|
|
|
508
633
|
4. Click **Run workflow**
|
|
509
634
|
|
|
510
635
|
The manual workflow will:
|
|
511
|
-
- Read the current version from `library.properties`
|
|
636
|
+
- Read the current version from `library.properties` and refuse to run if it
|
|
637
|
+
disagrees with `package.json` (npm publishes the `package.json` version)
|
|
638
|
+
- Validate `NPM_TOKEN` against the registry before attempting to publish, so an
|
|
639
|
+
expired token fails immediately with rotation instructions
|
|
512
640
|
- Publish to NPM (if selected)
|
|
513
641
|
- Publish to GitHub Packages (if selected)
|
|
514
642
|
- Show success/failure status for each
|
|
515
643
|
|
|
644
|
+
It does **not** publish to the PlatformIO registry — use
|
|
645
|
+
`gh workflow run platformio-publish.yml --ref v<version> -f version=<version>`
|
|
646
|
+
for that.
|
|
647
|
+
|
|
516
648
|
Alternatively, from command line:
|
|
517
649
|
```bash
|
|
518
650
|
# Trigger via GitHub CLI
|
|
@@ -647,9 +779,16 @@ Monitor your releases:
|
|
|
647
779
|
### Required GitHub Secrets
|
|
648
780
|
|
|
649
781
|
- `GITHUB_TOKEN`: Automatically provided by GitHub Actions
|
|
650
|
-
- `NPM_TOKEN`: Required for NPM publishing
|
|
782
|
+
- `NPM_TOKEN`: Required for NPM publishing. Lives in the **Alteriom
|
|
783
|
+
organisation** secrets and is inherited — do not add a repository-level copy,
|
|
784
|
+
which would shadow it (see [Where NPM_TOKEN actually lives](#where-npm_token-actually-lives))
|
|
651
785
|
- `PLATFORMIO_AUTH_TOKEN`: Required for PlatformIO Library Registry publishing
|
|
652
786
|
|
|
787
|
+
Both `NPM_TOKEN` and `PLATFORMIO_AUTH_TOKEN` are user-minted tokens that
|
|
788
|
+
**expire**. Their expiry is invisible until a release fails, so rotate them
|
|
789
|
+
together and re-check after any expiry date you set. See
|
|
790
|
+
[NPM Token Expired / Invalid](#-troubleshooting) for the rotation runbook.
|
|
791
|
+
|
|
653
792
|
### Repository Settings
|
|
654
793
|
- **Actions**: Enabled with write permissions
|
|
655
794
|
- **Packages**: Enabled for GitHub Packages publication
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* QUICK START
|
|
18
18
|
* -----------
|
|
19
19
|
* To create your own custom package:
|
|
20
|
-
* 1. Pick an unused Type ID from the table below (use
|
|
20
|
+
* 1. Pick an unused Type ID from the table below (use 206+ range)
|
|
21
21
|
* 2. Choose a base class: BroadcastPackage (all nodes) or SinglePackage (one
|
|
22
22
|
* node)
|
|
23
23
|
* 3. Add your data fields with appropriate types
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
*
|
|
31
31
|
* 200 : SensorPackage (environmental sensors: temp, humidity, pressure)
|
|
32
32
|
* 202 : StatusPackage (device health and configuration)
|
|
33
|
-
* 203 : MpptPackage (MPPT solar charge controller data) <-- this file
|
|
34
33
|
* 204 : MetricsPackage (network performance metrics)
|
|
34
|
+
* 205 : MpptPackage (MPPT solar charge controller data) <-- this file
|
|
35
35
|
* 400 : CommandPackage (device control commands)
|
|
36
36
|
* 600 : MeshNodeListPackage
|
|
37
37
|
* 601 : MeshTopologyPackage
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* 612 : BridgeTakeoverPackage
|
|
45
45
|
* 614 : NTPTimeSyncPackage
|
|
46
46
|
*
|
|
47
|
-
* Available ranges:
|
|
47
|
+
* Available ranges: 206-399 (add your package here and update this table).
|
|
48
48
|
*
|
|
49
49
|
*
|
|
50
50
|
* CHOOSING BASE CLASS
|
|
@@ -138,15 +138,15 @@
|
|
|
138
138
|
* TSTRING myText = "";
|
|
139
139
|
*
|
|
140
140
|
* // MQTT message_type (set to your chosen type ID)
|
|
141
|
-
* uint16_t messageType =
|
|
141
|
+
* uint16_t messageType = 206;
|
|
142
142
|
*
|
|
143
|
-
* MyCustomPackage() : BroadcastPackage(
|
|
143
|
+
* MyCustomPackage() : BroadcastPackage(206) {}
|
|
144
144
|
*
|
|
145
145
|
* MyCustomPackage(JsonObject jsonObj) : BroadcastPackage(jsonObj) {
|
|
146
146
|
* myId = jsonObj["id"];
|
|
147
147
|
* myValue = jsonObj["val"];
|
|
148
148
|
* myText = jsonObj["txt"].as<TSTRING>();
|
|
149
|
-
* messageType = jsonObj["message_type"] |
|
|
149
|
+
* messageType = jsonObj["message_type"] | 206;
|
|
150
150
|
* }
|
|
151
151
|
*
|
|
152
152
|
* JsonObject addTo(JsonObject&& jsonObj) const {
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
* CONCRETE EXAMPLE: MpptPackage
|
|
174
174
|
* ==============================
|
|
175
175
|
*
|
|
176
|
-
* The MpptPackage (Type
|
|
176
|
+
* The MpptPackage (Type 205) transmits real-time telemetry from an MPPT solar
|
|
177
177
|
* charge controller (e.g. Renegy, Epever, Victron). It is a BroadcastPackage
|
|
178
178
|
* so every node in the mesh receives the data automatically.
|
|
179
179
|
*
|
|
@@ -219,7 +219,7 @@ enum ChargeState : uint8_t {
|
|
|
219
219
|
* values from your hardware, assign them to the struct fields, then call
|
|
220
220
|
* sendBroadcast() as shown in alteriom_mppt_example.ino.
|
|
221
221
|
*
|
|
222
|
-
* Type ID:
|
|
222
|
+
* Type ID: 205
|
|
223
223
|
*/
|
|
224
224
|
class MpptPackage : public painlessmesh::plugin::BroadcastPackage {
|
|
225
225
|
public:
|
|
@@ -247,13 +247,13 @@ class MpptPackage : public painlessmesh::plugin::BroadcastPackage {
|
|
|
247
247
|
uint32_t timestamp = 0;
|
|
248
248
|
|
|
249
249
|
// MQTT Schema message_type for fast classification at the bridge
|
|
250
|
-
uint16_t messageType =
|
|
250
|
+
uint16_t messageType = 205; // MPPT_DATA
|
|
251
251
|
|
|
252
252
|
// -------------------------------------------------------------------------
|
|
253
253
|
// Constructors
|
|
254
254
|
// -------------------------------------------------------------------------
|
|
255
255
|
|
|
256
|
-
MpptPackage() : BroadcastPackage(
|
|
256
|
+
MpptPackage() : BroadcastPackage(205) {}
|
|
257
257
|
|
|
258
258
|
/**
|
|
259
259
|
* @brief Deserialise from a JSON object received over the mesh
|
|
@@ -272,7 +272,7 @@ class MpptPackage : public painlessmesh::plugin::BroadcastPackage {
|
|
|
272
272
|
controllerTemp = jsonObj["ct"];
|
|
273
273
|
deviceId = jsonObj["did"];
|
|
274
274
|
timestamp = jsonObj["ts"];
|
|
275
|
-
messageType = jsonObj["message_type"] |
|
|
275
|
+
messageType = jsonObj["message_type"] | 205;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
// -------------------------------------------------------------------------
|
|
@@ -149,7 +149,7 @@ void handleIncomingPackage(uint32_t from, String& msg) {
|
|
|
149
149
|
uint16_t msgType = obj["type"];
|
|
150
150
|
|
|
151
151
|
switch (msgType) {
|
|
152
|
-
case
|
|
152
|
+
case 205: { // MpptPackage
|
|
153
153
|
MpptPackage received(obj);
|
|
154
154
|
Serial.printf(
|
|
155
155
|
"MPPT from %u: PV=%.1fV/%.1fA/%dW Bat=%.1fV/%d%% "
|
|
@@ -240,7 +240,7 @@ void setup() {
|
|
|
240
240
|
|
|
241
241
|
// Monitor bridge coordination (fires every ~30s per bridge)
|
|
242
242
|
mesh.onBridgeCoordination(
|
|
243
|
-
[](const plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode) {
|
|
243
|
+
[](const painlessmesh::plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode) {
|
|
244
244
|
Serial.printf("Bridge %u: priority=%d, load=%d%%\n",
|
|
245
245
|
fromNode, pkg.priority, pkg.load);
|
|
246
246
|
}
|
|
@@ -248,7 +248,7 @@ void setup() {
|
|
|
248
248
|
|
|
249
249
|
// Get notified when bridge state changes (new/updated/lost)
|
|
250
250
|
mesh.onBridgeCoordinationChanged(
|
|
251
|
-
[](const plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode,
|
|
251
|
+
[](const painlessmesh::plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode,
|
|
252
252
|
TSTRING changeType) {
|
|
253
253
|
Serial.printf("Bridge %s: %u (role=%s)\n",
|
|
254
254
|
changeType.c_str(), fromNode, pkg.role.c_str());
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# tcpRetryConfig
|
|
2
|
+
|
|
3
|
+
Tuning painlessMesh's TCP connection retry behaviour with
|
|
4
|
+
`setTcpRetryConfig()` (issue
|
|
5
|
+
[#378](https://github.com/Alteriom/painlessMesh/issues/378)).
|
|
6
|
+
|
|
7
|
+
## What this controls
|
|
8
|
+
|
|
9
|
+
When a node acquires an IP and tries to open its TCP connection to the mesh,
|
|
10
|
+
that connection can fail — the parent's TCP server may not be ready yet, the
|
|
11
|
+
network stack may still be settling, or several nodes may be connecting at
|
|
12
|
+
once. painlessMesh retries with exponential backoff before giving up and
|
|
13
|
+
falling back to a full WiFi reconnect.
|
|
14
|
+
|
|
15
|
+
Five parameters describe that behaviour:
|
|
16
|
+
|
|
17
|
+
| Field | Default | Meaning |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `maxRetries` | 5 | TCP connect attempts after the first before giving up |
|
|
20
|
+
| `retryDelayMs` | 1000 | Base delay between retries; scaled 1x, 2x, 4x, 8x, 8x |
|
|
21
|
+
| `stabilizationDelayMs` | 500 | Wait after IP acquisition before the first attempt |
|
|
22
|
+
| `exhaustionReconnectDelayMs` | 10000 | Wait before the WiFi reconnect that follows exhaustion |
|
|
23
|
+
| `failureBlockDurationMs` | 60000 | How long a failed peer is skipped during AP selection |
|
|
24
|
+
|
|
25
|
+
With the defaults, a node that cannot reach its parent spends
|
|
26
|
+
1 + 2 + 4 + 8 + 8 = **23 s** retrying, then waits another **10 s** before
|
|
27
|
+
reconnecting WiFi, and will not re-select that same peer for **60 s**.
|
|
28
|
+
|
|
29
|
+
## Profiles in this sketch
|
|
30
|
+
|
|
31
|
+
Switch with `#define ACTIVE_PROFILE`.
|
|
32
|
+
|
|
33
|
+
| | `PROFILE_REALTIME` | default | `PROFILE_RELIABLE` | `PROFILE_BATTERY` |
|
|
34
|
+
|---|---|---|---|---|
|
|
35
|
+
| `maxRetries` | 1 | 5 | 10 | 2 |
|
|
36
|
+
| `retryDelayMs` | 200 | 1000 | 2000 | 3000 |
|
|
37
|
+
| `stabilizationDelayMs` | 100 | 500 | 1000 | 500 |
|
|
38
|
+
| `exhaustionReconnectDelayMs` | 1000 | 10000 | 30000 | 60000 |
|
|
39
|
+
| `failureBlockDurationMs` | 5000 | 60000 | 180000 | 300000 |
|
|
40
|
+
| worst-case retry time | 0.2 s | 23 s | 126 s | 9 s |
|
|
41
|
+
| full failure cycle | 1.2 s | 33 s | 156 s | 69 s |
|
|
42
|
+
|
|
43
|
+
- **`PROFILE_REALTIME`** — real-time sensor and LED meshes, the use case from
|
|
44
|
+
[discussion #368](https://github.com/Alteriom/painlessMesh/discussions/368).
|
|
45
|
+
A node stuck in a 23 s backoff is worse than one that drops and re-scans, so
|
|
46
|
+
fail fast and move on.
|
|
47
|
+
- **`PROFILE_RELIABLE`** — industrial meshes where getting connected matters
|
|
48
|
+
more than how long it takes. `maxRetries = 10` is the maximum the library
|
|
49
|
+
accepts.
|
|
50
|
+
- **`PROFILE_BATTERY`** — every retry is radio-on time. Few attempts, spaced
|
|
51
|
+
widely, and a long blocklist so the node stops waking up for a peer that is
|
|
52
|
+
known to be down.
|
|
53
|
+
|
|
54
|
+
## Clamping
|
|
55
|
+
|
|
56
|
+
`setTcpRetryConfig()` coerces the two values that can render a node unusable:
|
|
57
|
+
|
|
58
|
+
- `maxRetries` is capped at **10**. Each retry allocates an `AsyncClient` and
|
|
59
|
+
schedules a task, so an unbounded value is a heap and recursion-depth hazard
|
|
60
|
+
on ESP8266.
|
|
61
|
+
- `retryDelayMs` is held between **50 ms** and **60000 ms**. A zero delay would
|
|
62
|
+
schedule retries with no spacing — a hot loop allocating an `AsyncClient`
|
|
63
|
+
every scheduler tick. The ceiling keeps `retryDelayMs * 8` clear of `uint32_t`
|
|
64
|
+
overflow.
|
|
65
|
+
|
|
66
|
+
Everything else passes through untouched, including zeros, which are
|
|
67
|
+
meaningful:
|
|
68
|
+
|
|
69
|
+
- `maxRetries = 0` — do not retry at all; fall straight back to a WiFi
|
|
70
|
+
reconnect on the first TCP error.
|
|
71
|
+
- `stabilizationDelayMs = 0` — attempt the TCP connection immediately on IP
|
|
72
|
+
acquisition.
|
|
73
|
+
- `exhaustionReconnectDelayMs = 0` — reconnect WiFi immediately after
|
|
74
|
+
exhaustion.
|
|
75
|
+
- `failureBlockDurationMs = 0` — never blocklist a failed peer.
|
|
76
|
+
|
|
77
|
+
Call `getTcpRetryConfig()` after setting to see what actually took effect; the
|
|
78
|
+
sketch prints this at startup.
|
|
79
|
+
|
|
80
|
+
## The defaults exist for a reason
|
|
81
|
+
|
|
82
|
+
painlessMesh 1.9.x deliberately *raised* these values (retries 3 → 5, base
|
|
83
|
+
delay 500 ms → 1000 ms) to fix real-world mesh instability. Tuning them back
|
|
84
|
+
down reintroduces the problems that change fixed. Symptoms of an over-aggressive
|
|
85
|
+
profile:
|
|
86
|
+
|
|
87
|
+
- **Connection churn** — nodes repeatedly connect and drop. `maxRetries` is too
|
|
88
|
+
low for how long your parent actually takes to be ready; raise it or raise
|
|
89
|
+
`stabilizationDelayMs`.
|
|
90
|
+
- **Rapid reconnect loops / network congestion** — a node hammers a parent whose
|
|
91
|
+
TCP server is down. `exhaustionReconnectDelayMs` is too short.
|
|
92
|
+
- **The same dead peer is picked over and over** — `failureBlockDurationMs` is
|
|
93
|
+
shorter than one full retry-plus-reconnect cycle, so the peer comes off the
|
|
94
|
+
blocklist before the node has finished failing over. Keep
|
|
95
|
+
`failureBlockDurationMs` greater than
|
|
96
|
+
`(sum of retry backoffs) + exhaustionReconnectDelayMs`. All three profiles
|
|
97
|
+
above satisfy this; `catch_tcp_blocklist.cpp` pins it as a test.
|
|
98
|
+
|
|
99
|
+
Change one parameter at a time and watch the serial log with
|
|
100
|
+
`mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION)`.
|
|
101
|
+
|
|
102
|
+
## Building
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# PlatformIO
|
|
106
|
+
pio run -e esp32 # or -e esp8266
|
|
107
|
+
|
|
108
|
+
# Arduino CLI
|
|
109
|
+
arduino-cli compile --fqbn esp32:esp32:esp32 examples/tcpRetryConfig/tcpRetryConfig.ino
|
|
110
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[platformio]
|
|
2
|
+
src_dir = .
|
|
3
|
+
|
|
4
|
+
[env]
|
|
5
|
+
lib_deps =
|
|
6
|
+
bblanchon/ArduinoJson
|
|
7
|
+
arkhipenko/TaskScheduler
|
|
8
|
+
|
|
9
|
+
lib_ldf_mode = deep+
|
|
10
|
+
[env:esp8266]
|
|
11
|
+
platform = espressif8266
|
|
12
|
+
board = nodemcuv2
|
|
13
|
+
framework = arduino
|
|
14
|
+
lib_extra_dirs = ../../
|
|
15
|
+
lib_deps =
|
|
16
|
+
${env.lib_deps} ; Inherit common dependencies
|
|
17
|
+
esp32async/ESPAsyncTCP@^2.0.0 ; Only for ESP8266
|
|
18
|
+
|
|
19
|
+
[env:esp32]
|
|
20
|
+
platform = espressif32
|
|
21
|
+
board = esp32dev
|
|
22
|
+
framework = arduino
|
|
23
|
+
lib_extra_dirs = ../../
|
|
24
|
+
lib_deps =
|
|
25
|
+
${env.lib_deps} ; Inherit common dependencies
|
|
26
|
+
esp32async/AsyncTCP
|