@alteriom/painlessmesh 1.8.9 → 1.8.11

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.
@@ -233,6 +233,10 @@ mesh.enableBridgeFailover(true);
233
233
  // Set election timeout (milliseconds)
234
234
  mesh.setElectionTimeout(5000);
235
235
 
236
+ // Set minimum RSSI for isolated bridge elections (default: -80 dBm)
237
+ // Prevents nodes with poor signal from becoming bridges when isolated
238
+ mesh.setMinimumBridgeRSSI(-80);
239
+
236
240
  // Set bridge status broadcast interval
237
241
  mesh.setBridgeStatusInterval(30000);
238
242
 
@@ -357,6 +361,37 @@ bool amBridge = mesh.isBridge();
357
361
  - Works reliably with all network types including mobile hotspots and tethering
358
362
  - Local IP check is more reliable than gateway IP, which may not be available on all network types
359
363
 
364
+ ### Isolated Node with Poor Signal Attempting Bridge
365
+
366
+ **Symptoms**: Node logs "Election Failed: Insufficient Signal Quality" or has -85+ dBm RSSI
367
+
368
+ **Root Cause**: Node cannot see the mesh (isolated) and has poor router signal below minimum threshold
369
+
370
+ **What's Happening**:
371
+ 1. Node comes online and doesn't detect existing bridge
372
+ 2. Triggers election where it's the only candidate
373
+ 3. Has poor router RSSI (e.g., -87 dBm, below -80 dBm threshold)
374
+ 4. Election automatically rejected to prevent unreliable bridge
375
+ 5. Node remains as regular node until mesh connection or better signal
376
+
377
+ **Expected Behavior** (v1.8.10+):
378
+ ```
379
+ === Evaluating Election ===
380
+ evaluateElection(): 1 candidates
381
+ === Election Failed: Insufficient Signal Quality ===
382
+ Single candidate with RSSI -87 dBm (minimum required: -80 dBm)
383
+ Node is isolated from mesh with poor router signal
384
+ Rejecting election to prevent unreliable bridge
385
+ Recommendation: Move closer to router or wait for mesh connection
386
+ ```
387
+
388
+ **Solutions**:
389
+ - **Best**: Move node closer to router for better signal strength
390
+ - **Wait**: Node will join existing bridge once mesh connection established
391
+ - **Adjust threshold**: `mesh.setMinimumBridgeRSSI(-85)` to relax requirement (not recommended)
392
+ - **Check existing bridge**: Ensure another node with good signal is already bridge
393
+ - **Verify mesh**: Ensure nodes can communicate with each other
394
+
360
395
  ### Election Doesn't Start
361
396
 
362
397
  **Symptoms**: Bridge fails but no election occurs
@@ -381,10 +416,23 @@ bool amBridge = mesh.isBridge();
381
416
 
382
417
  **Symptoms**: Node with weak signal becomes bridge
383
418
 
384
- **Solutions**:
419
+ **Root Cause (Fixed in v1.8.10+)**:
420
+ When a node with poor router signal (e.g., -87 dBm) comes online and cannot see the existing bridge, it may trigger an election where it's the only candidate. Previously, it could win by default despite inadequate signal strength.
421
+
422
+ **Solution (Automatic)**:
423
+ Starting in v1.8.10, the election system enforces a minimum RSSI threshold (-80 dBm by default) for single-candidate elections. This prevents isolated nodes with poor signal from becoming bridges.
424
+
425
+ **Behavior**:
426
+ - **Single candidate with poor RSSI**: Election fails with warning message
427
+ - **Multiple candidates**: Best RSSI wins regardless of threshold (mesh is connected)
428
+ - **Single candidate with good RSSI**: Election proceeds normally
429
+
430
+ **Manual Solutions**:
431
+ - Update to painlessMesh v1.8.10 or later
432
+ - Adjust minimum RSSI threshold: `mesh.setMinimumBridgeRSSI(-75)` (stricter)
385
433
  - Verify RSSI values in election logs
386
434
  - Check router positioning and interference
387
- - Consider adjusting tiebreaker weightings
435
+ - Move nodes closer to router for better signal
388
436
  - Review signal strength readings
389
437
 
390
438
  ### Frequent Re-elections
@@ -136,6 +136,10 @@ void setup() {
136
136
  mesh.setRouterCredentials(ROUTER_SSID, ROUTER_PASSWORD);
137
137
  mesh.enableBridgeFailover(true);
138
138
  mesh.setElectionTimeout(5000); // 5 second election window
139
+
140
+ // Optional: Set minimum RSSI for isolated bridge elections (default: -80 dBm)
141
+ // This prevents nodes with poor signal from becoming bridges when isolated
142
+ // mesh.setMinimumBridgeRSSI(-80); // Uncomment to customize threshold
139
143
  }
140
144
 
