@alteriom/painlessmesh 1.7.9 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -2
- package/README.md +159 -12
- package/docs/BRIDGE_FAILOVER.md +512 -0
- package/docs/BRIDGE_HEALTH_MONITORING.md +293 -0
- package/docs/CREATE_MISSING_RELEASES.md +321 -0
- package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +523 -0
- package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +542 -0
- package/examples/alteriom/alteriom_sensor_package.hpp +213 -0
- package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +1014 -11
- package/examples/basic/basic.ino +6 -2
- package/examples/bridge/bridge.ino +44 -23
- package/examples/bridge/bridge_health_monitoring_example.ino +188 -0
- package/examples/bridgeAwareSensorNode/alteriom_sensor_package.hpp +1227 -0
- package/examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino +343 -0
- package/examples/bridgeAwareSensorNode/platformio.ini +26 -0
- package/examples/bridge_failover/README.md +358 -0
- package/examples/bridge_failover/bridge_failover.ino +180 -0
- package/examples/bridge_failover/platformio.ini +27 -0
- package/examples/diagnosticsExample/diagnosticsExample.ino +171 -0
- package/examples/diagnosticsExample/platformio.ini +26 -0
- package/examples/multi_bridge/README.md +346 -0
- package/examples/multi_bridge/primary_bridge.ino +96 -0
- package/examples/multi_bridge/regular_node.ino +141 -0
- package/examples/multi_bridge/secondary_bridge.ino +111 -0
- package/examples/ntpTimeSyncBridge/alteriom_sensor_package.hpp +1383 -0
- package/examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino +81 -0
- package/examples/ntpTimeSyncNode/alteriom_sensor_package.hpp +1383 -0
- package/examples/ntpTimeSyncNode/ntpTimeSyncNode.ino +109 -0
- package/examples/queued_alarms/README.md +390 -0
- package/examples/queued_alarms/queued_alarms.ino +265 -0
- package/examples/rtcIntegration/README.md +235 -0
- package/examples/rtcIntegration/rtcIntegration.ino +196 -0
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/arduino/wifi.hpp +888 -0
- package/src/painlessMeshSTA.cpp +63 -0
- package/src/painlessMeshSTA.h +3 -0
- package/src/painlessmesh/mesh.hpp +1327 -4
- package/src/painlessmesh/message_queue.hpp +368 -0
- package/src/painlessmesh/plugin.hpp +69 -0
- package/src/painlessmesh/rtc.hpp +203 -0
package/src/arduino/wifi.hpp
CHANGED
|
@@ -18,6 +18,13 @@ namespace painlessmesh {
|
|
|
18
18
|
namespace wifi {
|
|
19
19
|
class Mesh : public painlessmesh::Mesh<Connection> {
|
|
20
20
|
public:
|
|
21
|
+
// Multi-bridge selection strategy enum (must be declared early)
|
|
22
|
+
enum BridgeSelectionStrategy {
|
|
23
|
+
PRIORITY_BASED = 0, // Use highest priority bridge (default)
|
|
24
|
+
ROUND_ROBIN = 1, // Distribute load evenly
|
|
25
|
+
BEST_SIGNAL = 2 // Use bridge with best RSSI
|
|
26
|
+
};
|
|
27
|
+
|
|
21
28
|
/** Initialize the mesh network
|
|
22
29
|
*
|
|
23
30
|
* Add this to your setup() function. This routine does the following things:
|
|
@@ -81,6 +88,72 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
81
88
|
|
|
82
89
|
this->init(nodeId);
|
|
83
90
|
|
|
91
|
+
// Add bridge election package handler (Type 611)
|
|
92
|
+
this->callbackList.onPackage(
|
|
93
|
+
611, // BRIDGE_ELECTION type
|
|
94
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
|
|
95
|
+
JsonDocument doc;
|
|
96
|
+
TSTRING str;
|
|
97
|
+
variant.printTo(str);
|
|
98
|
+
deserializeJson(doc, str);
|
|
99
|
+
JsonObject obj = doc.as<JsonObject>();
|
|
100
|
+
|
|
101
|
+
if (obj["routerRSSI"].is<int>()) {
|
|
102
|
+
uint32_t fromNode = obj["from"];
|
|
103
|
+
int8_t routerRSSI = obj["routerRSSI"];
|
|
104
|
+
uint32_t uptime = obj["uptime"] | 0;
|
|
105
|
+
uint32_t freeMemory = obj["freeMemory"] | 0;
|
|
106
|
+
|
|
107
|
+
this->handleBridgeElection(fromNode, routerRSSI, uptime, freeMemory);
|
|
108
|
+
|
|
109
|
+
Log(CONNECTION, "Bridge election candidate from %u: RSSI %d dBm\n",
|
|
110
|
+
fromNode, routerRSSI);
|
|
111
|
+
}
|
|
112
|
+
return false; // Don't consume the package
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Add bridge takeover package handler (Type 612)
|
|
116
|
+
this->callbackList.onPackage(
|
|
117
|
+
612, // BRIDGE_TAKEOVER type
|
|
118
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
|
|
119
|
+
JsonDocument doc;
|
|
120
|
+
TSTRING str;
|
|
121
|
+
variant.printTo(str);
|
|
122
|
+
deserializeJson(doc, str);
|
|
123
|
+
JsonObject obj = doc.as<JsonObject>();
|
|
124
|
+
|
|
125
|
+
if (obj["previousBridge"].is<unsigned int>()) {
|
|
126
|
+
uint32_t newBridge = obj["from"];
|
|
127
|
+
uint32_t previousBridge = obj["previousBridge"];
|
|
128
|
+
TSTRING reason = obj["reason"].as<TSTRING>();
|
|
129
|
+
|
|
130
|
+
Log(CONNECTION, "Bridge takeover: Node %u replaced %u (%s)\n",
|
|
131
|
+
newBridge, previousBridge, reason.c_str());
|
|
132
|
+
|
|
133
|
+
// Notify callback if this node was not the winner
|
|
134
|
+
if (newBridge != this->nodeId && bridgeRoleChangedCallback) {
|
|
135
|
+
bridgeRoleChangedCallback(false, "Another node won election");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false; // Don't consume the package
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Add callback to detect bridge failures and trigger elections
|
|
142
|
+
this->onBridgeStatusChanged([this](uint32_t bridgeNodeId, bool hasInternet) {
|
|
143
|
+
if (!hasInternet && bridgeFailoverEnabled && routerCredentialsConfigured) {
|
|
144
|
+
Log(CONNECTION, "Bridge %u lost Internet, considering election...\n", bridgeNodeId);
|
|
145
|
+
|
|
146
|
+
// Check if we still have any healthy bridges
|
|
147
|
+
if (!this->hasInternetConnection()) {
|
|
148
|
+
Log(CONNECTION, "No healthy bridges, starting election\n");
|
|
149
|
+
// Small delay to let all nodes detect the failure
|
|
150
|
+
this->addTask(2000, TASK_ONCE, [this]() {
|
|
151
|
+
this->startBridgeElection();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
84
157
|
tcpServerInit();
|
|
85
158
|
eventHandleInit();
|
|
86
159
|
|
|
@@ -119,6 +192,145 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
119
192
|
init(ssid, password, port, connectMode, channel, hidden, maxconn);
|
|
120
193
|
}
|
|
121
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Initialize mesh as a bridge node with automatic channel detection
|
|
197
|
+
*
|
|
198
|
+
* This method connects to a router first, detects its channel, then
|
|
199
|
+
* initializes the mesh on the same channel. This ensures the bridge
|
|
200
|
+
* can maintain both router and mesh connections on the same channel.
|
|
201
|
+
*
|
|
202
|
+
* The bridge node will automatically:
|
|
203
|
+
* - Connect to the specified router in STA mode
|
|
204
|
+
* - Detect the router's WiFi channel
|
|
205
|
+
* - Initialize the mesh AP on the detected channel
|
|
206
|
+
* - Set itself as root node
|
|
207
|
+
* - Maintain the router connection
|
|
208
|
+
*
|
|
209
|
+
* @param meshSSID The name of your mesh network
|
|
210
|
+
* @param meshPassword WiFi password for the mesh
|
|
211
|
+
* @param routerSSID SSID of the router to connect to
|
|
212
|
+
* @param routerPassword Password for the router
|
|
213
|
+
* @param baseScheduler Task scheduler for mesh operations
|
|
214
|
+
* @param port TCP port for mesh communication (default: 5555)
|
|
215
|
+
*/
|
|
216
|
+
void initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
|
|
217
|
+
TSTRING routerSSID, TSTRING routerPassword,
|
|
218
|
+
Scheduler *baseScheduler, uint16_t port = 5555) {
|
|
219
|
+
using namespace logger;
|
|
220
|
+
|
|
221
|
+
Log(STARTUP, "=== Bridge Mode Initialization ===\n");
|
|
222
|
+
Log(STARTUP, "Step 1: Connecting to router %s...\n", routerSSID.c_str());
|
|
223
|
+
|
|
224
|
+
// Step 1: Connect to router first to detect its channel
|
|
225
|
+
// Shut Wifi down and start with a blank slate
|
|
226
|
+
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
227
|
+
|
|
228
|
+
Log(STARTUP, "initAsBridge(): %d\n",
|
|
229
|
+
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
|
230
|
+
WiFi.setAutoReconnect(false));
|
|
231
|
+
#else
|
|
232
|
+
WiFi.setAutoConnect(false));
|
|
233
|
+
#endif
|
|
234
|
+
WiFi.persistent(false);
|
|
235
|
+
WiFi.mode(WIFI_STA);
|
|
236
|
+
|
|
237
|
+
// Connect to router and wait for connection
|
|
238
|
+
WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
|
|
239
|
+
|
|
240
|
+
// Wait for connection (with timeout)
|
|
241
|
+
int timeout = 30; // 30 seconds timeout
|
|
242
|
+
while (WiFi.status() != WL_CONNECTED && timeout > 0) {
|
|
243
|
+
delay(1000);
|
|
244
|
+
timeout--;
|
|
245
|
+
Log(STARTUP, ".");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
uint8_t detectedChannel = 1; // Default fallback
|
|
249
|
+
|
|
250
|
+
if (WiFi.status() == WL_CONNECTED) {
|
|
251
|
+
detectedChannel = WiFi.channel();
|
|
252
|
+
// Validate channel is in valid range (1-13 for 2.4GHz)
|
|
253
|
+
if (detectedChannel < 1 || detectedChannel > 13) {
|
|
254
|
+
Log(ERROR, "\n✗ Invalid channel detected: %d, using default channel 1\n", detectedChannel);
|
|
255
|
+
detectedChannel = 1;
|
|
256
|
+
} else {
|
|
257
|
+
Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
|
|
258
|
+
Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
Log(ERROR, "\n✗ Failed to connect to router, using default channel 1\n");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n", detectedChannel);
|
|
265
|
+
|
|
266
|
+
// Step 2: Initialize mesh on detected channel
|
|
267
|
+
init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
|
|
268
|
+
detectedChannel, 0, MAX_CONN);
|
|
269
|
+
|
|
270
|
+
Log(STARTUP, "Step 3: Establishing bridge connection...\n");
|
|
271
|
+
|
|
272
|
+
// Step 3: Re-establish router connection using stationManual
|
|
273
|
+
stationManual(routerSSID, routerPassword, 0);
|
|
274
|
+
|
|
275
|
+
// Step 4: Configure as root/bridge node
|
|
276
|
+
this->setRoot(true);
|
|
277
|
+
this->setContainsRoot(true);
|
|
278
|
+
|
|
279
|
+
// Step 5: Setup bridge status broadcasting
|
|
280
|
+
initBridgeStatusBroadcast();
|
|
281
|
+
|
|
282
|
+
Log(STARTUP, "=== Bridge Mode Active ===\n");
|
|
283
|
+
Log(STARTUP, " Mesh SSID: %s\n", meshSSID.c_str());
|
|
284
|
+
Log(STARTUP, " Mesh Channel: %d (matches router)\n", detectedChannel);
|
|
285
|
+
Log(STARTUP, " Router: %s\n", routerSSID.c_str());
|
|
286
|
+
Log(STARTUP, " Port: %d\n", port);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Initialize mesh as a bridge node with priority (for multi-bridge mode)
|
|
291
|
+
*
|
|
292
|
+
* This overload adds bridge priority configuration for multi-bridge deployments.
|
|
293
|
+
* Priority determines which bridge is preferred when multiple bridges are available.
|
|
294
|
+
*
|
|
295
|
+
* @param meshSSID The name of your mesh network
|
|
296
|
+
* @param meshPassword WiFi password for the mesh
|
|
297
|
+
* @param routerSSID SSID of the router to connect to
|
|
298
|
+
* @param routerPassword Password for the router
|
|
299
|
+
* @param baseScheduler Task scheduler for mesh operations
|
|
300
|
+
* @param port TCP port for mesh communication (default: 5555)
|
|
301
|
+
* @param priority Bridge priority: 10=highest (primary), 5=medium (secondary), 1=lowest (default: 5)
|
|
302
|
+
*/
|
|
303
|
+
void initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
|
|
304
|
+
TSTRING routerSSID, TSTRING routerPassword,
|
|
305
|
+
Scheduler *baseScheduler, uint16_t port, uint8_t priority) {
|
|
306
|
+
using namespace logger;
|
|
307
|
+
|
|
308
|
+
// Validate and store priority
|
|
309
|
+
if (priority < 1) priority = 1;
|
|
310
|
+
if (priority > 10) priority = 10;
|
|
311
|
+
bridgePriority = priority;
|
|
312
|
+
|
|
313
|
+
// Store role based on priority
|
|
314
|
+
if (priority >= 8) {
|
|
315
|
+
bridgeRole = "primary";
|
|
316
|
+
} else if (priority >= 5) {
|
|
317
|
+
bridgeRole = "secondary";
|
|
318
|
+
} else {
|
|
319
|
+
bridgeRole = "standby";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
Log(STARTUP, "=== Bridge Mode Initialization (Priority: %d, Role: %s) ===\n",
|
|
323
|
+
priority, bridgeRole.c_str());
|
|
324
|
+
|
|
325
|
+
// Call the base initAsBridge method
|
|
326
|
+
initAsBridge(meshSSID, meshPassword, routerSSID, routerPassword, baseScheduler, port);
|
|
327
|
+
|
|
328
|
+
// Setup multi-bridge coordination if enabled
|
|
329
|
+
if (multiBridgeEnabled) {
|
|
330
|
+
initBridgeCoordination();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
122
334
|
/**
|
|
123
335
|
* Connect (as a station) to a specified network and ip
|
|
124
336
|
*
|
|
@@ -212,6 +424,194 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
212
424
|
IPAddress getStationIP() { return WiFi.localIP(); }
|
|
213
425
|
IPAddress getAPIP() { return _apIp; }
|
|
214
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Enable or disable automatic bridge failover
|
|
429
|
+
*
|
|
430
|
+
* When enabled, nodes will participate in bridge elections if the primary
|
|
431
|
+
* bridge goes offline and they have router credentials configured.
|
|
432
|
+
*
|
|
433
|
+
* @param enabled true to enable automatic failover (default), false to disable
|
|
434
|
+
*/
|
|
435
|
+
void enableBridgeFailover(bool enabled) {
|
|
436
|
+
bridgeFailoverEnabled = enabled;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Set router credentials for bridge election participation
|
|
441
|
+
*
|
|
442
|
+
* Nodes must have router credentials configured to participate in bridge
|
|
443
|
+
* elections. When a bridge fails, only nodes with credentials can become
|
|
444
|
+
* the new bridge.
|
|
445
|
+
*
|
|
446
|
+
* @param ssid Router SSID
|
|
447
|
+
* @param password Router password
|
|
448
|
+
*/
|
|
449
|
+
void setRouterCredentials(TSTRING ssid, TSTRING password) {
|
|
450
|
+
routerSSID = ssid;
|
|
451
|
+
routerPassword = password;
|
|
452
|
+
routerCredentialsConfigured = true;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Set the election timeout (how long to collect candidates)
|
|
457
|
+
*
|
|
458
|
+
* @param timeoutMs Timeout in milliseconds (default: 5000 = 5 seconds)
|
|
459
|
+
*/
|
|
460
|
+
void setElectionTimeout(uint32_t timeoutMs) {
|
|
461
|
+
electionTimeoutMs = timeoutMs;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Set callback for when this node's bridge role changes
|
|
466
|
+
*
|
|
467
|
+
* @param callback Function to call when role changes
|
|
468
|
+
*/
|
|
469
|
+
void onBridgeRoleChanged(std::function<void(bool isBridge, TSTRING reason)> callback) {
|
|
470
|
+
bridgeRoleChangedCallback = callback;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Enable or disable multi-bridge coordination mode
|
|
475
|
+
*
|
|
476
|
+
* When enabled, multiple bridges can operate simultaneously for:
|
|
477
|
+
* - Load balancing across multiple Internet connections
|
|
478
|
+
* - Geographic distribution
|
|
479
|
+
* - Hot standby redundancy without failover delays
|
|
480
|
+
*
|
|
481
|
+
* @param enabled true to enable multi-bridge mode, false for single-bridge (default)
|
|
482
|
+
*/
|
|
483
|
+
void enableMultiBridge(bool enabled) {
|
|
484
|
+
multiBridgeEnabled = enabled;
|
|
485
|
+
if (enabled) {
|
|
486
|
+
Log(logger::GENERAL, "enableMultiBridge(): Multi-bridge coordination enabled\n");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Set bridge selection strategy for multi-bridge mode
|
|
492
|
+
*
|
|
493
|
+
* @param strategy Selection strategy:
|
|
494
|
+
* - PRIORITY_BASED: Always use highest priority bridge (default)
|
|
495
|
+
* - ROUND_ROBIN: Distribute load evenly across bridges
|
|
496
|
+
* - BEST_SIGNAL: Always use bridge with best RSSI
|
|
497
|
+
*/
|
|
498
|
+
void setBridgeSelectionStrategy(BridgeSelectionStrategy strategy) {
|
|
499
|
+
bridgeSelectionStrategy = strategy;
|
|
500
|
+
Log(logger::GENERAL, "setBridgeSelectionStrategy(): Strategy set to %d\n", (int)strategy);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Set maximum number of concurrent bridges in multi-bridge mode
|
|
505
|
+
*
|
|
506
|
+
* @param maxBridges Maximum bridges to track (default: 2, max: 5)
|
|
507
|
+
*/
|
|
508
|
+
void setMaxBridges(uint8_t maxBridges) {
|
|
509
|
+
if (maxBridges < 1) maxBridges = 1;
|
|
510
|
+
if (maxBridges > 5) maxBridges = 5;
|
|
511
|
+
maxConcurrentBridges = maxBridges;
|
|
512
|
+
Log(logger::GENERAL, "setMaxBridges(): Max concurrent bridges set to %d\n", maxBridges);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Get list of all active bridges (with Internet connection)
|
|
517
|
+
*
|
|
518
|
+
* @return vector of node IDs for active bridges
|
|
519
|
+
*/
|
|
520
|
+
std::vector<uint32_t> getActiveBridges() {
|
|
521
|
+
std::vector<uint32_t> activeBridges;
|
|
522
|
+
auto bridges = this->getBridges();
|
|
523
|
+
|
|
524
|
+
for (const auto& bridge : bridges) {
|
|
525
|
+
if (bridge.internetConnected && bridge.isHealthy()) {
|
|
526
|
+
activeBridges.push_back(bridge.nodeId);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return activeBridges;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get recommended bridge for message transmission
|
|
535
|
+
*
|
|
536
|
+
* Uses the configured bridge selection strategy to pick the best bridge.
|
|
537
|
+
* Returns 0 if no suitable bridge is available.
|
|
538
|
+
*
|
|
539
|
+
* @return node ID of recommended bridge, or 0 if none available
|
|
540
|
+
*/
|
|
541
|
+
uint32_t getRecommendedBridge() {
|
|
542
|
+
auto activeBridges = getActiveBridges();
|
|
543
|
+
|
|
544
|
+
if (activeBridges.empty()) {
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Single bridge - return it
|
|
549
|
+
if (activeBridges.size() == 1) {
|
|
550
|
+
return activeBridges[0];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Multi-bridge mode: apply selection strategy
|
|
554
|
+
switch (bridgeSelectionStrategy) {
|
|
555
|
+
case ROUND_ROBIN: {
|
|
556
|
+
// Simple round-robin: cycle through bridges
|
|
557
|
+
lastSelectedBridgeIndex = (lastSelectedBridgeIndex + 1) % activeBridges.size();
|
|
558
|
+
return activeBridges[lastSelectedBridgeIndex];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
case BEST_SIGNAL: {
|
|
562
|
+
// Find bridge with best RSSI
|
|
563
|
+
uint32_t bestBridge = 0;
|
|
564
|
+
int8_t bestRSSI = -127;
|
|
565
|
+
|
|
566
|
+
for (const auto& bridge : this->getBridges()) {
|
|
567
|
+
if (bridge.internetConnected && bridge.isHealthy() && bridge.routerRSSI > bestRSSI) {
|
|
568
|
+
bestRSSI = bridge.routerRSSI;
|
|
569
|
+
bestBridge = bridge.nodeId;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return bestBridge;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
case PRIORITY_BASED:
|
|
576
|
+
default: {
|
|
577
|
+
// Use highest priority bridge (stored in bridgePriorities map)
|
|
578
|
+
uint32_t bestBridge = 0;
|
|
579
|
+
uint8_t highestPriority = 0;
|
|
580
|
+
|
|
581
|
+
for (uint32_t bridgeId : activeBridges) {
|
|
582
|
+
uint8_t priority = bridgePriorities[bridgeId];
|
|
583
|
+
if (priority > highestPriority) {
|
|
584
|
+
highestPriority = priority;
|
|
585
|
+
bestBridge = bridgeId;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// If no priority info, use first active bridge
|
|
590
|
+
return bestBridge ? bestBridge : activeBridges[0];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Select a specific bridge for next transmission
|
|
597
|
+
*
|
|
598
|
+
* This overrides the automatic bridge selection for one message.
|
|
599
|
+
*
|
|
600
|
+
* @param bridgeNodeId Node ID of bridge to use
|
|
601
|
+
*/
|
|
602
|
+
void selectBridge(uint32_t bridgeNodeId) {
|
|
603
|
+
selectedBridgeOverride = bridgeNodeId;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Check if multi-bridge mode is enabled
|
|
608
|
+
*
|
|
609
|
+
* @return true if multi-bridge coordination is enabled
|
|
610
|
+
*/
|
|
611
|
+
bool isMultiBridgeEnabled() const {
|
|
612
|
+
return multiBridgeEnabled;
|
|
613
|
+
}
|
|
614
|
+
|
|
215
615
|
void stop() {
|
|
216
616
|
// remove all WiFi events
|
|
217
617
|
#ifdef ESP32
|
|
@@ -265,6 +665,455 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
265
665
|
WiFi.softAP(_meshSSID.c_str(), _meshPassword.c_str(), _meshChannel,
|
|
266
666
|
_meshHidden, _meshMaxConn);
|
|
267
667
|
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Initialize bridge status broadcasting
|
|
671
|
+
* Sets up a periodic task to broadcast bridge status to the mesh
|
|
672
|
+
*/
|
|
673
|
+
void initBridgeStatusBroadcast() {
|
|
674
|
+
using namespace logger;
|
|
675
|
+
|
|
676
|
+
if (!this->isBridge() || !this->bridgeStatusBroadcastEnabled) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
Log(STARTUP, "initBridgeStatusBroadcast(): Setting up bridge status broadcast\n");
|
|
681
|
+
|
|
682
|
+
// Create periodic task to broadcast bridge status
|
|
683
|
+
bridgeStatusTask = this->addTask(
|
|
684
|
+
this->bridgeStatusIntervalMs,
|
|
685
|
+
TASK_FOREVER,
|
|
686
|
+
[this]() {
|
|
687
|
+
this->sendBridgeStatus();
|
|
688
|
+
}
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
|
|
692
|
+
this->bridgeStatusIntervalMs);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Initialize bridge coordination broadcasting
|
|
697
|
+
* Sets up periodic coordination messages between bridges
|
|
698
|
+
*/
|
|
699
|
+
void initBridgeCoordination() {
|
|
700
|
+
using namespace logger;
|
|
701
|
+
|
|
702
|
+
if (!this->isBridge() || !multiBridgeEnabled) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
Log(STARTUP, "initBridgeCoordination(): Setting up multi-bridge coordination\n");
|
|
707
|
+
|
|
708
|
+
// Register handler for incoming coordination messages (Type 613)
|
|
709
|
+
this->callbackList.onPackage(
|
|
710
|
+
613, // BRIDGE_COORDINATION type
|
|
711
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
|
|
712
|
+
JsonDocument doc;
|
|
713
|
+
TSTRING str;
|
|
714
|
+
variant.printTo(str);
|
|
715
|
+
deserializeJson(doc, str);
|
|
716
|
+
JsonObject obj = doc.as<JsonObject>();
|
|
717
|
+
|
|
718
|
+
if (obj["priority"].is<unsigned int>()) {
|
|
719
|
+
uint32_t fromNode = obj["from"];
|
|
720
|
+
uint8_t priority = obj["priority"];
|
|
721
|
+
TSTRING role = obj["role"].as<TSTRING>();
|
|
722
|
+
uint8_t load = obj["load"] | 0;
|
|
723
|
+
|
|
724
|
+
// Store bridge priority for selection decisions
|
|
725
|
+
bridgePriorities[fromNode] = priority;
|
|
726
|
+
|
|
727
|
+
// Update peer bridges list
|
|
728
|
+
if (obj["peerBridges"].is<JsonArray>()) {
|
|
729
|
+
JsonArray peers = obj["peerBridges"];
|
|
730
|
+
for (JsonVariant peer : peers) {
|
|
731
|
+
uint32_t peerId = peer.as<uint32_t>();
|
|
732
|
+
if (peerId != this->nodeId &&
|
|
733
|
+
std::find(knownBridgePeers.begin(), knownBridgePeers.end(), peerId) == knownBridgePeers.end()) {
|
|
734
|
+
knownBridgePeers.push_back(peerId);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
Log(CONNECTION, "Bridge coordination from %u: priority=%d, role=%s, load=%d%%\n",
|
|
740
|
+
fromNode, priority, role.c_str(), load);
|
|
741
|
+
}
|
|
742
|
+
return false; // Don't consume the package
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Create periodic task to send coordination messages
|
|
746
|
+
bridgeCoordinationTask = this->addTask(
|
|
747
|
+
30000, // 30 seconds interval
|
|
748
|
+
TASK_FOREVER,
|
|
749
|
+
[this]() {
|
|
750
|
+
this->sendBridgeCoordination();
|
|
751
|
+
}
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
Log(STARTUP, "Bridge coordination enabled (priority: %d, role: %s)\n",
|
|
755
|
+
bridgePriority, bridgeRole.c_str());
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Send bridge coordination message to other bridges
|
|
760
|
+
* Called periodically in multi-bridge mode
|
|
761
|
+
*/
|
|
762
|
+
void sendBridgeCoordination() {
|
|
763
|
+
using namespace logger;
|
|
764
|
+
|
|
765
|
+
if (!this->isBridge() || !multiBridgeEnabled) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Calculate current load (simplified: based on node count)
|
|
770
|
+
uint8_t currentLoad = 0;
|
|
771
|
+
auto nodeCount = this->getNodeList(false).size();
|
|
772
|
+
if (nodeCount > 0) {
|
|
773
|
+
currentLoad = (nodeCount * 100) / MAX_CONN;
|
|
774
|
+
if (currentLoad > 100) currentLoad = 100;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Create coordination message
|
|
778
|
+
JsonDocument doc;
|
|
779
|
+
JsonObject obj = doc.to<JsonObject>();
|
|
780
|
+
|
|
781
|
+
obj["type"] = 613; // BRIDGE_COORDINATION
|
|
782
|
+
obj["from"] = this->nodeId;
|
|
783
|
+
obj["routing"] = 2; // BROADCAST
|
|
784
|
+
obj["priority"] = bridgePriority;
|
|
785
|
+
obj["role"] = bridgeRole;
|
|
786
|
+
obj["load"] = currentLoad;
|
|
787
|
+
obj["timestamp"] = this->getNodeTime();
|
|
788
|
+
obj["message_type"] = 613;
|
|
789
|
+
|
|
790
|
+
// Add peer bridges list
|
|
791
|
+
JsonArray peers = obj["peerBridges"].to<JsonArray>();
|
|
792
|
+
for (uint32_t peerId : knownBridgePeers) {
|
|
793
|
+
peers.add(peerId);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
String msg;
|
|
797
|
+
serializeJson(doc, msg);
|
|
798
|
+
this->sendBroadcast(msg);
|
|
799
|
+
|
|
800
|
+
Log(CONNECTION, "Bridge coordination sent: priority=%d, role=%s, load=%d%%\n",
|
|
801
|
+
bridgePriority, bridgeRole.c_str(), currentLoad);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Scan for router and return its signal strength
|
|
806
|
+
*
|
|
807
|
+
* @param routerSSID SSID of router to scan for
|
|
808
|
+
* @return RSSI in dBm (negative number, -127 to 0), or 0 if not found
|
|
809
|
+
*/
|
|
810
|
+
int8_t scanRouterSignalStrength(TSTRING routerSSID) {
|
|
811
|
+
using namespace logger;
|
|
812
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n", routerSSID.c_str());
|
|
813
|
+
|
|
814
|
+
int n = WiFi.scanNetworks(false, false);
|
|
815
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Found %d networks\n", n);
|
|
816
|
+
|
|
817
|
+
for (int i = 0; i < n; i++) {
|
|
818
|
+
if (WiFi.SSID(i) == routerSSID) {
|
|
819
|
+
int8_t rssi = WiFi.RSSI(i);
|
|
820
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Found %s with RSSI %d dBm\n",
|
|
821
|
+
routerSSID.c_str(), rssi);
|
|
822
|
+
return rssi;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Router %s not found\n", routerSSID.c_str());
|
|
827
|
+
return 0; // Router not found
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Start bridge election process
|
|
832
|
+
* Called when primary bridge failure is detected
|
|
833
|
+
*/
|
|
834
|
+
void startBridgeElection() {
|
|
835
|
+
using namespace logger;
|
|
836
|
+
|
|
837
|
+
if (!bridgeFailoverEnabled) {
|
|
838
|
+
Log(CONNECTION, "startBridgeElection(): Failover disabled\n");
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (!routerCredentialsConfigured) {
|
|
843
|
+
Log(CONNECTION, "startBridgeElection(): No router credentials, cannot participate\n");
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (electionState != ELECTION_IDLE) {
|
|
848
|
+
Log(CONNECTION, "startBridgeElection(): Election already in progress\n");
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Prevent rapid role changes
|
|
853
|
+
if (millis() - lastRoleChangeTime < 60000) {
|
|
854
|
+
Log(CONNECTION, "startBridgeElection(): Too soon after last role change\n");
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
Log(CONNECTION, "=== Bridge Election Started ===\n");
|
|
859
|
+
electionState = ELECTION_SCANNING;
|
|
860
|
+
|
|
861
|
+
// Scan for router to get RSSI
|
|
862
|
+
int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
|
|
863
|
+
|
|
864
|
+
if (routerRSSI == 0) {
|
|
865
|
+
Log(CONNECTION, "startBridgeElection(): Router not visible, cannot participate\n");
|
|
866
|
+
electionState = ELECTION_IDLE;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
Log(CONNECTION, "startBridgeElection(): My router RSSI: %d dBm\n", routerRSSI);
|
|
871
|
+
|
|
872
|
+
// Clear previous candidates
|
|
873
|
+
electionCandidates.clear();
|
|
874
|
+
|
|
875
|
+
// Add self as candidate
|
|
876
|
+
BridgeCandidate selfCandidate;
|
|
877
|
+
selfCandidate.nodeId = this->nodeId;
|
|
878
|
+
selfCandidate.routerRSSI = routerRSSI;
|
|
879
|
+
selfCandidate.uptime = millis();
|
|
880
|
+
selfCandidate.freeMemory = ESP.getFreeHeap();
|
|
881
|
+
electionCandidates.push_back(selfCandidate);
|
|
882
|
+
|
|
883
|
+
// Broadcast candidacy using JSON directly (avoiding dependency on alteriom package)
|
|
884
|
+
JsonDocument doc;
|
|
885
|
+
JsonObject obj = doc.to<JsonObject>();
|
|
886
|
+
obj["type"] = 611; // BRIDGE_ELECTION
|
|
887
|
+
obj["from"] = this->nodeId;
|
|
888
|
+
obj["routing"] = 2; // BROADCAST
|
|
889
|
+
obj["routerRSSI"] = routerRSSI;
|
|
890
|
+
obj["uptime"] = millis();
|
|
891
|
+
obj["freeMemory"] = ESP.getFreeHeap();
|
|
892
|
+
obj["timestamp"] = this->getNodeTime();
|
|
893
|
+
obj["routerSSID"] = routerSSID;
|
|
894
|
+
obj["message_type"] = 611;
|
|
895
|
+
|
|
896
|
+
String msg;
|
|
897
|
+
serializeJson(doc, msg);
|
|
898
|
+
this->sendBroadcast(msg);
|
|
899
|
+
|
|
900
|
+
Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
|
|
901
|
+
|
|
902
|
+
// Set election timeout
|
|
903
|
+
electionDeadline = millis() + electionTimeoutMs;
|
|
904
|
+
electionState = ELECTION_COLLECTING;
|
|
905
|
+
|
|
906
|
+
// Schedule election evaluation
|
|
907
|
+
this->addTask(electionTimeoutMs + 100, TASK_ONCE, [this]() {
|
|
908
|
+
this->evaluateElection();
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Evaluate election and determine winner
|
|
914
|
+
* Called after election timeout expires
|
|
915
|
+
*/
|
|
916
|
+
void evaluateElection() {
|
|
917
|
+
using namespace logger;
|
|
918
|
+
|
|
919
|
+
if (electionState != ELECTION_COLLECTING) {
|
|
920
|
+
Log(CONNECTION, "evaluateElection(): Not in collecting state\n");
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
Log(CONNECTION, "=== Evaluating Election ===\n");
|
|
925
|
+
Log(CONNECTION, "evaluateElection(): %d candidates\n", electionCandidates.size());
|
|
926
|
+
|
|
927
|
+
// Find best candidate
|
|
928
|
+
BridgeCandidate* winner = nullptr;
|
|
929
|
+
int8_t bestRSSI = -127; // Worst possible RSSI
|
|
930
|
+
|
|
931
|
+
for (auto& candidate : electionCandidates) {
|
|
932
|
+
Log(CONNECTION, "evaluateElection(): Candidate %u: RSSI=%d, uptime=%u, mem=%u\n",
|
|
933
|
+
candidate.nodeId, candidate.routerRSSI, candidate.uptime, candidate.freeMemory);
|
|
934
|
+
|
|
935
|
+
if (candidate.routerRSSI > bestRSSI) {
|
|
936
|
+
bestRSSI = candidate.routerRSSI;
|
|
937
|
+
winner = &candidate;
|
|
938
|
+
} else if (candidate.routerRSSI == bestRSSI && winner != nullptr) {
|
|
939
|
+
// Tiebreaker 1: Higher uptime
|
|
940
|
+
if (candidate.uptime > winner->uptime) {
|
|
941
|
+
winner = &candidate;
|
|
942
|
+
} else if (candidate.uptime == winner->uptime) {
|
|
943
|
+
// Tiebreaker 2: More free memory
|
|
944
|
+
if (candidate.freeMemory > winner->freeMemory) {
|
|
945
|
+
winner = &candidate;
|
|
946
|
+
} else if (candidate.freeMemory == winner->freeMemory) {
|
|
947
|
+
// Tiebreaker 3: Lower node ID (deterministic)
|
|
948
|
+
if (candidate.nodeId < winner->nodeId) {
|
|
949
|
+
winner = &candidate;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (winner == nullptr) {
|
|
957
|
+
Log(ERROR, "evaluateElection(): No winner found!\n");
|
|
958
|
+
electionState = ELECTION_IDLE;
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
Log(CONNECTION, "=== Election Winner: Node %u ===\n", winner->nodeId);
|
|
963
|
+
Log(CONNECTION, " Router RSSI: %d dBm\n", winner->routerRSSI);
|
|
964
|
+
Log(CONNECTION, " Uptime: %u ms\n", winner->uptime);
|
|
965
|
+
Log(CONNECTION, " Free Memory: %u bytes\n", winner->freeMemory);
|
|
966
|
+
|
|
967
|
+
// Record election in diagnostics history
|
|
968
|
+
if (this->diagnosticsEnabled) {
|
|
969
|
+
ElectionRecord record;
|
|
970
|
+
record.timestamp = millis();
|
|
971
|
+
record.winnerNodeId = winner->nodeId;
|
|
972
|
+
record.winnerRSSI = winner->routerRSSI;
|
|
973
|
+
record.candidateCount = electionCandidates.size();
|
|
974
|
+
record.reason = "Bridge failure detected";
|
|
975
|
+
|
|
976
|
+
this->electionHistory.push_back(record);
|
|
977
|
+
|
|
978
|
+
// Keep history limited to MAX_ELECTION_HISTORY
|
|
979
|
+
if (this->electionHistory.size() > this->MAX_ELECTION_HISTORY) {
|
|
980
|
+
this->electionHistory.erase(this->electionHistory.begin());
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
Log(CONNECTION, "evaluateElection(): Election recorded in history\n");
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (winner->nodeId == this->nodeId) {
|
|
987
|
+
Log(CONNECTION, "🎯 I WON! Promoting to bridge...\n");
|
|
988
|
+
promoteToBridge();
|
|
989
|
+
} else {
|
|
990
|
+
Log(CONNECTION, "Winner is node %u, remaining as regular node\n", winner->nodeId);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
electionState = ELECTION_IDLE;
|
|
994
|
+
electionCandidates.clear();
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Promote this node to bridge role
|
|
999
|
+
* Called when node wins election
|
|
1000
|
+
*/
|
|
1001
|
+
void promoteToBridge() {
|
|
1002
|
+
using namespace logger;
|
|
1003
|
+
|
|
1004
|
+
Log(STARTUP, "=== Becoming Bridge Node ===\n");
|
|
1005
|
+
|
|
1006
|
+
// Store previous bridge (if any)
|
|
1007
|
+
auto primaryBridge = this->getPrimaryBridge();
|
|
1008
|
+
uint32_t previousBridgeId = primaryBridge ? primaryBridge->nodeId : 0;
|
|
1009
|
+
|
|
1010
|
+
// Reconfigure as bridge
|
|
1011
|
+
this->stop();
|
|
1012
|
+
delay(1000);
|
|
1013
|
+
|
|
1014
|
+
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
1015
|
+
mScheduler, _meshPort);
|
|
1016
|
+
|
|
1017
|
+
lastRoleChangeTime = millis();
|
|
1018
|
+
|
|
1019
|
+
Log(STARTUP, "✓ Bridge promotion complete\n");
|
|
1020
|
+
|
|
1021
|
+
// Notify via callback
|
|
1022
|
+
if (bridgeRoleChangedCallback) {
|
|
1023
|
+
bridgeRoleChangedCallback(true, "Election winner - best router signal");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Broadcast takeover announcement
|
|
1027
|
+
JsonDocument doc;
|
|
1028
|
+
JsonObject obj = doc.to<JsonObject>();
|
|
1029
|
+
obj["type"] = 612; // BRIDGE_TAKEOVER
|
|
1030
|
+
obj["from"] = this->nodeId;
|
|
1031
|
+
obj["routing"] = 2; // BROADCAST
|
|
1032
|
+
obj["previousBridge"] = previousBridgeId;
|
|
1033
|
+
obj["reason"] = "Election winner - best router signal";
|
|
1034
|
+
obj["routerRSSI"] = WiFi.RSSI();
|
|
1035
|
+
obj["timestamp"] = this->getNodeTime();
|
|
1036
|
+
obj["message_type"] = 612;
|
|
1037
|
+
|
|
1038
|
+
String msg;
|
|
1039
|
+
serializeJson(doc, msg);
|
|
1040
|
+
|
|
1041
|
+
// Small delay to ensure mesh is ready
|
|
1042
|
+
delay(2000);
|
|
1043
|
+
this->sendBroadcast(msg);
|
|
1044
|
+
|
|
1045
|
+
Log(STARTUP, "✓ Takeover announcement sent\n");
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Handle received bridge election package
|
|
1050
|
+
* Called by package handler when election message arrives
|
|
1051
|
+
*/
|
|
1052
|
+
void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI, uint32_t uptime,
|
|
1053
|
+
uint32_t freeMemory) {
|
|
1054
|
+
using namespace logger;
|
|
1055
|
+
|
|
1056
|
+
if (electionState != ELECTION_COLLECTING) {
|
|
1057
|
+
Log(CONNECTION, "handleBridgeElection(): Not collecting candidates, ignoring\n");
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Check if candidate already exists
|
|
1062
|
+
for (auto& candidate : electionCandidates) {
|
|
1063
|
+
if (candidate.nodeId == fromNode) {
|
|
1064
|
+
Log(CONNECTION, "handleBridgeElection(): Duplicate candidate from %u, ignoring\n", fromNode);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
BridgeCandidate candidate;
|
|
1070
|
+
candidate.nodeId = fromNode;
|
|
1071
|
+
candidate.routerRSSI = routerRSSI;
|
|
1072
|
+
candidate.uptime = uptime;
|
|
1073
|
+
candidate.freeMemory = freeMemory;
|
|
1074
|
+
|
|
1075
|
+
electionCandidates.push_back(candidate);
|
|
1076
|
+
|
|
1077
|
+
Log(CONNECTION, "handleBridgeElection(): Added candidate %u (RSSI: %d dBm)\n",
|
|
1078
|
+
fromNode, routerRSSI);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Send bridge status broadcast
|
|
1083
|
+
* Called periodically by bridge nodes to report connectivity status
|
|
1084
|
+
*/
|
|
1085
|
+
void sendBridgeStatus() {
|
|
1086
|
+
using namespace logger;
|
|
1087
|
+
|
|
1088
|
+
if (!this->bridgeStatusBroadcastEnabled) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Create bridge status package
|
|
1093
|
+
// We need to include the package header here since we're in wifi namespace
|
|
1094
|
+
// The package will be sent as a JSON string
|
|
1095
|
+
JsonDocument doc;
|
|
1096
|
+
JsonObject obj = doc.to<JsonObject>();
|
|
1097
|
+
|
|
1098
|
+
obj["type"] = 610; // BRIDGE_STATUS type
|
|
1099
|
+
obj["from"] = this->nodeId;
|
|
1100
|
+
obj["routing"] = 2; // BROADCAST routing
|
|
1101
|
+
obj["timestamp"] = this->getNodeTime();
|
|
1102
|
+
obj["internetConnected"] = (WiFi.status() == WL_CONNECTED);
|
|
1103
|
+
obj["routerRSSI"] = WiFi.RSSI();
|
|
1104
|
+
obj["routerChannel"] = WiFi.channel();
|
|
1105
|
+
obj["uptime"] = millis();
|
|
1106
|
+
obj["gatewayIP"] = WiFi.gatewayIP().toString();
|
|
1107
|
+
obj["message_type"] = 610;
|
|
1108
|
+
|
|
1109
|
+
String msg;
|
|
1110
|
+
serializeJson(doc, msg);
|
|
1111
|
+
|
|
1112
|
+
Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)\n",
|
|
1113
|
+
(WiFi.status() == WL_CONNECTED) ? "Connected" : "Disconnected");
|
|
1114
|
+
|
|
1115
|
+
this->sendBroadcast(msg);
|
|
1116
|
+
}
|
|
268
1117
|
void eventHandleInit() {
|
|
269
1118
|
using namespace logger;
|
|
270
1119
|
#ifdef ESP32
|
|
@@ -365,6 +1214,45 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
365
1214
|
WiFiEventHandler eventSTAGotIPHandler;
|
|
366
1215
|
#endif // ESP8266
|
|
367
1216
|
AsyncServer *_tcpListener;
|
|
1217
|
+
std::shared_ptr<Task> bridgeStatusTask;
|
|
1218
|
+
|
|
1219
|
+
// Bridge failover state and configuration
|
|
1220
|
+
enum ElectionState {
|
|
1221
|
+
ELECTION_IDLE,
|
|
1222
|
+
ELECTION_SCANNING,
|
|
1223
|
+
ELECTION_COLLECTING
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
struct BridgeCandidate {
|
|
1227
|
+
uint32_t nodeId;
|
|
1228
|
+
int8_t routerRSSI;
|
|
1229
|
+
uint32_t uptime;
|
|
1230
|
+
uint32_t freeMemory;
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
bool bridgeFailoverEnabled = true;
|
|
1234
|
+
bool routerCredentialsConfigured = false;
|
|
1235
|
+
TSTRING routerSSID = "";
|
|
1236
|
+
TSTRING routerPassword = "";
|
|
1237
|
+
uint32_t electionTimeoutMs = 5000; // Default 5 seconds
|
|
1238
|
+
uint32_t lastRoleChangeTime = 0;
|
|
1239
|
+
ElectionState electionState = ELECTION_IDLE;
|
|
1240
|
+
uint32_t electionDeadline = 0;
|
|
1241
|
+
std::vector<BridgeCandidate> electionCandidates;
|
|
1242
|
+
std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
|
|
1243
|
+
|
|
1244
|
+
// Multi-bridge coordination state and configuration
|
|
1245
|
+
protected:
|
|
1246
|
+
bool multiBridgeEnabled = false;
|
|
1247
|
+
BridgeSelectionStrategy bridgeSelectionStrategy = PRIORITY_BASED;
|
|
1248
|
+
uint8_t maxConcurrentBridges = 2;
|
|
1249
|
+
uint8_t bridgePriority = 5; // Default medium priority
|
|
1250
|
+
TSTRING bridgeRole = "secondary"; // Default role
|
|
1251
|
+
std::shared_ptr<Task> bridgeCoordinationTask;
|
|
1252
|
+
std::map<uint32_t, uint8_t> bridgePriorities; // nodeId -> priority mapping
|
|
1253
|
+
std::vector<uint32_t> knownBridgePeers; // List of peer bridge node IDs
|
|
1254
|
+
uint32_t selectedBridgeOverride = 0; // Manual bridge selection override
|
|
1255
|
+
size_t lastSelectedBridgeIndex = 0; // For round-robin selection
|
|
368
1256
|
};
|
|
369
1257
|
} // namespace wifi
|
|
370
1258
|
}; // namespace painlessmesh
|