@alteriom/painlessmesh 1.9.19 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/README.md +102 -63
  3. package/RELEASE_GUIDE.md +147 -8
  4. package/examples/alteriom/README.md +4 -4
  5. package/examples/alteriom/alteriom_custom_package_template.hpp +320 -0
  6. package/examples/alteriom/alteriom_sensor_package.hpp +1 -1
  7. package/examples/alteriom/mppt_example/alteriom_mppt_example.ino +208 -0
  8. package/examples/bridge_failover/bridge_failover.ino +17 -0
  9. package/examples/sendToInternet/CMakeLists.txt +54 -0
  10. package/examples/sendToInternet/PC_NODE_README.md +517 -0
  11. package/examples/sendToInternet/README.md +39 -1
  12. package/examples/sendToInternet/build.sh +153 -0
  13. package/examples/sendToInternet/mock_server_test.ino +361 -0
  14. package/examples/sendToInternet/pc_mesh_node.cpp +361 -0
  15. package/examples/tcpRetryConfig/README.md +110 -0
  16. package/examples/tcpRetryConfig/platformio.ini +26 -0
  17. package/examples/tcpRetryConfig/tcpRetryConfig.ino +154 -0
  18. package/keywords.txt +3 -0
  19. package/library.json +4 -1
  20. package/library.properties +1 -1
  21. package/package.json +3 -3
  22. package/src/AlteriomPainlessMesh.h +6 -14
  23. package/src/arduino/wifi.hpp +352 -114
  24. package/src/connection.cpp +10 -0
  25. package/src/painlessMesh.h +2 -15
  26. package/src/painlessTaskOptions.h +9 -0
  27. package/src/painlessmesh/buffer.hpp +4 -1
  28. package/src/painlessmesh/configuration.hpp +13 -2
  29. package/src/painlessmesh/connection.hpp +36 -21
  30. package/src/painlessmesh/gateway.hpp +0 -1061
  31. package/src/painlessmesh/mesh.hpp +102 -107
  32. package/src/painlessmesh/message_queue.hpp +25 -15
  33. package/src/painlessmesh/metrics.hpp +2 -262
  34. package/src/painlessmesh/plugin.hpp +27 -5
  35. package/src/painlessmesh/tcp.hpp +158 -29
  36. package/src/painlessmesh/validation.hpp +0 -143
  37. package/docs/README.md +0 -132
  38. package/docs/alteriom/overview.md +0 -531
  39. package/docs/api/core-api.md +0 -607
  40. package/docs/api/shared-gateway.md +0 -1207
  41. package/docs/architecture/mesh-architecture.md +0 -399
  42. package/docs/architecture/plugin-system.md +0 -517
  43. package/docs/getting-started/arduino-manual-install.md +0 -313
  44. package/docs/getting-started/first-mesh.md +0 -410
  45. package/docs/getting-started/installation.md +0 -275
  46. package/docs/getting-started/quickstart.md +0 -158
  47. package/docs/troubleshooting/common-issues.md +0 -679
  48. package/docs/troubleshooting/debugging.md +0 -455
  49. package/docs/troubleshooting/external-device-connection.md +0 -283
  50. package/docs/troubleshooting/faq.md +0 -574
  51. package/docs/tutorials/basic-examples.md +0 -718
package/CHANGELOG.md CHANGED
@@ -7,12 +7,180 @@ 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
+
73
+ ### Fixed
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
+
14
92
  ### Fixed
