@alteriom/painlessmesh 2.0.1 → 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 +145 -0
- package/CONTRIBUTING.md +15 -0
- package/README.md +6 -2
- package/RELEASE_GUIDE.md +27 -5
- 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 +178 -77
- package/src/painlessmesh/callback.hpp +7 -0
- package/src/painlessmesh/gateway.hpp +335 -7
- package/src/painlessmesh/logger.hpp +14 -1
- package/src/painlessmesh/mesh.hpp +232 -20
- package/src/painlessmesh/router.hpp +17 -7
package/src/arduino/wifi.hpp
CHANGED
|
@@ -492,6 +492,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
492
492
|
// Step 6: Setup gateway Internet handler
|
|
493
493
|
initGatewayInternetHandler();
|
|
494
494
|
|
|
495
|
+
// Step 7: Start the health checker that drives hasLocalInternet().
|
|
496
|
+
// Without it that flag stays false for the life of the node, so the
|
|
497
|
+
// local short-circuit in sendToInternet() never fires and a bridge
|
|
498
|
+
// asking its own uplink fell through to the mesh-routing path -- which
|
|
499
|
+
// fails with "No active mesh connections" whenever the bridge has no
|
|
500
|
+
// peer yet, even though the router link is up (issue #445).
|
|
501
|
+
// initAsSharedGateway() has always done this; initAsBridge() did not.
|
|
502
|
+
//
|
|
503
|
+
// No configureInternetHealthCheck() call here: a bridge has no
|
|
504
|
+
// SharedGatewayConfig, the checker's own defaults (8.8.8.8:53, 30 s,
|
|
505
|
+
// 5 s) already match that struct's, and configuring would clobber any
|
|
506
|
+
// setInternetCheckTarget() the sketch made before init.
|
|
507
|
+
enableInternetHealthCheck();
|
|
508
|
+
|
|
495
509
|
Log(STARTUP, "=== Bridge Mode Active ===\n");
|
|
496
510
|
Log(STARTUP, " Mesh SSID: %s\n", meshSSID.c_str());
|
|
497
511
|
Log(STARTUP, " Mesh Channel: %d%s\n", detectedChannel,
|
|
@@ -2707,54 +2721,53 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2707
2721
|
/**
|
|
2708
2722
|
* Helper method to send gateway acknowledgment
|
|
2709
2723
|
*/
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
std::shared_ptr<Connection> ingressConnection = nullptr) {
|
|
2714
|
-
using namespace logger;
|
|
2715
|
-
|
|
2716
|
-
gateway::GatewayAckPackage ack;
|
|
2717
|
-
ack.from = this->nodeId;
|
|
2718
|
-
ack.dest = request.originNode;
|
|
2719
|
-
ack.messageId = request.messageId;
|
|
2720
|
-
ack.originNode = request.originNode;
|
|
2721
|
-
ack.success = success;
|
|
2722
|
-
ack.httpStatus = httpStatus;
|
|
2723
|
-
ack.error = error;
|
|
2724
|
-
ack.timestamp = this->getNodeTime();
|
|
2725
|
-
|
|
2726
|
-
if (request.originNode == this->nodeId) {
|
|
2727
|
-
protocol::Variant variant(&ack);
|
|
2728
|
-
this->callbackList.execute(protocol::GATEWAY_ACK, variant, nullptr, 0);
|
|
2729
|
-
Log(COMMUNICATION,
|
|
2730
|
-
"Completed local GATEWAY_ACK (success=%d, http=%d)\n", success,
|
|
2731
|
-
httpStatus);
|
|
2732
|
-
return;
|
|
2733
|
-
}
|
|
2724
|
+
/** Hosts that recently failed to resolve; fed on ESP32 only, see the
|
|
2725
|
+
* GATEWAY_DATA handler. */
|
|
2726
|
+
gateway::NegativeDnsCache negativeDns;
|
|
2734
2727
|
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
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
|
+
}
|
|
2756
2763
|
}
|
|
2764
|
+
return head;
|
|
2757
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.
|
|
2758
2771
|
|
|
2759
2772
|
/**
|
|
2760
2773
|
* Initialize gateway Internet handler
|
|
@@ -2859,12 +2872,63 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2859
2872
|
}
|
|
2860
2873
|
|
|
2861
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
|
+
|
|
2862
2918
|
// Make HTTP/HTTPS request
|
|
2863
2919
|
HTTPClient http;
|
|
2864
2920
|
http.setTimeout(GATEWAY_HTTP_TIMEOUT_MS);
|
|
2865
2921
|
|
|
2866
2922
|
bool success = false;
|
|
2867
|
-
|
|
2923
|
+
// HTTPClient returns an int: a positive value is an HTTP status
|
|
2924
|
+
// code, a negative one is its own transport error (HTTPC_ERROR_*,
|
|
2925
|
+
// -1 for a refused connection, -11 for a read timeout, ...). This
|
|
2926
|
+
// was a uint16_t, which wrapped -1 to 65535 -- a value that passes
|
|
2927
|
+
// `httpCode > 0`, so every transport failure was reported to the
|
|
2928
|
+
// origin node as "HTTP 65535" (issue #446), the errorToString()
|
|
2929
|
+
// branch below was unreachable, and handleGatewayAck() classified a
|
|
2930
|
+
// transient network failure as a non-retryable HTTP status.
|
|
2931
|
+
int httpCode = 0;
|
|
2868
2932
|
TSTRING error = "";
|
|
2869
2933
|
|
|
2870
2934
|
#ifdef ESP8266
|
|
@@ -2903,50 +2967,70 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2903
2967
|
httpCode = http.GET();
|
|
2904
2968
|
}
|
|
2905
2969
|
|
|
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.
|
|
2975
|
+
//
|
|
2976
|
+
// 3xx redirects are not automatically followed.
|
|
2977
|
+
//
|
|
2978
|
+
// The classification lives in painlessmesh::gateway so the desktop
|
|
2979
|
+
// test suite can exercise it; this file is ESP-only and never
|
|
2980
|
+
// compiled by the Catch2 build.
|
|
2981
|
+
TSTRING responseHead;
|
|
2906
2982
|
if (httpCode > 0) {
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
success = (httpCode == 200 || httpCode == 201 ||
|
|
2919
|
-
httpCode == 202 || httpCode == 204);
|
|
2920
|
-
|
|
2983
|
+
responseHead =
|
|
2984
|
+
readResponseHead(http, gateway::GATEWAY_RESPONSE_HEAD_BYTES);
|
|
2985
|
+
}
|
|
2986
|
+
const auto outcome =
|
|
2987
|
+
gateway::classifyHttpResult(httpCode, responseHead);
|
|
2988
|
+
success = outcome.success;
|
|
2989
|
+
|
|
2990
|
+
if (!outcome.transportError) {
|
|
2991
|
+
char errorBuf[192];
|
|
2992
|
+
const char* reason = outcome.reason.c_str();
|
|
2993
|
+
const char* sep = outcome.reason.length() > 0 ? ": " : "";
|
|
2921
2994
|
if (success) {
|
|
2922
2995
|
Log(COMMUNICATION, "HTTP request completed: code=%d\n", httpCode);
|
|
2923
|
-
} else if (
|
|
2924
|
-
//
|
|
2925
|
-
//
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
httpCode);
|
|
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);
|
|
2930
3002
|
error = TSTRING(errorBuf);
|
|
2931
|
-
Log(
|
|
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);
|
|
3014
|
+
error = TSTRING(errorBuf);
|
|
3015
|
+
Log(ERROR, "HTTP response unverified: %s\n", errorBuf);
|
|
2932
3016
|
} else if (httpCode >= 500 && httpCode < 600) {
|
|
2933
3017
|
// 5xx server errors are retryable, log at COMMUNICATION level
|
|
2934
|
-
|
|
2935
|
-
|
|
3018
|
+
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
|
|
3019
|
+
reason);
|
|
2936
3020
|
error = TSTRING(errorBuf);
|
|
2937
3021
|
Log(COMMUNICATION, "HTTP server error: code=%d (will retry)\n", httpCode);
|
|
2938
3022
|
} else if (httpCode == 429) {
|
|
2939
3023
|
// HTTP 429 rate limit is retryable, log at COMMUNICATION level
|
|
2940
|
-
|
|
2941
|
-
|
|
3024
|
+
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
|
|
3025
|
+
reason);
|
|
2942
3026
|
error = TSTRING(errorBuf);
|
|
2943
3027
|
Log(COMMUNICATION, "HTTP rate limit: code=%d (will retry)\n", httpCode);
|
|
2944
3028
|
} else {
|
|
2945
3029
|
// 1xx, 3xx, 4xx (except 429) - non-retryable, log at ERROR level
|
|
2946
|
-
|
|
2947
|
-
|
|
3030
|
+
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
|
|
3031
|
+
reason);
|
|
2948
3032
|
error = TSTRING(errorBuf);
|
|
2949
|
-
Log(ERROR, "HTTP request failed:
|
|
3033
|
+
Log(ERROR, "HTTP request failed: %s\n", errorBuf);
|
|
2950
3034
|
}
|
|
2951
3035
|
} else {
|
|
2952
3036
|
// Network errors (httpCode <= 0) are retryable but indicate serious issues
|
|
@@ -2957,8 +3041,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2957
3041
|
|
|
2958
3042
|
http.end();
|
|
2959
3043
|
|
|
2960
|
-
// Send acknowledgment back
|
|
2961
|
-
|
|
3044
|
+
// Send acknowledgment back. Transport errors carry status 0, the
|
|
3045
|
+
// value handleGatewayAck() treats as a retryable network error.
|
|
3046
|
+
finish(success, outcome.ackStatus, error);
|
|
2962
3047
|
#else
|
|
2963
3048
|
// Non-ESP platform - send error
|
|
2964
3049
|
finish(false, 0, "HTTP client not available on this platform");
|
|
@@ -2969,6 +3054,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2969
3054
|
});
|
|
2970
3055
|
}
|
|
2971
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
|
+
|
|
2972
3071
|
void eventHandleInit() {
|
|
2973
3072
|
using namespace logger;
|
|
2974
3073
|
// Where the station scan learns the channel the mesh is rooted on.
|
|
@@ -3077,6 +3176,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
3077
3176
|
this->stationScan.stationAttemptOver();
|
|
3078
3177
|
this->stationScan.stationUp();
|
|
3079
3178
|
this->tcpConnect(); // Connect to TCP port
|
|
3179
|
+
this->probeUplinkAfterAssociation();
|
|
3080
3180
|
this->semaphoreGive();
|
|
3081
3181
|
}
|
|
3082
3182
|
},
|
|
@@ -3113,6 +3213,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
3113
3213
|
this->stationScan.stationAttemptOver();
|
|
3114
3214
|
this->stationScan.stationUp();
|
|
3115
3215
|
this->tcpConnect(); // Connect to TCP port
|
|
3216
|
+
this->probeUplinkAfterAssociation();
|
|
3116
3217
|
});
|
|
3117
3218
|
#endif // ESP32
|
|
3118
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;
|