Initial commit
ESP32-S3 firmware acting as BMC for another board, controlling an ATX power supply and providing access to UART. Co-authored with GLM-5
This commit is contained in:
745
main/web_server.c
Normal file
745
main/web_server.c
Normal file
@@ -0,0 +1,745 @@
|
||||
#include "web_server.h"
|
||||
#include "config.h"
|
||||
#include "gpio_controller.h"
|
||||
#include "uart_handler.h"
|
||||
#include "ethernet_manager.h"
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
#include <string.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
static const char *TAG = TAG_HTTP;
|
||||
|
||||
// Server handle
|
||||
static httpd_handle_t server = NULL;
|
||||
static bool server_running = false;
|
||||
|
||||
// ============================================================================
|
||||
// HTML Content (Embedded)
|
||||
// ============================================================================
|
||||
|
||||
extern const uint8_t index_html_start[] asm("_binary_index_html_start");
|
||||
extern const uint8_t index_html_end[] asm("_binary_index_html_end");
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
static esp_err_t set_content_type_json(httpd_req_t *req) {
|
||||
return httpd_resp_set_type(req, "application/json");
|
||||
}
|
||||
|
||||
static esp_err_t send_json_response(httpd_req_t *req, const char *json_str) {
|
||||
set_content_type_json(req);
|
||||
return httpd_resp_sendstr(req, json_str);
|
||||
}
|
||||
|
||||
static esp_err_t send_json_error(httpd_req_t *req, const char *message) {
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "success", false);
|
||||
cJSON_AddStringToObject(root, "error", message);
|
||||
|
||||
char *json_str = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json_str);
|
||||
free(json_str);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t send_json_success(httpd_req_t *req, cJSON *data) {
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "success", true);
|
||||
if (data) {
|
||||
cJSON_AddItemToObject(root, "data", data);
|
||||
}
|
||||
|
||||
char *json_str = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json_str);
|
||||
free(json_str);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Static File Handlers
|
||||
// ============================================================================
|
||||
|
||||
static esp_err_t index_handler(httpd_req_t *req) {
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_send(req, (const char *)index_html_start, index_html_end - index_html_start);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GPIO API Handlers
|
||||
// ============================================================================
|
||||
|
||||
static esp_err_t api_gpio_get_all_handler(httpd_req_t *req) {
|
||||
bmc_gpio_state_t states[BMC_GPIO_COUNT];
|
||||
esp_err_t ret = gpio_get_all_states(states);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
return send_json_error(req, "Failed to get GPIO states");
|
||||
}
|
||||
|
||||
cJSON *gpios = cJSON_CreateArray();
|
||||
for (int i = 0; i < BMC_GPIO_COUNT; i++) {
|
||||
cJSON *gpio = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(gpio, "pin", states[i].pin);
|
||||
cJSON_AddStringToObject(gpio, "name", states[i].name);
|
||||
cJSON_AddStringToObject(gpio, "mode",
|
||||
states[i].mode == GPIO_MODE_INPUT ? "input"
|
||||
: states[i].mode == GPIO_MODE_OUTPUT ? "output"
|
||||
: "inout");
|
||||
cJSON_AddNumberToObject(gpio, "value", states[i].value);
|
||||
cJSON_AddBoolToObject(gpio, "inverted", states[i].inverted);
|
||||
cJSON_AddItemToArray(gpios, gpio);
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "success", true);
|
||||
cJSON_AddItemToObject(root, "gpios", gpios);
|
||||
|
||||
char *json_str = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
|
||||
esp_err_t resp_ret = send_json_response(req, json_str);
|
||||
free(json_str);
|
||||
return resp_ret;
|
||||
}
|
||||
|
||||
// static esp_err_t api_gpio_get_handler(httpd_req_t *req)
|
||||
// {
|
||||
// char pin_str[8];
|
||||
// if (httpd_req_get_url_query_str(req, pin_str, sizeof(pin_str)) != ESP_OK) {
|
||||
// return send_json_error(req, "Missing pin parameter");
|
||||
// }
|
||||
|
||||
// int pin = atoi(pin_str);
|
||||
// bmc_gpio_state_t state;
|
||||
|
||||
// esp_err_t ret = gpio_get_state((gpio_num_t)pin, &state);
|
||||
// if (ret == ESP_ERR_NOT_FOUND) {
|
||||
// return send_json_error(req, "GPIO not found");
|
||||
// }
|
||||
|
||||
// cJSON *gpio = cJSON_CreateObject();
|
||||
// cJSON_AddNumberToObject(gpio, "pin", state.pin);
|
||||
// cJSON_AddStringToObject(gpio, "name", state.name);
|
||||
// cJSON_AddStringToObject(gpio, "mode",
|
||||
// state.mode == GPIO_MODE_INPUT ? "input" :
|
||||
// state.mode == GPIO_MODE_OUTPUT ? "output" : "inout");
|
||||
// cJSON_AddNumberToObject(gpio, "value", state.value);
|
||||
// cJSON_AddBoolToObject(gpio, "inverted", state.inverted);
|
||||
|
||||
// return send_json_success(req, gpio);
|
||||
// }
|
||||
|
||||
static esp_err_t api_gpio_set_handler(httpd_req_t *req) {
|
||||
// Get pin from query string
|
||||
char query[64];
|
||||
if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) {
|
||||
return send_json_error(req, "Missing query parameters");
|
||||
}
|
||||
|
||||
char pin_str[8];
|
||||
if (httpd_query_key_value(query, "pin", pin_str, sizeof(pin_str)) != ESP_OK) {
|
||||
return send_json_error(req, "Missing pin parameter");
|
||||
}
|
||||
int pin = atoi(pin_str);
|
||||
|
||||
// Parse JSON body
|
||||
char buf[128];
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (ret <= 0) {
|
||||
return send_json_error(req, "No data received");
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *json = cJSON_Parse(buf);
|
||||
if (!json) {
|
||||
return send_json_error(req, "Invalid JSON");
|
||||
}
|
||||
|
||||
cJSON *value_item = cJSON_GetObjectItem(json, "value");
|
||||
if (!value_item || !cJSON_IsNumber(value_item)) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, "Missing or invalid 'value' field");
|
||||
}
|
||||
|
||||
int value = value_item->valueint;
|
||||
cJSON_Delete(json);
|
||||
|
||||
// Set GPIO
|
||||
ret = gpio_write((gpio_num_t)pin, value);
|
||||
if (ret == ESP_ERR_NOT_FOUND) {
|
||||
return send_json_error(req, "GPIO not found");
|
||||
} else if (ret == ESP_ERR_NOT_SUPPORTED) {
|
||||
return send_json_error(req, "GPIO is input-only");
|
||||
} else if (ret != ESP_OK) {
|
||||
return send_json_error(req, "Failed to set GPIO");
|
||||
}
|
||||
|
||||
cJSON *response = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(response, "pin", pin);
|
||||
cJSON_AddNumberToObject(response, "value", value);
|
||||
|
||||
return send_json_success(req, response);
|
||||
}
|
||||
|
||||
static esp_err_t api_gpio_config_handler(httpd_req_t *req) {
|
||||
// Get pin from query string
|
||||
char query[64];
|
||||
if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) {
|
||||
return send_json_error(req, "Missing query parameters");
|
||||
}
|
||||
|
||||
char pin_str[8];
|
||||
if (httpd_query_key_value(query, "pin", pin_str, sizeof(pin_str)) != ESP_OK) {
|
||||
return send_json_error(req, "Missing pin parameter");
|
||||
}
|
||||
int pin = atoi(pin_str);
|
||||
|
||||
// Parse JSON body
|
||||
char buf[128];
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (ret <= 0) {
|
||||
return send_json_error(req, "No data received");
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *json = cJSON_Parse(buf);
|
||||
if (!json) {
|
||||
return send_json_error(req, "Invalid JSON");
|
||||
}
|
||||
|
||||
cJSON *mode_item = cJSON_GetObjectItem(json, "mode");
|
||||
if (!mode_item || !cJSON_IsString(mode_item)) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, "Missing or invalid 'mode' field");
|
||||
}
|
||||
|
||||
gpio_mode_t mode;
|
||||
const char *mode_str = mode_item->valuestring;
|
||||
if (strcmp(mode_str, "input") == 0) {
|
||||
mode = GPIO_MODE_INPUT;
|
||||
} else if (strcmp(mode_str, "output") == 0) {
|
||||
mode = GPIO_MODE_OUTPUT;
|
||||
} else {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, "Invalid mode. Use 'input' or 'output'");
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
// Configure GPIO
|
||||
ret = gpio_configure((gpio_num_t)pin, mode);
|
||||
if (ret == ESP_ERR_NOT_FOUND) {
|
||||
return send_json_error(req, "GPIO not found");
|
||||
} else if (ret == ESP_ERR_NOT_SUPPORTED) {
|
||||
return send_json_error(req, "GPIO is input-only, cannot set as output");
|
||||
} else if (ret != ESP_OK) {
|
||||
return send_json_error(req, "Failed to configure GPIO");
|
||||
}
|
||||
|
||||
cJSON *response = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(response, "pin", pin);
|
||||
cJSON_AddStringToObject(response, "mode", mode_str);
|
||||
|
||||
return send_json_success(req, response);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Power Control API Handlers
|
||||
// ============================================================================
|
||||
|
||||
static esp_err_t api_power_status_handler(httpd_req_t *req) {
|
||||
cJSON *status = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(status, "power_good", gpio_is_power_good());
|
||||
|
||||
// Get power control GPIO states
|
||||
int power_on_idx = gpio_find_index(BMC_GPIO_POWER_ON);
|
||||
int power_off_idx = gpio_find_index(BMC_GPIO_POWER_OFF);
|
||||
|
||||
if (power_on_idx >= 0) {
|
||||
bmc_gpio_state_t state;
|
||||
gpio_get_state(BMC_GPIO_POWER_ON, &state);
|
||||
cJSON_AddNumberToObject(status, "power_on_state", state.value);
|
||||
}
|
||||
|
||||
if (power_off_idx >= 0) {
|
||||
bmc_gpio_state_t state;
|
||||
gpio_get_state(BMC_GPIO_POWER_OFF, &state);
|
||||
cJSON_AddNumberToObject(status, "power_off_state", state.value);
|
||||
}
|
||||
|
||||
return send_json_success(req, status);
|
||||
}
|
||||
|
||||
static esp_err_t api_power_on_handler(httpd_req_t *req) {
|
||||
esp_err_t ret = gpio_power_on();
|
||||
if (ret != ESP_OK) {
|
||||
return send_json_error(req, "Power on failed");
|
||||
}
|
||||
|
||||
cJSON *response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "message", "Power on sequence initiated");
|
||||
cJSON_AddBoolToObject(response, "power_good", gpio_is_power_good());
|
||||
|
||||
return send_json_success(req, response);
|
||||
}
|
||||
|
||||
static esp_err_t api_power_off_handler(httpd_req_t *req) {
|
||||
esp_err_t ret = gpio_power_off();
|
||||
if (ret != ESP_OK) {
|
||||
return send_json_error(req, "Power off failed");
|
||||
}
|
||||
|
||||
cJSON *response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "message", "Power off sequence initiated");
|
||||
|
||||
return send_json_success(req, response);
|
||||
}
|
||||
|
||||
static esp_err_t api_power_reset_handler(httpd_req_t *req) {
|
||||
esp_err_t ret = gpio_reset();
|
||||
if (ret != ESP_OK) {
|
||||
return send_json_error(req, "Reset failed");
|
||||
}
|
||||
|
||||
cJSON *response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "message", "Reset sequence initiated");
|
||||
|
||||
return send_json_success(req, response);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Serial API Handlers
|
||||
// ============================================================================
|
||||
|
||||
static esp_err_t api_serial_config_get_handler(httpd_req_t *req) {
|
||||
bmc_uart_config_t config;
|
||||
esp_err_t ret = uart_get_config(&config);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
return send_json_error(req, "Failed to get serial config");
|
||||
}
|
||||
|
||||
cJSON *config_json = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(config_json, "baud_rate", config.baud_rate);
|
||||
cJSON_AddNumberToObject(config_json, "data_bits", config.data_bits);
|
||||
cJSON_AddNumberToObject(config_json, "parity", config.parity);
|
||||
cJSON_AddNumberToObject(config_json, "stop_bits", config.stop_bits);
|
||||
|
||||
return send_json_success(req, config_json);
|
||||
}
|
||||
|
||||
static esp_err_t api_serial_config_set_handler(httpd_req_t *req) {
|
||||
char buf[256];
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (ret <= 0) {
|
||||
return send_json_error(req, "No data received");
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *json = cJSON_Parse(buf);
|
||||
if (!json) {
|
||||
return send_json_error(req, "Invalid JSON");
|
||||
}
|
||||
|
||||
bmc_uart_config_t config;
|
||||
uart_get_config(&config); // Get current config
|
||||
|
||||
cJSON *baud = cJSON_GetObjectItem(json, "baud_rate");
|
||||
if (baud && cJSON_IsNumber(baud)) {
|
||||
config.baud_rate = baud->valueint;
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
ret = uart_set_baud_rate(config.baud_rate);
|
||||
if (ret != ESP_OK) {
|
||||
return send_json_error(req, "Failed to set baud rate");
|
||||
}
|
||||
|
||||
cJSON *response = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(response, "baud_rate", config.baud_rate);
|
||||
|
||||
return send_json_success(req, response);
|
||||
}
|
||||
|
||||
static esp_err_t api_serial_send_handler(httpd_req_t *req) {
|
||||
char buf[1024];
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (ret <= 0) {
|
||||
return send_json_error(req, "No data received");
|
||||
}
|
||||
|
||||
int written = uart_write_data((uint8_t *)buf, ret);
|
||||
if (written < 0) {
|
||||
return send_json_error(req, "Failed to send data");
|
||||
}
|
||||
|
||||
cJSON *response = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(response, "bytes_sent", written);
|
||||
|
||||
return send_json_success(req, response);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WebSocket Handler for Serial Console
|
||||
// ============================================================================
|
||||
|
||||
static void uart_to_ws_callback(const uint8_t *data, size_t len) {
|
||||
// Always broadcast - the function will dynamically check for connected clients
|
||||
web_server_ws_broadcast(data, len);
|
||||
}
|
||||
|
||||
static esp_err_t ws_serial_handler(httpd_req_t *req) {
|
||||
// For WebSocket handlers with is_websocket=true, ESP-IDF handles the handshake
|
||||
// internally. This handler is only called for frame processing.
|
||||
// Client tracking is done dynamically in the broadcast function.
|
||||
|
||||
// Handle WebSocket frames
|
||||
httpd_ws_frame_t ws_pkt;
|
||||
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
|
||||
|
||||
// First call gets the frame info
|
||||
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
|
||||
if (ret != ESP_OK) {
|
||||
// This is normal when client disconnects - don't log as error
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Handle different frame types
|
||||
switch (ws_pkt.type) {
|
||||
case HTTPD_WS_TYPE_CLOSE:
|
||||
// Send close frame in response
|
||||
ws_pkt.len = 0;
|
||||
ws_pkt.payload = NULL;
|
||||
httpd_ws_send_frame(req, &ws_pkt);
|
||||
return ESP_OK;
|
||||
|
||||
case HTTPD_WS_TYPE_PING:
|
||||
// Respond with pong
|
||||
ws_pkt.type = HTTPD_WS_TYPE_PONG;
|
||||
return httpd_ws_send_frame(req, &ws_pkt);
|
||||
|
||||
case HTTPD_WS_TYPE_TEXT:
|
||||
case HTTPD_WS_TYPE_BINARY:
|
||||
// Handle text/binary data (send to UART)
|
||||
if (ws_pkt.len > 0) {
|
||||
uint8_t *buf = malloc(ws_pkt.len + 1);
|
||||
if (buf) {
|
||||
ws_pkt.payload = buf;
|
||||
ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
|
||||
if (ret == ESP_OK) {
|
||||
uart_write_data(ws_pkt.payload, ws_pkt.len);
|
||||
}
|
||||
free(buf);
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
|
||||
default:
|
||||
ESP_LOGI(TAG_HTTP, "WS frame of unknown type received, ignored");
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// System API Handlers
|
||||
// ============================================================================
|
||||
|
||||
static esp_err_t api_system_info_handler(httpd_req_t *req) {
|
||||
cJSON *info = cJSON_CreateObject();
|
||||
|
||||
// Network info
|
||||
char ip[16], netmask[16], gateway[16], mac[18];
|
||||
if (ethernet_get_network_info(ip, netmask, gateway) == ESP_OK) {
|
||||
cJSON_AddStringToObject(info, "ip", ip);
|
||||
cJSON_AddStringToObject(info, "netmask", netmask);
|
||||
cJSON_AddStringToObject(info, "gateway", gateway);
|
||||
}
|
||||
|
||||
if (ethernet_get_mac(mac) == ESP_OK) {
|
||||
cJSON_AddStringToObject(info, "mac", mac);
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(info, "ethernet_connected", ethernet_is_connected());
|
||||
cJSON_AddNumberToObject(info, "link_speed", ethernet_get_link_speed());
|
||||
cJSON_AddBoolToObject(info, "full_duplex", ethernet_is_full_duplex());
|
||||
|
||||
// UART info
|
||||
cJSON_AddNumberToObject(info, "uart_baud_rate", uart_get_baud_rate());
|
||||
|
||||
return send_json_success(req, info);
|
||||
}
|
||||
|
||||
static esp_err_t api_system_status_handler(httpd_req_t *req) {
|
||||
cJSON *status = cJSON_CreateObject();
|
||||
|
||||
// Power status
|
||||
cJSON *power = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(power, "power_good", gpio_is_power_good());
|
||||
cJSON_AddItemToObject(status, "power", power);
|
||||
|
||||
// Network status
|
||||
cJSON *network = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(network, "connected", ethernet_is_connected());
|
||||
char ip[16];
|
||||
if (ethernet_get_ip(ip) == ESP_OK) {
|
||||
cJSON_AddStringToObject(network, "ip", ip);
|
||||
}
|
||||
cJSON_AddItemToObject(status, "network", network);
|
||||
|
||||
// Serial status
|
||||
cJSON *serial = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(serial, "baud_rate", uart_get_baud_rate());
|
||||
cJSON_AddNumberToObject(serial, "rx_available", uart_data_available());
|
||||
cJSON_AddItemToObject(status, "serial", serial);
|
||||
|
||||
return send_json_success(req, status);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// URI Registration
|
||||
// ============================================================================
|
||||
|
||||
static const httpd_uri_t uri_index = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = index_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_gpio = {
|
||||
.uri = "/api/gpio",
|
||||
.method = HTTP_GET,
|
||||
.handler = api_gpio_get_all_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_gpio_set = {
|
||||
.uri = "/api/gpio/set",
|
||||
.method = HTTP_POST,
|
||||
.handler = api_gpio_set_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_gpio_config = {
|
||||
.uri = "/api/gpio/config",
|
||||
.method = HTTP_PUT,
|
||||
.handler = api_gpio_config_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_power_status = {
|
||||
.uri = "/api/power/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = api_power_status_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_power_on = {
|
||||
.uri = "/api/power/on",
|
||||
.method = HTTP_POST,
|
||||
.handler = api_power_on_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_power_off = {
|
||||
.uri = "/api/power/off",
|
||||
.method = HTTP_POST,
|
||||
.handler = api_power_off_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_power_reset = {
|
||||
.uri = "/api/power/reset",
|
||||
.method = HTTP_POST,
|
||||
.handler = api_power_reset_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_serial_config_get = {
|
||||
.uri = "/api/serial/config",
|
||||
.method = HTTP_GET,
|
||||
.handler = api_serial_config_get_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_serial_config_set = {
|
||||
.uri = "/api/serial/config",
|
||||
.method = HTTP_PUT,
|
||||
.handler = api_serial_config_set_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_serial_send = {
|
||||
.uri = "/api/serial/send",
|
||||
.method = HTTP_POST,
|
||||
.handler = api_serial_send_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_ws_serial = {
|
||||
.uri = "/api/serial/ws",
|
||||
.method = HTTP_GET,
|
||||
.handler = ws_serial_handler,
|
||||
.is_websocket = true,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_system_info = {
|
||||
.uri = "/api/system/info",
|
||||
.method = HTTP_GET,
|
||||
.handler = api_system_info_handler,
|
||||
};
|
||||
|
||||
static const httpd_uri_t uri_api_system_status = {
|
||||
.uri = "/api/system/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = api_system_status_handler,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Public Functions
|
||||
// ============================================================================
|
||||
|
||||
esp_err_t web_server_start(void) {
|
||||
if (server_running) {
|
||||
ESP_LOGW(TAG, "Web server already running");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting web server on port %d", BMC_HTTP_PORT);
|
||||
|
||||
// Configure server
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = BMC_HTTP_PORT;
|
||||
config.max_uri_handlers = 20;
|
||||
config.stack_size = BMC_HTTP_STACK_SIZE;
|
||||
config.max_open_sockets = BMC_HTTP_MAX_CONN;
|
||||
config.lru_purge_enable = false; // Don't close connections when limit reached
|
||||
config.keep_alive_enable = true; // Enable keep-alive for better connection handling
|
||||
config.keep_alive_idle = 5; // Seconds before keep-alive starts
|
||||
config.keep_alive_interval = 5; // Seconds between keep-alive probes
|
||||
config.keep_alive_count = 3; // Number of failed probes before closing
|
||||
|
||||
// Start server
|
||||
esp_err_t ret = httpd_start(&server, &config);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start web server: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Register URI handlers
|
||||
httpd_register_uri_handler(server, &uri_index);
|
||||
httpd_register_uri_handler(server, &uri_api_gpio);
|
||||
httpd_register_uri_handler(server, &uri_api_gpio_set);
|
||||
httpd_register_uri_handler(server, &uri_api_gpio_config);
|
||||
httpd_register_uri_handler(server, &uri_api_power_status);
|
||||
httpd_register_uri_handler(server, &uri_api_power_on);
|
||||
httpd_register_uri_handler(server, &uri_api_power_off);
|
||||
httpd_register_uri_handler(server, &uri_api_power_reset);
|
||||
httpd_register_uri_handler(server, &uri_api_serial_config_get);
|
||||
httpd_register_uri_handler(server, &uri_api_serial_config_set);
|
||||
httpd_register_uri_handler(server, &uri_api_serial_send);
|
||||
httpd_register_uri_handler(server, &uri_ws_serial);
|
||||
httpd_register_uri_handler(server, &uri_api_system_info);
|
||||
httpd_register_uri_handler(server, &uri_api_system_status);
|
||||
|
||||
// Register UART callback for WebSocket broadcast at startup
|
||||
esp_err_t cb_ret = uart_register_rx_callback(uart_to_ws_callback);
|
||||
if (cb_ret == ESP_OK) {
|
||||
ESP_LOGI(TAG, "UART RX callback registered for WebSocket broadcast");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to register UART RX callback: %s", esp_err_to_name(cb_ret));
|
||||
}
|
||||
|
||||
server_running = true;
|
||||
ESP_LOGI(TAG, "Web server started successfully");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t web_server_stop(void) {
|
||||
if (!server_running) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stopping web server");
|
||||
|
||||
httpd_stop(server);
|
||||
server = NULL;
|
||||
server_running = false;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool web_server_is_running(void) {
|
||||
return server_running;
|
||||
}
|
||||
|
||||
httpd_handle_t web_server_get_handle(void) {
|
||||
return server;
|
||||
}
|
||||
|
||||
esp_err_t web_server_ws_broadcast(const uint8_t *data, size_t len) {
|
||||
if (!server_running || !data || len == 0) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
httpd_ws_frame_t ws_pkt = {
|
||||
.payload = (uint8_t *)data,
|
||||
.len = len,
|
||||
.type = HTTPD_WS_TYPE_TEXT,
|
||||
.final = true,
|
||||
};
|
||||
|
||||
// Use a fixed-size array to avoid stack issues
|
||||
#define MAX_WS_BROADCAST_CLIENTS 4
|
||||
int client_fds[MAX_WS_BROADCAST_CLIENTS];
|
||||
size_t clients = MAX_WS_BROADCAST_CLIENTS;
|
||||
|
||||
// Get client list - httpd has its own internal locking
|
||||
esp_err_t ret = httpd_get_client_list(server, &clients, client_fds);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to get client list: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Count and send to all WebSocket clients
|
||||
int ws_count = 0;
|
||||
for (size_t i = 0; i < clients && i < MAX_WS_BROADCAST_CLIENTS; i++) {
|
||||
int fd = client_fds[i];
|
||||
httpd_ws_client_info_t info = httpd_ws_get_fd_info(server, fd);
|
||||
if (info == HTTPD_WS_CLIENT_WEBSOCKET) {
|
||||
ws_count++;
|
||||
esp_err_t err = httpd_ws_send_frame_async(server, fd, &ws_pkt);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGD(TAG, "WS send to fd %d failed: %s", fd, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log if we have multiple clients
|
||||
if (ws_count > 1) {
|
||||
ESP_LOGD(TAG, "Broadcast to %d WS clients", ws_count);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int web_server_ws_client_count(void) {
|
||||
// Dynamically count WebSocket clients
|
||||
if (!server_running) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define MAX_WS_COUNT_CLIENTS 4
|
||||
int client_fds[MAX_WS_COUNT_CLIENTS];
|
||||
size_t clients = MAX_WS_COUNT_CLIENTS;
|
||||
int count = 0;
|
||||
|
||||
if (httpd_get_client_list(server, &clients, client_fds) == ESP_OK) {
|
||||
for (size_t i = 0; i < clients && i < MAX_WS_COUNT_CLIENTS; i++) {
|
||||
if (httpd_ws_get_fd_info(server, client_fds[i]) == HTTPD_WS_CLIENT_WEBSOCKET) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
Reference in New Issue
Block a user