@alteriom/painlessmesh 1.9.5 → 1.9.7
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 +78 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +60 -184
- package/docs/README.md +1 -1
- package/docs/troubleshooting/common-issues.md +101 -0
- package/docs/troubleshooting/external-device-connection.md +283 -0
- package/examples/bridge/bridge.ino +23 -0
- package/examples/bridge_failover/bridge_failover.ino +27 -0
- package/examples/sendToInternet/sendToInternet.ino +21 -3
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +4 -4
- package/src/AlteriomPainlessMesh.h +3 -3
- package/src/arduino/wifi.hpp +874 -535
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +11 -2
- package/src/painlessmesh/mesh.hpp +6 -2
- package/src/painlessmesh/tcp.hpp +103 -4
package/src/arduino/wifi.hpp
CHANGED
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
#include "painlessmesh/router.hpp"
|
|
14
14
|
#include "painlessmesh/tcp.hpp"
|
|
15
15
|
|
|
16
|
+
#ifdef ESP32
|
|
17
|
+
#include <HTTPClient.h>
|
|
18
|
+
#include <WiFiClientSecure.h>
|
|
19
|
+
#elif defined(ESP8266)
|
|
20
|
+
#include <ESP8266HTTPClient.h>
|
|
21
|
+
#include <WiFiClientSecure.h>
|
|
22
|
+
#endif
|
|
23
|
+
|
|
16
24
|
extern painlessmesh::logger::LogClass Log;
|
|
17
25
|
|
|
18
26
|
namespace painlessmesh {
|
|
@@ -93,21 +101,23 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
93
101
|
// Add bridge election package handler (Type BRIDGE_ELECTION)
|
|
94
102
|
this->callbackList.onPackage(
|
|
95
103
|
protocol::BRIDGE_ELECTION,
|
|
96
|
-
[this](protocol::Variant& variant, std::shared_ptr<Connection>,
|
|
104
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>,
|
|
105
|
+
uint32_t) {
|
|
97
106
|
JsonDocument doc;
|
|
98
107
|
TSTRING str;
|
|
99
108
|
variant.printTo(str);
|
|
100
109
|
deserializeJson(doc, str);
|
|
101
110
|
JsonObject obj = doc.as<JsonObject>();
|
|
102
|
-
|
|
111
|
+
|
|
103
112
|
if (obj["routerRSSI"].is<int>()) {
|
|
104
113
|
uint32_t fromNode = obj["from"];
|
|
105
114
|
int8_t routerRSSI = obj["routerRSSI"];
|
|
106
115
|
uint32_t uptime = obj["uptime"] | 0;
|
|
107
116
|
uint32_t freeMemory = obj["freeMemory"] | 0;
|
|
108
|
-
|
|
109
|
-
this->handleBridgeElection(fromNode, routerRSSI, uptime,
|
|
110
|
-
|
|
117
|
+
|
|
118
|
+
this->handleBridgeElection(fromNode, routerRSSI, uptime,
|
|
119
|
+
freeMemory);
|
|
120
|
+
|
|
111
121
|
Log(CONNECTION, "Bridge election candidate from %u: RSSI %d dBm\n",
|
|
112
122
|
fromNode, routerRSSI);
|
|
113
123
|
}
|
|
@@ -117,21 +127,22 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
117
127
|
// Add bridge takeover package handler (Type BRIDGE_TAKEOVER)
|
|
118
128
|
this->callbackList.onPackage(
|
|
119
129
|
protocol::BRIDGE_TAKEOVER,
|
|
120
|
-
[this](protocol::Variant& variant, std::shared_ptr<Connection>,
|
|
130
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>,
|
|
131
|
+
uint32_t) {
|
|
121
132
|
JsonDocument doc;
|
|
122
133
|
TSTRING str;
|
|
123
134
|
variant.printTo(str);
|
|
124
135
|
deserializeJson(doc, str);
|
|
125
136
|
JsonObject obj = doc.as<JsonObject>();
|
|
126
|
-
|
|
137
|
+
|
|
127
138
|
if (obj["previousBridge"].is<unsigned int>()) {
|
|
128
139
|
uint32_t newBridge = obj["from"];
|
|
129
140
|
uint32_t previousBridge = obj["previousBridge"];
|
|
130
141
|
TSTRING reason = obj["reason"].as<TSTRING>();
|
|
131
|
-
|
|
142
|
+
|
|
132
143
|
Log(CONNECTION, "Bridge takeover: Node %u replaced %u (%s)\n",
|
|
133
144
|
newBridge, previousBridge, reason.c_str());
|
|
134
|
-
|
|
145
|
+
|
|
135
146
|
// Notify callback if this node was not the winner
|
|
136
147
|
if (newBridge != this->nodeId && bridgeRoleChangedCallback) {
|
|
137
148
|
bridgeRoleChangedCallback(false, "Another node won election");
|
|
@@ -141,17 +152,19 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
141
152
|
});
|
|
142
153
|
|
|
143
154
|
// Add callback to detect bridge failures and trigger elections
|
|
144
|
-
this->onBridgeStatusChanged([this](uint32_t bridgeNodeId,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
this->onBridgeStatusChanged([this](uint32_t bridgeNodeId,
|
|
156
|
+
bool hasInternet) {
|
|
157
|
+
if (!hasInternet && bridgeFailoverEnabled &&
|
|
158
|
+
routerCredentialsConfigured) {
|
|
159
|
+
Log(CONNECTION, "Bridge %u lost Internet, considering election...\n",
|
|
160
|
+
bridgeNodeId);
|
|
161
|
+
|
|
148
162
|
// Check if we still have any healthy bridges
|
|
149
163
|
if (!this->hasInternetConnection()) {
|
|
150
164
|
Log(CONNECTION, "No healthy bridges, starting election\n");
|
|
151
165
|
// Small delay to let all nodes detect the failure
|
|
152
|
-
this->addTask(2000, TASK_ONCE,
|
|
153
|
-
|
|
154
|
-
});
|
|
166
|
+
this->addTask(2000, TASK_ONCE,
|
|
167
|
+
[this]() { this->startBridgeElection(); });
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
});
|
|
@@ -163,27 +176,28 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
163
176
|
if (!bridgeFailoverEnabled || !routerCredentialsConfigured) {
|
|
164
177
|
return;
|
|
165
178
|
}
|
|
166
|
-
|
|
179
|
+
|
|
167
180
|
// Don't check if we're already a bridge
|
|
168
181
|
if (this->isBridge()) {
|
|
169
182
|
return;
|
|
170
183
|
}
|
|
171
|
-
|
|
184
|
+
|
|
172
185
|
// Skip check during startup period to allow initial bridge discovery
|
|
173
186
|
if (millis() < electionStartupDelayMs) {
|
|
174
187
|
return;
|
|
175
188
|
}
|
|
176
|
-
|
|
189
|
+
|
|
177
190
|
// IMPORTANT: Don't trigger election if we're disconnected from the mesh
|
|
178
191
|
// When isolated, we can't receive bridge status broadcasts, so lack of
|
|
179
|
-
// healthy bridge could simply mean WE are disconnected, not that the
|
|
180
|
-
// is unavailable. Wait until mesh connectivity is restored before
|
|
181
|
-
// an election.
|
|
192
|
+
// healthy bridge could simply mean WE are disconnected, not that the
|
|
193
|
+
// bridge is unavailable. Wait until mesh connectivity is restored before
|
|
194
|
+
// considering an election.
|
|
182
195
|
if (!this->hasActiveMeshConnections()) {
|
|
183
|
-
Log(CONNECTION,
|
|
196
|
+
Log(CONNECTION,
|
|
197
|
+
"Bridge monitor: Skipping - no active mesh connections\n");
|
|
184
198
|
return;
|
|
185
199
|
}
|
|
186
|
-
|
|
200
|
+
|
|
187
201
|
// Check if there are any healthy bridges
|
|
188
202
|
bool hasHealthyBridge = false;
|
|
189
203
|
for (const auto& bridge : this->getBridges()) {
|
|
@@ -192,16 +206,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
192
206
|
break;
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
|
-
|
|
209
|
+
|
|
196
210
|
// If no healthy bridge exists, trigger an election
|
|
197
211
|
if (!hasHealthyBridge) {
|
|
198
|
-
Log(CONNECTION,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
Log(CONNECTION,
|
|
213
|
+
"Bridge monitor: No healthy bridge detected, triggering "
|
|
214
|
+
"election\n");
|
|
215
|
+
// Random delay to prevent simultaneous elections when multiple nodes
|
|
216
|
+
// start together
|
|
217
|
+
uint32_t randomDelay =
|
|
218
|
+
random(electionRandomDelayMinMs, electionRandomDelayMaxMs);
|
|
219
|
+
Log(CONNECTION, "Bridge monitor: Scheduling election in %u ms\n",
|
|
220
|
+
randomDelay);
|
|
221
|
+
this->addTask(randomDelay, TASK_ONCE,
|
|
222
|
+
[this]() { this->startBridgeElection(); });
|
|
205
223
|
}
|
|
206
224
|
});
|
|
207
225
|
|
|
@@ -210,62 +228,83 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
210
228
|
// - Has router credentials configured
|
|
211
229
|
// - Is isolated (no mesh connections)
|
|
212
230
|
// - Should attempt to become a bridge directly
|
|
213
|
-
// This is different from the election mechanism which requires mesh
|
|
231
|
+
// This is different from the election mechanism which requires mesh
|
|
232
|
+
// connectivity
|
|
214
233
|
this->addTask(isolatedBridgeRetryIntervalMs, TASK_FOREVER, [this]() {
|
|
215
234
|
// Only retry if failover is enabled and we have credentials
|
|
216
235
|
if (!bridgeFailoverEnabled || !routerCredentialsConfigured) {
|
|
217
236
|
return;
|
|
218
237
|
}
|
|
219
|
-
|
|
238
|
+
|
|
220
239
|
// Don't retry if we're already a bridge
|
|
221
240
|
if (this->isBridge()) {
|
|
222
241
|
return;
|
|
223
242
|
}
|
|
224
|
-
|
|
243
|
+
|
|
225
244
|
// Skip during startup period
|
|
226
245
|
if (millis() < electionStartupDelayMs) {
|
|
227
246
|
return;
|
|
228
247
|
}
|
|
229
|
-
|
|
248
|
+
|
|
230
249
|
// Only retry when isolated (no mesh connections found)
|
|
231
250
|
if (this->hasActiveMeshConnections()) {
|
|
232
|
-
// Reset retry counter when mesh is active
|
|
251
|
+
// Reset retry counter and pending flag when mesh is active (no longer
|
|
252
|
+
// isolated)
|
|
233
253
|
_isolatedBridgeRetryAttempts = 0;
|
|
254
|
+
_isolatedRetryPending = false;
|
|
234
255
|
return;
|
|
235
256
|
}
|
|
236
|
-
|
|
257
|
+
|
|
237
258
|
// Limit retry attempts with reset after timeout
|
|
238
259
|
if (_isolatedBridgeRetryAttempts >= MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS) {
|
|
239
260
|
// Check if enough time has passed to reset the counter
|
|
240
261
|
if (millis() > _isolatedBridgeRetryResetTime) {
|
|
241
|
-
Log(CONNECTION,
|
|
262
|
+
Log(CONNECTION,
|
|
263
|
+
"Isolated bridge retry: Reset timeout reached, resetting attempt "
|
|
264
|
+
"counter\n");
|
|
242
265
|
_isolatedBridgeRetryAttempts = 0;
|
|
243
266
|
} else {
|
|
244
|
-
Log(CONNECTION,
|
|
245
|
-
|
|
267
|
+
Log(CONNECTION,
|
|
268
|
+
"Isolated bridge retry: Max attempts (%d) reached, reset in %u "
|
|
269
|
+
"seconds\n",
|
|
270
|
+
MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS,
|
|
271
|
+
(_isolatedBridgeRetryResetTime - millis()) / 1000);
|
|
246
272
|
return;
|
|
247
273
|
}
|
|
248
274
|
}
|
|
249
|
-
|
|
250
|
-
// Check if mesh network exists on any channel before trying to become
|
|
251
|
-
// If mesh exists but we can't connect, don't try to become bridge
|
|
275
|
+
|
|
276
|
+
// Check if mesh network exists on any channel before trying to become
|
|
277
|
+
// bridge If mesh exists but we can't connect, don't try to become bridge
|
|
278
|
+
// Skip this check if we already confirmed isolation from a previous
|
|
279
|
+
// failed attempt
|
|
252
280
|
uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
|
|
253
|
-
if (
|
|
254
|
-
|
|
281
|
+
if (!_isolatedRetryPending &&
|
|
282
|
+
emptyScans < ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD) {
|
|
283
|
+
Log(CONNECTION,
|
|
284
|
+
"Isolated bridge retry: Only %d empty scans, waiting for more "
|
|
285
|
+
"scans\n",
|
|
255
286
|
emptyScans);
|
|
256
287
|
return;
|
|
257
288
|
}
|
|
258
|
-
|
|
259
|
-
|
|
289
|
+
|
|
290
|
+
// Clear the pending flag now that we're proceeding
|
|
291
|
+
_isolatedRetryPending = false;
|
|
292
|
+
|
|
293
|
+
Log(CONNECTION,
|
|
294
|
+
"Isolated bridge retry: Node isolated with %d empty scans, "
|
|
295
|
+
"attempting bridge promotion\n",
|
|
260
296
|
emptyScans);
|
|
261
|
-
|
|
262
|
-
// Attempt to become bridge directly (bypassing election since we're
|
|
263
|
-
// Only increment retry counter if we actually attempted
|
|
297
|
+
|
|
298
|
+
// Attempt to become bridge directly (bypassing election since we're
|
|
299
|
+
// isolated) Only increment retry counter if we actually attempted
|
|
300
|
+
// promotion
|
|
264
301
|
if (this->attemptIsolatedBridgePromotion()) {
|
|
265
302
|
_isolatedBridgeRetryAttempts++;
|
|
266
303
|
// Set reset time when reaching max attempts
|
|
267
|
-
if (_isolatedBridgeRetryAttempts >=
|
|
268
|
-
|
|
304
|
+
if (_isolatedBridgeRetryAttempts >=
|
|
305
|
+
MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS) {
|
|
306
|
+
_isolatedBridgeRetryResetTime =
|
|
307
|
+
millis() + isolatedBridgeRetryResetIntervalMs;
|
|
269
308
|
}
|
|
270
309
|
}
|
|
271
310
|
});
|
|
@@ -277,16 +316,16 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
277
316
|
if (connectMode & WIFI_AP) {
|
|
278
317
|
apInit(nodeId); // setup AP
|
|
279
318
|
}
|
|
280
|
-
|
|
319
|
+
|
|
281
320
|
// Initialize TCP server AFTER AP is configured
|
|
282
321
|
// This ensures the network interfaces are ready when the server starts
|
|
283
322
|
// Fixes TCP connection error -14 when nodes try to connect to bridge
|
|
284
323
|
tcpServerInit();
|
|
285
|
-
|
|
324
|
+
|
|
286
325
|
if (connectMode & WIFI_STA) {
|
|
287
326
|
this->initStation();
|
|
288
327
|
}
|
|
289
|
-
|
|
328
|
+
|
|
290
329
|
// If station credentials provided, connect to router
|
|
291
330
|
if (!stationSSID.isEmpty() && (connectMode & WIFI_STA)) {
|
|
292
331
|
Log(STARTUP, "init(): Connecting to station %s\n", stationSSID.c_str());
|
|
@@ -311,30 +350,29 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
311
350
|
* @param connectMode Switch between WIFI_AP, WIFI_STA and WIFI_AP_STA
|
|
312
351
|
* (default) mode
|
|
313
352
|
*/
|
|
314
|
-
void init(TSTRING ssid, TSTRING password, Scheduler
|
|
353
|
+
void init(TSTRING ssid, TSTRING password, Scheduler* baseScheduler,
|
|
315
354
|
uint16_t port = 5555, WiFiMode_t connectMode = WIFI_AP_STA,
|
|
316
|
-
uint8_t channel = 1, uint8_t hidden = 0,
|
|
317
|
-
uint8_t maxconn = MAX_CONN,
|
|
355
|
+
uint8_t channel = 1, uint8_t hidden = 0, uint8_t maxconn = MAX_CONN,
|
|
318
356
|
TSTRING stationSSID = "", TSTRING stationPassword = "") {
|
|
319
357
|
this->setScheduler(baseScheduler);
|
|
320
|
-
init(ssid, password, port, connectMode, channel, hidden, maxconn,
|
|
358
|
+
init(ssid, password, port, connectMode, channel, hidden, maxconn,
|
|
321
359
|
stationSSID, stationPassword);
|
|
322
360
|
}
|
|
323
361
|
|
|
324
362
|
/**
|
|
325
363
|
* Initialize mesh as a bridge node with automatic channel detection
|
|
326
|
-
*
|
|
364
|
+
*
|
|
327
365
|
* This method connects to a router first, detects its channel, then
|
|
328
366
|
* initializes the mesh on the same channel. This ensures the bridge
|
|
329
367
|
* can maintain both router and mesh connections on the same channel.
|
|
330
|
-
*
|
|
368
|
+
*
|
|
331
369
|
* The bridge node will automatically:
|
|
332
370
|
* - Connect to the specified router in STA mode
|
|
333
371
|
* - Detect the router's WiFi channel
|
|
334
372
|
* - Initialize the mesh AP on the detected channel
|
|
335
373
|
* - Set itself as root node
|
|
336
374
|
* - Maintain the router connection
|
|
337
|
-
*
|
|
375
|
+
*
|
|
338
376
|
* @param meshSSID The name of your mesh network
|
|
339
377
|
* @param meshPassword WiFi password for the mesh
|
|
340
378
|
* @param routerSSID SSID of the router to connect to
|
|
@@ -342,18 +380,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
342
380
|
* @param baseScheduler Task scheduler for mesh operations
|
|
343
381
|
* @param port TCP port for mesh communication (default: 5555)
|
|
344
382
|
*/
|
|
345
|
-
bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
|
|
346
|
-
TSTRING
|
|
347
|
-
|
|
383
|
+
bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword, TSTRING routerSSID,
|
|
384
|
+
TSTRING routerPassword, Scheduler* baseScheduler,
|
|
385
|
+
uint16_t port = 5555) {
|
|
348
386
|
using namespace logger;
|
|
349
|
-
|
|
387
|
+
|
|
350
388
|
Log(STARTUP, "=== Bridge Mode Initialization ===\n");
|
|
351
389
|
Log(STARTUP, "Step 1: Connecting to router %s...\n", routerSSID.c_str());
|
|
352
|
-
|
|
390
|
+
|
|
353
391
|
// Step 1: Connect to router first to detect its channel
|
|
354
392
|
// Shut Wifi down and start with a blank slate
|
|
355
393
|
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
356
|
-
|
|
394
|
+
|
|
357
395
|
Log(STARTUP, "initAsBridge(): %d\n",
|
|
358
396
|
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
|
359
397
|
WiFi.setAutoReconnect(false));
|
|
@@ -362,10 +400,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
362
400
|
#endif
|
|
363
401
|
WiFi.persistent(false);
|
|
364
402
|
WiFi.mode(WIFI_STA);
|
|
365
|
-
|
|
403
|
+
|
|
366
404
|
// Connect to router and wait for connection
|
|
367
405
|
WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
|
|
368
|
-
|
|
406
|
+
|
|
369
407
|
// Wait for connection (with timeout)
|
|
370
408
|
int timeout = 30; // 30 seconds timeout
|
|
371
409
|
while (WiFi.status() != WL_CONNECTED && timeout > 0) {
|
|
@@ -373,14 +411,16 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
373
411
|
timeout--;
|
|
374
412
|
Log(STARTUP, ".");
|
|
375
413
|
}
|
|
376
|
-
|
|
414
|
+
|
|
377
415
|
uint8_t detectedChannel = 1; // Default fallback
|
|
378
|
-
|
|
416
|
+
|
|
379
417
|
if (WiFi.status() == WL_CONNECTED) {
|
|
380
418
|
detectedChannel = WiFi.channel();
|
|
381
419
|
// Validate channel is in valid range (1-13 for 2.4GHz)
|
|
382
420
|
if (detectedChannel < 1 || detectedChannel > 13) {
|
|
383
|
-
Log(ERROR,
|
|
421
|
+
Log(ERROR,
|
|
422
|
+
"\n✗ Invalid channel detected: %d, falling back to channel 1\n",
|
|
423
|
+
detectedChannel);
|
|
384
424
|
detectedChannel = 1;
|
|
385
425
|
} else {
|
|
386
426
|
Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
|
|
@@ -392,29 +432,33 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
392
432
|
Log(ERROR, "Bridge initialization aborted - remaining as regular node\n");
|
|
393
433
|
return false;
|
|
394
434
|
}
|
|
395
|
-
|
|
396
|
-
Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
|
|
397
|
-
|
|
435
|
+
|
|
436
|
+
Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
|
|
437
|
+
detectedChannel);
|
|
438
|
+
|
|
398
439
|
// Step 2: Initialize mesh on detected channel
|
|
399
|
-
init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
|
|
440
|
+
init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
|
|
400
441
|
detectedChannel, 0, MAX_CONN);
|
|
401
|
-
|
|
442
|
+
|
|
402
443
|
// Allow network stack to stabilize after AP and TCP server initialization
|
|
403
444
|
// This prevents TCP connection errors (-14) when nodes connect
|
|
404
445
|
delay(100);
|
|
405
|
-
|
|
446
|
+
|
|
406
447
|
Log(STARTUP, "Step 3: Establishing bridge connection...\n");
|
|
407
|
-
|
|
448
|
+
|
|
408
449
|
// Step 3: Re-establish router connection using stationManual
|
|
409
450
|
stationManual(routerSSID, routerPassword, 0);
|
|
410
|
-
|
|
451
|
+
|
|
411
452
|
// Step 4: Configure as root/bridge node
|
|
412
453
|
this->setRoot(true);
|
|
413
454
|
this->setContainsRoot(true);
|
|
414
|
-
|
|
455
|
+
|
|
415
456
|
// Step 5: Setup bridge status broadcasting
|
|
416
457
|
initBridgeStatusBroadcast();
|
|
417
|
-
|
|
458
|
+
|
|
459
|
+
// Step 6: Setup gateway Internet handler
|
|
460
|
+
initGatewayInternetHandler();
|
|
461
|
+
|
|
418
462
|
Log(STARTUP, "=== Bridge Mode Active ===\n");
|
|
419
463
|
Log(STARTUP, " Mesh SSID: %s\n", meshSSID.c_str());
|
|
420
464
|
Log(STARTUP, " Mesh Channel: %d (matches router)\n", detectedChannel);
|
|
@@ -425,28 +469,30 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
425
469
|
|
|
426
470
|
/**
|
|
427
471
|
* Initialize mesh as a bridge node with priority (for multi-bridge mode)
|
|
428
|
-
*
|
|
429
|
-
* This overload adds bridge priority configuration for multi-bridge
|
|
430
|
-
* Priority determines which bridge is preferred when multiple
|
|
431
|
-
*
|
|
472
|
+
*
|
|
473
|
+
* This overload adds bridge priority configuration for multi-bridge
|
|
474
|
+
* deployments. Priority determines which bridge is preferred when multiple
|
|
475
|
+
* bridges are available.
|
|
476
|
+
*
|
|
432
477
|
* @param meshSSID The name of your mesh network
|
|
433
478
|
* @param meshPassword WiFi password for the mesh
|
|
434
479
|
* @param routerSSID SSID of the router to connect to
|
|
435
480
|
* @param routerPassword Password for the router
|
|
436
481
|
* @param baseScheduler Task scheduler for mesh operations
|
|
437
482
|
* @param port TCP port for mesh communication (default: 5555)
|
|
438
|
-
* @param priority Bridge priority: 10=highest (primary), 5=medium
|
|
483
|
+
* @param priority Bridge priority: 10=highest (primary), 5=medium
|
|
484
|
+
* (secondary), 1=lowest (default: 5)
|
|
439
485
|
*/
|
|
440
|
-
bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
|
|
441
|
-
TSTRING
|
|
442
|
-
|
|
486
|
+
bool initAsBridge(TSTRING meshSSID, TSTRING meshPassword, TSTRING routerSSID,
|
|
487
|
+
TSTRING routerPassword, Scheduler* baseScheduler,
|
|
488
|
+
uint16_t port, uint8_t priority) {
|
|
443
489
|
using namespace logger;
|
|
444
|
-
|
|
490
|
+
|
|
445
491
|
// Validate and store priority
|
|
446
492
|
if (priority < 1) priority = 1;
|
|
447
493
|
if (priority > 10) priority = 10;
|
|
448
494
|
bridgePriority = priority;
|
|
449
|
-
|
|
495
|
+
|
|
450
496
|
// Store role based on priority
|
|
451
497
|
if (priority >= 8) {
|
|
452
498
|
bridgeRole = "primary";
|
|
@@ -455,36 +501,38 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
455
501
|
} else {
|
|
456
502
|
bridgeRole = "standby";
|
|
457
503
|
}
|
|
458
|
-
|
|
459
|
-
Log(STARTUP,
|
|
504
|
+
|
|
505
|
+
Log(STARTUP,
|
|
506
|
+
"=== Bridge Mode Initialization (Priority: %d, Role: %s) ===\n",
|
|
460
507
|
priority, bridgeRole.c_str());
|
|
461
|
-
|
|
508
|
+
|
|
462
509
|
// Call the base initAsBridge method
|
|
463
|
-
bool success = initAsBridge(meshSSID, meshPassword, routerSSID,
|
|
464
|
-
|
|
510
|
+
bool success = initAsBridge(meshSSID, meshPassword, routerSSID,
|
|
511
|
+
routerPassword, baseScheduler, port);
|
|
512
|
+
|
|
465
513
|
// Setup multi-bridge coordination if enabled and bridge init succeeded
|
|
466
514
|
if (success && multiBridgeEnabled) {
|
|
467
515
|
initBridgeCoordination();
|
|
468
516
|
}
|
|
469
|
-
|
|
517
|
+
|
|
470
518
|
return success;
|
|
471
519
|
}
|
|
472
520
|
|
|
473
521
|
/**
|
|
474
522
|
* Initialize mesh as a shared gateway node
|
|
475
|
-
*
|
|
523
|
+
*
|
|
476
524
|
* This method initializes all mesh nodes in AP+STA mode with router
|
|
477
525
|
* connectivity. Unlike initAsBridge() which creates a single bridge node,
|
|
478
526
|
* initAsSharedGateway() allows all nodes to connect to the router while
|
|
479
527
|
* maintaining mesh communication.
|
|
480
|
-
*
|
|
528
|
+
*
|
|
481
529
|
* Key features:
|
|
482
530
|
* - All nodes operate in AP+STA mode
|
|
483
531
|
* - All nodes connect to the same router
|
|
484
532
|
* - Mesh and router operate on the same channel for reliability
|
|
485
533
|
* - Automatic router reconnection on disconnect
|
|
486
534
|
* - Channel synchronization between mesh and router
|
|
487
|
-
*
|
|
535
|
+
*
|
|
488
536
|
* @param meshPrefix The name prefix for the mesh network
|
|
489
537
|
* @param meshPassword WiFi password for the mesh network
|
|
490
538
|
* @param routerSSID SSID of the router to connect to
|
|
@@ -494,14 +542,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
494
542
|
* @param config SharedGatewayConfig with advanced settings (optional)
|
|
495
543
|
* @return true if initialization succeeded, false otherwise
|
|
496
544
|
*/
|
|
497
|
-
bool initAsSharedGateway(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
545
|
+
bool initAsSharedGateway(
|
|
546
|
+
TSTRING meshPrefix, TSTRING meshPassword, TSTRING routerSSID,
|
|
547
|
+
TSTRING routerPassword, Scheduler* userScheduler, uint16_t port = 5555,
|
|
548
|
+
gateway::SharedGatewayConfig config = gateway::SharedGatewayConfig()) {
|
|
501
549
|
using namespace logger;
|
|
502
|
-
|
|
550
|
+
|
|
503
551
|
Log(STARTUP, "=== Shared Gateway Mode Initialization ===\n");
|
|
504
|
-
|
|
552
|
+
|
|
505
553
|
// Validate configuration if enabled
|
|
506
554
|
if (config.enabled) {
|
|
507
555
|
auto result = config.validate();
|
|
@@ -511,20 +559,21 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
511
559
|
return false;
|
|
512
560
|
}
|
|
513
561
|
}
|
|
514
|
-
|
|
562
|
+
|
|
515
563
|
// Store shared gateway configuration
|
|
516
564
|
_sharedGatewayConfig = config;
|
|
517
565
|
_sharedGatewayConfig.routerSSID = routerSSID;
|
|
518
566
|
_sharedGatewayConfig.routerPassword = routerPassword;
|
|
519
567
|
_sharedGatewayConfig.enabled = true;
|
|
520
568
|
_sharedGatewayMode = true;
|
|
521
|
-
|
|
522
|
-
Log(STARTUP, "Step 1: Scanning for router %s to detect channel...\n",
|
|
523
|
-
|
|
569
|
+
|
|
570
|
+
Log(STARTUP, "Step 1: Scanning for router %s to detect channel...\n",
|
|
571
|
+
routerSSID.c_str());
|
|
572
|
+
|
|
524
573
|
// Step 1: Scan for router to detect its channel
|
|
525
574
|
// We need to ensure mesh and router operate on the same channel
|
|
526
575
|
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
527
|
-
|
|
576
|
+
|
|
528
577
|
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
|
529
578
|
WiFi.setAutoReconnect(false);
|
|
530
579
|
Log(STARTUP, "initAsSharedGateway(): AutoReconnect disabled\n");
|
|
@@ -534,10 +583,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
534
583
|
#endif
|
|
535
584
|
WiFi.persistent(false);
|
|
536
585
|
WiFi.mode(WIFI_STA);
|
|
537
|
-
|
|
586
|
+
|
|
538
587
|
// Connect to router to detect channel
|
|
539
588
|
WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
|
|
540
|
-
|
|
589
|
+
|
|
541
590
|
// Wait for connection with timeout (using constant for configurability)
|
|
542
591
|
int timeout = ROUTER_CONNECTION_TIMEOUT_SECONDS;
|
|
543
592
|
while (WiFi.status() != WL_CONNECTED && timeout > 0) {
|
|
@@ -545,14 +594,17 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
545
594
|
timeout--;
|
|
546
595
|
Log(STARTUP, ".");
|
|
547
596
|
}
|
|
548
|
-
|
|
597
|
+
|
|
549
598
|
uint8_t detectedChannel = 1; // Default fallback
|
|
550
|
-
|
|
599
|
+
|
|
551
600
|
if (WiFi.status() == WL_CONNECTED) {
|
|
552
601
|
detectedChannel = WiFi.channel();
|
|
553
602
|
// Validate channel is in valid range (1-14 for 2.4GHz, region-dependent)
|
|
554
|
-
if (detectedChannel < MIN_WIFI_CHANNEL ||
|
|
555
|
-
|
|
603
|
+
if (detectedChannel < MIN_WIFI_CHANNEL ||
|
|
604
|
+
detectedChannel > MAX_WIFI_CHANNEL) {
|
|
605
|
+
Log(ERROR,
|
|
606
|
+
"\n✗ Invalid channel detected: %d, falling back to channel 1\n",
|
|
607
|
+
detectedChannel);
|
|
556
608
|
detectedChannel = 1;
|
|
557
609
|
} else {
|
|
558
610
|
Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
|
|
@@ -560,58 +612,64 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
560
612
|
}
|
|
561
613
|
} else {
|
|
562
614
|
Log(ERROR, "\n✗ Failed to connect to router during channel detection\n");
|
|
563
|
-
Log(ERROR,
|
|
615
|
+
Log(ERROR,
|
|
616
|
+
"Continuing with default channel 1, will retry router connection "
|
|
617
|
+
"later\n");
|
|
564
618
|
}
|
|
565
|
-
|
|
619
|
+
|
|
566
620
|
// Disconnect from router, we'll reconnect after mesh init
|
|
567
621
|
WiFi.disconnect();
|
|
568
622
|
delay(100);
|
|
569
|
-
|
|
570
|
-
Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
|
|
571
|
-
|
|
623
|
+
|
|
624
|
+
Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n",
|
|
625
|
+
detectedChannel);
|
|
626
|
+
|
|
572
627
|
// Step 2: Initialize mesh on detected channel with AP+STA mode
|
|
573
628
|
// Set scheduler before init
|
|
574
629
|
this->setScheduler(userScheduler);
|
|
575
|
-
init(meshPrefix, meshPassword, port, WIFI_AP_STA, detectedChannel, 0,
|
|
576
|
-
|
|
630
|
+
init(meshPrefix, meshPassword, port, WIFI_AP_STA, detectedChannel, 0,
|
|
631
|
+
MAX_CONN);
|
|
632
|
+
|
|
577
633
|
// Allow network stack to stabilize after AP and TCP server initialization
|
|
578
634
|
// This prevents TCP connection errors (-14) when nodes connect
|
|
579
635
|
delay(100);
|
|
580
|
-
|
|
581
|
-
Log(STARTUP,
|
|
582
|
-
|
|
636
|
+
|
|
637
|
+
Log(STARTUP,
|
|
638
|
+
"Step 3: Establishing router connection in shared gateway mode...\n");
|
|
639
|
+
|
|
583
640
|
// Step 3: Establish router connection using stationManual
|
|
584
641
|
// Port 0 means we don't expect TCP mesh connection to the router
|
|
585
642
|
stationManual(routerSSID, routerPassword, 0);
|
|
586
|
-
|
|
643
|
+
|
|
587
644
|
// Step 4: Setup router connection monitoring and reconnection logic
|
|
588
645
|
initSharedGatewayMonitoring();
|
|
589
|
-
|
|
646
|
+
|
|
647
|
+
// Step 5: Setup gateway Internet handler
|
|
648
|
+
initGatewayInternetHandler();
|
|
649
|
+
|
|
590
650
|
// Store router credentials for reconnection
|
|
591
651
|
setRouterCredentials(routerSSID, routerPassword);
|
|
592
|
-
|
|
652
|
+
|
|
593
653
|
Log(STARTUP, "=== Shared Gateway Mode Active ===\n");
|
|
594
654
|
Log(STARTUP, " Mesh Prefix: %s\n", meshPrefix.c_str());
|
|
595
655
|
Log(STARTUP, " Mesh Channel: %d (synced with router)\n", detectedChannel);
|
|
596
656
|
Log(STARTUP, " Router: %s\n", routerSSID.c_str());
|
|
597
657
|
Log(STARTUP, " Port: %d\n", port);
|
|
598
658
|
Log(STARTUP, " Mode: AP+STA (all nodes can connect to router)\n");
|
|
599
|
-
|
|
659
|
+
|
|
600
660
|
return true;
|
|
601
661
|
}
|
|
602
662
|
|
|
603
663
|
/**
|
|
604
664
|
* Check if shared gateway mode is enabled
|
|
605
|
-
*
|
|
665
|
+
*
|
|
606
666
|
* @return true if node is operating in shared gateway mode
|
|
607
667
|
*/
|
|
608
|
-
bool isSharedGatewayMode() const {
|
|
609
|
-
return _sharedGatewayMode;
|
|
610
|
-
}
|
|
668
|
+
bool isSharedGatewayMode() const { return _sharedGatewayMode; }
|
|
611
669
|
|
|
612
670
|
/**
|
|
613
671
|
* Get the shared gateway configuration
|
|
614
|
-
*
|
|
672
|
+
*
|
|
615
673
|
* @return const reference to the SharedGatewayConfig
|
|
616
674
|
*/
|
|
617
675
|
const gateway::SharedGatewayConfig& getSharedGatewayConfig() const {
|
|
@@ -693,16 +751,25 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
693
751
|
|
|
694
752
|
/**
|
|
695
753
|
* Establish TCP connection to mesh network
|
|
696
|
-
*
|
|
754
|
+
*
|
|
697
755
|
* This method is called by WiFi event handlers when station gets IP address.
|
|
698
756
|
* It creates a TCP client connection to the mesh network gateway.
|
|
699
|
-
*
|
|
757
|
+
*
|
|
700
758
|
* Architecture Note: This is intentionally kept in the Mesh class rather than
|
|
701
759
|
* extracted to a separate StationConnection class because:
|
|
702
760
|
* - It's tightly coupled with WiFi event lifecycle
|
|
703
761
|
* - Needs access to mesh state and callbacks
|
|
704
762
|
* - Moving it would increase complexity without clear benefits
|
|
705
763
|
* - The existing design keeps connection logic cohesive with WiFi management
|
|
764
|
+
*
|
|
765
|
+
* TCP Connection Retry:
|
|
766
|
+
* The TCP connection now includes automatic retry with exponential backoff.
|
|
767
|
+
* If the initial connection fails (error -14 ERR_CONN or other errors),
|
|
768
|
+
* the system will retry up to TCP_CONNECT_MAX_RETRIES times before
|
|
769
|
+
* triggering a full WiFi reconnection cycle. This helps handle:
|
|
770
|
+
* - Timing issues where TCP server is not ready immediately
|
|
771
|
+
* - Network stack stabilization after IP acquisition
|
|
772
|
+
* - Transient network conditions
|
|
706
773
|
*/
|
|
707
774
|
void tcpConnect() {
|
|
708
775
|
using namespace logger;
|
|
@@ -712,18 +779,41 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
712
779
|
|
|
713
780
|
if (WiFi.status() == WL_CONNECTED && WiFi.localIP()) {
|
|
714
781
|
// Determine target IP and port for connection
|
|
715
|
-
IPAddress targetIP =
|
|
782
|
+
IPAddress targetIP =
|
|
783
|
+
stationScan.manualIP ? stationScan.manualIP : WiFi.gatewayIP();
|
|
716
784
|
uint16_t targetPort = stationScan.port;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
785
|
+
|
|
786
|
+
Log(CONNECTION, "tcpConnect(): Connecting to %s:%d\n",
|
|
787
|
+
targetIP.toString().c_str(), targetPort);
|
|
788
|
+
|
|
789
|
+
// Add a small stabilization delay before attempting TCP connection
|
|
790
|
+
// This helps prevent error -14 (ERR_CONN) by allowing the network stack
|
|
791
|
+
// and TCP server to be fully ready. The delay is added via task scheduler
|
|
792
|
+
// to avoid blocking the event loop.
|
|
793
|
+
this->addTask(
|
|
794
|
+
painlessmesh::tcp::TCP_CONNECT_STABILIZATION_DELAY_MS, TASK_ONCE,
|
|
795
|
+
[this, targetIP, targetPort]() {
|
|
796
|
+
// Verify WiFi is still connected after the delay
|
|
797
|
+
if (WiFi.status() != WL_CONNECTED || !WiFi.localIP()) {
|
|
798
|
+
Log(CONNECTION,
|
|
799
|
+
"tcpConnect(): WiFi disconnected during stabilization "
|
|
800
|
+
"delay\n");
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
Log(CONNECTION,
|
|
805
|
+
"tcpConnect(): Starting TCP connection after stabilization\n");
|
|
806
|
+
AsyncClient* pConn = new AsyncClient();
|
|
807
|
+
painlessmesh::tcp::connect<Connection,
|
|
808
|
+
painlessmesh::Mesh<Connection>>(
|
|
809
|
+
(*pConn), targetIP, targetPort, (*this));
|
|
810
|
+
});
|
|
721
811
|
} else {
|
|
722
812
|
Log(ERROR, "tcpConnect(): err Something unexpected in tcpConnect()\n");
|
|
723
813
|
}
|
|
724
814
|
}
|
|
725
815
|
|
|
726
|
-
bool setHostname(const char
|
|
816
|
+
bool setHostname(const char* hostname) {
|
|
727
817
|
#ifdef ESP8266
|
|
728
818
|
return WiFi.hostname(hostname);
|
|
729
819
|
#elif defined(ESP32)
|
|
@@ -739,23 +829,22 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
739
829
|
|
|
740
830
|
/**
|
|
741
831
|
* Enable or disable automatic bridge failover
|
|
742
|
-
*
|
|
832
|
+
*
|
|
743
833
|
* When enabled, nodes will participate in bridge elections if the primary
|
|
744
834
|
* bridge goes offline and they have router credentials configured.
|
|
745
|
-
*
|
|
746
|
-
* @param enabled true to enable automatic failover (default), false to
|
|
835
|
+
*
|
|
836
|
+
* @param enabled true to enable automatic failover (default), false to
|
|
837
|
+
* disable
|
|
747
838
|
*/
|
|
748
|
-
void enableBridgeFailover(bool enabled) {
|
|
749
|
-
bridgeFailoverEnabled = enabled;
|
|
750
|
-
}
|
|
839
|
+
void enableBridgeFailover(bool enabled) { bridgeFailoverEnabled = enabled; }
|
|
751
840
|
|
|
752
841
|
/**
|
|
753
842
|
* Set router credentials for bridge election participation
|
|
754
|
-
*
|
|
843
|
+
*
|
|
755
844
|
* Nodes must have router credentials configured to participate in bridge
|
|
756
845
|
* elections. When a bridge fails, only nodes with credentials can become
|
|
757
846
|
* the new bridge.
|
|
758
|
-
*
|
|
847
|
+
*
|
|
759
848
|
* @param ssid Router SSID
|
|
760
849
|
* @param password Router password
|
|
761
850
|
*/
|
|
@@ -767,20 +856,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
767
856
|
|
|
768
857
|
/**
|
|
769
858
|
* Set the election timeout (how long to collect candidates)
|
|
770
|
-
*
|
|
859
|
+
*
|
|
771
860
|
* @param timeoutMs Timeout in milliseconds (default: 5000 = 5 seconds)
|
|
772
861
|
*/
|
|
773
|
-
void setElectionTimeout(uint32_t timeoutMs) {
|
|
774
|
-
electionTimeoutMs = timeoutMs;
|
|
775
|
-
}
|
|
862
|
+
void setElectionTimeout(uint32_t timeoutMs) { electionTimeoutMs = timeoutMs; }
|
|
776
863
|
|
|
777
864
|
/**
|
|
778
865
|
* Set the minimum RSSI required for bridge election
|
|
779
|
-
*
|
|
866
|
+
*
|
|
780
867
|
* Prevents nodes with poor router signal from becoming bridges in isolated
|
|
781
868
|
* elections. When a node is the only candidate, it must meet this threshold.
|
|
782
869
|
* When multiple candidates exist, the best RSSI wins regardless of threshold.
|
|
783
|
-
*
|
|
870
|
+
*
|
|
784
871
|
* @param minRSSI Minimum RSSI in dBm (default: -80 dBm, range: -100 to -30)
|
|
785
872
|
*/
|
|
786
873
|
void setMinimumBridgeRSSI(int8_t minRSSI) {
|
|
@@ -791,12 +878,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
791
878
|
|
|
792
879
|
/**
|
|
793
880
|
* Set the startup delay before first bridge election check
|
|
794
|
-
*
|
|
881
|
+
*
|
|
795
882
|
* Allows time for mesh network formation before starting bridge elections.
|
|
796
883
|
* Longer delays reduce the risk of split-brain scenarios when multiple nodes
|
|
797
884
|
* start simultaneously, ensuring nodes discover each other before elections.
|
|
798
|
-
*
|
|
799
|
-
* @param delayMs Startup delay in milliseconds (default: 60000 = 60 seconds,
|
|
885
|
+
*
|
|
886
|
+
* @param delayMs Startup delay in milliseconds (default: 60000 = 60 seconds,
|
|
887
|
+
* min: 10000)
|
|
800
888
|
*/
|
|
801
889
|
void setElectionStartupDelay(uint32_t delayMs) {
|
|
802
890
|
if (delayMs < 10000) delayMs = 10000; // Minimum 10 seconds
|
|
@@ -805,16 +893,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
805
893
|
|
|
806
894
|
/**
|
|
807
895
|
* Set the random delay range for bridge elections
|
|
808
|
-
*
|
|
896
|
+
*
|
|
809
897
|
* When multiple nodes detect missing bridge simultaneously, randomized delays
|
|
810
|
-
* prevent all nodes from starting elections at the same instant. Longer
|
|
811
|
-
* provide more time for mesh discovery and reduce split-brain risk.
|
|
812
|
-
*
|
|
813
|
-
* @param minMs Minimum random delay in milliseconds (default: 1000 = 1
|
|
814
|
-
*
|
|
898
|
+
* prevent all nodes from starting elections at the same instant. Longer
|
|
899
|
+
* delays provide more time for mesh discovery and reduce split-brain risk.
|
|
900
|
+
*
|
|
901
|
+
* @param minMs Minimum random delay in milliseconds (default: 1000 = 1
|
|
902
|
+
* second)
|
|
903
|
+
* @param maxMs Maximum random delay in milliseconds (default: 3000 = 3
|
|
904
|
+
* seconds)
|
|
815
905
|
*/
|
|
816
906
|
void setElectionRandomDelay(uint32_t minMs, uint32_t maxMs) {
|
|
817
|
-
if (minMs < 100) minMs = 100;
|
|
907
|
+
if (minMs < 100) minMs = 100; // Minimum 100ms
|
|
818
908
|
if (maxMs < minMs) maxMs = minMs + 1000; // Ensure max > min
|
|
819
909
|
electionRandomDelayMinMs = minMs;
|
|
820
910
|
electionRandomDelayMaxMs = maxMs;
|
|
@@ -822,33 +912,36 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
822
912
|
|
|
823
913
|
/**
|
|
824
914
|
* Set callback for when this node's bridge role changes
|
|
825
|
-
*
|
|
915
|
+
*
|
|
826
916
|
* @param callback Function to call when role changes
|
|
827
917
|
*/
|
|
828
|
-
void onBridgeRoleChanged(
|
|
918
|
+
void onBridgeRoleChanged(
|
|
919
|
+
std::function<void(bool isBridge, TSTRING reason)> callback) {
|
|
829
920
|
bridgeRoleChangedCallback = callback;
|
|
830
921
|
}
|
|
831
922
|
|
|
832
923
|
/**
|
|
833
924
|
* Enable or disable multi-bridge coordination mode
|
|
834
|
-
*
|
|
925
|
+
*
|
|
835
926
|
* When enabled, multiple bridges can operate simultaneously for:
|
|
836
927
|
* - Load balancing across multiple Internet connections
|
|
837
928
|
* - Geographic distribution
|
|
838
929
|
* - Hot standby redundancy without failover delays
|
|
839
|
-
*
|
|
840
|
-
* @param enabled true to enable multi-bridge mode, false for single-bridge
|
|
930
|
+
*
|
|
931
|
+
* @param enabled true to enable multi-bridge mode, false for single-bridge
|
|
932
|
+
* (default)
|
|
841
933
|
*/
|
|
842
934
|
void enableMultiBridge(bool enabled) {
|
|
843
935
|
multiBridgeEnabled = enabled;
|
|
844
936
|
if (enabled) {
|
|
845
|
-
Log(logger::GENERAL,
|
|
937
|
+
Log(logger::GENERAL,
|
|
938
|
+
"enableMultiBridge(): Multi-bridge coordination enabled\n");
|
|
846
939
|
}
|
|
847
940
|
}
|
|
848
941
|
|
|
849
942
|
/**
|
|
850
943
|
* Set bridge selection strategy for multi-bridge mode
|
|
851
|
-
*
|
|
944
|
+
*
|
|
852
945
|
* @param strategy Selection strategy:
|
|
853
946
|
* - PRIORITY_BASED: Always use highest priority bridge (default)
|
|
854
947
|
* - ROUND_ROBIN: Distribute load evenly across bridges
|
|
@@ -856,132 +949,140 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
856
949
|
*/
|
|
857
950
|
void setBridgeSelectionStrategy(BridgeSelectionStrategy strategy) {
|
|
858
951
|
bridgeSelectionStrategy = strategy;
|
|
859
|
-
Log(logger::GENERAL, "setBridgeSelectionStrategy(): Strategy set to %d\n",
|
|
952
|
+
Log(logger::GENERAL, "setBridgeSelectionStrategy(): Strategy set to %d\n",
|
|
953
|
+
(int)strategy);
|
|
860
954
|
}
|
|
861
955
|
|
|
862
956
|
/**
|
|
863
957
|
* Set maximum number of concurrent bridges in multi-bridge mode
|
|
864
|
-
*
|
|
958
|
+
*
|
|
865
959
|
* @param maxBridges Maximum bridges to track (default: 2, max: 5)
|
|
866
960
|
*/
|
|
867
961
|
void setMaxBridges(uint8_t maxBridges) {
|
|
868
962
|
if (maxBridges < 1) maxBridges = 1;
|
|
869
963
|
if (maxBridges > 5) maxBridges = 5;
|
|
870
964
|
maxConcurrentBridges = maxBridges;
|
|
871
|
-
Log(logger::GENERAL, "setMaxBridges(): Max concurrent bridges set to %d\n",
|
|
965
|
+
Log(logger::GENERAL, "setMaxBridges(): Max concurrent bridges set to %d\n",
|
|
966
|
+
maxBridges);
|
|
872
967
|
}
|
|
873
968
|
|
|
874
969
|
/**
|
|
875
970
|
* Get list of all active bridges (with Internet connection)
|
|
876
|
-
*
|
|
971
|
+
*
|
|
877
972
|
* @return vector of node IDs for active bridges
|
|
878
973
|
*/
|
|
879
974
|
std::vector<uint32_t> getActiveBridges() {
|
|
880
975
|
std::vector<uint32_t> activeBridges;
|
|
881
976
|
auto bridges = this->getBridges();
|
|
882
|
-
|
|
977
|
+
|
|
883
978
|
for (const auto& bridge : bridges) {
|
|
884
979
|
if (bridge.internetConnected && bridge.isHealthy()) {
|
|
885
980
|
activeBridges.push_back(bridge.nodeId);
|
|
886
981
|
}
|
|
887
982
|
}
|
|
888
|
-
|
|
983
|
+
|
|
889
984
|
return activeBridges;
|
|
890
985
|
}
|
|
891
986
|
|
|
892
987
|
/**
|
|
893
988
|
* Check if any bridge/gateway in the mesh has Internet connectivity
|
|
894
|
-
*
|
|
989
|
+
*
|
|
895
990
|
* IMPORTANT: This method checks if a GATEWAY node (bridge) in the mesh has
|
|
896
|
-
* Internet access, NOT whether THIS node can directly make HTTP/HTTPS
|
|
897
|
-
*
|
|
991
|
+
* Internet access, NOT whether THIS node can directly make HTTP/HTTPS
|
|
992
|
+
* requests.
|
|
993
|
+
*
|
|
898
994
|
* Regular mesh nodes do NOT have direct IP routing to the Internet. They only
|
|
899
|
-
* communicate via the painlessMesh protocol. To send data to the Internet
|
|
900
|
-
* a regular node, you must use sendToInternet() which routes through a
|
|
901
|
-
* or use initAsSharedGateway(meshSSID, meshPwd, ROUTER_SSID,
|
|
902
|
-
* to give all nodes direct router access
|
|
903
|
-
*
|
|
995
|
+
* communicate via the painlessMesh protocol. To send data to the Internet
|
|
996
|
+
* from a regular node, you must use sendToInternet() which routes through a
|
|
997
|
+
* gateway, or use initAsSharedGateway(meshSSID, meshPwd, ROUTER_SSID,
|
|
998
|
+
* ROUTER_PWD, scheduler, port) to give all nodes direct router access
|
|
999
|
+
* (requires router credentials).
|
|
1000
|
+
*
|
|
904
1001
|
* Override of base class method to also check if THIS node is a bridge
|
|
905
1002
|
* with Internet connectivity, not just other bridges in the mesh.
|
|
906
|
-
*
|
|
1003
|
+
*
|
|
907
1004
|
* \code
|
|
908
1005
|
* if (mesh.hasInternetConnection()) {
|
|
909
1006
|
* // A gateway exists - use sendToInternet() to reach Internet
|
|
910
1007
|
* mesh.sendToInternet("https://api.example.com", data, callback);
|
|
911
1008
|
* }
|
|
912
|
-
*
|
|
1009
|
+
*
|
|
913
1010
|
* // DON'T DO THIS on regular nodes - will fail with "connection refused":
|
|
914
1011
|
* // HTTPClient http;
|
|
915
1012
|
* // http.begin("https://api.example.com");
|
|
916
1013
|
* \endcode
|
|
917
|
-
*
|
|
1014
|
+
*
|
|
918
1015
|
* @return true if at least one bridge (including this node) has Internet
|
|
919
1016
|
* @see hasLocalInternet() to check if THIS node has direct Internet access
|
|
920
1017
|
* @see sendToInternet() to send data to Internet via gateway
|
|
921
|
-
* @see initAsSharedGateway() requires router credentials (ROUTER_SSID,
|
|
1018
|
+
* @see initAsSharedGateway() requires router credentials (ROUTER_SSID,
|
|
1019
|
+
* ROUTER_PASSWORD)
|
|
922
1020
|
*/
|
|
923
1021
|
bool hasInternetConnection() {
|
|
924
1022
|
// First check if THIS node is a bridge with Internet
|
|
925
1023
|
if (this->isBridge()) {
|
|
926
1024
|
// Check Internet connectivity: WiFi connected AND valid IP address
|
|
927
|
-
bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
|
|
1025
|
+
bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
|
|
1026
|
+
(WiFi.localIP() != IPAddress(0, 0, 0, 0));
|
|
928
1027
|
if (hasInternet) {
|
|
929
1028
|
return true;
|
|
930
1029
|
}
|
|
931
1030
|
}
|
|
932
|
-
|
|
1031
|
+
|
|
933
1032
|
// Then check other bridges in the mesh (call parent implementation)
|
|
934
1033
|
return painlessmesh::Mesh<Connection>::hasInternetConnection();
|
|
935
1034
|
}
|
|
936
1035
|
|
|
937
1036
|
/**
|
|
938
1037
|
* Get recommended bridge for message transmission
|
|
939
|
-
*
|
|
1038
|
+
*
|
|
940
1039
|
* Uses the configured bridge selection strategy to pick the best bridge.
|
|
941
1040
|
* Returns 0 if no suitable bridge is available.
|
|
942
|
-
*
|
|
1041
|
+
*
|
|
943
1042
|
* @return node ID of recommended bridge, or 0 if none available
|
|
944
1043
|
*/
|
|
945
1044
|
uint32_t getRecommendedBridge() {
|
|
946
1045
|
auto activeBridges = getActiveBridges();
|
|
947
|
-
|
|
1046
|
+
|
|
948
1047
|
if (activeBridges.empty()) {
|
|
949
1048
|
return 0;
|
|
950
1049
|
}
|
|
951
|
-
|
|
1050
|
+
|
|
952
1051
|
// Single bridge - return it
|
|
953
1052
|
if (activeBridges.size() == 1) {
|
|
954
1053
|
return activeBridges[0];
|
|
955
1054
|
}
|
|
956
|
-
|
|
1055
|
+
|
|
957
1056
|
// Multi-bridge mode: apply selection strategy
|
|
958
1057
|
switch (bridgeSelectionStrategy) {
|
|
959
1058
|
case ROUND_ROBIN: {
|
|
960
1059
|
// Simple round-robin: cycle through bridges
|
|
961
|
-
lastSelectedBridgeIndex =
|
|
1060
|
+
lastSelectedBridgeIndex =
|
|
1061
|
+
(lastSelectedBridgeIndex + 1) % activeBridges.size();
|
|
962
1062
|
return activeBridges[lastSelectedBridgeIndex];
|
|
963
1063
|
}
|
|
964
|
-
|
|
1064
|
+
|
|
965
1065
|
case BEST_SIGNAL: {
|
|
966
1066
|
// Find bridge with best RSSI
|
|
967
1067
|
uint32_t bestBridge = 0;
|
|
968
1068
|
int8_t bestRSSI = -127;
|
|
969
|
-
|
|
1069
|
+
|
|
970
1070
|
for (const auto& bridge : this->getBridges()) {
|
|
971
|
-
if (bridge.internetConnected && bridge.isHealthy() &&
|
|
1071
|
+
if (bridge.internetConnected && bridge.isHealthy() &&
|
|
1072
|
+
bridge.routerRSSI > bestRSSI) {
|
|
972
1073
|
bestRSSI = bridge.routerRSSI;
|
|
973
1074
|
bestBridge = bridge.nodeId;
|
|
974
1075
|
}
|
|
975
1076
|
}
|
|
976
1077
|
return bestBridge;
|
|
977
1078
|
}
|
|
978
|
-
|
|
1079
|
+
|
|
979
1080
|
case PRIORITY_BASED:
|
|
980
1081
|
default: {
|
|
981
1082
|
// Use highest priority bridge (stored in bridgePriorities map)
|
|
982
1083
|
uint32_t bestBridge = 0;
|
|
983
1084
|
uint8_t highestPriority = 0;
|
|
984
|
-
|
|
1085
|
+
|
|
985
1086
|
for (uint32_t bridgeId : activeBridges) {
|
|
986
1087
|
uint8_t priority = bridgePriorities[bridgeId];
|
|
987
1088
|
if (priority > highestPriority) {
|
|
@@ -989,7 +1090,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
989
1090
|
bestBridge = bridgeId;
|
|
990
1091
|
}
|
|
991
1092
|
}
|
|
992
|
-
|
|
1093
|
+
|
|
993
1094
|
// If no priority info, use first active bridge
|
|
994
1095
|
return bestBridge ? bestBridge : activeBridges[0];
|
|
995
1096
|
}
|
|
@@ -998,9 +1099,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
998
1099
|
|
|
999
1100
|
/**
|
|
1000
1101
|
* Select a specific bridge for next transmission
|
|
1001
|
-
*
|
|
1102
|
+
*
|
|
1002
1103
|
* This overrides the automatic bridge selection for one message.
|
|
1003
|
-
*
|
|
1104
|
+
*
|
|
1004
1105
|
* @param bridgeNodeId Node ID of bridge to use
|
|
1005
1106
|
*/
|
|
1006
1107
|
void selectBridge(uint32_t bridgeNodeId) {
|
|
@@ -1009,12 +1110,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1009
1110
|
|
|
1010
1111
|
/**
|
|
1011
1112
|
* Check if multi-bridge mode is enabled
|
|
1012
|
-
*
|
|
1113
|
+
*
|
|
1013
1114
|
* @return true if multi-bridge coordination is enabled
|
|
1014
1115
|
*/
|
|
1015
|
-
bool isMultiBridgeEnabled() const {
|
|
1016
|
-
return multiBridgeEnabled;
|
|
1017
|
-
}
|
|
1116
|
+
bool isMultiBridgeEnabled() const { return multiBridgeEnabled; }
|
|
1018
1117
|
|
|
1019
1118
|
void stop() {
|
|
1020
1119
|
// remove all WiFi events
|
|
@@ -1055,19 +1154,31 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1055
1154
|
IPAddress _apIp;
|
|
1056
1155
|
StationScan stationScan;
|
|
1057
1156
|
|
|
1058
|
-
void init(Scheduler
|
|
1157
|
+
void init(Scheduler* scheduler, uint32_t id) {
|
|
1059
1158
|
painlessmesh::Mesh<Connection>::init(scheduler, id);
|
|
1060
1159
|
}
|
|
1061
1160
|
|
|
1062
1161
|
void init(uint32_t id) { painlessmesh::Mesh<Connection>::init(id); }
|
|
1063
1162
|
|
|
1064
1163
|
void apInit(uint32_t nodeId) {
|
|
1164
|
+
using namespace logger;
|
|
1065
1165
|
_apIp = IPAddress(10, (nodeId & 0xFF00) >> 8, (nodeId & 0xFF), 1);
|
|
1066
1166
|
IPAddress netmask(255, 255, 255, 0);
|
|
1067
1167
|
|
|
1068
1168
|
WiFi.softAPConfig(_apIp, _apIp, netmask);
|
|
1169
|
+
|
|
1170
|
+
#ifdef ESP32
|
|
1171
|
+
// ESP32: Explicitly enable AP mode to ensure DHCP server starts properly
|
|
1172
|
+
// This is particularly important after channel changes or AP restarts
|
|
1173
|
+
WiFi.enableAP(true);
|
|
1174
|
+
#endif
|
|
1175
|
+
|
|
1069
1176
|
WiFi.softAP(_meshSSID.c_str(), _meshPassword.c_str(), _meshChannel,
|
|
1070
1177
|
_meshHidden, _meshMaxConn);
|
|
1178
|
+
|
|
1179
|
+
Log(STARTUP, "apInit(): AP configured - SSID: %s, Channel: %d, IP: %s\n",
|
|
1180
|
+
_meshSSID.c_str(), _meshChannel, _apIp.toString().c_str());
|
|
1181
|
+
Log(STARTUP, "apInit(): AP active - Max connections: %d\n", _meshMaxConn);
|
|
1071
1182
|
}
|
|
1072
1183
|
|
|
1073
1184
|
/**
|
|
@@ -1076,80 +1187,84 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1076
1187
|
*/
|
|
1077
1188
|
void initBridgeStatusBroadcast() {
|
|
1078
1189
|
using namespace logger;
|
|
1079
|
-
|
|
1190
|
+
|
|
1080
1191
|
if (!this->isBridge() || !this->bridgeStatusBroadcastEnabled) {
|
|
1081
1192
|
return;
|
|
1082
1193
|
}
|
|
1083
|
-
|
|
1084
|
-
Log(STARTUP,
|
|
1085
|
-
|
|
1194
|
+
|
|
1195
|
+
Log(STARTUP,
|
|
1196
|
+
"initBridgeStatusBroadcast(): Setting up bridge status broadcast\n");
|
|
1197
|
+
|
|
1086
1198
|
// Register ourselves as a bridge in the knownBridges list
|
|
1087
1199
|
// This ensures the bridge knows about itself and reports correct status
|
|
1088
1200
|
this->addTask([this]() {
|
|
1089
1201
|
// Check Internet connectivity: WiFi connected AND valid IP address
|
|
1090
|
-
bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
|
|
1202
|
+
bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
|
|
1091
1203
|
(WiFi.localIP() != IPAddress(0, 0, 0, 0));
|
|
1092
|
-
|
|
1093
|
-
this->updateBridgeStatus(
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
this->getNodeTime() // timestamp
|
|
1204
|
+
|
|
1205
|
+
this->updateBridgeStatus(this->nodeId, // bridgeNodeId
|
|
1206
|
+
hasInternet, // internetConnected
|
|
1207
|
+
WiFi.RSSI(), // routerRSSI
|
|
1208
|
+
WiFi.channel(), // routerChannel
|
|
1209
|
+
millis(), // uptime
|
|
1210
|
+
WiFi.gatewayIP().toString(), // gatewayIP
|
|
1211
|
+
this->getNodeTime() // timestamp
|
|
1101
1212
|
);
|
|
1102
|
-
|
|
1103
|
-
Log(STARTUP,
|
|
1213
|
+
|
|
1214
|
+
Log(STARTUP,
|
|
1215
|
+
"initBridgeStatusBroadcast(): Registered self as bridge (nodeId: "
|
|
1216
|
+
"%u)\n",
|
|
1104
1217
|
this->nodeId);
|
|
1105
1218
|
});
|
|
1106
|
-
|
|
1219
|
+
|
|
1107
1220
|
// Create periodic task to broadcast bridge status
|
|
1108
|
-
bridgeStatusTask = this->addTask(
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
[this]() {
|
|
1112
|
-
this->sendBridgeStatus();
|
|
1113
|
-
}
|
|
1114
|
-
);
|
|
1115
|
-
|
|
1221
|
+
bridgeStatusTask = this->addTask(this->bridgeStatusIntervalMs, TASK_FOREVER,
|
|
1222
|
+
[this]() { this->sendBridgeStatus(); });
|
|
1223
|
+
|
|
1116
1224
|
// Send immediate broadcast so nodes can discover this bridge right away
|
|
1117
1225
|
// This ensures bridge is discoverable before the first periodic broadcast
|
|
1118
1226
|
this->addTask([this]() {
|
|
1119
1227
|
Log(STARTUP, "Sending initial bridge status broadcast\n");
|
|
1120
1228
|
this->sendBridgeStatus();
|
|
1121
1229
|
});
|
|
1122
|
-
|
|
1123
|
-
// Also send bridge status when new nodes connect so they can discover the
|
|
1124
|
-
// Send directly to the new node to ensure delivery,
|
|
1125
|
-
// Using changedConnectionCallbacks instead of
|
|
1230
|
+
|
|
1231
|
+
// Also send bridge status when new nodes connect so they can discover the
|
|
1232
|
+
// bridge immediately Send directly to the new node to ensure delivery,
|
|
1233
|
+
// independent of time sync Using changedConnectionCallbacks instead of
|
|
1234
|
+
// newConnectionCallbacks ensures routing is ready
|
|
1126
1235
|
this->changedConnectionCallbacks.push_back([this](uint32_t nodeId) {
|
|
1127
|
-
Log(CONNECTION,
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1236
|
+
Log(CONNECTION,
|
|
1237
|
+
"Node %u connection changed, sending bridge status directly\n",
|
|
1238
|
+
nodeId);
|
|
1239
|
+
|
|
1240
|
+
// Small delay to ensure connection is fully stable, then send directly to
|
|
1241
|
+
// the new node This avoids issues with time sync blocking broadcast
|
|
1242
|
+
// messages
|
|
1131
1243
|
this->addTask(500, TASK_ONCE, [this, nodeId]() {
|
|
1132
|
-
// Check if the connection is still valid - the node may have
|
|
1133
|
-
// during the 500ms delay (e.g., due to timeout or network
|
|
1134
|
-
// This prevents attempting to send messages to dropped
|
|
1135
|
-
// findRoute returns nullptr if node is not in the routing
|
|
1244
|
+
// Check if the connection is still valid - the node may have
|
|
1245
|
+
// disconnected during the 500ms delay (e.g., due to timeout or network
|
|
1246
|
+
// issues) This prevents attempting to send messages to dropped
|
|
1247
|
+
// connections findRoute returns nullptr if node is not in the routing
|
|
1248
|
+
// table
|
|
1136
1249
|
auto conn = router::findRoute<Connection>((*this), nodeId);
|
|
1137
1250
|
if (!conn || !conn->connected()) {
|
|
1138
|
-
Log(CONNECTION,
|
|
1251
|
+
Log(CONNECTION,
|
|
1252
|
+
"Bridge status send cancelled: Node %u no longer connected\n",
|
|
1253
|
+
nodeId);
|
|
1139
1254
|
return;
|
|
1140
1255
|
}
|
|
1141
|
-
|
|
1256
|
+
|
|
1142
1257
|
// Create bridge status message
|
|
1143
1258
|
JsonDocument doc;
|
|
1144
1259
|
JsonObject obj = doc.to<JsonObject>();
|
|
1145
|
-
|
|
1260
|
+
|
|
1146
1261
|
obj["type"] = protocol::BRIDGE_STATUS;
|
|
1147
1262
|
obj["from"] = this->nodeId;
|
|
1148
1263
|
obj["routing"] = 1; // SINGLE routing (direct to node)
|
|
1149
1264
|
obj["dest"] = nodeId;
|
|
1150
1265
|
obj["timestamp"] = this->getNodeTime();
|
|
1151
|
-
|
|
1152
|
-
bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
|
|
1266
|
+
|
|
1267
|
+
bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
|
|
1153
1268
|
(WiFi.localIP() != IPAddress(0, 0, 0, 0));
|
|
1154
1269
|
obj["internetConnected"] = hasInternet;
|
|
1155
1270
|
obj["routerRSSI"] = WiFi.RSSI();
|
|
@@ -1157,13 +1272,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1157
1272
|
obj["uptime"] = millis();
|
|
1158
1273
|
obj["gatewayIP"] = WiFi.gatewayIP().toString();
|
|
1159
1274
|
obj["message_type"] = protocol::BRIDGE_STATUS;
|
|
1160
|
-
|
|
1275
|
+
|
|
1161
1276
|
String msg;
|
|
1162
1277
|
serializeJson(doc, msg);
|
|
1163
|
-
|
|
1164
|
-
Log(CONNECTION,
|
|
1278
|
+
|
|
1279
|
+
Log(CONNECTION,
|
|
1280
|
+
"Sending bridge status directly to node %u (Internet: %s)\n",
|
|
1165
1281
|
nodeId, hasInternet ? "YES" : "NO");
|
|
1166
|
-
|
|
1282
|
+
|
|
1167
1283
|
// Send directly to the connection with high priority
|
|
1168
1284
|
// This ensures the message is sent immediately rather than queued
|
|
1169
1285
|
// The JSON message format is the same as what router::send() produces
|
|
@@ -1171,8 +1287,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1171
1287
|
conn->addMessage(msg, true);
|
|
1172
1288
|
});
|
|
1173
1289
|
});
|
|
1174
|
-
|
|
1175
|
-
Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
|
|
1290
|
+
|
|
1291
|
+
Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
|
|
1176
1292
|
this->bridgeStatusIntervalMs);
|
|
1177
1293
|
}
|
|
1178
1294
|
|
|
@@ -1182,66 +1298,70 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1182
1298
|
*/
|
|
1183
1299
|
void initBridgeCoordination() {
|
|
1184
1300
|
using namespace logger;
|
|
1185
|
-
|
|
1301
|
+
|
|
1186
1302
|
if (!this->isBridge() || !multiBridgeEnabled) {
|
|
1187
1303
|
return;
|
|
1188
1304
|
}
|
|
1189
|
-
|
|
1190
|
-
Log(STARTUP,
|
|
1191
|
-
|
|
1305
|
+
|
|
1306
|
+
Log(STARTUP,
|
|
1307
|
+
"initBridgeCoordination(): Setting up multi-bridge coordination\n");
|
|
1308
|
+
|
|
1192
1309
|
// Register our own priority in the bridgePriorities map
|
|
1193
|
-
// This ensures getRecommendedBridge() with PRIORITY_BASED strategy works
|
|
1310
|
+
// This ensures getRecommendedBridge() with PRIORITY_BASED strategy works
|
|
1311
|
+
// correctly
|
|
1194
1312
|
bridgePriorities[this->nodeId] = bridgePriority;
|
|
1195
|
-
Log(STARTUP,
|
|
1313
|
+
Log(STARTUP,
|
|
1314
|
+
"initBridgeCoordination(): Registered self priority (nodeId: %u, "
|
|
1315
|
+
"priority: %d)\n",
|
|
1196
1316
|
this->nodeId, bridgePriority);
|
|
1197
|
-
|
|
1317
|
+
|
|
1198
1318
|
// Register handler for incoming coordination messages (Type 613)
|
|
1199
1319
|
this->callbackList.onPackage(
|
|
1200
1320
|
613, // BRIDGE_COORDINATION type
|
|
1201
|
-
[this](protocol::Variant& variant, std::shared_ptr<Connection>,
|
|
1321
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>,
|
|
1322
|
+
uint32_t) {
|
|
1202
1323
|
JsonDocument doc;
|
|
1203
1324
|
TSTRING str;
|
|
1204
1325
|
variant.printTo(str);
|
|
1205
1326
|
deserializeJson(doc, str);
|
|
1206
1327
|
JsonObject obj = doc.as<JsonObject>();
|
|
1207
|
-
|
|
1328
|
+
|
|
1208
1329
|
if (obj["priority"].is<unsigned int>()) {
|
|
1209
1330
|
uint32_t fromNode = obj["from"];
|
|
1210
1331
|
uint8_t priority = obj["priority"];
|
|
1211
1332
|
TSTRING role = obj["role"].as<TSTRING>();
|
|
1212
1333
|
uint8_t load = obj["load"] | 0;
|
|
1213
|
-
|
|
1334
|
+
|
|
1214
1335
|
// Store bridge priority for selection decisions
|
|
1215
1336
|
bridgePriorities[fromNode] = priority;
|
|
1216
|
-
|
|
1337
|
+
|
|
1217
1338
|
// Update peer bridges list
|
|
1218
1339
|
if (obj["peerBridges"].is<JsonArray>()) {
|
|
1219
1340
|
JsonArray peers = obj["peerBridges"];
|
|
1220
1341
|
for (JsonVariant peer : peers) {
|
|
1221
1342
|
uint32_t peerId = peer.as<uint32_t>();
|
|
1222
|
-
if (peerId != this->nodeId &&
|
|
1223
|
-
std::find(knownBridgePeers.begin(), knownBridgePeers.end(),
|
|
1343
|
+
if (peerId != this->nodeId &&
|
|
1344
|
+
std::find(knownBridgePeers.begin(), knownBridgePeers.end(),
|
|
1345
|
+
peerId) == knownBridgePeers.end()) {
|
|
1224
1346
|
knownBridgePeers.push_back(peerId);
|
|
1225
1347
|
}
|
|
1226
1348
|
}
|
|
1227
1349
|
}
|
|
1228
|
-
|
|
1229
|
-
Log(CONNECTION,
|
|
1350
|
+
|
|
1351
|
+
Log(CONNECTION,
|
|
1352
|
+
"Bridge coordination from %u: priority=%d, role=%s, "
|
|
1353
|
+
"load=%d%%\n",
|
|
1230
1354
|
fromNode, priority, role.c_str(), load);
|
|
1231
1355
|
}
|
|
1232
1356
|
return false; // Don't consume the package
|
|
1233
1357
|
});
|
|
1234
|
-
|
|
1358
|
+
|
|
1235
1359
|
// Create periodic task to send coordination messages
|
|
1236
1360
|
bridgeCoordinationTask = this->addTask(
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
}
|
|
1242
|
-
);
|
|
1243
|
-
|
|
1244
|
-
Log(STARTUP, "Bridge coordination enabled (priority: %d, role: %s)\n",
|
|
1361
|
+
30000, // 30 seconds interval
|
|
1362
|
+
TASK_FOREVER, [this]() { this->sendBridgeCoordination(); });
|
|
1363
|
+
|
|
1364
|
+
Log(STARTUP, "Bridge coordination enabled (priority: %d, role: %s)\n",
|
|
1245
1365
|
bridgePriority, bridgeRole.c_str());
|
|
1246
1366
|
}
|
|
1247
1367
|
|
|
@@ -1251,11 +1371,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1251
1371
|
*/
|
|
1252
1372
|
void sendBridgeCoordination() {
|
|
1253
1373
|
using namespace logger;
|
|
1254
|
-
|
|
1374
|
+
|
|
1255
1375
|
if (!this->isBridge() || !multiBridgeEnabled) {
|
|
1256
1376
|
return;
|
|
1257
1377
|
}
|
|
1258
|
-
|
|
1378
|
+
|
|
1259
1379
|
// Calculate current load (simplified: based on node count)
|
|
1260
1380
|
uint8_t currentLoad = 0;
|
|
1261
1381
|
auto nodeCount = this->getNodeList(false).size();
|
|
@@ -1263,11 +1383,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1263
1383
|
currentLoad = (nodeCount * 100) / MAX_CONN;
|
|
1264
1384
|
if (currentLoad > 100) currentLoad = 100;
|
|
1265
1385
|
}
|
|
1266
|
-
|
|
1386
|
+
|
|
1267
1387
|
// Create coordination message
|
|
1268
1388
|
JsonDocument doc;
|
|
1269
1389
|
JsonObject obj = doc.to<JsonObject>();
|
|
1270
|
-
|
|
1390
|
+
|
|
1271
1391
|
obj["type"] = 613; // BRIDGE_COORDINATION
|
|
1272
1392
|
obj["from"] = this->nodeId;
|
|
1273
1393
|
obj["routing"] = 2; // BROADCAST
|
|
@@ -1276,49 +1396,53 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1276
1396
|
obj["load"] = currentLoad;
|
|
1277
1397
|
obj["timestamp"] = this->getNodeTime();
|
|
1278
1398
|
obj["message_type"] = 613;
|
|
1279
|
-
|
|
1399
|
+
|
|
1280
1400
|
// Add peer bridges list
|
|
1281
1401
|
JsonArray peers = obj["peerBridges"].to<JsonArray>();
|
|
1282
1402
|
for (uint32_t peerId : knownBridgePeers) {
|
|
1283
1403
|
peers.add(peerId);
|
|
1284
1404
|
}
|
|
1285
|
-
|
|
1405
|
+
|
|
1286
1406
|
String msg;
|
|
1287
1407
|
serializeJson(doc, msg);
|
|
1288
|
-
|
|
1408
|
+
|
|
1289
1409
|
// Update our own priority in bridgePriorities map
|
|
1290
1410
|
// This ensures priority-based selection always has current data
|
|
1291
1411
|
bridgePriorities[this->nodeId] = bridgePriority;
|
|
1292
|
-
|
|
1412
|
+
|
|
1293
1413
|
this->sendBroadcast(msg);
|
|
1294
|
-
|
|
1295
|
-
Log(CONNECTION,
|
|
1414
|
+
|
|
1415
|
+
Log(CONNECTION,
|
|
1416
|
+
"Bridge coordination sent: priority=%d, role=%s, load=%d%%\n",
|
|
1296
1417
|
bridgePriority, bridgeRole.c_str(), currentLoad);
|
|
1297
1418
|
}
|
|
1298
1419
|
|
|
1299
1420
|
/**
|
|
1300
1421
|
* Scan for router and return its signal strength
|
|
1301
|
-
*
|
|
1422
|
+
*
|
|
1302
1423
|
* @param routerSSID SSID of router to scan for
|
|
1303
1424
|
* @return RSSI in dBm (negative number, -127 to 0), or 0 if not found
|
|
1304
1425
|
*/
|
|
1305
1426
|
int8_t scanRouterSignalStrength(TSTRING routerSSID) {
|
|
1306
1427
|
using namespace logger;
|
|
1307
|
-
Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n",
|
|
1308
|
-
|
|
1428
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n",
|
|
1429
|
+
routerSSID.c_str());
|
|
1430
|
+
|
|
1309
1431
|
int n = WiFi.scanNetworks(false, false);
|
|
1310
1432
|
Log(CONNECTION, "scanRouterSignalStrength(): Found %d networks\n", n);
|
|
1311
|
-
|
|
1433
|
+
|
|
1312
1434
|
for (int i = 0; i < n; i++) {
|
|
1313
1435
|
if (WiFi.SSID(i) == routerSSID) {
|
|
1314
1436
|
int8_t rssi = WiFi.RSSI(i);
|
|
1315
|
-
Log(CONNECTION,
|
|
1437
|
+
Log(CONNECTION,
|
|
1438
|
+
"scanRouterSignalStrength(): Found %s with RSSI %d dBm\n",
|
|
1316
1439
|
routerSSID.c_str(), rssi);
|
|
1317
1440
|
return rssi;
|
|
1318
1441
|
}
|
|
1319
1442
|
}
|
|
1320
|
-
|
|
1321
|
-
Log(CONNECTION, "scanRouterSignalStrength(): Router %s not found\n",
|
|
1443
|
+
|
|
1444
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Router %s not found\n",
|
|
1445
|
+
routerSSID.c_str());
|
|
1322
1446
|
return 0; // Router not found
|
|
1323
1447
|
}
|
|
1324
1448
|
|
|
@@ -1328,68 +1452,75 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1328
1452
|
*/
|
|
1329
1453
|
void startBridgeElection() {
|
|
1330
1454
|
using namespace logger;
|
|
1331
|
-
|
|
1455
|
+
|
|
1332
1456
|
if (!bridgeFailoverEnabled) {
|
|
1333
1457
|
Log(CONNECTION, "startBridgeElection(): Failover disabled\n");
|
|
1334
1458
|
return;
|
|
1335
1459
|
}
|
|
1336
|
-
|
|
1460
|
+
|
|
1337
1461
|
if (!routerCredentialsConfigured) {
|
|
1338
|
-
Log(CONNECTION,
|
|
1462
|
+
Log(CONNECTION,
|
|
1463
|
+
"startBridgeElection(): No router credentials, cannot participate\n");
|
|
1339
1464
|
return;
|
|
1340
1465
|
}
|
|
1341
|
-
|
|
1466
|
+
|
|
1342
1467
|
if (electionState != ELECTION_IDLE) {
|
|
1343
1468
|
Log(CONNECTION, "startBridgeElection(): Election already in progress\n");
|
|
1344
1469
|
return;
|
|
1345
1470
|
}
|
|
1346
|
-
|
|
1471
|
+
|
|
1347
1472
|
// Prevent rapid role changes
|
|
1348
1473
|
if (millis() - lastRoleChangeTime < 60000) {
|
|
1349
|
-
Log(CONNECTION,
|
|
1474
|
+
Log(CONNECTION,
|
|
1475
|
+
"startBridgeElection(): Too soon after last role change\n");
|
|
1350
1476
|
return;
|
|
1351
1477
|
}
|
|
1352
|
-
|
|
1478
|
+
|
|
1353
1479
|
// CRITICAL: Check if mesh channel re-synchronization is needed first
|
|
1354
|
-
// If we haven't found any mesh nodes and are approaching the re-sync
|
|
1355
|
-
// prioritize finding the mesh over becoming a bridge. This
|
|
1356
|
-
// where a node tries to become a bridge when it
|
|
1357
|
-
// the mesh on a different channel (e.g., after
|
|
1358
|
-
// switched channels to match the router).
|
|
1480
|
+
// If we haven't found any mesh nodes and are approaching the re-sync
|
|
1481
|
+
// threshold, prioritize finding the mesh over becoming a bridge. This
|
|
1482
|
+
// prevents the scenario where a node tries to become a bridge when it
|
|
1483
|
+
// should be re-syncing to find the mesh on a different channel (e.g., after
|
|
1484
|
+
// another node became bridge and switched channels to match the router).
|
|
1359
1485
|
uint16_t emptyScans = stationScan.getConsecutiveEmptyScans();
|
|
1360
1486
|
if (emptyScans >= 3 && WiFi.status() != WL_CONNECTED) {
|
|
1361
|
-
Log(CONNECTION,
|
|
1487
|
+
Log(CONNECTION,
|
|
1362
1488
|
"startBridgeElection(): Mesh connectivity lost (%d empty scans), "
|
|
1363
|
-
"deferring election to allow channel re-sync\n",
|
|
1364
|
-
|
|
1489
|
+
"deferring election to allow channel re-sync\n",
|
|
1490
|
+
emptyScans);
|
|
1491
|
+
|
|
1365
1492
|
// Schedule a retry after channel re-sync has had a chance to run
|
|
1366
|
-
// The channel re-sync threshold is StationScan::EMPTY_SCAN_THRESHOLD
|
|
1367
|
-
// Fast scan interval is 0.5 * SCAN_INTERVAL = 15
|
|
1368
|
-
// Wait for re-sync to complete plus a buffer
|
|
1369
|
-
uint32_t retryDelay =
|
|
1370
|
-
|
|
1371
|
-
|
|
1493
|
+
// The channel re-sync threshold is StationScan::EMPTY_SCAN_THRESHOLD
|
|
1494
|
+
// scans (default 6) Fast scan interval is 0.5 * SCAN_INTERVAL = 15
|
|
1495
|
+
// seconds Wait for re-sync to complete plus a buffer
|
|
1496
|
+
uint32_t retryDelay =
|
|
1497
|
+
(StationScan::EMPTY_SCAN_THRESHOLD - emptyScans + 2) * 15000;
|
|
1498
|
+
Log(CONNECTION,
|
|
1499
|
+
"startBridgeElection(): Will retry election in %u seconds if still "
|
|
1500
|
+
"needed\n",
|
|
1372
1501
|
retryDelay / 1000);
|
|
1373
1502
|
return;
|
|
1374
1503
|
}
|
|
1375
|
-
|
|
1504
|
+
|
|
1376
1505
|
Log(CONNECTION, "=== Bridge Election Started ===\n");
|
|
1377
1506
|
electionState = ELECTION_SCANNING;
|
|
1378
|
-
|
|
1507
|
+
|
|
1379
1508
|
// Scan for router to get RSSI
|
|
1380
1509
|
int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
|
|
1381
|
-
|
|
1510
|
+
|
|
1382
1511
|
if (routerRSSI == 0) {
|
|
1383
|
-
Log(CONNECTION,
|
|
1512
|
+
Log(CONNECTION,
|
|
1513
|
+
"startBridgeElection(): Router not visible, cannot participate\n");
|
|
1384
1514
|
electionState = ELECTION_IDLE;
|
|
1385
1515
|
return;
|
|
1386
1516
|
}
|
|
1387
|
-
|
|
1388
|
-
Log(CONNECTION, "startBridgeElection(): My router RSSI: %d dBm\n",
|
|
1389
|
-
|
|
1517
|
+
|
|
1518
|
+
Log(CONNECTION, "startBridgeElection(): My router RSSI: %d dBm\n",
|
|
1519
|
+
routerRSSI);
|
|
1520
|
+
|
|
1390
1521
|
// Clear previous candidates
|
|
1391
1522
|
electionCandidates.clear();
|
|
1392
|
-
|
|
1523
|
+
|
|
1393
1524
|
// Add self as candidate
|
|
1394
1525
|
BridgeCandidate selfCandidate;
|
|
1395
1526
|
selfCandidate.nodeId = this->nodeId;
|
|
@@ -1397,8 +1528,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1397
1528
|
selfCandidate.uptime = millis();
|
|
1398
1529
|
selfCandidate.freeMemory = ESP.getFreeHeap();
|
|
1399
1530
|
electionCandidates.push_back(selfCandidate);
|
|
1400
|
-
|
|
1401
|
-
// Broadcast candidacy using JSON directly (avoiding dependency on alteriom
|
|
1531
|
+
|
|
1532
|
+
// Broadcast candidacy using JSON directly (avoiding dependency on alteriom
|
|
1533
|
+
// package)
|
|
1402
1534
|
JsonDocument doc;
|
|
1403
1535
|
JsonObject obj = doc.to<JsonObject>();
|
|
1404
1536
|
obj["type"] = protocol::BRIDGE_ELECTION;
|
|
@@ -1410,24 +1542,24 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1410
1542
|
obj["timestamp"] = this->getNodeTime();
|
|
1411
1543
|
obj["routerSSID"] = routerSSID;
|
|
1412
1544
|
obj["message_type"] = protocol::BRIDGE_ELECTION;
|
|
1413
|
-
|
|
1545
|
+
|
|
1414
1546
|
String msg;
|
|
1415
1547
|
serializeJson(doc, msg);
|
|
1416
|
-
|
|
1417
|
-
// Send election message using raw broadcast to preserve type
|
|
1548
|
+
|
|
1549
|
+
// Send election message using raw broadcast to preserve type
|
|
1550
|
+
// BRIDGE_ELECTION
|
|
1418
1551
|
protocol::Variant variant(msg);
|
|
1419
1552
|
router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
|
|
1420
|
-
|
|
1553
|
+
|
|
1421
1554
|
Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
|
|
1422
|
-
|
|
1555
|
+
|
|
1423
1556
|
// Set election timeout
|
|
1424
1557
|
electionDeadline = millis() + electionTimeoutMs;
|
|
1425
1558
|
electionState = ELECTION_COLLECTING;
|
|
1426
|
-
|
|
1559
|
+
|
|
1427
1560
|
// Schedule election evaluation
|
|
1428
|
-
this->addTask(electionTimeoutMs + 100, TASK_ONCE,
|
|
1429
|
-
|
|
1430
|
-
});
|
|
1561
|
+
this->addTask(electionTimeoutMs + 100, TASK_ONCE,
|
|
1562
|
+
[this]() { this->evaluateElection(); });
|
|
1431
1563
|
}
|
|
1432
1564
|
|
|
1433
1565
|
/**
|
|
@@ -1436,23 +1568,26 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1436
1568
|
*/
|
|
1437
1569
|
void evaluateElection() {
|
|
1438
1570
|
using namespace logger;
|
|
1439
|
-
|
|
1571
|
+
|
|
1440
1572
|
if (electionState != ELECTION_COLLECTING) {
|
|
1441
1573
|
Log(CONNECTION, "evaluateElection(): Not in collecting state\n");
|
|
1442
1574
|
return;
|
|
1443
1575
|
}
|
|
1444
|
-
|
|
1576
|
+
|
|
1445
1577
|
Log(CONNECTION, "=== Evaluating Election ===\n");
|
|
1446
|
-
Log(CONNECTION, "evaluateElection(): %d candidates\n",
|
|
1447
|
-
|
|
1578
|
+
Log(CONNECTION, "evaluateElection(): %d candidates\n",
|
|
1579
|
+
electionCandidates.size());
|
|
1580
|
+
|
|
1448
1581
|
// Find best candidate
|
|
1449
1582
|
BridgeCandidate* winner = nullptr;
|
|
1450
1583
|
int8_t bestRSSI = -127; // Worst possible RSSI
|
|
1451
|
-
|
|
1584
|
+
|
|
1452
1585
|
for (auto& candidate : electionCandidates) {
|
|
1453
|
-
Log(CONNECTION,
|
|
1454
|
-
|
|
1455
|
-
|
|
1586
|
+
Log(CONNECTION,
|
|
1587
|
+
"evaluateElection(): Candidate %u: RSSI=%d, uptime=%u, mem=%u\n",
|
|
1588
|
+
candidate.nodeId, candidate.routerRSSI, candidate.uptime,
|
|
1589
|
+
candidate.freeMemory);
|
|
1590
|
+
|
|
1456
1591
|
if (candidate.routerRSSI > bestRSSI) {
|
|
1457
1592
|
bestRSSI = candidate.routerRSSI;
|
|
1458
1593
|
winner = &candidate;
|
|
@@ -1473,40 +1608,46 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1473
1608
|
}
|
|
1474
1609
|
}
|
|
1475
1610
|
}
|
|
1476
|
-
|
|
1611
|
+
|
|
1477
1612
|
if (winner == nullptr) {
|
|
1478
1613
|
Log(ERROR, "evaluateElection(): No winner found!\n");
|
|
1479
1614
|
electionState = ELECTION_IDLE;
|
|
1480
1615
|
return;
|
|
1481
1616
|
}
|
|
1482
|
-
|
|
1617
|
+
|
|
1483
1618
|
// Validate RSSI threshold for single-candidate elections
|
|
1484
|
-
// When only one candidate exists, it indicates the node is isolated from
|
|
1485
|
-
// In this case, require minimum signal quality to prevent poor
|
|
1486
|
-
// When multiple candidates exist, the mesh is connected and
|
|
1487
|
-
|
|
1619
|
+
// When only one candidate exists, it indicates the node is isolated from
|
|
1620
|
+
// the mesh. In this case, require minimum signal quality to prevent poor
|
|
1621
|
+
// connections. When multiple candidates exist, the mesh is connected and
|
|
1622
|
+
// best RSSI wins.
|
|
1623
|
+
if (electionCandidates.size() == 1 &&
|
|
1624
|
+
winner->routerRSSI < minimumBridgeRSSI) {
|
|
1488
1625
|
Log(CONNECTION, "=== Election Failed: Insufficient Signal Quality ===\n");
|
|
1489
|
-
Log(CONNECTION,
|
|
1626
|
+
Log(CONNECTION,
|
|
1627
|
+
" Single candidate with RSSI %d dBm (minimum required: %d dBm)\n",
|
|
1490
1628
|
winner->routerRSSI, minimumBridgeRSSI);
|
|
1491
1629
|
Log(CONNECTION, " Node is isolated from mesh with poor router signal\n");
|
|
1492
1630
|
Log(CONNECTION, " Rejecting election to prevent unreliable bridge\n");
|
|
1493
|
-
Log(CONNECTION,
|
|
1494
|
-
|
|
1631
|
+
Log(CONNECTION,
|
|
1632
|
+
" Recommendation: Move closer to router or wait for mesh "
|
|
1633
|
+
"connection\n");
|
|
1634
|
+
|
|
1495
1635
|
electionState = ELECTION_IDLE;
|
|
1496
1636
|
electionCandidates.clear();
|
|
1497
|
-
|
|
1637
|
+
|
|
1498
1638
|
// Notify via callback that election failed
|
|
1499
1639
|
if (bridgeRoleChangedCallback) {
|
|
1500
|
-
bridgeRoleChangedCallback(
|
|
1640
|
+
bridgeRoleChangedCallback(
|
|
1641
|
+
false, "Insufficient signal quality for isolated bridge");
|
|
1501
1642
|
}
|
|
1502
1643
|
return;
|
|
1503
1644
|
}
|
|
1504
|
-
|
|
1645
|
+
|
|
1505
1646
|
Log(CONNECTION, "=== Election Winner: Node %u ===\n", winner->nodeId);
|
|
1506
1647
|
Log(CONNECTION, " Router RSSI: %d dBm\n", winner->routerRSSI);
|
|
1507
1648
|
Log(CONNECTION, " Uptime: %u ms\n", winner->uptime);
|
|
1508
1649
|
Log(CONNECTION, " Free Memory: %u bytes\n", winner->freeMemory);
|
|
1509
|
-
|
|
1650
|
+
|
|
1510
1651
|
// Record election in diagnostics history
|
|
1511
1652
|
if (this->diagnosticsEnabled) {
|
|
1512
1653
|
ElectionRecord record;
|
|
@@ -1515,24 +1656,25 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1515
1656
|
record.winnerRSSI = winner->routerRSSI;
|
|
1516
1657
|
record.candidateCount = electionCandidates.size();
|
|
1517
1658
|
record.reason = "Bridge failure detected";
|
|
1518
|
-
|
|
1659
|
+
|
|
1519
1660
|
this->electionHistory.push_back(record);
|
|
1520
|
-
|
|
1661
|
+
|
|
1521
1662
|
// Keep history limited to MAX_ELECTION_HISTORY
|
|
1522
1663
|
if (this->electionHistory.size() > this->MAX_ELECTION_HISTORY) {
|
|
1523
1664
|
this->electionHistory.erase(this->electionHistory.begin());
|
|
1524
1665
|
}
|
|
1525
|
-
|
|
1666
|
+
|
|
1526
1667
|
Log(CONNECTION, "evaluateElection(): Election recorded in history\n");
|
|
1527
1668
|
}
|
|
1528
|
-
|
|
1669
|
+
|
|
1529
1670
|
if (winner->nodeId == this->nodeId) {
|
|
1530
1671
|
Log(CONNECTION, "🎯 I WON! Promoting to bridge...\n");
|
|
1531
1672
|
promoteToBridge();
|
|
1532
1673
|
} else {
|
|
1533
|
-
Log(CONNECTION, "Winner is node %u, remaining as regular node\n",
|
|
1674
|
+
Log(CONNECTION, "Winner is node %u, remaining as regular node\n",
|
|
1675
|
+
winner->nodeId);
|
|
1534
1676
|
}
|
|
1535
|
-
|
|
1677
|
+
|
|
1536
1678
|
electionState = ELECTION_IDLE;
|
|
1537
1679
|
electionCandidates.clear();
|
|
1538
1680
|
}
|
|
@@ -1543,16 +1685,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1543
1685
|
*/
|
|
1544
1686
|
void promoteToBridge() {
|
|
1545
1687
|
using namespace logger;
|
|
1546
|
-
|
|
1688
|
+
|
|
1547
1689
|
Log(STARTUP, "=== Becoming Bridge Node ===\n");
|
|
1548
|
-
|
|
1690
|
+
|
|
1549
1691
|
// Store previous bridge (if any)
|
|
1550
1692
|
auto primaryBridge = this->getPrimaryBridge();
|
|
1551
1693
|
uint32_t previousBridgeId = primaryBridge ? primaryBridge->nodeId : 0;
|
|
1552
|
-
|
|
1694
|
+
|
|
1553
1695
|
// IMPORTANT: Send takeover announcement BEFORE switching channels
|
|
1554
1696
|
// This ensures other nodes on the current channel receive the announcement
|
|
1555
|
-
Log(STARTUP,
|
|
1697
|
+
Log(STARTUP,
|
|
1698
|
+
"Sending takeover announcement on current channel before "
|
|
1699
|
+
"switching...\n");
|
|
1556
1700
|
JsonDocument doc;
|
|
1557
1701
|
JsonObject obj = doc.to<JsonObject>();
|
|
1558
1702
|
obj["type"] = protocol::BRIDGE_TAKEOVER;
|
|
@@ -1563,62 +1707,68 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1563
1707
|
obj["routerRSSI"] = 0; // Not yet connected to router
|
|
1564
1708
|
obj["timestamp"] = this->getNodeTime();
|
|
1565
1709
|
obj["message_type"] = protocol::BRIDGE_TAKEOVER;
|
|
1566
|
-
|
|
1710
|
+
|
|
1567
1711
|
String msg;
|
|
1568
1712
|
serializeJson(doc, msg);
|
|
1569
|
-
|
|
1570
|
-
// Send takeover message using raw broadcast to preserve type
|
|
1713
|
+
|
|
1714
|
+
// Send takeover message using raw broadcast to preserve type
|
|
1715
|
+
// BRIDGE_TAKEOVER
|
|
1571
1716
|
protocol::Variant variant(msg);
|
|
1572
1717
|
router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
|
|
1573
|
-
|
|
1718
|
+
|
|
1574
1719
|
// Give time for announcement to propagate before channel switch
|
|
1575
1720
|
delay(1000);
|
|
1576
1721
|
Log(STARTUP, "✓ Takeover announcement sent on channel %d\n", _meshChannel);
|
|
1577
|
-
|
|
1722
|
+
|
|
1578
1723
|
// Save current mesh configuration to restore if bridge init fails
|
|
1579
1724
|
uint8_t savedChannel = _meshChannel;
|
|
1580
|
-
|
|
1725
|
+
|
|
1581
1726
|
// Now reconfigure as bridge (this will switch to router's channel)
|
|
1582
1727
|
this->stop();
|
|
1583
1728
|
delay(1000);
|
|
1584
|
-
|
|
1585
|
-
bool bridgeInitSuccess =
|
|
1586
|
-
|
|
1587
|
-
|
|
1729
|
+
|
|
1730
|
+
bool bridgeInitSuccess =
|
|
1731
|
+
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1732
|
+
mScheduler, _meshPort);
|
|
1733
|
+
|
|
1588
1734
|
if (!bridgeInitSuccess) {
|
|
1589
1735
|
Log(ERROR, "✗ Bridge promotion failed - router unreachable\n");
|
|
1590
1736
|
Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
|
|
1591
|
-
|
|
1737
|
+
|
|
1592
1738
|
// Re-initialize as regular node on the original channel
|
|
1593
1739
|
this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
|
|
1594
1740
|
savedChannel, _meshHidden, MAX_CONN);
|
|
1595
|
-
|
|
1596
|
-
// Reset election state and clear candidates (consistent with normal
|
|
1741
|
+
|
|
1742
|
+
// Reset election state and clear candidates (consistent with normal
|
|
1743
|
+
// election completion)
|
|
1597
1744
|
electionState = ELECTION_IDLE;
|
|
1598
1745
|
electionCandidates.clear();
|
|
1599
|
-
|
|
1746
|
+
|
|
1600
1747
|
// Notify via callback
|
|
1601
1748
|
if (bridgeRoleChangedCallback) {
|
|
1602
|
-
bridgeRoleChangedCallback(
|
|
1749
|
+
bridgeRoleChangedCallback(
|
|
1750
|
+
false, "Bridge promotion failed - router unreachable");
|
|
1603
1751
|
}
|
|
1604
|
-
|
|
1752
|
+
|
|
1605
1753
|
return;
|
|
1606
1754
|
}
|
|
1607
|
-
|
|
1755
|
+
|
|
1608
1756
|
lastRoleChangeTime = millis();
|
|
1609
|
-
|
|
1757
|
+
|
|
1610
1758
|
Log(STARTUP, "✓ Bridge promotion complete on channel %d\n", _meshChannel);
|
|
1611
|
-
|
|
1759
|
+
|
|
1612
1760
|
// Notify via callback
|
|
1613
1761
|
if (bridgeRoleChangedCallback) {
|
|
1614
1762
|
bridgeRoleChangedCallback(true, "Election winner - best router signal");
|
|
1615
1763
|
}
|
|
1616
|
-
|
|
1764
|
+
|
|
1617
1765
|
// Send a follow-up announcement on the new channel
|
|
1618
|
-
// This helps nodes that have already switched channels to discover the new
|
|
1619
|
-
// Schedule it after a delay to ensure mesh is fully initialized
|
|
1766
|
+
// This helps nodes that have already switched channels to discover the new
|
|
1767
|
+
// bridge Schedule it after a delay to ensure mesh is fully initialized
|
|
1620
1768
|
this->addTask(3000, TASK_ONCE, [this, previousBridgeId]() {
|
|
1621
|
-
Log(STARTUP,
|
|
1769
|
+
Log(STARTUP,
|
|
1770
|
+
"Sending follow-up takeover announcement on new channel %d\n",
|
|
1771
|
+
_meshChannel);
|
|
1622
1772
|
JsonDocument doc2;
|
|
1623
1773
|
JsonObject obj2 = doc2.to<JsonObject>();
|
|
1624
1774
|
obj2["type"] = protocol::BRIDGE_TAKEOVER;
|
|
@@ -1629,104 +1779,123 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1629
1779
|
obj2["routerRSSI"] = WiFi.RSSI();
|
|
1630
1780
|
obj2["timestamp"] = this->getNodeTime();
|
|
1631
1781
|
obj2["message_type"] = protocol::BRIDGE_TAKEOVER;
|
|
1632
|
-
|
|
1782
|
+
|
|
1633
1783
|
String msg2;
|
|
1634
1784
|
serializeJson(doc2, msg2);
|
|
1635
|
-
|
|
1636
|
-
// Send follow-up takeover using raw broadcast to preserve type
|
|
1785
|
+
|
|
1786
|
+
// Send follow-up takeover using raw broadcast to preserve type
|
|
1787
|
+
// BRIDGE_TAKEOVER
|
|
1637
1788
|
protocol::Variant variant2(msg2);
|
|
1638
1789
|
router::broadcast<protocol::Variant, Connection>(variant2, (*this), 0);
|
|
1639
|
-
|
|
1790
|
+
|
|
1640
1791
|
Log(STARTUP, "✓ Follow-up takeover announcement sent\n");
|
|
1641
1792
|
});
|
|
1642
1793
|
}
|
|
1643
1794
|
|
|
1644
1795
|
/**
|
|
1645
1796
|
* Attempt to promote an isolated node to bridge
|
|
1646
|
-
*
|
|
1797
|
+
*
|
|
1647
1798
|
* This method handles the case where a node is isolated (no mesh connections)
|
|
1648
1799
|
* but has router credentials. Unlike the election-based promotion, this
|
|
1649
|
-
* directly attempts to connect to the router without requiring mesh
|
|
1650
|
-
*
|
|
1800
|
+
* directly attempts to connect to the router without requiring mesh
|
|
1801
|
+
* connectivity.
|
|
1802
|
+
*
|
|
1651
1803
|
* This is useful for:
|
|
1652
1804
|
* - Nodes that failed initial bridge setup and need to retry
|
|
1653
1805
|
* - Nodes that are the first to start and no mesh exists yet
|
|
1654
1806
|
* - Recovery scenarios where mesh network is unavailable
|
|
1655
1807
|
*
|
|
1656
|
-
* @return true if promotion was attempted (regardless of success), false if
|
|
1808
|
+
* @return true if promotion was attempted (regardless of success), false if
|
|
1809
|
+
* skipped
|
|
1657
1810
|
*/
|
|
1658
1811
|
bool attemptIsolatedBridgePromotion() {
|
|
1659
1812
|
using namespace logger;
|
|
1660
|
-
|
|
1813
|
+
|
|
1661
1814
|
Log(CONNECTION, "=== Isolated Bridge Promotion Attempt ===\n");
|
|
1662
|
-
Log(CONNECTION, "Attempt %d of %d\n", _isolatedBridgeRetryAttempts + 1,
|
|
1663
|
-
|
|
1815
|
+
Log(CONNECTION, "Attempt %d of %d\n", _isolatedBridgeRetryAttempts + 1,
|
|
1816
|
+
MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS);
|
|
1817
|
+
|
|
1664
1818
|
// First, scan for router to check if it's visible
|
|
1665
1819
|
int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
|
|
1666
|
-
|
|
1820
|
+
|
|
1667
1821
|
if (routerRSSI == 0) {
|
|
1668
|
-
Log(CONNECTION,
|
|
1822
|
+
Log(CONNECTION,
|
|
1823
|
+
"attemptIsolatedBridgePromotion(): Router %s not visible\n",
|
|
1824
|
+
routerSSID.c_str());
|
|
1669
1825
|
return false; // Don't count as an attempt - router not visible
|
|
1670
1826
|
}
|
|
1671
|
-
|
|
1827
|
+
|
|
1672
1828
|
// Check minimum RSSI threshold for isolated promotion
|
|
1673
1829
|
if (routerRSSI < minimumBridgeRSSI) {
|
|
1674
|
-
Log(CONNECTION,
|
|
1830
|
+
Log(CONNECTION,
|
|
1831
|
+
"attemptIsolatedBridgePromotion(): Router RSSI %d dBm below "
|
|
1832
|
+
"threshold %d dBm\n",
|
|
1675
1833
|
routerRSSI, minimumBridgeRSSI);
|
|
1676
1834
|
return false; // Don't count as an attempt - signal too weak
|
|
1677
1835
|
}
|
|
1678
|
-
|
|
1679
|
-
Log(CONNECTION,
|
|
1680
|
-
|
|
1681
|
-
|
|
1836
|
+
|
|
1837
|
+
Log(CONNECTION,
|
|
1838
|
+
"attemptIsolatedBridgePromotion(): Router visible with RSSI %d dBm\n",
|
|
1839
|
+
routerRSSI);
|
|
1840
|
+
Log(CONNECTION,
|
|
1841
|
+
"Attempting direct bridge promotion (bypassing election)\n");
|
|
1842
|
+
|
|
1682
1843
|
// Save current mesh configuration
|
|
1683
1844
|
uint8_t savedChannel = _meshChannel;
|
|
1684
|
-
|
|
1845
|
+
|
|
1685
1846
|
// Stop current mesh operations
|
|
1686
1847
|
this->stop();
|
|
1687
1848
|
delay(1000);
|
|
1688
|
-
|
|
1849
|
+
|
|
1689
1850
|
// Attempt to initialize as bridge
|
|
1690
|
-
bool bridgeInitSuccess =
|
|
1691
|
-
|
|
1692
|
-
|
|
1851
|
+
bool bridgeInitSuccess =
|
|
1852
|
+
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1853
|
+
mScheduler, _meshPort);
|
|
1854
|
+
|
|
1693
1855
|
if (!bridgeInitSuccess) {
|
|
1694
1856
|
Log(ERROR, "✗ Isolated bridge promotion failed - router unreachable\n");
|
|
1695
1857
|
Log(ERROR, "Reverting to regular node on channel %d\n", savedChannel);
|
|
1696
|
-
|
|
1858
|
+
|
|
1697
1859
|
// Re-initialize as regular node on the original channel
|
|
1698
1860
|
this->init(_meshSSID, _meshPassword, mScheduler, _meshPort, WIFI_AP_STA,
|
|
1699
1861
|
savedChannel, _meshHidden, MAX_CONN);
|
|
1700
|
-
|
|
1862
|
+
|
|
1701
1863
|
// Re-configure router credentials for future retry attempts
|
|
1702
1864
|
this->setRouterCredentials(routerSSID, routerPassword);
|
|
1703
1865
|
this->enableBridgeFailover(true);
|
|
1704
|
-
|
|
1866
|
+
|
|
1867
|
+
// Set flag to skip empty scan check on next retry attempt
|
|
1868
|
+
// since we already confirmed isolation before this failed attempt
|
|
1869
|
+
_isolatedRetryPending = true;
|
|
1870
|
+
|
|
1705
1871
|
// Notify via callback
|
|
1706
1872
|
if (bridgeRoleChangedCallback) {
|
|
1707
|
-
bridgeRoleChangedCallback(
|
|
1873
|
+
bridgeRoleChangedCallback(
|
|
1874
|
+
false, "Isolated bridge promotion failed - router unreachable");
|
|
1708
1875
|
}
|
|
1709
|
-
|
|
1876
|
+
|
|
1710
1877
|
return true; // Count as an attempt - we tried but failed
|
|
1711
1878
|
}
|
|
1712
|
-
|
|
1879
|
+
|
|
1713
1880
|
// Success! Reset retry counter
|
|
1714
1881
|
_isolatedBridgeRetryAttempts = 0;
|
|
1715
1882
|
lastRoleChangeTime = millis();
|
|
1716
|
-
|
|
1717
|
-
Log(STARTUP, "✓ Isolated bridge promotion complete on channel %d\n",
|
|
1718
|
-
|
|
1883
|
+
|
|
1884
|
+
Log(STARTUP, "✓ Isolated bridge promotion complete on channel %d\n",
|
|
1885
|
+
_meshChannel);
|
|
1886
|
+
|
|
1719
1887
|
// Notify via callback
|
|
1720
1888
|
if (bridgeRoleChangedCallback) {
|
|
1721
1889
|
bridgeRoleChangedCallback(true, "Isolated node promoted to bridge");
|
|
1722
1890
|
}
|
|
1723
|
-
|
|
1891
|
+
|
|
1724
1892
|
// Send bridge status announcement to attract other nodes
|
|
1725
1893
|
this->addTask(3000, TASK_ONCE, [this]() {
|
|
1726
|
-
Log(STARTUP, "Sending bridge status announcement on channel %d\n",
|
|
1894
|
+
Log(STARTUP, "Sending bridge status announcement on channel %d\n",
|
|
1895
|
+
_meshChannel);
|
|
1727
1896
|
this->sendBridgeStatus();
|
|
1728
1897
|
});
|
|
1729
|
-
|
|
1898
|
+
|
|
1730
1899
|
return true; // Count as an attempt - we succeeded
|
|
1731
1900
|
}
|
|
1732
1901
|
|
|
@@ -1734,33 +1903,37 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1734
1903
|
* Handle received bridge election package
|
|
1735
1904
|
* Called by package handler when election message arrives
|
|
1736
1905
|
*/
|
|
1737
|
-
void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI,
|
|
1738
|
-
uint32_t freeMemory) {
|
|
1906
|
+
void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI,
|
|
1907
|
+
uint32_t uptime, uint32_t freeMemory) {
|
|
1739
1908
|
using namespace logger;
|
|
1740
|
-
|
|
1909
|
+
|
|
1741
1910
|
if (electionState != ELECTION_COLLECTING) {
|
|
1742
|
-
Log(CONNECTION,
|
|
1911
|
+
Log(CONNECTION,
|
|
1912
|
+
"handleBridgeElection(): Not collecting candidates, ignoring\n");
|
|
1743
1913
|
return;
|
|
1744
1914
|
}
|
|
1745
|
-
|
|
1915
|
+
|
|
1746
1916
|
// Check if candidate already exists
|
|
1747
1917
|
for (auto& candidate : electionCandidates) {
|
|
1748
1918
|
if (candidate.nodeId == fromNode) {
|
|
1749
|
-
Log(CONNECTION,
|
|
1919
|
+
Log(CONNECTION,
|
|
1920
|
+
"handleBridgeElection(): Duplicate candidate from %u, ignoring\n",
|
|
1921
|
+
fromNode);
|
|
1750
1922
|
return;
|
|
1751
1923
|
}
|
|
1752
1924
|
}
|
|
1753
|
-
|
|
1925
|
+
|
|
1754
1926
|
BridgeCandidate candidate;
|
|
1755
1927
|
candidate.nodeId = fromNode;
|
|
1756
1928
|
candidate.routerRSSI = routerRSSI;
|
|
1757
1929
|
candidate.uptime = uptime;
|
|
1758
1930
|
candidate.freeMemory = freeMemory;
|
|
1759
|
-
|
|
1931
|
+
|
|
1760
1932
|
electionCandidates.push_back(candidate);
|
|
1761
|
-
|
|
1762
|
-
Log(CONNECTION,
|
|
1763
|
-
fromNode,
|
|
1933
|
+
|
|
1934
|
+
Log(CONNECTION,
|
|
1935
|
+
"handleBridgeElection(): Added candidate %u (RSSI: %d dBm)\n", fromNode,
|
|
1936
|
+
routerRSSI);
|
|
1764
1937
|
}
|
|
1765
1938
|
|
|
1766
1939
|
/**
|
|
@@ -1769,60 +1942,200 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1769
1942
|
*/
|
|
1770
1943
|
void sendBridgeStatus() {
|
|
1771
1944
|
using namespace logger;
|
|
1772
|
-
|
|
1945
|
+
|
|
1773
1946
|
if (!this->bridgeStatusBroadcastEnabled) {
|
|
1774
1947
|
return;
|
|
1775
1948
|
}
|
|
1776
|
-
|
|
1949
|
+
|
|
1777
1950
|
// Create bridge status package
|
|
1778
1951
|
// We need to include the package header here since we're in wifi namespace
|
|
1779
1952
|
// The package will be sent as a JSON string
|
|
1780
1953
|
JsonDocument doc;
|
|
1781
1954
|
JsonObject obj = doc.to<JsonObject>();
|
|
1782
|
-
|
|
1955
|
+
|
|
1783
1956
|
obj["type"] = protocol::BRIDGE_STATUS;
|
|
1784
1957
|
obj["from"] = this->nodeId;
|
|
1785
1958
|
obj["routing"] = 2; // BROADCAST routing
|
|
1786
1959
|
obj["timestamp"] = this->getNodeTime();
|
|
1787
|
-
|
|
1960
|
+
|
|
1788
1961
|
// Check Internet connectivity: WiFi connected AND valid IP address
|
|
1789
1962
|
// We check for valid local IP instead of gateway IP because:
|
|
1790
1963
|
// 1. Gateway IP might not be immediately available after connection
|
|
1791
1964
|
// 2. Some networks (mobile hotspots) may not provide gateway IP via DHCP
|
|
1792
|
-
// 3. Having a valid local IP + being connected is sufficient for internet
|
|
1793
|
-
|
|
1965
|
+
// 3. Having a valid local IP + being connected is sufficient for internet
|
|
1966
|
+
// access
|
|
1967
|
+
bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
|
|
1794
1968
|
(WiFi.localIP() != IPAddress(0, 0, 0, 0));
|
|
1795
1969
|
obj["internetConnected"] = hasInternet;
|
|
1796
|
-
|
|
1970
|
+
|
|
1797
1971
|
int8_t rssi = WiFi.RSSI();
|
|
1798
1972
|
uint8_t channel = WiFi.channel();
|
|
1799
1973
|
uint32_t uptime = millis();
|
|
1800
1974
|
TSTRING gatewayIP = WiFi.gatewayIP().toString();
|
|
1801
|
-
|
|
1975
|
+
|
|
1802
1976
|
obj["routerRSSI"] = rssi;
|
|
1803
1977
|
obj["routerChannel"] = channel;
|
|
1804
1978
|
obj["uptime"] = uptime;
|
|
1805
1979
|
obj["gatewayIP"] = gatewayIP;
|
|
1806
1980
|
obj["message_type"] = protocol::BRIDGE_STATUS;
|
|
1807
|
-
|
|
1981
|
+
|
|
1808
1982
|
String msg;
|
|
1809
1983
|
serializeJson(doc, msg);
|
|
1810
|
-
|
|
1984
|
+
|
|
1811
1985
|
Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)\n",
|
|
1812
1986
|
hasInternet ? "Connected" : "Disconnected");
|
|
1813
|
-
Log(GENERAL,
|
|
1814
|
-
WiFi
|
|
1815
|
-
|
|
1987
|
+
Log(GENERAL,
|
|
1988
|
+
"sendBridgeStatus(): WiFi status=%d, localIP=%s, gatewayIP=%s\n",
|
|
1989
|
+
WiFi.status(), WiFi.localIP().toString().c_str(),
|
|
1990
|
+
WiFi.gatewayIP().toString().c_str());
|
|
1991
|
+
|
|
1816
1992
|
// Update our own bridge status in knownBridges list
|
|
1817
1993
|
// This ensures the bridge reports itself correctly when queried
|
|
1818
|
-
this->updateBridgeStatus(this->nodeId, hasInternet, rssi, channel,
|
|
1819
|
-
|
|
1820
|
-
|
|
1994
|
+
this->updateBridgeStatus(this->nodeId, hasInternet, rssi, channel, uptime,
|
|
1995
|
+
gatewayIP, this->getNodeTime());
|
|
1996
|
+
|
|
1821
1997
|
// Send bridge status using raw broadcast to preserve type BRIDGE_STATUS
|
|
1822
|
-
// Using sendBroadcast(msg) would wrap it in type 8 (BROADCAST) and hide
|
|
1998
|
+
// Using sendBroadcast(msg) would wrap it in type 8 (BROADCAST) and hide
|
|
1999
|
+
// type BRIDGE_STATUS
|
|
1823
2000
|
protocol::Variant variant(msg);
|
|
1824
2001
|
router::broadcast<protocol::Variant, Connection>(variant, (*this), 0);
|
|
1825
2002
|
}
|
|
2003
|
+
|
|
2004
|
+
/**
|
|
2005
|
+
* Helper method to send gateway acknowledgment
|
|
2006
|
+
*/
|
|
2007
|
+
void sendGatewayAck(const gateway::GatewayDataPackage& request, bool success,
|
|
2008
|
+
uint16_t httpStatus, const TSTRING& error) {
|
|
2009
|
+
using namespace logger;
|
|
2010
|
+
|
|
2011
|
+
gateway::GatewayAckPackage ack;
|
|
2012
|
+
ack.from = this->nodeId;
|
|
2013
|
+
ack.dest = request.originNode;
|
|
2014
|
+
ack.messageId = request.messageId;
|
|
2015
|
+
ack.originNode = request.originNode;
|
|
2016
|
+
ack.success = success;
|
|
2017
|
+
ack.httpStatus = httpStatus;
|
|
2018
|
+
ack.error = error;
|
|
2019
|
+
ack.timestamp = this->getNodeTime();
|
|
2020
|
+
|
|
2021
|
+
auto conn = router::findRoute<Connection>((*this), request.originNode);
|
|
2022
|
+
if (conn) {
|
|
2023
|
+
protocol::Variant variant(&ack);
|
|
2024
|
+
router::send(std::move(variant), conn);
|
|
2025
|
+
Log(COMMUNICATION, "Sent GATEWAY_ACK to node %u (success=%d, http=%d)\n",
|
|
2026
|
+
request.originNode, success, httpStatus);
|
|
2027
|
+
} else {
|
|
2028
|
+
Log(ERROR, "Failed to send GATEWAY_ACK: no route to node %u\n",
|
|
2029
|
+
request.originNode);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/**
|
|
2034
|
+
* Initialize gateway Internet handler
|
|
2035
|
+
* Registers GATEWAY_DATA package handler for bridge/gateway nodes
|
|
2036
|
+
*
|
|
2037
|
+
* This handler processes GATEWAY_DATA packages from mesh nodes requesting
|
|
2038
|
+
* HTTP/HTTPS requests to Internet destinations. It validates connectivity,
|
|
2039
|
+
* makes the request, and sends back a GATEWAY_ACK with the result.
|
|
2040
|
+
*
|
|
2041
|
+
* Security notes:
|
|
2042
|
+
* - HTTPS on ESP8266 uses setInsecure() which disables SSL certificate
|
|
2043
|
+
* validation to reduce memory overhead. This makes connections vulnerable to
|
|
2044
|
+
* MITM attacks.
|
|
2045
|
+
* - ESP32 uses default SSL settings with certificate validation.
|
|
2046
|
+
*
|
|
2047
|
+
* Limitations:
|
|
2048
|
+
* - HTTP redirects (3xx) are not automatically followed
|
|
2049
|
+
* - Only 2xx status codes are treated as success
|
|
2050
|
+
* - Request timeout is fixed at 30 seconds
|
|
2051
|
+
*/
|
|
2052
|
+
void initGatewayInternetHandler() {
|
|
2053
|
+
using namespace logger;
|
|
2054
|
+
Log(STARTUP,
|
|
2055
|
+
"initGatewayInternetHandler(): Registering GATEWAY_DATA handler\n");
|
|
2056
|
+
|
|
2057
|
+
this->callbackList.onPackage(
|
|
2058
|
+
protocol::GATEWAY_DATA, [this](protocol::Variant& variant,
|
|
2059
|
+
std::shared_ptr<Connection>, uint32_t) {
|
|
2060
|
+
auto pkg = variant.to<gateway::GatewayDataPackage>();
|
|
2061
|
+
|
|
2062
|
+
Log(COMMUNICATION,
|
|
2063
|
+
"Gateway received Internet request: msgId=%u dest=%s\n",
|
|
2064
|
+
pkg.messageId, pkg.destination.c_str());
|
|
2065
|
+
|
|
2066
|
+
// Check Internet connectivity
|
|
2067
|
+
if (WiFi.status() != WL_CONNECTED) {
|
|
2068
|
+
sendGatewayAck(pkg, false, 0, "Gateway not connected to Internet");
|
|
2069
|
+
return true; // Consume package - we handled it (with error)
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
#if defined(ESP32) || defined(ESP8266)
|
|
2073
|
+
// Make HTTP/HTTPS request
|
|
2074
|
+
HTTPClient http;
|
|
2075
|
+
http.setTimeout(GATEWAY_HTTP_TIMEOUT_MS);
|
|
2076
|
+
|
|
2077
|
+
bool success = false;
|
|
2078
|
+
uint16_t httpCode = 0;
|
|
2079
|
+
TSTRING error = "";
|
|
2080
|
+
|
|
2081
|
+
#ifdef ESP8266
|
|
2082
|
+
// ESP8266: Declare clients at function scope to ensure
|
|
2083
|
+
// they survive until after the HTTP request completes
|
|
2084
|
+
WiFiClient client;
|
|
2085
|
+
WiFiClientSecure secureClient;
|
|
2086
|
+
#endif
|
|
2087
|
+
|
|
2088
|
+
if (pkg.destination.startsWith("https://")) {
|
|
2089
|
+
#ifdef ESP32
|
|
2090
|
+
// ESP32: Use default SSL settings with certificate validation
|
|
2091
|
+
http.begin(pkg.destination.c_str());
|
|
2092
|
+
#elif defined(ESP8266)
|
|
2093
|
+
// ESP8266: Use insecure mode to reduce memory overhead
|
|
2094
|
+
// WARNING: This disables SSL certificate validation
|
|
2095
|
+
secureClient.setInsecure();
|
|
2096
|
+
http.begin(secureClient, pkg.destination.c_str());
|
|
2097
|
+
#endif
|
|
2098
|
+
} else {
|
|
2099
|
+
#ifdef ESP32
|
|
2100
|
+
http.begin(pkg.destination.c_str());
|
|
2101
|
+
#elif defined(ESP8266)
|
|
2102
|
+
// ESP8266: begin() requires a client parameter
|
|
2103
|
+
http.begin(client, pkg.destination.c_str());
|
|
2104
|
+
#endif
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// Make request (GET if no payload, POST if payload)
|
|
2108
|
+
if (pkg.payload.length() > 0) {
|
|
2109
|
+
http.addHeader("Content-Type", pkg.contentType.c_str());
|
|
2110
|
+
httpCode = http.POST(pkg.payload.c_str());
|
|
2111
|
+
} else {
|
|
2112
|
+
httpCode = http.GET();
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
if (httpCode > 0) {
|
|
2116
|
+
// Only 2xx status codes are treated as success
|
|
2117
|
+
// 3xx redirects are not automatically followed
|
|
2118
|
+
success = (httpCode >= 200 && httpCode < 300);
|
|
2119
|
+
Log(COMMUNICATION, "HTTP request completed: code=%d\n", httpCode);
|
|
2120
|
+
} else {
|
|
2121
|
+
error = http.errorToString(httpCode);
|
|
2122
|
+
Log(ERROR, "HTTP request failed: %s\n", error.c_str());
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
http.end();
|
|
2126
|
+
|
|
2127
|
+
// Send acknowledgment back
|
|
2128
|
+
sendGatewayAck(pkg, success, httpCode, error);
|
|
2129
|
+
#else
|
|
2130
|
+
// Non-ESP platform - send error
|
|
2131
|
+
sendGatewayAck(pkg, false, 0, "HTTP client not available on this platform");
|
|
2132
|
+
#endif
|
|
2133
|
+
|
|
2134
|
+
return true; // Consume package - we have processed it and sent
|
|
2135
|
+
// acknowledgment
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
|
|
1826
2139
|
void eventHandleInit() {
|
|
1827
2140
|
using namespace logger;
|
|
1828
2141
|
#ifdef ESP32
|
|
@@ -1890,14 +2203,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1890
2203
|
|
|
1891
2204
|
#elif defined(ESP8266)
|
|
1892
2205
|
eventSTAConnectedHandler = WiFi.onStationModeConnected(
|
|
1893
|
-
[&](const WiFiEventStationModeConnected
|
|
2206
|
+
[&](const WiFiEventStationModeConnected& event) {
|
|
1894
2207
|
// Log(CONNECTION, "Event: Station Mode Connected to \"%s\"\n",
|
|
1895
2208
|
// event.ssid.c_str());
|
|
1896
2209
|
Log(CONNECTION, "Event: Station Mode Connected\n");
|
|
1897
2210
|
});
|
|
1898
2211
|
|
|
1899
2212
|
eventSTADisconnectedHandler = WiFi.onStationModeDisconnected(
|
|
1900
|
-
[&](const WiFiEventStationModeDisconnected
|
|
2213
|
+
[&](const WiFiEventStationModeDisconnected& event) {
|
|
1901
2214
|
Log(CONNECTION, "Event: Station Mode Disconnected\n");
|
|
1902
2215
|
this->droppedConnectionCallbacks.execute(0, true);
|
|
1903
2216
|
// Handle station disconnect completion after callbacks
|
|
@@ -1905,7 +2218,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1905
2218
|
});
|
|
1906
2219
|
|
|
1907
2220
|
eventSTAGotIPHandler =
|
|
1908
|
-
WiFi.onStationModeGotIP([&](const WiFiEventStationModeGotIP
|
|
2221
|
+
WiFi.onStationModeGotIP([&](const WiFiEventStationModeGotIP& event) {
|
|
1909
2222
|
Log(CONNECTION,
|
|
1910
2223
|
"Event: Station Mode Got IP (IP: %s Mask: %s Gateway: %s)\n",
|
|
1911
2224
|
event.ip.toString().c_str(), event.mask.toString().c_str(),
|
|
@@ -1926,18 +2239,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1926
2239
|
WiFiEventHandler eventSTADisconnectedHandler;
|
|
1927
2240
|
WiFiEventHandler eventSTAGotIPHandler;
|
|
1928
2241
|
#endif // ESP8266
|
|
1929
|
-
AsyncServer
|
|
2242
|
+
AsyncServer* _tcpListener;
|
|
1930
2243
|
std::shared_ptr<Task> bridgeStatusTask;
|
|
1931
|
-
|
|
2244
|
+
|
|
1932
2245
|
// Station disconnect handling state
|
|
1933
2246
|
bool _pendingStationReconnect = false;
|
|
1934
2247
|
|
|
1935
2248
|
// Bridge failover state and configuration
|
|
1936
|
-
enum ElectionState {
|
|
1937
|
-
ELECTION_IDLE,
|
|
1938
|
-
ELECTION_SCANNING,
|
|
1939
|
-
ELECTION_COLLECTING
|
|
1940
|
-
};
|
|
2249
|
+
enum ElectionState { ELECTION_IDLE, ELECTION_SCANNING, ELECTION_COLLECTING };
|
|
1941
2250
|
|
|
1942
2251
|
struct BridgeCandidate {
|
|
1943
2252
|
uint32_t nodeId;
|
|
@@ -1951,10 +2260,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1951
2260
|
TSTRING routerSSID = "";
|
|
1952
2261
|
TSTRING routerPassword = "";
|
|
1953
2262
|
uint32_t electionTimeoutMs = 5000; // Default 5 seconds
|
|
1954
|
-
int8_t minimumBridgeRSSI =
|
|
1955
|
-
|
|
1956
|
-
uint32_t
|
|
1957
|
-
|
|
2263
|
+
int8_t minimumBridgeRSSI =
|
|
2264
|
+
-80; // Default -80 dBm minimum for isolated elections
|
|
2265
|
+
uint32_t electionStartupDelayMs =
|
|
2266
|
+
60000; // Default 60 seconds before first election check
|
|
2267
|
+
uint32_t electionRandomDelayMinMs =
|
|
2268
|
+
1000; // Default min 1 second random delay
|
|
2269
|
+
uint32_t electionRandomDelayMaxMs =
|
|
2270
|
+
3000; // Default max 3 seconds random delay
|
|
1958
2271
|
uint32_t lastRoleChangeTime = 0;
|
|
1959
2272
|
ElectionState electionState = ELECTION_IDLE;
|
|
1960
2273
|
uint32_t electionDeadline = 0;
|
|
@@ -1963,24 +2276,31 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1963
2276
|
|
|
1964
2277
|
// Isolated bridge retry state and configuration
|
|
1965
2278
|
uint8_t _isolatedBridgeRetryAttempts = 0;
|
|
1966
|
-
uint32_t _isolatedBridgeRetryResetTime =
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
static const
|
|
2279
|
+
uint32_t _isolatedBridgeRetryResetTime =
|
|
2280
|
+
0; // Time when retry counter can be reset
|
|
2281
|
+
bool _isolatedRetryPending =
|
|
2282
|
+
false; // Flag to skip empty scan check after failed promotion
|
|
2283
|
+
static const uint8_t MAX_ISOLATED_BRIDGE_RETRY_ATTEMPTS =
|
|
2284
|
+
5; // Max retry attempts before waiting
|
|
2285
|
+
static const uint32_t isolatedBridgeRetryIntervalMs =
|
|
2286
|
+
60000; // Retry every 60 seconds
|
|
2287
|
+
static const uint32_t isolatedBridgeRetryResetIntervalMs =
|
|
2288
|
+
300000; // Reset counter after 5 minutes
|
|
2289
|
+
static const uint16_t ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD =
|
|
2290
|
+
6; // Require 6 empty scans before retrying
|
|
1971
2291
|
|
|
1972
2292
|
// Multi-bridge coordination state and configuration
|
|
1973
2293
|
protected:
|
|
1974
2294
|
bool multiBridgeEnabled = false;
|
|
1975
2295
|
BridgeSelectionStrategy bridgeSelectionStrategy = PRIORITY_BASED;
|
|
1976
2296
|
uint8_t maxConcurrentBridges = 2;
|
|
1977
|
-
uint8_t bridgePriority = 5;
|
|
2297
|
+
uint8_t bridgePriority = 5; // Default medium priority
|
|
1978
2298
|
TSTRING bridgeRole = "secondary"; // Default role
|
|
1979
2299
|
std::shared_ptr<Task> bridgeCoordinationTask;
|
|
1980
2300
|
std::map<uint32_t, uint8_t> bridgePriorities; // nodeId -> priority mapping
|
|
1981
|
-
std::vector<uint32_t> knownBridgePeers;
|
|
2301
|
+
std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
|
|
1982
2302
|
uint32_t selectedBridgeOverride = 0; // Manual bridge selection override
|
|
1983
|
-
size_t lastSelectedBridgeIndex = 0;
|
|
2303
|
+
size_t lastSelectedBridgeIndex = 0; // For round-robin selection
|
|
1984
2304
|
|
|
1985
2305
|
// Shared gateway mode state and configuration
|
|
1986
2306
|
bool _sharedGatewayMode = false;
|
|
@@ -1989,72 +2309,83 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1989
2309
|
uint32_t _lastRouterReconnectAttempt = 0;
|
|
1990
2310
|
uint8_t _routerReconnectAttempts = 0;
|
|
1991
2311
|
static const uint8_t MAX_ROUTER_RECONNECT_ATTEMPTS = 10;
|
|
1992
|
-
static const uint32_t ROUTER_RECONNECT_BASE_INTERVAL =
|
|
1993
|
-
|
|
1994
|
-
static const
|
|
2312
|
+
static const uint32_t ROUTER_RECONNECT_BASE_INTERVAL =
|
|
2313
|
+
5000; // 5 seconds base interval
|
|
2314
|
+
static const uint32_t ROUTER_RECONNECT_MAX_INTERVAL =
|
|
2315
|
+
300000; // 5 minutes max interval
|
|
2316
|
+
static const int ROUTER_CONNECTION_TIMEOUT_SECONDS =
|
|
2317
|
+
30; // Router connection timeout
|
|
1995
2318
|
static const uint8_t MIN_WIFI_CHANNEL = 1;
|
|
1996
|
-
static const uint8_t MAX_WIFI_CHANNEL =
|
|
2319
|
+
static const uint8_t MAX_WIFI_CHANNEL =
|
|
2320
|
+
14; // Support channels 1-14 for regions that allow it
|
|
2321
|
+
static const uint32_t GATEWAY_HTTP_TIMEOUT_MS =
|
|
2322
|
+
30000; // 30 second timeout for gateway HTTP requests
|
|
1997
2323
|
|
|
1998
2324
|
/**
|
|
1999
2325
|
* Initialize shared gateway monitoring
|
|
2000
|
-
*
|
|
2326
|
+
*
|
|
2001
2327
|
* Sets up periodic monitoring of router connection and automatic
|
|
2002
2328
|
* reconnection logic for shared gateway mode.
|
|
2003
2329
|
*/
|
|
2004
2330
|
void initSharedGatewayMonitoring() {
|
|
2005
2331
|
using namespace logger;
|
|
2006
|
-
|
|
2332
|
+
|
|
2007
2333
|
if (!_sharedGatewayMode) {
|
|
2008
2334
|
return;
|
|
2009
2335
|
}
|
|
2010
|
-
|
|
2011
|
-
Log(STARTUP,
|
|
2012
|
-
|
|
2336
|
+
|
|
2337
|
+
Log(STARTUP,
|
|
2338
|
+
"initSharedGatewayMonitoring(): Setting up router connection "
|
|
2339
|
+
"monitoring\n");
|
|
2340
|
+
|
|
2013
2341
|
// Add callback for router disconnection in shared gateway mode
|
|
2014
2342
|
this->droppedConnectionCallbacks.push_back(
|
|
2015
2343
|
[this](uint32_t nodeId, bool station) {
|
|
2016
2344
|
if (station && _sharedGatewayMode) {
|
|
2017
|
-
Log(CONNECTION,
|
|
2345
|
+
Log(CONNECTION,
|
|
2346
|
+
"Router disconnected in shared gateway mode, scheduling "
|
|
2347
|
+
"reconnection\n");
|
|
2018
2348
|
scheduleRouterReconnect();
|
|
2019
2349
|
}
|
|
2020
2350
|
});
|
|
2021
|
-
|
|
2351
|
+
|
|
2022
2352
|
// Create periodic monitoring task
|
|
2023
|
-
_sharedGatewayMonitorTask =
|
|
2024
|
-
_sharedGatewayConfig.internetCheckInterval,
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
monitorRouterConnection();
|
|
2028
|
-
});
|
|
2029
|
-
|
|
2353
|
+
_sharedGatewayMonitorTask =
|
|
2354
|
+
this->addTask(_sharedGatewayConfig.internetCheckInterval, TASK_FOREVER,
|
|
2355
|
+
[this]() { monitorRouterConnection(); });
|
|
2356
|
+
|
|
2030
2357
|
Log(STARTUP, "Router connection monitoring enabled (interval: %u ms)\n",
|
|
2031
2358
|
_sharedGatewayConfig.internetCheckInterval);
|
|
2032
2359
|
}
|
|
2033
2360
|
|
|
2034
2361
|
/**
|
|
2035
2362
|
* Monitor router connection in shared gateway mode
|
|
2036
|
-
*
|
|
2363
|
+
*
|
|
2037
2364
|
* Checks router connectivity and triggers reconnection if needed.
|
|
2038
2365
|
*/
|
|
2039
2366
|
void monitorRouterConnection() {
|
|
2040
2367
|
using namespace logger;
|
|
2041
|
-
|
|
2368
|
+
|
|
2042
2369
|
if (!_sharedGatewayMode) {
|
|
2043
2370
|
return;
|
|
2044
2371
|
}
|
|
2045
|
-
|
|
2046
|
-
bool isConnected = (WiFi.status() == WL_CONNECTED) &&
|
|
2372
|
+
|
|
2373
|
+
bool isConnected = (WiFi.status() == WL_CONNECTED) &&
|
|
2047
2374
|
(WiFi.localIP() != IPAddress(0, 0, 0, 0));
|
|
2048
|
-
|
|
2375
|
+
|
|
2049
2376
|
if (!isConnected) {
|
|
2050
|
-
Log(CONNECTION,
|
|
2377
|
+
Log(CONNECTION,
|
|
2378
|
+
"monitorRouterConnection(): Router connection lost, triggering "
|
|
2379
|
+
"reconnect\n");
|
|
2051
2380
|
scheduleRouterReconnect();
|
|
2052
2381
|
} else {
|
|
2053
2382
|
// Connection is healthy, reset reconnect attempts
|
|
2054
2383
|
_routerReconnectAttempts = 0;
|
|
2055
|
-
|
|
2384
|
+
|
|
2056
2385
|
// Log periodic status
|
|
2057
|
-
Log(GENERAL,
|
|
2386
|
+
Log(GENERAL,
|
|
2387
|
+
"monitorRouterConnection(): Router connected (RSSI: %d dBm, IP: "
|
|
2388
|
+
"%s)\n",
|
|
2058
2389
|
WiFi.RSSI(), WiFi.localIP().toString().c_str());
|
|
2059
2390
|
}
|
|
2060
2391
|
}
|
|
@@ -2064,46 +2395,51 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2064
2395
|
*/
|
|
2065
2396
|
void scheduleRouterReconnect() {
|
|
2066
2397
|
using namespace logger;
|
|
2067
|
-
|
|
2398
|
+
|
|
2068
2399
|
if (!_sharedGatewayMode) {
|
|
2069
2400
|
return;
|
|
2070
2401
|
}
|
|
2071
|
-
|
|
2402
|
+
|
|
2072
2403
|
// Don't schedule if already connected
|
|
2073
2404
|
if (WiFi.status() == WL_CONNECTED) {
|
|
2074
2405
|
return;
|
|
2075
2406
|
}
|
|
2076
|
-
|
|
2407
|
+
|
|
2077
2408
|
// Limit reconnection attempts
|
|
2078
2409
|
if (_routerReconnectAttempts >= MAX_ROUTER_RECONNECT_ATTEMPTS) {
|
|
2079
|
-
Log(ERROR,
|
|
2410
|
+
Log(ERROR,
|
|
2411
|
+
"scheduleRouterReconnect(): Max reconnection attempts reached (%d)\n",
|
|
2080
2412
|
MAX_ROUTER_RECONNECT_ATTEMPTS);
|
|
2081
|
-
Log(ERROR,
|
|
2413
|
+
Log(ERROR,
|
|
2414
|
+
"Router reconnection suspended. Manual intervention may be "
|
|
2415
|
+
"required.\n");
|
|
2082
2416
|
return;
|
|
2083
2417
|
}
|
|
2084
|
-
|
|
2418
|
+
|
|
2085
2419
|
// Calculate delay with exponential backoff, preventing overflow
|
|
2086
2420
|
// Limit shift amount to prevent overflow (5000 * 2^6 = 320000 is safe)
|
|
2087
|
-
uint8_t shiftAmount =
|
|
2421
|
+
uint8_t shiftAmount =
|
|
2422
|
+
(_routerReconnectAttempts > 6) ? 6 : _routerReconnectAttempts;
|
|
2088
2423
|
uint32_t delay = ROUTER_RECONNECT_BASE_INTERVAL * (1UL << shiftAmount);
|
|
2089
|
-
if (delay > ROUTER_RECONNECT_MAX_INTERVAL)
|
|
2090
|
-
|
|
2424
|
+
if (delay > ROUTER_RECONNECT_MAX_INTERVAL)
|
|
2425
|
+
delay = ROUTER_RECONNECT_MAX_INTERVAL;
|
|
2426
|
+
|
|
2091
2427
|
// Don't reconnect too frequently
|
|
2092
2428
|
uint32_t now = millis();
|
|
2093
2429
|
if (now - _lastRouterReconnectAttempt < delay) {
|
|
2094
2430
|
return;
|
|
2095
2431
|
}
|
|
2096
|
-
|
|
2432
|
+
|
|
2097
2433
|
_routerReconnectAttempts++;
|
|
2098
2434
|
_lastRouterReconnectAttempt = now;
|
|
2099
|
-
|
|
2100
|
-
Log(CONNECTION,
|
|
2435
|
+
|
|
2436
|
+
Log(CONNECTION,
|
|
2437
|
+
"scheduleRouterReconnect(): Attempting reconnection (attempt %d/%d, "
|
|
2438
|
+
"delay %u ms)\n",
|
|
2101
2439
|
_routerReconnectAttempts, MAX_ROUTER_RECONNECT_ATTEMPTS, delay);
|
|
2102
|
-
|
|
2440
|
+
|
|
2103
2441
|
// Schedule reconnection
|
|
2104
|
-
this->addTask(delay, TASK_ONCE, [this]() {
|
|
2105
|
-
attemptRouterReconnect();
|
|
2106
|
-
});
|
|
2442
|
+
this->addTask(delay, TASK_ONCE, [this]() { attemptRouterReconnect(); });
|
|
2107
2443
|
}
|
|
2108
2444
|
|
|
2109
2445
|
/**
|
|
@@ -2111,23 +2447,26 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2111
2447
|
*/
|
|
2112
2448
|
void attemptRouterReconnect() {
|
|
2113
2449
|
using namespace logger;
|
|
2114
|
-
|
|
2450
|
+
|
|
2115
2451
|
if (!_sharedGatewayMode) {
|
|
2116
2452
|
return;
|
|
2117
2453
|
}
|
|
2118
|
-
|
|
2454
|
+
|
|
2119
2455
|
// Check if already connected
|
|
2120
2456
|
if (WiFi.status() == WL_CONNECTED) {
|
|
2121
|
-
Log(CONNECTION,
|
|
2457
|
+
Log(CONNECTION,
|
|
2458
|
+
"attemptRouterReconnect(): Already connected to router\n");
|
|
2122
2459
|
_routerReconnectAttempts = 0;
|
|
2123
2460
|
return;
|
|
2124
2461
|
}
|
|
2125
|
-
|
|
2462
|
+
|
|
2126
2463
|
Log(CONNECTION, "attemptRouterReconnect(): Reconnecting to router %s...\n",
|
|
2127
2464
|
_sharedGatewayConfig.routerSSID.c_str());
|
|
2128
|
-
|
|
2129
|
-
// Use stationManual to reconnect (port 0 means no TCP mesh connection to
|
|
2130
|
-
|
|
2465
|
+
|
|
2466
|
+
// Use stationManual to reconnect (port 0 means no TCP mesh connection to
|
|
2467
|
+
// router)
|
|
2468
|
+
stationManual(_sharedGatewayConfig.routerSSID,
|
|
2469
|
+
_sharedGatewayConfig.routerPassword, 0);
|
|
2131
2470
|
}
|
|
2132
2471
|
};
|
|
2133
2472
|
} // namespace wifi
|