141
145
  // Register callbacks
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.9",
9
+ "version": "1.8.11",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=Alteriom PainlessMesh
2
- version=1.8.9
2
+ version=1.8.11
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.9",
3
+ "version": "1.8.11",
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",
@@ -29,10 +29,10 @@
29
29
  /**
30
30
  * @brief AlteriomPainlessMesh library version information
31
31
  */
32
- #define ALTERIOM_PAINLESS_MESH_VERSION "1.8.9"
32
+ #define ALTERIOM_PAINLESS_MESH_VERSION "1.8.11"
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 9
35
+ #define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 11
36
36
 
37
37
  /**
38
38
  * @brief Library description and usage information
@@ -534,6 +534,21 @@ class Mesh : public painlessmesh::Mesh<Connection> {
534
534
  electionTimeoutMs = timeoutMs;
535
535
  }
536
536
 
537
+ /**
538
+ * Set the minimum RSSI required for bridge election
539
+ *
540
+ * Prevents nodes with poor router signal from becoming bridges in isolated
541
+ * elections. When a node is the only candidate, it must meet this threshold.
542
+ * When multiple candidates exist, the best RSSI wins regardless of threshold.
543
+ *
544
+ * @param minRSSI Minimum RSSI in dBm (default: -80 dBm, range: -100 to -30)
545
+ */
546
+ void setMinimumBridgeRSSI(int8_t minRSSI) {
547
+ if (minRSSI < -100) minRSSI = -100;
548
+ if (minRSSI > -30) minRSSI = -30;
549
+ minimumBridgeRSSI = minRSSI;
550
+ }
551
+
537
552
  /**
538
553
  * Set callback for when this node's bridge role changes
539
554
  *
@@ -789,10 +804,43 @@ class Mesh : public painlessmesh::Mesh<Connection> {
789
804
  this->sendBridgeStatus();
790
805
  });
791
806
 
792
- // Also broadcast when new nodes connect so they can discover the bridge immediately
793
- this->newConnectionCallbacks.push_back([this](uint32_t nodeId) {
794
- Log(CONNECTION, "New node %u connected, sending bridge status\n", nodeId);
795
- this->sendBridgeStatus();
807
+ // Also send bridge status when new nodes connect so they can discover the bridge immediately
808
+ // Send directly to the new node to ensure delivery, independent of time sync
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);
812
+
813
+ // Small delay to ensure connection is fully stable, then send directly to the new node
814
+ // This avoids issues with time sync blocking broadcast messages
815
+ this->addTask(500, TASK_ONCE, [this, nodeId]() {
816
+ // Create bridge status message
817
+ JsonDocument doc;
818
+ JsonObject obj = doc.to<JsonObject>();
819
+
820
+ obj["type"] = 610; // BRIDGE_STATUS type
821
+ obj["from"] = this->nodeId;
822
+ obj["routing"] = 1; // SINGLE routing (direct to node)
823
+ obj["dest"] = nodeId;
824
+ obj["timestamp"] = this->getNodeTime();
825
+
826
+ bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
827
+ (WiFi.localIP() != IPAddress(0, 0, 0, 0));
828
+ obj["internetConnected"] = hasInternet;
829
+ obj["routerRSSI"] = WiFi.RSSI();
830
+ obj["routerChannel"] = WiFi.channel();
831
+ obj["uptime"] = millis();
832
+ obj["gatewayIP"] = WiFi.gatewayIP().toString();
833
+ obj["message_type"] = 610;
834
+
835
+ String msg;
836
+ serializeJson(doc, msg);
837
+
838
+ Log(CONNECTION, "Sending bridge status directly to node %u (Internet: %s)\n",
839
+ nodeId, hasInternet ? "YES" : "NO");
840
+
841
+ // Send directly to the new node using routing table (which is now ready)
842
+ this->sendSingle(nodeId, msg);
843
+ });
796
844
  });
797
845
 
798
846
  Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
@@ -1077,6 +1125,28 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1077
1125
  return;
1078
1126
  }
1079
1127
 
1128
+ // Validate RSSI threshold for single-candidate elections
1129
+ // When only one candidate exists, it indicates the node is isolated from the mesh.
1130
+ // In this case, require minimum signal quality to prevent poor connections.
1131
+ // When multiple candidates exist, the mesh is connected and best RSSI wins.
1132
+ if (electionCandidates.size() == 1 && winner->routerRSSI < minimumBridgeRSSI) {
1133
+ Log(CONNECTION, "=== Election Failed: Insufficient Signal Quality ===\n");
1134
+ Log(CONNECTION, " Single candidate with RSSI %d dBm (minimum required: %d dBm)\n",
1135
+ winner->routerRSSI, minimumBridgeRSSI);
1136
+ Log(CONNECTION, " Node is isolated from mesh with poor router signal\n");
1137
+ Log(CONNECTION, " Rejecting election to prevent unreliable bridge\n");
1138
+ Log(CONNECTION, " Recommendation: Move closer to router or wait for mesh connection\n");
1139
+
1140
+ electionState = ELECTION_IDLE;
1141
+ electionCandidates.clear();
1142
+
1143
+ // Notify via callback that election failed
1144
+ if (bridgeRoleChangedCallback) {
1145
+ bridgeRoleChangedCallback(false, "Insufficient signal quality for isolated bridge");
1146
+ }
1147
+ return;
1148
+ }
1149
+
1080
1150
  Log(CONNECTION, "=== Election Winner: Node %u ===\n", winner->nodeId);
1081
1151
  Log(CONNECTION, " Router RSSI: %d dBm\n", winner->routerRSSI);
1082
1152
  Log(CONNECTION, " Uptime: %u ms\n", winner->uptime);
@@ -1381,6 +1451,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1381
1451
  TSTRING routerSSID = "";
1382
1452
  TSTRING routerPassword = "";
1383
1453
  uint32_t electionTimeoutMs = 5000; // Default 5 seconds
1454
+ int8_t minimumBridgeRSSI = -80; // Default -80 dBm minimum for isolated elections
1384
1455
  uint32_t lastRoleChangeTime = 0;
1385
1456
  ElectionState electionState = ELECTION_IDLE;
1386
1457
  uint32_t electionDeadline = 0;
@@ -233,8 +233,13 @@ class AsyncServer {
233
233
  void begin() {
234
234
  mAcceptor.open(tcp::v4());
235
235
  int one = 1;
236
+ #ifdef _WIN32
237
+ setsockopt(mAcceptor.native_handle(), SOL_SOCKET,
238
+ SO_REUSEADDR | SO_REUSEPORT, reinterpret_cast<const char*>(&one), sizeof(one));
239
+ #else
236
240
  setsockopt(mAcceptor.native_handle(), SOL_SOCKET,
237
241
  SO_REUSEADDR | SO_REUSEPORT, &one, sizeof(one));
242
+ #endif
238
243
  boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), _port);
239
244
  mAcceptor.set_option(boost::asio::socket_base::reuse_address(true));
240
245
  mAcceptor.set_option(boost::asio::ip::tcp::no_delay(true));
@@ -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
9
- * @date 2025-11-12
8
+ * @version 1.8.11
9
+ * @date 2025-11-18
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
@@ -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
  /**
@@ -10,6 +10,11 @@ namespace logger {
10
10
 
11
11
  #define REMOTE_QUEUE_SIZE 10
12
12
 
13
+ // Windows defines ERROR as a macro, undefine it to avoid conflicts
14
+ #ifdef ERROR
15
+ #undef ERROR
16
+ #endif
17
+
13
18
  typedef enum {
14
19
  ERROR = 1 << 0,
15
20
  STARTUP = 1 << 1,
@@ -132,7 +137,7 @@ class LogClass {
132
137
 
133
138
  vsnprintf(str, 200, format, args);
134
139
 
135
- remote_queue.push_back(std::pair<uint, TSTRING>(remote_uuid, str));
140
+ remote_queue.push_back(std::pair<uint32_t, TSTRING>(remote_uuid, str));
136
141
  ++remote_uuid;
137
142
  if (remote_queue.size() > REMOTE_QUEUE_SIZE) {
138
143
  // No place to store the reason, but do signify the queue is full,
@@ -142,15 +147,15 @@ class LogClass {
142
147
  }
143
148
  }
144
149
 
145
- std::list<std::pair<uint, TSTRING>> &get_remote_queue() {
150
+ std::list<std::pair<uint32_t, TSTRING>> &get_remote_queue() {
146
151
  return remote_queue;
147
152
  }
148
153
 
149
154
  private:
150
155
  uint16_t types = 0;
151
156
  char str[200];
152
- std::list<std::pair<uint, TSTRING>> remote_queue;
153
- uint remote_uuid;
157
+ std::list<std::pair<uint32_t, TSTRING>> remote_queue;
158
+ uint32_t remote_uuid;
154
159
  };
155
160
 
156
161
  } // namespace logger
@@ -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
  *
@@ -1861,7 +1861,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1861
1861
  * // === painlessMesh Diagnostics ===
1862
1862
  * // Mode: Regular Node
1863
1863
  * // Mesh: ProductionMesh (25 nodes)
1864
- * // Bridge: 123456789 (RSSI: -42 dBm, Internet: ✓)
1864
+ * // Bridge: 123456789 (RSSI: -42 dBm, Internet: ✓)
1865
1865
  * // Queue: 3 messages (2 CRITICAL, 1 NORMAL)
1866
1866
  * // Uptime: 02:15:33
1867
1867
  * // Last Election: 00:45:12 ago (Winner: 123456789)
@@ -1889,7 +1889,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1889
1889
  if (primaryBridge != nullptr) {
1890
1890
  report += "Bridge: " + TSTRING(std::to_string(primaryBridge->nodeId).c_str());
1891
1891
  report += " (RSSI: " + TSTRING(std::to_string(primaryBridge->routerRSSI).c_str()) + " dBm";
1892
- report += ", Internet: " + TSTRING(primaryBridge->internetConnected ? "✓" : "✗") + ")\n";
1892
+ report += ", Internet: " + TSTRING(primaryBridge->internetConnected ? "✓" : "✗") + ")\n";
1893
1893
  } else {
1894
1894
  report += "Bridge: None available\n";
1895
1895
  }
@@ -1960,6 +1960,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1960
1960
  isExternalScheduler = true;
1961
1961
  }
1962
1962
 
1963
+ public: // Windows MSVC: router functions need access
1963
1964
  void startTimeSync(std::shared_ptr<T> conn) {
1964
1965
  using namespace logger;
1965
1966
  Log(S_TIME, "startTimeSync(): from %u with %u\n", this->nodeId,
@@ -2033,6 +2034,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
2033
2034
  [](const std::shared_ptr<T> &conn) { return !conn->connected(); });
2034
2035
  }
2035
2036
 
2037
+ public: // Windows MSVC: TCP lambdas need access to droppedConnectionCallbacks
2036
2038
  // Callback functions
2037
2039
  callback::List<uint32_t> newConnectionCallbacks;
2038
2040
  callback::List<uint32_t, bool> droppedConnectionCallbacks;
@@ -2056,6 +2058,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
2056
2058
 
2057
2059
  Scheduler *mScheduler;
2058
2060
 
2061
+ public: // Windows MSVC: lambdas in friend functions need public access
2062
+
2059
2063
  /**
2060
2064
  * Wrapper function for ESP32 semaphore function
2061
2065
  *
@@ -2285,7 +2289,7 @@ class Connection : public painlessmesh::layout::Neighbour,
2285
2289
  quality -= (80 + rssi); // e.g., -90 dBm = penalty of 10
2286
2290
  }
2287
2291
 
2288
- return std::max(0, std::min(100, quality));
2292
+ return (std::max)(0, (std::min)(100, quality));
2289
2293
  }
2290
2294
 
2291
2295
  /**
@@ -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
 
@@ -380,7 +380,7 @@ class TimeSync : public PackageInterface {
380
380
  /**
381
381
  * Create a reply to the current message with the new time set
382
382
  */
