@alteriom/painlessmesh 1.9.6 → 1.9.8
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 +55 -0
- package/README.md +42 -175
- package/docs/troubleshooting/common-issues.md +26 -0
- package/docs/troubleshooting/external-device-connection.md +283 -0
- package/examples/bridge/bridge.ino +23 -0
- package/examples/bridge_failover/bridge_failover.ino +24 -0
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +4 -4
- package/src/AlteriomPainlessMesh.h +3 -3
- package/src/arduino/wifi.hpp +850 -547
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +11 -2
- package/src/painlessmesh/mesh.hpp +34 -29
- package/src/painlessmesh/tcp.hpp +34 -8
package/src/painlessMesh.h
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* @file painlessMesh.h
|
|
6
6
|
* @brief Main header file for Alteriom painlessMesh library
|
|
7
7
|
*
|
|
8
|
-
* @version 1.
|
|
9
|
-
* @date 2025-
|
|
8
|
+
* @version 1.9.7
|
|
9
|
+
* @date 2025-12-13
|
|
10
10
|
*
|
|
11
11
|
* painlessMesh is a user-friendly library for creating mesh networks with
|
|
12
12
|
* ESP8266 and ESP32 devices. This Alteriom fork includes additional packages
|
package/src/painlessMeshSTA.cpp
CHANGED
|
@@ -233,10 +233,19 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
|
|
|
233
233
|
Log(CONNECTION,
|
|
234
234
|
"connectToAP(): Restarting AP from channel %d to channel %d\n",
|
|
235
235
|
oldChannel, detectedChannel);
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
|
|
237
|
+
// Disconnect AP and allow WiFi stack to fully reset
|
|
238
|
+
// Using true parameter ensures DHCP server is properly stopped
|
|
239
|
+
WiFi.softAPdisconnect(true);
|
|
240
|
+
delay(200); // Increased delay to ensure complete WiFi stack reset
|
|
241
|
+
|
|
238
242
|
// Call apInit via friend class access (StationScan is friend of wifi::Mesh)
|
|
239
243
|
mesh->apInit(mesh->getNodeId());
|
|
244
|
+
|
|
245
|
+
// Additional stabilization delay after AP restart
|
|
246
|
+
// This ensures DHCP server is fully initialized before clients connect
|
|
247
|
+
delay(100);
|
|
248
|
+
|
|
240
249
|
Log(CONNECTION, "connectToAP(): AP restarted on channel %d\n", detectedChannel);
|
|
241
250
|
}
|
|
242
251
|
// Reset counter only when mesh is found on a new channel
|
|
@@ -622,10 +622,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
622
622
|
*
|
|
623
623
|
* Use hasLocalInternet() to check if THIS specific node has direct Internet access.
|
|
624
624
|
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
625
|
+
* This method now ALWAYS requires healthy (recent) bridge status to prevent
|
|
626
|
+
* false positives when mesh connectivity is lost. Without active mesh connections,
|
|
627
|
+
* stale bridge data cannot be relied upon, as the bridge may have lost Internet
|
|
628
|
+
* connectivity or become unreachable.
|
|
629
629
|
*
|
|
630
630
|
* \code
|
|
631
631
|
* if (mesh.hasInternetConnection()) {
|
|
@@ -644,16 +644,10 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
644
644
|
* @see initAsSharedGateway() to give all nodes direct Internet access (requires router credentials)
|
|
645
645
|
*/
|
|
646
646
|
bool hasInternetConnection() {
|
|
647
|
-
|
|
648
|
-
|
|
647
|
+
// Always require healthy bridge status to prevent false positives
|
|
648
|
+
// when mesh is disconnected. Stale bridge data is unreliable.
|
|
649
649
|
for (const auto& bridge : knownBridges) {
|
|
650
|
-
|
|
651
|
-
// When disconnected: use last known state
|
|
652
|
-
bool isUsable = hasConnections
|
|
653
|
-
? (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected)
|
|
654
|
-
: bridge.internetConnected;
|
|
655
|
-
|
|
656
|
-
if (isUsable) {
|
|
650
|
+
if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
|
|
657
651
|
return true;
|
|
658
652
|
}
|
|
659
653
|
}
|
|
@@ -698,14 +692,17 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
698
692
|
* Get the primary (best) bridge node
|
|
699
693
|
*
|
|
700
694
|
* Primary bridge is selected based on:
|
|
701
|
-
* 1. Must be healthy (seen within timeout)
|
|
695
|
+
* 1. Must be healthy (seen within timeout)
|
|
702
696
|
* 2. Must have Internet connection
|
|
703
697
|
* 3. Best WiFi RSSI to router
|
|
704
698
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
699
|
+
* This method now ALWAYS requires healthy (recent) bridge status to prevent
|
|
700
|
+
* routing messages to unreachable or outdated bridges when mesh connectivity
|
|
701
|
+
* is lost. Without active mesh connections and fresh status, we cannot reliably
|
|
702
|
+
* route messages to any bridge.
|
|
703
|
+
*
|
|
704
|
+
* If you need access to the last known bridge regardless of health status,
|
|
705
|
+
* use getLastKnownBridge() instead.
|
|
709
706
|
*
|
|
710
707
|
* @return pointer to BridgeInfo of primary bridge, or nullptr if no suitable bridge
|
|
711
708
|
*/
|
|
@@ -713,17 +710,9 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
713
710
|
BridgeInfo* primary = nullptr;
|
|
714
711
|
int8_t bestRSSI = -127; // Worst possible RSSI
|
|
715
712
|
|
|
716
|
-
//
|
|
717
|
-
bool hasConnections = hasActiveMeshConnections();
|
|
718
|
-
|
|
713
|
+
// Always require healthy bridge status to prevent routing to stale/unreachable bridges
|
|
719
714
|
for (auto& bridge : knownBridges) {
|
|
720
|
-
|
|
721
|
-
// When disconnected: use any bridge that reported Internet (stale info is better than none)
|
|
722
|
-
bool isUsable = hasConnections
|
|
723
|
-
? (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected)
|
|
724
|
-
: bridge.internetConnected;
|
|
725
|
-
|
|
726
|
-
if (isUsable) {
|
|
715
|
+
if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
|
|
727
716
|
if (bridge.routerRSSI > bestRSSI) {
|
|
728
717
|
bestRSSI = bridge.routerRSSI;
|
|
729
718
|
primary = &bridge;
|
|
@@ -1290,6 +1279,18 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
1290
1279
|
Log(COMMUNICATION, "sendToInternet(): Local Internet available, using gateway protocol for consistency\n");
|
|
1291
1280
|
}
|
|
1292
1281
|
|
|
1282
|
+
// Validate mesh connectivity before attempting to send
|
|
1283
|
+
if (!hasActiveMeshConnections()) {
|
|
1284
|
+
Log(ERROR, "sendToInternet(): No active mesh connections\n");
|
|
1285
|
+
if (callback) {
|
|
1286
|
+
// Schedule callback to avoid blocking
|
|
1287
|
+
this->addTask([callback]() {
|
|
1288
|
+
callback(false, 0, "No mesh connections - cannot route to gateway");
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
return 0;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1293
1294
|
// Find the best gateway to route through
|
|
1294
1295
|
BridgeInfo* gateway = getPrimaryBridge();
|
|
1295
1296
|
if (gateway == nullptr) {
|
|
@@ -3134,7 +3135,11 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
|
|
|
3134
3135
|
using namespace logger;
|
|
3135
3136
|
Log(CONNECTION, "eraseClosedConnections():\n");
|
|
3136
3137
|
this->subs.remove_if(
|
|
3137
|
-
[](const std::shared_ptr<T> &conn) {
|
|
3138
|
+
[](const std::shared_ptr<T> &conn) {
|
|
3139
|
+
// Null check for safety - should never happen but prevents crashes
|
|
3140
|
+
if (!conn) return true;
|
|
3141
|
+
return !conn->connected();
|
|
3142
|
+
});
|
|
3138
3143
|
}
|
|
3139
3144
|
|
|
3140
3145
|
public: // Windows MSVC: TCP lambdas need access to droppedConnectionCallbacks
|
package/src/painlessmesh/tcp.hpp
CHANGED
|
@@ -20,6 +20,10 @@ namespace tcp {
|
|
|
20
20
|
static const uint8_t TCP_CONNECT_MAX_RETRIES = 5; // Max retry attempts before giving up
|
|
21
21
|
static const uint32_t TCP_CONNECT_RETRY_DELAY_MS = 1000; // Delay between retry attempts (1 second)
|
|
22
22
|
static const uint32_t TCP_CONNECT_STABILIZATION_DELAY_MS = 500; // Delay after IP acquisition (500ms)
|
|
23
|
+
// Delay before WiFi reconnection after all TCP retries are exhausted
|
|
24
|
+
// This prevents rapid reconnection loops when TCP server is persistently unavailable
|
|
25
|
+
// Gives the TCP server more time to recover and reduces network congestion
|
|
26
|
+
static const uint32_t TCP_EXHAUSTION_RECONNECT_DELAY_MS = 10000; // 10 seconds before reconnection
|
|
23
27
|
|
|
24
28
|
inline uint32_t encodeNodeId(const uint8_t *hwaddr) {
|
|
25
29
|
using namespace painlessmesh::logger;
|
|
@@ -129,20 +133,42 @@ void connect(AsyncClient &client, IPAddress ip, uint16_t port, M &mesh,
|
|
|
129
133
|
connect<T, M>((*pRetryConn), ip, port, mesh, retryCount + 1);
|
|
130
134
|
}, retryDelay);
|
|
131
135
|
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
136
|
+
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
137
|
+
// Deleting from within the error callback can cause use-after-free issues
|
|
138
|
+
// as the AsyncTCP library may still be referencing the object
|
|
139
|
+
// Note: client is captured by value (pointer copy) and we are the sole owner
|
|
140
|
+
mesh.addTask([client]() {
|
|
141
|
+
Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (retry path)\n");
|
|
142
|
+
delete client;
|
|
143
|
+
}, 0);
|
|
135
144
|
|
|
136
145
|
mesh.semaphoreGive();
|
|
137
146
|
return;
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
// All retries exhausted -
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
// All retries exhausted - schedule delayed reconnection
|
|
150
|
+
// Adding a significant delay before reconnection prevents rapid reconnection loops
|
|
151
|
+
// when the TCP server is persistently unavailable or overloaded
|
|
152
|
+
Log(CONNECTION, "tcp_err(): All %d retries exhausted, scheduling WiFi reconnection in %u ms\n",
|
|
153
|
+
TCP_CONNECT_MAX_RETRIES + 1, TCP_EXHAUSTION_RECONNECT_DELAY_MS);
|
|
154
|
+
|
|
155
|
+
// Defer deletion of the failed AsyncClient to prevent heap corruption
|
|
156
|
+
// Deleting from within the error callback can cause use-after-free issues
|
|
157
|
+
// as the AsyncTCP library may still be referencing the object
|
|
158
|
+
// Note: client is captured by value (pointer copy) and we are the sole owner
|
|
159
|
+
mesh.addTask([client]() {
|
|
160
|
+
Log(CONNECTION, "tcp_err(): Cleaning up failed AsyncClient (exhaustion path)\n");
|
|
161
|
+
delete client;
|
|
162
|
+
}, 0);
|
|
144
163
|
#endif
|
|
145
|
-
|
|
164
|
+
// Defer callback execution to avoid crashes in error handler context
|
|
165
|
+
// Execute callbacks after semaphore is released and error handler completes
|
|
166
|
+
// The delay helps prevent endless rapid reconnection loops by giving the TCP server
|
|
167
|
+
// more time to recover and reducing network congestion from multiple retrying nodes
|
|
168
|
+
mesh.addTask([&mesh]() {
|
|
169
|
+
Log(CONNECTION, "tcp_err(): Executing delayed WiFi reconnection after retry exhaustion\n");
|
|
170
|
+
mesh.droppedConnectionCallbacks.execute(0, true);
|
|
171
|
+
}, TCP_EXHAUSTION_RECONNECT_DELAY_MS);
|
|
146
172
|
mesh.semaphoreGive();
|
|
147
173
|
}
|
|
148
174
|
});
|