15
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
+
137
+ ## [1.9.20] - 2026-03-27
138
+
139
+ ### Added
140
+
141
+ - **Bridge Coordination Monitoring Callbacks** - User-facing API for observing bridge coordination events
142
+ - `mesh.onBridgeCoordination(cb)` — fires on every coordination message (~30s per bridge)
143
+ - `mesh.onBridgeCoordinationChanged(cb)` — fires on state changes: "new", "updated", "lost"
144
+ - Works on both bridge and non-bridge nodes (monitoring is not bridge-only)
145
+ - Lost bridge detection with 60-second timeout
146
+ - 5 new test scenarios with 18 assertions for change detection logic
147
+
148
+ ### Fixed
149
+
150
+ - **Full Repo Cleanup** - Comprehensive audit and cleanup of the entire codebase (#357)
151
+ - Fix double-free of mScheduler in ~Mesh() destructor
152
+ - Fix int8_t RSSI aggregation overflow (widened to int32_t)
153
+ - Fix Task object memory leak in scheduleAsyncClientDeletion
154
+ - Replace blocking delay(1000) with yield-based approach in bridge init
155
+ - Add max-iteration guard to getPathToNode (prevents infinite loop)
156
+ - Move static vars from connection.hpp to connection.cpp (ODR violation fix)
157
+ - Initialize shouldContainRoot to false (undefined behavior fix)
158
+ - Fix getAccurateTime() to always return seconds
159
+ - Fix cancelInternetRequest reentrancy via scheduled callback
160
+ - Widen BFS hop counter from uint8_t to uint16_t
161
+ - Remove dead revert path in promoteToBridge
162
+ - Cap knownBridgePeers at 32 entries
163
+ - Increase WiFi event semaphore timeout from 100 to 1000 ticks
164
+ - Inline getGatewayCount() to avoid temporary vector allocation
165
+ - Remove MessageQueue::reserve(1000) (wasted heap on ESP8266)
166
+ - Fix Timer::elapsed_us() to return microseconds on ESP32
167
+ - Cache hasActualInternetAccess() with 60s TTL
168
+ - Remove ~3,600 lines of dead code (unused classes, stubs, AI-generated shelf code)
169
+ - Remove 40 AI-generated markdown files from root directory
170
+ - Delete abandoned documentation systems (docs-website/, website/)
171
+ - Fix all CI/CD workflow action versions
172
+ - Remove 15 fake test files (only REQUIRE(true) assertions)
173
+ - Fix millis()/micros() test mocks to prevent uint32_t overflow
174
+ - **Documentation Consistency** - Feature audit and doc fixes (#358)
175
+ - Fix README version (1.9.17 -> 1.9.20)
176
+ - Fix 6 broken documentation links
177
+ - Fix Message Queue API examples to use real enableMessageQueue/queueMessage API
178
+ - Remove non-existent onBridgeCoordination callback from examples
179
+ - Fix EnhancedStatusPackage type ID comment (203 -> 604)
180
+ - Rewrite Quick Start example to use TaskScheduler instead of delay()
181
+ - Remove dead conditional includes from painlessMesh.h
182
+ - Add missing examples to library.json
183
+
16
184
  ## [1.9.19] - 2025-12-21
17
185
 
18
186
  ### Fixed
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div align="center">
6
6
 
7
- **Version 1.9.19** - Gateway connectivity error non-retryable fix for infrastructure issues
7
+ **Version 1.10.0** - Tunable TCP connect-retry behaviour via `setTcpRetryConfig()` (#378)
8
8
 
9
9
  [![CI/CD Pipeline](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml/badge.svg)](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
10
10
  [![Documentation](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml/badge.svg)](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
@@ -209,11 +209,25 @@ mesh.initAsBridge(MESH_PREFIX, MESH_PASSWORD,
209
209
  ROUTER_SSID, ROUTER_PASSWORD,
210
210
  &userScheduler, MESH_PORT, 10);
211
211
 
212
- // Configure load balancing strategy
213
- mesh.setBridgeSelectionStrategy(ROUND_ROBIN);
212
+ // Enable bridge failover for high availability
213
+ mesh.enableBridgeFailover(true);
214
+
215
+ // Monitor bridge coordination (fires every ~30s per bridge)
216
+ mesh.onBridgeCoordination(
217
+ [](const plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode) {
218
+ Serial.printf("Bridge %u: priority=%d, load=%d%%\n",
219
+ fromNode, pkg.priority, pkg.load);
220
+ }
221
+ );
214
222
 
215
- // Monitor bridge coordination
216
- mesh.onBridgeCoordination(&bridgeCoordinationCallback);
223
+ // Get notified when bridge state changes
224
+ mesh.onBridgeCoordinationChanged(
225
+ [](const plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode,
226
+ TSTRING changeType) {
227
+ Serial.printf("Bridge %s: %u (role=%s)\n",
228
+ changeType.c_str(), fromNode, pkg.role.c_str());
229
+ }
230
+ );
217
231
  ```
218
232
 
219
233
  See [BRIDGE_TO_INTERNET.md](BRIDGE_TO_INTERNET.md) for multi-bridge documentation.
@@ -239,16 +253,11 @@ See [BRIDGE_TO_INTERNET.md](BRIDGE_TO_INTERNET.md) for multi-bridge documentatio
239
253
  **Example:**
240
254
  ```cpp
241
255
  // Enable message queue with max 100 messages
242
- mesh.enableMessageQueue(true);
243
- mesh.setMaxQueueSize(100);
256
+ mesh.enableMessageQueue(true, 100);
244
257
 
245
258
  // Queue critical alarm message
246
259
  String criticalAlarm = "{\"sensor\":\"O2\",\"value\":2.5,\"alarm\":true}";
247
- mesh.queueMessage(criticalAlarm, CRITICAL);
248
-
249
- // Set callbacks
250
- mesh.onQueueFull(&queueFullCallback);
251
- mesh.onQueueFlushed(&queueFlushedCallback);
260
+ mesh.queueMessage(bridgeNodeId, criticalAlarm);
252
261
  ```
253
262
 
254
263
  See [BRIDGE_TO_INTERNET.md](BRIDGE_TO_INTERNET.md) for message queue documentation.
@@ -393,46 +402,34 @@ If platformio is used to install the library, then the dependencies will be inst
393
402
 
394
403
  ```cpp
395
404
  #include "painlessMesh.h"
396
- #include "examples/alteriom/alteriom_sensor_package.hpp"
397
-
398
- using namespace alteriom;
399
405
 
400
- #define MESH_PREFIX "AlteriomMesh"
401
- #define MESH_PASSWORD "your_password"
402
- #define MESH_PORT 5555
406
+ #define MESH_PREFIX "whateverYouLike"
407
+ #define MESH_PASSWORD "somethingSneaky"
408
+ #define MESH_PORT 5555
403
409
 
404
410
  Scheduler userScheduler;
405
411
  painlessMesh mesh;
406
412
 
407
- void setup() {
408
- Serial.begin(115200);
409
- mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
410
- mesh.onReceive(&receivedCallback);
413
+ Task taskSendMessage(TASK_SECOND * 30, TASK_FOREVER, []() {
414
+ String msg = "Hello from node ";
415
+ msg += mesh.getNodeId();
416
+ mesh.sendBroadcast(msg);
417
+ });
418
+
419
+ void receivedCallback(uint32_t from, String &msg) {
420
+ Serial.printf("Received from %u: %s\n", from, msg.c_str());
411
421
  }
412
422
 
413
- void loop() {
414
- mesh.update();
415
-
416
- // Create and send sensor data
417
- SensorPackage sensor;
418
- sensor.temperature = 25.5;
419
- sensor.humidity = 60.0;
420
- sensor.sensorId = mesh.getNodeId();
421
- sensor.timestamp = mesh.getNodeTime();
422
-
423
- mesh.sendBroadcast(sensor.toJsonString());
424
- delay(30000); // Send every 30 seconds
423
+ void setup() {
424
+ Serial.begin(115200);
425
+ mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
426
+ mesh.onReceive(&receivedCallback);
427
+ userScheduler.addTask(taskSendMessage);
428
+ taskSendMessage.enable();
425
429
  }
426
430
 
427
- void receivedCallback(uint32_t from, String& msg) {
428
- JsonDocument doc; // ArduinoJson v7
429
- deserializeJson(doc, msg);
430
-
431
- if (doc["type"] == 200) { // SensorPackage
432
- SensorPackage sensor(doc.as<JsonObject>());
433
- Serial.printf("Sensor %u: %.1f°C, %.1f%% RH\n",
434
- sensor.sensorId, sensor.temperature, sensor.humidity);
435
- }
431
+ void loop() {
432
+ mesh.update();
436
433
  }
437
434
  ```
438
435
 
@@ -563,14 +560,36 @@ These are the message types used by applications built on painlessMesh:
563
560
  - **Event Coordination** - Synchronized displays, distributed processing
564
561
  - **Bridge Networks** - Connect mesh to WiFi/Internet/MQTT - [📖 Bridge Guide](BRIDGE_TO_INTERNET.md)
565
562
 
566
- ## Latest Release: v1.9.17 (December 21, 2025)
563
+ ## Latest Release: v1.10.0 (August 12, 2026)
564
+
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
567
584
 
568
- **Documentation Enhancement & Package Organization**
585
+ **Previous Release: v1.9.20 (March 27, 2026)** — Full repo cleanup, bug fixes & bridge coordination callbacks:
569
586
 
570
- - 📚 **Production-Level Documentation** - Removed phase terminology for clearer product positioning
571
- - 📦 **Complete Package Catalog** - All 19 application-level package types documented in numerical order
572
- - 🎯 **Enhanced Feature Clarity** - Improved Advanced Features section with comprehensive capability list
573
- - **Professional Quality** - Documentation suitable for enterprise adoption and production deployments
587
+ - New `onBridgeCoordination()` and `onBridgeCoordinationChanged()` monitoring callbacks
588
+ - Fixed 13 critical/high/medium bugs (double-free, RSSI overflow, memory leaks, blocking delays)
589
+ - Removed ~3,600 lines of dead code and 40 AI-generated docs
590
+ - Fixed all CI/CD workflows (corrected action versions)
591
+ - Removed 15 fake test files, fixed test infrastructure
592
+ - Documentation consistency audit — fixed broken links, API examples, version references
574
593
 
575
594
  **Recent Key Features (v1.9.0 - v1.9.16):**
576
595
 
@@ -578,16 +597,16 @@ These are the message types used by applications built on painlessMesh:
578
597
  - 🌉 **Improved Bridge Detection** - `getPrimaryBridge()` returns last known bridge when disconnected
579
598
  - ⚡ **Enhanced TCP Reliability** - Exponential backoff and increased retries for mesh connections
580
599
  - 🛡️ **Race Condition Fixes** - Improved bridge status and connection validation
581
- - 📦 **Consolidated Examples** - Streamlined to 14 essential examples
600
+ - 📦 **Consolidated Examples** - Streamlined to 15 essential examples
582
601
  - ⚙️ **Configurable Election Timing** - Prevent split-brain with `setElectionStartupDelay()` and `setElectionRandomDelay()`
583
602
 
584
603
  **[📋 Full CHANGELOG](CHANGELOG.md)**
585
604
 
586
605
  ## Getting Help
587
606
 
588
- - **[FAQ](docs/troubleshooting/faq.md)** - Common questions and solutions
589
- - **[Common Issues](docs/troubleshooting/common-issues.md)** - Troubleshooting guide
590
- - **[GitHub Issues](https://github.com/Alteriom/painlessMesh/issues)** - Bug reports and feature requests
607
+ - **[FAQ](USER_GUIDE.md)** - Common questions and solutions
608
+ - **[Common Issues](USER_GUIDE.md)** - Troubleshooting guide
609
+ - **[GitHub Issues](https://github.com/Alteriom/painlessMesh/issues)** - Bug reports and feature requests
591
610
  - **[Community Forum](https://groups.google.com/forum/#!forum/painlessmesh-user)** - Community support
592
611
  - **[API Documentation](https://alteriom.github.io/painlessMesh/#/api/doxygen)** - Generated API docs
593
612
 
@@ -610,6 +629,28 @@ run-parts --regex catch_ bin/ # Run tests
610
629
  - **Dependencies**: ArduinoJson 7.x, TaskScheduler 4.x
611
630
  - **Development**: CMake, Ninja, Boost (for desktop testing)
612
631
 
632
+ ### Testing Bridge/Internet Functionality
633
+
634
+ painlessMesh includes a **Mock HTTP Server** for testing `sendToInternet()` functionality without requiring actual Internet connectivity. This enables:
635
+
636
+ - 🚀 **Fast testing cycles** - Instant responses instead of waiting for external APIs
637
+ - 🔧 **Offline development** - No Internet connection required
638
+ - ✅ **Reproducible scenarios** - Control all test conditions precisely
639
+ - 🤖 **CI/CD automation** - Automated testing in pipelines
640
+
641
+ ```bash
642
+ # Start mock server
643
+ cd test/mock-http-server
644
+ python3 server.py
645
+
646
+ # Test various HTTP scenarios
647
+ curl http://localhost:8080/status/200 # Success
648
+ curl http://localhost:8080/status/404 # Not Found
649
+ curl http://localhost:8080/whatsapp?... # WhatsApp API simulation
650
+ ```
651
+
652
+ See [Mock HTTP Server Documentation](test/mock-http-server/README.md) for complete usage guide.
653
+
613
654
  ### CI/CD Pipeline
614
655
 
615
656
  painlessMesh features a state-of-the-art automated CI/CD pipeline:
@@ -651,8 +692,6 @@ We try to follow the [git flow](https://www.atlassian.com/git/tutorials/comparin
651
692
 
652
693
  If you like the library please consider supporting its development. Your contributions help me spend more time improving painlessMesh.
653
694
 
654
- [![PayPal Donation](paypal/qrcode.png)](https://www.paypal.com/paypalme/domlavoie)
655
-
656
695
  **[Donate via PayPal](https://www.paypal.com/paypalme/domlavoie)** • [dominic.lavoie@gmail.com](mailto:dominic.lavoie@gmail.com)
657
696
 
658
697
  ## 📚 Documentation
@@ -669,18 +708,18 @@ If you like the library please consider supporting its development. Your contrib
669
708
  ### 🚀 Quick Links
670
709
 
671
710
  **New to AlteriomPainlessMesh?**
672
- - [Quick Start](docs/getting-started/quickstart.md) - Get your first mesh running in 5 minutes
673
- - [Installation](docs/getting-started/installation.md) - Arduino IDE and PlatformIO setup
674
- - [First Mesh](docs/getting-started/first-mesh.md) - Build a multi-node network
711
+ - [Quick Start](USER_GUIDE.md) - Get your first mesh running in 5 minutes
712
+ - [Installation](USER_GUIDE.md) - Arduino IDE and PlatformIO setup
713
+ - [First Mesh](USER_GUIDE.md) - Build a multi-node network
675
714
 
676
715
  **Reference Documentation:**
677
- - [Core API](docs/api/core-api.md) - painlessMesh class methods
678
- - [Alteriom Extensions](docs/alteriom/overview.md) - SensorPackage, CommandPackage, StatusPackage
679
- - [Examples](examples/) - 16 working examples for common scenarios
716
+ - [Core API](USER_GUIDE.md) - painlessMesh class methods
717
+ - [Alteriom Extensions](examples/alteriom/README.md) - SensorPackage, CommandPackage, StatusPackage
718
+ - [Examples](examples/) - 15 working examples for common scenarios
680
719
 
681
720
  **Need Help?**
682
- - [FAQ](docs/troubleshooting/faq.md) - Frequently asked questions
683
- - [Common Issues](docs/troubleshooting/common-issues.md) - Troubleshooting guide
721
+ - [FAQ](USER_GUIDE.md) - Frequently asked questions
722
+ - [Common Issues](USER_GUIDE.md) - Troubleshooting guide
684
723
  - [GitHub Issues](https://github.com/Alteriom/painlessMesh/issues) - Bug reports and support
685
724
 
686
725
  ## 🔧 Quick API Reference
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 automatically triggers on:
212
+ The PlatformIO workflow is started by:
213
213
 
214
- - New GitHub releases (tags)
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
- # Verify NPM authentication
460
- npm whoami
461
- # If not logged in: npm login
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 (add in repository secrets)
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
@@ -34,7 +34,7 @@ Broadcast package for sharing device health and status information.
34
34
  - `wifiStrength` - WiFi signal strength (0-100)
35
35
  - `firmwareVersion` - Current firmware version string
36
36
 
37
- ### EnhancedStatusPackage (Type 203) - Phase 1
37
+ ### EnhancedStatusPackage (Type 604)
38
38
  Extended status package with comprehensive health metrics (18 fields).
39
39
 
40
40
  **Additional Fields:**
@@ -177,12 +177,12 @@ Complete Arduino sketch demonstrating:
177
177
  - Message type discrimination
178
178
  - Integration with painlessMesh
179
179
 
180
- ### `phase1_features.ino` (NEW)
181
- Phase 1 OTA enhancement example demonstrating:
180
+ ### `phase1_features.ino`
181
+ OTA enhancement example demonstrating:
182
182
  - Compressed OTA transfer infrastructure
183
183
  - Enhanced status reporting with comprehensive metrics
184
184
  - Alert system implementation
185
- - Usage patterns for Phase 1 features
185
+ - Usage patterns for advanced features
186
186
 
187
187
  ### `metrics_health_node.ino` (NEW in v1.7.7)
188
188
  Comprehensive monitoring node example demonstrating: