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:
2026-03-11 17:39:43 +01:00
commit 064e8812a4
21 changed files with 4235 additions and 0 deletions

745
main/web_server.c Normal file
View 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;
}