Initial commit
ESP32-S3 firmware acting as BMC for another board, controlling an ATX power supply and providing access to UART. Co-authored with GLM-5
This commit is contained in:
154
.clang-format
Normal file
154
.clang-format
Normal file
@@ -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
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
build
|
||||||
|
managed_components
|
||||||
|
sdkconfig
|
||||||
|
sdkconfig.old
|
||||||
52
.vscode/settings.json
vendored
Normal file
52
.vscode/settings.json
vendored
Normal file
@@ -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"
|
||||||
|
}
|
||||||
9
CMakeLists.txt
Normal file
9
CMakeLists.txt
Normal file
@@ -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)
|
||||||
253
README.md
Normal file
253
README.md
Normal file
@@ -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://<ip-address>/` 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.
|
||||||
31
dependencies.lock
Normal file
31
dependencies.lock
Normal file
@@ -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
|
||||||
11
main/CMakeLists.txt
Normal file
11
main/CMakeLists.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
idf_component_register(SRCS "main.c"
|
||||||
|
"gpio_controller.c"
|
||||||
|
"uart_handler.c"
|
||||||
|
"ethernet_manager.c"
|
||||||
|
"web_server.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES esp_driver_gpio esp_driver_uart esp_driver_spi
|
||||||
|
esp_http_server esp_eth esp_netif lwip spi_flash nvs_flash log)
|
||||||
|
|
||||||
|
# Embed HTML file
|
||||||
|
target_add_binary_data(${COMPONENT_TARGET} "html/index.html" TEXT)
|
||||||
122
main/config.h
Normal file
122
main/config.h
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#ifndef BMC_CONFIG_H
|
||||||
|
#define BMC_CONFIG_H
|
||||||
|
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Ethernet Configuration (W5500 SPI)
|
||||||
|
// ============================================================================
|
||||||
|
#define ETH_SPI_HOST SPI2_HOST
|
||||||
|
#define ETH_SPI_MISO_GPIO GPIO_NUM_12
|
||||||
|
#define ETH_SPI_MOSI_GPIO GPIO_NUM_11
|
||||||
|
#define ETH_SPI_SCLK_GPIO GPIO_NUM_13
|
||||||
|
#define ETH_SPI_CS_GPIO GPIO_NUM_14
|
||||||
|
#define ETH_SPI_INT_GPIO GPIO_NUM_10
|
||||||
|
#define ETH_SPI_RST_GPIO GPIO_NUM_9
|
||||||
|
|
||||||
|
// MAC address (can be customized)
|
||||||
|
#define ETH_MAC_ADDR {0x02, 0x00, 0x00, 0x12, 0x34, 0x56}
|
||||||
|
|
||||||
|
// Network configuration (used if DHCP fails or is disabled)
|
||||||
|
#define BMC_USE_DHCP 1
|
||||||
|
#define BMC_STATIC_IP "192.168.1.100"
|
||||||
|
#define BMC_NETMASK "255.255.255.0"
|
||||||
|
#define BMC_GATEWAY "192.168.1.1"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// UART Serial Configuration
|
||||||
|
// ============================================================================
|
||||||
|
#define BMC_UART_NUM UART_NUM_1
|
||||||
|
#define BMC_UART_TX_GPIO GPIO_NUM_43
|
||||||
|
#define BMC_UART_RX_GPIO GPIO_NUM_44
|
||||||
|
#define BMC_UART_BAUD_RATE 115200
|
||||||
|
#define BMC_UART_BUF_SIZE 2048
|
||||||
|
#define BMC_UART_TASK_STACK 4096
|
||||||
|
#define BMC_UART_TASK_PRIO 5
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HTTP Server Configuration
|
||||||
|
// ============================================================================
|
||||||
|
#define BMC_HTTP_PORT 80
|
||||||
|
#define BMC_HTTP_MAX_CONN 4
|
||||||
|
#define BMC_HTTP_STACK_SIZE 8192
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GPIO Configuration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Number of user-configurable GPIOs
|
||||||
|
#define BMC_GPIO_COUNT 16
|
||||||
|
|
||||||
|
// GPIO pin definitions with names and default modes
|
||||||
|
typedef struct {
|
||||||
|
gpio_num_t pin;
|
||||||
|
const char *name;
|
||||||
|
gpio_mode_t mode;
|
||||||
|
int default_value;
|
||||||
|
bool inverted; // Active-low logic
|
||||||
|
bool is_input_only; // Some pins are input-only
|
||||||
|
} bmc_gpio_def_t;
|
||||||
|
|
||||||
|
// Predefined GPIO assignments
|
||||||
|
#define BMC_GPIO_POWER_ON GPIO_NUM_4
|
||||||
|
#define BMC_GPIO_POWER_OFF GPIO_NUM_5
|
||||||
|
#define BMC_GPIO_RESET GPIO_NUM_6
|
||||||
|
#define BMC_GPIO_STATUS_LED GPIO_NUM_7
|
||||||
|
#define BMC_GPIO_PWR_GOOD GPIO_NUM_15
|
||||||
|
#define BMC_GPIO_TEMP_ALERT GPIO_NUM_16
|
||||||
|
#define BMC_GPIO_FAN_CTRL GPIO_NUM_17
|
||||||
|
#define BMC_GPIO_USER_1 GPIO_NUM_18
|
||||||
|
#define BMC_GPIO_USER_2 GPIO_NUM_21
|
||||||
|
#define BMC_GPIO_USER_3 GPIO_NUM_35
|
||||||
|
#define BMC_GPIO_USER_4 GPIO_NUM_36
|
||||||
|
#define BMC_GPIO_USER_5 GPIO_NUM_37
|
||||||
|
#define BMC_GPIO_USER_6 GPIO_NUM_38
|
||||||
|
#define BMC_GPIO_USER_7 GPIO_NUM_47
|
||||||
|
#define BMC_GPIO_USER_8 GPIO_NUM_48
|
||||||
|
|
||||||
|
// Default GPIO configuration table
|
||||||
|
static const bmc_gpio_def_t bmc_gpio_defaults[BMC_GPIO_COUNT] = {
|
||||||
|
// Power control outputs
|
||||||
|
{ BMC_GPIO_POWER_ON, "POWER_ON", GPIO_MODE_OUTPUT, 0, false, false},
|
||||||
|
{ BMC_GPIO_POWER_OFF, "POWER_OFF", GPIO_MODE_OUTPUT, 0, false, false},
|
||||||
|
{ BMC_GPIO_RESET, "RESET", GPIO_MODE_OUTPUT, 0, true, false}, // Active-low reset
|
||||||
|
{BMC_GPIO_STATUS_LED, "STATUS_LED", GPIO_MODE_OUTPUT, 0, false, false},
|
||||||
|
|
||||||
|
// Status inputs
|
||||||
|
{ BMC_GPIO_PWR_GOOD, "PWR_GOOD", GPIO_MODE_INPUT, 0, false, false},
|
||||||
|
{BMC_GPIO_TEMP_ALERT, "TEMP_ALERT", GPIO_MODE_INPUT, 0, true, false}, // Active-low alert
|
||||||
|
|
||||||
|
// PWM output
|
||||||
|
{ BMC_GPIO_FAN_CTRL, "FAN_CTRL", GPIO_MODE_OUTPUT, 0, false, false},
|
||||||
|
|
||||||
|
// User-configurable GPIOs
|
||||||
|
{ BMC_GPIO_USER_1, "USER_1", GPIO_MODE_INPUT, 0, false, false},
|
||||||
|
{ BMC_GPIO_USER_2, "USER_2", GPIO_MODE_INPUT, 0, false, false},
|
||||||
|
{ BMC_GPIO_USER_3, "USER_3", GPIO_MODE_INPUT, 0, false, false},
|
||||||
|
{ BMC_GPIO_USER_4, "USER_4", GPIO_MODE_INPUT, 0, false, false},
|
||||||
|
{ BMC_GPIO_USER_5, "USER_5", GPIO_MODE_INPUT, 0, false, true}, // Input-only on ESP32-S3
|
||||||
|
{ BMC_GPIO_USER_6, "USER_6", GPIO_MODE_INPUT, 0, false, true}, // Input-only on ESP32-S3
|
||||||
|
{ BMC_GPIO_USER_7, "USER_7", GPIO_MODE_INPUT, 0, false, true}, // Input-only on ESP32-S3
|
||||||
|
{ BMC_GPIO_USER_8, "USER_8", GPIO_MODE_INPUT, 0, false, true}, // Input-only on ESP32-S3
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Power Control Timing
|
||||||
|
// ============================================================================
|
||||||
|
#define BMC_POWER_ON_PULSE_MS 100
|
||||||
|
#define BMC_POWER_OFF_PULSE_MS 5000
|
||||||
|
#define BMC_RESET_PULSE_MS 100
|
||||||
|
#define BMC_POWER_GOOD_DELAY_MS 2000
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Logging Tags
|
||||||
|
// ============================================================================
|
||||||
|
#define TAG_BMC "BMC"
|
||||||
|
#define TAG_GPIO "GPIO"
|
||||||
|
#define TAG_UART "UART"
|
||||||
|
#define TAG_ETH "ETH"
|
||||||
|
#define TAG_HTTP "HTTP"
|
||||||
|
|
||||||
|
#endif // BMC_CONFIG_H
|
||||||
362
main/ethernet_manager.c
Normal file
362
main/ethernet_manager.c
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
#include "ethernet_manager.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_eth_driver.h"
|
||||||
|
#include "esp_eth_mac.h"
|
||||||
|
#include "esp_eth_phy.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "driver/spi_master.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "lwip/ip_addr.h"
|
||||||
|
#include "esp_eth_phy_w5500.h"
|
||||||
|
#include "esp_eth_mac_w5500.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = TAG_ETH;
|
||||||
|
|
||||||
|
// Ethernet handles
|
||||||
|
static esp_eth_handle_t eth_handle = NULL;
|
||||||
|
static esp_eth_mac_t *eth_mac = NULL;
|
||||||
|
static esp_eth_phy_t *eth_phy = NULL;
|
||||||
|
static esp_netif_t *eth_netif = NULL;
|
||||||
|
|
||||||
|
// State tracking
|
||||||
|
static bool eth_initialized = false;
|
||||||
|
static bool eth_connected = false;
|
||||||
|
static bool eth_has_ip = false;
|
||||||
|
static SemaphoreHandle_t eth_mutex = NULL;
|
||||||
|
|
||||||
|
// Event callback
|
||||||
|
static eth_event_callback_t event_callback = NULL;
|
||||||
|
|
||||||
|
// Current configuration
|
||||||
|
static bmc_eth_config_t current_config = {
|
||||||
|
.use_dhcp = true,
|
||||||
|
.static_ip = BMC_STATIC_IP,
|
||||||
|
.netmask = BMC_NETMASK,
|
||||||
|
.gateway = BMC_GATEWAY,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Event Handler
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) {
|
||||||
|
switch (event_id) {
|
||||||
|
case ETHERNET_EVENT_CONNECTED:
|
||||||
|
ESP_LOGI(TAG, "Ethernet link up");
|
||||||
|
eth_connected = true;
|
||||||
|
if (event_callback) {
|
||||||
|
event_callback(ETH_EVENT_CONNECTED);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ETHERNET_EVENT_DISCONNECTED:
|
||||||
|
ESP_LOGI(TAG, "Ethernet link down");
|
||||||
|
eth_connected = false;
|
||||||
|
eth_has_ip = false;
|
||||||
|
if (event_callback) {
|
||||||
|
event_callback(ETH_EVENT_DISCONNECTED);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ETHERNET_EVENT_START:
|
||||||
|
ESP_LOGI(TAG, "Ethernet started");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ETHERNET_EVENT_STOP:
|
||||||
|
ESP_LOGI(TAG, "Ethernet stopped");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IP_EVENT_ETH_GOT_IP: {
|
||||||
|
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||||
|
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
|
||||||
|
ESP_LOGI(TAG, "Netmask: " IPSTR, IP2STR(&event->ip_info.netmask));
|
||||||
|
ESP_LOGI(TAG, "Gateway: " IPSTR, IP2STR(&event->ip_info.gw));
|
||||||
|
eth_has_ip = true;
|
||||||
|
if (event_callback) {
|
||||||
|
event_callback(ETH_EVENT_GOT_IP);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SPI Configuration for W5500
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static esp_err_t init_spi_for_ethernet(void) {
|
||||||
|
spi_bus_config_t buscfg = {
|
||||||
|
.miso_io_num = ETH_SPI_MISO_GPIO,
|
||||||
|
.mosi_io_num = ETH_SPI_MOSI_GPIO,
|
||||||
|
.sclk_io_num = ETH_SPI_SCLK_GPIO,
|
||||||
|
.quadwp_io_num = -1,
|
||||||
|
.quadhd_io_num = -1,
|
||||||
|
.max_transfer_sz = 4096,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t ret = spi_bus_initialize(ETH_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO);
|
||||||
|
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "SPI bus initialized for W5500");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Public Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
esp_err_t ethernet_manager_init(void) {
|
||||||
|
if (eth_initialized) {
|
||||||
|
ESP_LOGW(TAG, "Ethernet manager already initialized");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Initializing Ethernet manager (W5500 SPI)");
|
||||||
|
|
||||||
|
// Create mutex
|
||||||
|
eth_mutex = xSemaphoreCreateMutex();
|
||||||
|
if (!eth_mutex) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create mutex");
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize TCP/IP stack
|
||||||
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
|
|
||||||
|
// Create default event loop if not already created
|
||||||
|
esp_err_t loop_ret = esp_event_loop_create_default();
|
||||||
|
if (loop_ret != ESP_OK && loop_ret != ESP_ERR_INVALID_STATE) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create event loop: %s", esp_err_to_name(loop_ret));
|
||||||
|
return loop_ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create default ETH netif
|
||||||
|
esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH();
|
||||||
|
eth_netif = esp_netif_new(&netif_cfg);
|
||||||
|
|
||||||
|
// Register event handlers
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ð_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;
|
||||||
|
}
|
||||||
117
main/ethernet_manager.h
Normal file
117
main/ethernet_manager.h
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#ifndef ETHERNET_MANAGER_H
|
||||||
|
#define ETHERNET_MANAGER_H
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_eth.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Configuration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool use_dhcp;
|
||||||
|
char static_ip[16];
|
||||||
|
char netmask[16];
|
||||||
|
char gateway[16];
|
||||||
|
char dns_primary[16];
|
||||||
|
char dns_secondary[16];
|
||||||
|
} bmc_eth_config_t;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Function Declarations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the Ethernet manager
|
||||||
|
*
|
||||||
|
* Sets up W5500 SPI Ethernet and network stack.
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t ethernet_manager_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if Ethernet is connected
|
||||||
|
*
|
||||||
|
* @return true if connected, false otherwise
|
||||||
|
*/
|
||||||
|
bool ethernet_is_connected(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the current IP address
|
||||||
|
*
|
||||||
|
* @param ip_str Buffer to store IP address string (min 16 bytes)
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t ethernet_get_ip(char *ip_str);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get network information
|
||||||
|
*
|
||||||
|
* @param ip IP address buffer (min 16 bytes)
|
||||||
|
* @param netmask Netmask buffer (min 16 bytes)
|
||||||
|
* @param gateway Gateway buffer (min 16 bytes)
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t ethernet_get_network_info(char *ip, char *netmask, char *gateway);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get MAC address
|
||||||
|
*
|
||||||
|
* @param mac_str Buffer to store MAC address string (min 18 bytes)
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t ethernet_get_mac(char *mac_str);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set static IP configuration
|
||||||
|
*
|
||||||
|
* @param config Network configuration
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t ethernet_set_static_ip(const bmc_eth_config_t *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enable DHCP
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t ethernet_enable_dhcp(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get Ethernet link speed
|
||||||
|
*
|
||||||
|
* @return Speed in Mbps, or 0 if not connected
|
||||||
|
*/
|
||||||
|
int ethernet_get_link_speed(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get Ethernet duplex mode
|
||||||
|
*
|
||||||
|
* @return true for full duplex, false for half duplex
|
||||||
|
*/
|
||||||
|
bool ethernet_is_full_duplex(void);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Event Callbacks
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ETH_EVENT_CONNECTED,
|
||||||
|
ETH_EVENT_DISCONNECTED,
|
||||||
|
ETH_EVENT_GOT_IP,
|
||||||
|
ETH_EVENT_LOST_IP,
|
||||||
|
} eth_event_type_t;
|
||||||
|
|
||||||
|
typedef void (*eth_event_callback_t)(eth_event_type_t event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register callback for Ethernet events
|
||||||
|
*
|
||||||
|
* @param callback Function to call on events
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t ethernet_register_event_callback(eth_event_callback_t callback);
|
||||||
|
|
||||||
|
#endif // ETHERNET_MANAGER_H
|
||||||
278
main/gpio_controller.c
Normal file
278
main/gpio_controller.c
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
#include "gpio_controller.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = TAG_GPIO;
|
||||||
|
|
||||||
|
// Runtime GPIO state storage
|
||||||
|
static bmc_gpio_state_t gpio_states[BMC_GPIO_COUNT];
|
||||||
|
static bool gpio_initialized = false;
|
||||||
|
|
||||||
|
esp_err_t gpio_controller_init(void) {
|
||||||
|
ESP_LOGI(TAG, "Initializing GPIO controller");
|
||||||
|
|
||||||
|
// Initialize all GPIOs from default configuration
|
||||||
|
for (int i = 0; i < BMC_GPIO_COUNT; i++) {
|
||||||
|
const bmc_gpio_def_t *def = &bmc_gpio_defaults[i];
|
||||||
|
|
||||||
|
// Copy configuration to runtime state
|
||||||
|
gpio_states[i].pin = def->pin;
|
||||||
|
gpio_states[i].name = def->name;
|
||||||
|
gpio_states[i].mode = def->mode;
|
||||||
|
gpio_states[i].inverted = def->inverted;
|
||||||
|
gpio_states[i].is_input_only = def->is_input_only;
|
||||||
|
|
||||||
|
// Skip configuration for input-only pins when mode is output
|
||||||
|
if (def->is_input_only && def->mode == GPIO_MODE_OUTPUT) {
|
||||||
|
gpio_states[i].mode = GPIO_MODE_INPUT;
|
||||||
|
ESP_LOGW(TAG, "Pin %d (%s) is input-only, forcing input mode", def->pin, def->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure GPIO
|
||||||
|
gpio_config_t io_conf = {
|
||||||
|
.pin_bit_mask = (1ULL << def->pin),
|
||||||
|
.mode = gpio_states[i].mode,
|
||||||
|
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||||
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||||
|
.intr_type = GPIO_INTR_DISABLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enable pull-up for inputs by default
|
||||||
|
if (gpio_states[i].mode == GPIO_MODE_INPUT) {
|
||||||
|
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ret = gpio_config(&io_conf);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to configure GPIO %d: %s", def->pin, esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default value for outputs
|
||||||
|
if (gpio_states[i].mode == GPIO_MODE_OUTPUT) {
|
||||||
|
int value = def->default_value;
|
||||||
|
if (def->inverted) {
|
||||||
|
value = !value;
|
||||||
|
}
|
||||||
|
gpio_set_level(def->pin, value);
|
||||||
|
gpio_states[i].value = value;
|
||||||
|
} else {
|
||||||
|
// Read current input value
|
||||||
|
gpio_states[i].value = gpio_get_level(def->pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "GPIO %d (%s) initialized: mode=%d, value=%d, inverted=%d", def->pin, def->name,
|
||||||
|
gpio_states[i].mode, gpio_states[i].value, gpio_states[i].inverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
gpio_initialized = true;
|
||||||
|
ESP_LOGI(TAG, "GPIO controller initialized with %d pins", BMC_GPIO_COUNT);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int gpio_find_index(gpio_num_t pin) {
|
||||||
|
for (int i = 0; i < BMC_GPIO_COUNT; i++) {
|
||||||
|
if (gpio_states[i].pin == pin) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *gpio_get_name(gpio_num_t pin) {
|
||||||
|
int idx = gpio_find_index(pin);
|
||||||
|
if (idx >= 0) {
|
||||||
|
return gpio_states[idx].name;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gpio_get_all_states(bmc_gpio_state_t *states) {
|
||||||
|
if (!gpio_initialized) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update input values
|
||||||
|
for (int i = 0; i < BMC_GPIO_COUNT; i++) {
|
||||||
|
if (gpio_states[i].mode == GPIO_MODE_INPUT || gpio_states[i].mode == GPIO_MODE_INPUT_OUTPUT) {
|
||||||
|
gpio_states[i].value = gpio_get_level(gpio_states[i].pin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(states, gpio_states, sizeof(gpio_states));
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gpio_get_state(gpio_num_t pin, bmc_gpio_state_t *state) {
|
||||||
|
int idx = gpio_find_index(pin);
|
||||||
|
if (idx < 0) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update input value
|
||||||
|
if (gpio_states[idx].mode == GPIO_MODE_INPUT || gpio_states[idx].mode == GPIO_MODE_INPUT_OUTPUT) {
|
||||||
|
gpio_states[idx].value = gpio_get_level(pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(state, &gpio_states[idx], sizeof(bmc_gpio_state_t));
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int gpio_read(gpio_num_t pin) {
|
||||||
|
int idx = gpio_find_index(pin);
|
||||||
|
if (idx < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int level = gpio_get_level(pin);
|
||||||
|
|
||||||
|
// Apply inverted logic for display
|
||||||
|
if (gpio_states[idx].inverted) {
|
||||||
|
level = !level;
|
||||||
|
}
|
||||||
|
|
||||||
|
gpio_states[idx].value = level;
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gpio_write(gpio_num_t pin, int value) {
|
||||||
|
int idx = gpio_find_index(pin);
|
||||||
|
if (idx < 0) {
|
||||||
|
ESP_LOGW(TAG, "GPIO %d not found in configuration", pin);
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gpio_states[idx].is_input_only) {
|
||||||
|
ESP_LOGW(TAG, "GPIO %d (%s) is input-only", pin, gpio_states[idx].name);
|
||||||
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure GPIO is configured as output
|
||||||
|
if (gpio_states[idx].mode != GPIO_MODE_OUTPUT && gpio_states[idx].mode != GPIO_MODE_INPUT_OUTPUT) {
|
||||||
|
ESP_LOGW(TAG, "GPIO %d (%s) is not configured as output", pin, gpio_states[idx].name);
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply inverted logic
|
||||||
|
int actual_value = value;
|
||||||
|
if (gpio_states[idx].inverted) {
|
||||||
|
actual_value = !value;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ret = gpio_set_level(pin, actual_value);
|
||||||
|
if (ret == ESP_OK) {
|
||||||
|
gpio_states[idx].value = value;
|
||||||
|
ESP_LOGD(TAG, "GPIO %d (%s) set to %d (actual: %d)", pin, gpio_states[idx].name, value, actual_value);
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Failed to set GPIO %d: %s", pin, esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gpio_configure(gpio_num_t pin, gpio_mode_t mode) {
|
||||||
|
int idx = gpio_find_index(pin);
|
||||||
|
if (idx < 0) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if trying to set input-only pin as output
|
||||||
|
if (gpio_states[idx].is_input_only && mode == GPIO_MODE_OUTPUT) {
|
||||||
|
ESP_LOGW(TAG, "GPIO %d (%s) is input-only, cannot set as output", pin, gpio_states[idx].name);
|
||||||
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
gpio_config_t io_conf = {
|
||||||
|
.pin_bit_mask = (1ULL << pin),
|
||||||
|
.mode = mode,
|
||||||
|
.pull_up_en = (mode == GPIO_MODE_INPUT) ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE,
|
||||||
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||||
|
.intr_type = GPIO_INTR_DISABLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t ret = gpio_config(&io_conf);
|
||||||
|
if (ret == ESP_OK) {
|
||||||
|
gpio_states[idx].mode = mode;
|
||||||
|
ESP_LOGI(TAG, "GPIO %d (%s) mode changed to %d", pin, gpio_states[idx].name, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gpio_toggle(gpio_num_t pin) {
|
||||||
|
int idx = gpio_find_index(pin);
|
||||||
|
if (idx < 0) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
int current = gpio_states[idx].value;
|
||||||
|
return gpio_write(pin, !current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Power Control Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
esp_err_t gpio_power_on(void) {
|
||||||
|
ESP_LOGI(TAG, "Power ON sequence initiated");
|
||||||
|
|
||||||
|
// Pulse the POWER_ON signal
|
||||||
|
gpio_write(BMC_GPIO_POWER_ON, 1);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(BMC_POWER_ON_PULSE_MS));
|
||||||
|
gpio_write(BMC_GPIO_POWER_ON, 0);
|
||||||
|
|
||||||
|
// Wait for power good signal
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(BMC_POWER_GOOD_DELAY_MS));
|
||||||
|
|
||||||
|
if (gpio_is_power_good()) {
|
||||||
|
ESP_LOGI(TAG, "Power ON successful - power good detected");
|
||||||
|
gpio_set_status_led(true);
|
||||||
|
return ESP_OK;
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Power ON completed - no power good signal");
|
||||||
|
return ESP_OK; // Still return OK, just no confirmation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gpio_power_off(void) {
|
||||||
|
ESP_LOGI(TAG, "Power OFF sequence initiated");
|
||||||
|
|
||||||
|
// Pulse the POWER_OFF signal
|
||||||
|
gpio_write(BMC_GPIO_POWER_OFF, 1);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(BMC_POWER_OFF_PULSE_MS));
|
||||||
|
gpio_write(BMC_GPIO_POWER_OFF, 0);
|
||||||
|
|
||||||
|
gpio_set_status_led(false);
|
||||||
|
ESP_LOGI(TAG, "Power OFF completed");
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gpio_reset(void) {
|
||||||
|
ESP_LOGI(TAG, "Reset sequence initiated");
|
||||||
|
|
||||||
|
// Pulse the RESET signal (active-low)
|
||||||
|
gpio_write(BMC_GPIO_RESET, 0); // Assert reset (active-low)
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(BMC_RESET_PULSE_MS));
|
||||||
|
gpio_write(BMC_GPIO_RESET, 1); // De-assert reset
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Reset completed");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gpio_is_power_good(void) {
|
||||||
|
int idx = gpio_find_index(BMC_GPIO_PWR_GOOD);
|
||||||
|
if (idx < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int level = gpio_get_level(BMC_GPIO_PWR_GOOD);
|
||||||
|
return (level == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gpio_set_status_led(bool on) {
|
||||||
|
return gpio_write(BMC_GPIO_STATUS_LED, on ? 1 : 0);
|
||||||
|
}
|
||||||
148
main/gpio_controller.h
Normal file
148
main/gpio_controller.h
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#ifndef GPIO_CONTROLLER_H
|
||||||
|
#define GPIO_CONTROLLER_H
|
||||||
|
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GPIO State Structure
|
||||||
|
// ============================================================================
|
||||||
|
typedef struct {
|
||||||
|
gpio_num_t pin;
|
||||||
|
const char *name;
|
||||||
|
gpio_mode_t mode;
|
||||||
|
int value;
|
||||||
|
bool inverted;
|
||||||
|
bool is_input_only;
|
||||||
|
} bmc_gpio_state_t;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Function Declarations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the GPIO controller
|
||||||
|
*
|
||||||
|
* Sets up all GPIOs according to the default configuration.
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_controller_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the current state of all GPIOs
|
||||||
|
*
|
||||||
|
* @param states Array to store GPIO states (must be BMC_GPIO_COUNT size)
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_get_all_states(bmc_gpio_state_t *states);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the state of a specific GPIO
|
||||||
|
*
|
||||||
|
* @param pin GPIO pin number
|
||||||
|
* @param state Pointer to store the GPIO state
|
||||||
|
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if pin not in config
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_get_state(gpio_num_t pin, bmc_gpio_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read the value of a GPIO
|
||||||
|
*
|
||||||
|
* @param pin GPIO pin number
|
||||||
|
* @return GPIO level (0 or 1), -1 on error
|
||||||
|
*/
|
||||||
|
int gpio_read(gpio_num_t pin);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Write a value to a GPIO
|
||||||
|
*
|
||||||
|
* Handles inverted logic automatically.
|
||||||
|
*
|
||||||
|
* @param pin GPIO pin number
|
||||||
|
* @param value Value to write (0 or 1)
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_write(gpio_num_t pin, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Configure a GPIO mode
|
||||||
|
*
|
||||||
|
* @param pin GPIO pin number
|
||||||
|
* @param mode GPIO mode (INPUT, OUTPUT, etc.)
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_configure(gpio_num_t pin, gpio_mode_t mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Toggle a GPIO output
|
||||||
|
*
|
||||||
|
* @param pin GPIO pin number
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_toggle(gpio_num_t pin);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Find GPIO index by pin number
|
||||||
|
*
|
||||||
|
* @param pin GPIO pin number
|
||||||
|
* @return Index in the GPIO array, or -1 if not found
|
||||||
|
*/
|
||||||
|
int gpio_find_index(gpio_num_t pin);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get GPIO name by pin number
|
||||||
|
*
|
||||||
|
* @param pin GPIO pin number
|
||||||
|
* @return GPIO name string, or NULL if not found
|
||||||
|
*/
|
||||||
|
const char *gpio_get_name(gpio_num_t pin);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Power Control Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Turn board power on
|
||||||
|
*
|
||||||
|
* Pulses the POWER_ON GPIO.
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_power_on(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Turn board power off
|
||||||
|
*
|
||||||
|
* Pulses the POWER_OFF GPIO.
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_power_off(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset the board
|
||||||
|
*
|
||||||
|
* Pulses the RESET GPIO (active-low).
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_reset(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if power is good
|
||||||
|
*
|
||||||
|
* @return true if power good signal is high, false otherwise
|
||||||
|
*/
|
||||||
|
bool gpio_is_power_good(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set status LED state
|
||||||
|
*
|
||||||
|
* @param on true to turn on, false to turn off
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t gpio_set_status_led(bool on);
|
||||||
|
|
||||||
|
#endif // GPIO_CONTROLLER_H
|
||||||
825
main/html/index.html
Normal file
825
main/html/index.html
Normal file
@@ -0,0 +1,825 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>VisionFive2 BMC Dashboard</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
color: #eee;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 15px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: linear-gradient(90deg, #00d4ff, #00ff88);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 30px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff4444;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.connected {
|
||||||
|
background: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 .icon {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Power Control */
|
||||||
|
.power-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-power-on {
|
||||||
|
background: linear-gradient(135deg, #00c853, #00e676);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-power-off {
|
||||||
|
background: linear-gradient(135deg, #ff1744, #ff5252);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset {
|
||||||
|
background: linear-gradient(135deg, #ff9100, #ffab40);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-status {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-indicator {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-indicator.on {
|
||||||
|
background: #00ff88;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GPIO Monitor */
|
||||||
|
.gpio-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpio-item {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpio-name {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpio-pin {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #00d4ff;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpio-value {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpio-led {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #333;
|
||||||
|
border: 2px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpio-led.high {
|
||||||
|
background: #00ff88;
|
||||||
|
border-color: #00ff88;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpio-toggle {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpio-toggle:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Serial Console */
|
||||||
|
.console-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-output {
|
||||||
|
flex: 1;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-output:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-output pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-output .input-line {
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-output .cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 1em;
|
||||||
|
background: #00ff88;
|
||||||
|
animation: blink 1s step-end infinite;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-controls select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System Info */
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 1em;
|
||||||
|
color: #00d4ff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Notifications */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
background: rgba(0, 255, 136, 0.2);
|
||||||
|
border: 1px solid #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
border: 1px solid #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>🖥️ VisionFive2 BMC Dashboard</h1>
|
||||||
|
<div class="status-bar">
|
||||||
|
<div class="status-item">
|
||||||
|
<div class="status-dot" id="eth-status"></div>
|
||||||
|
<span>Ethernet: <span id="eth-text">Disconnected</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span>IP: <span id="ip-address">-</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span>UART: <span id="uart-baud">115200</span> baud</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<!-- Power Control Card -->
|
||||||
|
<div class="card">
|
||||||
|
<h2><span class="icon">⚡</span> Power Control</h2>
|
||||||
|
<div class="power-buttons">
|
||||||
|
<button class="btn btn-power-on" onclick="powerOn()">Power On</button>
|
||||||
|
<button class="btn btn-power-off" onclick="powerOff()">Power Off</button>
|
||||||
|
<button class="btn btn-reset" onclick="reset()">Reset</button>
|
||||||
|
</div>
|
||||||
|
<div class="power-status">
|
||||||
|
<div class="power-indicator" id="power-indicator"></div>
|
||||||
|
<span>Power Status: <strong id="power-status">Unknown</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Info Card -->
|
||||||
|
<div class="card">
|
||||||
|
<h2><span class="icon">ℹ️</span> System Info</h2>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">MAC Address</div>
|
||||||
|
<div class="info-value" id="mac-address">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Netmask</div>
|
||||||
|
<div class="info-value" id="netmask">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Gateway</div>
|
||||||
|
<div class="info-value" id="gateway">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Link Speed</div>
|
||||||
|
<div class="info-value" id="link-speed">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<!-- GPIO Monitor Card -->
|
||||||
|
<div class="card">
|
||||||
|
<h2><span class="icon">🔌</span> GPIO Monitor</h2>
|
||||||
|
<div class="gpio-grid" id="gpio-grid">
|
||||||
|
<!-- GPIO items will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Serial Console Card -->
|
||||||
|
<div class="card">
|
||||||
|
<h2><span class="icon">💻</span> Serial Console</h2>
|
||||||
|
<div class="console-container">
|
||||||
|
<div class="console-output" id="console-output" tabindex="0">
|
||||||
|
<pre>Connecting to serial console...</pre>
|
||||||
|
</div>
|
||||||
|
<div class="console-controls">
|
||||||
|
<label>Baud Rate:</label>
|
||||||
|
<select id="baud-select" onchange="setBaudRate()">
|
||||||
|
<option value="9600">9600</option>
|
||||||
|
<option value="19200">19200</option>
|
||||||
|
<option value="38400">38400</option>
|
||||||
|
<option value="57600">57600</option>
|
||||||
|
<option value="115200" selected>115200</option>
|
||||||
|
<option value="230400">230400</option>
|
||||||
|
<option value="460800">460800</option>
|
||||||
|
<option value="921600">921600</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-secondary" onclick="clearConsole()">Clear</button>
|
||||||
|
<button class="btn btn-secondary" onclick="connectWebSocket()">Reconnect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// WebSocket connection
|
||||||
|
let ws = null;
|
||||||
|
let wsConnected = false;
|
||||||
|
|
||||||
|
// API base URL
|
||||||
|
const API_BASE = '';
|
||||||
|
|
||||||
|
// Toast notification
|
||||||
|
function showToast(message, type = 'success') {
|
||||||
|
const container = document.getElementById('toast-container');
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API helper
|
||||||
|
async function apiCall(endpoint, method = 'GET', data = null) {
|
||||||
|
const options = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
options.body = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}${endpoint}`, options);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Unknown error');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
showToast(error.message, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power control functions
|
||||||
|
async function powerOn() {
|
||||||
|
try {
|
||||||
|
await apiCall('/api/power/on', 'POST');
|
||||||
|
showToast('Power on sequence initiated');
|
||||||
|
updatePowerStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Power on failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function powerOff() {
|
||||||
|
try {
|
||||||
|
await apiCall('/api/power/off', 'POST');
|
||||||
|
showToast('Power off sequence initiated');
|
||||||
|
updatePowerStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Power off failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
try {
|
||||||
|
await apiCall('/api/power/reset', 'POST');
|
||||||
|
showToast('Reset sequence initiated');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reset failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePowerStatus() {
|
||||||
|
try {
|
||||||
|
const result = await apiCall('/api/power/status');
|
||||||
|
const indicator = document.getElementById('power-indicator');
|
||||||
|
const status = document.getElementById('power-status');
|
||||||
|
|
||||||
|
if (result.data.power_good) {
|
||||||
|
indicator.classList.add('on');
|
||||||
|
status.textContent = 'ON';
|
||||||
|
} else {
|
||||||
|
indicator.classList.remove('on');
|
||||||
|
status.textContent = 'OFF';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get power status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPIO functions
|
||||||
|
async function updateGPIOStates() {
|
||||||
|
try {
|
||||||
|
const result = await apiCall('/api/gpio');
|
||||||
|
const grid = document.getElementById('gpio-grid');
|
||||||
|
grid.innerHTML = '';
|
||||||
|
|
||||||
|
result.gpios.forEach(gpio => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'gpio-item';
|
||||||
|
|
||||||
|
const isOutput = gpio.mode === 'output';
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="gpio-name">${gpio.name}</div>
|
||||||
|
<div class="gpio-pin">GPIO ${gpio.pin}</div>
|
||||||
|
<div class="gpio-value">
|
||||||
|
<div class="gpio-led ${gpio.value ? 'high' : ''}"></div>
|
||||||
|
<span>${gpio.value}</span>
|
||||||
|
${isOutput ? `<button class="gpio-toggle" onclick="toggleGPIO(${gpio.pin})">Toggle</button>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
grid.appendChild(item);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get GPIO states:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleGPIO(pin) {
|
||||||
|
try {
|
||||||
|
// Get current state
|
||||||
|
const result = await apiCall('/api/gpio');
|
||||||
|
const gpio = result.gpios.find(g => g.pin === pin);
|
||||||
|
|
||||||
|
if (gpio) {
|
||||||
|
const newValue = gpio.value ? 0 : 1;
|
||||||
|
await apiCall(`/api/gpio/set?pin=${pin}`, 'POST', { value: newValue });
|
||||||
|
updateGPIOStates();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle GPIO:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serial console functions
|
||||||
|
let inputBuffer = '';
|
||||||
|
let cursorVisible = true;
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
const protocol = window.location.protocol === 'https' ? 'wss' : 'ws';
|
||||||
|
const wsUrl = `${protocol}://${window.location.host}/api/serial/ws`;
|
||||||
|
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
wsConnected = true;
|
||||||
|
clearConsole();
|
||||||
|
appendConsole('Connected to serial console\n');
|
||||||
|
updateCursor();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
appendConsole(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
wsConnected = false;
|
||||||
|
appendConsole('\nDisconnected from serial console\n');
|
||||||
|
|
||||||
|
// Try to reconnect after 3 seconds
|
||||||
|
setTimeout(connectWebSocket, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
appendConsole('WebSocket error\n');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip ANSI escape sequences from text
|
||||||
|
function stripAnsi(text) {
|
||||||
|
// Remove ANSI escape sequences (cursor movement, colors, etc.)
|
||||||
|
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
||||||
|
.replace(/\x1b\][^\x07]*\x07/g, '')
|
||||||
|
.replace(/\x1b[()][AB012]/g, '')
|
||||||
|
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendConsole(data) {
|
||||||
|
const output = document.getElementById('console-output');
|
||||||
|
const pre = output.querySelector('pre') || document.createElement('pre');
|
||||||
|
|
||||||
|
if (!output.contains(pre)) {
|
||||||
|
output.innerHTML = '';
|
||||||
|
output.appendChild(pre);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove cursor before appending, then re-add
|
||||||
|
const cursor = pre.querySelector('.cursor');
|
||||||
|
if (cursor) cursor.remove();
|
||||||
|
|
||||||
|
// Strip ANSI escape sequences for cleaner display
|
||||||
|
const cleanData = stripAnsi(data);
|
||||||
|
pre.textContent += cleanData;
|
||||||
|
updateCursor();
|
||||||
|
output.scrollTop = output.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCursor() {
|
||||||
|
const output = document.getElementById('console-output');
|
||||||
|
const pre = output.querySelector('pre');
|
||||||
|
if (!pre) return;
|
||||||
|
|
||||||
|
// Remove existing cursor
|
||||||
|
const existingCursor = pre.querySelector('.cursor');
|
||||||
|
if (existingCursor) existingCursor.remove();
|
||||||
|
|
||||||
|
// Add cursor at end
|
||||||
|
if (wsConnected) {
|
||||||
|
const cursor = document.createElement('span');
|
||||||
|
cursor.className = 'cursor';
|
||||||
|
pre.appendChild(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendChar(char) {
|
||||||
|
if (wsConnected) {
|
||||||
|
ws.send(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendBuffer() {
|
||||||
|
if (inputBuffer && wsConnected) {
|
||||||
|
ws.send(inputBuffer + '\n');
|
||||||
|
inputBuffer = '';
|
||||||
|
updateCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConsoleKeyDown(event) {
|
||||||
|
if (!wsConnected) return;
|
||||||
|
|
||||||
|
const output = document.getElementById('console-output');
|
||||||
|
|
||||||
|
// Handle special keys
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
sendChar('\n');
|
||||||
|
inputBuffer = '';
|
||||||
|
} else if (event.key === 'Backspace') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (inputBuffer.length > 0) {
|
||||||
|
inputBuffer = inputBuffer.slice(0, -1);
|
||||||
|
sendChar('\b'); // Send backspace to remote
|
||||||
|
}
|
||||||
|
} else if (event.key === 'Tab') {
|
||||||
|
event.preventDefault();
|
||||||
|
sendChar('\t');
|
||||||
|
inputBuffer += '\t';
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
sendChar('\x1b');
|
||||||
|
} else if (event.ctrlKey && event.key.length === 1) {
|
||||||
|
// Handle Ctrl+C, Ctrl+D, etc.
|
||||||
|
event.preventDefault();
|
||||||
|
const ctrlChar = String.fromCharCode(event.key.charCodeAt(0) - 96);
|
||||||
|
sendChar(ctrlChar);
|
||||||
|
} else if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||||
|
// Regular character
|
||||||
|
event.preventDefault();
|
||||||
|
sendChar(event.key);
|
||||||
|
inputBuffer += event.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearConsole() {
|
||||||
|
const output = document.getElementById('console-output');
|
||||||
|
output.innerHTML = '<pre></pre>';
|
||||||
|
inputBuffer = '';
|
||||||
|
updateCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize console keyboard handling
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const output = document.getElementById('console-output');
|
||||||
|
output.addEventListener('keydown', handleConsoleKeyDown);
|
||||||
|
|
||||||
|
// Focus console on click
|
||||||
|
output.addEventListener('click', () => {
|
||||||
|
output.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setBaudRate() {
|
||||||
|
const baud = document.getElementById('baud-select').value;
|
||||||
|
try {
|
||||||
|
await apiCall('/api/serial/config', 'PUT', { baud_rate: parseInt(baud) });
|
||||||
|
document.getElementById('uart-baud').textContent = baud;
|
||||||
|
showToast(`Baud rate set to ${baud}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set baud rate:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System info functions
|
||||||
|
async function updateSystemInfo() {
|
||||||
|
try {
|
||||||
|
const result = await apiCall('/api/system/info');
|
||||||
|
|
||||||
|
document.getElementById('ip-address').textContent = result.data.ip || '-';
|
||||||
|
document.getElementById('mac-address').textContent = result.data.mac || '-';
|
||||||
|
document.getElementById('netmask').textContent = result.data.netmask || '-';
|
||||||
|
document.getElementById('gateway').textContent = result.data.gateway || '-';
|
||||||
|
document.getElementById('link-speed').textContent = result.data.link_speed ? `${result.data.link_speed} Mbps` : '-';
|
||||||
|
|
||||||
|
// Update Ethernet status
|
||||||
|
const ethStatus = document.getElementById('eth-status');
|
||||||
|
const ethText = document.getElementById('eth-text');
|
||||||
|
|
||||||
|
if (result.data.ethernet_connected) {
|
||||||
|
ethStatus.classList.add('connected');
|
||||||
|
ethText.textContent = 'Connected';
|
||||||
|
} else {
|
||||||
|
ethStatus.classList.remove('connected');
|
||||||
|
ethText.textContent = 'Disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('uart-baud').textContent = result.data.uart_baud_rate;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get system info:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
updateSystemInfo();
|
||||||
|
updateGPIOStates();
|
||||||
|
updatePowerStatus();
|
||||||
|
connectWebSocket();
|
||||||
|
|
||||||
|
// Periodic updates
|
||||||
|
setInterval(updateSystemInfo, 5000);
|
||||||
|
setInterval(updateGPIOStates, 2000);
|
||||||
|
setInterval(updatePowerStatus, 2000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
main/idf_component.yml
Normal file
3
main/idf_component.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dependencies:
|
||||||
|
espressif/cjson: ^1.7.19~1
|
||||||
|
espressif/w5500: ^1.0.1
|
||||||
129
main/main.c
Normal file
129
main/main.c
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "gpio_controller.h"
|
||||||
|
#include "uart_handler.h"
|
||||||
|
#include "ethernet_manager.h"
|
||||||
|
#include "web_server.h"
|
||||||
|
|
||||||
|
static const char *TAG = TAG_BMC;
|
||||||
|
|
||||||
|
// Ethernet event callback
|
||||||
|
static void eth_event_callback(eth_event_type_t event) {
|
||||||
|
switch (event) {
|
||||||
|
case ETH_EVENT_CONNECTED:
|
||||||
|
ESP_LOGI(TAG, "Ethernet connected");
|
||||||
|
gpio_set_status_led(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ETH_EVENT_DISCONNECTED:
|
||||||
|
ESP_LOGW(TAG, "Ethernet disconnected");
|
||||||
|
gpio_set_status_led(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ETH_EVENT_GOT_IP: {
|
||||||
|
char ip[16];
|
||||||
|
if (ethernet_get_ip(ip) == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "Got IP: %s", ip);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case ETH_EVENT_LOST_IP:
|
||||||
|
ESP_LOGW(TAG, "Lost IP address");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void app_main(void) {
|
||||||
|
ESP_LOGI(TAG, "========================================");
|
||||||
|
ESP_LOGI(TAG, "ESP32-S3 BMC Firmware Starting...");
|
||||||
|
ESP_LOGI(TAG, "========================================");
|
||||||
|
|
||||||
|
// Initialize GPIO controller
|
||||||
|
ESP_LOGI(TAG, "Initializing GPIO controller...");
|
||||||
|
esp_err_t ret = gpio_controller_init();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize GPIO controller: %s", esp_err_to_name(ret));
|
||||||
|
// Continue anyway - some GPIOs might still work
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set status LED to indicate initialization
|
||||||
|
gpio_set_status_led(false);
|
||||||
|
|
||||||
|
// Initialize UART handler
|
||||||
|
ESP_LOGI(TAG, "Initializing UART handler...");
|
||||||
|
ret = uart_handler_init();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize UART handler: %s", esp_err_to_name(ret));
|
||||||
|
// Continue anyway - serial console is optional
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Ethernet manager
|
||||||
|
ESP_LOGI(TAG, "Initializing Ethernet manager...");
|
||||||
|
ethernet_register_event_callback(eth_event_callback);
|
||||||
|
ret = ethernet_manager_init();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize Ethernet: %s", esp_err_to_name(ret));
|
||||||
|
// Cannot continue without network
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for Ethernet connection (with timeout)
|
||||||
|
ESP_LOGI(TAG, "Waiting for Ethernet connection...");
|
||||||
|
int timeout = 30; // 30 seconds timeout
|
||||||
|
while (!ethernet_is_connected() && timeout > 0) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
timeout--;
|
||||||
|
ESP_LOGD(TAG, "Waiting for Ethernet... (%d seconds remaining)", timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ethernet_is_connected()) {
|
||||||
|
ESP_LOGW(TAG, "Ethernet not connected after timeout, starting server anyway...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start web server
|
||||||
|
ESP_LOGI(TAG, "Starting web server...");
|
||||||
|
ret = web_server_start();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to start web server: %s", esp_err_to_name(ret));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print network information
|
||||||
|
char ip[16], netmask[16], gateway[16], mac[18];
|
||||||
|
if (ethernet_get_network_info(ip, netmask, gateway) == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "Network Configuration:");
|
||||||
|
ESP_LOGI(TAG, " IP Address: %s", ip);
|
||||||
|
ESP_LOGI(TAG, " Netmask: %s", netmask);
|
||||||
|
ESP_LOGI(TAG, " Gateway: %s", gateway);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ethernet_get_mac(mac) == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, " MAC Address: %s", mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "========================================");
|
||||||
|
ESP_LOGI(TAG, "BMC Ready!");
|
||||||
|
ESP_LOGI(TAG, "Web interface: http://%s/", ip);
|
||||||
|
ESP_LOGI(TAG, "========================================");
|
||||||
|
|
||||||
|
// Main loop - monitor system health
|
||||||
|
while (1) {
|
||||||
|
// Update status LED based on connection state
|
||||||
|
if (ethernet_is_connected()) {
|
||||||
|
gpio_set_status_led(true);
|
||||||
|
} else {
|
||||||
|
// Blink LED if not connected
|
||||||
|
gpio_set_status_led(false);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
gpio_set_status_led(true);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
gpio_set_status_led(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
410
main/uart_handler.c
Normal file
410
main/uart_handler.c
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
#include "uart_handler.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = TAG_UART;
|
||||||
|
|
||||||
|
// Runtime UART configuration
|
||||||
|
static bmc_uart_config_t current_config;
|
||||||
|
static bool uart_initialized = false;
|
||||||
|
static TaskHandle_t uart_rx_task_handle = NULL;
|
||||||
|
static SemaphoreHandle_t uart_mutex = NULL;
|
||||||
|
|
||||||
|
// Callback for WebSocket bridge
|
||||||
|
static uart_rx_callback_t rx_callback = NULL;
|
||||||
|
static SemaphoreHandle_t callback_mutex = NULL;
|
||||||
|
|
||||||
|
// Ring buffer for received data
|
||||||
|
#define UART_RING_BUF_SIZE 4096
|
||||||
|
static uint8_t *ring_buffer = NULL;
|
||||||
|
static volatile size_t ring_head = 0;
|
||||||
|
static volatile size_t ring_tail = 0;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Ring Buffer Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static size_t ring_buffer_available(void) {
|
||||||
|
if (ring_head >= ring_tail) {
|
||||||
|
return ring_head - ring_tail;
|
||||||
|
} else {
|
||||||
|
return UART_RING_BUF_SIZE - ring_tail + ring_head;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t ring_buffer_read(uint8_t *buf, size_t len) {
|
||||||
|
size_t available = ring_buffer_available();
|
||||||
|
size_t to_read = (len < available) ? len : available;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < to_read; i++) {
|
||||||
|
buf[i] = ring_buffer[ring_tail];
|
||||||
|
ring_tail = (ring_tail + 1) % UART_RING_BUF_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_read;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t ring_buffer_write(const uint8_t *buf, size_t len) {
|
||||||
|
size_t written = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
size_t next_head = (ring_head + 1) % UART_RING_BUF_SIZE;
|
||||||
|
if (next_head == ring_tail) {
|
||||||
|
// Buffer full, overwrite oldest data
|
||||||
|
ring_tail = (ring_tail + 1) % UART_RING_BUF_SIZE;
|
||||||
|
}
|
||||||
|
ring_buffer[ring_head] = buf[i];
|
||||||
|
ring_head = next_head;
|
||||||
|
written++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// UART Receive Task
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static void uart_rx_task(void *arg) {
|
||||||
|
uint8_t *temp_buf = malloc(256);
|
||||||
|
if (!temp_buf) {
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate UART RX buffer");
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UART RX task started, waiting for data on UART%d", BMC_UART_NUM);
|
||||||
|
|
||||||
|
while (uart_initialized) {
|
||||||
|
// Check for data from UART with longer timeout
|
||||||
|
int len = uart_read_bytes(BMC_UART_NUM, temp_buf, 256, pdMS_TO_TICKS(100));
|
||||||
|
|
||||||
|
if (len > 0) {
|
||||||
|
// Store in ring buffer
|
||||||
|
ring_buffer_write(temp_buf, len);
|
||||||
|
|
||||||
|
// Call callback if registered (outside mutex to avoid deadlock)
|
||||||
|
uart_rx_callback_t cb = NULL;
|
||||||
|
xSemaphoreTake(callback_mutex, pdMS_TO_TICKS(100));
|
||||||
|
cb = rx_callback;
|
||||||
|
xSemaphoreGive(callback_mutex);
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
cb(temp_buf, len);
|
||||||
|
}
|
||||||
|
} else if (len < 0) {
|
||||||
|
ESP_LOGW(TAG, "UART read error: %d", len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(temp_buf);
|
||||||
|
ESP_LOGI(TAG, "UART RX task stopped");
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Public Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
esp_err_t uart_handler_init(void) {
|
||||||
|
if (uart_initialized) {
|
||||||
|
ESP_LOGW(TAG, "UART handler already initialized");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Initializing UART handler");
|
||||||
|
|
||||||
|
// Allocate ring buffer
|
||||||
|
ring_buffer = calloc(UART_RING_BUF_SIZE, 1);
|
||||||
|
if (!ring_buffer) {
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate ring buffer");
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
ring_head = 0;
|
||||||
|
ring_tail = 0;
|
||||||
|
|
||||||
|
// Create mutex for UART access
|
||||||
|
uart_mutex = xSemaphoreCreateMutex();
|
||||||
|
if (!uart_mutex) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create UART mutex");
|
||||||
|
free(ring_buffer);
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mutex for callback
|
||||||
|
callback_mutex = xSemaphoreCreateMutex();
|
||||||
|
if (!callback_mutex) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create callback mutex");
|
||||||
|
vSemaphoreDelete(uart_mutex);
|
||||||
|
free(ring_buffer);
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default configuration
|
||||||
|
current_config.port = BMC_UART_NUM;
|
||||||
|
current_config.baud_rate = BMC_UART_BAUD_RATE;
|
||||||
|
current_config.data_bits = UART_DATA_8_BITS;
|
||||||
|
current_config.parity = UART_PARITY_DISABLE;
|
||||||
|
current_config.stop_bits = UART_STOP_BITS_1;
|
||||||
|
current_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||||
|
|
||||||
|
// Configure UART
|
||||||
|
uart_config_t uart_cfg = {
|
||||||
|
.baud_rate = current_config.baud_rate,
|
||||||
|
.data_bits = current_config.data_bits,
|
||||||
|
.parity = current_config.parity,
|
||||||
|
.stop_bits = current_config.stop_bits,
|
||||||
|
.flow_ctrl = current_config.flow_ctrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t ret = uart_param_config(BMC_UART_NUM, &uart_cfg);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to configure UART: %s", esp_err_to_name(ret));
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set UART pins
|
||||||
|
ret = uart_set_pin(BMC_UART_NUM, BMC_UART_TX_GPIO, BMC_UART_RX_GPIO, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to set UART pins: %s", esp_err_to_name(ret));
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install UART driver
|
||||||
|
ret = uart_driver_install(BMC_UART_NUM, BMC_UART_BUF_SIZE * 2, 0, 0, NULL, 0);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to install UART driver: %s", esp_err_to_name(ret));
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
uart_initialized = true;
|
||||||
|
|
||||||
|
// Start receive task
|
||||||
|
BaseType_t task_ret =
|
||||||
|
xTaskCreate(uart_rx_task, "uart_rx", BMC_UART_TASK_STACK, NULL, BMC_UART_TASK_PRIO, &uart_rx_task_handle);
|
||||||
|
|
||||||
|
if (task_ret != pdPASS) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create UART RX task");
|
||||||
|
uart_driver_delete(BMC_UART_NUM);
|
||||||
|
uart_initialized = false;
|
||||||
|
ret = ESP_FAIL;
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UART initialized: port=%d, baud=%d, tx=%d, rx=%d", BMC_UART_NUM, current_config.baud_rate,
|
||||||
|
BMC_UART_TX_GPIO, BMC_UART_RX_GPIO);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
vSemaphoreDelete(callback_mutex);
|
||||||
|
vSemaphoreDelete(uart_mutex);
|
||||||
|
free(ring_buffer);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t uart_handler_deinit(void) {
|
||||||
|
if (!uart_initialized) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Deinitializing UART handler");
|
||||||
|
|
||||||
|
uart_initialized = false;
|
||||||
|
|
||||||
|
// Wait for task to finish
|
||||||
|
if (uart_rx_task_handle) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
uart_rx_task_handle = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete UART driver
|
||||||
|
uart_driver_delete(BMC_UART_NUM);
|
||||||
|
|
||||||
|
// Clean up resources
|
||||||
|
if (callback_mutex) {
|
||||||
|
vSemaphoreDelete(callback_mutex);
|
||||||
|
callback_mutex = NULL;
|
||||||
|
}
|
||||||
|
if (uart_mutex) {
|
||||||
|
vSemaphoreDelete(uart_mutex);
|
||||||
|
uart_mutex = NULL;
|
||||||
|
}
|
||||||
|
if (ring_buffer) {
|
||||||
|
free(ring_buffer);
|
||||||
|
ring_buffer = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
rx_callback = NULL;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UART handler deinitialized");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t uart_get_config(bmc_uart_config_t *config) {
|
||||||
|
if (!uart_initialized) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
memcpy(config, ¤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;
|
||||||
|
}
|
||||||
138
main/uart_handler.h
Normal file
138
main/uart_handler.h
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#ifndef UART_HANDLER_H
|
||||||
|
#define UART_HANDLER_H
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Configuration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Default UART settings (can be changed at runtime)
|
||||||
|
typedef struct {
|
||||||
|
uart_port_t port;
|
||||||
|
int baud_rate;
|
||||||
|
uart_word_length_t data_bits;
|
||||||
|
uart_parity_t parity;
|
||||||
|
uart_stop_bits_t stop_bits;
|
||||||
|
uart_hw_flowcontrol_t flow_ctrl;
|
||||||
|
} bmc_uart_config_t;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Function Declarations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the UART handler
|
||||||
|
*
|
||||||
|
* Sets up UART with default configuration and starts the receive task.
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t uart_handler_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Deinitialize the UART handler
|
||||||
|
*
|
||||||
|
* Stops the receive task and releases UART resources.
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t uart_handler_deinit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get current UART configuration
|
||||||
|
*
|
||||||
|
* @param config Pointer to store the configuration
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t uart_get_config(bmc_uart_config_t *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set UART configuration
|
||||||
|
*
|
||||||
|
* @param config New configuration to apply
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t uart_set_config(const bmc_uart_config_t *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read data from UART buffer
|
||||||
|
*
|
||||||
|
* Non-blocking read from the internal ring buffer.
|
||||||
|
*
|
||||||
|
* @param buf Buffer to store received data
|
||||||
|
* @param len Maximum bytes to read
|
||||||
|
* @param timeout_ms Timeout in milliseconds
|
||||||
|
* @return Number of bytes read, or -1 on error
|
||||||
|
*/
|
||||||
|
int uart_read_data(uint8_t *buf, size_t len, uint32_t timeout_ms);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Write data to UART
|
||||||
|
*
|
||||||
|
* @param buf Data to send
|
||||||
|
* @param len Number of bytes to send
|
||||||
|
* @return Number of bytes written, or -1 on error
|
||||||
|
*/
|
||||||
|
int uart_write_data(const uint8_t *buf, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if data is available in receive buffer
|
||||||
|
*
|
||||||
|
* @return Number of bytes available, or -1 on error
|
||||||
|
*/
|
||||||
|
int uart_data_available(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Flush UART buffers
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t uart_flush_buffers(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set baud rate
|
||||||
|
*
|
||||||
|
* @param baud_rate New baud rate
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t uart_set_baud_rate(int baud_rate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get current baud rate
|
||||||
|
*
|
||||||
|
* @return Current baud rate
|
||||||
|
*/
|
||||||
|
int uart_get_baud_rate(void);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WebSocket Bridge Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Callback type for UART data received
|
||||||
|
*
|
||||||
|
* @param data Received data
|
||||||
|
* @param len Length of data
|
||||||
|
*/
|
||||||
|
typedef void (*uart_rx_callback_t)(const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register a callback for received UART data
|
||||||
|
*
|
||||||
|
* Used for WebSocket bridge to forward data to connected clients.
|
||||||
|
*
|
||||||
|
* @param callback Function to call when data is received
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t uart_register_rx_callback(uart_rx_callback_t callback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unregister the UART receive callback
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t uart_unregister_rx_callback(void);
|
||||||
|
|
||||||
|
#endif // UART_HANDLER_H
|
||||||
745
main/web_server.c
Normal file
745
main/web_server.c
Normal file
@@ -0,0 +1,745 @@
|
|||||||
|
#include "web_server.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "gpio_controller.h"
|
||||||
|
#include "uart_handler.h"
|
||||||
|
#include "ethernet_manager.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/param.h>
|
||||||
|
|
||||||
|
static const char *TAG = TAG_HTTP;
|
||||||
|
|
||||||
|
// Server handle
|
||||||
|
static httpd_handle_t server = NULL;
|
||||||
|
static bool server_running = false;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HTML Content (Embedded)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
extern const uint8_t index_html_start[] asm("_binary_index_html_start");
|
||||||
|
extern const uint8_t index_html_end[] asm("_binary_index_html_end");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helper Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static esp_err_t set_content_type_json(httpd_req_t *req) {
|
||||||
|
return httpd_resp_set_type(req, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_json_response(httpd_req_t *req, const char *json_str) {
|
||||||
|
set_content_type_json(req);
|
||||||
|
return httpd_resp_sendstr(req, json_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_json_error(httpd_req_t *req, const char *message) {
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddBoolToObject(root, "success", false);
|
||||||
|
cJSON_AddStringToObject(root, "error", message);
|
||||||
|
|
||||||
|
char *json_str = cJSON_PrintUnformatted(root);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
esp_err_t ret = send_json_response(req, json_str);
|
||||||
|
free(json_str);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_json_success(httpd_req_t *req, cJSON *data) {
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddBoolToObject(root, "success", true);
|
||||||
|
if (data) {
|
||||||
|
cJSON_AddItemToObject(root, "data", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
char *json_str = cJSON_PrintUnformatted(root);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
esp_err_t ret = send_json_response(req, json_str);
|
||||||
|
free(json_str);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Static File Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static esp_err_t index_handler(httpd_req_t *req) {
|
||||||
|
httpd_resp_set_type(req, "text/html");
|
||||||
|
httpd_resp_send(req, (const char *)index_html_start, index_html_end - index_html_start);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GPIO API Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static esp_err_t api_gpio_get_all_handler(httpd_req_t *req) {
|
||||||
|
bmc_gpio_state_t states[BMC_GPIO_COUNT];
|
||||||
|
esp_err_t ret = gpio_get_all_states(states);
|
||||||
|
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
return send_json_error(req, "Failed to get GPIO states");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *gpios = cJSON_CreateArray();
|
||||||
|
for (int i = 0; i < BMC_GPIO_COUNT; i++) {
|
||||||
|
cJSON *gpio = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(gpio, "pin", states[i].pin);
|
||||||
|
cJSON_AddStringToObject(gpio, "name", states[i].name);
|
||||||
|
cJSON_AddStringToObject(gpio, "mode",
|
||||||
|
states[i].mode == GPIO_MODE_INPUT ? "input"
|
||||||
|
: states[i].mode == GPIO_MODE_OUTPUT ? "output"
|
||||||
|
: "inout");
|
||||||
|
cJSON_AddNumberToObject(gpio, "value", states[i].value);
|
||||||
|
cJSON_AddBoolToObject(gpio, "inverted", states[i].inverted);
|
||||||
|
cJSON_AddItemToArray(gpios, gpio);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddBoolToObject(root, "success", true);
|
||||||
|
cJSON_AddItemToObject(root, "gpios", gpios);
|
||||||
|
|
||||||
|
char *json_str = cJSON_PrintUnformatted(root);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
esp_err_t resp_ret = send_json_response(req, json_str);
|
||||||
|
free(json_str);
|
||||||
|
return resp_ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// static esp_err_t api_gpio_get_handler(httpd_req_t *req)
|
||||||
|
// {
|
||||||
|
// char pin_str[8];
|
||||||
|
// if (httpd_req_get_url_query_str(req, pin_str, sizeof(pin_str)) != ESP_OK) {
|
||||||
|
// return send_json_error(req, "Missing pin parameter");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// int pin = atoi(pin_str);
|
||||||
|
// bmc_gpio_state_t state;
|
||||||
|
|
||||||
|
// esp_err_t ret = gpio_get_state((gpio_num_t)pin, &state);
|
||||||
|
// if (ret == ESP_ERR_NOT_FOUND) {
|
||||||
|
// return send_json_error(req, "GPIO not found");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// cJSON *gpio = cJSON_CreateObject();
|
||||||
|
// cJSON_AddNumberToObject(gpio, "pin", state.pin);
|
||||||
|
// cJSON_AddStringToObject(gpio, "name", state.name);
|
||||||
|
// cJSON_AddStringToObject(gpio, "mode",
|
||||||
|
// state.mode == GPIO_MODE_INPUT ? "input" :
|
||||||
|
// state.mode == GPIO_MODE_OUTPUT ? "output" : "inout");
|
||||||
|
// cJSON_AddNumberToObject(gpio, "value", state.value);
|
||||||
|
// cJSON_AddBoolToObject(gpio, "inverted", state.inverted);
|
||||||
|
|
||||||
|
// return send_json_success(req, gpio);
|
||||||
|
// }
|
||||||
|
|
||||||
|
static esp_err_t api_gpio_set_handler(httpd_req_t *req) {
|
||||||
|
// Get pin from query string
|
||||||
|
char query[64];
|
||||||
|
if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) {
|
||||||
|
return send_json_error(req, "Missing query parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
char pin_str[8];
|
||||||
|
if (httpd_query_key_value(query, "pin", pin_str, sizeof(pin_str)) != ESP_OK) {
|
||||||
|
return send_json_error(req, "Missing pin parameter");
|
||||||
|
}
|
||||||
|
int pin = atoi(pin_str);
|
||||||
|
|
||||||
|
// Parse JSON body
|
||||||
|
char buf[128];
|
||||||
|
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||||
|
if (ret <= 0) {
|
||||||
|
return send_json_error(req, "No data received");
|
||||||
|
}
|
||||||
|
buf[ret] = '\0';
|
||||||
|
|
||||||
|
cJSON *json = cJSON_Parse(buf);
|
||||||
|
if (!json) {
|
||||||
|
return send_json_error(req, "Invalid JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *value_item = cJSON_GetObjectItem(json, "value");
|
||||||
|
if (!value_item || !cJSON_IsNumber(value_item)) {
|
||||||
|
cJSON_Delete(json);
|
||||||
|
return send_json_error(req, "Missing or invalid 'value' field");
|
||||||
|
}
|
||||||
|
|
||||||
|
int value = value_item->valueint;
|
||||||
|
cJSON_Delete(json);
|
||||||
|
|
||||||
|
// Set GPIO
|
||||||
|
ret = gpio_write((gpio_num_t)pin, value);
|
||||||
|
if (ret == ESP_ERR_NOT_FOUND) {
|
||||||
|
return send_json_error(req, "GPIO not found");
|
||||||
|
} else if (ret == ESP_ERR_NOT_SUPPORTED) {
|
||||||
|
return send_json_error(req, "GPIO is input-only");
|
||||||
|
} else if (ret != ESP_OK) {
|
||||||
|
return send_json_error(req, "Failed to set GPIO");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(response, "pin", pin);
|
||||||
|
cJSON_AddNumberToObject(response, "value", value);
|
||||||
|
|
||||||
|
return send_json_success(req, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t api_gpio_config_handler(httpd_req_t *req) {
|
||||||
|
// Get pin from query string
|
||||||
|
char query[64];
|
||||||
|
if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) {
|
||||||
|
return send_json_error(req, "Missing query parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
char pin_str[8];
|
||||||
|
if (httpd_query_key_value(query, "pin", pin_str, sizeof(pin_str)) != ESP_OK) {
|
||||||
|
return send_json_error(req, "Missing pin parameter");
|
||||||
|
}
|
||||||
|
int pin = atoi(pin_str);
|
||||||
|
|
||||||
|
// Parse JSON body
|
||||||
|
char buf[128];
|
||||||
|
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||||
|
if (ret <= 0) {
|
||||||
|
return send_json_error(req, "No data received");
|
||||||
|
}
|
||||||
|
buf[ret] = '\0';
|
||||||
|
|
||||||
|
cJSON *json = cJSON_Parse(buf);
|
||||||
|
if (!json) {
|
||||||
|
return send_json_error(req, "Invalid JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *mode_item = cJSON_GetObjectItem(json, "mode");
|
||||||
|
if (!mode_item || !cJSON_IsString(mode_item)) {
|
||||||
|
cJSON_Delete(json);
|
||||||
|
return send_json_error(req, "Missing or invalid 'mode' field");
|
||||||
|
}
|
||||||
|
|
||||||
|
gpio_mode_t mode;
|
||||||
|
const char *mode_str = mode_item->valuestring;
|
||||||
|
if (strcmp(mode_str, "input") == 0) {
|
||||||
|
mode = GPIO_MODE_INPUT;
|
||||||
|
} else if (strcmp(mode_str, "output") == 0) {
|
||||||
|
mode = GPIO_MODE_OUTPUT;
|
||||||
|
} else {
|
||||||
|
cJSON_Delete(json);
|
||||||
|
return send_json_error(req, "Invalid mode. Use 'input' or 'output'");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(json);
|
||||||
|
|
||||||
|
// Configure GPIO
|
||||||
|
ret = gpio_configure((gpio_num_t)pin, mode);
|
||||||
|
if (ret == ESP_ERR_NOT_FOUND) {
|
||||||
|
return send_json_error(req, "GPIO not found");
|
||||||
|
} else if (ret == ESP_ERR_NOT_SUPPORTED) {
|
||||||
|
return send_json_error(req, "GPIO is input-only, cannot set as output");
|
||||||
|
} else if (ret != ESP_OK) {
|
||||||
|
return send_json_error(req, "Failed to configure GPIO");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(response, "pin", pin);
|
||||||
|
cJSON_AddStringToObject(response, "mode", mode_str);
|
||||||
|
|
||||||
|
return send_json_success(req, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Power Control API Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static esp_err_t api_power_status_handler(httpd_req_t *req) {
|
||||||
|
cJSON *status = cJSON_CreateObject();
|
||||||
|
cJSON_AddBoolToObject(status, "power_good", gpio_is_power_good());
|
||||||
|
|
||||||
|
// Get power control GPIO states
|
||||||
|
int power_on_idx = gpio_find_index(BMC_GPIO_POWER_ON);
|
||||||
|
int power_off_idx = gpio_find_index(BMC_GPIO_POWER_OFF);
|
||||||
|
|
||||||
|
if (power_on_idx >= 0) {
|
||||||
|
bmc_gpio_state_t state;
|
||||||
|
gpio_get_state(BMC_GPIO_POWER_ON, &state);
|
||||||
|
cJSON_AddNumberToObject(status, "power_on_state", state.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (power_off_idx >= 0) {
|
||||||
|
bmc_gpio_state_t state;
|
||||||
|
gpio_get_state(BMC_GPIO_POWER_OFF, &state);
|
||||||
|
cJSON_AddNumberToObject(status, "power_off_state", state.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return send_json_success(req, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t api_power_on_handler(httpd_req_t *req) {
|
||||||
|
esp_err_t ret = gpio_power_on();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
return send_json_error(req, "Power on failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "message", "Power on sequence initiated");
|
||||||
|
cJSON_AddBoolToObject(response, "power_good", gpio_is_power_good());
|
||||||
|
|
||||||
|
return send_json_success(req, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t api_power_off_handler(httpd_req_t *req) {
|
||||||
|
esp_err_t ret = gpio_power_off();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
return send_json_error(req, "Power off failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "message", "Power off sequence initiated");
|
||||||
|
|
||||||
|
return send_json_success(req, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t api_power_reset_handler(httpd_req_t *req) {
|
||||||
|
esp_err_t ret = gpio_reset();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
return send_json_error(req, "Reset failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "message", "Reset sequence initiated");
|
||||||
|
|
||||||
|
return send_json_success(req, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Serial API Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static esp_err_t api_serial_config_get_handler(httpd_req_t *req) {
|
||||||
|
bmc_uart_config_t config;
|
||||||
|
esp_err_t ret = uart_get_config(&config);
|
||||||
|
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
return send_json_error(req, "Failed to get serial config");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *config_json = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(config_json, "baud_rate", config.baud_rate);
|
||||||
|
cJSON_AddNumberToObject(config_json, "data_bits", config.data_bits);
|
||||||
|
cJSON_AddNumberToObject(config_json, "parity", config.parity);
|
||||||
|
cJSON_AddNumberToObject(config_json, "stop_bits", config.stop_bits);
|
||||||
|
|
||||||
|
return send_json_success(req, config_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t api_serial_config_set_handler(httpd_req_t *req) {
|
||||||
|
char buf[256];
|
||||||
|
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||||
|
if (ret <= 0) {
|
||||||
|
return send_json_error(req, "No data received");
|
||||||
|
}
|
||||||
|
buf[ret] = '\0';
|
||||||
|
|
||||||
|
cJSON *json = cJSON_Parse(buf);
|
||||||
|
if (!json) {
|
||||||
|
return send_json_error(req, "Invalid JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
bmc_uart_config_t config;
|
||||||
|
uart_get_config(&config); // Get current config
|
||||||
|
|
||||||
|
cJSON *baud = cJSON_GetObjectItem(json, "baud_rate");
|
||||||
|
if (baud && cJSON_IsNumber(baud)) {
|
||||||
|
config.baud_rate = baud->valueint;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(json);
|
||||||
|
|
||||||
|
ret = uart_set_baud_rate(config.baud_rate);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
return send_json_error(req, "Failed to set baud rate");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(response, "baud_rate", config.baud_rate);
|
||||||
|
|
||||||
|
return send_json_success(req, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t api_serial_send_handler(httpd_req_t *req) {
|
||||||
|
char buf[1024];
|
||||||
|
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||||
|
if (ret <= 0) {
|
||||||
|
return send_json_error(req, "No data received");
|
||||||
|
}
|
||||||
|
|
||||||
|
int written = uart_write_data((uint8_t *)buf, ret);
|
||||||
|
if (written < 0) {
|
||||||
|
return send_json_error(req, "Failed to send data");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(response, "bytes_sent", written);
|
||||||
|
|
||||||
|
return send_json_success(req, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WebSocket Handler for Serial Console
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static void uart_to_ws_callback(const uint8_t *data, size_t len) {
|
||||||
|
// Always broadcast - the function will dynamically check for connected clients
|
||||||
|
web_server_ws_broadcast(data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t ws_serial_handler(httpd_req_t *req) {
|
||||||
|
// For WebSocket handlers with is_websocket=true, ESP-IDF handles the handshake
|
||||||
|
// internally. This handler is only called for frame processing.
|
||||||
|
// Client tracking is done dynamically in the broadcast function.
|
||||||
|
|
||||||
|
// Handle WebSocket frames
|
||||||
|
httpd_ws_frame_t ws_pkt;
|
||||||
|
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
|
||||||
|
|
||||||
|
// First call gets the frame info
|
||||||
|
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
// This is normal when client disconnects - don't log as error
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different frame types
|
||||||
|
switch (ws_pkt.type) {
|
||||||
|
case HTTPD_WS_TYPE_CLOSE:
|
||||||
|
// Send close frame in response
|
||||||
|
ws_pkt.len = 0;
|
||||||
|
ws_pkt.payload = NULL;
|
||||||
|
httpd_ws_send_frame(req, &ws_pkt);
|
||||||
|
return ESP_OK;
|
||||||
|
|
||||||
|
case HTTPD_WS_TYPE_PING:
|
||||||
|
// Respond with pong
|
||||||
|
ws_pkt.type = HTTPD_WS_TYPE_PONG;
|
||||||
|
return httpd_ws_send_frame(req, &ws_pkt);
|
||||||
|
|
||||||
|
case HTTPD_WS_TYPE_TEXT:
|
||||||
|
case HTTPD_WS_TYPE_BINARY:
|
||||||
|
// Handle text/binary data (send to UART)
|
||||||
|
if (ws_pkt.len > 0) {
|
||||||
|
uint8_t *buf = malloc(ws_pkt.len + 1);
|
||||||
|
if (buf) {
|
||||||
|
ws_pkt.payload = buf;
|
||||||
|
ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
|
||||||
|
if (ret == ESP_OK) {
|
||||||
|
uart_write_data(ws_pkt.payload, ws_pkt.len);
|
||||||
|
}
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
|
||||||
|
default:
|
||||||
|
ESP_LOGI(TAG_HTTP, "WS frame of unknown type received, ignored");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// System API Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static esp_err_t api_system_info_handler(httpd_req_t *req) {
|
||||||
|
cJSON *info = cJSON_CreateObject();
|
||||||
|
|
||||||
|
// Network info
|
||||||
|
char ip[16], netmask[16], gateway[16], mac[18];
|
||||||
|
if (ethernet_get_network_info(ip, netmask, gateway) == ESP_OK) {
|
||||||
|
cJSON_AddStringToObject(info, "ip", ip);
|
||||||
|
cJSON_AddStringToObject(info, "netmask", netmask);
|
||||||
|
cJSON_AddStringToObject(info, "gateway", gateway);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ethernet_get_mac(mac) == ESP_OK) {
|
||||||
|
cJSON_AddStringToObject(info, "mac", mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddBoolToObject(info, "ethernet_connected", ethernet_is_connected());
|
||||||
|
cJSON_AddNumberToObject(info, "link_speed", ethernet_get_link_speed());
|
||||||
|
cJSON_AddBoolToObject(info, "full_duplex", ethernet_is_full_duplex());
|
||||||
|
|
||||||
|
// UART info
|
||||||
|
cJSON_AddNumberToObject(info, "uart_baud_rate", uart_get_baud_rate());
|
||||||
|
|
||||||
|
return send_json_success(req, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t api_system_status_handler(httpd_req_t *req) {
|
||||||
|
cJSON *status = cJSON_CreateObject();
|
||||||
|
|
||||||
|
// Power status
|
||||||
|
cJSON *power = cJSON_CreateObject();
|
||||||
|
cJSON_AddBoolToObject(power, "power_good", gpio_is_power_good());
|
||||||
|
cJSON_AddItemToObject(status, "power", power);
|
||||||
|
|
||||||
|
// Network status
|
||||||
|
cJSON *network = cJSON_CreateObject();
|
||||||
|
cJSON_AddBoolToObject(network, "connected", ethernet_is_connected());
|
||||||
|
char ip[16];
|
||||||
|
if (ethernet_get_ip(ip) == ESP_OK) {
|
||||||
|
cJSON_AddStringToObject(network, "ip", ip);
|
||||||
|
}
|
||||||
|
cJSON_AddItemToObject(status, "network", network);
|
||||||
|
|
||||||
|
// Serial status
|
||||||
|
cJSON *serial = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(serial, "baud_rate", uart_get_baud_rate());
|
||||||
|
cJSON_AddNumberToObject(serial, "rx_available", uart_data_available());
|
||||||
|
cJSON_AddItemToObject(status, "serial", serial);
|
||||||
|
|
||||||
|
return send_json_success(req, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// URI Registration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_index = {
|
||||||
|
.uri = "/",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = index_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_gpio = {
|
||||||
|
.uri = "/api/gpio",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = api_gpio_get_all_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_gpio_set = {
|
||||||
|
.uri = "/api/gpio/set",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = api_gpio_set_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_gpio_config = {
|
||||||
|
.uri = "/api/gpio/config",
|
||||||
|
.method = HTTP_PUT,
|
||||||
|
.handler = api_gpio_config_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_power_status = {
|
||||||
|
.uri = "/api/power/status",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = api_power_status_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_power_on = {
|
||||||
|
.uri = "/api/power/on",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = api_power_on_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_power_off = {
|
||||||
|
.uri = "/api/power/off",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = api_power_off_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_power_reset = {
|
||||||
|
.uri = "/api/power/reset",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = api_power_reset_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_serial_config_get = {
|
||||||
|
.uri = "/api/serial/config",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = api_serial_config_get_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_serial_config_set = {
|
||||||
|
.uri = "/api/serial/config",
|
||||||
|
.method = HTTP_PUT,
|
||||||
|
.handler = api_serial_config_set_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_serial_send = {
|
||||||
|
.uri = "/api/serial/send",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = api_serial_send_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_ws_serial = {
|
||||||
|
.uri = "/api/serial/ws",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = ws_serial_handler,
|
||||||
|
.is_websocket = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_system_info = {
|
||||||
|
.uri = "/api/system/info",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = api_system_info_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const httpd_uri_t uri_api_system_status = {
|
||||||
|
.uri = "/api/system/status",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = api_system_status_handler,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Public Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
esp_err_t web_server_start(void) {
|
||||||
|
if (server_running) {
|
||||||
|
ESP_LOGW(TAG, "Web server already running");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Starting web server on port %d", BMC_HTTP_PORT);
|
||||||
|
|
||||||
|
// Configure server
|
||||||
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
|
config.server_port = BMC_HTTP_PORT;
|
||||||
|
config.max_uri_handlers = 20;
|
||||||
|
config.stack_size = BMC_HTTP_STACK_SIZE;
|
||||||
|
config.max_open_sockets = BMC_HTTP_MAX_CONN;
|
||||||
|
config.lru_purge_enable = false; // Don't close connections when limit reached
|
||||||
|
config.keep_alive_enable = true; // Enable keep-alive for better connection handling
|
||||||
|
config.keep_alive_idle = 5; // Seconds before keep-alive starts
|
||||||
|
config.keep_alive_interval = 5; // Seconds between keep-alive probes
|
||||||
|
config.keep_alive_count = 3; // Number of failed probes before closing
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
esp_err_t ret = httpd_start(&server, &config);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to start web server: %s", esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register URI handlers
|
||||||
|
httpd_register_uri_handler(server, &uri_index);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_gpio);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_gpio_set);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_gpio_config);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_power_status);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_power_on);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_power_off);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_power_reset);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_serial_config_get);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_serial_config_set);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_serial_send);
|
||||||
|
httpd_register_uri_handler(server, &uri_ws_serial);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_system_info);
|
||||||
|
httpd_register_uri_handler(server, &uri_api_system_status);
|
||||||
|
|
||||||
|
// Register UART callback for WebSocket broadcast at startup
|
||||||
|
esp_err_t cb_ret = uart_register_rx_callback(uart_to_ws_callback);
|
||||||
|
if (cb_ret == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "UART RX callback registered for WebSocket broadcast");
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Failed to register UART RX callback: %s", esp_err_to_name(cb_ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
server_running = true;
|
||||||
|
ESP_LOGI(TAG, "Web server started successfully");
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t web_server_stop(void) {
|
||||||
|
if (!server_running) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Stopping web server");
|
||||||
|
|
||||||
|
httpd_stop(server);
|
||||||
|
server = NULL;
|
||||||
|
server_running = false;
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool web_server_is_running(void) {
|
||||||
|
return server_running;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_handle_t web_server_get_handle(void) {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t web_server_ws_broadcast(const uint8_t *data, size_t len) {
|
||||||
|
if (!server_running || !data || len == 0) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_ws_frame_t ws_pkt = {
|
||||||
|
.payload = (uint8_t *)data,
|
||||||
|
.len = len,
|
||||||
|
.type = HTTPD_WS_TYPE_TEXT,
|
||||||
|
.final = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use a fixed-size array to avoid stack issues
|
||||||
|
#define MAX_WS_BROADCAST_CLIENTS 4
|
||||||
|
int client_fds[MAX_WS_BROADCAST_CLIENTS];
|
||||||
|
size_t clients = MAX_WS_BROADCAST_CLIENTS;
|
||||||
|
|
||||||
|
// Get client list - httpd has its own internal locking
|
||||||
|
esp_err_t ret = httpd_get_client_list(server, &clients, client_fds);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to get client list: %s", esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count and send to all WebSocket clients
|
||||||
|
int ws_count = 0;
|
||||||
|
for (size_t i = 0; i < clients && i < MAX_WS_BROADCAST_CLIENTS; i++) {
|
||||||
|
int fd = client_fds[i];
|
||||||
|
httpd_ws_client_info_t info = httpd_ws_get_fd_info(server, fd);
|
||||||
|
if (info == HTTPD_WS_CLIENT_WEBSOCKET) {
|
||||||
|
ws_count++;
|
||||||
|
esp_err_t err = httpd_ws_send_frame_async(server, fd, &ws_pkt);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGD(TAG, "WS send to fd %d failed: %s", fd, esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log if we have multiple clients
|
||||||
|
if (ws_count > 1) {
|
||||||
|
ESP_LOGD(TAG, "Broadcast to %d WS clients", ws_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int web_server_ws_client_count(void) {
|
||||||
|
// Dynamically count WebSocket clients
|
||||||
|
if (!server_running) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MAX_WS_COUNT_CLIENTS 4
|
||||||
|
int client_fds[MAX_WS_COUNT_CLIENTS];
|
||||||
|
size_t clients = MAX_WS_COUNT_CLIENTS;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
if (httpd_get_client_list(server, &clients, client_fds) == ESP_OK) {
|
||||||
|
for (size_t i = 0; i < clients && i < MAX_WS_COUNT_CLIENTS; i++) {
|
||||||
|
if (httpd_ws_get_fd_info(server, client_fds[i]) == HTTPD_WS_CLIENT_WEBSOCKET) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
61
main/web_server.h
Normal file
61
main/web_server.h
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#ifndef WEB_SERVER_H
|
||||||
|
#define WEB_SERVER_H
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Function Declarations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize and start the web server
|
||||||
|
*
|
||||||
|
* Starts the HTTP server and registers all URI handlers.
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t web_server_start(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stop the web server
|
||||||
|
*
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t web_server_stop(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if web server is running
|
||||||
|
*
|
||||||
|
* @return true if running, false otherwise
|
||||||
|
*/
|
||||||
|
bool web_server_is_running(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the server handle
|
||||||
|
*
|
||||||
|
* @return httpd_handle_t or NULL if not running
|
||||||
|
*/
|
||||||
|
httpd_handle_t web_server_get_handle(void);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WebSocket Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Broadcast data to all connected WebSocket clients
|
||||||
|
*
|
||||||
|
* @param data Data to send
|
||||||
|
* @param len Length of data
|
||||||
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t web_server_ws_broadcast(const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get number of connected WebSocket clients
|
||||||
|
*
|
||||||
|
* @return Number of connected clients
|
||||||
|
*/
|
||||||
|
int web_server_ws_client_count(void);
|
||||||
|
|
||||||
|
#endif // WEB_SERVER_H
|
||||||
348
plans/esp32-bmc-plan.md
Normal file
348
plans/esp32-bmc-plan.md
Normal file
@@ -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
|
||||||
35
sdkconfig.defaults
Normal file
35
sdkconfig.defaults
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user