All checks were successful
Build ESP32 BMC Firmware / build (push) Successful in 53s
1005 lines
30 KiB
C
1005 lines
30 KiB
C
#include "web_server.h"
|
|
#include "config.h"
|
|
#include "gpio_controller.h"
|
|
#include "uart_handler.h"
|
|
#include "ethernet_manager.h"
|
|
#include "ota_handler.h"
|
|
#include "log_handler.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_AddStringToObject(status, "power_status", gpio_power_status() ? "on" : "off");
|
|
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_AddStringToObject(response, "power_status", "on");
|
|
|
|
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");
|
|
cJSON_AddStringToObject(response, "power_status", "off");
|
|
|
|
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");
|
|
cJSON_AddStringToObject(response, "power_status", "on");
|
|
|
|
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_status", gpio_power_status());
|
|
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);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Log API Handlers
|
|
// ============================================================================
|
|
|
|
static esp_err_t api_logs_get_handler(httpd_req_t *req) {
|
|
// Get log buffer size
|
|
size_t log_size = log_get_size();
|
|
if (log_size == 0) {
|
|
httpd_resp_set_type(req, "text/plain");
|
|
httpd_resp_sendstr(req, "No logs available");
|
|
return ESP_OK;
|
|
}
|
|
|
|
// Allocate buffer for logs
|
|
char *log_buf = malloc(log_size + 1);
|
|
if (!log_buf) {
|
|
return send_json_error(req, "Failed to allocate memory for logs");
|
|
}
|
|
|
|
// Get logs
|
|
size_t copied = log_get_buffer(log_buf, log_size);
|
|
log_buf[copied] = '\0';
|
|
|
|
// Send response
|
|
httpd_resp_set_type(req, "text/plain");
|
|
httpd_resp_sendstr(req, log_buf);
|
|
|
|
free(log_buf);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t api_logs_clear_handler(httpd_req_t *req) {
|
|
log_clear_buffer();
|
|
return send_json_success(req, cJSON_CreateObject());
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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,
|
|
};
|
|
|
|
static const httpd_uri_t uri_api_logs_get = {
|
|
.uri = "/api/logs",
|
|
.method = HTTP_GET,
|
|
.handler = api_logs_get_handler,
|
|
};
|
|
|
|
static const httpd_uri_t uri_api_logs_clear = {
|
|
.uri = "/api/logs/clear",
|
|
.method = HTTP_POST,
|
|
.handler = api_logs_clear_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);
|
|
httpd_register_uri_handler(server, &uri_api_logs_get);
|
|
httpd_register_uri_handler(server, &uri_api_logs_clear);
|
|
|
|
// 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) {
|
|
// This can happen when there are no clients or server is busy - not a critical error
|
|
ESP_LOGD(TAG, "Could not get client list: %s (may be no clients connected)", esp_err_to_name(ret));
|
|
return ESP_OK; // Return OK since this is not a critical error
|
|
}
|
|
|
|
// 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;
|
|
}
|