ESP32-S3 firmware acting as BMC for another board, controlling an ATX power supply and providing access to UART. Co-authored with GLM-5
746 lines
22 KiB
C
746 lines
22 KiB
C
#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;
|
|
}
|