@alteriom/painlessmesh 1.8.11 → 1.8.13

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 CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.8.12] - 2025-11-19
11
+
12
+ ### Changed
13
+
14
+ - **Documentation Updates** - Comprehensive documentation improvements
15
+ - Updated all documentation to reflect current library state
16
+ - Improved code examples and usage instructions
17
+ - Enhanced API reference documentation
18
+ - Verified all links and references
19
+ - Merged via PRs #152, #153
20
+
21
+ ### Fixed
22
+
23
+ - **Code Quality** - Resolved linting and formatting issues
24
+ - Fixed clang-format compliance across codebase
25
+ - Ensured prettier formatting consistency
26
+ - Improved code quality and maintainability
27
+
10
28
  ## [1.8.11] - 2025-11-18
11
29
 
12
30
  ### Fixed
@@ -0,0 +1,146 @@
1
+ # AlteriomPainlessMesh Documentation Index
2
+
3
+ Complete guide to finding documentation in the AlteriomPainlessMesh library.
4
+
5
+ ## Quick Links
6
+
7
+ - 🌐 **[Online Documentation](https://alteriom.github.io/painlessMesh/)** - Interactive documentation website
8
+ - 📖 **[API Reference](https://alteriom.github.io/painlessMesh/#/api/doxygen)** - Complete API documentation
9
+ - 🎯 **[Examples](https://alteriom.github.io/painlessMesh/#/tutorials/basic-examples)** - Code examples and tutorials
10
+
11
+ ## Core Documentation
12
+
13
+ ### Getting Started
14
+ - **[README.md](README.md)** - Project overview, features, and quick start
15
+ - **[docs/getting-started/quickstart.md](docs/getting-started/quickstart.md)** - Quick start guide
16
+ - **[docs/getting-started/installation.md](docs/getting-started/installation.md)** - Installation instructions
17
+ - **[docs/getting-started/first-mesh.md](docs/getting-started/first-mesh.md)** - Your first mesh network
18
+
19
+ ### Release Information
20
+ - **[CHANGELOG.md](CHANGELOG.md)** - Complete version history
21
+ - **[RELEASE_GUIDE.md](RELEASE_GUIDE.md)** - Release process for maintainers
22
+ - **[RELEASE_NOTES_1.8.12.md](RELEASE_NOTES_1.8.12.md)** - Latest release notes
23
+ - **[RELEASE_CHECKLIST_1.8.12.md](RELEASE_CHECKLIST_1.8.12.md)** - Release checklist
24
+
25
+ ### Contributing
26
+ - **[CONTRIBUTING.md](CONTRIBUTING.md)** - How to contribute to the project
27
+ - **[LICENSE](LICENSE)** - LGPL-3.0 license terms
28
+
29
+ ## Technical Documentation
30
+
31
+ ### Alteriom Extensions
32
+ - **[docs/alteriom/overview.md](docs/alteriom/overview.md)** - Alteriom extensions overview
33
+ - **[examples/alteriom/README.md](examples/alteriom/README.md)** - Alteriom package documentation
34
+ - **[examples/alteriom/alteriom_sensor_package.hpp](examples/alteriom/alteriom_sensor_package.hpp)** - Package definitions with extensive inline documentation
35
+
36
+ ### MQTT Integration
37
+ - **[docs/MQTT_BRIDGE_COMMANDS.md](docs/MQTT_BRIDGE_COMMANDS.md)** - MQTT command API
38
+ - **[docs/MQTT_BRIDGE_IMPLEMENTATION_SUMMARY.md](docs/MQTT_BRIDGE_IMPLEMENTATION_SUMMARY.md)** - Implementation details
39
+ - **[docs/MQTT_SCHEMA_COMPLIANCE.md](docs/MQTT_SCHEMA_COMPLIANCE.md)** - Schema validation
40
+ - **[docs/OTA_COMMANDS_REFERENCE.md](docs/OTA_COMMANDS_REFERENCE.md)** - OTA update commands
41
+
42
+ ### Advanced Features
43
+ - **[docs/MESH_TOPOLOGY_GUIDE.md](docs/MESH_TOPOLOGY_GUIDE.md)** - Network topology reporting
44
+ - **[docs/BRIDGE_FAILOVER.md](docs/BRIDGE_FAILOVER.md)** - Bridge failover documentation
45
+ - **[docs/BRIDGE_HEALTH_MONITORING.md](docs/BRIDGE_HEALTH_MONITORING.md)** - Bridge health monitoring
46
+ - **[BRIDGE_TO_INTERNET.md](BRIDGE_TO_INTERNET.md)** - Connecting mesh to internet
47
+
48
+ ### Development
49
+ - **[docs/API_DESIGN_GUIDELINES.md](docs/API_DESIGN_GUIDELINES.md)** - API design patterns
50
+ - **[docs/VERSION_MANAGEMENT.md](docs/VERSION_MANAGEMENT.md)** - Version management guide
51
+ - **[docs/development/DOCKER_TESTING.md](docs/development/DOCKER_TESTING.md)** - Docker testing guide
52
+ - **[docs/development/TESTING_SUMMARY.md](docs/development/TESTING_SUMMARY.md)** - Test suite overview
53
+ - **[docs/development/ARDUINO_COMPLIANCE_SUMMARY.md](docs/development/ARDUINO_COMPLIANCE_SUMMARY.md)** - Arduino standards
54
+
55
+ ### Phase Documentation
56
+ - **[docs/PHASE1_GUIDE.md](docs/PHASE1_GUIDE.md)** - Phase 1 features (v1.6.x)
57
+ - **[docs/PHASE2_GUIDE.md](docs/PHASE2_GUIDE.md)** - Phase 2 features (v1.7.x+)
58
+ - **[docs/releases/FEATURE_HISTORY.md](docs/releases/FEATURE_HISTORY.md)** - Consolidated feature history
59
+
60
+ ## Example Code
61
+
62
+ ### Basic Examples
63
+ - **[examples/basic/](examples/basic/)** - Basic mesh networking
64
+ - **[examples/startHere/](examples/startHere/)** - Simple starting point
65
+
66
+ ### Alteriom Examples
67
+ - **[examples/alteriom/](examples/alteriom/)** - Core Alteriom package examples
68
+ - **[examples/alteriomSensorNode/](examples/alteriomSensorNode/)** - Sensor node implementation
69
+ - **[examples/alteriomImproved/](examples/alteriomImproved/)** - Enhanced sensor node
70
+ - **[examples/alteriomMetricsHealth/](examples/alteriomMetricsHealth/)** - Metrics and health monitoring
71
+ - **[examples/alteriomPhase1/](examples/alteriomPhase1/)** - Phase 1 features demo
72
+ - **[examples/alteriomPhase2/](examples/alteriomPhase2/)** - Phase 2 features demo
73
+
74
+ ### Bridge Examples
75
+ - **[examples/bridge/](examples/bridge/)** - Basic bridge examples
76
+ - **[examples/bridge_failover/](examples/bridge_failover/)** - Bridge failover implementation
77
+ - **[examples/multi_bridge/](examples/multi_bridge/)** - Multiple bridge setup
78
+ - **[examples/bridgeAwareSensorNode/](examples/bridgeAwareSensorNode/)** - Bridge-aware nodes
79
+
80
+ ### MQTT Examples
81
+ - **[examples/mqttBridge/](examples/mqttBridge/)** - MQTT bridge
82
+ - **[examples/mqttCommandBridge/](examples/mqttCommandBridge/)** - Command bridge
83
+ - **[examples/mqttStatusBridge/](examples/mqttStatusBridge/)** - Status reporting bridge
84
+ - **[examples/mqttTopologyTest/](examples/mqttTopologyTest/)** - Topology testing
85
+
86
+ ### Advanced Examples
87
+ - **[examples/otaReceiver/](examples/otaReceiver/)** - OTA update receiver
88
+ - **[examples/otaSender/](examples/otaSender/)** - OTA update sender
89
+ - **[examples/ntpTimeSyncBridge/](examples/ntpTimeSyncBridge/)** - NTP time sync bridge
90
+ - **[examples/ntpTimeSyncNode/](examples/ntpTimeSyncNode/)** - NTP time sync node
91
+ - **[examples/webServer/](examples/webServer/)** - Mesh web server
92
+ - **[examples/diagnosticsExample/](examples/diagnosticsExample/)** - Diagnostics tools
93
+
94
+ ## Troubleshooting
95
+
96
+ - **[docs/troubleshooting/common-issues.md](docs/troubleshooting/common-issues.md)** - Common problems and solutions
97
+ - **[docs/troubleshooting/ESP32_C6_COMPATIBILITY.md](docs/troubleshooting/ESP32_C6_COMPATIBILITY.md)** - ESP32-C6 issues
98
+ - **[docs/troubleshooting/debugging.md](docs/troubleshooting/debugging.md)** - Debugging techniques
99
+ - **[docs/troubleshooting/faq.md](docs/troubleshooting/faq.md)** - Frequently asked questions
100
+
101
+ ## Architecture Documentation
102
+
103
+ - **[docs/architecture/mesh-architecture.md](docs/architecture/mesh-architecture.md)** - Mesh architecture overview
104
+ - **[docs/architecture/plugin-system.md](docs/architecture/plugin-system.md)** - Plugin architecture
105
+ - **[docs/architecture/routing.md](docs/architecture/routing.md)** - Message routing
106
+ - **[docs/architecture/time-sync.md](docs/architecture/time-sync.md)** - Time synchronization
107
+
108
+ ## Support & Community
109
+
110
+ ### Getting Help
111
+ - **GitHub Issues**: https://github.com/Alteriom/painlessMesh/issues
112
+ - **GitHub Discussions**: https://github.com/Alteriom/painlessMesh/discussions
113
+
114
+ ### Package Registries
115
+ - **NPM**: https://www.npmjs.com/package/@alteriom/painlessmesh
116
+ - **PlatformIO**: https://registry.platformio.org/libraries/alteriom/painlessMesh
117
+ - **Arduino Library Manager**: Search for "AlteriomPainlessMesh"
118
+
119
+ ## Version-Specific Documentation
120
+
121
+ ### Current Version (1.8.12)
122
+ - Focus on documentation improvements and code quality
123
+ - Added prettier configuration for consistent formatting
124
+ - Enhanced inline documentation
125
+ - See [RELEASE_NOTES_1.8.12.md](RELEASE_NOTES_1.8.12.md) for details
126
+
127
+ ### Previous Versions
128
+ - **v1.8.11** - Bridge discovery and Windows MSVC compatibility fixes
129
+ - **v1.8.10** - Bridge status direct messaging improvements
130
+ - **v1.8.9** - Bridge self-registration fixes
131
+ - **v1.8.0** - Bridge failover introduction
132
+ - **v1.7.0** - Phase 2 features (broadcast OTA, MQTT status bridge)
133
+ - **v1.6.0** - Phase 1 features (Alteriom packages)
134
+
135
+ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
136
+
137
+ ## Contributing to Documentation
138
+
139
+ To contribute to documentation:
140
+ 1. Follow the style guide in existing documentation
141
+ 2. Update this index when adding new documentation files
142
+ 3. Ensure all links are functional
143
+ 4. Use Markdown for all documentation
144
+ 5. Include code examples where appropriate
145
+
146
+ For detailed contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md).
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AlteriomPainlessMesh
2
2
 
3
- > **📚 [Complete Documentation](https://alteriom.github.io/painlessMesh/)** | **📖 [API Reference](https://alteriom.github.io/painlessMesh/#/api/doxygen)** | **🎯 [Examples](https://alteriom.github.io/painlessMesh/#/tutorials/basic-examples)**
3
+ > **📚 [Complete Documentation](https://alteriom.github.io/painlessMesh/)** | **📖 [API Reference](https://alteriom.github.io/painlessMesh/#/api/doxygen)** | **🎯 [Examples](https://alteriom.github.io/painlessMesh/#/tutorials/basic-examples)** | **🗂️ [Documentation Index](DOCUMENTATION_INDEX.md)**
4
4
 
5
5
  <div align="center">
6
6
 
@@ -0,0 +1,209 @@
1
+ # Channel Synchronization in painlessMesh
2
+
3
+ ## Problem Statement
4
+
5
+ When a mesh network operates on one channel and a node promotes to bridge via election, it may connect to a router operating on a different channel. This creates a channel mismatch where:
6
+
7
+ 1. Bridge node switches to router's channel (e.g., channel 6)
8
+ 2. Other mesh nodes remain on original channel (e.g., channel 1)
9
+ 3. Bridge takeover announcements sent on new channel are not heard by nodes on old channel
10
+ 4. Nodes cannot find the mesh network and become isolated
11
+
12
+ ## Solution Overview
13
+
14
+ The solution has two complementary parts:
15
+
16
+ ### Part 1: Automatic Channel Re-detection
17
+
18
+ Nodes automatically detect when they can't find the mesh on their current channel and trigger a full channel scan to locate it.
19
+
20
+ **Implementation:** `src/painlessMeshSTA.cpp` and `src/painlessMeshSTA.h`
21
+
22
+ **Key Components:**
23
+ - `consecutiveEmptyScans` counter tracks scans with no mesh nodes found
24
+ - `EMPTY_SCAN_THRESHOLD` constant (6 scans) determines when to trigger re-scan
25
+ - Full channel scan using `scanForMeshChannel()` when threshold reached
26
+ - Automatic channel update and AP restart when mesh found on different channel
27
+
28
+ **Flow:**
29
+ ```
30
+ 1. Node scans on current channel (e.g., channel 1)
31
+ 2. Finds no mesh nodes → increment consecutiveEmptyScans
32
+ 3. Repeat for EMPTY_SCAN_THRESHOLD scans (~30 seconds with fast scanning)
33
+ 4. Trigger scanForMeshChannel() to scan ALL channels (1-13)
34
+ 5. If mesh found on different channel:
35
+ a. Update mesh->_meshChannel to detected channel
36
+ b. Restart AP on new channel
37
+ c. Reset consecutiveEmptyScans counter
38
+ 6. Continue normal scanning on new channel
39
+ ```
40
+
41
+ **Safeguards:**
42
+ - Only triggers when WiFi.status() != WL_CONNECTED (not when stably connected)
43
+ - Only triggers when channel > 0 (channel 0 already means auto-detect)
44
+ - Resets counter when mesh nodes are found (prevents false triggers)
45
+
46
+ ### Part 2: Dual-Channel Takeover Announcements
47
+
48
+ Bridge promotion sends takeover announcements on both the old and new channels to ensure all nodes are notified.
49
+
50
+ **Implementation:** `src/arduino/wifi.hpp` in `promoteToBridge()` method
51
+
52
+ **Flow:**
53
+ ```
54
+ 1. Node wins bridge election
55
+ 2. Send takeover announcement on CURRENT channel (e.g., channel 1)
56
+ → Nodes still on channel 1 receive this announcement
57
+ 3. Wait 1 second for announcement to propagate
58
+ 4. Stop mesh and reinitialize as bridge via initAsBridge()
59
+ → Connects to router, detects router's channel (e.g., channel 6)
60
+ → Initializes mesh on channel 6
61
+ 5. Schedule follow-up takeover announcement on NEW channel (channel 6)
62
+ → Sent 3 seconds after initialization
63
+ → Nodes that switched to channel 6 receive this announcement
64
+ ```
65
+
66
+ **Benefits:**
67
+ - Early announcement notifies nodes on old channel before bridge switches
68
+ - Delayed announcement notifies nodes that switched early or quickly found new channel
69
+ - Ensures complete mesh awareness regardless of node timing
70
+
71
+ ## Expected Behavior
72
+
73
+ ### Scenario: Bridge Promotion with Channel Change
74
+
75
+ **Setup:**
76
+ - Node1: Regular mesh node on channel 1
77
+ - Node2: Regular mesh node on channel 1 (weak router signal)
78
+ - Router: Operating on channel 6
79
+
80
+ **Sequence:**
81
+ 1. Node2 starts mesh on channel 1 (default, no router connection)
82
+ 2. Node1 joins mesh on channel 1
83
+ 3. Node1 wins bridge election (better router signal)
84
+ 4. Node1 sends "Becoming bridge" announcement on channel 1
85
+ 5. Node2 receives announcement
86
+ 6. Node1 connects to router on channel 6, initializes mesh on channel 6
87
+ 7. Node1 sends follow-up "I'm the bridge" announcement on channel 6
88
+ 8. Node2 can't find mesh on channel 1 for 6 consecutive scans
89
+ 9. Node2 triggers full channel scan, finds mesh on channel 6
90
+ 10. Node2 updates to channel 6, restarts AP on channel 6
91
+ 11. Node2 reconnects to Node1 on channel 6
92
+ 12. Mesh network is now unified on channel 6
93
+
94
+ **Timeline:**
95
+ - T+0s: Node1 wins election, sends announcement on channel 1
96
+ - T+1s: Node1 stops mesh on channel 1
97
+ - T+2s: Node1 initializes mesh on channel 6
98
+ - T+5s: Node1 sends follow-up announcement on channel 6
99
+ - T+30s: Node2 can't find mesh on channel 1 (fast scanning)
100
+ - T+30s: Node2 triggers channel re-scan
101
+ - T+31s: Node2 finds mesh on channel 6
102
+ - T+32s: Node2 restarts AP on channel 6
103
+ - T+35s: Node2 reconnects to mesh on channel 6
104
+
105
+ ## Configuration
106
+
107
+ ### Adjusting Re-scan Threshold
108
+
109
+ The `EMPTY_SCAN_THRESHOLD` is defined in `src/painlessMeshSTA.h`:
110
+
111
+ ```cpp
112
+ static const uint16_t EMPTY_SCAN_THRESHOLD = 6; // ~30 seconds at default SCAN_INTERVAL
113
+ ```
114
+
115
+ **Trade-offs:**
116
+ - **Lower value** (e.g., 3): Faster channel detection, but more susceptible to false triggers
117
+ - **Higher value** (e.g., 10): More stable, but slower response to channel changes
118
+
119
+ ### Scan Timing
120
+
121
+ Default scan intervals defined in `src/painlessmesh/configuration.hpp`:
122
+
123
+ ```cpp
124
+ #define SCAN_INTERVAL 30 * TASK_SECOND // AP scan period in ms
125
+ ```
126
+
127
+ When no mesh nodes found, scanning switches to fast mode:
128
+ ```cpp
129
+ task.setInterval(0.5 * SCAN_INTERVAL); // 15 seconds
130
+ ```
131
+
132
+ With EMPTY_SCAN_THRESHOLD=6 and fast scanning:
133
+ - Time to trigger re-scan: 6 × 15s = 90 seconds (~1.5 minutes)
134
+
135
+ ## Debugging
136
+
137
+ ### Log Messages
138
+
139
+ **Channel Re-detection:**
140
+ ```
141
+ CONNECTION: connectToAP(): No mesh nodes found for 6 scans, triggering channel re-detection
142
+ CONNECTION: scanForMeshChannel(): Scanning all channels for mesh 'YourMesh'...
143
+ CONNECTION: scanForMeshChannel(): Found mesh on channel 6 (RSSI: -45)
144
+ CONNECTION: connectToAP(): Mesh found on different channel 6 (was 1), updating...
145
+ CONNECTION: connectToAP(): Restarting AP from channel 1 to channel 6
146
+ CONNECTION: connectToAP(): AP restarted on channel 6
147
+ ```
148
+
149
+ **Bridge Promotion:**
150
+ ```
151
+ STARTUP: === Becoming Bridge Node ===
152
+ STARTUP: Sending takeover announcement on current channel before switching...
153
+ STARTUP: ✓ Takeover announcement sent on channel 1
154
+ STARTUP: Step 1: Connecting to router YourRouter...
155
+ STARTUP: ✓ Router connected on channel 6
156
+ STARTUP: Step 2: Initializing mesh on channel 6...
157
+ STARTUP: ✓ Bridge promotion complete on channel 6
158
+ STARTUP: Sending follow-up takeover announcement on new channel 6
159
+ STARTUP: ✓ Follow-up takeover announcement sent
160
+ ```
161
+
162
+ ### Common Issues
163
+
164
+ **Issue:** Nodes don't switch channels
165
+ - **Check:** Ensure `channel=0` in init() for auto-detection
166
+ - **Check:** Verify nodes are not connected via stationManual() to router
167
+ - **Check:** Increase log level to see scanning activity
168
+
169
+ **Issue:** Channel switching takes too long
170
+ - **Solution:** Reduce EMPTY_SCAN_THRESHOLD in painlessMeshSTA.h
171
+ - **Trade-off:** May increase false triggers during temporary network instability
172
+
173
+ **Issue:** Bridge promotion doesn't work
174
+ - **Check:** Verify router credentials are configured
175
+ - **Check:** Ensure router has good signal strength (> -80 dBm)
176
+ - **Check:** Confirm router is on a valid channel (1-13 for 2.4GHz)
177
+
178
+ ## Testing
179
+
180
+ ### Manual Testing Procedure
181
+
182
+ 1. Setup two ESP32/ESP8266 nodes with painlessMesh
183
+ 2. Configure router on channel 6
184
+ 3. Start both nodes without router credentials (will use channel 1)
185
+ 4. Configure one node with router credentials and trigger election
186
+ 5. Monitor serial output for channel switching messages
187
+ 6. Verify both nodes eventually operate on channel 6
188
+ 7. Verify mesh connectivity is maintained
189
+
190
+ ### Expected Results
191
+
192
+ - Bridge node switches to router channel within 5 seconds
193
+ - Non-bridge nodes detect channel change within 90 seconds
194
+ - All nodes reconnect on new channel
195
+ - No loss of mesh connectivity (except during transition)
196
+
197
+ ## Related Files
198
+
199
+ - `src/painlessMeshSTA.h` - StationScan class definition with channel re-detection
200
+ - `src/painlessMeshSTA.cpp` - Channel re-detection implementation
201
+ - `src/arduino/wifi.hpp` - Bridge promotion with dual announcements
202
+ - `test/catch/catch_channel_resync.cpp` - Unit tests for channel synchronization
203
+ - `BRIDGE_TO_INTERNET.md` - Bridge setup documentation
204
+
205
+ ## References
206
+
207
+ - Issue #137: Bridge takeover announcements not heard across channels
208
+ - WiFi channels: 1-13 for 2.4GHz (channels 12-13 restricted in some regions)
209
+ - ESP32/ESP8266 can only operate on one channel at a time in AP+STA mode
@@ -211,6 +211,63 @@ void rtcSyncCompleteCallback(uint32_t timestamp) {
211
211
  - Check NTP server is accessible
212
212
  - Ensure `syncRTCFromNTP()` called with valid timestamp
213
213
 
214
+ ## Time Authority
215
+
216
+ painlessMesh v1.8.12+ includes **time authority** support to prevent nodes from adopting incorrect time from nodes without accurate time sources.
217
+
218
+ ### How It Works
219
+
220
+ Nodes with time authority (RTC or Internet) are prioritized during mesh time synchronization:
221
+ - Nodes **without** time authority will adopt time from nodes **with** time authority
222
+ - Nodes **with** time authority will **NOT** adopt time from nodes without
223
+ - When both nodes have same authority status, existing subnet/node ID logic applies
224
+
225
+ ### Setting Time Authority
226
+
227
+ Time authority is automatically set when:
228
+ - RTC is enabled via `enableRTC()` (time authority = true)
229
+ - RTC is disabled via `disableRTC()` (time authority = false)
230
+
231
+ For bridge nodes with Internet, set time authority manually:
232
+
233
+ ```cpp
234
+ void bridgeStatusCallback(uint32_t bridgeNodeId, bool hasInternet) {
235
+ // If THIS node is the bridge, update time authority
236
+ if (bridgeNodeId == mesh.getNodeId()) {
237
+ if (hasInternet) {
238
+ mesh.setTimeAuthority(true); // Internet available
239
+ } else if (!mesh.hasRTC()) {
240
+ mesh.setTimeAuthority(false); // No Internet and no RTC
241
+ }
242
+ }
243
+ }
244
+ ```
245
+
246
+ ### Checking Time Authority
247
+
248
+ ```cpp
249
+ if (mesh.getTimeAuthority()) {
250
+ Serial.println("This node has authoritative time source");
251
+ }
252
+ ```
253
+
254
+ ### Use Cases
255
+
256
+ **Scenario 1: Mixed RTC nodes**
257
+ - Node A: Has RTC (time authority = true)
258
+ - Node B: No RTC (time authority = false)
259
+ - Result: Node B adopts time from Node A ✓
260
+
261
+ **Scenario 2: Bridge with Internet**
262
+ - Node A: Bridge with Internet (time authority = true)
263
+ - Node B: Regular node (time authority = false)
264
+ - Result: Node B adopts time from bridge ✓
265
+
266
+ **Scenario 3: Network split**
267
+ - Subnet A: All nodes have RTC
268
+ - Subnet B: No nodes have RTC
269
+ - Result: When subnets reconnect, Subnet B adopts from Subnet A ✓
270
+
214
271
  ## Best Practices
215
272
 
216
273
  1. **Always check RTC availability** before relying on timestamps
@@ -218,6 +275,8 @@ void rtcSyncCompleteCallback(uint32_t timestamp) {
218
275
  3. **Monitor battery** on RTC modules for continuous operation
219
276
  4. **Implement fallback** to mesh time if RTC fails
220
277
  5. **Log sync events** for debugging and maintenance
278
+ 6. **Set time authority** for bridge nodes when Internet is available
279
+ 7. **Use RTC on at least one node** in offline deployments for accurate timestamps
221
280
 
222
281
  ## Regulatory Compliance
223
282
 
@@ -160,6 +160,20 @@ void bridgeStatusCallback(uint32_t bridgeNodeId, bool hasInternet) {
160
160
  bridgeNodeId,
161
161
  hasInternet ? "Connected" : "Disconnected");
162
162
 
163
+ // If THIS node is the bridge, set time authority based on Internet availability
164
+ if (bridgeNodeId == mesh.getNodeId()) {
165
+ if (hasInternet) {
166
+ Serial.println("This node has Internet - setting time authority");
167
+ mesh.setTimeAuthority(true);
168
+ } else {
169
+ // Lost Internet - remove time authority if no RTC
170
+ if (!mesh.hasRTC()) {
171
+ Serial.println("Lost Internet and no RTC - removing time authority");
172
+ mesh.setTimeAuthority(false);
173
+ }
174
+ }
175
+ }
176
+
163
177
  if (hasInternet && needsNTPSync && mesh.hasRTC()) {
164
178
  // Internet is available and we need to sync RTC
165
179
  // In a real application, you would get NTP time here
package/library.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/Alteriom/painlessMesh"
8
8
  },
9
- "version": "1.8.11",
9
+ "version": "1.8.13",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=Alteriom PainlessMesh
2
- version=1.8.11
2
+ version=1.8.13
3
3
  author=Coopdis,Scotty Franzyshen,Edwin van Leeuwen,Germán Martín,Maximilian Schwarz,Doanh Doanh,Alteriom
4
4
  maintainer=Alteriom
5
5
  sentence=A painless way to setup a mesh with ESP8266 and ESP32 devices with Alteriom extensions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alteriom/painlessmesh",
3
- "version": "1.8.11",
3
+ "version": "1.8.13",
4
4
  "description": "painlessMesh is a user-friendly library for creating mesh networks with ESP8266 and ESP32 devices. This Alteriom fork includes additional packages for sensor data (SensorPackage), device commands (CommandPackage), and status monitoring (StatusPackage). It handles routing and network management automatically, so you can focus on your application. The library uses JSON-based messaging and syncs time across all nodes, making it ideal for coordinated behaviour like synchronized light displays or sensor networks reporting to a central node.",
5
5
  "keywords": [
6
6
  "arduino",
@@ -51,7 +51,9 @@
51
51
  "dev:prebuild": "git submodule update --init",
52
52
  "validate-library": "./scripts/validate-release.sh",
53
53
  "prepublishOnly": "npm run validate-library",
54
- "postpublish": "echo 'Package published successfully to NPM!'"
54
+ "postpublish": "echo 'Package published successfully to NPM!'",
55
+ "format": "prettier --write \"**/*.{json,md,yml,yaml}\"",
56
+ "format:check": "prettier --check \"**/*.{json,md,yml,yaml}\""
55
57
  },
56
58
  "engines": {
57
59
  "node": ">=14.0.0"
@@ -81,6 +83,8 @@
81
83
  "README.md",
82
84
  "LICENSE",
83
85
  "CHANGELOG.md",
84
- "RELEASE_GUIDE.md"
86
+ "RELEASE_GUIDE.md",
87
+ "DOCUMENTATION_INDEX.md",
88
+ "RELEASE_NOTES_1.8.13.md"
85
89
  ]
86
90
  }
@@ -29,10 +29,10 @@
29
29
  /**
30
30
  * @brief AlteriomPainlessMesh library version information
31
31
  */
32
- #define ALTERIOM_PAINLESS_MESH_VERSION "1.8.11"
32
+ #define ALTERIOM_PAINLESS_MESH_VERSION "1.8.13"
33
33
  #define ALTERIOM_PAINLESS_MESH_VERSION_MAJOR 1
34
34
  #define ALTERIOM_PAINLESS_MESH_VERSION_MINOR 8
35
- #define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 11
35
+ #define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 13
36
36
 
37
37
  /**
38
38
  * @brief Library description and usage information
@@ -89,9 +89,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
89
89
 
90
90
  this->init(nodeId);
91
91
 
92
- // Add bridge election package handler (Type 611)
92
+ // Add bridge election package handler (Type BRIDGE_ELECTION)
93
93
  this->callbackList.onPackage(
94
- 611, // BRIDGE_ELECTION type
94
+ protocol::BRIDGE_ELECTION,
95
95
  [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
96
96
  JsonDocument doc;
97
97
  TSTRING str;
@@ -113,9 +113,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
113
113
  return false; // Don't consume the package
114
114
  });
115
115
 
116
- // Add bridge takeover package handler (Type 612)
116
+ // Add bridge takeover package handler (Type BRIDGE_TAKEOVER)
117
117
  this->callbackList.onPackage(
118
- 612, // BRIDGE_TAKEOVER type
118
+ protocol::BRIDGE_TAKEOVER,
119
119
  [this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
120
120
  JsonDocument doc;
121
121
  TSTRING str;
@@ -817,7 +817,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
817
817
  JsonDocument doc;
818
818
  JsonObject obj = doc.to<JsonObject>();
819
819
 
820
- obj["type"] = 610; // BRIDGE_STATUS type
820
+ obj["type"] = protocol::BRIDGE_STATUS;
821
821
  obj["from"] = this->nodeId;
822
822
  obj["routing"] = 1; // SINGLE routing (direct to node)
823
823
  obj["dest"] = nodeId;
@@ -830,7 +830,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
830
830
  obj["routerChannel"] = WiFi.channel();
831
831
  obj["uptime"] = millis();
832
832
  obj["gatewayIP"] = WiFi.gatewayIP().toString();
833
- obj["message_type"] = 610;
833
+ obj["message_type"] = protocol::BRIDGE_STATUS;
834
834
 
835
835
  String msg;
836
836
  serializeJson(doc, msg);
@@ -838,8 +838,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
838
838
  Log(CONNECTION, "Sending bridge status directly to node %u (Internet: %s)\n",
839
839
  nodeId, hasInternet ? "YES" : "NO");
840
840
 
841
- // Send directly to the new node using routing table (which is now ready)
842
- this->sendSingle(nodeId, msg);
841
+ // Send directly to the new node using raw message to preserve type BRIDGE_STATUS
842
+ // Using sendSingle() would wrap it in type 1 (SINGLE) and hide type BRIDGE_STATUS
843
+ protocol::Variant variant(msg);
844
+ router::send<Connection>(variant, (*this));
843
845
  });
844
846
  });
845
847
 
@@ -1021,6 +1023,29 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1021
1023
  return;
1022
1024
  }
1023
1025
 
1026
+ // CRITICAL: Check if mesh channel re-synchronization is needed first
1027
+ // If we haven't found any mesh nodes and are approaching the re-sync threshold,
1028
+ // prioritize finding the mesh over becoming a bridge. This prevents the scenario
1029
+ // where a node tries to become a bridge when it should be re-syncing to find
1030
+ // the mesh on a different channel (e.g., after another node became bridge and
1031
+ // switched channels to match the router).
1032
+ uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
1033
+ if (emptyScans >= 3 && WiFi.status() != WL_CONNECTED) {
1034
+ Log(CONNECTION,
1035
+ "startBridgeElection(): Mesh connectivity lost (%d empty scans), "
1036
+ "deferring election to allow channel re-sync\n", emptyScans);
1037
+
1038
+ // Schedule a retry after channel re-sync has had a chance to run
1039
+ // The channel re-sync threshold is StationScan::EMPTY_SCAN_THRESHOLD scans (default 6)
1040
+ // Fast scan interval is 0.5 * SCAN_INTERVAL = 15 seconds
1041
+ // Wait for re-sync to complete plus a buffer
1042
+ uint32_t retryDelay = (StationScan::EMPTY_SCAN_THRESHOLD - emptyScans + 2) * 15000;
1043
+ Log(CONNECTION,
1044
+ "startBridgeElection(): Will retry election in %u seconds if still needed\n",
1045
+ retryDelay / 1000);
1046
+ return;
1047
+ }
1048
+
1024
1049
  Log(CONNECTION, "=== Bridge Election Started ===\n");
1025
1050
  electionState = ELECTION_SCANNING;
1026
1051
 
@@ -1049,7 +1074,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1049
1074
  // Broadcast candidacy using JSON directly (avoiding dependency on alteriom package)
1050
1075
  JsonDocument doc;
1051
1076
  JsonObject obj = doc.to<JsonObject>();
1052
- obj["type"] = 611; // BRIDGE_ELECTION
1077
+ obj["type"] = protocol::BRIDGE_ELECTION;
1053
1078
  obj["from"] = this->nodeId;
1054
1079
  obj["routing"] = 2; // BROADCAST
1055
1080
  obj["routerRSSI"] = routerRSSI;
@@ -1057,11 +1082,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1057
1082
  obj["freeMemory"] = ESP.getFreeHeap();
1058
1083
  obj["timestamp"] = this->getNodeTime();
1059
1084
  obj["routerSSID"] = routerSSID;
1060
- obj["message_type"] = 611;
1085
+ obj["message_type"] = protocol::BRIDGE_ELECTION;
1061
1086
 
1062
1087
  String msg;
1063
1088
  serializeJson(doc, msg);
1064
- this->sendBroadcast(msg);
1089
+
1090
+ // Send election message using raw broadcast to preserve type BRIDGE_ELECTION
1091
+ protocol::Variant variant(msg);
1092
+ router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
1065
1093
 
1066
1094
  Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
1067
1095
 
@@ -1195,7 +1223,32 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1195
1223
  auto primaryBridge = this->getPrimaryBridge();
1196
1224
  uint32_t previousBridgeId = primaryBridge ? primaryBridge->nodeId : 0;
1197
1225
 
1198
- // Reconfigure as bridge
1226
+ // IMPORTANT: Send takeover announcement BEFORE switching channels
1227
+ // This ensures other nodes on the current channel receive the announcement
1228
+ Log(STARTUP, "Sending takeover announcement on current channel before switching...\n");
1229
+ JsonDocument doc;
1230
+ JsonObject obj = doc.to<JsonObject>();
1231
+ obj["type"] = protocol::BRIDGE_TAKEOVER;
1232
+ obj["from"] = this->nodeId;
1233
+ obj["routing"] = 2; // BROADCAST
1234
+ obj["previousBridge"] = previousBridgeId;
1235
+ obj["reason"] = "Election winner - best router signal";
1236
+ obj["routerRSSI"] = 0; // Not yet connected to router
1237
+ obj["timestamp"] = this->getNodeTime();
1238
+ obj["message_type"] = protocol::BRIDGE_TAKEOVER;
1239
+
1240
+ String msg;
1241
+ serializeJson(doc, msg);
1242
+
1243
+ // Send takeover message using raw broadcast to preserve type BRIDGE_TAKEOVER
1244
+ protocol::Variant variant(msg);
1245
+ router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
1246
+
1247
+ // Give time for announcement to propagate before channel switch
1248
+ delay(1000);
1249
+ Log(STARTUP, "✓ Takeover announcement sent on channel %d\n", _meshChannel);
1250
+
1251
+ // Now reconfigure as bridge (this will switch to router's channel)
1199
1252
  this->stop();
1200
1253
  delay(1000);
1201
1254
 
@@ -1204,33 +1257,38 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1204
1257
 
1205
1258
  lastRoleChangeTime = millis();
1206
1259
 
1207
- Log(STARTUP, "✓ Bridge promotion complete\n");
1260
+ Log(STARTUP, "✓ Bridge promotion complete on channel %d\n", _meshChannel);
1208
1261
 
1209
1262
  // Notify via callback
1210
1263
  if (bridgeRoleChangedCallback) {
1211
1264
  bridgeRoleChangedCallback(true, "Election winner - best router signal");
1212
1265
  }
1213
1266
 
1214
- // Broadcast takeover announcement
1215
- JsonDocument doc;
1216
- JsonObject obj = doc.to<JsonObject>();
1217
- obj["type"] = 612; // BRIDGE_TAKEOVER
1218
- obj["from"] = this->nodeId;
1219
- obj["routing"] = 2; // BROADCAST
1220
- obj["previousBridge"] = previousBridgeId;
1221
- obj["reason"] = "Election winner - best router signal";
1222
- obj["routerRSSI"] = WiFi.RSSI();
1223
- obj["timestamp"] = this->getNodeTime();
1224
- obj["message_type"] = 612;
1225
-
1226
- String msg;
1227
- serializeJson(doc, msg);
1228
-
1229
- // Small delay to ensure mesh is ready
1230
- delay(2000);
1231
- this->sendBroadcast(msg);
1232
-
1233
- Log(STARTUP, "✓ Takeover announcement sent\n");
1267
+ // Send a follow-up announcement on the new channel
1268
+ // This helps nodes that have already switched channels to discover the new bridge
1269
+ // Schedule it after a delay to ensure mesh is fully initialized
1270
+ this->addTask(3000, TASK_ONCE, [this, previousBridgeId]() {
1271
+ Log(STARTUP, "Sending follow-up takeover announcement on new channel %d\n", _meshChannel);
1272
+ JsonDocument doc2;
1273
+ JsonObject obj2 = doc2.to<JsonObject>();
1274
+ obj2["type"] = protocol::BRIDGE_TAKEOVER;
1275
+ obj2["from"] = this->nodeId;
1276
+ obj2["routing"] = 2; // BROADCAST
1277
+ obj2["previousBridge"] = previousBridgeId;
1278
+ obj2["reason"] = "Election winner - best router signal";
1279
+ obj2["routerRSSI"] = WiFi.RSSI();
1280
+ obj2["timestamp"] = this->getNodeTime();
1281
+ obj2["message_type"] = protocol::BRIDGE_TAKEOVER;
1282
+
1283
+ String msg2;
1284
+ serializeJson(doc2, msg2);
1285
+
1286
+ // Send follow-up takeover using raw broadcast to preserve type BRIDGE_TAKEOVER
1287
+ protocol::Variant variant2(msg2);
1288
+ router::broadcast<protocol::Variant, Connection>(variant2, (*this), 0);
1289
+
1290
+ Log(STARTUP, "✓ Follow-up takeover announcement sent\n");
1291
+ });
1234
1292
  }
1235
1293
 
1236
1294
  /**
@@ -1283,7 +1341,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1283
1341
  JsonDocument doc;
1284
1342
  JsonObject obj = doc.to<JsonObject>();
1285
1343
 
1286
- obj["type"] = 610; // BRIDGE_STATUS type
1344
+ obj["type"] = protocol::BRIDGE_STATUS;
1287
1345
  obj["from"] = this->nodeId;
1288
1346
  obj["routing"] = 2; // BROADCAST routing
1289
1347
  obj["timestamp"] = this->getNodeTime();
@@ -1306,7 +1364,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1306
1364
  obj["routerChannel"] = channel;
1307
1365
  obj["uptime"] = uptime;
1308
1366
  obj["gatewayIP"] = gatewayIP;
1309
- obj["message_type"] = 610;
1367
+ obj["message_type"] = protocol::BRIDGE_STATUS;
1310
1368
 
1311
1369
  String msg;
1312
1370
  serializeJson(doc, msg);
@@ -1321,7 +1379,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1321
1379
  this->updateBridgeStatus(this->nodeId, hasInternet, rssi, channel,
1322
1380
  uptime, gatewayIP, this->getNodeTime());
1323
1381
 
1324
- this->sendBroadcast(msg);
1382
+ // Send bridge status using raw broadcast to preserve type BRIDGE_STATUS
1383
+ // Using sendBroadcast(msg) would wrap it in type 8 (BROADCAST) and hide type BRIDGE_STATUS
1384
+ protocol::Variant variant(msg);
1385
+ router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
1325
1386
  }
1326
1387
  void eventHandleInit() {
1327
1388
  using namespace logger;
@@ -5,8 +5,8 @@
5
5
  * @file painlessMesh.h
6
6
  * @brief Main header file for Alteriom painlessMesh library
7
7
  *
8
- * @version 1.8.11
9
- * @date 2025-11-18
8
+ * @version 1.8.13
9
+ * @date 2025-11-20
10
10
  *
11
11
  * painlessMesh is a user-friendly library for creating mesh networks with
12
12
  * ESP8266 and ESP32 devices. This Alteriom fork includes additional packages
@@ -204,6 +204,50 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
204
204
  bool isRooted = layout::isRooted(mesh->asNodeTree());
205
205
  if (aps.empty()) {
206
206
  // No unknown nodes found
207
+ consecutiveEmptyScans++;
208
+
209
+ // If we've had multiple consecutive empty scans and we're not connected,
210
+ // trigger a full channel re-scan to find the mesh
211
+ if (consecutiveEmptyScans >= EMPTY_SCAN_THRESHOLD &&
212
+ WiFi.status() != WL_CONNECTED &&
213
+ channel > 0) {
214
+ Log(CONNECTION,
215
+ "connectToAP(): No mesh nodes found for %d scans, triggering channel re-detection\n",
216
+ consecutiveEmptyScans);
217
+
218
+ // Perform full channel scan to find the mesh
219
+ uint8_t detectedChannel = scanForMeshChannel(ssid, hidden);
220
+ if (detectedChannel > 0 && detectedChannel != mesh->_meshChannel) {
221
+ Log(CONNECTION,
222
+ "connectToAP(): Mesh found on different channel %d (was %d), updating...\n",
223
+ detectedChannel, mesh->_meshChannel);
224
+
225
+ // Update mesh channel
226
+ uint8_t oldChannel = mesh->_meshChannel;
227
+ mesh->_meshChannel = detectedChannel;
228
+ channel = detectedChannel;
229
+
230
+ // Restart AP on new channel to match the mesh
231
+ // This ensures this node's AP is also discoverable on the correct channel
232
+ if (WiFi.getMode() & WIFI_AP) {
233
+ Log(CONNECTION,
234
+ "connectToAP(): Restarting AP from channel %d to channel %d\n",
235
+ oldChannel, detectedChannel);
236
+ WiFi.softAPdisconnect(false);
237
+ delay(100);
238
+ // Call apInit via friend class access (StationScan is friend of wifi::Mesh)
239
+ mesh->apInit(mesh->getNodeId());
240
+ Log(CONNECTION, "connectToAP(): AP restarted on channel %d\n", detectedChannel);
241
+ }
242
+ } else if (detectedChannel == 0) {
243
+ Log(CONNECTION,
244
+ "connectToAP(): Mesh not found on any channel during re-scan\n");
245
+ }
246
+
247
+ // Reset counter after re-scan attempt
248
+ consecutiveEmptyScans = 0;
249
+ }
250
+
207
251
  if (WiFi.status() == WL_CONNECTED &&
208
252
  !(mesh->shouldContainRoot && !isRooted)) {
209
253
  // if already connected -> scan slow
@@ -220,6 +264,8 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
220
264
  }
221
265
  mesh->stability += min(1000 - mesh->stability, (size_t)25);
222
266
  } else {
267
+ // Reset counter when APs are found
268
+ consecutiveEmptyScans = 0;
223
269
  if (WiFi.status() == WL_CONNECTED) {
224
270
  // TODO: Use %u instead of String() here and below
225
271
  // Also prob is always equal to stability, so we should use that directly
@@ -36,6 +36,16 @@ class StationScan {
36
36
 
37
37
  // Helper to scan all channels for a specific mesh SSID
38
38
  static uint8_t scanForMeshChannel(TSTRING meshSSID, bool meshHidden);
39
+
40
+ // Check if channel re-synchronization is needed or in progress
41
+ bool isChannelResyncNeeded() const {
42
+ return consecutiveEmptyScans >= EMPTY_SCAN_THRESHOLD;
43
+ }
44
+
45
+ // Get the number of consecutive empty scans
46
+ uint16_t getConsecutiveEmptyScans() const {
47
+ return consecutiveEmptyScans;
48
+ }
39
49
 
40
50
  /// Valid APs found during the last scan
41
51
  std::list<WiFi_AP_Record_t> lastAPs;
@@ -54,6 +64,10 @@ class StationScan {
54
64
  // Manually configure network and ip
55
65
  bool manual = false;
56
66
  IPAddress manualIP = IPAddress(0, 0, 0, 0);
67
+
68
+ // Track consecutive scans with no mesh nodes found (for channel re-detection)
69
+ uint16_t consecutiveEmptyScans = 0;
70
+ static const uint16_t EMPTY_SCAN_THRESHOLD = 6; // ~30 seconds at default SCAN_INTERVAL
57
71
 
58
72
  friend painlessMesh;
59
73
  };
@@ -52,7 +52,7 @@ class Layout {
52
52
  bool isRoot() { return root; }
53
53
 
54
54
  protocol::NodeTree asNodeTree() {
55
- auto nt = protocol::NodeTree(nodeId, root);
55
+ auto nt = protocol::NodeTree(nodeId, root, hasTimeAuthority);
56
56
  for (auto&& s : subs) {
57
57
  if (s->nodeId == 0) continue;
58
58
  nt.subs.push_back(protocol::NodeTree(*s));
@@ -63,6 +63,7 @@ class Layout {
63
63
  protected:
64
64
  uint32_t nodeId = 0;
65
65
  bool root = false;
66
+ bool hasTimeAuthority = false;
66
67
  };
67
68
 
68
69
  template <class T>
@@ -114,6 +115,7 @@ class Neighbour : public protocol::NodeTree {
114
115
  nodeId = tree.nodeId;
115
116
  subs = tree.subs;
116
117
  root = tree.root;
118
+ hasTimeAuthority = tree.hasTimeAuthority;
117
119
  return true;
118
120
  }
119
121
  return false;
@@ -124,8 +126,10 @@ class Neighbour : public protocol::NodeTree {
124
126
  */
125
127
  protocol::NodeSyncRequest request(NodeTree&& layout) {
126
128
  auto subTree = excludeRoute(std::move(layout), nodeId);
127
- return protocol::NodeSyncRequest(subTree.nodeId, nodeId, subTree.subs,
129
+ auto req = protocol::NodeSyncRequest(subTree.nodeId, nodeId, subTree.subs,
128
130
  subTree.root);
131
+ req.hasTimeAuthority = subTree.hasTimeAuthority;
132
+ return req;
129
133
  }
130
134
 
131
135
  /**
@@ -167,10 +167,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
167
167
  this->callbackList = painlessmesh::router::addPackageCallback(
168
168
  std::move(this->callbackList), (*this));
169
169
 
170
- // Add bridge status package handler (Type 610)
170
+ // Add bridge status package handler (Type BRIDGE_STATUS)
171
171
  // This will be called when any node receives a bridge status broadcast
172
172
  this->callbackList.onPackage(
173
- 610, // BRIDGE_STATUS type
173
+ protocol::BRIDGE_STATUS,
174
174
  [this](protocol::Variant& variant, std::shared_ptr<T>, uint32_t) {
175
175
  // We need to manually parse the JSON since BridgeStatusPackage is in alteriom namespace
176
176
  // and may not be available in all contexts. We'll parse the critical fields directly.
@@ -750,7 +750,12 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
750
750
  bool enableRTC(rtc::RTCInterface* rtcInterface) {
751
751
  using namespace logger;
752
752
  Log(GENERAL, "enableRTC(): Initializing RTC\n");
753
- return rtcManager.enable(rtcInterface);
753
+ bool success = rtcManager.enable(rtcInterface);
754
+ if (success) {
755
+ // RTC enabled successfully - mark node as having time authority
756
+ setTimeAuthority(true);
757
+ }
758
+ return success;
754
759
  }
755
760
 
756
761
  /**
@@ -760,6 +765,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
760
765
  using namespace logger;
761
766
  Log(GENERAL, "disableRTC(): Disabling RTC\n");
762
767
  rtcManager.disable();
768
+ // RTC disabled - remove time authority if no other source
769
+ // Note: Bridge nodes may still have time authority from Internet
770
+ setTimeAuthority(false);
763
771
  }
764
772
 
765
773
  /**
@@ -864,6 +872,50 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
864
872
  rtcSyncCompleteCallback = onRTCSyncComplete;
865
873
  }
866
874
 
875
+ /**
876
+ * Set time authority status for this node
877
+ *
878
+ * Nodes with time authority (RTC or Internet) are preferred as time sources
879
+ * during mesh time synchronization. This prevents nodes from adopting time
880
+ * from nodes without accurate time sources.
881
+ *
882
+ * This is automatically set to true when:
883
+ * - RTC is enabled via enableRTC()
884
+ * - Bridge has Internet connectivity
885
+ *
886
+ * \code
887
+ * // Manual control (advanced usage)
888
+ * mesh.setTimeAuthority(true); // Mark as authoritative time source
889
+ * mesh.setTimeAuthority(false); // Mark as non-authoritative
890
+ * \endcode
891
+ *
892
+ * @param hasAuthority True if node has accurate time source (RTC/Internet)
893
+ */
894
+ void setTimeAuthority(bool hasAuthority) {
895
+ using namespace logger;
896
+ if (this->hasTimeAuthority != hasAuthority) {
897
+ this->hasTimeAuthority = hasAuthority;
898
+ Log(GENERAL, "setTimeAuthority(): Time authority %s\n",
899
+ hasAuthority ? "enabled" : "disabled");
900
+
901
+ // Trigger time sync with all connections to propagate authority status
902
+ for (auto&& connection : this->subs) {
903
+ if (connection->nodeId != 0) {
904
+ connection->timeSyncTask.forceNextIteration();
905
+ }
906
+ }
907
+ }
908
+ }
909
+
910
+ /**
911
+ * Check if this node has time authority
912
+ *
913
+ * @return True if node has RTC or Internet time source
914
+ */
915
+ bool getTimeAuthority() const {
916
+ return this->hasTimeAuthority;
917
+ }
918
+
867
919
  //
868
920
  // Message Queue API
869
921
  //
@@ -76,6 +76,22 @@ inline int32_t tripDelay(uint32_t time0, uint32_t time1, uint32_t time2,
76
76
  }
77
77
 
78
78
  inline bool adopt(protocol::NodeTree mesh, protocol::NodeTree connection) {
79
+ // Prioritize nodes with time authority (RTC or Internet)
80
+ // Only adopt from nodes with time authority if we don't have one
81
+ if (!mesh.hasTimeAuthority && connection.hasTimeAuthority) {
82
+ Log(logger::S_TIME, "adopt(): Adopting from %u (has time authority)\n",
83
+ connection.nodeId);
84
+ return true;
85
+ }
86
+
87
+ // Don't adopt from nodes without time authority if we have one
88
+ if (mesh.hasTimeAuthority && !connection.hasTimeAuthority) {
89
+ Log(logger::S_TIME, "adopt(): Not adopting from %u (no time authority)\n",
90
+ connection.nodeId);
91
+ return false;
92
+ }
93
+
94
+ // If both have same time authority status, use existing logic
79
95
  auto mySubCount =
80
96
  layout::size(layout::excludeRoute(std::move(mesh), connection.nodeId));
81
97
  auto remoteSubCount = layout::size(connection);
@@ -49,6 +49,13 @@ enum TimeType {
49
49
  TIME_REPLY
50
50
  };
51
51
 
52
+ // Bridge protocol package types
53
+ // These are used for bridge discovery, election, and coordination
54
+ constexpr int BRIDGE_STATUS = 610; // Bridge status broadcast (internet, RSSI, etc.)
55
+ constexpr int BRIDGE_ELECTION = 611; // Bridge election candidacy announcement
56
+ constexpr int BRIDGE_TAKEOVER = 612; // Bridge takeover notification
57
+ constexpr int BRIDGE_COORDINATION = 613; // Multi-bridge coordination (defined in plugin.hpp)
58
+
52
59
  class PackageInterface {
53
60
  public:
54
61
  virtual JsonObject addTo(JsonObject&& jsonObj) const = 0;
@@ -123,23 +130,27 @@ class NodeTree : public PackageInterface {
123
130
  public:
124
131
  uint32_t nodeId = 0;
125
132
  bool root = false;
133
+ bool hasTimeAuthority = false; // True if node has RTC or Internet time source
126
134
  std::list<NodeTree> subs;
127
135
 
128
136
  NodeTree() {}
129
137
  virtual ~NodeTree() = default; // Add virtual destructor
130
138
 
131
- NodeTree(uint32_t nodeID, bool iAmRoot) {
139
+ NodeTree(uint32_t nodeID, bool iAmRoot, bool timeAuthority = false) {
132
140
  nodeId = nodeID;
133
141
  root = iAmRoot;
142
+ hasTimeAuthority = timeAuthority;
134
143
  }
135
144
 
136
145
  NodeTree(JsonObject jsonObj) {
137
146
  #if ARDUINOJSON_VERSION_MAJOR < 7
138
147
  if (jsonObj.containsKey("root")) root = jsonObj["root"].as<bool>();
148
+ if (jsonObj.containsKey("hasTimeAuthority")) hasTimeAuthority = jsonObj["hasTimeAuthority"].as<bool>();
139
149
  if (jsonObj.containsKey("nodeId"))
140
150
  #else
141
151
 
142
152
  if (jsonObj["root"].is<bool>()) root = jsonObj["root"].as<bool>();
153
+ if (jsonObj["hasTimeAuthority"].is<bool>()) hasTimeAuthority = jsonObj["hasTimeAuthority"].as<bool>();
143
154
  if (jsonObj["nodeId"].is<uint32_t>())
144
155
  #endif
145
156
  nodeId = jsonObj["nodeId"].as<uint32_t>();
@@ -161,6 +172,7 @@ class NodeTree : public PackageInterface {
161
172
  JsonObject addTo(JsonObject&& jsonObj) const {
162
173
  jsonObj["nodeId"] = nodeId;
163
174
  if (root) jsonObj["root"] = root;
175
+ if (hasTimeAuthority) jsonObj["hasTimeAuthority"] = hasTimeAuthority;
164
176
  if (subs.size() > 0) {
165
177
  #if ARDUINOJSON_VERSION_MAJOR == 7
166
178
  JsonArray subsArr = jsonObj["subs"].to<JsonArray>();
@@ -181,6 +193,7 @@ class NodeTree : public PackageInterface {
181
193
 
182
194
  bool operator==(const NodeTree& b) const {
183
195
  if (!(this->nodeId == b.nodeId && this->root == b.root &&
196
+ this->hasTimeAuthority == b.hasTimeAuthority &&
184
197
  this->subs.size() == b.subs.size()))
185
198
  return false;
186
199
  auto itA = this->subs.begin();
@@ -203,6 +216,7 @@ class NodeTree : public PackageInterface {
203
216
  size_t jsonObjectSize() const {
204
217
  size_t base = 1;
205
218
  if (root) ++base;
219
+ if (hasTimeAuthority) ++base;
206
220
  if (subs.size() > 0) ++base;
207
221
  size_t size = JSON_OBJECT_SIZE(base);
208
222
  if (subs.size() > 0) size += JSON_ARRAY_SIZE(subs.size());
@@ -215,6 +229,7 @@ class NodeTree : public PackageInterface {
215
229
  nodeId = 0;
216
230
  subs.clear();
217
231
  root = false;
232
+ hasTimeAuthority = false;
218
233
  }
219
234
  };
220
235
 
@@ -263,6 +278,7 @@ class NodeSyncRequest : public NodeTree {
263
278
  size_t jsonObjectSize() const {
264
279
  size_t base = 4;
265
280
  if (root) ++base;
281
+ if (hasTimeAuthority) ++base;
266
282
  if (subs.size() > 0) ++base;
267
283
  size_t size = JSON_OBJECT_SIZE(base);
268
284
  if (subs.size() > 0) size += JSON_ARRAY_SIZE(subs.size());