@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 +18 -0
- package/DOCUMENTATION_INDEX.md +146 -0
- package/README.md +1 -1
- package/docs/CHANNEL_SYNCHRONIZATION.md +209 -0
- package/examples/rtcIntegration/README.md +59 -0
- package/examples/rtcIntegration/rtcIntegration.ino +14 -0
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +7 -3
- package/src/AlteriomPainlessMesh.h +2 -2
- package/src/arduino/wifi.hpp +97 -36
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +46 -0
- package/src/painlessMeshSTA.h +14 -0
- package/src/painlessmesh/layout.hpp +6 -2
- package/src/painlessmesh/mesh.hpp +55 -3
- package/src/painlessmesh/ntp.hpp +16 -0
- package/src/painlessmesh/protocol.hpp +17 -1
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
package/library.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name=Alteriom PainlessMesh
|
|
2
|
-
version=1.8.
|
|
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.
|
|
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.
|
|
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
|
|
35
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 13
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @brief Library description and usage information
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -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
|
|
92
|
+
// Add bridge election package handler (Type BRIDGE_ELECTION)
|
|
93
93
|
this->callbackList.onPackage(
|
|
94
|
-
|
|
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
|
|
116
|
+
// Add bridge takeover package handler (Type BRIDGE_TAKEOVER)
|
|
117
117
|
this->callbackList.onPackage(
|
|
118
|
-
|
|
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"] =
|
|
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"] =
|
|
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
|
|
842
|
-
|
|
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"] =
|
|
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"] =
|
|
1085
|
+
obj["message_type"] = protocol::BRIDGE_ELECTION;
|
|
1061
1086
|
|
|
1062
1087
|
String msg;
|
|
1063
1088
|
serializeJson(doc, msg);
|
|
1064
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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"] =
|
|
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"] =
|
|
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
|
-
|
|
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;
|
package/src/painlessMesh.h
CHANGED
|
@@ -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.
|
|
9
|
-
* @date 2025-11-
|
|
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
|
package/src/painlessMeshSTA.cpp
CHANGED
|
@@ -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
|
package/src/painlessMeshSTA.h
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
//
|
package/src/painlessmesh/ntp.hpp
CHANGED
|
@@ -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());
|