@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
|
@@ -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,15 @@ 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;
|
|
156
|
+
/// Why the last attempt failed, when retries were spent waiting for a
|
|
157
|
+
/// gateway; the final callback reports it instead of a bare retry count.
|
|
158
|
+
TSTRING lastError = "";
|
|
101
159
|
|
|
102
160
|
/**
|
|
103
161
|
* Check if this request has timed out
|
|
@@ -243,6 +301,37 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
243
301
|
painlessmesh::ntp::addPackageCallback(this->callbackList, (*this));
|
|
244
302
|
painlessmesh::router::addPackageCallback(this->callbackList, (*this));
|
|
245
303
|
|
|
304
|
+
// Every node answers a gateway request it cannot serve. A bridge that
|
|
305
|
+
// reboots, crashes or is reflashed as a regular node announces nothing
|
|
306
|
+
// -- only a bridge stepping down in-process sends `leaving` -- so its
|
|
307
|
+
// peers keep it in knownBridges until its last status ages out
|
|
308
|
+
// (bridgeTimeoutMs, 60 s) and may route every Internet request to it.
|
|
309
|
+
// A regular node had no GATEWAY_DATA handler and dropped them without a
|
|
310
|
+
// word; the origin waited out its request timeout, 30 s, each time.
|
|
311
|
+
// Found on the HIL rig while validating 2.0.3.
|
|
312
|
+
//
|
|
313
|
+
// Registered first, so a gateway's own handler (initGatewayInternetHandler
|
|
314
|
+
// in wifi.hpp, or a test's stand-in) is always the second: this one
|
|
315
|
+
// stays silent whenever any other GATEWAY_DATA handler exists.
|
|
316
|
+
this->callbackList.onPackage(
|
|
317
|
+
protocol::GATEWAY_DATA,
|
|
318
|
+
[this](protocol::Variant& variant, std::shared_ptr<T> ingress,
|
|
319
|
+
uint32_t) {
|
|
320
|
+
if (this->callbackList.count(protocol::GATEWAY_DATA) > 1) {
|
|
321
|
+
return false; // this node is a gateway; its handler answers
|
|
322
|
+
}
|
|
323
|
+
auto pkg = variant.to<gateway::GatewayDataPackage>();
|
|
324
|
+
char errorBuf[96];
|
|
325
|
+
snprintf(errorBuf, sizeof(errorBuf), "Node %u %s",
|
|
326
|
+
static_cast<unsigned>(this->nodeId),
|
|
327
|
+
gateway::GATEWAY_NOT_A_GATEWAY_PHRASE);
|
|
328
|
+
Log(logger::ERROR,
|
|
329
|
+
"GATEWAY_DATA from node %u answered: %s\n",
|
|
330
|
+
static_cast<unsigned>(pkg.originNode), errorBuf);
|
|
331
|
+
this->sendGatewayAck(pkg, false, 0, TSTRING(errorBuf), ingress);
|
|
332
|
+
return true;
|
|
333
|
+
});
|
|
334
|
+
|
|
246
335
|
// Seed delivery-confirmation ids with a random value so they do not
|
|
247
336
|
// restart at 1 after a reboot — a delayed ACK for a pre-reboot
|
|
248
337
|
// message could otherwise match a fresh message's id and report a
|
|
@@ -1349,7 +1438,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1349
1438
|
|
|
1350
1439
|
size_t removed = sizeBefore - knownBridges.size();
|
|
1351
1440
|
if (removed > 0) {
|
|
1352
|
-
Log(GENERAL, "cleanupExpiredBridges(): Removed %
|
|
1441
|
+
Log(GENERAL, "cleanupExpiredBridges(): Removed %lu expired bridges\n",
|
|
1442
|
+
static_cast<unsigned long>(removed));
|
|
1353
1443
|
}
|
|
1354
1444
|
}
|
|
1355
1445
|
|
|
@@ -1432,6 +1522,31 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1432
1522
|
return internetHealthChecker.hasLocalInternet();
|
|
1433
1523
|
}
|
|
1434
1524
|
|
|
1525
|
+
/**
|
|
1526
|
+
* Can this node serve a gateway request on its own uplink right now?
|
|
1527
|
+
*
|
|
1528
|
+
* hasLocalInternet() is the last periodic probe's answer. On a bridge the
|
|
1529
|
+
* first probe runs while the station is still associating and fails, and
|
|
1530
|
+
* the next is a full interval (30 s) away, so for that whole window a send
|
|
1531
|
+
* fell through to the mesh path and was refused for want of a peer (issue
|
|
1532
|
+
* #450). When the flag says no and the health check is running, spend the
|
|
1533
|
+
* one on-demand probe the checker allows per period: a send made after the
|
|
1534
|
+
* uplink came up is then served locally, and a node whose uplink really is
|
|
1535
|
+
* down pays one probe per interval, not one per call.
|
|
1536
|
+
*
|
|
1537
|
+
* @return true if the request should go to this node's own gateway handler
|
|
1538
|
+
*/
|
|
1539
|
+
bool uplinkServesLocally() {
|
|
1540
|
+
if (hasLocalInternet()) return true;
|
|
1541
|
+
if (!isInternetHealthCheckEnabled()) return false;
|
|
1542
|
+
if (internetHealthChecker.checkOnDemand()) {
|
|
1543
|
+
Log(logger::COMMUNICATION,
|
|
1544
|
+
"uplinkServesLocally(): on-demand probe found the uplink up\n");
|
|
1545
|
+
return true;
|
|
1546
|
+
}
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1435
1550
|
/**
|
|
1436
1551
|
* Register callback for local Internet connectivity changes
|
|
1437
1552
|
*
|
|
@@ -1661,6 +1776,61 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1661
1776
|
TSTRING payload,
|
|
1662
1777
|
internetResultCallback_t callback,
|
|
1663
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) {
|
|
1664
1834
|
using namespace logger;
|
|
1665
1835
|
|
|
1666
1836
|
// Generate unique message ID
|
|
@@ -1672,7 +1842,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1672
1842
|
// A shared gateway can execute its own gateway package locally. Keep the
|
|
1673
1843
|
// same package handler, pending-request tracking and callback semantics,
|
|
1674
1844
|
// but do not require a mesh peer merely to reach this node's own uplink.
|
|
1675
|
-
if (
|
|
1845
|
+
if (uplinkServesLocally()) {
|
|
1676
1846
|
PendingInternetRequest request;
|
|
1677
1847
|
request.messageId = messageId;
|
|
1678
1848
|
request.timestamp = millis();
|
|
@@ -1685,20 +1855,32 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1685
1855
|
request.destination = destination;
|
|
1686
1856
|
request.payload = payload;
|
|
1687
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;
|
|
1688
1863
|
pendingInternetRequests[messageId] = request;
|
|
1689
1864
|
|
|
1690
1865
|
dispatchInternetRequestLocally(messageId, priority, 0, destination,
|
|
1691
|
-
payload);
|
|
1866
|
+
payload, nonce);
|
|
1692
1867
|
return messageId;
|
|
1693
1868
|
}
|
|
1694
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
|
+
|
|
1695
1877
|
// Validate mesh connectivity before attempting to send
|
|
1696
1878
|
if (!hasActiveMeshConnections()) {
|
|
1697
1879
|
Log(ERROR, "sendToInternet(): No active mesh connections\n");
|
|
1698
|
-
if (callback) {
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1880
|
+
if (callback || onResult) {
|
|
1881
|
+
this->addTask([refused]() {
|
|
1882
|
+
deliverInternetResult(refused, failedInternetResult(
|
|
1883
|
+
"No mesh connections - cannot route to gateway"));
|
|
1702
1884
|
});
|
|
1703
1885
|
}
|
|
1704
1886
|
return 0;
|
|
@@ -1708,10 +1890,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1708
1890
|
BridgeInfo* gateway = getPrimaryBridge();
|
|
1709
1891
|
if (gateway == nullptr) {
|
|
1710
1892
|
Log(ERROR, "sendToInternet(): No gateway available\n");
|
|
1711
|
-
if (callback) {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
callback(false, 0, "No gateway available");
|
|
1893
|
+
if (callback || onResult) {
|
|
1894
|
+
this->addTask([refused]() {
|
|
1895
|
+
deliverInternetResult(refused, failedInternetResult("No gateway available"));
|
|
1715
1896
|
});
|
|
1716
1897
|
}
|
|
1717
1898
|
return 0;
|
|
@@ -1730,6 +1911,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1730
1911
|
request.destination = destination;
|
|
1731
1912
|
request.payload = payload;
|
|
1732
1913
|
request.callback = callback;
|
|
1914
|
+
request.onResult = onResult;
|
|
1915
|
+
request.requestNonce = gateway::newRequestNonce();
|
|
1733
1916
|
|
|
1734
1917
|
// Store pending request
|
|
1735
1918
|
pendingInternetRequests[messageId] = request;
|
|
@@ -1747,6 +1930,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1747
1930
|
pkg.contentType = "application/json";
|
|
1748
1931
|
pkg.retryCount = 0;
|
|
1749
1932
|
pkg.requiresAck = true;
|
|
1933
|
+
pkg.requestNonce = request.requestNonce;
|
|
1750
1934
|
|
|
1751
1935
|
// Send the package with priority
|
|
1752
1936
|
bool sent = false;
|
|
@@ -1769,6 +1953,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1769
1953
|
scheduleInternetRetry(messageId);
|
|
1770
1954
|
} else {
|
|
1771
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;
|
|
1772
1958
|
// Schedule timeout check
|
|
1773
1959
|
scheduleInternetTimeout(messageId);
|
|
1774
1960
|
}
|
|
@@ -1776,6 +1962,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1776
1962
|
return messageId;
|
|
1777
1963
|
}
|
|
1778
1964
|
|
|
1965
|
+
public:
|
|
1779
1966
|
/**
|
|
1780
1967
|
* Configure Internet request timeout
|
|
1781
1968
|
*
|
|
@@ -1847,11 +2034,11 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1847
2034
|
bool cancelInternetRequest(uint32_t messageId) {
|
|
1848
2035
|
auto it = pendingInternetRequests.find(messageId);
|
|
1849
2036
|
if (it != pendingInternetRequests.end()) {
|
|
1850
|
-
|
|
2037
|
+
const PendingInternetRequest cancelled = it->second;
|
|
1851
2038
|
pendingInternetRequests.erase(it);
|
|
1852
|
-
if (callback) {
|
|
1853
|
-
this->addTask([
|
|
1854
|
-
|
|
2039
|
+
if (cancelled.callback || cancelled.onResult) {
|
|
2040
|
+
this->addTask([cancelled]() {
|
|
2041
|
+
deliverInternetResult(cancelled, failedInternetResult("Request cancelled"));
|
|
1855
2042
|
});
|
|
1856
2043
|
}
|
|
1857
2044
|
Log(logger::GENERAL, "cancelInternetRequest(): Cancelled msgId=%u\n", messageId);
|
|
@@ -1913,9 +2100,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1913
2100
|
|
|
1914
2101
|
// Cancel all pending requests
|
|
1915
2102
|
for (auto& pair : pendingInternetRequests) {
|
|
1916
|
-
|
|
1917
|
-
pair.second.callback(false, 0, "API disabled");
|
|
1918
|
-
}
|
|
2103
|
+
deliverInternetResult(pair.second, failedInternetResult("API disabled"));
|
|
1919
2104
|
}
|
|
1920
2105
|
pendingInternetRequests.clear();
|
|
1921
2106
|
|
|
@@ -1935,6 +2120,79 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1935
2120
|
/**
|
|
1936
2121
|
* Handle incoming gateway acknowledgment package
|
|
1937
2122
|
*/
|
|
2123
|
+
public:
|
|
2124
|
+
// Public: the gateway handler in wifi.hpp, a derived class, answers with it.
|
|
2125
|
+
/**
|
|
2126
|
+
* Answer a gateway request: send its GATEWAY_ACK back to the origin node.
|
|
2127
|
+
*
|
|
2128
|
+
* A request this node originated completes locally, without a mesh hop.
|
|
2129
|
+
* Otherwise the ack is routed to the origin; when the NodeTree has not yet
|
|
2130
|
+
* converged enough for findRoute() to resolve it -- a freshly promoted
|
|
2131
|
+
* gateway can receive data before that -- the connection the request
|
|
2132
|
+
* arrived on is a valid reverse path, since every intermediate node routes
|
|
2133
|
+
* the addressed ack onward toward the origin.
|
|
2134
|
+
*
|
|
2135
|
+
* Used by the gateway's own handler (wifi.hpp) and by the fallback every
|
|
2136
|
+
* node registers for requests it cannot serve.
|
|
2137
|
+
*/
|
|
2138
|
+
void sendGatewayAck(const gateway::GatewayDataPackage& request, bool success,
|
|
2139
|
+
uint16_t httpStatus, const TSTRING& error,
|
|
2140
|
+
std::shared_ptr<T> ingressConnection = nullptr,
|
|
2141
|
+
const TSTRING& response = TSTRING(), int8_t retryable = -1,
|
|
2142
|
+
uint32_t retryAfterMs = 0) {
|
|
2143
|
+
using namespace logger;
|
|
2144
|
+
|
|
2145
|
+
gateway::GatewayAckPackage ack;
|
|
2146
|
+
ack.from = this->nodeId;
|
|
2147
|
+
ack.dest = request.originNode;
|
|
2148
|
+
ack.messageId = request.messageId;
|
|
2149
|
+
ack.originNode = request.originNode;
|
|
2150
|
+
ack.success = success;
|
|
2151
|
+
ack.httpStatus = httpStatus;
|
|
2152
|
+
ack.error = error;
|
|
2153
|
+
ack.timestamp = this->getNodeTime();
|
|
2154
|
+
ack.response = response;
|
|
2155
|
+
ack.retryable = retryable;
|
|
2156
|
+
ack.retryAfterMs = retryAfterMs;
|
|
2157
|
+
|
|
2158
|
+
if (request.originNode == this->nodeId) {
|
|
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
|
+
this->addTask([this, ack]() {
|
|
2166
|
+
gateway::GatewayAckPackage local = ack;
|
|
2167
|
+
protocol::Variant variant(&local);
|
|
2168
|
+
this->callbackList.execute(protocol::GATEWAY_ACK, variant, nullptr, 0);
|
|
2169
|
+
});
|
|
2170
|
+
Log(COMMUNICATION,
|
|
2171
|
+
"Completing local GATEWAY_ACK from the scheduler (success=%d, http=%d)\n",
|
|
2172
|
+
success, httpStatus);
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
auto conn = router::findRoute<T>((*this), request.originNode);
|
|
2177
|
+
if (!conn && ingressConnection) {
|
|
2178
|
+
conn = ingressConnection;
|
|
2179
|
+
Log(COMMUNICATION,
|
|
2180
|
+
"Routing GATEWAY_ACK to node %u through request ingress while "
|
|
2181
|
+
"topology converges\n",
|
|
2182
|
+
request.originNode);
|
|
2183
|
+
}
|
|
2184
|
+
if (conn) {
|
|
2185
|
+
protocol::Variant variant(&ack);
|
|
2186
|
+
router::send(std::move(variant), conn);
|
|
2187
|
+
Log(COMMUNICATION, "Sent GATEWAY_ACK to node %u (success=%d, http=%d)\n",
|
|
2188
|
+
request.originNode, success, httpStatus);
|
|
2189
|
+
} else {
|
|
2190
|
+
Log(ERROR, "Failed to send GATEWAY_ACK: no route to node %u\n",
|
|
2191
|
+
request.originNode);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
private:
|
|
1938
2196
|
void handleGatewayAck(const gateway::GatewayAckPackage& ack) {
|
|
1939
2197
|
using namespace logger;
|
|
1940
2198
|
Log(COMMUNICATION, "handleGatewayAck(): msgId=%u success=%d http=%u\n",
|
|
@@ -1948,94 +2206,127 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1948
2206
|
|
|
1949
2207
|
PendingInternetRequest& request = it->second;
|
|
1950
2208
|
|
|
2209
|
+
InternetResult result;
|
|
2210
|
+
result.success = ack.success;
|
|
2211
|
+
result.httpStatus = ack.httpStatus;
|
|
2212
|
+
result.error = ack.error;
|
|
2213
|
+
result.response = ack.response;
|
|
2214
|
+
|
|
1951
2215
|
// Check if this is a success response
|
|
1952
2216
|
if (ack.success) {
|
|
1953
|
-
|
|
1954
|
-
if (request.callback) {
|
|
1955
|
-
request.callback(ack.success, ack.httpStatus, ack.error);
|
|
1956
|
-
}
|
|
2217
|
+
const PendingInternetRequest done = request;
|
|
1957
2218
|
pendingInternetRequests.erase(it);
|
|
2219
|
+
deliverInternetResult(done, result);
|
|
1958
2220
|
return;
|
|
1959
2221
|
}
|
|
1960
2222
|
|
|
1961
|
-
//
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
//
|
|
1965
|
-
//
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
Log(
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
// Arduino environment: TSTRING is String, use indexOf()
|
|
1999
|
-
routerError = (ack.error.indexOf("Router has no internet") >= 0);
|
|
2000
|
-
wifiError = (ack.error.indexOf("Gateway WiFi not connected") >= 0);
|
|
2001
|
-
captivePortalError = (ack.error.indexOf("Captive portal detected") >= 0);
|
|
2002
|
-
#endif
|
|
2003
|
-
|
|
2004
|
-
if (routerError || wifiError || captivePortalError) {
|
|
2005
|
-
isGatewayConnectivityError = true;
|
|
2006
|
-
Log(COMMUNICATION, "handleGatewayAck(): Gateway connectivity error detected (non-retryable): %s\n",
|
|
2007
|
-
ack.error.c_str());
|
|
2223
|
+
// The node this request went to is not a gateway (issue found on the
|
|
2224
|
+
// HIL rig while validating 2.0.3): it rebooted or was reflashed as a
|
|
2225
|
+
// regular node without announcing it, and its last bridge status is
|
|
2226
|
+
// still inside bridgeTimeoutMs. Forget it and send again through the
|
|
2227
|
+
// next gateway at once. The retry budget is not spent: nothing was
|
|
2228
|
+
// attempted against the Internet, and every such answer removes one
|
|
2229
|
+
// entry from knownBridges, so this cannot loop.
|
|
2230
|
+
if (gateway::responseContains(ack.error,
|
|
2231
|
+
gateway::GATEWAY_NOT_A_GATEWAY_PHRASE)) {
|
|
2232
|
+
const uint32_t staleGateway = request.gatewayNodeId;
|
|
2233
|
+
bool forgot = this->forgetBridge(staleGateway);
|
|
2234
|
+
if (ack.from != staleGateway) forgot = this->forgetBridge(ack.from) || forgot;
|
|
2235
|
+
Log(ERROR, "handleGatewayAck(): %s; forgot it as a gateway for msgId=%u\n",
|
|
2236
|
+
ack.error.c_str(), ack.messageId);
|
|
2237
|
+
|
|
2238
|
+
if (!forgot) {
|
|
2239
|
+
// Nothing to forget, so a redispatch would reach the same node.
|
|
2240
|
+
// Fall through to the ordinary failure path below.
|
|
2241
|
+
} else if (this->getPrimaryBridge() == nullptr &&
|
|
2242
|
+
!this->uplinkServesLocally()) {
|
|
2243
|
+
// No gateway left right now. On the rig the live bridge's first
|
|
2244
|
+
// status arrived half a second after the ex-bridge answered, so an
|
|
2245
|
+
// accepted request rides the ordinary retry backoff rather than
|
|
2246
|
+
// failing on the spot; retryInternetRequest() reschedules while no
|
|
2247
|
+
// gateway is known, and the final callback carries this reason.
|
|
2248
|
+
char errorBuf[160];
|
|
2249
|
+
snprintf(errorBuf, sizeof(errorBuf),
|
|
2250
|
+
"No Internet gateway available: %s", ack.error.c_str());
|
|
2251
|
+
request.lastError = TSTRING(errorBuf);
|
|
2252
|
+
scheduleInternetRetry(ack.messageId);
|
|
2253
|
+
return;
|
|
2254
|
+
} else {
|
|
2255
|
+
const uint32_t messageId = ack.messageId;
|
|
2256
|
+
this->addTask([this, messageId]() {
|
|
2257
|
+
this->retryInternetRequest(messageId);
|
|
2258
|
+
});
|
|
2259
|
+
return;
|
|
2008
2260
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// Failure: may the identical request be sent again?
|
|
2264
|
+
//
|
|
2265
|
+
// A gateway that says so decides (2.1.0 and later): it saw the transport
|
|
2266
|
+
// error or the status, and it knows whether the request can have reached
|
|
2267
|
+
// the server. Resending one that may have been processed delivers it
|
|
2268
|
+
// twice -- on the rig one timed-out send was issued four times -- so only
|
|
2269
|
+
// a request that cannot have arrived, or one the server told us to bring
|
|
2270
|
+
// back (429, 503), is retried.
|
|
2271
|
+
bool isRetryable = false;
|
|
2272
|
+
uint32_t minDelayMs = 0;
|
|
2273
|
+
if (ack.retryable >= 0) {
|
|
2274
|
+
isRetryable = ack.retryable == 1;
|
|
2275
|
+
if (isRetryable && ack.retryAfterMs > gateway::GATEWAY_RETRY_AFTER_MAX_MS) {
|
|
2276
|
+
// The server asked to wait longer than a retry will: say when, and
|
|
2277
|
+
// leave the resend to the application.
|
|
2278
|
+
isRetryable = false;
|
|
2279
|
+
char errorBuf[224];
|
|
2280
|
+
snprintf(errorBuf, sizeof(errorBuf), "%s (the server asked to retry after %lu s)",
|
|
2281
|
+
ack.error.c_str(),
|
|
2282
|
+
static_cast<unsigned long>(ack.retryAfterMs / 1000));
|
|
2283
|
+
result.error = TSTRING(errorBuf);
|
|
2284
|
+
} else if (isRetryable) {
|
|
2285
|
+
minDelayMs = ack.retryAfterMs;
|
|
2014
2286
|
}
|
|
2287
|
+
} else {
|
|
2288
|
+
// A gateway that predates the field. Keep its behaviour -- it has
|
|
2289
|
+
// retried network errors and 5xx since 1.x -- except for a 2xx: the
|
|
2290
|
+
// server answered, so a resend is a second copy.
|
|
2291
|
+
isRetryable = legacyAckIsRetryable(ack);
|
|
2015
2292
|
}
|
|
2016
|
-
|
|
2017
|
-
// HTTP 3xx redirects are NOT retryable (should be followed by HTTPClient)
|
|
2018
|
-
// Other status codes are NOT retryable
|
|
2293
|
+
result.retryable = isRetryable;
|
|
2019
2294
|
|
|
2020
2295
|
// If retryable and have retries left, schedule retry
|
|
2021
2296
|
if (isRetryable && request.retryCount < request.maxRetries) {
|
|
2022
2297
|
Log(COMMUNICATION, "handleGatewayAck(): Scheduling retry for msgId=%u (attempt %u/%u)\n",
|
|
2023
2298
|
ack.messageId, request.retryCount + 1, request.maxRetries);
|
|
2024
|
-
|
|
2299
|
+
request.lastError = result.error;
|
|
2300
|
+
scheduleInternetRetry(ack.messageId, minDelayMs);
|
|
2025
2301
|
} else {
|
|
2026
|
-
|
|
2027
|
-
if (request.retryCount >= request.maxRetries) {
|
|
2302
|
+
if (isRetryable) {
|
|
2028
2303
|
Log(ERROR, "handleGatewayAck(): Max retries reached for msgId=%u\n", ack.messageId);
|
|
2029
2304
|
} else {
|
|
2030
|
-
Log(COMMUNICATION, "handleGatewayAck():
|
|
2305
|
+
Log(COMMUNICATION, "handleGatewayAck(): Final failure for msgId=%u (HTTP %u)\n",
|
|
2031
2306
|
ack.messageId, ack.httpStatus);
|
|
2032
2307
|
}
|
|
2033
|
-
|
|
2034
|
-
if (request.callback) {
|
|
2035
|
-
request.callback(ack.success, ack.httpStatus, ack.error);
|
|
2036
|
-
}
|
|
2308
|
+
const PendingInternetRequest done = request;
|
|
2037
2309
|
pendingInternetRequests.erase(it);
|
|
2310
|
+
deliverInternetResult(done, result);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
/**
|
|
2315
|
+
* How an ack from a gateway that does not say whether to retry was read
|
|
2316
|
+
* before 2.1.0, minus retrying a 2xx.
|
|
2317
|
+
*/
|
|
2318
|
+
static bool legacyAckIsRetryable(const gateway::GatewayAckPackage& ack) {
|
|
2319
|
+
if (ack.httpStatus >= 500 && ack.httpStatus < 600) return true;
|
|
2320
|
+
if (ack.httpStatus == 429) return true;
|
|
2321
|
+
if (ack.httpStatus != 0) return false;
|
|
2322
|
+
// Gateway-level connectivity failures are infrastructure, not transient.
|
|
2323
|
+
static const char* const FINAL[] = {"Router has no internet",
|
|
2324
|
+
"Gateway WiFi not connected",
|
|
2325
|
+
"Captive portal detected"};
|
|
2326
|
+
for (const char* phrase : FINAL) {
|
|
2327
|
+
if (gateway::responseContains(ack.error, phrase)) return false;
|
|
2038
2328
|
}
|
|
2329
|
+
return true;
|
|
2039
2330
|
}
|
|
2040
2331
|
|
|
2041
2332
|
/**
|
|
@@ -2045,14 +2336,16 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2045
2336
|
* acknowledgment, pending-request tracking and callback semantics stay
|
|
2046
2337
|
* identical to a request served by a peer gateway.
|
|
2047
2338
|
*
|
|
2048
|
-
* The handler runs synchronously
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
2339
|
+
* The handler runs synchronously. sendGatewayAck() completes a local
|
|
2340
|
+
* acknowledgment from the scheduler, but a handler is free to do otherwise,
|
|
2341
|
+
* so callers must not hold a reference into pendingInternetRequests across
|
|
2342
|
+
* this call -- pass copies.
|
|
2051
2343
|
*/
|
|
2052
2344
|
void dispatchInternetRequestLocally(uint32_t messageId, uint8_t priority,
|
|
2053
2345
|
uint8_t retryCount,
|
|
2054
2346
|
const TSTRING& destination,
|
|
2055
|
-
const TSTRING& payload
|
|
2347
|
+
const TSTRING& payload,
|
|
2348
|
+
uint32_t requestNonce) {
|
|
2056
2349
|
gateway::GatewayDataPackage pkg;
|
|
2057
2350
|
pkg.from = this->nodeId;
|
|
2058
2351
|
pkg.dest = this->nodeId;
|
|
@@ -2065,6 +2358,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2065
2358
|
pkg.contentType = "application/json";
|
|
2066
2359
|
pkg.retryCount = retryCount;
|
|
2067
2360
|
pkg.requiresAck = true;
|
|
2361
|
+
pkg.requestNonce = requestNonce;
|
|
2068
2362
|
protocol::Variant variant(&pkg);
|
|
2069
2363
|
this->callbackList.execute(protocol::GATEWAY_DATA, variant, nullptr, 0);
|
|
2070
2364
|
}
|
|
@@ -2072,7 +2366,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2072
2366
|
/**
|
|
2073
2367
|
* Schedule retry for a failed Internet request
|
|
2074
2368
|
*/
|
|
2075
|
-
void scheduleInternetRetry(uint32_t messageId) {
|
|
2369
|
+
void scheduleInternetRetry(uint32_t messageId, uint32_t minDelayMs = 0) {
|
|
2076
2370
|
auto it = pendingInternetRequests.find(messageId);
|
|
2077
2371
|
if (it == pendingInternetRequests.end()) {
|
|
2078
2372
|
return;
|
|
@@ -2081,20 +2375,32 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2081
2375
|
PendingInternetRequest& request = it->second;
|
|
2082
2376
|
|
|
2083
2377
|
if (request.retryCount >= request.maxRetries) {
|
|
2084
|
-
// Max retries reached - fail the request
|
|
2378
|
+
// Max retries reached - fail the request, with the reason the retries
|
|
2379
|
+
// were spent on when there is one
|
|
2085
2380
|
Log(logger::ERROR, "scheduleInternetRetry(): Max retries reached for msgId=%u\n",
|
|
2086
2381
|
messageId);
|
|
2087
|
-
|
|
2088
|
-
request.callback(false, 0, "Max retries exceeded");
|
|
2089
|
-
}
|
|
2382
|
+
const PendingInternetRequest done = request;
|
|
2090
2383
|
pendingInternetRequests.erase(it);
|
|
2384
|
+
deliverInternetResult(done, failedInternetResult(
|
|
2385
|
+
done.lastError.length() > 0 ? done.lastError : TSTRING("Max retries exceeded")));
|
|
2091
2386
|
return;
|
|
2092
2387
|
}
|
|
2093
2388
|
|
|
2094
|
-
//
|
|
2389
|
+
// Exponential backoff, never shorter than what the server asked for
|
|
2095
2390
|
uint32_t delay = request.retryDelayMs * (1 << request.retryCount);
|
|
2391
|
+
if (delay < minDelayMs) delay = minDelayMs;
|
|
2096
2392
|
request.retryCount++;
|
|
2097
2393
|
|
|
2394
|
+
// A wait the server asked for moves the request's deadline with it:
|
|
2395
|
+
// otherwise a Retry-After longer than what is left of the request
|
|
2396
|
+
// timeout (30 s by default) times the request out before the retry the
|
|
2397
|
+
// server invited can run. The retry gets a full timeout of its own.
|
|
2398
|
+
if (minDelayMs > 0) {
|
|
2399
|
+
const uint32_t elapsed = static_cast<uint32_t>(millis()) - request.timestamp;
|
|
2400
|
+
const uint32_t needed = elapsed + delay + internetRequestTimeout;
|
|
2401
|
+
if (needed > request.timeoutMs) request.timeoutMs = needed;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2098
2404
|
Log(logger::COMMUNICATION, "scheduleInternetRetry(): Retry %u for msgId=%u in %u ms\n",
|
|
2099
2405
|
request.retryCount, messageId, delay);
|
|
2100
2406
|
|
|
@@ -2124,16 +2430,18 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2124
2430
|
//
|
|
2125
2431
|
// Copy what the redispatch needs first: the local handler runs
|
|
2126
2432
|
// synchronously and can erase this entry, leaving `request` dangling.
|
|
2127
|
-
if (request.gatewayNodeId == this->nodeId &&
|
|
2433
|
+
if (request.gatewayNodeId == this->nodeId && uplinkServesLocally()) {
|
|
2128
2434
|
const uint8_t priority = request.priority;
|
|
2129
2435
|
const uint8_t retryCount = request.retryCount;
|
|
2130
2436
|
const TSTRING destination = request.destination;
|
|
2131
2437
|
const TSTRING payload = request.payload;
|
|
2438
|
+
const uint32_t nonce = request.requestNonce;
|
|
2439
|
+
request.attempts++;
|
|
2132
2440
|
Log(logger::COMMUNICATION,
|
|
2133
2441
|
"retryInternetRequest(): Retrying msgId=%u on the local uplink\n",
|
|
2134
2442
|
messageId);
|
|
2135
2443
|
dispatchInternetRequestLocally(messageId, priority, retryCount,
|
|
2136
|
-
destination, payload);
|
|
2444
|
+
destination, payload, nonce);
|
|
2137
2445
|
return;
|
|
2138
2446
|
}
|
|
2139
2447
|
|
|
@@ -2171,6 +2479,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2171
2479
|
pkg.contentType = "application/json";
|
|
2172
2480
|
pkg.retryCount = request.retryCount;
|
|
2173
2481
|
pkg.requiresAck = true;
|
|
2482
|
+
pkg.requestNonce = request.requestNonce;
|
|
2174
2483
|
|
|
2175
2484
|
// Send the package
|
|
2176
2485
|
auto conn = painlessmesh::router::findRoute<T>((*this), gateway->nodeId);
|
|
@@ -2184,6 +2493,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2184
2493
|
Log(logger::ERROR, "retryInternetRequest(): Retry send failed msgId=%u\n", messageId);
|
|
2185
2494
|
scheduleInternetRetry(messageId);
|
|
2186
2495
|
} else {
|
|
2496
|
+
request.attempts++;
|
|
2187
2497
|
Log(logger::COMMUNICATION, "retryInternetRequest(): Retry sent msgId=%u to gateway %u\n",
|
|
2188
2498
|
messageId, gateway->nodeId);
|
|
2189
2499
|
}
|
|
@@ -2219,10 +2529,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2219
2529
|
if (request.isTimedOut()) {
|
|
2220
2530
|
Log(logger::ERROR, "checkInternetRequestTimeout(): Request timed out msgId=%u\n",
|
|
2221
2531
|
messageId);
|
|
2222
|
-
|
|
2223
|
-
request.callback(false, 0, "Request timed out");
|
|
2224
|
-
}
|
|
2532
|
+
const PendingInternetRequest done = request;
|
|
2225
2533
|
pendingInternetRequests.erase(it);
|
|
2534
|
+
deliverInternetResult(done, failedInternetResult("Request timed out"));
|
|
2226
2535
|
}
|
|
2227
2536
|
}
|
|
2228
2537
|
|
|
@@ -2235,9 +2544,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2235
2544
|
if (it->second.isTimedOut()) {
|
|
2236
2545
|
Log(logger::GENERAL, "cleanupTimedOutRequests(): Cleaning up msgId=%u\n",
|
|
2237
2546
|
it->first);
|
|
2238
|
-
|
|
2239
|
-
it->second.callback(false, 0, "Request timed out");
|
|
2240
|
-
}
|
|
2547
|
+
deliverInternetResult(it->second, failedInternetResult("Request timed out"));
|
|
2241
2548
|
it = pendingInternetRequests.erase(it);
|
|
2242
2549
|
} else {
|
|
2243
2550
|
++it;
|