@alteriom/painlessmesh 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/CONTRIBUTING.md +15 -0
- package/README.md +4 -2
- package/RELEASE_GUIDE.md +11 -1
- package/examples/sendToInternet/README.md +3 -3
- package/examples/sendToInternet/sendToInternet.ino +58 -23
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/AlteriomPainlessMesh.h +2 -2
- package/src/arduino/wifi.hpp +144 -67
- package/src/painlessmesh/callback.hpp +7 -0
- package/src/painlessmesh/gateway.hpp +294 -14
- package/src/painlessmesh/logger.hpp +14 -1
- package/src/painlessmesh/mesh.hpp +167 -5
- package/src/painlessmesh/router.hpp +17 -7
|
@@ -568,6 +568,8 @@ class InternetHealthChecker {
|
|
|
568
568
|
* @return true if connection succeeded
|
|
569
569
|
*/
|
|
570
570
|
bool checkNow() {
|
|
571
|
+
// A full probe answers the question the on-demand budget exists for.
|
|
572
|
+
onDemandSpent_ = false;
|
|
571
573
|
status_.checkCount++;
|
|
572
574
|
status_.lastCheckTime = millis();
|
|
573
575
|
|
|
@@ -592,6 +594,27 @@ class InternetHealthChecker {
|
|
|
592
594
|
return connected;
|
|
593
595
|
}
|
|
594
596
|
|
|
597
|
+
/**
|
|
598
|
+
* @brief Probe once outside the schedule, for a caller that needs the
|
|
599
|
+
* answer now (issue #450)
|
|
600
|
+
*
|
|
601
|
+
* A bridge's first periodic probe runs while its station is still
|
|
602
|
+
* associating and fails, and the next is a full interval away. A send in
|
|
603
|
+
* that window may spend one extra probe to learn that the uplink has come
|
|
604
|
+
* up since. One: the probe is a blocking connect with a timeout, so the
|
|
605
|
+
* budget is a single on-demand probe per periodic one. A node whose uplink
|
|
606
|
+
* really is down pays it once per interval, not on every call.
|
|
607
|
+
*
|
|
608
|
+
* @return true if Internet is reachable now
|
|
609
|
+
*/
|
|
610
|
+
bool checkOnDemand() {
|
|
611
|
+
if (status_.available) return true;
|
|
612
|
+
if (onDemandSpent_) return false;
|
|
613
|
+
const bool connected = checkNow();
|
|
614
|
+
onDemandSpent_ = true;
|
|
615
|
+
return connected;
|
|
616
|
+
}
|
|
617
|
+
|
|
595
618
|
/**
|
|
596
619
|
* @brief Get check interval
|
|
597
620
|
* @return Check interval in milliseconds
|
|
@@ -667,6 +690,13 @@ class InternetHealthChecker {
|
|
|
667
690
|
status_.lastError = "Mock: No Internet in test environment";
|
|
668
691
|
return false;
|
|
669
692
|
#elif defined(ESP32) || defined(ESP8266)
|
|
693
|
+
// No station, no uplink: answer at once instead of spending the connect
|
|
694
|
+
// timeout on a link that does not exist. This is what keeps the
|
|
695
|
+
// on-demand probe cheap on a bridge whose router is down.
|
|
696
|
+
if (WiFi.status() != WL_CONNECTED) {
|
|
697
|
+
status_.lastError = "Station not connected";
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
670
700
|
WiFiClient client;
|
|
671
701
|
uint32_t started = millis();
|
|
672
702
|
#ifdef ESP32
|
|
@@ -704,6 +734,7 @@ class InternetHealthChecker {
|
|
|
704
734
|
// State
|
|
705
735
|
InternetStatus status_;
|
|
706
736
|
InternetChangedCallback_t connectivityChangedCallback_;
|
|
737
|
+
bool onDemandSpent_ = false;
|
|
707
738
|
|
|
708
739
|
#ifdef PAINLESSMESH_BOOST
|
|
709
740
|
bool mockConnected_ = false;
|
|
@@ -988,19 +1019,126 @@ struct HttpRequestOutcome {
|
|
|
988
1019
|
|
|
989
1020
|
/** True when the client failed before any HTTP status was received. */
|
|
990
1021
|
bool transportError = false;
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* True when the status was success-class but the response body said the
|
|
1025
|
+
* service refused the request (issue #450).
|
|
1026
|
+
*/
|
|
1027
|
+
bool refusedByBody = false;
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* True for a 2xx the gateway cannot vouch for: anything but 200, 201, 202
|
|
1031
|
+
* and 204. CallMeBot answers HTTP 208 to a message that never arrives
|
|
1032
|
+
* (issue #452), so a status outside the verified set is reported as a
|
|
1033
|
+
* failure that carries the body, never as a delivery.
|
|
1034
|
+
*/
|
|
1035
|
+
bool unverifiedStatus = false;
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* One-line reason for a failure, taken from the response body when there
|
|
1039
|
+
* is one; empty on success. Sized to fit a GatewayAckPackage::error.
|
|
1040
|
+
*/
|
|
1041
|
+
TSTRING reason;
|
|
991
1042
|
};
|
|
992
1043
|
|
|
1044
|
+
/** Longest response-body excerpt carried in an acknowledgment error. */
|
|
1045
|
+
static const size_t GATEWAY_RESPONSE_REASON_MAX = 120;
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* How much of a response body the gateway reads for classification and for
|
|
1049
|
+
* the error it reports. Enough for a service's one-line verdict, small enough
|
|
1050
|
+
* that an HTML error page cannot eat an ESP8266's heap.
|
|
1051
|
+
*/
|
|
1052
|
+
static const size_t GATEWAY_RESPONSE_HEAD_BYTES = 512;
|
|
1053
|
+
|
|
1054
|
+
/** How long the gateway waits for that head to arrive after the status. */
|
|
1055
|
+
static const uint32_t GATEWAY_RESPONSE_HEAD_TIMEOUT_MS = 250;
|
|
1056
|
+
|
|
1057
|
+
inline bool responseContains(const TSTRING& haystack, const char* needle) {
|
|
1058
|
+
#if defined(PAINLESSMESH_BOOST)
|
|
1059
|
+
return haystack.find(needle) != std::string::npos;
|
|
1060
|
+
#else
|
|
1061
|
+
return haystack.indexOf(needle) >= 0;
|
|
1062
|
+
#endif
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* @brief Reduce a response body to one line fit for a log or an error string
|
|
1067
|
+
*
|
|
1068
|
+
* Tags are dropped, whitespace collapsed and the result cut at maxLen with an
|
|
1069
|
+
* ellipsis, so an HTML error page reads "Oops! Too many requests... You have
|
|
1070
|
+
* called to the API to often." in a serial log instead of markup.
|
|
1071
|
+
*/
|
|
1072
|
+
inline TSTRING summarizeResponseBody(
|
|
1073
|
+
const TSTRING& body, size_t maxLen = GATEWAY_RESPONSE_REASON_MAX) {
|
|
1074
|
+
TSTRING out;
|
|
1075
|
+
bool inTag = false;
|
|
1076
|
+
bool pendingSpace = false;
|
|
1077
|
+
for (size_t i = 0; i < body.length(); ++i) {
|
|
1078
|
+
const char c = body[i];
|
|
1079
|
+
if (c == '<') {
|
|
1080
|
+
inTag = true;
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
if (c == '>') {
|
|
1084
|
+
inTag = false;
|
|
1085
|
+
pendingSpace = true;
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (inTag) continue;
|
|
1089
|
+
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
|
|
1090
|
+
pendingSpace = true;
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (pendingSpace && out.length() > 0) out += ' ';
|
|
1094
|
+
pendingSpace = false;
|
|
1095
|
+
out += c;
|
|
1096
|
+
if (out.length() >= maxLen) {
|
|
1097
|
+
out += "...";
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return out;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* @brief Does a success-class response body say the service refused the request?
|
|
1106
|
+
*
|
|
1107
|
+
* Some services answer a refusal with a 2xx. CallMeBot's WhatsApp API, which
|
|
1108
|
+
* the sendToInternet example integrates, returns its "Too many requests" page
|
|
1109
|
+
* under HTTP 201 and HTTP 203 (issue #450). The phrases are the ones seen
|
|
1110
|
+
* from services this library's examples target, and they are deliberately
|
|
1111
|
+
* specific: a JSON payload that merely contains the word "error" is not a
|
|
1112
|
+
* refusal.
|
|
1113
|
+
*/
|
|
1114
|
+
inline bool responseBodyRefuses(const TSTRING& body) {
|
|
1115
|
+
static const char* const REFUSALS[] = {"Too many requests", "Oops!"};
|
|
1116
|
+
for (const char* phrase : REFUSALS) {
|
|
1117
|
+
if (responseContains(body, phrase)) return true;
|
|
1118
|
+
}
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
993
1122
|
/**
|
|
994
|
-
* @brief Classify an HTTPClient
|
|
1123
|
+
* @brief Classify an HTTPClient result for the gateway acknowledgment
|
|
995
1124
|
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
1125
|
+
* A transport error (rawCode <= 0) is neither success nor an HTTP status.
|
|
1126
|
+
* Only 200, 201, 202 and 204 count as delivery on the status alone, and even
|
|
1127
|
+
* those are overturned by a body that says the service refused the request:
|
|
1128
|
+
* CallMeBot answers "Too many requests" under 201 and 203 (issue #450). Any
|
|
1129
|
+
* other 2xx is unverified. 203 means a proxy transformed the reply; 208 is
|
|
1130
|
+
* what CallMeBot answers to a message that never arrives (issue #452); none
|
|
1131
|
+
* of them is a delivery this library can vouch for, so they are failures
|
|
1132
|
+
* that carry the body. Every failure's reason is a one-line excerpt of the
|
|
1133
|
+
* body when there is one, so the origin node learns why and not only a
|
|
1134
|
+
* number.
|
|
999
1135
|
*
|
|
1000
1136
|
* @param rawCode The int returned by HTTPClient::GET() or ::POST()
|
|
1137
|
+
* @param body The start of the response body, empty if none was read
|
|
1001
1138
|
* @return Classified outcome, safe to place in a GatewayAckPackage
|
|
1002
1139
|
*/
|
|
1003
|
-
inline HttpRequestOutcome classifyHttpResult(int rawCode
|
|
1140
|
+
inline HttpRequestOutcome classifyHttpResult(int rawCode,
|
|
1141
|
+
const TSTRING& body = TSTRING()) {
|
|
1004
1142
|
HttpRequestOutcome outcome;
|
|
1005
1143
|
if (rawCode <= 0) {
|
|
1006
1144
|
outcome.transportError = true;
|
|
@@ -1008,11 +1146,151 @@ inline HttpRequestOutcome classifyHttpResult(int rawCode) {
|
|
|
1008
1146
|
}
|
|
1009
1147
|
outcome.ackStatus =
|
|
1010
1148
|
static_cast<uint16_t>(rawCode > 0xFFFF ? 0xFFFF : rawCode);
|
|
1011
|
-
|
|
1012
|
-
|
|
1149
|
+
const bool verified =
|
|
1150
|
+
rawCode == 200 || rawCode == 201 || rawCode == 202 || rawCode == 204;
|
|
1151
|
+
if (verified && responseBodyRefuses(body)) {
|
|
1152
|
+
outcome.refusedByBody = true;
|
|
1153
|
+
outcome.reason = summarizeResponseBody(body);
|
|
1154
|
+
return outcome;
|
|
1155
|
+
}
|
|
1156
|
+
outcome.success = verified;
|
|
1157
|
+
if (!verified) {
|
|
1158
|
+
outcome.unverifiedStatus = rawCode >= 200 && rawCode < 300;
|
|
1159
|
+
outcome.reason = summarizeResponseBody(body);
|
|
1160
|
+
}
|
|
1013
1161
|
return outcome;
|
|
1014
1162
|
}
|
|
1015
1163
|
|
|
1164
|
+
/**
|
|
1165
|
+
* The phrase a node puts in a failed GATEWAY_ACK when it received a gateway
|
|
1166
|
+
* request but serves none: a bridge that rebooted, crashed or was reflashed
|
|
1167
|
+
* as a regular node announces nothing, and its peers keep routing Internet
|
|
1168
|
+
* requests to it until its last bridge status ages out. The origin node
|
|
1169
|
+
* recognises the phrase, forgets that node as a gateway, and retries through
|
|
1170
|
+
* the next one. Changing it breaks that recognition across a mixed fleet.
|
|
1171
|
+
*/
|
|
1172
|
+
static const char GATEWAY_NOT_A_GATEWAY_PHRASE[] = "is not an Internet gateway";
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* How long a destination whose name failed to resolve is refused without
|
|
1176
|
+
* another lookup (issue #453). On ESP32 a DNS lookup has no timeout this
|
|
1177
|
+
* library can set, so a dead name stalls the cooperative scheduler for the
|
|
1178
|
+
* resolver's own patience; once a minute is survivable, once per attempt --
|
|
1179
|
+
* with the origin node retrying and the bridge's own sends adding theirs --
|
|
1180
|
+
* was not. On ESP8266 the core bounds HTTPClient's own lookup by the HTTP
|
|
1181
|
+
* timeout, so the gateway performs no separate lookup there and this cache
|
|
1182
|
+
* is never fed.
|
|
1183
|
+
*/
|
|
1184
|
+
static const uint32_t GATEWAY_DNS_NEGATIVE_TTL_MS = 60000;
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* @brief The host of an http(s) URL, without scheme, port, path or query
|
|
1188
|
+
*
|
|
1189
|
+
* "https://api.example.com:8443/x?y" -> "api.example.com". Empty when the
|
|
1190
|
+
* URL has no host.
|
|
1191
|
+
*/
|
|
1192
|
+
inline TSTRING hostFromUrl(const TSTRING& url) {
|
|
1193
|
+
const char* s = url.c_str();
|
|
1194
|
+
const size_t n = url.length();
|
|
1195
|
+
size_t i = 0;
|
|
1196
|
+
for (size_t k = 0; k + 2 < n; ++k) {
|
|
1197
|
+
if (s[k] == ':' && s[k + 1] == '/' && s[k + 2] == '/') {
|
|
1198
|
+
i = k + 3;
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
size_t j = i;
|
|
1203
|
+
while (j < n && s[j] != '/' && s[j] != '?' && s[j] != '#' && s[j] != ':') ++j;
|
|
1204
|
+
TSTRING host;
|
|
1205
|
+
for (size_t k = i; k < j; ++k) host += s[k];
|
|
1206
|
+
return host;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/** True for a dotted-decimal IPv4 literal, which needs no DNS. */
|
|
1210
|
+
inline bool looksLikeIpLiteral(const TSTRING& host) {
|
|
1211
|
+
if (host.length() == 0) return false;
|
|
1212
|
+
for (size_t i = 0; i < host.length(); ++i) {
|
|
1213
|
+
const char c = host[i];
|
|
1214
|
+
if (!(c >= '0' && c <= '9') && c != '.') return false;
|
|
1215
|
+
}
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* @brief A few hosts that recently failed to resolve, and when
|
|
1221
|
+
*
|
|
1222
|
+
* Small and fixed: a gateway talks to a handful of destinations. A host is
|
|
1223
|
+
* remembered with a TTL; while it is within it, isFailing() says so and the
|
|
1224
|
+
* caller answers the request without a lookup.
|
|
1225
|
+
*/
|
|
1226
|
+
class NegativeDnsCache {
|
|
1227
|
+
public:
|
|
1228
|
+
// An enum, not a static const member: the test suite passes it to Catch2 by
|
|
1229
|
+
// reference, which needs a definition C++14 cannot give an in-class
|
|
1230
|
+
// constant without an out-of-line one.
|
|
1231
|
+
enum : size_t { SLOTS = 4 };
|
|
1232
|
+
|
|
1233
|
+
void remember(const TSTRING& host, uint32_t nowMs,
|
|
1234
|
+
uint32_t ttlMs = GATEWAY_DNS_NEGATIVE_TTL_MS) {
|
|
1235
|
+
Entry* slot = find(host);
|
|
1236
|
+
if (slot == nullptr) {
|
|
1237
|
+
slot = &entries_[0];
|
|
1238
|
+
for (size_t i = 0; i < SLOTS; ++i) {
|
|
1239
|
+
if (!entries_[i].used) {
|
|
1240
|
+
slot = &entries_[i];
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
if (static_cast<int32_t>(entries_[i].at - slot->at) < 0) slot = &entries_[i];
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
slot->used = true;
|
|
1247
|
+
slot->host = host;
|
|
1248
|
+
slot->at = nowMs;
|
|
1249
|
+
slot->ttl = ttlMs;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/** Is `host` inside its negative TTL? `ageMs` receives how long ago it failed. */
|
|
1253
|
+
bool isFailing(const TSTRING& host, uint32_t nowMs, uint32_t* ageMs = nullptr) {
|
|
1254
|
+
Entry* slot = find(host);
|
|
1255
|
+
if (slot == nullptr) return false;
|
|
1256
|
+
const uint32_t age = nowMs - slot->at;
|
|
1257
|
+
if (age >= slot->ttl) {
|
|
1258
|
+
slot->used = false;
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
if (ageMs != nullptr) *ageMs = age;
|
|
1262
|
+
return true;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
void forget(const TSTRING& host) {
|
|
1266
|
+
Entry* slot = find(host);
|
|
1267
|
+
if (slot != nullptr) slot->used = false;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
size_t size() const {
|
|
1271
|
+
size_t n = 0;
|
|
1272
|
+
for (size_t i = 0; i < SLOTS; ++i) n += entries_[i].used ? 1 : 0;
|
|
1273
|
+
return n;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private:
|
|
1277
|
+
struct Entry {
|
|
1278
|
+
TSTRING host;
|
|
1279
|
+
uint32_t at = 0;
|
|
1280
|
+
uint32_t ttl = 0;
|
|
1281
|
+
bool used = false;
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
Entry* find(const TSTRING& host) {
|
|
1285
|
+
for (size_t i = 0; i < SLOTS; ++i) {
|
|
1286
|
+
if (entries_[i].used && entries_[i].host == host) return &entries_[i];
|
|
1287
|
+
}
|
|
1288
|
+
return nullptr;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
Entry entries_[SLOTS];
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1016
1294
|
/**
|
|
1017
1295
|
* @brief Gateway Acknowledgment Package for delivery confirmations
|
|
1018
1296
|
*
|
|
@@ -1262,13 +1540,15 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1262
1540
|
* GATEWAY_DNS_TIMEOUT_MS on ESP8266, skipped on ESP32) runs before them
|
|
1263
1541
|
* whenever the GATEWAY_CONNECTIVITY_CACHE_MS window has expired.
|
|
1264
1542
|
*
|
|
1265
|
-
* @warning One residual is not in this budget:
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
* network with blackholed DNS the request path can
|
|
1270
|
-
* exceed this ceiling on ESP32
|
|
1271
|
-
*
|
|
1543
|
+
* @warning One residual is not in this budget: hostname resolution on ESP32
|
|
1544
|
+
* is not separately boundable in the cores this library targets.
|
|
1545
|
+
* Since issue #453 the handler performs that lookup itself on
|
|
1546
|
+
* ESP32 and remembers a failure for GATEWAY_DNS_NEGATIVE_TTL_MS, so
|
|
1547
|
+
* on a network with blackholed DNS the request path can still
|
|
1548
|
+
* exceed this ceiling on ESP32, but at most once per TTL per
|
|
1549
|
+
* destination host rather than once per attempt. On ESP8266 the
|
|
1550
|
+
* core bounds HTTPClient's own lookup by the HTTP timeout. See
|
|
1551
|
+
* SECURITY.md "Gateway blocking: the mesh partition risk".
|
|
1272
1552
|
*/
|
|
1273
1553
|
constexpr unsigned long gatewayBlockingBudgetMs() {
|
|
1274
1554
|
return 2UL * static_cast<unsigned long>(GATEWAY_HTTP_TIMEOUT_MS) +
|
|
@@ -30,6 +30,19 @@ typedef enum {
|
|
|
30
30
|
DEBUG = 1 << 11
|
|
31
31
|
} LogLevel;
|
|
32
32
|
|
|
33
|
+
// Log() is printf-like. In the desktop test build the compiler is told so,
|
|
34
|
+
// and -Wall -Werror in CI then rejects any argument that does not match its
|
|
35
|
+
// specifier. That class of bug reached main twice in routePackage(): size_t
|
|
36
|
+
// through %d/%u, and a DeserializationError object through %u (CodeQL
|
|
37
|
+
// cpp/wrong-type-format-argument). The check is kept to the desktop build on
|
|
38
|
+
// purpose: ESP-IDF 5 makes uint32_t `unsigned long`, so on ESP32 every %u of
|
|
39
|
+
// a node id would warn, harmlessly, in every user's build.
|
|
40
|
+
#if defined(PAINLESSMESH_BOOST) && defined(__GNUC__)
|
|
41
|
+
#define PAINLESSMESH_LOG_FORMAT __attribute__((format(printf, 3, 4)))
|
|
42
|
+
#else
|
|
43
|
+
#define PAINLESSMESH_LOG_FORMAT
|
|
44
|
+
#endif
|
|
45
|
+
|
|
33
46
|
class LogClass {
|
|
34
47
|
public:
|
|
35
48
|
// Where messages go instead of Serial. The sketch owns framing: on a board
|
|
@@ -83,7 +96,7 @@ class LogClass {
|
|
|
83
96
|
Serial.println();
|
|
84
97
|
return;
|
|
85
98
|
}
|
|
86
|
-
void operator()(LogLevel type, const char *format...) {
|
|
99
|
+
void operator()(LogLevel type, const char *format...) PAINLESSMESH_LOG_FORMAT {
|
|
87
100
|
if (type & types) { // Print only the message types set for output
|
|
88
101
|
va_list args;
|
|
89
102
|
va_start(args, format);
|
|
@@ -98,6 +98,9 @@ struct PendingInternetRequest {
|
|
|
98
98
|
TSTRING destination = ""; ///< Internet destination URL
|
|
99
99
|
TSTRING payload = ""; ///< Request payload
|
|
100
100
|
internetResultCallback_t callback; ///< User callback for result
|
|
101
|
+
/// Why the last attempt failed, when retries were spent waiting for a
|
|
102
|
+
/// gateway; the final callback reports it instead of a bare retry count.
|
|
103
|
+
TSTRING lastError = "";
|
|
101
104
|
|
|
102
105
|
/**
|
|
103
106
|
* Check if this request has timed out
|
|
@@ -243,6 +246,37 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
243
246
|
painlessmesh::ntp::addPackageCallback(this->callbackList, (*this));
|
|
244
247
|
painlessmesh::router::addPackageCallback(this->callbackList, (*this));
|
|
245
248
|
|
|
249
|
+
// Every node answers a gateway request it cannot serve. A bridge that
|
|
250
|
+
// reboots, crashes or is reflashed as a regular node announces nothing
|
|
251
|
+
// -- only a bridge stepping down in-process sends `leaving` -- so its
|
|
252
|
+
// peers keep it in knownBridges until its last status ages out
|
|
253
|
+
// (bridgeTimeoutMs, 60 s) and may route every Internet request to it.
|
|
254
|
+
// A regular node had no GATEWAY_DATA handler and dropped them without a
|
|
255
|
+
// word; the origin waited out its request timeout, 30 s, each time.
|
|
256
|
+
// Found on the HIL rig while validating 2.0.3.
|
|
257
|
+
//
|
|
258
|
+
// Registered first, so a gateway's own handler (initGatewayInternetHandler
|
|
259
|
+
// in wifi.hpp, or a test's stand-in) is always the second: this one
|
|
260
|
+
// stays silent whenever any other GATEWAY_DATA handler exists.
|
|
261
|
+
this->callbackList.onPackage(
|
|
262
|
+
protocol::GATEWAY_DATA,
|
|
263
|
+
[this](protocol::Variant& variant, std::shared_ptr<T> ingress,
|
|
264
|
+
uint32_t) {
|
|
265
|
+
if (this->callbackList.count(protocol::GATEWAY_DATA) > 1) {
|
|
266
|
+
return false; // this node is a gateway; its handler answers
|
|
267
|
+
}
|
|
268
|
+
auto pkg = variant.to<gateway::GatewayDataPackage>();
|
|
269
|
+
char errorBuf[96];
|
|
270
|
+
snprintf(errorBuf, sizeof(errorBuf), "Node %u %s",
|
|
271
|
+
static_cast<unsigned>(this->nodeId),
|
|
272
|
+
gateway::GATEWAY_NOT_A_GATEWAY_PHRASE);
|
|
273
|
+
Log(logger::ERROR,
|
|
274
|
+
"GATEWAY_DATA from node %u answered: %s\n",
|
|
275
|
+
static_cast<unsigned>(pkg.originNode), errorBuf);
|
|
276
|
+
this->sendGatewayAck(pkg, false, 0, TSTRING(errorBuf), ingress);
|
|
277
|
+
return true;
|
|
278
|
+
});
|
|
279
|
+
|
|
246
280
|
// Seed delivery-confirmation ids with a random value so they do not
|
|
247
281
|
// restart at 1 after a reboot — a delayed ACK for a pre-reboot
|
|
248
282
|
// message could otherwise match a fresh message's id and report a
|
|
@@ -1349,7 +1383,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1349
1383
|
|
|
1350
1384
|
size_t removed = sizeBefore - knownBridges.size();
|
|
1351
1385
|
if (removed > 0) {
|
|
1352
|
-
Log(GENERAL, "cleanupExpiredBridges(): Removed %
|
|
1386
|
+
Log(GENERAL, "cleanupExpiredBridges(): Removed %lu expired bridges\n",
|
|
1387
|
+
static_cast<unsigned long>(removed));
|
|
1353
1388
|
}
|
|
1354
1389
|
}
|
|
1355
1390
|
|
|
@@ -1432,6 +1467,31 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1432
1467
|
return internetHealthChecker.hasLocalInternet();
|
|
1433
1468
|
}
|
|
1434
1469
|
|
|
1470
|
+
/**
|
|
1471
|
+
* Can this node serve a gateway request on its own uplink right now?
|
|
1472
|
+
*
|
|
1473
|
+
* hasLocalInternet() is the last periodic probe's answer. On a bridge the
|
|
1474
|
+
* first probe runs while the station is still associating and fails, and
|
|
1475
|
+
* the next is a full interval (30 s) away, so for that whole window a send
|
|
1476
|
+
* fell through to the mesh path and was refused for want of a peer (issue
|
|
1477
|
+
* #450). When the flag says no and the health check is running, spend the
|
|
1478
|
+
* one on-demand probe the checker allows per period: a send made after the
|
|
1479
|
+
* uplink came up is then served locally, and a node whose uplink really is
|
|
1480
|
+
* down pays one probe per interval, not one per call.
|
|
1481
|
+
*
|
|
1482
|
+
* @return true if the request should go to this node's own gateway handler
|
|
1483
|
+
*/
|
|
1484
|
+
bool uplinkServesLocally() {
|
|
1485
|
+
if (hasLocalInternet()) return true;
|
|
1486
|
+
if (!isInternetHealthCheckEnabled()) return false;
|
|
1487
|
+
if (internetHealthChecker.checkOnDemand()) {
|
|
1488
|
+
Log(logger::COMMUNICATION,
|
|
1489
|
+
"uplinkServesLocally(): on-demand probe found the uplink up\n");
|
|
1490
|
+
return true;
|
|
1491
|
+
}
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1435
1495
|
/**
|
|
1436
1496
|
* Register callback for local Internet connectivity changes
|
|
1437
1497
|
*
|
|
@@ -1672,7 +1732,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1672
1732
|
// A shared gateway can execute its own gateway package locally. Keep the
|
|
1673
1733
|
// same package handler, pending-request tracking and callback semantics,
|
|
1674
1734
|
// but do not require a mesh peer merely to reach this node's own uplink.
|
|
1675
|
-
if (
|
|
1735
|
+
if (uplinkServesLocally()) {
|
|
1676
1736
|
PendingInternetRequest request;
|
|
1677
1737
|
request.messageId = messageId;
|
|
1678
1738
|
request.timestamp = millis();
|
|
@@ -1935,6 +1995,65 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1935
1995
|
/**
|
|
1936
1996
|
* Handle incoming gateway acknowledgment package
|
|
1937
1997
|
*/
|
|
1998
|
+
public:
|
|
1999
|
+
// Public: the gateway handler in wifi.hpp, a derived class, answers with it.
|
|
2000
|
+
/**
|
|
2001
|
+
* Answer a gateway request: send its GATEWAY_ACK back to the origin node.
|
|
2002
|
+
*
|
|
2003
|
+
* A request this node originated completes locally, without a mesh hop.
|
|
2004
|
+
* Otherwise the ack is routed to the origin; when the NodeTree has not yet
|
|
2005
|
+
* converged enough for findRoute() to resolve it -- a freshly promoted
|
|
2006
|
+
* gateway can receive data before that -- the connection the request
|
|
2007
|
+
* arrived on is a valid reverse path, since every intermediate node routes
|
|
2008
|
+
* the addressed ack onward toward the origin.
|
|
2009
|
+
*
|
|
2010
|
+
* Used by the gateway's own handler (wifi.hpp) and by the fallback every
|
|
2011
|
+
* node registers for requests it cannot serve.
|
|
2012
|
+
*/
|
|
2013
|
+
void sendGatewayAck(const gateway::GatewayDataPackage& request, bool success,
|
|
2014
|
+
uint16_t httpStatus, const TSTRING& error,
|
|
2015
|
+
std::shared_ptr<T> ingressConnection = nullptr) {
|
|
2016
|
+
using namespace logger;
|
|
2017
|
+
|
|
2018
|
+
gateway::GatewayAckPackage ack;
|
|
2019
|
+
ack.from = this->nodeId;
|
|
2020
|
+
ack.dest = request.originNode;
|
|
2021
|
+
ack.messageId = request.messageId;
|
|
2022
|
+
ack.originNode = request.originNode;
|
|
2023
|
+
ack.success = success;
|
|
2024
|
+
ack.httpStatus = httpStatus;
|
|
2025
|
+
ack.error = error;
|
|
2026
|
+
ack.timestamp = this->getNodeTime();
|
|
2027
|
+
|
|
2028
|
+
if (request.originNode == this->nodeId) {
|
|
2029
|
+
protocol::Variant variant(&ack);
|
|
2030
|
+
this->callbackList.execute(protocol::GATEWAY_ACK, variant, nullptr, 0);
|
|
2031
|
+
Log(COMMUNICATION,
|
|
2032
|
+
"Completed local GATEWAY_ACK (success=%d, http=%d)\n", success,
|
|
2033
|
+
httpStatus);
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
auto conn = router::findRoute<T>((*this), request.originNode);
|
|
2038
|
+
if (!conn && ingressConnection) {
|
|
2039
|
+
conn = ingressConnection;
|
|
2040
|
+
Log(COMMUNICATION,
|
|
2041
|
+
"Routing GATEWAY_ACK to node %u through request ingress while "
|
|
2042
|
+
"topology converges\n",
|
|
2043
|
+
request.originNode);
|
|
2044
|
+
}
|
|
2045
|
+
if (conn) {
|
|
2046
|
+
protocol::Variant variant(&ack);
|
|
2047
|
+
router::send(std::move(variant), conn);
|
|
2048
|
+
Log(COMMUNICATION, "Sent GATEWAY_ACK to node %u (success=%d, http=%d)\n",
|
|
2049
|
+
request.originNode, success, httpStatus);
|
|
2050
|
+
} else {
|
|
2051
|
+
Log(ERROR, "Failed to send GATEWAY_ACK: no route to node %u\n",
|
|
2052
|
+
request.originNode);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
private:
|
|
1938
2057
|
void handleGatewayAck(const gateway::GatewayAckPackage& ack) {
|
|
1939
2058
|
using namespace logger;
|
|
1940
2059
|
Log(COMMUNICATION, "handleGatewayAck(): msgId=%u success=%d http=%u\n",
|
|
@@ -1958,6 +2077,46 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1958
2077
|
return;
|
|
1959
2078
|
}
|
|
1960
2079
|
|
|
2080
|
+
// The node this request went to is not a gateway (issue found on the
|
|
2081
|
+
// HIL rig while validating 2.0.3): it rebooted or was reflashed as a
|
|
2082
|
+
// regular node without announcing it, and its last bridge status is
|
|
2083
|
+
// still inside bridgeTimeoutMs. Forget it and send again through the
|
|
2084
|
+
// next gateway at once. The retry budget is not spent: nothing was
|
|
2085
|
+
// attempted against the Internet, and every such answer removes one
|
|
2086
|
+
// entry from knownBridges, so this cannot loop.
|
|
2087
|
+
if (gateway::responseContains(ack.error,
|
|
2088
|
+
gateway::GATEWAY_NOT_A_GATEWAY_PHRASE)) {
|
|
2089
|
+
const uint32_t staleGateway = request.gatewayNodeId;
|
|
2090
|
+
bool forgot = this->forgetBridge(staleGateway);
|
|
2091
|
+
if (ack.from != staleGateway) forgot = this->forgetBridge(ack.from) || forgot;
|
|
2092
|
+
Log(ERROR, "handleGatewayAck(): %s; forgot it as a gateway for msgId=%u\n",
|
|
2093
|
+
ack.error.c_str(), ack.messageId);
|
|
2094
|
+
|
|
2095
|
+
if (!forgot) {
|
|
2096
|
+
// Nothing to forget, so a redispatch would reach the same node.
|
|
2097
|
+
// Fall through to the ordinary failure path below.
|
|
2098
|
+
} else if (this->getPrimaryBridge() == nullptr &&
|
|
2099
|
+
!this->uplinkServesLocally()) {
|
|
2100
|
+
// No gateway left right now. On the rig the live bridge's first
|
|
2101
|
+
// status arrived half a second after the ex-bridge answered, so an
|
|
2102
|
+
// accepted request rides the ordinary retry backoff rather than
|
|
2103
|
+
// failing on the spot; retryInternetRequest() reschedules while no
|
|
2104
|
+
// gateway is known, and the final callback carries this reason.
|
|
2105
|
+
char errorBuf[160];
|
|
2106
|
+
snprintf(errorBuf, sizeof(errorBuf),
|
|
2107
|
+
"No Internet gateway available: %s", ack.error.c_str());
|
|
2108
|
+
request.lastError = TSTRING(errorBuf);
|
|
2109
|
+
scheduleInternetRetry(ack.messageId);
|
|
2110
|
+
return;
|
|
2111
|
+
} else {
|
|
2112
|
+
const uint32_t messageId = ack.messageId;
|
|
2113
|
+
this->addTask([this, messageId]() {
|
|
2114
|
+
this->retryInternetRequest(messageId);
|
|
2115
|
+
});
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
1961
2120
|
// Failure response - determine if retryable
|
|
1962
2121
|
bool isRetryable = false;
|
|
1963
2122
|
|
|
@@ -2081,11 +2240,14 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2081
2240
|
PendingInternetRequest& request = it->second;
|
|
2082
2241
|
|
|
2083
2242
|
if (request.retryCount >= request.maxRetries) {
|
|
2084
|
-
// Max retries reached - fail the request
|
|
2243
|
+
// Max retries reached - fail the request, with the reason the retries
|
|
2244
|
+
// were spent on when there is one
|
|
2085
2245
|
Log(logger::ERROR, "scheduleInternetRetry(): Max retries reached for msgId=%u\n",
|
|
2086
2246
|
messageId);
|
|
2087
2247
|
if (request.callback) {
|
|
2088
|
-
request.callback(false, 0,
|
|
2248
|
+
request.callback(false, 0,
|
|
2249
|
+
request.lastError.length() > 0 ? request.lastError
|
|
2250
|
+
: TSTRING("Max retries exceeded"));
|
|
2089
2251
|
}
|
|
2090
2252
|
pendingInternetRequests.erase(it);
|
|
2091
2253
|
return;
|
|
@@ -2124,7 +2286,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
2124
2286
|
//
|
|
2125
2287
|
// Copy what the redispatch needs first: the local handler runs
|
|
2126
2288
|
// synchronously and can erase this entry, leaving `request` dangling.
|
|
2127
|
-
if (request.gatewayNodeId == this->nodeId &&
|
|
2289
|
+
if (request.gatewayNodeId == this->nodeId && uplinkServesLocally()) {
|
|
2128
2290
|
const uint8_t priority = request.priority;
|
|
2129
2291
|
const uint8_t retryCount = request.retryCount;
|
|
2130
2292
|
const TSTRING destination = request.destination;
|
|
@@ -204,9 +204,14 @@ void routePackage(const layout::Layout<T>& layout,
|
|
|
204
204
|
#if ARDUINOJSON_VERSION_MAJOR == 7
|
|
205
205
|
protocol::Variant variant(pkg);
|
|
206
206
|
if (variant.error) {
|
|
207
|
+
// The error is a DeserializationError object, not an integer: passing it
|
|
208
|
+
// through varargs is undefined behaviour. size_t is unsigned long on the
|
|
209
|
+
// 64-bit desktop build and unsigned int on ESP32/ESP8266, so it is cast
|
|
210
|
+
// to one type and printed with the matching specifier.
|
|
207
211
|
Log(ERROR,
|
|
208
|
-
"routePackage(): parsing failed. err=%
|
|
209
|
-
variant.error
|
|
212
|
+
"routePackage(): parsing failed. err=%s, total_length=%lu, data=%s<--\n",
|
|
213
|
+
variant.error.c_str(), static_cast<unsigned long>(pkg.length()),
|
|
214
|
+
pkg.c_str());
|
|
210
215
|
return;
|
|
211
216
|
}
|
|
212
217
|
|
|
@@ -248,15 +253,20 @@ void routePackage(const layout::Layout<T>& layout,
|
|
|
248
253
|
auto variant = std::make_shared<protocol::Variant>(pkg, capacity);
|
|
249
254
|
|
|
250
255
|
if (variant->error) {
|
|
256
|
+
// Same two defects as the ArduinoJson 7 branch above (CodeQL
|
|
257
|
+
// cpp/wrong-type-format-argument, alerts #6-#9).
|
|
251
258
|
if (variant->error == DeserializationError::NoMemory) {
|
|
252
259
|
Log(ERROR,
|
|
253
|
-
"routePackage(): Message too large. length=%
|
|
254
|
-
"nesting_depth=%
|
|
255
|
-
pkg.length(),
|
|
260
|
+
"routePackage(): Message too large. length=%lu, calculated_capacity=%lu, "
|
|
261
|
+
"nesting_depth=%lu. Consider increasing MAX_MESSAGE_CAPACITY if needed.\n",
|
|
262
|
+
static_cast<unsigned long>(pkg.length()),
|
|
263
|
+
static_cast<unsigned long>(calculatedCapacity),
|
|
264
|
+
static_cast<unsigned long>(nestingDepth));
|
|
256
265
|
} else {
|
|
257
266
|
Log(ERROR,
|
|
258
|
-
"routePackage(): parsing failed. err=%
|
|
259
|
-
variant->error
|
|
267
|
+
"routePackage(): parsing failed. err=%s, length=%lu, data=%s<--\n",
|
|
268
|
+
variant->error.c_str(), static_cast<unsigned long>(pkg.length()),
|
|
269
|
+
pkg.c_str());
|
|
260
270
|
}
|
|
261
271
|
return;
|
|
262
272
|
}
|