@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.
@@ -211,6 +211,63 @@ void rtcSyncCompleteCallback(uint32_t timestamp) {
211
211
  - Check NTP server is accessible
212
212
  - Ensure `syncRTCFromNTP()` called with valid timestamp
213
213
 
214
+ ## Time Authority
215
+
216
+ painlessMesh v1.8.12+ includes **time authority** support to prevent nodes from adopting incorrect time from nodes without accurate time sources.
217
+
218
+ ### How It Works
219
+
220
+ Nodes with time authority (RTC or Internet) are prioritized during mesh time synchronization:
221
+ - Nodes **without** time authority will adopt time from nodes **with** time authority
222
+ - Nodes **with** time authority will **NOT** adopt time from nodes without
223
+ - When both nodes have same authority status, existing subnet/node ID logic applies
224
+
225
+ ### Setting Time Authority
226
+
227
+ Time authority is automatically set when:
228
+ - RTC is enabled via `enableRTC()` (time authority = true)
229
+ - RTC is disabled via `disableRTC()` (time authority = false)
230
+
231
+ For bridge nodes with Internet, set time authority manually:
232
+
233
+ ```cpp
234
+ void bridgeStatusCallback(uint32_t bridgeNodeId, bool hasInternet) {
235
+ // If THIS node is the bridge, update time authority
236
+ if (bridgeNodeId == mesh.getNodeId()) {
237
+ if (hasInternet) {
238
+ mesh.setTimeAuthority(true); // Internet available
239
+ } else if (!mesh.hasRTC()) {
240
+ mesh.setTimeAuthority(false); // No Internet and no RTC
241
+ }
242
+ }
243
+ }
244
+ ```
245
+
246
+ ### Checking Time Authority
247
+
248
+ ```cpp
249
+ if (mesh.getTimeAuthority()) {
250
+ Serial.println("This node has authoritative time source");
251
+ }
252
+ ```
253
+
254
+ ### Use Cases
255
+
256
+ **Scenario 1: Mixed RTC nodes**
257
+ - Node A: Has RTC (time authority = true)
258
+ - Node B: No RTC (time authority = false)
259
+ - Result: Node B adopts time from Node A ✓
260
+
261
+ **Scenario 2: Bridge with Internet**
262
+ - Node A: Bridge with Internet (time authority = true)
263
+ - Node B: Regular node (time authority = false)
264
+ - Result: Node B adopts time from bridge ✓
265
+
266
+ **Scenario 3: Network split**
267
+ - Subnet A: All nodes have RTC
268
+ - Subnet B: No nodes have RTC
269
+ - Result: When subnets reconnect, Subnet B adopts from Subnet A ✓
270
+
214
271
  ## Best Practices
215
272
 
216
273
  1. **Always check RTC availability** before relying on timestamps
@@ -218,6 +275,8 @@ void rtcSyncCompleteCallback(uint32_t timestamp) {
218
275
  3. **Monitor battery** on RTC modules for continuous operation
219
276
  4. **Implement fallback** to mesh time if RTC fails
220
277
  5. **Log sync events** for debugging and maintenance
278
+ 6. **Set time authority** for bridge nodes when Internet is available
279
+ 7. **Use RTC on at least one node** in offline deployments for accurate timestamps
221
280
 
222
281
  ## Regulatory Compliance
223
282
 
@@ -160,6 +160,20 @@ void bridgeStatusCallback(uint32_t bridgeNodeId, bool hasInternet) {
160
160
  bridgeNodeId,
161
161
  hasInternet ? "Connected" : "Disconnected");
162
162
 
163
+ // If THIS node is the bridge, set time authority based on Internet availability
164
+ if (bridgeNodeId == mesh.getNodeId()) {
165
+ if (hasInternet) {
166
+ Serial.println("This node has Internet - setting time authority");
167
+ mesh.setTimeAuthority(true);
168
+ } else {
169
+ // Lost Internet - remove time authority if no RTC
170
+ if (!mesh.hasRTC()) {
171
+ Serial.println("Lost Internet and no RTC - removing time authority");
172
+ mesh.setTimeAuthority(false);
173
+ }
174
+ }
175
+ }
176
+
163
177
  if (hasInternet && needsNTPSync && mesh.hasRTC()) {
164
178
  // Internet is available and we need to sync RTC
165
179
  // In a real application, you would get NTP time here
package/library.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/Alteriom/painlessMesh"
8
8
  },
9
- "version": "1.8.10",
9
+ "version": "1.8.12",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=Alteriom PainlessMesh
2
- version=1.8.10
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.10",
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.10"
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 10
35
+ #define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 12
36
36
 
37
37
  /**
38
38
  * @brief Library description and usage information
@@ -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
- this->newConnectionCallbacks.push_back([this](uint32_t nodeId) {
810
- Log(CONNECTION, "New node %u connected, sending bridge status directly\n", nodeId);
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 ready, then send directly to the new node
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, bypassing broadcast routing
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
- // Reconfigure as bridge
1198
- this->stop();
1199
- delay(1000);
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"] = WiFi.RSSI();
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
- Log(STARTUP, "✓ Takeover announcement sent\n");
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
  /**
@@ -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.10
9
- * @date 2025-11-18
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
@@ -204,6 +204,50 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
204
204
  bool isRooted = layout::isRooted(mesh->asNodeTree());
205
205
  if (aps.empty()) {
206
206
  // No unknown nodes found
207
+ consecutiveEmptyScans++;
208
+
209
+ // If we've had multiple consecutive empty scans and we're not connected,
210
+ // trigger a full channel re-scan to find the mesh
211
+ if (consecutiveEmptyScans >= EMPTY_SCAN_THRESHOLD &&
212
+ WiFi.status() != WL_CONNECTED &&
213
+ channel > 0) {
214
+ Log(CONNECTION,
215
+ "connectToAP(): No mesh nodes found for %d scans, triggering channel re-detection\n",
216
+ consecutiveEmptyScans);
217
+
218
+ // Perform full channel scan to find the mesh
219
+ uint8_t detectedChannel = scanForMeshChannel(ssid, hidden);
220
+ if (detectedChannel > 0 && detectedChannel != mesh->_meshChannel) {
221
+ Log(CONNECTION,
222
+ "connectToAP(): Mesh found on different channel %d (was %d), updating...\n",
223
+ detectedChannel, mesh->_meshChannel);
224
+
225
+ // Update mesh channel
226
+ uint8_t oldChannel = mesh->_meshChannel;
227
+ mesh->_meshChannel = detectedChannel;
228
+ channel = detectedChannel;
229
+
230
+ // Restart AP on new channel to match the mesh
231
+ // This ensures this node's AP is also discoverable on the correct channel
232
+ if (WiFi.getMode() & WIFI_AP) {
233
+ Log(CONNECTION,
234
+ "connectToAP(): Restarting AP from channel %d to channel %d\n",
235
+ oldChannel, detectedChannel);
236
+ WiFi.softAPdisconnect(false);
237
+ delay(100);
238
+ // Call apInit via friend class access (StationScan is friend of wifi::Mesh)
239
+ mesh->apInit(mesh->getNodeId());
240
+ Log(CONNECTION, "connectToAP(): AP restarted on channel %d\n", detectedChannel);
241
+ }
242
+ } else if (detectedChannel == 0) {
243
+ Log(CONNECTION,
244
+ "connectToAP(): Mesh not found on any channel during re-scan\n");
245
+ }
246
+
247
+ // Reset counter after re-scan attempt
248
+ consecutiveEmptyScans = 0;
249
+ }
250
+
207
251
  if (WiFi.status() == WL_CONNECTED &&
208
252
  !(mesh->shouldContainRoot && !isRooted)) {
209
253
  // if already connected -> scan slow
@@ -220,6 +264,8 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
220
264
  }
221
265
  mesh->stability += min(1000 - mesh->stability, (size_t)25);
222
266
  } else {
267
+ // Reset counter when APs are found
268
+ consecutiveEmptyScans = 0;
223
269
  if (WiFi.status() == WL_CONNECTED) {
224
270
  // TODO: Use %u instead of String() here and below
225
271
  // Also prob is always equal to stability, so we should use that directly
@@ -36,6 +36,16 @@ class StationScan {
36
36
 
37
37
  // Helper to scan all channels for a specific mesh SSID
38
38
  static uint8_t scanForMeshChannel(TSTRING meshSSID, bool meshHidden);
39
+
40
+ // Check if channel re-synchronization is needed or in progress
41
+ bool isChannelResyncNeeded() const {
42
+ return consecutiveEmptyScans >= EMPTY_SCAN_THRESHOLD;
43
+ }
44
+
45
+ // Get the number of consecutive empty scans
46
+ uint16_t getConsecutiveEmptyScans() const {
47
+ return consecutiveEmptyScans;
48
+ }
39
49
 
40
50
  /// Valid APs found during the last scan
41
51
  std::list<WiFi_AP_Record_t> lastAPs;
@@ -54,6 +64,10 @@ class StationScan {
54
64
  // Manually configure network and ip
55
65
  bool manual = false;
56
66
  IPAddress manualIP = IPAddress(0, 0, 0, 0);
67
+
68
+ // Track consecutive scans with no mesh nodes found (for channel re-detection)
69
+ uint16_t consecutiveEmptyScans = 0;
70
+ static const uint16_t EMPTY_SCAN_THRESHOLD = 6; // ~30 seconds at default SCAN_INTERVAL
57
71
 
58
72
  friend painlessMesh;
59
73
  };
@@ -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
- return protocol::NodeSyncRequest(subTree.nodeId, nodeId, subTree.subs,
129
+ auto req = protocol::NodeSyncRequest(subTree.nodeId, nodeId, subTree.subs,
128
130
  subTree.root);
131
+ req.hasTimeAuthority = subTree.hasTimeAuthority;
132
+ return req;
129
133
  }
130
134
 
131
135
  /**
@@ -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. “from” is the id of the original sender of the message, and “msg”
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
- return rtcManager.enable(rtcInterface);
753
+ bool success = rtcManager.enable(rtcInterface);
754
+ if (success) {
755
+ // RTC enabled successfully - mark node as having time authority
756
+ setTimeAuthority(true);
757
+ }
758
+ return success;
754
759
  }
755
760
 
756
761
  /**
@@ -760,6 +765,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
760
765
  using namespace logger;
761
766
  Log(GENERAL, "disableRTC(): Disabling RTC\n");
762
767
  rtcManager.disable();
768
+ // RTC disabled - remove time authority if no other source
769
+ // Note: Bridge nodes may still have time authority from Internet
770
+ setTimeAuthority(false);
763
771
  }
764
772
 
765
773
  /**
@@ -864,6 +872,50 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
864
872
  rtcSyncCompleteCallback = onRTCSyncComplete;
865
873
  }
866
874
 
875
+ /**
876
+ * Set time authority status for this node
877
+ *
878
+ * Nodes with time authority (RTC or Internet) are preferred as time sources
879
+ * during mesh time synchronization. This prevents nodes from adopting time
880
+ * from nodes without accurate time sources.
881
+ *
882
+ * This is automatically set to true when:
883
+ * - RTC is enabled via enableRTC()
884
+ * - Bridge has Internet connectivity
885
+ *
886
+ * \code
887
+ * // Manual control (advanced usage)
888
+ * mesh.setTimeAuthority(true); // Mark as authoritative time source
889
+ * mesh.setTimeAuthority(false); // Mark as non-authoritative
890
+ * \endcode
891
+ *
892
+ * @param hasAuthority True if node has accurate time source (RTC/Internet)
893
+ */
894
+ void setTimeAuthority(bool hasAuthority) {
895
+ using namespace logger;
896
+ if (this->hasTimeAuthority != hasAuthority) {
897
+ this->hasTimeAuthority = hasAuthority;
898
+ Log(GENERAL, "setTimeAuthority(): Time authority %s\n",
899
+ hasAuthority ? "enabled" : "disabled");
900
+
901
+ // Trigger time sync with all connections to propagate authority status
902
+ for (auto&& connection : this->subs) {
903
+ if (connection->nodeId != 0) {
904
+ connection->timeSyncTask.forceNextIteration();
905
+ }
906
+ }
907
+ }
908
+ }
909
+
910
+ /**
911
+ * Check if this node has time authority
912
+ *
913
+ * @return True if node has RTC or Internet time source
914
+ */
915
+ bool getTimeAuthority() const {
916
+ return this->hasTimeAuthority;
917
+ }
918
+
867
919
  //
868
920
  // Message Queue API
869
921
  //
@@ -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 ? "✓" : "✗") + ")\n";
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=%u/%u)\n",
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=%u)\n",
170
+ Log(logger::GENERAL, "MessageQueue: Removed message #%u (size=%zu)\n",
171
171
  messageId, messages.size());
172
172
  return true;
173
173
  }
@@ -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
- protected:
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);