#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 #include 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; }