From 064e8812a4a9ba5ab9d8dd3fdc6a51f1a5db44b4 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 11 Mar 2026 17:39:43 +0100 Subject: [PATCH] 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 --- .clang-format | 154 ++++++++ .gitignore | 4 + .vscode/settings.json | 52 +++ CMakeLists.txt | 9 + README.md | 253 ++++++++++++ dependencies.lock | 31 ++ main/CMakeLists.txt | 11 + main/config.h | 122 ++++++ main/ethernet_manager.c | 362 ++++++++++++++++++ main/ethernet_manager.h | 117 ++++++ main/gpio_controller.c | 278 ++++++++++++++ main/gpio_controller.h | 148 +++++++ main/html/index.html | 825 ++++++++++++++++++++++++++++++++++++++++ main/idf_component.yml | 3 + main/main.c | 129 +++++++ main/uart_handler.c | 410 ++++++++++++++++++++ main/uart_handler.h | 138 +++++++ main/web_server.c | 745 ++++++++++++++++++++++++++++++++++++ main/web_server.h | 61 +++ plans/esp32-bmc-plan.md | 348 +++++++++++++++++ sdkconfig.defaults | 35 ++ 21 files changed, 4235 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 dependencies.lock create mode 100644 main/CMakeLists.txt create mode 100644 main/config.h create mode 100644 main/ethernet_manager.c create mode 100644 main/ethernet_manager.h create mode 100644 main/gpio_controller.c create mode 100644 main/gpio_controller.h create mode 100644 main/html/index.html create mode 100644 main/idf_component.yml create mode 100644 main/main.c create mode 100644 main/uart_handler.c create mode 100644 main/uart_handler.h create mode 100644 main/web_server.c create mode 100644 main/web_server.h create mode 100644 plans/esp32-bmc-plan.md create mode 100644 sdkconfig.defaults diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a1afb26 --- /dev/null +++ b/.clang-format @@ -0,0 +1,154 @@ +# clang-format configuration for ESP32-S3 BMC project +# Based on LLVM style with ESP-IDF conventions + +BasedOnStyle: LLVM +Language: C + +# Indentation +IndentWidth: 4 +TabWidth: 4 +UseTab: ForIndentation +ContinuationIndentWidth: 4 +IndentCaseLabels: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentWrappedFunctionNames: false +NamespaceIndentation: None + +# Alignment +AlignAfterOpenBracket: Align +AlignArrayOfStructures: Right +AlignConsecutiveAssignments: false +AlignConsecutiveBitFields: false +AlignConsecutiveDeclarations: false +AlignConsecutiveMacros: false +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: true + +# Line Breaking +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeConceptDeclarations: true +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true + +# Braces +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true + +# Spacing +BitFieldColonSpacing: Both +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false + +# Pointer/Reference Alignment +DerivePointerAlignment: false +PointerAlignment: Right + +# Columns +ColumnLimit: 120 +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyIndentedWhitespace: 0 +PenaltyReturnTypeOnItsOwnLine: 60 + +# Comments +FixNamespaceComments: true +ReflowComments: true + +# Includes +IncludeBlocks: Preserve +SortIncludes: Never + +# Macros +AttributeMacros: [] +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +MacroBlockBegin: '' +MacroBlockEnd: '' +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION + +# Other +BinPackArguments: true +BinPackParameters: true +CompactNamespaces: false +Cpp11BracedListStyle: true +DeriveLineEnding: true +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +InsertBraces: false +InsertTrailingCommas: None +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 +PackConstructorInitializers: BinPack +QualifierAlignment: Leave +SeparateDefinitionBlocks: Leave +SortUsingDeclarations: true +UseCRLF: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e1fedc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build +managed_components +sdkconfig +sdkconfig.old diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1b2ffc7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,52 @@ +{ + // Editor settings + "editor.formatOnSave": true, + "editor.defaultFormatter": "xaver.clang-format", + "editor.tabSize": 4, + "editor.insertSpaces": false, + + // C/C++ extension settings + "C_Cpp.formatting": "clangFormat", + "C_Cpp.format.clang_format_path": "clang-format", + "C_Cpp.format.clang_format_fallbackStyle": "LLVM", + "C_Cpp.format.clang_format_style": "file", + "C_Cpp.errorSquiggles": "enabled", + "C_Cpp.intelliSenseEngine": "default", + "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", + + // Files to exclude + "files.exclude": { + "**/.git": true, + "**/build": false, + "**/managed_components": true + }, + + // Files to watch + "files.watcherExclude": { + "**/.git/**": true, + "**/build/**": false, + "**/managed_components/**": true + }, + + // CMake settings + "cmake.configureOnOpen": false, + "cmake.buildDirectory": "${workspaceFolder}/build", + + // File associations + "files.associations": { + "*.h": "c", + "*.c": "c", + "sdkconfig.defaults": "ini", + "Kconfig.projbuild": "kconfig" + }, + + // Trailing whitespace + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + + // Clang-format specific + "clang-format.executable": "clang-format", + "clang-format.style": "file", + "clang-format.fallbackStyle": "LLVM", + "clang-format.assumeFilename": "main.c" +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7a25b9f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +# ESP-IDF version check +set(IDF_TARGET "esp32s3") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(esp32-bmc) diff --git a/README.md b/README.md new file mode 100644 index 0000000..616067b --- /dev/null +++ b/README.md @@ -0,0 +1,253 @@ +# ESP32-S3 BMC Firmware + +A Baseboard Management Controller (BMC) firmware for ESP32-S3 that provides remote management capabilities for external boards via Ethernet. + +## Features + +- **Ethernet Connectivity**: W5500 SPI Ethernet module support +- **GPIO Control**: 16 GPIO pins for power control, status monitoring, and user functions +- **Serial Console**: UART bridge with WebSocket support for remote console access +- **Web Interface**: Modern HTML/CSS dashboard for easy management +- **REST API**: Full REST API for programmatic control + +## Hardware Requirements + +- ESP32-S3 development board +- W5500 SPI Ethernet module +- Target board to manage + +### Pin Connections + +#### W5500 SPI Ethernet +| ESP32-S3 | W5500 | +|----------|-------| +| GPIO 11 | MOSI | +| GPIO 13 | MISO | +| GPIO 12 | SCLK | +| GPIO 10 | CS | +| GPIO 9 | RST | +| GPIO 14 | INT | + +#### UART Serial +| ESP32-S3 | Target Board | +|----------|--------------| +| GPIO 43 | TX | +| GPIO 44 | RX | + +#### BMC GPIOs +| GPIO | Name | Description | +|------|------|-------------| +| 4 | POWER_ON | Power control output | +| 5 | POWER_OFF | Power control output | +| 6 | RESET | Reset control (active-low) | +| 7 | STATUS_LED | Status LED | +| 15 | PWR_GOOD | Power good input | +| 16 | TEMP_ALERT | Temperature alert input | +| 17 | FAN_CTRL | Fan control PWM | +| 18-25 | USER_1-8 | User configurable GPIOs | +| 35-38 | USER_5-8 | Input-only GPIOs | + +## Building + +### Prerequisites + +- ESP-IDF v5.0 or later +- Python 3.8+ +- CMake 3.16+ + +### Setup ESP-IDF + +```bash +# Install ESP-IDF (if not already installed) +git clone --recursive https://github.com/espressif/esp-idf.git +cd esp-idf +./install.sh esp32s3 +source export.sh +``` + +### Build and Flash + +```bash +# Navigate to project directory +cd esp32-bmc + +# Configure the project (optional) +idf.py menuconfig + +# Build +idf.py build + +# Flash to ESP32-S3 +idf.py -p /dev/ttyUSB0 flash + +# Monitor serial output +idf.py -p /dev/ttyUSB0 monitor +``` + +## Configuration + +### Network Settings + +By default, the firmware uses DHCP. To configure static IP: + +1. Run `idf.py menuconfig` +2. Navigate to "BMC Configuration" +3. Disable "Use DHCP" and set static IP, netmask, and gateway + +Or use the REST API to change network settings at runtime. + +### GPIO Configuration + +GPIO pins can be configured in `main/config.h`. Modify the `bmc_gpio_defaults` array to change pin assignments, names, and default modes. + +## REST API + +### GPIO Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/gpio` | Get all GPIO states | +| POST | `/api/gpio/set?pin=X` | Set GPIO value (body: `{"value": 0/1}`) | +| PUT | `/api/gpio/config?pin=X` | Configure GPIO mode (body: `{"mode": "input/output"}`) | + +### Power Control Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/power/status` | Get power status | +| POST | `/api/power/on` | Turn board power on | +| POST | `/api/power/off` | Turn board power off | +| POST | `/api/power/reset` | Reset the board | + +### Serial Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/serial/config` | Get serial configuration | +| PUT | `/api/serial/config` | Set serial configuration | +| POST | `/api/serial/send` | Send data to serial | +| GET | `/api/serial/ws` | WebSocket for serial console | + +### System Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/system/info` | Get system information | +| GET | `/api/system/status` | Get BMC status | + +## Web Interface + +Access the web interface by navigating to `http:///` in your browser. + +Features: +- Power control panel with on/off/reset buttons +- Real-time GPIO monitoring +- Serial console with WebSocket support +- System information display + +## Example Usage + +### Power On + +```bash +curl -X POST http://192.168.1.100/api/power/on +``` + +### Set GPIO + +```bash +curl -X POST "http://192.168.1.100/api/gpio/set?pin=18" \ + -H "Content-Type: application/json" \ + -d '{"value": 1}' +``` + +### Get System Info + +```bash +curl http://192.168.1.100/api/system/info +``` + +### Serial Console via WebSocket + +```javascript +const ws = new WebSocket('ws://192.168.1.100/api/serial/ws'); +ws.onmessage = (event) => console.log(event.data); +ws.send('command\n'); +``` + +## Project Structure + +``` +esp32-bmc/ +├── CMakeLists.txt # Project CMake configuration +├── sdkconfig.defaults # Default SDK configuration +├── .clang-format # Code formatter configuration +├── main/ +│ ├── CMakeLists.txt # Main component CMake +│ ├── main.c # Application entry point +│ ├── config.h # Pin definitions and settings +│ ├── gpio_controller.c/h # GPIO management +│ ├── uart_handler.c/h # UART serial communication +│ ├── ethernet_manager.c/h # W5500 Ethernet setup +│ ├── web_server.c/h # HTTP server and routes +│ └── html/ +│ └── index.html # Web interface +└── README.md +``` + +## Code Quality Tools + +This project uses clang-format for code formatting. + +### clang-format + +Format all C source files: + +```bash +# Format all files in-place +find main -name "*.c" -o -name "*.h" | xargs clang-format -i + +# Format a specific file +clang-format -i main/web_server.c + +# Check formatting without modifying (dry run) +clang-format --dry-run --Werror main/web_server.c +``` + +### VS Code Integration + +The project includes VS Code settings for automatic formatting on save. Install the following extensions: +- **C/C++** (ms-vscode.cpptools) - IntelliSense and debugging +- **clang-format** (xaver.clang-format) - Code formatting +- **ESP-IDF** (espressif.esp-idf-extension) - ESP-IDF development + +With these extensions installed, code will be automatically formatted when you save files. + +## Troubleshooting + +### Ethernet Not Connecting + +1. Check W5500 module connections +2. Verify SPI pins are correct +3. Check Ethernet cable and link lights +4. Monitor serial output for error messages + +### GPIO Not Working + +1. Verify pin is not input-only (GPIO 35-38 on ESP32-S3) +2. Check pin is not already used by Ethernet or UART +3. Ensure proper GPIO mode is set + +### Serial Console Issues + +1. Verify UART TX/RX connections +2. Check baud rate matches target board +3. Ensure WebSocket connection is established + +## License + +MIT License + +## Contributing + +Contributions are welcome! Please submit pull requests or open issues for any improvements. diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..c37724c --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,31 @@ +dependencies: + espressif/cjson: + component_hash: 002c6d1872ee4c97d333938ebe107a29841cc847f9de89e676714bd2844057ea + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.7.19~1 + espressif/w5500: + component_hash: e6e47022a1f509d1d1197652a554aea84a8cd4d99161061e187ecf4b78a7f6ca + dependencies: + - name: idf + require: private + version: '>=6.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.0.1 + idf: + source: + type: idf + version: 6.1.0 +direct_dependencies: +- espressif/cjson +- espressif/w5500 +manifest_hash: bce4dc0e6c6fb0ff282ed34e9e61e176299db42d266e76e5b467d7ae762f489a +target: esp32s3 +version: 2.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..16702f2 --- /dev/null +++ b/main/CMakeLists.txt @@ -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) diff --git a/main/config.h b/main/config.h new file mode 100644 index 0000000..f811a20 --- /dev/null +++ b/main/config.h @@ -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 diff --git a/main/ethernet_manager.c b/main/ethernet_manager.c new file mode 100644 index 0000000..b08826e --- /dev/null +++ b/main/ethernet_manager.c @@ -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 + +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, ð_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ð_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(ð_config, ð_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(¤t_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; +} diff --git a/main/ethernet_manager.h b/main/ethernet_manager.h new file mode 100644 index 0000000..f76fec4 --- /dev/null +++ b/main/ethernet_manager.h @@ -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 diff --git a/main/gpio_controller.c b/main/gpio_controller.c new file mode 100644 index 0000000..68f8d99 --- /dev/null +++ b/main/gpio_controller.c @@ -0,0 +1,278 @@ +#include "gpio_controller.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include + +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); +} diff --git a/main/gpio_controller.h b/main/gpio_controller.h new file mode 100644 index 0000000..491c044 --- /dev/null +++ b/main/gpio_controller.h @@ -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 diff --git a/main/html/index.html b/main/html/index.html new file mode 100644 index 0000000..4f128a6 --- /dev/null +++ b/main/html/index.html @@ -0,0 +1,825 @@ + + + + + + VisionFive2 BMC Dashboard + + + +
+
+

