@alteriom/painlessmesh 1.9.0 → 1.9.1

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
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.1] - 2025-12-01
9
+
10
+ ### Added
11
+
12
+ - **Isolated Bridge Retry Mechanism** - Nodes that fail initial bridge setup can now retry automatically
13
+ - New `attemptIsolatedBridgePromotion()` method for direct bridge promotion when isolated
14
+ - Periodic retry task runs every 60 seconds when node is isolated (no mesh connections)
15
+ - Requires 6 consecutive empty mesh scans before attempting retry
16
+ - Limited to 5 actual retry attempts before 5-minute cooldown
17
+ - Counter resets on success, mesh reconnection, or after cooldown
18
+ - **Impact**: Fixes issue where nodes with `INITIAL_BRIDGE=true` that fail router connection
19
+ would never retry becoming a bridge (endless "Bridge monitor: Skipping - no active mesh connections")
20
+
21
+ - **Comprehensive Test Coverage** - Added tests for all woodlist use cases
22
+ - Use Case 1: INITIAL_BRIDGE=true with router temporarily unavailable
23
+ - Use Case 2: Regular node with no mesh found - isolated retry
24
+ - Use Case 3: Router association refused error handling
25
+ - Use Case 4: Node loses mesh connection to bridge
26
+ - Use Case 5: Multiple retry attempts with cooldown
27
+ - Use Case 6: Correct serial output for bridge failure
28
+
29
+ ### Fixed
30
+
31
+ - **Bridge Retry for Isolated Nodes** (#212) - Fixed nodes not retrying bridge connection
32
+ - **Root Cause**: Bridge monitor task skips isolated nodes to prevent split-brain scenarios,
33
+ but this prevented retry when initial bridge setup fails
34
+ - **Symptom**: Endless "Bridge monitor: Skipping - no active mesh connections" log messages
35
+ - **Solution**: Added separate isolated bridge retry mechanism that activates when:
36
+ - Node has router credentials configured
37
+ - Node is isolated (no mesh connections)
38
+ - Multiple empty scans have occurred (mesh not found)
39
+ - **Behavior**: Node scans for router, checks RSSI, and attempts direct bridge promotion
40
+ - Addresses feedback from @woodlist regarding failed initial bridge setup scenarios
41
+
42
+ ### Changed
43
+
44
+ - **Updated bridge_failover Example** - Added documentation about automatic retry behavior
45
+ - Example now notes that isolated nodes will retry bridge connection periodically
46
+ - Clearer messaging about fallback and retry mechanisms
47
+
8
48
  ## [1.9.0] - 2025-11-30
9
49
 
10
50
  ### Added
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div align="center">
6
6
 
7
- **Version 1.9.0** - Major update with improved bridge detection, consolidated examples, and cleaned documentation
7
+ **Version 1.9.1** - Isolated bridge retry mechanism for failed initial bridge setup
8
8
 
9
9
  [![CI/CD Pipeline](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml/badge.svg)](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
10
10
  [![Documentation](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml/badge.svg)](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
@@ -483,6 +483,38 @@ evaluateElection(): 1 candidates
483
483
  - Ensure bridge timeout passed (60 seconds)
484
484
  - Verify nodes can see router (RSSI scan)
485
485
 
486
+ ### Isolated Node Never Retries Bridge Connection (Fixed in v1.9.1)
487
+
488
+ **Symptoms**: Node with `INITIAL_BRIDGE=true` fails to connect to router at startup, then endlessly logs "Bridge monitor: Skipping - no active mesh connections" without ever retrying to become a bridge.
489
+
490
+ **Root Cause (Before v1.9.1)**:
491
+ When a node configured as `INITIAL_BRIDGE=true` failed to connect to the router during setup(), it correctly fell back to regular node mode with failover enabled. However, the bridge monitor task skipped isolated nodes (those with no mesh connections) to prevent split-brain scenarios. This prevented the node from ever retrying to become a bridge.
492
+
493
+ **Solution (Automatic in v1.9.1+)**:
494
+ A new isolated bridge retry mechanism now handles this case:
495
+ 1. Periodic task runs every 60 seconds when the node is isolated
496
+ 2. After 6 consecutive empty mesh scans, retry is triggered
497
+ 3. Node scans for router signal and checks minimum RSSI threshold (-80 dBm)
498
+ 4. If router is visible with adequate signal, attempts direct bridge promotion
499
+ 5. Limited to 5 retry attempts before 5-minute cooldown
500
+ 6. Counter resets on success, mesh reconnection, or after cooldown
501
+
502
+ **Expected Behavior** (v1.9.1+):
503
+ ```
504
+ Isolated bridge retry: Node isolated with 6 empty scans, attempting bridge promotion
505
+ === Isolated Bridge Promotion Attempt ===
506
+ Attempt 1 of 5
507
+ attemptIsolatedBridgePromotion(): Router visible with RSSI -45 dBm
508
+ Attempting direct bridge promotion (bypassing election)
509
+ ✓ Isolated bridge promotion complete on channel 6
510
+ ```
511
+
512
+ **Manual Solutions**:
513
+ - Update to painlessMesh v1.9.1 or later
514
+ - Ensure router credentials are configured: `mesh.setRouterCredentials()`
515
+ - Ensure failover is enabled: `mesh.enableBridgeFailover(true)`
516
+ - Check router is visible and signal strength is adequate (>= -80 dBm)
517
+
486
518
  ### Multiple Nodes Claim Bridge Role
487
519
 
488
520
  **Symptoms**: Split-brain scenario with multiple bridges
@@ -35,6 +35,8 @@
35
35
  // - Seamless promotion to bridge role
36
36
  // - Bridge takeover announcements
37
37
  // - Automatic mesh channel detection for bridge discovery
38
+ // - Isolated node retry: Nodes that fail initial bridge setup will
39
+ // periodically retry connecting to the router when no mesh is found
38
40
  //
39
41
  // Important Note:
40
42
  // Regular nodes MUST use channel auto-detection (channel=0) to discover
@@ -145,6 +147,7 @@ void setup() {
145
147
  mesh.setElectionTimeout(5000);
146
148
 
147
149
  Serial.println("✓ Running as regular node - will auto-promote when router available");
150
+ Serial.println("Note: If isolated (no mesh found), will retry bridge connection periodically");
148
151
  }
149
152
  } else {
150
153
  Serial.println("Mode: REGULAR NODE (Failover Enabled)");
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.9.0",
9
+ "version": "1.9.1",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=Alteriom PainlessMesh
2
- version=1.9.0
2
+ version=1.9.1
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.9.0",
3
+ "version": "1.9.1",
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",
@@ -205,6 +205,71 @@ class Mesh : public painlessmesh::Mesh<Connection> {
205
205
  }
206
206
  });
207
207
 
208
+ // Add separate periodic task for isolated bridge retry
209
+ // This handles the case where a node:
210
+ // - Has router credentials configured
211
+ // - Is isolated (no mesh connections)
212
+ // - Should attempt to become a bridge directly
213
+ // This is different from the election mechanism which requires mesh connectivity
214
+ this->addTask(isolatedBridgeRetryIntervalMs, TASK_FOREVER, [this]() {
215
+ // Only retry if failover is enabled and we have credentials
216
+ if (!bridgeFailoverEnabled || !routerCredentialsConfigured) {
217
+ return;
218
+ }
219
+
220
+ // Don't retry if we're already a bridge
221
+ if (this->isBridge()) {
222
+ return;
223
+ }
224
+
225
+ // Skip during startup period
226
+ if (millis() < electionStartupDelayMs) {
227
+ return;
228
+ }
229
+
230
+ // Only retry when isolated (no mesh connections found)
231
+ if (this->hasActiveMeshConnections()) {
232
+ // Reset retry counter when mesh is active
233
+ _isolatedBridgeRetryAttempts = 0;
234
+ return;
235
+ }
236
+
237
+ // Limit retry attempts with reset after timeout
238
+ if (_isolatedBridgeRetryAttempts >= MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS) {
239
+ // Check if enough time has passed to reset the counter
240
+ if (millis() > _isolatedBridgeRetryResetTime) {
241
+ Log(CONNECTION, "Isolated bridge retry: Reset timeout reached, resetting attempt counter\n");
242
+ _isolatedBridgeRetryAttempts = 0;
243
+ } else {
244
+ Log(CONNECTION, "Isolated bridge retry: Max attempts (%d) reached, reset in %u seconds\n",
245
+ MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS, (_isolatedBridgeRetryResetTime - millis()) / 1000);
246
+ return;
247
+ }
248
+ }
249
+
250
+ // Check if mesh network exists on any channel before trying to become bridge
251
+ // If mesh exists but we can't connect, don't try to become bridge
252
+ uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
253
+ if (emptyScans < ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD) {
254
+ Log(CONNECTION, "Isolated bridge retry: Only %d empty scans, waiting for more scans\n",
255
+ emptyScans);
256
+ return;
257
+ }
258
+
259
+ Log(CONNECTION, "Isolated bridge retry: Node isolated with %d empty scans, attempting bridge promotion\n",
260
+ emptyScans);
261
+
262
+ // Attempt to become bridge directly (bypassing election since we're isolated)
263
+ // Only increment retry counter if we actually attempted promotion
264
+ if (this->attemptIsolatedBridgePromotion()) {
265
+ _isolatedBridgeRetryAttempts++;
266
+ // Set reset time when reaching max attempts
267
+ if (_isolatedBridgeRetryAttempts >= MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS) {
268
+ _isolatedBridgeRetryResetTime = millis() + isolatedBridgeRetryResetIntervalMs;
269
+ }
270
+ }
271
+ });
272
+
208
273
  tcpServerInit();
209
274
  eventHandleInit();
210
275
 
@@ -1529,6 +1594,95 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1529
1594
  });
