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

11
main/CMakeLists.txt Normal file
View File

@@ -0,0 +1,11 @@
idf_component_register(SRCS "main.c"
"gpio_controller.c"
"uart_handler.c"
"ethernet_manager.c"
"web_server.c"
INCLUDE_DIRS "."
REQUIRES esp_driver_gpio esp_driver_uart esp_driver_spi
esp_http_server esp_eth esp_netif lwip spi_flash nvs_flash log)
# Embed HTML file
target_add_binary_data(${COMPONENT_TARGET} "html/index.html" TEXT)

122
main/config.h Normal file
View File

@@ -0,0 +1,122 @@
#ifndef BMC_CONFIG_H
#define BMC_CONFIG_H
#include "driver/gpio.h"
#include "driver/uart.h"
// ============================================================================
// Ethernet Configuration (W5500 SPI)
// ============================================================================
#define ETH_SPI_HOST SPI2_HOST
#define ETH_SPI_MISO_GPIO GPIO_NUM_12
#define ETH_SPI_MOSI_GPIO GPIO_NUM_11
#define ETH_SPI_SCLK_GPIO GPIO_NUM_13
#define ETH_SPI_CS_GPIO GPIO_NUM_14
#define ETH_SPI_INT_GPIO GPIO_NUM_10
#define ETH_SPI_RST_GPIO GPIO_NUM_9
// MAC address (can be customized)
#define ETH_MAC_ADDR {0x02, 0x00, 0x00, 0x12, 0x34, 0x56}
// Network configuration (used if DHCP fails or is disabled)
#define BMC_USE_DHCP 1
#define BMC_STATIC_IP "192.168.1.100"
#define BMC_NETMASK "255.255.255.0"
#define BMC_GATEWAY "192.168.1.1"
// ============================================================================
// UART Serial Configuration
// ============================================================================
#define BMC_UART_NUM UART_NUM_1
#define BMC_UART_TX_GPIO GPIO_NUM_43
#define BMC_UART_RX_GPIO GPIO_NUM_44
#define BMC_UART_BAUD_RATE 115200
#define BMC_UART_BUF_SIZE 2048
#define BMC_UART_TASK_STACK 4096
#define BMC_UART_TASK_PRIO 5
// ============================================================================
// HTTP Server Configuration
// ============================================================================
#define BMC_HTTP_PORT 80
#define BMC_HTTP_MAX_CONN 4
#define BMC_HTTP_STACK_SIZE 8192
// ============================================================================
// GPIO Configuration
// ============================================================================
// Number of user-configurable GPIOs
#define BMC_GPIO_COUNT 16
// GPIO pin definitions with names and default modes
typedef struct {
gpio_num_t pin;
const char *name;
gpio_mode_t mode;
int default_value;
bool inverted; // Active-low logic
bool is_input_only; // Some pins are input-only
} bmc_gpio_def_t;
// Predefined GPIO assignments
#define BMC_GPIO_POWER_ON GPIO_NUM_4
#define BMC_GPIO_POWER_OFF GPIO_NUM_5
#define BMC_GPIO_RESET GPIO_NUM_6
#define BMC_GPIO_STATUS_LED GPIO_NUM_7
#define BMC_GPIO_PWR_GOOD GPIO_NUM_15
#define BMC_GPIO_TEMP_ALERT GPIO_NUM_16
#define BMC_GPIO_FAN_CTRL GPIO_NUM_17
#define BMC_GPIO_USER_1 GPIO_NUM_18
#define BMC_GPIO_USER_2 GPIO_NUM_21
#define BMC_GPIO_USER_3 GPIO_NUM_35
#define BMC_GPIO_USER_4 GPIO_NUM_36
#define BMC_GPIO_USER_5 GPIO_NUM_37
#define BMC_GPIO_USER_6 GPIO_NUM_38
#define BMC_GPIO_USER_7 GPIO_NUM_47
#define BMC_GPIO_USER_8 GPIO_NUM_48
// Default GPIO configuration table
static const bmc_gpio_def_t bmc_gpio_defaults[BMC_GPIO_COUNT] = {
// Power control outputs
{ BMC_GPIO_POWER_ON, "POWER_ON", GPIO_MODE_OUTPUT, 0, false, false},
{ BMC_GPIO_POWER_OFF, "POWER_OFF", GPIO_MODE_OUTPUT, 0, false, false},
{ BMC_GPIO_RESET, "RESET", GPIO_MODE_OUTPUT, 0, true, false}, // Active-low reset
{BMC_GPIO_STATUS_LED, "STATUS_LED", GPIO_MODE_OUTPUT, 0, false, false},
// Status inputs
{ BMC_GPIO_PWR_GOOD, "PWR_GOOD", GPIO_MODE_INPUT, 0, false, false},
{BMC_GPIO_TEMP_ALERT, "TEMP_ALERT", GPIO_MODE_INPUT, 0, true, false}, // Active-low alert
// PWM output
{ BMC_GPIO_FAN_CTRL, "FAN_CTRL", GPIO_MODE_OUTPUT, 0, false, false},
// User-configurable GPIOs
{ BMC_GPIO_USER_1, "USER_1", GPIO_MODE_INPUT, 0, false, false},
{ BMC_GPIO_USER_2, "USER_2", GPIO_MODE_INPUT, 0, false, false},
{ BMC_GPIO_USER_3, "USER_3", GPIO_MODE_INPUT, 0, false, false},
{ BMC_GPIO_USER_4, "USER_4", GPIO_MODE_INPUT, 0, false, false},
{ BMC_GPIO_USER_5, "USER_5", GPIO_MODE_INPUT, 0, false, true}, // Input-only on ESP32-S3
{ BMC_GPIO_USER_6, "USER_6", GPIO_MODE_INPUT, 0, false, true}, // Input-only on ESP32-S3
{ BMC_GPIO_USER_7, "USER_7", GPIO_MODE_INPUT, 0, false, true}, // Input-only on ESP32-S3
{ BMC_GPIO_USER_8, "USER_8", GPIO_MODE_INPUT, 0, false, true}, // Input-only on ESP32-S3
};
// ============================================================================
// Power Control Timing
// ============================================================================
#define BMC_POWER_ON_PULSE_MS 100
#define BMC_POWER_OFF_PULSE_MS 5000
#define BMC_RESET_PULSE_MS 100
#define BMC_POWER_GOOD_DELAY_MS 2000
// ============================================================================
// Logging Tags
// ============================================================================
#define TAG_BMC "BMC"
#define TAG_GPIO "GPIO"
#define TAG_UART "UART"
#define TAG_ETH "ETH"
#define TAG_HTTP "HTTP"
#endif // BMC_CONFIG_H

362
main/ethernet_manager.c Normal file
View File

