@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.
@@ -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 %u expired bridges\n", 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
  *
@@ -1546,8 +1606,17 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1546
1606
  void enableInternetHealthCheck() {
1547
1607
  using namespace logger;
1548
1608
  if (internetHealthCheckTask != nullptr) {
1549
- Log(GENERAL, "enableInternetHealthCheck(): Already enabled\n");
1550
- return;
1609
+ if (internetHealthCheckTask->isEnabled()) {
1610
+ Log(GENERAL, "enableInternetHealthCheck(): Already enabled\n");
1611
+ return;
1612
+ }
1613
+ // A stop()/re-init cycle -- what bridge promotion does before calling
1614
+ // initAsBridge() again -- disables every task and drops it from the
1615
+ // reusable pool, but this member still points at it. Refusing here
1616
+ // would leave a re-initialised node with no health check at all, and
1617
+ // so with hasLocalInternet() stuck false (issue #445). Re-arm instead,
1618
+ // releasing the spent task so the pool can reclaim it.
1619
+ internetHealthCheckTask = nullptr;
1551
1620
  }
1552
1621
 
1553
1622
  Log(GENERAL, "enableInternetHealthCheck(): Starting health check task (interval: %u ms)\n",
@@ -1663,7 +1732,7 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1663
1732
  // A shared gateway can execute its own gateway package locally. Keep the
1664
1733
  // same package handler, pending-request tracking and callback semantics,
1665
1734
  // but do not require a mesh peer merely to reach this node's own uplink.
1666
- if (hasLocalInternet()) {
1735
+ if (uplinkServesLocally()) {
1667
1736
  PendingInternetRequest request;
1668
1737
  request.messageId = messageId;
1669
1738
  request.timestamp = millis();
@@ -1678,20 +1747,8 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1678
1747
  request.callback = callback;
1679
1748
  pendingInternetRequests[messageId] = request;
1680
1749
 
1681
- gateway::GatewayDataPackage pkg;
1682
- pkg.from = this->nodeId;
1683
- pkg.dest = this->nodeId;
1684
- pkg.messageId = messageId;
1685
- pkg.originNode = this->nodeId;
1686
- pkg.timestamp = this->getNodeTime();
1687
- pkg.priority = priority;
1688
- pkg.destination = destination;
1689
- pkg.payload = payload;
1690
- pkg.contentType = "application/json";
1691
- pkg.retryCount = 0;
1692
- pkg.requiresAck = true;
1693
- protocol::Variant variant(&pkg);
1694
- this->callbackList.execute(protocol::GATEWAY_DATA, variant, nullptr, 0);
1750
+ dispatchInternetRequestLocally(messageId, priority, 0, destination,
1751
+ payload);
1695
1752
  return messageId;
1696
1753
  }
1697
1754
 
@@ -1938,6 +1995,65 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1938
1995
  /**
1939
1996
  * Handle incoming gateway acknowledgment package
1940
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:
1941
2057
  void handleGatewayAck(const gateway::GatewayAckPackage& ack) {
1942
2058
  using namespace logger;
1943
2059
  Log(COMMUNICATION, "handleGatewayAck(): msgId=%u success=%d http=%u\n",
@@ -1961,6 +2077,46 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1961
2077
  return;
1962
2078
  }
1963
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
+
1964
2120
  // Failure response - determine if retryable
1965
2121
  bool isRetryable = false;
1966
2122
 
@@ -2041,6 +2197,37 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
2041
2197
  }
2042
2198
  }
2043
2199
 
2200
+ /**
2201
+ * Run a gateway request against this node's own uplink
2202
+ *
2203
+ * The package goes to this node's own GATEWAY_DATA handler, so the
2204
+ * acknowledgment, pending-request tracking and callback semantics stay
2205
+ * identical to a request served by a peer gateway.
2206
+ *
2207
+ * The handler runs synchronously and its acknowledgment can erase the
2208
+ * pending entry, so callers must not hold a reference into
2209
+ * pendingInternetRequests across this call -- pass copies.
2210
+ */
2211
+ void dispatchInternetRequestLocally(uint32_t messageId, uint8_t priority,
2212
+ uint8_t retryCount,
2213
+ const TSTRING& destination,
2214
+ const TSTRING& payload) {
2215
+ gateway::GatewayDataPackage pkg;
2216
+ pkg.from = this->nodeId;
2217
+ pkg.dest = this->nodeId;
2218
+ pkg.messageId = messageId;
2219
+ pkg.originNode = this->nodeId;
2220
+ pkg.timestamp = this->getNodeTime();
2221
+ pkg.priority = priority;
2222
+ pkg.destination = destination;
2223
+ pkg.payload = payload;
2224
+ pkg.contentType = "application/json";
2225
+ pkg.retryCount = retryCount;
2226
+ pkg.requiresAck = true;
2227
+ protocol::Variant variant(&pkg);
2228
+ this->callbackList.execute(protocol::GATEWAY_DATA, variant, nullptr, 0);
2229
+ }
2230
+
2044
2231
  /**
2045
2232
  * Schedule retry for a failed Internet request
2046
2233
  */
@@ -2053,11 +2240,14 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
2053
2240
  PendingInternetRequest& request = it->second;
2054
2241
 
2055
2242
  if (request.retryCount >= request.maxRetries) {
2056
- // 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
2057
2245
  Log(logger::ERROR, "scheduleInternetRetry(): Max retries reached for msgId=%u\n",
2058
2246
  messageId);
2059
2247
  if (request.callback) {
2060
- request.callback(false, 0, "Max retries exceeded");
2248
+ request.callback(false, 0,
2249
+ request.lastError.length() > 0 ? request.lastError
2250
+ : TSTRING("Max retries exceeded"));
2061
2251
  }
2062
2252
  pendingInternetRequests.erase(it);
2063
2253
  return;
@@ -2087,6 +2277,28 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
2087
2277
 
2088
2278
  PendingInternetRequest& request = it->second;
2089
2279
 
2280
+ // A request this node is serving from its own uplink never had a mesh
2281
+ // hop to repeat. The routing path below needs an active peer and a
2282
+ // discovered bridge, so a lone gateway would burn every retry without
2283
+ // issuing a single HTTP request and then report "Max retries exceeded"
2284
+ // in place of the transport error that actually occurred -- the same
2285
+ // dishonest failure issue #446 was about.
2286
+ //
2287
+ // Copy what the redispatch needs first: the local handler runs
2288
+ // synchronously and can erase this entry, leaving `request` dangling.
2289
+ if (request.gatewayNodeId == this->nodeId && uplinkServesLocally()) {
2290
+ const uint8_t priority = request.priority;
2291
+ const uint8_t retryCount = request.retryCount;
2292
+ const TSTRING destination = request.destination;
2293
+ const TSTRING payload = request.payload;
2294
+ Log(logger::COMMUNICATION,
2295
+ "retryInternetRequest(): Retrying msgId=%u on the local uplink\n",
2296
+ messageId);
2297
+ dispatchInternetRequestLocally(messageId, priority, retryCount,
2298
+ destination, payload);
2299
+ return;
2300
+ }
2301
+
2090
2302
  // Check mesh connectivity before attempting retry
2091
2303
  // During bridge failover, connection may be temporarily lost
2092
2304
  if (!hasActiveMeshConnections()) {
@@ -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=%u, total_length=%d, data=%s<--\n",
209
- variant.error, pkg.length(), pkg.c_str());
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=%d, calculated_capacity=%u, "
254
- "nesting_depth=%u. Consider increasing MAX_MESSAGE_CAPACITY if needed.\n",
255
- pkg.length(), calculatedCapacity, nestingDepth);
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=%u, length=%d, data=%s<--\n",
259
- variant->error, pkg.length(), pkg.c_str());
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
  }