@alteriom/painlessmesh 1.8.11 → 1.8.12
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/RELEASE_NOTES_1.8.12.md +96 -0
- 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 +68 -22
- 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 +53 -1
- package/src/painlessmesh/ntp.hpp +16 -0
- package/src/painlessmesh/protocol.hpp +10 -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,96 @@
|
|
|
1
|
+
# Release Notes - Version 1.8.12
|
|
2
|
+
|
|
3
|
+
**Release Date:** 2025-11-19
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Version 1.8.12 is a maintenance release focused on documentation improvements and code quality enhancements. This release includes changes from PRs #152 and #153, ensuring comprehensive documentation coverage and adherence to code formatting standards.
|
|
8
|
+
|
|
9
|
+
## What's New
|
|
10
|
+
|
|
11
|
+
### Documentation Enhancements
|
|
12
|
+
|
|
13
|
+
- **Comprehensive Documentation Review** - All documentation has been reviewed and updated to reflect the current state of the library
|
|
14
|
+
- **API Reference Updates** - Improved clarity and completeness in API documentation
|
|
15
|
+
- **Example Code Documentation** - Enhanced inline documentation for example projects
|
|
16
|
+
- **Package Documentation** - Updated documentation for all Alteriom custom packages
|
|
17
|
+
|
|
18
|
+
### Code Quality Improvements
|
|
19
|
+
|
|
20
|
+
- **Prettier Configuration** - Added prettier configuration for consistent JSON and Markdown formatting
|
|
21
|
+
- New `.prettierrc` configuration file
|
|
22
|
+
- New `.prettierignore` file to exclude build artifacts and dependencies
|
|
23
|
+
- Added `npm run format` and `npm run format:check` scripts
|
|
24
|
+
- **Clang-Format Compliance** - Ensured all C++ code follows established formatting standards
|
|
25
|
+
- **Code Organization** - Improved code structure and maintainability
|
|
26
|
+
|
|
27
|
+
### Version Updates
|
|
28
|
+
|
|
29
|
+
- Updated all version references across:
|
|
30
|
+
- `package.json` - NPM package version
|
|
31
|
+
- `library.json` - PlatformIO library version
|
|
32
|
+
- `library.properties` - Arduino library version
|
|
33
|
+
- `src/painlessMesh.h` - Header file version comments
|
|
34
|
+
- `src/AlteriomPainlessMesh.h` - Version defines and comments
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
### Arduino Library Manager
|
|
39
|
+
```
|
|
40
|
+
Sketch → Include Library → Manage Libraries → Search "AlteriomPainlessMesh" → Install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### PlatformIO
|
|
44
|
+
```ini
|
|
45
|
+
[env:myenv]
|
|
46
|
+
lib_deps =
|
|
47
|
+
alteriom/AlteriomPainlessMesh @ ^1.8.12
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### NPM
|
|
51
|
+
```bash
|
|
52
|
+
npm install @alteriom/painlessmesh@1.8.12
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Compatibility
|
|
56
|
+
|
|
57
|
+
- **ESP32** - All variants supported
|
|
58
|
+
- **ESP8266** - All variants supported
|
|
59
|
+
- **Arduino IDE** - Version 1.8.0 or higher
|
|
60
|
+
- **PlatformIO** - Latest version
|
|
61
|
+
- **ArduinoJson** - Version 7.4.2 or higher
|
|
62
|
+
- **TaskScheduler** - Version 4.0.0 or higher
|
|
63
|
+
|
|
64
|
+
## Migration Guide
|
|
65
|
+
|
|
66
|
+
This is a patch release with no breaking changes. Users can upgrade from any 1.8.x version without code modifications.
|
|
67
|
+
|
|
68
|
+
## Known Issues
|
|
69
|
+
|
|
70
|
+
None identified in this release.
|
|
71
|
+
|
|
72
|
+
## Pull Requests Included
|
|
73
|
+
|
|
74
|
+
- PR #152 - Documentation improvements
|
|
75
|
+
- PR #153 - Code quality enhancements
|
|
76
|
+
|
|
77
|
+
## Contributors
|
|
78
|
+
|
|
79
|
+
Special thanks to all contributors who helped make this release possible!
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
- **Main Documentation**: https://alteriom.github.io/painlessMesh/
|
|
84
|
+
- **API Reference**: https://alteriom.github.io/painlessMesh/#/api/doxygen
|
|
85
|
+
- **Examples**: https://alteriom.github.io/painlessMesh/#/tutorials/basic-examples
|
|
86
|
+
- **GitHub Repository**: https://github.com/Alteriom/painlessMesh
|
|
87
|
+
|
|
88
|
+
## Support
|
|
89
|
+
|
|
90
|
+
For questions, issues, or feature requests:
|
|
91
|
+
- **GitHub Issues**: https://github.com/Alteriom/painlessMesh/issues
|
|
92
|
+
- **Documentation**: https://alteriom.github.io/painlessMesh/
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
This project is licensed under LGPL-3.0. See the LICENSE file for details.
|
|
@@ -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.12
|
|
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.12",
|
|
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.12.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.12"
|
|
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 12
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @brief Library description and usage information
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -1021,6 +1021,29 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1021
1021
|
return;
|
|
1022
1022
|
}
|
|
1023
1023
|
|
|
1024
|
+
// CRITICAL: Check if mesh channel re-synchronization is needed first
|
|
1025
|
+
// If we haven't found any mesh nodes and are approaching the re-sync threshold,
|
|
1026
|
+
// prioritize finding the mesh over becoming a bridge. This prevents the scenario
|
|
1027
|
+
// where a node tries to become a bridge when it should be re-syncing to find
|
|
1028
|
+
// the mesh on a different channel (e.g., after another node became bridge and
|
|
1029
|
+
// switched channels to match the router).
|
|
1030
|
+
uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
|
|
1031
|
+
if (emptyScans >= 3 && WiFi.status() != WL_CONNECTED) {
|
|
1032
|
+
Log(CONNECTION,
|
|
1033
|
+
"startBridgeElection(): Mesh connectivity lost (%d empty scans), "
|
|
1034
|
+
"deferring election to allow channel re-sync\n", emptyScans);
|
|
1035
|
+
|
|
1036
|
+
// Schedule a retry after channel re-sync has had a chance to run
|
|
1037
|
+
// The channel re-sync threshold is StationScan::EMPTY_SCAN_THRESHOLD scans (default 6)
|
|
1038
|
+
// Fast scan interval is 0.5 * SCAN_INTERVAL = 15 seconds
|
|
1039
|
+
// Wait for re-sync to complete plus a buffer
|
|
1040
|
+
uint32_t retryDelay = (StationScan::EMPTY_SCAN_THRESHOLD - emptyScans + 2) * 15000;
|
|
1041
|
+
Log(CONNECTION,
|
|
1042
|
+
"startBridgeElection(): Will retry election in %u seconds if still needed\n",
|
|
1043
|
+
retryDelay / 1000);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1024
1047
|
Log(CONNECTION, "=== Bridge Election Started ===\n");
|
|
1025
1048
|
electionState = ELECTION_SCANNING;
|
|
1026
1049
|
|
|
@@ -1195,23 +1218,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1195
1218
|
auto primaryBridge = this->getPrimaryBridge();
|
|
1196
1219
|
uint32_t previousBridgeId = primaryBridge ? primaryBridge->nodeId : 0;
|
|
1197
1220
|
|
|
1198
|
-
//
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1203
|
-
mScheduler, _meshPort);
|
|
1204
|
-
|
|
1205
|
-
lastRoleChangeTime = millis();
|
|
1206
|
-
|
|
1207
|
-
Log(STARTUP, "✓ Bridge promotion complete\n");
|
|
1208
|
-
|
|
1209
|
-
// Notify via callback
|
|
1210
|
-
if (bridgeRoleChangedCallback) {
|
|
1211
|
-
bridgeRoleChangedCallback(true, "Election winner - best router signal");
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Broadcast takeover announcement
|
|
1221
|
+
// IMPORTANT: Send takeover announcement BEFORE switching channels
|
|
1222
|
+
// This ensures other nodes on the current channel receive the announcement
|
|
1223
|
+
Log(STARTUP, "Sending takeover announcement on current channel before switching...\n");
|
|
1215
1224
|
JsonDocument doc;
|
|
1216
1225
|
JsonObject obj = doc.to<JsonObject>();
|
|
1217
1226
|
obj["type"] = 612; // BRIDGE_TAKEOVER
|
|
@@ -1219,18 +1228,55 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1219
1228
|
obj["routing"] = 2; // BROADCAST
|
|
1220
1229
|
obj["previousBridge"] = previousBridgeId;
|
|
1221
1230
|
obj["reason"] = "Election winner - best router signal";
|
|
1222
|
-
obj["routerRSSI"] =
|
|
1231
|
+
obj["routerRSSI"] = 0; // Not yet connected to router
|
|
1223
1232
|
obj["timestamp"] = this->getNodeTime();
|
|
1224
1233
|
obj["message_type"] = 612;
|
|
1225
1234
|
|
|
1226
1235
|
String msg;
|
|
1227
1236
|
serializeJson(doc, msg);
|
|
1228
|
-
|
|
1229
|
-
// Small delay to ensure mesh is ready
|
|
1230
|
-
delay(2000);
|
|
1231
1237
|
this->sendBroadcast(msg);
|
|
1232
1238
|
|
|
1233
|
-
|
|
1239
|
+
// Give time for announcement to propagate before channel switch
|
|
1240
|
+
delay(1000);
|
|
1241
|
+
Log(STARTUP, "✓ Takeover announcement sent on channel %d\n", _meshChannel);
|
|
1242
|
+
|
|
1243
|
+
// Now reconfigure as bridge (this will switch to router's channel)
|
|
1244
|
+
this->stop();
|
|
1245
|
+
delay(1000);
|
|
1246
|
+
|
|
1247
|
+
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1248
|
+
mScheduler, _meshPort);
|
|
1249
|
+
|
|
1250
|
+
lastRoleChangeTime = millis();
|
|
1251
|
+
|
|
1252
|
+
Log(STARTUP, "✓ Bridge promotion complete on channel %d\n", _meshChannel);
|
|
1253
|
+
|
|
1254
|
+
// Notify via callback
|
|
1255
|
+
if (bridgeRoleChangedCallback) {
|
|
1256
|
+
bridgeRoleChangedCallback(true, "Election winner - best router signal");
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Send a follow-up announcement on the new channel
|
|
1260
|
+
// This helps nodes that have already switched channels to discover the new bridge
|
|
1261
|
+
// Schedule it after a delay to ensure mesh is fully initialized
|
|
1262
|
+
this->addTask(3000, TASK_ONCE, [this, previousBridgeId]() {
|
|
1263
|
+
Log(STARTUP, "Sending follow-up takeover announcement on new channel %d\n", _meshChannel);
|
|
1264
|
+
JsonDocument doc2;
|
|
1265
|
+
JsonObject obj2 = doc2.to<JsonObject>();
|
|
1266
|
+
obj2["type"] = 612; // BRIDGE_TAKEOVER
|
|
1267
|
+
obj2["from"] = this->nodeId;
|
|
1268
|
+
obj2["routing"] = 2; // BROADCAST
|
|
1269
|
+
obj2["previousBridge"] = previousBridgeId;
|
|
1270
|
+
obj2["reason"] = "Election winner - best router signal";
|
|
1271
|
+
obj2["routerRSSI"] = WiFi.RSSI();
|
|
1272
|
+
obj2["timestamp"] = this->getNodeTime();
|
|
1273
|
+
obj2["message_type"] = 612;
|
|
1274
|
+
|
|
1275
|
+
String msg2;
|
|
1276
|
+
serializeJson(doc2, msg2);
|
|
1277
|
+
this->sendBroadcast(msg2);
|
|
1278
|
+
Log(STARTUP, "✓ Follow-up takeover announcement sent\n");
|
|
1279
|
+
});
|
|
1234
1280
|
}
|
|
1235
1281
|
|
|
1236
1282
|
/**
|
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.12
|
|
9
|
+
* @date 2025-11-19
|
|
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
|
/**
|
|
@@ -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);
|
|
@@ -123,23 +123,27 @@ class NodeTree : public PackageInterface {
|
|
|
123
123
|
public:
|
|
124
124
|
uint32_t nodeId = 0;
|
|
125
125
|
bool root = false;
|
|
126
|
+
bool hasTimeAuthority = false; // True if node has RTC or Internet time source
|
|
126
127
|
std::list<NodeTree> subs;
|
|
127
128
|
|
|
128
129
|
NodeTree() {}
|
|
129
130
|
virtual ~NodeTree() = default; // Add virtual destructor
|
|
130
131
|
|
|
131
|
-
NodeTree(uint32_t nodeID, bool iAmRoot) {
|
|
132
|
+
NodeTree(uint32_t nodeID, bool iAmRoot, bool timeAuthority = false) {
|
|
132
133
|
nodeId = nodeID;
|
|
133
134
|
root = iAmRoot;
|
|
135
|
+
hasTimeAuthority = timeAuthority;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
NodeTree(JsonObject jsonObj) {
|
|
137
139
|
#if ARDUINOJSON_VERSION_MAJOR < 7
|
|
138
140
|
if (jsonObj.containsKey("root")) root = jsonObj["root"].as<bool>();
|
|
141
|
+
if (jsonObj.containsKey("hasTimeAuthority")) hasTimeAuthority = jsonObj["hasTimeAuthority"].as<bool>();
|
|
139
142
|
if (jsonObj.containsKey("nodeId"))
|
|
140
143
|
#else
|
|
141
144
|
|
|
142
145
|
if (jsonObj["root"].is<bool>()) root = jsonObj["root"].as<bool>();
|
|
146
|
+
if (jsonObj["hasTimeAuthority"].is<bool>()) hasTimeAuthority = jsonObj["hasTimeAuthority"].as<bool>();
|
|
143
147
|
if (jsonObj["nodeId"].is<uint32_t>())
|
|
144
148
|
#endif
|
|
145
149
|
nodeId = jsonObj["nodeId"].as<uint32_t>();
|
|
@@ -161,6 +165,7 @@ class NodeTree : public PackageInterface {
|
|
|
161
165
|
JsonObject addTo(JsonObject&& jsonObj) const {
|
|
162
166
|
jsonObj["nodeId"] = nodeId;
|
|
163
167
|
if (root) jsonObj["root"] = root;
|
|
168
|
+
if (hasTimeAuthority) jsonObj["hasTimeAuthority"] = hasTimeAuthority;
|
|
164
169
|
if (subs.size() > 0) {
|
|
165
170
|
#if ARDUINOJSON_VERSION_MAJOR == 7
|
|
166
171
|
JsonArray subsArr = jsonObj["subs"].to<JsonArray>();
|
|
@@ -181,6 +186,7 @@ class NodeTree : public PackageInterface {
|
|
|
181
186
|
|
|
182
187
|
bool operator==(const NodeTree& b) const {
|
|
183
188
|
if (!(this->nodeId == b.nodeId && this->root == b.root &&
|
|
189
|
+
this->hasTimeAuthority == b.hasTimeAuthority &&
|
|
184
190
|
this->subs.size() == b.subs.size()))
|
|
185
191
|
return false;
|
|
186
192
|
auto itA = this->subs.begin();
|
|
@@ -203,6 +209,7 @@ class NodeTree : public PackageInterface {
|
|
|
203
209
|
size_t jsonObjectSize() const {
|
|
204
210
|
size_t base = 1;
|
|
205
211
|
if (root) ++base;
|
|
212
|
+
if (hasTimeAuthority) ++base;
|
|
206
213
|
if (subs.size() > 0) ++base;
|
|
207
214
|
size_t size = JSON_OBJECT_SIZE(base);
|
|
208
215
|
if (subs.size() > 0) size += JSON_ARRAY_SIZE(subs.size());
|
|
@@ -215,6 +222,7 @@ class NodeTree : public PackageInterface {
|
|
|
215
222
|
nodeId = 0;
|
|
216
223
|
subs.clear();
|
|
217
224
|
root = false;
|
|
225
|
+
hasTimeAuthority = false;
|
|
218
226
|
}
|
|
219
227
|
};
|
|
220
228
|
|
|
@@ -263,6 +271,7 @@ class NodeSyncRequest : public NodeTree {
|
|
|
263
271
|
size_t jsonObjectSize() const {
|
|
264
272
|
size_t base = 4;
|
|
265
273
|
if (root) ++base;
|
|
274
|
+
if (hasTimeAuthority) ++base;
|
|
266
275
|
if (subs.size() > 0) ++base;
|
|
267
276
|
size_t size = JSON_OBJECT_SIZE(base);
|
|
268
277
|
if (subs.size() > 0) size += JSON_ARRAY_SIZE(subs.size());
|