@@ -0,0 +1,362 @@
#include "ethernet_manager.h"
#include "config.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_eth_driver.h"
#include "esp_eth_mac.h"
#include "esp_eth_phy.h"
#include "esp_netif.h"
#include "driver/spi_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "lwip/ip_addr.h"
#include "esp_eth_phy_w5500.h"
#include "esp_eth_mac_w5500.h"
#include <string.h>
static const char *TAG = TAG_ETH;
// Ethernet handles
static esp_eth_handle_t eth_handle = NULL;
static esp_eth_mac_t *eth_mac = NULL;
static esp_eth_phy_t *eth_phy = NULL;
static esp_netif_t *eth_netif = NULL;
// State tracking
static bool eth_initialized = false;
static bool eth_connected = false;
static bool eth_has_ip = false;
static SemaphoreHandle_t eth_mutex = NULL;
// Event callback
static eth_event_callback_t event_callback = NULL;
// Current configuration
static bmc_eth_config_t current_config = {
.use_dhcp = true,
.static_ip = BMC_STATIC_IP,
.netmask = BMC_NETMASK,
.gateway = BMC_GATEWAY,
};
// ============================================================================
// Event Handler
// ============================================================================
static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) {
switch (event_id) {
case ETHERNET_EVENT_CONNECTED:
ESP_LOGI(TAG, "Ethernet link up");
eth_connected = true;
if (event_callback) {
event_callback(ETH_EVENT_CONNECTED);
}
break;
case ETHERNET_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "Ethernet link down");
eth_connected = false;
eth_has_ip = false;
if (event_callback) {
event_callback(ETH_EVENT_DISCONNECTED);
}
break;
case ETHERNET_EVENT_START:
ESP_LOGI(TAG, "Ethernet started");
break;
case ETHERNET_EVENT_STOP:
ESP_LOGI(TAG, "Ethernet stopped");
break;
case IP_EVENT_ETH_GOT_IP: {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
ESP_LOGI(TAG, "Netmask: " IPSTR, IP2STR(&event->ip_info.netmask));
ESP_LOGI(TAG, "Gateway: " IPSTR, IP2STR(&event->ip_info.gw));
eth_has_ip = true;
if (event_callback) {
event_callback(ETH_EVENT_GOT_IP);
}
} break;
default:
break;
}
}
// ============================================================================
// SPI Configuration for W5500
// ============================================================================
static esp_err_t init_spi_for_ethernet(void) {
spi_bus_config_t buscfg = {
.miso_io_num = ETH_SPI_MISO_GPIO,
.mosi_io_num = ETH_SPI_MOSI_GPIO,
.sclk_io_num = ETH_SPI_SCLK_GPIO,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4096,
};
esp_err_t ret = spi_bus_initialize(ETH_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "SPI bus initialized for W5500");
return ESP_OK;
}
// ============================================================================
// Public Functions
// ============================================================================
esp_err_t ethernet_manager_init(void) {
if (eth_initialized) {
ESP_LOGW(TAG, "Ethernet manager already initialized");
return ESP_OK;
}
ESP_LOGI(TAG, "Initializing Ethernet manager (W5500 SPI)");
// Create mutex
eth_mutex = xSemaphoreCreateMutex();
if (!eth_mutex) {
ESP_LOGE(TAG, "Failed to create mutex");
return ESP_ERR_NO_MEM;
}
// Initialize TCP/IP stack
ESP_ERROR_CHECK(esp_netif_init());
// Create default event loop if not already created
esp_err_t loop_ret = esp_event_loop_create_default();
if (loop_ret != ESP_OK && loop_ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "Failed to create event loop: %s", esp_err_to_name(loop_ret));
return loop_ret;
}
// Create default ETH netif
esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH();
eth_netif = esp_netif_new(&netif_cfg);
// Register event handlers
ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &eth_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &eth_event_handler, NULL));
// Initialize SPI bus
esp_err_t ret = init_spi_for_ethernet();
if (ret != ESP_OK) {
return ret;
}
// Configure W5500 MAC address
uint8_t mac_addr[6] = ETH_MAC_ADDR;
// Create SPI device configuration for W5500
spi_device_interface_config_t devcfg = {
.mode = 0,
.clock_speed_hz = 36 * 1000 * 1000, // 36 MHz
.queue_size = 20,
.spics_io_num = ETH_SPI_CS_GPIO,
};
// Create W5500 MAC instance using SPI (new API uses spi_host and device config pointer)
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(ETH_SPI_HOST, &devcfg);
w5500_config.int_gpio_num = ETH_SPI_INT_GPIO;
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
mac_config.rx_task_stack_size = 4096;
mac_config.rx_task_prio = 15;
eth_mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
if (!eth_mac) {
ESP_LOGE(TAG, "Failed to create W5500 MAC");
return ESP_FAIL;
}
// Create W5500 PHY instance
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
phy_config.phy_addr = 1; // W5500 uses address 1
phy_config.reset_gpio_num = ETH_SPI_RST_GPIO;
phy_config.reset_timeout_ms = 1000;
eth_phy = esp_eth_phy_new_w5500(&phy_config);
if (!eth_phy) {
ESP_LOGE(TAG, "Failed to create W5500 PHY");
return ESP_FAIL;
}
// Create Ethernet driver
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(eth_mac, eth_phy);
ret = esp_eth_driver_install(&eth_config, &eth_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to install Ethernet driver: %s", esp_err_to_name(ret));
return ret;
}
// Set MAC address
ret = esp_eth_ioctl(eth_handle, ETH_CMD_S_MAC_ADDR, mac_addr);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to set MAC address: %s", esp_err_to_name(ret));
}
// Attach Ethernet driver to TCP/IP stack
ret = esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to attach Ethernet to netif: %s", esp_err_to_name(ret));
esp_eth_driver_uninstall(eth_handle);
return ret;
}
// Start Ethernet driver
ret = esp_eth_start(eth_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start Ethernet: %s", esp_err_to_name(ret));
return ret;
}
eth_initialized = true;
ESP_LOGI(TAG, "Ethernet manager initialized successfully");
return ESP_OK;
}
bool ethernet_is_connected(void) {
return eth_connected && eth_has_ip;
}
esp_err_t ethernet_get_ip(char *ip_str) {
if (!eth_initialized || !ip_str) {
return ESP_ERR_INVALID_STATE;
}
esp_netif_ip_info_t ip_info;
esp_err_t ret = esp_netif_get_ip_info(eth_netif, &ip_info);
if (ret == ESP_OK) {
snprintf(ip_str, 16, IPSTR, IP2STR(&ip_info.ip));
}
return ret;
}
esp_err_t ethernet_get_network_info(char *ip, char *netmask, char *gateway) {
if (!eth_initialized) {
return ESP_ERR_INVALID_STATE;
}
esp_netif_ip_info_t ip_info;
esp_err_t ret = esp_netif_get_ip_info(eth_netif, &ip_info);
if (ret == ESP_OK) {
if (ip)
snprintf(ip, 16, IPSTR, IP2STR(&ip_info.ip));
if (netmask)
snprintf(netmask, 16, IPSTR, IP2STR(&ip_info.netmask));
if (gateway)
snprintf(gateway, 16, IPSTR, IP2STR(&ip_info.gw));
}
return ret;
}
esp_err_t ethernet_get_mac(char *mac_str) {
if (!eth_initialized || !mac_str) {
return ESP_ERR_INVALID_STATE;
}
uint8_t mac[6];
esp_err_t ret = esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac);
if (ret == ESP_OK) {
snprintf(mac_str, 18, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
return ret;
}
esp_err_t ethernet_set_static_ip(const bmc_eth_config_t *config) {
if (!eth_initialized || !config) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(eth_mutex, portMAX_DELAY);
// Stop DHCP
esp_netif_dhcpc_stop(eth_netif);
// Set static IP
esp_netif_ip_info_t ip_info;
ip_info.ip.addr = ipaddr_addr(config->static_ip);
ip_info.netmask.addr = ipaddr_addr(config->netmask);
ip_info.gw.addr = ipaddr_addr(config->gateway);
esp_err_t ret = esp_netif_set_ip_info(eth_netif, &ip_info);
if (ret == ESP_OK) {
memcpy(&current_config, config, sizeof(bmc_eth_config_t));
current_config.use_dhcp = false;
ESP_LOGI(TAG, "Static IP configured: %s", config->static_ip);
} else {
ESP_LOGE(TAG, "Failed to set static IP: %s", esp_err_to_name(ret));
}
xSemaphoreGive(eth_mutex);
return ret;
}
esp_err_t ethernet_enable_dhcp(void) {
if (!eth_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(eth_mutex, portMAX_DELAY);
esp_err_t ret = esp_netif_dhcpc_start(eth_netif);
if (ret == ESP_OK) {
current_config.use_dhcp = true;
ESP_LOGI(TAG, "DHCP enabled");
} else {
ESP_LOGE(TAG, "Failed to enable DHCP: %s", esp_err_to_name(ret));
}
xSemaphoreGive(eth_mutex);
return ret;
}
int ethernet_get_link_speed(void) {
if (!eth_initialized || !eth_connected) {
return 0;
}
eth_speed_t speed;
esp_err_t ret = esp_eth_ioctl(eth_handle, ETH_CMD_G_SPEED, &speed);
if (ret == ESP_OK) {
return (speed == ETH_SPEED_100M) ? 100 : 10;
}
return 0;
}
bool ethernet_is_full_duplex(void) {
if (!eth_initialized || !eth_connected) {
return false;
}
eth_duplex_t duplex;
esp_err_t ret = esp_eth_ioctl(eth_handle, ETH_CMD_G_DUPLEX_MODE, &duplex);
return (ret == ESP_OK && duplex == ETH_DUPLEX_FULL);
}
esp_err_t ethernet_register_event_callback(eth_event_callback_t callback) {
event_callback = callback;
return ESP_OK;
}

117
main/ethernet_manager.h Normal file
View File

@@ -0,0 +1,117 @@
#ifndef ETHERNET_MANAGER_H
#define ETHERNET_MANAGER_H
#include "esp_err.h"
#include "esp_eth.h"
#include "esp_netif.h"
// ============================================================================
// Configuration
// ============================================================================
typedef struct {
bool use_dhcp;
char static_ip[16];
char netmask[16];
char gateway[16];
char dns_primary[16];
char dns_secondary[16];
} bmc_eth_config_t;
// ============================================================================
// Function Declarations
// ============================================================================
/**
* @brief Initialize the Ethernet manager
*
* Sets up W5500 SPI Ethernet and network stack.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t ethernet_manager_init(void);
/**
* @brief Check if Ethernet is connected
*
* @return true if connected, false otherwise
*/
bool ethernet_is_connected(void);
/**
* @brief Get the current IP address
*
* @param ip_str Buffer to store IP address string (min 16 bytes)
* @return ESP_OK on success, error code otherwise
*/
esp_err_t ethernet_get_ip(char *ip_str);
/**
* @brief Get network information
*
* @param ip IP address buffer (min 16 bytes)
* @param netmask Netmask buffer (min 16 bytes)
* @param gateway Gateway buffer (min 16 bytes)
* @return ESP_OK on success, error code otherwise
*/
esp_err_t ethernet_get_network_info(char *ip, char *netmask, char *gateway);
/**
* @brief Get MAC address
*
* @param mac_str Buffer to store MAC address string (min 18 bytes)
* @return ESP_OK on success, error code otherwise
*/
esp_err_t ethernet_get_mac(char *mac_str);
/**
* @brief Set static IP configuration
*
* @param config Network configuration
* @return ESP_OK on success, error code otherwise
*/
esp_err_t ethernet_set_static_ip(const bmc_eth_config_t *config);
/**
* @brief Enable DHCP
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t ethernet_enable_dhcp(void);
/**
* @brief Get Ethernet link speed
*
* @return Speed in Mbps, or 0 if not connected
*/
int ethernet_get_link_speed(void);
/**
* @brief Get Ethernet duplex mode
*
* @return true for full duplex, false for half duplex
*/
bool ethernet_is_full_duplex(void);
// ============================================================================
// Event Callbacks
// ============================================================================
typedef enum {
ETH_EVENT_CONNECTED,
ETH_EVENT_DISCONNECTED,
ETH_EVENT_GOT_IP,
ETH_EVENT_LOST_IP,
} eth_event_type_t;
typedef void (*eth_event_callback_t)(eth_event_type_t event);
/**
* @brief Register callback for Ethernet events
*
* @param callback Function to call on events
* @return ESP_OK on success, error code otherwise
*/
esp_err_t ethernet_register_event_callback(eth_event_callback_t callback);
#endif // ETHERNET_MANAGER_H

278
main/gpio_controller.c Normal file
View File

@@ -0,0 +1,278 @@
#include "gpio_controller.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
static const char *TAG = TAG_GPIO;
// Runtime GPIO state storage
static bmc_gpio_state_t gpio_states[BMC_GPIO_COUNT];
static bool gpio_initialized = false;
esp_err_t gpio_controller_init(void) {
ESP_LOGI(TAG, "Initializing GPIO controller");
// Initialize all GPIOs from default configuration
for (int i = 0; i < BMC_GPIO_COUNT; i++) {
const bmc_gpio_def_t *def = &bmc_gpio_defaults[i];
// Copy configuration to runtime state
gpio_states[i].pin = def->pin;
gpio_states[i].name = def->name;
gpio_states[i].mode = def->mode;
gpio_states[i].inverted = def->inverted;
gpio_states[i].is_input_only = def->is_input_only;
// Skip configuration for input-only pins when mode is output
if (def->is_input_only && def->mode == GPIO_MODE_OUTPUT) {
gpio_states[i].mode = GPIO_MODE_INPUT;
ESP_LOGW(TAG, "Pin %d (%s) is input-only, forcing input mode", def->pin, def->name);
}
// Configure GPIO
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << def->pin),
.mode = gpio_states[i].mode,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
// Enable pull-up for inputs by default
if (gpio_states[i].mode == GPIO_MODE_INPUT) {
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
}
esp_err_t ret = gpio_config(&io_conf);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure GPIO %d: %s", def->pin, esp_err_to_name(ret));
return ret;
}
// Set default value for outputs
if (gpio_states[i].mode == GPIO_MODE_OUTPUT) {
int value = def->default_value;
if (def->inverted) {
value = !value;
}
gpio_set_level(def->pin, value);
gpio_states[i].value = value;
} else {
// Read current input value
gpio_states[i].value = gpio_get_level(def->pin);
}
ESP_LOGI(TAG, "GPIO %d (%s) initialized: mode=%d, value=%d, inverted=%d", def->pin, def->name,
gpio_states[i].mode, gpio_states[i].value, gpio_states[i].inverted);
}
gpio_initialized = true;
ESP_LOGI(TAG, "GPIO controller initialized with %d pins", BMC_GPIO_COUNT);
return ESP_OK;
}
int gpio_find_index(gpio_num_t pin) {
for (int i = 0; i < BMC_GPIO_COUNT; i++) {
if (gpio_states[i].pin == pin) {
return i;
}
}
return -1;
}
const char *gpio_get_name(gpio_num_t pin) {
int idx = gpio_find_index(pin);
if (idx >= 0) {
return gpio_states[idx].name;
}
return NULL;
}
esp_err_t gpio_get_all_states(bmc_gpio_state_t *states) {
if (!gpio_initialized) {
return ESP_ERR_INVALID_STATE;
}
// Update input values
for (int i = 0; i < BMC_GPIO_COUNT; i++) {
if (gpio_states[i].mode == GPIO_MODE_INPUT || gpio_states[i].mode == GPIO_MODE_INPUT_OUTPUT) {
gpio_states[i].value = gpio_get_level(gpio_states[i].pin);
}
}
memcpy(states, gpio_states, sizeof(gpio_states));
return ESP_OK;
}
esp_err_t gpio_get_state(gpio_num_t pin, bmc_gpio_state_t *state) {
int idx = gpio_find_index(pin);
if (idx < 0) {
return ESP_ERR_NOT_FOUND;
}
// Update input value
if (gpio_states[idx].mode == GPIO_MODE_INPUT || gpio_states[idx].mode == GPIO_MODE_INPUT_OUTPUT) {
gpio_states[idx].value = gpio_get_level(pin);
}
memcpy(state, &gpio_states[idx], sizeof(bmc_gpio_state_t));
return ESP_OK;
}
int gpio_read(gpio_num_t pin) {
int idx = gpio_find_index(pin);
if (idx < 0) {
return -1;
}
int level = gpio_get_level(pin);
// Apply inverted logic for display
if (gpio_states[idx].inverted) {
level = !level;
}
gpio_states[idx].value = level;
return level;
}
esp_err_t gpio_write(gpio_num_t pin, int value) {
int idx = gpio_find_index(pin);
if (idx < 0) {
ESP_LOGW(TAG, "GPIO %d not found in configuration", pin);
return ESP_ERR_NOT_FOUND;
}
if (gpio_states[idx].is_input_only) {
ESP_LOGW(TAG, "GPIO %d (%s) is input-only", pin, gpio_states[idx].name);
return ESP_ERR_NOT_SUPPORTED;
}
// Ensure GPIO is configured as output
if (gpio_states[idx].mode != GPIO_MODE_OUTPUT && gpio_states[idx].mode != GPIO_MODE_INPUT_OUTPUT) {
ESP_LOGW(TAG, "GPIO %d (%s) is not configured as output", pin, gpio_states[idx].name);
return ESP_ERR_INVALID_STATE;
}
// Apply inverted logic
int actual_value = value;
if (gpio_states[idx].inverted) {
actual_value = !value;
}
esp_err_t ret = gpio_set_level(pin, actual_value);
if (ret == ESP_OK) {
gpio_states[idx].value = value;
ESP_LOGD(TAG, "GPIO %d (%s) set to %d (actual: %d)", pin, gpio_states[idx].name, value, actual_value);
} else {
ESP_LOGE(TAG, "Failed to set GPIO %d: %s", pin, esp_err_to_name(ret));
}
return ret;
}
esp_err_t gpio_configure(gpio_num_t pin, gpio_mode_t mode) {
int idx = gpio_find_index(pin);
if (idx < 0) {
return ESP_ERR_NOT_FOUND;
}
// Check if trying to set input-only pin as output
if (gpio_states[idx].is_input_only && mode == GPIO_MODE_OUTPUT) {
ESP_LOGW(TAG, "GPIO %d (%s) is input-only, cannot set as output", pin, gpio_states[idx].name);
return ESP_ERR_NOT_SUPPORTED;
}
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << pin),
.mode = mode,
.pull_up_en = (mode == GPIO_MODE_INPUT) ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t ret = gpio_config(&io_conf);
if (ret == ESP_OK) {
gpio_states[idx].mode = mode;
ESP_LOGI(TAG, "GPIO %d (%s) mode changed to %d", pin, gpio_states[idx].name, mode);
}
return ret;
}
esp_err_t gpio_toggle(gpio_num_t pin) {
int idx = gpio_find_index(pin);
if (idx < 0) {
return ESP_ERR_NOT_FOUND;
}
int current = gpio_states[idx].value;
return gpio_write(pin, !current);
}
// ============================================================================
// Power Control Functions
// ============================================================================
esp_err_t gpio_power_on(void) {
ESP_LOGI(TAG, "Power ON sequence initiated");
// Pulse the POWER_ON signal
gpio_write(BMC_GPIO_POWER_ON, 1);
vTaskDelay(pdMS_TO_TICKS(BMC_POWER_ON_PULSE_MS));
gpio_write(BMC_GPIO_POWER_ON, 0);
// Wait for power good signal
vTaskDelay(pdMS_TO_TICKS(BMC_POWER_GOOD_DELAY_MS));
if (gpio_is_power_good()) {
ESP_LOGI(TAG, "Power ON successful - power good detected");
gpio_set_status_led(true);
return ESP_OK;
} else {
ESP_LOGW(TAG, "Power ON completed - no power good signal");
return ESP_OK; // Still return OK, just no confirmation
}
}
esp_err_t gpio_power_off(void) {
ESP_LOGI(TAG, "Power OFF sequence initiated");
// Pulse the POWER_OFF signal
gpio_write(BMC_GPIO_POWER_OFF, 1);
vTaskDelay(pdMS_TO_TICKS(BMC_POWER_OFF_PULSE_MS));
gpio_write(BMC_GPIO_POWER_OFF, 0);
gpio_set_status_led(false);
ESP_LOGI(TAG, "Power OFF completed");
return ESP_OK;
}
esp_err_t gpio_reset(void) {
ESP_LOGI(TAG, "Reset sequence initiated");
// Pulse the RESET signal (active-low)
gpio_write(BMC_GPIO_RESET, 0); // Assert reset (active-low)
vTaskDelay(pdMS_TO_TICKS(BMC_RESET_PULSE_MS));
gpio_write(BMC_GPIO_RESET, 1); // De-assert reset
ESP_LOGI(TAG, "Reset completed");
return ESP_OK;
}
bool gpio_is_power_good(void) {
int idx = gpio_find_index(BMC_GPIO_PWR_GOOD);
if (idx < 0) {
return false;
}
int level = gpio_get_level(BMC_GPIO_PWR_GOOD);
return (level == 1);
}
esp_err_t gpio_set_status_led(bool on) {
return gpio_write(BMC_GPIO_STATUS_LED, on ? 1 : 0);
}

