@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.
@@ -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
- void sendGatewayAck(
2711
- const gateway::GatewayDataPackage& request, bool success,
2712
- uint16_t httpStatus, const TSTRING& error,
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
- auto conn = router::findRoute<Connection>((*this), request.originNode);
2736
- if (!conn && ingressConnection) {
2737
- // A newly promoted gateway can receive data before its NodeTree has
2738
- // converged enough for findRoute() to resolve the request origin. The
2739
- // ingress connection is nevertheless a valid reverse path: the request
2740
- // just arrived through it and every intermediate node can continue
2741
- // routing the addressed acknowledgment toward originNode.
2742
- conn = ingressConnection;
2743
- Log(COMMUNICATION,
2744
- "Routing GATEWAY_ACK to node %u through request ingress while "
2745
- "topology converges\n",
2746
- request.originNode);
2747
- }
2748
- if (conn) {
2749
- protocol::Variant variant(&ack);
2750
- router::send(std::move(variant), conn);
2751
- Log(COMMUNICATION, "Sent GATEWAY_ACK to node %u (success=%d, http=%d)\n",
2752
- request.originNode, success, httpStatus);
2753
- } else {
2754
- Log(ERROR, "Failed to send GATEWAY_ACK: no route to node %u\n",
2755
- request.originNode);
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
- uint16_t httpCode = 0;
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
- // Only specific 2xx status codes indicate genuine success
2908
- // 200 OK: Standard successful response
2909
- // 201 Created: Resource successfully created
2910
- // 202 Accepted: Request accepted for processing
2911
- // 204 No Content: Successful with no response body
2912
- //
2913
- // Other 2xx codes like 203 (Non-Authoritative Information) often
2914
- // indicate cached/proxied responses that may not represent actual
2915
- // delivery to the destination service (e.g., WhatsApp API).
2916
- //
2917
- // 3xx redirects are not automatically followed
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 (httpCode >= 200 && httpCode < 300) {
2924
- // Other 2xx codes - ambiguous success
2925
- // HTTP 203 is retryable, so log at COMMUNICATION level to reduce noise
2926
- char errorBuf[128];
2927
- snprintf(errorBuf, sizeof(errorBuf),
2928
- "Ambiguous response - HTTP %d may indicate cached/proxied response, not actual delivery",
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(COMMUNICATION, "HTTP request ambiguous: code=%d (treated as failure, will retry)\n", httpCode);
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
- char errorBuf[32];
2935
- snprintf(errorBuf, sizeof(errorBuf), "HTTP %d", httpCode);
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
- char errorBuf[32];
2941
- snprintf(errorBuf, sizeof(errorBuf), "HTTP %d", httpCode);
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
- char errorBuf[32];
2947
- snprintf(errorBuf, sizeof(errorBuf), "HTTP %d", httpCode);
3030
+ snprintf(errorBuf, sizeof(errorBuf), "HTTP %d%s%s", httpCode, sep,
3031
+ reason);
2948
3032
  error = TSTRING(errorBuf);
2949
- Log(ERROR, "HTTP request failed: code=%d\n", httpCode);
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
- finish(success, httpCode, error);
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;