@alteriom/painlessmesh 1.8.9 → 1.8.10

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 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.10] - 2025-11-18
11
+
12
+ ### Fixed
13
+
14
+ - **Bridge Status Discovery - Direct Messaging** - Fixed newly connected nodes not receiving bridge status
15
+ - **Root Cause**: Broadcast messages were not reaching newly connected nodes reliably
16
+ - Time sync (NTP) was interfering with bridge discovery
17
+ - Broadcast routing may not be fully established immediately after connection
18
+ - **Solution**: Send bridge status directly to new node using `sendSingle()`
19
+ - Changed from broadcast (`routing=2`) to single (`routing=1`)
20
+ - Minimal 500ms delay (just for connection stability)
21
+ - Direct targeted delivery ensures message reaches the new node
22
+ - Time sync no longer affects bridge discovery
23
+ - Location: `src/arduino/wifi.hpp` line ~809 in `initBridgeStatusBroadcast()`
24
+ - Impact: Nodes discover bridges immediately (within 500ms) after connecting
25
+ - Backward compatible: No API changes, internal delivery mechanism improved
26
+ - Resolves GitHub issue #135 "The latest fix does not work"
27
+
10
28
  ## [1.8.9] - 2025-11-12
11
29
 
12
30
  ### Fixed
@@ -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.10",
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.10
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.10",
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.10"
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 10
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,42 @@ 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
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
793
809
  this->newConnectionCallbacks.push_back([this](uint32_t nodeId) {
794
- Log(CONNECTION, "New node %u connected, sending bridge status\n", nodeId);
795
- this->sendBridgeStatus();
810
+ Log(CONNECTION, "New node %u connected, sending bridge status directly\n", nodeId);
811
+
812
+ // Small delay to ensure connection is ready, then send directly to the new node
813
+ // This avoids issues with time sync blocking broadcast messages
814
+ this->addTask(500, TASK_ONCE, [this, nodeId]() {
815
+ // Create bridge status message
816
+ JsonDocument doc;
817
+ JsonObject obj = doc.to<JsonObject>();
818
+
819
+ obj["type"] = 610; // BRIDGE_STATUS type
820
+ obj["from"] = this->nodeId;
821
+ obj["routing"] = 1; // SINGLE routing (direct to node)
822
+ obj["dest"] = nodeId;
823
+ obj["timestamp"] = this->getNodeTime();
824
+
825
+ bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
826
+ (WiFi.localIP() != IPAddress(0, 0, 0, 0));
827
+ obj["internetConnected"] = hasInternet;
828
+ obj["routerRSSI"] = WiFi.RSSI();
829
+ obj["routerChannel"] = WiFi.channel();
830
+ obj["uptime"] = millis();
831
+ obj["gatewayIP"] = WiFi.gatewayIP().toString();
832
+ obj["message_type"] = 610;
833
+
834
+ String msg;
835
+ serializeJson(doc, msg);
836
+
837
+ Log(CONNECTION, "Sending bridge status directly to node %u (Internet: %s)\n",
838
+ nodeId, hasInternet ? "YES" : "NO");
839
+
840
+ // Send directly to the new node, bypassing broadcast routing
841
+ this->sendSingle(nodeId, msg);
842
+ });
796
843
  });
797
844
 
798
845
  Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
@@ -1077,6 +1124,28 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1077
1124
  return;
1078
1125
  }
1079
1126
 
1127
+ // Validate RSSI threshold for single-candidate elections
1128
+ // When only one candidate exists, it indicates the node is isolated from the mesh.
1129
+ // In this case, require minimum signal quality to prevent poor connections.
1130
+ // When multiple candidates exist, the mesh is connected and best RSSI wins.
1131
+ if (electionCandidates.size() == 1 && winner->routerRSSI < minimumBridgeRSSI) {
1132
+ Log(CONNECTION, "=== Election Failed: Insufficient Signal Quality ===\n");
1133
+ Log(CONNECTION, " Single candidate with RSSI %d dBm (minimum required: %d dBm)\n",
1134
+ winner->routerRSSI, minimumBridgeRSSI);
1135
+ Log(CONNECTION, " Node is isolated from mesh with poor router signal\n");
1136
+ Log(CONNECTION, " Rejecting election to prevent unreliable bridge\n");
1137
+ Log(CONNECTION, " Recommendation: Move closer to router or wait for mesh connection\n");
1138
+
1139
+ electionState = ELECTION_IDLE;
1140
+ electionCandidates.clear();
1141
+
1142
+ // Notify via callback that election failed
1143
+ if (bridgeRoleChangedCallback) {
1144
+ bridgeRoleChangedCallback(false, "Insufficient signal quality for isolated bridge");
1145
+ }
1146
+ return;
1147
+ }
1148
+
1080
1149
  Log(CONNECTION, "=== Election Winner: Node %u ===\n", winner->nodeId);
1081
1150
  Log(CONNECTION, " Router RSSI: %d dBm\n", winner->routerRSSI);
1082
1151
  Log(CONNECTION, " Uptime: %u ms\n", winner->uptime);
@@ -1381,6 +1450,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1381
1450
  TSTRING routerSSID = "";
1382
1451
  TSTRING routerPassword = "";
1383
1452
  uint32_t electionTimeoutMs = 5000; // Default 5 seconds
1453
+ int8_t minimumBridgeRSSI = -80; // Default -80 dBm minimum for isolated elections
1384
1454
  uint32_t lastRoleChangeTime = 0;
1385
1455
  ElectionState electionState = ELECTION_IDLE;
1386
1456
  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.10
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
@@ -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
@@ -2285,7 +2285,7 @@ class Connection : public painlessmesh::layout::Neighbour,
2285
2285
  quality -= (80 + rssi); // e.g., -90 dBm = penalty of 10
2286
2286
  }
2287
2287
 
2288
- return std::max(0, std::min(100, quality));
2288
+ return (std::max)(0, (std::min)(100, quality));
2289
2289
  }
2290
2290
 
2291
2291
  /**
@@ -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);