@alteriom/painlessmesh 2.0.3 → 2.1.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.
@@ -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
- #include <WiFiClientSecure.h>
68
+
69
+ #include "callmebot.h"
56
70
 
57
71
  // ============================================
58
72
  // Mesh Network Configuration
@@ -95,9 +109,18 @@
95
109
  #define TEMP_RANGE 10.0 // Temperature range (20-30°C)
96
110
  #define HUMIDITY_MIN 40.0 // Minimum humidity (%)
97
111
  #define HUMIDITY_RANGE 40.0 // Humidity range (40-80%)
98
- #define O2_MIN 5.0 // Minimum O2 level (mg/L)
99
- #define O2_RANGE 5.0 // O2 range (5-10 mg/L)
100
- #define O2_ALARM_THRESHOLD 6.0 // O2 level below this triggers alarm
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)
101
124
 
102
125
  // ============================================
103
126
  // Mode Selection
@@ -115,7 +138,7 @@ painlessMesh mesh;
115
138
  // ============================================
116
139
  // Function Prototypes
117
140
  // ============================================
118
- void sendAlertToWhatsApp(String message);
141
+ bool sendAlertToWhatsApp(String message, bool isO2Alarm = false);
119
142
  void sendSensorDataToCloud();
120
143
  void receivedCallback(uint32_t from, String& msg);
121
144
  void newConnectionCallback(uint32_t nodeId);
@@ -137,6 +160,13 @@ void sendStartupAlert();
137
160
  Task taskStartupAlert(30000, TASK_FOREVER, &sendStartupAlert);
138
161
  String startupMsg;
139
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
+
140
170
  // ============================================
141
171
  // URL Encoding Helper
142
172
  // ============================================
