@alteriom/painlessmesh 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +145 -0
- package/CONTRIBUTING.md +15 -0
- package/README.md +6 -2
- package/RELEASE_GUIDE.md +27 -5
- 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 +178 -77
- package/src/painlessmesh/callback.hpp +7 -0
- package/src/painlessmesh/gateway.hpp +335 -7
- package/src/painlessmesh/logger.hpp +14 -1
- package/src/painlessmesh/mesh.hpp +232 -20
- 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;
|
|
@@ -965,6 +996,301 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
965
996
|
|
|
966
997
|
};
|
|
967
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* @brief The outcome of a gateway HTTP request, classified for the ack
|
|
1001
|
+
*
|
|
1002
|
+
* HTTPClient::GET()/POST() return an int with two distinct meanings: a
|
|
1003
|
+
* positive value is an HTTP status code from the destination, while a
|
|
1004
|
+
* negative value is one of the client's own transport errors (HTTPC_ERROR_*,
|
|
1005
|
+
* e.g. -1 for a refused connection or -11 for a read timeout). Zero is not
|
|
1006
|
+
* produced by either.
|
|
1007
|
+
*
|
|
1008
|
+
* GatewayAckPackage::httpStatus is a uint16_t, so a negative code cannot be
|
|
1009
|
+
* forwarded as-is. Transport errors are reported as httpStatus 0, which is
|
|
1010
|
+
* what Mesh::handleGatewayAck() already treats as a retryable network error;
|
|
1011
|
+
* the human-readable cause travels in GatewayAckPackage::error instead.
|
|
1012
|
+
*/
|
|
1013
|
+
struct HttpRequestOutcome {
|
|
1014
|
+
/** True only for status codes that indicate genuine delivery. */
|
|
1015
|
+
bool success = false;
|
|
1016
|
+
|
|
1017
|
+
/** Value to place in GatewayAckPackage::httpStatus (0 for transport errors). */
|
|
1018
|
+
uint16_t ackStatus = 0;
|
|
1019
|
+
|
|
1020
|
+
/** True when the client failed before any HTTP status was received. */
|
|
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;
|
|
1042
|
+
};
|
|
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
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* @brief Classify an HTTPClient result for the gateway acknowledgment
|
|
1124
|
+
*
|
|
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.
|
|
1135
|
+
*
|
|
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
|
|
1138
|
+
* @return Classified outcome, safe to place in a GatewayAckPackage
|
|
1139
|
+
*/
|
|
1140
|
+
inline HttpRequestOutcome classifyHttpResult(int rawCode,
|
|
1141
|
+
const TSTRING& body = TSTRING()) {
|
|
1142
|
+
HttpRequestOutcome outcome;
|
|
1143
|
+
if (rawCode <= 0) {
|
|
1144
|
+
outcome.transportError = true;
|
|
1145
|
+
return outcome;
|
|
1146
|
+
}
|
|
1147
|
+
outcome.ackStatus =
|
|
1148
|
+
static_cast<uint16_t>(rawCode > 0xFFFF ? 0xFFFF : rawCode);
|
|
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
|
+
}
|
|
1161
|
+
return outcome;
|
|
1162
|
+
}
|
|
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
|
+
|
|
968
1294
|
/**
|
|
969
1295
|
* @brief Gateway Acknowledgment Package for delivery confirmations
|
|
970
1296
|
*
|
|
@@ -1214,13 +1540,15 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1214
1540
|
* GATEWAY_DNS_TIMEOUT_MS on ESP8266, skipped on ESP32) runs before them
|
|
1215
1541
|
* whenever the GATEWAY_CONNECTIVITY_CACHE_MS window has expired.
|
|
1216
1542
|
*
|
|
1217
|
-
* @warning One residual is not in this budget:
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
* network with blackholed DNS the request path can
|
|
1222
|
-
* exceed this ceiling on ESP32
|
|
1223
|
-
*
|
|
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".
|
|
1224
1552
|
*/
|
|
1225
1553
|
constexpr unsigned long gatewayBlockingBudgetMs() {
|
|
1226
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);
|