@alteriom/painlessmesh 2.0.2 → 2.1.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 +213 -0
- package/CONTRIBUTING.md +66 -8
- package/README.md +4 -2
- package/RELEASE_GUIDE.md +11 -1
- package/examples/sendToInternet/README.md +56 -4
- package/examples/sendToInternet/callmebot.h +139 -0
- package/examples/sendToInternet/sendToInternet.ino +166 -53
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/AlteriomPainlessMesh.h +3 -3
- package/src/arduino/wifi.hpp +179 -86
- package/src/painlessMeshSTA.cpp +38 -4
- package/src/painlessMeshSTA.h +13 -1
- package/src/painlessmesh/buffer.hpp +11 -6
- package/src/painlessmesh/callback.hpp +7 -0
- package/src/painlessmesh/gateway.hpp +649 -27
- package/src/painlessmesh/layout.hpp +16 -0
- package/src/painlessmesh/logger.hpp +14 -1
- package/src/painlessmesh/mesh.hpp +410 -103
- package/src/painlessmesh/router.hpp +17 -7
|
@@ -49,10 +49,24 @@
|
|
|
49
49
|
// For Callmebot WhatsApp API:
|
|
50
50
|
// - Get your API key from https://www.callmebot.com/blog/free-api-whatsapp-messages/
|
|
51
51
|
// - Format: https://api.callmebot.com/whatsapp.php?phone=PHONE&apikey=KEY&text=MESSAGE
|
|
52
|
+
// - CallMeBot is a free service with a rate limit, and it does not say in
|
|
53
|
+
// the HTTP status whether it will deliver: callmebot.h reads its reply.
|
|
54
|
+
// Send it alerts, not a message every minute.
|
|
55
|
+
//
|
|
56
|
+
// What painlessMesh does and does not do for you:
|
|
57
|
+
// - It issues each request once. It retries only when the request cannot
|
|
58
|
+
// have reached the server (the connection was refused, say) or the server
|
|
59
|
+
// said to come back (HTTP 429/503, honouring Retry-After). A timeout after
|
|
60
|
+
// the request was sent is NOT retried: resending could deliver it twice.
|
|
61
|
+
// - Every attempt carries the same X-Request-Id / Idempotency-Key header.
|
|
62
|
+
// - The callback gets the HTTP status and the start of the response body.
|
|
63
|
+
// Whether that reply means "delivered" is the service's language, so the
|
|
64
|
+
// sketch decides -- here with callmebot::judge().
|
|
52
65
|
//
|
|
53
66
|
//************************************************************
|
|
54
67
|
#include "painlessMesh.h"
|
|
55
|
-
|
|
68
|
+
|
|
69
|
+
#include "callmebot.h"
|
|
56
70
|
|
|
57
71
|
// ============================================
|
|
58
72
|
// Mesh Network Configuration
|
|
@@ -74,6 +88,19 @@
|
|
|
74
88
|
#define WHATSAPP_PHONE "+1234567890" // Your phone number with country code
|
|
75
89
|
#define WHATSAPP_APIKEY "your_api_key" // Your Callmebot API key
|
|
76
90
|
|
|
91
|
+
// ============================================
|
|
92
|
+
// Cloud API Configuration
|
|
93
|
+
// ============================================
|
|
94
|
+
// The endpoint the periodic sensor readings are POSTed to. Replace it with
|
|
95
|
+
// your own; the placeholder below does not resolve, so until you do the
|
|
96
|
+
// sketch skips the cloud send and says so rather than reporting
|
|
97
|
+
// "connection refused" every minute (issue #450).
|
|
98
|
+
// Example endpoints:
|
|
99
|
+
// - ThingsBoard: "https://demo.thingsboard.io/api/v1/YOUR_TOKEN/telemetry"
|
|
100
|
+
// - AWS IoT: "https://YOUR_ENDPOINT.iot.us-east-1.amazonaws.com/topics/sensors"
|
|
101
|
+
// - Custom API: "https://api.yourserver.com/sensors/data"
|
|
102
|
+
#define CLOUD_URL "https://api.example.com/sensors"
|
|
103
|
+
|
|
77
104
|
// ============================================
|
|
78
105
|
// Sensor Simulation Configuration
|
|
79
106
|
// ============================================
|
|
@@ -82,9 +109,18 @@
|
|
|
82
109
|
#define TEMP_RANGE 10.0 // Temperature range (20-30°C)
|
|
83
110
|
#define HUMIDITY_MIN 40.0 // Minimum humidity (%)
|
|
84
111
|
#define HUMIDITY_RANGE 40.0 // Humidity range (40-80%)
|
|
85
|
-
#define O2_MIN 5.0 //
|
|
86
|
-
#define
|
|
87
|
-
#define O2_ALARM_THRESHOLD 6.0 // O2 level below this
|
|
112
|
+
#define O2_MIN 5.0 // Lowest simulated O2 level (mg/L)
|
|
113
|
+
#define O2_MAX 10.0 // Highest simulated O2 level (mg/L)
|
|
114
|
+
#define O2_ALARM_THRESHOLD 6.0 // O2 level below this raises the alarm
|
|
115
|
+
#define O2_ALARM_CLEAR 6.5 // ...and it clears only above this (hysteresis)
|
|
116
|
+
|
|
117
|
+
// ============================================
|
|
118
|
+
// Alert pacing
|
|
119
|
+
// ============================================
|
|
120
|
+
// One WhatsApp per alarm, not one per reading, and never more often than
|
|
121
|
+
// this per node. A reading every minute that alerted every time it was low
|
|
122
|
+
// is how a sketch runs into CallMeBot's rate limit.
|
|
123
|
+
#define ALERT_MIN_INTERVAL_MS (10UL * 60UL * 1000UL)
|
|
88
124
|
|
|
89
125
|
// ============================================
|
|
90
126
|
// Mode Selection
|
|
@@ -102,7 +138,7 @@ painlessMesh mesh;
|
|
|
102
138
|
// ============================================
|
|
103
139
|
// Function Prototypes
|
|
104
140
|
// ============================================
|
|
105
|
-
|
|
141
|
+
bool sendAlertToWhatsApp(String message, bool isO2Alarm = false);
|
|
106
142
|
void sendSensorDataToCloud();
|
|
107
143
|
void receivedCallback(uint32_t from, String& msg);
|
|
108
144
|
void newConnectionCallback(uint32_t nodeId);
|
|
@@ -115,6 +151,22 @@ String urlEncode(const String& str);
|
|
|
115
151
|
// Task to periodically send sensor data (simulated)
|
|
116
152
|
Task taskSendSensorData(60000, TASK_FOREVER, &sendSensorDataToCloud);
|
|
117
153
|
|
|
154
|
+
// The startup WhatsApp used to be a single 30 s one-shot. A regular node that
|
|
155
|
+
// took longer than that to find the mesh -- following a bridge to another
|
|
156
|
+
// channel takes about a minute -- fired it with no gateway in sight and never
|
|
157
|
+
// tried again (issue #450). It now retries until a gateway with Internet is
|
|
158
|
+
// known, then disables itself.
|
|
159
|
+
void sendStartupAlert();
|
|
160
|
+
Task taskStartupAlert(30000, TASK_FOREVER, &sendStartupAlert);
|
|
161
|
+
String startupMsg;
|
|
162
|
+
|
|
163
|
+
// Alert state
|
|
164
|
+
bool alertInFlight = false; // a WhatsApp request is on its way
|
|
165
|
+
uint32_t lastAlertMs = 0; // when the last one was handed over
|
|
166
|
+
uint32_t alertHoldMs = 0; // how long to wait after it
|
|
167
|
+
bool o2Alarm = false; // the alarm is raised
|
|
168
|
+
float o2Level = 8.0; // simulated reading, a slow random walk
|
|
169
|
+
|
|
118
170
|
// ============================================
|
|
119
171
|
// URL Encoding Helper
|
|
120
172
|
// ============================================
|
|
@@ -153,62 +205,97 @@ String urlEncode(const String& str) {
|
|
|
153
205
|
|
|
154
206
|
/**
|
|
155
207
|
* Send a WhatsApp message via Callmebot using sendToInternet()
|
|
156
|
-
*
|
|
208
|
+
*
|
|
157
209
|
* This function demonstrates how to use mesh.sendToInternet() to send
|
|
158
210
|
* data to an Internet endpoint. The request is automatically routed
|
|
159
211
|
* through a gateway node that has Internet access.
|
|
160
|
-
*
|
|
212
|
+
*
|
|
161
213
|
* @param message The message to send via WhatsApp
|
|
214
|
+
* @param isO2Alarm true for the O2 alarm, which is re-armed if CallMeBot
|
|
215
|
+
* does not take the message
|
|
216
|
+
* @return true when the request was handed to the mesh
|
|
162
217
|
*/
|
|
163
|
-
|
|
218
|
+
bool sendAlertToWhatsApp(String message, bool isO2Alarm) {
|
|
164
219
|
// Check if Internet is available via any gateway
|
|
165
220
|
if (!mesh.hasInternetConnection()) {
|
|
166
221
|
Serial.println("❌ No Internet available - no gateway with Internet found");
|
|
167
222
|
Serial.println(" Make sure at least one node is a bridge with router access");
|
|
168
|
-
return;
|
|
223
|
+
return false;
|
|
169
224
|
}
|
|
170
|
-
|
|
225
|
+
if (alertInFlight) {
|
|
226
|
+
Serial.println("(a WhatsApp message is still on its way; not sending another)");
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (lastAlertMs != 0 && millis() - lastAlertMs < alertHoldMs) {
|
|
230
|
+
Serial.printf("(WhatsApp paused for another %lu s after the last message)\n",
|
|
231
|
+
(unsigned long)((alertHoldMs - (millis() - lastAlertMs)) / 1000));
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
171
235
|
// URL-encode the message for safe transmission
|
|
172
236
|
String encodedMessage = urlEncode(message);
|
|
173
|
-
|
|
237
|
+
|
|
174
238
|
// Build the Callmebot WhatsApp API URL
|
|
175
239
|
// Format: https://api.callmebot.com/whatsapp.php?phone=PHONE&apikey=KEY&text=MESSAGE
|
|
176
240
|
String url = "https://api.callmebot.com/whatsapp.php";
|
|
177
|
-
url += "?phone=" +
|
|
241
|
+
url += "?phone=" + urlEncode(WHATSAPP_PHONE);
|
|
178
242
|
url += "&apikey=" + String(WHATSAPP_APIKEY);
|
|
179
243
|
url += "&text=" + encodedMessage;
|
|
180
|
-
|
|
244
|
+
|
|
181
245
|
Serial.println("\n📱 Sending WhatsApp message via sendToInternet()...");
|
|
182
246
|
Serial.printf(" Message: %s\n", message.c_str());
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
247
|
+
// Logs end up pasted into bug reports; the key stays out of them.
|
|
248
|
+
Serial.printf(" URL: %s\n", callmebot::redactApiKey(url).c_str());
|
|
249
|
+
|
|
250
|
+
// Use sendToInternet() to route the request through a gateway.
|
|
251
|
+
//
|
|
252
|
+
// This callback receives the whole InternetResult: the HTTP status, the
|
|
253
|
+
// start of CallMeBot's reply, whether the library could retry it and how
|
|
254
|
+
// many requests it took. painlessMesh says what HTTP says; callmebot::judge()
|
|
255
|
+
// says what CallMeBot meant -- a "Too many requests" page can arrive under
|
|
256
|
+
// HTTP 201, and HTTP 208 has meant "not delivered".
|
|
257
|
+
alertInFlight = true;
|
|
258
|
+
lastAlertMs = millis();
|
|
259
|
+
alertHoldMs = ALERT_MIN_INTERVAL_MS;
|
|
194
260
|
uint32_t msgId = mesh.sendToInternet(
|
|
195
261
|
url,
|
|
196
262
|
"", // No payload needed for GET request - params are in URL
|
|
197
|
-
[](
|
|
198
|
-
|
|
199
|
-
|
|
263
|
+
[isO2Alarm](const painlessmesh::InternetResult& result) {
|
|
264
|
+
alertInFlight = false;
|
|
265
|
+
const auto reply = callmebot::judge(result.httpStatus, result.response);
|
|
266
|
+
if (reply.accepted) {
|
|
267
|
+
Serial.printf("✅ WhatsApp message queued by CallMeBot (request %u, HTTP %u, %u attempt(s))\n",
|
|
268
|
+
result.messageId, result.httpStatus, result.attempts);
|
|
200
269
|
} else {
|
|
201
|
-
Serial.printf("❌
|
|
270
|
+
Serial.printf("❌ WhatsApp message not sent (request %u): %s\n",
|
|
271
|
+
result.messageId, reply.meaning);
|
|
272
|
+
Serial.printf(" HTTP %u after %u attempt(s)\n", result.httpStatus, result.attempts);
|
|
273
|
+
if (result.error.length() > 0) {
|
|
274
|
+
Serial.printf(" Error: %s\n", result.error.c_str());
|
|
275
|
+
}
|
|
276
|
+
if (result.response.length() > 0) {
|
|
277
|
+
Serial.printf(" CallMeBot said: %s\n", result.response.c_str());
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (reply.holdOffMs > alertHoldMs) {
|
|
281
|
+
alertHoldMs = reply.holdOffMs;
|
|
282
|
+
}
|
|
283
|
+
// An alarm CallMeBot did not take is still an alarm nobody has seen:
|
|
284
|
+
// re-arm it, so a later reading sends it again once the pause is over.
|
|
285
|
+
if (isO2Alarm && !reply.accepted) {
|
|
286
|
+
o2Alarm = false;
|
|
202
287
|
}
|
|
203
288
|
},
|
|
204
289
|
static_cast<uint8_t>(painlessmesh::gateway::GatewayPriority::PRIORITY_HIGH)
|
|
205
290
|
);
|
|
206
|
-
|
|
291
|
+
|
|
207
292
|
if (msgId > 0) {
|
|
208
|
-
Serial.printf("
|
|
209
|
-
|
|
210
|
-
Serial.println(" ❌ Failed to queue message - no gateway available");
|
|
293
|
+
Serial.printf(" Handed to the mesh as request %u; the result follows when the gateway answers\n", msgId);
|
|
294
|
+
return true;
|
|
211
295
|
}
|
|
296
|
+
// The callback reports why, from the scheduler, in a moment.
|
|
297
|
+
Serial.println(" Not handed to the mesh");
|
|
298
|
+
return false;
|
|
212
299
|
}
|
|
213
300
|
|
|
214
301
|
/**
|
|
@@ -222,7 +309,10 @@ void sendSensorDataToCloud() {
|
|
|
222
309
|
// Simulate sensor readings using configured ranges
|
|
223
310
|
float temperature = TEMP_MIN + random(0, (int)(TEMP_RANGE * 10)) / 10.0;
|
|
224
311
|
float humidity = HUMIDITY_MIN + random(0, (int)(HUMIDITY_RANGE * 10)) / 10.0;
|
|
225
|
-
|
|
312
|
+
// O2 drifts rather than jumping, so an alarm is an episode, not a coin toss
|
|
313
|
+
o2Level += random(-3, 4) / 10.0;
|
|
314
|
+
if (o2Level < O2_MIN) o2Level = O2_MIN;
|
|
315
|
+
if (o2Level > O2_MAX) o2Level = O2_MAX;
|
|
226
316
|
|
|
227
317
|
// Create JSON payload
|
|
228
318
|
String payload = "{";
|
|
@@ -236,11 +326,15 @@ void sendSensorDataToCloud() {
|
|
|
236
326
|
Serial.println("\n📊 Sending sensor data to cloud...");
|
|
237
327
|
Serial.printf(" Payload: %s\n", payload.c_str());
|
|
238
328
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
329
|
+
// Alert once when O2 falls below the threshold; re-arm once it recovers.
|
|
330
|
+
// An alert that could not be sent (no gateway yet, paused after a refusal)
|
|
331
|
+
// is tried again at the next reading while the alarm lasts.
|
|
332
|
+
if (!o2Alarm && o2Level < O2_ALARM_THRESHOLD) {
|
|
242
333
|
String alertMsg = "⚠️ ALARM: O2 level critical at " + String(o2Level, 1) + " mg/L! Node: " + String(mesh.getNodeId());
|
|
243
|
-
sendAlertToWhatsApp(alertMsg);
|
|
334
|
+
o2Alarm = sendAlertToWhatsApp(alertMsg, true);
|
|
335
|
+
} else if (o2Alarm && o2Level > O2_ALARM_CLEAR) {
|
|
336
|
+
Serial.printf(" O2 back to %.1f mg/L; alarm cleared\n", o2Level);
|
|
337
|
+
o2Alarm = false;
|
|
244
338
|
}
|
|
245
339
|
|
|
246
340
|
// Only send if Internet is available
|
|
@@ -249,14 +343,13 @@ void sendSensorDataToCloud() {
|
|
|
249
343
|
return;
|
|
250
344
|
}
|
|
251
345
|
|
|
252
|
-
// Send to cloud API
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
346
|
+
// Send to the cloud API configured at the top of the sketch
|
|
347
|
+
String cloudUrl = CLOUD_URL;
|
|
348
|
+
if (cloudUrl.indexOf("example.com") >= 0) {
|
|
349
|
+
Serial.println(" (CLOUD_URL is still the placeholder; set your endpoint to send readings)");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
260
353
|
uint32_t msgId = mesh.sendToInternet(
|
|
261
354
|
cloudUrl,
|
|
262
355
|
payload,
|
|
@@ -276,6 +369,22 @@ void sendSensorDataToCloud() {
|
|
|
276
369
|
// Mesh Callbacks
|
|
277
370
|
// ============================================
|
|
278
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Send the startup notification once a gateway with Internet is known.
|
|
374
|
+
*
|
|
375
|
+
* Runs every 30 s from taskStartupAlert until the message has been handed to
|
|
376
|
+
* sendToInternet(), then disables itself.
|
|
377
|
+
*/
|
|
378
|
+
void sendStartupAlert() {
|
|
379
|
+
if (!mesh.hasInternetConnection()) {
|
|
380
|
+
Serial.println("(startup WhatsApp waiting for a gateway with Internet; retrying in 30 s)");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (sendAlertToWhatsApp(startupMsg)) {
|
|
384
|
+
taskStartupAlert.disable();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
279
388
|
void receivedCallback(uint32_t from, String& msg) {
|
|
280
389
|
Serial.printf("📨 Received from %u: %s\n", from, msg.c_str());
|
|
281
390
|
}
|
|
@@ -347,14 +456,18 @@ void setup() {
|
|
|
347
456
|
Serial.printf("Is Bridge: %s\n", mesh.isBridge() ? "YES" : "NO");
|
|
348
457
|
Serial.println("================================================\n");
|
|
349
458
|
|
|
350
|
-
// Send a startup notification via WhatsApp (demonstrates sendToInternet)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
459
|
+
// Send a startup notification via WhatsApp (demonstrates sendToInternet).
|
|
460
|
+
// First attempt in 30 s, then every 30 s until a gateway with Internet is
|
|
461
|
+
// known: a bridge can use its own uplink at once, a regular node has to
|
|
462
|
+
// find the mesh first.
|
|
463
|
+
// A boot tag makes every start's text different: a service that drops a
|
|
464
|
+
// repeat of the last message (or answers it with HTTP 208) will not
|
|
465
|
+
// swallow this one because the previous boot sent the same words.
|
|
466
|
+
startupMsg = "🚀 Node " + String(mesh.getNodeId()) + " started (boot " +
|
|
467
|
+
String((uint32_t)random(0x10000), HEX) + ")";
|
|
468
|
+
Serial.println("Will send the startup WhatsApp as soon as a gateway with Internet is known (first try in 30 s)...\n");
|
|
469
|
+
userScheduler.addTask(taskStartupAlert);
|
|
470
|
+
taskStartupAlert.enableDelayed();
|
|
358
471
|
}
|
|
359
472
|
|
|
360
473
|
// ============================================
|
package/library.json
CHANGED
package/library.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name=Alteriom PainlessMesh
|
|
2
|
-
version=2.0
|
|
2
|
+
version=2.1.0
|
|
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": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
/**
|
|
30
30
|
* @brief AlteriomPainlessMesh library version information
|
|
31
31
|
*/
|
|
32
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION "2.0
|
|
32
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION "2.1.0"
|
|
33
33
|
#define ALTERIOM_PAINLESS_MESH_VERSION_MAJOR 2
|
|
34
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION_MINOR
|
|
35
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH
|
|
34
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION_MINOR 1
|
|
35
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 0
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @brief Library description and usage information
|