@alteriom/painlessmesh 1.7.8 → 1.8.0
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 +139 -3
- package/README.md +114 -4
- package/RELEASE_GUIDE.md +57 -8
- 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/README.md +2 -1
- package/docs/RELEASE_AGENT_SUMMARY.md +386 -0
- package/docs/releases/RELEASE_SUMMARY_v1.7.8.md +523 -0
- package/docs/releases/RELEASE_SUMMARY_v1.7.9.md +542 -0
- package/docs/troubleshooting/ESP32_C6_COMPATIBILITY.md +157 -0
- package/docs/troubleshooting/common-issues.md +28 -0
- package/examples/alteriom/alteriom_sensor_package.hpp +233 -1
- package/examples/alteriom/platformio.ini +1 -1
- package/examples/alteriomImproved/platformio.ini +1 -1
- package/examples/alteriomMetricsHealth/metrics_health_node.ino +18 -7
- package/examples/alteriomMetricsHealth/platformio.ini +1 -1
- package/examples/alteriomPhase1/platformio.ini +1 -1
- package/examples/alteriomPhase2/platformio.ini +1 -1
- package/examples/alteriomSensorNode/alteriom_sensor_package.hpp +1014 -11
- package/examples/alteriomSensorNode/platformio.ini +1 -1
- package/examples/basic/basic.ino +6 -2
- package/examples/basic/platformio.ini +1 -1
- package/examples/bridge/alteriom_sensor_package.hpp +1170 -0
- package/examples/bridge/bridge.ino +44 -23
- package/examples/bridge/bridge_health_monitoring_example.ino +188 -0
- package/examples/bridge/enhanced_mqtt_bridge.hpp +1 -1
- package/examples/bridge/mqtt_command_bridge.hpp +2 -2
- package/examples/bridge/platformio.ini +2 -1
- 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/echoNode/platformio.ini +1 -1
- package/examples/logClient/platformio.ini +1 -1
- package/examples/logServer/platformio.ini +1 -1
- package/examples/mqttStatusBridge/platformio.ini +1 -1
- package/examples/namedMesh/platformio.ini +1 -1
- 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/otaReceiver/platformio.ini +1 -1
- package/examples/rtcIntegration/README.md +235 -0
- package/examples/rtcIntegration/rtcIntegration.ino +196 -0
- package/examples/startHere/platformio.ini +1 -1
- package/examples/webServer/platformio.ini +1 -1
- package/library.json +93 -53
- package/library.properties +1 -1
- package/package.json +2 -2
- package/src/arduino/wifi.hpp +581 -0
- package/src/painlessMeshSTA.cpp +68 -0
- package/src/painlessMeshSTA.h +3 -0
- package/src/painlessmesh/mesh.hpp +1127 -4
- package/src/painlessmesh/rtc.hpp +203 -0
package/src/arduino/wifi.hpp
CHANGED
|
@@ -63,6 +63,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
63
63
|
_meshSSID = ssid;
|
|
64
64
|
_meshPassword = password;
|
|
65
65
|
_meshChannel = channel;
|
|
66
|
+
Log(STARTUP, "init(): Mesh channel set to %d\n", _meshChannel);
|
|
66
67
|
_meshHidden = hidden;
|
|
67
68
|
_meshMaxConn = maxconn;
|
|
68
69
|
_meshPort = port;
|
|
@@ -80,6 +81,72 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
80
81
|
|
|
81
82
|
this->init(nodeId);
|
|
82
83
|
|
|
84
|
+
// Add bridge election package handler (Type 611)
|
|
85
|
+
this->callbackList.onPackage(
|
|
86
|
+
611, // BRIDGE_ELECTION type
|
|
87
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
|
|
88
|
+
JsonDocument doc;
|
|
89
|
+
TSTRING str;
|
|
90
|
+
variant.printTo(str);
|
|
91
|
+
deserializeJson(doc, str);
|
|
92
|
+
JsonObject obj = doc.as<JsonObject>();
|
|
93
|
+
|
|
94
|
+
if (obj["routerRSSI"].is<int>()) {
|
|
95
|
+
uint32_t fromNode = obj["from"];
|
|
96
|
+
int8_t routerRSSI = obj["routerRSSI"];
|
|
97
|
+
uint32_t uptime = obj["uptime"] | 0;
|
|
98
|
+
uint32_t freeMemory = obj["freeMemory"] | 0;
|
|
99
|
+
|
|
100
|
+
this->handleBridgeElection(fromNode, routerRSSI, uptime, freeMemory);
|
|
101
|
+
|
|
102
|
+
Log(CONNECTION, "Bridge election candidate from %u: RSSI %d dBm\n",
|
|
103
|
+
fromNode, routerRSSI);
|
|
104
|
+
}
|
|
105
|
+
return false; // Don't consume the package
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Add bridge takeover package handler (Type 612)
|
|
109
|
+
this->callbackList.onPackage(
|
|
110
|
+
612, // BRIDGE_TAKEOVER type
|
|
111
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
|
|
112
|
+
JsonDocument doc;
|
|
113
|
+
TSTRING str;
|
|
114
|
+
variant.printTo(str);
|
|
115
|
+
deserializeJson(doc, str);
|
|
116
|
+
JsonObject obj = doc.as<JsonObject>();
|
|
117
|
+
|
|
118
|
+
if (obj["previousBridge"].is<unsigned int>()) {
|
|
119
|
+
uint32_t newBridge = obj["from"];
|
|
120
|
+
uint32_t previousBridge = obj["previousBridge"];
|
|
121
|
+
TSTRING reason = obj["reason"].as<TSTRING>();
|
|
122
|
+
|
|
123
|
+
Log(CONNECTION, "Bridge takeover: Node %u replaced %u (%s)\n",
|
|
124
|
+
newBridge, previousBridge, reason.c_str());
|
|
125
|
+
|
|
126
|
+
// Notify callback if this node was not the winner
|
|
127
|
+
if (newBridge != this->nodeId && bridgeRoleChangedCallback) {
|
|
128
|
+
bridgeRoleChangedCallback(false, "Another node won election");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false; // Don't consume the package
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Add callback to detect bridge failures and trigger elections
|
|
135
|
+
this->onBridgeStatusChanged([this](uint32_t bridgeNodeId, bool hasInternet) {
|
|
136
|
+
if (!hasInternet && bridgeFailoverEnabled && routerCredentialsConfigured) {
|
|
137
|
+
Log(CONNECTION, "Bridge %u lost Internet, considering election...\n", bridgeNodeId);
|
|
138
|
+
|
|
139
|
+
// Check if we still have any healthy bridges
|
|
140
|
+
if (!this->hasInternetConnection()) {
|
|
141
|
+
Log(CONNECTION, "No healthy bridges, starting election\n");
|
|
142
|
+
// Small delay to let all nodes detect the failure
|
|
143
|
+
this->addTask(2000, TASK_ONCE, [this]() {
|
|
144
|
+
this->startBridgeElection();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
83
150
|
tcpServerInit();
|
|
84
151
|
eventHandleInit();
|
|
85
152
|
|
|
@@ -118,6 +185,100 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
118
185
|
init(ssid, password, port, connectMode, channel, hidden, maxconn);
|
|
119
186
|
}
|
|
120
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Initialize mesh as a bridge node with automatic channel detection
|
|
190
|
+
*
|
|
191
|
+
* This method connects to a router first, detects its channel, then
|
|
192
|
+
* initializes the mesh on the same channel. This ensures the bridge
|
|
193
|
+
* can maintain both router and mesh connections on the same channel.
|
|
194
|
+
*
|
|
195
|
+
* The bridge node will automatically:
|
|
196
|
+
* - Connect to the specified router in STA mode
|
|
197
|
+
* - Detect the router's WiFi channel
|
|
198
|
+
* - Initialize the mesh AP on the detected channel
|
|
199
|
+
* - Set itself as root node
|
|
200
|
+
* - Maintain the router connection
|
|
201
|
+
*
|
|
202
|
+
* @param meshSSID The name of your mesh network
|
|
203
|
+
* @param meshPassword WiFi password for the mesh
|
|
204
|
+
* @param routerSSID SSID of the router to connect to
|
|
205
|
+
* @param routerPassword Password for the router
|
|
206
|
+
* @param baseScheduler Task scheduler for mesh operations
|
|
207
|
+
* @param port TCP port for mesh communication (default: 5555)
|
|
208
|
+
*/
|
|
209
|
+
void initAsBridge(TSTRING meshSSID, TSTRING meshPassword,
|
|
210
|
+
TSTRING routerSSID, TSTRING routerPassword,
|
|
211
|
+
Scheduler *baseScheduler, uint16_t port = 5555) {
|
|
212
|
+
using namespace logger;
|
|
213
|
+
|
|
214
|
+
Log(STARTUP, "=== Bridge Mode Initialization ===\n");
|
|
215
|
+
Log(STARTUP, "Step 1: Connecting to router %s...\n", routerSSID.c_str());
|
|
216
|
+
|
|
217
|
+
// Step 1: Connect to router first to detect its channel
|
|
218
|
+
// Shut Wifi down and start with a blank slate
|
|
219
|
+
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
220
|
+
|
|
221
|
+
Log(STARTUP, "initAsBridge(): %d\n",
|
|
222
|
+
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
|
223
|
+
WiFi.setAutoReconnect(false));
|
|
224
|
+
#else
|
|
225
|
+
WiFi.setAutoConnect(false));
|
|
226
|
+
#endif
|
|
227
|
+
WiFi.persistent(false);
|
|
228
|
+
WiFi.mode(WIFI_STA);
|
|
229
|
+
|
|
230
|
+
// Connect to router and wait for connection
|
|
231
|
+
WiFi.begin(routerSSID.c_str(), routerPassword.c_str());
|
|
232
|
+
|
|
233
|
+
// Wait for connection (with timeout)
|
|
234
|
+
int timeout = 30; // 30 seconds timeout
|
|
235
|
+
while (WiFi.status() != WL_CONNECTED && timeout > 0) {
|
|
236
|
+
delay(1000);
|
|
237
|
+
timeout--;
|
|
238
|
+
Log(STARTUP, ".");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
uint8_t detectedChannel = 1; // Default fallback
|
|
242
|
+
|
|
243
|
+
if (WiFi.status() == WL_CONNECTED) {
|
|
244
|
+
detectedChannel = WiFi.channel();
|
|
245
|
+
// Validate channel is in valid range (1-13 for 2.4GHz)
|
|
246
|
+
if (detectedChannel < 1 || detectedChannel > 13) {
|
|
247
|
+
Log(ERROR, "\n✗ Invalid channel detected: %d, using default channel 1\n", detectedChannel);
|
|
248
|
+
detectedChannel = 1;
|
|
249
|
+
} else {
|
|
250
|
+
Log(STARTUP, "\n✓ Router connected on channel %d\n", detectedChannel);
|
|
251
|
+
Log(STARTUP, "✓ Router IP: %s\n", WiFi.localIP().toString().c_str());
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
Log(ERROR, "\n✗ Failed to connect to router, using default channel 1\n");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
Log(STARTUP, "Step 2: Initializing mesh on channel %d...\n", detectedChannel);
|
|
258
|
+
|
|
259
|
+
// Step 2: Initialize mesh on detected channel
|
|
260
|
+
init(meshSSID, meshPassword, baseScheduler, port, WIFI_AP_STA,
|
|
261
|
+
detectedChannel, 0, MAX_CONN);
|
|
262
|
+
|
|
263
|
+
Log(STARTUP, "Step 3: Establishing bridge connection...\n");
|
|
264
|
+
|
|
265
|
+
// Step 3: Re-establish router connection using stationManual
|
|
266
|
+
stationManual(routerSSID, routerPassword, 0);
|
|
267
|
+
|
|
268
|
+
// Step 4: Configure as root/bridge node
|
|
269
|
+
this->setRoot(true);
|
|
270
|
+
this->setContainsRoot(true);
|
|
271
|
+
|
|
272
|
+
// Step 5: Setup bridge status broadcasting
|
|
273
|
+
initBridgeStatusBroadcast();
|
|
274
|
+
|
|
275
|
+
Log(STARTUP, "=== Bridge Mode Active ===\n");
|
|
276
|
+
Log(STARTUP, " Mesh SSID: %s\n", meshSSID.c_str());
|
|
277
|
+
Log(STARTUP, " Mesh Channel: %d (matches router)\n", detectedChannel);
|
|
278
|
+
Log(STARTUP, " Router: %s\n", routerSSID.c_str());
|
|
279
|
+
Log(STARTUP, " Port: %d\n", port);
|
|
280
|
+
}
|
|
281
|
+
|
|
121
282
|
/**
|
|
122
283
|
* Connect (as a station) to a specified network and ip
|
|
123
284
|
*
|
|
@@ -128,13 +289,21 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
128
289
|
*/
|
|
129
290
|
void stationManual(TSTRING ssid, TSTRING password, uint16_t port = 0,
|
|
130
291
|
IPAddress remote_ip = IPAddress(0, 0, 0, 0)) {
|
|
292
|
+
using namespace logger;
|
|
131
293
|
// Set station config
|
|
132
294
|
stationScan.manualIP = remote_ip;
|
|
133
295
|
|
|
296
|
+
Log(STARTUP, "stationManual(): Connecting to %s\n", ssid.c_str());
|
|
297
|
+
|
|
134
298
|
// Start scan
|
|
135
299
|
stationScan.init(this, ssid, password, port, _meshChannel,
|
|
136
300
|
static_cast<bool>(_meshHidden));
|
|
137
301
|
stationScan.manual = true;
|
|
302
|
+
|
|
303
|
+
// Directly initiate connection - ESP will auto-detect router's channel
|
|
304
|
+
WiFi.begin(ssid.c_str(), password.c_str());
|
|
305
|
+
|
|
306
|
+
Log(STARTUP, "stationManual(): Connection initiated\n");
|
|
138
307
|
}
|
|
139
308
|
|
|
140
309
|
void initStation() {
|
|
@@ -203,6 +372,52 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
203
372
|
IPAddress getStationIP() { return WiFi.localIP(); }
|
|
204
373
|
IPAddress getAPIP() { return _apIp; }
|
|
205
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Enable or disable automatic bridge failover
|
|
377
|
+
*
|
|
378
|
+
* When enabled, nodes will participate in bridge elections if the primary
|
|
379
|
+
* bridge goes offline and they have router credentials configured.
|
|
380
|
+
*
|
|
381
|
+
* @param enabled true to enable automatic failover (default), false to disable
|
|
382
|
+
*/
|
|
383
|
+
void enableBridgeFailover(bool enabled) {
|
|
384
|
+
bridgeFailoverEnabled = enabled;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Set router credentials for bridge election participation
|
|
389
|
+
*
|
|
390
|
+
* Nodes must have router credentials configured to participate in bridge
|
|
391
|
+
* elections. When a bridge fails, only nodes with credentials can become
|
|
392
|
+
* the new bridge.
|
|
393
|
+
*
|
|
394
|
+
* @param ssid Router SSID
|
|
395
|
+
* @param password Router password
|
|
396
|
+
*/
|
|
397
|
+
void setRouterCredentials(TSTRING ssid, TSTRING password) {
|
|
398
|
+
routerSSID = ssid;
|
|
399
|
+
routerPassword = password;
|
|
400
|
+
routerCredentialsConfigured = true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Set the election timeout (how long to collect candidates)
|
|
405
|
+
*
|
|
406
|
+
* @param timeoutMs Timeout in milliseconds (default: 5000 = 5 seconds)
|
|
407
|
+
*/
|
|
408
|
+
void setElectionTimeout(uint32_t timeoutMs) {
|
|
409
|
+
electionTimeoutMs = timeoutMs;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Set callback for when this node's bridge role changes
|
|
414
|
+
*
|
|
415
|
+
* @param callback Function to call when role changes
|
|
416
|
+
*/
|
|
417
|
+
void onBridgeRoleChanged(std::function<void(bool isBridge, TSTRING reason)> callback) {
|
|
418
|
+
bridgeRoleChangedCallback = callback;
|
|
419
|
+
}
|
|
420
|
+
|
|
206
421
|
void stop() {
|
|
207
422
|
// remove all WiFi events
|
|
208
423
|
#ifdef ESP32
|
|
@@ -256,6 +471,346 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
256
471
|
WiFi.softAP(_meshSSID.c_str(), _meshPassword.c_str(), _meshChannel,
|
|
257
472
|
_meshHidden, _meshMaxConn);
|
|
258
473
|
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Initialize bridge status broadcasting
|
|
477
|
+
* Sets up a periodic task to broadcast bridge status to the mesh
|
|
478
|
+
*/
|
|
479
|
+
void initBridgeStatusBroadcast() {
|
|
480
|
+
using namespace logger;
|
|
481
|
+
|
|
482
|
+
if (!this->isBridge() || !this->bridgeStatusBroadcastEnabled) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
Log(STARTUP, "initBridgeStatusBroadcast(): Setting up bridge status broadcast\n");
|
|
487
|
+
|
|
488
|
+
// Create periodic task to broadcast bridge status
|
|
489
|
+
bridgeStatusTask = this->addTask(
|
|
490
|
+
this->bridgeStatusIntervalMs,
|
|
491
|
+
TASK_FOREVER,
|
|
492
|
+
[this]() {
|
|
493
|
+
this->sendBridgeStatus();
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
Log(STARTUP, "Bridge status broadcast enabled (interval: %d ms)\n",
|
|
498
|
+
this->bridgeStatusIntervalMs);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Scan for router and return its signal strength
|
|
503
|
+
*
|
|
504
|
+
* @param routerSSID SSID of router to scan for
|
|
505
|
+
* @return RSSI in dBm (negative number, -127 to 0), or 0 if not found
|
|
506
|
+
*/
|
|
507
|
+
int8_t scanRouterSignalStrength(TSTRING routerSSID) {
|
|
508
|
+
using namespace logger;
|
|
509
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n", routerSSID.c_str());
|
|
510
|
+
|
|
511
|
+
int n = WiFi.scanNetworks(false, false);
|
|
512
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Found %d networks\n", n);
|
|
513
|
+
|
|
514
|
+
for (int i = 0; i < n; i++) {
|
|
515
|
+
if (WiFi.SSID(i) == routerSSID) {
|
|
516
|
+
int8_t rssi = WiFi.RSSI(i);
|
|
517
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Found %s with RSSI %d dBm\n",
|
|
518
|
+
routerSSID.c_str(), rssi);
|
|
519
|
+
return rssi;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
Log(CONNECTION, "scanRouterSignalStrength(): Router %s not found\n", routerSSID.c_str());
|
|
524
|
+
return 0; // Router not found
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Start bridge election process
|
|
529
|
+
* Called when primary bridge failure is detected
|
|
530
|
+
*/
|
|
531
|
+
void startBridgeElection() {
|
|
532
|
+
using namespace logger;
|
|
533
|
+
|
|
534
|
+
if (!bridgeFailoverEnabled) {
|
|
535
|
+
Log(CONNECTION, "startBridgeElection(): Failover disabled\n");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!routerCredentialsConfigured) {
|
|
540
|
+
Log(CONNECTION, "startBridgeElection(): No router credentials, cannot participate\n");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (electionState != ELECTION_IDLE) {
|
|
545
|
+
Log(CONNECTION, "startBridgeElection(): Election already in progress\n");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Prevent rapid role changes
|
|
550
|
+
if (millis() - lastRoleChangeTime < 60000) {
|
|
551
|
+
Log(CONNECTION, "startBridgeElection(): Too soon after last role change\n");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
Log(CONNECTION, "=== Bridge Election Started ===\n");
|
|
556
|
+
electionState = ELECTION_SCANNING;
|
|
557
|
+
|
|
558
|
+
// Scan for router to get RSSI
|
|
559
|
+
int8_t routerRSSI = scanRouterSignalStrength(routerSSID);
|
|
560
|
+
|
|
561
|
+
if (routerRSSI == 0) {
|
|
562
|
+
Log(CONNECTION, "startBridgeElection(): Router not visible, cannot participate\n");
|
|
563
|
+
electionState = ELECTION_IDLE;
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
Log(CONNECTION, "startBridgeElection(): My router RSSI: %d dBm\n", routerRSSI);
|
|
568
|
+
|
|
569
|
+
// Clear previous candidates
|
|
570
|
+
electionCandidates.clear();
|
|
571
|
+
|
|
572
|
+
// Add self as candidate
|
|
573
|
+
BridgeCandidate selfCandidate;
|
|
574
|
+
selfCandidate.nodeId = this->nodeId;
|
|
575
|
+
selfCandidate.routerRSSI = routerRSSI;
|
|
576
|
+
selfCandidate.uptime = millis();
|
|
577
|
+
selfCandidate.freeMemory = ESP.getFreeHeap();
|
|
578
|
+
electionCandidates.push_back(selfCandidate);
|
|
579
|
+
|
|
580
|
+
// Broadcast candidacy using JSON directly (avoiding dependency on alteriom package)
|
|
581
|
+
DynamicJsonDocument doc(256);
|
|
582
|
+
JsonObject obj = doc.to<JsonObject>();
|
|
583
|
+
obj["type"] = 611; // BRIDGE_ELECTION
|
|
584
|
+
obj["from"] = this->nodeId;
|
|
585
|
+
obj["routing"] = 2; // BROADCAST
|
|
586
|
+
obj["routerRSSI"] = routerRSSI;
|
|
587
|
+
obj["uptime"] = millis();
|
|
588
|
+
obj["freeMemory"] = ESP.getFreeHeap();
|
|
589
|
+
obj["timestamp"] = this->getNodeTime();
|
|
590
|
+
obj["routerSSID"] = routerSSID;
|
|
591
|
+
obj["message_type"] = 611;
|
|
592
|
+
|
|
593
|
+
String msg;
|
|
594
|
+
serializeJson(doc, msg);
|
|
595
|
+
this->sendBroadcast(msg);
|
|
596
|
+
|
|
597
|
+
Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
|
|
598
|
+
|
|
599
|
+
// Set election timeout
|
|
600
|
+
electionDeadline = millis() + electionTimeoutMs;
|
|
601
|
+
electionState = ELECTION_COLLECTING;
|
|
602
|
+
|
|
603
|
+
// Schedule election evaluation
|
|
604
|
+
this->addTask(electionTimeoutMs + 100, TASK_ONCE, [this]() {
|
|
605
|
+
this->evaluateElection();
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Evaluate election and determine winner
|
|
611
|
+
* Called after election timeout expires
|
|
612
|
+
*/
|
|
613
|
+
void evaluateElection() {
|
|
614
|
+
using namespace logger;
|
|
615
|
+
|
|
616
|
+
if (electionState != ELECTION_COLLECTING) {
|
|
617
|
+
Log(CONNECTION, "evaluateElection(): Not in collecting state\n");
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
Log(CONNECTION, "=== Evaluating Election ===\n");
|
|
622
|
+
Log(CONNECTION, "evaluateElection(): %d candidates\n", electionCandidates.size());
|
|
623
|
+
|
|
624
|
+
// Find best candidate
|
|
625
|
+
BridgeCandidate* winner = nullptr;
|
|
626
|
+
int8_t bestRSSI = -127; // Worst possible RSSI
|
|
627
|
+
|
|
628
|
+
for (auto& candidate : electionCandidates) {
|
|
629
|
+
Log(CONNECTION, "evaluateElection(): Candidate %u: RSSI=%d, uptime=%u, mem=%u\n",
|
|
630
|
+
candidate.nodeId, candidate.routerRSSI, candidate.uptime, candidate.freeMemory);
|
|
631
|
+
|
|
632
|
+
if (candidate.routerRSSI > bestRSSI) {
|
|
633
|
+
bestRSSI = candidate.routerRSSI;
|
|
634
|
+
winner = &candidate;
|
|
635
|
+
} else if (candidate.routerRSSI == bestRSSI && winner != nullptr) {
|
|
636
|
+
// Tiebreaker 1: Higher uptime
|
|
637
|
+
if (candidate.uptime > winner->uptime) {
|
|
638
|
+
winner = &candidate;
|
|
639
|
+
} else if (candidate.uptime == winner->uptime) {
|
|
640
|
+
// Tiebreaker 2: More free memory
|
|
641
|
+
if (candidate.freeMemory > winner->freeMemory) {
|
|
642
|
+
winner = &candidate;
|
|
643
|
+
} else if (candidate.freeMemory == winner->freeMemory) {
|
|
644
|
+
// Tiebreaker 3: Lower node ID (deterministic)
|
|
645
|
+
if (candidate.nodeId < winner->nodeId) {
|
|
646
|
+
winner = &candidate;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (winner == nullptr) {
|
|
654
|
+
Log(ERROR, "evaluateElection(): No winner found!\n");
|
|
655
|
+
electionState = ELECTION_IDLE;
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
Log(CONNECTION, "=== Election Winner: Node %u ===\n", winner->nodeId);
|
|
660
|
+
Log(CONNECTION, " Router RSSI: %d dBm\n", winner->routerRSSI);
|
|
661
|
+
Log(CONNECTION, " Uptime: %u ms\n", winner->uptime);
|
|
662
|
+
Log(CONNECTION, " Free Memory: %u bytes\n", winner->freeMemory);
|
|
663
|
+
|
|
664
|
+
// Record election in diagnostics history
|
|
665
|
+
if (this->diagnosticsEnabled) {
|
|
666
|
+
ElectionRecord record;
|
|
667
|
+
record.timestamp = millis();
|
|
668
|
+
record.winnerNodeId = winner->nodeId;
|
|
669
|
+
record.winnerRSSI = winner->routerRSSI;
|
|
670
|
+
record.candidateCount = electionCandidates.size();
|
|
671
|
+
record.reason = "Bridge failure detected";
|
|
672
|
+
|
|
673
|
+
this->electionHistory.push_back(record);
|
|
674
|
+
|
|
675
|
+
// Keep history limited to MAX_ELECTION_HISTORY
|
|
676
|
+
if (this->electionHistory.size() > this->MAX_ELECTION_HISTORY) {
|
|
677
|
+
this->electionHistory.erase(this->electionHistory.begin());
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
Log(CONNECTION, "evaluateElection(): Election recorded in history\n");
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (winner->nodeId == this->nodeId) {
|
|
684
|
+
Log(CONNECTION, "🎯 I WON! Promoting to bridge...\n");
|
|
685
|
+
promoteToBridge();
|
|
686
|
+
} else {
|
|
687
|
+
Log(CONNECTION, "Winner is node %u, remaining as regular node\n", winner->nodeId);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
electionState = ELECTION_IDLE;
|
|
691
|
+
electionCandidates.clear();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Promote this node to bridge role
|
|
696
|
+
* Called when node wins election
|
|
697
|
+
*/
|
|
698
|
+
void promoteToBridge() {
|
|
699
|
+
using namespace logger;
|
|
700
|
+
|
|
701
|
+
Log(STARTUP, "=== Becoming Bridge Node ===\n");
|
|
702
|
+
|
|
703
|
+
// Store previous bridge (if any)
|
|
704
|
+
auto primaryBridge = this->getPrimaryBridge();
|
|
705
|
+
uint32_t previousBridgeId = primaryBridge ? primaryBridge->nodeId : 0;
|
|
706
|
+
|
|
707
|
+
// Reconfigure as bridge
|
|
708
|
+
this->stop();
|
|
709
|
+
delay(1000);
|
|
710
|
+
|
|
711
|
+
this->initAsBridge(_meshSSID, _meshPassword, routerSSID, routerPassword,
|
|
712
|
+
mScheduler, _meshPort);
|
|
713
|
+
|
|
714
|
+
lastRoleChangeTime = millis();
|
|
715
|
+
|
|
716
|
+
Log(STARTUP, "✓ Bridge promotion complete\n");
|
|
717
|
+
|
|
718
|
+
// Notify via callback
|
|
719
|
+
if (bridgeRoleChangedCallback) {
|
|
720
|
+
bridgeRoleChangedCallback(true, "Election winner - best router signal");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Broadcast takeover announcement
|
|
724
|
+
DynamicJsonDocument doc(256);
|
|
725
|
+
JsonObject obj = doc.to<JsonObject>();
|
|
726
|
+
obj["type"] = 612; // BRIDGE_TAKEOVER
|
|
727
|
+
obj["from"] = this->nodeId;
|
|
728
|
+
obj["routing"] = 2; // BROADCAST
|
|
729
|
+
obj["previousBridge"] = previousBridgeId;
|
|
730
|
+
obj["reason"] = "Election winner - best router signal";
|
|
731
|
+
obj["routerRSSI"] = WiFi.RSSI();
|
|
732
|
+
obj["timestamp"] = this->getNodeTime();
|
|
733
|
+
obj["message_type"] = 612;
|
|
734
|
+
|
|
735
|
+
String msg;
|
|
736
|
+
serializeJson(doc, msg);
|
|
737
|
+
|
|
738
|
+
// Small delay to ensure mesh is ready
|
|
739
|
+
delay(2000);
|
|
740
|
+
this->sendBroadcast(msg);
|
|
741
|
+
|
|
742
|
+
Log(STARTUP, "✓ Takeover announcement sent\n");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Handle received bridge election package
|
|
747
|
+
* Called by package handler when election message arrives
|
|
748
|
+
*/
|
|
749
|
+
void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI, uint32_t uptime,
|
|
750
|
+
uint32_t freeMemory) {
|
|
751
|
+
using namespace logger;
|
|
752
|
+
|
|
753
|
+
if (electionState != ELECTION_COLLECTING) {
|
|
754
|
+
Log(CONNECTION, "handleBridgeElection(): Not collecting candidates, ignoring\n");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Check if candidate already exists
|
|
759
|
+
for (auto& candidate : electionCandidates) {
|
|
760
|
+
if (candidate.nodeId == fromNode) {
|
|
761
|
+
Log(CONNECTION, "handleBridgeElection(): Duplicate candidate from %u, ignoring\n", fromNode);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
BridgeCandidate candidate;
|
|
767
|
+
candidate.nodeId = fromNode;
|
|
768
|
+
candidate.routerRSSI = routerRSSI;
|
|
769
|
+
candidate.uptime = uptime;
|
|
770
|
+
candidate.freeMemory = freeMemory;
|
|
771
|
+
|
|
772
|
+
electionCandidates.push_back(candidate);
|
|
773
|
+
|
|
774
|
+
Log(CONNECTION, "handleBridgeElection(): Added candidate %u (RSSI: %d dBm)\n",
|
|
775
|
+
fromNode, routerRSSI);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Send bridge status broadcast
|
|
780
|
+
* Called periodically by bridge nodes to report connectivity status
|
|
781
|
+
*/
|
|
782
|
+
void sendBridgeStatus() {
|
|
783
|
+
using namespace logger;
|
|
784
|
+
|
|
785
|
+
if (!this->bridgeStatusBroadcastEnabled) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Create bridge status package
|
|
790
|
+
// We need to include the package header here since we're in wifi namespace
|
|
791
|
+
// The package will be sent as a JSON string
|
|
792
|
+
DynamicJsonDocument doc(256);
|
|
793
|
+
JsonObject obj = doc.to<JsonObject>();
|
|
794
|
+
|
|
795
|
+
obj["type"] = 610; // BRIDGE_STATUS type
|
|
796
|
+
obj["from"] = this->nodeId;
|
|
797
|
+
obj["routing"] = 2; // BROADCAST routing
|
|
798
|
+
obj["timestamp"] = this->getNodeTime();
|
|
799
|
+
obj["internetConnected"] = (WiFi.status() == WL_CONNECTED);
|
|
800
|
+
obj["routerRSSI"] = WiFi.RSSI();
|
|
801
|
+
obj["routerChannel"] = WiFi.channel();
|
|
802
|
+
obj["uptime"] = millis();
|
|
803
|
+
obj["gatewayIP"] = WiFi.gatewayIP().toString();
|
|
804
|
+
obj["message_type"] = 610;
|
|
805
|
+
|
|
806
|
+
String msg;
|
|
807
|
+
serializeJson(doc, msg);
|
|
808
|
+
|
|
809
|
+
Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)\n",
|
|
810
|
+
(WiFi.status() == WL_CONNECTED) ? "Connected" : "Disconnected");
|
|
811
|
+
|
|
812
|
+
this->sendBroadcast(msg);
|
|
813
|
+
}
|
|
259
814
|
void eventHandleInit() {
|
|
260
815
|
using namespace logger;
|
|
261
816
|
#ifdef ESP32
|
|
@@ -356,6 +911,32 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
356
911
|
WiFiEventHandler eventSTAGotIPHandler;
|
|
357
912
|
#endif // ESP8266
|
|
358
913
|
AsyncServer *_tcpListener;
|
|
914
|
+
std::shared_ptr<Task> bridgeStatusTask;
|
|
915
|
+
|
|
916
|
+
// Bridge failover state and configuration
|
|
917
|
+
enum ElectionState {
|
|
918
|
+
ELECTION_IDLE,
|
|
919
|
+
ELECTION_SCANNING,
|
|
920
|
+
ELECTION_COLLECTING
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
struct BridgeCandidate {
|
|
924
|
+
uint32_t nodeId;
|
|
925
|
+
int8_t routerRSSI;
|
|
926
|
+
uint32_t uptime;
|
|
927
|
+
uint32_t freeMemory;
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
bool bridgeFailoverEnabled = true;
|
|
931
|
+
bool routerCredentialsConfigured = false;
|
|
932
|
+
TSTRING routerSSID = "";
|
|
933
|
+
TSTRING routerPassword = "";
|
|
934
|
+
uint32_t electionTimeoutMs = 5000; // Default 5 seconds
|
|
935
|
+
uint32_t lastRoleChangeTime = 0;
|
|
936
|
+
ElectionState electionState = ELECTION_IDLE;
|
|
937
|
+
uint32_t electionDeadline = 0;
|
|
938
|
+
std::vector<BridgeCandidate> electionCandidates;
|
|
939
|
+
std::function<void(bool isBridge, TSTRING reason)> bridgeRoleChangedCallback;
|
|
359
940
|
};
|
|
360
941
|
} // namespace wifi
|
|
361
942
|
}; // namespace painlessmesh
|