@alteriom/painlessmesh 1.9.19 → 1.10.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 +168 -0
- package/README.md +102 -63
- package/RELEASE_GUIDE.md +147 -8
- package/examples/alteriom/README.md +4 -4
- package/examples/alteriom/alteriom_custom_package_template.hpp +320 -0
- package/examples/alteriom/alteriom_sensor_package.hpp +1 -1
- package/examples/alteriom/mppt_example/alteriom_mppt_example.ino +208 -0
- package/examples/bridge_failover/bridge_failover.ino +17 -0
- package/examples/sendToInternet/CMakeLists.txt +54 -0
- package/examples/sendToInternet/PC_NODE_README.md +517 -0
- package/examples/sendToInternet/README.md +39 -1
- package/examples/sendToInternet/build.sh +153 -0
- package/examples/sendToInternet/mock_server_test.ino +361 -0
- package/examples/sendToInternet/pc_mesh_node.cpp +361 -0
- package/examples/tcpRetryConfig/README.md +110 -0
- package/examples/tcpRetryConfig/platformio.ini +26 -0
- package/examples/tcpRetryConfig/tcpRetryConfig.ino +154 -0
- package/keywords.txt +3 -0
- package/library.json +4 -1
- package/library.properties +1 -1
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +6 -14
- package/src/arduino/wifi.hpp +352 -114
- package/src/connection.cpp +10 -0
- package/src/painlessMesh.h +2 -15
- package/src/painlessTaskOptions.h +9 -0
- package/src/painlessmesh/buffer.hpp +4 -1
- package/src/painlessmesh/configuration.hpp +13 -2
- package/src/painlessmesh/connection.hpp +36 -21
- package/src/painlessmesh/gateway.hpp +0 -1061
- package/src/painlessmesh/mesh.hpp +102 -107
- package/src/painlessmesh/message_queue.hpp +25 -15
- package/src/painlessmesh/metrics.hpp +2 -262
- package/src/painlessmesh/plugin.hpp +27 -5
- package/src/painlessmesh/tcp.hpp +158 -29
- package/src/painlessmesh/validation.hpp +0 -143
- package/docs/README.md +0 -132
- package/docs/alteriom/overview.md +0 -531
- package/docs/api/core-api.md +0 -607
- package/docs/api/shared-gateway.md +0 -1207
- package/docs/architecture/mesh-architecture.md +0 -399
- package/docs/architecture/plugin-system.md +0 -517
- package/docs/getting-started/arduino-manual-install.md +0 -313
- package/docs/getting-started/first-mesh.md +0 -410
- package/docs/getting-started/installation.md +0 -275
- package/docs/getting-started/quickstart.md +0 -158
- package/docs/troubleshooting/common-issues.md +0 -679
- package/docs/troubleshooting/debugging.md +0 -455
- package/docs/troubleshooting/external-device-connection.md +0 -283
- package/docs/troubleshooting/faq.md +0 -574
- package/docs/tutorials/basic-examples.md +0 -718
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PC Mesh Node - Test sendToInternet() from Regular Node Through Bridge
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates a PC-based mesh node that can join a painlessMesh
|
|
5
|
+
* network with ESP32/ESP8266 devices and test sendToInternet() functionality
|
|
6
|
+
* as a REGULAR NODE (not a bridge).
|
|
7
|
+
*
|
|
8
|
+
* PURPOSE:
|
|
9
|
+
* Test the complete flow: PC Node → Bridge → Internet → Bridge → PC Node
|
|
10
|
+
*
|
|
11
|
+
* CONNECTION METHOD:
|
|
12
|
+
* ESP nodes use WiFi mesh credentials: mesh.init(MESH_PREFIX, MESH_PASSWORD, &scheduler, MESH_PORT)
|
|
13
|
+
* PC node uses TCP/IP connection: connects to bridge's IP:port (e.g., 192.168.1.100:5555)
|
|
14
|
+
*
|
|
15
|
+
* Both methods result in a mesh node that can use sendToInternet() - the difference
|
|
16
|
+
* is the transport layer (WiFi mesh vs TCP/IP), not the painlessMesh protocol.
|
|
17
|
+
*
|
|
18
|
+
* SETUP:
|
|
19
|
+
* 1. Configure ESP bridge with: mesh.init("FishFarmMesh", "securepass", &scheduler, 5555)
|
|
20
|
+
* 2. Run this PC node: ./pc_mesh_node <bridge_ip> 5555
|
|
21
|
+
* 3. Run mock HTTP server: cd test/mock-http-server && python3 server.py
|
|
22
|
+
* 4. PC node sends HTTP requests via sendToInternet() through bridge
|
|
23
|
+
*
|
|
24
|
+
* COMPILE:
|
|
25
|
+
* g++ -std=c++14 -o pc_mesh_node pc_mesh_node.cpp \
|
|
26
|
+
* -I../../src -I../../test/include -I../../test/ArduinoJson/src \
|
|
27
|
+
* -I../../test/TaskScheduler/src -I../../test/boost \
|
|
28
|
+
* ../../src/scheduler.cpp \
|
|
29
|
+
* -lboost_system -pthread
|
|
30
|
+
*
|
|
31
|
+
* Or use CMake (see CMakeLists.txt in this directory)
|
|
32
|
+
*
|
|
33
|
+
* RUN:
|
|
34
|
+
* ./pc_mesh_node 192.168.1.100 5555
|
|
35
|
+
*
|
|
36
|
+
* Where:
|
|
37
|
+
* - 192.168.1.100 is your ESP bridge's IP address on the LAN
|
|
38
|
+
* - 5555 is the TCP port the bridge is listening on (MESH_PORT)
|
|
39
|
+
**/
|
|
40
|
+
|
|
41
|
+
#include <iostream>
|
|
42
|
+
#include <string>
|
|
43
|
+
#include <memory>
|
|
44
|
+
#include <chrono>
|
|
45
|
+
#include <thread>
|
|
46
|
+
|
|
47
|
+
// Boost includes
|
|
48
|
+
#include <boost/asio/ip/address.hpp>
|
|
49
|
+
|
|
50
|
+
// Arduino emulation for PC
|
|
51
|
+
#include "Arduino.h"
|
|
52
|
+
#include "boost/asynctcp.hpp"
|
|
53
|
+
|
|
54
|
+
// painlessMesh includes
|
|
55
|
+
#include "painlessmesh/mesh.hpp"
|
|
56
|
+
#include "painlessmesh/gateway.hpp"
|
|
57
|
+
|
|
58
|
+
// Global objects required by Arduino emulation
|
|
59
|
+
WiFiClass WiFi;
|
|
60
|
+
ESPClass ESP;
|
|
61
|
+
|
|
62
|
+
using PMesh = painlessmesh::Mesh<painlessmesh::Connection>;
|
|
63
|
+
using namespace painlessmesh;
|
|
64
|
+
|
|
65
|
+
// Global logger
|
|
66
|
+
painlessmesh::logger::LogClass Log;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* PC Mesh Node Class
|
|
70
|
+
*
|
|
71
|
+
* This class wraps a painlessMesh instance and provides methods
|
|
72
|
+
* to connect to a bridge node and send Internet requests through it.
|
|
73
|
+
*/
|
|
74
|
+
class PCMeshNode : public PMesh {
|
|
75
|
+
public:
|
|
76
|
+
PCMeshNode(Scheduler* scheduler, uint32_t nodeId, boost::asio::io_context& io)
|
|
77
|
+
: io_service(io), scheduler_(scheduler) {
|
|
78
|
+
this->nodeId = nodeId;
|
|
79
|
+
this->init(scheduler, this->nodeId);
|
|
80
|
+
|
|
81
|
+
// Start listening server (so bridge can connect back to us if needed)
|
|
82
|
+
pServer = std::make_shared<AsyncServer>(io_service, this->nodeId);
|
|
83
|
+
painlessmesh::tcp::initServer<painlessmesh::Connection, PMesh>(*pServer, (*this));
|
|
84
|
+
|
|
85
|
+
std::cout << "✓ PC Mesh Node initialized with ID: " << this->nodeId << std::endl;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Connect to a bridge node
|
|
90
|
+
*/
|
|
91
|
+
bool connectToBridge(const std::string& bridgeIP, uint16_t bridgePort) {
|
|
92
|
+
std::cout << "Connecting to bridge at " << bridgeIP << ":" << bridgePort << "..." << std::endl;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
auto pClient = new AsyncClient(io_service);
|
|
96
|
+
|
|
97
|
+
// Set up connection callbacks
|
|
98
|
+
bool connected = false;
|
|
99
|
+
pClient->onConnect([&connected](void*, AsyncClient* client) {
|
|
100
|
+
connected = true;
|
|
101
|
+
std::cout << "✓ Connected to bridge!" << std::endl;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
pClient->onDisconnect([](void*, AsyncClient* client) {
|
|
105
|
+
std::cout << "✗ Disconnected from bridge" << std::endl;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Connect to the bridge
|
|
109
|
+
painlessmesh::tcp::connect<Connection, PMesh>(
|
|
110
|
+
(*pClient),
|
|
111
|
+
boost::asio::ip::make_address(bridgeIP),
|
|
112
|
+
bridgePort,
|
|
113
|
+
(*this)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Wait for connection with timeout
|
|
117
|
+
auto startTime = std::chrono::steady_clock::now();
|
|
118
|
+
while (!connected) {
|
|
119
|
+
this->update();
|
|
120
|
+
io_service.poll();
|
|
121
|
+
|
|
122
|
+
// Use longer sleep to reduce CPU usage during wait
|
|
123
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
124
|
+
|
|
125
|
+
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
|
126
|
+
std::chrono::steady_clock::now() - startTime
|
|
127
|
+
).count();
|
|
128
|
+
|
|
129
|
+
if (elapsed > 30) {
|
|
130
|
+
std::cout << "✗ Connection timeout after 30 seconds" << std::endl;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
} catch (const std::exception& e) {
|
|
137
|
+
std::cout << "✗ Connection error: " << e.what() << std::endl;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Send an HTTP request to the Internet via the bridge
|
|
144
|
+
*/
|
|
145
|
+
void testSendToInternet(const std::string& url, const std::string& payload = "") {
|
|
146
|
+
std::cout << "\n📡 Testing sendToInternet()..." << std::endl;
|
|
147
|
+
std::cout << " URL: " << url << std::endl;
|
|
148
|
+
if (!payload.empty()) {
|
|
149
|
+
std::cout << " Payload: " << payload << std::endl;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if we have Internet connection through bridge
|
|
153
|
+
if (!this->hasInternetConnection()) {
|
|
154
|
+
std::cout << " ⚠️ No Internet connection available through bridge" << std::endl;
|
|
155
|
+
std::cout << " Make sure the bridge node has router access and is connected" << std::endl;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
std::cout << " ✓ Bridge with Internet found" << std::endl;
|
|
160
|
+
|
|
161
|
+
// Send the request
|
|
162
|
+
uint32_t msgId = this->sendToInternet(
|
|
163
|
+
url,
|
|
164
|
+
payload,
|
|
165
|
+
[url](bool success, uint16_t httpStatus, std::string error) {
|
|
166
|
+
std::cout << "\n📥 Response received:" << std::endl;
|
|
167
|
+
std::cout << " Success: " << (success ? "✓ YES" : "✗ NO") << std::endl;
|
|
168
|
+
std::cout << " HTTP Status: " << httpStatus << std::endl;
|
|
169
|
+
if (!error.empty()) {
|
|
170
|
+
std::cout << " Error: " << error << std::endl;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Validate the result
|
|
174
|
+
if (success && httpStatus == 200) {
|
|
175
|
+
std::cout << " 🎉 TEST PASSED: Request successful!" << std::endl;
|
|
176
|
+
} else {
|
|
177
|
+
std::cout << " ❌ TEST FAILED: Request unsuccessful" << std::endl;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (msgId > 0) {
|
|
183
|
+
std::cout << " ✓ Request queued with message ID: " << msgId << std::endl;
|
|
184
|
+
std::cout << " Waiting for response from bridge..." << std::endl;
|
|
185
|
+
} else {
|
|
186
|
+
std::cout << " ✗ Failed to queue request" << std::endl;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Main update loop
|
|
192
|
+
*/
|
|
193
|
+
void runUpdateLoop(int durationSeconds = 0) {
|
|
194
|
+
std::cout << "\n🔄 Starting update loop";
|
|
195
|
+
if (durationSeconds > 0) {
|
|
196
|
+
std::cout << " (running for " << durationSeconds << " seconds)";
|
|
197
|
+
}
|
|
198
|
+
std::cout << "..." << std::endl;
|
|
199
|
+
|
|
200
|
+
auto startTime = std::chrono::steady_clock::now();
|
|
201
|
+
|
|
202
|
+
while (true) {
|
|
203
|
+
this->update();
|
|
204
|
+
io_service.poll();
|
|
205
|
+
scheduler_->execute();
|
|
206
|
+
|
|
207
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
208
|
+
|
|
209
|
+
if (durationSeconds > 0) {
|
|
210
|
+
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
|
211
|
+
std::chrono::steady_clock::now() - startTime
|
|
212
|
+
).count();
|
|
213
|
+
|
|
214
|
+
if (elapsed >= durationSeconds) {
|
|
215
|
+
std::cout << "✓ Update loop completed after " << elapsed << " seconds" << std::endl;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
std::shared_ptr<AsyncServer> pServer;
|
|
223
|
+
boost::asio::io_context& io_service;
|
|
224
|
+
Scheduler* scheduler_;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Print usage information
|
|
229
|
+
*/
|
|
230
|
+
void printUsage(const char* programName) {
|
|
231
|
+
std::cout << "\nUsage: " << programName << " <bridge_ip> <bridge_port> [mock_server_ip] [mock_server_port]\n" << std::endl;
|
|
232
|
+
std::cout << "Arguments:" << std::endl;
|
|
233
|
+
std::cout << " bridge_ip IP address of ESP bridge node (e.g., 192.168.1.100)" << std::endl;
|
|
234
|
+
std::cout << " bridge_port Mesh port of bridge (default: 5555)" << std::endl;
|
|
235
|
+
std::cout << " mock_server_ip IP address of mock HTTP server (optional, default: localhost)" << std::endl;
|
|
236
|
+
std::cout << " mock_server_port Port of mock HTTP server (optional, default: 8080)" << std::endl;
|
|
237
|
+
std::cout << "\nExample:" << std::endl;
|
|
238
|
+
std::cout << " " << programName << " 192.168.1.100 5555" << std::endl;
|
|
239
|
+
std::cout << " " << programName << " 192.168.1.100 5555 192.168.1.50 8080" << std::endl;
|
|
240
|
+
std::cout << std::endl;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Run automated test suite
|
|
245
|
+
*/
|
|
246
|
+
void runTests(PCMeshNode& node, const std::string& mockServerUrl) {
|
|
247
|
+
std::cout << "\n" << std::string(60, '=') << std::endl;
|
|
248
|
+
std::cout << "Starting Automated Test Suite" << std::endl;
|
|
249
|
+
std::cout << std::string(60, '=') << std::endl;
|
|
250
|
+
|
|
251
|
+
// Wait for mesh to stabilize
|
|
252
|
+
std::cout << "\nWaiting 5 seconds for mesh to stabilize..." << std::endl;
|
|
253
|
+
std::this_thread::sleep_for(std::chrono::seconds(5));
|
|
254
|
+
|
|
255
|
+
// Test 1: HTTP 200 Success
|
|
256
|
+
std::cout << "\n[Test 1/5] HTTP 200 Success" << std::endl;
|
|
257
|
+
node.testSendToInternet(mockServerUrl + "/status/200");
|
|
258
|
+
node.runUpdateLoop(5);
|
|
259
|
+
|
|
260
|
+
// Test 2: HTTP 404 Not Found
|
|
261
|
+
std::cout << "\n[Test 2/5] HTTP 404 Not Found" << std::endl;
|
|
262
|
+
node.testSendToInternet(mockServerUrl + "/status/404");
|
|
263
|
+
node.runUpdateLoop(5);
|
|
264
|
+
|
|
265
|
+
// Test 3: HTTP 500 Server Error
|
|
266
|
+
std::cout << "\n[Test 3/5] HTTP 500 Server Error" << std::endl;
|
|
267
|
+
node.testSendToInternet(mockServerUrl + "/status/500");
|
|
268
|
+
node.runUpdateLoop(5);
|
|
269
|
+
|
|
270
|
+
// Test 4: WhatsApp API Simulation
|
|
271
|
+
std::cout << "\n[Test 4/5] WhatsApp API Simulation" << std::endl;
|
|
272
|
+
std::string whatsappUrl = mockServerUrl + "/whatsapp?phone=%2B1234567890&apikey=test&text=Hello%20from%20PC%20node";
|
|
273
|
+
node.testSendToInternet(whatsappUrl);
|
|
274
|
+
node.runUpdateLoop(5);
|
|
275
|
+
|
|
276
|
+
// Test 5: Echo with JSON Payload
|
|
277
|
+
std::cout << "\n[Test 5/5] Echo Endpoint with JSON Payload" << std::endl;
|
|
278
|
+
// Using raw string literal for safer JSON construction
|
|
279
|
+
// In production, consider using ArduinoJson for proper JSON handling
|
|
280
|
+
char jsonBuffer[256];
|
|
281
|
+
snprintf(jsonBuffer, sizeof(jsonBuffer),
|
|
282
|
+
R"({"source":"pc_node","test":"sendToInternet","timestamp":%lu})",
|
|
283
|
+
(unsigned long)millis());
|
|
284
|
+
std::string payload(jsonBuffer);
|
|
285
|
+
node.testSendToInternet(mockServerUrl + "/echo", payload);
|
|
286
|
+
node.runUpdateLoop(5);
|
|
287
|
+
|
|
288
|
+
std::cout << "\n" << std::string(60, '=') << std::endl;
|
|
289
|
+
std::cout << "Test Suite Complete!" << std::endl;
|
|
290
|
+
std::cout << std::string(60, '=') << std::endl;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Main entry point
|
|
295
|
+
*/
|
|
296
|
+
int main(int argc, char* argv[]) {
|
|
297
|
+
std::cout << "\n" << std::string(60, '=') << std::endl;
|
|
298
|
+
std::cout << "painlessMesh - PC Mesh Node Example" << std::endl;
|
|
299
|
+
std::cout << "Testing sendToInternet() Through Bridge" << std::endl;
|
|
300
|
+
std::cout << std::string(60, '=') << std::endl;
|
|
301
|
+
|
|
302
|
+
// Parse command line arguments
|
|
303
|
+
if (argc < 3) {
|
|
304
|
+
printUsage(argv[0]);
|
|
305
|
+
return 1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
std::string bridgeIP = argv[1];
|
|
309
|
+
uint16_t bridgePort = std::stoi(argv[2]);
|
|
310
|
+
std::string mockServerIP = (argc > 3) ? argv[3] : "127.0.0.1";
|
|
311
|
+
uint16_t mockServerPort = (argc > 4) ? std::stoi(argv[4]) : 8080;
|
|
312
|
+
|
|
313
|
+
std::cout << "\nConfiguration:" << std::endl;
|
|
314
|
+
std::cout << " Bridge: " << bridgeIP << ":" << bridgePort << std::endl;
|
|
315
|
+
std::cout << " Mock Server: http://" << mockServerIP << ":" << mockServerPort << std::endl;
|
|
316
|
+
std::cout << std::endl;
|
|
317
|
+
|
|
318
|
+
// Setup logging
|
|
319
|
+
Log.setLogLevel(painlessmesh::logger::ERROR | painlessmesh::logger::STARTUP |
|
|
320
|
+
painlessmesh::logger::CONNECTION | painlessmesh::logger::COMMUNICATION);
|
|
321
|
+
|
|
322
|
+
// Create IO context and scheduler
|
|
323
|
+
boost::asio::io_context io_service;
|
|
324
|
+
Scheduler scheduler;
|
|
325
|
+
|
|
326
|
+
// Create PC mesh node with a random node ID
|
|
327
|
+
uint32_t nodeId = 1000000 + (millis() % 1000000);
|
|
328
|
+
PCMeshNode node(&scheduler, nodeId, io_service);
|
|
329
|
+
|
|
330
|
+
// Enable sendToInternet API
|
|
331
|
+
node.enableSendToInternet();
|
|
332
|
+
std::cout << "✓ sendToInternet() API enabled" << std::endl;
|
|
333
|
+
|
|
334
|
+
// Connect to bridge
|
|
335
|
+
if (!node.connectToBridge(bridgeIP, bridgePort)) {
|
|
336
|
+
std::cerr << "\n✗ Failed to connect to bridge" << std::endl;
|
|
337
|
+
std::cerr << "Make sure:" << std::endl;
|
|
338
|
+
std::cerr << " 1. Bridge node is running on " << bridgeIP << ":" << bridgePort << std::endl;
|
|
339
|
+
std::cerr << " 2. Firewall allows TCP connections on port " << bridgePort << std::endl;
|
|
340
|
+
std::cerr << " 3. PC and bridge are on the same network" << std::endl;
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Wait for mesh to establish
|
|
345
|
+
std::cout << "\nWaiting for mesh to establish..." << std::endl;
|
|
346
|
+
node.runUpdateLoop(10);
|
|
347
|
+
|
|
348
|
+
// Build mock server URL
|
|
349
|
+
std::string mockServerUrl = "http://" + mockServerIP + ":" + std::to_string(mockServerPort);
|
|
350
|
+
|
|
351
|
+
// Run automated tests
|
|
352
|
+
runTests(node, mockServerUrl);
|
|
353
|
+
|
|
354
|
+
// Keep running for a bit to receive any late responses
|
|
355
|
+
std::cout << "\nKeeping connection alive for 10 more seconds..." << std::endl;
|
|
356
|
+
node.runUpdateLoop(10);
|
|
357
|
+
|
|
358
|
+
std::cout << "\n✓ PC Mesh Node shutting down..." << std::endl;
|
|
359
|
+
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# tcpRetryConfig
|
|
2
|
+
|
|
3
|
+
Tuning painlessMesh's TCP connection retry behaviour with
|
|
4
|
+
`setTcpRetryConfig()` (issue
|
|
5
|
+
[#378](https://github.com/Alteriom/painlessMesh/issues/378)).
|
|
6
|
+
|
|
7
|
+
## What this controls
|
|
8
|
+
|
|
9
|
+
When a node acquires an IP and tries to open its TCP connection to the mesh,
|
|
10
|
+
that connection can fail — the parent's TCP server may not be ready yet, the
|
|
11
|
+
network stack may still be settling, or several nodes may be connecting at
|
|
12
|
+
once. painlessMesh retries with exponential backoff before giving up and
|
|
13
|
+
falling back to a full WiFi reconnect.
|
|
14
|
+
|
|
15
|
+
Five parameters describe that behaviour:
|
|
16
|
+
|
|
17
|
+
| Field | Default | Meaning |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `maxRetries` | 5 | TCP connect attempts after the first before giving up |
|
|
20
|
+
| `retryDelayMs` | 1000 | Base delay between retries; scaled 1x, 2x, 4x, 8x, 8x |
|
|
21
|
+
| `stabilizationDelayMs` | 500 | Wait after IP acquisition before the first attempt |
|
|
22
|
+
| `exhaustionReconnectDelayMs` | 10000 | Wait before the WiFi reconnect that follows exhaustion |
|
|
23
|
+
| `failureBlockDurationMs` | 60000 | How long a failed peer is skipped during AP selection |
|
|
24
|
+
|
|
25
|
+
With the defaults, a node that cannot reach its parent spends
|
|
26
|
+
1 + 2 + 4 + 8 + 8 = **23 s** retrying, then waits another **10 s** before
|
|
27
|
+
reconnecting WiFi, and will not re-select that same peer for **60 s**.
|
|
28
|
+
|
|
29
|
+
## Profiles in this sketch
|
|
30
|
+
|
|
31
|
+
Switch with `#define ACTIVE_PROFILE`.
|
|
32
|
+
|
|
33
|
+
| | `PROFILE_REALTIME` | default | `PROFILE_RELIABLE` | `PROFILE_BATTERY` |
|
|
34
|
+
|---|---|---|---|---|
|
|
35
|
+
| `maxRetries` | 1 | 5 | 10 | 2 |
|
|
36
|
+
| `retryDelayMs` | 200 | 1000 | 2000 | 3000 |
|
|
37
|
+
| `stabilizationDelayMs` | 100 | 500 | 1000 | 500 |
|
|
38
|
+
| `exhaustionReconnectDelayMs` | 1000 | 10000 | 30000 | 60000 |
|
|
39
|
+
| `failureBlockDurationMs` | 5000 | 60000 | 180000 | 300000 |
|
|
40
|
+
| worst-case retry time | 0.2 s | 23 s | 126 s | 9 s |
|
|
41
|
+
| full failure cycle | 1.2 s | 33 s | 156 s | 69 s |
|
|
42
|
+
|
|
43
|
+
- **`PROFILE_REALTIME`** — real-time sensor and LED meshes, the use case from
|
|
44
|
+
[discussion #368](https://github.com/Alteriom/painlessMesh/discussions/368).
|
|
45
|
+
A node stuck in a 23 s backoff is worse than one that drops and re-scans, so
|
|
46
|
+
fail fast and move on.
|
|
47
|
+
- **`PROFILE_RELIABLE`** — industrial meshes where getting connected matters
|
|
48
|
+
more than how long it takes. `maxRetries = 10` is the maximum the library
|
|
49
|
+
accepts.
|
|
50
|
+
- **`PROFILE_BATTERY`** — every retry is radio-on time. Few attempts, spaced
|
|
51
|
+
widely, and a long blocklist so the node stops waking up for a peer that is
|
|
52
|
+
known to be down.
|
|
53
|
+
|
|
54
|
+
## Clamping
|
|
55
|
+
|
|
56
|
+
`setTcpRetryConfig()` coerces the two values that can render a node unusable:
|
|
57
|
+
|
|
58
|
+
- `maxRetries` is capped at **10**. Each retry allocates an `AsyncClient` and
|
|
59
|
+
schedules a task, so an unbounded value is a heap and recursion-depth hazard
|
|
60
|
+
on ESP8266.
|
|
61
|
+
- `retryDelayMs` is held between **50 ms** and **60000 ms**. A zero delay would
|
|
62
|
+
schedule retries with no spacing — a hot loop allocating an `AsyncClient`
|
|
63
|
+
every scheduler tick. The ceiling keeps `retryDelayMs * 8` clear of `uint32_t`
|
|
64
|
+
overflow.
|
|
65
|
+
|
|
66
|
+
Everything else passes through untouched, including zeros, which are
|
|
67
|
+
meaningful:
|
|
68
|
+
|
|
69
|
+
- `maxRetries = 0` — do not retry at all; fall straight back to a WiFi
|
|
70
|
+
reconnect on the first TCP error.
|
|
71
|
+
- `stabilizationDelayMs = 0` — attempt the TCP connection immediately on IP
|
|
72
|
+
acquisition.
|
|
73
|
+
- `exhaustionReconnectDelayMs = 0` — reconnect WiFi immediately after
|
|
74
|
+
exhaustion.
|
|
75
|
+
- `failureBlockDurationMs = 0` — never blocklist a failed peer.
|
|
76
|
+
|
|
77
|
+
Call `getTcpRetryConfig()` after setting to see what actually took effect; the
|
|
78
|
+
sketch prints this at startup.
|
|
79
|
+
|
|
80
|
+
## The defaults exist for a reason
|
|
81
|
+
|
|
82
|
+
painlessMesh 1.9.x deliberately *raised* these values (retries 3 → 5, base
|
|
83
|
+
delay 500 ms → 1000 ms) to fix real-world mesh instability. Tuning them back
|
|
84
|
+
down reintroduces the problems that change fixed. Symptoms of an over-aggressive
|
|
85
|
+
profile:
|
|
86
|
+
|
|
87
|
+
- **Connection churn** — nodes repeatedly connect and drop. `maxRetries` is too
|
|
88
|
+
low for how long your parent actually takes to be ready; raise it or raise
|
|
89
|
+
`stabilizationDelayMs`.
|
|
90
|
+
- **Rapid reconnect loops / network congestion** — a node hammers a parent whose
|
|
91
|
+
TCP server is down. `exhaustionReconnectDelayMs` is too short.
|
|
92
|
+
- **The same dead peer is picked over and over** — `failureBlockDurationMs` is
|
|
93
|
+
shorter than one full retry-plus-reconnect cycle, so the peer comes off the
|
|
94
|
+
blocklist before the node has finished failing over. Keep
|
|
95
|
+
`failureBlockDurationMs` greater than
|
|
96
|
+
`(sum of retry backoffs) + exhaustionReconnectDelayMs`. All three profiles
|
|
97
|
+
above satisfy this; `catch_tcp_blocklist.cpp` pins it as a test.
|
|
98
|
+
|
|
99
|
+
Change one parameter at a time and watch the serial log with
|
|
100
|
+
`mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION)`.
|
|
101
|
+
|
|
102
|
+
## Building
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# PlatformIO
|
|
106
|
+
pio run -e esp32 # or -e esp8266
|
|
107
|
+
|
|
108
|
+
# Arduino CLI
|
|
109
|
+
arduino-cli compile --fqbn esp32:esp32:esp32 examples/tcpRetryConfig/tcpRetryConfig.ino
|
|
110
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[platformio]
|
|
2
|
+
src_dir = .
|
|
3
|
+
|
|
4
|
+
[env]
|
|
5
|
+
lib_deps =
|
|
6
|
+
bblanchon/ArduinoJson
|
|
7
|
+
arkhipenko/TaskScheduler
|
|
8
|
+
|
|
9
|
+
lib_ldf_mode = deep+
|
|
10
|
+
[env:esp8266]
|
|
11
|
+
platform = espressif8266
|
|
12
|
+
board = nodemcuv2
|
|
13
|
+
framework = arduino
|
|
14
|
+
lib_extra_dirs = ../../
|
|
15
|
+
lib_deps =
|
|
16
|
+
${env.lib_deps} ; Inherit common dependencies
|
|
17
|
+
esp32async/ESPAsyncTCP@^2.0.0 ; Only for ESP8266
|
|
18
|
+
|
|
19
|
+
[env:esp32]
|
|
20
|
+
platform = espressif32
|
|
21
|
+
board = esp32dev
|
|
22
|
+
framework = arduino
|
|
23
|
+
lib_extra_dirs = ../../
|
|
24
|
+
lib_deps =
|
|
25
|
+
${env.lib_deps} ; Inherit common dependencies
|
|
26
|
+
esp32async/AsyncTCP
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//************************************************************
|
|
2
|
+
// Tuning the TCP connection retry behaviour (issue #378)
|
|
3
|
+
//
|
|
4
|
+
// painlessMesh retries a failed TCP connection with exponential backoff
|
|
5
|
+
// before giving up and falling back to a full WiFi reconnect. The defaults
|
|
6
|
+
// (5 retries, 1s base delay -> 1s, 2s, 4s, 8s, 8s) are tuned for general
|
|
7
|
+
// purpose meshes and deliberately favour reliability over speed.
|
|
8
|
+
//
|
|
9
|
+
// setTcpRetryConfig() lets you pick a different trade-off. Three ready-made
|
|
10
|
+
// profiles are shown below; switch between them with ACTIVE_PROFILE.
|
|
11
|
+
//
|
|
12
|
+
// Read examples/tcpRetryConfig/README.md before changing these values - the
|
|
13
|
+
// defaults exist because 1.9.x raised them to fix real mesh instability.
|
|
14
|
+
//************************************************************
|
|
15
|
+
#include <painlessMesh.h>
|
|
16
|
+
|
|
17
|
+
#define MESH_SSID "whateverYouLike"
|
|
18
|
+
#define MESH_PASSWORD "somethingSneaky"
|
|
19
|
+
#define MESH_PORT 5555
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Profiles
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
#define PROFILE_REALTIME 1 // low latency: fail fast, reconnect fast
|
|
25
|
+
#define PROFILE_RELIABLE 2 // industrial: many retries, long backoff
|
|
26
|
+
#define PROFILE_BATTERY 3 // conserve power: few retries, long block
|
|
27
|
+
|
|
28
|
+
// >>> Change this line to try a different profile <<<
|
|
29
|
+
#define ACTIVE_PROFILE PROFILE_REALTIME
|
|
30
|
+
|
|
31
|
+
// Prototypes
|
|
32
|
+
void sendMessage();
|
|
33
|
+
void receivedCallback(uint32_t from, String &msg);
|
|
34
|
+
void newConnectionCallback(uint32_t nodeId);
|
|
35
|
+
void droppedConnectionCallback(uint32_t nodeId);
|
|
36
|
+
|
|
37
|
+
Scheduler userScheduler;
|
|
38
|
+
painlessMesh mesh;
|
|
39
|
+
|
|
40
|
+
Task taskSendMessage(TASK_SECOND * 5, TASK_FOREVER, &sendMessage);
|
|
41
|
+
|
|
42
|
+
// Build the retry configuration for the selected profile.
|
|
43
|
+
painlessmesh::tcp::TcpRetryConfig buildRetryConfig() {
|
|
44
|
+
painlessmesh::tcp::TcpRetryConfig cfg; // starts at the library defaults
|
|
45
|
+
|
|
46
|
+
#if ACTIVE_PROFILE == PROFILE_REALTIME
|
|
47
|
+
// Real-time sensor / LED meshes: a stalled node is worse than a dropped
|
|
48
|
+
// one. Give up on the TCP handshake almost immediately and go straight
|
|
49
|
+
// back to scanning for another parent.
|
|
50
|
+
cfg.maxRetries = 1; // default 5
|
|
51
|
+
cfg.retryDelayMs = 200; // default 1000
|
|
52
|
+
cfg.stabilizationDelayMs = 100; // default 500
|
|
53
|
+
cfg.exhaustionReconnectDelayMs = 1000; // default 10000
|
|
54
|
+
cfg.failureBlockDurationMs = 5000; // default 60000
|
|
55
|
+
|
|
56
|
+
#elif ACTIVE_PROFILE == PROFILE_RELIABLE
|
|
57
|
+
// Industrial / high-reliability meshes: connectivity matters more than
|
|
58
|
+
// how long it takes to get there. Retry patiently and keep a failed peer
|
|
59
|
+
// out of the running for a good while.
|
|
60
|
+
cfg.maxRetries = 10; // default 5 (this is the maximum)
|
|
61
|
+
cfg.retryDelayMs = 2000; // default 1000
|
|
62
|
+
cfg.stabilizationDelayMs = 1000; // default 500
|
|
63
|
+
cfg.exhaustionReconnectDelayMs = 30000; // default 10000
|
|
64
|
+
// Must exceed one full failure cycle (126s of retries + 30s reconnect),
|
|
65
|
+
// otherwise a dead peer leaves the blocklist before we finished failing
|
|
66
|
+
// over and gets re-selected immediately.
|
|
67
|
+
cfg.failureBlockDurationMs = 180000; // default 60000
|
|
68
|
+
|
|
69
|
+
#elif ACTIVE_PROFILE == PROFILE_BATTERY
|
|
70
|
+
// Battery-powered nodes: every retry is radio time. Few attempts, spaced
|
|
71
|
+
// widely, and a long blocklist so we do not keep waking up for a peer
|
|
72
|
+
// that is known to be down.
|
|
73
|
+
cfg.maxRetries = 2; // default 5
|
|
74
|
+
cfg.retryDelayMs = 3000; // default 1000
|
|
75
|
+
cfg.stabilizationDelayMs = 500; // default 500
|
|
76
|
+
cfg.exhaustionReconnectDelayMs = 60000; // default 10000
|
|
77
|
+
cfg.failureBlockDurationMs = 300000; // default 60000
|
|
78
|
+
|
|
79
|
+
#else
|
|
80
|
+
#error "ACTIVE_PROFILE must be one of PROFILE_REALTIME, PROFILE_RELIABLE, PROFILE_BATTERY"
|
|
81
|
+
#endif
|
|
82
|
+
|
|
83
|
+
return cfg;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
void setup() {
|
|
87
|
+
Serial.begin(115200);
|
|
88
|
+
|
|
89
|
+
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
|
|
90
|
+
|
|
91
|
+
// Apply the retry configuration BEFORE init() so the very first connection
|
|
92
|
+
// attempt already uses it.
|
|
93
|
+
mesh.setTcpRetryConfig(buildRetryConfig());
|
|
94
|
+
|
|
95
|
+
mesh.init(MESH_SSID, MESH_PASSWORD, &userScheduler, MESH_PORT);
|
|
96
|
+
|
|
97
|
+
// Read the configuration back. Values outside safe operating bounds are
|
|
98
|
+
// clamped by the setter, so this prints what is actually in effect - not
|
|
99
|
+
// necessarily what was requested.
|
|
100
|
+
painlessmesh::tcp::TcpRetryConfig active = mesh.getTcpRetryConfig();
|
|
101
|
+
Serial.println();
|
|
102
|
+
Serial.println(F("Effective TCP retry configuration:"));
|
|
103
|
+
Serial.printf(" maxRetries = %u\n",
|
|
104
|
+
(unsigned)active.maxRetries);
|
|
105
|
+
Serial.printf(" retryDelayMs = %u\n",
|
|
106
|
+
(unsigned)active.retryDelayMs);
|
|
107
|
+
Serial.printf(" stabilizationDelayMs = %u\n",
|
|
108
|
+
(unsigned)active.stabilizationDelayMs);
|
|
109
|
+
Serial.printf(" exhaustionReconnectDelayMs = %u\n",
|
|
110
|
+
(unsigned)active.exhaustionReconnectDelayMs);
|
|
111
|
+
Serial.printf(" failureBlockDurationMs = %u\n",
|
|
112
|
+
(unsigned)active.failureBlockDurationMs);
|
|
113
|
+
|
|
114
|
+
// Worst-case time spent retrying before falling back to a WiFi reconnect.
|
|
115
|
+
uint32_t worstCase = 0;
|
|
116
|
+
for (uint8_t i = 0; i < active.maxRetries; ++i) {
|
|
117
|
+
worstCase += painlessmesh::tcp::retryBackoffDelay(active, i);
|
|
118
|
+
}
|
|
119
|
+
Serial.printf(" -> worst-case retry time = %u ms\n", (unsigned)worstCase);
|
|
120
|
+
Serial.printf(" -> plus reconnect delay = %u ms\n",
|
|
121
|
+
(unsigned)(worstCase + active.exhaustionReconnectDelayMs));
|
|
122
|
+
Serial.println();
|
|
123
|
+
|
|
124
|
+
mesh.onReceive(&receivedCallback);
|
|
125
|
+
mesh.onNewConnection(&newConnectionCallback);
|
|
126
|
+
mesh.onDroppedConnection(&droppedConnectionCallback);
|
|
127
|
+
|
|
128
|
+
userScheduler.addTask(taskSendMessage);
|
|
129
|
+
taskSendMessage.enable();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
void loop() {
|
|
133
|
+
mesh.update();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
void sendMessage() {
|
|
137
|
+
String msg = "Hello from node ";
|
|
138
|
+
msg += mesh.getNodeId();
|
|
139
|
+
mesh.sendBroadcast(msg);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
void receivedCallback(uint32_t from, String &msg) {
|
|
143
|
+
Serial.printf("tcpRetryConfig: Received from %u msg=%s\n", from, msg.c_str());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
void newConnectionCallback(uint32_t nodeId) {
|
|
147
|
+
Serial.printf("--> Connected to node %u\n", nodeId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
void droppedConnectionCallback(uint32_t nodeId) {
|
|
151
|
+
// With an aggressive profile you should expect to see this more often -
|
|
152
|
+
// and to see the reconnect that follows it happen much sooner.
|
|
153
|
+
Serial.printf("--> Dropped connection to node %u\n", nodeId);
|
|
154
|
+
}
|
package/keywords.txt
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
painlessMesh KEYWORD1
|
|
5
5
|
Scheduler KEYWORD1
|
|
6
6
|
Task KEYWORD1
|
|
7
|
+
TcpRetryConfig KEYWORD1
|
|
7
8
|
|
|
8
9
|
# Methods and Functions (KEYWORD2)
|
|
9
10
|
init KEYWORD2
|
|
@@ -18,6 +19,8 @@ getNodeList KEYWORD2
|
|
|
18
19
|
getNodeId KEYWORD2
|
|
19
20
|
getNodeTime KEYWORD2
|
|
20
21
|
setDebugMsgTypes KEYWORD2
|
|
22
|
+
setTcpRetryConfig KEYWORD2
|
|
23
|
+
getTcpRetryConfig KEYWORD2
|
|
21
24
|
subConnectionJson KEYWORD2
|
|
22
25
|
asNodeTree KEYWORD2
|
|
23
26
|
|
package/library.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/Alteriom/painlessMesh"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.10.0",
|
|
10
10
|
"frameworks": [
|
|
11
11
|
"arduino"
|
|
12
12
|
],
|
|
@@ -87,7 +87,10 @@
|
|
|
87
87
|
"examples/namedMesh/namedMesh.ino",
|
|
88
88
|
"examples/otaReceiver/otaReceiver.ino",
|
|
89
89
|
"examples/otaSender/otaSender.ino",
|
|
90
|
+
"examples/alteriom/mppt_example/alteriom_mppt_example.ino",
|
|
90
91
|
"examples/priority/priority_basic_example.ino",
|
|
92
|
+
"examples/priority/priority_with_queue.ino",
|
|
93
|
+
"examples/sendToInternet/mock_server_test.ino",
|
|
91
94
|
"examples/sendToInternet/sendToInternet.ino",
|
|
92
95
|
"examples/sharedGateway/sharedGateway.ino",
|
|
93
96
|
"examples/startHere/startHere.ino",
|