148
main/gpio_controller.h Normal file
View File

@@ -0,0 +1,148 @@
#ifndef GPIO_CONTROLLER_H
#define GPIO_CONTROLLER_H
#include "driver/gpio.h"
#include "esp_err.h"
#include "config.h"
// ============================================================================
// GPIO State Structure
// ============================================================================
typedef struct {
gpio_num_t pin;
const char *name;
gpio_mode_t mode;
int value;
bool inverted;
bool is_input_only;
} bmc_gpio_state_t;
// ============================================================================
// Function Declarations
// ============================================================================
/**
* @brief Initialize the GPIO controller
*
* Sets up all GPIOs according to the default configuration.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t gpio_controller_init(void);
/**
* @brief Get the current state of all GPIOs
*
* @param states Array to store GPIO states (must be BMC_GPIO_COUNT size)
* @return ESP_OK on success, error code otherwise
*/
esp_err_t gpio_get_all_states(bmc_gpio_state_t *states);
/**
* @brief Get the state of a specific GPIO
*
* @param pin GPIO pin number
* @param state Pointer to store the GPIO state
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if pin not in config
*/
esp_err_t gpio_get_state(gpio_num_t pin, bmc_gpio_state_t *state);
/**
* @brief Read the value of a GPIO
*
* @param pin GPIO pin number
* @return GPIO level (0 or 1), -1 on error
*/
int gpio_read(gpio_num_t pin);
/**
* @brief Write a value to a GPIO
*
* Handles inverted logic automatically.
*
* @param pin GPIO pin number
* @param value Value to write (0 or 1)
* @return ESP_OK on success, error code otherwise
*/
esp_err_t gpio_write(gpio_num_t pin, int value);
/**
* @brief Configure a GPIO mode
*
* @param pin GPIO pin number
* @param mode GPIO mode (INPUT, OUTPUT, etc.)
* @return ESP_OK on success, error code otherwise
*/
esp_err_t gpio_configure(gpio_num_t pin, gpio_mode_t mode);
/**
* @brief Toggle a GPIO output
*
* @param pin GPIO pin number
* @return ESP_OK on success, error code otherwise
*/
esp_err_t gpio_toggle(gpio_num_t pin);
/**
* @brief Find GPIO index by pin number
*
* @param pin GPIO pin number
* @return Index in the GPIO array, or -1 if not found
*/
int gpio_find_index(gpio_num_t pin);
/**
* @brief Get GPIO name by pin number
*
* @param pin GPIO pin number
* @return GPIO name string, or NULL if not found
*/
const char *gpio_get_name(gpio_num_t pin);
// ============================================================================
// Power Control Functions
// ============================================================================
/**
* @brief Turn board power on
*
* Pulses the POWER_ON GPIO.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t gpio_power_on(void);
/**
* @brief Turn board power off
*
* Pulses the POWER_OFF GPIO.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t gpio_power_off(void);
/**
* @brief Reset the board
*
* Pulses the RESET GPIO (active-low).
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t gpio_reset(void);
/**
* @brief Check if power is good
*
* @return true if power good signal is high, false otherwise
*/
bool gpio_is_power_good(void);
/**
* @brief Set status LED state
*
* @param on true to turn on, false to turn off
* @return ESP_OK on success, error code otherwise
*/
esp_err_t gpio_set_status_led(bool on);
#endif // GPIO_CONTROLLER_H