@@ -175,61 +205,97 @@ String urlEncode(const String& str) {
175
205
 
176
206
  /**
177
207
  * Send a WhatsApp message via Callmebot using sendToInternet()
178
- *
208
+ *
179
209
  * This function demonstrates how to use mesh.sendToInternet() to send
180
210
  * data to an Internet endpoint. The request is automatically routed
181
211
  * through a gateway node that has Internet access.
182
- *
212
+ *
183
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
184
217
  */
185
- void sendAlertToWhatsApp(String message) {
218
+ bool sendAlertToWhatsApp(String message, bool isO2Alarm) {
186
219
  // Check if Internet is available via any gateway
187
220
  if (!mesh.hasInternetConnection()) {
188
221
  Serial.println("❌ No Internet available - no gateway with Internet found");
189
222
  Serial.println(" Make sure at least one node is a bridge with router access");
190
- return;
223
+ return false;
191
224
  }
192
-
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
+
193
235
  // URL-encode the message for safe transmission
194
236
  String encodedMessage = urlEncode(message);
195
-
237
+
196
238
  // Build the Callmebot WhatsApp API URL
197
239
  // Format: https://api.callmebot.com/whatsapp.php?phone=PHONE&apikey=KEY&text=MESSAGE
198
240
  String url = "https://api.callmebot.com/whatsapp.php";
199
- url += "?phone=" + String(WHATSAPP_PHONE);
241
+ url += "?phone=" + urlEncode(WHATSAPP_PHONE);
200
242
  url += "&apikey=" + String(WHATSAPP_APIKEY);
201
243
  url += "&text=" + encodedMessage;
202
-
244
+
203
245
  Serial.println("\n📱 Sending WhatsApp message via sendToInternet()...");
204
246
  Serial.printf(" Message: %s\n", message.c_str());
205
- Serial.printf(" URL: %s\n", url.c_str());
206
-
207
- // Use sendToInternet() to route the request through a gateway
208
- // The callback will be invoked when we get a response (or timeout)
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.
209
251
  //
210
- // The gateway reads the response body, not only the status: CallMeBot
211
- // answers a refusal ("Too many requests") with HTTP 201 or 203, so a 2xx
212
- // alone proves nothing. A refusal arrives here as success=false with the
213
- // service's own words in `error`; HTTP 203 stays a failure because a proxy,
214
- // not WhatsApp, may have answered.
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;
215
260
  uint32_t msgId = mesh.sendToInternet(
216
261
  url,
217
262
  "", // No payload needed for GET request - params are in URL
218
- [](bool success, uint16_t httpStatus, String error) {
219
- if (success) {
220
- Serial.printf("✅ WhatsApp message sent! HTTP Status: %d\n", httpStatus);
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);
221
269
  } else {
222
- Serial.printf("❌ Failed to send WhatsApp: %s (HTTP: %d)\n", error.c_str(), httpStatus);
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;
223
287
  }
224
288
  },
225
289
  static_cast<uint8_t>(painlessmesh::gateway::GatewayPriority::PRIORITY_HIGH)
226
290
  );
227
-
291
+
228
292
  if (msgId > 0) {
229
- Serial.printf(" Message queued with ID: %u\n", msgId);
230
- } else {
231
- 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;
232
295
  }
296
+ // The callback reports why, from the scheduler, in a moment.
297
+ Serial.println(" Not handed to the mesh");
298
+ return false;
233
299
  }
234
300
 
235
301
  /**
@@ -243,7 +309,10 @@ void sendSensorDataToCloud() {
243
309
  // Simulate sensor readings using configured ranges
244
310
  float temperature = TEMP_MIN + random(0, (int)(TEMP_RANGE * 10)) / 10.0;
245
311
  float humidity = HUMIDITY_MIN + random(0, (int)(HUMIDITY_RANGE * 10)) / 10.0;
246
- float o2Level = O2_MIN + random(0, (int)(O2_RANGE * 10)) / 10.0;
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;
247
316
 
248
317
  // Create JSON payload
249
318
  String payload = "{";
@@ -257,11 +326,15 @@ void sendSensorDataToCloud() {
257
326
  Serial.println("\n📊 Sending sensor data to cloud...");
258
327
  Serial.printf(" Payload: %s\n", payload.c_str());
259
328
 
260
- // Check for alarm conditions using configured threshold
261
- if (o2Level < O2_ALARM_THRESHOLD) {
262
- // O2 level critical - send WhatsApp alert!
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) {
263
333
  String alertMsg = "⚠️ ALARM: O2 level critical at " + String(o2Level, 1) + " mg/L! Node: " + String(mesh.getNodeId());
264
- 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;
265
338
  }
266
339
 
267
340
  // Only send if Internet is available
@@ -299,7 +372,7 @@ void sendSensorDataToCloud() {
299
372
  /**
300
373
  * Send the startup notification once a gateway with Internet is known.
301
374
  *
302
- * Runs every 30 s from taskStartupAlert until it has handed the message to
375
+ * Runs every 30 s from taskStartupAlert until the message has been handed to
303
376
  * sendToInternet(), then disables itself.
304
377
  */
305
378
  void sendStartupAlert() {
@@ -307,8 +380,9 @@ void sendStartupAlert() {
307
380
  Serial.println("(startup WhatsApp waiting for a gateway with Internet; retrying in 30 s)");
308
381
  return;
309
382
  }
310
- sendAlertToWhatsApp(startupMsg);
311
- taskStartupAlert.disable();
383
+ if (sendAlertToWhatsApp(startupMsg)) {
384
+ taskStartupAlert.disable();
385
+ }
312
386
  }
313
387
 
314
388
  void receivedCallback(uint32_t from, String& msg) {
@@ -386,7 +460,11 @@ void setup() {
386
460
  // First attempt in 30 s, then every 30 s until a gateway with Internet is
387
461
  // known: a bridge can use its own uplink at once, a regular node has to
388
462
  // find the mesh first.
389
- startupMsg = "🚀 Node " + String(mesh.getNodeId()) + " started!";
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) + ")";
390
468
  Serial.println("Will send the startup WhatsApp as soon as a gateway with Internet is known (first try in 30 s)...\n");
391
469
  userScheduler.addTask(taskStartupAlert);
392
470
  taskStartupAlert.enableDelayed();
package/keywords.txt CHANGED
@@ -55,6 +55,7 @@ getPrimaryBridge KEYWORD2
55
55
  getPrimaryGateway KEYWORD2
56
56
  enableSendToInternet KEYWORD2
57
57
  sendToInternet KEYWORD2
58
+ tcpListening KEYWORD2
58
59
  enableMessageQueue KEYWORD2
59
60
  queueMessage KEYWORD2
60
61
  flushMessageQueue KEYWORD2
package/library.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/Alteriom/painlessMesh"
8
8
  },
9
- "version": "2.0.3",
9
+ "version": "2.1.1",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=Alteriom PainlessMesh
2
- version=2.0.3
2
+ version=2.1.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": "2.0.3",
3
+ "version": "2.1.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",
@@ -29,10 +29,10 @@
29
29
  /**
30
30
  * @brief AlteriomPainlessMesh library version information
31
31
  */
32
- #define ALTERIOM_PAINLESS_MESH_VERSION "2.0.3"
32
+ #define ALTERIOM_PAINLESS_MESH_VERSION "2.1.1"
33
33
  #define ALTERIOM_PAINLESS_MESH_VERSION_MAJOR 2
34
- #define ALTERIOM_PAINLESS_MESH_VERSION_MINOR 0
35
- #define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 3
34
+ #define ALTERIOM_PAINLESS_MESH_VERSION_MINOR 1
35
+ #define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 1
36
36
 
37
37
  /**
38
38
  * @brief Library description and usage information