@alteriom/painlessmesh 1.10.0 → 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 +483 -0
- package/CONTRIBUTING.md +56 -53
- package/README.md +100 -95
- package/RELEASE_GUIDE.md +81 -780
- 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/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 +3 -3
- 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/painlessMeshSTA.cpp
CHANGED
|
@@ -33,6 +33,35 @@ void ICACHE_FLASH_ATTR StationScan::init(painlessmesh::wifi::Mesh *pMesh,
|
|
|
33
33
|
channel = pchannel;
|
|
34
34
|
hidden = phidden;
|
|
35
35
|
|
|
36
|
+
// A node re-initialised in place — promoted to bridge, or back to a
|
|
37
|
+
// regular node — keeps this object, and everything it had learned in
|
|
38
|
+
// its previous life came with it: a newly promoted bridge ran an
|
|
39
|
+
// all-channel re-detection because the request was still set from
|
|
40
|
+
// when it was a rootless regular node, and its empty-scan count,
|
|
41
|
+
// back-offs and "has ever seen a root" carried over the same way. A
|
|
42
|
+
// new life starts with none of that. The manual flag is set by
|
|
43
|
+
// stationManual() after this call, for the link that needs it.
|
|
44
|
+
manual = false;
|
|
45
|
+
consecutiveEmptyScans = 0;
|
|
46
|
+
scanRequested = false;
|
|
47
|
+
redetectRequested = false;
|
|
48
|
+
scanAllChannels = false;
|
|
49
|
+
scanSlice = 0;
|
|
50
|
+
huntChannel = 0;
|
|
51
|
+
huntPending = false;
|
|
52
|
+
huntCounts.clear();
|
|
53
|
+
huntRssi.clear();
|
|
54
|
+
orphanScanBackoff = 0;
|
|
55
|
+
orphanRedetects = 0;
|
|
56
|
+
everRooted = false;
|
|
57
|
+
rootedChannel = 0;
|
|
58
|
+
homeStays = 0;
|
|
59
|
+
pendingElsewhere = 0;
|
|
60
|
+
partitionScans = 0;
|
|
61
|
+
halfOpenDropped = false;
|
|
62
|
+
connectAttemptStarted = 0;
|
|
63
|
+
aps.clear();
|
|
64
|
+
|
|
36
65
|
task.set(SCAN_INTERVAL, TASK_FOREVER, [this]() { stationScan(); });
|
|
37
66
|
}
|
|
38
67
|
|
|
@@ -42,23 +71,87 @@ void ICACHE_FLASH_ATTR StationScan::stationScan() {
|
|
|
42
71
|
Log(CONNECTION, "stationScan(): %s\n", ssid.c_str());
|
|
43
72
|
|
|
44
73
|
// If channel is 0, auto-detect the mesh channel first
|
|
45
|
-
if (channel == 0
|
|
74
|
+
if (channel == 0) {
|
|
46
75
|
Log(STARTUP, "stationScan(): Auto-detecting mesh channel...\n");
|
|
47
|
-
uint8_t detectedChannel = scanForMeshChannel(
|
|
76
|
+
uint8_t detectedChannel = scanForMeshChannel(
|
|
77
|
+
ssid, hidden, 0,
|
|
78
|
+
mesh->routerCredentialsConfigured ? mesh->routerSSID : TSTRING(""));
|
|
48
79
|
if (detectedChannel > 0) {
|
|
80
|
+
uint8_t oldChannel = mesh->_meshChannel;
|
|
49
81
|
mesh->_meshChannel = detectedChannel;
|
|
50
82
|
channel = detectedChannel;
|
|
51
83
|
Log(STARTUP, "stationScan(): Mesh channel auto-detected: %d\n", detectedChannel);
|
|
84
|
+
// init() has already created the AP. When channel 0 was requested and
|
|
85
|
+
// no peer was visible during that first instant, the ESP Wi-Fi stack
|
|
86
|
+
// created it on channel 1. Recreate it on the detected channel before
|
|
87
|
+
// connecting the station, otherwise a temporary station disconnect can
|
|
88
|
+
// snap the AP back to channel 1 and isolate a failover candidate.
|
|
89
|
+
if (oldChannel != detectedChannel && (WiFi.getMode() & WIFI_AP)) {
|
|
90
|
+
WiFi.softAPdisconnect(true);
|
|
91
|
+
delay(200);
|
|
92
|
+
mesh->apInit(mesh->getNodeId());
|
|
93
|
+
delay(100);
|
|
94
|
+
}
|
|
52
95
|
} else {
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
96
|
+
// Keep channel == 0 so the next station scan retries all-channel
|
|
97
|
+
// detection. Permanently replacing it with channel 1 after one miss
|
|
98
|
+
// made a node unable to follow a bridge that was still starting or had
|
|
99
|
+
// just moved the mesh to its router channel.
|
|
100
|
+
if (mesh->_meshChannel == 0) mesh->_meshChannel = 1;
|
|
101
|
+
Log(CONNECTION,
|
|
102
|
+
"stationScan(): Mesh not found, using channel 1 temporarily and "
|
|
103
|
+
"retrying auto-detection\n");
|
|
57
104
|
}
|
|
58
105
|
}
|
|
59
106
|
|
|
107
|
+
// Channel 0 scans them all. A re-detection dwells a shorter time per
|
|
108
|
+
// channel than the single-channel scan: thirteen channels at 300 ms is
|
|
109
|
+
// four seconds off the mesh channel, and the point of doing it
|
|
110
|
+
// asynchronously is lost if the radio is away that long.
|
|
111
|
+
uint8_t scanChannel = channel;
|
|
112
|
+
bool allChannels = false;
|
|
113
|
+
uint8_t slice = 0;
|
|
114
|
+
if (redetectRequested) {
|
|
115
|
+
redetectRequested = false;
|
|
116
|
+
size_t stationsUnderAp = 0;
|
|
117
|
+
for (auto&& sub : mesh->subs) {
|
|
118
|
+
if (sub->connected() && !sub->station) ++stationsUnderAp;
|
|
119
|
+
}
|
|
120
|
+
if (stationsUnderAp == 0) {
|
|
121
|
+
Log(CONNECTION,
|
|
122
|
+
"stationScan(): re-detecting the mesh channel, scanning all channels\n");
|
|
123
|
+
allChannels = true;
|
|
124
|
+
} else {
|
|
125
|
+
Log(CONNECTION,
|
|
126
|
+
"stationScan(): re-detecting the mesh channel a channel at a time: "
|
|
127
|
+
"%u station(s) under this AP would drop during an all-channel scan\n",
|
|
128
|
+
(unsigned)stationsUnderAp);
|
|
129
|
+
huntChannel = 1;
|
|
130
|
+
huntCounts.clear();
|
|
131
|
+
huntRssi.clear();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (huntChannel != 0) {
|
|
135
|
+
if (huntChannel == mesh->_meshChannel) ++huntChannel;
|
|
136
|
+
if (huntChannel > 13) {
|
|
137
|
+
// Every other channel has been looked at; this scan is the node's own
|
|
138
|
+
// channel, and scanComplete() decides with the tally.
|
|
139
|
+
huntChannel = 0;
|
|
140
|
+
huntPending = true;
|
|
141
|
+
} else {
|
|
142
|
+
slice = huntChannel;
|
|
143
|
+
scanChannel = slice;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (allChannels) scanChannel = 0;
|
|
60
147
|
#ifdef ESP32
|
|
61
|
-
|
|
148
|
+
// A slice dwells as long as a regular scan: 120 ms on one channel missed
|
|
149
|
+
// the bridge's AP on the rig, and the hunt concluded the mesh was on no
|
|
150
|
+
// other channel. The all-channel scan keeps the short dwell, since it
|
|
151
|
+
// runs only on a node with nothing under its AP.
|
|
152
|
+
int16_t started = WiFi.scanNetworks(true, hidden, false,
|
|
153
|
+
allChannels ? 120U : 300U,
|
|
154
|
+
scanChannel);
|
|
62
155
|
#elif defined(ESP8266)
|
|
63
156
|
// WiFi.scanNetworksAsync([&](int networks) { this->scanComplete(); }, true);
|
|
64
157
|
// Try 600 times (60 seconds). If not completed after that, give up
|
|
@@ -71,39 +164,160 @@ void ICACHE_FLASH_ATTR StationScan::stationScan() {
|
|
|
71
164
|
});
|
|
72
165
|
mesh->mScheduler->addTask(asyncTask);
|
|
73
166
|
asyncTask.enableDelayed();
|
|
74
|
-
WiFi.scanNetworks(true, hidden,
|
|
167
|
+
int16_t started = WiFi.scanNetworks(true, hidden, scanChannel);
|
|
75
168
|
#endif
|
|
76
169
|
|
|
170
|
+
if (started == WIFI_SCAN_FAILED) {
|
|
171
|
+
// The radio refused to start a scan — on ESP32 that is what it does
|
|
172
|
+
// while the station is mid-association. No scan-done event will ever
|
|
173
|
+
// come, so the ten-interval safety net below would be a five-minute
|
|
174
|
+
// silence; a node that has just lost its uplink cannot afford it.
|
|
175
|
+
Log(ERROR, "stationScan(): scan could not start, retrying in %d s\n",
|
|
176
|
+
(int)(0.5 * SCAN_INTERVAL / TASK_SECOND));
|
|
177
|
+
#ifdef ESP8266
|
|
178
|
+
asyncTask.disable();
|
|
179
|
+
#endif
|
|
180
|
+
task.delay(0.5 * SCAN_INTERVAL);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
scanRequested = true;
|
|
184
|
+
scanAllChannels = allChannels;
|
|
185
|
+
scanSlice = slice;
|
|
186
|
+
|
|
77
187
|
task.delay(10 * SCAN_INTERVAL); // Scan should be completed by then and next
|
|
78
188
|
// step called. If not then we restart here.
|
|
79
189
|
return;
|
|
80
190
|
}
|
|
81
191
|
|
|
192
|
+
void ICACHE_FLASH_ATTR StationScan::scanDone() {
|
|
193
|
+
using namespace painlessmesh::logger;
|
|
194
|
+
if (!scanRequested) {
|
|
195
|
+
// A synchronous scan's event (see scanComplete()). Not ours, and the
|
|
196
|
+
// task may be holding a yielded connectToAP() that must not be lost.
|
|
197
|
+
Log(CONNECTION, "scanDone(): not this task's scan, ignoring\n");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
task.yield([this]() { scanComplete(); });
|
|
201
|
+
}
|
|
202
|
+
|
|
82
203
|
void ICACHE_FLASH_ATTR StationScan::scanComplete() {
|
|
83
204
|
using namespace painlessmesh::logger;
|
|
205
|
+
// Reached by yield from scanDone(): the task's callback is the scan
|
|
206
|
+
// again from here, whatever this decides.
|
|
207
|
+
task.setCallback([this]() { stationScan(); });
|
|
208
|
+
if (!scanRequested) {
|
|
209
|
+
// The scan-done event of a synchronous scan — channel re-detection or
|
|
210
|
+
// a bridge takeover — whose results are consumed and deleted by the
|
|
211
|
+
// code that ran it. Treating it as ours found nothing, logged a scan
|
|
212
|
+
// failure, and rescanned immediately: on a rootless mesh every node
|
|
213
|
+
// did that back to back, and an OTA transfer through them stalled.
|
|
214
|
+
Log(CONNECTION, "scanComplete(): not this task's scan, ignoring\n");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
auto num = WiFi.scanComplete();
|
|
219
|
+
if (num == WIFI_SCAN_RUNNING) {
|
|
220
|
+
// A stale scan-done event — a synchronous scan's, delivered after this
|
|
221
|
+
// task started its own — while ours is still in flight. Ours is still
|
|
222
|
+
// owed a result, so the flag stays; clearing it here left the real
|
|
223
|
+
// completion ignored and the node never joined.
|
|
224
|
+
Log(CONNECTION,
|
|
225
|
+
"scanComplete(): a scan is still running, waiting for it\n");
|
|
226
|
+
task.delay(10 * SCAN_INTERVAL); // as stationScan() left it
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
scanRequested = false;
|
|
84
230
|
Log(CONNECTION, "scanComplete(): Scan finished\n");
|
|
85
231
|
|
|
86
232
|
aps.clear();
|
|
87
233
|
Log(CONNECTION, "scanComplete():-- > Cleared old APs.\n");
|
|
88
234
|
|
|
89
|
-
auto num = WiFi.scanComplete();
|
|
90
235
|
if (num == WIFI_SCAN_FAILED) {
|
|
91
236
|
Log(ERROR, "wifi scan failed. Retrying....\n");
|
|
92
237
|
task.forceNextIteration();
|
|
93
238
|
return;
|
|
94
|
-
} else if (num == WIFI_SCAN_RUNNING) {
|
|
95
|
-
Log(ERROR,
|
|
96
|
-
"scanComplete should never be called when scan is still running.\n");
|
|
97
|
-
return;
|
|
98
239
|
}
|
|
99
240
|
|
|
100
241
|
Log(CONNECTION, "scanComplete(): num = %d\n", num);
|
|
101
242
|
|
|
243
|
+
// One slice of a sliced hunt: tally the mesh APs it saw on channels other
|
|
244
|
+
// than this node's, then rest at home before the next slice. Nothing
|
|
245
|
+
// else is decided until the hunt has covered every channel.
|
|
246
|
+
uint8_t slice = scanSlice;
|
|
247
|
+
scanSlice = 0;
|
|
248
|
+
if (slice != 0) {
|
|
249
|
+
size_t seen = 0;
|
|
250
|
+
for (auto i = 0; i < num; ++i) {
|
|
251
|
+
TSTRING found = WiFi.SSID(i);
|
|
252
|
+
bool isMesh = found == ssid || (found.equals("") && mesh->_meshHidden);
|
|
253
|
+
uint8_t ch = WiFi.channel(i);
|
|
254
|
+
if (!isMesh || ch == mesh->_meshChannel ||
|
|
255
|
+
!painlessmesh::gateway::isValidMeshChannel(ch))
|
|
256
|
+
continue;
|
|
257
|
+
++huntCounts[ch];
|
|
258
|
+
++seen;
|
|
259
|
+
int8_t rssi = WiFi.RSSI(i);
|
|
260
|
+
if (!huntRssi.count(ch) || rssi > huntRssi[ch]) huntRssi[ch] = rssi;
|
|
261
|
+
}
|
|
262
|
+
Log(CONNECTION, "scanComplete(): hunt slice channel %u: %u mesh AP(s)\n",
|
|
263
|
+
slice, (unsigned)seen);
|
|
264
|
+
++huntChannel;
|
|
265
|
+
task.delay(1500 * TASK_MILLISECOND);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// A re-detection scan covered every channel. The mesh seen on a channel
|
|
270
|
+
// other than this node's may be the partition it is looking for — or a
|
|
271
|
+
// straggler: on the rig, a node still in gateway mode on the router's
|
|
272
|
+
// channel while the others are already back on the mesh channel, seen
|
|
273
|
+
// by a node that had three peers here and left them for it. What tells
|
|
274
|
+
// the two apart is size. The mesh APs on each other channel are
|
|
275
|
+
// counted; the channel with the most is the candidate, the strongest
|
|
276
|
+
// signal breaks a tie, and whether to go is decided below. A sliced hunt
|
|
277
|
+
// arrives here with its tally already made.
|
|
278
|
+
bool redetecting = scanAllChannels || huntPending;
|
|
279
|
+
scanAllChannels = false;
|
|
280
|
+
uint8_t elsewhere = 0;
|
|
281
|
+
int8_t elsewhereRssi = -128;
|
|
282
|
+
size_t elsewhereCount = 0;
|
|
283
|
+
std::map<uint8_t, size_t> meshApsOnChannel;
|
|
284
|
+
if (huntPending) {
|
|
285
|
+
huntPending = false;
|
|
286
|
+
for (auto&& entry : huntCounts) {
|
|
287
|
+
meshApsOnChannel[entry.first] = entry.second;
|
|
288
|
+
int8_t rssi = huntRssi.count(entry.first) ? huntRssi[entry.first] : -128;
|
|
289
|
+
if (entry.second > elsewhereCount ||
|
|
290
|
+
(entry.second == elsewhereCount && rssi > elsewhereRssi)) {
|
|
291
|
+
elsewhere = entry.first;
|
|
292
|
+
elsewhereCount = entry.second;
|
|
293
|
+
elsewhereRssi = rssi;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
Log(CONNECTION,
|
|
297
|
+
"scanComplete(): sliced hunt done: mesh on %u other channel(s)%s\n",
|
|
298
|
+
(unsigned)huntCounts.size(), elsewhere ? "" : ", none elsewhere");
|
|
299
|
+
huntCounts.clear();
|
|
300
|
+
huntRssi.clear();
|
|
301
|
+
}
|
|
302
|
+
|
|
102
303
|
for (auto i = 0; i < num; ++i) {
|
|
103
304
|
WiFi_AP_Record_t record;
|
|
104
305
|
record.ssid = WiFi.SSID(i);
|
|
306
|
+
bool isMesh = record.ssid == ssid ||
|
|
307
|
+
(record.ssid.equals("") && mesh->_meshHidden);
|
|
105
308
|
|
|
106
309
|
if (WiFi.channel(i) != mesh->_meshChannel) {
|
|
310
|
+
if (redetecting && isMesh &&
|
|
311
|
+
painlessmesh::gateway::isValidMeshChannel(WiFi.channel(i))) {
|
|
312
|
+
uint8_t ch = WiFi.channel(i);
|
|
313
|
+
size_t count = ++meshApsOnChannel[ch];
|
|
314
|
+
if (count > elsewhereCount ||
|
|
315
|
+
(count == elsewhereCount && WiFi.RSSI(i) > elsewhereRssi)) {
|
|
316
|
+
elsewhere = ch;
|
|
317
|
+
elsewhereCount = count;
|
|
318
|
+
elsewhereRssi = WiFi.RSSI(i);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
107
321
|
continue;
|
|
108
322
|
}
|
|
109
323
|
|
|
@@ -127,6 +341,128 @@ void ICACHE_FLASH_ATTR StationScan::scanComplete() {
|
|
|
127
341
|
|
|
128
342
|
Log(CONNECTION, "\tFound %d nodes\n", aps.size());
|
|
129
343
|
|
|
344
|
+
if (redetecting) {
|
|
345
|
+
// A disconnected node follows the mesh wherever it is. A connected
|
|
346
|
+
// node is already in a partition, and leaves it only for a bigger
|
|
347
|
+
// one: the APs it can see on its own channel against those on the
|
|
348
|
+
// other. Strictly bigger — at the start of a test, with one AP up on
|
|
349
|
+
// each channel, a tie is exactly the straggler case, and following it
|
|
350
|
+
// took a node and its subtree out of the mesh for a minute at a time.
|
|
351
|
+
// A stranded partition still finds a bridge that has moved: its top
|
|
352
|
+
// node lost its station link and follows unconditionally, and each
|
|
353
|
+
// node it takes along drops its own children the same way.
|
|
354
|
+
// Size cannot tell where the root is: a bridge that has just moved to
|
|
355
|
+
// the router's channel is one AP against the rest of the mesh, and it
|
|
356
|
+
// is the one to follow. Time can: a node still in gateway mode during
|
|
357
|
+
// the sequential teardown — the straggler the rig saw a node follow and
|
|
358
|
+
// sit alone with for a minute — is gone by the next scan; a bridge, or
|
|
359
|
+
// the partition that has formed around it, is not. So a bigger
|
|
360
|
+
// partition elsewhere is followed at once, and a smaller one only when
|
|
361
|
+
// the same channel shows the mesh on two consecutive re-detections. A
|
|
362
|
+
// disconnected node makes no connection while it looks again: joined
|
|
363
|
+
// to this channel it would be "connected", and the rootless partition
|
|
364
|
+
// it joined would take a re-detection or two longer to leave.
|
|
365
|
+
// Not even a bigger one at once: during the teardown two nodes still
|
|
366
|
+
// in gateway mode outnumbered the one AP a connected node could see
|
|
367
|
+
// on its own channel, and it left the soak for them.
|
|
368
|
+
bool connected = WiFi.status() == WL_CONNECTED;
|
|
369
|
+
bool follow = false;
|
|
370
|
+
// Home first. A node that was rooted knows the bridge's channel, and a
|
|
371
|
+
// bridge — the old one back, or the backup promoted in its place — is
|
|
372
|
+
// pinned to it by its router. Away from home with the mesh visible
|
|
373
|
+
// there, home is the channel to follow whatever the sizes; at home
|
|
374
|
+
// with a partition here, nothing elsewhere is worth leaving for. A
|
|
375
|
+
// node alone at home with nothing here follows the mesh as before,
|
|
376
|
+
// and comes back with it when a bridge appears.
|
|
377
|
+
bool atHome = rootedChannel != 0 && mesh->_meshChannel == rootedChannel;
|
|
378
|
+
if (rootedChannel != 0 && !atHome && meshApsOnChannel[rootedChannel] > 0 &&
|
|
379
|
+
elsewhere != rootedChannel) {
|
|
380
|
+
Log(CONNECTION,
|
|
381
|
+
"scanComplete(): Mesh on channel %d, where it was rooted; going "
|
|
382
|
+
"there rather than channel %d\n",
|
|
383
|
+
rootedChannel, elsewhere);
|
|
384
|
+
elsewhere = rootedChannel;
|
|
385
|
+
elsewhereCount = meshApsOnChannel[rootedChannel];
|
|
386
|
+
}
|
|
387
|
+
if (atHome && elsewhere > 0 && (connected || !aps.empty())) {
|
|
388
|
+
if (++homeStays >= 4) {
|
|
389
|
+
Log(CONNECTION,
|
|
390
|
+
"scanComplete(): The mesh has been elsewhere for %u re-detections "
|
|
391
|
+
"with no root here; forgetting this as home\n",
|
|
392
|
+
(unsigned)homeStays);
|
|
393
|
+
rootedChannel = 0;
|
|
394
|
+
homeStays = 0;
|
|
395
|
+
} else {
|
|
396
|
+
Log(CONNECTION,
|
|
397
|
+
"scanComplete(): Mesh also on channel %d with %u nodes; this is "
|
|
398
|
+
"the channel the mesh was rooted on, staying (%u of 4)\n",
|
|
399
|
+
elsewhere, (unsigned)elsewhereCount, (unsigned)homeStays);
|
|
400
|
+
elsewhere = 0;
|
|
401
|
+
}
|
|
402
|
+
} else if (atHome) {
|
|
403
|
+
homeStays = 0;
|
|
404
|
+
}
|
|
405
|
+
if (elsewhere > 0) {
|
|
406
|
+
if (!connected && aps.empty()) {
|
|
407
|
+
follow = true; // nothing here to lose, nothing to look again for
|
|
408
|
+
} else if (pendingElsewhere == elsewhere) {
|
|
409
|
+
follow = true; // still there a scan later: not a straggler
|
|
410
|
+
} else {
|
|
411
|
+
Log(CONNECTION,
|
|
412
|
+
"scanComplete(): Mesh also on channel %d with %u nodes, %u "
|
|
413
|
+
"here; looking again before following\n",
|
|
414
|
+
elsewhere, (unsigned)elsewhereCount, (unsigned)aps.size());
|
|
415
|
+
pendingElsewhere = elsewhere;
|
|
416
|
+
redetectRequested = true;
|
|
417
|
+
// Look again soon, connected or not. A connected node used to fall
|
|
418
|
+
// through to connectToAP(), whose "no root in sight" back-off put
|
|
419
|
+
// the second look up to a minute away: on the rig the node the
|
|
420
|
+
// bridge-discovery fixture sends from saw the new bridge's channel
|
|
421
|
+
// at 103 s and looked again at 172 s, and the fixture's window had
|
|
422
|
+
// closed. Skipping one round of connectToAP() costs nothing the
|
|
423
|
+
// second look does not give back.
|
|
424
|
+
aps.clear();
|
|
425
|
+
task.delay(0.5 * SCAN_INTERVAL);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
pendingElsewhere = 0;
|
|
430
|
+
}
|
|
431
|
+
if (follow) pendingElsewhere = 0;
|
|
432
|
+
if (follow) {
|
|
433
|
+
Log(CONNECTION,
|
|
434
|
+
"scanComplete(): Mesh found on different channel %d (was %d): %u "
|
|
435
|
+
"nodes there, %u here; following it\n",
|
|
436
|
+
elsewhere, mesh->_meshChannel, (unsigned)elsewhereCount,
|
|
437
|
+
(unsigned)aps.size());
|
|
438
|
+
// followBridgeChannel() does the whole move: it closes the station
|
|
439
|
+
// link, so an orphan actually leaves its old partition instead of
|
|
440
|
+
// restarting its AP on the new channel while still attached to the
|
|
441
|
+
// old one.
|
|
442
|
+
followBridgeChannel(elsewhere);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (elsewhere > 0) {
|
|
446
|
+
Log(CONNECTION,
|
|
447
|
+
"scanComplete(): Mesh also on channel %d with %u nodes; this "
|
|
448
|
+
"channel has %u, staying\n",
|
|
449
|
+
elsewhere, (unsigned)elsewhereCount, (unsigned)aps.size());
|
|
450
|
+
consecutiveEmptyScans = 0;
|
|
451
|
+
} else if (aps.empty()) {
|
|
452
|
+
// The mesh is on no channel at all. The empty-scan count stands: it
|
|
453
|
+
// is what lets an isolated bridge retry once it passes
|
|
454
|
+
// ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD.
|
|
455
|
+
Log(CONNECTION,
|
|
456
|
+
"scanComplete(): Mesh not found on any channel during re-scan\n");
|
|
457
|
+
} else {
|
|
458
|
+
Log(CONNECTION,
|
|
459
|
+
"scanComplete(): Mesh found on current channel %d, no channel "
|
|
460
|
+
"change needed\n",
|
|
461
|
+
mesh->_meshChannel);
|
|
462
|
+
consecutiveEmptyScans = 0;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
130
466
|
task.yield([this]() {
|
|
131
467
|
// Task filter all unknown
|
|
132
468
|
filterAPs();
|
|
@@ -222,6 +558,8 @@ void ICACHE_FLASH_ATTR StationScan::requestIP(WiFi_AP_Record_t &ap) {
|
|
|
222
558
|
mesh->_meshChannel,
|
|
223
559
|
ap.bssid[0], ap.bssid[1], ap.bssid[2],
|
|
224
560
|
ap.bssid[3], ap.bssid[4], ap.bssid[5]);
|
|
561
|
+
connectAttemptStarted = millis();
|
|
562
|
+
halfOpenDropped = false;
|
|
225
563
|
WiFi.begin(ap.ssid.c_str(), password.c_str(), mesh->_meshChannel, ap.bssid);
|
|
226
564
|
return;
|
|
227
565
|
}
|
|
@@ -257,71 +595,73 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
|
|
|
257
595
|
}
|
|
258
596
|
}
|
|
259
597
|
}
|
|
598
|
+
|
|
599
|
+
#ifdef ESP32
|
|
600
|
+
if (WiFi.status() == WL_IDLE_STATUS && !halfOpenDropped &&
|
|
601
|
+
connectAttemptStarted != 0 &&
|
|
602
|
+
millis() - connectAttemptStarted > (uint32_t)(0.5 * SCAN_INTERVAL)) {
|
|
603
|
+
// The Arduino core reports WL_IDLE_STATUS from association until an
|
|
604
|
+
// address arrives. Half a scan interval after the attempt began, that
|
|
605
|
+
// means this station is associated with an AP that never gave it an
|
|
606
|
+
// address — a peer whose DHCP server was restarting, or one that
|
|
607
|
+
// rebooted under it. Nothing times that out: no disconnect event comes,
|
|
608
|
+
// and the mesh never learns of the failure. Drop the half-open link;
|
|
609
|
+
// the disconnect event schedules the rescan.
|
|
610
|
+
//
|
|
611
|
+
// Once per attempt. When the status is WL_IDLE_STATUS with nothing
|
|
612
|
+
// to disconnect — the core's own retry left it there — the disconnect
|
|
613
|
+
// changes nothing, and this guard, firing on every pass, returned
|
|
614
|
+
// before the scan results were ever looked at: the failover test's
|
|
615
|
+
// sender logged "dropping it" every thirty seconds with the count
|
|
616
|
+
// growing past three minutes and never connected to anything again.
|
|
617
|
+
// The second pass falls through to the scan, whose requestIP() starts
|
|
618
|
+
// a fresh attempt.
|
|
619
|
+
Log(CONNECTION,
|
|
620
|
+
"connectToAP(): Station associated without an address for %u ms, "
|
|
621
|
+
"dropping it\n",
|
|
622
|
+
millis() - connectAttemptStarted);
|
|
623
|
+
halfOpenDropped = true;
|
|
624
|
+
WiFi.disconnect();
|
|
625
|
+
task.delay(SCAN_INTERVAL); // Only reached if the event never fires
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
#endif
|
|
260
629
|
bool isRooted = layout::isRooted(mesh->asNodeTree());
|
|
630
|
+
if (isRooted) everRooted = true;
|
|
261
631
|
if (aps.empty()) {
|
|
262
632
|
// No unknown nodes found
|
|
263
633
|
consecutiveEmptyScans++;
|
|
634
|
+
partitionScans = 0; // nothing unrouted in sight: not partitioned
|
|
264
635
|
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
636
|
+
// Re-detect the mesh channel once the empty scans pile up. Two cases
|
|
637
|
+
// need it, and the second used to be excluded:
|
|
638
|
+
// - the station is disconnected, so the mesh has left this channel;
|
|
639
|
+
// - the station is connected but to a partition with no root while the
|
|
640
|
+
// mesh is meant to have one — the node is orphaned. That is what a
|
|
641
|
+
// bridge start does to everyone it does not directly serve: it moves
|
|
642
|
+
// to the router's channel, the nodes it served drop and re-scan, and
|
|
643
|
+
// the nodes behind *them* stay connected to each other on the old
|
|
644
|
+
// channel, filter their peers as known, count empty scans, and were
|
|
645
|
+
// gated out of re-detection by the WL_CONNECTED check for good.
|
|
646
|
+
bool orphaned = mesh->shouldContainRoot && !isRooted;
|
|
647
|
+
if (consecutiveEmptyScans >= EMPTY_SCAN_THRESHOLD &&
|
|
648
|
+
(WiFi.status() != WL_CONNECTED || orphaned) && channel > 0) {
|
|
270
649
|
Log(CONNECTION,
|
|
271
|
-
"connectToAP(): No mesh nodes found for %d scans,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// Restart AP on new channel to match the mesh
|
|
287
|
-
// This ensures this node's AP is also discoverable on the correct channel
|
|
288
|
-
if (WiFi.getMode() & WIFI_AP) {
|
|
289
|
-
Log(CONNECTION,
|
|
290
|
-
"connectToAP(): Restarting AP from channel %d to channel %d\n",
|
|
291
|
-
oldChannel, detectedChannel);
|
|
292
|
-
|
|
293
|
-
// Disconnect AP and allow WiFi stack to fully reset
|
|
294
|
-
// Using true parameter ensures DHCP server is properly stopped
|
|
295
|
-
WiFi.softAPdisconnect(true);
|
|
296
|
-
delay(200); // Increased delay to ensure complete WiFi stack reset
|
|
297
|
-
|
|
298
|
-
// Call apInit via friend class access (StationScan is friend of wifi::Mesh)
|
|
299
|
-
mesh->apInit(mesh->getNodeId());
|
|
300
|
-
|
|
301
|
-
// Additional stabilization delay after AP restart
|
|
302
|
-
// This ensures DHCP server is fully initialized before clients connect
|
|
303
|
-
delay(100);
|
|
304
|
-
|
|
305
|
-
Log(CONNECTION, "connectToAP(): AP restarted on channel %d\n", detectedChannel);
|
|
306
|
-
}
|
|
307
|
-
// Reset counter only when mesh is found on a new channel
|
|
308
|
-
// This allows isolated bridge retry to continue when mesh is truly absent
|
|
309
|
-
consecutiveEmptyScans = 0;
|
|
310
|
-
} else if (detectedChannel == 0) {
|
|
311
|
-
Log(CONNECTION,
|
|
312
|
-
"connectToAP(): Mesh not found on any channel during re-scan\n");
|
|
313
|
-
// Do NOT reset consecutiveEmptyScans here - mesh is still absent
|
|
314
|
-
// This allows isolated bridge retry mechanism to trigger when
|
|
315
|
-
// the counter exceeds ISOLATED_BRIDGE_RETRY_SCAN_THRESHOLD
|
|
316
|
-
} else {
|
|
317
|
-
// detectedChannel == mesh->_meshChannel
|
|
318
|
-
// Mesh found on same channel we're already on - no channel change needed
|
|
319
|
-
// Reset counter since mesh exists, nodes may appear in subsequent scans
|
|
320
|
-
Log(CONNECTION,
|
|
321
|
-
"connectToAP(): Mesh found on current channel %d, no channel change needed\n",
|
|
322
|
-
detectedChannel);
|
|
323
|
-
consecutiveEmptyScans = 0;
|
|
324
|
-
}
|
|
650
|
+
"connectToAP(): No mesh nodes found for %d scans%s, re-detecting "
|
|
651
|
+
"the mesh channel on the next scan\n",
|
|
652
|
+
consecutiveEmptyScans, orphaned ? " (connected but unrooted)" : "");
|
|
653
|
+
// The next scan of this task covers every channel and scanComplete()
|
|
654
|
+
// follows the mesh if it is elsewhere. It used to run a synchronous
|
|
655
|
+
// all-channel scan right here: four to seven seconds with the main
|
|
656
|
+
// loop held and the radio off the mesh channel, on every node of a
|
|
657
|
+
// rootless mesh in turn, and an ACK owed through the scanning node
|
|
658
|
+
// arrived after the sender's budget — the soak's recurring loss.
|
|
659
|
+
// The re-detection is one scan interval later than it was; the
|
|
660
|
+
// interval below is the disconnected node's fast one or the orphan's
|
|
661
|
+
// backed-off one, so a stranded follower still catches up within
|
|
662
|
+
// the gateway contract, and a mesh that is simply rootless is not
|
|
663
|
+
// deaf for seconds at a time.
|
|
664
|
+
redetectRequested = true;
|
|
325
665
|
}
|
|
326
666
|
|
|
327
667
|
if (WiFi.status() == WL_CONNECTED &&
|
|
@@ -331,6 +671,66 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
|
|
|
331
671
|
"connectToAP(): Already connected, and no unknown nodes found: "
|
|
332
672
|
"scan rate set to slow\n");
|
|
333
673
|
task.delay(4 * SCAN_INTERVAL);
|
|
674
|
+
} else if (orphaned && WiFi.status() == WL_CONNECTED) {
|
|
675
|
+
// Connected, told the mesh has a root, and not seeing one. The first
|
|
676
|
+
// re-detections come quickly — that is how a follower stranded by a
|
|
677
|
+
// bridge's channel move catches up — but a mesh that is simply
|
|
678
|
+
// rootless must not keep every node scanning all channels every
|
|
679
|
+
// half interval for as long as it stays so. Back off to two
|
|
680
|
+
// intervals; anything new on the air resets it.
|
|
681
|
+
// A leaf that has re-detected twice while connected and rootless,
|
|
682
|
+
// and found the mesh only on its own channel, is in a rootless
|
|
683
|
+
// partition that its scans cannot get it out of: every AP it can
|
|
684
|
+
// see is "known" — in its tree — including a bridge that was
|
|
685
|
+
// promoted a minute ago and is listed where it used to be, before
|
|
686
|
+
// its restart. On the rig the failover test's sender sat like that
|
|
687
|
+
// through the whole promotion window. Dropping the station link
|
|
688
|
+
// empties the tree, so the next scan sees every AP as new and the
|
|
689
|
+
// bridge's among them. Only a leaf: an interior node would take its
|
|
690
|
+
// subtree with it. The count resets when anything new is heard.
|
|
691
|
+
// And only if this mesh ever had a root: one that never did is
|
|
692
|
+
// rootless by design, and its leaves must not keep leaving.
|
|
693
|
+
size_t apChildren = 0;
|
|
694
|
+
for (auto&& sub : mesh->subs) {
|
|
695
|
+
if (sub->connected() && !sub->station) ++apChildren;
|
|
696
|
+
}
|
|
697
|
+
// Not from home, and not a failover candidate. At home the root will
|
|
698
|
+
// reappear here — the old bridge back, or a backup promoted in this
|
|
699
|
+
// partition — and a candidate must stay connected to hold the
|
|
700
|
+
// election at all: the bridge monitor skips a node with no mesh
|
|
701
|
+
// connections. On the rig the backup left its partition at 90 s,
|
|
702
|
+
// which skipped the election, rejoined, sent its candidacy at 129 s
|
|
703
|
+
// and left again at 133 s, before the votes were counted.
|
|
704
|
+
bool atHome = rootedChannel != 0 && mesh->_meshChannel == rootedChannel;
|
|
705
|
+
bool candidate =
|
|
706
|
+
mesh->bridgeFailoverEnabled && mesh->routerCredentialsConfigured;
|
|
707
|
+
bool stranded = ++orphanRedetects >= 2 && apChildren == 0 && everRooted;
|
|
708
|
+
if (stranded && (atHome || candidate)) {
|
|
709
|
+
Log(CONNECTION,
|
|
710
|
+
"connectToAP(): Still no root after %u re-detections; staying: "
|
|
711
|
+
"%s\n",
|
|
712
|
+
(unsigned)orphanRedetects,
|
|
713
|
+
candidate ? "this node is a failover candidate"
|
|
714
|
+
: "this is the channel the mesh was rooted on");
|
|
715
|
+
}
|
|
716
|
+
if (stranded && !atHome && !candidate) {
|
|
717
|
+
Log(CONNECTION,
|
|
718
|
+
"connectToAP(): Still no root after %u re-detections and nothing "
|
|
719
|
+
"new in sight; leaving this partition to look for it\n",
|
|
720
|
+
(unsigned)orphanRedetects);
|
|
721
|
+
orphanRedetects = 0;
|
|
722
|
+
orphanScanBackoff = 0;
|
|
723
|
+
mesh->closeConnectionSTA();
|
|
724
|
+
mesh->stability = 0;
|
|
725
|
+
task.delay(0.5 * SCAN_INTERVAL);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
uint32_t interval = (0.5 * SCAN_INTERVAL) * (1u << orphanScanBackoff);
|
|
729
|
+
Log(CONNECTION,
|
|
730
|
+
"connectToAP(): No root in sight, next scan in %u s\n",
|
|
731
|
+
(unsigned)(interval / TASK_SECOND));
|
|
732
|
+
task.delay(interval);
|
|
733
|
+
if (orphanScanBackoff < 2) orphanScanBackoff++;
|
|
334
734
|
} else {
|
|
335
735
|
// else scan fast (SCAN_INTERVAL)
|
|
336
736
|
Log(CONNECTION,
|
|
@@ -342,6 +742,7 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
|
|
|
342
742
|
} else {
|
|
343
743
|
// Reset counter when APs are found
|
|
344
744
|
consecutiveEmptyScans = 0;
|
|
745
|
+
orphanScanBackoff = 0;
|
|
345
746
|
if (WiFi.status() == WL_CONNECTED) {
|
|
346
747
|
// TODO: Use %u instead of String() here and below
|
|
347
748
|
// Also prob is always equal to stability, so we should use that directly
|
|
@@ -349,8 +750,39 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
|
|
|
349
750
|
"connectToAP(): Unknown nodes found. Current stability: %s\n",
|
|
350
751
|
String(mesh->stability).c_str());
|
|
351
752
|
|
|
753
|
+
// A node that is connected, told the mesh has a root, cannot see one,
|
|
754
|
+
// and can see nodes it has no route to is in a partition — and the
|
|
755
|
+
// root is in the other one. The probabilistic reconfigure below is
|
|
756
|
+
// gated by `stability`, which only grows on scans that find nothing
|
|
757
|
+
// unknown, so a partitioned node's probability is near zero after
|
|
758
|
+
// its first attempt and the partition stands. On the rig one such
|
|
759
|
+
// partition held for the whole of a five-minute OTA transfer while
|
|
760
|
+
// the receiver's ten requests went to a sender it had no route to.
|
|
761
|
+
// Two consecutive scans showing the other partition is enough grace
|
|
762
|
+
// for a transient; then it reconnects, deterministically.
|
|
763
|
+
// Only a leaf may jump. An interior node that drops its station link
|
|
764
|
+
// takes its whole subtree with it and creates the fragmentation it
|
|
765
|
+
// was meant to heal — measured: fourteen such jumps in one suite and
|
|
766
|
+
// every delivery test failed. Leaves jumping one at a time still
|
|
767
|
+
// converge: each leaf that leaves makes its parent a leaf.
|
|
768
|
+
size_t apChildren = 0;
|
|
769
|
+
for (auto&& sub : mesh->subs) {
|
|
770
|
+
if (sub->connected() && !sub->station) ++apChildren;
|
|
771
|
+
}
|
|
772
|
+
if (!isRooted && mesh->shouldContainRoot && apChildren == 0) {
|
|
773
|
+
++partitionScans;
|
|
774
|
+
} else {
|
|
775
|
+
partitionScans = 0;
|
|
776
|
+
}
|
|
352
777
|
int prob = mesh->stability;
|
|
353
|
-
if (!isRooted && random(0, 1000) < prob) {
|
|
778
|
+
if (!isRooted && (partitionScans >= 2 || random(0, 1000) < prob)) {
|
|
779
|
+
if (partitionScans >= 2) {
|
|
780
|
+
Log(CONNECTION,
|
|
781
|
+
"connectToAP(): Nodes without a route seen on %u scans while "
|
|
782
|
+
"unrooted; joining that partition\n",
|
|
783
|
+
partitionScans);
|
|
784
|
+
partitionScans = 0;
|
|
785
|
+
}
|
|
354
786
|
Log(CONNECTION, "connectToAP(): Reconfigure network: %s\n",
|
|
355
787
|
String(prob).c_str());
|
|
356
788
|
// close STA connection, this will trigger station disconnect which
|
|
@@ -374,18 +806,75 @@ void ICACHE_FLASH_ATTR StationScan::connectToAP() {
|
|
|
374
806
|
aps.pop_front(); // drop bestAP from mesh list, so if doesn't work out,
|
|
375
807
|
// we can try the next one
|
|
376
808
|
requestIP(ap);
|
|
377
|
-
//
|
|
809
|
+
// A rejected attempt raises a disconnect event, which rescans at
|
|
810
|
+
// once; this delay only bounds the silent failures — an association
|
|
811
|
+
// that never gets an address — and two minutes was too long for a
|
|
812
|
+
// node whose bridge has just moved.
|
|
378
813
|
Log(CONNECTION,
|
|
379
|
-
"connectToAP(): Trying to connect, scan
|
|
380
|
-
|
|
381
|
-
task.delay(4 * SCAN_INTERVAL);
|
|
814
|
+
"connectToAP(): Trying to connect, next scan in one interval\n");
|
|
815
|
+
task.delay(SCAN_INTERVAL);
|
|
382
816
|
}
|
|
383
817
|
}
|
|
384
818
|
}
|
|
385
819
|
|
|
820
|
+
bool ICACHE_FLASH_ATTR StationScan::followBridgeChannel(
|
|
821
|
+
uint8_t targetChannel) {
|
|
822
|
+
using namespace painlessmesh::logger;
|
|
823
|
+
|
|
824
|
+
if (!painlessmesh::gateway::isValidMeshChannel(targetChannel) ||
|
|
825
|
+
mesh == nullptr) {
|
|
826
|
+
Log(ERROR,
|
|
827
|
+
"followBridgeChannel(): Ignoring invalid bridge channel %u\n",
|
|
828
|
+
targetChannel);
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (mesh->_meshChannel == targetChannel) return false;
|
|
833
|
+
|
|
834
|
+
uint8_t previousChannel = mesh->_meshChannel;
|
|
835
|
+
Log(CONNECTION,
|
|
836
|
+
"followBridgeChannel(): Moving mesh from channel %u to bridge channel "
|
|
837
|
+
"%u\n",
|
|
838
|
+
previousChannel, targetChannel);
|
|
839
|
+
|
|
840
|
+
// Discard any asynchronous result from the old channel before changing the
|
|
841
|
+
// radio, otherwise its callback can move the node back after the takeover.
|
|
842
|
+
WiFi.scanDelete();
|
|
843
|
+
task.disable();
|
|
844
|
+
// The drops this causes are this node's doing, not a loss: judged as one
|
|
845
|
+
// they re-detected the channel just left, found the remnant there bigger,
|
|
846
|
+
// and moved back — on the rig two nodes followed the bridge to its
|
|
847
|
+
// channel and were back on the old one 28 s later, as the sender
|
|
848
|
+
// arrived.
|
|
849
|
+
channelMovedAt = millis();
|
|
850
|
+
mesh->closeConnectionSTA();
|
|
851
|
+
WiFi.disconnect();
|
|
852
|
+
delay(100);
|
|
853
|
+
|
|
854
|
+
mesh->_meshChannel = targetChannel;
|
|
855
|
+
channel = targetChannel;
|
|
856
|
+
consecutiveEmptyScans = 0;
|
|
857
|
+
|
|
858
|
+
if (WiFi.getMode() & WIFI_AP) {
|
|
859
|
+
WiFi.softAPdisconnect(true);
|
|
860
|
+
delay(100);
|
|
861
|
+
mesh->apInit(mesh->getNodeId());
|
|
862
|
+
delay(100);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Resume discovery immediately. The old recovery path required repeated
|
|
866
|
+
// empty scans and exceeded the gateway failover contract.
|
|
867
|
+
orphanScanBackoff = 0;
|
|
868
|
+
task.enable();
|
|
869
|
+
task.forceNextIteration();
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
|
|
386
873
|
// Helper function to scan all channels for a specific mesh SSID
|
|
387
874
|
// Returns the channel number if found, or 0 if not found
|
|
388
|
-
uint8_t ICACHE_FLASH_ATTR StationScan::scanForMeshChannel(TSTRING meshSSID, bool meshHidden
|
|
875
|
+
uint8_t ICACHE_FLASH_ATTR StationScan::scanForMeshChannel(TSTRING meshSSID, bool meshHidden,
|
|
876
|
+
uint8_t avoidChannel,
|
|
877
|
+
TSTRING routerSSID) {
|
|
389
878
|
using namespace painlessmesh::logger;
|
|
390
879
|
Log(CONNECTION, "scanForMeshChannel(): Scanning all channels for mesh '%s'...\n", meshSSID.c_str());
|
|
391
880
|
|
|
@@ -398,32 +887,63 @@ uint8_t ICACHE_FLASH_ATTR StationScan::scanForMeshChannel(TSTRING meshSSID, bool
|
|
|
398
887
|
|
|
399
888
|
if (numNetworks == WIFI_SCAN_FAILED) {
|
|
400
889
|
Log(ERROR, "scanForMeshChannel(): WiFi scan failed\n");
|
|
890
|
+
// The only exit that used to leave the results allocated, and the one
|
|
891
|
+
// taken when the radio is already busy — which is exactly when a node
|
|
892
|
+
// is retrying this every re-detection interval.
|
|
893
|
+
WiFi.scanDelete();
|
|
401
894
|
return 0;
|
|
402
895
|
}
|
|
403
896
|
|
|
404
897
|
Log(CONNECTION, "scanForMeshChannel(): Found %d networks\n", numNetworks);
|
|
405
898
|
|
|
406
|
-
//
|
|
899
|
+
// Collect every channel the mesh is on, then choose. Returning the first
|
|
900
|
+
// match made a stranded node's fate depend on scan order: seeing its own
|
|
901
|
+
// partition first, it concluded nothing had changed and stayed put.
|
|
902
|
+
std::vector<painlessmesh::gateway::MeshChannelCandidate> candidates;
|
|
903
|
+
uint8_t routerChannel = 0;
|
|
407
904
|
for (int16_t i = 0; i < numNetworks; ++i) {
|
|
408
905
|
TSTRING foundSSID = WiFi.SSID(i);
|
|
409
906
|
uint8_t foundChannel = WiFi.channel(i);
|
|
410
907
|
int32_t rssi = WiFi.RSSI(i);
|
|
411
|
-
|
|
412
|
-
|
|
908
|
+
|
|
909
|
+
if (routerSSID.length() > 0 && foundSSID == routerSSID &&
|
|
910
|
+
foundChannel >= 1 && foundChannel <= 13) {
|
|
911
|
+
Log(CONNECTION,
|
|
912
|
+
"scanForMeshChannel(): Router %s on channel %d (RSSI: %d)\n",
|
|
913
|
+
routerSSID.c_str(), foundChannel, rssi);
|
|
914
|
+
routerChannel = foundChannel;
|
|
915
|
+
}
|
|
413
916
|
if (foundSSID == meshSSID || (foundSSID == "" && meshHidden)) {
|
|
414
|
-
// Validate channel is in valid range (1-13 for 2.4GHz)
|
|
415
917
|
if (foundChannel >= 1 && foundChannel <= 13) {
|
|
416
|
-
Log(CONNECTION, "scanForMeshChannel(): Found mesh on channel %d (RSSI: %d)\n",
|
|
918
|
+
Log(CONNECTION, "scanForMeshChannel(): Found mesh on channel %d (RSSI: %d)\n",
|
|
417
919
|
foundChannel, rssi);
|
|
418
|
-
|
|
419
|
-
return foundChannel;
|
|
920
|
+
candidates.push_back({foundChannel, rssi});
|
|
420
921
|
} else {
|
|
421
|
-
Log(ERROR, "scanForMeshChannel(): Found mesh on invalid channel %d, ignoring\n",
|
|
922
|
+
Log(ERROR, "scanForMeshChannel(): Found mesh on invalid channel %d, ignoring\n",
|
|
422
923
|
foundChannel);
|
|
423
924
|
}
|
|
424
925
|
}
|
|
425
926
|
}
|
|
426
|
-
|
|
927
|
+
|
|
928
|
+
uint8_t chosen = painlessmesh::gateway::pickMeshChannel(candidates, avoidChannel,
|
|
929
|
+
routerChannel);
|
|
930
|
+
if (chosen != 0) {
|
|
931
|
+
if (routerChannel != 0 && chosen == routerChannel && candidates.size() > 1) {
|
|
932
|
+
Log(CONNECTION,
|
|
933
|
+
"scanForMeshChannel(): Mesh on %u channels; taking the router's, "
|
|
934
|
+
"channel %d, where a bridge would be\n",
|
|
935
|
+
(unsigned)candidates.size(), chosen);
|
|
936
|
+
}
|
|
937
|
+
if (avoidChannel != 0 && chosen != avoidChannel) {
|
|
938
|
+
Log(CONNECTION,
|
|
939
|
+
"scanForMeshChannel(): Mesh also on channel %d; preferring it over "
|
|
940
|
+
"current channel %d\n",
|
|
941
|
+
chosen, avoidChannel);
|
|
942
|
+
}
|
|
943
|
+
WiFi.scanDelete();
|
|
944
|
+
return chosen;
|
|
945
|
+
}
|
|
946
|
+
|
|
427
947
|
Log(CONNECTION, "scanForMeshChannel(): Mesh '%s' not found on any channel\n", meshSSID.c_str());
|
|
428
948
|
WiFi.scanDelete();
|
|
429
949
|
return 0; // Not found
|