| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066 |
- #include "web_server.h"
- #include "esp_log.h"
- #include "esp_http_server.h"
- #include "esp_system.h"
- #include "cJSON.h"
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <sys/stat.h>
- #include <dirent.h>
- #include "esp_wifi.h"
- #include "settings.h"
- #include "led.h"
- #include "wifi.h"
- #include "ethernet.h"
- #include "ota.h"
- #include "log_stream.h"
- #include "rtsp_server.h"
- #include "esp_app_desc.h"
- #include "freertos/FreeRTOS.h"
- #include "freertos/task.h"
- #include "dac_njw1195.h"
- #include "panel_uart.h"
- #include "playback_control.h"
- #include "rtsp_events.h"
- #ifdef CONFIG_BT_A2DP_ENABLE
- #include "a2dp_sink.h"
- #endif
- #ifdef CONFIG_DAC_TAS58XX
- #include "eq_events.h"
- #include "dac_tas58xx_eq.h"
- #endif
- static const char *TAG = "web_server";
- static httpd_handle_t s_server = NULL;
- #define SPIFFS_CHUNK_SIZE 1024
- static esp_err_t serve_spiffs_file(httpd_req_t *req, const char *path,
- const char *content_type) {
- FILE *f = fopen(path, "r");
- if (!f) {
- ESP_LOGE(TAG, "Failed to open %s", path);
- httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found");
- return ESP_FAIL;
- }
- httpd_resp_set_type(req, content_type);
- char buf[SPIFFS_CHUNK_SIZE];
- size_t n;
- while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
- if (httpd_resp_send_chunk(req, buf, (ssize_t)n) != ESP_OK) {
- fclose(f);
- httpd_resp_send_chunk(req, NULL, 0);
- return ESP_FAIL;
- }
- }
- fclose(f);
- httpd_resp_send_chunk(req, NULL, 0);
- return ESP_OK;
- }
- // API handlers
- static esp_err_t root_handler(httpd_req_t *req) {
- return serve_spiffs_file(req, "/spiffs/www/index.html", "text/html");
- }
- static esp_err_t favicon_handler(httpd_req_t *req) {
- httpd_resp_set_status(req, "204 No Content");
- httpd_resp_send(req, NULL, 0);
- return ESP_OK;
- }
- static esp_err_t logs_page_handler(httpd_req_t *req) {
- return serve_spiffs_file(req, "/spiffs/www/logs.html", "text/html");
- }
- static esp_err_t speedtest_page_handler(httpd_req_t *req) {
- return serve_spiffs_file(req, "/spiffs/www/speedtest.html", "text/html");
- }
- // Tiny endpoint used by JS for RTT timing. Returns minimal body.
- static esp_err_t speedtest_ping_handler(httpd_req_t *req) {
- httpd_resp_set_type(req, "text/plain");
- httpd_resp_set_hdr(req, "Cache-Control", "no-store");
- httpd_resp_send(req, "ok", 2);
- return ESP_OK;
- }
- // Streams `bytes` octets of filler data so the browser can measure DL speed.
- // Capped to avoid pathological requests starving audio.
- #define SPEEDTEST_MAX_BYTES ((size_t)16 * 1024 * 1024)
- #define SPEEDTEST_CHUNK 2048
- static esp_err_t speedtest_download_handler(httpd_req_t *req) {
- size_t bytes = (size_t)1024 * 1024;
- char qbuf[64];
- if (httpd_req_get_url_query_str(req, qbuf, sizeof(qbuf)) == ESP_OK) {
- char val[16];
- if (httpd_query_key_value(qbuf, "bytes", val, sizeof(val)) == ESP_OK) {
- long v = strtol(val, NULL, 10);
- if (v > 0) {
- bytes = (size_t)v;
- }
- }
- }
- if (bytes > SPEEDTEST_MAX_BYTES) {
- bytes = SPEEDTEST_MAX_BYTES;
- }
- // Reuse a single buffer of filler bytes. Static so we don't repeatedly
- // hammer the heap; content is irrelevant but non-zero to thwart any
- // compression along the way.
- static uint8_t filler[SPEEDTEST_CHUNK];
- static bool filler_init = false;
- if (!filler_init) {
- for (size_t i = 0; i < sizeof(filler); i++) {
- filler[i] = (uint8_t)(i * 37);
- }
- filler_init = true;
- }
- httpd_resp_set_type(req, "application/octet-stream");
- httpd_resp_set_hdr(req, "Cache-Control", "no-store");
- size_t remaining = bytes;
- while (remaining > 0) {
- ssize_t n =
- remaining < SPEEDTEST_CHUNK ? (ssize_t)remaining : SPEEDTEST_CHUNK;
- if (httpd_resp_send_chunk(req, (const char *)filler, n) != ESP_OK) {
- return ESP_FAIL;
- }
- remaining -= (size_t)n;
- }
- httpd_resp_send_chunk(req, NULL, 0);
- return ESP_OK;
- }
- // Consumes a POST body and reports how many bytes were received.
- static esp_err_t speedtest_upload_handler(httpd_req_t *req) {
- size_t total = req->content_len;
- size_t got = 0;
- uint8_t buf[SPEEDTEST_CHUNK];
- while (got < total) {
- size_t want = total - got;
- if (want > sizeof(buf)) {
- want = sizeof(buf);
- }
- int r = httpd_req_recv(req, (char *)buf, want);
- if (r <= 0) {
- if (r == HTTPD_SOCK_ERR_TIMEOUT) {
- continue;
- }
- return ESP_FAIL;
- }
- got += (size_t)r;
- }
- char reply[64];
- int n = snprintf(reply, sizeof(reply), "received=%u", (unsigned)got);
- httpd_resp_set_type(req, "text/plain");
- httpd_resp_send(req, reply, n);
- return ESP_OK;
- }
- // Captive portal detection handlers
- // These endpoints are requested by various OS to detect captive portals
- static esp_err_t captive_portal_redirect(httpd_req_t *req) {
- // Redirect to the configuration page
- httpd_resp_set_status(req, "302 Found");
- httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
- httpd_resp_send(req, NULL, 0);
- return ESP_OK;
- }
- // Apple devices (iOS/macOS) check these
- static esp_err_t captive_apple_handler(httpd_req_t *req) {
- // Apple expects specific response, redirect instead
- return captive_portal_redirect(req);
- }
- // Android checks this
- static esp_err_t captive_android_handler(httpd_req_t *req) {
- // Android expects 204 for no captive portal, anything else triggers portal
- return captive_portal_redirect(req);
- }
- // Windows checks this
- static esp_err_t captive_windows_handler(httpd_req_t *req) {
- return captive_portal_redirect(req);
- }
- // ============================================================================
- // Metadata Listener
- // ============================================================================
- static rtsp_metadata_t s_current_metadata = {0};
- static bool s_client_connected = false;
- static void web_server_rtsp_event_cb(rtsp_event_t event, const rtsp_event_data_t *data, void *user_data) {
- if (event == RTSP_EVENT_METADATA && data) {
- memcpy(&s_current_metadata, &data->metadata, sizeof(rtsp_metadata_t));
- } else if (event == RTSP_EVENT_CLIENT_CONNECTED) {
- s_client_connected = true;
- } else if (event == RTSP_EVENT_DISCONNECTED) {
- s_client_connected = false;
- memset(&s_current_metadata, 0, sizeof(rtsp_metadata_t));
- }
- }
- // ============================================================================
- // Dashboard API Handlers
- // ============================================================================
- static esp_err_t api_status_handler(httpd_req_t *req) {
- cJSON *json = cJSON_CreateObject();
- cJSON_AddNumberToObject(json, "battery", panel_uart_get_battery_percent());
- cJSON_AddBoolToObject(json, "charging", panel_uart_get_charging_status());
- cJSON_AddNumberToObject(json, "volume", dac_njw1195_get_vol_main());
- cJSON_AddNumberToObject(json, "prompt_volume", dac_njw1195_get_prompt_volume());
- cJSON_AddNumberToObject(json, "bass_volume", dac_njw1195_get_vol_bass());
- cJSON_AddNumberToObject(json, "source", dac_njw1195_get_source());
-
- if (dac_njw1195_get_source() == NJW_SOURCE_AUX) {
- cJSON_AddStringToObject(json, "conn_type", "AUX");
- } else {
- playback_source_t src = playback_control_get_source();
- if (src == PLAYBACK_SOURCE_AIRPLAY) {
- cJSON_AddStringToObject(json, "conn_type", "AirPlay");
- } else if (src == PLAYBACK_SOURCE_BLUETOOTH) {
- cJSON_AddStringToObject(json, "conn_type", "蓝牙");
- } else {
- cJSON_AddStringToObject(json, "conn_type", "未连接");
- }
- }
-
- // Basic sys_status logic
- if (panel_uart_get_battery_percent() <= 10 && !panel_uart_get_charging_status()) {
- cJSON_AddStringToObject(json, "sys_status", "低电量");
- } else if (panel_uart_get_charging_status()) {
- cJSON_AddStringToObject(json, "sys_status", "充电中");
- } else {
- cJSON_AddStringToObject(json, "sys_status", "正常");
- }
-
- if (strlen(s_current_metadata.title) > 0) {
- cJSON_AddStringToObject(json, "title", s_current_metadata.title);
- cJSON_AddStringToObject(json, "artist", s_current_metadata.artist);
- cJSON_AddStringToObject(json, "album", s_current_metadata.album);
- cJSON_AddNumberToObject(json, "duration", s_current_metadata.duration_secs);
- cJSON_AddNumberToObject(json, "position", s_current_metadata.position_secs);
- } else {
- cJSON_AddStringToObject(json, "title", "");
- }
-
- const char *resp = cJSON_PrintUnformatted(json);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, resp, strlen(resp));
- free((void *)resp);
- cJSON_Delete(json);
- return ESP_OK;
- }
- static esp_err_t api_control_handler(httpd_req_t *req) {
- char buf[256];
- int ret, remaining = req->content_len;
- if (remaining >= sizeof(buf)) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Payload too large");
- return ESP_FAIL;
- }
- if ((ret = httpd_req_recv(req, buf, remaining)) <= 0) {
- return ESP_FAIL;
- }
- buf[ret] = '\0';
-
- cJSON *json = cJSON_Parse(buf);
- if (!json) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
- return ESP_FAIL;
- }
-
- cJSON *action = cJSON_GetObjectItem(json, "action");
- if (action && cJSON_IsString(action)) {
- if (strcmp(action->valuestring, "set_volume") == 0) {
- cJSON *vol = cJSON_GetObjectItem(json, "volume");
- if (vol && cJSON_IsNumber(vol)) {
- dac_njw1195_set_vol_main(vol->valueint);
- playback_control_set_volume_percent(vol->valueint);
- panel_uart_report(0x0C, vol->valueint); // ITEM_VOL_LR
- }
- } else if (strcmp(action->valuestring, "set_prompt_volume") == 0) {
- cJSON *vol = cJSON_GetObjectItem(json, "volume");
- if (vol && cJSON_IsNumber(vol)) {
- dac_njw1195_set_prompt_volume(vol->valueint);
- }
- } else if (strcmp(action->valuestring, "set_bass_volume") == 0) {
- cJSON *vol = cJSON_GetObjectItem(json, "volume");
- if (vol && cJSON_IsNumber(vol)) {
- dac_njw1195_set_vol_bass(vol->valueint);
- }
- } else if (strcmp(action->valuestring, "set_source") == 0) {
- cJSON *src = cJSON_GetObjectItem(json, "source");
- if (src && cJSON_IsNumber(src)) {
- dac_njw1195_set_source((njw_source_t)src->valueint);
- }
- } else if (strcmp(action->valuestring, "clear_bt") == 0) {
- #ifdef CONFIG_BT_A2DP_ENABLE
- settings_clear_last_bt_bda();
- bt_a2dp_sink_disconnect_current();
- #endif
- }
- }
-
- cJSON_Delete(json);
-
- const char *resp = "{\"success\":true}";
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, resp, strlen(resp));
- return ESP_OK;
- }
- static esp_err_t wifi_scan_handler(httpd_req_t *req) {
- wifi_ap_record_t *ap_list = NULL;
- uint16_t ap_count = 0;
- cJSON *json = cJSON_CreateObject();
- esp_err_t err = wifi_scan(&ap_list, &ap_count);
- if (err == ESP_OK && ap_list) {
- cJSON *networks = cJSON_CreateArray();
- for (uint16_t i = 0; i < ap_count; i++) {
- cJSON *net = cJSON_CreateObject();
- cJSON_AddStringToObject(net, "ssid", (char *)ap_list[i].ssid);
- cJSON_AddNumberToObject(net, "rssi", ap_list[i].rssi);
- cJSON_AddNumberToObject(net, "channel", ap_list[i].primary);
- cJSON_AddItemToArray(networks, net);
- }
- cJSON_AddItemToObject(json, "networks", networks);
- cJSON_AddBoolToObject(json, "success", true);
- free(ap_list);
- } else {
- cJSON_AddBoolToObject(json, "success", false);
- cJSON_AddStringToObject(json, "error", esp_err_to_name(err));
- }
- char *json_str = cJSON_Print(json);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- return ESP_OK;
- }
- static esp_err_t wifi_config_handler(httpd_req_t *req) {
- char content[512];
- int ret = httpd_req_recv(req, content, sizeof(content) - 1);
- if (ret <= 0) {
- httpd_resp_send_500(req);
- return ESP_FAIL;
- }
- content[ret] = '\0';
- cJSON *json = cJSON_Parse(content);
- if (!json) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
- return ESP_FAIL;
- }
- cJSON *ssid_json = cJSON_GetObjectItem(json, "ssid");
- cJSON *password_json = cJSON_GetObjectItem(json, "password");
- cJSON *response = cJSON_CreateObject();
- if (ssid_json && cJSON_IsString(ssid_json)) {
- const char *ssid = cJSON_GetStringValue(ssid_json);
- const char *password = password_json && cJSON_IsString(password_json)
- ? cJSON_GetStringValue(password_json)
- : "";
- esp_err_t err = settings_set_wifi_credentials(ssid, password);
- if (err == ESP_OK) {
- cJSON_AddBoolToObject(response, "success", true);
- ESP_LOGI(TAG, "WiFi credentials saved. We are restarting...");
- // Schedule restart
- vTaskDelay(pdMS_TO_TICKS(1000));
- esp_restart();
- } else {
- cJSON_AddBoolToObject(response, "success", false);
- cJSON_AddStringToObject(response, "error", esp_err_to_name(err));
- }
- } else {
- cJSON_AddBoolToObject(response, "success", false);
- cJSON_AddStringToObject(response, "error", "Invalid SSID");
- }
- char *json_str = cJSON_Print(response);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- cJSON_Delete(response);
- return ESP_OK;
- }
- static esp_err_t device_name_handler(httpd_req_t *req) {
- char content[256];
- int ret = httpd_req_recv(req, content, sizeof(content) - 1);
- if (ret <= 0) {
- httpd_resp_send_500(req);
- return ESP_FAIL;
- }
- content[ret] = '\0';
- cJSON *json = cJSON_Parse(content);
- if (!json) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
- return ESP_FAIL;
- }
- cJSON *name_json = cJSON_GetObjectItem(json, "name");
- cJSON *response = cJSON_CreateObject();
- if (name_json && cJSON_IsString(name_json)) {
- const char *name = cJSON_GetStringValue(name_json);
- esp_err_t err = settings_set_device_name(name);
- if (err == ESP_OK) {
- wifi_set_hostname(name);
- ethernet_set_hostname(name);
- cJSON_AddBoolToObject(response, "success", true);
- } else {
- cJSON_AddBoolToObject(response, "success", false);
- cJSON_AddStringToObject(response, "error", esp_err_to_name(err));
- }
- } else {
- cJSON_AddBoolToObject(response, "success", false);
- cJSON_AddStringToObject(response, "error", "Invalid name");
- }
- char *json_str = cJSON_Print(response);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- cJSON_Delete(response);
- return ESP_OK;
- }
- static esp_err_t led_brightness_get_handler(httpd_req_t *req) {
- cJSON *json = cJSON_CreateObject();
- cJSON_AddNumberToObject(json, "brightness", led_get_brightness());
- cJSON_AddBoolToObject(json, "success", true);
- char *json_str = cJSON_Print(json);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- return ESP_OK;
- }
- static esp_err_t led_brightness_post_handler(httpd_req_t *req) {
- char content[64];
- int ret = httpd_req_recv(req, content, sizeof(content) - 1);
- if (ret <= 0) {
- httpd_resp_send_500(req);
- return ESP_FAIL;
- }
- content[ret] = '\0';
- cJSON *json = cJSON_Parse(content);
- if (!json) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
- return ESP_FAIL;
- }
- cJSON *response = cJSON_CreateObject();
- cJSON *val = cJSON_GetObjectItem(json, "brightness");
- if (val && cJSON_IsNumber(val)) {
- int b = (int)val->valuedouble;
- if (b < 0) {
- b = 0;
- }
- if (b > 255) {
- b = 255;
- }
- esp_err_t err = led_set_brightness((uint8_t)b);
- if (err == ESP_OK) {
- cJSON_AddBoolToObject(response, "success", true);
- } else {
- cJSON_AddBoolToObject(response, "success", false);
- cJSON_AddStringToObject(response, "error", esp_err_to_name(err));
- }
- } else {
- cJSON_AddBoolToObject(response, "success", false);
- cJSON_AddStringToObject(response, "error",
- "Expected {\"brightness\": 0-255}");
- }
- char *json_str = cJSON_Print(response);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- cJSON_Delete(response);
- return ESP_OK;
- }
- static esp_err_t ota_update_handler(httpd_req_t *req) {
- if (req->content_len == 0) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "No firmware uploaded");
- return ESP_FAIL;
- }
- // Stop AirPlay to free resources during OTA
- ESP_LOGI(TAG, "Stopping AirPlay for OTA update");
- rtsp_server_stop();
- esp_err_t err = ota_start_from_http(req);
- if (err != ESP_OK) {
- httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
- esp_err_to_name(err));
- return ESP_FAIL;
- }
- // Send response before restarting
- httpd_resp_sendstr(req, "Firmware update complete, rebooting now!\n");
- vTaskDelay(pdMS_TO_TICKS(500));
- esp_restart();
- return ESP_OK;
- }
- static esp_err_t system_info_handler(httpd_req_t *req) {
- cJSON *json = cJSON_CreateObject();
- cJSON *info = cJSON_CreateObject();
- char ip_str[16] = {0};
- char mac_str[18] = {0};
- char device_name[65] = {0};
- bool wifi_connected = wifi_is_connected();
- bool eth_connected = ethernet_is_connected();
- // Show IP and MAC for the active interface
- if (eth_connected) {
- ethernet_get_ip_str(ip_str, sizeof(ip_str));
- ethernet_get_mac_str(mac_str, sizeof(mac_str));
- } else {
- wifi_get_ip_str(ip_str, sizeof(ip_str));
- wifi_get_mac_str(mac_str, sizeof(mac_str));
- }
- settings_get_device_name(device_name, sizeof(device_name));
- cJSON_AddStringToObject(info, "ip", ip_str);
- cJSON_AddStringToObject(info, "mac", mac_str);
- cJSON_AddStringToObject(info, "device_name", device_name);
- cJSON_AddBoolToObject(info, "wifi_connected", wifi_connected);
- cJSON_AddBoolToObject(info, "eth_connected", eth_connected);
- cJSON_AddNumberToObject(info, "free_heap", esp_get_free_heap_size());
- // WiFi link diagnostics (only meaningful when associated as STA)
- if (wifi_connected) {
- wifi_ap_record_t ap;
- if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
- char ssid_buf[33];
- size_t slen = strnlen((const char *)ap.ssid, sizeof(ap.ssid));
- if (slen > sizeof(ssid_buf) - 1) {
- slen = sizeof(ssid_buf) - 1;
- }
- memcpy(ssid_buf, ap.ssid, slen);
- ssid_buf[slen] = '\0';
- char bssid_buf[18];
- snprintf(bssid_buf, sizeof(bssid_buf), "%02x:%02x:%02x:%02x:%02x:%02x",
- ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4],
- ap.bssid[5]);
- const char *phy = "?";
- if (ap.phy_11n) {
- phy = "11n";
- } else if (ap.phy_11g) {
- phy = "11g";
- } else if (ap.phy_11b) {
- phy = "11b";
- } else if (ap.phy_lr) {
- phy = "LR";
- }
- cJSON_AddStringToObject(info, "wifi_ssid", ssid_buf);
- cJSON_AddStringToObject(info, "wifi_bssid", bssid_buf);
- cJSON_AddNumberToObject(info, "wifi_rssi", ap.rssi);
- cJSON_AddNumberToObject(info, "wifi_channel", ap.primary);
- cJSON_AddStringToObject(info, "wifi_phy", phy);
- }
- }
- const esp_app_desc_t *app_desc = esp_app_get_description();
- cJSON_AddStringToObject(info, "firmware_version", app_desc->version);
- #ifdef CONFIG_DAC_TAS58XX
- cJSON_AddBoolToObject(info, "eq_supported", true);
- #else
- cJSON_AddBoolToObject(info, "eq_supported", false);
- #endif
- cJSON_AddItemToObject(json, "info", info);
- cJSON_AddBoolToObject(json, "success", true);
- char *json_str = cJSON_Print(json);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- return ESP_OK;
- }
- static esp_err_t system_restart_handler(httpd_req_t *req) {
- cJSON *json = cJSON_CreateObject();
- cJSON_AddBoolToObject(json, "success", true);
- char *json_str = cJSON_Print(json);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- ESP_LOGI(TAG, "Restart requested via web interface");
- vTaskDelay(pdMS_TO_TICKS(500));
- esp_restart();
- return ESP_OK;
- }
- /* ================================================================== */
- /* SPIFFS File Management API */
- /* ================================================================== */
- // Allowed path prefixes for file upload (prevent writes outside SPIFFS)
- static const char *ALLOWED_PREFIXES[] = {"/spiffs/"};
- static bool is_path_allowed(const char *path) {
- for (int i = 0; i < sizeof(ALLOWED_PREFIXES) / sizeof(ALLOWED_PREFIXES[0]);
- i++) {
- if (strncmp(path, ALLOWED_PREFIXES[i], strlen(ALLOWED_PREFIXES[i])) == 0) {
- // Reject path traversal
- if (strstr(path, "..") != NULL) {
- return false;
- }
- return true;
- }
- }
- return false;
- }
- static esp_err_t fs_upload_handler(httpd_req_t *req) {
- // Get target path from query string
- char query[128] = {0};
- char path[64] = {0};
- if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK ||
- httpd_query_key_value(query, "path", path, sizeof(path)) != ESP_OK) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
- "Missing 'path' query parameter");
- return ESP_FAIL;
- }
- if (!is_path_allowed(path)) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Path not allowed");
- return ESP_FAIL;
- }
- if (req->content_len == 0 || req->content_len > (size_t)(64 * 1024)) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Body required (max 64KB)");
- return ESP_FAIL;
- }
- FILE *f = fopen(path, "wb");
- if (!f) {
- ESP_LOGE(TAG, "Failed to create %s", path);
- httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
- "Failed to create file");
- return ESP_FAIL;
- }
- char buf[SPIFFS_CHUNK_SIZE];
- size_t remaining = req->content_len;
- while (remaining > 0) {
- size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf);
- int received = httpd_req_recv(req, buf, to_read);
- if (received <= 0) {
- fclose(f);
- remove(path);
- httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
- "Receive failed");
- return ESP_FAIL;
- }
- fwrite(buf, 1, (size_t)received, f);
- remaining -= (size_t)received;
- }
- fclose(f);
- ESP_LOGI(TAG, "Uploaded %u bytes to %s", (unsigned)req->content_len, path);
- cJSON *json = cJSON_CreateObject();
- cJSON_AddBoolToObject(json, "success", true);
- cJSON_AddNumberToObject(json, "size", (double)req->content_len);
- cJSON_AddStringToObject(json, "path", path);
- char *json_str = cJSON_Print(json);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- return ESP_OK;
- }
- static esp_err_t fs_delete_handler(httpd_req_t *req) {
- char query[128] = {0};
- char path[64] = {0};
- if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK ||
- httpd_query_key_value(query, "path", path, sizeof(path)) != ESP_OK) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
- "Missing 'path' query parameter");
- return ESP_FAIL;
- }
- if (!is_path_allowed(path)) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Path not allowed");
- return ESP_FAIL;
- }
- cJSON *json = cJSON_CreateObject();
- if (remove(path) == 0) {
- ESP_LOGI(TAG, "Deleted %s", path);
- cJSON_AddBoolToObject(json, "success", true);
- } else {
- cJSON_AddBoolToObject(json, "success", false);
- cJSON_AddStringToObject(json, "error", "File not found");
- }
- char *json_str = cJSON_Print(json);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- return ESP_OK;
- }
- static esp_err_t fs_list_handler(httpd_req_t *req) {
- char query[128] = {0};
- char dir_path[64] = "/spiffs";
- if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) {
- httpd_query_key_value(query, "dir", dir_path, sizeof(dir_path));
- }
- if (!is_path_allowed(dir_path) && strcmp(dir_path, "/spiffs") != 0) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Path not allowed");
- return ESP_FAIL;
- }
- DIR *d = opendir(dir_path);
- cJSON *json = cJSON_CreateObject();
- cJSON *files = cJSON_CreateArray();
- if (d) {
- struct dirent *entry;
- while ((entry = readdir(d)) != NULL) {
- cJSON *item = cJSON_CreateObject();
- cJSON_AddStringToObject(item, "name", entry->d_name);
- char full_path[320];
- snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
- struct stat st;
- if (stat(full_path, &st) == 0) {
- cJSON_AddNumberToObject(item, "size", (double)st.st_size);
- }
- cJSON_AddItemToArray(files, item);
- }
- closedir(d);
- cJSON_AddBoolToObject(json, "success", true);
- } else {
- cJSON_AddBoolToObject(json, "success", false);
- cJSON_AddStringToObject(json, "error", "Cannot open directory");
- }
- cJSON_AddItemToObject(json, "files", files);
- char *json_str = cJSON_Print(json);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- return ESP_OK;
- }
- /* ================================================================== */
- /* EQ Page + API (only when TAS58xx DAC is configured) */
- /* ================================================================== */
- #ifdef CONFIG_DAC_TAS58XX
- static esp_err_t eq_page_handler(httpd_req_t *req) {
- return serve_spiffs_file(req, "/spiffs/www/eq.html", "text/html");
- }
- static esp_err_t eq_get_handler(httpd_req_t *req) {
- cJSON *json = cJSON_CreateObject();
- cJSON *arr = cJSON_CreateArray();
- float gains[SETTINGS_EQ_BANDS];
- if (settings_get_eq_gains(gains) == ESP_OK) {
- for (int i = 0; i < SETTINGS_EQ_BANDS; i++) {
- cJSON_AddItemToArray(arr, cJSON_CreateNumber((double)gains[i]));
- }
- } else {
- /* No saved EQ — return all zeros (flat) */
- for (int i = 0; i < SETTINGS_EQ_BANDS; i++) {
- cJSON_AddItemToArray(arr, cJSON_CreateNumber(0.0));
- }
- }
- cJSON_AddItemToObject(json, "gains", arr);
- cJSON_AddNumberToObject(json, "bands", SETTINGS_EQ_BANDS);
- cJSON_AddBoolToObject(json, "success", true);
- char *json_str = cJSON_Print(json);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- return ESP_OK;
- }
- static esp_err_t eq_post_handler(httpd_req_t *req) {
- char content[512];
- int ret = httpd_req_recv(req, content, sizeof(content) - 1);
- if (ret <= 0) {
- httpd_resp_send_500(req);
- return ESP_FAIL;
- }
- content[ret] = '\0';
- cJSON *json = cJSON_Parse(content);
- if (!json) {
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
- return ESP_FAIL;
- }
- cJSON *response = cJSON_CreateObject();
- cJSON *gains_arr = cJSON_GetObjectItem(json, "gains");
- if (gains_arr && cJSON_IsArray(gains_arr) &&
- cJSON_GetArraySize(gains_arr) == SETTINGS_EQ_BANDS) {
- float gains[SETTINGS_EQ_BANDS];
- for (int i = 0; i < SETTINGS_EQ_BANDS; i++) {
- cJSON *item = cJSON_GetArrayItem(gains_arr, i);
- gains[i] = cJSON_IsNumber(item) ? (float)item->valuedouble : 0.0f;
- /* Clamp */
- if (gains[i] > 15.0f) {
- gains[i] = 15.0f;
- }
- if (gains[i] < -15.0f) {
- gains[i] = -15.0f;
- }
- }
- /* Emit event — listeners (settings + DAC) will handle it */
- eq_event_data_t ev_data;
- memcpy(ev_data.all_bands.gains_db, gains, sizeof(gains));
- eq_events_emit(EQ_EVENT_ALL_BANDS_SET, &ev_data);
- cJSON_AddBoolToObject(response, "success", true);
- } else {
- cJSON_AddBoolToObject(response, "success", false);
- cJSON_AddStringToObject(response, "error",
- "Expected 'gains' array with 15 values");
- }
- char *json_str = cJSON_Print(response);
- httpd_resp_set_type(req, "application/json");
- httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
- free(json_str);
- cJSON_Delete(json);
- cJSON_Delete(response);
- return ESP_OK;
- }
- #endif /* CONFIG_DAC_TAS58XX */
- esp_err_t web_server_start(uint16_t port) {
- if (s_server) {
- ESP_LOGW(TAG, "Web server already running");
- return ESP_OK;
- }
-
- rtsp_events_register(web_server_rtsp_event_cb, NULL);
- httpd_config_t config = HTTPD_DEFAULT_CONFIG();
- config.server_port = port;
- #ifdef CONFIG_BT_ENABLED
- config.max_open_sockets = 2; // BT: tighter socket budget (LWIP 12)
- config.send_wait_timeout = 10; // BT/WiFi coexistence slows TCP drain
- #else
- config.max_open_sockets = 3; // Limit to save lwIP socket slots for AirPlay
- #endif
- config.lru_purge_enable = true; // Reclaim stale sockets when all are in use
- config.max_uri_handlers =
- 28; // Room for captive portal + EQ + speedtest + brightness
- config.max_resp_headers = 8;
- config.stack_size = 8192;
- esp_err_t err = httpd_start(&s_server, &config);
- if (err != ESP_OK) {
- ESP_LOGE(TAG, "Failed to start web server: %s", esp_err_to_name(err));
- return err;
- }
- // Register handlers
- httpd_uri_t root_uri = {
- .uri = "/", .method = HTTP_GET, .handler = root_handler};
- httpd_register_uri_handler(s_server, &root_uri);
- httpd_uri_t favicon_uri = {
- .uri = "/favicon.ico", .method = HTTP_GET, .handler = favicon_handler};
- httpd_register_uri_handler(s_server, &favicon_uri);
- httpd_uri_t logs_uri = {
- .uri = "/logs", .method = HTTP_GET, .handler = logs_page_handler};
- httpd_register_uri_handler(s_server, &logs_uri);
- httpd_uri_t speedtest_page_uri = {.uri = "/speedtest",
- .method = HTTP_GET,
- .handler = speedtest_page_handler};
- httpd_register_uri_handler(s_server, &speedtest_page_uri);
- httpd_uri_t speedtest_ping_uri = {.uri = "/api/speedtest/ping",
- .method = HTTP_GET,
- .handler = speedtest_ping_handler};
- httpd_register_uri_handler(s_server, &speedtest_ping_uri);
- httpd_uri_t speedtest_dl_uri = {.uri = "/api/speedtest/download",
- .method = HTTP_GET,
- .handler = speedtest_download_handler};
- httpd_register_uri_handler(s_server, &speedtest_dl_uri);
- httpd_uri_t speedtest_ul_uri = {.uri = "/api/speedtest/upload",
- .method = HTTP_POST,
- .handler = speedtest_upload_handler};
- httpd_register_uri_handler(s_server, &speedtest_ul_uri);
- httpd_uri_t wifi_scan_uri = {.uri = "/api/wifi/scan",
- .method = HTTP_GET,
- .handler = wifi_scan_handler};
- httpd_register_uri_handler(s_server, &wifi_scan_uri);
- httpd_uri_t wifi_config_uri = {.uri = "/api/wifi/config",
- .method = HTTP_POST,
- .handler = wifi_config_handler};
- httpd_register_uri_handler(s_server, &wifi_config_uri);
- httpd_uri_t device_name_uri = {.uri = "/api/device/name",
- .method = HTTP_POST,
- .handler = device_name_handler};
- httpd_register_uri_handler(s_server, &device_name_uri);
- httpd_uri_t led_brightness_get_uri = {.uri = "/api/led/brightness",
- .method = HTTP_GET,
- .handler = led_brightness_get_handler};
- httpd_register_uri_handler(s_server, &led_brightness_get_uri);
- httpd_uri_t led_brightness_post_uri = {.uri = "/api/led/brightness",
- .method = HTTP_POST,
- .handler =
- led_brightness_post_handler};
- httpd_register_uri_handler(s_server, &led_brightness_post_uri);
- httpd_uri_t ota_uri = {.uri = "/api/ota/update",
- .method = HTTP_POST,
- .handler = ota_update_handler};
- httpd_register_uri_handler(s_server, &ota_uri);
- httpd_uri_t system_info_uri = {.uri = "/api/system/info",
- .method = HTTP_GET,
- .handler = system_info_handler};
- httpd_register_uri_handler(s_server, &system_info_uri);
- httpd_uri_t system_restart_uri = {.uri = "/api/system/restart",
- .method = HTTP_POST,
- .handler = system_restart_handler};
- httpd_register_uri_handler(s_server, &system_restart_uri);
- // File management API
- httpd_uri_t fs_upload_uri = {.uri = "/api/fs/upload",
- .method = HTTP_POST,
- .handler = fs_upload_handler};
- httpd_register_uri_handler(s_server, &fs_upload_uri);
- httpd_uri_t fs_delete_uri = {.uri = "/api/fs/delete",
- .method = HTTP_POST,
- .handler = fs_delete_handler};
- httpd_register_uri_handler(s_server, &fs_delete_uri);
- httpd_uri_t fs_list_uri = {
- .uri = "/api/fs/list", .method = HTTP_GET, .handler = fs_list_handler};
- httpd_register_uri_handler(s_server, &fs_list_uri);
- httpd_uri_t api_status_uri = {
- .uri = "/api/status", .method = HTTP_GET, .handler = api_status_handler};
- httpd_register_uri_handler(s_server, &api_status_uri);
- httpd_uri_t api_control_uri = {
- .uri = "/api/control", .method = HTTP_POST, .handler = api_control_handler};
- httpd_register_uri_handler(s_server, &api_control_uri);
- // Captive portal detection endpoints
- // Apple iOS/macOS
- httpd_uri_t apple_captive1 = {.uri = "/hotspot-detect.html",
- .method = HTTP_GET,
- .handler = captive_apple_handler};
- httpd_register_uri_handler(s_server, &apple_captive1);
- httpd_uri_t apple_captive2 = {.uri = "/library/test/success.html",
- .method = HTTP_GET,
- .handler = captive_apple_handler};
- httpd_register_uri_handler(s_server, &apple_captive2);
- // Android
- httpd_uri_t android_captive = {.uri = "/generate_204",
- .method = HTTP_GET,
- .handler = captive_android_handler};
- httpd_register_uri_handler(s_server, &android_captive);
- // Windows
- httpd_uri_t windows_captive = {.uri = "/connecttest.txt",
- .method = HTTP_GET,
- .handler = captive_windows_handler};
- httpd_register_uri_handler(s_server, &windows_captive);
- #ifdef CONFIG_DAC_TAS58XX
- httpd_uri_t eq_page_uri = {
- .uri = "/eq", .method = HTTP_GET, .handler = eq_page_handler};
- httpd_register_uri_handler(s_server, &eq_page_uri);
- httpd_uri_t eq_get_uri = {
- .uri = "/api/eq", .method = HTTP_GET, .handler = eq_get_handler};
- httpd_register_uri_handler(s_server, &eq_get_uri);
- httpd_uri_t eq_post_uri = {
- .uri = "/api/eq", .method = HTTP_POST, .handler = eq_post_handler};
- httpd_register_uri_handler(s_server, &eq_post_uri);
- #endif
- log_stream_register(s_server);
- ESP_LOGI(TAG, "Web server started on port %d with captive portal support",
- port);
- return ESP_OK;
- }
- void web_server_stop(void) {
- if (s_server) {
- httpd_stop(s_server);
- s_server = NULL;
- ESP_LOGI(TAG, "Web server stopped");
- }
- }
|