@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 +40 -0
- package/README.md +1 -1
- package/examples/bridge_failover/README.md +32 -0
- package/examples/bridge_failover/bridge_failover.ino +3 -0
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/arduino/wifi.hpp +162 -0
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.
|
|
7
|
+
**Version 1.9.1** - Isolated bridge retry mechanism for failed initial bridge setup
|
|
8
8
|
|
|
9
9
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
|
|
10
10
|
[](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
package/library.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name=Alteriom PainlessMesh
|
|
2
|
-
version=1.9.
|
|
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.
|
|
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",
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -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;
|