@alteriom/painlessmesh 1.8.10 → 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 +71 -12
- 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/docs/releases/RELEASE_CHECKLIST_1.8.10.md +331 -0
- package/docs/releases/RELEASE_CHECKLIST_1.8.9.md +207 -0
- package/docs/releases/RELEASE_NOTES_1.8.10.md +239 -0
- package/docs/releases/RELEASE_NOTES_1.8.9.md +213 -0
- package/docs/releases/RELEASE_SUMMARY_1.8.10.md +193 -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 +73 -26
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +46 -0
- package/src/painlessMeshSTA.h +14 -0
- package/src/painlessmesh/buffer.hpp +3 -3
- package/src/painlessmesh/layout.hpp +6 -2
- package/src/painlessmesh/mesh.hpp +61 -5
- package/src/painlessmesh/message_queue.hpp +3 -3
- package/src/painlessmesh/ntp.hpp +18 -2
- package/src/painlessmesh/protocol.hpp +10 -1
- package/src/painlessmesh/router.hpp +4 -4
|
@@ -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
|
@@ -806,10 +806,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
806
806
|
|
|
807
807
|
// Also send bridge status when new nodes connect so they can discover the bridge immediately
|
|
808
808
|
// Send directly to the new node to ensure delivery, independent of time sync
|
|
809
|
-
|
|
810
|
-
|
|
809
|
+
// Using changedConnectionCallbacks instead of newConnectionCallbacks ensures routing is ready
|
|
810
|
+
this->changedConnectionCallbacks.push_back([this](uint32_t nodeId) {
|
|
811
|
+
Log(CONNECTION, "Node %u connection changed, sending bridge status directly\n", nodeId);
|
|
811
812
|
|
|
812
|
-
// Small delay to ensure connection is
|
|
813
|
+
// Small delay to ensure connection is fully stable, then send directly to the new node
|
|
813
814
|
// This avoids issues with time sync blocking broadcast messages
|
|
814
815
|
this->addTask(500, TASK_ONCE, [this, nodeId]() {
|
|
815
816
|
// Create bridge status message
|
|
@@ -837,7 +838,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
837
838
|
Log(CONNECTION, "Sending bridge status directly to node %u (Internet: %s)\n",
|
|
838
839
|
nodeId, hasInternet ? "YES" : "NO");
|
|
839
840
|
|
|
840
|
-
// Send directly to the new node
|
|
841
|
+
// Send directly to the new node using routing table (which is now ready)
|
|
841
842
|
this->sendSingle(nodeId, msg);
|
|
842
843
|
});
|
|
843
844
|
});
|
|
@@ -1020,6 +1021,29 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1020
1021
|
return;
|
|
1021
1022
|
}
|
|
1022
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
|
+
|
|
1023
1047
|
Log(CONNECTION, "=== Bridge Election Started ===\n");
|
|
1024
1048
|
electionState = ELECTION_SCANNING;
|
|
1025
1049
|
|
|
@@ -1194,23 +1218,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1194
1218
|
auto primaryBridge = this->getPrimaryBridge();
|
|
1195
1219
|
uint32_t previousBridgeId = primaryBridge ? primaryBridge->nodeId : 0;
|
|
1196
1220
|
|
|
1197
|
-
//
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1202
|
-
mScheduler, _meshPort);
|
|
1203
|
-
|
|
1204
|
-
lastRoleChangeTime = millis();
|
|
1205
|
-
|
|
1206
|
-
Log(STARTUP, "✓ Bridge promotion complete\n");
|
|
1207
|
-
|
|
1208
|
-
// Notify via callback
|
|
1209
|
-
if (bridgeRoleChangedCallback) {
|
|
1210
|
-
bridgeRoleChangedCallback(true, "Election winner - best router signal");
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
// 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");
|
|
1214
1224
|
JsonDocument doc;
|
|
1215
1225
|
JsonObject obj = doc.to<JsonObject>();
|
|
1216
1226
|
obj["type"] = 612; // BRIDGE_TAKEOVER
|
|
@@ -1218,18 +1228,55 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1218
1228
|
obj["routing"] = 2; // BROADCAST
|
|
1219
1229
|
obj["previousBridge"] = previousBridgeId;
|
|
1220
1230
|
obj["reason"] = "Election winner - best router signal";
|
|
1221
|
-
obj["routerRSSI"] =
|
|
1231
|
+
obj["routerRSSI"] = 0; // Not yet connected to router
|
|
1222
1232
|
obj["timestamp"] = this->getNodeTime();
|
|
1223
1233
|
obj["message_type"] = 612;
|
|
1224
1234
|
|
|
1225
1235
|
String msg;
|
|
1226
1236
|
serializeJson(doc, msg);
|
|
1227
|
-
|
|
1228
|
-
// Small delay to ensure mesh is ready
|
|
1229
|
-
delay(2000);
|
|
1230
1237
|
this->sendBroadcast(msg);
|
|
1231
1238
|
|
|
1232
|
-
|
|
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
|
+
});
|
|
1233
1280
|
}
|
|
1234
1281
|
|
|
1235
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
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#ifndef _PAINLESS_MESH_BUFFER_HPP_
|
|
1
|
+
#ifndef _PAINLESS_MESH_BUFFER_HPP_
|
|
2
2
|
#define _PAINLESS_MESH_BUFFER_HPP_
|
|
3
3
|
|
|
4
4
|
#include <list>
|
|
@@ -37,7 +37,7 @@ class ReceiveBuffer {
|
|
|
37
37
|
do {
|
|
38
38
|
auto len = strnlen(data_ptr, length);
|
|
39
39
|
do {
|
|
40
|
-
auto read_len = std::min(len, buf.length);
|
|
40
|
+
auto read_len = (std::min)(len, buf.length);
|
|
41
41
|
memcpy(buf.buffer, data_ptr, read_len);
|
|
42
42
|
buf.buffer[read_len] = '\0';
|
|
43
43
|
auto newBuffer = T(buf.buffer);
|
|
@@ -179,7 +179,7 @@ class SentBuffer {
|
|
|
179
179
|
else
|
|
180
180
|
// String.toCharArray automatically turns the last character into
|
|
181
181
|
// a \0, we need the extra space to deal with that annoyance
|
|
182
|
-
return std::min(buffer_length - 1, msg->length() + 1);
|
|
182
|
+
return (std::min)(buffer_length - 1, msg->length() + 1);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/**
|
|
@@ -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
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#ifndef _PAINLESS_MESH_MESH_HPP_
|
|
1
|
+
#ifndef _PAINLESS_MESH_MESH_HPP_
|
|
2
2
|
#define _PAINLESS_MESH_MESH_HPP_
|
|
3
3
|
|
|
4
4
|
#include <vector>
|
|
@@ -428,7 +428,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
428
428
|
/** Set a callback routine for any messages that are addressed to this node.
|
|
429
429
|
*
|
|
430
430
|
* Every time this node receives a message, this callback routine will the
|
|
431
|
-
* called.
|
|
431
|
+
* called. “from†is the id of the original sender of the message, and “msgâ€
|
|
432
432
|
* is a string that contains the message. The message can be anything. A
|
|
433
433
|
* JSON, some other text string, or binary data.
|
|
434
434
|
*
|
|
@@ -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
|
//
|
|
@@ -1861,7 +1913,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1861
1913
|
* // === painlessMesh Diagnostics ===
|
|
1862
1914
|
* // Mode: Regular Node
|
|
1863
1915
|
* // Mesh: ProductionMesh (25 nodes)
|
|
1864
|
-
* // Bridge: 123456789 (RSSI: -42 dBm, Internet:
|
|
1916
|
+
* // Bridge: 123456789 (RSSI: -42 dBm, Internet: ✓)
|
|
1865
1917
|
* // Queue: 3 messages (2 CRITICAL, 1 NORMAL)
|
|
1866
1918
|
* // Uptime: 02:15:33
|
|
1867
1919
|
* // Last Election: 00:45:12 ago (Winner: 123456789)
|
|
@@ -1889,7 +1941,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1889
1941
|
if (primaryBridge != nullptr) {
|
|
1890
1942
|
report += "Bridge: " + TSTRING(std::to_string(primaryBridge->nodeId).c_str());
|
|
1891
1943
|
report += " (RSSI: " + TSTRING(std::to_string(primaryBridge->routerRSSI).c_str()) + " dBm";
|
|
1892
|
-
report += ", Internet: " + TSTRING(primaryBridge->internetConnected ? "
|
|
1944
|
+
report += ", Internet: " + TSTRING(primaryBridge->internetConnected ? "✓" : "✗") + ")\n";
|
|
1893
1945
|
} else {
|
|
1894
1946
|
report += "Bridge: None available\n";
|
|
1895
1947
|
}
|
|
@@ -1960,6 +2012,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1960
2012
|
isExternalScheduler = true;
|
|
1961
2013
|
}
|
|
1962
2014
|
|
|
2015
|
+
public: // Windows MSVC: router functions need access
|
|
1963
2016
|
void startTimeSync(std::shared_ptr<T> conn) {
|
|
1964
2017
|
using namespace logger;
|
|
1965
2018
|
Log(S_TIME, "startTimeSync(): from %u with %u\n", this->nodeId,
|
|
@@ -2033,6 +2086,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2033
2086
|
[](const std::shared_ptr<T> &conn) { return !conn->connected(); });
|
|
2034
2087
|
}
|
|
2035
2088
|
|
|
2089
|
+
public: // Windows MSVC: TCP lambdas need access to droppedConnectionCallbacks
|
|
2036
2090
|
// Callback functions
|
|
2037
2091
|
callback::List<uint32_t> newConnectionCallbacks;
|
|
2038
2092
|
callback::List<uint32_t, bool> droppedConnectionCallbacks;
|
|
@@ -2056,6 +2110,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2056
2110
|
|
|
2057
2111
|
Scheduler *mScheduler;
|
|
2058
2112
|
|
|
2113
|
+
public: // Windows MSVC: lambdas in friend functions need public access
|
|
2114
|
+
|
|
2059
2115
|
/**
|
|
2060
2116
|
* Wrapper function for ESP32 semaphore function
|
|
2061
2117
|
*
|
|
@@ -143,8 +143,8 @@ public:
|
|
|
143
143
|
// Check for state change
|
|
144
144
|
checkStateChange();
|
|
145
145
|
|
|
146
|
-
Log(logger::GENERAL, "MessageQueue: Enqueued message #%u (priority=%d, size=%
|
|
147
|
-
msgId, priority, messages.size(), maxQueueSize);
|
|
146
|
+
Log(logger::GENERAL, "MessageQueue: Enqueued message #%u (priority=%d, size=%zu/%zu)\n",
|
|
147
|
+
msgId, priority, messages.size(), (size_t)maxQueueSize);
|
|
148
148
|
|
|
149
149
|
return msgId;
|
|
150
150
|
}
|
|
@@ -167,7 +167,7 @@ public:
|
|
|
167
167
|
updatePriorityStats();
|
|
168
168
|
checkStateChange();
|
|
169
169
|
|
|
170
|
-
Log(logger::GENERAL, "MessageQueue: Removed message #%u (size=%
|
|
170
|
+
Log(logger::GENERAL, "MessageQueue: Removed message #%u (size=%zu)\n",
|
|
171
171
|
messageId, messages.size());
|
|
172
172
|
return true;
|
|
173
173
|
}
|
package/src/painlessmesh/ntp.hpp
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#ifndef _PAINLESS_MESH_NTP_HPP_
|
|
1
|
+
#ifndef _PAINLESS_MESH_NTP_HPP_
|
|
2
2
|
#define _PAINLESS_MESH_NTP_HPP_
|
|
3
3
|
|
|
4
4
|
#ifndef TIME_SYNC_INTERVAL
|
|
@@ -42,7 +42,7 @@ class MeshTime {
|
|
|
42
42
|
*/
|
|
43
43
|
uint32_t getNodeTime() { return micros() + timeOffset; }
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
public: // Windows MSVC: friend functions need access to timeOffset
|
|
46
46
|
uint32_t timeOffset = 0;
|
|
47
47
|
};
|
|
48
48
|
|
|
@@ -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);
|