@alteriom/painlessmesh 1.9.0 → 1.9.2
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 +52 -0
- package/README.md +1 -1
- package/examples/bridge_failover/README.md +82 -0
- package/examples/bridge_failover/bridge_failover.ino +29 -1
- package/examples/sendToInternet/README.md +158 -0
- package/examples/sendToInternet/platformio.ini +23 -0
- package/examples/sendToInternet/sendToInternet.ino +340 -0
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/arduino/wifi.hpp +186 -1
- package/src/painlessmesh/mesh.hpp +30 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,58 @@ 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.2] - 2025-12-01
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Release Version Update** - Consolidated release across all distribution channels
|
|
13
|
+
- Synchronized version numbers in library.properties, library.json, and package.json
|
|
14
|
+
- Ensures consistent versioning for NPM, PlatformIO, and Arduino Library Manager
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
- **README Update** - Updated version banner to reflect 1.9.2 release
|
|
19
|
+
|
|
20
|
+
## [1.9.1] - 2025-12-01
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Isolated Bridge Retry Mechanism** - Nodes that fail initial bridge setup can now retry automatically
|
|
25
|
+
- New `attemptIsolatedBridgePromotion()` method for direct bridge promotion when isolated
|
|
26
|
+
- Periodic retry task runs every 60 seconds when node is isolated (no mesh connections)
|
|
27
|
+
- Requires 6 consecutive empty mesh scans before attempting retry
|
|
28
|
+
- Limited to 5 actual retry attempts before 5-minute cooldown
|
|
29
|
+
- Counter resets on success, mesh reconnection, or after cooldown
|
|
30
|
+
- **Impact**: Fixes issue where nodes with `INITIAL_BRIDGE=true` that fail router connection
|
|
31
|
+
would never retry becoming a bridge (endless "Bridge monitor: Skipping - no active mesh connections")
|
|
32
|
+
|
|
33
|
+
- **Comprehensive Test Coverage** - Added tests for all woodlist use cases
|
|
34
|
+
- Use Case 1: INITIAL_BRIDGE=true with router temporarily unavailable
|
|
35
|
+
- Use Case 2: Regular node with no mesh found - isolated retry
|
|
36
|
+
- Use Case 3: Router association refused error handling
|
|
37
|
+
- Use Case 4: Node loses mesh connection to bridge
|
|
38
|
+
- Use Case 5: Multiple retry attempts with cooldown
|
|
39
|
+
- Use Case 6: Correct serial output for bridge failure
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **Bridge Retry for Isolated Nodes** (#212) - Fixed nodes not retrying bridge connection
|
|
44
|
+
- **Root Cause**: Bridge monitor task skips isolated nodes to prevent split-brain scenarios,
|
|
45
|
+
but this prevented retry when initial bridge setup fails
|
|
46
|
+
- **Symptom**: Endless "Bridge monitor: Skipping - no active mesh connections" log messages
|
|
47
|
+
- **Solution**: Added separate isolated bridge retry mechanism that activates when:
|
|
48
|
+
- Node has router credentials configured
|
|
49
|
+
- Node is isolated (no mesh connections)
|
|
50
|
+
- Multiple empty scans have occurred (mesh not found)
|
|
51
|
+
- **Behavior**: Node scans for router, checks RSSI, and attempts direct bridge promotion
|
|
52
|
+
- Addresses feedback from @woodlist regarding failed initial bridge setup scenarios
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **Updated bridge_failover Example** - Added documentation about automatic retry behavior
|
|
57
|
+
- Example now notes that isolated nodes will retry bridge connection periodically
|
|
58
|
+
- Clearer messaging about fallback and retry mechanisms
|
|
59
|
+
|
|
8
60
|
## [1.9.0] - 2025-11-30
|
|
9
61
|
|
|
10
62
|
### 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.2** - Consolidated release across all distribution channels
|
|
8
8
|
|
|
9
9
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
|
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
This example demonstrates automatic bridge failover with RSSI-based election in painlessMesh networks.
|
|
4
4
|
|
|
5
|
+
## ⚠️ Important: Understanding Internet Connectivity
|
|
6
|
+
|
|
7
|
+
**`hasInternetConnection()` indicates a GATEWAY has Internet access, NOT that this node can make HTTP requests!**
|
|
8
|
+
|
|
9
|
+
Regular mesh nodes do NOT have direct IP routing to the Internet. They only communicate via the painlessMesh protocol (node-to-node messages). When `hasInternetConnection()` returns `true`, it means a bridge/gateway node in the mesh has Internet access.
|
|
10
|
+
|
|
11
|
+
**To send data to the Internet from a regular mesh node, you must:**
|
|
12
|
+
|
|
13
|
+
1. **Use `sendToInternet()`** - Routes data through a gateway node
|
|
14
|
+
2. **Use `initAsSharedGateway()`** - Configures all nodes with direct router access (requires router credentials - see below)
|
|
15
|
+
3. **Send mesh messages to the bridge** - Bridge node handles Internet communication
|
|
16
|
+
|
|
17
|
+
**DON'T do this on regular mesh nodes:**
|
|
18
|
+
```cpp
|
|
19
|
+
// This will FAIL with "connection refused" on regular mesh nodes!
|
|
20
|
+
HTTPClient http;
|
|
21
|
+
http.begin("https://api.example.com");
|
|
22
|
+
int httpCode = http.GET(); // FAILS!
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**DO this instead:**
|
|
26
|
+
```cpp
|
|
27
|
+
// Check if a gateway is available, then use sendToInternet()
|
|
28
|
+
if (mesh.hasInternetConnection()) {
|
|
29
|
+
mesh.sendToInternet(
|
|
30
|
+
"https://api.example.com/data",
|
|
31
|
+
jsonPayload,
|
|
32
|
+
[](bool success, uint16_t status, String error) {
|
|
33
|
+
Serial.printf("Delivery: %s\n", success ? "OK" : error.c_str());
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For use cases where all nodes need direct Internet access, see the [sharedGateway example](../sharedGateway/).
|
|
40
|
+
|
|
41
|
+
**Note on `initAsSharedGateway()` usage:**
|
|
42
|
+
```cpp
|
|
43
|
+
// initAsSharedGateway() requires ROUTER credentials since all nodes connect to router
|
|
44
|
+
// Signature: initAsSharedGateway(meshSSID, meshPassword, ROUTER_SSID, ROUTER_PASSWORD, scheduler, port)
|
|
45
|
+
mesh.initAsSharedGateway(
|
|
46
|
+
MESH_PREFIX, // Mesh network name
|
|
47
|
+
MESH_PASSWORD, // Mesh network password
|
|
48
|
+
ROUTER_SSID, // Your WiFi router SSID (required!)
|
|
49
|
+
ROUTER_PASSWORD, // Your WiFi router password (required!)
|
|
50
|
+
&userScheduler, // Task scheduler
|
|
51
|
+
MESH_PORT // TCP port for mesh
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
5
55
|
## Problem Statement
|
|
6
56
|
|
|
7
57
|
In a typical mesh network with a bridge node connecting to the Internet, the bridge represents a single point of failure. If the bridge loses Internet connectivity or goes offline, the entire mesh loses its gateway to the Internet.
|
|
@@ -483,6 +533,38 @@ evaluateElection(): 1 candidates
|
|
|
483
533
|
- Ensure bridge timeout passed (60 seconds)
|
|
484
534
|
- Verify nodes can see router (RSSI scan)
|
|
485
535
|
|
|
536
|
+
### Isolated Node Never Retries Bridge Connection (Fixed in v1.9.1)
|
|
537
|
+
|
|
538
|
+
**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.
|
|
539
|
+
|
|
540
|
+
**Root Cause (Before v1.9.1)**:
|
|
541
|
+
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.
|
|
542
|
+
|
|
543
|
+
**Solution (Automatic in v1.9.1+)**:
|
|
544
|
+
A new isolated bridge retry mechanism now handles this case:
|
|
545
|
+
1. Periodic task runs every 60 seconds when the node is isolated
|
|
546
|
+
2. After 6 consecutive empty mesh scans, retry is triggered
|
|
547
|
+
3. Node scans for router signal and checks minimum RSSI threshold (-80 dBm)
|
|
548
|
+
4. If router is visible with adequate signal, attempts direct bridge promotion
|
|
549
|
+
5. Limited to 5 retry attempts before 5-minute cooldown
|
|
550
|
+
6. Counter resets on success, mesh reconnection, or after cooldown
|
|
551
|
+
|
|
552
|
+
**Expected Behavior** (v1.9.1+):
|
|
553
|
+
```
|
|
554
|
+
Isolated bridge retry: Node isolated with 6 empty scans, attempting bridge promotion
|
|
555
|
+
=== Isolated Bridge Promotion Attempt ===
|
|
556
|
+
Attempt 1 of 5
|
|
557
|
+
attemptIsolatedBridgePromotion(): Router visible with RSSI -45 dBm
|
|
558
|
+
Attempting direct bridge promotion (bypassing election)
|
|
559
|
+
✓ Isolated bridge promotion complete on channel 6
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Manual Solutions**:
|
|
563
|
+
- Update to painlessMesh v1.9.1 or later
|
|
564
|
+
- Ensure router credentials are configured: `mesh.setRouterCredentials()`
|
|
565
|
+
- Ensure failover is enabled: `mesh.enableBridgeFailover(true)`
|
|
566
|
+
- Check router is visible and signal strength is adequate (>= -80 dBm)
|
|
567
|
+
|
|
486
568
|
### Multiple Nodes Claim Bridge Role
|
|
487
569
|
|
|
488
570
|
**Symptoms**: Split-brain scenario with multiple bridges
|
|
@@ -5,6 +5,27 @@
|
|
|
5
5
|
// When the primary bridge loses Internet connectivity, nodes automatically
|
|
6
6
|
// hold an election to select a new bridge based on router signal strength.
|
|
7
7
|
//
|
|
8
|
+
// IMPORTANT - UNDERSTANDING INTERNET CONNECTIVITY:
|
|
9
|
+
// ================================================
|
|
10
|
+
// The mesh.hasInternetConnection() method checks if a GATEWAY (bridge) node
|
|
11
|
+
// has Internet access - it does NOT mean THIS node can make HTTP requests!
|
|
12
|
+
//
|
|
13
|
+
// Regular mesh nodes do NOT have direct IP routing to the Internet.
|
|
14
|
+
// They only communicate via the painlessMesh protocol (node-to-node).
|
|
15
|
+
//
|
|
16
|
+
// To send data to the Internet from a regular node:
|
|
17
|
+
// 1. Use mesh.sendToInternet() to route through a gateway
|
|
18
|
+
// 2. Use initAsSharedGateway() so all nodes have router access
|
|
19
|
+
// NOTE: initAsSharedGateway() requires ROUTER credentials:
|
|
20
|
+
// mesh.initAsSharedGateway(MESH_PREFIX, MESH_PASSWORD,
|
|
21
|
+
// ROUTER_SSID, ROUTER_PASSWORD, // Router creds required!
|
|
22
|
+
// &userScheduler, MESH_PORT);
|
|
23
|
+
// 3. Send mesh messages to bridge node which handles Internet comms
|
|
24
|
+
//
|
|
25
|
+
// DON'T DO THIS on regular mesh nodes:
|
|
26
|
+
// HTTPClient http;
|
|
27
|
+
// http.begin("https://api.example.com"); // FAILS with "connection refused"
|
|
28
|
+
//
|
|
8
29
|
// Hardware Required:
|
|
9
30
|
// - ESP32 or ESP8266
|
|
10
31
|
// - WiFi router with Internet connection
|
|
@@ -35,6 +56,8 @@
|
|
|
35
56
|
// - Seamless promotion to bridge role
|
|
36
57
|
// - Bridge takeover announcements
|
|
37
58
|
// - Automatic mesh channel detection for bridge discovery
|
|
59
|
+
// - Isolated node retry: Nodes that fail initial bridge setup will
|
|
60
|
+
// periodically retry connecting to the router when no mesh is found
|
|
38
61
|
//
|
|
39
62
|
// Important Note:
|
|
40
63
|
// Regular nodes MUST use channel auto-detection (channel=0) to discover
|
|
@@ -145,6 +168,7 @@ void setup() {
|
|
|
145
168
|
mesh.setElectionTimeout(5000);
|
|
146
169
|
|
|
147
170
|
Serial.println("✓ Running as regular node - will auto-promote when router available");
|
|
171
|
+
Serial.println("Note: If isolated (no mesh found), will retry bridge connection periodically");
|
|
148
172
|
}
|
|
149
173
|
} else {
|
|
150
174
|
Serial.println("Mode: REGULAR NODE (Failover Enabled)");
|
|
@@ -201,7 +225,11 @@ void loop() {
|
|
|
201
225
|
|
|
202
226
|
Serial.println("\n--- Bridge Status ---");
|
|
203
227
|
Serial.printf("I am bridge: %s\n", mesh.isBridge() ? "YES" : "NO");
|
|
204
|
-
|
|
228
|
+
|
|
229
|
+
// NOTE: "Internet available" means a GATEWAY node has Internet access.
|
|
230
|
+
// This does NOT mean THIS node can make direct HTTP requests!
|
|
231
|
+
// Regular mesh nodes must use sendToInternet() to reach the Internet.
|
|
232
|
+
Serial.printf("Internet available via gateway: %s\n", mesh.hasInternetConnection() ? "YES" : "NO");
|
|
205
233
|
Serial.printf("Mesh connections active: %s\n", mesh.hasActiveMeshConnections() ? "YES" : "NO");
|
|
206
234
|
|
|
207
235
|
auto bridges = mesh.getBridges();
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# sendToInternet Example - WhatsApp/Callmebot Integration
|
|
2
|
+
|
|
3
|
+
This example demonstrates how to use `mesh.sendToInternet()` to send data to Internet endpoints (like WhatsApp via Callmebot) from **any node** in the mesh network.
|
|
4
|
+
|
|
5
|
+
## ⚠️ Important: Understanding sendToInternet()
|
|
6
|
+
|
|
7
|
+
Regular mesh nodes do **NOT** have direct IP routing to the Internet. The `sendToInternet()` API routes your data **through a gateway node** that has Internet access.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
11
|
+
│ Sensor Node │ ──sendToInternet──▶│ Gateway │ ──HTTP Request──▶ │ Internet │
|
|
12
|
+
│ (no WiFi) │ │ (has WiFi) │ │ (Callmebot)│
|
|
13
|
+
└─────────────┘ └─────────────┘ └─────────────┘
|
|
14
|
+
▲ │ │
|
|
15
|
+
│ │ │
|
|
16
|
+
└──────────── ACK/Result ◀─────────┴──────────── HTTP Response ◀──────┘
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**This is different from making direct HTTP requests** (which would fail on regular mesh nodes with "connection refused").
|
|
20
|
+
|
|
21
|
+
## Use Cases
|
|
22
|
+
|
|
23
|
+
- 🐟 **Fish farm sensors** sending O2 alarms to WhatsApp
|
|
24
|
+
- 🏭 **Industrial IoT** sending alerts to cloud APIs
|
|
25
|
+
- 🏠 **Smart home sensors** reporting to home automation servers
|
|
26
|
+
- 📊 **Remote monitoring** sending data to ThingsBoard, AWS IoT, etc.
|
|
27
|
+
|
|
28
|
+
## Prerequisites
|
|
29
|
+
|
|
30
|
+
1. **At least one node must be a bridge/gateway** with Internet access
|
|
31
|
+
2. **OR** use `initAsSharedGateway()` so all nodes have Internet
|
|
32
|
+
3. Enable the API after mesh init: `mesh.enableSendToInternet()`
|
|
33
|
+
|
|
34
|
+
## Setup for Callmebot WhatsApp
|
|
35
|
+
|
|
36
|
+
1. Get your API key from: https://www.callmebot.com/blog/free-api-whatsapp-messages/
|
|
37
|
+
2. Update the configuration in the sketch:
|
|
38
|
+
|
|
39
|
+
```cpp
|
|
40
|
+
#define WHATSAPP_PHONE "+1234567890" // Your phone with country code
|
|
41
|
+
#define WHATSAPP_APIKEY "your_api_key" // Your Callmebot API key
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API Usage
|
|
45
|
+
|
|
46
|
+
### Basic Usage
|
|
47
|
+
|
|
48
|
+
```cpp
|
|
49
|
+
// Check if Internet is available via any gateway
|
|
50
|
+
if (mesh.hasInternetConnection()) {
|
|
51
|
+
|
|
52
|
+
// Send data to Internet - routed through gateway automatically
|
|
53
|
+
uint32_t msgId = mesh.sendToInternet(
|
|
54
|
+
"https://api.callmebot.com/whatsapp.php?phone=+1234567890&apikey=KEY&text=Hello",
|
|
55
|
+
"", // Empty payload for GET (params in URL)
|
|
56
|
+
[](bool success, uint16_t httpStatus, String error) {
|
|
57
|
+
if (success) {
|
|
58
|
+
Serial.printf("✅ Sent! HTTP: %d\n", httpStatus);
|
|
59
|
+
} else {
|
|
60
|
+
Serial.printf("❌ Failed: %s\n", error.c_str());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Sending JSON to REST API
|
|
68
|
+
|
|
69
|
+
```cpp
|
|
70
|
+
String payload = "{\"temperature\": 25.5, \"humidity\": 60}";
|
|
71
|
+
|
|
72
|
+
mesh.sendToInternet(
|
|
73
|
+
"https://api.yourserver.com/sensors",
|
|
74
|
+
payload,
|
|
75
|
+
[](bool success, uint16_t httpStatus, String error) {
|
|
76
|
+
Serial.printf("Result: %s, HTTP: %d\n",
|
|
77
|
+
success ? "OK" : error.c_str(), httpStatus);
|
|
78
|
+
},
|
|
79
|
+
static_cast<uint8_t>(painlessmesh::gateway::GatewayPriority::PRIORITY_HIGH)
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Priority Levels
|
|
84
|
+
|
|
85
|
+
| Value | Enum | Use Case |
|
|
86
|
+
|-------|------|----------|
|
|
87
|
+
| 0 | `PRIORITY_CRITICAL` | Alarms, emergencies |
|
|
88
|
+
| 1 | `PRIORITY_HIGH` | Important alerts |
|
|
89
|
+
| 2 | `PRIORITY_NORMAL` | Regular sensor data |
|
|
90
|
+
| 3 | `PRIORITY_LOW` | Bulk/background data |
|
|
91
|
+
|
|
92
|
+
## Hardware Setup
|
|
93
|
+
|
|
94
|
+
### Option A: Bridge + Sensor Nodes
|
|
95
|
+
|
|
96
|
+
**Bridge Node** (has Internet):
|
|
97
|
+
```cpp
|
|
98
|
+
#define IS_BRIDGE_NODE true
|
|
99
|
+
// Update ROUTER_SSID and ROUTER_PASSWORD
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Sensor Nodes** (no direct Internet):
|
|
103
|
+
```cpp
|
|
104
|
+
#define IS_BRIDGE_NODE false
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Option B: Shared Gateway Mode
|
|
108
|
+
|
|
109
|
+
All nodes connect to the same router:
|
|
110
|
+
```cpp
|
|
111
|
+
mesh.initAsSharedGateway(
|
|
112
|
+
MESH_PREFIX, MESH_PASSWORD,
|
|
113
|
+
ROUTER_SSID, ROUTER_PASSWORD, // Router credentials required!
|
|
114
|
+
&userScheduler, MESH_PORT
|
|
115
|
+
);
|
|
116
|
+
mesh.enableSendToInternet();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Common Issues
|
|
120
|
+
|
|
121
|
+
### "No Internet available - no gateway with Internet found"
|
|
122
|
+
|
|
123
|
+
- Make sure at least one node is initialized as a bridge with router credentials
|
|
124
|
+
- Check that the bridge has successfully connected to the router
|
|
125
|
+
- Use `mesh.hasInternetConnection()` to verify gateway availability
|
|
126
|
+
|
|
127
|
+
### "connection refused" when using HTTPClient directly
|
|
128
|
+
|
|
129
|
+
**DON'T do this on regular mesh nodes:**
|
|
130
|
+
```cpp
|
|
131
|
+
// This FAILS on regular mesh nodes!
|
|
132
|
+
HTTPClient http;
|
|
133
|
+
http.begin("https://api.callmebot.com/...");
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**DO this instead:**
|
|
137
|
+
```cpp
|
|
138
|
+
// This works - routes through gateway
|
|
139
|
+
mesh.sendToInternet("https://api.callmebot.com/...", "", callback);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### WhatsApp message not received
|
|
143
|
+
|
|
144
|
+
1. Verify your Callmebot API key is correct
|
|
145
|
+
2. Ensure phone number includes country code (e.g., `+1234567890`)
|
|
146
|
+
3. Check HTTP status code in callback (200 = success)
|
|
147
|
+
4. URL-encode special characters in the message
|
|
148
|
+
|
|
149
|
+
## Files
|
|
150
|
+
|
|
151
|
+
- `sendToInternet.ino` - Main example sketch
|
|
152
|
+
- `README.md` - This documentation
|
|
153
|
+
|
|
154
|
+
## Related Examples
|
|
155
|
+
|
|
156
|
+
- [sharedGateway](../sharedGateway/) - All nodes with direct Internet access
|
|
157
|
+
- [bridge_failover](../bridge_failover/) - Automatic gateway failover
|
|
158
|
+
- [mqttBridge](../mqttBridge/) - MQTT integration
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
; PlatformIO Project Configuration File
|
|
2
|
+
;
|
|
3
|
+
; sendToInternet Example - WhatsApp/Callmebot Integration
|
|
4
|
+
;
|
|
5
|
+
; Upload to ESP32 or ESP8266 to test mesh.sendToInternet() API
|
|
6
|
+
|
|
7
|
+
[env:esp32dev]
|
|
8
|
+
platform = espressif32
|
|
9
|
+
board = esp32dev
|
|
10
|
+
framework = arduino
|
|
11
|
+
lib_deps =
|
|
12
|
+
alteriom/AlteriomPainlessMesh
|
|
13
|
+
bblanchon/ArduinoJson@^7.0.0
|
|
14
|
+
monitor_speed = 115200
|
|
15
|
+
|
|
16
|
+
[env:esp8266]
|
|
17
|
+
platform = espressif8266
|
|
18
|
+
board = nodemcuv2
|
|
19
|
+
framework = arduino
|
|
20
|
+
lib_deps =
|
|
21
|
+
alteriom/AlteriomPainlessMesh
|
|
22
|
+
bblanchon/ArduinoJson@^7.0.0
|
|
23
|
+
monitor_speed = 115200
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
//************************************************************
|
|
2
|
+
// sendToInternet Example - WhatsApp/Callmebot Integration
|
|
3
|
+
//
|
|
4
|
+
// This example demonstrates how to use mesh.sendToInternet() to send
|
|
5
|
+
// messages to Internet endpoints (like WhatsApp via Callmebot) from
|
|
6
|
+
// ANY node in the mesh network - not just the bridge/gateway.
|
|
7
|
+
//
|
|
8
|
+
// IMPORTANT:
|
|
9
|
+
// Regular mesh nodes do NOT have direct IP routing to the Internet.
|
|
10
|
+
// The sendToInternet() API routes your data THROUGH a gateway node
|
|
11
|
+
// that has Internet access. This is different from making direct
|
|
12
|
+
// HTTP requests (which would fail on regular mesh nodes).
|
|
13
|
+
//
|
|
14
|
+
// How it works:
|
|
15
|
+
// 1. Node calls mesh.sendToInternet() with destination URL and payload
|
|
16
|
+
// 2. The mesh routes the request to a gateway node with Internet
|
|
17
|
+
// 3. Gateway makes the actual HTTP request to the destination
|
|
18
|
+
// 4. Gateway sends acknowledgment back through the mesh
|
|
19
|
+
// 5. Your callback is invoked with the result
|
|
20
|
+
//
|
|
21
|
+
// Use Cases:
|
|
22
|
+
// - Fish farm sensors sending O2 alarms to WhatsApp
|
|
23
|
+
// - Industrial IoT sending alerts to cloud APIs
|
|
24
|
+
// - Smart home sensors reporting to home automation servers
|
|
25
|
+
//
|
|
26
|
+
// Prerequisites:
|
|
27
|
+
// - At least one node must be a bridge/gateway with Internet access
|
|
28
|
+
// - OR use initAsSharedGateway() so all nodes have Internet
|
|
29
|
+
// - Enable sendToInternet() after mesh.init(): mesh.enableSendToInternet()
|
|
30
|
+
//
|
|
31
|
+
// For Callmebot WhatsApp API:
|
|
32
|
+
// - Get your API key from https://www.callmebot.com/blog/free-api-whatsapp-messages/
|
|
33
|
+
// - Format: https://api.callmebot.com/whatsapp.php?phone=PHONE&apikey=KEY&text=MESSAGE
|
|
34
|
+
//
|
|
35
|
+
//************************************************************
|
|
36
|
+
#include "painlessMesh.h"
|
|
37
|
+
#include <WiFiClientSecure.h>
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// Mesh Network Configuration
|
|
41
|
+
// ============================================
|
|
42
|
+
#define MESH_PREFIX "SensorMesh"
|
|
43
|
+
#define MESH_PASSWORD "meshPassword123"
|
|
44
|
+
#define MESH_PORT 5555
|
|
45
|
+
|
|
46
|
+
// ============================================
|
|
47
|
+
// Router Configuration (for bridge/gateway)
|
|
48
|
+
// ============================================
|
|
49
|
+
#define ROUTER_SSID "YourRouterSSID"
|
|
50
|
+
#define ROUTER_PASSWORD "YourRouterPassword"
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// Callmebot/WhatsApp Configuration
|
|
54
|
+
// ============================================
|
|
55
|
+
// Get your API key from: https://www.callmebot.com/blog/free-api-whatsapp-messages/
|
|
56
|
+
#define WHATSAPP_PHONE "+1234567890" // Your phone number with country code
|
|
57
|
+
#define WHATSAPP_APIKEY "your_api_key" // Your Callmebot API key
|
|
58
|
+
|
|
59
|
+
// ============================================
|
|
60
|
+
// Sensor Simulation Configuration
|
|
61
|
+
// ============================================
|
|
62
|
+
// These define the ranges for simulated sensor values
|
|
63
|
+
#define TEMP_MIN 20.0 // Minimum temperature (°C)
|
|
64
|
+
#define TEMP_RANGE 10.0 // Temperature range (20-30°C)
|
|
65
|
+
#define HUMIDITY_MIN 40.0 // Minimum humidity (%)
|
|
66
|
+
#define HUMIDITY_RANGE 40.0 // Humidity range (40-80%)
|
|
67
|
+
#define O2_MIN 5.0 // Minimum O2 level (mg/L)
|
|
68
|
+
#define O2_RANGE 5.0 // O2 range (5-10 mg/L)
|
|
69
|
+
#define O2_ALARM_THRESHOLD 6.0 // O2 level below this triggers alarm
|
|
70
|
+
|
|
71
|
+
// ============================================
|
|
72
|
+
// Mode Selection
|
|
73
|
+
// ============================================
|
|
74
|
+
// Set to true to make this node a bridge with Internet access
|
|
75
|
+
// Set to false for regular mesh nodes that will send via the bridge
|
|
76
|
+
#define IS_BRIDGE_NODE false
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// Task Scheduler and Mesh Instance
|
|
80
|
+
// ============================================
|
|
81
|
+
Scheduler userScheduler;
|
|
82
|
+
painlessMesh mesh;
|
|
83
|
+
|
|
84
|
+
// ============================================
|
|
85
|
+
// Function Prototypes
|
|
86
|
+
// ============================================
|
|
87
|
+
void sendAlertToWhatsApp(String message);
|
|
88
|
+
void sendSensorDataToCloud();
|
|
89
|
+
void receivedCallback(uint32_t from, String& msg);
|
|
90
|
+
void newConnectionCallback(uint32_t nodeId);
|
|
91
|
+
void changedConnectionCallback();
|
|
92
|
+
String urlEncode(const String& str);
|
|
93
|
+
|
|
94
|
+
// ============================================
|
|
95
|
+
// Task Definitions
|
|
96
|
+
// ============================================
|
|
97
|
+
// Task to periodically send sensor data (simulated)
|
|
98
|
+
Task taskSendSensorData(60000, TASK_FOREVER, &sendSensorDataToCloud);
|
|
99
|
+
|
|
100
|
+
// ============================================
|
|
101
|
+
// URL Encoding Helper
|
|
102
|
+
// ============================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* URL-encode a string for safe transmission in URLs
|
|
106
|
+
*
|
|
107
|
+
* Encodes special characters to their percent-encoded equivalents.
|
|
108
|
+
* This is required for WhatsApp messages containing special characters.
|
|
109
|
+
*
|
|
110
|
+
* @param str The string to encode
|
|
111
|
+
* @return URL-encoded string
|
|
112
|
+
*/
|
|
113
|
+
String urlEncode(const String& str) {
|
|
114
|
+
String encoded = "";
|
|
115
|
+
for (size_t i = 0; i < str.length(); i++) {
|
|
116
|
+
char c = str.charAt(i);
|
|
117
|
+
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
|
118
|
+
// Safe characters - no encoding needed
|
|
119
|
+
encoded += c;
|
|
120
|
+
} else if (c == ' ') {
|
|
121
|
+
encoded += "%20";
|
|
122
|
+
} else {
|
|
123
|
+
// Encode other characters as %XX
|
|
124
|
+
char hex[4];
|
|
125
|
+
snprintf(hex, sizeof(hex), "%%%02X", (unsigned char)c);
|
|
126
|
+
encoded += hex;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return encoded;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================
|
|
133
|
+
// sendToInternet() Usage Example
|
|
134
|
+
// ============================================
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Send a WhatsApp message via Callmebot using sendToInternet()
|
|
138
|
+
*
|
|
139
|
+
* This function demonstrates how to use mesh.sendToInternet() to send
|
|
140
|
+
* data to an Internet endpoint. The request is automatically routed
|
|
141
|
+
* through a gateway node that has Internet access.
|
|
142
|
+
*
|
|
143
|
+
* @param message The message to send via WhatsApp
|
|
144
|
+
*/
|
|
145
|
+
void sendAlertToWhatsApp(String message) {
|
|
146
|
+
// Check if Internet is available via any gateway
|
|
147
|
+
if (!mesh.hasInternetConnection()) {
|
|
148
|
+
Serial.println("❌ No Internet available - no gateway with Internet found");
|
|
149
|
+
Serial.println(" Make sure at least one node is a bridge with router access");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// URL-encode the message for safe transmission
|
|
154
|
+
String encodedMessage = urlEncode(message);
|
|
155
|
+
|
|
156
|
+
// Build the Callmebot WhatsApp API URL
|
|
157
|
+
// Format: https://api.callmebot.com/whatsapp.php?phone=PHONE&apikey=KEY&text=MESSAGE
|
|
158
|
+
String url = "https://api.callmebot.com/whatsapp.php";
|
|
159
|
+
url += "?phone=" + String(WHATSAPP_PHONE);
|
|
160
|
+
url += "&apikey=" + String(WHATSAPP_APIKEY);
|
|
161
|
+
url += "&text=" + encodedMessage;
|
|
162
|
+
|
|
163
|
+
Serial.println("\n📱 Sending WhatsApp message via sendToInternet()...");
|
|
164
|
+
Serial.printf(" Message: %s\n", message.c_str());
|
|
165
|
+
Serial.printf(" URL: %s\n", url.c_str());
|
|
166
|
+
|
|
167
|
+
// Use sendToInternet() to route the request through a gateway
|
|
168
|
+
// The callback will be invoked when we get a response (or timeout)
|
|
169
|
+
uint32_t msgId = mesh.sendToInternet(
|
|
170
|
+
url,
|
|
171
|
+
"", // No payload needed for GET request - params are in URL
|
|
172
|
+
[](bool success, uint16_t httpStatus, String error) {
|
|
173
|
+
if (success) {
|
|
174
|
+
Serial.printf("✅ WhatsApp message sent! HTTP Status: %d\n", httpStatus);
|
|
175
|
+
} else {
|
|
176
|
+
Serial.printf("❌ Failed to send WhatsApp: %s (HTTP: %d)\n", error.c_str(), httpStatus);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
static_cast<uint8_t>(painlessmesh::gateway::GatewayPriority::PRIORITY_HIGH)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (msgId > 0) {
|
|
183
|
+
Serial.printf(" Message queued with ID: %u\n", msgId);
|
|
184
|
+
} else {
|
|
185
|
+
Serial.println(" ❌ Failed to queue message - no gateway available");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Send sensor data to a cloud API
|
|
191
|
+
*
|
|
192
|
+
* This demonstrates sending JSON sensor data to a REST API endpoint.
|
|
193
|
+
* In a real application, you would replace the URL with your actual
|
|
194
|
+
* cloud API endpoint (AWS, Azure, ThingsBoard, etc.)
|
|
195
|
+
*/
|
|
196
|
+
void sendSensorDataToCloud() {
|
|
197
|
+
// Simulate sensor readings using configured ranges
|
|
198
|
+
float temperature = TEMP_MIN + random(0, (int)(TEMP_RANGE * 10)) / 10.0;
|
|
199
|
+
float humidity = HUMIDITY_MIN + random(0, (int)(HUMIDITY_RANGE * 10)) / 10.0;
|
|
200
|
+
float o2Level = O2_MIN + random(0, (int)(O2_RANGE * 10)) / 10.0;
|
|
201
|
+
|
|
202
|
+
// Create JSON payload
|
|
203
|
+
String payload = "{";
|
|
204
|
+
payload += "\"nodeId\":" + String(mesh.getNodeId()) + ",";
|
|
205
|
+
payload += "\"temperature\":" + String(temperature, 1) + ",";
|
|
206
|
+
payload += "\"humidity\":" + String(humidity, 1) + ",";
|
|
207
|
+
payload += "\"o2Level\":" + String(o2Level, 1) + ",";
|
|
208
|
+
payload += "\"timestamp\":" + String(millis());
|
|
209
|
+
payload += "}";
|
|
210
|
+
|
|
211
|
+
Serial.println("\n📊 Sending sensor data to cloud...");
|
|
212
|
+
Serial.printf(" Payload: %s\n", payload.c_str());
|
|
213
|
+
|
|
214
|
+
// Check for alarm conditions using configured threshold
|
|
215
|
+
if (o2Level < O2_ALARM_THRESHOLD) {
|
|
216
|
+
// O2 level critical - send WhatsApp alert!
|
|
217
|
+
String alertMsg = "⚠️ ALARM: O2 level critical at " + String(o2Level, 1) + " mg/L! Node: " + String(mesh.getNodeId());
|
|
218
|
+
sendAlertToWhatsApp(alertMsg);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Only send if Internet is available
|
|
222
|
+
if (!mesh.hasInternetConnection()) {
|
|
223
|
+
Serial.println(" ⚠️ No Internet - data not sent (would be queued in production)");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Send to cloud API (replace with your actual endpoint)
|
|
228
|
+
// Example endpoints:
|
|
229
|
+
// - ThingsBoard: "https://demo.thingsboard.io/api/v1/YOUR_TOKEN/telemetry"
|
|
230
|
+
// - AWS IoT: "https://YOUR_ENDPOINT.iot.us-east-1.amazonaws.com/topics/sensors"
|
|
231
|
+
// - Custom API: "https://api.yourserver.com/sensors/data"
|
|
232
|
+
|
|
233
|
+
String cloudUrl = "https://api.example.com/sensors"; // Replace with your endpoint
|
|
234
|
+
|
|
235
|
+
uint32_t msgId = mesh.sendToInternet(
|
|
236
|
+
cloudUrl,
|
|
237
|
+
payload,
|
|
238
|
+
[](bool success, uint16_t httpStatus, String error) {
|
|
239
|
+
if (success) {
|
|
240
|
+
Serial.printf(" ✅ Cloud data sent! HTTP: %d\n", httpStatus);
|
|
241
|
+
} else {
|
|
242
|
+
Serial.printf(" ❌ Cloud send failed: %s\n", error.c_str());
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
Serial.printf(" Message ID: %u\n", msgId);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================
|
|
251
|
+
// Mesh Callbacks
|
|
252
|
+
// ============================================
|
|
253
|
+
|
|
254
|
+
void receivedCallback(uint32_t from, String& msg) {
|
|
255
|
+
Serial.printf("📨 Received from %u: %s\n", from, msg.c_str());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
void newConnectionCallback(uint32_t nodeId) {
|
|
259
|
+
Serial.printf("✓ New connection: Node %u\n", nodeId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
void changedConnectionCallback() {
|
|
263
|
+
Serial.printf("🔄 Mesh topology changed. Nodes: %d\n", mesh.getNodeList().size());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================
|
|
267
|
+
// Setup Function
|
|
268
|
+
// ============================================
|
|
269
|
+
void setup() {
|
|
270
|
+
Serial.begin(115200);
|
|
271
|
+
delay(1000);
|
|
272
|
+
|
|
273
|
+
Serial.println("\n");
|
|
274
|
+
Serial.println("================================================");
|
|
275
|
+
Serial.println(" painlessMesh - sendToInternet Example");
|
|
276
|
+
Serial.println(" WhatsApp/Callmebot Integration Demo");
|
|
277
|
+
Serial.println("================================================\n");
|
|
278
|
+
|
|
279
|
+
// Configure debug output
|
|
280
|
+
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
|
|
281
|
+
|
|
282
|
+
bool success = false;
|
|
283
|
+
|
|
284
|
+
#if IS_BRIDGE_NODE
|
|
285
|
+
// Initialize as bridge with router connection
|
|
286
|
+
Serial.println("Mode: BRIDGE (Gateway with Internet access)\n");
|
|
287
|
+
success = mesh.initAsBridge(
|
|
288
|
+
MESH_PREFIX, MESH_PASSWORD,
|
|
289
|
+
ROUTER_SSID, ROUTER_PASSWORD,
|
|
290
|
+
&userScheduler, MESH_PORT
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (success) {
|
|
294
|
+
Serial.println("✓ Bridge initialized - this node has Internet access");
|
|
295
|
+
Serial.println(" Other nodes can use sendToInternet() through this gateway\n");
|
|
296
|
+
} else {
|
|
297
|
+
Serial.println("✗ Bridge init failed - falling back to regular mesh");
|
|
298
|
+
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
|
|
299
|
+
}
|
|
300
|
+
#else
|
|
301
|
+
// Initialize as regular mesh node
|
|
302
|
+
Serial.println("Mode: REGULAR NODE (sends to Internet via gateway)\n");
|
|
303
|
+
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
|
|
304
|
+
success = true;
|
|
305
|
+
#endif
|
|
306
|
+
|
|
307
|
+
// IMPORTANT: Enable the sendToInternet() API
|
|
308
|
+
mesh.enableSendToInternet();
|
|
309
|
+
|
|
310
|
+
// Register callbacks
|
|
311
|
+
mesh.onReceive(&receivedCallback);
|
|
312
|
+
mesh.onNewConnection(&newConnectionCallback);
|
|
313
|
+
mesh.onChangedConnections(&changedConnectionCallback);
|
|
314
|
+
|
|
315
|
+
// Start periodic sensor data task
|
|
316
|
+
userScheduler.addTask(taskSendSensorData);
|
|
317
|
+
taskSendSensorData.enable();
|
|
318
|
+
|
|
319
|
+
// Print startup info
|
|
320
|
+
Serial.println("================================================");
|
|
321
|
+
Serial.printf("Node ID: %u\n", mesh.getNodeId());
|
|
322
|
+
Serial.printf("Is Bridge: %s\n", mesh.isBridge() ? "YES" : "NO");
|
|
323
|
+
Serial.println("================================================\n");
|
|
324
|
+
|
|
325
|
+
// Send a startup notification via WhatsApp (demonstrates sendToInternet)
|
|
326
|
+
String startupMsg = "🚀 Node " + String(mesh.getNodeId()) + " started!";
|
|
327
|
+
|
|
328
|
+
// Delay to allow mesh to connect first
|
|
329
|
+
Serial.println("Will attempt to send startup WhatsApp in 30 seconds...\n");
|
|
330
|
+
mesh.addTask([startupMsg]() {
|
|
331
|
+
sendAlertToWhatsApp(startupMsg);
|
|
332
|
+
}, 30000); // 30 second delay
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ============================================
|
|
336
|
+
// Main Loop
|
|
337
|
+
// ============================================
|
|
338
|
+
void loop() {
|
|
339
|
+
mesh.update();
|
|
340
|
+
}
|
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.2
|
|
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.2",
|
|
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
|
|
|
@@ -812,12 +877,35 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
812
877
|
}
|
|
813
878
|
|
|
814
879
|
/**
|
|
815
|
-
* Check if any bridge in the mesh has Internet connectivity
|
|
880
|
+
* Check if any bridge/gateway in the mesh has Internet connectivity
|
|
881
|
+
*
|
|
882
|
+
* IMPORTANT: This method checks if a GATEWAY node (bridge) in the mesh has
|
|
883
|
+
* Internet access, NOT whether THIS node can directly make HTTP/HTTPS requests.
|
|
884
|
+
*
|
|
885
|
+
* Regular mesh nodes do NOT have direct IP routing to the Internet. They only
|
|
886
|
+
* communicate via the painlessMesh protocol. To send data to the Internet from
|
|
887
|
+
* a regular node, you must use sendToInternet() which routes through a gateway,
|
|
888
|
+
* or use initAsSharedGateway(meshSSID, meshPwd, ROUTER_SSID, ROUTER_PWD, scheduler, port)
|
|
889
|
+
* to give all nodes direct router access (requires router credentials).
|
|
816
890
|
*
|
|
817
891
|
* Override of base class method to also check if THIS node is a bridge
|
|
818
892
|
* with Internet connectivity, not just other bridges in the mesh.
|
|
819
893
|
*
|
|
894
|
+
* \code
|
|
895
|
+
* if (mesh.hasInternetConnection()) {
|
|
896
|
+
* // A gateway exists - use sendToInternet() to reach Internet
|
|
897
|
+
* mesh.sendToInternet("https://api.example.com", data, callback);
|
|
898
|
+
* }
|
|
899
|
+
*
|
|
900
|
+
* // DON'T DO THIS on regular nodes - will fail with "connection refused":
|
|
901
|
+
* // HTTPClient http;
|
|
902
|
+
* // http.begin("https://api.example.com");
|
|
903
|
+
* \endcode
|
|
904
|
+
*
|
|
820
905
|
* @return true if at least one bridge (including this node) has Internet
|
|
906
|
+
* @see hasLocalInternet() to check if THIS node has direct Internet access
|
|
907
|
+
* @see sendToInternet() to send data to Internet via gateway
|
|
908
|
+
* @see initAsSharedGateway() requires router credentials (ROUTER_SSID, ROUTER_PASSWORD)
|
|
821
909
|
*/
|
|
822
910
|
bool hasInternetConnection() {
|
|
823
911
|
// First check if THIS node is a bridge with Internet
|
|
@@ -1529,6 +1617,95 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1529
1617
|
});
|
|
1530
1618
|
}
|
|
1531
1619
|
|
|
1620
|
+
/**
|
|
1621
|
+
* Attempt to promote an isolated node to bridge
|
|
1622
|
+
*
|
|
1623
|
+
* This method handles the case where a node is isolated (no mesh connections)
|
|
1624
|
+
* but has router credentials. Unlike the election-based promotion, this
|
|
1625
|
+
* directly attempts to connect to the router without requiring mesh connectivity.
|
|
1626
|
+
*
|
|
1627
|
+
* This is useful for:
|
|
1628
|
+
* - Nodes that failed initial bridge setup and need to retry
|
|
1629
|
+
* - Nodes that are the first to start and no mesh exists yet
|
|
1630
|
+
* - Recovery scenarios where mesh network is unavailable
|
|
1631
|
+
*
|
|
1632
|
+
* @return true if promotion was attempted (regardless of success), false if skipped
|
|
1633
|
+
*/
|
|
1634
|
+
bool attemptIsolatedBridgePromotion() {
|
|
1635
|
+
using namespace logger;
|
|
1636
|
+
|
|
1637
|
+
Log(CONNECTION, "=== Isolated Bridge Promotion Attempt ===\n");
|
|
1638
|
+
Log(CONNECTION, "Attempt %d of %d\n", _isolatedBridgeRetryAttempts + 1, MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS);
|
|
1639
|
+
|
|
1640
|
+
// First, scan for router to check if it's visible
|
|
1641
|
+
int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
|
|
1642
|
+
|
|
1643
|
+
if (routerRSSI == 0) {
|
|
1644
|
+
Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router %s not visible\n", routerSSID.c_str());
|
|
1645
|
+
return false; // Don't count as an attempt - router not visible
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// Check minimum RSSI threshold for isolated promotion
|
|
1649
|
+
if (routerRSSI < minimumBridgeRSSI) {
|
|
1650
|
+
Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router RSSI %d dBm below threshold %d dBm\n",
|
|
1651
|
+
routerRSSI, minimumBridgeRSSI);
|
|
1652
|
+
return false; // Don't count as an attempt - signal too weak
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
Log(CONNECTION, "attemptIsolatedBridgePromotion(): Router visible with RSSI %d dBm\n", routerRSSI);
|
|
1656
|
+
Log(CONNECTION, "Attempting direct bridge promotion (bypassing election)\n");
|
|
1657
|
+
|
|
1658
|
+
// Save current mesh configuration
|
|
1659
|
+
uint8_t savedChannel = _meshChannel;
|
|
1660
|
+
|
|
1661
|
+
// Stop current mesh operations
|
|
1662
|
+
this->stop();
|
|
1663
|
+
delay(1000);
|
|
1664
|
+
|
|
1665
|
+
// Attempt to initialize as bridge
|
|
1666
|
+
bool bridgeInitSuccess = this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1667
|
+
mScheduler, _meshPort);
|
|
1668
|
+
|
|
1669
|
+
if (!bridgeInitSuccess) {
|
|
1670
|
+
Log(ERROR, "✗ Isolated bridge promotion failed - router unreachable\n");
|
|
1671
|
+
Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
|
|
1672
|
+
|
|
1673
|
+
// Re-initialize as regular node on the original channel
|
|
1674
|
+
this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
|
|
1675
|
+
savedChannel, _meshHidden, MAX_CONN);
|
|
1676
|
+
|
|
1677
|
+
// Re-configure router credentials for future retry attempts
|
|
1678
|
+
this->setRouterCredentials(routerSSID, routerPassword);
|
|
1679
|
+
this->enableBridgeFailover(true);
|
|
1680
|
+
|
|
1681
|
+
// Notify via callback
|
|
1682
|
+
if (bridgeRoleChangedCallback) {
|
|
1683
|
+
bridgeRoleChangedCallback(false, "Isolated bridge promotion failed - router unreachable");
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
return true; // Count as an attempt - we tried but failed
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Success! Reset retry counter
|
|
1690
|
+
_isolatedBridgeRetryAttempts = 0;
|
|
1691
|
+
lastRoleChangeTime = millis();
|
|
1692
|
+
|
|
1693
|
+
Log(STARTUP, "✓ Isolated bridge promotion complete on channel %d\n", _meshChannel);
|
|
1694
|
+
|
|
1695
|
+
// Notify via callback
|
|
1696
|
+
if (bridgeRoleChangedCallback) {
|
|
1697
|
+
bridgeRoleChangedCallback(true, "Isolated node promoted to bridge");
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// Send bridge status announcement to attract other nodes
|
|
1701
|
+
this->addTask(3000, TASK_ONCE, [this]() {
|
|
1702
|
+
Log(STARTUP, "Sending bridge status announcement on channel %d\n", _meshChannel);
|
|
1703
|
+
this->sendBridgeStatus();
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
return true; // Count as an attempt - we succeeded
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1532
1709
|
/**
|
|
1533
1710
|
* Handle received bridge election package
|
|
1534
1711
|
* Called by package handler when election message arrives
|
|
@@ -1760,6 +1937,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1760
1937
|
std::vector<BridgeCandidate> electionCandidates;
|
|
1761
1938
|
std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
|
|
1762
1939
|
|
|
1940
|
+
// Isolated bridge retry state and configuration
|
|
1941
|
+
uint8_t _isolatedBridgeRetryAttempts = 0;
|
|
1942
|
+
uint32_t _isolatedBridgeRetryResetTime = 0; // Time when retry counter can be reset
|
|
1943
|
+
static const uint8_t MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS = 5; // Max retry attempts before waiting
|
|
1944
|
+
static const uint32_t isolatedBridgeRetryIntervalMs = 60000; // Retry every 60 seconds
|
|
1945
|
+
static const uint32_t isolatedBridgeRetryResetIntervalMs = 300000; // Reset counter after 5 minutes
|
|
1946
|
+
static const uint16_t ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD = 6; // Require 6 empty scans before retrying
|
|
1947
|
+
|
|
1763
1948
|
// Multi-bridge coordination state and configuration
|
|
1764
1949
|
protected:
|
|
1765
1950
|
bool multiBridgeEnabled = false;
|
|
@@ -606,14 +606,42 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
606
606
|
}
|
|
607
607
|
|
|
608
608
|
/**
|
|
609
|
-
* Check if any bridge in the mesh has Internet connectivity
|
|
609
|
+
* Check if any bridge/gateway in the mesh has Internet connectivity
|
|
610
|
+
*
|
|
611
|
+
* IMPORTANT: This method checks if a GATEWAY node (bridge) in the mesh has
|
|
612
|
+
* Internet access, NOT whether THIS node can directly make HTTP/HTTPS requests.
|
|
613
|
+
*
|
|
614
|
+
* Regular mesh nodes do NOT have direct IP routing to the Internet - they only
|
|
615
|
+
* communicate over the painlessMesh protocol. To send data to the Internet from
|
|
616
|
+
* a regular node, you must either:
|
|
617
|
+
*
|
|
618
|
+
* 1. Use sendToInternet() to route data through a gateway node
|
|
619
|
+
* 2. Use initAsSharedGateway(meshSSID, meshPwd, ROUTER_SSID, ROUTER_PWD, scheduler, port)
|
|
620
|
+
* to give all nodes direct router access (requires router credentials)
|
|
621
|
+
* 3. Send mesh messages to a bridge node that handles Internet communication
|
|
622
|
+
*
|
|
623
|
+
* Use hasLocalInternet() to check if THIS specific node has direct Internet access.
|
|
610
624
|
*
|
|
611
625
|
* When connected to mesh, requires recent bridge status (within timeout).
|
|
612
626
|
* When disconnected from mesh, returns true if any bridge previously
|
|
613
627
|
* reported Internet connectivity - allowing operations to be queued for
|
|
614
628
|
* when mesh connectivity is restored.
|
|
615
629
|
*
|
|
616
|
-
*
|
|
630
|
+
* \code
|
|
631
|
+
* if (mesh.hasInternetConnection()) {
|
|
632
|
+
* // A gateway exists with Internet - use sendToInternet() to reach Internet
|
|
633
|
+
* mesh.sendToInternet("https://api.example.com/data", jsonPayload, callback);
|
|
634
|
+
* }
|
|
635
|
+
*
|
|
636
|
+
* // DON'T DO THIS - regular mesh nodes cannot make direct HTTP requests:
|
|
637
|
+
* // HTTPClient http;
|
|
638
|
+
* // http.begin("https://api.example.com"); // Will fail with "connection refused"
|
|
639
|
+
* \endcode
|
|
640
|
+
*
|
|
641
|
+
* @return true if at least one gateway/bridge reports Internet connection
|
|
642
|
+
* @see hasLocalInternet() to check if THIS node has direct Internet access
|
|
643
|
+
* @see sendToInternet() to send data to Internet via gateway
|
|
644
|
+
* @see initAsSharedGateway() to give all nodes direct Internet access (requires router credentials)
|
|
617
645
|
*/
|
|
618
646
|
bool hasInternetConnection() {
|
|
619
647
|
bool hasConnections = hasActiveMeshConnections();
|