825
main/html/index.html Normal file
View File

@@ -0,0 +1,825 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VisionFive2 BMC Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #eee;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
backdrop-filter: blur(10px);
}
h1 {
font-size: 2.5em;
margin-bottom: 10px;
background: linear-gradient(90deg, #00d4ff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.status-bar {
display: flex;
justify-content: center;
gap: 30px;
flex-wrap: wrap;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ff4444;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #00ff88;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.card {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.card h2 {
font-size: 1.3em;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 10px;
}
.card h2 .icon {
font-size: 1.5em;
}
/* Power Control */
.power-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1em;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
}
.btn:active {
transform: translateY(0);
}
.btn-power-on {
background: linear-gradient(135deg, #00c853, #00e676);
color: #000;
}
.btn-power-off {
background: linear-gradient(135deg, #ff1744, #ff5252);
color: #fff;
}
.btn-reset {
background: linear-gradient(135deg, #ff9100, #ffab40);
color: #000;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.power-status {
margin-top: 15px;
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.power-indicator {
width: 20px;
height: 20px;
border-radius: 50%;
background: #ff4444;
}
.power-indicator.on {
background: #00ff88;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
}
/* GPIO Monitor */
.gpio-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.gpio-item {
background: rgba(0, 0, 0, 0.2);
padding: 10px;
border-radius: 8px;
text-align: center;
}
.gpio-name {
font-size: 0.8em;
color: #888;
margin-bottom: 5px;
}
.gpio-pin {
font-size: 0.9em;
color: #00d4ff;
margin-bottom: 5px;
}
.gpio-value {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.gpio-led {
width: 16px;
height: 16px;
border-radius: 50%;
background: #333;
border: 2px solid #555;
}
.gpio-led.high {
background: #00ff88;
border-color: #00ff88;
box-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
}
.gpio-toggle {
padding: 4px 8px;
font-size: 0.8em;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.gpio-toggle:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Serial Console */
.console-container {
display: flex;
flex-direction: column;
height: 400px;
}
.console-output {
flex: 1;
background: #0a0a0a;
border-radius: 8px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
overflow-y: auto;
margin-bottom: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: text;
}
.console-output:focus {
outline: none;
border-color: #00d4ff;
}
.console-output pre {
white-space: pre-wrap;
word-wrap: break-word;
}
.console-output .input-line {
color: #00ff88;
}
.console-output .cursor {
display: inline-block;
width: 8px;
height: 1em;
background: #00ff88;
animation: blink 1s step-end infinite;
vertical-align: middle;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.console-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.console-controls select {
padding: 8px 12px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
}
/* System Info */
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.info-item {
padding: 10px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
.info-label {
font-size: 0.8em;
color: #888;
margin-bottom: 3px;
}
.info-value {
font-size: 1em;
color: #00d4ff;
font-family: 'Courier New', monospace;
}
/* Toast Notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.toast {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 10px;
animation: slideIn 0.3s ease;
max-width: 300px;
}
.toast.success {
background: rgba(0, 255, 136, 0.2);
border: 1px solid #00ff88;
}
.toast.error {
background: rgba(255, 68, 68, 0.2);
border: 1px solid #ff4444;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Responsive */
@media (max-width: 600px) {
h1 {
font-size: 1.8em;
}
.status-bar {
flex-direction: column;
align-items: center;
}
.power-buttons {
flex-direction: column;
}
.btn {
width: 100%;
}
.info-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🖥️ VisionFive2 BMC Dashboard</h1>
<div class="status-bar">
<div class="status-item">
<div class="status-dot" id="eth-status"></div>
<span>Ethernet: <span id="eth-text">Disconnected</span></span>
</div>
<div class="status-item">
<span>IP: <span id="ip-address">-</span></span>
</div>
<div class="status-item">
<span>UART: <span id="uart-baud">115200</span> baud</span>
</div>
</div>
</header>
<div class="grid">
<!-- Power Control Card -->
<div class="card">
<h2><span class="icon"></span> Power Control</h2>
<div class="power-buttons">
<button class="btn btn-power-on" onclick="powerOn()">Power On</button>
<button class="btn btn-power-off" onclick="powerOff()">Power Off</button>
<button class="btn btn-reset" onclick="reset()">Reset</button>
</div>
<div class="power-status">
<div class="power-indicator" id="power-indicator"></div>
<span>Power Status: <strong id="power-status">Unknown</strong></span>
</div>
</div>
<!-- System Info Card -->
<div class="card">
<h2><span class="icon"></span> System Info</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">MAC Address</div>
<div class="info-value" id="mac-address">-</div>
</div>
<div class="info-item">
<div class="info-label">Netmask</div>
<div class="info-value" id="netmask">-</div>
</div>
<div class="info-item">
<div class="info-label">Gateway</div>
<div class="info-value" id="gateway">-</div>
</div>
<div class="info-item">
<div class="info-label">Link Speed</div>
<div class="info-value" id="link-speed">-</div>
</div>
</div>
</div>
</div>
<div class="grid">
<!-- GPIO Monitor Card -->
<div class="card">
<h2><span class="icon">🔌</span> GPIO Monitor</h2>
<div class="gpio-grid" id="gpio-grid">
<!-- GPIO items will be populated by JavaScript -->
</div>
</div>
<!-- Serial Console Card -->
<div class="card">
<h2><span class="icon">💻</span> Serial Console</h2>
<div class="console-container">
<div class="console-output" id="console-output" tabindex="0">
<pre>Connecting to serial console...</pre>
</div>
<div class="console-controls">
<label>Baud Rate:</label>
<select id="baud-select" onchange="setBaudRate()">
<option value="9600">9600</option>
<option value="19200">19200</option>
<option value="38400">38400</option>
<option value="57600">57600</option>
<option value="115200" selected>115200</option>
<option value="230400">230400</option>
<option value="460800">460800</option>
<option value="921600">921600</option>
</select>
<button class="btn btn-secondary" onclick="clearConsole()">Clear</button>
<button class="btn btn-secondary" onclick="connectWebSocket()">Reconnect</button>
</div>
</div>
</div>
</div>
</div>
<div class="toast-container" id="toast-container"></div>
<script>
// WebSocket connection
let ws = null;
let wsConnected = false;
// API base URL
const API_BASE = '';
// Toast notification
function showToast(message, type = 'success') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// API helper
async function apiCall(endpoint, method = 'GET', data = null) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if (data) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(`${API_BASE}${endpoint}`, options);
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Unknown error');
}
return result;
} catch (error) {
showToast(error.message, 'error');
throw error;
}
}
// Power control functions
async function powerOn() {
try {
await apiCall('/api/power/on', 'POST');
showToast('Power on sequence initiated');
updatePowerStatus();
} catch (error) {
console.error('Power on failed:', error);
}
}
async function powerOff() {
try {
await apiCall('/api/power/off', 'POST');
showToast('Power off sequence initiated');
updatePowerStatus();
} catch (error) {
console.error('Power off failed:', error);
}
}
async function reset() {
try {
await apiCall('/api/power/reset', 'POST');
showToast('Reset sequence initiated');
} catch (error) {
console.error('Reset failed:', error);
}
}
async function updatePowerStatus() {
try {
const result = await apiCall('/api/power/status');
const indicator = document.getElementById('power-indicator');
const status = document.getElementById('power-status');
if (result.data.power_good) {
indicator.classList.add('on');
status.textContent = 'ON';
} else {
indicator.classList.remove('on');
status.textContent = 'OFF';
}
} catch (error) {
console.error('Failed to get power status:', error);
}
}
// GPIO functions
async function updateGPIOStates() {
try {
const result = await apiCall('/api/gpio');
const grid = document.getElementById('gpio-grid');
grid.innerHTML = '';
result.gpios.forEach(gpio => {
const item = document.createElement('div');
item.className = 'gpio-item';
const isOutput = gpio.mode === 'output';
item.innerHTML = `
<div class="gpio-name">${gpio.name}</div>
<div class="gpio-pin">GPIO ${gpio.pin}</div>
<div class="gpio-value">
<div class="gpio-led ${gpio.value ? 'high' : ''}"></div>
<span>${gpio.value}</span>
${isOutput ? `<button class="gpio-toggle" onclick="toggleGPIO(${gpio.pin})">Toggle</button>` : ''}
</div>
`;
grid.appendChild(item);
});
} catch (error) {
console.error('Failed to get GPIO states:', error);
}
}
async function toggleGPIO(pin) {
try {
// Get current state
const result = await apiCall('/api/gpio');
const gpio = result.gpios.find(g => g.pin === pin);
if (gpio) {
const newValue = gpio.value ? 0 : 1;
await apiCall(`/api/gpio/set?pin=${pin}`, 'POST', { value: newValue });
updateGPIOStates();
}
} catch (error) {
console.error('Failed to toggle GPIO:', error);
}
}
// Serial console functions
let inputBuffer = '';
let cursorVisible = true;
function connectWebSocket() {
const protocol = window.location.protocol === 'https' ? 'wss' : 'ws';
const wsUrl = `${protocol}://${window.location.host}/api/serial/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
wsConnected = true;
clearConsole();
appendConsole('Connected to serial console\n');
updateCursor();
};
ws.onmessage = (event) => {
appendConsole(event.data);
};
ws.onclose = () => {
wsConnected = false;
appendConsole('\nDisconnected from serial console\n');
// Try to reconnect after 3 seconds
setTimeout(connectWebSocket, 3000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
appendConsole('WebSocket error\n');
};
}
// Strip ANSI escape sequences from text
function stripAnsi(text) {
// Remove ANSI escape sequences (cursor movement, colors, etc.)
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
.replace(/\x1b\][^\x07]*\x07/g, '')
.replace(/\x1b[()][AB012]/g, '')
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
}
function appendConsole(data) {
const output = document.getElementById('console-output');
const pre = output.querySelector('pre') || document.createElement('pre');
if (!output.contains(pre)) {
output.innerHTML = '';
output.appendChild(pre);
}
// Remove cursor before appending, then re-add
const cursor = pre.querySelector('.cursor');
if (cursor) cursor.remove();
// Strip ANSI escape sequences for cleaner display
const cleanData = stripAnsi(data);
pre.textContent += cleanData;
updateCursor();
output.scrollTop = output.scrollHeight;
}
function updateCursor() {
const output = document.getElementById('console-output');
const pre = output.querySelector('pre');
if (!pre) return;
// Remove existing cursor
const existingCursor = pre.querySelector('.cursor');
if (existingCursor) existingCursor.remove();
// Add cursor at end
if (wsConnected) {
const cursor = document.createElement('span');
cursor.className = 'cursor';
pre.appendChild(cursor);
}
}
function sendChar(char) {
if (wsConnected) {
ws.send(char);
}
}
function sendBuffer() {
if (inputBuffer && wsConnected) {
ws.send(inputBuffer + '\n');
inputBuffer = '';
updateCursor();
}
}
function handleConsoleKeyDown(event) {
if (!wsConnected) return;
const output = document.getElementById('console-output');
// Handle special keys
if (event.key === 'Enter') {
event.preventDefault();
sendChar('\n');
inputBuffer = '';
} else if (event.key === 'Backspace') {
event.preventDefault();
if (inputBuffer.length > 0) {
inputBuffer = inputBuffer.slice(0, -1);
sendChar('\b'); // Send backspace to remote
}
} else if (event.key === 'Tab') {
event.preventDefault();
sendChar('\t');
inputBuffer += '\t';
} else if (event.key === 'Escape') {
event.preventDefault();
sendChar('\x1b');
} else if (event.ctrlKey && event.key.length === 1) {
// Handle Ctrl+C, Ctrl+D, etc.
event.preventDefault();
const ctrlChar = String.fromCharCode(event.key.charCodeAt(0) - 96);
sendChar(ctrlChar);
} else if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
// Regular character
event.preventDefault();
sendChar(event.key);
inputBuffer += event.key;
}
}
function clearConsole() {
const output = document.getElementById('console-output');
output.innerHTML = '<pre></pre>';
inputBuffer = '';
updateCursor();
}
// Initialize console keyboard handling
document.addEventListener('DOMContentLoaded', () => {
const output = document.getElementById('console-output');
output.addEventListener('keydown', handleConsoleKeyDown);
// Focus console on click
output.addEventListener('click', () => {
output.focus();
});
});
async function setBaudRate() {
const baud = document.getElementById('baud-select').value;
try {
await apiCall('/api/serial/config', 'PUT', { baud_rate: parseInt(baud) });
document.getElementById('uart-baud').textContent = baud;
showToast(`Baud rate set to ${baud}`);
} catch (error) {
console.error('Failed to set baud rate:', error);
}
}
// System info functions
async function updateSystemInfo() {
try {
const result = await apiCall('/api/system/info');
document.getElementById('ip-address').textContent = result.data.ip || '-';
document.getElementById('mac-address').textContent = result.data.mac || '-';
document.getElementById('netmask').textContent = result.data.netmask || '-';
document.getElementById('gateway').textContent = result.data.gateway || '-';
document.getElementById('link-speed').textContent = result.data.link_speed ? `${result.data.link_speed} Mbps` : '-';
// Update Ethernet status
const ethStatus = document.getElementById('eth-status');
const ethText = document.getElementById('eth-text');
if (result.data.ethernet_connected) {
ethStatus.classList.add('connected');
ethText.textContent = 'Connected';
} else {
ethStatus.classList.remove('connected');
ethText.textContent = 'Disconnected';
}
document.getElementById('uart-baud').textContent = result.data.uart_baud_rate;
} catch (error) {
console.error('Failed to get system info:', error);
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
updateSystemInfo();
updateGPIOStates();
updatePowerStatus();
connectWebSocket();
// Periodic updates
setInterval(updateSystemInfo, 5000);
setInterval(updateGPIOStates, 2000);
setInterval(updatePowerStatus, 2000);
});
</script>
</body>
</html>

3
main/idf_component.yml Normal file
View File

@@ -0,0 +1,3 @@
dependencies:
espressif/cjson: ^1.7.19~1
espressif/w5500: ^1.0.1

129
main/main.c Normal file
View File

@@ -0,0 +1,129 @@
#include <stdio.h>
#include "esp_log.h"
#include "esp_event.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "config.h"
#include "gpio_controller.h"
#include "uart_handler.h"
#include "ethernet_manager.h"
#include "web_server.h"
static const char *TAG = TAG_BMC;
// Ethernet event callback
static void eth_event_callback(eth_event_type_t event) {
switch (event) {
case ETH_EVENT_CONNECTED:
ESP_LOGI(TAG, "Ethernet connected");
gpio_set_status_led(true);
break;
case ETH_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "Ethernet disconnected");
gpio_set_status_led(false);
break;
case ETH_EVENT_GOT_IP: {
char ip[16];
if (ethernet_get_ip(ip) == ESP_OK) {
ESP_LOGI(TAG, "Got IP: %s", ip);
}
} break;
case ETH_EVENT_LOST_IP:
ESP_LOGW(TAG, "Lost IP address");
break;
}
}
void app_main(void) {
ESP_LOGI(TAG, "========================================");
ESP_LOGI(TAG, "ESP32-S3 BMC Firmware Starting...");
ESP_LOGI(TAG, "========================================");
// Initialize GPIO controller
ESP_LOGI(TAG, "Initializing GPIO controller...");
esp_err_t ret = gpio_controller_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize GPIO controller: %s", esp_err_to_name(ret));
// Continue anyway - some GPIOs might still work
}
// Set status LED to indicate initialization
gpio_set_status_led(false);
// Initialize UART handler
ESP_LOGI(TAG, "Initializing UART handler...");
ret = uart_handler_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize UART handler: %s", esp_err_to_name(ret));
// Continue anyway - serial console is optional
}
// Initialize Ethernet manager
ESP_LOGI(TAG, "Initializing Ethernet manager...");
ethernet_register_event_callback(eth_event_callback);
ret = ethernet_manager_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize Ethernet: %s", esp_err_to_name(ret));
// Cannot continue without network
return;
}
// Wait for Ethernet connection (with timeout)
ESP_LOGI(TAG, "Waiting for Ethernet connection...");
int timeout = 30; // 30 seconds timeout
while (!ethernet_is_connected() && timeout > 0) {
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
ESP_LOGD(TAG, "Waiting for Ethernet... (%d seconds remaining)", timeout);
}
if (!ethernet_is_connected()) {
ESP_LOGW(TAG, "Ethernet not connected after timeout, starting server anyway...");
}
// Start web server
ESP_LOGI(TAG, "Starting web server...");
ret = web_server_start();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start web server: %s", esp_err_to_name(ret));
return;
}
// Print network information
char ip[16], netmask[16], gateway[16], mac[18];
if (ethernet_get_network_info(ip, netmask, gateway) == ESP_OK) {
ESP_LOGI(TAG, "Network Configuration:");
ESP_LOGI(TAG, " IP Address: %s", ip);
ESP_LOGI(TAG, " Netmask: %s", netmask);
ESP_LOGI(TAG, " Gateway: %s", gateway);
}
if (ethernet_get_mac(mac) == ESP_OK) {
ESP_LOGI(TAG, " MAC Address: %s", mac);
}
ESP_LOGI(TAG, "========================================");
ESP_LOGI(TAG, "BMC Ready!");
ESP_LOGI(TAG, "Web interface: http://%s/", ip);
ESP_LOGI(TAG, "========================================");
// Main loop - monitor system health
while (1) {
// Update status LED based on connection state
if (ethernet_is_connected()) {
gpio_set_status_led(true);
} else {
// Blink LED if not connected
gpio_set_status_led(false);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_status_led(true);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_status_led(false);
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
}

410
main/uart_handler.c Normal file
View File

@@ -0,0 +1,410 @@
#include "uart_handler.h"
#include "config.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include <string.h>
static const char *TAG = TAG_UART;
// Runtime UART configuration
static bmc_uart_config_t current_config;
static bool uart_initialized = false;
static TaskHandle_t uart_rx_task_handle = NULL;
static SemaphoreHandle_t uart_mutex = NULL;
// Callback for WebSocket bridge
static uart_rx_callback_t rx_callback = NULL;
static SemaphoreHandle_t callback_mutex = NULL;
// Ring buffer for received data
#define UART_RING_BUF_SIZE 4096
static uint8_t *ring_buffer = NULL;
static volatile size_t ring_head = 0;
static volatile size_t ring_tail = 0;
// ============================================================================
// Ring Buffer Functions
// ============================================================================
static size_t ring_buffer_available(void) {
if (ring_head >= ring_tail) {
return ring_head - ring_tail;
} else {
return UART_RING_BUF_SIZE - ring_tail + ring_head;
}
}
static size_t ring_buffer_read(uint8_t *buf, size_t len) {
size_t available = ring_buffer_available();
size_t to_read = (len < available) ? len : available;
for (size_t i = 0; i < to_read; i++) {
buf[i] = ring_buffer[ring_tail];
ring_tail = (ring_tail + 1) % UART_RING_BUF_SIZE;
}
return to_read;
}
static size_t ring_buffer_write(const uint8_t *buf, size_t len) {
size_t written = 0;
for (size_t i = 0; i < len; i++) {
size_t next_head = (ring_head + 1) % UART_RING_BUF_SIZE;
if (next_head == ring_tail) {
// Buffer full, overwrite oldest data
ring_tail = (ring_tail + 1) % UART_RING_BUF_SIZE;
}
ring_buffer[ring_head] = buf[i];
ring_head = next_head;
written++;
}
return written;
}
// ============================================================================
// UART Receive Task
// ============================================================================
static void uart_rx_task(void *arg) {
uint8_t *temp_buf = malloc(256);
if (!temp_buf) {
ESP_LOGE(TAG, "Failed to allocate UART RX buffer");
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "UART RX task started, waiting for data on UART%d", BMC_UART_NUM);
while (uart_initialized) {
// Check for data from UART with longer timeout
int len = uart_read_bytes(BMC_UART_NUM, temp_buf, 256, pdMS_TO_TICKS(100));
if (len > 0) {
// Store in ring buffer
ring_buffer_write(temp_buf, len);
// Call callback if registered (outside mutex to avoid deadlock)
uart_rx_callback_t cb = NULL;
xSemaphoreTake(callback_mutex, pdMS_TO_TICKS(100));
cb = rx_callback;
xSemaphoreGive(callback_mutex);
if (cb) {
cb(temp_buf, len);
}
} else if (len < 0) {
ESP_LOGW(TAG, "UART read error: %d", len);
}
}
free(temp_buf);
ESP_LOGI(TAG, "UART RX task stopped");
vTaskDelete(NULL);
}
// ============================================================================
// Public Functions
// ============================================================================
esp_err_t uart_handler_init(void) {
if (uart_initialized) {
ESP_LOGW(TAG, "UART handler already initialized");
return ESP_OK;
}
ESP_LOGI(TAG, "Initializing UART handler");
// Allocate ring buffer
ring_buffer = calloc(UART_RING_BUF_SIZE, 1);
if (!ring_buffer) {
ESP_LOGE(TAG, "Failed to allocate ring buffer");
return ESP_ERR_NO_MEM;
}
ring_head = 0;
ring_tail = 0;
// Create mutex for UART access
uart_mutex = xSemaphoreCreateMutex();
if (!uart_mutex) {
ESP_LOGE(TAG, "Failed to create UART mutex");
free(ring_buffer);
return ESP_ERR_NO_MEM;
}
// Create mutex for callback
callback_mutex = xSemaphoreCreateMutex();
if (!callback_mutex) {
ESP_LOGE(TAG, "Failed to create callback mutex");
vSemaphoreDelete(uart_mutex);
free(ring_buffer);
return ESP_ERR_NO_MEM;
}
// Set default configuration
current_config.port = BMC_UART_NUM;
current_config.baud_rate = BMC_UART_BAUD_RATE;
current_config.data_bits = UART_DATA_8_BITS;
current_config.parity = UART_PARITY_DISABLE;
current_config.stop_bits = UART_STOP_BITS_1;
current_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
// Configure UART
uart_config_t uart_cfg = {
.baud_rate = current_config.baud_rate,
.data_bits = current_config.data_bits,
.parity = current_config.parity,
.stop_bits = current_config.stop_bits,
.flow_ctrl = current_config.flow_ctrl,
};
esp_err_t ret = uart_param_config(BMC_UART_NUM, &uart_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure UART: %s", esp_err_to_name(ret));
goto cleanup;
}
// Set UART pins
ret = uart_set_pin(BMC_UART_NUM, BMC_UART_TX_GPIO, BMC_UART_RX_GPIO, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to set UART pins: %s", esp_err_to_name(ret));
goto cleanup;
}
// Install UART driver
ret = uart_driver_install(BMC_UART_NUM, BMC_UART_BUF_SIZE * 2, 0, 0, NULL, 0);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to install UART driver: %s", esp_err_to_name(ret));
goto cleanup;
}
uart_initialized = true;
// Start receive task
BaseType_t task_ret =
xTaskCreate(uart_rx_task, "uart_rx", BMC_UART_TASK_STACK, NULL, BMC_UART_TASK_PRIO, &uart_rx_task_handle);
if (task_ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create UART RX task");
uart_driver_delete(BMC_UART_NUM);
uart_initialized = false;
ret = ESP_FAIL;
goto cleanup;
}
ESP_LOGI(TAG, "UART initialized: port=%d, baud=%d, tx=%d, rx=%d", BMC_UART_NUM, current_config.baud_rate,
BMC_UART_TX_GPIO, BMC_UART_RX_GPIO);
return ESP_OK;
cleanup:
vSemaphoreDelete(callback_mutex);
vSemaphoreDelete(uart_mutex);
free(ring_buffer);
return ret;
}
esp_err_t uart_handler_deinit(void) {
if (!uart_initialized) {
return ESP_OK;
}
ESP_LOGI(TAG, "Deinitializing UART handler");
uart_initialized = false;
// Wait for task to finish
if (uart_rx_task_handle) {
vTaskDelay(pdMS_TO_TICKS(100));
uart_rx_task_handle = NULL;
}
// Delete UART driver
uart_driver_delete(BMC_UART_NUM);
// Clean up resources
if (callback_mutex) {
vSemaphoreDelete(callback_mutex);
callback_mutex = NULL;
}
if (uart_mutex) {
vSemaphoreDelete(uart_mutex);
uart_mutex = NULL;
}
if (ring_buffer) {
free(ring_buffer);
ring_buffer = NULL;
}
rx_callback = NULL;
ESP_LOGI(TAG, "UART handler deinitialized");
return ESP_OK;
}
esp_err_t uart_get_config(bmc_uart_config_t *config) {
if (!uart_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (config) {
memcpy(config, &current_config, sizeof(bmc_uart_config_t));
}
return ESP_OK;
}
esp_err_t uart_set_config(const bmc_uart_config_t *config) {
if (!uart_initialized || !config) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(uart_mutex, portMAX_DELAY);
// Apply new configuration
uart_config_t uart_cfg = {
.baud_rate = config->baud_rate,
.data_bits = config->data_bits,
.parity = config->parity,
.stop_bits = config->stop_bits,
.flow_ctrl = config->flow_ctrl,
};
esp_err_t ret = uart_param_config(BMC_UART_NUM, &uart_cfg);
if (ret == ESP_OK) {
memcpy(&current_config, config, sizeof(bmc_uart_config_t));
ESP_LOGI(TAG, "UART configuration updated: baud=%d", config->baud_rate);
}
xSemaphoreGive(uart_mutex);
return ret;
}
int uart_read_data(uint8_t *buf, size_t len, uint32_t timeout_ms) {
if (!uart_initialized || !buf || len == 0) {
return -1;
}
xSemaphoreTake(uart_mutex, pdMS_TO_TICKS(timeout_ms));
size_t available = ring_buffer_available();
if (available == 0) {
xSemaphoreGive(uart_mutex);
return 0;
}
size_t to_read = (len < available) ? len : available;
size_t read = ring_buffer_read(buf, to_read);
xSemaphoreGive(uart_mutex);
return read;
}
int uart_write_data(const uint8_t *buf, size_t len) {
if (!uart_initialized || !buf || len == 0) {
return -1;
}
xSemaphoreTake(uart_mutex, portMAX_DELAY);
int written = uart_write_bytes(BMC_UART_NUM, buf, len);
xSemaphoreGive(uart_mutex);
if (written < 0) {
ESP_LOGE(TAG, "Failed to write to UART");
return -1;
}
ESP_LOGD(TAG, "Wrote %d bytes to UART", written);
return written;
}
int uart_data_available(void) {
if (!uart_initialized) {
return -1;
}
return ring_buffer_available();
}
esp_err_t uart_flush_buffers(void) {
if (!uart_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(uart_mutex, portMAX_DELAY);
// Flush hardware buffers
uart_flush_input(BMC_UART_NUM);
// Clear ring buffer
ring_head = 0;
ring_tail = 0;
xSemaphoreGive(uart_mutex);
ESP_LOGI(TAG, "UART buffers flushed");
return ESP_OK;
}
esp_err_t uart_set_baud_rate(int baud_rate) {
if (!uart_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(uart_mutex, portMAX_DELAY);
esp_err_t ret = uart_set_baudrate(BMC_UART_NUM, baud_rate);
if (ret == ESP_OK) {
current_config.baud_rate = baud_rate;
ESP_LOGI(TAG, "UART baud rate changed to %d", baud_rate);
} else {
ESP_LOGE(TAG, "Failed to set baud rate: %s", esp_err_to_name(ret));
}
xSemaphoreGive(uart_mutex);
return ret;
}
int uart_get_baud_rate(void) {
return current_config.baud_rate;
}
// ============================================================================
// WebSocket Bridge Functions
// ============================================================================
esp_err_t uart_register_rx_callback(uart_rx_callback_t callback) {
if (!uart_initialized) {
ESP_LOGW(TAG, "UART not initialized, cannot register callback");
return ESP_ERR_INVALID_STATE;
}
if (!callback_mutex) {
ESP_LOGE(TAG, "callback_mutex is NULL!");
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(callback_mutex, portMAX_DELAY);
rx_callback = callback;
xSemaphoreGive(callback_mutex);
ESP_LOGI(TAG, "UART RX callback registered successfully, rx_callback=%p", rx_callback);
return ESP_OK;
}
esp_err_t uart_unregister_rx_callback(void) {
if (!uart_initialized) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(callback_mutex, portMAX_DELAY);
rx_callback = NULL;
xSemaphoreGive(callback_mutex);
ESP_LOGI(TAG, "UART RX callback unregistered");
return ESP_OK;
}

138
main/uart_handler.h Normal file
View File

@@ -0,0 +1,138 @@
#ifndef UART_HANDLER_H
#define UART_HANDLER_H
#include "esp_err.h"
#include "driver/uart.h"
// ============================================================================
// Configuration
// ============================================================================
// Default UART settings (can be changed at runtime)
typedef struct {
uart_port_t port;
int baud_rate;
uart_word_length_t data_bits;
uart_parity_t parity;
uart_stop_bits_t stop_bits;
uart_hw_flowcontrol_t flow_ctrl;
} bmc_uart_config_t;
// ============================================================================
// Function Declarations
// ============================================================================
/**
* @brief Initialize the UART handler
*
* Sets up UART with default configuration and starts the receive task.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t uart_handler_init(void);
/**
* @brief Deinitialize the UART handler
*
* Stops the receive task and releases UART resources.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t uart_handler_deinit(void);
/**
* @brief Get current UART configuration
*
* @param config Pointer to store the configuration
* @return ESP_OK on success, error code otherwise
*/
esp_err_t uart_get_config(bmc_uart_config_t *config);
/**
* @brief Set UART configuration
*
* @param config New configuration to apply
* @return ESP_OK on success, error code otherwise
*/
esp_err_t uart_set_config(const bmc_uart_config_t *config);
/**
* @brief Read data from UART buffer
*
* Non-blocking read from the internal ring buffer.
*
* @param buf Buffer to store received data
* @param len Maximum bytes to read
* @param timeout_ms Timeout in milliseconds
* @return Number of bytes read, or -1 on error
*/
int uart_read_data(uint8_t *buf, size_t len, uint32_t timeout_ms);
/**
* @brief Write data to UART
*
* @param buf Data to send
* @param len Number of bytes to send
* @return Number of bytes written, or -1 on error
*/
int uart_write_data(const uint8_t *buf, size_t len);
/**
* @brief Check if data is available in receive buffer
*
* @return Number of bytes available, or -1 on error
*/
int uart_data_available(void);
/**
* @brief Flush UART buffers
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t uart_flush_buffers(void);
/**
* @brief Set baud rate
*
* @param baud_rate New baud rate
* @return ESP_OK on success, error code otherwise
*/
esp_err_t uart_set_baud_rate(int baud_rate);
/**
* @brief Get current baud rate
*
* @return Current baud rate
*/
int uart_get_baud_rate(void);
// ============================================================================
// WebSocket Bridge Functions
// ============================================================================
/**
* @brief Callback type for UART data received
*
* @param data Received data
* @param len Length of data
*/
typedef void (*uart_rx_callback_t)(const uint8_t *data, size_t len);
/**
* @brief Register a callback for received UART data
*
* Used for WebSocket bridge to forward data to connected clients.
*
* @param callback Function to call when data is received
* @return ESP_OK on success, error code otherwise
*/
esp_err_t uart_register_rx_callback(uart_rx_callback_t callback);
/**
* @brief Unregister the UART receive callback
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t uart_unregister_rx_callback(void);
#endif // UART_HANDLER_H

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

61
main/web_server.h Normal file
View File

@@ -0,0 +1,61 @@
#ifndef WEB_SERVER_H
#define WEB_SERVER_H
#include "esp_err.h"
#include "esp_http_server.h"
// ============================================================================
// Function Declarations
// ============================================================================
/**
* @brief Initialize and start the web server
*
* Starts the HTTP server and registers all URI handlers.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t web_server_start(void);
/**
* @brief Stop the web server
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t web_server_stop(void);
/**
* @brief Check if web server is running
*
* @return true if running, false otherwise
*/
bool web_server_is_running(void);
/**
* @brief Get the server handle
*
* @return httpd_handle_t or NULL if not running
*/
httpd_handle_t web_server_get_handle(void);
// ============================================================================
// WebSocket Functions
// ============================================================================
/**
* @brief Broadcast data to all connected WebSocket clients
*
* @param data Data to send
* @param len Length of data
* @return ESP_OK on success, error code otherwise
*/
esp_err_t web_server_ws_broadcast(const uint8_t *data, size_t len);
/**
* @brief Get number of connected WebSocket clients
*
* @return Number of connected clients
*/
int web_server_ws_client_count(void);
#endif // WEB_SERVER_H