#include "web_server.h" #include "config.h" #include "gpio_controller.h" #include "uart_handler.h" #include "ethernet_manager.h" #include "ota_handler.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); } // ============================================================================ // OTA API Handlers // ============================================================================ static esp_err_t api_ota_status_handler(httpd_req_t *req) { cJSON *status = cJSON_CreateObject(); cJSON_AddStringToObject(status, "partition", ota_get_running_partition()); cJSON_AddBoolToObject(status, "updating", ota_is_updating()); cJSON_AddNumberToObject(status, "progress", ota_get_progress()); return send_json_success(req, status); } static esp_err_t api_ota_update_handler(httpd_req_t *req) { // Check if already updating if (ota_is_updating()) { return send_json_error(req, "OTA update already in progress"); } // Parse JSON body char buf[512]; 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 *url_item = cJSON_GetObjectItem(json, "url"); if (!url_item || !cJSON_IsString(url_item)) { cJSON_Delete(json); return send_json_error(req, "Missing or invalid 'url' field"); } const char *url = url_item->valuestring; // Start OTA update esp_err_t err = ota_start_update(url); cJSON_Delete(json); if (err != ESP_OK) { return send_json_error(req, "Failed to start OTA update"); } cJSON *response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "message", "OTA update started"); return send_json_success(req, response); } static esp_err_t api_ota_upload_handler(httpd_req_t *req) { // Check if already updating if (ota_is_updating()) { ESP_LOGE(TAG_HTTP, "OTA update already in progress"); httpd_resp_set_status(req, "400 Bad Request"); httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"OTA update already in progress\"}"); return ESP_OK; } // Get content type to check for multipart char content_type[128] = {0}; esp_err_t hdr_ret = httpd_req_get_hdr_value_str(req, "Content-Type", content_type, sizeof(content_type)); if (hdr_ret != ESP_OK || strstr(content_type, "multipart/form-data") == NULL) { ESP_LOGE(TAG_HTTP, "Invalid content type for upload"); httpd_resp_set_status(req, "400 Bad Request"); httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Expected multipart/form-data\"}"); return ESP_OK; } // Find boundary char *boundary_start = strstr(content_type, "boundary="); if (!boundary_start) { ESP_LOGE(TAG_HTTP, "No boundary in content type"); httpd_resp_set_status(req, "400 Bad Request"); httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"No boundary found\"}"); return ESP_OK; } boundary_start += 9; // Skip "boundary=" // Extract boundary string char boundary[64] = {0}; char *boundary_end = boundary_start; while (*boundary_end && *boundary_end != '\r' && *boundary_end != '\n' && *boundary_end != ';') { boundary_end++; } int boundary_len = boundary_end - boundary_start; if (boundary_len >= (int)sizeof(boundary)) { boundary_len = sizeof(boundary) - 1; } strncpy(boundary, boundary_start, boundary_len); // Start OTA upload esp_err_t err = ota_start_upload(); if (err != ESP_OK) { ESP_LOGE(TAG_HTTP, "Failed to start OTA upload: %s", esp_err_to_name(err)); httpd_resp_set_status(req, "500 Internal Server Error"); httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Failed to start OTA upload\"}"); return ESP_OK; } ESP_LOGI(TAG_HTTP, "OTA upload started, receiving firmware data..."); // Buffer for reading data char recv_buf[1024]; int total_received = 0; bool found_header = false; while (true) { int received = httpd_req_recv(req, recv_buf, sizeof(recv_buf)); if (received < 0) { ESP_LOGE(TAG_HTTP, "Error receiving data"); ota_abort_upload(); httpd_resp_set_status(req, "500 Internal Server Error"); httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Error receiving data\"}"); return ESP_OK; } if (received == 0) { break; // End of data } total_received += received; // Parse multipart data - find file data start if (!found_header) { // Look for end of headers (double CRLF) char *header_end = strstr(recv_buf, "\r\n\r\n"); if (header_end) { // Skip headers int header_len = (header_end + 4) - recv_buf; found_header = true; // Write remaining data after headers int data_len = received - header_len; if (data_len > 0) { // Check if this is the end boundary char *boundary_pos = strstr(header_end + 4, boundary); if (boundary_pos) { // This is the end, don't write boundary data_len = (boundary_pos - 2) - (header_end + 4); // -2 for \r\n before boundary if (data_len > 0) { err = ota_write_data((uint8_t *)(header_end + 4), data_len); } } else { err = ota_write_data((uint8_t *)(header_end + 4), data_len); } } } } else { // Already in file data - check for boundary char *boundary_pos = strstr(recv_buf, boundary); if (boundary_pos) { // Found end boundary - write data up to boundary (minus \r\n) int data_len = boundary_pos - recv_buf - 2; // -2 for \r\n before boundary if (data_len > 0) { err = ota_write_data((uint8_t *)recv_buf, data_len); } break; // Done } else { // Write all data err = ota_write_data((uint8_t *)recv_buf, received); } } if (err != ESP_OK) { ESP_LOGE(TAG_HTTP, "Failed to write OTA data"); ota_abort_upload(); httpd_resp_set_status(req, "500 Internal Server Error"); httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Failed to write OTA data\"}"); return ESP_OK; } } ESP_LOGI(TAG_HTTP, "OTA upload complete, total received: %d bytes", total_received); // Finish OTA upload (this will restart) err = ota_finish_upload(); if (err != ESP_OK) { ESP_LOGE(TAG_HTTP, "Failed to finish OTA upload"); httpd_resp_set_status(req, "500 Internal Server Error"); httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Failed to finish OTA upload\"}"); return ESP_OK; } // Send success (device will restart, client may not receive this) httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, "{\"success\":true,\"data\":{\"message\":\"OTA update complete, restarting...\"}}"); return ESP_OK; } // ============================================================================ // 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, }; static const httpd_uri_t uri_api_ota_status = { .uri = "/api/ota/status", .method = HTTP_GET, .handler = api_ota_status_handler, }; static const httpd_uri_t uri_api_ota_update = { .uri = "/api/ota/update", .method = HTTP_POST, .handler = api_ota_update_handler, }; static const httpd_uri_t uri_api_ota_upload = { .uri = "/api/ota/upload", .method = HTTP_POST, .handler = api_ota_upload_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); httpd_register_uri_handler(server, &uri_api_ota_status); httpd_register_uri_handler(server, &uri_api_ota_update); httpd_register_uri_handler(server, &uri_api_ota_upload); // 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; }