@alteriom/painlessmesh 1.8.0 → 1.8.2
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 +83 -1
- package/README.md +167 -12
- package/docs/architecture/mesh-architecture.md +20 -0
- package/docs/multi-bridge-setup.md +1025 -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/queued_alarms/README.md +390 -0
- package/examples/queued_alarms/queued_alarms.ino +265 -0
- package/library.json +22 -3
- 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,390 @@
|
|
|
1
|
+
# Queued Alarms Example
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This example demonstrates **priority-based message queueing** for critical IoT systems that cannot afford to lose data during Internet outages. It's designed for the fish farm dissolved oxygen (O2) monitoring use case described in [Issue #66](https://github.com/Alteriom/painlessMesh/issues/66).
|
|
6
|
+
|
|
7
|
+
## Problem Statement
|
|
8
|
+
|
|
9
|
+
In production IoT systems like fish farm monitoring, **critical alarms must never be lost**. When the bridge node loses Internet connectivity:
|
|
10
|
+
|
|
11
|
+
- ❌ **Without queueing**: Critical O2 alarms are lost → fish die
|
|
12
|
+
- ✅ **With queueing**: Alarms are queued and delivered when connection restored → supervisor notified, fish saved
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
### ✅ Priority-Based Queueing
|
|
17
|
+
|
|
18
|
+
- **CRITICAL** (Priority 0): Life-safety alarms - never dropped
|
|
19
|
+
- **HIGH** (Priority 1): Important warnings - preserved up to 80% capacity
|
|
20
|
+
- **NORMAL** (Priority 2): Regular data - preserved up to 60% capacity
|
|
21
|
+
- **LOW** (Priority 3): Telemetry - dropped first when queue full
|
|
22
|
+
|
|
23
|
+
### ✅ Automatic Queue Management
|
|
24
|
+
|
|
25
|
+
- Queues messages when Internet unavailable
|
|
26
|
+
- Flushes queue when Internet restored
|
|
27
|
+
- Prunes old messages (configurable age)
|
|
28
|
+
- Monitors queue health with callbacks
|
|
29
|
+
|
|
30
|
+
### ✅ Production Ready
|
|
31
|
+
|
|
32
|
+
- Survives Internet outages (queuing)
|
|
33
|
+
- Handles queue overflow intelligently
|
|
34
|
+
- Provides queue statistics
|
|
35
|
+
- Retry logic with attempt tracking
|
|
36
|
+
|
|
37
|
+
## Hardware Requirements
|
|
38
|
+
|
|
39
|
+
- **ESP32** or **ESP8266**
|
|
40
|
+
- At least 2 nodes (1 bridge + 1 sensor node)
|
|
41
|
+
- Bridge node needs WiFi router access
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
### 1. Mesh Network Settings
|
|
46
|
+
|
|
47
|
+
```cpp
|
|
48
|
+
#define MESH_PREFIX "FishFarmMesh"
|
|
49
|
+
#define MESH_PASSWORD "somethingSneaky"
|
|
50
|
+
#define MESH_PORT 5555
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Router Credentials (Bridge Node)
|
|
54
|
+
|
|
55
|
+
```cpp
|
|
56
|
+
#define ROUTER_SSID "YourWiFiSSID"
|
|
57
|
+
#define ROUTER_PASSWORD "YourWiFiPassword"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Enable in setup for bridge node:
|
|
61
|
+
```cpp
|
|
62
|
+
mesh.stationManual(ROUTER_SSID, ROUTER_PASSWORD);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. Sensor Thresholds
|
|
66
|
+
|
|
67
|
+
Adjust for your sensors (dissolved oxygen in mg/L):
|
|
68
|
+
|
|
69
|
+
```cpp
|
|
70
|
+
#define CRITICAL_O2_THRESHOLD 3.0 // Life-critical
|
|
71
|
+
#define WARNING_O2_THRESHOLD 5.0 // Warning level
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4. Queue Configuration
|
|
75
|
+
|
|
76
|
+
```cpp
|
|
77
|
+
#define MAX_QUEUE_SIZE 500 // Max messages
|
|
78
|
+
#define QUEUE_PRUNE_AGE (24 * 60 * 60 * 1000) // 24 hours
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## How It Works
|
|
82
|
+
|
|
83
|
+
### Normal Operation (Internet Available)
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Sensor → Mesh → Bridge → Internet → Cloud/MQTT
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Messages sent immediately, no queueing.
|
|
90
|
+
|
|
91
|
+
### Offline Mode (No Internet)
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Sensor → Mesh → Bridge → Queue (Priority-based)
|
|
95
|
+
↓
|
|
96
|
+
[CRITICAL never dropped]
|
|
97
|
+
[LOW dropped first]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Messages queued with priority, delivered when Internet restored.
|
|
101
|
+
|
|
102
|
+
### Internet Restored
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Queue → Flush → MQTT/HTTP → Cloud
|
|
106
|
+
↓
|
|
107
|
+
Remove on success
|
|
108
|
+
Retry on failure (max 3 attempts)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Usage
|
|
112
|
+
|
|
113
|
+
### 1. Flash Bridge Node
|
|
114
|
+
|
|
115
|
+
1. Uncomment these lines in `setup()`:
|
|
116
|
+
```cpp
|
|
117
|
+
mesh.stationManual(ROUTER_SSID, ROUTER_PASSWORD);
|
|
118
|
+
mesh.setHostname("FishFarmBridge");
|
|
119
|
+
```
|
|
120
|
+
2. Upload to ESP32/ESP8266 with router access
|
|
121
|
+
3. Bridge connects to router and provides Internet to mesh
|
|
122
|
+
|
|
123
|
+
### 2. Flash Sensor Nodes
|
|
124
|
+
|
|
125
|
+
1. Leave router lines commented
|
|
126
|
+
2. Upload to sensor node ESP32/ESP8266
|
|
127
|
+
3. Node joins mesh and monitors sensors
|
|
128
|
+
|
|
129
|
+
### 3. Monitor Serial Output
|
|
130
|
+
|
|
131
|
+
**Normal operation:**
|
|
132
|
+
```
|
|
133
|
+
✅ ONLINE MODE - Internet restored
|
|
134
|
+
📊 Telemetry: 7.32 mg/L
|
|
135
|
+
📊 Telemetry: 6.85 mg/L
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Internet lost:**
|
|
139
|
+
```
|
|
140
|
+
⚠️ OFFLINE MODE ACTIVATED
|
|
141
|
+
Queue size: 0 messages
|
|
142
|
+
📊 Telemetry: 6.42 mg/L - queued #1
|
|
143
|
+
[Queue: 1 messages]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Critical alarm (offline):**
|
|
147
|
+
```
|
|
148
|
+
🚨 CRITICAL O2 ALARM: 2.87 mg/L - QUEUED #5
|
|
149
|
+
[Queue: 5 messages (1 CRITICAL)]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Internet restored:**
|
|
153
|
+
```
|
|
154
|
+
✅ ONLINE MODE - Internet restored
|
|
155
|
+
Flushing 5 queued messages...
|
|
156
|
+
Sending queued message #1 (priority=3, attempts=0)
|
|
157
|
+
Sending queued message #5 (priority=0, attempts=0)
|
|
158
|
+
✅ Queue flushed (5 messages sent)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Queue States
|
|
162
|
+
|
|
163
|
+
The example monitors queue health:
|
|
164
|
+
|
|
165
|
+
- **EMPTY**: No messages queued
|
|
166
|
+
- **NORMAL**: Queue has space available
|
|
167
|
+
- **75% FULL**: Warning - queue reaching capacity
|
|
168
|
+
- **FULL**: Queue full - dropping LOW priority messages
|
|
169
|
+
|
|
170
|
+
Example output:
|
|
171
|
+
```
|
|
172
|
+
⚠️ Queue 75% full (375 messages)
|
|
173
|
+
🚨 Queue FULL (500 messages) - dropping LOW priority
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Testing Without Hardware
|
|
177
|
+
|
|
178
|
+
### Simulate Internet Loss
|
|
179
|
+
|
|
180
|
+
In real deployment, Internet loss is automatic. For testing, you can:
|
|
181
|
+
|
|
182
|
+
1. **Disconnect router**: Physically disconnect Ethernet/WAN
|
|
183
|
+
2. **Block MAC address**: Router settings → Block bridge MAC
|
|
184
|
+
3. **Power cycle router**: Turn off router
|
|
185
|
+
4. **Modify code**: Add test button to toggle `offlineMode`
|
|
186
|
+
|
|
187
|
+
### Verify Queue Behavior
|
|
188
|
+
|
|
189
|
+
1. Start with Internet connected
|
|
190
|
+
2. Cause Internet loss (any method above)
|
|
191
|
+
3. Wait for critical alarms to queue
|
|
192
|
+
4. Restore Internet
|
|
193
|
+
5. Verify messages are flushed
|
|
194
|
+
|
|
195
|
+
Expected sequence:
|
|
196
|
+
```
|
|
197
|
+
✅ Online → ⚠️ Offline (queueing) → ✅ Online (flush queue)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Integration with Cloud Services
|
|
201
|
+
|
|
202
|
+
### MQTT Example
|
|
203
|
+
|
|
204
|
+
Replace simulated sending with MQTT:
|
|
205
|
+
|
|
206
|
+
```cpp
|
|
207
|
+
#include <PubSubClient.h>
|
|
208
|
+
|
|
209
|
+
WiFiClient wifiClient;
|
|
210
|
+
PubSubClient mqttClient(wifiClient);
|
|
211
|
+
|
|
212
|
+
// In setup()
|
|
213
|
+
mqttClient.setServer("mqtt.example.com", 1883);
|
|
214
|
+
|
|
215
|
+
// In bridgeStatusCallback()
|
|
216
|
+
for (auto& msg : messages) {
|
|
217
|
+
bool sent = mqttClient.publish(
|
|
218
|
+
msg.destination.c_str(), // Topic from queueMessage()
|
|
219
|
+
msg.payload.c_str()
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (sent) {
|
|
223
|
+
mesh.removeQueuedMessage(msg.id);
|
|
224
|
+
} else {
|
|
225
|
+
mesh.incrementQueuedMessageAttempts(msg.id);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### HTTP Example
|
|
231
|
+
|
|
232
|
+
Replace simulated sending with HTTP POST:
|
|
233
|
+
|
|
234
|
+
```cpp
|
|
235
|
+
#include <HTTPClient.h>
|
|
236
|
+
|
|
237
|
+
HTTPClient http;
|
|
238
|
+
|
|
239
|
+
for (auto& msg : messages) {
|
|
240
|
+
http.begin(msg.destination); // URL from queueMessage()
|
|
241
|
+
http.addHeader("Content-Type", "application/json");
|
|
242
|
+
|
|
243
|
+
int httpCode = http.POST(msg.payload);
|
|
244
|
+
bool sent = (httpCode == 200 || httpCode == 201);
|
|
245
|
+
|
|
246
|
+
if (sent) {
|
|
247
|
+
mesh.removeQueuedMessage(msg.id);
|
|
248
|
+
} else {
|
|
249
|
+
mesh.incrementQueuedMessageAttempts(msg.id);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
http.end();
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Message Format
|
|
257
|
+
|
|
258
|
+
Example JSON message structure:
|
|
259
|
+
|
|
260
|
+
### Critical Alarm
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"type": "CRITICAL_ALARM",
|
|
264
|
+
"sensor": "O2",
|
|
265
|
+
"value": 2.87,
|
|
266
|
+
"threshold": 3.0,
|
|
267
|
+
"tankId": "TANK_A",
|
|
268
|
+
"nodeId": 123456789,
|
|
269
|
+
"timestamp": 1234567890
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Warning
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"type": "WARNING",
|
|
277
|
+
"sensor": "O2",
|
|
278
|
+
"value": 4.5,
|
|
279
|
+
"threshold": 5.0,
|
|
280
|
+
"nodeId": 123456789
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Telemetry
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"sensor": "O2",
|
|
288
|
+
"value": 7.32,
|
|
289
|
+
"nodeId": 123456789
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## API Reference
|
|
294
|
+
|
|
295
|
+
### Enable Queue
|
|
296
|
+
|
|
297
|
+
```cpp
|
|
298
|
+
mesh.enableMessageQueue(true, MAX_QUEUE_SIZE);
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Queue Message
|
|
302
|
+
|
|
303
|
+
```cpp
|
|
304
|
+
uint32_t msgId = mesh.queueMessage(
|
|
305
|
+
payload, // Message content
|
|
306
|
+
destination, // Cloud endpoint/topic
|
|
307
|
+
PRIORITY_CRITICAL // Priority level
|
|
308
|
+
);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Flush Queue
|
|
312
|
+
|
|
313
|
+
```cpp
|
|
314
|
+
auto messages = mesh.flushMessageQueue();
|
|
315
|
+
for (auto& msg : messages) {
|
|
316
|
+
if (sendToCloud(msg)) {
|
|
317
|
+
mesh.removeQueuedMessage(msg.id);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Query Queue
|
|
323
|
+
|
|
324
|
+
```cpp
|
|
325
|
+
uint32_t total = mesh.getQueuedMessageCount();
|
|
326
|
+
uint32_t critical = mesh.getQueuedMessageCount(PRIORITY_CRITICAL);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Callbacks
|
|
330
|
+
|
|
331
|
+
```cpp
|
|
332
|
+
mesh.onBridgeStatusChanged([](uint32_t bridgeId, bool hasInternet) {
|
|
333
|
+
// Handle Internet connectivity change
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
mesh.onQueueStateChanged([](QueueState state, uint32_t count) {
|
|
337
|
+
// Handle queue state change (EMPTY, NORMAL, 75%, FULL)
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Troubleshooting
|
|
342
|
+
|
|
343
|
+
### Queue Always Full
|
|
344
|
+
|
|
345
|
+
- Increase `MAX_QUEUE_SIZE`
|
|
346
|
+
- Decrease message frequency
|
|
347
|
+
- Lower message priorities
|
|
348
|
+
- Reduce `QUEUE_PRUNE_AGE`
|
|
349
|
+
|
|
350
|
+
### Messages Not Flushing
|
|
351
|
+
|
|
352
|
+
- Check `bridgeStatusCallback()` is called
|
|
353
|
+
- Verify Internet connectivity with `mesh.hasInternetConnection()`
|
|
354
|
+
- Check MQTT/HTTP sending code
|
|
355
|
+
- Monitor serial for errors
|
|
356
|
+
|
|
357
|
+
### High Memory Usage
|
|
358
|
+
|
|
359
|
+
- Reduce `MAX_QUEUE_SIZE`
|
|
360
|
+
- Enable aggressive pruning
|
|
361
|
+
- Use shorter message payloads
|
|
362
|
+
- Monitor with `ESP.getFreeHeap()`
|
|
363
|
+
|
|
364
|
+
## Performance
|
|
365
|
+
|
|
366
|
+
### Memory Usage
|
|
367
|
+
|
|
368
|
+
| Queue Size | RAM Usage (approx) |
|
|
369
|
+
|------------|-------------------|
|
|
370
|
+
| 100 | ~20 KB |
|
|
371
|
+
| 500 | ~100 KB |
|
|
372
|
+
| 1000 | ~200 KB |
|
|
373
|
+
|
|
374
|
+
**ESP32**: Can handle 1000+ messages
|
|
375
|
+
**ESP8266**: Recommend ≤500 messages
|
|
376
|
+
|
|
377
|
+
### Throughput
|
|
378
|
+
|
|
379
|
+
- **Queue**: ~1000 msg/sec
|
|
380
|
+
- **Flush**: Limited by MQTT/HTTP send rate (~10-50 msg/sec)
|
|
381
|
+
|
|
382
|
+
## Related Documentation
|
|
383
|
+
|
|
384
|
+
- [Issue #66: Message Queueing Feature](https://github.com/Alteriom/painlessMesh/issues/66)
|
|
385
|
+
- [Issue #63: Bridge Status Broadcast](https://github.com/Alteriom/painlessMesh/issues/63)
|
|
386
|
+
- [painlessMesh Documentation](https://gitlab.com/painlessMesh/painlessMesh)
|
|
387
|
+
|
|
388
|
+
## License
|
|
389
|
+
|
|
390
|
+
MIT License - See repository LICENSE file
|
|
@@ -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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/Alteriom/painlessMesh"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.8.
|
|
9
|
+
"version": "1.8.2",
|
|
10
10
|
"frameworks": [
|
|
11
11
|
"arduino"
|
|
12
12
|
],
|
|
@@ -78,17 +78,36 @@
|
|
|
78
78
|
],
|
|
79
79
|
"examples": [
|
|
80
80
|
"examples/alteriom/alteriom.ino",
|
|
81
|
-
"examples/
|
|
82
|
-
"examples/
|
|
81
|
+
"examples/alteriomSensorNode/alteriom_sensor_node.ino",
|
|
82
|
+
"examples/alteriomImproved/improved_sensor_node.ino",
|
|
83
|
+
"examples/alteriomMetricsHealth/metrics_health_node.ino",
|
|
84
|
+
"examples/alteriomPhase1/phase1_features.ino",
|
|
85
|
+
"examples/alteriomPhase2/phase2_features.ino",
|
|
83
86
|
"examples/basic/basic.ino",
|
|
84
87
|
"examples/bridge/bridge.ino",
|
|
88
|
+
"examples/bridge/bridge_health_monitoring_example.ino",
|
|
89
|
+
"examples/bridge/enhanced_mqtt_bridge_example.ino",
|
|
90
|
+
"examples/bridgeAwareSensorNode/bridgeAwareSensorNode.ino",
|
|
91
|
+
"examples/bridge_failover/bridge_failover.ino",
|
|
92
|
+
"examples/diagnosticsExample/diagnosticsExample.ino",
|
|
85
93
|
"examples/echoNode/echoNode.ino",
|
|
86
94
|
"examples/logClient/logClient.ino",
|
|
87
95
|
"examples/logServer/logServer.ino",
|
|
96
|
+
"examples/meshCommandNode/meshCommandNode.ino",
|
|
88
97
|
"examples/mqttBridge/mqttBridge.ino",
|
|
98
|
+
"examples/mqttCommandBridge/mqttCommandBridge.ino",
|
|
99
|
+
"examples/mqttStatusBridge/mqttStatusBridge.ino",
|
|
100
|
+
"examples/mqttTopologyTest/mqttTopologyTest.ino",
|
|
101
|
+
"examples/multi_bridge/primary_bridge.ino",
|
|
102
|
+
"examples/multi_bridge/secondary_bridge.ino",
|
|
103
|
+
"examples/multi_bridge/regular_node.ino",
|
|
89
104
|
"examples/namedMesh/namedMesh.ino",
|
|
105
|
+
"examples/ntpTimeSyncBridge/ntpTimeSyncBridge.ino",
|
|
106
|
+
"examples/ntpTimeSyncNode/ntpTimeSyncNode.ino",
|
|
90
107
|
"examples/otaReceiver/otaReceiver.ino",
|
|
91
108
|
"examples/otaSender/otaSender.ino",
|
|
109
|
+
"examples/queued_alarms/queued_alarms.ino",
|
|
110
|
+
"examples/rtcIntegration/rtcIntegration.ino",
|
|
92
111
|
"examples/startHere/startHere.ino",
|
|
93
112
|
"examples/webServer/webServer.ino"
|
|
94
113
|
]
|
package/library.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name=AlteriomPainlessMesh
|
|
2
|
-
version=1.8.
|
|
2
|
+
version=1.8.2
|
|
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
|