@alteriom/painlessmesh 2.0.3 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +190 -0
- package/CONTRIBUTING.md +59 -11
- package/examples/alteriom/platformio.ini +4 -1
- package/examples/basic/platformio.ini +4 -1
- package/examples/sendToInternet/README.md +54 -2
- package/examples/sendToInternet/callmebot.h +139 -0
- package/examples/sendToInternet/sendToInternet.ino +117 -39
- package/keywords.txt +1 -0
- 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 +147 -81
- package/src/boost/asynctcp.hpp +1 -1
- package/src/painlessMeshSTA.cpp +38 -4
- package/src/painlessMeshSTA.h +18 -5
- package/src/painlessmesh/buffer.hpp +22 -17
- package/src/painlessmesh/gateway.hpp +420 -73
- package/src/painlessmesh/layout.hpp +16 -0
- package/src/painlessmesh/logger.hpp +1 -1
- package/src/painlessmesh/mesh.hpp +276 -116
- package/src/painlessmesh/message_queue.hpp +6 -6
- package/src/painlessmesh/metrics.hpp +1 -1
- package/src/painlessmesh/ota.hpp +1 -1
- package/src/painlessmesh/plugin.hpp +10 -7
- package/src/painlessmesh/protocol.hpp +10 -6
- package/src/painlessmesh/rtc.hpp +3 -3
- package/src/plugin/performance.hpp +5 -5
|
@@ -127,6 +127,22 @@ class Layout {
|
|
|
127
127
|
bool hasTimeAuthority = false;
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
/** How many of this node's connections are up.
|
|
131
|
+
*
|
|
132
|
+
* Zero means the node is out of the mesh: it has no route to anywhere, and
|
|
133
|
+
* nothing it is told about the topology can be acted on. A closed
|
|
134
|
+
* connection stays in `subs` until eraseClosedConnections() next runs, and
|
|
135
|
+
* a count of `subs` therefore says a node is connected when it is not --
|
|
136
|
+
* which is the same trap findRoute() was fixed for.
|
|
137
|
+
*/
|
|
138
|
+
template <class T>
|
|
139
|
+
size_t liveSubs(const Layout<T>& layout) {
|
|
140
|
+
size_t live = 0;
|
|
141
|
+
for (auto&& sub : layout.subs)
|
|
142
|
+
if (sub->connected()) ++live;
|
|
143
|
+
return live;
|
|
144
|
+
}
|
|
145
|
+
|
|
130
146
|
template <class T>
|
|
131
147
|
void syncLayout(Layout<T>& layout, uint32_t changedId) {
|
|
132
148
|
// TODO: this should be called from changed connections and dropped
|
|
@@ -81,6 +81,55 @@ struct SendOptions {
|
|
|
81
81
|
typedef std::function<void(bool success, uint16_t httpStatus, TSTRING error)>
|
|
82
82
|
internetResultCallback_t;
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Defined when sendToInternet() accepts an InternetResult callback, the
|
|
86
|
+
* gateway only retries requests that cannot have reached the server, and
|
|
87
|
+
* each request carries X-Request-Id. Code that must build against older
|
|
88
|
+
* releases too can test for it with #ifdef.
|
|
89
|
+
*/
|
|
90
|
+
#define PAINLESSMESH_HAS_INTERNET_RESULT 1
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Everything the library learned about one sendToInternet() call
|
|
94
|
+
*
|
|
95
|
+
* The library applies HTTP's meaning of a reply and nothing more. Whether a
|
|
96
|
+
* particular service's answer means what the application wanted -- a message
|
|
97
|
+
* really queued, a record really stored -- depends on that service, so the
|
|
98
|
+
* application decides, from `httpStatus` and `response`.
|
|
99
|
+
*/
|
|
100
|
+
struct InternetResult {
|
|
101
|
+
/** The id sendToInternet() returned. */
|
|
102
|
+
uint32_t messageId = 0;
|
|
103
|
+
/** True for HTTP 200, 201, 202 and 204. */
|
|
104
|
+
bool success = false;
|
|
105
|
+
/** The destination's HTTP status; 0 when no reply arrived. */
|
|
106
|
+
uint16_t httpStatus = 0;
|
|
107
|
+
/** Why it failed, for a log or a user; empty on success. */
|
|
108
|
+
TSTRING error;
|
|
109
|
+
/** The start of the response body as one line, when a body was read. */
|
|
110
|
+
TSTRING response;
|
|
111
|
+
/**
|
|
112
|
+
* True when the request cannot have reached the server, or the server said
|
|
113
|
+
* it did not take it. False when it may have been processed: resending it
|
|
114
|
+
* is then the application's decision, because it could arrive twice.
|
|
115
|
+
*/
|
|
116
|
+
bool retryable = false;
|
|
117
|
+
/** How many times the request was issued, retries included. */
|
|
118
|
+
uint8_t attempts = 0;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Callback type for Internet request results, with the whole result
|
|
123
|
+
*
|
|
124
|
+
* \code
|
|
125
|
+
* mesh.sendToInternet(url, "", [](const InternetResult& result) {
|
|
126
|
+
* Serial.printf("HTTP %u: %s\n", result.httpStatus, result.response.c_str());
|
|
127
|
+
* });
|
|
128
|
+
* \endcode
|
|
129
|
+
*/
|
|
130
|
+
typedef std::function<void(const InternetResult& result)>
|
|
131
|
+
internetResponseCallback_t;
|
|
132
|
+
|
|
84
133
|
/**
|
|
85
134
|
* Pending Internet request entry
|
|
86
135
|
*
|
|
@@ -98,6 +147,12 @@ struct PendingInternetRequest {
|
|
|
98
147
|
TSTRING destination = ""; ///< Internet destination URL
|
|
99
148
|
TSTRING payload = ""; ///< Request payload
|
|
100
149
|
internetResultCallback_t callback; ///< User callback for result
|
|
150
|
+
internetResponseCallback_t onResult; ///< Or the whole-result callback
|
|
151
|
+
/// Times the request was handed to a gateway: 0 until the first send
|
|
152
|
+
/// leaves this node, whatever retries were scheduled meanwhile.
|
|
153
|
+
uint8_t attempts = 0;
|
|
154
|
+
/// Drawn once for the call, sent on every attempt (GatewayDataPackage).
|
|
155
|
+
uint32_t requestNonce = 0;
|
|
101
156
|
/// Why the last attempt failed, when retries were spent waiting for a
|
|
102
157
|
/// gateway; the final callback reports it instead of a bare retry count.
|
|
103
158
|
TSTRING lastError = "";
|
|
@@ -1721,6 +1776,61 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1721
1776
|
TSTRING payload,
|
|
1722
1777
|
internetResultCallback_t callback,
|
|
1723
1778
|
uint8_t priority = static_cast<uint8_t>(gateway::GatewayPriority::PRIORITY_NORMAL)) {
|
|
1779
|
+
return startInternetRequest(destination, payload, callback, nullptr, priority);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* Send data to an Internet destination, receiving the whole result
|
|
1784
|
+
*
|
|
1785
|
+
* The same request as the (success, httpStatus, error) form, but the
|
|
1786
|
+
* callback also gets the start of the response body, whether the request
|
|
1787
|
+
* may be resent safely, and how many times it was issued. Use it when a
|
|
1788
|
+
* service's status alone does not say whether it did what was asked -- the
|
|
1789
|
+
* sendToInternet example judges CallMeBot's replies this way.
|
|
1790
|
+
*
|
|
1791
|
+
* @return Unique message ID for tracking, or 0 if immediate failure
|
|
1792
|
+
*/
|
|
1793
|
+
uint32_t sendToInternet(
|
|
1794
|
+
TSTRING destination,
|
|
1795
|
+
TSTRING payload,
|
|
1796
|
+
internetResponseCallback_t onResult,
|
|
1797
|
+
uint8_t priority = static_cast<uint8_t>(gateway::GatewayPriority::PRIORITY_NORMAL)) {
|
|
1798
|
+
return startInternetRequest(destination, payload, nullptr, onResult, priority);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
private:
|
|
1802
|
+
/** Hand a finished request's result to whichever callback it was given. */
|
|
1803
|
+
static void deliverInternetResult(const PendingInternetRequest& request,
|
|
1804
|
+
InternetResult result) {
|
|
1805
|
+
result.messageId = request.messageId;
|
|
1806
|
+
result.attempts = request.attempts;
|
|
1807
|
+
// A request that never left this node -- refused for want of a mesh or a
|
|
1808
|
+
// gateway, or whose every routing attempt failed to send -- cannot have
|
|
1809
|
+
// reached a server, whatever ended it: resending it cannot duplicate.
|
|
1810
|
+
if (!result.success && request.attempts == 0) {
|
|
1811
|
+
result.retryable = true;
|
|
1812
|
+
}
|
|
1813
|
+
if (request.onResult) {
|
|
1814
|
+
request.onResult(result);
|
|
1815
|
+
} else if (request.callback) {
|
|
1816
|
+
request.callback(result.success, result.httpStatus, result.error);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
static InternetResult failedInternetResult(const TSTRING& error,
|
|
1821
|
+
uint16_t httpStatus = 0) {
|
|
1822
|
+
InternetResult result;
|
|
1823
|
+
result.httpStatus = httpStatus;
|
|
1824
|
+
result.error = error;
|
|
1825
|
+
return result;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
uint32_t startInternetRequest(
|
|
1829
|
+
TSTRING destination,
|
|
1830
|
+
TSTRING payload,
|
|
1831
|
+
internetResultCallback_t callback,
|
|
1832
|
+
internetResponseCallback_t onResult,
|
|
1833
|
+
uint8_t priority) {
|
|
1724
1834
|
using namespace logger;
|
|
1725
1835
|
|
|
1726
1836
|
// Generate unique message ID
|
|
@@ -1745,20 +1855,32 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1745
1855
|
request.destination = destination;
|
|
1746
1856
|
request.payload = payload;
|
|
1747
1857
|
request.callback = callback;
|
|
1858
|
+
request.onResult = onResult;
|
|
1859
|
+
request.requestNonce = gateway::newRequestNonce();
|
|
1860
|
+
// Handed to this node's own handler below, unconditionally.
|
|
1861
|
+
request.attempts = 1;
|
|
1862
|
+
const uint32_t nonce = request.requestNonce;
|
|
1748
1863
|
pendingInternetRequests[messageId] = request;
|
|
1749
1864
|
|
|
1750
1865
|
dispatchInternetRequestLocally(messageId, priority, 0, destination,
|
|
1751
|
-
payload);
|
|
1866
|
+
payload, nonce);
|
|
1752
1867
|
return messageId;
|
|
1753
1868
|
}
|
|
1754
1869
|
|
|
1870
|
+
// A request refused before it is tracked still reports through the
|
|
1871
|
+
// callback it was given, on the scheduler rather than inside this call.
|
|
1872
|
+
PendingInternetRequest refused;
|
|
1873
|
+
refused.callback = callback;
|
|
1874
|
+
refused.onResult = onResult;
|
|
1875
|
+
refused.attempts = 0;
|
|
1876
|
+
|
|
1755
1877
|
// Validate mesh connectivity before attempting to send
|
|
1756
1878
|
if (!hasActiveMeshConnections()) {
|
|
1757
1879
|
Log(ERROR, "sendToInternet(): No active mesh connections\n");
|
|
1758
|
-
if (callback) {
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1880
|
+
if (callback || onResult) {
|
|
1881
|
+
this->addTask([refused]() {
|
|
1882
|
+
deliverInternetResult(refused, failedInternetResult(
|
|
1883
|
+
"No mesh connections - cannot route to gateway"));
|
|
1762
1884
|
});
|
|
1763
1885
|
}
|
|
1764
1886
|
return 0;
|
|
@@ -1768,10 +1890,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1768
1890
|
BridgeInfo* gateway = getPrimaryBridge();
|
|
1769
1891
|
if (gateway == nullptr) {
|
|
1770
1892
|
Log(ERROR, "sendToInternet(): No gateway available\n");
|
|
1771
|
-
if (callback) {
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
callback(false, 0, "No gateway available");
|
|
1893
|
+
if (callback || onResult) {
|
|
1894
|
+
this->addTask([refused]() {
|
|
1895
|
+
deliverInternetResult(refused, failedInternetResult("No gateway available"));
|
|
1775
1896
|
});
|
|
1776
1897
|
}
|
|
1777
1898
|
return 0;
|
|
@@ -1790,6 +1911,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1790
1911
|
request.destination = destination;
|
|
1791
1912
|
request.payload = payload;
|
|
1792
1913
|
request.callback = callback;
|
|
1914
|
+
request.onResult = onResult;
|
|
1915
|
+
request.requestNonce = gateway::newRequestNonce();
|
|
1793
1916
|
|
|
1794
1917
|
// Store pending request
|
|
1795
1918
|
pendingInternetRequests[messageId] = request;
|
|
@@ -1807,6 +1930,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1807
1930
|
pkg.contentType = "application/json";
|
|
1808
1931
|
pkg.retryCount = 0;
|
|
1809
1932
|
pkg.requiresAck = true;
|
|
1933
|
+
pkg.requestNonce = request.requestNonce;
|
|
1810
1934
|
|
|
1811
1935
|
// Send the package with priority
|
|
1812
1936
|
bool sent = false;
|
|
@@ -1829,6 +1953,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1829
1953
|
scheduleInternetRetry(messageId);
|
|
1830
1954
|
} else {
|
|
1831
1955
|
Log(COMMUNICATION, "sendToInternet(): Sent to gateway %u\n", gateway->nodeId);
|
|
1956
|
+
// Only a send that left this node is an attempt.
|
|
1957
|
+
pendingInternetRequests[messageId].attempts = 1;
|
|
1832
1958
|
// Schedule timeout check
|
|
1833
1959
|
scheduleInternetTimeout(messageId);
|
|
1834
1960
|
}
|
|
@@ -1836,6 +1962,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1836
1962
|
return messageId;
|
|
1837
1963
|
}
|
|
1838
1964
|
|
|
1965
|
+
public:
|
|
1839
1966
|
/**
|
|
1840
1967
|
* Configure Internet request timeout
|
|
1841
1968
|
*
|
|
@@ -1907,11 +2034,11 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1907
2034
|
bool cancelInternetRequest(uint32_t messageId) {
|
|
1908
2035
|
auto it = pendingInternetRequests.find(messageId);
|
|
1909
2036
|
if (it != pendingInternetRequests.end()) {
|
|
1910
|
-
|
|
2037
|
+
const PendingInternetRequest cancelled = it->second;
|
|
1911
2038
|
pendingInternetRequests.erase(it);
|
|
1912
|
-
if (callback) {
|
|
1913
|
-
this->addTask([
|
|
1914
|
-
|
|
2039
|
+
if (cancelled.callback || cancelled.onResult) {
|
|
2040
|
+
this->addTask([cancelled]() {
|
|
2041
|
+
deliverInternetResult(cancelled, failedInternetResult("Request cancelled"));
|
|
1915
2042
|
});
|
|
1916
2043
|
}
|
|
1917
2044
|
Log(logger::GENERAL, "cancelInternetRequest(): Cancelled msgId=%u\n", messageId);
|
|
@@ -1973,9 +2100,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1973
2100
|
|
|
1974
2101
|
// Cancel all pending requests
|
|
1975
2102
|
for (auto& pair : pendingInternetRequests) {
|
|
1976
|
-
|
|
1977
|
-
pair.second.callback(false, 0, "API disabled");
|
|
1978
|
-
}
|
|
2103
|
+
deliverInternetResult(pair.second, failedInternetResult("API disabled"));
|
|
1979
2104
|
}
|
|
1980
2105
|
pendingInternetRequests.clear();
|
|
1981
2106
|
|
|
@@ -2012,7 +2137,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2012
2137
|
*/
|
|
2013
2138
|
void sendGatewayAck(const gateway::GatewayDataPackage& request, bool success,
|
|
2014
2139
|
uint16_t httpStatus, const TSTRING& error,
|
|
2015
|
-
std::shared_ptr<T> ingressConnection = nullptr
|
|
2140
|
+
std::shared_ptr<T> ingressConnection = nullptr,
|
|
2141
|
+
const TSTRING& response = TSTRING(), int8_t retryable = -1,
|
|
2142
|
+
uint32_t retryAfterMs = 0) {
|
|
2016
2143
|
using namespace logger;
|
|
2017
2144
|
|
|
2018
2145
|
gateway::GatewayAckPackage ack;
|
|
@@ -2024,13 +2151,35 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2024
2151
|
ack.httpStatus = httpStatus;
|
|
2025
2152
|
ack.error = error;
|
|
2026
2153
|
ack.timestamp = this->getNodeTime();
|
|
2154
|
+
ack.response = response;
|
|
2155
|
+
ack.retryable = retryable;
|
|
2156
|
+
ack.retryAfterMs = retryAfterMs;
|
|
2027
2157
|
|
|
2028
2158
|
if (request.originNode == this->nodeId) {
|
|
2029
|
-
|
|
2030
|
-
this
|
|
2159
|
+
// Completed from the scheduler, not from inside the gateway handler
|
|
2160
|
+
// that called this. There the handler's HTTPClient, its WiFiClient
|
|
2161
|
+
// and the response buffers are all still allocated, and the
|
|
2162
|
+
// application's callback runs on what is left: on the HIL rig an
|
|
2163
|
+
// ESP8266 shared gateway with ~11 KB free delivered its own result
|
|
2164
|
+
// into a callback that could no longer allocate the event it built.
|
|
2165
|
+
//
|
|
2166
|
+
// The ack is moved into one heap block and handed to the handler as
|
|
2167
|
+
// it is. It used to be copied into the task's closure, copied again
|
|
2168
|
+
// inside it, serialised into a JSON document by the Variant and
|
|
2169
|
+
// parsed back into a third package by the handler -- the reply
|
|
2170
|
+
// excerpt and the error text five times over on the heap the
|
|
2171
|
+
// handler had already spent -- and on the rig the closure's own
|
|
2172
|
+
// 76-byte allocation was the one that failed (#469). Nothing but
|
|
2173
|
+
// handleGatewayAck() listens for GATEWAY_ACK, and a request that
|
|
2174
|
+
// originated here exists only if enableSendToInternet() registered
|
|
2175
|
+
// it, so the round trip through the callback list bought nothing.
|
|
2176
|
+
// A shared_ptr rather than a C++14 init-capture: the ESP32 Arduino
|
|
2177
|
+
// 2.x core still builds as gnu++11.
|
|
2178
|
+
auto held = std::make_shared<gateway::GatewayAckPackage>(std::move(ack));
|
|
2179
|
+
this->addTask([this, held]() { this->handleGatewayAck(*held); });
|
|
2031
2180
|
Log(COMMUNICATION,
|
|
2032
|
-
"
|
|
2033
|
-
httpStatus);
|
|
2181
|
+
"Completing local GATEWAY_ACK from the scheduler (success=%d, http=%d)\n",
|
|
2182
|
+
success, httpStatus);
|
|
2034
2183
|
return;
|
|
2035
2184
|
}
|
|
2036
2185
|
|
|
@@ -2067,13 +2216,21 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2067
2216
|
|
|
2068
2217
|
PendingInternetRequest& request = it->second;
|
|
2069
2218
|
|
|
2219
|
+
InternetResult result;
|
|
2220
|
+
result.success = ack.success;
|
|
2221
|
+
result.httpStatus = ack.httpStatus;
|
|
2222
|
+
result.error = ack.error;
|
|
2223
|
+
result.response = ack.response;
|
|
2224
|
+
|
|
2070
2225
|
// Check if this is a success response
|
|
2071
2226
|
if (ack.success) {
|
|
2072
|
-
//
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2227
|
+
// Taken out of the map, not copied out of it, and the result handed
|
|
2228
|
+
// on rather than copied: on an ESP8266 shared gateway every extra
|
|
2229
|
+
// copy of the destination, the payload and the reply excerpt lands
|
|
2230
|
+
// on a heap the request itself has nearly spent (#469).
|
|
2231
|
+
PendingInternetRequest done = std::move(request);
|
|
2076
2232
|
pendingInternetRequests.erase(it);
|
|
2233
|
+
deliverInternetResult(done, std::move(result));
|
|
2077
2234
|
return;
|
|
2078
2235
|
}
|
|
2079
2236
|
|
|
@@ -2117,86 +2274,75 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2117
2274
|
}
|
|
2118
2275
|
}
|
|
2119
2276
|
|
|
2120
|
-
// Failure
|
|
2277
|
+
// Failure: may the identical request be sent again?
|
|
2278
|
+
//
|
|
2279
|
+
// A gateway that says so decides (2.1.0 and later): it saw the transport
|
|
2280
|
+
// error or the status, and it knows whether the request can have reached
|
|
2281
|
+
// the server. Resending one that may have been processed delivers it
|
|
2282
|
+
// twice -- on the rig one timed-out send was issued four times -- so only
|
|
2283
|
+
// a request that cannot have arrived, or one the server told us to bring
|
|
2284
|
+
// back (429, 503), is retried.
|
|
2121
2285
|
bool isRetryable = false;
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
isRetryable = true;
|
|
2137
|
-
Log(COMMUNICATION, "handleGatewayAck(): HTTP 429 rate limit, marking as retryable\n");
|
|
2138
|
-
}
|
|
2139
|
-
// Network errors (httpStatus == 0) EXCEPT gateway connectivity errors
|
|
2140
|
-
// Gateway connectivity errors are infrastructure issues (not transient)
|
|
2141
|
-
else if (ack.httpStatus == 0) {
|
|
2142
|
-
// Check if this is a gateway-level connectivity error (non-retryable)
|
|
2143
|
-
bool isGatewayConnectivityError = false;
|
|
2144
|
-
|
|
2145
|
-
// These errors indicate infrastructure issues that won't be fixed by retrying:
|
|
2146
|
-
// - "Router has no internet access" - WAN connection down
|
|
2147
|
-
// - "Gateway WiFi not connected" - ESP not associated with WiFi
|
|
2148
|
-
// - "Captive portal detected" - Router requires web authentication
|
|
2149
|
-
// Use find() for std::string (test env) or indexOf() for Arduino String
|
|
2150
|
-
bool routerError = false, wifiError = false, captivePortalError = false;
|
|
2151
|
-
#if defined(PAINLESSMESH_BOOST)
|
|
2152
|
-
// Test environment: TSTRING is std::string, use find()
|
|
2153
|
-
routerError = (ack.error.find("Router has no internet") != std::string::npos);
|
|
2154
|
-
wifiError = (ack.error.find("Gateway WiFi not connected") != std::string::npos);
|
|
2155
|
-
captivePortalError = (ack.error.find("Captive portal detected") != std::string::npos);
|
|
2156
|
-
#else
|
|
2157
|
-
// Arduino environment: TSTRING is String, use indexOf()
|
|
2158
|
-
routerError = (ack.error.indexOf("Router has no internet") >= 0);
|
|
2159
|
-
wifiError = (ack.error.indexOf("Gateway WiFi not connected") >= 0);
|
|
2160
|
-
captivePortalError = (ack.error.indexOf("Captive portal detected") >= 0);
|
|
2161
|
-
#endif
|
|
2162
|
-
|
|
2163
|
-
if (routerError || wifiError || captivePortalError) {
|
|
2164
|
-
isGatewayConnectivityError = true;
|
|
2165
|
-
Log(COMMUNICATION, "handleGatewayAck(): Gateway connectivity error detected (non-retryable): %s\n",
|
|
2166
|
-
ack.error.c_str());
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
// Only mark as retryable if it's NOT a gateway connectivity error
|
|
2170
|
-
if (!isGatewayConnectivityError) {
|
|
2171
|
-
isRetryable = true;
|
|
2172
|
-
Log(COMMUNICATION, "handleGatewayAck(): Network error, marking as retryable\n");
|
|
2286
|
+
uint32_t minDelayMs = 0;
|
|
2287
|
+
if (ack.retryable >= 0) {
|
|
2288
|
+
isRetryable = ack.retryable == 1;
|
|
2289
|
+
if (isRetryable && ack.retryAfterMs > gateway::GATEWAY_RETRY_AFTER_MAX_MS) {
|
|
2290
|
+
// The server asked to wait longer than a retry will: say when, and
|
|
2291
|
+
// leave the resend to the application.
|
|
2292
|
+
isRetryable = false;
|
|
2293
|
+
char errorBuf[224];
|
|
2294
|
+
snprintf(errorBuf, sizeof(errorBuf), "%s (the server asked to retry after %lu s)",
|
|
2295
|
+
ack.error.c_str(),
|
|
2296
|
+
static_cast<unsigned long>(ack.retryAfterMs / 1000));
|
|
2297
|
+
result.error = TSTRING(errorBuf);
|
|
2298
|
+
} else if (isRetryable) {
|
|
2299
|
+
minDelayMs = ack.retryAfterMs;
|
|
2173
2300
|
}
|
|
2301
|
+
} else {
|
|
2302
|
+
// A gateway that predates the field. Keep its behaviour -- it has
|
|
2303
|
+
// retried network errors and 5xx since 1.x -- except for a 2xx: the
|
|
2304
|
+
// server answered, so a resend is a second copy.
|
|
2305
|
+
isRetryable = legacyAckIsRetryable(ack);
|
|
2174
2306
|
}
|
|
2175
|
-
|
|
2176
|
-
// HTTP 3xx redirects are NOT retryable (should be followed by HTTPClient)
|
|
2177
|
-
// Other status codes are NOT retryable
|
|
2307
|
+
result.retryable = isRetryable;
|
|
2178
2308
|
|
|
2179
2309
|
// If retryable and have retries left, schedule retry
|
|
2180
2310
|
if (isRetryable && request.retryCount < request.maxRetries) {
|
|
2181
2311
|
Log(COMMUNICATION, "handleGatewayAck(): Scheduling retry for msgId=%u (attempt %u/%u)\n",
|
|
2182
2312
|
ack.messageId, request.retryCount + 1, request.maxRetries);
|
|
2183
|
-
|
|
2313
|
+
request.lastError = result.error;
|
|
2314
|
+
scheduleInternetRetry(ack.messageId, minDelayMs);
|
|
2184
2315
|
} else {
|
|
2185
|
-
|
|
2186
|
-
if (request.retryCount >= request.maxRetries) {
|
|
2316
|
+
if (isRetryable) {
|
|
2187
2317
|
Log(ERROR, "handleGatewayAck(): Max retries reached for msgId=%u\n", ack.messageId);
|
|
2188
2318
|
} else {
|
|
2189
|
-
Log(COMMUNICATION, "handleGatewayAck():
|
|
2319
|
+
Log(COMMUNICATION, "handleGatewayAck(): Final failure for msgId=%u (HTTP %u)\n",
|
|
2190
2320
|
ack.messageId, ack.httpStatus);
|
|
2191
2321
|
}
|
|
2192
|
-
|
|
2193
|
-
if (request.callback) {
|
|
2194
|
-
request.callback(ack.success, ack.httpStatus, ack.error);
|
|
2195
|
-
}
|
|
2322
|
+
PendingInternetRequest done = std::move(request);
|
|
2196
2323
|
pendingInternetRequests.erase(it);
|
|
2324
|
+
deliverInternetResult(done, std::move(result));
|
|
2197
2325
|
}
|
|
2198
2326
|
}
|
|
2199
2327
|
|
|
2328
|
+
/**
|
|
2329
|
+
* How an ack from a gateway that does not say whether to retry was read
|
|
2330
|
+
* before 2.1.0, minus retrying a 2xx.
|
|
2331
|
+
*/
|
|
2332
|
+
static bool legacyAckIsRetryable(const gateway::GatewayAckPackage& ack) {
|
|
2333
|
+
if (ack.httpStatus >= 500 && ack.httpStatus < 600) return true;
|
|
2334
|
+
if (ack.httpStatus == 429) return true;
|
|
2335
|
+
if (ack.httpStatus != 0) return false;
|
|
2336
|
+
// Gateway-level connectivity failures are infrastructure, not transient.
|
|
2337
|
+
static const char* const FINAL[] = {"Router has no internet",
|
|
2338
|
+
"Gateway WiFi not connected",
|
|
2339
|
+
"Captive portal detected"};
|
|
2340
|
+
for (const char* phrase : FINAL) {
|
|
2341
|
+
if (gateway::responseContains(ack.error, phrase)) return false;
|
|
2342
|
+
}
|
|
2343
|
+
return true;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2200
2346
|
/**
|
|
2201
2347
|
* Run a gateway request against this node's own uplink
|
|
2202
2348
|
*
|
|
@@ -2204,14 +2350,16 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2204
2350
|
* acknowledgment, pending-request tracking and callback semantics stay
|
|
2205
2351
|
* identical to a request served by a peer gateway.
|
|
2206
2352
|
*
|
|
2207
|
-
* The handler runs synchronously
|
|
2208
|
-
*
|
|
2209
|
-
*
|
|
2353
|
+
* The handler runs synchronously. sendGatewayAck() completes a local
|
|
2354
|
+
* acknowledgment from the scheduler, but a handler is free to do otherwise,
|
|
2355
|
+
* so callers must not hold a reference into pendingInternetRequests across
|
|
2356
|
+
* this call -- pass copies.
|
|
2210
2357
|
*/
|
|
2211
2358
|
void dispatchInternetRequestLocally(uint32_t messageId, uint8_t priority,
|
|
2212
2359
|
uint8_t retryCount,
|
|
2213
2360
|
const TSTRING& destination,
|
|
2214
|
-
const TSTRING& payload
|
|
2361
|
+
const TSTRING& payload,
|
|
2362
|
+
uint32_t requestNonce) {
|
|
2215
2363
|
gateway::GatewayDataPackage pkg;
|
|
2216
2364
|
pkg.from = this->nodeId;
|
|
2217
2365
|
pkg.dest = this->nodeId;
|
|
@@ -2224,6 +2372,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2224
2372
|
pkg.contentType = "application/json";
|
|
2225
2373
|
pkg.retryCount = retryCount;
|
|
2226
2374
|
pkg.requiresAck = true;
|
|
2375
|
+
pkg.requestNonce = requestNonce;
|
|
2227
2376
|
protocol::Variant variant(&pkg);
|
|
2228
2377
|
this->callbackList.execute(protocol::GATEWAY_DATA, variant, nullptr, 0);
|
|
2229
2378
|
}
|
|
@@ -2231,7 +2380,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2231
2380
|
/**
|
|
2232
2381
|
* Schedule retry for a failed Internet request
|
|
2233
2382
|
*/
|
|
2234
|
-
void scheduleInternetRetry(uint32_t messageId) {
|
|
2383
|
+
void scheduleInternetRetry(uint32_t messageId, uint32_t minDelayMs = 0) {
|
|
2235
2384
|
auto it = pendingInternetRequests.find(messageId);
|
|
2236
2385
|
if (it == pendingInternetRequests.end()) {
|
|
2237
2386
|
return;
|
|
@@ -2244,19 +2393,28 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2244
2393
|
// were spent on when there is one
|
|
2245
2394
|
Log(logger::ERROR, "scheduleInternetRetry(): Max retries reached for msgId=%u\n",
|
|
2246
2395
|
messageId);
|
|
2247
|
-
|
|
2248
|
-
request.callback(false, 0,
|
|
2249
|
-
request.lastError.length() > 0 ? request.lastError
|
|
2250
|
-
: TSTRING("Max retries exceeded"));
|
|
2251
|
-
}
|
|
2396
|
+
const PendingInternetRequest done = request;
|
|
2252
2397
|
pendingInternetRequests.erase(it);
|
|
2398
|
+
deliverInternetResult(done, failedInternetResult(
|
|
2399
|
+
done.lastError.length() > 0 ? done.lastError : TSTRING("Max retries exceeded")));
|
|
2253
2400
|
return;
|
|
2254
2401
|
}
|
|
2255
2402
|
|
|
2256
|
-
//
|
|
2403
|
+
// Exponential backoff, never shorter than what the server asked for
|
|
2257
2404
|
uint32_t delay = request.retryDelayMs * (1 << request.retryCount);
|
|
2405
|
+
if (delay < minDelayMs) delay = minDelayMs;
|
|
2258
2406
|
request.retryCount++;
|
|
2259
2407
|
|
|
2408
|
+
// A wait the server asked for moves the request's deadline with it:
|
|
2409
|
+
// otherwise a Retry-After longer than what is left of the request
|
|
2410
|
+
// timeout (30 s by default) times the request out before the retry the
|
|
2411
|
+
// server invited can run. The retry gets a full timeout of its own.
|
|
2412
|
+
if (minDelayMs > 0) {
|
|
2413
|
+
const uint32_t elapsed = static_cast<uint32_t>(millis()) - request.timestamp;
|
|
2414
|
+
const uint32_t needed = elapsed + delay + internetRequestTimeout;
|
|
2415
|
+
if (needed > request.timeoutMs) request.timeoutMs = needed;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2260
2418
|
Log(logger::COMMUNICATION, "scheduleInternetRetry(): Retry %u for msgId=%u in %u ms\n",
|
|
2261
2419
|
request.retryCount, messageId, delay);
|
|
2262
2420
|
|
|
@@ -2291,11 +2449,13 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2291
2449
|
const uint8_t retryCount = request.retryCount;
|
|
2292
2450
|
const TSTRING destination = request.destination;
|
|
2293
2451
|
const TSTRING payload = request.payload;
|
|
2452
|
+
const uint32_t nonce = request.requestNonce;
|
|
2453
|
+
request.attempts++;
|
|
2294
2454
|
Log(logger::COMMUNICATION,
|
|
2295
2455
|
"retryInternetRequest(): Retrying msgId=%u on the local uplink\n",
|
|
2296
2456
|
messageId);
|
|
2297
2457
|
dispatchInternetRequestLocally(messageId, priority, retryCount,
|
|
2298
|
-
destination, payload);
|
|
2458
|
+
destination, payload, nonce);
|
|
2299
2459
|
return;
|
|
2300
2460
|
}
|
|
2301
2461
|
|
|
@@ -2333,6 +2493,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2333
2493
|
pkg.contentType = "application/json";
|
|
2334
2494
|
pkg.retryCount = request.retryCount;
|
|
2335
2495
|
pkg.requiresAck = true;
|
|
2496
|
+
pkg.requestNonce = request.requestNonce;
|
|
2336
2497
|
|
|
2337
2498
|
// Send the package
|
|
2338
2499
|
auto conn = painlessmesh::router::findRoute<T>((*this), gateway->nodeId);
|
|
@@ -2346,6 +2507,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2346
2507
|
Log(logger::ERROR, "retryInternetRequest(): Retry send failed msgId=%u\n", messageId);
|
|
2347
2508
|
scheduleInternetRetry(messageId);
|
|
2348
2509
|
} else {
|
|
2510
|
+
request.attempts++;
|
|
2349
2511
|
Log(logger::COMMUNICATION, "retryInternetRequest(): Retry sent msgId=%u to gateway %u\n",
|
|
2350
2512
|
messageId, gateway->nodeId);
|
|
2351
2513
|
}
|
|
@@ -2381,10 +2543,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2381
2543
|
if (request.isTimedOut()) {
|
|
2382
2544
|
Log(logger::ERROR, "checkInternetRequestTimeout(): Request timed out msgId=%u\n",
|
|
2383
2545
|
messageId);
|
|
2384
|
-
|
|
2385
|
-
request.callback(false, 0, "Request timed out");
|
|
2386
|
-
}
|
|
2546
|
+
const PendingInternetRequest done = request;
|
|
2387
2547
|
pendingInternetRequests.erase(it);
|
|
2548
|
+
deliverInternetResult(done, failedInternetResult("Request timed out"));
|
|
2388
2549
|
}
|
|
2389
2550
|
}
|
|
2390
2551
|
|
|
@@ -2397,9 +2558,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2397
2558
|
if (it->second.isTimedOut()) {
|
|
2398
2559
|
Log(logger::GENERAL, "cleanupTimedOutRequests(): Cleaning up msgId=%u\n",
|
|
2399
2560
|
it->first);
|
|
2400
|
-
|
|
2401
|
-
it->second.callback(false, 0, "Request timed out");
|
|
2402
|
-
}
|
|
2561
|
+
deliverInternetResult(it->second, failedInternetResult("Request timed out"));
|
|
2403
2562
|
it = pendingInternetRequests.erase(it);
|
|
2404
2563
|
} else {
|
|
2405
2564
|
++it;
|
|
@@ -2947,15 +3106,15 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2947
3106
|
* Structure to hold detailed connection information
|
|
2948
3107
|
*/
|
|
2949
3108
|
struct ConnectionInfo {
|
|
2950
|
-
uint32_t nodeId; // Connected node ID
|
|
2951
|
-
uint32_t lastSeen; // Timestamp of last message (ms)
|
|
2952
|
-
int rssi; // Signal strength (dBm)
|
|
2953
|
-
int avgLatency; // Average round-trip time (ms)
|
|
2954
|
-
int hopCount; // Hops from current node
|
|
2955
|
-
int quality; // Connection quality (0-100)
|
|
2956
|
-
uint32_t messagesRx; // Messages received
|
|
2957
|
-
uint32_t messagesTx; // Messages sent
|
|
2958
|
-
uint32_t messagesDropped; // Failed transmissions
|
|
3109
|
+
uint32_t nodeId = 0; // Connected node ID
|
|
3110
|
+
uint32_t lastSeen = 0; // Timestamp of last message (ms)
|
|
3111
|
+
int rssi = 0; // Signal strength (dBm)
|
|
3112
|
+
int avgLatency = 0; // Average round-trip time (ms)
|
|
3113
|
+
int hopCount = 0; // Hops from current node
|
|
3114
|
+
int quality = 0; // Connection quality (0-100)
|
|
3115
|
+
uint32_t messagesRx = 0; // Messages received
|
|
3116
|
+
uint32_t messagesTx = 0; // Messages sent
|
|
3117
|
+
uint32_t messagesDropped = 0; // Failed transmissions
|
|
2959
3118
|
};
|
|
2960
3119
|
|
|
2961
3120
|
/**
|
|
@@ -3953,7 +4112,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
3953
4112
|
/// Is the node a root node
|
|
3954
4113
|
bool shouldContainRoot = false;
|
|
3955
4114
|
|
|
3956
|
-
|
|
4115
|
+
// init() either creates one or takes the caller's; null until then.
|
|
4116
|
+
Scheduler *mScheduler = nullptr;
|
|
3957
4117
|
|
|
3958
4118
|
public: // Windows MSVC: lambdas in friend functions need public access
|
|
3959
4119
|
|