4#include "BluetoothSerial.h"
8#include <ESPAsyncWebServer.h>
9#include <WebHandlerImpl.h>
10#include <WebResponseImpl.h>
15#define DEBUG_PRINTF(...)
17#define ALWAYS_PRINTF(...) Serial.printf(__VA_ARGS__)
37constexpr int MAX_WIFI_CONNECT_WAIT = 100;
38constexpr int MAX_NETWORKS = 15;
39constexpr int END_OF_DATA = 9999;
43 uint8_t EncryptionType;
60 const char* description;
64 { WL_NO_SHIELD,
"WiFi shield not present" },
65 { WL_IDLE_STATUS,
"WiFi is in idle state" },
66 { WL_NO_SSID_AVAIL,
"Configured SSID cannot be found" },
67 { WL_SCAN_COMPLETED,
"Scan completed" },
68 { WL_CONNECTED,
"Connected to network" },
69 { WL_CONNECT_FAILED,
"Connection failed" },
70 { WL_CONNECTION_LOST,
"Connection lost" },
71 { WL_DISCONNECTED,
"Disconnected from network" }
74const char* status_description;
76TimerHandle_t join_timer;
77SemaphoreHandle_t join_status_mutex;
79bool join_in_progress =
false;
80bool join_succeeded =
false;
86const char* ap_ssid =
"AUDIOLUX";
87const char* ap_password =
"12345678";
89const char* DEFAULT_HOSTNAME =
"audiolux";
90static String hostname;
97const char* SETTINGS_FILE =
"/settings.json";
98static StaticJsonDocument<384> settings;
99const char* EMPTY_SETTING =
"#_None_#";
100volatile bool dirty_settings =
false;
107constexpr int HTTP_OK = 200;
108constexpr int HTTP_ACCEPTED = 202;
109constexpr int HTTP_BAD_REQUEST = 400;
110constexpr int HTTP_UNAUTHORIZED = 401;
111constexpr int HTTP_METHOD_NOT_ALLOWED = 405;
112constexpr int HTTP_UNPROCESSABLE = 422;
113constexpr int HTTP_INTERNAL_ERROR = 500;
114constexpr int HTTP_UNAVAILABLE = 503;
116const char* CONTENT_JSON =
"application/json";
117const char* CONTENT_TEXT =
"text/plain";
119static String http_response;
120const char* URL_FILE =
"/assets/url.json";
121volatile bool server_unavailable =
false;
128 ArRequestHandlerFunction handler;
133 ArJsonRequestHandlerFunction request_handler;
137AsyncWebServer webServer(80);
142inline void initialize_file_system() {
145 DEBUG_PRINTF(
"Initializing SD FS...");
146 SPI.begin(SCK, MISO, MOSI, CS);
148 DEBUG_PRINTF(
"Card Mount Failed");
149 DEBUG_PRINTF(
"Card mount successful.");
151 DEBUG_PRINTF(
"Initializing FS...");
152 if (LittleFS.begin()) {
153 DEBUG_PRINTF(
"done.\n");
155 DEBUG_PRINTF(
"fail.\n");
163inline void save_settings() {
164 if (!settings.containsKey(
"wifi")) {
165 settings.createNestedObject(
"wifi");
167 settings[
"hostname"] = hostname;
168 settings[
"wifi"][
"ssid"] = current_wifi.SSID;
169 settings[
"wifi"][
"key"] = current_wifi.Key;
172 File saved_settings = SD.open(SETTINGS_FILE,
"w");
174 File saved_settings = LittleFS.open(SETTINGS_FILE,
"w");
177 if (saved_settings) {
178 serializeJson(settings, saved_settings);
179 DEBUG_PRINTF(
"Saving settings:\n");
180 serializeJsonPretty(settings, Serial);
183 DEBUG_PRINTF(
"Unable to save settings file.\n");
188inline void load_settings() {
189 DEBUG_PRINTF(
"Checking if settings are available.\n");
192 File saved_settings = SD.open(SETTINGS_FILE,
"r");
194 File saved_settings = LittleFS.open(SETTINGS_FILE,
"r");
197 if (saved_settings) {
198 const DeserializationError error = deserializeJson(settings, saved_settings);
200 DEBUG_PRINTF(
"Settings loaded:\n");
201 serializeJsonPretty(settings, Serial);
204 hostname = settings[
"hostname"].as<String>();
205 current_wifi.SSID = settings[
"wifi"][
"ssid"].as<String>();
206 current_wifi.Key = settings[
"wifi"][
"key"].as<String>();
211 DEBUG_PRINTF(
"Error loading saved file: %s\n", error.c_str());
214 DEBUG_PRINTF(
"Unable to load settings. Will use defaults (re)create file.\n");
216 current_wifi.SSID = EMPTY_SETTING;
217 current_wifi.Key = EMPTY_SETTING;
218 hostname = DEFAULT_HOSTNAME;
223inline const String& build_response(
const bool success,
const char* message,
const char* details) {
224 http_response = String(
"{\"success\": ") + (success ?
"true" :
"false");
225 if (message !=
nullptr) {
226 http_response += String(
", \"message\": \"") + message + String(
"\"");
228 if (details !=
nullptr) {
229 http_response += String(
", \"details\": \"") + details + String(
"\"");
231 http_response +=
"}";
233 return http_response;
240inline void scan_ssids() {
241 static int long_scan_count = 0;
242 const int MAX_SCAN_ITERATIONS = 2;
252 available_networks[0].RSSI = END_OF_DATA;
253 const int n = WiFi.scanComplete();
254 if (n == WIFI_SCAN_FAILED) {
255 WiFi.scanNetworks(
true);
257 }
else if (n == WIFI_SCAN_RUNNING) {
260 if (long_scan_count >= MAX_SCAN_ITERATIONS) {
263 WiFi.scanNetworks(
true);
269 const int network_count = min(n, MAX_NETWORKS);
270 for (
int i = 0; i < network_count; ++i) {
271 available_networks[i].SSID = String(WiFi.SSID(i));
272 available_networks[i].RSSI = WiFi.RSSI(i);
273 available_networks[i].EncryptionType = WiFi.encryptionType(i);
276 if (network_count < MAX_NETWORKS) {
277 available_networks[network_count].RSSI = END_OF_DATA;
281 if (WiFi.scanComplete() == WIFI_SCAN_FAILED) {
282 WiFi.scanNetworks(
true);
289inline bool initialize_wifi_connection() {
290 server_unavailable =
true;
299 WiFi.begin(current_wifi.SSID.c_str(), current_wifi.Key.c_str());
301 while (WiFi.status() != WL_CONNECTED && wait_count < MAX_WIFI_CONNECT_WAIT) {
305 server_unavailable =
false;
307 if (WiFi.status() == WL_CONNECTED) {
317inline void initialize_mdns(
bool use_user_hostname) {
318 bool success = MDNS.begin(
319 (use_user_hostname ? hostname : DEFAULT_HOSTNAME).c_str());
323 ALWAYS_PRINTF(
"mDNS connected. The AudioLux can be reached at %s.local\n", hostname.c_str());
325 ALWAYS_PRINTF(
"Unable to setup mDNS\n");
330inline const char* get_status_description(
const wl_status_t status) {
331 for (
int i = 0; i <
sizeof(wl_status_to_string) /
sizeof(
WlStatusToString); i++) {
332 if (wl_status_to_string[i].status == status) {
333 status_description = wl_status_to_string[i].description;
338 return status_description;
342inline void on_join_timer(TimerHandle_t timer) {
343 DEBUG_PRINTF(
"Timer: Checking WiFi Join Status.\n");
344 if (xSemaphoreTake(join_status_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
345 if (join_in_progress) {
346 wl_status_t status = WiFi.status();
347 if (status == WL_CONNECTED) {
348 join_in_progress =
false;
349 join_succeeded =
true;
350 xTimerStop(timer, 0);
351 ALWAYS_PRINTF(
"Timer: WiFi join succeeded.\n");
356 current_wifi.SSID = candidate_wifi.SSID;
357 current_wifi.Key = candidate_wifi.Key;
358 dirty_settings =
true;
360 initialize_mdns(
true);
361 }
else if (status != WL_IDLE_STATUS && status != WL_CONNECT_FAILED && status != WL_NO_SHIELD) {
362 join_in_progress =
false;
363 join_succeeded =
false;
364 xTimerStop(timer, 0);
365 DEBUG_PRINTF(
"Timer: WiFi join failed. Reason: %s.\n", get_status_description(status));
369 xSemaphoreGive(join_status_mutex);
377inline bool join_wifi(
const char* ssid,
const char* key) {
378 DEBUG_PRINTF(
"Trying to join network %s ...\n", ssid);
384 WiFi.setHostname(hostname.c_str());
386 candidate_wifi.SSID = ssid;
387 candidate_wifi.Key = key;
390 WiFi.begin(ssid, key);
391 if (xSemaphoreTake(join_status_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
392 join_in_progress =
true;
393 join_succeeded =
false;
395 if (xTimerStart(join_timer, 0) != pdPASS) {
396 DEBUG_PRINTF(
"Unable to star timer. Join unsupervised.\n");
400 xSemaphoreGive(join_status_mutex);
404 DEBUG_PRINTF(
"Unable to get mutex. Join unsupervised.\n");
412inline void serve_wifi_list(AsyncWebServerRequest* request) {
413 if (join_in_progress) {
414 request->send(HTTP_UNAVAILABLE);
420 StaticJsonDocument<1024> json_list;
422 while (wifi_number < MAX_NETWORKS && available_networks[wifi_number].RSSI != END_OF_DATA) {
423 JsonObject wifi = json_list.createNestedObject();
424 wifi[
"ssid"] = available_networks[wifi_number].SSID;
425 wifi[
"rssi"] = available_networks[wifi_number].RSSI;
426 wifi[
"lock"] = available_networks[wifi_number].EncryptionType != WIFI_AUTH_OPEN;
432 serializeJson(json_list, wifi_list);
433 if (wifi_list ==
"null") {
437 DEBUG_PRINTF(
"Sending networks:\n%s\\n", wifi_list.c_str());
438 request->send(HTTP_OK, CONTENT_JSON, wifi_list);
442inline void handle_wifi_put_request(AsyncWebServerRequest* request, JsonVariant& json) {
443 if (request->method() == HTTP_PUT) {
444 const JsonObject& payload = json.as<JsonObject>();
446 int status = HTTP_OK;
449 if (payload[
"ssid"] ==
nullptr) {
450 DEBUG_PRINTF(
"/api/wifi: Forgetting current network.\n");
454 current_wifi.SSID = EMPTY_SETTING;
455 current_wifi.Key = EMPTY_SETTING;
460 DEBUG_PRINTF(
"/api/wifi: Joining network.\n");
461 joined = join_wifi(payload[
"ssid"], payload[
"key"]);
467 response_status = HTTP_ACCEPTED;
468 message =
"Operation completed.";
470 response_status = HTTP_INTERNAL_ERROR;
471 message =
"Unable to monitor join operation: could not start timer or get mutex.";
472 DEBUG_PRINTF(
"%s\n", message.c_str());
474 request->send(response_status, CONTENT_JSON, build_response(joined, message.c_str(),
nullptr));
476 request->send(HTTP_METHOD_NOT_ALLOWED);
481inline void handle_wifi_get_request(AsyncWebServerRequest* request) {
482 const String wifi = current_wifi.SSID == EMPTY_SETTING ? String(
"null") : String(
"\"") + String(current_wifi.SSID) + String(
"\"");
483 const bool connected = current_wifi.SSID == EMPTY_SETTING ? false : (WiFi.status() == WL_CONNECTED);
484 const String response = String(
"{ \"ssid\": ") + String(wifi) + String(
", \"connected\": ") + (connected ?
"true" :
"false") + String(
" }");
486 DEBUG_PRINTF(
"Sending current wifi: %s\n", response.c_str());
487 request->send(HTTP_OK, CONTENT_JSON, response);
490inline void handle_wifi_status_request(AsyncWebServerRequest* request) {
491 const String wifi = current_wifi.SSID == EMPTY_SETTING ? String(
"null") : String(
"\"") + String(current_wifi.SSID) + String(
"\"");
494 if (xSemaphoreTake(join_status_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
495 if (join_in_progress) {
497 }
else if (join_succeeded) {
503 xSemaphoreGive(join_status_mutex);
506 const String response = String(
"{ \"ssid\": ") + String(wifi) + String(
", \"status\": \"") + status + String(
"\" }");
508 DEBUG_PRINTF(
"Sending wifi status: %s\n", response.c_str());
509 request->send(HTTP_OK, CONTENT_JSON, response);
516inline void handle_hostname_put_request(AsyncWebServerRequest* request, JsonVariant& json) {
517 if (server_unavailable) {
518 request->send(HTTP_UNAVAILABLE);
522 if (request->method() == HTTP_PUT) {
523 const JsonObject& payload = json.as<JsonObject>();
525 int status = HTTP_OK;
527 hostname = payload[
"hostname"].as<String>();
529 DEBUG_PRINTF(
"Hostname %s saved.\n", hostname.c_str());
530 request->send(HTTP_OK,
532 build_response(
true,
"New hostname saved.",
nullptr));
536inline void handle_hostname_get_request(AsyncWebServerRequest* request) {
537 if (server_unavailable) {
538 request->send(HTTP_UNAVAILABLE);
542 const String response = String(
"{ \"hostname\": ") +
"\"" + String(hostname) + String(
"\" }");
544 DEBUG_PRINTF(
"Sending current hostname: %s\n", response.c_str());
545 request->send(HTTP_OK, CONTENT_JSON, response);
552inline void handle_health_check(AsyncWebServerRequest* request) {
553 if (server_unavailable) {
554 request->send(HTTP_UNAVAILABLE);
558 DEBUG_PRINTF(
"Pong.\n");
559 request->send(HTTP_OK);
566inline void handle_unknown_url(AsyncWebServerRequest* request) {
571 if (request->method() == HTTP_OPTIONS) {
579 switch (request->method()) {
592 DEBUG_PRINTF(
"Not Found: %s -> http://%s%s\n", method.c_str(), request->host().c_str(), request->url().c_str());
594 if (request->contentLength()) {
595 DEBUG_PRINTF(
"_CONTENT_TYPE: %s\n", request->contentType().c_str());
596 DEBUG_PRINTF(
"_CONTENT_LENGTH: %u\n", request->contentLength());
599 const int headers = request->headers();
600 for (
int i = 0; i < headers; i++) {
601 const AsyncWebHeader* header = request->getHeader(i);
602 DEBUG_PRINTF(
"_HEADER[%s]: %s\n", header->name().c_str(), header->value().c_str());
609inline void save_url(
const String& url) {
615 File saved_url = SD.open(URL_FILE,
"w");
617 File saved_url = LittleFS.open(URL_FILE,
"w");
621 StaticJsonDocument<192> data;
626 serializeJson(data, saved_url);
627 DEBUG_PRINTF(
"%s saved as Web App URL.\n", url.c_str());
629 DEBUG_PRINTF(
"Unable to save Web App URL, will default to http://192.168.4.1.\n");
634inline void setup_networking(
const char* password) {
635 initialize_file_system();
641 WiFi.setSleep(
false);
645 bool wifi_okay =
false;
646 if (current_wifi.SSID !=
nullptr && current_wifi.SSID != EMPTY_SETTING) {
647 DEBUG_PRINTF(
"Attempting to connect to saved WiFi: %s\n", current_wifi.SSID.c_str());
648 wifi_okay = initialize_wifi_connection();
650 ALWAYS_PRINTF(
"WiFi IP: %s\n", WiFi.localIP().toString().c_str());
653 ALWAYS_PRINTF(
"****\n");
654 ALWAYS_PRINTF(
"No wifi saved. AudioLux available via Access Point:\n");
655 ALWAYS_PRINTF(
"SSID: %s Password: %s\n", ap_ssid, password);
656 ALWAYS_PRINTF(
"****\n");
660 WiFi.mode(WIFI_MODE_APSTA);
661 if (password[0] ==
'\0') {
662 WiFi.softAP(
"AudioluxUnsecured");
663 ALWAYS_PRINTF(
"WIFI IS UNSECURED!!!\n");
664 initialize_mdns(
false);
666 WiFi.softAP(hostname, password);
667 initialize_mdns(
true);
673 const IPAddress ap_ip = WiFi.softAPIP();
677 String api_url =
"http://";
682 api_url += ap_ip.toString();
685 ALWAYS_PRINTF(
"Backend availbale at: %s", api_url.c_str());
689inline 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) {
693 join_status_mutex = xSemaphoreCreateMutex();
694 if (join_status_mutex ==
nullptr) {
697 DEBUG_PRINTF(
"WebServer: failed to create mutex. Process halted.\n");
704 join_timer = xTimerCreate(
711 setup_networking(password);
714 DEBUG_PRINTF(
"Registering main APIs.\n");
715 for (
int i = 0; i < get_hook_count; i++) {
716 DEBUG_PRINTF(
"%s\n", api_get_hooks[i].path.c_str());
717 webServer.on(api_get_hooks[i].path.c_str(), HTTP_GET, api_get_hooks[i].handler);
719 for (
int i = 0; i < put_hook_count; i++) {
720 DEBUG_PRINTF(
"%s\n", api_put_hooks[i].path.c_str());
721 webServer.addHandler(
new AsyncCallbackJsonWebHandler(api_put_hooks[i].path.c_str(), api_put_hooks[i].request_handler));
725 webServer.on(
"/api/wifis", HTTP_GET, serve_wifi_list);
726 webServer.on(
"/api/wifi", HTTP_GET, handle_wifi_get_request);
727 webServer.on(
"/api/wifi_status", HTTP_GET, handle_wifi_status_request);
728 webServer.on(
"/api/hostname", HTTP_GET, handle_hostname_get_request);
729 webServer.on(
"/api/health", HTTP_GET, handle_health_check);
731 webServer.addHandler(
new AsyncCallbackJsonWebHandler(
"/api/wifi", handle_wifi_put_request));
732 webServer.addHandler(
new AsyncCallbackJsonWebHandler(
"/api/hostname", handle_hostname_put_request));
736 DEBUG_PRINTF(
"Registering Web App files.\n");
739 webServer.serveStatic(
"/", SD,
"/").setDefaultFile(
"index.html");
741 webServer.serveStatic(
"/", LittleFS,
"/").setDefaultFile(
"index.html");
745 webServer.onNotFound(handle_unknown_url);
748 DefaultHeaders::Instance().addHeader(
"Access-Control-Allow-Origin",
"*");
749 DefaultHeaders::Instance().addHeader(
"Access-Control-Allow-Methods",
"*");
750 DefaultHeaders::Instance().addHeader(
"Access-Control-Allow-Headers",
"*");
751 esp_wifi_set_ps(WIFI_PS_NONE);
753 MDNS.addService(
"http",
"tcp", 80);
Config_Data config
The currently-loaded device config.
Definition main.ino:61
Definition webServer.h:126
Definition webServer.h:131
A structure holding system configuration data.
Definition storage.h:66
Definition webServer.h:47
Definition webServer.h:40
Definition webServer.h:58