From ebf0dc6556a90881d5f66bb5fcf7ec22b2f71cba Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Mon, 5 Jan 2026 23:47:00 +0100 Subject: [PATCH] show website via mdns Signed-off-by: Peter Siegmund --- firmware/components/api-server/CMakeLists.txt | 14 + firmware/components/api-server/Kconfig | 38 + .../components/api-server/idf_component.yml | 5 + .../api-server/include/api_handlers.h | 63 ++ .../api-server/include/api_server.h | 119 +++ .../api-server/include/websocket_handler.h | 60 ++ .../components/api-server/src/api_handlers.c | 771 ++++++++++++++++++ .../components/api-server/src/api_server.c | 196 +++++ .../api-server/src/websocket_handler.c | 255 ++++++ .../connectivity-manager/CMakeLists.txt | 1 + .../connectivity-manager/src/wifi_manager.c | 5 + .../components/simulator/include/storage.h | 11 +- firmware/components/simulator/src/storage.cpp | 10 +- firmware/main/idf_component.yml | 3 +- firmware/sdkconfig.defaults | 3 + 15 files changed, 1549 insertions(+), 5 deletions(-) create mode 100644 firmware/components/api-server/CMakeLists.txt create mode 100644 firmware/components/api-server/Kconfig create mode 100644 firmware/components/api-server/idf_component.yml create mode 100644 firmware/components/api-server/include/api_handlers.h create mode 100644 firmware/components/api-server/include/api_server.h create mode 100644 firmware/components/api-server/include/websocket_handler.h create mode 100644 firmware/components/api-server/src/api_handlers.c create mode 100644 firmware/components/api-server/src/api_server.c create mode 100644 firmware/components/api-server/src/websocket_handler.c diff --git a/firmware/components/api-server/CMakeLists.txt b/firmware/components/api-server/CMakeLists.txt new file mode 100644 index 0000000..bee7a48 --- /dev/null +++ b/firmware/components/api-server/CMakeLists.txt @@ -0,0 +1,14 @@ +idf_component_register(SRCS + src/api_server.c + src/api_handlers.c + src/websocket_handler.c + INCLUDE_DIRS "include" + REQUIRES + esp_http_server + mdns + esp_wifi + esp_netif + esp_event + json + simulator +) diff --git a/firmware/components/api-server/Kconfig b/firmware/components/api-server/Kconfig new file mode 100644 index 0000000..06fe4aa --- /dev/null +++ b/firmware/components/api-server/Kconfig @@ -0,0 +1,38 @@ +menu "API Server Configuration" + + config API_SERVER_HOSTNAME + string "mDNS Hostname" + default "system-control" + help + The mDNS hostname for the API server. + The device will be accessible at .local + + config API_SERVER_PORT + int "HTTP Server Port" + default 80 + range 1 65535 + help + The port number for the HTTP server. + + config API_SERVER_MAX_WS_CLIENTS + int "Maximum WebSocket Clients" + default 4 + range 1 8 + help + Maximum number of concurrent WebSocket connections. + + config API_SERVER_ENABLE_CORS + bool "Enable CORS" + default y + help + Enable Cross-Origin Resource Sharing (CORS) headers. + This is required for web interfaces served from different origins. + + config API_SERVER_STATIC_FILES_PATH + string "Static Files Path" + default "/spiffs/www" + help + Base path for serving static web files. + +endmenu + diff --git a/firmware/components/api-server/idf_component.yml b/firmware/components/api-server/idf_component.yml new file mode 100644 index 0000000..3c6c200 --- /dev/null +++ b/firmware/components/api-server/idf_component.yml @@ -0,0 +1,5 @@ +dependencies: + idf: + version: '>=5.0.0' + espressif/mdns: + version: '*' diff --git a/firmware/components/api-server/include/api_handlers.h b/firmware/components/api-server/include/api_handlers.h new file mode 100644 index 0000000..03635e1 --- /dev/null +++ b/firmware/components/api-server/include/api_handlers.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Register all API handlers with the HTTP server + * + * @param server HTTP server handle + * @return esp_err_t ESP_OK on success + */ + esp_err_t api_handlers_register(httpd_handle_t server); + + // Capabilities API + esp_err_t api_capabilities_get_handler(httpd_req_t *req); + + // WiFi API + esp_err_t api_wifi_scan_handler(httpd_req_t *req); + esp_err_t api_wifi_config_handler(httpd_req_t *req); + esp_err_t api_wifi_status_handler(httpd_req_t *req); + + // Light Control API + esp_err_t api_light_power_handler(httpd_req_t *req); + esp_err_t api_light_thunder_handler(httpd_req_t *req); + esp_err_t api_light_mode_handler(httpd_req_t *req); + esp_err_t api_light_schema_handler(httpd_req_t *req); + esp_err_t api_light_status_handler(httpd_req_t *req); + + // LED Configuration API + esp_err_t api_wled_config_get_handler(httpd_req_t *req); + esp_err_t api_wled_config_post_handler(httpd_req_t *req); + + // Schema API + esp_err_t api_schema_get_handler(httpd_req_t *req); + esp_err_t api_schema_post_handler(httpd_req_t *req); + + // Devices API (Matter) + esp_err_t api_devices_scan_handler(httpd_req_t *req); + esp_err_t api_devices_pair_handler(httpd_req_t *req); + esp_err_t api_devices_paired_handler(httpd_req_t *req); + esp_err_t api_devices_update_handler(httpd_req_t *req); + esp_err_t api_devices_unpair_handler(httpd_req_t *req); + esp_err_t api_devices_toggle_handler(httpd_req_t *req); + + // Scenes API + esp_err_t api_scenes_get_handler(httpd_req_t *req); + esp_err_t api_scenes_post_handler(httpd_req_t *req); + esp_err_t api_scenes_delete_handler(httpd_req_t *req); + esp_err_t api_scenes_activate_handler(httpd_req_t *req); + + // Static file serving + esp_err_t api_static_file_handler(httpd_req_t *req); + + // Captive portal detection + esp_err_t api_captive_portal_handler(httpd_req_t *req); + +#ifdef __cplusplus +} +#endif diff --git a/firmware/components/api-server/include/api_server.h b/firmware/components/api-server/include/api_server.h new file mode 100644 index 0000000..1b1d85e --- /dev/null +++ b/firmware/components/api-server/include/api_server.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Configuration for the API server + */ + typedef struct + { + const char *hostname; ///< mDNS hostname (default: "system-control") + uint16_t port; ///< HTTP server port (default: 80) + const char *base_path; ///< Base path for static files (default: "/storage/www") + bool enable_cors; ///< Enable CORS headers (default: true) + } api_server_config_t; + +#ifdef CONFIG_API_SERVER_ENABLE_CORS +#define API_SERVER_ENABLE_CORS true +#else +#define API_SERVER_ENABLE_CORS false +#endif + +/** + * @brief Default configuration for the API server + */ +#define API_SERVER_CONFIG_DEFAULT() \ + { \ + .hostname = CONFIG_API_SERVER_HOSTNAME, \ + .port = CONFIG_API_SERVER_PORT, \ + .base_path = CONFIG_API_SERVER_STATIC_FILES_PATH, \ + .enable_cors = API_SERVER_ENABLE_CORS, \ + } + + /** + * @brief Initialize and start the API server with mDNS + * + * This function starts an HTTP server with: + * - REST API endpoints + * - WebSocket endpoint at /ws + * - mDNS registration (system-control.local) + * - Static file serving from SPIFFS + * + * @param config Pointer to server configuration, or NULL for defaults + * @return esp_err_t ESP_OK on success + */ + esp_err_t api_server_start(const api_server_config_t *config); + + /** + * @brief Stop the API server and mDNS + * + * @return esp_err_t ESP_OK on success + */ + esp_err_t api_server_stop(void); + + /** + * @brief Check if the API server is running + * + * @return true if server is running + */ + bool api_server_is_running(void); + + /** + * @brief Get the HTTP server handle + * + * @return httpd_handle_t Server handle, or NULL if not running + */ + httpd_handle_t api_server_get_handle(void); + + /** + * @brief Broadcast a message to all connected WebSocket clients + * + * @param message JSON message to broadcast + * @return esp_err_t ESP_OK on success + */ + esp_err_t api_server_ws_broadcast(const char *message); + + /** + * @brief Broadcast a status update to all WebSocket clients + * + * @param on Light power state + * @param mode Current mode (day/night/simulation) + * @param schema Active schema filename + * @param r Red value (0-255) + * @param g Green value (0-255) + * @param b Blue value (0-255) + * @return esp_err_t ESP_OK on success + */ + esp_err_t api_server_ws_broadcast_status(bool on, const char *mode, const char *schema, uint8_t r, uint8_t g, + uint8_t b); + + /** + * @brief Broadcast a color update to all WebSocket clients + * + * @param r Red value (0-255) + * @param g Green value (0-255) + * @param b Blue value (0-255) + * @return esp_err_t ESP_OK on success + */ + esp_err_t api_server_ws_broadcast_color(uint8_t r, uint8_t g, uint8_t b); + + /** + * @brief Broadcast a WiFi status update to all WebSocket clients + * + * @param connected Connection state + * @param ip IP address (can be NULL if not connected) + * @param rssi Signal strength + * @return esp_err_t ESP_OK on success + */ + esp_err_t api_server_ws_broadcast_wifi(bool connected, const char *ip, int rssi); + +#ifdef __cplusplus +} +#endif diff --git a/firmware/components/api-server/include/websocket_handler.h b/firmware/components/api-server/include/websocket_handler.h new file mode 100644 index 0000000..64b57bc --- /dev/null +++ b/firmware/components/api-server/include/websocket_handler.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * @brief Maximum number of concurrent WebSocket connections + */ +#define WS_MAX_CLIENTS CONFIG_API_SERVER_MAX_WS_CLIENTS + + /** + * @brief Initialize WebSocket handler + * + * @param server HTTP server handle + * @return esp_err_t ESP_OK on success + */ + esp_err_t websocket_handler_init(httpd_handle_t server); + + /** + * @brief WebSocket request handler + * + * @param req HTTP request + * @return esp_err_t ESP_OK on success + */ + esp_err_t websocket_handler(httpd_req_t *req); + + /** + * @brief Send message to a specific WebSocket client + * + * @param server HTTP server handle + * @param fd Socket file descriptor + * @param message Message to send + * @return esp_err_t ESP_OK on success + */ + esp_err_t websocket_send(httpd_handle_t server, int fd, const char *message); + + /** + * @brief Broadcast message to all connected WebSocket clients + * + * @param server HTTP server handle + * @param message Message to broadcast + * @return esp_err_t ESP_OK on success + */ + esp_err_t websocket_broadcast(httpd_handle_t server, const char *message); + + /** + * @brief Get number of connected WebSocket clients + * + * @return int Number of connected clients + */ + int websocket_get_client_count(void); + +#ifdef __cplusplus +} +#endif diff --git a/firmware/components/api-server/src/api_handlers.c b/firmware/components/api-server/src/api_handlers.c new file mode 100644 index 0000000..8e96f67 --- /dev/null +++ b/firmware/components/api-server/src/api_handlers.c @@ -0,0 +1,771 @@ +#include "api_handlers.h" + +#include +#include +#include +#include + +static const char *TAG = "api_handlers"; + +// Helper function to set CORS headers +static esp_err_t set_cors_headers(httpd_req_t *req) +{ + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type"); + return ESP_OK; +} + +// Helper function to send JSON response +static esp_err_t send_json_response(httpd_req_t *req, const char *json) +{ + set_cors_headers(req); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_sendstr(req, json); +} + +// Helper function to send error response +static esp_err_t send_error_response(httpd_req_t *req, int status_code, const char *message) +{ + set_cors_headers(req); + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_status(req, status_code == 400 ? "400 Bad Request" + : status_code == 404 ? "404 Not Found" + : "500 Internal Server Error"); + char buffer[128]; + snprintf(buffer, sizeof(buffer), "{\"error\":\"%s\"}", message); + return httpd_resp_sendstr(req, buffer); +} + +// OPTIONS handler for CORS preflight +static esp_err_t options_handler(httpd_req_t *req) +{ + set_cors_headers(req); + httpd_resp_set_status(req, "204 No Content"); + httpd_resp_send(req, NULL, 0); + return ESP_OK; +} + +// ============================================================================ +// Capabilities API +// ============================================================================ + +esp_err_t api_capabilities_get_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "GET /api/capabilities"); + + // TODO: Implement actual capability detection + const char *response = "{\"thread\":false}"; + return send_json_response(req, response); +} + +// ============================================================================ +// WiFi API +// ============================================================================ + +esp_err_t api_wifi_scan_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "GET /api/wifi/scan"); + + // TODO: Implement actual WiFi scanning + const char *response = "[" + "{\"ssid\":\"Network1\",\"rssi\":-45}," + "{\"ssid\":\"Network2\",\"rssi\":-72}" + "]"; + return send_json_response(req, response); +} + +esp_err_t api_wifi_config_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/wifi/config"); + + char buf[256]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Received WiFi config: %s", buf); + + // TODO: Parse JSON and connect to WiFi + set_cors_headers(req); + httpd_resp_set_status(req, "200 OK"); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_wifi_status_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "GET /api/wifi/status"); + + // TODO: Implement actual WiFi status retrieval + const char *response = "{" + "\"connected\":true," + "\"ssid\":\"NetworkName\"," + "\"ip\":\"192.168.1.100\"," + "\"rssi\":-45" + "}"; + return send_json_response(req, response); +} + +// ============================================================================ +// Light Control API +// ============================================================================ + +esp_err_t api_light_power_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/light/power"); + + char buf[64]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Received light power: %s", buf); + + // TODO: Parse JSON and control light + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_light_thunder_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/light/thunder"); + + char buf[64]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Received thunder setting: %s", buf); + + // TODO: Parse JSON and control thunder effect + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_light_mode_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/light/mode"); + + char buf[64]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Received light mode: %s", buf); + + // TODO: Parse JSON and set light mode + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_light_schema_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/light/schema"); + + char buf[128]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Received schema setting: %s", buf); + + // TODO: Parse JSON and set active schema + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_light_status_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "GET /api/light/status"); + + // TODO: Implement actual light status retrieval + const char *response = "{" + "\"on\":true," + "\"thunder\":false," + "\"mode\":\"simulation\"," + "\"schema\":\"schema_01.csv\"," + "\"color\":{\"r\":255,\"g\":240,\"b\":220}" + "}"; + return send_json_response(req, response); +} + +// ============================================================================ +// LED Configuration API +// ============================================================================ + +esp_err_t api_wled_config_get_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "GET /api/wled/config"); + + // TODO: Implement actual LED config retrieval + const char *response = "{" + "\"segments\":[" + "{\"name\":\"Main Light\",\"start\":0,\"leds\":60}," + "{\"name\":\"Accent Light\",\"start\":60,\"leds\":30}" + "]" + "}"; + return send_json_response(req, response); +} + +esp_err_t api_wled_config_post_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/wled/config"); + + char buf[512]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Received WLED config: %s", buf); + + // TODO: Parse JSON and save LED configuration + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +// ============================================================================ +// Schema API +// ============================================================================ + +esp_err_t api_schema_get_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "GET /api/schema/*"); + + // Extract filename from URI + const char *uri = req->uri; + const char *filename = strrchr(uri, '/'); + if (filename == NULL) + { + return send_error_response(req, 400, "Invalid schema path"); + } + filename++; // Skip the '/' + + ESP_LOGI(TAG, "Requested schema: %s", filename); + + // TODO: Read actual schema file from storage + // For now, return sample CSV data + set_cors_headers(req); + httpd_resp_set_type(req, "text/csv"); + const char *sample_csv = "255,240,220,0,100,250\n" + "255,230,200,0,120,250\n" + "255,220,180,0,140,250\n"; + return httpd_resp_sendstr(req, sample_csv); +} + +esp_err_t api_schema_post_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/schema/*"); + + // Extract filename from URI + const char *uri = req->uri; + const char *filename = strrchr(uri, '/'); + if (filename == NULL) + { + return send_error_response(req, 400, "Invalid schema path"); + } + filename++; + + char buf[2048]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Saving schema %s, size: %d bytes", filename, ret); + + // TODO: Save schema to storage + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +// ============================================================================ +// Devices API (Matter) +// ============================================================================ + +esp_err_t api_devices_scan_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "GET /api/devices/scan"); + + // TODO: Implement Matter device scanning + const char *response = "[" + "{\"id\":\"matter-001\",\"type\":\"light\",\"name\":\"Matter Lamp\"}," + "{\"id\":\"matter-002\",\"type\":\"sensor\",\"name\":\"Temperature Sensor\"}" + "]"; + return send_json_response(req, response); +} + +esp_err_t api_devices_pair_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/devices/pair"); + + char buf[256]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Pairing device: %s", buf); + + // TODO: Implement Matter device pairing + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_devices_paired_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "GET /api/devices/paired"); + + // TODO: Get list of paired devices + const char *response = "[" + "{\"id\":\"matter-001\",\"type\":\"light\",\"name\":\"Living Room Lamp\"}" + "]"; + return send_json_response(req, response); +} + +esp_err_t api_devices_update_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/devices/update"); + + char buf[256]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Updating device: %s", buf); + + // TODO: Update device name + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_devices_unpair_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/devices/unpair"); + + char buf[128]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Unpairing device: %s", buf); + + // TODO: Unpair device + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_devices_toggle_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/devices/toggle"); + + char buf[128]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Toggling device: %s", buf); + + // TODO: Toggle device + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +// ============================================================================ +// Scenes API +// ============================================================================ + +esp_err_t api_scenes_get_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "GET /api/scenes"); + + // TODO: Get scenes from storage + const char *response = "[" + "{" + "\"id\":\"scene-1\"," + "\"name\":\"Evening Mood\"," + "\"icon\":\"🌅\"," + "\"actions\":{" + "\"light\":\"on\"," + "\"mode\":\"simulation\"," + "\"schema\":\"schema_02.csv\"" + "}" + "}," + "{" + "\"id\":\"scene-2\"," + "\"name\":\"Night Mode\"," + "\"icon\":\"🌙\"," + "\"actions\":{" + "\"light\":\"on\"," + "\"mode\":\"night\"" + "}" + "}" + "]"; + return send_json_response(req, response); +} + +esp_err_t api_scenes_post_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/scenes"); + + char buf[512]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Creating/updating scene: %s", buf); + + // TODO: Save scene to storage + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_scenes_delete_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "DELETE /api/scenes"); + + char buf[128]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Deleting scene: %s", buf); + + // TODO: Delete scene from storage + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +esp_err_t api_scenes_activate_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "POST /api/scenes/activate"); + + char buf[128]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) + { + return send_error_response(req, 400, "Failed to receive request body"); + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Activating scene: %s", buf); + + // TODO: Activate scene + set_cors_headers(req); + return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); +} + +// ============================================================================ +// Static file serving +// ============================================================================ + +// Get MIME type from file extension +static const char *get_mime_type(const char *path) +{ + const char *ext = strrchr(path, '.'); + if (ext == NULL) + return "text/plain"; + + if (strcmp(ext, ".html") == 0) + return "text/html"; + if (strcmp(ext, ".css") == 0) + return "text/css"; + if (strcmp(ext, ".js") == 0) + return "application/javascript"; + if (strcmp(ext, ".json") == 0) + return "application/json"; + if (strcmp(ext, ".png") == 0) + return "image/png"; + if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) + return "image/jpeg"; + if (strcmp(ext, ".svg") == 0) + return "image/svg+xml"; + if (strcmp(ext, ".ico") == 0) + return "image/x-icon"; + if (strcmp(ext, ".csv") == 0) + return "text/csv"; + + return "text/plain"; +} + +esp_err_t api_static_file_handler(httpd_req_t *req) +{ + char filepath[CONFIG_HTTPD_MAX_URI_LEN + 16]; + const char *uri = req->uri; + + // Default to index.html for root + if (strcmp(uri, "/") == 0) + { + uri = "/index.html"; + } + + const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH; + int written = snprintf(filepath, sizeof(filepath), "%s%s", base_path, uri); + if (written < 0 || (size_t)written >= sizeof(filepath)) + { + ESP_LOGE(TAG, "URI too long: %s", uri); + return send_error_response(req, 400, "URI too long"); + } + + ESP_LOGI(TAG, "Serving static file: %s", filepath); + + // Check if file exists + struct stat st; + if (stat(filepath, &st) != 0) + { + ESP_LOGW(TAG, "File not found: %s", filepath); + return send_error_response(req, 404, "File not found"); + } + + // Open and serve file + FILE *f = fopen(filepath, "r"); + if (f == NULL) + { + ESP_LOGE(TAG, "Failed to open file: %s", filepath); + return send_error_response(req, 500, "Failed to open file"); + } + + set_cors_headers(req); + httpd_resp_set_type(req, get_mime_type(filepath)); + + char buf[512]; + size_t read_bytes; + while ((read_bytes = fread(buf, 1, sizeof(buf), f)) > 0) + { + if (httpd_resp_send_chunk(req, buf, read_bytes) != ESP_OK) + { + fclose(f); + ESP_LOGE(TAG, "Failed to send file chunk"); + return ESP_FAIL; + } + } + + fclose(f); + httpd_resp_send_chunk(req, NULL, 0); // End response + return ESP_OK; +} + +// ============================================================================ +// Captive portal detection +// ============================================================================ + +esp_err_t api_captive_portal_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "Captive portal detection: %s", req->uri); + + // Redirect to captive portal page + httpd_resp_set_status(req, "302 Found"); + httpd_resp_set_hdr(req, "Location", "/captive.html"); + httpd_resp_send(req, NULL, 0); + return ESP_OK; +} + +// ============================================================================ +// Handler Registration +// ============================================================================ + +esp_err_t api_handlers_register(httpd_handle_t server) +{ + esp_err_t err; + + // Capabilities + httpd_uri_t capabilities_get = { + .uri = "/api/capabilities", .method = HTTP_GET, .handler = api_capabilities_get_handler}; + err = httpd_register_uri_handler(server, &capabilities_get); + if (err != ESP_OK) + return err; + + // WiFi endpoints + httpd_uri_t wifi_scan = {.uri = "/api/wifi/scan", .method = HTTP_GET, .handler = api_wifi_scan_handler}; + err = httpd_register_uri_handler(server, &wifi_scan); + if (err != ESP_OK) + return err; + + httpd_uri_t wifi_config = {.uri = "/api/wifi/config", .method = HTTP_POST, .handler = api_wifi_config_handler}; + err = httpd_register_uri_handler(server, &wifi_config); + if (err != ESP_OK) + return err; + + httpd_uri_t wifi_status = {.uri = "/api/wifi/status", .method = HTTP_GET, .handler = api_wifi_status_handler}; + err = httpd_register_uri_handler(server, &wifi_status); + if (err != ESP_OK) + return err; + + // Light endpoints + httpd_uri_t light_power = {.uri = "/api/light/power", .method = HTTP_POST, .handler = api_light_power_handler}; + err = httpd_register_uri_handler(server, &light_power); + if (err != ESP_OK) + return err; + + httpd_uri_t light_thunder = { + .uri = "/api/light/thunder", .method = HTTP_POST, .handler = api_light_thunder_handler}; + err = httpd_register_uri_handler(server, &light_thunder); + if (err != ESP_OK) + return err; + + httpd_uri_t light_mode = {.uri = "/api/light/mode", .method = HTTP_POST, .handler = api_light_mode_handler}; + err = httpd_register_uri_handler(server, &light_mode); + if (err != ESP_OK) + return err; + + httpd_uri_t light_schema = {.uri = "/api/light/schema", .method = HTTP_POST, .handler = api_light_schema_handler}; + err = httpd_register_uri_handler(server, &light_schema); + if (err != ESP_OK) + return err; + + httpd_uri_t light_status = {.uri = "/api/light/status", .method = HTTP_GET, .handler = api_light_status_handler}; + err = httpd_register_uri_handler(server, &light_status); + if (err != ESP_OK) + return err; + + // WLED config endpoints + httpd_uri_t wled_config_get = { + .uri = "/api/wled/config", .method = HTTP_GET, .handler = api_wled_config_get_handler}; + err = httpd_register_uri_handler(server, &wled_config_get); + if (err != ESP_OK) + return err; + + httpd_uri_t wled_config_post = { + .uri = "/api/wled/config", .method = HTTP_POST, .handler = api_wled_config_post_handler}; + err = httpd_register_uri_handler(server, &wled_config_post); + if (err != ESP_OK) + return err; + + // Schema endpoints (wildcard) + httpd_uri_t schema_get = {.uri = "/api/schema/*", .method = HTTP_GET, .handler = api_schema_get_handler}; + err = httpd_register_uri_handler(server, &schema_get); + if (err != ESP_OK) + return err; + + httpd_uri_t schema_post = {.uri = "/api/schema/*", .method = HTTP_POST, .handler = api_schema_post_handler}; + err = httpd_register_uri_handler(server, &schema_post); + if (err != ESP_OK) + return err; + + // Devices endpoints + httpd_uri_t devices_scan = {.uri = "/api/devices/scan", .method = HTTP_GET, .handler = api_devices_scan_handler}; + err = httpd_register_uri_handler(server, &devices_scan); + if (err != ESP_OK) + return err; + + httpd_uri_t devices_pair = {.uri = "/api/devices/pair", .method = HTTP_POST, .handler = api_devices_pair_handler}; + err = httpd_register_uri_handler(server, &devices_pair); + if (err != ESP_OK) + return err; + + httpd_uri_t devices_paired = { + .uri = "/api/devices/paired", .method = HTTP_GET, .handler = api_devices_paired_handler}; + err = httpd_register_uri_handler(server, &devices_paired); + if (err != ESP_OK) + return err; + + httpd_uri_t devices_update = { + .uri = "/api/devices/update", .method = HTTP_POST, .handler = api_devices_update_handler}; + err = httpd_register_uri_handler(server, &devices_update); + if (err != ESP_OK) + return err; + + httpd_uri_t devices_unpair = { + .uri = "/api/devices/unpair", .method = HTTP_POST, .handler = api_devices_unpair_handler}; + err = httpd_register_uri_handler(server, &devices_unpair); + if (err != ESP_OK) + return err; + + httpd_uri_t devices_toggle = { + .uri = "/api/devices/toggle", .method = HTTP_POST, .handler = api_devices_toggle_handler}; + err = httpd_register_uri_handler(server, &devices_toggle); + if (err != ESP_OK) + return err; + + // Scenes endpoints + httpd_uri_t scenes_get = {.uri = "/api/scenes", .method = HTTP_GET, .handler = api_scenes_get_handler}; + err = httpd_register_uri_handler(server, &scenes_get); + if (err != ESP_OK) + return err; + + httpd_uri_t scenes_post = {.uri = "/api/scenes", .method = HTTP_POST, .handler = api_scenes_post_handler}; + err = httpd_register_uri_handler(server, &scenes_post); + if (err != ESP_OK) + return err; + + httpd_uri_t scenes_delete = {.uri = "/api/scenes", .method = HTTP_DELETE, .handler = api_scenes_delete_handler}; + err = httpd_register_uri_handler(server, &scenes_delete); + if (err != ESP_OK) + return err; + + httpd_uri_t scenes_activate = { + .uri = "/api/scenes/activate", .method = HTTP_POST, .handler = api_scenes_activate_handler}; + err = httpd_register_uri_handler(server, &scenes_activate); + if (err != ESP_OK) + return err; + + // Captive portal detection endpoints + httpd_uri_t captive_generate_204 = { + .uri = "/generate_204", .method = HTTP_GET, .handler = api_captive_portal_handler}; + err = httpd_register_uri_handler(server, &captive_generate_204); + if (err != ESP_OK) + return err; + + httpd_uri_t captive_hotspot = { + .uri = "/hotspot-detect.html", .method = HTTP_GET, .handler = api_captive_portal_handler}; + err = httpd_register_uri_handler(server, &captive_hotspot); + if (err != ESP_OK) + return err; + + httpd_uri_t captive_connecttest = { + .uri = "/connecttest.txt", .method = HTTP_GET, .handler = api_captive_portal_handler}; + err = httpd_register_uri_handler(server, &captive_connecttest); + if (err != ESP_OK) + return err; + + // OPTIONS handler for CORS preflight (wildcard) + httpd_uri_t options = {.uri = "/api/*", .method = HTTP_OPTIONS, .handler = options_handler}; + err = httpd_register_uri_handler(server, &options); + if (err != ESP_OK) + return err; + + // Static file handler (must be last due to wildcard) + httpd_uri_t static_files = {.uri = "/*", .method = HTTP_GET, .handler = api_static_file_handler}; + err = httpd_register_uri_handler(server, &static_files); + if (err != ESP_OK) + return err; + + ESP_LOGI(TAG, "All API handlers registered"); + return ESP_OK; +} diff --git a/firmware/components/api-server/src/api_server.c b/firmware/components/api-server/src/api_server.c new file mode 100644 index 0000000..f25f0db --- /dev/null +++ b/firmware/components/api-server/src/api_server.c @@ -0,0 +1,196 @@ +#include "api_server.h" +#include "api_handlers.h" +#include "websocket_handler.h" + +#include "storage.h" +#include +#include +#include +#include + +static const char *TAG = "api_server"; + +static httpd_handle_t s_server = NULL; +static api_server_config_t s_config = API_SERVER_CONFIG_DEFAULT(); + +static esp_err_t init_mdns(const char *hostname) +{ + esp_err_t err = mdns_init(); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to initialize mDNS: %s", esp_err_to_name(err)); + return err; + } + + err = mdns_hostname_set(hostname); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to set mDNS hostname: %s", esp_err_to_name(err)); + return err; + } + + err = mdns_instance_name_set("System Control"); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to set mDNS instance name: %s", esp_err_to_name(err)); + return err; + } + + // Add HTTP service + err = mdns_service_add("System Control Web Server", "_http", "_tcp", s_config.port, NULL, 0); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to add mDNS HTTP service: %s", esp_err_to_name(err)); + return err; + } + + ESP_LOGI(TAG, "mDNS initialized: %s.local", hostname); + return ESP_OK; +} + +static esp_err_t start_webserver(void) +{ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = s_config.port; + config.lru_purge_enable = true; + config.max_uri_handlers = 32; + config.max_open_sockets = 7; + config.uri_match_fn = httpd_uri_match_wildcard; + + ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port); + + esp_err_t err = httpd_start(&s_server, &config); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(err)); + return err; + } + + // WebSocket-Handler explizit vor allen API-Handlern + err = websocket_handler_init(s_server); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to initialize WebSocket handler: %s", esp_err_to_name(err)); + httpd_stop(s_server); + s_server = NULL; + return err; + } + + // Register API handlers + err = api_handlers_register(s_server); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to register API handlers: %s", esp_err_to_name(err)); + httpd_stop(s_server); + s_server = NULL; + return err; + } + + ESP_LOGI(TAG, "HTTP server started successfully"); + return ESP_OK; +} + +esp_err_t api_server_start(const api_server_config_t *config) +{ + if (s_server != NULL) + { + ESP_LOGW(TAG, "Server already running"); + return ESP_ERR_INVALID_STATE; + } + + if (config != NULL) + { + s_config = *config; + } + + initialize_storage(); + + // Initialize mDNS + esp_err_t err = init_mdns(s_config.hostname); + if (err != ESP_OK) + { + return err; + } + + // Start web server + err = start_webserver(); + if (err != ESP_OK) + { + mdns_free(); + return err; + } + + return ESP_OK; +} + +esp_err_t api_server_stop(void) +{ + if (s_server == NULL) + { + ESP_LOGW(TAG, "Server not running"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t err = httpd_stop(s_server); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to stop HTTP server: %s", esp_err_to_name(err)); + return err; + } + + s_server = NULL; + mdns_free(); + + ESP_LOGI(TAG, "Server stopped"); + return ESP_OK; +} + +bool api_server_is_running(void) +{ + return s_server != NULL; +} + +httpd_handle_t api_server_get_handle(void) +{ + return s_server; +} + +esp_err_t api_server_ws_broadcast(const char *message) +{ + if (s_server == NULL) + { + return ESP_ERR_INVALID_STATE; + } + return websocket_broadcast(s_server, message); +} + +esp_err_t api_server_ws_broadcast_status(bool on, const char *mode, const char *schema, uint8_t r, uint8_t g, uint8_t b) +{ + char buffer[256]; + snprintf(buffer, sizeof(buffer), + "{\"type\":\"status\",\"on\":%s,\"mode\":\"%s\",\"schema\":\"%s\"," + "\"color\":{\"r\":%d,\"g\":%d,\"b\":%d}}", + on ? "true" : "false", mode, schema, r, g, b); + return api_server_ws_broadcast(buffer); +} + +esp_err_t api_server_ws_broadcast_color(uint8_t r, uint8_t g, uint8_t b) +{ + char buffer[64]; + snprintf(buffer, sizeof(buffer), "{\"type\":\"color\",\"r\":%d,\"g\":%d,\"b\":%d}", r, g, b); + return api_server_ws_broadcast(buffer); +} + +esp_err_t api_server_ws_broadcast_wifi(bool connected, const char *ip, int rssi) +{ + char buffer[128]; + if (connected && ip != NULL) + { + snprintf(buffer, sizeof(buffer), "{\"type\":\"wifi\",\"connected\":true,\"ip\":\"%s\",\"rssi\":%d}", ip, rssi); + } + else + { + snprintf(buffer, sizeof(buffer), "{\"type\":\"wifi\",\"connected\":false}"); + } + return api_server_ws_broadcast(buffer); +} diff --git a/firmware/components/api-server/src/websocket_handler.c b/firmware/components/api-server/src/websocket_handler.c new file mode 100644 index 0000000..7c25e12 --- /dev/null +++ b/firmware/components/api-server/src/websocket_handler.c @@ -0,0 +1,255 @@ +#include "websocket_handler.h" + +#include +#include +#include + +static const char *TAG = "websocket_handler"; + +// Store connected WebSocket client file descriptors +static int ws_clients[WS_MAX_CLIENTS]; +static int ws_client_count = 0; + +static void ws_clients_init(void) +{ + for (int i = 0; i < WS_MAX_CLIENTS; i++) + ws_clients[i] = -1; +} + +// Add a client to the list +static void add_client(int fd) +{ + for (int i = 0; i < WS_MAX_CLIENTS; i++) + { + if (ws_clients[i] == -1) + { + ws_clients[i] = fd; + ws_client_count++; + ESP_LOGI(TAG, "WebSocket client connected: fd=%d (total: %d)", fd, ws_client_count); + return; + } + } + ESP_LOGW(TAG, "Max WebSocket clients reached, cannot add fd=%d", fd); +} + +// Remove a client from the list +static void remove_client(int fd) +{ + for (int i = 0; i < WS_MAX_CLIENTS; i++) + { + if (ws_clients[i] == fd) + { + ws_clients[i] = -1; + ws_client_count--; + ESP_LOGI(TAG, "WebSocket client disconnected: fd=%d (total: %d)", fd, ws_client_count); + return; + } + } +} + +// Handle incoming WebSocket message +static esp_err_t handle_ws_message(httpd_req_t *req, httpd_ws_frame_t *ws_pkt) +{ + ESP_LOGI(TAG, "Received WS message: %s", (char *)ws_pkt->payload); + + // Parse the message and handle different types + // For now, we just check if it's a status request + if (ws_pkt->payload != NULL && strstr((char *)ws_pkt->payload, "getStatus") != NULL) + { + // Send status response + // TODO: Get actual status values + const char *response = "{" + "\"type\":\"status\"," + "\"on\":true," + "\"mode\":\"simulation\"," + "\"schema\":\"schema_01.csv\"," + "\"color\":{\"r\":255,\"g\":240,\"b\":220}" + "}"; + + httpd_ws_frame_t ws_resp = {.final = true, + .fragmented = false, + .type = HTTPD_WS_TYPE_TEXT, + .payload = (uint8_t *)response, + .len = strlen(response)}; + + return httpd_ws_send_frame(req, &ws_resp); + } + + return ESP_OK; +} + +esp_err_t websocket_handler(httpd_req_t *req) +{ + if (req->method == HTTP_GET) + { + // This is the handshake + ESP_LOGI(TAG, "WebSocket handshake"); + return ESP_OK; + } + + // Receive the frame + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + + // Get frame length first + esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len: %s", esp_err_to_name(ret)); + return ret; + } + + if (ws_pkt.len > 0) + { + // Allocate buffer for payload + ws_pkt.payload = malloc(ws_pkt.len + 1); + if (ws_pkt.payload == NULL) + { + ESP_LOGE(TAG, "Failed to allocate memory for WS payload"); + return ESP_ERR_NO_MEM; + } + + // Receive the payload + ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed: %s", esp_err_to_name(ret)); + free(ws_pkt.payload); + return ret; + } + + // Null-terminate the payload + ws_pkt.payload[ws_pkt.len] = '\0'; + + // Handle the message + ret = handle_ws_message(req, &ws_pkt); + + free(ws_pkt.payload); + return ret; + } + + // Handle close frame + if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE) + { + int fd = httpd_req_to_sockfd(req); + remove_client(fd); + } + + return ESP_OK; +} + +// Async send structure +typedef struct +{ + httpd_handle_t hd; + int fd; + char *message; +} ws_async_arg_t; + +// Async send work function +static void ws_async_send(void *arg) +{ + ws_async_arg_t *async_arg = (ws_async_arg_t *)arg; + + httpd_ws_frame_t ws_pkt = {.final = true, + .fragmented = false, + .type = HTTPD_WS_TYPE_TEXT, + .payload = (uint8_t *)async_arg->message, + .len = strlen(async_arg->message)}; + + esp_err_t ret = httpd_ws_send_frame_async(async_arg->hd, async_arg->fd, &ws_pkt); + if (ret != ESP_OK) + { + ESP_LOGW(TAG, "Failed to send WS frame to fd=%d: %s", async_arg->fd, esp_err_to_name(ret)); + // Remove client on error + remove_client(async_arg->fd); + } + + free(async_arg->message); + free(async_arg); +} + +esp_err_t websocket_handler_init(httpd_handle_t server) +{ + ws_clients_init(); + // Register WebSocket URI handler + httpd_uri_t ws_uri = {.uri = "/ws", + .method = HTTP_GET, + .handler = websocket_handler, + .is_websocket = true, + .handle_ws_control_frames = true, + .user_ctx = NULL}; + + esp_err_t ret = httpd_register_uri_handler(server, &ws_uri); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "Failed to register WebSocket handler: %s", esp_err_to_name(ret)); + return ret; + } + + ESP_LOGI(TAG, "WebSocket handler initialized at /ws"); + return ESP_OK; +} + +esp_err_t websocket_send(httpd_handle_t server, int fd, const char *message) +{ + if (server == NULL || message == NULL) + { + return ESP_ERR_INVALID_ARG; + } + + ws_async_arg_t *arg = malloc(sizeof(ws_async_arg_t)); + if (arg == NULL) + { + return ESP_ERR_NO_MEM; + } + + arg->hd = server; + arg->fd = fd; + arg->message = strdup(message); + if (arg->message == NULL) + { + free(arg); + return ESP_ERR_NO_MEM; + } + + esp_err_t ret = httpd_queue_work(server, ws_async_send, arg); + if (ret != ESP_OK) + { + free(arg->message); + free(arg); + } + + return ret; +} + +esp_err_t websocket_broadcast(httpd_handle_t server, const char *message) +{ + if (server == NULL || message == NULL) + { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = ESP_OK; + + for (int i = 0; i < WS_MAX_CLIENTS; i++) + { + if (ws_clients[i] != -1) + { + esp_err_t send_ret = websocket_send(server, ws_clients[i], message); + if (send_ret != ESP_OK) + { + ESP_LOGW(TAG, "Failed to queue WS message for fd=%d", ws_clients[i]); + ret = send_ret; // Return last error + } + } + } + + return ret; +} + +int websocket_get_client_count(void) +{ + return ws_client_count; +} diff --git a/firmware/components/connectivity-manager/CMakeLists.txt b/firmware/components/connectivity-manager/CMakeLists.txt index 28bd01e..ecbe78a 100644 --- a/firmware/components/connectivity-manager/CMakeLists.txt +++ b/firmware/components/connectivity-manager/CMakeLists.txt @@ -10,4 +10,5 @@ idf_component_register(SRCS nvs_flash esp_insights led-manager + api-server ) diff --git a/firmware/components/connectivity-manager/src/wifi_manager.c b/firmware/components/connectivity-manager/src/wifi_manager.c index 559bb1a..66abe2e 100644 --- a/firmware/components/connectivity-manager/src/wifi_manager.c +++ b/firmware/components/connectivity-manager/src/wifi_manager.c @@ -1,5 +1,7 @@ #include "wifi_manager.h" +#include "api_server.h" + #include #include #include @@ -216,6 +218,9 @@ void wifi_manager_init() if (bits & WIFI_CONNECTED_BIT) { ESP_DIAG_EVENT(TAG, "Connected to AP SSID:%s", s_wifi_networks[s_current_network_index].ssid); + + api_server_config_t s_config = API_SERVER_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(api_server_start(&s_config)); } else if (bits & WIFI_FAIL_BIT) { diff --git a/firmware/components/simulator/include/storage.h b/firmware/components/simulator/include/storage.h index 642ac66..81dd8a6 100644 --- a/firmware/components/simulator/include/storage.h +++ b/firmware/components/simulator/include/storage.h @@ -1,4 +1,11 @@ #pragma once -void initialize_storage(); -void load_file(const char *filename); +#ifdef __cplusplus +extern "C" +{ +#endif + void initialize_storage(); + void load_file(const char *filename); +#ifdef __cplusplus +} +#endif diff --git a/firmware/components/simulator/src/storage.cpp b/firmware/components/simulator/src/storage.cpp index 6f5a3d5..e8accfd 100644 --- a/firmware/components/simulator/src/storage.cpp +++ b/firmware/components/simulator/src/storage.cpp @@ -3,14 +3,20 @@ #include "esp_log.h" #include "esp_spiffs.h" #include "simulator.h" -#include #include #include static const char *TAG = "storage"; +static bool is_spiffs_mounted = false; + void initialize_storage() { + if (is_spiffs_mounted) + { + return; + } + esp_vfs_spiffs_conf_t conf = { .base_path = "/spiffs", .partition_label = NULL, @@ -36,6 +42,8 @@ void initialize_storage() } return; } + + is_spiffs_mounted = true; } void load_file(const char *filename) diff --git a/firmware/main/idf_component.yml b/firmware/main/idf_component.yml index dc3a1fe..0a9790b 100755 --- a/firmware/main/idf_component.yml +++ b/firmware/main/idf_component.yml @@ -3,6 +3,5 @@ dependencies: git: https://github.com/olikraus/u8g2.git # u8g2_hal: # git: https://github.com/mkfrey/u8g2-hal-esp-idf.git - espressif/button: ^4.1.3 + espressif/button: ^4.1.4 espressif/esp_insights: ^1.2.7 - espressif/mqtt: ^1.0.0 diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults index a4dd8c4..e70ad99 100755 --- a/firmware/sdkconfig.defaults +++ b/firmware/sdkconfig.defaults @@ -42,3 +42,6 @@ CONFIG_SPIRAM=y # SPI RAM config CONFIG_SPIRAM_SPEED=80 CONFIG_SPIRAM_USE_CAPS_ALLOC=y + +# HTTP Server WebSocket Support +CONFIG_HTTPD_WS_SUPPORT=y