@alteriom/painlessmesh 1.8.0 → 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 +24 -0
- package/README.md +51 -11
- 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/queued_alarms/README.md +390 -0
- package/examples/queued_alarms/queued_alarms.ino +265 -0
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/arduino/wifi.hpp +319 -3
- package/src/painlessmesh/mesh.hpp +200 -0
- package/src/painlessmesh/message_queue.hpp +368 -0
- package/src/painlessmesh/plugin.hpp +69 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
//************************************************************
|
|
2
|
+
// Queued Alarms Example - Message Queueing for Offline Mode
|
|
3
|
+
//
|
|
4
|
+
// Demonstrates priority-based message queueing for critical alarms
|
|
5
|
+
// when Internet connection is unavailable. Perfect for IoT systems
|
|
6
|
+
// that cannot afford to lose critical data.
|
|
7
|
+
//
|
|
8
|
+
// Use Case: Fish farm dissolved oxygen monitoring
|
|
9
|
+
// - CRITICAL alarms (low O2) must never be lost
|
|
10
|
+
// - Queue messages during Internet outages
|
|
11
|
+
// - Automatic delivery when connection restored
|
|
12
|
+
//
|
|
13
|
+
// Hardware: ESP32 or ESP8266
|
|
14
|
+
//************************************************************
|
|
15
|
+
|
|
16
|
+
#include "painlessMesh.h"
|
|
17
|
+
|
|
18
|
+
// Mesh configuration
|
|
19
|
+
#define MESH_PREFIX "FishFarmMesh"
|
|
20
|
+
#define MESH_PASSWORD "somethingSneaky"
|
|
21
|
+
#define MESH_PORT 5555
|
|
22
|
+
|
|
23
|
+
// Router credentials for bridge node
|
|
24
|
+
#define ROUTER_SSID "YourWiFiSSID"
|
|
25
|
+
#define ROUTER_PASSWORD "YourWiFiPassword"
|
|
26
|
+
|
|
27
|
+
// Sensor thresholds (mg/L for dissolved oxygen)
|
|
28
|
+
#define CRITICAL_O2_THRESHOLD 3.0
|
|
29
|
+
#define WARNING_O2_THRESHOLD 5.0
|
|
30
|
+
|
|
31
|
+
// Queue configuration
|
|
32
|
+
#define MAX_QUEUE_SIZE 500
|
|
33
|
+
#define QUEUE_PRUNE_AGE (24 * 60 * 60 * 1000) // 24 hours in ms
|
|
34
|
+
|
|
35
|
+
Scheduler userScheduler;
|
|
36
|
+
painlessMesh mesh;
|
|
37
|
+
|
|
38
|
+
bool offlineMode = false;
|
|
39
|
+
uint32_t lastO2Check = 0;
|
|
40
|
+
uint32_t lastQueuePrune = 0;
|
|
41
|
+
|
|
42
|
+
// Simulated sensor reading (replace with actual sensor code)
|
|
43
|
+
float readDissolvedOxygenSensor() {
|
|
44
|
+
// In real application, read from actual sensor
|
|
45
|
+
// For demo, simulate varying O2 levels
|
|
46
|
+
static float o2Level = 7.0;
|
|
47
|
+
o2Level += random(-20, 20) / 10.0; // +/- 2.0 mg/L variation
|
|
48
|
+
o2Level = constrain(o2Level, 2.0, 10.0);
|
|
49
|
+
return o2Level;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Send critical O2 alarm
|
|
53
|
+
void sendCriticalAlarm(float o2Level) {
|
|
54
|
+
// Create alarm message (in real app, use JSON)
|
|
55
|
+
String payload = String("{\"type\":\"CRITICAL_ALARM\",\"sensor\":\"O2\",\"value\":")
|
|
56
|
+
+ String(o2Level, 2) + ",\"threshold\":"
|
|
57
|
+
+ String(CRITICAL_O2_THRESHOLD, 2) + ",\"tankId\":\"TANK_A\",\"nodeId\":"
|
|
58
|
+
+ mesh.getNodeId() + ",\"timestamp\":" + mesh.getNodeTime() + "}";
|
|
59
|
+
|
|
60
|
+
if (offlineMode || !mesh.hasInternetConnection()) {
|
|
61
|
+
// CRITICAL: Queue for guaranteed delivery
|
|
62
|
+
uint32_t msgId = mesh.queueMessage(
|
|
63
|
+
payload,
|
|
64
|
+
"mqtt://cloud.farm.com/alarms/critical",
|
|
65
|
+
PRIORITY_CRITICAL
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (msgId) {
|
|
69
|
+
Serial.printf("🚨 CRITICAL O2 ALARM: %.2f mg/L - QUEUED #%u\n", o2Level, msgId);
|
|
70
|
+
} else {
|
|
71
|
+
Serial.printf("🚨 CRITICAL O2 ALARM: %.2f mg/L - QUEUE FAILED!\n", o2Level);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// Send immediately via bridge (in real app, use MQTT client)
|
|
75
|
+
Serial.printf("🚨 CRITICAL O2 ALARM: %.2f mg/L - SENT IMMEDIATELY\n", o2Level);
|
|
76
|
+
// mqttClient.publish("alarms/critical", payload.c_str());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Send warning alarm
|
|
81
|
+
void sendWarningAlarm(float o2Level) {
|
|
82
|
+
String payload = String("{\"type\":\"WARNING\",\"sensor\":\"O2\",\"value\":")
|
|
83
|
+
+ String(o2Level, 2) + ",\"threshold\":"
|
|
84
|
+
+ String(WARNING_O2_THRESHOLD, 2) + ",\"nodeId\":"
|
|
85
|
+
+ mesh.getNodeId() + "}";
|
|
86
|
+
|
|
87
|
+
if (offlineMode) {
|
|
88
|
+
uint32_t msgId = mesh.queueMessage(
|
|
89
|
+
payload,
|
|
90
|
+
"mqtt://cloud.farm.com/alarms/warning",
|
|
91
|
+
PRIORITY_HIGH
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (msgId) {
|
|
95
|
+
Serial.printf("⚠️ WARNING O2: %.2f mg/L - QUEUED #%u\n", o2Level, msgId);
|
|
96
|
+
} else {
|
|
97
|
+
Serial.printf("⚠️ WARNING O2: %.2f mg/L - QUEUE FULL\n", o2Level);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
Serial.printf("⚠️ WARNING O2: %.2f mg/L - SENT\n", o2Level);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Send normal telemetry
|
|
105
|
+
void sendNormalTelemetry(float o2Level) {
|
|
106
|
+
String payload = String("{\"sensor\":\"O2\",\"value\":") + String(o2Level, 2)
|
|
107
|
+
+ ",\"nodeId\":" + mesh.getNodeId() + "}";
|
|
108
|
+
|
|
109
|
+
if (offlineMode) {
|
|
110
|
+
// Low priority - queue only if space available
|
|
111
|
+
uint32_t msgId = mesh.queueMessage(
|
|
112
|
+
payload,
|
|
113
|
+
"mqtt://cloud.farm.com/telemetry",
|
|
114
|
+
PRIORITY_LOW
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (msgId) {
|
|
118
|
+
Serial.printf("📊 Telemetry: %.2f mg/L - queued #%u\n", o2Level, msgId);
|
|
119
|
+
} else {
|
|
120
|
+
Serial.printf("📊 Telemetry: %.2f mg/L - dropped (queue full)\n", o2Level);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
Serial.printf("📊 Telemetry: %.2f mg/L\n", o2Level);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check O2 sensor and send appropriate message
|
|
128
|
+
void checkO2Sensor() {
|
|
129
|
+
float o2Level = readDissolvedOxygenSensor();
|
|
130
|
+
|
|
131
|
+
if (o2Level < CRITICAL_O2_THRESHOLD) {
|
|
132
|
+
sendCriticalAlarm(o2Level);
|
|
133
|
+
} else if (o2Level < WARNING_O2_THRESHOLD) {
|
|
134
|
+
sendWarningAlarm(o2Level);
|
|
135
|
+
} else {
|
|
136
|
+
sendNormalTelemetry(o2Level);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Bridge status callback - Internet connectivity changed
|
|
141
|
+
void bridgeStatusCallback(uint32_t bridgeNodeId, bool hasInternet) {
|
|
142
|
+
if (!hasInternet) {
|
|
143
|
+
offlineMode = true;
|
|
144
|
+
Serial.println("\n⚠️ OFFLINE MODE ACTIVATED");
|
|
145
|
+
Serial.printf(" Bridge node %u lost Internet\n", bridgeNodeId);
|
|
146
|
+
Serial.printf(" Queue size: %u messages\n", mesh.getQueuedMessageCount());
|
|
147
|
+
|
|
148
|
+
uint32_t critical = mesh.getQueuedMessageCount(PRIORITY_CRITICAL);
|
|
149
|
+
if (critical > 0) {
|
|
150
|
+
Serial.printf(" ⚠️ %u CRITICAL messages queued!\n", critical);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
offlineMode = false;
|
|
154
|
+
Serial.println("\n✅ ONLINE MODE - Internet restored");
|
|
155
|
+
Serial.printf(" Bridge node %u has Internet\n", bridgeNodeId);
|
|
156
|
+
|
|
157
|
+
// Flush queued messages
|
|
158
|
+
uint32_t queuedCount = mesh.getQueuedMessageCount();
|
|
159
|
+
if (queuedCount > 0) {
|
|
160
|
+
Serial.printf(" Flushing %u queued messages...\n", queuedCount);
|
|
161
|
+
|
|
162
|
+
auto messages = mesh.flushMessageQueue();
|
|
163
|
+
for (auto& msg : messages) {
|
|
164
|
+
// In real application, send via MQTT or HTTP
|
|
165
|
+
Serial.printf(" Sending queued message #%u (priority=%d, attempts=%u)\n",
|
|
166
|
+
msg.id, msg.priority, msg.attempts);
|
|
167
|
+
|
|
168
|
+
// Simulate sending (in real app, check if send succeeded)
|
|
169
|
+
bool sent = true; // Replace with: mqttClient.publish(...)
|
|
170
|
+
|
|
171
|
+
if (sent) {
|
|
172
|
+
mesh.removeQueuedMessage(msg.id);
|
|
173
|
+
} else {
|
|
174
|
+
// Increment attempt counter
|
|
175
|
+
mesh.incrementQueuedMessageAttempts(msg.id);
|
|
176
|
+
|
|
177
|
+
// Remove if too many attempts
|
|
178
|
+
if (msg.attempts >= 3) {
|
|
179
|
+
Serial.printf(" ❌ Message #%u failed after 3 attempts, removing\n", msg.id);
|
|
180
|
+
mesh.removeQueuedMessage(msg.id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
Serial.printf(" ✅ Queue flushed (%u messages sent)\n", queuedCount);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Queue state callback - Monitor queue health
|
|
191
|
+
void queueStateCallback(QueueState state, uint32_t messageCount) {
|
|
192
|
+
switch (state) {
|
|
193
|
+
case QUEUE_EMPTY:
|
|
194
|
+
Serial.println("ℹ️ Queue empty");
|
|
195
|
+
break;
|
|
196
|
+
case QUEUE_NORMAL:
|
|
197
|
+
Serial.printf("ℹ️ Queue normal (%u messages)\n", messageCount);
|
|
198
|
+
break;
|
|
199
|
+
case QUEUE_75_PERCENT:
|
|
200
|
+
Serial.printf("⚠️ Queue 75%% full (%u messages)\n", messageCount);
|
|
201
|
+
break;
|
|
202
|
+
case QUEUE_FULL:
|
|
203
|
+
Serial.printf("🚨 Queue FULL (%u messages) - dropping LOW priority\n", messageCount);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
void setup() {
|
|
209
|
+
Serial.begin(115200);
|
|
210
|
+
Serial.println("\n\n=== Fish Farm O2 Monitoring with Message Queue ===\n");
|
|
211
|
+
|
|
212
|
+
// Initialize mesh
|
|
213
|
+
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
|
|
214
|
+
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
|
|
215
|
+
|
|
216
|
+
// For bridge node: set router credentials
|
|
217
|
+
// Uncomment if this is the bridge node
|
|
218
|
+
// mesh.stationManual(ROUTER_SSID, ROUTER_PASSWORD);
|
|
219
|
+
// mesh.setHostname("FishFarmBridge");
|
|
220
|
+
|
|
221
|
+
// Enable message queue
|
|
222
|
+
mesh.enableMessageQueue(true, MAX_QUEUE_SIZE);
|
|
223
|
+
Serial.printf("Message queue enabled (capacity: %u)\n", MAX_QUEUE_SIZE);
|
|
224
|
+
|
|
225
|
+
// Set callbacks
|
|
226
|
+
mesh.onBridgeStatusChanged(&bridgeStatusCallback);
|
|
227
|
+
mesh.onQueueStateChanged(&queueStateCallback);
|
|
228
|
+
|
|
229
|
+
Serial.println("Setup complete. Monitoring O2 levels...\n");
|
|
230
|
+
|
|
231
|
+
// Initialize random for sensor simulation
|
|
232
|
+
randomSeed(analogRead(0));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
void loop() {
|
|
236
|
+
mesh.update();
|
|
237
|
+
|
|
238
|
+
// Check O2 sensor every 10 seconds
|
|
239
|
+
if (millis() - lastO2Check > 10000) {
|
|
240
|
+
lastO2Check = millis();
|
|
241
|
+
checkO2Sensor();
|
|
242
|
+
|
|
243
|
+
// Print queue status
|
|
244
|
+
uint32_t queueSize = mesh.getQueuedMessageCount();
|
|
245
|
+
if (queueSize > 0) {
|
|
246
|
+
Serial.printf(" [Queue: %u messages", queueSize);
|
|
247
|
+
|
|
248
|
+
uint32_t critical = mesh.getQueuedMessageCount(PRIORITY_CRITICAL);
|
|
249
|
+
if (critical > 0) {
|
|
250
|
+
Serial.printf(" (%u CRITICAL)", critical);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Serial.println("]");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Prune old messages every hour
|
|
258
|
+
if (millis() - lastQueuePrune > 3600000) {
|
|
259
|
+
lastQueuePrune = millis();
|
|
260
|
+
uint32_t pruned = mesh.pruneQueue(QUEUE_PRUNE_AGE);
|
|
261
|
+
if (pruned > 0) {
|
|
262
|
+
Serial.printf("ℹ️ Pruned %u old messages from queue\n", pruned);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
package/library.json
CHANGED
package/library.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name=AlteriomPainlessMesh
|
|
2
|
-
version=1.8.
|
|
2
|
+
version=1.8.1
|
|
3
3
|
author=Coopdis,Scotty Franzyshen,Edwin van Leeuwen,Germán Martín,Maximilian Schwarz,Doanh Doanh,Alteriom
|
|
4
4
|
maintainer=Alteriom
|
|
5
5
|
sentence=A painless way to setup a mesh with ESP8266 and ESP32 devices with Alteriom extensions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alteriom/painlessmesh",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "painlessMesh is a user-friendly library for creating mesh networks with ESP8266 and ESP32 devices. This Alteriom fork includes additional packages for sensor data (SensorPackage), device commands (CommandPackage), and status monitoring (StatusPackage). It handles routing and network management automatically, so you can focus on your application. The library uses JSON-based messaging and syncs time across all nodes, making it ideal for coordinated behaviour like synchronized light displays or sensor networks reporting to a central node.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"arduino",
|
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:
|
|
@@ -279,6 +286,51 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
279
286
|
Log(STARTUP, " Port: %d\n", port);
|
|
280
287
|
}
|
|
281
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
|
+
|
|
282
334
|
/**
|
|
283
335
|
* Connect (as a station) to a specified network and ip
|
|
284
336
|
*
|
|
@@ -418,6 +470,148 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
418
470
|
bridgeRoleChangedCallback = callback;
|
|
419
471
|
}
|
|
420
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
|
+
|
|
421
615
|
void stop() {
|
|
422
616
|
// remove all WiFi events
|
|
423
617
|
#ifdef ESP32
|
|
@@ -498,6 +692,115 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
498
692
|
this->bridgeStatusIntervalMs);
|
|
499
693
|
}
|
|
500
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
|
+
|
|
501
804
|
/**
|
|
502
805
|
* Scan for router and return its signal strength
|
|
503
806
|
*
|
|
@@ -578,7 +881,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
578
881
|
electionCandidates.push_back(selfCandidate);
|
|
579
882
|
|
|
580
883
|
// Broadcast candidacy using JSON directly (avoiding dependency on alteriom package)
|
|
581
|
-
|
|
884
|
+
JsonDocument doc;
|
|
582
885
|
JsonObject obj = doc.to<JsonObject>();
|
|
583
886
|
obj["type"] = 611; // BRIDGE_ELECTION
|
|
584
887
|
obj["from"] = this->nodeId;
|
|
@@ -721,7 +1024,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
721
1024
|
}
|
|
722
1025
|
|
|
723
1026
|
// Broadcast takeover announcement
|
|
724
|
-
|
|
1027
|
+
JsonDocument doc;
|
|
725
1028
|
JsonObject obj = doc.to<JsonObject>();
|
|
726
1029
|
obj["type"] = 612; // BRIDGE_TAKEOVER
|
|
727
1030
|
obj["from"] = this->nodeId;
|
|
@@ -789,7 +1092,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
789
1092
|
// Create bridge status package
|
|
790
1093
|
// We need to include the package header here since we're in wifi namespace
|
|
791
1094
|
// The package will be sent as a JSON string
|
|
792
|
-
|
|
1095
|
+
JsonDocument doc;
|
|
793
1096
|
JsonObject obj = doc.to<JsonObject>();
|
|
794
1097
|
|
|
795
1098
|
obj["type"] = 610; // BRIDGE_STATUS type
|
|
@@ -937,6 +1240,19 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
937
1240
|
uint32_t electionDeadline = 0;
|
|
938
1241
|
std::vector<BridgeCandidate> electionCandidates;
|
|
939
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
|
|
940
1256
|
};
|
|
941
1257
|
} // namespace wifi
|
|
942
1258
|
}; // namespace painlessmesh
|