@alteriom/painlessmesh 1.9.20 → 2.0.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/BRIDGE_TO_INTERNET.md +167 -29
- package/CHANGELOG.md +604 -0
- package/CONTRIBUTING.md +56 -53
- package/README.md +100 -75
- package/RELEASE_GUIDE.md +81 -641
- package/examples/alteriom/README.md +8 -10
- package/examples/alteriom/alteriom.ino +2 -2
- package/examples/alteriom/alteriom_custom_package_template.hpp +11 -11
- package/examples/alteriom/alteriom_sensor_package.hpp +17 -11
- package/examples/alteriom/mppt_example/alteriom_custom_package_template.hpp +320 -0
- package/examples/alteriom/mppt_example/alteriom_sensor_package.hpp +1389 -0
- package/examples/alteriom/mppt_example/{alteriom_mppt_example.ino → mppt_example.ino} +5 -1
- package/examples/basic/test/simulator/README.md +3 -3
- package/examples/bridge_failover/README.md +51 -14
- package/examples/bridge_failover/bridge_failover.ino +2 -2
- package/examples/commandControl/commandControl.ino +86 -0
- package/examples/commandControl/platformio.ini +26 -0
- package/examples/mqttBridge/mqttBridge.ino +4 -0
- package/examples/mqttBridge/platformio.ini +1 -1
- package/examples/otaSender/otaSender.ino +5 -1
- package/examples/priority/README.md +1 -1
- package/examples/priority/{priority_basic_example.ino → priority_basic_example/priority_basic_example.ino} +4 -4
- package/examples/priority/{priority_with_queue.ino → priority_with_queue/priority_with_queue.ino} +20 -2
- package/examples/reliableSensorLogging/platformio.ini +26 -0
- package/examples/reliableSensorLogging/reliableSensorLogging.ino +151 -0
- package/examples/sendToInternet/README.md +12 -5
- package/examples/sendToInternet/{CMakeLists.txt → pc_node/CMakeLists.txt} +7 -7
- package/examples/sendToInternet/{PC_NODE_README.md → pc_node/PC_NODE_README.md} +15 -15
- package/examples/sendToInternet/{build.sh → pc_node/build.sh} +5 -5
- package/examples/sendToInternet/{pc_mesh_node.cpp → pc_node/pc_mesh_node.cpp} +12 -1
- package/examples/sharedGateway/README.md +1 -2
- package/examples/tcpRetryConfig/README.md +110 -0
- package/examples/tcpRetryConfig/platformio.ini +26 -0
- package/examples/tcpRetryConfig/tcpRetryConfig.ino +154 -0
- package/keywords.txt +53 -1
- package/library.json +8 -6
- package/library.properties +2 -2
- package/package.json +3 -3
- package/src/AlteriomPainlessMesh.h +4 -4
- package/src/arduino/wifi.hpp +605 -143
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +607 -87
- package/src/painlessMeshSTA.h +135 -3
- package/src/painlessTaskOptions.h +9 -0
- package/src/painlessmesh/ack.hpp +283 -0
- package/src/painlessmesh/buffer.hpp +74 -9
- package/src/painlessmesh/callback.hpp +38 -5
- package/src/painlessmesh/configuration.hpp +82 -3
- package/src/painlessmesh/connection.hpp +43 -16
- package/src/painlessmesh/gateway.hpp +270 -5
- package/src/painlessmesh/layout.hpp +70 -2
- package/src/painlessmesh/logger.hpp +15 -0
- package/src/painlessmesh/mesh.hpp +625 -70
- package/src/painlessmesh/message_queue.hpp +24 -13
- package/src/painlessmesh/ntp.hpp +2 -4
- package/src/painlessmesh/plugin.hpp +52 -6
- package/src/painlessmesh/protocol.hpp +55 -2
- package/src/painlessmesh/router.hpp +192 -77
- package/src/painlessmesh/tcp.hpp +168 -29
- package/src/painlessmesh/message_tracker.hpp +0 -311
- /package/examples/sendToInternet/{mock_server_test.ino → mock_server_test/mock_server_test.ino} +0 -0
package/src/arduino/wifi.hpp
CHANGED
|
@@ -62,13 +62,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
62
62
|
// Shut Wifi down and start with a blank slage
|
|
63
63
|
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
// The mesh reconnects on its own terms: it scans, chooses an AP, and
|
|
66
|
+
// connects. The core must not. On core 2.x this used to call
|
|
67
|
+
// setAutoConnect(false) — the "connect at boot from stored
|
|
68
|
+
// credentials" flag, a different thing — and left the core's
|
|
69
|
+
// auto-reconnect at its default of on. After a parent's AP vanished
|
|
70
|
+
// (BEACON_TIMEOUT) the core then tried the gone BSSID again every seven
|
|
71
|
+
// seconds (NO_AP_FOUND, a full-channel search each time) for as long as
|
|
72
|
+
// it stayed gone, and every attempt kept the radio busy so the mesh's
|
|
73
|
+
// own scan "could not start": the node sat outside the mesh for the
|
|
74
|
+
// rest of the test. Measured on the rig on every core-2.x board (ESP32,
|
|
75
|
+
// C3, S3); the core-3.x boards, which got the right call, did not do it.
|
|
76
|
+
Log(STARTUP, "init(): %d\n", WiFi.setAutoReconnect(false));
|
|
77
|
+
#if ESP_ARDUINO_VERSION_MAJOR < 3
|
|
78
|
+
WiFi.setAutoConnect(false);
|
|
72
79
|
#endif
|
|
73
80
|
WiFi.persistent(false);
|
|
74
81
|
|
|
@@ -76,6 +83,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
76
83
|
if (!WiFi.mode(connectMode)) {
|
|
77
84
|
Log(GENERAL, "WiFi.mode() false");
|
|
78
85
|
}
|
|
86
|
+
#ifdef ESP32
|
|
87
|
+
#if SOC_WIFI_SUPPORT_5G && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
|
|
88
|
+
// The mesh lives on 2.4 GHz. A dual-band part left in its default band
|
|
89
|
+
// mode scans the 5 GHz channels as well every time it looks for the
|
|
90
|
+
// mesh: measured on an ESP32-C5, the all-channel scan took 13–14 s
|
|
91
|
+
// against 4 s on a single-band part — at boot, where it is the whole
|
|
92
|
+
// of the C5's 15 s to its first reply, and on every channel
|
|
93
|
+
// re-detection, where it swallowed the console commands sent
|
|
94
|
+
// meanwhile. Nothing the mesh does needs the other band.
|
|
95
|
+
if (!WiFi.setBandMode(WIFI_BAND_MODE_2G_ONLY)) {
|
|
96
|
+
Log(ERROR, "init(): could not restrict the radio to 2.4 GHz\n");
|
|
97
|
+
}
|
|
98
|
+
#endif
|
|
99
|
+
#endif
|
|
79
100
|
|
|
80
101
|
_meshSSID = ssid;
|
|
81
102
|
_meshPassword = password;
|
|
@@ -98,6 +119,17 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
98
119
|
|
|
99
120
|
this->init(nodeId);
|
|
100
121
|
|
|
122
|
+
#ifdef ESP8266
|
|
123
|
+
// The ESP8266 is specified for small meshes, or as a leaf in larger
|
|
124
|
+
// ones. Say so at runtime when a deployment has put it in the middle of
|
|
125
|
+
// one: see Mesh::capacityCheck(). Thirty seconds is often enough to
|
|
126
|
+
// catch it and rare enough to cost nothing.
|
|
127
|
+
this->addTask(30 * TASK_SECOND, TASK_FOREVER, [this]() {
|
|
128
|
+
this->capacityCheck(ESP.getFreeHeap(),
|
|
129
|
+
painlessmesh::Mesh<Connection>::ESP8266_CAPACITY_FLOOR);
|
|
130
|
+
});
|
|
131
|
+
#endif
|
|
132
|
+
|
|
101
133
|
// Add bridge election package handler (Type BRIDGE_ELECTION)
|
|
102
134
|
this->callbackList.onPackage(
|
|
103
135
|
protocol::BRIDGE_ELECTION,
|
|
@@ -112,11 +144,12 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
112
144
|
if (obj["routerRSSI"].is<int>()) {
|
|
113
145
|
uint32_t fromNode = obj["from"];
|
|
114
146
|
int8_t routerRSSI = obj["routerRSSI"];
|
|
147
|
+
uint8_t routerChannel = obj["routerChannel"] | 0;
|
|
115
148
|
uint32_t uptime = obj["uptime"] | 0;
|
|
116
149
|
uint32_t freeMemory = obj["freeMemory"] | 0;
|
|
117
150
|
|
|
118
|
-
this->handleBridgeElection(fromNode, routerRSSI,
|
|
119
|
-
freeMemory);
|
|
151
|
+
this->handleBridgeElection(fromNode, routerRSSI, routerChannel,
|
|
152
|
+
uptime, freeMemory);
|
|
120
153
|
|
|
121
154
|
Log(CONNECTION, "Bridge election candidate from %u: RSSI %d dBm\n",
|
|
122
155
|
fromNode, routerRSSI);
|
|
@@ -139,9 +172,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
139
172
|
uint32_t newBridge = obj["from"];
|
|
140
173
|
uint32_t previousBridge = obj["previousBridge"];
|
|
141
174
|
TSTRING reason = obj["reason"].as<TSTRING>();
|
|
175
|
+
uint8_t routerChannel = obj["routerChannel"] | 0;
|
|
142
176
|
|
|
143
|
-
Log(CONNECTION,
|
|
144
|
-
|
|
177
|
+
Log(CONNECTION,
|
|
178
|
+
"Bridge takeover: Node %u replaced %u on channel %u (%s)\n",
|
|
179
|
+
newBridge, previousBridge, routerChannel, reason.c_str());
|
|
180
|
+
|
|
181
|
+
// AP+STA radios must use one channel. Follow the elected bridge
|
|
182
|
+
// promptly instead of waiting for the slow empty-scan recovery.
|
|
183
|
+
if (gateway::shouldFollowBridgeChannel(
|
|
184
|
+
this->nodeId, newBridge, _meshChannel, routerChannel)) {
|
|
185
|
+
this->addTask(1000, TASK_ONCE, [this, routerChannel]() {
|
|
186
|
+
stationScan.followBridgeChannel(routerChannel);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
145
189
|
|
|
146
190
|
// Notify callback if this node was not the winner
|
|
147
191
|
if (newBridge != this->nodeId && bridgeRoleChangedCallback) {
|
|
@@ -171,57 +215,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
171
215
|
|
|
172
216
|
// Add periodic monitoring task to detect when no bridge exists
|
|
173
217
|
// This handles the case where no node was initially configured as a bridge
|
|
174
|
-
this->addTask(30000, TASK_FOREVER, [this]() {
|
|
175
|
-
// Only check if failover is enabled and we have credentials
|
|
176
|
-
if (!bridgeFailoverEnabled || !routerCredentialsConfigured) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Don't check if we're already a bridge
|
|
181
|
-
if (this->isBridge()) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Skip check during startup period to allow initial bridge discovery
|
|
186
|
-
if (millis() < electionStartupDelayMs) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// IMPORTANT: Don't trigger election if we're disconnected from the mesh
|
|
191
|
-
// When isolated, we can't receive bridge status broadcasts, so lack of
|
|
192
|
-
// healthy bridge could simply mean WE are disconnected, not that the
|
|
193
|
-
// bridge is unavailable. Wait until mesh connectivity is restored before
|
|
194
|
-
// considering an election.
|
|
195
|
-
if (!this->hasActiveMeshConnections()) {
|
|
196
|
-
Log(CONNECTION,
|
|
197
|
-
"Bridge monitor: Skipping - no active mesh connections\n");
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Check if there are any healthy bridges
|
|
202
|
-
bool hasHealthyBridge = false;
|
|
203
|
-
for (const auto& bridge : this->getBridges()) {
|
|
204
|
-
if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
|
|
205
|
-
hasHealthyBridge = true;
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// If no healthy bridge exists, trigger an election
|
|
211
|
-
if (!hasHealthyBridge) {
|
|
212
|
-
Log(CONNECTION,
|
|
213
|
-
"Bridge monitor: No healthy bridge detected, triggering "
|
|
214
|
-
"election\n");
|
|
215
|
-
// Random delay to prevent simultaneous elections when multiple nodes
|
|
216
|
-
// start together
|
|
217
|
-
uint32_t randomDelay =
|
|
218
|
-
random(electionRandomDelayMinMs, electionRandomDelayMaxMs);
|
|
219
|
-
Log(CONNECTION, "Bridge monitor: Scheduling election in %u ms\n",
|
|
220
|
-
randomDelay);
|
|
221
|
-
this->addTask(randomDelay, TASK_ONCE,
|
|
222
|
-
[this]() { this->startBridgeElection(); });
|
|
223
|
-
}
|
|
224
|
-
});
|
|
218
|
+
this->addTask(30000, TASK_FOREVER, [this]() { this->checkForBridge(); });
|
|
225
219
|
|
|
226
220
|
// Add separate periodic task for isolated bridge retry
|
|
227
221
|
// This handles the case where a node:
|
|
@@ -395,11 +389,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
395
389
|
// Shut Wifi down and start with a blank slate
|
|
396
390
|
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
397
391
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
WiFi.setAutoConnect(false));
|
|
392
|
+
// See init(): the core's auto-reconnect is off on every core version.
|
|
393
|
+
Log(STARTUP, "initAsBridge(): %d\n", WiFi.setAutoReconnect(false));
|
|
394
|
+
#if ESP_ARDUINO_VERSION_MAJOR < 3
|
|
395
|
+
WiFi.setAutoConnect(false);
|
|
403
396
|
#endif
|
|
404
397
|
WiFi.persistent(false);
|
|
405
398
|
WiFi.mode(WIFI_STA);
|
|
@@ -625,13 +618,12 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
625
618
|
// We need to ensure mesh and router operate on the same channel
|
|
626
619
|
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
627
620
|
|
|
628
|
-
|
|
621
|
+
// See init(): the core's auto-reconnect is off on every core version.
|
|
629
622
|
WiFi.setAutoReconnect(false);
|
|
630
|
-
|
|
631
|
-
#else
|
|
623
|
+
#if ESP_ARDUINO_VERSION_MAJOR < 3
|
|
632
624
|
WiFi.setAutoConnect(false);
|
|
633
|
-
Log(STARTUP, "initAsSharedGateway(): AutoConnect disabled\n");
|
|
634
625
|
#endif
|
|
626
|
+
Log(STARTUP, "initAsSharedGateway(): AutoReconnect disabled\n");
|
|
635
627
|
WiFi.persistent(false);
|
|
636
628
|
WiFi.mode(WIFI_STA);
|
|
637
629
|
|
|
@@ -699,6 +691,14 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
699
691
|
// Step 5: Setup gateway Internet handler
|
|
700
692
|
initGatewayInternetHandler();
|
|
701
693
|
|
|
694
|
+
// Step 6: Start the health checker that drives hasLocalInternet().
|
|
695
|
+
// Previously shared-gateway nodes associated and received an IP address,
|
|
696
|
+
// but the checker was never configured or scheduled, so local Internet
|
|
697
|
+
// availability remained false forever and every request was needlessly
|
|
698
|
+
// routed toward a mesh gateway.
|
|
699
|
+
configureInternetHealthCheck(_sharedGatewayConfig);
|
|
700
|
+
enableInternetHealthCheck();
|
|
701
|
+
|
|
702
702
|
// Store router credentials for reconnection
|
|
703
703
|
setRouterCredentials(routerSSID, routerPassword);
|
|
704
704
|
|
|
@@ -749,6 +749,18 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
749
749
|
static_cast<bool>(_meshHidden));
|
|
750
750
|
stationScan.manual = true;
|
|
751
751
|
|
|
752
|
+
// A manual station is the router link of a bridge or shared gateway:
|
|
753
|
+
// one known AP, no mesh scan choosing among candidates. For that link
|
|
754
|
+
// the core's own auto-reconnect is the right mechanism, and it is what
|
|
755
|
+
// kept every bridge's upstream alive until now — by accident, on core
|
|
756
|
+
// 2.x, where init() had never really turned it off. With it off
|
|
757
|
+
// everywhere, a bridge whose second association with the router failed
|
|
758
|
+
// (init() drops the first to start the mesh) sat at WL_IDLE_STATUS for
|
|
759
|
+
// the whole run with no upstream, and no node ever learned of the
|
|
760
|
+
// Internet. On for the manual link; the mesh station in init() keeps
|
|
761
|
+
// it off.
|
|
762
|
+
WiFi.setAutoReconnect(true);
|
|
763
|
+
|
|
752
764
|
// Directly initiate connection - ESP will auto-detect router's channel
|
|
753
765
|
WiFi.begin(ssid.c_str(), password.c_str());
|
|
754
766
|
|
|
@@ -769,9 +781,61 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
769
781
|
// Schedule reconnection after disconnect completes
|
|
770
782
|
// The WiFi event handler will signal when disconnect is complete
|
|
771
783
|
_pendingStationReconnect = true;
|
|
772
|
-
} else {
|
|
773
|
-
//
|
|
784
|
+
} else if (_pendingStationReconnect) {
|
|
785
|
+
// A drop this node asked for: reconnect from the last scan
|
|
774
786
|
handleStationDisconnectComplete();
|
|
787
|
+
} else if (this->stationScan.droppedByMove()) {
|
|
788
|
+
// The drop this node's own channel follow caused.
|
|
789
|
+
// followBridgeChannel() closed the link and scans next;
|
|
790
|
+
// nothing to do here, and above all no re-detection.
|
|
791
|
+
using namespace logger;
|
|
792
|
+
Log(CONNECTION,
|
|
793
|
+
"Station link closed by this node's channel move\n");
|
|
794
|
+
} else if (!this->stationScan.stationLinkUp) {
|
|
795
|
+
// An attempt that never got an address — the association
|
|
796
|
+
// timed out, or the AP was gone by the time it was tried.
|
|
797
|
+
// Not a loss: scan this channel again, where the next AP is.
|
|
798
|
+
// Re-detecting here sent the sender in sweep 45 run 2, just
|
|
799
|
+
// arrived on the bridge's channel after a peer there had
|
|
800
|
+
// left, to look at the old channel and consider going back,
|
|
801
|
+
// with the bridge's AP at -55 dBm beside it.
|
|
802
|
+
using namespace logger;
|
|
803
|
+
Log(CONNECTION,
|
|
804
|
+
"Station attempt failed, scanning this channel again\n");
|
|
805
|
+
this->stationScan.task.forceNextIteration();
|
|
806
|
+
} else {
|
|
807
|
+
// A drop nobody asked for — the AP this station was on went
|
|
808
|
+
// away (its node rebooted, or a bridge moved the mesh). Scan
|
|
809
|
+
// now rather than when the current slow-scan delay runs out,
|
|
810
|
+
// which is up to four intervals away for a node that was
|
|
811
|
+
// stable a moment ago.
|
|
812
|
+
using namespace logger;
|
|
813
|
+
Log(CONNECTION,
|
|
814
|
+
"Station link lost unexpectedly, scanning now\n");
|
|
815
|
+
if (this->shouldContainRoot && this->stationScan.atHome()) {
|
|
816
|
+
// At home the bridge's AP is on this channel; the uplink
|
|
817
|
+
// that went was a relay (in sweep 46 run 2, the backup
|
|
818
|
+
// rebooting into its failover role). Scan this channel
|
|
819
|
+
// now: the all-channel hunt below took the sender twenty
|
|
820
|
+
// seconds, and its request through the bridge ran out of
|
|
821
|
+
// retries in the gap.
|
|
822
|
+
Log(CONNECTION,
|
|
823
|
+
"Station link lost at home: scanning this channel\n");
|
|
824
|
+
} else if (this->shouldContainRoot) {
|
|
825
|
+
// In a mesh that should have a root, the AP that went away
|
|
826
|
+
// most likely left for the bridge's channel, and the nodes
|
|
827
|
+
// still on this one are about to. Re-attaching here first
|
|
828
|
+
// cost the node the failover test sends from a hundred
|
|
829
|
+
// seconds at the bridge's start: it joined one remnant,
|
|
830
|
+
// lost it, joined the next, lost that, and only then looked
|
|
831
|
+
// at every channel. Look at every channel now; the rules in
|
|
832
|
+
// scanComplete() decide whether to go.
|
|
833
|
+
Log(CONNECTION,
|
|
834
|
+
"Station link lost in a rooted mesh: re-detecting the "
|
|
835
|
+
"mesh channel on this scan\n");
|
|
836
|
+
this->stationScan.redetectOnNextScan();
|
|
837
|
+
}
|
|
838
|
+
this->stationScan.task.forceNextIteration();
|
|
775
839
|
}
|
|
776
840
|
}
|
|
777
841
|
});
|
|
@@ -794,10 +858,36 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
794
858
|
void tcpServerInit() {
|
|
795
859
|
using namespace logger;
|
|
796
860
|
Log(GENERAL, "tcpServerInit():\n");
|
|
861
|
+
// A listener that exists and listens is kept. It is bound to every
|
|
862
|
+
// address, so the AP a re-initialised node brings up is served by it,
|
|
863
|
+
// and re-creating it is not merely needless: the connections the old
|
|
864
|
+
// one accepted share its local port, and lwIP refuses to bind a new
|
|
865
|
+
// listener to a port any of them still holds — for the two minutes
|
|
866
|
+
// they sit in TIME_WAIT after stop() closes them. A node promoted to
|
|
867
|
+
// bridge on the rig logged "bind error: -8" on every attempt for
|
|
868
|
+
// exactly that long, reset every peer that came to join it, and only
|
|
869
|
+
// then had a bridge's listener. AsyncTCP keeps its pcb private, so
|
|
870
|
+
// SO_REUSEADDR cannot be set from here; not re-binding is the fix.
|
|
871
|
+
if (_tcpListener != nullptr) {
|
|
872
|
+
if (_tcpListener->status() == 1) {
|
|
873
|
+
Log(CONNECTION,
|
|
874
|
+
"tcpServerInit(): listener on port %d already listening, kept\n",
|
|
875
|
+
_meshPort);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
delete _tcpListener;
|
|
879
|
+
_tcpListener = nullptr;
|
|
880
|
+
}
|
|
797
881
|
_tcpListener = new AsyncServer(_meshPort);
|
|
798
882
|
painlessmesh::tcp::initServer<Connection, painlessmesh::Mesh<Connection>>(
|
|
799
883
|
(*_tcpListener), (*this));
|
|
800
884
|
Log(STARTUP, "AP tcp server established on port %d\n", _meshPort);
|
|
885
|
+
// The listener's state, at the level the rig keeps: a node promoted to
|
|
886
|
+
// bridge re-creates its listener, and one such node reset every
|
|
887
|
+
// connection to its AP for its whole time as bridge while its log said
|
|
888
|
+
// nothing. LISTEN is 1 on both cores; anything else is the finding.
|
|
889
|
+
Log(CONNECTION, "tcpServerInit(): listener on port %d, state %u\n",
|
|
890
|
+
_meshPort, (unsigned)_tcpListener->status());
|
|
801
891
|
return;
|
|
802
892
|
}
|
|
803
893
|
|
|
@@ -817,8 +907,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
817
907
|
* TCP Connection Retry:
|
|
818
908
|
* The TCP connection now includes automatic retry with exponential backoff.
|
|
819
909
|
* If the initial connection fails (error -14 ERR_CONN or other errors),
|
|
820
|
-
* the system will retry up to
|
|
821
|
-
* triggering a full WiFi reconnection cycle
|
|
910
|
+
* the system will retry up to the configured maxRetries times before
|
|
911
|
+
* triggering a full WiFi reconnection cycle (see setTcpRetryConfig()).
|
|
912
|
+
* This helps handle:
|
|
822
913
|
* - Timing issues where TCP server is not ready immediately
|
|
823
914
|
* - Network stack stabilization after IP acquisition
|
|
824
915
|
* - Transient network conditions
|
|
@@ -842,8 +933,11 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
842
933
|
// This helps prevent error -14 (ERR_CONN) by allowing the network stack
|
|
843
934
|
// and TCP server to be fully ready. The delay is added via task scheduler
|
|
844
935
|
// to avoid blocking the event loop.
|
|
936
|
+
// The delay is read at schedule time, so calling setTcpRetryConfig()
|
|
937
|
+
// while a connect is already pending affects the next attempt, not the
|
|
938
|
+
// in-flight one.
|
|
845
939
|
this->addTask(
|
|
846
|
-
|
|
940
|
+
this->getTcpRetryConfig().stabilizationDelayMs, TASK_ONCE,
|
|
847
941
|
[this, targetIP, targetPort]() {
|
|
848
942
|
// Verify WiFi is still connected after the delay
|
|
849
943
|
if (WiFi.status() != WL_CONNECTED || !WiFi.localIP()) {
|
|
@@ -1300,7 +1394,87 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1300
1394
|
*/
|
|
1301
1395
|
bool isMultiBridgeEnabled() const { return multiBridgeEnabled; }
|
|
1302
1396
|
|
|
1397
|
+
// The bridge monitor's check, one tick of the 30 s task: with no
|
|
1398
|
+
// healthy bridge known, an election is scheduled. Also run when a
|
|
1399
|
+
// bridge announces that it is stepping down, as soon as the startup
|
|
1400
|
+
// period allows.
|
|
1401
|
+
void checkForBridge() {
|
|
1402
|
+
using namespace logger;
|
|
1403
|
+
// Only a failover candidate checks: one with failover enabled and the
|
|
1404
|
+
// router's credentials. Every other node scheduled an election it
|
|
1405
|
+
// could not join, every 30 s, when this guard went missing.
|
|
1406
|
+
if (!bridgeFailoverEnabled || !routerCredentialsConfigured) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Don't check if we're already a bridge
|
|
1411
|
+
if (this->isBridge()) {
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Skip check during startup period to allow initial bridge discovery
|
|
1416
|
+
if (millis() < electionStartupDelayMs) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// IMPORTANT: Don't trigger election if we're disconnected from the mesh
|
|
1421
|
+
// When isolated, we can't receive bridge status broadcasts, so lack of
|
|
1422
|
+
// healthy bridge could simply mean WE are disconnected, not that the
|
|
1423
|
+
// bridge is unavailable. Wait until mesh connectivity is restored before
|
|
1424
|
+
// considering an election.
|
|
1425
|
+
if (!this->hasActiveMeshConnections()) {
|
|
1426
|
+
Log(CONNECTION,
|
|
1427
|
+
"Bridge monitor: Skipping - no active mesh connections\n");
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Check if there are any healthy bridges
|
|
1432
|
+
bool hasHealthyBridge = false;
|
|
1433
|
+
for (const auto& bridge : this->getBridges()) {
|
|
1434
|
+
if (bridge.isHealthy(bridgeTimeoutMs) && bridge.internetConnected) {
|
|
1435
|
+
hasHealthyBridge = true;
|
|
1436
|
+
break;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// If no healthy bridge exists, trigger an election
|
|
1441
|
+
if (!hasHealthyBridge) {
|
|
1442
|
+
Log(CONNECTION,
|
|
1443
|
+
"Bridge monitor: No healthy bridge detected, triggering "
|
|
1444
|
+
"election\n");
|
|
1445
|
+
// Random delay to prevent simultaneous elections when multiple nodes
|
|
1446
|
+
// start together
|
|
1447
|
+
uint32_t randomDelay =
|
|
1448
|
+
random(electionRandomDelayMinMs, electionRandomDelayMaxMs);
|
|
1449
|
+
Log(CONNECTION, "Bridge monitor: Scheduling election in %u ms\n",
|
|
1450
|
+
randomDelay);
|
|
1451
|
+
this->addTask(randomDelay, TASK_ONCE,
|
|
1452
|
+
[this]() { this->startBridgeElection(); });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Schedule checkForBridge() once, after the startup period if it is
|
|
1457
|
+
// still running, plus a second for the news to settle.
|
|
1458
|
+
void checkForBridgeSoon() {
|
|
1459
|
+
if (bridgeCheckPending) return;
|
|
1460
|
+
uint32_t wait = millis() < electionStartupDelayMs
|
|
1461
|
+
? electionStartupDelayMs - millis()
|
|
1462
|
+
: 0;
|
|
1463
|
+
bridgeCheckPending = true;
|
|
1464
|
+
this->addTask(wait + 1000, TASK_ONCE, [this]() {
|
|
1465
|
+
bridgeCheckPending = false;
|
|
1466
|
+
this->checkForBridge();
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1303
1470
|
void stop() {
|
|
1471
|
+
// A bridge stepping down says so before its connections close, so the
|
|
1472
|
+
// candidates hold an election now rather than when its last status
|
|
1473
|
+
// ages out. The short wait lets the broadcast leave the buffers.
|
|
1474
|
+
if (this->isBridge() && bridgeStatusBroadcastEnabled) {
|
|
1475
|
+
sendBridgeStatus(true);
|
|
1476
|
+
delay(200);
|
|
1477
|
+
}
|
|
1304
1478
|
// remove all WiFi events
|
|
1305
1479
|
#ifdef ESP32
|
|
1306
1480
|
WiFi.removeEvent(eventScanDoneHandler);
|
|
@@ -1323,8 +1497,12 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1323
1497
|
// Shutdown wifi hardware
|
|
1324
1498
|
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
1325
1499
|
|
|
1326
|
-
//
|
|
1327
|
-
|
|
1500
|
+
// The TCP listener stays. A node that stops to re-initialise in place
|
|
1501
|
+
// — a promotion to bridge, a return to a regular node — needs a
|
|
1502
|
+
// listener again at once, and a new one cannot bind while the
|
|
1503
|
+
// connections this one accepted are still in TIME_WAIT on the same
|
|
1504
|
+
// port (see tcpServerInit()). The connections themselves are closed
|
|
1505
|
+
// above; a client accepted in the gap before init() is closed by it.
|
|
1328
1506
|
}
|
|
1329
1507
|
|
|
1330
1508
|
protected:
|
|
@@ -1433,6 +1611,24 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1433
1611
|
"Node %u connection changed, sending bridge status directly\n",
|
|
1434
1612
|
nodeId);
|
|
1435
1613
|
|
|
1614
|
+
// The direct send below reaches the neighbour whose connection
|
|
1615
|
+
// changed. A node that joined behind that neighbour is why it changed,
|
|
1616
|
+
// and it learns of this bridge only from the next periodic broadcast,
|
|
1617
|
+
// up to thirty seconds on: on the rig the node the discovery fixture
|
|
1618
|
+
// sends from joined a child of the bridge at 185 s and heard the
|
|
1619
|
+
// bridge at 214 s, after the fixture's window. So a change also
|
|
1620
|
+
// brings the broadcast forward, at most once every five seconds.
|
|
1621
|
+
if (millis() - _lastBridgeStatusBroadcast >= 5000) {
|
|
1622
|
+
this->addTask(1000, TASK_ONCE, [this]() {
|
|
1623
|
+
if (millis() - _lastBridgeStatusBroadcast >= 5000) {
|
|
1624
|
+
Log(CONNECTION,
|
|
1625
|
+
"Topology changed; broadcasting bridge status now rather "
|
|
1626
|
+
"than at the next interval\n");
|
|
1627
|
+
this->sendBridgeStatus();
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1436
1632
|
// Small delay to ensure connection is fully stable, then send directly to
|
|
1437
1633
|
// the new node This avoids issues with time sync blocking broadcast
|
|
1438
1634
|
// messages
|
|
@@ -1652,20 +1848,60 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1652
1848
|
* @param routerSSID SSID of router to scan for
|
|
1653
1849
|
* @return RSSI in dBm (negative number, -127 to 0), or 0 if not found
|
|
1654
1850
|
*/
|
|
1655
|
-
int8_t scanRouterSignalStrength(TSTRING routerSSID
|
|
1851
|
+
int8_t scanRouterSignalStrength(TSTRING routerSSID,
|
|
1852
|
+
uint8_t* routerChannel = nullptr) {
|
|
1656
1853
|
using namespace logger;
|
|
1657
1854
|
Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n",
|
|
1658
1855
|
routerSSID.c_str());
|
|
1659
1856
|
|
|
1660
1857
|
int n = WiFi.scanNetworks(false, false);
|
|
1858
|
+
if (n == WIFI_SCAN_RUNNING) {
|
|
1859
|
+
// The station task's asynchronous scan is in flight; a synchronous
|
|
1860
|
+
// scan cannot start until it ends, and -2 is not "no networks". A
|
|
1861
|
+
// failover backup read it as the router being out of sight — twice,
|
|
1862
|
+
// thirty seconds apart — and never stood for election while the
|
|
1863
|
+
// primary was gone. Wait the scan out, bounded, then look again.
|
|
1864
|
+
Log(CONNECTION,
|
|
1865
|
+
"scanRouterSignalStrength(): a scan is already running, waiting "
|
|
1866
|
+
"for it\n");
|
|
1867
|
+
uint32_t waitedUntil = millis() + 8000;
|
|
1868
|
+
#ifdef ESP32
|
|
1869
|
+
// Not WiFi.scanComplete(): on this core it gives a scan twenty
|
|
1870
|
+
// dwell times to finish and then declares it failed — 2.4 s for the
|
|
1871
|
+
// 120 ms all-channel re-detection, which takes six in AP+STA mode
|
|
1872
|
+
// as the radio keeps returning to serve the AP. Asked here at three
|
|
1873
|
+
// seconds, it dropped the scanning flag, the scan below started on
|
|
1874
|
+
// top of the one in flight, and both came back with nothing: the
|
|
1875
|
+
// backup read "router not found" and the station "mesh not found on
|
|
1876
|
+
// any channel", and lost the sighting it needed to follow the mesh.
|
|
1877
|
+
// The scanning bit is the flag the driver clears when the scan
|
|
1878
|
+
// really ends.
|
|
1879
|
+
while ((WiFiGenericClass::getStatusBits() & WIFI_SCANNING_BIT) &&
|
|
1880
|
+
(int32_t)(waitedUntil - millis()) > 0) {
|
|
1881
|
+
delay(50);
|
|
1882
|
+
}
|
|
1883
|
+
// The scan-done event hands the station its results next; let it
|
|
1884
|
+
// read them before this scan deletes them.
|
|
1885
|
+
delay(300);
|
|
1886
|
+
#else
|
|
1887
|
+
while (WiFi.scanComplete() == WIFI_SCAN_RUNNING &&
|
|
1888
|
+
(int32_t)(waitedUntil - millis()) > 0) {
|
|
1889
|
+
delay(50);
|
|
1890
|
+
}
|
|
1891
|
+
#endif
|
|
1892
|
+
n = WiFi.scanNetworks(false, false);
|
|
1893
|
+
}
|
|
1661
1894
|
Log(CONNECTION, "scanRouterSignalStrength(): Found %d networks\n", n);
|
|
1662
1895
|
|
|
1663
1896
|
for (int i = 0; i < n; i++) {
|
|
1664
1897
|
if (WiFi.SSID(i) == routerSSID) {
|
|
1665
1898
|
int8_t rssi = WiFi.RSSI(i);
|
|
1899
|
+
uint8_t channel = WiFi.channel(i);
|
|
1900
|
+
if (routerChannel != nullptr) *routerChannel = channel;
|
|
1666
1901
|
Log(CONNECTION,
|
|
1667
|
-
"scanRouterSignalStrength(): Found %s with RSSI %d dBm
|
|
1668
|
-
|
|
1902
|
+
"scanRouterSignalStrength(): Found %s with RSSI %d dBm on "
|
|
1903
|
+
"channel %u\n",
|
|
1904
|
+
routerSSID.c_str(), rssi, channel);
|
|
1669
1905
|
return rssi;
|
|
1670
1906
|
}
|
|
1671
1907
|
}
|
|
@@ -1698,10 +1934,23 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1698
1934
|
return;
|
|
1699
1935
|
}
|
|
1700
1936
|
|
|
1701
|
-
// Prevent rapid role changes
|
|
1937
|
+
// Prevent rapid role changes — but look again when the hold is over.
|
|
1938
|
+
// A candidate that stood down a moment ago, or that only just booted,
|
|
1939
|
+
// returned here silently and waited for the next 30 s tick to find
|
|
1940
|
+
// out that there was still no bridge.
|
|
1702
1941
|
if (millis() - lastRoleChangeTime < 60000) {
|
|
1942
|
+
uint32_t wait = 60000 - (millis() - lastRoleChangeTime);
|
|
1703
1943
|
Log(CONNECTION,
|
|
1704
|
-
"startBridgeElection(): Too soon after last role change
|
|
1944
|
+
"startBridgeElection(): Too soon after last role change, "
|
|
1945
|
+
"checking again in %u ms\n",
|
|
1946
|
+
wait);
|
|
1947
|
+
if (!bridgeCheckPending) {
|
|
1948
|
+
bridgeCheckPending = true;
|
|
1949
|
+
this->addTask(wait, TASK_ONCE, [this]() {
|
|
1950
|
+
bridgeCheckPending = false;
|
|
1951
|
+
this->checkForBridge();
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1705
1954
|
return;
|
|
1706
1955
|
}
|
|
1707
1956
|
|
|
@@ -1720,7 +1969,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1720
1969
|
|
|
1721
1970
|
// Schedule a retry after channel re-sync has had a chance to run
|
|
1722
1971
|
// The channel re-sync threshold is StationScan::EMPTY_SCAN_THRESHOLD
|
|
1723
|
-
// scans (
|
|
1972
|
+
// scans (2, ~30 s). Fast scan interval is 0.5 * SCAN_INTERVAL = 15
|
|
1724
1973
|
// seconds Wait for re-sync to complete plus a buffer
|
|
1725
1974
|
uint32_t retryDelay =
|
|
1726
1975
|
(StationScan::EMPTY_SCAN_THRESHOLD - emptyScans + 2) * 15000;
|
|
@@ -1735,7 +1984,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1735
1984
|
electionState = ELECTION_SCANNING;
|
|
1736
1985
|
|
|
1737
1986
|
// Scan for router to get RSSI
|
|
1738
|
-
|
|
1987
|
+
uint8_t routerChannel = 0;
|
|
1988
|
+
int8_t routerRSSI =
|
|
1989
|
+
scanRouterSignalStrength(routerSSID, &routerChannel);
|
|
1739
1990
|
|
|
1740
1991
|
if (routerRSSI == 0) {
|
|
1741
1992
|
Log(CONNECTION,
|
|
@@ -1754,6 +2005,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1754
2005
|
BridgeCandidate selfCandidate;
|
|
1755
2006
|
selfCandidate.nodeId = this->nodeId;
|
|
1756
2007
|
selfCandidate.routerRSSI = routerRSSI;
|
|
2008
|
+
selfCandidate.routerChannel = routerChannel;
|
|
1757
2009
|
selfCandidate.uptime = millis();
|
|
1758
2010
|
selfCandidate.freeMemory = ESP.getFreeHeap();
|
|
1759
2011
|
electionCandidates.push_back(selfCandidate);
|
|
@@ -1766,6 +2018,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1766
2018
|
obj["from"] = this->nodeId;
|
|
1767
2019
|
obj["routing"] = 2; // BROADCAST
|
|
1768
2020
|
obj["routerRSSI"] = routerRSSI;
|
|
2021
|
+
obj["routerChannel"] = routerChannel;
|
|
1769
2022
|
obj["uptime"] = millis();
|
|
1770
2023
|
obj["freeMemory"] = ESP.getFreeHeap();
|
|
1771
2024
|
obj["timestamp"] = this->getNodeTime();
|
|
@@ -1778,7 +2031,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1778
2031
|
// Send election message using raw broadcast to preserve type
|
|
1779
2032
|
// BRIDGE_ELECTION
|
|
1780
2033
|
protocol::Variant variant(msg);
|
|
1781
|
-
router::broadcast<
|
|
2034
|
+
router::broadcast<Connection>(variant, (*this), 0);
|
|
1782
2035
|
|
|
1783
2036
|
Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
|
|
1784
2037
|
|
|
@@ -1898,7 +2151,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1898
2151
|
|
|
1899
2152
|
if (winner->nodeId == this->nodeId) {
|
|
1900
2153
|
Log(CONNECTION, "[TARGET] I WON! Promoting to bridge...\n");
|
|
1901
|
-
promoteToBridge();
|
|
2154
|
+
promoteToBridge(winner->routerChannel);
|
|
1902
2155
|
} else {
|
|
1903
2156
|
Log(CONNECTION, "Winner is node %u, remaining as regular node\n",
|
|
1904
2157
|
winner->nodeId);
|
|
@@ -1920,7 +2173,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1920
2173
|
* Instead, we schedule the actual promotion work to run after the current
|
|
1921
2174
|
* task completes, allowing safe cleanup of task structures.
|
|
1922
2175
|
*/
|
|
1923
|
-
void promoteToBridge() {
|
|
2176
|
+
void promoteToBridge(uint8_t routerChannel) {
|
|
1924
2177
|
using namespace logger;
|
|
1925
2178
|
|
|
1926
2179
|
Log(STARTUP, "=== Becoming Bridge Node ===\n");
|
|
@@ -1946,6 +2199,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1946
2199
|
obj["previousBridge"] = previousBridgeId;
|
|
1947
2200
|
obj["reason"] = "Election winner - best router signal";
|
|
1948
2201
|
obj["routerRSSI"] = 0; // Not yet connected to router
|
|
2202
|
+
obj["routerChannel"] = routerChannel;
|
|
1949
2203
|
obj["timestamp"] = this->getNodeTime();
|
|
1950
2204
|
obj["message_type"] = protocol::BRIDGE_TAKEOVER;
|
|
1951
2205
|
|
|
@@ -1955,21 +2209,38 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1955
2209
|
// Send takeover message using raw broadcast to preserve type
|
|
1956
2210
|
// BRIDGE_TAKEOVER
|
|
1957
2211
|
protocol::Variant variant(msg);
|
|
1958
|
-
router::broadcast<
|
|
2212
|
+
router::broadcast<Connection>(variant, (*this), 0);
|
|
1959
2213
|
|
|
1960
2214
|
// Give time for announcement to propagate before channel switch
|
|
1961
2215
|
// Allow event loop processing during hardware settling
|
|
1962
2216
|
for (int i = 0; i < 100; i++) { delay(10); yield(); }
|
|
1963
2217
|
Log(STARTUP, "[OK] Takeover announcement sent on channel %d\n", _meshChannel);
|
|
1964
2218
|
|
|
1965
|
-
// Save current mesh configuration to restore if bridge init fails
|
|
2219
|
+
// Save current mesh configuration to restore if bridge init fails.
|
|
2220
|
+
// The copies below are captured by value into the deferred lambda so the
|
|
2221
|
+
// stop() call inside it cannot clear/mutate these members before the
|
|
2222
|
+
// reinit reads them. (The Mesh object itself outlives the lambda; this
|
|
2223
|
+
// protects against member mutation, not object lifetime.)
|
|
2224
|
+
// TODO(#373 follow-up): if the mesh owns its scheduler
|
|
2225
|
+
// (!isExternalScheduler), stop() deletes mScheduler and savedScheduler
|
|
2226
|
+
// dangles before initAsBridge() uses it. Pre-existing limitation —
|
|
2227
|
+
// bridge promotion requires a user-supplied scheduler.
|
|
1966
2228
|
uint8_t savedChannel = _meshChannel;
|
|
2229
|
+
TSTRING savedMeshSSID = _meshSSID;
|
|
2230
|
+
TSTRING savedMeshPassword = _meshPassword;
|
|
2231
|
+
TSTRING savedRouterSSID = routerSSID;
|
|
2232
|
+
TSTRING savedRouterPassword = routerPassword;
|
|
2233
|
+
Scheduler *savedScheduler = mScheduler;
|
|
2234
|
+
auto savedBridgeRoleChangedCallback = bridgeRoleChangedCallback;
|
|
1967
2235
|
|
|
1968
2236
|
// CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
|
|
1969
2237
|
// This prevents use-after-free crash when stop() clears taskList while
|
|
1970
2238
|
// evaluateElection() task is still executing
|
|
1971
2239
|
// Use minimal delay to allow current task to complete first
|
|
1972
|
-
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
2240
|
+
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
2241
|
+
[this, savedChannel, savedMeshSSID, savedMeshPassword,
|
|
2242
|
+
savedRouterSSID, savedRouterPassword, savedScheduler,
|
|
2243
|
+
savedBridgeRoleChangedCallback]() {
|
|
1973
2244
|
using namespace logger;
|
|
1974
2245
|
|
|
1975
2246
|
Log(STARTUP, "Executing bridge promotion (stop/reinit cycle)\n");
|
|
@@ -1981,18 +2252,17 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1981
2252
|
|
|
1982
2253
|
// initAsBridge always returns true: bridge mesh functionality is active
|
|
1983
2254
|
// regardless of router connection status (router connection is opportunistic)
|
|
1984
|
-
this->initAsBridge(
|
|
1985
|
-
|
|
2255
|
+
this->initAsBridge(savedMeshSSID, savedMeshPassword, savedRouterSSID,
|
|
2256
|
+
savedRouterPassword, savedScheduler, _meshPort);
|
|
1986
2257
|
|
|
1987
2258
|
lastRoleChangeTime = millis();
|
|
1988
2259
|
|
|
1989
2260
|
Log(STARTUP, "[OK] Bridge promotion complete on channel %d\n", _meshChannel);
|
|
1990
2261
|
|
|
1991
2262
|
// Notify via callback
|
|
1992
|
-
|
|
1993
|
-
if (bridgeRoleChangedCallback) {
|
|
2263
|
+
if (savedBridgeRoleChangedCallback) {
|
|
1994
2264
|
static const TSTRING reason = "Election winner - best router signal";
|
|
1995
|
-
|
|
2265
|
+
savedBridgeRoleChangedCallback(true, reason);
|
|
1996
2266
|
}
|
|
1997
2267
|
|
|
1998
2268
|
// Note: The initial takeover announcement was already sent earlier
|
|
@@ -2065,14 +2335,27 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2065
2335
|
Log(CONNECTION,
|
|
2066
2336
|
"Scheduling stop/reinit (async to avoid task corruption)\n");
|
|
2067
2337
|
|
|
2068
|
-
// Save current mesh configuration
|
|
2338
|
+
// Save current mesh configuration. Captured by value into the deferred
|
|
2339
|
+
// lambda so stop() cannot clear/mutate these members before the reinit
|
|
2340
|
+
// reads them (see the matching comment in promoteToBridge(); same
|
|
2341
|
+
// TODO(#373 follow-up) about savedScheduler and internal schedulers
|
|
2342
|
+
// applies here).
|
|
2069
2343
|
uint8_t savedChannel = _meshChannel;
|
|
2344
|
+
TSTRING savedMeshSSID = _meshSSID;
|
|
2345
|
+
TSTRING savedMeshPassword = _meshPassword;
|
|
2346
|
+
TSTRING savedRouterSSID = routerSSID;
|
|
2347
|
+
TSTRING savedRouterPassword = routerPassword;
|
|
2348
|
+
Scheduler *savedScheduler = mScheduler;
|
|
2349
|
+
auto savedBridgeRoleChangedCallback = bridgeRoleChangedCallback;
|
|
2070
2350
|
|
|
2071
2351
|
// CRITICAL FIX: Schedule the stop/reinit work to run after current task completes
|
|
2072
2352
|
// This prevents use-after-free crash when stop() clears taskList while
|
|
2073
2353
|
// the retry task is still executing
|
|
2074
2354
|
// Use minimal delay to allow current task to complete first
|
|
2075
|
-
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
2355
|
+
this->addTask(ASYNC_PROMOTION_DELAY_MS, TASK_ONCE,
|
|
2356
|
+
[this, savedChannel, savedMeshSSID, savedMeshPassword,
|
|
2357
|
+
savedRouterSSID, savedRouterPassword, savedScheduler,
|
|
2358
|
+
savedBridgeRoleChangedCallback]() {
|
|
2076
2359
|
using namespace logger;
|
|
2077
2360
|
|
|
2078
2361
|
Log(CONNECTION, "Executing isolated bridge promotion (stop/reinit cycle)\n");
|
|
@@ -2084,8 +2367,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2084
2367
|
|
|
2085
2368
|
// initAsBridge always returns true: bridge mesh functionality is active
|
|
2086
2369
|
// regardless of router connection status (router connection is opportunistic)
|
|
2087
|
-
this->initAsBridge(
|
|
2088
|
-
|
|
2370
|
+
this->initAsBridge(savedMeshSSID, savedMeshPassword, savedRouterSSID,
|
|
2371
|
+
savedRouterPassword, savedScheduler, _meshPort);
|
|
2089
2372
|
|
|
2090
2373
|
// Reset retry counter
|
|
2091
2374
|
_isolatedBridgeRetryAttempts = 0;
|
|
@@ -2095,10 +2378,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2095
2378
|
_meshChannel);
|
|
2096
2379
|
|
|
2097
2380
|
// Notify via callback
|
|
2098
|
-
|
|
2099
|
-
if (bridgeRoleChangedCallback) {
|
|
2381
|
+
if (savedBridgeRoleChangedCallback) {
|
|
2100
2382
|
static const TSTRING reason = "Isolated node promoted to bridge";
|
|
2101
|
-
|
|
2383
|
+
savedBridgeRoleChangedCallback(true, reason);
|
|
2102
2384
|
}
|
|
2103
2385
|
|
|
2104
2386
|
// Note: Bridge status announcement will be sent automatically by
|
|
@@ -2120,7 +2402,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2120
2402
|
* Called by package handler when election message arrives
|
|
2121
2403
|
*/
|
|
2122
2404
|
void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI,
|
|
2123
|
-
|
|
2405
|
+
uint8_t routerChannel, uint32_t uptime,
|
|
2406
|
+
uint32_t freeMemory) {
|
|
2124
2407
|
using namespace logger;
|
|
2125
2408
|
|
|
2126
2409
|
if (electionState != ELECTION_COLLECTING) {
|
|
@@ -2142,6 +2425,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2142
2425
|
BridgeCandidate candidate;
|
|
2143
2426
|
candidate.nodeId = fromNode;
|
|
2144
2427
|
candidate.routerRSSI = routerRSSI;
|
|
2428
|
+
candidate.routerChannel = routerChannel;
|
|
2145
2429
|
candidate.uptime = uptime;
|
|
2146
2430
|
candidate.freeMemory = freeMemory;
|
|
2147
2431
|
|
|
@@ -2156,13 +2440,29 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2156
2440
|
* Send bridge status broadcast
|
|
2157
2441
|
* Called periodically by bridge nodes to report connectivity status
|
|
2158
2442
|
*/
|
|
2159
|
-
void sendBridgeStatus() {
|
|
2443
|
+
void sendBridgeStatus(bool leaving = false) {
|
|
2160
2444
|
using namespace logger;
|
|
2161
2445
|
|
|
2162
2446
|
if (!this->bridgeStatusBroadcastEnabled) {
|
|
2163
2447
|
return;
|
|
2164
2448
|
}
|
|
2165
2449
|
|
|
2450
|
+
// A bridge that nobody can reach is no bridge. On the rig a node
|
|
2451
|
+
// promoted in place reset every TCP connection to its AP for its whole
|
|
2452
|
+
// time as bridge — its listener, re-created by the promotion's
|
|
2453
|
+
// stop/re-init, was not listening, and nothing looked. This task runs
|
|
2454
|
+
// every thirty seconds on a bridge: if the listener is not in LISTEN
|
|
2455
|
+
// (1 on both cores) it is re-created, and the log says so.
|
|
2456
|
+
if (_tcpListener != nullptr && _tcpListener->status() != 1) {
|
|
2457
|
+
Log(ERROR,
|
|
2458
|
+
"sendBridgeStatus(): TCP listener on port %d is in state %u, not "
|
|
2459
|
+
"LISTEN; re-creating it\n",
|
|
2460
|
+
_meshPort, (unsigned)_tcpListener->status());
|
|
2461
|
+
delete _tcpListener;
|
|
2462
|
+
_tcpListener = nullptr;
|
|
2463
|
+
tcpServerInit();
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2166
2466
|
// Create bridge status package
|
|
2167
2467
|
// We need to include the package header here since we're in wifi namespace
|
|
2168
2468
|
// The package will be sent as a JSON string
|
|
@@ -2180,9 +2480,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2180
2480
|
// 2. Some networks (mobile hotspots) may not provide gateway IP via DHCP
|
|
2181
2481
|
// 3. Having a valid local IP + being connected is sufficient for internet
|
|
2182
2482
|
// access
|
|
2183
|
-
bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
|
|
2483
|
+
bool hasInternet = !leaving && (WiFi.status() == WL_CONNECTED) &&
|
|
2184
2484
|
(WiFi.localIP() != IPAddress(0, 0, 0, 0));
|
|
2185
2485
|
obj["internetConnected"] = hasInternet;
|
|
2486
|
+
// A bridge stepping down says so. Every candidate would otherwise hold
|
|
2487
|
+
// it healthy until its last status aged out, a minute and more, before
|
|
2488
|
+
// holding an election — on the rig, past the failover contract.
|
|
2489
|
+
if (leaving) obj["leaving"] = true;
|
|
2186
2490
|
|
|
2187
2491
|
int8_t rssi = WiFi.RSSI();
|
|
2188
2492
|
uint8_t channel = WiFi.channel();
|
|
@@ -2198,8 +2502,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2198
2502
|
String msg;
|
|
2199
2503
|
serializeJson(doc, msg);
|
|
2200
2504
|
|
|
2201
|
-
Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)\n",
|
|
2202
|
-
hasInternet ? "Connected" : "Disconnected"
|
|
2505
|
+
Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)%s\n",
|
|
2506
|
+
hasInternet ? "Connected" : "Disconnected",
|
|
2507
|
+
leaving ? " — stepping down" : "");
|
|
2203
2508
|
Log(GENERAL,
|
|
2204
2509
|
"sendBridgeStatus(): WiFi status=%d, localIP=%s, gatewayIP=%s\n",
|
|
2205
2510
|
WiFi.status(), WiFi.localIP().toString().c_str(),
|
|
@@ -2214,7 +2519,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2214
2519
|
// Using sendBroadcast(msg) would wrap it in type 8 (BROADCAST) and hide
|
|
2215
2520
|
// type BRIDGE_STATUS
|
|
2216
2521
|
protocol::Variant variant(msg);
|
|
2217
|
-
router::broadcast<
|
|
2522
|
+
router::broadcast<Connection>(variant, (*this), 0);
|
|
2523
|
+
_lastBridgeStatusBroadcast = millis();
|
|
2218
2524
|
}
|
|
2219
2525
|
|
|
2220
2526
|
/**
|
|
@@ -2233,7 +2539,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2233
2539
|
static uint32_t lastCheckTime = 0;
|
|
2234
2540
|
static bool lastResult = false;
|
|
2235
2541
|
uint32_t now = millis();
|
|
2236
|
-
if (lastCheckTime > 0 && (now - lastCheckTime) <
|
|
2542
|
+
if (lastCheckTime > 0 && (now - lastCheckTime) < GATEWAY_CONNECTIVITY_CACHE_MS) {
|
|
2237
2543
|
return lastResult;
|
|
2238
2544
|
}
|
|
2239
2545
|
|
|
@@ -2255,9 +2561,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2255
2561
|
// Using Google's servers as they have high availability globally
|
|
2256
2562
|
IPAddress result;
|
|
2257
2563
|
|
|
2258
|
-
#if defined(
|
|
2259
|
-
//
|
|
2260
|
-
|
|
2564
|
+
#if defined(ESP8266)
|
|
2565
|
+
// ESP8266's hostByName() has a timeout overload, so the resolver cannot
|
|
2566
|
+
// stall the cooperative scheduler past GATEWAY_DNS_TIMEOUT_MS — that term
|
|
2567
|
+
// is part of gatewayBlockingBudgetMs() and covered by its static_assert
|
|
2568
|
+
// (issue #416).
|
|
2569
|
+
int dnsResult =
|
|
2570
|
+
WiFi.hostByName("www.google.com", result, GATEWAY_DNS_TIMEOUT_MS);
|
|
2261
2571
|
|
|
2262
2572
|
// Check if DNS resolution succeeded
|
|
2263
2573
|
if (dnsResult != 1) {
|
|
@@ -2276,6 +2586,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2276
2586
|
lastResult = false;
|
|
2277
2587
|
return false;
|
|
2278
2588
|
}
|
|
2589
|
+
#elif defined(ESP32)
|
|
2590
|
+
// ESP32's hostByName() has no timeout parameter in the cores this library
|
|
2591
|
+
// targets, so a standalone DNS probe would be an unbounded stall on the
|
|
2592
|
+
// cooperative scheduler whenever DNS is slow or blackholed — the exact
|
|
2593
|
+
// failure mode of issues #318/#332, recurring once per
|
|
2594
|
+
// GATEWAY_CONNECTIVITY_CACHE_MS. The probe is therefore skipped on ESP32
|
|
2595
|
+
// (issue #416): actual reachability is established by the captive-portal
|
|
2596
|
+
// probe that runs right after this check on the same path — an HTTP
|
|
2597
|
+
// round trip bounded by GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS that fails on
|
|
2598
|
+
// a router without Internet just as the DNS probe would.
|
|
2599
|
+
(void)result;
|
|
2600
|
+
lastCheckTime = millis();
|
|
2601
|
+
lastResult = true;
|
|
2602
|
+
return true;
|
|
2279
2603
|
#else
|
|
2280
2604
|
// Other platforms: assume internet is available if WiFi connected
|
|
2281
2605
|
// (no reliable way to test without platform-specific APIs)
|
|
@@ -2309,8 +2633,24 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2309
2633
|
using namespace logger;
|
|
2310
2634
|
|
|
2311
2635
|
#if defined(ESP32) || defined(ESP8266)
|
|
2636
|
+
// Cache the verdict, exactly as hasActualInternetAccess() does. This probe
|
|
2637
|
+
// is a full HTTP round trip to an external host; running it per message
|
|
2638
|
+
// put that round trip in front of every single mesh->Internet send, on the
|
|
2639
|
+
// cooperative scheduler, where it stacks with the request's own timeout.
|
|
2640
|
+
static uint32_t lastCheckTime = 0;
|
|
2641
|
+
static bool lastResult = true;
|
|
2642
|
+
uint32_t now = millis();
|
|
2643
|
+
if (lastCheckTime > 0 && (now - lastCheckTime) < GATEWAY_CONNECTIVITY_CACHE_MS) {
|
|
2644
|
+
return lastResult;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// Record the attempt up front so that every early return below is cached
|
|
2648
|
+
// too -- otherwise a failing probe would be retried on every message.
|
|
2649
|
+
lastCheckTime = millis();
|
|
2650
|
+
lastResult = false;
|
|
2651
|
+
|
|
2312
2652
|
HTTPClient http;
|
|
2313
|
-
http.setTimeout(
|
|
2653
|
+
http.setTimeout(GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS);
|
|
2314
2654
|
|
|
2315
2655
|
WiFiClient client;
|
|
2316
2656
|
|
|
@@ -2355,6 +2695,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2355
2695
|
}
|
|
2356
2696
|
|
|
2357
2697
|
Log(COMMUNICATION, "detectCaptivePortal(): No captive portal detected\n");
|
|
2698
|
+
lastResult = true;
|
|
2358
2699
|
return true;
|
|
2359
2700
|
|
|
2360
2701
|
#else
|
|
@@ -2366,8 +2707,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2366
2707
|
/**
|
|
2367
2708
|
* Helper method to send gateway acknowledgment
|
|
2368
2709
|
*/
|
|
2369
|
-
void sendGatewayAck(
|
|
2370
|
-
|
|
2710
|
+
void sendGatewayAck(
|
|
2711
|
+
const gateway::GatewayDataPackage& request, bool success,
|
|
2712
|
+
uint16_t httpStatus, const TSTRING& error,
|
|
2713
|
+
std::shared_ptr<Connection> ingressConnection = nullptr) {
|
|
2371
2714
|
using namespace logger;
|
|
2372
2715
|
|
|
2373
2716
|
gateway::GatewayAckPackage ack;
|
|
@@ -2380,7 +2723,28 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2380
2723
|
ack.error = error;
|
|
2381
2724
|
ack.timestamp = this->getNodeTime();
|
|
2382
2725
|
|
|
2726
|
+
if (request.originNode == this->nodeId) {
|
|
2727
|
+
protocol::Variant variant(&ack);
|
|
2728
|
+
this->callbackList.execute(protocol::GATEWAY_ACK, variant, nullptr, 0);
|
|
2729
|
+
Log(COMMUNICATION,
|
|
2730
|
+
"Completed local GATEWAY_ACK (success=%d, http=%d)\n", success,
|
|
2731
|
+
httpStatus);
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2383
2735
|
auto conn = router::findRoute<Connection>((*this), request.originNode);
|
|
2736
|
+
if (!conn && ingressConnection) {
|
|
2737
|
+
// A newly promoted gateway can receive data before its NodeTree has
|
|
2738
|
+
// converged enough for findRoute() to resolve the request origin. The
|
|
2739
|
+
// ingress connection is nevertheless a valid reverse path: the request
|
|
2740
|
+
// just arrived through it and every intermediate node can continue
|
|
2741
|
+
// routing the addressed acknowledgment toward originNode.
|
|
2742
|
+
conn = ingressConnection;
|
|
2743
|
+
Log(COMMUNICATION,
|
|
2744
|
+
"Routing GATEWAY_ACK to node %u through request ingress while "
|
|
2745
|
+
"topology converges\n",
|
|
2746
|
+
request.originNode);
|
|
2747
|
+
}
|
|
2384
2748
|
if (conn) {
|
|
2385
2749
|
protocol::Variant variant(&ack);
|
|
2386
2750
|
router::send(std::move(variant), conn);
|
|
@@ -2401,15 +2765,26 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2401
2765
|
* makes the request, and sends back a GATEWAY_ACK with the result.
|
|
2402
2766
|
*
|
|
2403
2767
|
* Security notes:
|
|
2404
|
-
* -
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2407
|
-
*
|
|
2768
|
+
* - Neither target authenticates the TLS peer. The library configures no
|
|
2769
|
+
* trust anchor anywhere: there is no setCACert, no certificate bundle and
|
|
2770
|
+
* no fingerprint API on the gateway config, so an https:// destination is
|
|
2771
|
+
* not protected against an active man in the middle.
|
|
2772
|
+
* - ESP8266 calls setInsecure(), which disables certificate validation
|
|
2773
|
+
* outright.
|
|
2774
|
+
* - ESP32 calls http.begin(url) with no trust anchor supplied. What that
|
|
2775
|
+
* yields depends entirely on the Arduino-ESP32 core in use; do not read
|
|
2776
|
+
* it as verified. (An earlier revision of this comment claimed ESP32
|
|
2777
|
+
* "uses default SSL settings with certificate validation" -- that was
|
|
2778
|
+
* never backed by anything in src/.)
|
|
2779
|
+
* Treat gateway HTTPS as transport encryption without authentication, and
|
|
2780
|
+
* see SECURITY.md for the full threat model.
|
|
2408
2781
|
*
|
|
2409
2782
|
* Limitations:
|
|
2410
2783
|
* - HTTP redirects (3xx) are not automatically followed
|
|
2411
2784
|
* - Only 2xx status codes are treated as success
|
|
2412
|
-
* - Request timeout is
|
|
2785
|
+
* - Request timeout is GATEWAY_HTTP_TIMEOUT_MS, which is derived from
|
|
2786
|
+
* NODE_TIMEOUT and asserted to stay below it (see
|
|
2787
|
+
* painlessmesh/gateway.hpp)
|
|
2413
2788
|
*/
|
|
2414
2789
|
void initGatewayInternetHandler() {
|
|
2415
2790
|
using namespace logger;
|
|
@@ -2418,44 +2793,68 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2418
2793
|
|
|
2419
2794
|
this->callbackList.onPackage(
|
|
2420
2795
|
protocol::GATEWAY_DATA, [this](protocol::Variant& variant,
|
|
2421
|
-
std::shared_ptr<Connection>
|
|
2796
|
+
std::shared_ptr<Connection> ingress,
|
|
2797
|
+
uint32_t) {
|
|
2422
2798
|
auto pkg = variant.to<gateway::GatewayDataPackage>();
|
|
2423
2799
|
|
|
2424
2800
|
Log(COMMUNICATION,
|
|
2425
2801
|
"Gateway received Internet request: msgId=%u dest=%s\n",
|
|
2426
2802
|
pkg.messageId, pkg.destination.c_str());
|
|
2427
2803
|
|
|
2428
|
-
//
|
|
2429
|
-
//
|
|
2430
|
-
//
|
|
2431
|
-
//
|
|
2432
|
-
//
|
|
2433
|
-
//
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2804
|
+
// Every exit path below goes through finish(), which re-arms the
|
|
2805
|
+
// mesh watchdog on all peers before the ack is routed.
|
|
2806
|
+
//
|
|
2807
|
+
// Everything from here on can block: the connectivity probes are
|
|
2808
|
+
// network round trips and the request itself runs to
|
|
2809
|
+
// GATEWAY_HTTP_TIMEOUT_MS. Nothing on the cooperative scheduler runs
|
|
2810
|
+
// during that, but wall-clock time keeps passing, so any peer whose
|
|
2811
|
+
// NODE_TIMEOUT deadline fell inside the stall is overdue the instant
|
|
2812
|
+
// the scheduler resumes and gets closed despite never having gone
|
|
2813
|
+
// missing (issues #318, #332).
|
|
2814
|
+
//
|
|
2815
|
+
// The previous mitigation disabled timeOutTask on the requesting
|
|
2816
|
+
// connection only. That left every other peer exposed, and it acted
|
|
2817
|
+
// before the stall rather than after it, which is the half that
|
|
2818
|
+
// cannot work: the deadline is wall-clock, so pushing it out ahead of
|
|
2819
|
+
// a stall longer than the window changes nothing.
|
|
2820
|
+
//
|
|
2821
|
+
// Compensation is by the *measured* stall (issue #417): exit paths
|
|
2822
|
+
// that never blocked measure ~0 ms and grant nothing, so a peer
|
|
2823
|
+
// that genuinely stopped answering NODE_SYNC still gets reaped even
|
|
2824
|
+
// under continuous gateway traffic from other peers.
|
|
2825
|
+
const auto blockingStartedMs = millis();
|
|
2826
|
+
auto finish = [this, &pkg, ingress, blockingStartedMs](
|
|
2827
|
+
bool ok, uint16_t code, const TSTRING& err) {
|
|
2828
|
+
const auto stalledMs = millis() - blockingStartedMs;
|
|
2829
|
+
auto refreshed = gateway::refreshPeerWatchdogs(*this, stalledMs);
|
|
2830
|
+
if (refreshed > 0) {
|
|
2831
|
+
Log(COMMUNICATION,
|
|
2832
|
+
"Gateway postponed the mesh watchdog on %u peer(s) by %lu ms "
|
|
2833
|
+
"after a blocking Internet request\n",
|
|
2834
|
+
static_cast<unsigned>(refreshed),
|
|
2835
|
+
static_cast<unsigned long>(stalledMs));
|
|
2836
|
+
}
|
|
2837
|
+
this->sendGatewayAck(pkg, ok, code, err, ingress);
|
|
2838
|
+
};
|
|
2440
2839
|
|
|
2441
2840
|
// Check Internet connectivity
|
|
2442
2841
|
// First check WiFi status for quick fail
|
|
2443
2842
|
if (WiFi.status() != WL_CONNECTED) {
|
|
2444
|
-
|
|
2843
|
+
finish(false, 0, "Gateway WiFi not connected");
|
|
2445
2844
|
return true; // Consume package - we handled it (with error)
|
|
2446
2845
|
}
|
|
2447
2846
|
|
|
2448
2847
|
// Then check actual internet access (DNS resolution)
|
|
2449
2848
|
// This detects when WiFi is connected but router has no internet
|
|
2450
2849
|
if (!hasActualInternetAccess()) {
|
|
2451
|
-
|
|
2850
|
+
finish(false, 0, "Router has no internet access - check WAN connection");
|
|
2452
2851
|
return true; // Consume package - we handled it (with error)
|
|
2453
2852
|
}
|
|
2454
2853
|
|
|
2455
2854
|
// Finally, check for captive portal interference
|
|
2456
2855
|
// This detects when DNS works but HTTP requests are intercepted
|
|
2457
2856
|
if (!detectCaptivePortal()) {
|
|
2458
|
-
|
|
2857
|
+
finish(false, 0, "Captive portal detected - requires web authentication. Check router/WiFi settings");
|
|
2459
2858
|
return true; // Consume package - we handled it (with error)
|
|
2460
2859
|
}
|
|
2461
2860
|
|
|
@@ -2477,7 +2876,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2477
2876
|
|
|
2478
2877
|
if (pkg.destination.startsWith("https://")) {
|
|
2479
2878
|
#ifdef ESP32
|
|
2480
|
-
// ESP32:
|
|
2879
|
+
// ESP32: no trust anchor is configured, so the certificate is not
|
|
2880
|
+
// validated against anything this library supplies. See the
|
|
2881
|
+
// security notes on initGatewayInternetHandler() and SECURITY.md.
|
|
2481
2882
|
http.begin(pkg.destination.c_str());
|
|
2482
2883
|
#elif defined(ESP8266)
|
|
2483
2884
|
// ESP8266: Use insecure mode to reduce memory overhead
|
|
@@ -2557,10 +2958,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2557
2958
|
http.end();
|
|
2558
2959
|
|
|
2559
2960
|
// Send acknowledgment back
|
|
2560
|
-
|
|
2961
|
+
finish(success, httpCode, error);
|
|
2561
2962
|
#else
|
|
2562
2963
|
// Non-ESP platform - send error
|
|
2563
|
-
|
|
2964
|
+
finish(false, 0, "HTTP client not available on this platform");
|
|
2564
2965
|
#endif
|
|
2565
2966
|
|
|
2566
2967
|
return true; // Consume package - we have processed it and sent
|
|
@@ -2570,13 +2971,61 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2570
2971
|
|
|
2571
2972
|
void eventHandleInit() {
|
|
2572
2973
|
using namespace logger;
|
|
2974
|
+
// Where the station scan learns the channel the mesh is rooted on.
|
|
2975
|
+
// It used to look at its own scan's end, which a node connected to a
|
|
2976
|
+
// rooted mesh reaches once a minute: the node the failover test sends
|
|
2977
|
+
// from joined the bridge, the bridge left 24 s later, and no scan had
|
|
2978
|
+
// run in between — so it had no home to keep and followed a partition
|
|
2979
|
+
// off the router's channel. Topology changes are the moment to look.
|
|
2980
|
+
// Only "ever rooted" comes from the tree: the tree can carry a root that
|
|
2981
|
+
// is gone. On the rig a node's cached tree still showed the bridge as
|
|
2982
|
+
// its root for the seconds after the bridge had moved to its router's
|
|
2983
|
+
// channel, a topology change fired in that window, and the node
|
|
2984
|
+
// recorded the channel it was left on as home — then sat there alone,
|
|
2985
|
+
// seeing the mesh's four nodes on the router's channel and "staying".
|
|
2986
|
+
// Home is learnt from the bridge itself, below: its status message
|
|
2987
|
+
// carries the channel it is on, and a message just received is live.
|
|
2988
|
+
auto noteRoot = [this](uint32_t) {
|
|
2989
|
+
if (layout::isRooted(this->asNodeTree())) this->stationScan.noteEverRooted();
|
|
2990
|
+
};
|
|
2991
|
+
this->newConnectionCallbacks.push_back(noteRoot);
|
|
2992
|
+
this->changedConnectionCallbacks.push_back(noteRoot);
|
|
2993
|
+
this->callbackList.onPackage(
|
|
2994
|
+
protocol::BRIDGE_STATUS,
|
|
2995
|
+
[this](protocol::Variant& variant, std::shared_ptr<Connection>, uint32_t) {
|
|
2996
|
+
JsonDocument doc;
|
|
2997
|
+
TSTRING str;
|
|
2998
|
+
variant.printTo(str);
|
|
2999
|
+
if (deserializeJson(doc, str)) return false;
|
|
3000
|
+
JsonObject obj = doc.as<JsonObject>();
|
|
3001
|
+
if (obj["leaving"] | false) {
|
|
3002
|
+
// The bridge is stepping down; the generic handler forgets it.
|
|
3003
|
+
// Look for another now, not at the monitor's next tick.
|
|
3004
|
+
this->checkForBridgeSoon();
|
|
3005
|
+
return false;
|
|
3006
|
+
}
|
|
3007
|
+
uint8_t routerChannel = obj["routerChannel"] | 0;
|
|
3008
|
+
if (gateway::isValidMeshChannel(routerChannel)) {
|
|
3009
|
+
this->stationScan.noteRooted(routerChannel);
|
|
3010
|
+
}
|
|
3011
|
+
return false; // the generic handler records the bridge
|
|
3012
|
+
});
|
|
2573
3013
|
#ifdef ESP32
|
|
2574
3014
|
eventScanDoneHandler = WiFi.onEvent(
|
|
2575
3015
|
[this](WiFiEvent_t event, WiFiEventInfo_t info) {
|
|
2576
3016
|
if (this->semaphoreTake()) {
|
|
2577
3017
|
Log(CONNECTION,
|
|
2578
3018
|
"eventScanDoneHandler: ARDUINO_EVENT_WIFI_SCAN_DONE\n");
|
|
2579
|
-
|
|
3019
|
+
// Not scanComplete() here. This callback runs on the core's
|
|
3020
|
+
// network-event task, under the lock that task dispatches
|
|
3021
|
+
// with, and scanComplete() can end in followBridgeChannel(),
|
|
3022
|
+
// which restarts the AP. On core 3.x that waits for events
|
|
3023
|
+
// only this task can deliver: on the rig every esp32-c5 and
|
|
3024
|
+
// esp32-c6 that followed the bridge's channel went silent for
|
|
3025
|
+
// the rest of the run, and stop() then blocked in
|
|
3026
|
+
// removeEvent() on the same lock. The result is consumed by
|
|
3027
|
+
// the station task instead, from update().
|
|
3028
|
+
this->stationScan.scanDone();
|
|
2580
3029
|
this->semaphoreGive();
|
|
2581
3030
|
}
|
|
2582
3031
|
},
|
|
@@ -2606,7 +3055,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2606
3055
|
Log(CONNECTION,
|
|
2607
3056
|
"eventSTADisconnectedHandler: "
|
|
2608
3057
|
"ARDUINO_EVENT_WIFI_STA_DISCONNECTED\n");
|
|
3058
|
+
this->stationScan.stationAttemptOver();
|
|
2609
3059
|
this->droppedConnectionCallbacks.execute(0, true);
|
|
3060
|
+
this->stationScan.stationDown();
|
|
2610
3061
|
// Handle station disconnect completion after callbacks
|
|
2611
3062
|
this->handleStationDisconnectComplete();
|
|
2612
3063
|
this->semaphoreGive();
|
|
@@ -2623,6 +3074,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2623
3074
|
if (this->semaphoreTake()) {
|
|
2624
3075
|
Log(CONNECTION,
|
|
2625
3076
|
"eventSTAGotIPHandler: ARDUINO_EVENT_WIFI_STA_GOT_IP\n");
|
|
3077
|
+
this->stationScan.stationAttemptOver();
|
|
3078
|
+
this->stationScan.stationUp();
|
|
2626
3079
|
this->tcpConnect(); // Connect to TCP port
|
|
2627
3080
|
this->semaphoreGive();
|
|
2628
3081
|
}
|
|
@@ -2644,7 +3097,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2644
3097
|
eventSTADisconnectedHandler = WiFi.onStationModeDisconnected(
|
|
2645
3098
|
[&](const WiFiEventStationModeDisconnected& event) {
|
|
2646
3099
|
Log(CONNECTION, "Event: Station Mode Disconnected\n");
|
|
3100
|
+
this->stationScan.stationAttemptOver();
|
|
2647
3101
|
this->droppedConnectionCallbacks.execute(0, true);
|
|
3102
|
+
this->stationScan.stationDown();
|
|
2648
3103
|
// Handle station disconnect completion after callbacks
|
|
2649
3104
|
this->handleStationDisconnectComplete();
|
|
2650
3105
|
});
|
|
@@ -2655,6 +3110,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2655
3110
|
"Event: Station Mode Got IP (IP: %s Mask: %s Gateway: %s)\n",
|
|
2656
3111
|
event.ip.toString().c_str(), event.mask.toString().c_str(),
|
|
2657
3112
|
event.gw.toString().c_str());
|
|
3113
|
+
this->stationScan.stationAttemptOver();
|
|
3114
|
+
this->stationScan.stationUp();
|
|
2658
3115
|
this->tcpConnect(); // Connect to TCP port
|
|
2659
3116
|
});
|
|
2660
3117
|
#endif // ESP32
|
|
@@ -2673,6 +3130,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2673
3130
|
#endif // ESP8266
|
|
2674
3131
|
AsyncServer* _tcpListener;
|
|
2675
3132
|
std::shared_ptr<Task> bridgeStatusTask;
|
|
3133
|
+
// millis() of the last status broadcast, periodic or brought forward by a
|
|
3134
|
+
// topology change; the latter is held to one every five seconds.
|
|
3135
|
+
uint32_t _lastBridgeStatusBroadcast = 0;
|
|
2676
3136
|
|
|
2677
3137
|
// Station disconnect handling state
|
|
2678
3138
|
bool _pendingStationReconnect = false;
|
|
@@ -2683,6 +3143,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2683
3143
|
struct BridgeCandidate {
|
|
2684
3144
|
uint32_t nodeId;
|
|
2685
3145
|
int8_t routerRSSI;
|
|
3146
|
+
uint8_t routerChannel;
|
|
2686
3147
|
uint32_t uptime;
|
|
2687
3148
|
uint32_t freeMemory;
|
|
2688
3149
|
};
|
|
@@ -2701,6 +3162,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2701
3162
|
uint32_t electionRandomDelayMaxMs =
|
|
2702
3163
|
3000; // Default max 3 seconds random delay
|
|
2703
3164
|
uint32_t lastRoleChangeTime = 0;
|
|
3165
|
+
// A checkForBridge() is already scheduled (a bridge stepping down, or
|
|
3166
|
+
// the role-change hold); one at a time is enough.
|
|
3167
|
+
bool bridgeCheckPending = false;
|
|
2704
3168
|
ElectionState electionState = ELECTION_IDLE;
|
|
2705
3169
|
uint32_t electionDeadline = 0;
|
|
2706
3170
|
std::vector<BridgeCandidate> electionCandidates;
|
|
@@ -2763,8 +3227,6 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2763
3227
|
static const uint8_t MIN_WIFI_CHANNEL = 1;
|
|
2764
3228
|
static const uint8_t MAX_WIFI_CHANNEL =
|
|
2765
3229
|
14; // Support channels 1-14 for regions that allow it
|
|
2766
|
-
static const uint32_t GATEWAY_HTTP_TIMEOUT_MS =
|
|
2767
|
-
30000; // 30 second timeout for gateway HTTP requests
|
|
2768
3230
|
|
|
2769
3231
|
/**
|
|
2770
3232
|
* Initialize shared gateway monitoring
|