Uniot Core
0.8.1
Loading...
Searching...
No Matches
NetworkScheduler.h
Go to the documentation of this file.
1/*
2 * This is a part of the Uniot project.
3 * Copyright (C) 2016-2025 Uniot <contact@uniot.io>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19#pragma once
20
21#if defined(ESP8266)
22#include <ESP8266WiFi.h>
23#include <ESP8266mDNS.h>
24#elif defined(ESP32)
25#include <ESPmDNS.h>
26#include <WiFi.h>
27#endif
28
29#include <Common.h>
30#include <ConfigCaptivePortal.h>
31#include <Credentials.h>
32#include <EventBus.h>
33#include <EventEmitter.h>
34#include <MicroJSON.h>
35#include <NetworkEvents.h>
36#include <Patches.h>
37#include <TaskScheduler.h>
38#include <WiFiNetworkScan.h>
39#include <WifiStorage.h>
40#include <config.min.html.gz.h>
41
82
83namespace uniot {
101 public:
113 : mpCredentials(&credentials),
114 mApSubnet(255, 255, 255, 0),
115 mConfigServer(IPAddress(1, 1, 1, 1),
116 [this](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
117 _handleWebSocketEvent(server, client, type, arg, data, len);
118 }) {
119 mApName = "UNIOT-" + String(mpCredentials->getShortDeviceId(), HEX);
120 mApName.toUpperCase();
121 mCanScan = true;
122 mApEnabled = false;
123 mLastSaveResult = -1;
124
125 // default wifi persistent storage brings unexpected behavior, I turn it off
126 WiFi.persistent(false);
127 WiFi.setAutoConnect(false);
128 WiFi.setAutoReconnect(false);
129 WiFi.setHostname(mApName.c_str());
130
131 _initTasks();
132 }
133
144 virtual void pushTo(TaskScheduler &scheduler) override {
145 scheduler.push("server_start", mTaskStart);
146 scheduler.push("server_serve", mTaskServe);
147 scheduler.push("server_stop", mTaskStop);
148 scheduler.push("ap_config", mTaskConfigAp);
149 scheduler.push("ap_stop", mTaskStopAp);
150 scheduler.push("sta_connect", mTaskConnectSta);
151 scheduler.push("sta_connecting", mTaskConnecting);
152 scheduler.push("wifi_monitor", mTaskMonitoring);
153 scheduler.push("wifi_scan", mTaskScan);
154 scheduler.push("wifi_check", mTaskAvailabilityCheck);
155#if defined(ESP32)
156 scheduler.push("wifi_scan_complete", mWifiScan.getTask());
157#endif
158 }
159
167 virtual void attach() override {
168 mWifiStorage.restore();
169 if (mWifiStorage.isCredentialsValid()) {
170 mTaskConnectSta->once(500);
171 } else {
172 mTaskConfigAp->once(500);
173 }
174 }
175
183 void config() {
184 if (_tryToRecoverAp()) {
185 UNIOT_LOG_DEBUG("Config already in progress. AP recovered");
186 return;
187 }
188 mTaskConfigAp->once(100);
189 }
190
197 void forget() {
198 UNIOT_LOG_DEBUG("Forget credentials: %s", mWifiStorage.getSsid().c_str());
199 mWifiStorage.clean();
201 mTaskConfigAp->once(500);
202 }
203
211 bool reconnect() {
212 if (mWifiStorage.isCredentialsValid()) {
214 mTaskConnectSta->once(500);
215
216 if (_tryToRecoverAp()) {
217 UNIOT_LOG_DEBUG("Reconnecting while AP is enabled. AP recovered");
218 }
219
220 return true;
221 }
222 return false;
223 }
224
234 bool setCredentials(const String &ssid, const String &password) {
235 if (!ssid.isEmpty()) {
236 mWifiStorage.setCredentials(ssid, password);
237 mWifiStorage.store();
238 return true;
239 }
240 return false;
241 }
242
243 private:
250 enum ACTIONS {
251 INVALID = 0,
252 STATUS = 100,
253 SAVE,
254 SCAN,
255 ASK
256 };
257
265 void _initTasks() {
266 // Server lifecycle tasks
267 mTaskStart = TaskScheduler::make([this](SchedulerTask &self, short t) {
268 mTaskStop->detach();
269 if (mConfigServer.start()) {
270 _initServerCallbacks();
271 mConfigServer.wsEnable(true); // Ensure that WS are enabled after disabling them in "Step 1 of Stopping Configuration" during the AP recovery process
272 mTaskServe->attach(10);
273 } else {
274 UNIOT_LOG_WARN("Start server failed. Restarting...");
275 self.once(1000);
276 }
277 });
278
279 mTaskServe = TaskScheduler::make(mConfigServer);
280
281 mTaskStop = TaskScheduler::make([this](SchedulerTask &self, short t) {
282 static bool wsClosed = false;
283 UNIOT_LOG_DEBUG("Stop server, state: %d", wsClosed);
284 // 1: close websocket
285 // 2: stop access point
286 // 3: stop server
287 if (!wsClosed) {
288 mConfigServer.wsCloseAll();
289 wsClosed = true;
290 self.once(10000); // Stopping Configuration. Step 3. Carefully change the deferrals.
291 return;
292 }
293 mTaskServe->detach();
294 mConfigServer.stop();
295 wsClosed = false;
296 mLastNetworks = static_cast<const char *>(nullptr); // invalidate String
297 });
298
299 // Access Point tasks
300 mTaskConfigAp = TaskScheduler::make([this](SchedulerTask &self, short t) {
301 WiFi.disconnect(true, true);
302 mTaskStopAp->detach();
303 if (WiFi.softAPConfig(mConfigServer.ip(), mConfigServer.ip(), mApSubnet) && WiFi.softAP(mApName.c_str())) {
304#if defined(ESP32) && defined(ENABLE_LOWER_WIFI_TX_POWER)
305 WiFi.setTxPower(WIFI_TX_POWER_LEVEL);
306#endif
307 mTaskStart->once(500);
308 mTaskScan->once(500);
309 mTaskAvailabilityCheck->attach(10000);
310 mApEnabled = true;
313 } else {
314 UNIOT_LOG_WARN("Start server failed");
315 mTaskConfigAp->attach(500, 1);
316 }
317 });
318
319 mTaskStopAp = TaskScheduler::make([this](SchedulerTask &self, short t) {
320 mApEnabled = false;
321 WiFi.softAPdisconnect(true); // check with 8266
322 });
323
324 // Station connection tasks
325 mTaskConnectSta = TaskScheduler::make([this](SchedulerTask &self, short t) {
326 WiFi.disconnect(false, true);
327 bool connect = WiFi.begin(mWifiStorage.getSsid().c_str(), mWifiStorage.getPassword().c_str()) != WL_CONNECT_FAILED;
328 if (connect) {
329#if defined(ESP32) && defined(ENABLE_LOWER_WIFI_TX_POWER)
330 WiFi.setTxPower(WIFI_TX_POWER_LEVEL);
331#endif
332 mTaskConnecting->attach(100, 50);
335 mCanScan = false;
336 mLastSaveResult = -1;
337 } else {
338 mTaskConnecting->detach();
340 mCanScan = true;
341 mLastSaveResult = 0;
342 }
343 });
344
345 mTaskConnecting = TaskScheduler::make([this](SchedulerTask &self, short times) {
346 auto __processFailure = [this](int triesBeforeGivingUp = 3) {
347 static int tries = 0;
348 if (++tries < triesBeforeGivingUp) {
349 UNIOT_LOG_INFO("Tries to connect until give up is %d", triesBeforeGivingUp - tries);
350 mTaskConnectSta->attach(500, 1);
351 } else {
352 tries = 0;
353 mWifiStorage.restore();
355 mCanScan = true;
356 mLastSaveResult = 0;
357 }
358 };
359
360 switch (WiFi.status()) {
361 case WL_CONNECTED:
362 self.detach();
363 mTaskMonitoring->attach(200);
364 mWifiStorage.store();
365 if (mpCredentials->isOwnerChanged()) {
366 mpCredentials->store();
367 }
368
369 mCanScan = true;
370 mLastSaveResult = 1;
371 mTaskStop->once(30000); // Stopping Configuration. Step 1. Carefully change the deferrals.
372 mTaskStopAp->once(35000); // Stopping Configuration. Step 2. Carefully change the deferrals.
373 mTaskAvailabilityCheck->detach();
375 break;
376
377 case WL_NO_SSID_AVAIL:
378 case WL_CONNECT_FAILED:
379 self.detach();
380 __processFailure();
381 break;
382#if defined(ESP8266)
383 case WL_WRONG_PASSWORD:
384 self.detach();
385 __processFailure(1);
386 break;
387#endif
388 case WL_IDLE_STATUS:
389 case WL_DISCONNECTED:
390 case WL_CONNECTION_LOST:
391 if (!times) {
392 __processFailure();
393 }
394 break;
395
396 default:
397 UNIOT_LOG_WARN("Unexpected WiFi status: %d", WiFi.status());
398 break;
399 }
400 });
401
402 mTaskMonitoring = TaskScheduler::make([this](SchedulerTask &self, short times) {
403 if (WiFi.status() != WL_CONNECTED) {
404 mTaskMonitoring->detach();
406 }
407 });
408
409 // Network scanning tasks
410 mTaskScan = TaskScheduler::make([this](SchedulerTask &self, short times) {
411 static auto __broadcastNets = [this](const String &netJsonArray) {
412 String nets;
413 JSON::Object(nets)
414 .put("nets", netJsonArray, false)
415 .close();
416 mConfigServer.wsTextAll(nets);
417 delay(50); // to allow for all clients to receive the message (relevant for ESP32)
418 };
419 if (mCanScan) {
420 mWifiScan.scanNetworksAsync([this](int n) {
421 mLastNetworks = static_cast<const char *>(nullptr); // invalidate String
422 JSON::Array jsonNets(mLastNetworks);
423 for (auto i = 0; i < n; ++i) {
424 jsonNets.appendArray()
425 .append(WiFi.BSSIDstr(i))
426 .append(WiFi.SSID(i))
427 .append(WiFi.RSSI(i))
428 .append(mWifiScan.isSecured(WiFi.encryptionType(i)))
429 .close();
430 }
431 jsonNets.close();
432 WiFi.scanDelete();
433 __broadcastNets(mLastNetworks);
434 });
435 } else {
436 __broadcastNets(mLastNetworks);
437 }
438 });
439
440 mTaskAvailabilityCheck = TaskScheduler::make([this](SchedulerTask &self, short times) {
441 static int scanInProgressFuse = 0;
442 if (scanInProgressFuse-- > 0) {
443 UNIOT_LOG_INFO("Availability check skipped, scan in progress");
444 return;
445 }
446
447 if (mCanScan &&
448 !mConfigServer.wsClientsActive() &&
449 mWifiStorage.isCredentialsValid()) {
450 UNIOT_LOG_INFO("Checking availability of the network [%s]", mWifiStorage.getSsid().c_str());
451 scanInProgressFuse = 3;
452
453 mWifiScan.scanNetworksAsync([&](int n) {
454 scanInProgressFuse = 0;
455 if (self.isAttached() &&
456 mCanScan &&
457 !mConfigServer.wsClientsActive() &&
458 mWifiStorage.isCredentialsValid()) {
459 for (auto i = 0; i < n; ++i) {
460 if (WiFi.SSID(i) == mWifiStorage.getSsid()) {
461 UNIOT_LOG_INFO("Network [%s] is available", WiFi.SSID(i).c_str());
463 break;
464 }
465 }
466 } else {
467 UNIOT_LOG_INFO("Scan done, skipping availability check");
468 }
469 WiFi.scanDelete();
470 });
471 }
472 });
473 }
474
482 void _initServerCallbacks() {
483 auto server = mConfigServer.get();
484 if (server) {
485 server->onNotFound([](AsyncWebServerRequest *request) {
486 // auto response = request->beginResponse(307);
487 // response->addHeader("Location", "/");
488 // request->send(response);
489 request->redirect("http://uniot.local/");
490 });
491
492 server->on("/", [this](AsyncWebServerRequest *request) {
493 auto response = request->beginResponse(200, "text/html", CONFIG_MIN_HTML_GZ, CONFIG_MIN_HTML_GZ_LENGTH, nullptr);
494 response->addHeader("Content-Encoding", "gzip");
495 request->send(response);
496 });
497 }
498 }
499
512 void _handleWebSocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
513 switch (type) {
514 case WS_EVT_CONNECT:
515 // mAppState.Network.WebSocketsClients = mWebSocket.count();
516 UNIOT_LOG_INFO("WebSocket client #%u connected from %s", client->id(), client->remoteIP().toString().c_str());
517 break;
518 case WS_EVT_DISCONNECT:
519 // mAppState.Network.WebSocketsClients = mWebSocket.count();
520 UNIOT_LOG_INFO("WebSocket client #%u disconnected", client->id());
521 break;
522 case WS_EVT_DATA:
523 _handleWebSocketMessage(client->id(), arg, data, len);
524 break;
525 default:
526 break;
527 }
528 }
529
541 void _handleWebSocketMessage(uint32_t clientId, void *arg, uint8_t *data, size_t len) {
542 auto *info = (AwsFrameInfo *)arg;
543 if (info->opcode == WS_BINARY) {
544 CBORObject msg(Bytes(data, len));
545 if (!msg.hasError()) {
546 auto action = msg.getInt("action");
547 if (action != ACTIONS::INVALID) {
548 switch (action) {
549 case ACTIONS::STATUS: {
550 String status;
551 JSON::Object(status)
552 .put("id", mpCredentials->getDeviceId())
553 .put("acc", mpCredentials->getOwnerId())
554 .put("nets", mLastNetworks.length() ? mLastNetworks : "[]", false)
555 .put("homeNet", WiFi.isConnected() ? WiFi.SSID() : "")
556 .close();
557 mConfigServer.wsTextAll(status);
558 break;
559 }
560 case ACTIONS::SAVE: {
561 mWifiStorage.setCredentials(msg.getString("ssid"), msg.getString("pass"));
562 if (mWifiStorage.isCredentialsValid()) {
563 mTaskConnectSta->once(500);
564 mpCredentials->setOwnerId(msg.getString("acc"));
565 UNIOT_LOG_DEBUG("Is owner changed: %d", mpCredentials->isOwnerChanged());
566 }
567 break;
568 }
569 case ACTIONS::SCAN: {
570 mTaskScan->once(1000);
571 break;
572 }
573 case ACTIONS::ASK: {
574 if (mLastSaveResult > -1) {
575 String success;
576 JSON::Object(success)
577 .put("success", mLastSaveResult)
578 .close();
579 mConfigServer.wsText(clientId, success);
580 }
581 break;
582 }
583 default:
584 break;
585 }
586 } else {
587 UNIOT_LOG_WARN("WebSocket message is not a valid action");
588 }
589 } else {
590 UNIOT_LOG_WARN("WebSocket message is not a valid CBOR");
591 }
592 }
593 }
594
603 bool _tryToRecoverAp() {
604 if (mApEnabled) {
605 // mTaskStop->detach();
606 mTaskStart->once(100);
607 mTaskStopAp->detach();
608 mConfigServer.wsEnable(true);
609 mTaskAvailabilityCheck->attach(10000);
610 return true;
611 }
612 return false;
613 }
614
615 Credentials *mpCredentials;
616 WifiStorage mWifiStorage;
617
618 String mApName;
619 IPAddress mApSubnet;
620 ConfigCaptivePortal mConfigServer;
621
622 String mLastNetworks;
623 int8_t mLastSaveResult;
624 bool mCanScan;
625 bool mApEnabled;
626
627 // Task pointers for all network operations
628 TaskScheduler::TaskPtr mTaskStart;
629 TaskScheduler::TaskPtr mTaskServe;
630 TaskScheduler::TaskPtr mTaskStop;
631 TaskScheduler::TaskPtr mTaskConfigAp;
632 TaskScheduler::TaskPtr mTaskStopAp;
633 TaskScheduler::TaskPtr mTaskConnectSta;
634 TaskScheduler::TaskPtr mTaskConnecting;
635 TaskScheduler::TaskPtr mTaskMonitoring;
636 TaskScheduler::TaskPtr mTaskScan;
637 TaskScheduler::TaskPtr mTaskAvailabilityCheck;
638
639#if defined(ESP32)
640 ESP32WifiScan mWifiScan;
641#elif defined(ESP8266)
642 ESP8266WifiScan mWifiScan;
643#endif
644};
645} // namespace uniot
646
Captive portal implementation for device configuration.
Network event definitions for the Uniot event system.
Platform-specific WiFi network scanning utilities.
void wsEnable(bool enable)
Enable or disable WebSocket functionality.
Definition ConfigCaptivePortal.h:279
bool start()
Start the captive portal services.
Definition ConfigCaptivePortal.h:159
Manages device identity and cryptographic credentials for Uniot devices.
Definition Credentials.h:61
uint32_t getShortDeviceId() const
Gets a shorter unique identifier for the device.
Definition Credentials.h:176
void emitEvent(unsigned int topic, int msg)
bool sendDataToChannel(T_topic channel, T_data data)
Sends data to a specific channel on all connected EventBus instances.
Definition EventEntity.h:98
Interface for connecting components to the TaskScheduler.
Definition ISchedulerConnectionKit.h:35
NetworkScheduler(Credentials &credentials)
Construct a new NetworkScheduler.
Definition NetworkScheduler.h:112
bool reconnect()
Attempt to reconnect using stored credentials.
Definition NetworkScheduler.h:211
void config()
Start or recover configuration mode.
Definition NetworkScheduler.h:183
virtual void attach() override
Attach the network scheduler and start initial connection.
Definition NetworkScheduler.h:167
void forget()
Forget stored WiFi credentials and enter configuration mode.
Definition NetworkScheduler.h:197
virtual void pushTo(TaskScheduler &scheduler) override
Push all network tasks to the scheduler.
Definition NetworkScheduler.h:144
bool setCredentials(const String &ssid, const String &password)
Set and store new WiFi credentials.
Definition NetworkScheduler.h:234
Definition TaskScheduler.h:164
EventEmitter< unsigned int, int, Bytes > CoreEventEmitter
A specialized EventEmitter for core system events.
Definition EventEmitter.h:78
#define UNIOT_LOG_INFO(...)
Log an INFO level message Used for general information about system operation. Only compiled if UNIOT...
Definition Logger.h:268
#define UNIOT_LOG_WARN(...)
Log an WARN level message Used for warnings about potentially problematic situations....
Definition Logger.h:247
#define UNIOT_LOG_DEBUG(...)
Log an DEBUG level message Used for general information about system operation. Only compiled if UNIO...
Definition Logger.h:293
SharedPointer< SchedulerTask > TaskPtr
Shared pointer type for scheduler tasks.
Definition TaskScheduler.h:171
static TaskPtr make(SchedulerTask::SchedulerTaskCallback callback)
Static factory method to create a task with a callback.
Definition TaskScheduler.h:193
TaskScheduler & push(const char *name, TaskPtr task)
Add a named task to the scheduler.
Definition TaskScheduler.h:214
@ DISCONNECTING
Currently disconnecting from network.
Definition NetworkEvents.h:88
@ SUCCESS
Network operation completed successfully.
Definition NetworkEvents.h:86
@ ACCESS_POINT
Device is operating in access point mode.
Definition NetworkEvents.h:90
@ AVAILABLE
Configured network is available for connection.
Definition NetworkEvents.h:91
@ DISCONNECTED
Network connection has been lost or terminated.
Definition NetworkEvents.h:89
@ FAILED
Network operation failed (connection, scan, etc.)
Definition NetworkEvents.h:85
@ CONNECTING
Currently attempting to connect to network.
Definition NetworkEvents.h:87
@ CONNECTION
WiFi connection state changes and operations.
Definition NetworkEvents.h:74
@ OUT_SSID
Channel for broadcasting current SSID information.
Definition NetworkEvents.h:63
Contains all classes and functions related to the Uniot Core.