@alteriom/painlessmesh 1.10.0 → 2.0.1
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 +542 -0
- package/CONTRIBUTING.md +56 -53
- package/README.md +102 -95
- package/RELEASE_GUIDE.md +94 -788
- package/examples/alteriom/README.md +8 -10
- package/examples/alteriom/alteriom.ino +2 -2
- 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} +4 -0
- package/examples/basic/test/simulator/README.md +3 -3
- package/examples/bridge_failover/README.md +51 -14
- 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/sendToInternet/platformio.ini +2 -2
- package/examples/sharedGateway/README.md +1 -2
- package/keywords.txt +50 -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 +556 -126
- package/src/painlessMesh.h +2 -2
- package/src/painlessMeshSTA.cpp +607 -87
- package/src/painlessMeshSTA.h +135 -3
- package/src/painlessmesh/ack.hpp +283 -0
- package/src/painlessmesh/buffer.hpp +70 -8
- package/src/painlessmesh/callback.hpp +38 -5
- package/src/painlessmesh/configuration.hpp +69 -1
- package/src/painlessmesh/connection.hpp +12 -5
- 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 +552 -48
- package/src/painlessmesh/ntp.hpp +2 -4
- package/src/painlessmesh/plugin.hpp +30 -6
- package/src/painlessmesh/protocol.hpp +55 -2
- package/src/painlessmesh/router.hpp +192 -77
- package/src/painlessmesh/tcp.hpp +10 -0
- 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
|
|
|
@@ -1304,7 +1394,87 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1304
1394
|
*/
|
|
1305
1395
|
bool isMultiBridgeEnabled() const { return multiBridgeEnabled; }
|
|
1306
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
|
+
|
|
1307
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
|
+
}
|
|
1308
1478
|
// remove all WiFi events
|
|
1309
1479
|
#ifdef ESP32
|
|
1310
1480
|
WiFi.removeEvent(eventScanDoneHandler);
|
|
@@ -1327,8 +1497,12 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1327
1497
|
// Shutdown wifi hardware
|
|
1328
1498
|
if (WiFi.status() != WL_DISCONNECTED) WiFi.disconnect();
|
|
1329
1499
|
|
|
1330
|
-
//
|
|
1331
|
-
|
|
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.
|
|
1332
1506
|
}
|
|
1333
1507
|
|
|
1334
1508
|
protected:
|
|
@@ -1437,6 +1611,24 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1437
1611
|
"Node %u connection changed, sending bridge status directly\n",
|
|
1438
1612
|
nodeId);
|
|
1439
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
|
+
|
|
1440
1632
|
// Small delay to ensure connection is fully stable, then send directly to
|
|
1441
1633
|
// the new node This avoids issues with time sync blocking broadcast
|
|
1442
1634
|
// messages
|
|
@@ -1656,20 +1848,60 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1656
1848
|
* @param routerSSID SSID of router to scan for
|
|
1657
1849
|
* @return RSSI in dBm (negative number, -127 to 0), or 0 if not found
|
|
1658
1850
|
*/
|
|
1659
|
-
int8_t scanRouterSignalStrength(TSTRING routerSSID
|
|
1851
|
+
int8_t scanRouterSignalStrength(TSTRING routerSSID,
|
|
1852
|
+
uint8_t* routerChannel = nullptr) {
|
|
1660
1853
|
using namespace logger;
|
|
1661
1854
|
Log(CONNECTION, "scanRouterSignalStrength(): Scanning for %s...\n",
|
|
1662
1855
|
routerSSID.c_str());
|
|
1663
1856
|
|
|
1664
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
|
+
}
|
|
1665
1894
|
Log(CONNECTION, "scanRouterSignalStrength(): Found %d networks\n", n);
|
|
1666
1895
|
|
|
1667
1896
|
for (int i = 0; i < n; i++) {
|
|
1668
1897
|
if (WiFi.SSID(i) == routerSSID) {
|
|
1669
1898
|
int8_t rssi = WiFi.RSSI(i);
|
|
1899
|
+
uint8_t channel = WiFi.channel(i);
|
|
1900
|
+
if (routerChannel != nullptr) *routerChannel = channel;
|
|
1670
1901
|
Log(CONNECTION,
|
|
1671
|
-
"scanRouterSignalStrength(): Found %s with RSSI %d dBm
|
|
1672
|
-
|
|
1902
|
+
"scanRouterSignalStrength(): Found %s with RSSI %d dBm on "
|
|
1903
|
+
"channel %u\n",
|
|
1904
|
+
routerSSID.c_str(), rssi, channel);
|
|
1673
1905
|
return rssi;
|
|
1674
1906
|
}
|
|
1675
1907
|
}
|
|
@@ -1702,10 +1934,23 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1702
1934
|
return;
|
|
1703
1935
|
}
|
|
1704
1936
|
|
|
1705
|
-
// 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.
|
|
1706
1941
|
if (millis() - lastRoleChangeTime < 60000) {
|
|
1942
|
+
uint32_t wait = 60000 - (millis() - lastRoleChangeTime);
|
|
1707
1943
|
Log(CONNECTION,
|
|
1708
|
-
"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
|
+
}
|
|
1709
1954
|
return;
|
|
1710
1955
|
}
|
|
1711
1956
|
|
|
@@ -1724,7 +1969,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1724
1969
|
|
|
1725
1970
|
// Schedule a retry after channel re-sync has had a chance to run
|
|
1726
1971
|
// The channel re-sync threshold is StationScan::EMPTY_SCAN_THRESHOLD
|
|
1727
|
-
// scans (
|
|
1972
|
+
// scans (2, ~30 s). Fast scan interval is 0.5 * SCAN_INTERVAL = 15
|
|
1728
1973
|
// seconds Wait for re-sync to complete plus a buffer
|
|
1729
1974
|
uint32_t retryDelay =
|
|
1730
1975
|
(StationScan::EMPTY_SCAN_THRESHOLD - emptyScans + 2) * 15000;
|
|
@@ -1739,7 +1984,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1739
1984
|
electionState = ELECTION_SCANNING;
|
|
1740
1985
|
|
|
1741
1986
|
// Scan for router to get RSSI
|
|
1742
|
-
|
|
1987
|
+
uint8_t routerChannel = 0;
|
|
1988
|
+
int8_t routerRSSI =
|
|
1989
|
+
scanRouterSignalStrength(routerSSID, &routerChannel);
|
|
1743
1990
|
|
|
1744
1991
|
if (routerRSSI == 0) {
|
|
1745
1992
|
Log(CONNECTION,
|
|
@@ -1758,6 +2005,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1758
2005
|
BridgeCandidate selfCandidate;
|
|
1759
2006
|
selfCandidate.nodeId = this->nodeId;
|
|
1760
2007
|
selfCandidate.routerRSSI = routerRSSI;
|
|
2008
|
+
selfCandidate.routerChannel = routerChannel;
|
|
1761
2009
|
selfCandidate.uptime = millis();
|
|
1762
2010
|
selfCandidate.freeMemory = ESP.getFreeHeap();
|
|
1763
2011
|
electionCandidates.push_back(selfCandidate);
|
|
@@ -1770,6 +2018,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1770
2018
|
obj["from"] = this->nodeId;
|
|
1771
2019
|
obj["routing"] = 2; // BROADCAST
|
|
1772
2020
|
obj["routerRSSI"] = routerRSSI;
|
|
2021
|
+
obj["routerChannel"] = routerChannel;
|
|
1773
2022
|
obj["uptime"] = millis();
|
|
1774
2023
|
obj["freeMemory"] = ESP.getFreeHeap();
|
|
1775
2024
|
obj["timestamp"] = this->getNodeTime();
|
|
@@ -1782,7 +2031,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1782
2031
|
// Send election message using raw broadcast to preserve type
|
|
1783
2032
|
// BRIDGE_ELECTION
|
|
1784
2033
|
protocol::Variant variant(msg);
|
|
1785
|
-
router::broadcast<
|
|
2034
|
+
router::broadcast<Connection>(variant, (*this), 0);
|
|
1786
2035
|
|
|
1787
2036
|
Log(CONNECTION, "startBridgeElection(): Candidacy broadcast sent\n");
|
|
1788
2037
|
|
|
@@ -1902,7 +2151,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1902
2151
|
|
|
1903
2152
|
if (winner->nodeId == this->nodeId) {
|
|
1904
2153
|
Log(CONNECTION, "[TARGET] I WON! Promoting to bridge...\n");
|
|
1905
|
-
promoteToBridge();
|
|
2154
|
+
promoteToBridge(winner->routerChannel);
|
|
1906
2155
|
} else {
|
|
1907
2156
|
Log(CONNECTION, "Winner is node %u, remaining as regular node\n",
|
|
1908
2157
|
winner->nodeId);
|
|
@@ -1924,7 +2173,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1924
2173
|
* Instead, we schedule the actual promotion work to run after the current
|
|
1925
2174
|
* task completes, allowing safe cleanup of task structures.
|
|
1926
2175
|
*/
|
|
1927
|
-
void promoteToBridge() {
|
|
2176
|
+
void promoteToBridge(uint8_t routerChannel) {
|
|
1928
2177
|
using namespace logger;
|
|
1929
2178
|
|
|
1930
2179
|
Log(STARTUP, "=== Becoming Bridge Node ===\n");
|
|
@@ -1950,6 +2199,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1950
2199
|
obj["previousBridge"] = previousBridgeId;
|
|
1951
2200
|
obj["reason"] = "Election winner - best router signal";
|
|
1952
2201
|
obj["routerRSSI"] = 0; // Not yet connected to router
|
|
2202
|
+
obj["routerChannel"] = routerChannel;
|
|
1953
2203
|
obj["timestamp"] = this->getNodeTime();
|
|
1954
2204
|
obj["message_type"] = protocol::BRIDGE_TAKEOVER;
|
|
1955
2205
|
|
|
@@ -1959,7 +2209,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
1959
2209
|
// Send takeover message using raw broadcast to preserve type
|
|
1960
2210
|
// BRIDGE_TAKEOVER
|
|
1961
2211
|
protocol::Variant variant(msg);
|
|
1962
|
-
router::broadcast<
|
|
2212
|
+
router::broadcast<Connection>(variant, (*this), 0);
|
|
1963
2213
|
|
|
1964
2214
|
// Give time for announcement to propagate before channel switch
|
|
1965
2215
|
// Allow event loop processing during hardware settling
|
|
@@ -2152,7 +2402,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2152
2402
|
* Called by package handler when election message arrives
|
|
2153
2403
|
*/
|
|
2154
2404
|
void handleBridgeElection(uint32_t fromNode, int8_t routerRSSI,
|
|
2155
|
-
|
|
2405
|
+
uint8_t routerChannel, uint32_t uptime,
|
|
2406
|
+
uint32_t freeMemory) {
|
|
2156
2407
|
using namespace logger;
|
|
2157
2408
|
|
|
2158
2409
|
if (electionState != ELECTION_COLLECTING) {
|
|
@@ -2174,6 +2425,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2174
2425
|
BridgeCandidate candidate;
|
|
2175
2426
|
candidate.nodeId = fromNode;
|
|
2176
2427
|
candidate.routerRSSI = routerRSSI;
|
|
2428
|
+
candidate.routerChannel = routerChannel;
|
|
2177
2429
|
candidate.uptime = uptime;
|
|
2178
2430
|
candidate.freeMemory = freeMemory;
|
|
2179
2431
|
|
|
@@ -2188,13 +2440,29 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2188
2440
|
* Send bridge status broadcast
|
|
2189
2441
|
* Called periodically by bridge nodes to report connectivity status
|
|
2190
2442
|
*/
|
|
2191
|
-
void sendBridgeStatus() {
|
|
2443
|
+
void sendBridgeStatus(bool leaving = false) {
|
|
2192
2444
|
using namespace logger;
|
|
2193
2445
|
|
|
2194
2446
|
if (!this->bridgeStatusBroadcastEnabled) {
|
|
2195
2447
|
return;
|
|
2196
2448
|
}
|
|
2197
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
|
+
|
|
2198
2466
|
// Create bridge status package
|
|
2199
2467
|
// We need to include the package header here since we're in wifi namespace
|
|
2200
2468
|
// The package will be sent as a JSON string
|
|
@@ -2212,9 +2480,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2212
2480
|
// 2. Some networks (mobile hotspots) may not provide gateway IP via DHCP
|
|
2213
2481
|
// 3. Having a valid local IP + being connected is sufficient for internet
|
|
2214
2482
|
// access
|
|
2215
|
-
bool hasInternet = (WiFi.status() == WL_CONNECTED) &&
|
|
2483
|
+
bool hasInternet = !leaving && (WiFi.status() == WL_CONNECTED) &&
|
|
2216
2484
|
(WiFi.localIP() != IPAddress(0, 0, 0, 0));
|
|
2217
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;
|
|
2218
2490
|
|
|
2219
2491
|
int8_t rssi = WiFi.RSSI();
|
|
2220
2492
|
uint8_t channel = WiFi.channel();
|
|
@@ -2230,8 +2502,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2230
2502
|
String msg;
|
|
2231
2503
|
serializeJson(doc, msg);
|
|
2232
2504
|
|
|
2233
|
-
Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)\n",
|
|
2234
|
-
hasInternet ? "Connected" : "Disconnected"
|
|
2505
|
+
Log(GENERAL, "sendBridgeStatus(): Broadcasting status (Internet: %s)%s\n",
|
|
2506
|
+
hasInternet ? "Connected" : "Disconnected",
|
|
2507
|
+
leaving ? " — stepping down" : "");
|
|
2235
2508
|
Log(GENERAL,
|
|
2236
2509
|
"sendBridgeStatus(): WiFi status=%d, localIP=%s, gatewayIP=%s\n",
|
|
2237
2510
|
WiFi.status(), WiFi.localIP().toString().c_str(),
|
|
@@ -2246,7 +2519,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2246
2519
|
// Using sendBroadcast(msg) would wrap it in type 8 (BROADCAST) and hide
|
|
2247
2520
|
// type BRIDGE_STATUS
|
|
2248
2521
|
protocol::Variant variant(msg);
|
|
2249
|
-
router::broadcast<
|
|
2522
|
+
router::broadcast<Connection>(variant, (*this), 0);
|
|
2523
|
+
_lastBridgeStatusBroadcast = millis();
|
|
2250
2524
|
}
|
|
2251
2525
|
|
|
2252
2526
|
/**
|
|
@@ -2265,7 +2539,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2265
2539
|
static uint32_t lastCheckTime = 0;
|
|
2266
2540
|
static bool lastResult = false;
|
|
2267
2541
|
uint32_t now = millis();
|
|
2268
|
-
if (lastCheckTime > 0 && (now - lastCheckTime) <
|
|
2542
|
+
if (lastCheckTime > 0 && (now - lastCheckTime) < GATEWAY_CONNECTIVITY_CACHE_MS) {
|
|
2269
2543
|
return lastResult;
|
|
2270
2544
|
}
|
|
2271
2545
|
|
|
@@ -2287,9 +2561,13 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2287
2561
|
// Using Google's servers as they have high availability globally
|
|
2288
2562
|
IPAddress result;
|
|
2289
2563
|
|
|
2290
|
-
#if defined(
|
|
2291
|
-
//
|
|
2292
|
-
|
|
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);
|
|
2293
2571
|
|
|
2294
2572
|
// Check if DNS resolution succeeded
|
|
2295
2573
|
if (dnsResult != 1) {
|
|
@@ -2308,6 +2586,20 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2308
2586
|
lastResult = false;
|
|
2309
2587
|
return false;
|
|
2310
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;
|
|
2311
2603
|
#else
|
|
2312
2604
|
// Other platforms: assume internet is available if WiFi connected
|
|
2313
2605
|
// (no reliable way to test without platform-specific APIs)
|
|
@@ -2341,8 +2633,24 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2341
2633
|
using namespace logger;
|
|
2342
2634
|
|
|
2343
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
|
+
|
|
2344
2652
|
HTTPClient http;
|
|
2345
|
-
http.setTimeout(
|
|
2653
|
+
http.setTimeout(GATEWAY_CAPTIVE_PORTAL_TIMEOUT_MS);
|
|
2346
2654
|
|
|
2347
2655
|
WiFiClient client;
|
|
2348
2656
|
|
|
@@ -2387,6 +2695,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2387
2695
|
}
|
|
2388
2696
|
|
|
2389
2697
|
Log(COMMUNICATION, "detectCaptivePortal(): No captive portal detected\n");
|
|
2698
|
+
lastResult = true;
|
|
2390
2699
|
return true;
|
|
2391
2700
|
|
|
2392
2701
|
#else
|
|
@@ -2398,8 +2707,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2398
2707
|
/**
|
|
2399
2708
|
* Helper method to send gateway acknowledgment
|
|
2400
2709
|
*/
|
|
2401
|
-
void sendGatewayAck(
|
|
2402
|
-
|
|
2710
|
+
void sendGatewayAck(
|
|
2711
|
+
const gateway::GatewayDataPackage& request, bool success,
|
|
2712
|
+
uint16_t httpStatus, const TSTRING& error,
|
|
2713
|
+
std::shared_ptr<Connection> ingressConnection = nullptr) {
|
|
2403
2714
|
using namespace logger;
|
|
2404
2715
|
|
|
2405
2716
|
gateway::GatewayAckPackage ack;
|
|
@@ -2412,7 +2723,28 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2412
2723
|
ack.error = error;
|
|
2413
2724
|
ack.timestamp = this->getNodeTime();
|
|
2414
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
|
+
|
|
2415
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
|
+
}
|
|
2416
2748
|
if (conn) {
|
|
2417
2749
|
protocol::Variant variant(&ack);
|
|
2418
2750
|
router::send(std::move(variant), conn);
|
|
@@ -2433,15 +2765,26 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2433
2765
|
* makes the request, and sends back a GATEWAY_ACK with the result.
|
|
2434
2766
|
*
|
|
2435
2767
|
* Security notes:
|
|
2436
|
-
* -
|
|
2437
|
-
*
|
|
2438
|
-
*
|
|
2439
|
-
*
|
|
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.
|
|
2440
2781
|
*
|
|
2441
2782
|
* Limitations:
|
|
2442
2783
|
* - HTTP redirects (3xx) are not automatically followed
|
|
2443
2784
|
* - Only 2xx status codes are treated as success
|
|
2444
|
-
* - 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)
|
|
2445
2788
|
*/
|
|
2446
2789
|
void initGatewayInternetHandler() {
|
|
2447
2790
|
using namespace logger;
|
|
@@ -2450,44 +2793,68 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2450
2793
|
|
|
2451
2794
|
this->callbackList.onPackage(
|
|
2452
2795
|
protocol::GATEWAY_DATA, [this](protocol::Variant& variant,
|
|
2453
|
-
std::shared_ptr<Connection>
|
|
2796
|
+
std::shared_ptr<Connection> ingress,
|
|
2797
|
+
uint32_t) {
|
|
2454
2798
|
auto pkg = variant.to<gateway::GatewayDataPackage>();
|
|
2455
2799
|
|
|
2456
2800
|
Log(COMMUNICATION,
|
|
2457
2801
|
"Gateway received Internet request: msgId=%u dest=%s\n",
|
|
2458
2802
|
pkg.messageId, pkg.destination.c_str());
|
|
2459
2803
|
|
|
2460
|
-
//
|
|
2461
|
-
//
|
|
2462
|
-
//
|
|
2463
|
-
//
|
|
2464
|
-
//
|
|
2465
|
-
//
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
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
|
+
};
|
|
2472
2839
|
|
|
2473
2840
|
// Check Internet connectivity
|
|
2474
2841
|
// First check WiFi status for quick fail
|
|
2475
2842
|
if (WiFi.status() != WL_CONNECTED) {
|
|
2476
|
-
|
|
2843
|
+
finish(false, 0, "Gateway WiFi not connected");
|
|
2477
2844
|
return true; // Consume package - we handled it (with error)
|
|
2478
2845
|
}
|
|
2479
2846
|
|
|
2480
2847
|
// Then check actual internet access (DNS resolution)
|
|
2481
2848
|
// This detects when WiFi is connected but router has no internet
|
|
2482
2849
|
if (!hasActualInternetAccess()) {
|
|
2483
|
-
|
|
2850
|
+
finish(false, 0, "Router has no internet access - check WAN connection");
|
|
2484
2851
|
return true; // Consume package - we handled it (with error)
|
|
2485
2852
|
}
|
|
2486
2853
|
|
|
2487
2854
|
// Finally, check for captive portal interference
|
|
2488
2855
|
// This detects when DNS works but HTTP requests are intercepted
|
|
2489
2856
|
if (!detectCaptivePortal()) {
|
|
2490
|
-
|
|
2857
|
+
finish(false, 0, "Captive portal detected - requires web authentication. Check router/WiFi settings");
|
|
2491
2858
|
return true; // Consume package - we handled it (with error)
|
|
2492
2859
|
}
|
|
2493
2860
|
|
|
@@ -2509,7 +2876,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2509
2876
|
|
|
2510
2877
|
if (pkg.destination.startsWith("https://")) {
|
|
2511
2878
|
#ifdef ESP32
|
|
2512
|
-
// 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.
|
|
2513
2882
|
http.begin(pkg.destination.c_str());
|
|
2514
2883
|
#elif defined(ESP8266)
|
|
2515
2884
|
// ESP8266: Use insecure mode to reduce memory overhead
|
|
@@ -2589,10 +2958,10 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2589
2958
|
http.end();
|
|
2590
2959
|
|
|
2591
2960
|
// Send acknowledgment back
|
|
2592
|
-
|
|
2961
|
+
finish(success, httpCode, error);
|
|
2593
2962
|
#else
|
|
2594
2963
|
// Non-ESP platform - send error
|
|
2595
|
-
|
|
2964
|
+
finish(false, 0, "HTTP client not available on this platform");
|
|
2596
2965
|
#endif
|
|
2597
2966
|
|
|
2598
2967
|
return true; // Consume package - we have processed it and sent
|
|
@@ -2602,13 +2971,61 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2602
2971
|
|
|
2603
2972
|
void eventHandleInit() {
|
|
2604
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
|
+
});
|
|
2605
3013
|
#ifdef ESP32
|
|
2606
3014
|
eventScanDoneHandler = WiFi.onEvent(
|
|
2607
3015
|
[this](WiFiEvent_t event, WiFiEventInfo_t info) {
|
|
2608
3016
|
if (this->semaphoreTake()) {
|
|
2609
3017
|
Log(CONNECTION,
|
|
2610
3018
|
"eventScanDoneHandler: ARDUINO_EVENT_WIFI_SCAN_DONE\n");
|
|
2611
|
-
|
|
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();
|
|
2612
3029
|
this->semaphoreGive();
|
|
2613
3030
|
}
|
|
2614
3031
|
},
|
|
@@ -2638,7 +3055,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2638
3055
|
Log(CONNECTION,
|
|
2639
3056
|
"eventSTADisconnectedHandler: "
|
|
2640
3057
|
"ARDUINO_EVENT_WIFI_STA_DISCONNECTED\n");
|
|
3058
|
+
this->stationScan.stationAttemptOver();
|
|
2641
3059
|
this->droppedConnectionCallbacks.execute(0, true);
|
|
3060
|
+
this->stationScan.stationDown();
|
|
2642
3061
|
// Handle station disconnect completion after callbacks
|
|
2643
3062
|
this->handleStationDisconnectComplete();
|
|
2644
3063
|
this->semaphoreGive();
|
|
@@ -2655,6 +3074,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2655
3074
|
if (this->semaphoreTake()) {
|
|
2656
3075
|
Log(CONNECTION,
|
|
2657
3076
|
"eventSTAGotIPHandler: ARDUINO_EVENT_WIFI_STA_GOT_IP\n");
|
|
3077
|
+
this->stationScan.stationAttemptOver();
|
|
3078
|
+
this->stationScan.stationUp();
|
|
2658
3079
|
this->tcpConnect(); // Connect to TCP port
|
|
2659
3080
|
this->semaphoreGive();
|
|
2660
3081
|
}
|
|
@@ -2676,7 +3097,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2676
3097
|
eventSTADisconnectedHandler = WiFi.onStationModeDisconnected(
|
|
2677
3098
|
[&](const WiFiEventStationModeDisconnected& event) {
|
|
2678
3099
|
Log(CONNECTION, "Event: Station Mode Disconnected\n");
|
|
3100
|
+
this->stationScan.stationAttemptOver();
|
|
2679
3101
|
this->droppedConnectionCallbacks.execute(0, true);
|
|
3102
|
+
this->stationScan.stationDown();
|
|
2680
3103
|
// Handle station disconnect completion after callbacks
|
|
2681
3104
|
this->handleStationDisconnectComplete();
|
|
2682
3105
|
});
|
|
@@ -2687,6 +3110,8 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2687
3110
|
"Event: Station Mode Got IP (IP: %s Mask: %s Gateway: %s)\n",
|
|
2688
3111
|
event.ip.toString().c_str(), event.mask.toString().c_str(),
|
|
2689
3112
|
event.gw.toString().c_str());
|
|
3113
|
+
this->stationScan.stationAttemptOver();
|
|
3114
|
+
this->stationScan.stationUp();
|
|
2690
3115
|
this->tcpConnect(); // Connect to TCP port
|
|
2691
3116
|
});
|
|
2692
3117
|
#endif // ESP32
|
|
@@ -2705,6 +3130,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2705
3130
|
#endif // ESP8266
|
|
2706
3131
|
AsyncServer* _tcpListener;
|
|
2707
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;
|
|
2708
3136
|
|
|
2709
3137
|
// Station disconnect handling state
|
|
2710
3138
|
bool _pendingStationReconnect = false;
|
|
@@ -2715,6 +3143,7 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2715
3143
|
struct BridgeCandidate {
|
|
2716
3144
|
uint32_t nodeId;
|
|
2717
3145
|
int8_t routerRSSI;
|
|
3146
|
+
uint8_t routerChannel;
|
|
2718
3147
|
uint32_t uptime;
|
|
2719
3148
|
uint32_t freeMemory;
|
|
2720
3149
|
};
|
|
@@ -2733,6 +3162,9 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2733
3162
|
uint32_t electionRandomDelayMaxMs =
|
|
2734
3163
|
3000; // Default max 3 seconds random delay
|
|
2735
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;
|
|
2736
3168
|
ElectionState electionState = ELECTION_IDLE;
|
|
2737
3169
|
uint32_t electionDeadline = 0;
|
|
2738
3170
|
std::vector<BridgeCandidate> electionCandidates;
|
|
@@ -2795,8 +3227,6 @@ class Mesh : public painlessmesh::Mesh<Connection> {
|
|
|
2795
3227
|
static const uint8_t MIN_WIFI_CHANNEL = 1;
|
|
2796
3228
|
static const uint8_t MAX_WIFI_CHANNEL =
|
|
2797
3229
|
14; // Support channels 1-14 for regions that allow it
|
|
2798
|
-
static const uint32_t GATEWAY_HTTP_TIMEOUT_MS =
|
|
2799
|
-
30000; // 30 second timeout for gateway HTTP requests
|
|
2800
3230
|
|
|
2801
3231
|
/**
|
|
2802
3232
|
* Initialize shared gateway monitoring
|