@alteriom/painlessmesh 2.0.2 → 2.0.3
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 +90 -0
- package/CONTRIBUTING.md +15 -0
- package/README.md +4 -2
- package/RELEASE_GUIDE.md +11 -1
- package/examples/sendToInternet/README.md +3 -3
- package/examples/sendToInternet/sendToInternet.ino +58 -23
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/AlteriomPainlessMesh.h +2 -2
- package/src/arduino/wifi.hpp +144 -67
- package/src/painlessmesh/callback.hpp +7 -0
- package/src/painlessmesh/gateway.hpp +294 -14
- package/src/painlessmesh/logger.hpp +14 -1
- package/src/painlessmesh/mesh.hpp +167 -5
- package/src/painlessmesh/router.hpp +17 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,96 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.0.3] - 2026-09-11
|
|
11
|
+
|
|
12
|
+
`sendToInternet()` fixes from one user's WhatsApp integration (#450, #452,
|
|
13
|
+
#453), and one the hardware rig found while validating them. **Upgrade if a
|
|
14
|
+
node of yours is a bridge or relays to the Internet**: on 2.0.2 a bridge's own
|
|
15
|
+
sends were refused for 30 s after boot, a service that answered a refusal with
|
|
16
|
+
HTTP 201 was reported as delivered, a destination that does not resolve
|
|
17
|
+
stalled an ESP32 gateway on every attempt, and a bridge that rebooted as a
|
|
18
|
+
regular node swallowed every request its peers still sent it. Every fix was
|
|
19
|
+
reproduced first -- against the new HTTP test point, which CI now runs, or
|
|
20
|
+
over real TCP on the desktop -- and is covered by a row on the hardware farm.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **A bridge's own `sendToInternet()` was refused for the first 30 s after
|
|
25
|
+
`initAsBridge()`** (#450). The Internet health check is armed one line after
|
|
26
|
+
`stationManual()` re-issues `WiFi.begin()`, so its first probe ran while the
|
|
27
|
+
station was still associating and failed, and the next was a full interval
|
|
28
|
+
away. Every send from the bridge in that window fell through to the mesh
|
|
29
|
+
path and was refused with "No active mesh connections" -- #445 again, with
|
|
30
|
+
a 30 s hole instead of forever. `sendToInternet()` and the retry path now
|
|
31
|
+
spend one on-demand probe per interval when the flag says no, and the
|
|
32
|
+
station's got-IP event re-probes at once on a bridge or shared gateway.
|
|
33
|
+
- **A gateway reported a refusal as delivered, and every failure as a bare
|
|
34
|
+
number** (#450, #452). The gateway decided on the HTTP status alone and
|
|
35
|
+
discarded the response body. CallMeBot's WhatsApp API answers "Too many
|
|
36
|
+
requests" with HTTP 201 and HTTP 203, the same page under both, so a refused
|
|
37
|
+
message sent through a gateway on 2.0.2 could be reported as delivered; and
|
|
38
|
+
it answers HTTP 208 to messages it never delivers, which 2.0.2 reported as
|
|
39
|
+
"Ambiguous response" with nothing to say why. The gateway now reads the
|
|
40
|
+
start of the body (bounded to 512 bytes and 250 ms). Only 200, 201, 202 and
|
|
41
|
+
204 count as delivered, and not when the body says the service refused the
|
|
42
|
+
request; every other 2xx is a failure. Every failure reaches the origin
|
|
43
|
+
node's callback with a one-line, tag-free excerpt of the body -- for example
|
|
44
|
+
`HTTP 201: service refused the request: Oops! Too many requests...` or
|
|
45
|
+
`HTTP 208: not a delivery the gateway can confirm: ...` -- and is logged at
|
|
46
|
+
ERROR level, so a sketch on the default log levels sees what the service
|
|
47
|
+
said.
|
|
48
|
+
- **A destination whose name does not resolve stalled an ESP32 gateway on
|
|
49
|
+
every attempt** (#453). A failed lookup surfaced as `connection refused`
|
|
50
|
+
after the resolver's own patience, which this library cannot bound on ESP32,
|
|
51
|
+
and every attempt -- the origin node's retries and, since the bridge fix
|
|
52
|
+
above, the bridge's own sends -- paid it again, until relayed requests timed
|
|
53
|
+
out on their origin nodes. An ESP32 gateway now resolves the host once,
|
|
54
|
+
reports `DNS lookup failed for <host>`, and refuses that host for 60 s
|
|
55
|
+
without another lookup. ESP8266 is unchanged: its core bounds the lookup by
|
|
56
|
+
the HTTP timeout. The gateway blocking budget is unchanged.
|
|
57
|
+
- **A bridge that rebooted as a regular node swallowed every Internet request
|
|
58
|
+
routed to it.** Found on the hardware rig while validating this release. A
|
|
59
|
+
bridge that reboots, crashes, loses power or is reflashed announces nothing
|
|
60
|
+
-- only a bridge stepping down in-process sends `leaving` -- so its peers
|
|
61
|
+
kept it in their bridge list for the 60 s they trust a status, and could
|
|
62
|
+
prefer it over a live bridge on RSSI. A regular node had no handler for
|
|
63
|
+
gateway requests and dropped them without a reply; each sender waited out
|
|
64
|
+
its 30 s request timeout and reported "Request timed out". Every node now
|
|
65
|
+
answers a gateway request it cannot serve with
|
|
66
|
+
`Node <id> is not an Internet gateway`; the sender forgets that node as a
|
|
67
|
+
gateway and sends again at once through the next one, without spending a
|
|
68
|
+
retry. When it knows no other gateway, the request rides the ordinary retry
|
|
69
|
+
backoff -- on the rig the live bridge was advertised half a second later --
|
|
70
|
+
and fails with `No Internet gateway available: ...` only when none appears.
|
|
71
|
+
- **Mismatched log format arguments in `routePackage()`** (CodeQL
|
|
72
|
+
`cpp/wrong-type-format-argument`). A message that failed to parse was logged
|
|
73
|
+
with its `size_t` lengths through `%d`/`%u` -- the wrong width on 64-bit
|
|
74
|
+
hosts -- and with its `DeserializationError` object passed through `%u`,
|
|
75
|
+
undefined behaviour on every platform, in the ArduinoJson 7 path every
|
|
76
|
+
current build compiles as well as the legacy one CodeQL flagged.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- **The mock HTTP server is now the gateway test point.** It keeps a delivery
|
|
81
|
+
ledger (`GET /requests/{tag}`) and emulates CallMeBot's status quirks
|
|
82
|
+
(`GET /callmebot/whatsapp.php`, profile chosen by `apikey`). The desktop CI
|
|
83
|
+
job starts it and `PAINLESSMESH_TESTPOINT` points the suite at it, so the
|
|
84
|
+
library's delivery verdict is checked against what the service actually did,
|
|
85
|
+
not against a table of status codes. The Alteriom farm's gateway probe serves
|
|
86
|
+
the same routes.
|
|
87
|
+
- **`Mesh::sendGatewayAck()` is public and portable.** It moved from the ESP
|
|
88
|
+
gateway code into `painlessmesh::Mesh`, because every node now needs it to
|
|
89
|
+
answer a request it cannot serve.
|
|
90
|
+
- **`Log()` is format-checked in the desktop build.** The test build marks it
|
|
91
|
+
printf-like, so CI's `-Wall -Werror` rejects an argument that does not match
|
|
92
|
+
its specifier. Device builds are unchanged: on ESP-IDF 5 `uint32_t` is
|
|
93
|
+
`unsigned long`, and every `%u` of a node id would warn there.
|
|
94
|
+
- **`sendToInternet` example.** The startup WhatsApp retries every 30 s until
|
|
95
|
+
a gateway with Internet is known instead of firing once before a regular
|
|
96
|
+
node has joined, and the cloud endpoint moved to a `CLOUD_URL` define whose
|
|
97
|
+
placeholder is skipped with a message rather than reported as
|
|
98
|
+
`connection refused`.
|
|
99
|
+
|
|
10
100
|
## [2.0.2] - 2026-09-08
|
|
11
101
|
|
|
12
102
|
Two gateway defects that reached users on 2.0.1, both in code the desktop test
|
package/CONTRIBUTING.md
CHANGED
|
@@ -55,6 +55,21 @@ and OTA behaviour is validated on the Alteriom hardware-in-the-loop farm; a
|
|
|
55
55
|
maintainer runs it on a pull request by adding the `run-hil` label, and the
|
|
56
56
|
release gate is three consecutive clean runs of the whole suite.
|
|
57
57
|
|
|
58
|
+
### The HTTP test point
|
|
59
|
+
|
|
60
|
+
`test/mock-http-server/server.py` is the controlled Internet destination for
|
|
61
|
+
`sendToInternet()` tests at every level. It answers the way real services do,
|
|
62
|
+
including the ways they get it wrong (its CallMeBot emulation returns refusals
|
|
63
|
+
as HTTP 201 and 203, as the real API does), and it keeps a delivery ledger:
|
|
64
|
+
`GET /requests/{tag}` says whether the service actually accepted a request.
|
|
65
|
+
The desktop CI job starts one and exports `PAINLESSMESH_TESTPOINT`, and
|
|
66
|
+
`catch_issue450_testpoint_semantics` makes real HTTP requests to it and
|
|
67
|
+
requires the library's verdict to match the ledger. The farm's gateway probe
|
|
68
|
+
serves the same routes with the same record shape, so a hardware row and a
|
|
69
|
+
desktop scenario are measured against one source of truth. When a gateway bug
|
|
70
|
+
comes in, add the service behaviour that exposed it to the server first, then
|
|
71
|
+
the test that fails against it.
|
|
72
|
+
|
|
58
73
|
### Adding tests for new features
|
|
59
74
|
|
|
60
75
|
1. **Unit tests**: add to `test/catch/` for new components.
|
package/README.md
CHANGED
|
@@ -596,9 +596,11 @@ These are the message types used by applications built on painlessMesh:
|
|
|
596
596
|
- **Event Coordination** - Synchronized displays, distributed processing
|
|
597
597
|
- **Bridge Networks** - Connect mesh to WiFi/Internet/MQTT - [📖 Bridge Guide](BRIDGE_TO_INTERNET.md)
|
|
598
598
|
|
|
599
|
-
## Latest Release: v2.0.
|
|
599
|
+
## Latest Release: v2.0.3 (September 11, 2026)
|
|
600
600
|
|
|
601
|
-
|
|
601
|
+
**`sendToInternet()` fixes — upgrade if any node of yours is a bridge or relays to the Internet.** On 2.0.2 a bridge's own sends were refused with "No active mesh connections" for the first 30 s after `initAsBridge()`, because the health check's first probe ran while the station was still associating. The gateway also decided delivery on the HTTP status alone: CallMeBot answers a refusal with HTTP 201, so a message it refused could be reported as sent, and every failure reached the origin node as a bare number. The gateway now reads the start of the response body, counts only 200/201/202/204 without a refusing body as delivered, and hands every failure to your callback with the service's own words. A destination whose name does not resolve no longer stalls an ESP32 gateway on every attempt: it is refused for 60 s after one failed lookup. And a bridge that reboots, crashes or is reflashed as a regular node no longer swallows the requests its peers still send it for a minute: it answers that it is not a gateway, and the sender moves to the next one at once.
|
|
602
|
+
|
|
603
|
+
**2.0.2 — two gateway fixes.** On 2.0.1 a bridge could not reach the Internet through its own uplink at all (`initAsBridge()` never started the health checker that `sendToInternet()`'s local path depends on, so a bridge with no peer yet failed with "No active mesh connections"), and any request that failed *below* HTTP — refused, unresolvable, timed out — was reported to the origin node as `HTTP 65535`, a truncated `-1`, which also stopped it from being retried. Both are now asserted on the hardware rig, including a bridge with no mesh peer at all.
|
|
602
604
|
|
|
603
605
|
**2.0.1 — a packaging fix over 2.0.0, no library behaviour changed.** 2.0.0's installation instructions named a PlatformIO package that has no 2.0.0 (`alteriom/…` stops at 1.10.0; releases go out under `sparck75`), and its GitHub release carried no library archive because the upload was refused by an immutable release.
|
|
604
606
|
|
package/RELEASE_GUIDE.md
CHANGED
|
@@ -38,13 +38,23 @@ These files must always contain the same semantic version:
|
|
|
38
38
|
- `library.properties`
|
|
39
39
|
- `library.json`
|
|
40
40
|
- `package.json`
|
|
41
|
+
- `package-lock.json` (the root `version` and `packages[""].version`)
|
|
42
|
+
- `src/AlteriomPainlessMesh.h` (the version string and the MAJOR/MINOR/PATCH
|
|
43
|
+
defines a sketch compiles against)
|
|
44
|
+
- `doxygen/Doxyfile` (`PROJECT_NUMBER`, the number on every generated API page)
|
|
41
45
|
|
|
42
|
-
Use the repository script to change them together
|
|
46
|
+
Use the repository script to change them together; it updates all six and then
|
|
47
|
+
verifies they agree:
|
|
43
48
|
|
|
44
49
|
```bash
|
|
45
50
|
./scripts/bump-version.sh patch
|
|
51
|
+
./scripts/bump-version.sh patch 2.0.3 --yes # no prompt, for CI or an agent
|
|
46
52
|
```
|
|
47
53
|
|
|
54
|
+
`jq` is used when it is installed and awk otherwise, and both paths change only
|
|
55
|
+
the package's own version — never a dependency range. `test/ci/test_bump_version.sh`
|
|
56
|
+
holds that, and the Code Quality job runs it.
|
|
57
|
+
|
|
48
58
|
You may use `minor`, `major`, or an explicit version when the release plan
|
|
49
59
|
requires it. Version 2.0 patch releases must remain wire-compatible with the
|
|
50
60
|
2.0 protocol.
|
|
@@ -187,10 +187,10 @@ The callback provides `httpStatus` to indicate the result:
|
|
|
187
187
|
|
|
188
188
|
**FAILURE (success = false):**
|
|
189
189
|
- `203 Non-Authoritative Information` - **Cached/proxied response, NOT actual delivery**
|
|
190
|
-
- `4xx` - Client error (bad request, unauthorized, not found, etc.)
|
|
191
|
-
- `5xx` - Server error (service unavailable, gateway timeout, etc.)
|
|
192
190
|
|
|
193
|
-
⚠️ **
|
|
191
|
+
⚠️ **The status alone does not say whether WhatsApp got the message.** CallMeBot answers a refusal (for example "Too many requests") with HTTP 201 or HTTP 203, the same error page under both. The gateway therefore reads the start of the response body: a 2xx whose body says the service refused the request is reported as a failure, with the service's own words in the callback's `error` string, and HTTP 203 stays a failure because a proxy, not the WhatsApp API, may have answered. If your callback prints `HTTP 201: service refused the request: Oops! Too many requests...`, that is CallMeBot talking, and the message was **NOT delivered**. Likewise `HTTP 208: not a delivery the gateway can confirm: ...`: CallMeBot answers 208 to messages that never arrive (issues #450 and #452), so only 200, 201, 202 and 204 with a clean body are reported as sent, and the rest of the line is whatever the service said.
|
|
192
|
+
|
|
193
|
+
Also replace `CLOUD_URL` at the top of the sketch: the placeholder `api.example.com` does not resolve, and until you change it the sketch skips the cloud send and says so instead of reporting `connection refused` every minute.
|
|
194
194
|
|
|
195
195
|
## Files
|
|
196
196
|
|
|
@@ -74,6 +74,19 @@
|
|
|
74
74
|
#define WHATSAPP_PHONE "+1234567890" // Your phone number with country code
|
|
75
75
|
#define WHATSAPP_APIKEY "your_api_key" // Your Callmebot API key
|
|
76
76
|
|
|
77
|
+
// ============================================
|
|
78
|
+
// Cloud API Configuration
|
|
79
|
+
// ============================================
|
|
80
|
+
// The endpoint the periodic sensor readings are POSTed to. Replace it with
|
|
81
|
+
// your own; the placeholder below does not resolve, so until you do the
|
|
82
|
+
// sketch skips the cloud send and says so rather than reporting
|
|
83
|
+
// "connection refused" every minute (issue #450).
|
|
84
|
+
// Example endpoints:
|
|
85
|
+
// - ThingsBoard: "https://demo.thingsboard.io/api/v1/YOUR_TOKEN/telemetry"
|
|
86
|
+
// - AWS IoT: "https://YOUR_ENDPOINT.iot.us-east-1.amazonaws.com/topics/sensors"
|
|
87
|
+
// - Custom API: "https://api.yourserver.com/sensors/data"
|
|
88
|
+
#define CLOUD_URL "https://api.example.com/sensors"
|
|
89
|
+
|
|
77
90
|
// ============================================
|
|
78
91
|
// Sensor Simulation Configuration
|
|
79
92
|
// ============================================
|
|
@@ -115,6 +128,15 @@ String urlEncode(const String& str);
|
|
|
115
128
|
// Task to periodically send sensor data (simulated)
|
|
116
129
|
Task taskSendSensorData(60000, TASK_FOREVER, &sendSensorDataToCloud);
|
|
117
130
|
|
|
131
|
+
// The startup WhatsApp used to be a single 30 s one-shot. A regular node that
|
|
132
|
+
// took longer than that to find the mesh -- following a bridge to another
|
|
133
|
+
// channel takes about a minute -- fired it with no gateway in sight and never
|
|
134
|
+
// tried again (issue #450). It now retries until a gateway with Internet is
|
|
135
|
+
// known, then disables itself.
|
|
136
|
+
void sendStartupAlert();
|
|
137
|
+
Task taskStartupAlert(30000, TASK_FOREVER, &sendStartupAlert);
|
|
138
|
+
String startupMsg;
|
|
139
|
+
|
|
118
140
|
// ============================================
|
|
119
141
|
// URL Encoding Helper
|
|
120
142
|
// ============================================
|
|
@@ -184,13 +206,12 @@ void sendAlertToWhatsApp(String message) {
|
|
|
184
206
|
|
|
185
207
|
// Use sendToInternet() to route the request through a gateway
|
|
186
208
|
// The callback will be invoked when we get a response (or timeout)
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// it indicates a cached/proxied response, not actual delivery to WhatsApp.
|
|
209
|
+
//
|
|
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.
|
|
194
215
|
uint32_t msgId = mesh.sendToInternet(
|
|
195
216
|
url,
|
|
196
217
|
"", // No payload needed for GET request - params are in URL
|
|
@@ -249,14 +270,13 @@ void sendSensorDataToCloud() {
|
|
|
249
270
|
return;
|
|
250
271
|
}
|
|
251
272
|
|
|
252
|
-
// Send to cloud API
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
273
|
+
// Send to the cloud API configured at the top of the sketch
|
|
274
|
+
String cloudUrl = CLOUD_URL;
|
|
275
|
+
if (cloudUrl.indexOf("example.com") >= 0) {
|
|
276
|
+
Serial.println(" (CLOUD_URL is still the placeholder; set your endpoint to send readings)");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
260
280
|
uint32_t msgId = mesh.sendToInternet(
|
|
261
281
|
cloudUrl,
|
|
262
282
|
payload,
|
|
@@ -276,6 +296,21 @@ void sendSensorDataToCloud() {
|
|
|
276
296
|
// Mesh Callbacks
|
|
277
297
|
// ============================================
|
|
278
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Send the startup notification once a gateway with Internet is known.
|
|
301
|
+
*
|
|
302
|
+
* Runs every 30 s from taskStartupAlert until it has handed the message to
|
|
303
|
+
* sendToInternet(), then disables itself.
|
|
304
|
+
*/
|
|
305
|
+
void sendStartupAlert() {
|
|
306
|
+
if (!mesh.hasInternetConnection()) {
|
|
307
|
+
Serial.println("(startup WhatsApp waiting for a gateway with Internet; retrying in 30 s)");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
sendAlertToWhatsApp(startupMsg);
|
|
311
|
+
taskStartupAlert.disable();
|
|
312
|
+
}
|
|
313
|
+
|
|
279
314
|
void receivedCallback(uint32_t from, String& msg) {
|
|
280
315
|
Serial.printf("📨 Received from %u: %s\n", from, msg.c_str());
|
|
281
316
|
}
|
|
@@ -347,14 +382,14 @@ void setup() {
|
|
|
347
382
|
Serial.printf("Is Bridge: %s\n", mesh.isBridge() ? "YES" : "NO");
|
|
348
383
|
Serial.println("================================================\n");
|
|
349
384
|
|
|
350
|
-
// Send a startup notification via WhatsApp (demonstrates sendToInternet)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
385
|
+
// Send a startup notification via WhatsApp (demonstrates sendToInternet).
|
|
386
|
+
// First attempt in 30 s, then every 30 s until a gateway with Internet is
|
|
387
|
+
// known: a bridge can use its own uplink at once, a regular node has to
|
|
388
|
+
// find the mesh first.
|
|
389
|
+
startupMsg = "🚀 Node " + String(mesh.getNodeId()) + " started!";
|
|
390
|
+
Serial.println("Will send the startup WhatsApp as soon as a gateway with Internet is known (first try in 30 s)...\n");
|
|
391
|
+
userScheduler.addTask(taskStartupAlert);
|
|
392
|
+
taskStartupAlert.enableDelayed();
|
|
358
393
|
}
|
|
359
394
|
|
|
360
395
|
// ============================================
|
package/library.json
CHANGED
package/library.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name=Alteriom PainlessMesh
|
|
2
|
-
version=2.0.
|
|
2
|
+
version=2.0.3
|
|
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.0.3",
|
|
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.0.3"
|
|
33
33
|
#define ALTERIOM_PAINLESS_MESH_VERSION_MAJOR 2
|
|
34
34
|
#define ALTERIOM_PAINLESS_MESH_VERSION_MINOR 0
|
|
35
|
-
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH
|
|
35
|
+
#define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 3
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @brief Library description and usage information
|
package/src/arduino/wifi.hpp
CHANGED
|
@@ -2721,54 +2721,53 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2721
2721
|
/**
|
|
2722
2722
|
* Helper method to send gateway acknowledgment
|
|
2723
2723
|
*/
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
std::shared_ptr<Connection> ingressConnection = nullptr) {
|
|
2728
|
-
using namespace logger;
|
|
2729
|
-
|
|
2730
|
-
gateway::GatewayAckPackage ack;
|
|
2731
|
-
ack.from = this->nodeId;
|
|
2732
|
-
ack.dest = request.originNode;
|
|
2733
|
-
ack.messageId = request.messageId;
|
|
2734
|
-
ack.originNode = request.originNode;
|
|
2735
|
-
ack.success = success;
|
|
2736
|
-
ack.httpStatus = httpStatus;
|
|
2737
|
-
ack.error = error;
|
|
2738
|
-
ack.timestamp = this->getNodeTime();
|
|
2739
|
-
|
|
2740
|
-
if (request.originNode == this->nodeId) {
|
|
2741
|
-
protocol::Variant variant(&ack);
|
|
2742
|
-
this->callbackList.execute(protocol::GATEWAY_ACK, variant, nullptr, 0);
|
|
2743
|
-
Log(COMMUNICATION,
|
|
2744
|
-
"Completed local GATEWAY_ACK (success=%d, http=%d)\n", success,
|
|
2745
|
-
httpStatus);
|
|
2746
|
-
return;
|
|
2747
|
-
}
|
|
2724
|
+
/** Hosts that recently failed to resolve; fed on ESP32 only, see the
|
|
2725
|
+
* GATEWAY_DATA handler. */
|
|
2726
|
+
gateway::NegativeDnsCache negativeDns;
|
|
2748
2727
|
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2728
|
+
#if defined(ESP32) || defined(ESP8266)
|
|
2729
|
+
/**
|
|
2730
|
+
* Read the start of an HTTP response body, bounded in bytes and in time
|
|
2731
|
+
*
|
|
2732
|
+
* The gateway used to discard the body, so a service that answers a refusal
|
|
2733
|
+
* with a 2xx (CallMeBot, issue #450) was reported as delivered, and a
|
|
2734
|
+
* failure reached the origin node as a bare status. When the length is
|
|
2735
|
+
* known and small the whole body is taken; otherwise up to maxBytes are
|
|
2736
|
+
* read from the stream, waiting at most GATEWAY_RESPONSE_HEAD_TIMEOUT_MS,
|
|
2737
|
+
* which keeps a large or chunked page from eating the heap or the
|
|
2738
|
+
* cooperative scheduler.
|
|
2739
|
+
*/
|
|
2740
|
+
static TSTRING readResponseHead(HTTPClient& http, size_t maxBytes) {
|
|
2741
|
+
const int size = http.getSize();
|
|
2742
|
+
if (size >= 0 && static_cast<size_t>(size) <= maxBytes) {
|
|
2743
|
+
return http.getString();
|
|
2744
|
+
}
|
|
2745
|
+
TSTRING head;
|
|
2746
|
+
WiFiClient* stream = http.getStreamPtr();
|
|
2747
|
+
if (stream == nullptr) return head;
|
|
2748
|
+
const uint32_t deadline =
|
|
2749
|
+
millis() + gateway::GATEWAY_RESPONSE_HEAD_TIMEOUT_MS;
|
|
2750
|
+
while (head.length() < maxBytes &&
|
|
2751
|
+
static_cast<int32_t>(deadline - millis()) > 0) {
|
|
2752
|
+
int available = stream->available();
|
|
2753
|
+
if (available <= 0) {
|
|
2754
|
+
if (!stream->connected()) break;
|
|
2755
|
+
delay(1);
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
while (available-- > 0 && head.length() < maxBytes) {
|
|
2759
|
+
const int c = stream->read();
|
|
2760
|
+
if (c < 0) break;
|
|
2761
|
+
head += static_cast<char>(c);
|
|
2762
|
+
}
|
|
2770
2763
|
}
|
|
2764
|
+
return head;
|
|
2771
2765
|
}
|
|
2766
|
+
#endif
|
|
2767
|
+
|
|
2768
|
+
// sendGatewayAck() is portable and lives in painlessmesh::Mesh
|
|
2769
|
+
// (mesh.hpp): every node needs it, not only a gateway, to answer a
|
|
2770
|
+
// request it cannot serve.
|
|
2772
2771
|
|
|
2773
2772
|
/**
|
|
2774
2773
|
* Initialize gateway Internet handler
|
|
@@ -2873,6 +2872,49 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2873
2872
|
}
|
|
2874
2873
|
|
|
2875
2874
|
#if defined(ESP32) || defined(ESP8266)
|
|
2875
|
+
#ifdef ESP32
|
|
2876
|
+
// A destination whose name failed to resolve is refused for
|
|
2877
|
+
// GATEWAY_DNS_NEGATIVE_TTL_MS without another lookup (issue #453).
|
|
2878
|
+
// HTTPClient reports a failed lookup as "connection refused" after
|
|
2879
|
+
// the resolver's own patience -- on ESP32 not boundable by this
|
|
2880
|
+
// library -- and it did so on every attempt: the origin node's
|
|
2881
|
+
// retries and, since #451, the bridge's own sends each paid it, and
|
|
2882
|
+
// the scheduler stalled long enough for relayed requests to time
|
|
2883
|
+
// out on their origin nodes. Resolving here, once, names the real
|
|
2884
|
+
// failure and caps the stall to one lookup per TTL per host.
|
|
2885
|
+
// ESP32 only: the ESP8266 core bounds HTTPClient's own lookup by the
|
|
2886
|
+
// HTTP timeout, and a second bounded wait would not fit the
|
|
2887
|
+
// blocking budget.
|
|
2888
|
+
const TSTRING host = gateway::hostFromUrl(pkg.destination);
|
|
2889
|
+
if (host.length() > 0 && !gateway::looksLikeIpLiteral(host)) {
|
|
2890
|
+
uint32_t failedAgoMs = 0;
|
|
2891
|
+
if (negativeDns.isFailing(host, millis(), &failedAgoMs)) {
|
|
2892
|
+
char dnsBuf[160];
|
|
2893
|
+
snprintf(dnsBuf, sizeof(dnsBuf),
|
|
2894
|
+
"DNS lookup for %s failed %lus ago; not retried for "
|
|
2895
|
+
"another %lus",
|
|
2896
|
+
host.c_str(),
|
|
2897
|
+
static_cast<unsigned long>(failedAgoMs / 1000),
|
|
2898
|
+
static_cast<unsigned long>(
|
|
2899
|
+
(gateway::GATEWAY_DNS_NEGATIVE_TTL_MS - failedAgoMs) /
|
|
2900
|
+
1000));
|
|
2901
|
+
Log(ERROR, "%s\n", dnsBuf);
|
|
2902
|
+
finish(false, 0, TSTRING(dnsBuf));
|
|
2903
|
+
return true;
|
|
2904
|
+
}
|
|
2905
|
+
IPAddress resolved;
|
|
2906
|
+
if (WiFi.hostByName(host.c_str(), resolved) != 1) {
|
|
2907
|
+
negativeDns.remember(host, millis());
|
|
2908
|
+
char dnsBuf[160];
|
|
2909
|
+
snprintf(dnsBuf, sizeof(dnsBuf), "DNS lookup failed for %s",
|
|
2910
|
+
host.c_str());
|
|
2911
|
+
Log(ERROR, "%s\n", dnsBuf);
|
|
2912
|
+
finish(false, 0, TSTRING(dnsBuf));
|
|
2913
|
+
return true;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
#endif // ESP32
|
|
2917
|
+
|
|
2876
2918
|
// Make HTTP/HTTPS request
|
|
2877
2919
|
HTTPClient http;
|
|
2878
2920
|
http.setTimeout(GATEWAY_HTTP_TIMEOUT_MS);
|
|
@@ -2925,51 +2967,70 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2925
2967
|
httpCode = http.GET();
|
|
2926
2968
|
}
|
|
2927
2969
|
|
|
2928
|
-
//
|
|
2929
|
-
//
|
|
2930
|
-
//
|
|
2931
|
-
//
|
|
2932
|
-
//
|
|
2933
|
-
// delivery to the destination service (e.g., WhatsApp API).
|
|
2970
|
+
// A 2xx is the origin's acceptance, except 203 (a proxy transformed
|
|
2971
|
+
// the response) and except when the body says the service refused
|
|
2972
|
+
// the request: CallMeBot answers "Too many requests" under 201 and
|
|
2973
|
+
// 203 (issue #450). The body is therefore read -- bounded -- before
|
|
2974
|
+
// classifying, and it is what the origin node is told on failure.
|
|
2934
2975
|
//
|
|
2935
2976
|
// 3xx redirects are not automatically followed.
|
|
2936
2977
|
//
|
|
2937
2978
|
// The classification lives in painlessmesh::gateway so the desktop
|
|
2938
2979
|
// test suite can exercise it; this file is ESP-only and never
|
|
2939
2980
|
// compiled by the Catch2 build.
|
|
2940
|
-
|
|
2981
|
+
TSTRING responseHead;
|
|
2982
|
+
if (httpCode > 0) {
|
|
2983
|
+
responseHead =
|
|
2984
|
+
readResponseHead(http, gateway::GATEWAY_RESPONSE_HEAD_BYTES);
|
|
2985
|
+
}
|
|
2986
|
+
const auto outcome =
|
|
2987
|
+
gateway::classifyHttpResult(httpCode, responseHead);
|
|
2941
2988
|
success = outcome.success;
|
|
2942
2989
|
|
|
2943
2990
|
if (!outcome.transportError) {
|
|
2991
|
+
char errorBuf[192];
|
|
2992
|
+
const char* reason = outcome.reason.c_str();
|
|
2993
|
+
const char* sep = outcome.reason.length() > 0 ? ": " : "";
|
|
2944
2994
|
if (success) {
|
|
2945
2995
|
Log(COMMUNICATION, "HTTP request completed: code=%d\n", httpCode);
|
|
2946
|
-
} else if (
|
|
2947
|
-
//
|
|
2948
|
-
//
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2996
|
+
} else if (outcome.refusedByBody) {
|
|
2997
|
+
// Success-class status, refusing body. Not retryable: the
|
|
2998
|
+
// service answered, and the origin node gets its words.
|
|
2999
|
+
snprintf(errorBuf, sizeof(errorBuf),
|
|
3000
|
+
"HTTP %d: service refused the request%s%s", httpCode,
|
|
3001
|
+
sep, reason);
|
|
3002
|
+
error = TSTRING(errorBuf);
|
|
3003
|
+
Log(ERROR, "HTTP request refused by the service: %s\n", errorBuf);
|
|
3004
|
+
} else if (outcome.unverifiedStatus) {
|
|
3005
|
+
// A 2xx outside 200/201/202/204. CallMeBot answers 208 to a
|
|
3006
|
+
// message that never arrives (issue #452), so this is a
|
|
3007
|
+
// failure, and the body -- the only place the service says
|
|
3008
|
+
// what it did -- goes to the origin node and to the log at
|
|
3009
|
+
// ERROR level, where a sketch running the default levels
|
|
3010
|
+
// sees it. 203 stays retryable; the rest are final.
|
|
3011
|
+
snprintf(errorBuf, sizeof(errorBuf),
|
|
3012
|
+
"HTTP %d: not a delivery the gateway can confirm%s%s",
|
|
3013
|
+
httpCode, sep, reason);
|
|
2953
3014
|
error = TSTRING(errorBuf);
|
|
2954
|
-
Log(
|
|
3015
|
+
Log(ERROR, "HTTP response unverified: %s\n", errorBuf);
|
|
2955
3016
|
} else if (httpCode >= 500 && httpCode < 600) {
|
|
2956
3017
|
// 5xx server errors are retryable, log at COMMUNICATION level
|
|
2957
|
-
|
|
2958
|
-
|
|
3018
|
+
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
|
|
3019
|
+
reason);
|
|
2959
3020
|
error = TSTRING(errorBuf);
|
|
2960
3021
|
Log(COMMUNICATION, "HTTP server error: code=%d (will retry)\n", httpCode);
|
|
2961
3022
|
} else if (httpCode == 429) {
|
|
2962
3023
|
// HTTP 429 rate limit is retryable, log at COMMUNICATION level
|
|
2963
|
-
|
|
2964
|
-
|
|
3024
|
+
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
|
|
3025
|
+
reason);
|
|
2965
3026
|
error = TSTRING(errorBuf);
|
|
2966
3027
|
Log(COMMUNICATION, "HTTP rate limit: code=%d (will retry)\n", httpCode);
|
|
2967
3028
|
} else {
|
|
2968
3029
|
// 1xx, 3xx, 4xx (except 429) - non-retryable, log at ERROR level
|
|
2969
|
-
|
|
2970
|
-
|
|
3030
|
+
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
|
|
3031
|
+
reason);
|
|
2971
3032
|
error = TSTRING(errorBuf);
|
|
2972
|
-
Log(ERROR, "HTTP request failed:
|
|
3033
|
+
Log(ERROR, "HTTP request failed: %s\n", errorBuf);
|
|
2973
3034
|
}
|
|
2974
3035
|
} else {
|
|
2975
3036
|
// Network errors (httpCode <= 0) are retryable but indicate serious issues
|
|
@@ -2993,6 +3054,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2993
3054
|
});
|
|
2994
3055
|
}
|
|
2995
3056
|
|
|
3057
|
+
/**
|
|
3058
|
+
* The station just got an IP. On a node that runs the Internet health
|
|
3059
|
+
* check -- a bridge or a shared gateway, whose station is the manual router
|
|
3060
|
+
* link -- the last probe most likely ran during association and failed,
|
|
3061
|
+
* and the next is a full interval away (issue #450). Probe again now, from
|
|
3062
|
+
* the scheduler rather than the Wi-Fi event task, so hasLocalInternet()
|
|
3063
|
+
* and the bridge's own sends stop waiting out the interval. A regular
|
|
3064
|
+
* node's mesh station never enables the check and is left alone.
|
|
3065
|
+
*/
|
|
3066
|
+
void probeUplinkAfterAssociation() {
|
|
3067
|
+
if (!this->isInternetHealthCheckEnabled() || !stationScan.manual) return;
|
|
3068
|
+
this->addTask([this]() { this->checkInternetNow(); });
|
|
3069
|
+
}
|
|
3070
|
+
|
|
2996
3071
|
void eventHandleInit() {
|
|
2997
3072
|
using namespace logger;
|
|
2998
3073
|
// Where the station scan learns the channel the mesh is rooted on.
|
|
@@ -3101,6 +3176,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
3101
3176
|
this->stationScan.stationAttemptOver();
|
|
3102
3177
|
this->stationScan.stationUp();
|
|
3103
3178
|
this->tcpConnect(); // Connect to TCP port
|
|
3179
|
+
this->probeUplinkAfterAssociation();
|
|
3104
3180
|
this->semaphoreGive();
|
|
3105
3181
|
}
|
|
3106
3182
|
},
|
|
@@ -3137,6 +3213,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
3137
3213
|
this->stationScan.stationAttemptOver();
|
|
3138
3214
|
this->stationScan.stationUp();
|
|
3139
3215
|
this->tcpConnect(); // Connect to TCP port
|
|
3216
|
+
this->probeUplinkAfterAssociation();
|
|
3140
3217
|
});
|
|
3141
3218
|
#endif // ESP32
|
|
3142
3219
|
return;
|
|
@@ -76,6 +76,13 @@ class PackageCallbackList {
|
|
|
76
76
|
return size;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** How many callbacks are registered for one package id. */
|
|
80
|
+
size_t count(int id) {
|
|
81
|
+
auto generation = clearPending ? pendingCallbackMap : callbackMap;
|
|
82
|
+
auto it = generation->find(id);
|
|
83
|
+
return it == generation->end() ? 0 : it->second.size();
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
void clear() {
|
|
80
87
|
if (dispatchDepth > 0) {
|
|
81
88
|
clearPending = true;
|