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