diff --git a/CMakeLists.txt b/CMakeLists.txt index d622b7e..0a5481f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,20 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html # The following five lines of boilerplate have to be in your project's # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.16) +# Clear any stale EXTRA_COMPONENT_DIRS entries (e.g. leftover from previous runs or PlatformIO) +set(EXTRA_COMPONENT_DIRS "") + include($ENV{IDF_PATH}/tools/cmake/project.cmake) + project(dmx-interface) + +# Disable filesystem image creation for now +# Can be enabled later when data/ folder is properly configured +if(FALSE) + if(COMMAND littlefs_create_partition_image) + littlefs_create_partition_image(storage ${CMAKE_SOURCE_DIR}/data FLASH_IN_PROJECT) + endif() +endif() diff --git a/components/web_server/CMakeLists.txt b/components/web_server/CMakeLists.txt new file mode 100644 index 0000000..38c5583 --- /dev/null +++ b/components/web_server/CMakeLists.txt @@ -0,0 +1,8 @@ +idf_component_register( + SRCS "src/web_server.c" + "src/wifi.c" + INCLUDE_DIRS "include" + REQUIRES esp_http_server + PRIV_REQUIRES freertos esp_wifi esp_event esp_netif nvs_flash +) + diff --git a/components/web_server/include/web_server.h b/components/web_server/include/web_server.h new file mode 100644 index 0000000..3ce5ccc --- /dev/null +++ b/components/web_server/include/web_server.h @@ -0,0 +1,58 @@ +/** + * @file web_server.h + * @brief Simple HTTP web server component for ESP32 with async FreeRTOS support. + */ + +#pragma once + +#include "esp_http_server.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Web server configuration structure. + */ + typedef struct + { + uint16_t port; ///< HTTP server port (default: 80) + size_t max_uri_handlers; ///< Maximum number of URI handlers + size_t stack_size; ///< FreeRTOS task stack size + UBaseType_t task_priority; ///< FreeRTOS task priority + } webserver_config_t; + + /** + * @brief Initialize and start the HTTP web server. + * + * This function creates a FreeRTOS task that manages the HTTP server. + * It serves static files from the data/ folder and supports dynamic handler registration. + * + * @param config Configuration structure. If NULL, default values are used. + * @return HTTP server handle on success, NULL on failure. + */ + httpd_handle_t webserver_start(const webserver_config_t *config); + + /** + * @brief Stop the web server and cleanup resources. + * + * @param server HTTP server handle returned by webserver_start(). + * Safe to pass NULL. + */ + void webserver_stop(httpd_handle_t server); + + /** + * @brief Register a custom URI handler. + * + * This allows dynamic registration of API endpoints and other custom handlers. + * + * @param server HTTP server handle. + * @param uri_handler Pointer to httpd_uri_t structure. + * @return ESP_OK on success, error code otherwise. + */ + esp_err_t webserver_register_handler(httpd_handle_t server, const httpd_uri_t *uri_handler); + +#ifdef __cplusplus +} +#endif diff --git a/components/web_server/include/wifi.h b/components/web_server/include/wifi.h new file mode 100644 index 0000000..c023355 --- /dev/null +++ b/components/web_server/include/wifi.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel, uint8_t max_connections); + void wifi_stop_ap(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/web_server/src/web_server.c b/components/web_server/src/web_server.c new file mode 100644 index 0000000..5fc09c4 --- /dev/null +++ b/components/web_server/src/web_server.c @@ -0,0 +1,201 @@ +#include "web_server.h" + +#include +#include +#include +#include + +#include "esp_err.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.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 HTTP handler for root (GET /) + */ +static esp_err_t root_handler(httpd_req_t *req) +{ + const char *html = "" + "" + "DMX Interface" + "" + "

DMX Interface

" + "

Web server is running!

" + "

Check Health