🖥️ VisionFive2 BMC Dashboard

+
+
+
+ Ethernet: Disconnected +
+
+ IP: - +
+
+ UART: 115200 baud +
+
+
+ +
+ +
+

Power Control

+
+ + + +
+
+
+ Power Status: Unknown +
+
+ + +
+

ℹ️ System Info

+
+
+
MAC Address
+
-
+
+
+
Netmask
+
-
+
+
+
Gateway
+
-
+
+
+
Link Speed
+ +
+
+
+
+ +
+ +
+

🔌 GPIO Monitor

+
+ +
+
+ + +
+

💻 Serial Console

+
+
+
Connecting to serial console...
+
+
+ + + + +
+
+
+
+
+ +
+ + + + diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..5cfee37 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + espressif/cjson: ^1.7.19~1 + espressif/w5500: ^1.0.1 diff --git a/main/main.c b/main/main.c new file mode 100644 index 0000000..f9e4d1a --- /dev/null +++ b/main/main.c @@ -0,0 +1,129 @@ +#include +#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)); + } +} diff --git a/main/uart_handler.c b/main/uart_handler.c new file mode 100644 index 0000000..b69604d --- /dev/null +++ b/main/uart_handler.c @@ -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 + +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, ¤t_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(¤t_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; +} diff --git a/main/uart_handler.h b/main/uart_handler.h new file mode 100644 index 0000000..72c324b --- /dev/null +++ b/main/uart_handler.h @@ -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 diff --git a/main/web_server.c b/main/web_server.c new file mode 100644 index 0000000..90f8857 --- /dev/null +++ b/main/web_server.c @@ -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 +#include + +static const char *TAG = TAG_HTTP; + +// Server handle +static httpd_handle_t server = NULL; +static bool server_running = false; + +// ============================================================================ +// HTML Content (Embedded) +// ============================================================================ + +extern const uint8_t index_html_start[] asm("_binary_index_html_start"); +extern const uint8_t index_html_end[] asm("_binary_index_html_end"); + +// ============================================================================ +// Helper Functions +// ============================================================================ + +static esp_err_t set_content_type_json(httpd_req_t *req) { + return httpd_resp_set_type(req, "application/json"); +} + +static esp_err_t send_json_response(httpd_req_t *req, const char *json_str) { + set_content_type_json(req); + return httpd_resp_sendstr(req, json_str); +} + +static esp_err_t send_json_error(httpd_req_t *req, const char *message) { + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "success", false); + cJSON_AddStringToObject(root, "error", message); + + char *json_str = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + + esp_err_t ret = send_json_response(req, json_str); + free(json_str); + return ret; +} + +static esp_err_t send_json_success(httpd_req_t *req, cJSON *data) { + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "success", true); + if (data) { + cJSON_AddItemToObject(root, "data", data); + } + + char *json_str = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + + esp_err_t ret = send_json_response(req, json_str); + free(json_str); + return ret; +} + +// ============================================================================ +// Static File Handlers +// ============================================================================ + +static esp_err_t index_handler(httpd_req_t *req) { + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, (const char *)index_html_start, index_html_end - index_html_start); + return ESP_OK; +} + +// ============================================================================ +// GPIO API Handlers +// ============================================================================ + +static esp_err_t api_gpio_get_all_handler(httpd_req_t *req) { + bmc_gpio_state_t states[BMC_GPIO_COUNT]; + esp_err_t ret = gpio_get_all_states(states); + + if (ret != ESP_OK) { + return send_json_error(req, "Failed to get GPIO states"); + } + + cJSON *gpios = cJSON_CreateArray(); + for (int i = 0; i < BMC_GPIO_COUNT; i++) { + cJSON *gpio = cJSON_CreateObject(); + cJSON_AddNumberToObject(gpio, "pin", states[i].pin); + cJSON_AddStringToObject(gpio, "name", states[i].name); + cJSON_AddStringToObject(gpio, "mode", + states[i].mode == GPIO_MODE_INPUT ? "input" + : states[i].mode == GPIO_MODE_OUTPUT ? "output" + : "inout"); + cJSON_AddNumberToObject(gpio, "value", states[i].value); + cJSON_AddBoolToObject(gpio, "inverted", states[i].inverted); + cJSON_AddItemToArray(gpios, gpio); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "success", true); + cJSON_AddItemToObject(root, "gpios", gpios); + + char *json_str = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + + esp_err_t resp_ret = send_json_response(req, json_str); + free(json_str); + return resp_ret; +} + +// static esp_err_t api_gpio_get_handler(httpd_req_t *req) +// { +// char pin_str[8]; +// if (httpd_req_get_url_query_str(req, pin_str, sizeof(pin_str)) != ESP_OK) { +// return send_json_error(req, "Missing pin parameter"); +// } + +// int pin = atoi(pin_str); +// bmc_gpio_state_t state; + +// esp_err_t ret = gpio_get_state((gpio_num_t)pin, &state); +// if (ret == ESP_ERR_NOT_FOUND) { +// return send_json_error(req, "GPIO not found"); +// } + +// cJSON *gpio = cJSON_CreateObject(); +// cJSON_AddNumberToObject(gpio, "pin", state.pin); +// cJSON_AddStringToObject(gpio, "name", state.name); +// cJSON_AddStringToObject(gpio, "mode", +// state.mode == GPIO_MODE_INPUT ? "input" : +// state.mode == GPIO_MODE_OUTPUT ? "output" : "inout"); +// cJSON_AddNumberToObject(gpio, "value", state.value); +// cJSON_AddBoolToObject(gpio, "inverted", state.inverted); + +// return send_json_success(req, gpio); +// } + +static esp_err_t api_gpio_set_handler(httpd_req_t *req) { + // Get pin from query string + char query[64]; + if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) { + return send_json_error(req, "Missing query parameters"); + } + + char pin_str[8]; + if (httpd_query_key_value(query, "pin", pin_str, sizeof(pin_str)) != ESP_OK) { + return send_json_error(req, "Missing pin parameter"); + } + int pin = atoi(pin_str); + + // Parse JSON body + char buf[128]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + return send_json_error(req, "No data received"); + } + buf[ret] = '\0'; + + cJSON *json = cJSON_Parse(buf); + if (!json) { + return send_json_error(req, "Invalid JSON"); + } + + cJSON *value_item = cJSON_GetObjectItem(json, "value"); + if (!value_item || !cJSON_IsNumber(value_item)) { + cJSON_Delete(json); + return send_json_error(req, "Missing or invalid 'value' field"); + } + + int value = value_item->valueint; + cJSON_Delete(json); + + // Set GPIO + ret = gpio_write((gpio_num_t)pin, value); + if (ret == ESP_ERR_NOT_FOUND) { + return send_json_error(req, "GPIO not found"); + } else if (ret == ESP_ERR_NOT_SUPPORTED) { + return send_json_error(req, "GPIO is input-only"); + } else if (ret != ESP_OK) { + return send_json_error(req, "Failed to set GPIO"); + } + + cJSON *response = cJSON_CreateObject(); + cJSON_AddNumberToObject(response, "pin", pin); + cJSON_AddNumberToObject(response, "value", value); + + return send_json_success(req, response); +} + +static esp_err_t api_gpio_config_handler(httpd_req_t *req) { + // Get pin from query string + char query[64]; + if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) { + return send_json_error(req, "Missing query parameters"); + } + + char pin_str[8]; + if (httpd_query_key_value(query, "pin", pin_str, sizeof(pin_str)) != ESP_OK) { + return send_json_error(req, "Missing pin parameter"); + } + int pin = atoi(pin_str); + + // Parse JSON body + char buf[128]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + return send_json_error(req, "No data received"); + } + buf[ret] = '\0'; + + cJSON *json = cJSON_Parse(buf); + if (!json) { + return send_json_error(req, "Invalid JSON"); + } + + cJSON *mode_item = cJSON_GetObjectItem(json, "mode"); + if (!mode_item || !cJSON_IsString(mode_item)) { + cJSON_Delete(json); + return send_json_error(req, "Missing or invalid 'mode' field"); + } + + gpio_mode_t mode; + const char *mode_str = mode_item->valuestring; + if (strcmp(mode_str, "input") == 0) { + mode = GPIO_MODE_INPUT; + } else if (strcmp(mode_str, "output") == 0) { + mode = GPIO_MODE_OUTPUT; + } else { + cJSON_Delete(json); + return send_json_error(req, "Invalid mode. Use 'input' or 'output'"); + } + + cJSON_Delete(json); + + // Configure GPIO + ret = gpio_configure((gpio_num_t)pin, mode); + if (ret == ESP_ERR_NOT_FOUND) { + return send_json_error(req, "GPIO not found"); + } else if (ret == ESP_ERR_NOT_SUPPORTED) { + return send_json_error(req, "GPIO is input-only, cannot set as output"); + } else if (ret != ESP_OK) { + return send_json_error(req, "Failed to configure GPIO"); + } + + cJSON *response = cJSON_CreateObject(); + cJSON_AddNumberToObject(response, "pin", pin); + cJSON_AddStringToObject(response, "mode", mode_str); + + return send_json_success(req, response); +} + +// ============================================================================ +// Power Control API Handlers +// ============================================================================ + +static esp_err_t api_power_status_handler(httpd_req_t *req) { + cJSON *status = cJSON_CreateObject(); + cJSON_AddBoolToObject(status, "power_good", gpio_is_power_good()); + + // Get power control GPIO states + int power_on_idx = gpio_find_index(BMC_GPIO_POWER_ON); + int power_off_idx = gpio_find_index(BMC_GPIO_POWER_OFF); + + if (power_on_idx >= 0) { + bmc_gpio_state_t state; + gpio_get_state(BMC_GPIO_POWER_ON, &state); + cJSON_AddNumberToObject(status, "power_on_state", state.value); + } + + if (power_off_idx >= 0) { + bmc_gpio_state_t state; + gpio_get_state(BMC_GPIO_POWER_OFF, &state); + cJSON_AddNumberToObject(status, "power_off_state", state.value); + } + + return send_json_success(req, status); +} + +static esp_err_t api_power_on_handler(httpd_req_t *req) { + esp_err_t ret = gpio_power_on(); + if (ret != ESP_OK) { + return send_json_error(req, "Power on failed"); + } + + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "message", "Power on sequence initiated"); + cJSON_AddBoolToObject(response, "power_good", gpio_is_power_good()); + + return send_json_success(req, response); +} + +static esp_err_t api_power_off_handler(httpd_req_t *req) { + esp_err_t ret = gpio_power_off(); + if (ret != ESP_OK) { + return send_json_error(req, "Power off failed"); + } + + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "message", "Power off sequence initiated"); + + return send_json_success(req, response); +} + +static esp_err_t api_power_reset_handler(httpd_req_t *req) { + esp_err_t ret = gpio_reset(); + if (ret != ESP_OK) { + return send_json_error(req, "Reset failed"); + } + + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "message", "Reset sequence initiated"); + + return send_json_success(req, response); +} + +// ============================================================================ +// Serial API Handlers +// ============================================================================ + +static esp_err_t api_serial_config_get_handler(httpd_req_t *req) { + bmc_uart_config_t config; + esp_err_t ret = uart_get_config(&config); + + if (ret != ESP_OK) { + return send_json_error(req, "Failed to get serial config"); + } + + cJSON *config_json = cJSON_CreateObject(); + cJSON_AddNumberToObject(config_json, "baud_rate", config.baud_rate); + cJSON_AddNumberToObject(config_json, "data_bits", config.data_bits); + cJSON_AddNumberToObject(config_json, "parity", config.parity); + cJSON_AddNumberToObject(config_json, "stop_bits", config.stop_bits); + + return send_json_success(req, config_json); +} + +static esp_err_t api_serial_config_set_handler(httpd_req_t *req) { + char buf[256]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + return send_json_error(req, "No data received"); + } + buf[ret] = '\0'; + + cJSON *json = cJSON_Parse(buf); + if (!json) { + return send_json_error(req, "Invalid JSON"); + } + + bmc_uart_config_t config; + uart_get_config(&config); // Get current config + + cJSON *baud = cJSON_GetObjectItem(json, "baud_rate"); + if (baud && cJSON_IsNumber(baud)) { + config.baud_rate = baud->valueint; + } + + cJSON_Delete(json); + + ret = uart_set_baud_rate(config.baud_rate); + if (ret != ESP_OK) { + return send_json_error(req, "Failed to set baud rate"); + } + + cJSON *response = cJSON_CreateObject(); + cJSON_AddNumberToObject(response, "baud_rate", config.baud_rate); + + return send_json_success(req, response); +} + +static esp_err_t api_serial_send_handler(httpd_req_t *req) { + char buf[1024]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + return send_json_error(req, "No data received"); + } + + int written = uart_write_data((uint8_t *)buf, ret); + if (written < 0) { + return send_json_error(req, "Failed to send data"); + } + + cJSON *response = cJSON_CreateObject(); + cJSON_AddNumberToObject(response, "bytes_sent", written); + + return send_json_success(req, response); +} + +// ============================================================================ +// WebSocket Handler for Serial Console +// ============================================================================ + +static void uart_to_ws_callback(const uint8_t *data, size_t len) { + // Always broadcast - the function will dynamically check for connected clients + web_server_ws_broadcast(data, len); +} + +static esp_err_t ws_serial_handler(httpd_req_t *req) { + // For WebSocket handlers with is_websocket=true, ESP-IDF handles the handshake + // internally. This handler is only called for frame processing. + // Client tracking is done dynamically in the broadcast function. + + // Handle WebSocket frames + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + + // First call gets the frame info + esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); + if (ret != ESP_OK) { + // This is normal when client disconnects - don't log as error + return ret; + } + + // Handle different frame types + switch (ws_pkt.type) { + case HTTPD_WS_TYPE_CLOSE: + // Send close frame in response + ws_pkt.len = 0; + ws_pkt.payload = NULL; + httpd_ws_send_frame(req, &ws_pkt); + return ESP_OK; + + case HTTPD_WS_TYPE_PING: + // Respond with pong + ws_pkt.type = HTTPD_WS_TYPE_PONG; + return httpd_ws_send_frame(req, &ws_pkt); + + case HTTPD_WS_TYPE_TEXT: + case HTTPD_WS_TYPE_BINARY: + // Handle text/binary data (send to UART) + if (ws_pkt.len > 0) { + uint8_t *buf = malloc(ws_pkt.len + 1); + if (buf) { + ws_pkt.payload = buf; + ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); + if (ret == ESP_OK) { + uart_write_data(ws_pkt.payload, ws_pkt.len); + } + free(buf); + } + } + return ESP_OK; + + default: + ESP_LOGI(TAG_HTTP, "WS frame of unknown type received, ignored"); + return ESP_OK; + } +} + +// ============================================================================ +// System API Handlers +// ============================================================================ + +static esp_err_t api_system_info_handler(httpd_req_t *req) { + cJSON *info = cJSON_CreateObject(); + + // Network info + char ip[16], netmask[16], gateway[16], mac[18]; + if (ethernet_get_network_info(ip, netmask, gateway) == ESP_OK) { + cJSON_AddStringToObject(info, "ip", ip); + cJSON_AddStringToObject(info, "netmask", netmask); + cJSON_AddStringToObject(info, "gateway", gateway); + } + + if (ethernet_get_mac(mac) == ESP_OK) { + cJSON_AddStringToObject(info, "mac", mac); + } + + cJSON_AddBoolToObject(info, "ethernet_connected", ethernet_is_connected()); + cJSON_AddNumberToObject(info, "link_speed", ethernet_get_link_speed()); + cJSON_AddBoolToObject(info, "full_duplex", ethernet_is_full_duplex()); + + // UART info + cJSON_AddNumberToObject(info, "uart_baud_rate", uart_get_baud_rate()); + + return send_json_success(req, info); +} + +static esp_err_t api_system_status_handler(httpd_req_t *req) { + cJSON *status = cJSON_CreateObject(); + + // Power status + cJSON *power = cJSON_CreateObject(); + cJSON_AddBoolToObject(power, "power_good", gpio_is_power_good()); + cJSON_AddItemToObject(status, "power", power); + + // Network status + cJSON *network = cJSON_CreateObject(); + cJSON_AddBoolToObject(network, "connected", ethernet_is_connected()); + char ip[16]; + if (ethernet_get_ip(ip) == ESP_OK) { + cJSON_AddStringToObject(network, "ip", ip); + } + cJSON_AddItemToObject(status, "network", network); + + // Serial status + cJSON *serial = cJSON_CreateObject(); + cJSON_AddNumberToObject(serial, "baud_rate", uart_get_baud_rate()); + cJSON_AddNumberToObject(serial, "rx_available", uart_data_available()); + cJSON_AddItemToObject(status, "serial", serial); + + return send_json_success(req, status); +} + +// ============================================================================ +// URI Registration +// ============================================================================ + +static const httpd_uri_t uri_index = { + .uri = "/", + .method = HTTP_GET, + .handler = index_handler, +}; + +static const httpd_uri_t uri_api_gpio = { + .uri = "/api/gpio", + .method = HTTP_GET, + .handler = api_gpio_get_all_handler, +}; + +static const httpd_uri_t uri_api_gpio_set = { + .uri = "/api/gpio/set", + .method = HTTP_POST, + .handler = api_gpio_set_handler, +}; + +static const httpd_uri_t uri_api_gpio_config = { + .uri = "/api/gpio/config", + .method = HTTP_PUT, + .handler = api_gpio_config_handler, +}; + +static const httpd_uri_t uri_api_power_status = { + .uri = "/api/power/status", + .method = HTTP_GET, + .handler = api_power_status_handler, +}; + +static const httpd_uri_t uri_api_power_on = { + .uri = "/api/power/on", + .method = HTTP_POST, + .handler = api_power_on_handler, +}; + +static const httpd_uri_t uri_api_power_off = { + .uri = "/api/power/off", + .method = HTTP_POST, + .handler = api_power_off_handler, +}; + +static const httpd_uri_t uri_api_power_reset = { + .uri = "/api/power/reset", + .method = HTTP_POST, + .handler = api_power_reset_handler, +}; + +static const httpd_uri_t uri_api_serial_config_get = { + .uri = "/api/serial/config", + .method = HTTP_GET, + .handler = api_serial_config_get_handler, +}; + +static const httpd_uri_t uri_api_serial_config_set = { + .uri = "/api/serial/config", + .method = HTTP_PUT, + .handler = api_serial_config_set_handler, +}; + +static const httpd_uri_t uri_api_serial_send = { + .uri = "/api/serial/send", + .method = HTTP_POST, + .handler = api_serial_send_handler, +}; + +static const httpd_uri_t uri_ws_serial = { + .uri = "/api/serial/ws", + .method = HTTP_GET, + .handler = ws_serial_handler, + .is_websocket = true, +}; + +static const httpd_uri_t uri_api_system_info = { + .uri = "/api/system/info", + .method = HTTP_GET, + .handler = api_system_info_handler, +}; + +static const httpd_uri_t uri_api_system_status = { + .uri = "/api/system/status", + .method = HTTP_GET, + .handler = api_system_status_handler, +}; + +// ============================================================================ +// Public Functions +// ============================================================================ + +esp_err_t web_server_start(void) { + if (server_running) { + ESP_LOGW(TAG, "Web server already running"); + return ESP_OK; + } + + ESP_LOGI(TAG, "Starting web server on port %d", BMC_HTTP_PORT); + + // Configure server + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = BMC_HTTP_PORT; + config.max_uri_handlers = 20; + config.stack_size = BMC_HTTP_STACK_SIZE; + config.max_open_sockets = BMC_HTTP_MAX_CONN; + config.lru_purge_enable = false; // Don't close connections when limit reached + config.keep_alive_enable = true; // Enable keep-alive for better connection handling + config.keep_alive_idle = 5; // Seconds before keep-alive starts + config.keep_alive_interval = 5; // Seconds between keep-alive probes + config.keep_alive_count = 3; // Number of failed probes before closing + + // Start server + esp_err_t ret = httpd_start(&server, &config); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to start web server: %s", esp_err_to_name(ret)); + return ret; + } + + // Register URI handlers + httpd_register_uri_handler(server, &uri_index); + httpd_register_uri_handler(server, &uri_api_gpio); + httpd_register_uri_handler(server, &uri_api_gpio_set); + httpd_register_uri_handler(server, &uri_api_gpio_config); + httpd_register_uri_handler(server, &uri_api_power_status); + httpd_register_uri_handler(server, &uri_api_power_on); + httpd_register_uri_handler(server, &uri_api_power_off); + httpd_register_uri_handler(server, &uri_api_power_reset); + httpd_register_uri_handler(server, &uri_api_serial_config_get); + httpd_register_uri_handler(server, &uri_api_serial_config_set); + httpd_register_uri_handler(server, &uri_api_serial_send); + httpd_register_uri_handler(server, &uri_ws_serial); + httpd_register_uri_handler(server, &uri_api_system_info); + httpd_register_uri_handler(server, &uri_api_system_status); + + // Register UART callback for WebSocket broadcast at startup + esp_err_t cb_ret = uart_register_rx_callback(uart_to_ws_callback); + if (cb_ret == ESP_OK) { + ESP_LOGI(TAG, "UART RX callback registered for WebSocket broadcast"); + } else { + ESP_LOGW(TAG, "Failed to register UART RX callback: %s", esp_err_to_name(cb_ret)); + } + + server_running = true; + ESP_LOGI(TAG, "Web server started successfully"); + + return ESP_OK; +} + +esp_err_t web_server_stop(void) { + if (!server_running) { + return ESP_OK; + } + + ESP_LOGI(TAG, "Stopping web server"); + + httpd_stop(server); + server = NULL; + server_running = false; + + return ESP_OK; +} + +bool web_server_is_running(void) { + return server_running; +} + +httpd_handle_t web_server_get_handle(void) { + return server; +} + +esp_err_t web_server_ws_broadcast(const uint8_t *data, size_t len) { + if (!server_running || !data || len == 0) { + return ESP_ERR_INVALID_STATE; + } + + httpd_ws_frame_t ws_pkt = { + .payload = (uint8_t *)data, + .len = len, + .type = HTTPD_WS_TYPE_TEXT, + .final = true, + }; + +// Use a fixed-size array to avoid stack issues +#define MAX_WS_BROADCAST_CLIENTS 4 + int client_fds[MAX_WS_BROADCAST_CLIENTS]; + size_t clients = MAX_WS_BROADCAST_CLIENTS; + + // Get client list - httpd has its own internal locking + esp_err_t ret = httpd_get_client_list(server, &clients, client_fds); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Failed to get client list: %s", esp_err_to_name(ret)); + return ret; + } + + // Count and send to all WebSocket clients + int ws_count = 0; + for (size_t i = 0; i < clients && i < MAX_WS_BROADCAST_CLIENTS; i++) { + int fd = client_fds[i]; + httpd_ws_client_info_t info = httpd_ws_get_fd_info(server, fd); + if (info == HTTPD_WS_CLIENT_WEBSOCKET) { + ws_count++; + esp_err_t err = httpd_ws_send_frame_async(server, fd, &ws_pkt); + if (err != ESP_OK) { + ESP_LOGD(TAG, "WS send to fd %d failed: %s", fd, esp_err_to_name(err)); + } + } + } + + // Log if we have multiple clients + if (ws_count > 1) { + ESP_LOGD(TAG, "Broadcast to %d WS clients", ws_count); + } + + return ESP_OK; +} + +int web_server_ws_client_count(void) { + // Dynamically count WebSocket clients + if (!server_running) { + return 0; + } + +#define MAX_WS_COUNT_CLIENTS 4 + int client_fds[MAX_WS_COUNT_CLIENTS]; + size_t clients = MAX_WS_COUNT_CLIENTS; + int count = 0; + + if (httpd_get_client_list(server, &clients, client_fds) == ESP_OK) { + for (size_t i = 0; i < clients && i < MAX_WS_COUNT_CLIENTS; i++) { + if (httpd_ws_get_fd_info(server, client_fds[i]) == HTTPD_WS_CLIENT_WEBSOCKET) { + count++; + } + } + } + + return count; +} diff --git a/main/web_server.h b/main/web_server.h new file mode 100644 index 0000000..2a590f4 --- /dev/null +++ b/main/web_server.h @@ -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 diff --git a/plans/esp32-bmc-plan.md b/plans/esp32-bmc-plan.md new file mode 100644 index 0000000..801c6fd --- /dev/null +++ b/plans/esp32-bmc-plan.md @@ -0,0 +1,348 @@ +# ESP32-S3 BMC Firmware Plan + +## Overview + +This project creates a Baseboard Management Controller (BMC) firmware for ESP32-S3 that provides remote management capabilities for an external board via Ethernet. + +## Framework Decision: ESP-IDF + +**Selected Framework:** ESP-IDF (Espressif IoT Development Framework) + +**Rationale:** +- Native W5500 SPI Ethernet driver support +- Excellent HTTP server performance with `esp_http_server` +- FreeRTOS enables efficient multitasking for concurrent operations +- Production-ready with long-term support +- Good balance between development speed and performance + +--- + +## System Architecture + +```mermaid +flowchart TB + subgraph ESP32-S3 + subgraph Software + WEB[HTTP Web Server] + API[REST API Handler] + GPIO[GPIO Controller] + UART[UART Serial Module] + ETH[Ethernet Manager - W5500] + NET[Network Stack - LwIP] + end + + subgraph Hardware + SPI[SPI Bus] + GP_IO[GPIO Pins 8-16] + UART_HW[UART Hardware] + end + end + + subgraph External + CLIENT[Web Browser Client] + BOARD[Target Board] + W5500[W5500 Ethernet Module] + end + + CLIENT -->|HTTP| WEB + WEB --> API + API --> GPIO + API --> UART + GPIO --> GP_IO --> BOARD + UART --> UART_HW --> BOARD + ETH --> SPI --> W5500 + NET --> ETH +``` + +--- + +## Project Structure + +``` +esp32-bmc/ +├── CMakeLists.txt +├── sdkconfig.defaults +├── main/ +│ ├── CMakeLists.txt +│ ├── main.c # Application entry point +│ ├── gpio_controller.c # GPIO management +│ ├── gpio_controller.h +│ ├── uart_handler.c # UART serial communication +│ ├── uart_handler.h +│ ├── ethernet_manager.c # W5500 Ethernet setup +│ ├── ethernet_manager.h +│ ├── web_server.c # HTTP server and routes +│ ├── web_server.h +│ └── html/ +│ └── index.html # Embedded web interface +├── components/ +│ └── w5500/ # W5500 driver component if needed +├── include/ +│ └── config.h # Pin definitions and settings +└── docs/ + └── README.md +``` + +--- + +## Hardware Configuration + +### Pin Assignments (ESP32-S3) + +| Function | GPIO Pin | Description | +|----------|----------|-------------| +| **SPI for W5500** | | | +| MOSI | GPIO 11 | SPI MOSI | +| MISO | GPIO 13 | SPI MISO | +| SCLK | GPIO 12 | SPI Clock | +| CS | GPIO 10 | Chip Select | +| RST | GPIO 9 | W5500 Reset | +| INT | GPIO 14 | W5500 Interrupt | +| **UART Serial** | | | +| TX | GPIO 43 | UART0 TX to target board | +| RX | GPIO 44 | UART0 RX from target board | +| **BMC GPIOs** | | | +| POWER_ON | GPIO 4 | Power control output | +| POWER_OFF | GPIO 5 | Power control output | +| RESET | GPIO 6 | Reset control output | +| STATUS_LED | GPIO 7 | Status LED output | +| PWR_GOOD | GPIO 15 | Power good input | +| TEMP_ALERT | GPIO 16 | Temperature alert input | +| FAN_CTRL | GPIO 17 | Fan control PWM | +| USER_GPIO_1-8 | GPIO 18-25 | User configurable GPIOs | + +--- + +## REST API Endpoints + +### GPIO Control + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/gpio` | Get all GPIO states | +| GET | `/api/gpio/{pin}` | Get specific GPIO state | +| POST | `/api/gpio/{pin}` | Set GPIO state | +| PUT | `/api/gpio/{pin}/config` | Configure GPIO mode | + +**Example Request/Response:** + +```json +// GET /api/gpio +{ + "gpios": [ + {"pin": 4, "name": "POWER_ON", "mode": "output", "value": 1}, + {"pin": 5, "name": "POWER_OFF", "mode": "output", "value": 0}, + {"pin": 15, "name": "PWR_GOOD", "mode": "input", "value": 1} + ] +} + +// POST /api/gpio/4 +// Request body: +{"value": 1} +// Response: +{"success": true, "pin": 4, "value": 1} +``` + +### Power Control + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/power/on` | Turn board power on | +| POST | `/api/power/off` | Turn board power off | +| POST | `/api/power/reset` | Reset the board | +| GET | `/api/power/status` | Get power status | + +### Serial Console + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/serial` | WebSocket for serial console | +| POST | `/api/serial/send` | Send data to serial | +| GET | `/api/serial/config` | Get serial configuration | +| PUT | `/api/serial/config` | Configure serial settings | + +### System + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/system/info` | Get system information | +| GET | `/api/system/status` | Get BMC status | + +--- + +## Web Interface + +A simple HTML/CSS dashboard will be embedded in the firmware providing: + +1. **Power Control Panel** - Buttons for power on/off/reset with status indicators +2. **GPIO Monitor** - Real-time display of GPIO states +3. **Serial Console** - Web-based terminal for serial communication +4. **System Info** - Display BMC status and network information + +```mermaid +flowchart LR + subgraph Web Interface + PWR[Power Control Panel] + GPIO[GPIO Monitor] + SER[Serial Console] + SYS[System Info] + end + + PWR -->|REST API| API[REST Endpoints] + GPIO -->|REST API| API + SER -->|WebSocket| WS[Serial Bridge] + SYS -->|REST API| API +``` + +--- + +## Implementation Details + +### 1. Ethernet Manager (W5500) + +The W5500 will be configured using SPI interface: + +```c +// Key configuration steps +1. Initialize SPI driver +2. Configure W5500 MAC address +3. Set up DHCP client or static IP +4. Handle network events (connect/disconnect) +5. Register network callbacks +``` + +### 2. GPIO Controller + +```c +// GPIO configuration structure +typedef struct { + gpio_num_t pin; + const char* name; + gpio_mode_t mode; + int default_value; + bool inverted; // For active-low signals +} bmc_gpio_config_t; + +// Functions +void gpio_controller_init(void); +int gpio_read(gpio_num_t pin); +esp_err_t gpio_write(gpio_num_t pin, int value); +esp_err_t gpio_configure(gpio_num_t pin, gpio_mode_t mode); +``` + +### 3. UART Handler + +```c +// UART configuration +#define UART_NUM UART_NUM_1 +#define UART_BAUD_RATE 115200 +#define UART_BUF_SIZE 2048 + +// Functions +void uart_handler_init(void); +int uart_read_bytes(uint8_t* buf, size_t len); +int uart_write_bytes(const uint8_t* buf, size_t len); +``` + +### 4. Web Server + +Using `esp_http_server` component: + +```c +// Server setup +httpd_handle_t server = NULL; +httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + +// Register URI handlers +httpd_register_uri_handler(server, &gpio_get_uri); +httpd_register_uri_handler(server, &gpio_post_uri); +httpd_register_uri_handler(server, &power_on_uri); +httpd_register_uri_handler(server, &serial_ws_uri); +``` + +### 5. Serial WebSocket Bridge + +The serial endpoint will use WebSocket for bidirectional communication: + +```mermaid +sequenceDiagram + participant Browser + participant WebSocket + participant UART Buffer + participant Target Board + + Browser->>WebSocket: Connect to /api/serial + WebSocket->>UART Buffer: Start reading task + + loop Serial Data + Target Board->>UART Buffer: Serial data + UART Buffer->>WebSocket: Data available + WebSocket->>Browser: Send data frame + end + + Browser->>WebSocket: Send command + WebSocket->>Target Board: Write to UART +``` + +--- + +## Configuration Options + +The firmware will support configuration via `sdkconfig` and a runtime config file: + +| Option | Default | Description | +|--------|---------|-------------| +| BMC_ETHERNET_DHCP | true | Use DHCP for IP | +| BMC_STATIC_IP | 192.168.1.100 | Static IP if DHCP disabled | +| BMC_NETMASK | 255.255.255.0 | Network mask | +| BMC_GATEWAY | 192.168.1.1 | Default gateway | +| BMC_HTTP_PORT | 80 | Web server port | +| BMC_UART_BAUD | 115200 | Serial baud rate | +| BMC_GPIO_COUNT | 16 | Number of GPIOs to manage | + +--- + +## Development Phases + +### Phase 1: Core Infrastructure +- Initialize ESP-IDF project +- Set up W5500 Ethernet connectivity +- Basic GPIO read/write functionality + +### Phase 2: Web Server +- Implement HTTP server +- Create REST API endpoints for GPIO +- Add power control endpoints + +### Phase 3: Serial Console +- Implement UART handler +- Create WebSocket bridge +- Add serial configuration endpoints + +### Phase 4: Web Interface +- Design and implement HTML/CSS dashboard +- Embed files in firmware +- Add JavaScript for dynamic updates + +### Phase 5: Polish and Testing +- Error handling and recovery +- Documentation +- Testing on hardware + +--- + +## Dependencies + +- ESP-IDF v5.0+ +- esp_http_server component +- driver component (SPI, GPIO, UART) +- lwIP stack + +--- + +## Next Steps + +1. Initialize the ESP-IDF project structure +2. Configure W5500 SPI Ethernet +3. Implement GPIO controller module +4. Build and test incrementally diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..0316e7e --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,35 @@ +# ESP32-S3 BMC Configuration + +# Target chip +CONFIG_IDF_TARGET="esp32s3" + +# Enable FreeRTOS +CONFIG_FREERTOS_HZ=1000 + +# HTTP Server configuration +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_PURGE_BUF_LEN=256 +CONFIG_HTTPD_WS_SUPPORT=y + +# LWIP configuration +CONFIG_LWIP_LOCAL_HOSTNAME="vf2-bmc" +CONFIG_LWIP_TCP_MSS=1436 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744 +CONFIG_LWIP_TCP_WND_DEFAULT=5744 +CONFIG_LWIP_MAX_SOCKETS=7 +CONFIG_LWIP_SO_REUSE=y +CONFIG_LWIP_SO_RCVBUF=y + +# Enable pthread +CONFIG_PTHREAD_TASK_PRIO_DEFAULT=5 + +# Main task stack size +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +# Log level +CONFIG_LOG_DEFAULT_LEVEL_INFO=y + +# USB Serial JTAG console output (ESP32-S3) +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y