1530
1595
  }
1531
1596
 
1597
+ /**
1598
+ * Attempt to promote an isolated node to bridge
1599
+ *
1600
+ * This method handles the case where a node is isolated (no mesh connections)
1601
+ * but has router credentials. Unlike the election-based promotion, this
1602
+ * directly attempts to connect to the router without requiring mesh connectivity.
1603
+ *
1604
+ * This is useful for:
1605
+ * - Nodes that failed initial bridge setup and need to retry
1606
+ * - Nodes that are the first to start and no mesh exists yet
1607
+ * - Recovery scenarios where mesh network is unavailable
1608
+ *
1609
+ * @return true if promotion was attempted (regardless of success), false if skipped
1610
+ */
1611
+ bool attemptIsolatedBridgePromotion() {
1612
+ using namespace logger;
1613
+
1614
+ Log(CONNECTION, "=== Isolated Bridge Promotion Attempt ===\n");
1615
+ Log(CONNECTION, "Attempt %d of %d\n", _isolatedBridgeRetryAttempts + 1, MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS);
1616
+
1617
+ // First, scan for router to check if it's visible
1618
+ int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
1619
+
1620
+ if (routerRSSI == 0) {
1621
+ Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router %s not visible\n", routerSSID.c_str());
1622
+ return false; // Don't count as an attempt - router not visible
1623
+ }
1624
+
1625
+ // Check minimum RSSI threshold for isolated promotion
1626
+ if (routerRSSI < minimumBridgeRSSI) {
1627
+ Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router RSSI %d dBm below threshold %d dBm\n",
1628
+ routerRSSI, minimumBridgeRSSI);
1629
+ return false; // Don't count as an attempt - signal too weak
1630
+ }
1631
+
1632
+ Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router visible with RSSI %d dBm\n", routerRSSI);
1633
+ Log(CONNECTION, "Attempting direct bridge promotion (bypassing election)\n");
1634
+
1635
+ // Save current mesh configuration
1636
+ uint8_t savedChannel = _meshChannel;
1637
+
1638
+ // Stop current mesh operations
1639
+ this->stop();
1640
+ delay(1000);
1641
+
1642
+ // Attempt to initialize as bridge
1643
+ bool bridgeInitSuccess = this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
1644
+ mScheduler, _meshPort);
1645
+
1646
+ if (!bridgeInitSuccess) {
1647
+ Log(ERROR, "✗ Isolated bridge promotion failed - router unreachable\n");
1648
+ Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
1649
+
1650
+ // Re-initialize as regular node on the original channel
1651
+ this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
1652
+ savedChannel, _meshHidden, MAX_CONN);
1653
+
1654
+ // Re-configure router credentials for future retry attempts
1655
+ this->setRouterCredentials(routerSSID, routerPassword);
1656
+ this->enableBridgeFailover(true);
1657
+
1658
+ // Notify via callback
1659
+ if (bridgeRoleChangedCallback) {
1660
+ bridgeRoleChangedCallback(false, "Isolated bridge promotion failed - router unreachable");
1661
+ }
1662
+
1663
+ return true; // Count as an attempt - we tried but failed
1664
+ }
1665
+
1666
+ // Success! Reset retry counter
1667
+ _isolatedBridgeRetryAttempts = 0;
1668
+ lastRoleChangeTime = millis();
1669
+
1670
+ Log(STARTUP, "✓ Isolated bridge promotion complete on channel %d\n", _meshChannel);
1671
+
1672
+ // Notify via callback
1673
+ if (bridgeRoleChangedCallback) {
1674
+ bridgeRoleChangedCallback(true, "Isolated node promoted to bridge");
1675
+ }
1676
+
1677
+ // Send bridge status announcement to attract other nodes
1678
+ this->addTask(3000, TASK_ONCE, [this]() {
1679
+ Log(STARTUP, "Sending bridge status announcement on channel %d\n", _meshChannel);
1680
+ this->sendBridgeStatus();
1681
+ });
1682
+
1683
+ return true; // Count as an attempt - we succeeded
1684
+ }
1685
+
1532
1686
  /**
1533
1687
  * Handle received bridge election package
1534
1688
  * Called by package handler when election message arrives
@@ -1760,6 +1914,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
1760
1914
  std::vector<BridgeCandidate> electionCandidates;
1761
1915
  std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
1762
1916
 
1917
+ // Isolated bridge retry state and configuration
1918
+ uint8_t _isolatedBridgeRetryAttempts = 0;
1919
+ uint32_t _isolatedBridgeRetryResetTime = 0; // Time when retry counter can be reset
1920
+ static const uint8_t MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS = 5; // Max retry attempts before waiting
1921
+ static const uint32_t isolatedBridgeRetryIntervalMs = 60000; // Retry every 60 seconds
1922
+ static const uint32_t isolatedBridgeRetryResetIntervalMs = 300000; // Reset counter after 5 minutes
1923
+ static const uint16_t ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD = 6; // Require 6 empty scans before retrying
1924
+
1763
1925
  // Multi-bridge coordination state and configuration
1764
1926
  protected:
1765
1927
  bool multiBridgeEnabled = false;