" + "" + ""; + + httpd_resp_set_type(req, "text/html"); + httpd_resp_sendstr(req, html); + 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) +{ + httpd_handle_t server = (httpd_handle_t)arg; + 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; + } + + // 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 + esp_err_t 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); + + // Root / index.html handler + httpd_uri_t root_uri = { + .uri = "/", + .method = HTTP_GET, + .handler = root_handler, + .user_ctx = NULL, + }; + httpd_register_uri_handler(s_server_handle, &root_uri); + + // Wildcard handler for 404 (must be last) + httpd_uri_t wildcard_uri = { + .uri = "/*", + .method = HTTP_GET, + .handler = NULL, // Let httpd handle as 404 + .user_ctx = NULL, + }; + // Don't register wildcard - just let httpd default to 404 + + // 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; +} diff --git a/components/web_server/src/wifi.c b/components/web_server/src/wifi.c new file mode 100644 index 0000000..bff0e31 --- /dev/null +++ b/components/web_server/src/wifi.c @@ -0,0 +1,89 @@ +#include "wifi.h" + +#include + +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "nvs_flash.h" + +static const char *TAG = "WIFI"; +static bool s_wifi_started = false; + +esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel, uint8_t max_connections) +{ + if (s_wifi_started) + { + return ESP_OK; + } + + if (!ssid || strlen(ssid) == 0 || strlen(ssid) > 32) + { + return ESP_ERR_INVALID_ARG; + } + + const bool has_password = password && strlen(password) > 0; + if (has_password && strlen(password) < 8) + { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + if (err != ESP_OK) + { + return err; + } + + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_ap(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + wifi_config_t wifi_config = { + .ap = { + .channel = channel, + .max_connection = max_connections, + .authmode = has_password ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN, + .pmf_cfg = { + .required = false, + }, + }, + }; + + strlcpy((char *)wifi_config.ap.ssid, ssid, sizeof(wifi_config.ap.ssid)); + wifi_config.ap.ssid_len = strlen(ssid); + + if (has_password) + { + strlcpy((char *)wifi_config.ap.password, password, sizeof(wifi_config.ap.password)); + } + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + + s_wifi_started = true; + ESP_LOGI(TAG, "WiFi AP started: SSID=%s channel=%u", ssid, channel); + return ESP_OK; +} + +void wifi_stop_ap(void) +{ + if (!s_wifi_started) + { + return; + } + + esp_wifi_stop(); + esp_wifi_deinit(); + s_wifi_started = false; + ESP_LOGI(TAG, "WiFi AP stopped"); +} diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..cf76260 --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,21 @@ +dependencies: + idf: + source: + type: idf + version: 5.5.2 + joltwallet/littlefs: + component_hash: 150ca47225f7c9917f7f610a5f85e5e93fe3c15402234c23496ff045ad558b0b + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.20.2 +direct_dependencies: +- idf +- joltwallet/littlefs +manifest_hash: ff4b0b01cddb86fe710ecb8fe90983fdab6a922a91a7dcfade112bc73ef373e8 +target: esp32c6 +version: 2.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 2adde77..27cbf72 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,2 +1,4 @@ idf_component_register(SRCS "dmx-interface.c" - INCLUDE_DIRS ".") + INCLUDE_DIRS "." + REQUIRES web_server) + diff --git a/main/dmx-interface.c b/main/dmx-interface.c index 7b66f33..45969d1 100644 --- a/main/dmx-interface.c +++ b/main/dmx-interface.c @@ -1,6 +1,39 @@ #include +#include "esp_err.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "web_server.h" +#include "wifi.h" + +static const char *TAG = "MAIN"; + void app_main(void) { + ESP_LOGI(TAG, "DMX Interface starting..."); + esp_err_t wifi_err = wifi_start_ap("DMX", "mbgmbgmbg", 1, 4); + if (wifi_err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to start WiFi AP: %s", esp_err_to_name(wifi_err)); + return; + } + + // Start HTTP web server + httpd_handle_t server = webserver_start(NULL); + if (server == NULL) + { + ESP_LOGE(TAG, "Failed to start web server!"); + return; + } + + ESP_LOGI(TAG, "Web server started successfully"); + ESP_LOGI(TAG, "Open http://192.168.4.1 in your browser"); + + // Keep the app running + while (1) + { + vTaskDelay(pdMS_TO_TICKS(1000)); + } } diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..9201729 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,5 @@ +## IDF Component Manager Manifest File +dependencies: + idf: + version: ">=4.1.0" + joltwallet/littlefs: ==1.20.2