dmx-interface/components/web_server/src/web_server.c
2026-03-05 23:05:35 +01:00

243 lines
6.5 KiB
C

#include "web_server.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "storage.h"
static const char *TAG = "WEBSERVER";
// Default configuration values
#define WEBSERVER_DEFAULT_PORT 80
#define WEBSERVER_DEFAULT_MAX_HANDLERS 32
#define WEBSERVER_DEFAULT_STACK_SIZE (8 * 1024)
#define WEBSERVER_DEFAULT_TASK_PRIORITY 5
static httpd_handle_t s_server_handle = NULL;
static TaskHandle_t s_server_task_handle = NULL;
/**
* @brief Get MIME type based on file extension
*/
static const char *get_mime_type(const char *filename) {
const char *dot = strrchr(filename, '.');
if (!dot)
return "application/octet-stream";
if (strcmp(dot, ".html") == 0)
return "text/html";
if (strcmp(dot, ".css") == 0)
return "text/css";
if (strcmp(dot, ".js") == 0)
return "application/javascript";
if (strcmp(dot, ".json") == 0)
return "application/json";
if (strcmp(dot, ".png") == 0)
return "image/png";
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return "image/jpeg";
if (strcmp(dot, ".gif") == 0)
return "image/gif";
if (strcmp(dot, ".svg") == 0)
return "image/svg+xml";
if (strcmp(dot, ".ico") == 0)
return "image/x-icon";
if (strcmp(dot, ".txt") == 0)
return "text/plain";
if (strcmp(dot, ".xml") == 0)
return "application/xml";
if (strcmp(dot, ".wav") == 0)
return "audio/wav";
if (strcmp(dot, ".mp3") == 0)
return "audio/mpeg";
return "application/octet-stream";
}
/**
* @brief HTTP handler for static files from LittleFS
*/
static esp_err_t static_file_handler(httpd_req_t *req) {
// Build the file path
char filepath[1024];
snprintf(filepath, sizeof(filepath), "%s%s", storage_get_mount_point(),
req->uri);
// Handle root path
if (strcmp(req->uri, "/") == 0) {
snprintf(filepath, sizeof(filepath), "%s/index.html",
storage_get_mount_point());
}
FILE *f = fopen(filepath, "r");
if (!f) {
ESP_LOGW(TAG, "File not found: %s", filepath);
httpd_resp_send_404(req);
return ESP_OK;
}
// Get MIME type and set content type
const char *mime_type = get_mime_type(filepath);
httpd_resp_set_type(req, mime_type);
// Send file in chunks
char buf[1024];
size_t read_len;
while ((read_len = fread(buf, 1, sizeof(buf), f)) > 0) {
if (httpd_resp_send_chunk(req, buf, read_len) != ESP_OK) {
ESP_LOGW(TAG, "Failed to send data chunk for %s", filepath);
break;
}
}
fclose(f);
httpd_resp_send_chunk(req, NULL, 0); // Send end marker
return ESP_OK;
}
/**
* @brief HTTP handler for API health check (GET /api/health)
*/
static esp_err_t health_check_handler(httpd_req_t *req) {
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
return ESP_OK;
}
/**
* @brief FreeRTOS task function for the HTTP server.
* Allows non-blocking server operation and future extensibility.
*/
static void webserver_task(void *arg) {
(void)arg; // Unused parameter
ESP_LOGI(TAG, "Web server task started");
// Keep task alive - the server runs in the background
while (s_server_handle != NULL) {
vTaskDelay(pdMS_TO_TICKS(10000)); // 10 second check interval
}
ESP_LOGI(TAG, "Web server task ending");
vTaskDelete(NULL);
}
httpd_handle_t webserver_start(const webserver_config_t *config) {
if (s_server_handle != NULL) {
ESP_LOGW(TAG, "Web server already running");
return s_server_handle;
}
// Initialize LittleFS
esp_err_t ret = storage_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize storage");
return NULL;
}
// Use provided config or defaults
uint16_t port = WEBSERVER_DEFAULT_PORT;
size_t max_handlers = WEBSERVER_DEFAULT_MAX_HANDLERS;
size_t stack_size = WEBSERVER_DEFAULT_STACK_SIZE;
UBaseType_t task_priority = WEBSERVER_DEFAULT_TASK_PRIORITY;
if (config) {
port = config->port;
max_handlers = config->max_uri_handlers;
stack_size = config->stack_size;
task_priority = config->task_priority;
}
// Create HTTP server configuration
httpd_config_t http_config = HTTPD_DEFAULT_CONFIG();
http_config.server_port = port;
http_config.max_uri_handlers = max_handlers;
http_config.stack_size = stack_size;
http_config.uri_match_fn = httpd_uri_match_wildcard;
// Start HTTP server
ret = httpd_start(&s_server_handle, &http_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(ret));
s_server_handle = NULL;
return NULL;
}
ESP_LOGI(TAG, "HTTP server started on port %d", port);
// Register default handlers
// Health check endpoint
httpd_uri_t health_uri = {
.uri = "/api/health",
.method = HTTP_GET,
.handler = health_check_handler,
.user_ctx = NULL,
};
httpd_register_uri_handler(s_server_handle, &health_uri);
// Wildcard handler for static files from LittleFS (must be last)
httpd_uri_t file_uri = {
.uri = "/*",
.method = HTTP_GET,
.handler = static_file_handler,
.user_ctx = NULL,
};
httpd_register_uri_handler(s_server_handle, &file_uri);
// Create FreeRTOS task for the server
// This allows other tasks to continue running and makes the server
// async-ready
BaseType_t task_ret = xTaskCreate(webserver_task, "webserver", stack_size,
(void *)s_server_handle, task_priority,
&s_server_task_handle);
if (task_ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create web server task");
httpd_stop(s_server_handle);
s_server_handle = NULL;
return NULL;
}
ESP_LOGI(TAG, "Web server initialized successfully");
return s_server_handle;
}
void webserver_stop(httpd_handle_t server) {
if (server == NULL) {
return;
}
httpd_stop(server);
s_server_handle = NULL;
// Wait for task to finish
if (s_server_task_handle != NULL) {
vTaskDelay(pdMS_TO_TICKS(100));
s_server_task_handle = NULL;
}
ESP_LOGI(TAG, "Web server stopped");
}
esp_err_t webserver_register_handler(httpd_handle_t server,
const httpd_uri_t *uri_handler) {
if (server == NULL || uri_handler == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t ret = httpd_register_uri_handler(server, uri_handler);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Registered handler: %s [%d]", uri_handler->uri,
uri_handler->method);
} else {
ESP_LOGE(TAG, "Failed to register handler %s: %s", uri_handler->uri,
esp_err_to_name(ret));
}
return ret;
}