show website via mdns
Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
14
firmware/components/api-server/CMakeLists.txt
Normal file
14
firmware/components/api-server/CMakeLists.txt
Normal file
@@ -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
|
||||
)
|
||||
38
firmware/components/api-server/Kconfig
Normal file
38
firmware/components/api-server/Kconfig
Normal file
@@ -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 <hostname>.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
|
||||
|
||||
5
firmware/components/api-server/idf_component.yml
Normal file
5
firmware/components/api-server/idf_component.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
idf:
|
||||
version: '>=5.0.0'
|
||||
espressif/mdns:
|
||||
version: '*'
|
||||
63
firmware/components/api-server/include/api_handlers.h
Normal file
63
firmware/components/api-server/include/api_handlers.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_http_server.h>
|
||||
|
||||
#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
|
||||
119
firmware/components/api-server/include/api_server.h
Normal file
119
firmware/components/api-server/include/api_server.h
Normal file
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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
|
||||
60
firmware/components/api-server/include/websocket_handler.h
Normal file
60
firmware/components/api-server/include/websocket_handler.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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
|
||||
771
firmware/components/api-server/src/api_handlers.c
Normal file
771
firmware/components/api-server/src/api_handlers.c
Normal file
@@ -0,0 +1,771 @@
|
||||
#include "api_handlers.h"
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_log.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
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;
|
||||
}
|
||||
196
firmware/components/api-server/src/api_server.c
Normal file
196
firmware/components/api-server/src/api_server.c
Normal file
@@ -0,0 +1,196 @@
|
||||
#include "api_server.h"
|
||||
#include "api_handlers.h"
|
||||
#include "websocket_handler.h"
|
||||
|
||||
#include "storage.h"
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_log.h>
|
||||
#include <mdns.h>
|
||||
#include <string.h>
|
||||
|
||||
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);
|
||||
}
|
||||
255
firmware/components/api-server/src/websocket_handler.c
Normal file
255
firmware/components/api-server/src/websocket_handler.c
Normal file
@@ -0,0 +1,255 @@
|
||||
#include "websocket_handler.h"
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_log.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user