web_server.c 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066
  1. #include "web_server.h"
  2. #include "esp_log.h"
  3. #include "esp_http_server.h"
  4. #include "esp_system.h"
  5. #include "cJSON.h"
  6. #include <stdio.h>
  7. #include <string.h>
  8. #include <stdlib.h>
  9. #include <sys/stat.h>
  10. #include <dirent.h>
  11. #include "esp_wifi.h"
  12. #include "settings.h"
  13. #include "led.h"
  14. #include "wifi.h"
  15. #include "ethernet.h"
  16. #include "ota.h"
  17. #include "log_stream.h"
  18. #include "rtsp_server.h"
  19. #include "esp_app_desc.h"
  20. #include "freertos/FreeRTOS.h"
  21. #include "freertos/task.h"
  22. #include "dac_njw1195.h"
  23. #include "panel_uart.h"
  24. #include "playback_control.h"
  25. #include "rtsp_events.h"
  26. #ifdef CONFIG_BT_A2DP_ENABLE
  27. #include "a2dp_sink.h"
  28. #endif
  29. #ifdef CONFIG_DAC_TAS58XX
  30. #include "eq_events.h"
  31. #include "dac_tas58xx_eq.h"
  32. #endif
  33. static const char *TAG = "web_server";
  34. static httpd_handle_t s_server = NULL;
  35. #define SPIFFS_CHUNK_SIZE 1024
  36. static esp_err_t serve_spiffs_file(httpd_req_t *req, const char *path,
  37. const char *content_type) {
  38. FILE *f = fopen(path, "r");
  39. if (!f) {
  40. ESP_LOGE(TAG, "Failed to open %s", path);
  41. httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found");
  42. return ESP_FAIL;
  43. }
  44. httpd_resp_set_type(req, content_type);
  45. char buf[SPIFFS_CHUNK_SIZE];
  46. size_t n;
  47. while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
  48. if (httpd_resp_send_chunk(req, buf, (ssize_t)n) != ESP_OK) {
  49. fclose(f);
  50. httpd_resp_send_chunk(req, NULL, 0);
  51. return ESP_FAIL;
  52. }
  53. }
  54. fclose(f);
  55. httpd_resp_send_chunk(req, NULL, 0);
  56. return ESP_OK;
  57. }
  58. // API handlers
  59. static esp_err_t root_handler(httpd_req_t *req) {
  60. return serve_spiffs_file(req, "/spiffs/www/index.html", "text/html");
  61. }
  62. static esp_err_t favicon_handler(httpd_req_t *req) {
  63. httpd_resp_set_status(req, "204 No Content");
  64. httpd_resp_send(req, NULL, 0);
  65. return ESP_OK;
  66. }
  67. static esp_err_t logs_page_handler(httpd_req_t *req) {
  68. return serve_spiffs_file(req, "/spiffs/www/logs.html", "text/html");
  69. }
  70. static esp_err_t speedtest_page_handler(httpd_req_t *req) {
  71. return serve_spiffs_file(req, "/spiffs/www/speedtest.html", "text/html");
  72. }
  73. // Tiny endpoint used by JS for RTT timing. Returns minimal body.
  74. static esp_err_t speedtest_ping_handler(httpd_req_t *req) {
  75. httpd_resp_set_type(req, "text/plain");
  76. httpd_resp_set_hdr(req, "Cache-Control", "no-store");
  77. httpd_resp_send(req, "ok", 2);
  78. return ESP_OK;
  79. }
  80. // Streams `bytes` octets of filler data so the browser can measure DL speed.
  81. // Capped to avoid pathological requests starving audio.
  82. #define SPEEDTEST_MAX_BYTES ((size_t)16 * 1024 * 1024)
  83. #define SPEEDTEST_CHUNK 2048
  84. static esp_err_t speedtest_download_handler(httpd_req_t *req) {
  85. size_t bytes = (size_t)1024 * 1024;
  86. char qbuf[64];
  87. if (httpd_req_get_url_query_str(req, qbuf, sizeof(qbuf)) == ESP_OK) {
  88. char val[16];
  89. if (httpd_query_key_value(qbuf, "bytes", val, sizeof(val)) == ESP_OK) {
  90. long v = strtol(val, NULL, 10);
  91. if (v > 0) {
  92. bytes = (size_t)v;
  93. }
  94. }
  95. }
  96. if (bytes > SPEEDTEST_MAX_BYTES) {
  97. bytes = SPEEDTEST_MAX_BYTES;
  98. }
  99. // Reuse a single buffer of filler bytes. Static so we don't repeatedly
  100. // hammer the heap; content is irrelevant but non-zero to thwart any
  101. // compression along the way.
  102. static uint8_t filler[SPEEDTEST_CHUNK];
  103. static bool filler_init = false;
  104. if (!filler_init) {
  105. for (size_t i = 0; i < sizeof(filler); i++) {
  106. filler[i] = (uint8_t)(i * 37);
  107. }
  108. filler_init = true;
  109. }
  110. httpd_resp_set_type(req, "application/octet-stream");
  111. httpd_resp_set_hdr(req, "Cache-Control", "no-store");
  112. size_t remaining = bytes;
  113. while (remaining > 0) {
  114. ssize_t n =
  115. remaining < SPEEDTEST_CHUNK ? (ssize_t)remaining : SPEEDTEST_CHUNK;
  116. if (httpd_resp_send_chunk(req, (const char *)filler, n) != ESP_OK) {
  117. return ESP_FAIL;
  118. }
  119. remaining -= (size_t)n;
  120. }
  121. httpd_resp_send_chunk(req, NULL, 0);
  122. return ESP_OK;
  123. }
  124. // Consumes a POST body and reports how many bytes were received.
  125. static esp_err_t speedtest_upload_handler(httpd_req_t *req) {
  126. size_t total = req->content_len;
  127. size_t got = 0;
  128. uint8_t buf[SPEEDTEST_CHUNK];
  129. while (got < total) {
  130. size_t want = total - got;
  131. if (want > sizeof(buf)) {
  132. want = sizeof(buf);
  133. }
  134. int r = httpd_req_recv(req, (char *)buf, want);
  135. if (r <= 0) {
  136. if (r == HTTPD_SOCK_ERR_TIMEOUT) {
  137. continue;
  138. }
  139. return ESP_FAIL;
  140. }
  141. got += (size_t)r;
  142. }
  143. char reply[64];
  144. int n = snprintf(reply, sizeof(reply), "received=%u", (unsigned)got);
  145. httpd_resp_set_type(req, "text/plain");
  146. httpd_resp_send(req, reply, n);
  147. return ESP_OK;
  148. }
  149. // Captive portal detection handlers
  150. // These endpoints are requested by various OS to detect captive portals
  151. static esp_err_t captive_portal_redirect(httpd_req_t *req) {
  152. // Redirect to the configuration page
  153. httpd_resp_set_status(req, "302 Found");
  154. httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
  155. httpd_resp_send(req, NULL, 0);
  156. return ESP_OK;
  157. }
  158. // Apple devices (iOS/macOS) check these
  159. static esp_err_t captive_apple_handler(httpd_req_t *req) {
  160. // Apple expects specific response, redirect instead
  161. return captive_portal_redirect(req);
  162. }
  163. // Android checks this
  164. static esp_err_t captive_android_handler(httpd_req_t *req) {
  165. // Android expects 204 for no captive portal, anything else triggers portal
  166. return captive_portal_redirect(req);
  167. }
  168. // Windows checks this
  169. static esp_err_t captive_windows_handler(httpd_req_t *req) {
  170. return captive_portal_redirect(req);
  171. }
  172. // ============================================================================
  173. // Metadata Listener
  174. // ============================================================================
  175. static rtsp_metadata_t s_current_metadata = {0};
  176. static bool s_client_connected = false;
  177. static void web_server_rtsp_event_cb(rtsp_event_t event, const rtsp_event_data_t *data, void *user_data) {
  178. if (event == RTSP_EVENT_METADATA && data) {
  179. memcpy(&s_current_metadata, &data->metadata, sizeof(rtsp_metadata_t));
  180. } else if (event == RTSP_EVENT_CLIENT_CONNECTED) {
  181. s_client_connected = true;
  182. } else if (event == RTSP_EVENT_DISCONNECTED) {
  183. s_client_connected = false;
  184. memset(&s_current_metadata, 0, sizeof(rtsp_metadata_t));
  185. }
  186. }
  187. // ============================================================================
  188. // Dashboard API Handlers
  189. // ============================================================================
  190. static esp_err_t api_status_handler(httpd_req_t *req) {
  191. cJSON *json = cJSON_CreateObject();
  192. cJSON_AddNumberToObject(json, "battery", panel_uart_get_battery_percent());
  193. cJSON_AddBoolToObject(json, "charging", panel_uart_get_charging_status());
  194. cJSON_AddNumberToObject(json, "volume", dac_njw1195_get_vol_main());
  195. cJSON_AddNumberToObject(json, "prompt_volume", dac_njw1195_get_prompt_volume());
  196. cJSON_AddNumberToObject(json, "bass_volume", dac_njw1195_get_vol_bass());
  197. cJSON_AddNumberToObject(json, "source", dac_njw1195_get_source());
  198. if (dac_njw1195_get_source() == NJW_SOURCE_AUX) {
  199. cJSON_AddStringToObject(json, "conn_type", "AUX");
  200. } else {
  201. playback_source_t src = playback_control_get_source();
  202. if (src == PLAYBACK_SOURCE_AIRPLAY) {
  203. cJSON_AddStringToObject(json, "conn_type", "AirPlay");
  204. } else if (src == PLAYBACK_SOURCE_BLUETOOTH) {
  205. cJSON_AddStringToObject(json, "conn_type", "蓝牙");
  206. } else {
  207. cJSON_AddStringToObject(json, "conn_type", "未连接");
  208. }
  209. }
  210. // Basic sys_status logic
  211. if (panel_uart_get_battery_percent() <= 10 && !panel_uart_get_charging_status()) {
  212. cJSON_AddStringToObject(json, "sys_status", "低电量");
  213. } else if (panel_uart_get_charging_status()) {
  214. cJSON_AddStringToObject(json, "sys_status", "充电中");
  215. } else {
  216. cJSON_AddStringToObject(json, "sys_status", "正常");
  217. }
  218. if (strlen(s_current_metadata.title) > 0) {
  219. cJSON_AddStringToObject(json, "title", s_current_metadata.title);
  220. cJSON_AddStringToObject(json, "artist", s_current_metadata.artist);
  221. cJSON_AddStringToObject(json, "album", s_current_metadata.album);
  222. cJSON_AddNumberToObject(json, "duration", s_current_metadata.duration_secs);
  223. cJSON_AddNumberToObject(json, "position", s_current_metadata.position_secs);
  224. } else {
  225. cJSON_AddStringToObject(json, "title", "");
  226. }
  227. const char *resp = cJSON_PrintUnformatted(json);
  228. httpd_resp_set_type(req, "application/json");
  229. httpd_resp_send(req, resp, strlen(resp));
  230. free((void *)resp);
  231. cJSON_Delete(json);
  232. return ESP_OK;
  233. }
  234. static esp_err_t api_control_handler(httpd_req_t *req) {
  235. char buf[256];
  236. int ret, remaining = req->content_len;
  237. if (remaining >= sizeof(buf)) {
  238. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Payload too large");
  239. return ESP_FAIL;
  240. }
  241. if ((ret = httpd_req_recv(req, buf, remaining)) <= 0) {
  242. return ESP_FAIL;
  243. }
  244. buf[ret] = '\0';
  245. cJSON *json = cJSON_Parse(buf);
  246. if (!json) {
  247. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
  248. return ESP_FAIL;
  249. }
  250. cJSON *action = cJSON_GetObjectItem(json, "action");
  251. if (action && cJSON_IsString(action)) {
  252. if (strcmp(action->valuestring, "set_volume") == 0) {
  253. cJSON *vol = cJSON_GetObjectItem(json, "volume");
  254. if (vol && cJSON_IsNumber(vol)) {
  255. dac_njw1195_set_vol_main(vol->valueint);
  256. playback_control_set_volume_percent(vol->valueint);
  257. panel_uart_report(0x0C, vol->valueint); // ITEM_VOL_LR
  258. }
  259. } else if (strcmp(action->valuestring, "set_prompt_volume") == 0) {
  260. cJSON *vol = cJSON_GetObjectItem(json, "volume");
  261. if (vol && cJSON_IsNumber(vol)) {
  262. dac_njw1195_set_prompt_volume(vol->valueint);
  263. }
  264. } else if (strcmp(action->valuestring, "set_bass_volume") == 0) {
  265. cJSON *vol = cJSON_GetObjectItem(json, "volume");
  266. if (vol && cJSON_IsNumber(vol)) {
  267. dac_njw1195_set_vol_bass(vol->valueint);
  268. }
  269. } else if (strcmp(action->valuestring, "set_source") == 0) {
  270. cJSON *src = cJSON_GetObjectItem(json, "source");
  271. if (src && cJSON_IsNumber(src)) {
  272. dac_njw1195_set_source((njw_source_t)src->valueint);
  273. }
  274. } else if (strcmp(action->valuestring, "clear_bt") == 0) {
  275. #ifdef CONFIG_BT_A2DP_ENABLE
  276. settings_clear_last_bt_bda();
  277. bt_a2dp_sink_disconnect_current();
  278. #endif
  279. }
  280. }
  281. cJSON_Delete(json);
  282. const char *resp = "{\"success\":true}";
  283. httpd_resp_set_type(req, "application/json");
  284. httpd_resp_send(req, resp, strlen(resp));
  285. return ESP_OK;
  286. }
  287. static esp_err_t wifi_scan_handler(httpd_req_t *req) {
  288. wifi_ap_record_t *ap_list = NULL;
  289. uint16_t ap_count = 0;
  290. cJSON *json = cJSON_CreateObject();
  291. esp_err_t err = wifi_scan(&ap_list, &ap_count);
  292. if (err == ESP_OK && ap_list) {
  293. cJSON *networks = cJSON_CreateArray();
  294. for (uint16_t i = 0; i < ap_count; i++) {
  295. cJSON *net = cJSON_CreateObject();
  296. cJSON_AddStringToObject(net, "ssid", (char *)ap_list[i].ssid);
  297. cJSON_AddNumberToObject(net, "rssi", ap_list[i].rssi);
  298. cJSON_AddNumberToObject(net, "channel", ap_list[i].primary);
  299. cJSON_AddItemToArray(networks, net);
  300. }
  301. cJSON_AddItemToObject(json, "networks", networks);
  302. cJSON_AddBoolToObject(json, "success", true);
  303. free(ap_list);
  304. } else {
  305. cJSON_AddBoolToObject(json, "success", false);
  306. cJSON_AddStringToObject(json, "error", esp_err_to_name(err));
  307. }
  308. char *json_str = cJSON_Print(json);
  309. httpd_resp_set_type(req, "application/json");
  310. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  311. free(json_str);
  312. cJSON_Delete(json);
  313. return ESP_OK;
  314. }
  315. static esp_err_t wifi_config_handler(httpd_req_t *req) {
  316. char content[512];
  317. int ret = httpd_req_recv(req, content, sizeof(content) - 1);
  318. if (ret <= 0) {
  319. httpd_resp_send_500(req);
  320. return ESP_FAIL;
  321. }
  322. content[ret] = '\0';
  323. cJSON *json = cJSON_Parse(content);
  324. if (!json) {
  325. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
  326. return ESP_FAIL;
  327. }
  328. cJSON *ssid_json = cJSON_GetObjectItem(json, "ssid");
  329. cJSON *password_json = cJSON_GetObjectItem(json, "password");
  330. cJSON *response = cJSON_CreateObject();
  331. if (ssid_json && cJSON_IsString(ssid_json)) {
  332. const char *ssid = cJSON_GetStringValue(ssid_json);
  333. const char *password = password_json && cJSON_IsString(password_json)
  334. ? cJSON_GetStringValue(password_json)
  335. : "";
  336. esp_err_t err = settings_set_wifi_credentials(ssid, password);
  337. if (err == ESP_OK) {
  338. cJSON_AddBoolToObject(response, "success", true);
  339. ESP_LOGI(TAG, "WiFi credentials saved. We are restarting...");
  340. // Schedule restart
  341. vTaskDelay(pdMS_TO_TICKS(1000));
  342. esp_restart();
  343. } else {
  344. cJSON_AddBoolToObject(response, "success", false);
  345. cJSON_AddStringToObject(response, "error", esp_err_to_name(err));
  346. }
  347. } else {
  348. cJSON_AddBoolToObject(response, "success", false);
  349. cJSON_AddStringToObject(response, "error", "Invalid SSID");
  350. }
  351. char *json_str = cJSON_Print(response);
  352. httpd_resp_set_type(req, "application/json");
  353. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  354. free(json_str);
  355. cJSON_Delete(json);
  356. cJSON_Delete(response);
  357. return ESP_OK;
  358. }
  359. static esp_err_t device_name_handler(httpd_req_t *req) {
  360. char content[256];
  361. int ret = httpd_req_recv(req, content, sizeof(content) - 1);
  362. if (ret <= 0) {
  363. httpd_resp_send_500(req);
  364. return ESP_FAIL;
  365. }
  366. content[ret] = '\0';
  367. cJSON *json = cJSON_Parse(content);
  368. if (!json) {
  369. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
  370. return ESP_FAIL;
  371. }
  372. cJSON *name_json = cJSON_GetObjectItem(json, "name");
  373. cJSON *response = cJSON_CreateObject();
  374. if (name_json && cJSON_IsString(name_json)) {
  375. const char *name = cJSON_GetStringValue(name_json);
  376. esp_err_t err = settings_set_device_name(name);
  377. if (err == ESP_OK) {
  378. wifi_set_hostname(name);
  379. ethernet_set_hostname(name);
  380. cJSON_AddBoolToObject(response, "success", true);
  381. } else {
  382. cJSON_AddBoolToObject(response, "success", false);
  383. cJSON_AddStringToObject(response, "error", esp_err_to_name(err));
  384. }
  385. } else {
  386. cJSON_AddBoolToObject(response, "success", false);
  387. cJSON_AddStringToObject(response, "error", "Invalid name");
  388. }
  389. char *json_str = cJSON_Print(response);
  390. httpd_resp_set_type(req, "application/json");
  391. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  392. free(json_str);
  393. cJSON_Delete(json);
  394. cJSON_Delete(response);
  395. return ESP_OK;
  396. }
  397. static esp_err_t led_brightness_get_handler(httpd_req_t *req) {
  398. cJSON *json = cJSON_CreateObject();
  399. cJSON_AddNumberToObject(json, "brightness", led_get_brightness());
  400. cJSON_AddBoolToObject(json, "success", true);
  401. char *json_str = cJSON_Print(json);
  402. httpd_resp_set_type(req, "application/json");
  403. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  404. free(json_str);
  405. cJSON_Delete(json);
  406. return ESP_OK;
  407. }
  408. static esp_err_t led_brightness_post_handler(httpd_req_t *req) {
  409. char content[64];
  410. int ret = httpd_req_recv(req, content, sizeof(content) - 1);
  411. if (ret <= 0) {
  412. httpd_resp_send_500(req);
  413. return ESP_FAIL;
  414. }
  415. content[ret] = '\0';
  416. cJSON *json = cJSON_Parse(content);
  417. if (!json) {
  418. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
  419. return ESP_FAIL;
  420. }
  421. cJSON *response = cJSON_CreateObject();
  422. cJSON *val = cJSON_GetObjectItem(json, "brightness");
  423. if (val && cJSON_IsNumber(val)) {
  424. int b = (int)val->valuedouble;
  425. if (b < 0) {
  426. b = 0;
  427. }
  428. if (b > 255) {
  429. b = 255;
  430. }
  431. esp_err_t err = led_set_brightness((uint8_t)b);
  432. if (err == ESP_OK) {
  433. cJSON_AddBoolToObject(response, "success", true);
  434. } else {
  435. cJSON_AddBoolToObject(response, "success", false);
  436. cJSON_AddStringToObject(response, "error", esp_err_to_name(err));
  437. }
  438. } else {
  439. cJSON_AddBoolToObject(response, "success", false);
  440. cJSON_AddStringToObject(response, "error",
  441. "Expected {\"brightness\": 0-255}");
  442. }
  443. char *json_str = cJSON_Print(response);
  444. httpd_resp_set_type(req, "application/json");
  445. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  446. free(json_str);
  447. cJSON_Delete(json);
  448. cJSON_Delete(response);
  449. return ESP_OK;
  450. }
  451. static esp_err_t ota_update_handler(httpd_req_t *req) {
  452. if (req->content_len == 0) {
  453. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "No firmware uploaded");
  454. return ESP_FAIL;
  455. }
  456. // Stop AirPlay to free resources during OTA
  457. ESP_LOGI(TAG, "Stopping AirPlay for OTA update");
  458. rtsp_server_stop();
  459. esp_err_t err = ota_start_from_http(req);
  460. if (err != ESP_OK) {
  461. httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
  462. esp_err_to_name(err));
  463. return ESP_FAIL;
  464. }
  465. // Send response before restarting
  466. httpd_resp_sendstr(req, "Firmware update complete, rebooting now!\n");
  467. vTaskDelay(pdMS_TO_TICKS(500));
  468. esp_restart();
  469. return ESP_OK;
  470. }
  471. static esp_err_t system_info_handler(httpd_req_t *req) {
  472. cJSON *json = cJSON_CreateObject();
  473. cJSON *info = cJSON_CreateObject();
  474. char ip_str[16] = {0};
  475. char mac_str[18] = {0};
  476. char device_name[65] = {0};
  477. bool wifi_connected = wifi_is_connected();
  478. bool eth_connected = ethernet_is_connected();
  479. // Show IP and MAC for the active interface
  480. if (eth_connected) {
  481. ethernet_get_ip_str(ip_str, sizeof(ip_str));
  482. ethernet_get_mac_str(mac_str, sizeof(mac_str));
  483. } else {
  484. wifi_get_ip_str(ip_str, sizeof(ip_str));
  485. wifi_get_mac_str(mac_str, sizeof(mac_str));
  486. }
  487. settings_get_device_name(device_name, sizeof(device_name));
  488. cJSON_AddStringToObject(info, "ip", ip_str);
  489. cJSON_AddStringToObject(info, "mac", mac_str);
  490. cJSON_AddStringToObject(info, "device_name", device_name);
  491. cJSON_AddBoolToObject(info, "wifi_connected", wifi_connected);
  492. cJSON_AddBoolToObject(info, "eth_connected", eth_connected);
  493. cJSON_AddNumberToObject(info, "free_heap", esp_get_free_heap_size());
  494. // WiFi link diagnostics (only meaningful when associated as STA)
  495. if (wifi_connected) {
  496. wifi_ap_record_t ap;
  497. if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
  498. char ssid_buf[33];
  499. size_t slen = strnlen((const char *)ap.ssid, sizeof(ap.ssid));
  500. if (slen > sizeof(ssid_buf) - 1) {
  501. slen = sizeof(ssid_buf) - 1;
  502. }
  503. memcpy(ssid_buf, ap.ssid, slen);
  504. ssid_buf[slen] = '\0';
  505. char bssid_buf[18];
  506. snprintf(bssid_buf, sizeof(bssid_buf), "%02x:%02x:%02x:%02x:%02x:%02x",
  507. ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4],
  508. ap.bssid[5]);
  509. const char *phy = "?";
  510. if (ap.phy_11n) {
  511. phy = "11n";
  512. } else if (ap.phy_11g) {
  513. phy = "11g";
  514. } else if (ap.phy_11b) {
  515. phy = "11b";
  516. } else if (ap.phy_lr) {
  517. phy = "LR";
  518. }
  519. cJSON_AddStringToObject(info, "wifi_ssid", ssid_buf);
  520. cJSON_AddStringToObject(info, "wifi_bssid", bssid_buf);
  521. cJSON_AddNumberToObject(info, "wifi_rssi", ap.rssi);
  522. cJSON_AddNumberToObject(info, "wifi_channel", ap.primary);
  523. cJSON_AddStringToObject(info, "wifi_phy", phy);
  524. }
  525. }
  526. const esp_app_desc_t *app_desc = esp_app_get_description();
  527. cJSON_AddStringToObject(info, "firmware_version", app_desc->version);
  528. #ifdef CONFIG_DAC_TAS58XX
  529. cJSON_AddBoolToObject(info, "eq_supported", true);
  530. #else
  531. cJSON_AddBoolToObject(info, "eq_supported", false);
  532. #endif
  533. cJSON_AddItemToObject(json, "info", info);
  534. cJSON_AddBoolToObject(json, "success", true);
  535. char *json_str = cJSON_Print(json);
  536. httpd_resp_set_type(req, "application/json");
  537. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  538. free(json_str);
  539. cJSON_Delete(json);
  540. return ESP_OK;
  541. }
  542. static esp_err_t system_restart_handler(httpd_req_t *req) {
  543. cJSON *json = cJSON_CreateObject();
  544. cJSON_AddBoolToObject(json, "success", true);
  545. char *json_str = cJSON_Print(json);
  546. httpd_resp_set_type(req, "application/json");
  547. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  548. free(json_str);
  549. cJSON_Delete(json);
  550. ESP_LOGI(TAG, "Restart requested via web interface");
  551. vTaskDelay(pdMS_TO_TICKS(500));
  552. esp_restart();
  553. return ESP_OK;
  554. }
  555. /* ================================================================== */
  556. /* SPIFFS File Management API */
  557. /* ================================================================== */
  558. // Allowed path prefixes for file upload (prevent writes outside SPIFFS)
  559. static const char *ALLOWED_PREFIXES[] = {"/spiffs/"};
  560. static bool is_path_allowed(const char *path) {
  561. for (int i = 0; i < sizeof(ALLOWED_PREFIXES) / sizeof(ALLOWED_PREFIXES[0]);
  562. i++) {
  563. if (strncmp(path, ALLOWED_PREFIXES[i], strlen(ALLOWED_PREFIXES[i])) == 0) {
  564. // Reject path traversal
  565. if (strstr(path, "..") != NULL) {
  566. return false;
  567. }
  568. return true;
  569. }
  570. }
  571. return false;
  572. }
  573. static esp_err_t fs_upload_handler(httpd_req_t *req) {
  574. // Get target path from query string
  575. char query[128] = {0};
  576. char path[64] = {0};
  577. if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK ||
  578. httpd_query_key_value(query, "path", path, sizeof(path)) != ESP_OK) {
  579. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
  580. "Missing 'path' query parameter");
  581. return ESP_FAIL;
  582. }
  583. if (!is_path_allowed(path)) {
  584. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Path not allowed");
  585. return ESP_FAIL;
  586. }
  587. if (req->content_len == 0 || req->content_len > (size_t)(64 * 1024)) {
  588. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Body required (max 64KB)");
  589. return ESP_FAIL;
  590. }
  591. FILE *f = fopen(path, "wb");
  592. if (!f) {
  593. ESP_LOGE(TAG, "Failed to create %s", path);
  594. httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
  595. "Failed to create file");
  596. return ESP_FAIL;
  597. }
  598. char buf[SPIFFS_CHUNK_SIZE];
  599. size_t remaining = req->content_len;
  600. while (remaining > 0) {
  601. size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf);
  602. int received = httpd_req_recv(req, buf, to_read);
  603. if (received <= 0) {
  604. fclose(f);
  605. remove(path);
  606. httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
  607. "Receive failed");
  608. return ESP_FAIL;
  609. }
  610. fwrite(buf, 1, (size_t)received, f);
  611. remaining -= (size_t)received;
  612. }
  613. fclose(f);
  614. ESP_LOGI(TAG, "Uploaded %u bytes to %s", (unsigned)req->content_len, path);
  615. cJSON *json = cJSON_CreateObject();
  616. cJSON_AddBoolToObject(json, "success", true);
  617. cJSON_AddNumberToObject(json, "size", (double)req->content_len);
  618. cJSON_AddStringToObject(json, "path", path);
  619. char *json_str = cJSON_Print(json);
  620. httpd_resp_set_type(req, "application/json");
  621. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  622. free(json_str);
  623. cJSON_Delete(json);
  624. return ESP_OK;
  625. }
  626. static esp_err_t fs_delete_handler(httpd_req_t *req) {
  627. char query[128] = {0};
  628. char path[64] = {0};
  629. if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK ||
  630. httpd_query_key_value(query, "path", path, sizeof(path)) != ESP_OK) {
  631. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
  632. "Missing 'path' query parameter");
  633. return ESP_FAIL;
  634. }
  635. if (!is_path_allowed(path)) {
  636. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Path not allowed");
  637. return ESP_FAIL;
  638. }
  639. cJSON *json = cJSON_CreateObject();
  640. if (remove(path) == 0) {
  641. ESP_LOGI(TAG, "Deleted %s", path);
  642. cJSON_AddBoolToObject(json, "success", true);
  643. } else {
  644. cJSON_AddBoolToObject(json, "success", false);
  645. cJSON_AddStringToObject(json, "error", "File not found");
  646. }
  647. char *json_str = cJSON_Print(json);
  648. httpd_resp_set_type(req, "application/json");
  649. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  650. free(json_str);
  651. cJSON_Delete(json);
  652. return ESP_OK;
  653. }
  654. static esp_err_t fs_list_handler(httpd_req_t *req) {
  655. char query[128] = {0};
  656. char dir_path[64] = "/spiffs";
  657. if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) {
  658. httpd_query_key_value(query, "dir", dir_path, sizeof(dir_path));
  659. }
  660. if (!is_path_allowed(dir_path) && strcmp(dir_path, "/spiffs") != 0) {
  661. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Path not allowed");
  662. return ESP_FAIL;
  663. }
  664. DIR *d = opendir(dir_path);
  665. cJSON *json = cJSON_CreateObject();
  666. cJSON *files = cJSON_CreateArray();
  667. if (d) {
  668. struct dirent *entry;
  669. while ((entry = readdir(d)) != NULL) {
  670. cJSON *item = cJSON_CreateObject();
  671. cJSON_AddStringToObject(item, "name", entry->d_name);
  672. char full_path[320];
  673. snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
  674. struct stat st;
  675. if (stat(full_path, &st) == 0) {
  676. cJSON_AddNumberToObject(item, "size", (double)st.st_size);
  677. }
  678. cJSON_AddItemToArray(files, item);
  679. }
  680. closedir(d);
  681. cJSON_AddBoolToObject(json, "success", true);
  682. } else {
  683. cJSON_AddBoolToObject(json, "success", false);
  684. cJSON_AddStringToObject(json, "error", "Cannot open directory");
  685. }
  686. cJSON_AddItemToObject(json, "files", files);
  687. char *json_str = cJSON_Print(json);
  688. httpd_resp_set_type(req, "application/json");
  689. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  690. free(json_str);
  691. cJSON_Delete(json);
  692. return ESP_OK;
  693. }
  694. /* ================================================================== */
  695. /* EQ Page + API (only when TAS58xx DAC is configured) */
  696. /* ================================================================== */
  697. #ifdef CONFIG_DAC_TAS58XX
  698. static esp_err_t eq_page_handler(httpd_req_t *req) {
  699. return serve_spiffs_file(req, "/spiffs/www/eq.html", "text/html");
  700. }
  701. static esp_err_t eq_get_handler(httpd_req_t *req) {
  702. cJSON *json = cJSON_CreateObject();
  703. cJSON *arr = cJSON_CreateArray();
  704. float gains[SETTINGS_EQ_BANDS];
  705. if (settings_get_eq_gains(gains) == ESP_OK) {
  706. for (int i = 0; i < SETTINGS_EQ_BANDS; i++) {
  707. cJSON_AddItemToArray(arr, cJSON_CreateNumber((double)gains[i]));
  708. }
  709. } else {
  710. /* No saved EQ — return all zeros (flat) */
  711. for (int i = 0; i < SETTINGS_EQ_BANDS; i++) {
  712. cJSON_AddItemToArray(arr, cJSON_CreateNumber(0.0));
  713. }
  714. }
  715. cJSON_AddItemToObject(json, "gains", arr);
  716. cJSON_AddNumberToObject(json, "bands", SETTINGS_EQ_BANDS);
  717. cJSON_AddBoolToObject(json, "success", true);
  718. char *json_str = cJSON_Print(json);
  719. httpd_resp_set_type(req, "application/json");
  720. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  721. free(json_str);
  722. cJSON_Delete(json);
  723. return ESP_OK;
  724. }
  725. static esp_err_t eq_post_handler(httpd_req_t *req) {
  726. char content[512];
  727. int ret = httpd_req_recv(req, content, sizeof(content) - 1);
  728. if (ret <= 0) {
  729. httpd_resp_send_500(req);
  730. return ESP_FAIL;
  731. }
  732. content[ret] = '\0';
  733. cJSON *json = cJSON_Parse(content);
  734. if (!json) {
  735. httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
  736. return ESP_FAIL;
  737. }
  738. cJSON *response = cJSON_CreateObject();
  739. cJSON *gains_arr = cJSON_GetObjectItem(json, "gains");
  740. if (gains_arr && cJSON_IsArray(gains_arr) &&
  741. cJSON_GetArraySize(gains_arr) == SETTINGS_EQ_BANDS) {
  742. float gains[SETTINGS_EQ_BANDS];
  743. for (int i = 0; i < SETTINGS_EQ_BANDS; i++) {
  744. cJSON *item = cJSON_GetArrayItem(gains_arr, i);
  745. gains[i] = cJSON_IsNumber(item) ? (float)item->valuedouble : 0.0f;
  746. /* Clamp */
  747. if (gains[i] > 15.0f) {
  748. gains[i] = 15.0f;
  749. }
  750. if (gains[i] < -15.0f) {
  751. gains[i] = -15.0f;
  752. }
  753. }
  754. /* Emit event — listeners (settings + DAC) will handle it */
  755. eq_event_data_t ev_data;
  756. memcpy(ev_data.all_bands.gains_db, gains, sizeof(gains));
  757. eq_events_emit(EQ_EVENT_ALL_BANDS_SET, &ev_data);
  758. cJSON_AddBoolToObject(response, "success", true);
  759. } else {
  760. cJSON_AddBoolToObject(response, "success", false);
  761. cJSON_AddStringToObject(response, "error",
  762. "Expected 'gains' array with 15 values");
  763. }
  764. char *json_str = cJSON_Print(response);
  765. httpd_resp_set_type(req, "application/json");
  766. httpd_resp_send(req, json_str, HTTPD_RESP_USE_STRLEN);
  767. free(json_str);
  768. cJSON_Delete(json);
  769. cJSON_Delete(response);
  770. return ESP_OK;
  771. }
  772. #endif /* CONFIG_DAC_TAS58XX */
  773. esp_err_t web_server_start(uint16_t port) {
  774. if (s_server) {
  775. ESP_LOGW(TAG, "Web server already running");
  776. return ESP_OK;
  777. }
  778. rtsp_events_register(web_server_rtsp_event_cb, NULL);
  779. httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  780. config.server_port = port;
  781. #ifdef CONFIG_BT_ENABLED
  782. config.max_open_sockets = 2; // BT: tighter socket budget (LWIP 12)
  783. config.send_wait_timeout = 10; // BT/WiFi coexistence slows TCP drain
  784. #else
  785. config.max_open_sockets = 3; // Limit to save lwIP socket slots for AirPlay
  786. #endif
  787. config.lru_purge_enable = true; // Reclaim stale sockets when all are in use
  788. config.max_uri_handlers =
  789. 28; // Room for captive portal + EQ + speedtest + brightness
  790. config.max_resp_headers = 8;
  791. config.stack_size = 8192;
  792. esp_err_t err = httpd_start(&s_server, &config);
  793. if (err != ESP_OK) {
  794. ESP_LOGE(TAG, "Failed to start web server: %s", esp_err_to_name(err));
  795. return err;
  796. }
  797. // Register handlers
  798. httpd_uri_t root_uri = {
  799. .uri = "/", .method = HTTP_GET, .handler = root_handler};
  800. httpd_register_uri_handler(s_server, &root_uri);
  801. httpd_uri_t favicon_uri = {
  802. .uri = "/favicon.ico", .method = HTTP_GET, .handler = favicon_handler};
  803. httpd_register_uri_handler(s_server, &favicon_uri);
  804. httpd_uri_t logs_uri = {
  805. .uri = "/logs", .method = HTTP_GET, .handler = logs_page_handler};
  806. httpd_register_uri_handler(s_server, &logs_uri);
  807. httpd_uri_t speedtest_page_uri = {.uri = "/speedtest",
  808. .method = HTTP_GET,
  809. .handler = speedtest_page_handler};
  810. httpd_register_uri_handler(s_server, &speedtest_page_uri);
  811. httpd_uri_t speedtest_ping_uri = {.uri = "/api/speedtest/ping",
  812. .method = HTTP_GET,
  813. .handler = speedtest_ping_handler};
  814. httpd_register_uri_handler(s_server, &speedtest_ping_uri);
  815. httpd_uri_t speedtest_dl_uri = {.uri = "/api/speedtest/download",
  816. .method = HTTP_GET,
  817. .handler = speedtest_download_handler};
  818. httpd_register_uri_handler(s_server, &speedtest_dl_uri);
  819. httpd_uri_t speedtest_ul_uri = {.uri = "/api/speedtest/upload",
  820. .method = HTTP_POST,
  821. .handler = speedtest_upload_handler};
  822. httpd_register_uri_handler(s_server, &speedtest_ul_uri);
  823. httpd_uri_t wifi_scan_uri = {.uri = "/api/wifi/scan",
  824. .method = HTTP_GET,
  825. .handler = wifi_scan_handler};
  826. httpd_register_uri_handler(s_server, &wifi_scan_uri);
  827. httpd_uri_t wifi_config_uri = {.uri = "/api/wifi/config",
  828. .method = HTTP_POST,
  829. .handler = wifi_config_handler};
  830. httpd_register_uri_handler(s_server, &wifi_config_uri);
  831. httpd_uri_t device_name_uri = {.uri = "/api/device/name",
  832. .method = HTTP_POST,
  833. .handler = device_name_handler};
  834. httpd_register_uri_handler(s_server, &device_name_uri);
  835. httpd_uri_t led_brightness_get_uri = {.uri = "/api/led/brightness",
  836. .method = HTTP_GET,
  837. .handler = led_brightness_get_handler};
  838. httpd_register_uri_handler(s_server, &led_brightness_get_uri);
  839. httpd_uri_t led_brightness_post_uri = {.uri = "/api/led/brightness",
  840. .method = HTTP_POST,
  841. .handler =
  842. led_brightness_post_handler};
  843. httpd_register_uri_handler(s_server, &led_brightness_post_uri);
  844. httpd_uri_t ota_uri = {.uri = "/api/ota/update",
  845. .method = HTTP_POST,
  846. .handler = ota_update_handler};
  847. httpd_register_uri_handler(s_server, &ota_uri);
  848. httpd_uri_t system_info_uri = {.uri = "/api/system/info",
  849. .method = HTTP_GET,
  850. .handler = system_info_handler};
  851. httpd_register_uri_handler(s_server, &system_info_uri);
  852. httpd_uri_t system_restart_uri = {.uri = "/api/system/restart",
  853. .method = HTTP_POST,
  854. .handler = system_restart_handler};
  855. httpd_register_uri_handler(s_server, &system_restart_uri);
  856. // File management API
  857. httpd_uri_t fs_upload_uri = {.uri = "/api/fs/upload",
  858. .method = HTTP_POST,
  859. .handler = fs_upload_handler};
  860. httpd_register_uri_handler(s_server, &fs_upload_uri);
  861. httpd_uri_t fs_delete_uri = {.uri = "/api/fs/delete",
  862. .method = HTTP_POST,
  863. .handler = fs_delete_handler};
  864. httpd_register_uri_handler(s_server, &fs_delete_uri);
  865. httpd_uri_t fs_list_uri = {
  866. .uri = "/api/fs/list", .method = HTTP_GET, .handler = fs_list_handler};
  867. httpd_register_uri_handler(s_server, &fs_list_uri);
  868. httpd_uri_t api_status_uri = {
  869. .uri = "/api/status", .method = HTTP_GET, .handler = api_status_handler};
  870. httpd_register_uri_handler(s_server, &api_status_uri);
  871. httpd_uri_t api_control_uri = {
  872. .uri = "/api/control", .method = HTTP_POST, .handler = api_control_handler};
  873. httpd_register_uri_handler(s_server, &api_control_uri);
  874. // Captive portal detection endpoints
  875. // Apple iOS/macOS
  876. httpd_uri_t apple_captive1 = {.uri = "/hotspot-detect.html",
  877. .method = HTTP_GET,
  878. .handler = captive_apple_handler};
  879. httpd_register_uri_handler(s_server, &apple_captive1);
  880. httpd_uri_t apple_captive2 = {.uri = "/library/test/success.html",
  881. .method = HTTP_GET,
  882. .handler = captive_apple_handler};
  883. httpd_register_uri_handler(s_server, &apple_captive2);
  884. // Android
  885. httpd_uri_t android_captive = {.uri = "/generate_204",
  886. .method = HTTP_GET,
  887. .handler = captive_android_handler};
  888. httpd_register_uri_handler(s_server, &android_captive);
  889. // Windows
  890. httpd_uri_t windows_captive = {.uri = "/connecttest.txt",
  891. .method = HTTP_GET,
  892. .handler = captive_windows_handler};
  893. httpd_register_uri_handler(s_server, &windows_captive);
  894. #ifdef CONFIG_DAC_TAS58XX
  895. httpd_uri_t eq_page_uri = {
  896. .uri = "/eq", .method = HTTP_GET, .handler = eq_page_handler};
  897. httpd_register_uri_handler(s_server, &eq_page_uri);
  898. httpd_uri_t eq_get_uri = {
  899. .uri = "/api/eq", .method = HTTP_GET, .handler = eq_get_handler};
  900. httpd_register_uri_handler(s_server, &eq_get_uri);
  901. httpd_uri_t eq_post_uri = {
  902. .uri = "/api/eq", .method = HTTP_POST, .handler = eq_post_handler};
  903. httpd_register_uri_handler(s_server, &eq_post_uri);
  904. #endif
  905. log_stream_register(s_server);
  906. ESP_LOGI(TAG, "Web server started on port %d with captive portal support",
  907. port);
  908. return ESP_OK;
  909. }
  910. void web_server_stop(void) {
  911. if (s_server) {
  912. httpd_stop(s_server);
  913. s_server = NULL;
  914. ESP_LOGI(TAG, "Web server stopped");
  915. }
  916. }