NanoLux (Device) 3.0
Codebase for the open-source AudioLux device.
Loading...
Searching...
No Matches
webServer.h
1#pragma once
2
3#include <ESPmDNS.h>
4#include "BluetoothSerial.h"
5#include <WiFi.h>
6#include <AsyncTCP.h>
7#include <AsyncJson.h>
8#include <ESPAsyncWebServer.h>
9#include <WebHandlerImpl.h>
10#include <WebResponseImpl.h>
11#include "esp_wifi.h"
12#include "FS.h"
13#include "SD.h"
14#include "SPI.h"
15
16
17//#define DEBUG_PRINTF(...) Serial.printf(__VA_ARGS__)
18#define DEBUG_PRINTF(...)
19
20#define ALWAYS_PRINTF(...) Serial.printf(__VA_ARGS__)
21
22// SD card pinouts
23#define SCK 5
24#define MISO 19
25#define MOSI 18
26#define CS 21
27
28/*
29 * WIFI management data.
30 */
31constexpr int MAX_WIFI_CONNECT_WAIT = 100;
32constexpr int MAX_NETWORKS = 15;
33constexpr int END_OF_DATA = 9999;
34typedef struct {
35 String SSID;
36 int32_t RSSI;
37 uint8_t EncryptionType;
39WiFiNetwork available_networks[MAX_NETWORKS];
40
41typedef struct {
42 String SSID;
43 String Key;
45static CurrentWifi current_wifi;
46static CurrentWifi candidate_wifi;
47extern Config_Data config; // Currently loaded config
48
49/*
50 * Artifacts used to manage the async WiFi join process.
51 */
53 wl_status_t status;
54 const char* description;
55};
56
57const WlStatusToString wl_status_to_string[] = {
58 { WL_NO_SHIELD, "WiFi shield not present" },
59 { WL_IDLE_STATUS, "WiFi is in idle state" },
60 { WL_NO_SSID_AVAIL, "Configured SSID cannot be found" },
61 { WL_SCAN_COMPLETED, "Scan completed" },
62 { WL_CONNECTED, "Connected to network" },
63 { WL_CONNECT_FAILED, "Connection failed" },
64 { WL_CONNECTION_LOST, "Connection lost" },
65 { WL_DISCONNECTED, "Disconnected from network" }
66};
67
68const char* status_description;
69
70TimerHandle_t join_timer;
71SemaphoreHandle_t join_status_mutex;
72
73bool join_in_progress = false;
74bool join_succeeded = false;
75
76
77/*
78 * Networking params
79 */
80const char* ap_ssid = "AUDIOLUX";
81const char* ap_password = "12345678";
82
83const char* DEFAULT_HOSTNAME = "audiolux";
84static String hostname;
85
86
87
88/*
89 * Settings
90 */
91const char* SETTINGS_FILE = "/settings.json";
92static StaticJsonDocument<384> settings;
93const char* EMPTY_SETTING = "#_None_#";
94volatile bool dirty_settings = false;
95
96
97
98/*
99 * Web Server related.
100 */
101constexpr int HTTP_OK = 200;
102constexpr int HTTP_ACCEPTED = 202;
103constexpr int HTTP_BAD_REQUEST = 400;
104constexpr int HTTP_UNAUTHORIZED = 401;
105constexpr int HTTP_METHOD_NOT_ALLOWED = 405;
106constexpr int HTTP_UNPROCESSABLE = 422;
107constexpr int HTTP_INTERNAL_ERROR = 500;
108constexpr int HTTP_UNAVAILABLE = 503;
109
110const char* CONTENT_JSON = "application/json";
111const char* CONTENT_TEXT = "text/plain";
112
113static String http_response;
114const char* URL_FILE = "/assets/url.json";
115volatile bool server_unavailable = false;
116
117/*
118 * Web Server API handlers
119 */
120typedef struct {
121 String path;
122 ArRequestHandlerFunction handler;
123} APIGetHook;
124
125typedef struct {
126 String path;
127 ArJsonRequestHandlerFunction request_handler;
128} APIPutHook;
129
130
131AsyncWebServer webServer(80);
132
133/*
134 * File system
135 */
136inline void initialize_file_system() {
137
138 DEBUG_PRINTF("Initializing SD FS...");
139 SPI.begin(SCK, MISO, MOSI, CS);
140 if (!SD.begin(CS))
141 DEBUG_PRINTF("Card Mount Failed");
142 DEBUG_PRINTF("Card mount successful.");
143}
144
145/*
146 * Settings Management
147 */
148inline void save_settings() {
149 if (!settings.containsKey("wifi")) {
150 settings.createNestedObject("wifi");
151 }
152 settings["hostname"] = hostname;
153 settings["wifi"]["ssid"] = current_wifi.SSID;
154 settings["wifi"]["key"] = current_wifi.Key;
155
156 File saved_settings = SD.open(SETTINGS_FILE, "w");
157
158 if (saved_settings) {
159 serializeJson(settings, saved_settings);
160 DEBUG_PRINTF("Saving settings:\n");
161 serializeJsonPretty(settings, Serial);
162 DEBUG_PRINTF("\n");
163 } else {
164 DEBUG_PRINTF("Unable to save settings file.\n");
165 }
166}
167
168
169inline void load_settings() {
170 DEBUG_PRINTF("Checking if settings are available.\n");
171
172 File saved_settings = SD.open(SETTINGS_FILE, "r");
173
174 if (saved_settings) {
175 const DeserializationError error = deserializeJson(settings, saved_settings);
176 if (!error) {
177 DEBUG_PRINTF("Settings loaded:\n");
178 serializeJsonPretty(settings, Serial);
179 DEBUG_PRINTF("\n");
180
181 hostname = settings["hostname"].as<String>();
182 current_wifi.SSID = settings["wifi"]["ssid"].as<String>();
183 current_wifi.Key = settings["wifi"]["key"].as<String>();
184
185 return;
186 }
187
188 DEBUG_PRINTF("Error loading saved file: %s\n", error.c_str());
189 }
190
191 DEBUG_PRINTF("Unable to load settings. Will use defaults (re)create file.\n");
192
193 current_wifi.SSID = EMPTY_SETTING;
194 current_wifi.Key = EMPTY_SETTING;
195 hostname = DEFAULT_HOSTNAME;
196 save_settings();
197}
198
199
200inline const String& build_response(const bool success, const char* message, const char* details) {
201 http_response = String("{\"success\": ") + (success ? "true" : "false");
202 if (message != nullptr) {
203 http_response += String(", \"message\": \"") + message + String("\"");
204 }
205 if (details != nullptr) {
206 http_response += String(", \"details\": \"") + details + String("\"");
207 }
208 http_response += "}";
209
210 return http_response;
211}
212
213
214/*
215 * Network configuration
216 */
217inline void scan_ssids() {
218 static int long_scan_count = 0;
219 const int MAX_SCAN_ITERATIONS = 2;
220
221 // Flow: check if there was a scan happening, and get its results.
222 // If there was no scan start one and be done. If the previous scan
223 // failed, start a new one and move on. If the previous scan succeeded
224 // then stash the results and start a new one. The main consequence here
225 // is that the very first time the scan is run, the result will be
226 // and empty array. It is up to the client to handle that.
227
228 // Start with the assumption we have an empty scan.
229 available_networks[0].RSSI = END_OF_DATA;
230 const int n = WiFi.scanComplete();
231 if (n == WIFI_SCAN_FAILED) {
232 WiFi.scanNetworks(true);
233 long_scan_count = 0;
234 } else if (n == WIFI_SCAN_RUNNING) {
235 long_scan_count++;
236
237 if (long_scan_count >= MAX_SCAN_ITERATIONS) {
238 // This scan has run for a while. Cancel it and start a new one.
239 WiFi.scanDelete();
240 WiFi.scanNetworks(true);
241 long_scan_count = 0;
242 }
243 } else if (n == 0) {
244 long_scan_count = 0;
245 } else if (n > 0) {
246 const int network_count = min(n, MAX_NETWORKS);
247 for (int i = 0; i < network_count; ++i) {
248 available_networks[i].SSID = String(WiFi.SSID(i));
249 available_networks[i].RSSI = WiFi.RSSI(i);
250 available_networks[i].EncryptionType = WiFi.encryptionType(i);
251 }
252
253 if (network_count < MAX_NETWORKS) {
254 available_networks[network_count].RSSI = END_OF_DATA;
255 }
256
257 WiFi.scanDelete();
258 if (WiFi.scanComplete() == WIFI_SCAN_FAILED) {
259 WiFi.scanNetworks(true);
260 }
261 long_scan_count = 0;
262 }
263}
264
265
266inline bool initialize_wifi_connection() {
267 server_unavailable = true;
268
269 // Stop any pending WiFi scans.
270 WiFi.scanDelete();
271
272 // Drop the current connection, if any.
273 WiFi.disconnect();
274 delay(100);
275
276 WiFi.begin(current_wifi.SSID.c_str(), current_wifi.Key.c_str());
277 int wait_count = 0;
278 while (WiFi.status() != WL_CONNECTED && wait_count < MAX_WIFI_CONNECT_WAIT) {
279 delay(500);
280 ++wait_count;
281 }
282 server_unavailable = false;
283
284 if (WiFi.status() == WL_CONNECTED) {
285 return true;
286 }
287
288 WiFi.disconnect();
289 delay(100);
290 return false;
291}
292
293
294inline void initialize_mdns(bool use_user_hostname) {
295 bool success = MDNS.begin(
296 (use_user_hostname ? hostname : DEFAULT_HOSTNAME).c_str());
297
298 // The assumption is that we are connected. Setup mDNS
299 if (success) {
300 ALWAYS_PRINTF("mDNS connected. The AudioLux can be reached at %s.local\n", hostname.c_str());
301 } else {
302 ALWAYS_PRINTF("Unable to setup mDNS\n");
303 }
304}
305
306
307inline const char* get_status_description(const wl_status_t status) {
308 for (int i = 0; i < sizeof(wl_status_to_string) / sizeof(WlStatusToString); i++) {
309 if (wl_status_to_string[i].status == status) {
310 status_description = wl_status_to_string[i].description;
311 break;
312 }
313 }
314
315 return status_description;
316}
317
318
319inline void on_join_timer(TimerHandle_t timer) {
320 DEBUG_PRINTF("Timer: Checking WiFi Join Status.\n");
321 if (xSemaphoreTake(join_status_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
322 if (join_in_progress) {
323 wl_status_t status = WiFi.status();
324 if (status == WL_CONNECTED) {
325 join_in_progress = false;
326 join_succeeded = true;
327 xTimerStop(timer, 0);
328 ALWAYS_PRINTF("Timer: WiFi join succeeded.\n");
329
330 // Queue the settings for saving. Can't do it here
331 // because FreeRTOS croaks with a stack overflow.
332 // Possibly writing to flash is resource-heavy.
333 current_wifi.SSID = candidate_wifi.SSID;
334 current_wifi.Key = candidate_wifi.Key;
335 dirty_settings = true;
336
337 initialize_mdns(true);
338 } else if (status != WL_IDLE_STATUS && status != WL_CONNECT_FAILED && status != WL_NO_SHIELD) {
339 join_in_progress = false;
340 join_succeeded = false;
341 xTimerStop(timer, 0);
342 DEBUG_PRINTF("Timer: WiFi join failed. Reason: %s.\n", get_status_description(status));
343 }
344 }
345
346 xSemaphoreGive(join_status_mutex);
347 }
348}
349
350
351/*
352 * Wifi management
353 */
354inline bool join_wifi(const char* ssid, const char* key) {
355 DEBUG_PRINTF("Trying to join network %s ...\n", ssid);
356
357 // Reset any radio activity.
358 WiFi.scanDelete();
359 WiFi.disconnect();
360 delay(100);
361 WiFi.setHostname(hostname.c_str());
362
363 candidate_wifi.SSID = ssid;
364 candidate_wifi.Key = key;
365
366 // Start the connection process.
367 WiFi.begin(ssid, key);
368 if (xSemaphoreTake(join_status_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
369 join_in_progress = true;
370 join_succeeded = false;
371
372 if (xTimerStart(join_timer, 0) != pdPASS) {
373 DEBUG_PRINTF("Unable to star timer. Join unsupervised.\n");
374 return false;
375 }
376
377 xSemaphoreGive(join_status_mutex);
378 return true;
379 }
380
381 DEBUG_PRINTF("Unable to get mutex. Join unsupervised.\n");
382 return false;
383}
384
385
386/*
387 * Wifi API handling
388 */
389inline void serve_wifi_list(AsyncWebServerRequest* request) {
390 if (join_in_progress) {
391 request->send(HTTP_UNAVAILABLE);
392 return;
393 }
394
395 scan_ssids();
396
397 StaticJsonDocument<1024> json_list;
398 int wifi_number = 0;
399 while (wifi_number < MAX_NETWORKS && available_networks[wifi_number].RSSI != END_OF_DATA) {
400 JsonObject wifi = json_list.createNestedObject();
401 wifi["ssid"] = available_networks[wifi_number].SSID;
402 wifi["rssi"] = available_networks[wifi_number].RSSI;
403 wifi["lock"] = available_networks[wifi_number].EncryptionType != WIFI_AUTH_OPEN;
404 wifi_number++;
405 }
406
407 // If no results, return an empty array.
408 String wifi_list;
409 serializeJson(json_list, wifi_list);
410 if (wifi_list == "null") {
411 wifi_list = "[]";
412 }
413
414 DEBUG_PRINTF("Sending networks:\n%s\\n", wifi_list.c_str());
415 request->send(HTTP_OK, CONTENT_JSON, wifi_list);
416}
417
418
419inline void handle_wifi_put_request(AsyncWebServerRequest* request, JsonVariant& json) {
420 if (request->method() == HTTP_PUT) {
421 const JsonObject& payload = json.as<JsonObject>();
422
423 int status = HTTP_OK;
424
425 bool joined = false;
426 if (payload["ssid"] == nullptr) {
427 DEBUG_PRINTF("/api/wifi: Forgetting current network.\n");
428 WiFi.disconnect();
429 delay(100);
430
431 current_wifi.SSID = EMPTY_SETTING;
432 current_wifi.Key = EMPTY_SETTING;
433 save_settings();
434
435 joined = true;
436 } else {
437 DEBUG_PRINTF("/api/wifi: Joining network.\n");
438 joined = join_wifi(payload["ssid"], payload["key"]);
439 }
440
441 int response_status;
442 String message;
443 if (joined) {
444 response_status = HTTP_ACCEPTED;
445 message = "Operation completed.";
446 } else {
447 response_status = HTTP_INTERNAL_ERROR;
448 message = "Unable to monitor join operation: could not start timer or get mutex.";
449 DEBUG_PRINTF("%s\n", message.c_str());
450 }
451 request->send(response_status, CONTENT_JSON, build_response(joined, message.c_str(), nullptr));
452 } else {
453 request->send(HTTP_METHOD_NOT_ALLOWED);
454 }
455}
456
457
458inline void handle_wifi_get_request(AsyncWebServerRequest* request) {
459 const String wifi = current_wifi.SSID == EMPTY_SETTING ? String("null") : String("\"") + String(current_wifi.SSID) + String("\"");
460 const bool connected = current_wifi.SSID == EMPTY_SETTING ? false : (WiFi.status() == WL_CONNECTED);
461 const String response = String("{ \"ssid\": ") + String(wifi) + String(", \"connected\": ") + (connected ? "true" : "false") + String(" }");
462
463 DEBUG_PRINTF("Sending current wifi: %s\n", response.c_str());
464 request->send(HTTP_OK, CONTENT_JSON, response);
465}
466
467inline void handle_wifi_status_request(AsyncWebServerRequest* request) {
468 const String wifi = current_wifi.SSID == EMPTY_SETTING ? String("null") : String("\"") + String(current_wifi.SSID) + String("\"");
469
470 String status;
471 if (xSemaphoreTake(join_status_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
472 if (join_in_progress) {
473 status = "pending";
474 } else if (join_succeeded) {
475 status = "success";
476 } else {
477 status = "fail";
478 }
479
480 xSemaphoreGive(join_status_mutex);
481 }
482
483 const String response = String("{ \"ssid\": ") + String(wifi) + String(", \"status\": \"") + status + String("\" }");
484
485 DEBUG_PRINTF("Sending wifi status: %s\n", response.c_str());
486 request->send(HTTP_OK, CONTENT_JSON, response);
487}
488
489
490/*
491 * Hostname API handling
492 */
493inline void handle_hostname_put_request(AsyncWebServerRequest* request, JsonVariant& json) {
494 if (server_unavailable) {
495 request->send(HTTP_UNAVAILABLE);
496 return;
497 }
498
499 if (request->method() == HTTP_PUT) {
500 const JsonObject& payload = json.as<JsonObject>();
501
502 int status = HTTP_OK;
503
504 hostname = payload["hostname"].as<String>();
505 save_settings();
506 DEBUG_PRINTF("Hostname %s saved.\n", hostname.c_str());
507 request->send(HTTP_OK,
508 CONTENT_TEXT,
509 build_response(true, "New hostname saved.", nullptr));
510 }
511}
512
513inline void handle_hostname_get_request(AsyncWebServerRequest* request) {
514 if (server_unavailable) {
515 request->send(HTTP_UNAVAILABLE);
516 return;
517 }
518
519 const String response = String("{ \"hostname\": ") + "\"" + String(hostname) + String("\" }");
520
521 DEBUG_PRINTF("Sending current hostname: %s\n", response.c_str());
522 request->send(HTTP_OK, CONTENT_JSON, response);
523}
524
525
526/*
527 * Health ping.
528 */
529inline void handle_health_check(AsyncWebServerRequest* request) {
530 if (server_unavailable) {
531 request->send(HTTP_UNAVAILABLE);
532 return;
533 }
534
535 DEBUG_PRINTF("Pong.\n");
536 request->send(HTTP_OK);
537}
538
539
540/*
541 * Unknown path (404) handler
542 */
543inline void handle_unknown_url(AsyncWebServerRequest* request) {
544 // If browser sends preflight to check for CORS we tell them
545 // it's okay. NOTE: Google is stubborn about it. You will need
546 // to disable strict CORS checking using the --disable-web-security
547 // option when starting it.
548 if (request->method() == HTTP_OPTIONS) {
549 request->send(200);
550 return;
551 }
552
553 // Otherwise, we got an unknown request. Print info about it
554 // that may be useful for debugging.
555 String method;
556 switch (request->method()) {
557 case HTTP_GET:
558 method = "GET";
559 break;
560 case HTTP_POST:
561 method = "POST";
562 break;
563 case HTTP_PUT:
564 method = "PUT";
565 break;
566 default:
567 method = "UNKNOWN";
568 }
569 DEBUG_PRINTF("Not Found: %s -> http://%s%s\n", method.c_str(), request->host().c_str(), request->url().c_str());
570
571 if (request->contentLength()) {
572 DEBUG_PRINTF("_CONTENT_TYPE: %s\n", request->contentType().c_str());
573 DEBUG_PRINTF("_CONTENT_LENGTH: %u\n", request->contentLength());
574 }
575
576 const int headers = request->headers();
577 for (int i = 0; i < headers; i++) {
578 const AsyncWebHeader* header = request->getHeader(i);
579 DEBUG_PRINTF("_HEADER[%s]: %s\n", header->name().c_str(), header->value().c_str());
580 }
581
582 request->send(404);
583}
584
585
586inline void save_url(const String& url) {
587 // This is a static file that lives in the "assets" of the web app.
588 // When it starts, it will check this file to know which URL to use to talk
589 // back to the server. The reason we need this is that we don't know which
590 // route is available (AP or STA) until runtime.
591
592 File saved_url = SD.open(URL_FILE, "w");
593
594 if (saved_url) {
595 StaticJsonDocument<192> data;
596
597 data["url"] = url;
598 Serial.println(url);
599
600 serializeJson(data, saved_url);
601 DEBUG_PRINTF("%s saved as Web App URL.\n", url.c_str());
602 } else {
603 DEBUG_PRINTF("Unable to save Web App URL, will default to http://192.168.4.1.\n");
604 }
605}
606
607
608inline void setup_networking(const char* password) {
609 initialize_file_system();
610
611 // Load saved settings. If we have an SSID, try to join the network.
612 load_settings();
613
614 // Prevent he radio from going to sleep.
615 WiFi.setSleep(false);
616
617 // Local WiFi connection depends on whether it has been configured
618 // by the user.
619 bool wifi_okay = false;
620 if (current_wifi.SSID != nullptr && current_wifi.SSID != EMPTY_SETTING) {
621 DEBUG_PRINTF("Attempting to connect to saved WiFi: %s\n", current_wifi.SSID.c_str());
622 wifi_okay = initialize_wifi_connection();
623 if (wifi_okay) {
624 ALWAYS_PRINTF("WiFi IP: %s\n", WiFi.localIP().toString().c_str());
625 }
626 } else {
627 ALWAYS_PRINTF("****\n");
628 ALWAYS_PRINTF("No wifi saved. AudioLux available via Access Point:\n");
629 ALWAYS_PRINTF("SSID: %s Password: %s\n", ap_ssid, password);
630 ALWAYS_PRINTF("****\n");
631 }
632
633 // AP mode is always active.
634 WiFi.mode(WIFI_MODE_APSTA);
635 if (password[0] == '\0') {
636 WiFi.softAP("AudioluxUnsecured");
637 ALWAYS_PRINTF("WIFI IS UNSECURED!!!\n");
638 initialize_mdns(false);
639 } else {
640 WiFi.softAP(hostname, password);
641 initialize_mdns(true);
642 }
643
644
645
646 delay(1000);
647 const IPAddress ap_ip = WiFi.softAPIP();
648
649 // Set up the URL that the Web App needs to talk to.
650 // We prefer user's network if available.
651 String api_url = "http://";
652 if (wifi_okay) {
653 api_url += hostname;
654 api_url += ".local";
655 } else {
656 api_url += ap_ip.toString();
657 }
658 save_url(api_url);
659 ALWAYS_PRINTF("Backend availbale at: %s", api_url.c_str());
660}
661
662
663inline void initialize_web_server(const APIGetHook api_get_hooks[], const int get_hook_count, APIPutHook api_put_hooks[], const int put_hook_count, const char* password) {
664 // Mutex to make join status globals thread safe.
665 // The timer below runs on a different context than the web server,
666 // so we need to properly marshall access between contexts.
667 join_status_mutex = xSemaphoreCreateMutex();
668 if (join_status_mutex == nullptr) {
669 // If we get here we are in serious trouble, and there is nothing the
670 // code can do. We just die unceremoniously.
671 DEBUG_PRINTF("WebServer: failed to create mutex. Process halted.\n");
672 for (;;) {
673 delay(1000);
674 }
675 }
676
677 // Software timer to monitor async WiFi joins.
678 join_timer = xTimerCreate(
679 "WiFiJoinTimer",
680 pdMS_TO_TICKS(200),
681 pdTRUE, // Auto re-trigger.
682 nullptr, // Timer ID pointer, not used.
683 on_join_timer);
684
685 setup_networking(password);
686
687 // Register the main process API handlers.
688 DEBUG_PRINTF("Registering main APIs.\n");
689 for (int i = 0; i < get_hook_count; i++) {
690 DEBUG_PRINTF("%s\n", api_get_hooks[i].path.c_str());
691 webServer.on(api_get_hooks[i].path.c_str(), HTTP_GET, api_get_hooks[i].handler);
692 }
693 for (int i = 0; i < put_hook_count; i++) {
694 DEBUG_PRINTF("%s\n", api_put_hooks[i].path.c_str());
695 webServer.addHandler(new AsyncCallbackJsonWebHandler(api_put_hooks[i].path.c_str(), api_put_hooks[i].request_handler));
696 }
697
698 // Now add internal APi endpoints (wifi and health)
699 webServer.on("/api/wifis", HTTP_GET, serve_wifi_list);
700 webServer.on("/api/wifi", HTTP_GET, handle_wifi_get_request);
701 webServer.on("/api/wifi_status", HTTP_GET, handle_wifi_status_request);
702 webServer.on("/api/hostname", HTTP_GET, handle_hostname_get_request);
703 webServer.on("/api/health", HTTP_GET, handle_health_check);
704
705 webServer.addHandler(new AsyncCallbackJsonWebHandler("/api/wifi", handle_wifi_put_request));
706 webServer.addHandler(new AsyncCallbackJsonWebHandler("/api/hostname", handle_hostname_put_request));
707
708
709 // Register the Web App
710 DEBUG_PRINTF("Registering Web App files.\n");
711
712 webServer.serveStatic("/", SD, "/").setDefaultFile("index.html");
713
714 webServer.onNotFound(handle_unknown_url);
715
716 // "Disable" CORS.
717 DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
718 DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "*");
719 DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "*");
720 esp_wifi_set_ps(WIFI_PS_NONE);
721 webServer.begin();
722 MDNS.addService("http", "tcp", 80);
723}
Config_Data config
The currently-loaded device config.
Definition main.ino:60
Definition webServer.h:120
Definition webServer.h:125
A structure holding system configuration data.
Definition storage.h:66
Definition webServer.h:41
Definition webServer.h:34
Definition webServer.h:52