#include "maze32_web.h" #include #include #include "esp_log.h" #define MAZE32_STATIC_CHUNK_SIZE 1024 static const char* TAG = "maze32_web"; typedef struct { const char* path; const char* content_type; } maze32_static_asset_t; static const maze32_static_asset_t INDEX_ASSET = {"/littlefs/index.html.gz", "text/html; charset=utf-8"}; static const maze32_static_asset_t PLAYER_STYLE_ASSET = {"/littlefs/player.css.gz", "text/css"}; static const maze32_static_asset_t PLAYER_SCRIPT_ASSET = {"/littlefs/player.js.gz", "text/javascript; charset=utf-8"}; static const maze32_static_asset_t ICON_ASSET = {"/littlefs/icons.svg.gz", "image/svg+xml"}; static void preserve_static_context(void* context) { (void)context; } static esp_err_t send_static_asset(httpd_req_t* request) { const maze32_static_asset_t* asset = request->user_ctx; if (asset == NULL) { return ESP_ERR_INVALID_ARG; } FILE* file = fopen(asset->path, "rb"); if (file == NULL) { ESP_LOGE(TAG, "Static asset is unavailable: %s", asset->path); httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Приложение временно недоступно"); return ESP_FAIL; } httpd_resp_set_type(request, asset->content_type); httpd_resp_set_hdr(request, "Content-Encoding", "gzip"); httpd_resp_set_hdr(request, "Cache-Control", "no-cache"); char buffer[MAZE32_STATIC_CHUNK_SIZE]; size_t bytes_read; esp_err_t result = ESP_OK; while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) { result = httpd_resp_send_chunk(request, buffer, bytes_read); if (result != ESP_OK) { break; } } if (ferror(file) != 0) { ESP_LOGE(TAG, "Failed while reading static asset: %s", asset->path); result = ESP_FAIL; } fclose(file); if (result == ESP_OK) { result = httpd_resp_send_chunk(request, NULL, 0); } return result; } static esp_err_t send_health(httpd_req_t* request) { const maze32_health_t* health = request->user_ctx; char response[192]; int length = snprintf(response, sizeof(response), "{\"status\":\"%s\",\"healthy\":%s,\"version\":\"%s\",\"errorCode\":%d}", maze32_health_state_name(health->state), maze32_health_is_healthy(health) ? "true" : "false", health->version, health->error_code); if (length < 0 || (size_t)length >= sizeof(response)) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Диагностический ответ слишком большой"); return ESP_FAIL; } httpd_resp_set_type(request, "application/json"); httpd_resp_set_hdr(request, "Cache-Control", "no-store"); return httpd_resp_send(request, response, length); } static esp_err_t send_diagnostics(httpd_req_t* request) { maze32_gateway_t* gateway = request->user_ctx; char response[1024]; size_t length = maze32_gateway_format_diagnostics(gateway, response, sizeof(response)); if (length == 0U) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Диагностика временно недоступна"); return ESP_FAIL; } httpd_resp_set_type(request, "application/json"); httpd_resp_set_hdr(request, "Cache-Control", "no-store"); return httpd_resp_send(request, response, (ssize_t)length); } esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health, maze32_gateway_t* gateway) { if (server == NULL || health == NULL || gateway == NULL) { return ESP_ERR_INVALID_ARG; } httpd_config_t configuration = HTTPD_DEFAULT_CONFIG(); configuration.max_uri_handlers = 13; configuration.max_open_sockets = 9; configuration.lru_purge_enable = false; configuration.recv_wait_timeout = 5; configuration.send_wait_timeout = 5; configuration.global_user_ctx = gateway; configuration.global_user_ctx_free_fn = preserve_static_context; configuration.close_fn = maze32_gateway_close_socket; esp_err_t result = httpd_start(server, &configuration); if (result != ESP_OK) { return result; } const httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = send_static_asset, .user_ctx = (void*)&INDEX_ASSET, }; const httpd_uri_t index = { .uri = "/index.html", .method = HTTP_GET, .handler = send_static_asset, .user_ctx = (void*)&INDEX_ASSET, }; const httpd_uri_t player_style = { .uri = "/player.css", .method = HTTP_GET, .handler = send_static_asset, .user_ctx = (void*)&PLAYER_STYLE_ASSET, }; const httpd_uri_t player_script = { .uri = "/player.js", .method = HTTP_GET, .handler = send_static_asset, .user_ctx = (void*)&PLAYER_SCRIPT_ASSET, }; const httpd_uri_t icons = { .uri = "/icons.svg", .method = HTTP_GET, .handler = send_static_asset, .user_ctx = (void*)&ICON_ASSET, }; const httpd_uri_t health_uri = { .uri = "/health", .method = HTTP_GET, .handler = send_health, .user_ctx = health, }; const httpd_uri_t diagnostics_uri = { .uri = "/diagnostics", .method = HTTP_GET, .handler = send_diagnostics, .user_ctx = gateway, }; result = httpd_register_uri_handler(*server, &root); if (result == ESP_OK) { result = httpd_register_uri_handler(*server, &index); } if (result == ESP_OK) { result = httpd_register_uri_handler(*server, &player_style); } if (result == ESP_OK) { result = httpd_register_uri_handler(*server, &player_script); } if (result == ESP_OK) { result = httpd_register_uri_handler(*server, &icons); } if (result == ESP_OK) { result = httpd_register_uri_handler(*server, &health_uri); } if (result == ESP_OK) { result = httpd_register_uri_handler(*server, &diagnostics_uri); } if (result != ESP_OK) { httpd_stop(*server); *server = NULL; return result; } ESP_LOGI(TAG, "HTTP server ready with static and health endpoints"); return ESP_OK; }