383
- void reply(uint newT0) {
383
+ void reply(uint32_t newT0) {
384
384
  msg.t0 = newT0;
385
385
  ++msg.type;
386
386
  std::swap(from, dest);
@@ -1,4 +1,4 @@
1
- #ifndef _PAINLESS_MESH_ROUTER_HPP_
1
+ #ifndef _PAINLESS_MESH_ROUTER_HPP_
2
2
  #define _PAINLESS_MESH_ROUTER_HPP_
3
3
 
4
4
  #include <algorithm>
@@ -235,14 +235,14 @@ void routePackage(layout::Layout<T> layout, std::shared_ptr<T> connection,
235
235
  // Base capacity: message length + overhead for JSON structure
236
236
  // Each nesting level adds overhead for pointers and metadata
237
237
  size_t calculatedCapacity = pkg.length() +
238
- JSON_OBJECT_SIZE(10) * std::max(nestingDepth, size_t(1)) +
238
+ JSON_OBJECT_SIZE(10) * (std::max)(nestingDepth, size_t(1)) +
239
239
  512; // Additional buffer for strings and padding
240
240
  #endif
241
241
 
242
242
  // Cap at 8KB for safety on ESP8266 (which has ~80KB total heap)
243
243
  // Messages larger than this should be rejected
244
244
  constexpr size_t MAX_MESSAGE_CAPACITY = 8192;
245
- size_t capacity = std::min(calculatedCapacity, MAX_MESSAGE_CAPACITY);
245
+ size_t capacity = (std::min)(calculatedCapacity, MAX_MESSAGE_CAPACITY);
246
246
 
247
247
  auto variant = std::make_shared<protocol::Variant>(pkg, capacity);
248
248
 
@@ -330,7 +330,7 @@ void handleNodeSync(T& mesh, protocol::NodeTree newTree,
330
330
  [&mesh, nodeId]() { mesh.changedConnectionCallbacks.execute(nodeId); });
331
331
  } else {
332
332
  conn->nodeSyncTask.delay();
333
- mesh.stability += std::min(1000 - mesh.stability, (size_t)25);
333
+ mesh.stability += (std::min)(1000 - mesh.stability, (size_t)25);
334
334
  }
335
335
  }
336
336