4#include "BluetoothSerial.h"
8#include <ESPAsyncWebServer.h>
9#include <WebHandlerImpl.h>
10#include <WebResponseImpl.h>
18#define DEBUG_PRINTF(...)
20#define ALWAYS_PRINTF(...) Serial.printf(__VA_ARGS__)
31constexpr int MAX_WIFI_CONNECT_WAIT = 100;
32constexpr int MAX_NETWORKS = 15;
33constexpr int END_OF_DATA = 9999;
37 uint8_t EncryptionType;
54 const char* description;
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" }
68const char* status_description;
70TimerHandle_t join_timer;
71SemaphoreHandle_t join_status_mutex;
73bool join_in_progress =
false;
74bool join_succeeded =
false;
80const char* ap_ssid =
"AUDIOLUX";
81const char* ap_password =
"12345678";
83const char* DEFAULT_HOSTNAME =
"audiolux";
84static String hostname;
91const char* SETTINGS_FILE =
"/settings.json";
92static StaticJsonDocument<384> settings;
93const char* EMPTY_SETTING =
"#_None_#";
94volatile bool dirty_settings =
false;
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;
110const char* CONTENT_JSON =
"application/json";
111const char* CONTENT_TEXT =
"text/plain";
113static String http_response;
114const char* URL_FILE =
"/assets/url.json";
115volatile bool server_unavailable =
false;
122 ArRequestHandlerFunction handler;
127 ArJsonRequestHandlerFunction request_handler;
131AsyncWebServer webServer(80);
136inline void initialize_file_system() {
138 DEBUG_PRINTF(
"Initializing SD FS...");
139 SPI.begin(SCK, MISO, MOSI, CS);
141 DEBUG_PRINTF(
"Card Mount Failed");
142 DEBUG_PRINTF(
"Card mount successful.");
148inline void save_settings() {
149 if (!settings.containsKey(
"wifi")) {
150 settings.createNestedObject(
"wifi");
152 settings[
"hostname"] = hostname;
153 settings[
"wifi"][
"ssid"] = current_wifi.SSID;
154 settings[
"wifi"][
"key"] = current_wifi.Key;
156 File saved_settings = SD.open(SETTINGS_FILE,
"w");
158 if (saved_settings) {
159 serializeJson(settings, saved_settings);
160 DEBUG_PRINTF(
"Saving settings:\n");
161 serializeJsonPretty(settings, Serial);
164 DEBUG_PRINTF(
"Unable to save settings file.\n");
169inline void load_settings() {
170 DEBUG_PRINTF(
"Checking if settings are available.\n");
172 File saved_settings = SD.open(SETTINGS_FILE,
"r");
174 if (saved_settings) {
175 const DeserializationError error = deserializeJson(settings, saved_settings);
177 DEBUG_PRINTF(
"Settings loaded:\n");
178 serializeJsonPretty(settings, Serial);
181 hostname = settings[
"hostname"].as<String>();
182 current_wifi.SSID = settings[
"wifi"][
"ssid"].as<String>();
183 current_wifi.Key = settings[
"wifi"][
"key"].as<String>();
188 DEBUG_PRINTF(
"Error loading saved file: %s\n", error.c_str());
191 DEBUG_PRINTF(
"Unable to load settings. Will use defaults (re)create file.\n");
193 current_wifi.SSID = EMPTY_SETTING;
194 current_wifi.Key = EMPTY_SETTING;
195 hostname = DEFAULT_HOSTNAME;
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(
"\"");
205 if (details !=
nullptr) {
206 http_response += String(
", \"details\": \"") + details + String(
"\"");
208 http_response +=
"}";
210 return http_response;
217inline void scan_ssids() {
218 static int long_scan_count = 0;
219 const int MAX_SCAN_ITERATIONS = 2;
229 available_networks[0].RSSI = END_OF_DATA;
230 const int n = WiFi.scanComplete();
231 if (n == WIFI_SCAN_FAILED) {
232 WiFi.scanNetworks(
true);
234 }
else if (n == WIFI_SCAN_RUNNING) {
237 if (long_scan_count >= MAX_SCAN_ITERATIONS) {
240 WiFi.scanNetworks(
true);
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);
253 if (network_count < MAX_NETWORKS) {
254 available_networks[network_count].RSSI = END_OF_DATA;
258 if (WiFi.scanComplete() == WIFI_SCAN_FAILED) {
259 WiFi.scanNetworks(
true);
266inline bool initialize_wifi_connection() {
267 server_unavailable =
true;
276 WiFi.begin(current_wifi.SSID.c_str(), current_wifi.Key.c_str());
278 while (WiFi.status() != WL_CONNECTED && wait_count < MAX_WIFI_CONNECT_WAIT) {
282 server_unavailable =
false;
284 if (WiFi.status() == WL_CONNECTED) {
294inline void initialize_mdns(
bool use_user_hostname) {
295 bool success = MDNS.begin(
296 (use_user_hostname ? hostname : DEFAULT_HOSTNAME).c_str());
300 ALWAYS_PRINTF(
"mDNS connected. The AudioLux can be reached at %s.local\n", hostname.c_str());
302 ALWAYS_PRINTF(
"Unable to setup mDNS\n");
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;
315 return status_description;
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");
333 current_wifi.SSID = candidate_wifi.SSID;
334 current_wifi.Key = candidate_wifi.Key;
335 dirty_settings =
true;
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));
346 xSemaphoreGive(join_status_mutex);
354inline bool join_wifi(
const char* ssid,
const char* key) {
355 DEBUG_PRINTF(
"Trying to join network %s ...\n", ssid);
361 WiFi.setHostname(hostname.c_str());
363 candidate_wifi.SSID = ssid;
364 candidate_wifi.Key = key;
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;
372 if (xTimerStart(join_timer, 0) != pdPASS) {
373 DEBUG_PRINTF(
"Unable to star timer. Join unsupervised.\n");
377 xSemaphoreGive(join_status_mutex);
381 DEBUG_PRINTF(
"Unable to get mutex. Join unsupervised.\n");
389inline void serve_wifi_list(AsyncWebServerRequest* request) {
390 if (join_in_progress) {
391 request->send(HTTP_UNAVAILABLE);
397 StaticJsonDocument<1024> json_list;
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;
409 serializeJson(json_list, wifi_list);
410 if (wifi_list ==
"null") {
414 DEBUG_PRINTF(
"Sending networks:\n%s\\n", wifi_list.c_str());
415 request->send(HTTP_OK, CONTENT_JSON, wifi_list);
419inline void handle_wifi_put_request(AsyncWebServerRequest* request, JsonVariant& json) {
420 if (request->method() == HTTP_PUT) {
421 const JsonObject& payload = json.as<JsonObject>();
423 int status = HTTP_OK;
426 if (payload[
"ssid"] ==
nullptr) {
427 DEBUG_PRINTF(
"/api/wifi: Forgetting current network.\n");
431 current_wifi.SSID = EMPTY_SETTING;
432 current_wifi.Key = EMPTY_SETTING;
437 DEBUG_PRINTF(
"/api/wifi: Joining network.\n");
438 joined = join_wifi(payload[
"ssid"], payload[
"key"]);
444 response_status = HTTP_ACCEPTED;
445 message =
"Operation completed.";
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());
451 request->send(response_status, CONTENT_JSON, build_response(joined, message.c_str(),
nullptr));
453 request->send(HTTP_METHOD_NOT_ALLOWED);
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(
" }");
463 DEBUG_PRINTF(
"Sending current wifi: %s\n", response.c_str());
464 request->send(HTTP_OK, CONTENT_JSON, response);
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(
"\"");
471 if (xSemaphoreTake(join_status_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
472 if (join_in_progress) {
474 }
else if (join_succeeded) {
480 xSemaphoreGive(join_status_mutex);
483 const String response = String(
"{ \"ssid\": ") + String(wifi) + String(
", \"status\": \"") + status + String(
"\" }");
485 DEBUG_PRINTF(
"Sending wifi status: %s\n", response.c_str());
486 request->send(HTTP_OK, CONTENT_JSON, response);
493inline void handle_hostname_put_request(AsyncWebServerRequest* request, JsonVariant& json) {
494 if (server_unavailable) {
495 request->send(HTTP_UNAVAILABLE);
499 if (request->method() == HTTP_PUT) {
500 const JsonObject& payload = json.as<JsonObject>();
502 int status = HTTP_OK;
504 hostname = payload[
"hostname"].as<String>();
506 DEBUG_PRINTF(
"Hostname %s saved.\n", hostname.c_str());
507 request->send(HTTP_OK,
509 build_response(
true,
"New hostname saved.",
nullptr));
513inline void handle_hostname_get_request(AsyncWebServerRequest* request) {
514 if (server_unavailable) {
515 request->send(HTTP_UNAVAILABLE);
519 const String response = String(
"{ \"hostname\": ") +
"\"" + String(hostname) + String(
"\" }");
521 DEBUG_PRINTF(
"Sending current hostname: %s\n", response.c_str());
522 request->send(HTTP_OK, CONTENT_JSON, response);
529inline void handle_health_check(AsyncWebServerRequest* request) {
530 if (server_unavailable) {
531 request->send(HTTP_UNAVAILABLE);
535 DEBUG_PRINTF(
"Pong.\n");
536 request->send(HTTP_OK);
543inline void handle_unknown_url(AsyncWebServerRequest* request) {
548 if (request->method() == HTTP_OPTIONS) {
556 switch (request->method()) {
569 DEBUG_PRINTF(
"Not Found: %s -> http://%s%s\n", method.c_str(), request->host().c_str(), request->url().c_str());
571 if (request->contentLength()) {
572 DEBUG_PRINTF(
"_CONTENT_TYPE: %s\n", request->contentType().c_str());
573 DEBUG_PRINTF(
"_CONTENT_LENGTH: %u\n", request->contentLength());
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());
586inline void save_url(
const String& url) {
592 File saved_url = SD.open(URL_FILE,
"w");
595 StaticJsonDocument<192> data;
600 serializeJson(data, saved_url);
601 DEBUG_PRINTF(
"%s saved as Web App URL.\n", url.c_str());
603 DEBUG_PRINTF(
"Unable to save Web App URL, will default to http://192.168.4.1.\n");
608inline void setup_networking(
const char* password) {
609 initialize_file_system();
615 WiFi.setSleep(
false);
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();
624 ALWAYS_PRINTF(
"WiFi IP: %s\n", WiFi.localIP().toString().c_str());
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");
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);
640 WiFi.softAP(hostname, password);
641 initialize_mdns(
true);
647 const IPAddress ap_ip = WiFi.softAPIP();
651 String api_url =
"http://";
656 api_url += ap_ip.toString();
659 ALWAYS_PRINTF(
"Backend availbale at: %s", api_url.c_str());
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) {
667 join_status_mutex = xSemaphoreCreateMutex();
668 if (join_status_mutex ==
nullptr) {
671 DEBUG_PRINTF(
"WebServer: failed to create mutex. Process halted.\n");
678 join_timer = xTimerCreate(
685 setup_networking(password);
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);
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));
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);
705 webServer.addHandler(
new AsyncCallbackJsonWebHandler(
"/api/wifi", handle_wifi_put_request));
706 webServer.addHandler(
new AsyncCallbackJsonWebHandler(
"/api/hostname", handle_hostname_put_request));
710 DEBUG_PRINTF(
"Registering Web App files.\n");
712 webServer.serveStatic(
"/", SD,
"/").setDefaultFile(
"index.html");
714 webServer.onNotFound(handle_unknown_url);
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);
722 MDNS.addService(
"http",
"tcp", 80);
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