@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 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.0** - Major update with improved bridge detection, consolidated examples, and cleaned documentation
7
+ **Version 1.9.2** - Consolidated release across all distribution channels
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)
@@ -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
- Serial.printf("Internet available: %s\n", mesh.hasInternetConnection() ? "YES" : "NO");
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
@@ -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.2",
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.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.0",
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",
@@ -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
- * @return true if at least one bridge reports Internet connection
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();