@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
package/src/arduino/wifi.hpp
CHANGED
|
@@ -2721,54 +2721,56 @@ 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
|
+
* The start and the end of the response body (gateway::ResponseExcerpt),
|
|
2731
|
+
* read from the stream for at most GATEWAY_RESPONSE_HEAD_TIMEOUT_MS and
|
|
2732
|
+
* GATEWAY_RESPONSE_SCAN_BYTES. A body that fits whole is read whole. A
|
|
2733
|
+
* chunked body is read through gateway::ChunkedBodyDecoder: the raw stream
|
|
2734
|
+
* carries the framing, which getString() would remove but only by keeping
|
|
2735
|
+
* the whole body.
|
|
2736
|
+
*/
|
|
2737
|
+
static TSTRING readResponseExcerpt(HTTPClient& http) {
|
|
2738
|
+
const int size = http.getSize();
|
|
2739
|
+
if (size >= 0 &&
|
|
2740
|
+
static_cast<size_t>(size) <=
|
|
2741
|
+
gateway::GATEWAY_RESPONSE_HEAD_BYTES + gateway::GATEWAY_RESPONSE_TAIL_BYTES) {
|
|
2742
|
+
return http.getString();
|
|
2743
|
+
}
|
|
2744
|
+
gateway::ResponseExcerpt excerpt;
|
|
2745
|
+
WiFiClient* stream = http.getStreamPtr();
|
|
2746
|
+
if (stream == nullptr) return excerpt.text();
|
|
2747
|
+
const bool chunked =
|
|
2748
|
+
size < 0 && gateway::transferEncodingIsChunked(http.header("Transfer-Encoding"));
|
|
2749
|
+
gateway::ChunkedBodyDecoder decoder(excerpt);
|
|
2750
|
+
const uint32_t deadline =
|
|
2751
|
+
millis() + gateway::GATEWAY_RESPONSE_HEAD_TIMEOUT_MS;
|
|
2752
|
+
bool more = true;
|
|
2753
|
+
while (more && static_cast<int32_t>(deadline - millis()) > 0) {
|
|
2754
|
+
int available = stream->available();
|
|
2755
|
+
if (available <= 0) {
|
|
2756
|
+
if (!stream->connected()) break;
|
|
2757
|
+
delay(1);
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
while (more && available-- > 0) {
|
|
2761
|
+
const int c = stream->read();
|
|
2762
|
+
if (c < 0) break;
|
|
2763
|
+
more = chunked ? decoder.add(static_cast<char>(c))
|
|
2764
|
+
: excerpt.add(static_cast<char>(c));
|
|
2765
|
+
}
|
|
2770
2766
|
}
|
|
2767
|
+
return excerpt.text();
|
|
2771
2768
|
}
|
|
2769
|
+
#endif
|
|
2770
|
+
|
|
2771
|
+
// sendGatewayAck() is portable and lives in painlessmesh::Mesh
|
|
2772
|
+
// (mesh.hpp): every node needs it, not only a gateway, to answer a
|
|
2773
|
+
// request it cannot serve.
|
|
2772
2774
|
|
|
2773
2775
|
/**
|
|
2774
2776
|
* Initialize gateway Internet handler
|
|
@@ -2837,8 +2839,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2837
2839
|
// that genuinely stopped answering NODE_SYNC still gets reaped even
|
|
2838
2840
|
// under continuous gateway traffic from other peers.
|
|
2839
2841
|
const auto blockingStartedMs = millis();
|
|
2842
|
+
// `retryable` is the gateway's word to the origin node on whether
|
|
2843
|
+
// the identical request may be sent again: 0 whenever it may have
|
|
2844
|
+
// reached the server, so a retry cannot deliver it twice.
|
|
2840
2845
|
auto finish = [this, &pkg, ingress, blockingStartedMs](
|
|
2841
|
-
bool ok, uint16_t code, const TSTRING& err
|
|
2846
|
+
bool ok, uint16_t code, const TSTRING& err,
|
|
2847
|
+
int8_t retryable, const TSTRING& response = TSTRING(),
|
|
2848
|
+
uint32_t retryAfterMs = 0) {
|
|
2842
2849
|
const auto stalledMs = millis() - blockingStartedMs;
|
|
2843
2850
|
auto refreshed = gateway::refreshPeerWatchdogs(*this, stalledMs);
|
|
2844
2851
|
if (refreshed > 0) {
|
|
@@ -2848,31 +2855,75 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2848
2855
|
static_cast<unsigned>(refreshed),
|
|
2849
2856
|
static_cast<unsigned long>(stalledMs));
|
|
2850
2857
|
}
|
|
2851
|
-
this->sendGatewayAck(pkg, ok, code, err, ingress
|
|
2858
|
+
this->sendGatewayAck(pkg, ok, code, err, ingress, response, retryable,
|
|
2859
|
+
retryAfterMs);
|
|
2852
2860
|
};
|
|
2853
2861
|
|
|
2854
2862
|
// Check Internet connectivity
|
|
2855
2863
|
// First check WiFi status for quick fail
|
|
2856
2864
|
if (WiFi.status() != WL_CONNECTED) {
|
|
2857
|
-
finish(false, 0, "Gateway WiFi not connected");
|
|
2865
|
+
finish(false, 0, "Gateway WiFi not connected", 0);
|
|
2858
2866
|
return true; // Consume package - we handled it (with error)
|
|
2859
2867
|
}
|
|
2860
2868
|
|
|
2861
2869
|
// Then check actual internet access (DNS resolution)
|
|
2862
2870
|
// This detects when WiFi is connected but router has no internet
|
|
2863
2871
|
if (!hasActualInternetAccess()) {
|
|
2864
|
-
finish(false, 0, "Router has no internet access - check WAN connection");
|
|
2872
|
+
finish(false, 0, "Router has no internet access - check WAN connection", 0);
|
|
2865
2873
|
return true; // Consume package - we handled it (with error)
|
|
2866
2874
|
}
|
|
2867
2875
|
|
|
2868
2876
|
// Finally, check for captive portal interference
|
|
2869
2877
|
// This detects when DNS works but HTTP requests are intercepted
|
|
2870
2878
|
if (!detectCaptivePortal()) {
|
|
2871
|
-
finish(false, 0, "Captive portal detected - requires web authentication. Check router/WiFi settings");
|
|
2879
|
+
finish(false, 0, "Captive portal detected - requires web authentication. Check router/WiFi settings", 0);
|
|
2872
2880
|
return true; // Consume package - we handled it (with error)
|
|
2873
2881
|
}
|
|
2874
2882
|
|
|
2875
2883
|
#if defined(ESP32) || defined(ESP8266)
|
|
2884
|
+
#ifdef ESP32
|
|
2885
|
+
// A destination whose name failed to resolve is refused for
|
|
2886
|
+
// GATEWAY_DNS_NEGATIVE_TTL_MS without another lookup (issue #453).
|
|
2887
|
+
// HTTPClient reports a failed lookup as "connection refused" after
|
|
2888
|
+
// the resolver's own patience -- on ESP32 not boundable by this
|
|
2889
|
+
// library -- and it did so on every attempt: the origin node's
|
|
2890
|
+
// retries and, since #451, the bridge's own sends each paid it, and
|
|
2891
|
+
// the scheduler stalled long enough for relayed requests to time
|
|
2892
|
+
// out on their origin nodes. Resolving here, once, names the real
|
|
2893
|
+
// failure and caps the stall to one lookup per TTL per host.
|
|
2894
|
+
// ESP32 only: the ESP8266 core bounds HTTPClient's own lookup by the
|
|
2895
|
+
// HTTP timeout, and a second bounded wait would not fit the
|
|
2896
|
+
// blocking budget.
|
|
2897
|
+
const TSTRING host = gateway::hostFromUrl(pkg.destination);
|
|
2898
|
+
if (host.length() > 0 && !gateway::looksLikeIpLiteral(host)) {
|
|
2899
|
+
uint32_t failedAgoMs = 0;
|
|
2900
|
+
if (negativeDns.isFailing(host, millis(), &failedAgoMs)) {
|
|
2901
|
+
char dnsBuf[160];
|
|
2902
|
+
snprintf(dnsBuf, sizeof(dnsBuf),
|
|
2903
|
+
"DNS lookup for %s failed %lus ago; not retried for "
|
|
2904
|
+
"another %lus",
|
|
2905
|
+
host.c_str(),
|
|
2906
|
+
static_cast<unsigned long>(failedAgoMs / 1000),
|
|
2907
|
+
static_cast<unsigned long>(
|
|
2908
|
+
(gateway::GATEWAY_DNS_NEGATIVE_TTL_MS - failedAgoMs) /
|
|
2909
|
+
1000));
|
|
2910
|
+
Log(ERROR, "%s\n", dnsBuf);
|
|
2911
|
+
finish(false, 0, TSTRING(dnsBuf), 0);
|
|
2912
|
+
return true;
|
|
2913
|
+
}
|
|
2914
|
+
IPAddress resolved;
|
|
2915
|
+
if (WiFi.hostByName(host.c_str(), resolved) != 1) {
|
|
2916
|
+
negativeDns.remember(host, millis());
|
|
2917
|
+
char dnsBuf[160];
|
|
2918
|
+
snprintf(dnsBuf, sizeof(dnsBuf), "DNS lookup failed for %s",
|
|
2919
|
+
host.c_str());
|
|
2920
|
+
Log(ERROR, "%s\n", dnsBuf);
|
|
2921
|
+
finish(false, 0, TSTRING(dnsBuf), 0);
|
|
2922
|
+
return true;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
#endif // ESP32
|
|
2926
|
+
|
|
2876
2927
|
// Make HTTP/HTTPS request
|
|
2877
2928
|
HTTPClient http;
|
|
2878
2929
|
http.setTimeout(GATEWAY_HTTP_TIMEOUT_MS);
|
|
@@ -2917,6 +2968,17 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2917
2968
|
#endif
|
|
2918
2969
|
}
|
|
2919
2970
|
|
|
2971
|
+
// The same id on every attempt at this call. A service that
|
|
2972
|
+
// honours Idempotency-Key treats a retry as the request it already
|
|
2973
|
+
// has; anything recording requests can count the copies.
|
|
2974
|
+
const TSTRING requestId =
|
|
2975
|
+
gateway::requestIdFor(pkg.originNode, pkg.messageId, pkg.requestNonce);
|
|
2976
|
+
http.addHeader("X-Request-Id", requestId.c_str());
|
|
2977
|
+
http.addHeader("Idempotency-Key", requestId.c_str());
|
|
2978
|
+
// Not const: both cores declare collectHeaders(const char* keys[], ...).
|
|
2979
|
+
const char* collected[] = {"Retry-After", "Transfer-Encoding"};
|
|
2980
|
+
http.collectHeaders(collected, 2);
|
|
2981
|
+
|
|
2920
2982
|
// Make request (GET if no payload, POST if payload)
|
|
2921
2983
|
if (pkg.payload.length() > 0) {
|
|
2922
2984
|
http.addHeader("Content-Type", pkg.contentType.c_str());
|
|
@@ -2925,67 +2987,82 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2925
2987
|
httpCode = http.GET();
|
|
2926
2988
|
}
|
|
2927
2989
|
|
|
2928
|
-
//
|
|
2929
|
-
//
|
|
2930
|
-
//
|
|
2931
|
-
//
|
|
2932
|
-
//
|
|
2933
|
-
// delivery to the destination service (e.g., WhatsApp API).
|
|
2990
|
+
// HTTP semantics only: 200/201/202/204 succeed, anything else fails,
|
|
2991
|
+
// and a retry is allowed only when it cannot deliver the request
|
|
2992
|
+
// twice (see gateway::classifyHttpResult). Whether a service's reply
|
|
2993
|
+
// means what the application wanted is the application's decision,
|
|
2994
|
+
// so the start of the body goes back on success as well as failure.
|
|
2934
2995
|
//
|
|
2935
2996
|
// 3xx redirects are not automatically followed.
|
|
2936
2997
|
//
|
|
2937
2998
|
// The classification lives in painlessmesh::gateway so the desktop
|
|
2938
2999
|
// test suite can exercise it; this file is ESP-only and never
|
|
2939
3000
|
// compiled by the Catch2 build.
|
|
2940
|
-
|
|
3001
|
+
TSTRING responseHead;
|
|
3002
|
+
if (httpCode > 0) {
|
|
3003
|
+
responseHead =
|
|
3004
|
+
readResponseExcerpt(http);
|
|
3005
|
+
}
|
|
3006
|
+
const auto outcome =
|
|
3007
|
+
gateway::classifyHttpResult(httpCode, responseHead);
|
|
2941
3008
|
success = outcome.success;
|
|
3009
|
+
const TSTRING responseSummary =
|
|
3010
|
+
httpCode > 0 ? gateway::summarizeResponseBody(responseHead) : TSTRING();
|
|
3011
|
+
uint32_t retryAfterMs = 0;
|
|
3012
|
+
if (outcome.retryable && httpCode > 0) {
|
|
3013
|
+
retryAfterMs = gateway::parseRetryAfterMs(http.header("Retry-After"));
|
|
3014
|
+
}
|
|
2942
3015
|
|
|
2943
3016
|
if (!outcome.transportError) {
|
|
3017
|
+
char errorBuf[320];
|
|
3018
|
+
const char* reason = outcome.reason.c_str();
|
|
3019
|
+
const char* sep = outcome.reason.length() > 0 ? ": " : "";
|
|
2944
3020
|
if (success) {
|
|
2945
3021
|
Log(COMMUNICATION, "HTTP request completed: code=%d\n", httpCode);
|
|
2946
|
-
} else if (
|
|
2947
|
-
//
|
|
2948
|
-
//
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
Log(COMMUNICATION, "HTTP request ambiguous: code=%d (treated as failure, will retry)\n", httpCode);
|
|
2955
|
-
} else if (httpCode >= 500 && httpCode < 600) {
|
|
2956
|
-
// 5xx server errors are retryable, log at COMMUNICATION level
|
|
2957
|
-
char errorBuf[32];
|
|
2958
|
-
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d", httpCode);
|
|
3022
|
+
} else if (outcome.unverifiedStatus) {
|
|
3023
|
+
// A 2xx outside 200/201/202/204: the server answered, but not
|
|
3024
|
+
// with a status this library can call a delivery. Never
|
|
3025
|
+
// retried. Logged at ERROR, where a sketch on the default
|
|
3026
|
+
// levels sees what the service said.
|
|
3027
|
+
snprintf(errorBuf, sizeof(errorBuf),
|
|
3028
|
+
"HTTP %d: not a delivery the gateway can confirm%s%s",
|
|
3029
|
+
httpCode, sep, reason);
|
|
2959
3030
|
error = TSTRING(errorBuf);
|
|
2960
|
-
Log(
|
|
2961
|
-
} else if (
|
|
2962
|
-
//
|
|
2963
|
-
|
|
2964
|
-
|
|
3031
|
+
Log(ERROR, "HTTP response unverified: %s\n", errorBuf);
|
|
3032
|
+
} else if (outcome.retryable) {
|
|
3033
|
+
// 429 or 503: the server says it did not take the request.
|
|
3034
|
+
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
|
|
3035
|
+
reason);
|
|
2965
3036
|
error = TSTRING(errorBuf);
|
|
2966
|
-
Log(COMMUNICATION, "HTTP
|
|
3037
|
+
Log(COMMUNICATION, "HTTP %d: the server asked to retry (after %lu ms)\n",
|
|
3038
|
+
httpCode, static_cast<unsigned long>(retryAfterMs));
|
|
2967
3039
|
} else {
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d", httpCode);
|
|
3040
|
+
snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
|
|
3041
|
+
reason);
|
|
2971
3042
|
error = TSTRING(errorBuf);
|
|
2972
|
-
Log(ERROR, "HTTP request failed:
|
|
3043
|
+
Log(ERROR, "HTTP request failed: %s\n", errorBuf);
|
|
2973
3044
|
}
|
|
2974
3045
|
} else {
|
|
2975
|
-
//
|
|
2976
|
-
//
|
|
3046
|
+
// Below HTTP. When the request may already be at the server -- a
|
|
3047
|
+
// read timeout, a connection lost after sending -- it is not
|
|
3048
|
+
// retried, and the error says why, so an application that
|
|
3049
|
+
// resends does so knowing it may duplicate.
|
|
2977
3050
|
error = http.errorToString(httpCode);
|
|
3051
|
+
if (!outcome.retryable) {
|
|
3052
|
+
error += " (the request may have reached the server; not retried)";
|
|
3053
|
+
}
|
|
2978
3054
|
Log(ERROR, "HTTP request failed: %s\n", error.c_str());
|
|
2979
3055
|
}
|
|
2980
3056
|
|
|
2981
3057
|
http.end();
|
|
2982
3058
|
|
|
2983
|
-
// Send acknowledgment back
|
|
2984
|
-
//
|
|
2985
|
-
finish(success, outcome.ackStatus, error
|
|
3059
|
+
// Send acknowledgment back, with the verdict on retrying and the
|
|
3060
|
+
// start of whatever the server said.
|
|
3061
|
+
finish(success, outcome.ackStatus, error, outcome.retryable ? 1 : 0,
|
|
3062
|
+
responseSummary, retryAfterMs);
|
|
2986
3063
|
#else
|
|
2987
3064
|
// Non-ESP platform - send error
|
|
2988
|
-
finish(false, 0, "HTTP client not available on this platform");
|
|
3065
|
+
finish(false, 0, "HTTP client not available on this platform", 0);
|
|
2989
3066
|
#endif
|
|
2990
3067
|
|
|
2991
3068
|
return true; // Consume package - we have processed it and sent
|
|
@@ -2993,6 +3070,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2993
3070
|
});
|
|
2994
3071
|
}
|
|
2995
3072
|
|
|
3073
|
+
/**
|
|
3074
|
+
* The station just got an IP. On a node that runs the Internet health
|
|
3075
|
+
* check -- a bridge or a shared gateway, whose station is the manual router
|
|
3076
|
+
* link -- the last probe most likely ran during association and failed,
|
|
3077
|
+
* and the next is a full interval away (issue #450). Probe again now, from
|
|
3078
|
+
* the scheduler rather than the Wi-Fi event task, so hasLocalInternet()
|
|
3079
|
+
* and the bridge's own sends stop waiting out the interval. A regular
|
|
3080
|
+
* node's mesh station never enables the check and is left alone.
|
|
3081
|
+
*/
|
|
3082
|
+
void probeUplinkAfterAssociation() {
|
|
3083
|
+
if (!this->isInternetHealthCheckEnabled() || !stationScan.manual) return;
|
|
3084
|
+
this->addTask([this]() { this->checkInternetNow(); });
|
|
3085
|
+
}
|
|
3086
|
+
|
|
2996
3087
|
void eventHandleInit() {
|
|
2997
3088
|
using namespace logger;
|
|
2998
3089
|
// Where the station scan learns the channel the mesh is rooted on.
|
|
@@ -3101,6 +3192,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
3101
3192
|
this->stationScan.stationAttemptOver();
|
|
3102
3193
|
this->stationScan.stationUp();
|
|
3103
3194
|
this->tcpConnect(); // Connect to TCP port
|
|
3195
|
+
this->probeUplinkAfterAssociation();
|
|
3104
3196
|
this->semaphoreGive();
|
|
3105
3197
|
}
|
|
3106
3198
|
},
|
|
@@ -3137,6 +3229,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
3137
3229
|
this->stationScan.stationAttemptOver();
|
|
3138
3230
|
this->stationScan.stationUp();
|
|
3139
3231
|
this->tcpConnect(); // Connect to TCP port
|
|
3232
|
+
this->probeUplinkAfterAssociation();
|
|
3140
3233
|
});
|
|
3141
3234
|
#endif // ESP32
|
|
3142
3235
|
return;
|
package/src/painlessMeshSTA.cpp
CHANGED
|
@@ -628,6 +628,9 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
|
|
|
628
628
|
#endif
|
|
629
629
|
bool isRooted = layout::isRooted(mesh->asNodeTree());
|
|
630
630
|
if (isRooted) everRooted = true;
|
|
631
|
+
// The immediate-rescan allowance is per outage, and this is the one place
|
|
632
|
+
// every scan passes through: a node with a connection has not spent it.
|
|
633
|
+
if (painlessmesh::layout::liveSubs(*mesh) > 0) rejoinScanned = false;
|
|
631
634
|
if (aps.empty()) {
|
|
632
635
|
// No unknown nodes found
|
|
633
636
|
consecutiveEmptyScans++;
|
|
@@ -732,11 +735,42 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
|
|
|
732
735
|
task.delay(interval);
|
|
733
736
|
if (orphanScanBackoff < 2) orphanScanBackoff++;
|
|
734
737
|
} else {
|
|
735
|
-
//
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
738
|
+
// Not connected, and this scan turned up nothing to connect to.
|
|
739
|
+
//
|
|
740
|
+
// "Fast" here is 0.5 * SCAN_INTERVAL -- fifteen seconds. For a node
|
|
741
|
+
// that still has a live connection (an AP-side child) that is soon
|
|
742
|
+
// enough: it is in the mesh and reachable while it looks for more.
|
|
743
|
+
// For a node with none it is the whole cost of the outage, and it is
|
|
744
|
+
// paid while the node's neighbours still have it in their routing
|
|
745
|
+
// tables: they keep sending to it until their own NODE_TIMEOUT, and
|
|
746
|
+
// those messages are dropped.
|
|
747
|
+
//
|
|
748
|
+
// Measured on the Alteriom HIL rig (issue #459): an ESP32-C3 closed
|
|
749
|
+
// its only uplink on a momentary nodeSync contradiction, logged
|
|
750
|
+
// "scan rate set to fast" 12 ms later, and did not scan again for
|
|
751
|
+
// exactly 15 000 ms. It was out of the mesh for 16.2 s and answered
|
|
752
|
+
// an empty node list throughout; a unicast sent to it in that window
|
|
753
|
+
// was accepted by the sender and lost. The scan that finally ran
|
|
754
|
+
// found five mesh APs at -30 to -58 dBm and was associated 1.2 s
|
|
755
|
+
// later. There was nothing to wait for.
|
|
756
|
+
//
|
|
757
|
+
// So a node with no connections left scans at once. Once per outage:
|
|
758
|
+
// `rejoinScanned` is cleared as soon as it has a connection again,
|
|
759
|
+
// so a node that is simply alone falls back to the interval instead
|
|
760
|
+
// of scanning back to back.
|
|
739
761
|
task.setInterval(0.5 * SCAN_INTERVAL);
|
|
762
|
+
if (painlessmesh::layout::liveSubs(*mesh) == 0 && !rejoinScanned) {
|
|
763
|
+
rejoinScanned = true;
|
|
764
|
+
Log(CONNECTION,
|
|
765
|
+
"connectToAP(): No connections left; scanning again now rather "
|
|
766
|
+
"than in %d s\n",
|
|
767
|
+
(int)(0.5 * SCAN_INTERVAL / TASK_SECOND));
|
|
768
|
+
task.forceNextIteration();
|
|
769
|
+
} else {
|
|
770
|
+
Log(CONNECTION,
|
|
771
|
+
"connectToAP(): No unknown nodes found scan rate set to "
|
|
772
|
+
"fast\n");
|
|
773
|
+
}
|
|
740
774
|
}
|
|
741
775
|
mesh->stability += min(1000 - mesh->stability, (size_t)25);
|
|
742
776
|
} else {
|
package/src/painlessMeshSTA.h
CHANGED
|
@@ -62,7 +62,14 @@ class StationScan {
|
|
|
62
62
|
// The station got an address: its link is up. Cleared once the drop
|
|
63
63
|
// callbacks have judged a disconnect, so they can tell a link that was
|
|
64
64
|
// up and went away from an attempt that never got that far.
|
|
65
|
-
void stationUp() {
|
|
65
|
+
void stationUp() {
|
|
66
|
+
stationLinkUp = true;
|
|
67
|
+
// A link again, so the immediate-rescan allowance is restored now
|
|
68
|
+
// rather than at the next scan. Without this a node that dropped
|
|
69
|
+
// twice inside one scan interval paid the full fifteen seconds for
|
|
70
|
+
// the second drop (#459).
|
|
71
|
+
rejoinScanned = false;
|
|
72
|
+
}
|
|
66
73
|
void stationDown() { stationLinkUp = false; }
|
|
67
74
|
bool stationLinkUp = false;
|
|
68
75
|
// The station link was closed by this node's own channel move a moment
|
|
@@ -147,6 +154,11 @@ class StationScan {
|
|
|
147
154
|
// channel re-detection runs, and consuming those results here found them
|
|
148
155
|
// already deleted, reported "wifi scan failed", and rescanned at once.
|
|
149
156
|
bool scanRequested = false;
|
|
157
|
+
// Whether this node has already scanned at once on losing its last
|
|
158
|
+
// connection. Cleared the moment it has one again, so the immediate
|
|
159
|
+
// rescan is one per outage and not a scan loop: a node that is out of
|
|
160
|
+
// the mesh and finds nothing still falls back to the interval below.
|
|
161
|
+
bool rejoinScanned = false;
|
|
150
162
|
// Set when the next station scan must cover every channel: the empty
|
|
151
163
|
// scans have piled up and the node is looking for the channel the mesh
|
|
152
164
|
// moved to. The re-detection is this task's ordinary asynchronous scan
|
|
@@ -206,12 +206,17 @@ class SentBuffer {
|
|
|
206
206
|
size_t requestLength(size_t buffer_length) {
|
|
207
207
|
// Use highest priority message available
|
|
208
208
|
auto* msg = getNextMessage();
|
|
209
|
-
if (!msg)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
if (!msg) return 0;
|
|
210
|
+
// A caller with no room is owed nothing. The subtraction below leaves
|
|
211
|
+
// space for the terminator toCharArray() always writes; on an unsigned
|
|
212
|
+
// zero it wraps to SIZE_MAX instead, min() then picks the message, and
|
|
213
|
+
// the answer comes back *larger* than was asked for -- which the
|
|
214
|
+
// documented contract above forbids and read() then acts on, writing
|
|
215
|
+
// length + 1 bytes into a buffer the caller said had none.
|
|
216
|
+
if (buffer_length == 0) return 0;
|
|
217
|
+
// String.toCharArray automatically turns the last character into
|
|
218
|
+
// a \0, we need the extra space to deal with that annoyance
|
|
219
|
+
return (std::min)(buffer_length - 1, msg->length() + 1);
|
|
215
220
|
}
|
|
216
221
|
|
|
217
222
|
/**
|
|
@@ -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;
|