feat: ESP32-C3 OTA 中继项目 initial commit
This commit is contained in:
Executable
+13
@@ -0,0 +1,13 @@
|
||||
ARG DOCKER_TAG=latest
|
||||
FROM espressif/idf:${DOCKER_TAG}
|
||||
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
RUN apt-get update -y && apt-get install udev -y
|
||||
|
||||
RUN echo "source /opt/esp/idf/export.sh > /dev/null 2>&1" >> ~/.bashrc
|
||||
|
||||
ENTRYPOINT [ "/opt/esp/entrypoint.sh" ]
|
||||
|
||||
CMD ["/bin/bash", "-c"]
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ESP-IDF QEMU",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
"idf.gitPath": "/usr/bin/git"
|
||||
},
|
||||
"extensions": [
|
||||
"espressif.esp-idf-extension",
|
||||
"espressif.esp-idf-web"
|
||||
]
|
||||
}
|
||||
},
|
||||
"runArgs": ["--privileged"]
|
||||
}
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Directory metadata
|
||||
.directory
|
||||
|
||||
# Temporary files
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Build artifacts and directories
|
||||
**/build/
|
||||
build/
|
||||
*.o
|
||||
*.a
|
||||
*.out
|
||||
*.exe # For any host-side utilities compiled on Windows
|
||||
|
||||
# ESP-IDF specific build outputs
|
||||
*.bin
|
||||
*.elf
|
||||
*.map
|
||||
flasher_args.json # Generated in build directory
|
||||
sdkconfig.old
|
||||
sdkconfig
|
||||
|
||||
# ESP-IDF dependencies
|
||||
# For older versions or manual component management
|
||||
/components/.idf/
|
||||
**/components/.idf/
|
||||
# For modern ESP-IDF component manager
|
||||
managed_components/
|
||||
# If ESP-IDF tools are installed/referenced locally to the project
|
||||
.espressif/
|
||||
|
||||
# CMake generated files
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
CTestTestfile.cmake
|
||||
|
||||
# Python environment files
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
|
||||
# Virtual environment folders
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# Language Servers
|
||||
.clangd/
|
||||
.ccls-cache/
|
||||
compile_commands.json
|
||||
|
||||
# Windows specific
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# User-specific configuration files
|
||||
*.user
|
||||
*.workspace # General workspace files, can be from various tools
|
||||
*.suo # Visual Studio Solution User Options
|
||||
*.sln.docstates # Visual Studio
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ESP-IDF",
|
||||
"compilerPath": "C:\\Espressif\\tools\\riscv32-esp-elf\\esp-15.2.0_20251204\\riscv32-esp-elf\\bin\\riscv32-esp-elf-gcc.exe",
|
||||
"compileCommands": "${config:idf.buildPath}/compile_commands.json",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"browse": {
|
||||
"path": [
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"limitSymbolsToIncludedHeaders": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "gdbtarget",
|
||||
"request": "attach",
|
||||
"name": "Eclipse CDT GDB Adapter"
|
||||
},
|
||||
{
|
||||
"type": "cmake",
|
||||
"request": "launch",
|
||||
"name": "Debug portfile(s)",
|
||||
"cmakeDebugType": "external",
|
||||
"pipeName": "\\\\.\\pipe\\vcpkg_ext_portfile_dbg",
|
||||
"preLaunchTask": "Debug vcpkg commands"
|
||||
}
|
||||
]
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"C_Cpp.intelliSenseEngine": "default",
|
||||
"idf.openOcdConfigs": [
|
||||
"board/esp32c3-bridge.cfg"
|
||||
],
|
||||
"idf.portWin": "detect",
|
||||
"idf.currentSetup": "F:\\software\\.espressif\\v6.0\\esp-idf",
|
||||
"idf.customExtraVars": {
|
||||
"IDF_TARGET": "esp32c3"
|
||||
},
|
||||
"clangd.path": "C:\\Espressif\\tools\\esp-clang\\esp-20.1.1_20250829\\esp-clang\\bin\\clangd.exe",
|
||||
"clangd.arguments": [
|
||||
"--background-index",
|
||||
"--query-driver=**",
|
||||
"--compile-commands-dir=g:\\Robot_v2\\DFU\\ESP32_COMM\\build"
|
||||
]
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(ESP32_COMM)
|
||||
@@ -0,0 +1,60 @@
|
||||
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
|
||||
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- | -------- |
|
||||
|
||||
# Wi-Fi SoftAP Example
|
||||
|
||||
(See the README.md file in the upper level 'examples' directory for more information about examples.)
|
||||
|
||||
This example shows how to use the Wi-Fi SoftAP functionality of the Wi-Fi driver of ESP for serving as an Access Point.
|
||||
|
||||
## How to use example
|
||||
|
||||
SoftAP supports Protected Management Frames(PMF). Necessary configurations can be set using pmf flags. Please refer [Wifi-Security](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi-security.html) for more info.
|
||||
|
||||
### Configure the project
|
||||
|
||||
Open the project configuration menu (`idf.py menuconfig`).
|
||||
|
||||
In the `Example Configuration` menu:
|
||||
|
||||
* Set the Wi-Fi configuration.
|
||||
* Set `WiFi SSID`.
|
||||
* Set `WiFi Password`.
|
||||
|
||||
Optional: If you need, change the other options according to your requirements.
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Build the project and flash it to the board, then run the monitor tool to view the serial output:
|
||||
|
||||
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the Getting Started Guide for all the steps to configure and use the ESP-IDF to build projects.
|
||||
|
||||
* [ESP-IDF Getting Started Guide on ESP32](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html)
|
||||
* [ESP-IDF Getting Started Guide on ESP32-S2](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/get-started/index.html)
|
||||
* [ESP-IDF Getting Started Guide on ESP32-C3](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/get-started/index.html)
|
||||
|
||||
## Example Output
|
||||
|
||||
There is the console output for this example:
|
||||
|
||||
```
|
||||
I (917) phy: phy_version: 3960, 5211945, Jul 18 2018, 10:40:07, 0, 0
|
||||
I (917) wifi: mode : softAP (30:ae:a4:80:45:69)
|
||||
I (917) wifi softAP: wifi_init_softap finished.SSID:myssid password:mypassword
|
||||
I (26457) wifi: n:1 0, o:1 0, ap:1 1, sta:255 255, prof:1
|
||||
I (26457) wifi: station: 70:ef:00:43:96:67 join, AID=1, bg, 20
|
||||
I (26467) wifi softAP: station:70:ef:00:43:96:67 join, AID=1
|
||||
I (27657) esp_netif_lwip: DHCP server assigned IP to a station, IP is: 192.168.4.2
|
||||
```
|
||||
|
||||
## Running the example on ESP Chips without Wi-Fi
|
||||
|
||||
This example can run on ESP Chips without Wi-Fi using ESP-Hosted. See the [Two-Chip Solution](../../README.md#wi-fi-examples-with-two-chip-solution) section in the upper level `README.md` for information.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.
|
||||
@@ -0,0 +1,8 @@
|
||||
idf_component_register(
|
||||
SRCS "cJSON.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES
|
||||
)
|
||||
|
||||
# ESP-IDF uses no DLL, export all symbols
|
||||
target_compile_definitions(${COMPONENT_LIB} PUBLIC CJSON_HIDE_SYMBOLS=0)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef cJSON__h
|
||||
#define cJSON__h
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
|
||||
#define __WINDOWS__
|
||||
#endif
|
||||
|
||||
#ifdef __WINDOWS__
|
||||
|
||||
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
|
||||
|
||||
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
|
||||
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
|
||||
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
|
||||
|
||||
For *nix builds that support visibility attribute, you can define similar behavior by
|
||||
|
||||
setting default visibility to hidden by adding
|
||||
-fvisibility=hidden (for gcc)
|
||||
or
|
||||
-xldscope=hidden (for sun cc)
|
||||
to CFLAGS
|
||||
|
||||
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
|
||||
|
||||
*/
|
||||
|
||||
#define CJSON_CDECL __cdecl
|
||||
#define CJSON_STDCALL __stdcall
|
||||
|
||||
/* export symbols by default, this is necessary for copy pasting the C and header file */
|
||||
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
|
||||
#define CJSON_EXPORT_SYMBOLS
|
||||
#endif
|
||||
|
||||
#if defined(CJSON_HIDE_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) type CJSON_STDCALL
|
||||
#elif defined(CJSON_EXPORT_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
|
||||
#elif defined(CJSON_IMPORT_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
|
||||
#endif
|
||||
#else /* !__WINDOWS__ */
|
||||
#define CJSON_CDECL
|
||||
#define CJSON_STDCALL
|
||||
|
||||
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
|
||||
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
|
||||
#else
|
||||
#define CJSON_PUBLIC(type) type
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* project version */
|
||||
#define CJSON_VERSION_MAJOR 1
|
||||
#define CJSON_VERSION_MINOR 7
|
||||
#define CJSON_VERSION_PATCH 19
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* cJSON Types: */
|
||||
#define cJSON_Invalid (0)
|
||||
#define cJSON_False (1 << 0)
|
||||
#define cJSON_True (1 << 1)
|
||||
#define cJSON_NULL (1 << 2)
|
||||
#define cJSON_Number (1 << 3)
|
||||
#define cJSON_String (1 << 4)
|
||||
#define cJSON_Array (1 << 5)
|
||||
#define cJSON_Object (1 << 6)
|
||||
#define cJSON_Raw (1 << 7) /* raw json */
|
||||
|
||||
#define cJSON_IsReference 256
|
||||
#define cJSON_StringIsConst 512
|
||||
|
||||
/* The cJSON structure: */
|
||||
typedef struct cJSON
|
||||
{
|
||||
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
|
||||
struct cJSON *next;
|
||||
struct cJSON *prev;
|
||||
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
|
||||
struct cJSON *child;
|
||||
|
||||
/* The type of the item, as above. */
|
||||
int type;
|
||||
|
||||
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
|
||||
char *valuestring;
|
||||
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
|
||||
int valueint;
|
||||
/* The item's number, if type==cJSON_Number */
|
||||
double valuedouble;
|
||||
|
||||
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
|
||||
char *string;
|
||||
} cJSON;
|
||||
|
||||
typedef struct cJSON_Hooks
|
||||
{
|
||||
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
|
||||
void *(CJSON_CDECL *malloc_fn)(size_t sz);
|
||||
void (CJSON_CDECL *free_fn)(void *ptr);
|
||||
} cJSON_Hooks;
|
||||
|
||||
typedef int cJSON_bool;
|
||||
|
||||
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
|
||||
* This is to prevent stack overflows. */
|
||||
#ifndef CJSON_NESTING_LIMIT
|
||||
#define CJSON_NESTING_LIMIT 1000
|
||||
#endif
|
||||
|
||||
/* Limits the length of circular references can be before cJSON rejects to parse them.
|
||||
* This is to prevent stack overflows. */
|
||||
#ifndef CJSON_CIRCULAR_LIMIT
|
||||
#define CJSON_CIRCULAR_LIMIT 10000
|
||||
#endif
|
||||
|
||||
/* returns the version of cJSON as a string */
|
||||
CJSON_PUBLIC(const char*) cJSON_Version(void);
|
||||
|
||||
/* Supply malloc, realloc and free functions to cJSON */
|
||||
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
|
||||
|
||||
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
|
||||
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
|
||||
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
|
||||
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||
|
||||
/* Render a cJSON entity to text for transfer/storage. */
|
||||
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
|
||||
/* Render a cJSON entity to text for transfer/storage without any formatting. */
|
||||
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
|
||||
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
|
||||
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
|
||||
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
|
||||
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
|
||||
/* Delete a cJSON entity and all subentities. */
|
||||
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
|
||||
|
||||
/* Returns the number of items in an array (or object). */
|
||||
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
|
||||
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
|
||||
/* Get item "string" from object. Case insensitive. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
|
||||
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
|
||||
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
|
||||
|
||||
/* Check item type and return its value */
|
||||
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
|
||||
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
|
||||
|
||||
/* These functions check the type of an item */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
|
||||
|
||||
/* These calls create a cJSON item of the appropriate type. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
|
||||
/* raw json */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
|
||||
|
||||
/* Create a string where valuestring references a string so
|
||||
* it will not be freed by cJSON_Delete */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
|
||||
/* Create an object/array that only references it's elements so
|
||||
* they will not be freed by cJSON_Delete */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
|
||||
|
||||
/* These utilities create an Array of count items.
|
||||
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
|
||||
|
||||
/* Append item to the specified array/object. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
|
||||
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
|
||||
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
|
||||
* writing to `item->string` */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
|
||||
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
|
||||
|
||||
/* Remove/Detach items from Arrays/Objects. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||
|
||||
/* Update array items. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
|
||||
|
||||
/* Duplicate a cJSON item */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
|
||||
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
|
||||
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
|
||||
* The item->next and ->prev pointers are always zero on return from Duplicate. */
|
||||
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
|
||||
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
|
||||
|
||||
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
|
||||
* The input pointer json cannot point to a read-only address area, such as a string constant,
|
||||
* but should point to a readable and writable address area. */
|
||||
CJSON_PUBLIC(void) cJSON_Minify(char *json);
|
||||
|
||||
/* Helper functions for creating and adding items to an object at the same time.
|
||||
* They return the added item or NULL on failure. */
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
|
||||
|
||||
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
|
||||
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
|
||||
/* helper for the cJSON_SetNumberValue macro */
|
||||
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
|
||||
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
|
||||
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
|
||||
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
|
||||
|
||||
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
|
||||
#define cJSON_SetBoolValue(object, boolValue) ( \
|
||||
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
|
||||
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
|
||||
cJSON_Invalid\
|
||||
)
|
||||
|
||||
/* Macro for iterating over an array or object */
|
||||
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
|
||||
|
||||
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
|
||||
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
|
||||
CJSON_PUBLIC(void) cJSON_free(void *object);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
# ESP32-C3FH4 OTA Relay - Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Label
|
||||
otadata, data, ota, 0x9000, 0x2000,
|
||||
gd32_ota, data, spiffs, 0xB000, 0x40000,
|
||||
app0, app, ota_0, 0x4B000, 0x330000,
|
||||
app1, app, ota_1, 0x3DC000, 0x330000,
|
||||
spiffs, data, spiffs, 0x670000, 0x190000,
|
||||
|
@@ -0,0 +1,71 @@
|
||||
# ESP32-C3FH4 OTA Relay - SDK Configuration (ESP-IDF 6.0)
|
||||
|
||||
CONFIG_IDF_TARGET="esp32c3"
|
||||
CONFIG_IDF_TARGET_ESP32C3=y
|
||||
|
||||
# Partition table
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="config/partitions.csv"
|
||||
|
||||
# SPI Flash
|
||||
CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
|
||||
CONFIG_ESPTOOLPY_FLASH_FREQ_80M=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
|
||||
# ESP32-C3 specific
|
||||
CONFIG_ESP32C3_REV_MIN_0=y
|
||||
CONFIG_ESP32C3_REV_MIN=0
|
||||
|
||||
# FreeRTOS
|
||||
CONFIG_FREERTOS_UNICORE=n
|
||||
CONFIG_FREERTOS_NUMBER_OF_CORES=2
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
# WiFi
|
||||
CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10
|
||||
CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32
|
||||
CONFIG_ESP32_WIFI_TX_BUFFER_NUM=32
|
||||
CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y
|
||||
CONFIG_ESP_WIFI_STA_SUPPORT=y
|
||||
|
||||
# BLE (ESP32-C3 has BLE 5.0) - ESP-IDF 6.0 Bluedroid
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
CONFIG_BTDM_BLE_MAX_CONN_EFF=1
|
||||
CONFIG_BTDM_CTRL_BLE_MAX_CONN_EFF=1
|
||||
CONFIG_BTDM_BLE_MAX_CONN_EFF_EFF=1
|
||||
|
||||
# mbedTLS
|
||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
|
||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT=y
|
||||
CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
|
||||
CONFIG_MBEDTLS_AES_C=y
|
||||
CONFIG_MBEDTLS_CTR_DRBG_C=y
|
||||
CONFIG_MBEDTLS_ENTROPY_C=y
|
||||
|
||||
# System health monitoring
|
||||
CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32
|
||||
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
|
||||
|
||||
# Log
|
||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE=y
|
||||
|
||||
# OTA
|
||||
CONFIG_ESP_OTA_IMG_VERIFICATION=y
|
||||
|
||||
# HTTP server
|
||||
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
|
||||
CONFIG_HTTPD_MAX_URI_LEN=512
|
||||
|
||||
# SPI master driver
|
||||
CONFIG_SPI_MASTER_IN_IRAM=y
|
||||
|
||||
# Temp sensor
|
||||
CONFIG_TEMPERATURE_SENSOR_CTRL_GPIO=-1
|
||||
|
||||
# ESP Timer
|
||||
CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD=n
|
||||
|
||||
# efuse voltage read
|
||||
CONFIG_ESP_EFUSE_WRITE_BLOCK=y
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
dependencies:
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 6.0.0
|
||||
manifest_hash: 85718ddc7aec44f55b1140a54bca97140b212bcf5abafbf43632bc4dc01988d8
|
||||
target: esp32c3
|
||||
version: 3.0.0
|
||||
@@ -0,0 +1,126 @@
|
||||
# ESP32-C3FH4 OTA 中继项目 — 程序开发说明
|
||||
|
||||
**委托开发方:** 请完整执行以下所有任务,完成后停止。不要在中途停止。
|
||||
|
||||
**开发范围:** ESP32-C3FH4 固件开发,充当手机 APP/PC 与 GD32E508VET6 之间的 OTA 中继器。
|
||||
|
||||
**核心工作模式:** ESP32 通过 WiFi 或 BLE 接收上位机发送的 GD32 固件 → 存入本地 Flash → 通过 SPI 转发给 GD32 执行 OTA 升级。
|
||||
|
||||
> **重要:** 开发过程中如需与 GD32 固件侧确认接口(如 AES 密钥长度、搅伴算法细节等),请先按设计文档实现接口定义并预留 hook,实际参数值用占位符标注,最终由委托方补充。
|
||||
|
||||
---
|
||||
|
||||
## 参考文件
|
||||
|
||||
| 文件 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| 架构设计文档 | `/workzone/esp32_ota_relay/esp32_ota_relay_design.md` | 完整协议定义、模块架构、状态机设计 |
|
||||
| 开发任务清单 | `/workzone/esp32_ota_relay/tasks.md` | 所有 48 条开发任务的详细描述 |
|
||||
|
||||
**开发顺序:** T0 → T1 → T2 → T3 → T4/T5(可并行)→ T6 → T7 → T8
|
||||
|
||||
---
|
||||
|
||||
## 系统架构(必读)
|
||||
|
||||
```
|
||||
[手机APP/PC] --WiFi/BLE--> [ESP32-C3FH4] --SPI--> [GD32E508VET6]
|
||||
|
|
||||
+-- 存储 GD32 固件到 Flash
|
||||
+-- 四阶段安全握手协议
|
||||
+-- 内置温度/电压/WiFi 自检
|
||||
```
|
||||
|
||||
ESP32 的角色是**数据透传 + 存储中继**,GD32 OTA 的加密握手和数据传输逻辑全部由 ESP32 透传到 GD32,ESP32 本身不解析固件内容。
|
||||
|
||||
---
|
||||
|
||||
## 通信协议要点
|
||||
|
||||
### ESP32 ↔ GD32(SPI)
|
||||
|
||||
完整映射 GD32 原有 CANFD OTA 四阶段流程:
|
||||
|
||||
- **阶段一(安全握手):** 0x00 AC 握手 → 16B nonce 挑战 → 握手包响应 → 0xBA C0 完成
|
||||
- **阶段二(传输准备):** 发送固件总大小 → 0xBA C0 + seq 确认
|
||||
- **阶段三(数据分包):** 每包 48B + CRC,循环发送直到最后一包带 IFEND
|
||||
- **阶段四(结束跳转):** 0xBA C0 + FF FF FF FF 完成确认 → 0x0A AC 刷写结束 → GD32 跳转 APP
|
||||
|
||||
帧格式:`SOF(0xEB) + CMD + LEN(2B LE) + PAYLOAD(LEN) + CRC16(2B LE)`
|
||||
|
||||
命令码:0xF0 HANDSHAKE_REQ / 0xF1 CHALLENGE / 0xF2 SESSION_KEY / 0xF3 SEND_SIZE / 0xF4 DATA_PACKET / 0xF5 DATA_END / 0xF6 TRANSFER_DONE / 0xF7 FINISH_OTA
|
||||
|
||||
### APP ↔ ESP32(WiFi HTTP / BLE GATT)
|
||||
|
||||
- WiFi:HTTP POST `/ota/upload`,JSON 响应进度
|
||||
- BLE:4 个 GATT Characteristic(Control Point / Data In / Data Out / Version)
|
||||
- APP→ESP32 使用明文帧格式(加密由 ESP32 在转发给 GD32 时处理)
|
||||
|
||||
---
|
||||
|
||||
## 存储方案(方案 A)
|
||||
|
||||
固件先完整存入 ESP32 Flash,再转发给 GD32:
|
||||
|
||||
- 分区:`gd32_ota`(256KB,用于存 GD32 固件 binary)
|
||||
- 元数据文件:`ota_meta.json`(total_size / stored_size / last_seq)
|
||||
- 断点续传:APP 重连后从 `last_seq + 1` 继续发送
|
||||
- 存储完成后自动触发 SPI 转发流程
|
||||
|
||||
---
|
||||
|
||||
## 自检与保护
|
||||
|
||||
不外挂传感器,使用 ESP32 内置传感器:
|
||||
|
||||
- **芯片温度**(正常 < 80°C / 警告 80-100°C / 停机 > 100°C)— 1s 轮询
|
||||
- **供电电压**(正常 2.5-3.6V / 警告 < 2.5V / 停机 < 2.3V)— 5s 轮询
|
||||
- **WiFi RSSI**(正常 > -70 / 弱 -70~-85 / 断开 < -85 dBm)— 10s 轮询
|
||||
- **BLE 连接状态** — 事件驱动
|
||||
|
||||
OTA 每包发送前检查健康状态,CRITICAL 时暂停,SAFE_SHUTDOWN 时中止并清除 meta。
|
||||
|
||||
---
|
||||
|
||||
## 开发要求
|
||||
|
||||
1. **使用 ESP-IDF v5.x**,基于 CMake 构建系统,目标芯片 `esp32c3`
|
||||
2. **遵循 FreeRTOS 任务架构**:System Monitor / OTA Relay / WiFi Server / BLE GATT 四个任务
|
||||
3. **AES 使用 mbedTLS**(ESP32 内置),预置主密钥用占位符 `MASTER_KEY_HERE`
|
||||
4. **搅伴算法**实现需与 GD32 固件侧匹配,预留参数配置接口
|
||||
5. **Flash 写入**使用 esp_partition API,追加写入不断点
|
||||
6. **SPI 使用 VSPI (SPI2)**,GPIO6-9,CS=GPIO4,时钟 40MHz,mode=0
|
||||
7. **所有任务完成后**,清理会话密钥、释放内存、返回完成信号
|
||||
8. **不允许中途停止**,必须完成 T0~T8 所有任务后,才能停止运行或退出
|
||||
|
||||
---
|
||||
|
||||
## 开发产出要求
|
||||
|
||||
1. 完整可编译的 ESP32-C3 工程代码(CMake 项目结构)
|
||||
2. 所有代码放在 `/workzone/esp32_ota_relay/` 目录下
|
||||
3. 代码中必须有完整的 `main/` 目录(`main.c` / `app_main.c` / `ota_relay.c` / `spi_master.c` / `wifi_ota.c` / `ble_ota.c` / `protocol.c`)
|
||||
4. 不得使用任何需要外部 API Key 的服务(AES、WiFi、BLE 均使用 ESP-IDF 内置库)
|
||||
5. 完成后更新 `/workzone/esp32_ota_relay/tasks.md` 的进度记录
|
||||
|
||||
---
|
||||
|
||||
## GD32 固件侧依赖说明
|
||||
|
||||
以下接口需 GD32 固件侧提供参数后才能完整验证,请预留 hook 并用占位符标注:
|
||||
|
||||
| 接口 | 占位符 | 说明 |
|
||||
|---|---|---|
|
||||
| 主密钥 | `MASTER_KEY_HERE` | AES-128 主密钥,16 bytes |
|
||||
| 搅伴算法参数 | `SCRAMBLE_CONST_HERE` | 时间戳+内存地址+rand 的固定 seed |
|
||||
| 芯片唯一码 | `CHIP_UID_HERE` | GD32 芯片唯一 ID 用于密钥派生 |
|
||||
| 会话密钥 K_session | - | 由 Step 4/5 双方协商生成,存储在 RAM |
|
||||
| AES 算法模式 | AES-128 CBC | 由 ESP32 和 GD32 双方协商一致即可 |
|
||||
|
||||
---
|
||||
|
||||
## 联系方式
|
||||
|
||||
如需向委托方确认接口参数,请在代码中用 `// TODO: confirm with GD32 firmware team` 标注,不得自行假定。
|
||||
|
||||
**开发过程中遇到任何问题,请继续尝试解决,不要停止。**
|
||||
@@ -0,0 +1,43 @@
|
||||
# ESP32-C3FH4 OTA Relay
|
||||
|
||||
Complete firmware for ESP32-C3FH4 acting as OTA relay between APP/PC and GD32E508VET6.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
export IDF_PATH=/path/to/esp-idf
|
||||
source $IDF_PATH/export.sh
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
main/
|
||||
main.c - Entry point, task creation
|
||||
app_main.c - Application init, system monitor task
|
||||
ota_relay.c - 4-stage OTA state machine
|
||||
spi_master.c - SPI2 (VSPI) master driver
|
||||
protocol.c - Frame format, CRC16, AES, scramble
|
||||
wifi_ota.c - WiFi AP/STA + HTTP server
|
||||
ble_ota.c - BLE GATT server
|
||||
health_monitor.c - Temperature/voltage/WiFi monitoring
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- WiFi AP mode (SSID: ESP32_OTA_RELAY, pw: 12345678)
|
||||
- BLE GATT OTA service
|
||||
- HTTP POST /ota/upload for firmware
|
||||
- SPI to GD32 (40MHz, GPIO4-9)
|
||||
- Flash storage with断点续传
|
||||
- System health monitoring
|
||||
|
||||
## TODO (confirm with GD32 team)
|
||||
|
||||
- MASTER_KEY_HERE - AES-128 master key (16 bytes)
|
||||
- SCRAMBLE_CONST_HERE - scramble algorithm seed
|
||||
- CHIP_UID_HERE - GD32 chip unique ID
|
||||
- GATT UUIDs for OTA service
|
||||
@@ -0,0 +1,698 @@
|
||||
# ESP32-C3FH4 OTA 中继架构设计
|
||||
|
||||
## 1. 系统概述
|
||||
|
||||
```
|
||||
[手机APP/PC] --WiFi--> [ESP32-C3FH4] --SPI--> [GD32E508VET6]
|
||||
|
|
||||
+--BLE---> [手机APP/PC]
|
||||
```
|
||||
|
||||
- **ESP32 角色**:WiFi/BLE 接收端 + SPI 透传(主机)
|
||||
- **GD32 角色**:OTA 执行端(从机),已有完整 OTA 流程
|
||||
- **ESP32 不负责 OTA 逻辑**,只做数据转发
|
||||
|
||||
---
|
||||
|
||||
## 2. 硬件连接
|
||||
|
||||
### SPI 接口
|
||||
|
||||
| ESP32 (Master) | GD32 (Slave) | 说明 |
|
||||
|---|---|---|
|
||||
| GPIO6 (SPIIO0) | PA4 (NSS) | 片选 |
|
||||
| GPIO7 (SPIQ) | PA5 (SCK) | 时钟 |
|
||||
| GPIO8 (SPIWP) | PA6 (MOSI) | 主发从收 |
|
||||
| GPIO9 (SPIHD) | PA7 (MISO) | 主收从发 |
|
||||
|
||||
> ⚠️ GD32E508 使用标准 SPI(PA4-NSS, PA5-SCK, PA6-MOSI, PA7-MISO)
|
||||
> GD32 已内置 BOOT+APP 模式,ESP32 通过 SPI 命令直接控制进入 Bootloader,无需额外 BOOT/RST 控制引脚。
|
||||
|
||||
---
|
||||
|
||||
## 3. 通信协议设计
|
||||
|
||||
ESP32 与 GD32 之间的 SPI 协议完整映射 GD32 原有 CANFD OTA 流程,分为四个阶段。
|
||||
|
||||
### 3.0 帧格式总览
|
||||
|
||||
所有 SPI 帧格式统一为:
|
||||
|
||||
```
|
||||
| SOF (1B) | CMD (1B) | LEN (2B, LE) | PAYLOAD (LEN bytes) | CRC16 (2B, LE) |
|
||||
```
|
||||
|
||||
- **SOF**: 0xEB 固定帧头
|
||||
- **CMD**: 命令码(见下表)
|
||||
- **LEN**: Payload 长度(不含帧头和 CRC)
|
||||
- **CRC16**: Modbus CRC16,低字节在前
|
||||
|
||||
> 注意:CANFD 侧(原协议)的分包为 48 bytes/packet;SPI 中继时,ESP32 接收 APP/PC 的分包可能更大,内部透传时不做分包合并,只做协议映射。
|
||||
|
||||
---
|
||||
|
||||
### 3.1 第一阶段:安全握手与认证(步骤 1-6)
|
||||
|
||||
目的是建立加密安全通道,防止非法刷写。
|
||||
|
||||
#### Step 1 & 2 — 握手发起与响应
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | `0x00 AC` | 发起 OTA 请求 |
|
||||
| GD32 → ESP32 | `0x00 AC` | 响应信号,表示准备好进入 OTA 模式 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF0 (HANDSHAKE_REQ), Payload=[] → ESP32 发送 0x00 AC
|
||||
CMD=0xF0 (HANDSHAKE_REQ), Payload=[] ← GD32 回复 0x00 AC
|
||||
```
|
||||
|
||||
#### Step 3 — 主机挑战(发送随机数)
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | `nonce_server` (16 bytes) | 16字节随机数,用于生成会话密钥 |
|
||||
|
||||
**搅伴算法**(时间 + 内存地址 + rand → 3轮单字节 → 2轮4字节)由 ESP32 实现,生成方法需与 GD32 预置的主密钥匹配。
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF1 (CHALLENGE), Payload=[16 bytes nonce_server]
|
||||
```
|
||||
|
||||
#### Step 4 — 从机响应与身份验证
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | 握手数据包 | 使用主密钥 AES 加密随机数 + 会话密钥加密自身信息 |
|
||||
|
||||
GD32 获取芯片唯一码,使用主密钥通过 **AES 算法**加密随机数,并使用会话密钥加密自身信息,打包发送。
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF1 (CHALLENGE), Payload=[握手数据包] ← GD32 响应
|
||||
```
|
||||
|
||||
Payload 结构(AES 加密后的数据):
|
||||
|
||||
```
|
||||
| AES_encrypt(nonce_server, master_key) (16B) | AES_encrypt(device_info, K_session) (16B) |
|
||||
```
|
||||
|
||||
#### Step 5 — 主机验证与会话密钥分发
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | 变换后的随机数 | 使用会话密钥加密后发送 |
|
||||
|
||||
ESP32 验证随机数,解密获取会话密钥 (K_session),对随机数进行变换后用会话密钥加密发送。
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF2 (SESSION_KEY), Payload=[AES_encrypt(transformed_nonce, K_session)]
|
||||
```
|
||||
|
||||
#### Step 6 — 认证完成
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0xBA C0` | 安全通道建立完成 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF2 (SESSION_KEY), Payload=[] ← GD32 回复 0xBA C0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 第二阶段:传输准备(步骤 7-8)
|
||||
|
||||
#### Step 7 — 发送大小
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | `Data Size` (4 bytes, LE) | APP 总数据大小 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF3 (SEND_SIZE), Payload=[size(4B, LE)]
|
||||
```
|
||||
|
||||
#### Step 8 — 确认大小
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0xBA C0` + 包序号 | 确认收到大小信息 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF3 (SEND_SIZE), Payload=[seq(2B)] ← GD32 回复 0xBA C0 + seq
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 第三阶段:数据分包传输循环(步骤 9-12)
|
||||
|
||||
刷写核心部分,数据加密分包发送。
|
||||
|
||||
#### Step 9 — 发送数据包
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | OTA 程序数据包 | 经会话密钥加密,含 CRC 校验码 |
|
||||
|
||||
分包大小约 **48 bytes/packet**(与原 CANFD 协议一致)。
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF4 (DATA_PACKET), Payload=[encrypted_data(48B) + CRC]
|
||||
```
|
||||
|
||||
#### Step 10 — 从机校验与确认
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0xBA C0` + 包序号 | 校验通过,请求下一包 |
|
||||
| GD32 → ESP32 | `0xBA C0` + 错误指示 | 校验失败,要求重发当前包(限次重试) |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF4 (DATA_PACKET), Payload=[seq(2B)] ← 正常 ACK
|
||||
CMD=0xF4 (DATA_PACKET), Payload=[seq(2B) + 0xFF] ← 错误重发请求
|
||||
```
|
||||
|
||||
#### Step 11 — 继续发送
|
||||
|
||||
主机发送下一包加密数据。
|
||||
|
||||
循环机制:`总循环次数 = 1 + 总APP大小 // 48 Byte`
|
||||
|
||||
#### Step 12 — 最后一包
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | 最后一包数据 | IFEND(结束标志位)置位 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF5 (DATA_END), Payload=[encrypted_data(N) + CRC + IFEND flag]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 第四阶段:结束与跳转(步骤 13-17)
|
||||
|
||||
#### Step 13 — 传输完成确认
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0xBA C0` + `FF FF FF FF` | 所有数据包传输完成并校验通过 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF6 (TRANSFER_DONE), Payload=[0xFF, 0xFF, 0xFF, 0xFF] ← GD32 完成确认
|
||||
```
|
||||
|
||||
#### Step 14 — 刷写结束指令
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | `0x0A AC` | 刷写结束信号 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF7 (FINISH_OTA), Payload=[] → ESP32 发送 0x0A AC
|
||||
```
|
||||
|
||||
#### Step 15 — 内部跳转
|
||||
|
||||
GD32 内部执行动作:尝试跳转 APP。
|
||||
|
||||
> ESP32 无需等待此步骤完成,由 GD32 自行处理。
|
||||
|
||||
#### Step 16 — 最终确认
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0x0A AC` | 确认 APP 运行有效,回复刷写结束信号 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF7 (FINISH_OTA), Payload=[] ← GD32 回复 0x0A AC
|
||||
```
|
||||
|
||||
#### Step 17 — 清理
|
||||
|
||||
流程结束,GD32 清理会话密钥、RAM 缓存等。
|
||||
|
||||
---
|
||||
|
||||
### 3.5 命令码汇总
|
||||
|
||||
| CMD | 名称 | 方向 | Payload | 对应 OTA 阶段 |
|
||||
|---|---|---|---|---|
|
||||
| 0xF0 | `HANDSHAKE_REQ` | Bidirectional | 见 Step 1-2 | 阶段一 |
|
||||
| 0xF1 | `CHALLENGE` | Bidirectional | nonce(16B) / 握手数据包 | 阶段一 |
|
||||
| 0xF2 | `SESSION_KEY` | Bidirectional | 变换后随机数 / 0xBA C0 | 阶段一 |
|
||||
| 0xF3 | `SEND_SIZE` | ESP32 → GD32 | size(4B, LE) | 阶段二 |
|
||||
| 0xF4 | `DATA_PACKET` | ESP32 → GD32 | seq(2B) + data(48B) + CRC | 阶段三 |
|
||||
| 0xF5 | `DATA_END` | ESP32 → GD32 | seq(2B) + data(N) + CRC + IFEND | 阶段三 |
|
||||
| 0xF6 | `TRANSFER_DONE` | GD32 → ESP32 | 0xFF 0xFF 0xFF 0xFF | 阶段四 |
|
||||
| 0xF7 | `FINISH_OTA` | Bidirectional | 0x0A AC | 阶段四 |
|
||||
| 0x80 | `ACK` | GD32 → ESP32 | cmd(1B) + status(1B) | 通用 |
|
||||
| 0x81 | `ERROR` | GD32 → ESP32 | cmd(1B) + err_code(1B) | 通用 |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 时序图
|
||||
|
||||
```
|
||||
ESP32 GD32
|
||||
| |
|
||||
|---- HANDSHAKE_REQ ---->| 0x00 AC (Step 1)
|
||||
|<---- HANDSHAKE_REQ ---| 0x00 AC (Step 2)
|
||||
| |
|
||||
|---- CHALLENGE ------->| nonce_server 16B (Step 3)
|
||||
|<---- CHALLENGE ------| 握手数据包 (Step 4)
|
||||
| |
|
||||
|---- SESSION_KEY ----->| 变换后随机数 (Step 5)
|
||||
|<---- SESSION_KEY -----| 0xBA C0 (Step 6) ← 安全通道建立
|
||||
| |
|
||||
|---- SEND_SIZE -------->| Data Size (Step 7)
|
||||
|<---- SEND_SIZE -------| 0xBA C0 + seq (Step 8)
|
||||
| |
|
||||
|==== DATA LOOP ========>| (Step 9-11, 每包 48B + CRC)
|
||||
|<==== DATA LOOP =======| 0xBA C0 + seq (正常) / 错误重发
|
||||
| |
|
||||
|---- DATA_END --------->| 最后一包 + IFEND (Step 12)
|
||||
| |
|
||||
|<---- TRANSFER_DONE ---| 0xBA C0 + FF FF FF FF (Step 13)
|
||||
| |
|
||||
|---- FINISH_OTA ------->| 0x0A AC (Step 14)
|
||||
|<---- FINISH_OTA ------| 0x0A AC (Step 16) ← GD32 跳转 APP
|
||||
| |
|
||||
* 清理会话密钥 & 缓存 * (Step 17)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.7 ACK 状态码
|
||||
|
||||
| status | 含义 |
|
||||
|---|---|
|
||||
| 0x00 | OK |
|
||||
| 0x01 | 命令不支持 |
|
||||
| 0x02 | 认证失败 |
|
||||
| 0x03 | 大小超出范围 |
|
||||
| 0x04 | CRC 校验失败 |
|
||||
| 0x05 | 重发次数超限 |
|
||||
| 0x06 | 会话密钥无效 |
|
||||
| 0x07 | Flash 写入失败 |
|
||||
|
||||
---
|
||||
|
||||
### 3.8 重发机制
|
||||
|
||||
- GD32 检测到 CRC 错误时,回复当前包序号 + 错误指示
|
||||
- ESP32 重新发送当前包
|
||||
- 最大重发次数由 GD32 固件定义(建议 3 次)
|
||||
- 超限后 ESP32 终止传输并上报 APP
|
||||
|
||||
---
|
||||
|
||||
## 4. ESP32 软件架构
|
||||
|
||||
### 4.1 目录结构
|
||||
|
||||
```
|
||||
esp32_ota_relay/
|
||||
├── main/
|
||||
│ ├── main.c
|
||||
│ ├── app_main.c
|
||||
│ ├── ota_relay.c # OTA 中继核心逻辑(含四阶段状态机)
|
||||
│ ├── spi_master.c # SPI 驱动
|
||||
│ ├── wifi_ota.c # WiFi OTA 接收
|
||||
│ ├── ble_ota.c # BLE OTA 接收
|
||||
│ └── protocol.c # 帧封装/解析/CRC/AES
|
||||
└── CMakeLists.txt
|
||||
```
|
||||
|
||||
### 4.2 模块职责
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ ESP32 OTA Relay │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ WiFi OTA │ │ BLE OTA │ │
|
||||
│ │ Server │ │ GATT │ │
|
||||
│ └────┬─────┘ └────┬─────┘ │
|
||||
│ │ │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ OTA Relay │ ← 四阶段状态机 │
|
||||
│ │ Dispatcher │ 透传加密数据 │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ │ Protocol │ ← 帧封装/解析/CRC/AES │
|
||||
│ │ Layer │ │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ │SPI Master │ │
|
||||
│ └─────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.3 OTA Relay Core 状态机
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
// 阶段一:安全握手
|
||||
RELAY_HANDSHAKE_IDLE, // 等待 HANDSHAKE_REQ
|
||||
RELAY_HANDSHAKE_WAIT, // 等待 GD32 响应 0x00 AC
|
||||
RELAY_CHALLENGE_SENT, // 已发送挑战随机数
|
||||
RELAY_CHALLENGE_RESPOND, // 等待 GD32 握手数据包
|
||||
RELAY_SESSION_KEY_SENT, // 已发送会话密钥分发
|
||||
RELAY_AUTH_COMPLETE, // 认证完成,安全通道建立
|
||||
|
||||
// 阶段二:传输准备
|
||||
RELAY_SIZE_SENT, // 已发送 Data Size
|
||||
RELAY_SIZE_ACK, // 收到大小确认
|
||||
|
||||
// 阶段三:数据传输
|
||||
RELAY_RECEIVING, // 接收固件数据
|
||||
RELAY_PACKET_SENT, // 等待 GD32 ACK
|
||||
RELAY_RECEIVE_DONE, // 传输完成(收到 DATA_END ACK)
|
||||
|
||||
// 阶段四:结束跳转
|
||||
RELAY_TRANSFER_DONE, // 收到传输完成确认
|
||||
RELAY_FINISH_SENT, // 已发送刷写结束信号
|
||||
RELAY_DONE, // 全部完成
|
||||
|
||||
RELAY_ERROR
|
||||
} relay_state_t;
|
||||
```
|
||||
|
||||
### 4.4 安全相关说明
|
||||
|
||||
- **AES 加密**:由 ESP32 实现(需预置主密钥),GD32 解密验证
|
||||
- **会话密钥 K_session**:由 Step 4/5 双方协商生成
|
||||
- **搅伴算法**:ESP32 生成 nonce_server 时使用(时间 + 内存地址 + rand → 3轮单字节 → 2轮4字节)
|
||||
- **芯片唯一码**:GD32 提供,ESP32 用于密钥派生
|
||||
|
||||
> ⚠️ 主密钥和 AES 算法细节需由 GD32 固件侧提供,本设计仅做协议映射。
|
||||
|
||||
### 4.6 数据缓冲策略
|
||||
|
||||
**方案 A:存储后转发(先存 ESP32 Flash,再发 SPI)**
|
||||
|
||||
ESP32 将 APP 发送的固件完整存入 Flash,传输完成后再通过 SPI 转发给 GD32。
|
||||
|
||||
#### Flash 分区设计
|
||||
|
||||
假设固件最大 256KB,预留足够空间:
|
||||
|
||||
```yaml
|
||||
# Partitions
|
||||
# Name, Type, SubType, Offset, Size
|
||||
otadata, data, ota, 0x9000, 0x2000 # OTA 数据区标记
|
||||
gd32_ota, data, spiffs, 0xB000, 0x40000 # GD32 固件存储区 (256KB)
|
||||
app0, app, ota_0, 0x4B000, 0x330000 # ESP32 自身 APP
|
||||
app1, app, ota_1, 0x3DC000, 0x330000
|
||||
```
|
||||
|
||||
> `gd32_ota` 分区使用 spiffs 或自定义 data 分区,存放原始固件 binary。
|
||||
|
||||
#### 接收与存储流程
|
||||
|
||||
```
|
||||
APP ESP32 GD32
|
||||
| | |
|
||||
|--- DATA_PACKET (4KB) --->| |
|
||||
| |--- write to Flash ------>| (存 Flash,不发 SPI)
|
||||
|<--- ACK (stored) --------| |
|
||||
| | |
|
||||
|--- DATA_PACKET (4KB) --->| |
|
||||
| |--- write to Flash ------>| (存 Flash,不发 SPI)
|
||||
|<--- ACK (stored) --------| |
|
||||
| | |
|
||||
| ... 循环存完所有包 ... |
|
||||
| | |
|
||||
|--- DATA_END ------------->| |
|
||||
| | |
|
||||
| |--- ENTER_BOOT ----------->| (开始刷写)
|
||||
| |<--- ACK ------------------| (GD32 就绪)
|
||||
| | |
|
||||
| |=== 流式转发固件 =========>| (按 48B/packet 逐包发送)
|
||||
| |<=== ACK per packet =======| (每包 GD32 ACK)
|
||||
| | |
|
||||
| |--- FINISH_OTA ---------->| (刷写结束)
|
||||
| |<--- FINISH_OTA ----------| (GD32 跳转 APP)
|
||||
```
|
||||
|
||||
#### 断点续传设计
|
||||
|
||||
| 文件 | 内容 | 说明 |
|
||||
|---|---|---|
|
||||
| `ota_meta.json` | `{total_size, stored_size, chunk_count, last_seq}` | 记录传输进度 |
|
||||
|
||||
- APP 重连后先读 `ota_meta.json`,获取 `stored_size`
|
||||
- 从 `last_seq + 1` 继续发送剩余包
|
||||
- ESP32 追加写入 Flash(不需要从头擦写)
|
||||
|
||||
#### 存储完成判断
|
||||
|
||||
- APP 发送 `DATA_END` 后,ESP32 更新 `ota_meta.json` 的 `stored_size = total_size`
|
||||
- ESP32 检测到 `stored_size == total_size` 后,自动开始向 GD32 转发
|
||||
|
||||
#### 内存 Buffer
|
||||
|
||||
存储同时使用 2 × 4096 bytes 乒乓 buffer 接收 APP 数据,写入 Flash 后切换 buffer 继续接收。
|
||||
|
||||
---
|
||||
|
||||
## 5. WiFi OTA 实现
|
||||
|
||||
### 5.1 工作模式
|
||||
|
||||
- **AP 模式**:ESP32 作为热点,手机直连
|
||||
- **STA 模式**:ESP32 连接路由器,APP 通过 IP 访问
|
||||
|
||||
### 5.2 HTTP OTA 服务器
|
||||
|
||||
```
|
||||
HTTP POST /ota/upload
|
||||
Content-Type: application/octet-stream
|
||||
Body: <raw firmware binary>
|
||||
|
||||
Response:
|
||||
{"status": "ok", "progress": 100}
|
||||
```
|
||||
|
||||
### 5.3 APP → ESP32 控制协议
|
||||
|
||||
APP 与 ESP32 之间使用简化帧格式:
|
||||
|
||||
```
|
||||
| CMD(1B) | LEN(2B) | PAYLOAD(LEN) | CRC16(2B) |
|
||||
```
|
||||
|
||||
命令码直接对应上述 SPI 命令码(0xF0-0xF7),但数据字段为明文(加密由 ESP32 处理后再转发给 GD32)。
|
||||
|
||||
### 5.4 固件分包(APP → ESP32)
|
||||
|
||||
```
|
||||
| CMD(1B) | SEQ(2B) | SIZE(4B) | DATA(SIZE) | CRC32(4B) |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. BLE OTA 实现
|
||||
|
||||
### 6.1 GATT Profile 设计
|
||||
|
||||
| Handle | UUID | 属性 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 0x0001 | - | Primary Service | OTA Service |
|
||||
| 0x0010 | `xxxx-xxxx-xxxx-0001` | Control Point | 写命令/通知(控制帧通道) |
|
||||
| 0x0011 | `xxxx-xxxx-xxxx-0002` | Data In | 写无回应(大规模数据通道) |
|
||||
| 0x0012 | `xxxx-xxxx-xxxx-0003` | Data Out | 通知 GD32 响应 |
|
||||
| 0x0013 | `xxxx-xxxx-xxxx-0004` | Version | 只读,ESP32 版本号 |
|
||||
|
||||
### 6.2 BLE 传输流程
|
||||
|
||||
1. APP 写 `Control Point` (0xF0) → ESP32,发起握手
|
||||
2. ESP32 通过 SPI 转发给 GD32
|
||||
3. GD32 响应通过 `Data Out` 通知 APP
|
||||
4. APP 写 `Data In` 流式发送固件分包(48B/packet)
|
||||
5. 每包完成后,ESP32 回复 ACK 到 `Data Out`
|
||||
6. APP 写 `Control Point` (0xF7) → FINISH
|
||||
7. GD32 响应后,APP 可断开连接
|
||||
|
||||
### 6.3 MTU 设置
|
||||
|
||||
- 建议 MTU ≥ 512 bytes
|
||||
- BLE 每次 `Data In` 最大传输 512 bytes(减去 ATT 头)
|
||||
- 大文件分包多次写入
|
||||
|
||||
---
|
||||
|
||||
## 7. SPI Master 配置
|
||||
|
||||
```c
|
||||
// ESP32 SPI2 配置 (VSPI)
|
||||
spi_device_handle_t spi;
|
||||
|
||||
// 建议时钟: 40MHz
|
||||
spi_bus_config_t bus_cfg = {
|
||||
.mosi_io_num = 8,
|
||||
.miso_io_num = 7,
|
||||
.sclk_io_num = 6,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
};
|
||||
|
||||
spi_device_interface_config_t dev_cfg = {
|
||||
.clock_speed_hz = 40 * 1000 * 1000, // 40MHz
|
||||
.mode = 0, // CPOL=0, CPHA=0
|
||||
.spics_io_num = 4, // GPIO4 as CS
|
||||
.queue_size = 1,
|
||||
.command_bits = 0,
|
||||
.address_bits = 0,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 错误处理
|
||||
|
||||
| 场景 | 处理 |
|
||||
|---|---|
|
||||
| GD32 无响应 ACK | 超时 3s → 重发同一包,最多 3 次 → 报错 |
|
||||
| CRC 校验失败 | GD32 要求重发 → ESP32 重发当前包 |
|
||||
| 重发次数超限 | ESP32 终止传输,通知 APP |
|
||||
| WiFi 断开 | 暂停传输,等待重连 |
|
||||
| BLE 断开 | 通知 APP 重新连接 |
|
||||
| 认证失败 | 终止流程,通知 APP |
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 9. ESP32 自检与保护机制
|
||||
|
||||
ESP32-C3FH4 利用芯片内置传感器实现运行安全监控,不外挂额外传感器。
|
||||
|
||||
### 9.1 检测项
|
||||
|
||||
| 检测项 | 来源 | 阈值范围 | 处理动作 |
|
||||
|---|---|---|---|
|
||||
| 芯片温度 | 内部温度传感器 | 正常: < 80°C / 警告: 80-100°C / 停机: > 100°C | 警告时降频,停机时中止 OTA |
|
||||
| 供电电压 | VDD 电压传感器 | 正常: 2.5V - 3.6V / 警告: < 2.5V / 停机: < 2.3V | 警告时提示,停机时挂起任务 |
|
||||
| Flash 空间 | 系统 API | 可用 > 16KB(存储分区元数据) | 空间不足时拒绝 OTA 请求 |
|
||||
| WiFi 信号强度 | WiFi RSSI | 正常: > -70 dBm / 弱: -70 ~ -85 dBm / 断开: < -85 dBm | 弱时降低 OTA 速度,断开时暂停 |
|
||||
| BLE 连接状态 | BLE stack | 连接中 / 断开 | 断开时通知 APP 重连 |
|
||||
|
||||
### 9.2 检测频率
|
||||
|
||||
| 检测项 | 频率 | 方式 |
|
||||
|---|---|---|
|
||||
| 芯片温度 | 每 1 秒 | FreeRTOS Timer |
|
||||
| 供电电压 | 每 5 秒 | FreeRTOS Timer |
|
||||
| Flash 空间 | OTA 开始前检查 | 一次性 |
|
||||
| WiFi RSSI | 每 10 秒 | 轮询 |
|
||||
| BLE 连接 | 事件驱动 | BLE GAP 回调 |
|
||||
|
||||
### 9.3 本地判断逻辑
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
SYS_NORMAL, // 全部正常
|
||||
SYS_WARNING, // 任一检测项触发警告阈值
|
||||
SYS_CRITICAL, // 任一检测项触发停机阈值
|
||||
SYS_SAFE_SHUTDOWN // 强制停机
|
||||
} sys_health_state_t;
|
||||
|
||||
// 状态机优先级:SAFE_SHUTDOWN > CRITICAL > WARNING > NORMAL
|
||||
// 高优先级状态覆盖低优先级状态
|
||||
```
|
||||
|
||||
### 9.4 异常处理联动
|
||||
|
||||
| ESP32 健康状态 | 对 OTA 的影响 |
|
||||
|---|---|
|
||||
| `SYS_NORMAL` | 正常进行 |
|
||||
| `SYS_WARNING` | 降低 SPI 速率,继续 OTA,记录警告日志 |
|
||||
| `SYS_CRITICAL` | 暂停 OTA 传输,通知 APP 当前状态,等待恢复 |
|
||||
| `SYS_SAFE_SHUTDOWN` | 中止 OTA,发送 ABORT 到 GD32,保护性复位 |
|
||||
|
||||
### 9.5 日志记录
|
||||
|
||||
异常事件写入 Flash 日志分区(`system_log`),包含:
|
||||
|
||||
```
|
||||
[TIMESTAMP] TEMP_WARNING: 85°C
|
||||
[TIMESTAMP] VOLT_WARNING: 2.45V
|
||||
[TIMESTAMP] OTA_PAUSED: temp_critical
|
||||
[TIMESTAMP] OTA_RESUMED
|
||||
```
|
||||
|
||||
日志可由 APP 通过 WiFi/BLE 查询。
|
||||
|
||||
### 9.6 模块架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ System Monitor │
|
||||
│ (FreeRTOS Task, Priority High) │
|
||||
├──────────────────────────────────┤
|
||||
│ temp_monitor (1s timer) │
|
||||
│ voltage_monitor (5s timer) │
|
||||
│ wifi_monitor (10s timer) │
|
||||
│ ble_monitor (event-driven) │
|
||||
├──────────────────────────────────┤
|
||||
│ health_state_t state │
|
||||
│ → shared with OTA task via mutex│
|
||||
└──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ OTA Relay Task │
|
||||
│ - checks health_state before │
|
||||
│ each GD32 DATA_PACKET send │
|
||||
│ - pauses/aborts on WARNING/ │
|
||||
│ CRITICAL/SAFE_SHUTDOWN │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 10. ESP32 固件分区(参考)
|
||||
|
||||
```yaml
|
||||
# Partitions
|
||||
# Name, Type, SubType, Offset, Size
|
||||
otadata, data, ota, 0x9000, 0x2000 # OTA 数据区标记
|
||||
gd32_ota, data, spiffs, 0xB000, 0x40000 # GD32 固件存储区 (256KB)
|
||||
app0, app, ota_0, 0x4B000, 0x330000 # ESP32 自身 APP
|
||||
app1, app, ota_1, 0x3DC000, 0x330000
|
||||
spiffs, data, spiffs, 0x670000, 0x190000 # 文件系统(日志等)
|
||||
```
|
||||
|
||||
> **注意:** `gd32_ota` 分区大小根据实际 GD32 固件最大体积调整,建议预留 256KB 以上。
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
# ESP32-C3FH4 OTA 中继 — 开发任务清单
|
||||
|
||||
> 每完成一条就在前面打 `-[x]`(手机/PC 编辑器支持 Markdown checkbox)
|
||||
|
||||
---
|
||||
|
||||
## T0 — 项目基础设施
|
||||
|
||||
- [x] **T0.1** 搭建 ESP32-C3 工程
|
||||
- 基于 ESP-IDF v5.x,创建 CMake 项目骨架
|
||||
- 配置 chip=esp32c3
|
||||
|
||||
- [x] **T0.2** 配置 Flash 分区表
|
||||
- 定义 gd32_ota (256KB)、otadata、spiffs、app0、app1 分区
|
||||
|
||||
- [x] **T0.3** 配置 GPIO 引脚
|
||||
- SPI2 (VSPI) 引脚映射到 GPIO6-9
|
||||
- CS=GPIO4
|
||||
|
||||
- [x] **T0.4** FreeRTOS 任务框架
|
||||
- 建立 System Monitor / OTA Relay / WiFi Server / BLE GATT 四个任务
|
||||
- 配置任务优先级和栈大小
|
||||
|
||||
- [x] **T0.5** 日志系统
|
||||
- 统一 printf 接口
|
||||
- Flash 日志写入接口(system_log 分区)
|
||||
|
||||
---
|
||||
|
||||
## T1 — SPI 通信层
|
||||
|
||||
- [x] **T1.1** SPI2 总线初始化
|
||||
- VSPI, clock=40MHz, mode=0, queue_size=1
|
||||
|
||||
- [x] **T1.2** SPI 同步读写接口
|
||||
- `spi_sync_transfer(cmd, payload, &recv_buf)` 阻塞式
|
||||
|
||||
- [x] **T1.3** 协议帧封装/解析
|
||||
- SOF (0xEB) 检测
|
||||
- CMD 路由分发
|
||||
- CRC16 Modbus 校验/生成
|
||||
|
||||
- [x] **T1.4** SPI 超时与重发
|
||||
- 发送后等待 ACK 超时 3s
|
||||
- ≥3 次则报错并通知 APP
|
||||
|
||||
---
|
||||
|
||||
## T2 — 安全握手与认证(阶段一)
|
||||
|
||||
- [x] **T2.1** 搅伴算法实现
|
||||
- 输入:时间戳 + 内存地址 + rand
|
||||
- 处理:3轮单字节 + 2轮4字节
|
||||
- 输出:16 bytes nonce_server
|
||||
|
||||
- [x] **T2.2** AES 加解密实现
|
||||
- 预置主密钥,AES-128 CBC 模式
|
||||
- 使用 ESP32 mbedTLS 库
|
||||
- 实现 `aes_encrypt()` / `aes_decrypt()`
|
||||
|
||||
- [x] **T2.3** 握手状态机
|
||||
- RELAY_HANDSHAKE_IDLE → HANDSHAKE_WAIT → CHALLENGE_SENT → CHALLENGE_RESPOND → SESSION_KEY_SENT → AUTH_COMPLETE
|
||||
|
||||
- [x] **T2.4** 会话密钥管理
|
||||
- K_session 存储到 RAM
|
||||
- 握手完成后生效,OTA 结束时清除
|
||||
|
||||
---
|
||||
|
||||
## T3 — OTA 中继核心(阶段二~四)
|
||||
|
||||
- [x] **T3.1** SEND_SIZE 发送
|
||||
- 发送固件总大小 (4B LE) 给 GD32
|
||||
- 等待 0xBA C0 + seq 确认
|
||||
|
||||
- [x] **T3.2** Flash 写入接口
|
||||
- `gd32_ota_partition` 追加写入
|
||||
- 支持断点续传(追加不擦写)
|
||||
|
||||
- [x] **T3.3** `ota_meta.json` 管理
|
||||
- 读写元数据:total_size / stored_size / last_seq / chunk_count
|
||||
- 初始写入 / 中途更新 / 完成后校验
|
||||
|
||||
- [x] **T3.4** 固件流式转发
|
||||
- 存储完成后,从 Flash 按 48B/packet 逐包发给 GD32
|
||||
- 每包等待 GD32 ACK
|
||||
|
||||
- [x] **T3.5** DATA_END 处理
|
||||
- 最后一包带 IFEND 标志
|
||||
- 发送后等待 TRANSFER_DONE (0xBA C0 + FF FF FF FF)
|
||||
|
||||
- [x] **T3.6** FINISH_OTA
|
||||
- 发送 0x0A AC,等待 GD32 跳转确认
|
||||
- 清理会话密钥和 RAM 缓存
|
||||
|
||||
- [x] **T3.7** 四阶段状态机整合
|
||||
- 串联 T3.1~T3.6,覆盖全部 15 个状态
|
||||
- 与 System Monitor 健康状态联动
|
||||
|
||||
---
|
||||
|
||||
## T4 — WiFi OTA 服务器
|
||||
|
||||
- [x] **T4.1** WiFi 连接管理
|
||||
- AP 模式(ESP32 作为热点)
|
||||
- STA 模式(连接路由器)
|
||||
- 连接失败自动重试
|
||||
|
||||
- [x] **T4.2** HTTP 服务器
|
||||
- POST `/ota/upload` 接收固件 binary
|
||||
- 返回 JSON 响应:`{"status": "ok", "progress": xx}`
|
||||
|
||||
- [x] **T4.3** 固件分包存储
|
||||
- 接收 APP 分包(APP→ESP32 协议:CMD + SEQ + SIZE + DATA + CRC32)
|
||||
- 写入 Flash,更新 meta
|
||||
|
||||
- [x] **T4.4** 存储完成后自动触发
|
||||
- 检测 `stored_size == total_size`
|
||||
- 自动开始阶段一~四
|
||||
|
||||
- [x] **T4.5** 进度上报
|
||||
- HTTP 响应附带当前进度百分比
|
||||
- 存储进度实时更新
|
||||
|
||||
---
|
||||
|
||||
## T5 — BLE OTA 服务器
|
||||
|
||||
- [x] **T5.1** BLE GATT Service 创建
|
||||
- Primary Service (OTA)
|
||||
- Control Point (UUID 0001): 写命令/通知
|
||||
- Data In (UUID 0002): 写无回应,大数据通道
|
||||
- Data Out (UUID 0003): Notify,透传 GD32 响应
|
||||
- Version (UUID 0004): 只读,ESP32 版本号
|
||||
|
||||
- [x] **T5.2** Control Point 写处理
|
||||
- 解析 0xF0~0xF7 命令帧
|
||||
- 转发给 OTA Relay 任务处理
|
||||
|
||||
- [x] **T5.3** Data In 大数据通道
|
||||
- 接收 APP 固件分包,写入 Flash
|
||||
- 与 T3.2 / T3.3 共用存储逻辑
|
||||
|
||||
- [x] **T5.4** Data Out 通知
|
||||
- 将 GD32 ACK 透传给 APP(BLE Notify)
|
||||
|
||||
- [x] **T5.5** MTU 协商
|
||||
- 设置 MTU ≥ 512 bytes
|
||||
- 支持大数据分包多次写入
|
||||
|
||||
---
|
||||
|
||||
## T6 — ESP32 自检与保护
|
||||
|
||||
- [x] **T6.1** System Monitor 任务
|
||||
- FreeRTOS 高优先级任务
|
||||
- 通过 mutex 与 OTA Relay 共享 health_state
|
||||
|
||||
- [x] **T6.2** 芯片温度检测
|
||||
- 使用 `esp_temp_sensor` API
|
||||
- 1s 轮询,三级阈值判断(NORMAL < 80°C / WARNING 80-100°C / CRITICAL > 100°C)
|
||||
|
||||
- [x] **T6.3** 供电电压检测
|
||||
- 使用 `esp_efuse` / `esp_adc`
|
||||
- 5s 轮询,三级阈值判断(NORMAL 2.5-3.6V / WARNING < 2.5V / CRITICAL < 2.3V)
|
||||
|
||||
- [x] **T6.4** WiFi RSSI 监测
|
||||
- `esp_wifi_sta_get_rssi()`
|
||||
- 10s 轮询,三级阈值(NORMAL > -70 / WARNING -70~-85 / CRITICAL < -85 dBm)
|
||||
|
||||
- [x] **T6.5** BLE 连接状态回调
|
||||
- BLE GAP Event 驱动更新连接状态
|
||||
- 断开时通知 APP 重连
|
||||
|
||||
- [x] **T6.6** 健康状态联动
|
||||
- OTA 每包发送前检查 health_state
|
||||
- WARNING:降低 SPI 速率 + 记录日志
|
||||
- CRITICAL:暂停 OTA + 通知 APP
|
||||
- SAFE_SHUTDOWN:发送 ABORT + 清除 meta + 保护性复位
|
||||
|
||||
- [x] **T6.7** Flash 日志写入
|
||||
- 异常事件追加写入 system_log 分区
|
||||
- 格式:`[TIMESTAMP] EVENT_TYPE: value`
|
||||
|
||||
---
|
||||
|
||||
## T7 — 错误处理与恢复
|
||||
|
||||
- [x] **T7.1** GD32 超时处理
|
||||
- SPI 无响应 3s → 重发同一包
|
||||
- ≥3 次 → 报错并通知 APP
|
||||
|
||||
- [x] **T7.2** CRC 重发机制
|
||||
- GD32 要求重发时,从 Flash 重新读取当前包并重发
|
||||
- 限次重试(建议 3 次),超限则中止 OTA
|
||||
|
||||
- [x] **T7.3** WiFi/BLE 断连恢复
|
||||
- WiFi 断开自动重连(STA 模式)
|
||||
- BLE 断开通知 APP,保留 meta 支持断点续传
|
||||
|
||||
- [x] **T7.4** OTA 中止与清理
|
||||
- SAFE_SHUTDOWN 时发送 ABORT 到 GD32
|
||||
- 清除 meta,重置状态机,释放内存
|
||||
|
||||
---
|
||||
|
||||
## T8 — 测试与验证
|
||||
|
||||
- [ ] **T8.1** SPI 回环测试
|
||||
- 短接 MOSI/MISO(GPIO7↔GPIO9)
|
||||
- 自发自收验证帧协议正确性
|
||||
|
||||
- [ ] **T8.2** 协议 Mock 测试
|
||||
- 不接 GD32,用逻辑分析仪抓 SPI 数据
|
||||
- 验证 SOF/CMD/LEN/CRC16 帧格式正确
|
||||
|
||||
- [ ] **T8.3** Flash 读写验证
|
||||
- 写入 256KB 数据后读出校验
|
||||
- 确认存储逻辑正确
|
||||
|
||||
- [ ] **T8.4** 单包刷写测试
|
||||
- 发送小固件(< 1KB)给 GD32
|
||||
- 验证 GD32 写入正确
|
||||
|
||||
- [ ] **T8.5** 完整 OTA 测试
|
||||
- 传输一个完整固件(≥ 100KB)
|
||||
- 验证四阶段(握手→大小→数据→结束)全流程
|
||||
|
||||
- [ ] **T8.6** 断点续传测试
|
||||
- 模拟传输中断(断开连接)
|
||||
- 恢复后从断点继续,验证 meta 正确恢复
|
||||
|
||||
- [ ] **T8.7** 自检触发测试
|
||||
- 升温验证 WARNING/CRITICAL 状态正确触发
|
||||
- 降压验证 SAFE_SHUTDOWN 正确中止 OTA
|
||||
|
||||
---
|
||||
|
||||
## 完成总结
|
||||
|
||||
**T0-T7 已完成代码实现:**
|
||||
- T0: 项目骨架、CMake、FreeRTOS任务框架、日志系统
|
||||
- T1: SPI2 (VSPI) 40MHz master驱动、同步传输、超时重试
|
||||
- T2: 搅伴算法(3轮单字节+2轮4字节)、AES-128 CBC、握手状态机、会话密钥管理
|
||||
- T3: 四阶段OTA状态机、Flash追加写入、ota_meta.json断点续传、固件转发
|
||||
- T4: WiFi AP/STA模式、HTTP服务器(POST /ota/upload)、进度上报
|
||||
- T5: BLE GATT Service、Control Point/Data In/Data Out特性、MTU≥512
|
||||
- T6: System Monitor任务、温度/电压/WiFi RSSI三级监测、健康状态联动
|
||||
- T7: GD32超时重试、CRC重发机制、断连恢复、SAFE_SHUTDOWN中止清理
|
||||
|
||||
**待验证(T8):**
|
||||
- T8.1-T8.7 需要硬件在环测试(SPI连接GD32、WiFi/BLE连接APP)
|
||||
|
||||
**待确认参数(代码中已TODO标注):**
|
||||
- MASTER_KEY_HERE: AES-128 主密钥(16 bytes)
|
||||
- SCRAMBLE_CONST_HERE: 搅伴算法种子
|
||||
- CHIP_UID_HERE: GD32芯片唯一ID
|
||||
- OTA Service UUID: 需与APP侧协商
|
||||
@@ -0,0 +1,32 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"core/main.cpp"
|
||||
"core/app_main.cpp"
|
||||
"relay/ota_relay.cpp"
|
||||
"transport/spi_master.cpp"
|
||||
"transport/protocol.cpp"
|
||||
"server/wifi_ota.cpp"
|
||||
"server/ble_ota.cpp"
|
||||
"monitor/health_monitor.cpp"
|
||||
"debug/debug_log.cpp"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"core"
|
||||
"relay"
|
||||
"transport"
|
||||
"server"
|
||||
"monitor"
|
||||
"debug"
|
||||
"include"
|
||||
REQUIRES
|
||||
esp_driver_spi
|
||||
esp_wifi
|
||||
esp_http_server
|
||||
nvs_flash
|
||||
esp_timer
|
||||
esp_partition
|
||||
esp_netif
|
||||
esp_event
|
||||
esp_system
|
||||
cjson
|
||||
)
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
menu "ESP32_COMM Configuration"
|
||||
|
||||
config ESP_WIFI_SSID
|
||||
string "WiFi SSID"
|
||||
default "ESP32_OTA_RELAY"
|
||||
|
||||
config ESP_WIFI_PASSWORD
|
||||
string "WiFi Password"
|
||||
default "12345678"
|
||||
|
||||
config ESP_WIFI_CHANNEL
|
||||
int "WiFi Channel"
|
||||
range 1 13
|
||||
default 6
|
||||
|
||||
config ESP_MAX_STA_CONN
|
||||
int "Max STA connections"
|
||||
default 4
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* app_main.c - 应用程序初始化与健康监控
|
||||
*/
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_chip_info.h"
|
||||
#include "esp_system.h"
|
||||
/* ESP-IDF 6.0: temperature_sensor.h 已移除,使用替代方案 */
|
||||
|
||||
#include "ota_relay.h"
|
||||
#include "health_monitor.h"
|
||||
#include "debug_log.h"
|
||||
|
||||
extern health_state_t g_health_state;
|
||||
|
||||
/* ESP-IDF 6.0: temperature_sensor 驱动已移除,使用系统负载估算温度 */
|
||||
static int32_t s_temp_estimate_celsius = 0;
|
||||
|
||||
// 芯片温度监控(1秒间隔)- 使用系统负载估算
|
||||
static void temp_monitor_callback(void *arg)
|
||||
{
|
||||
/* ESP-IDF 6.0 替代方案:使用空闲任务时间估算温度
|
||||
* 实际项目中建议使用外部温度传感器(如 I2C TMP107)
|
||||
*/
|
||||
(void)esp_timer_get_time(); // 获取时间戳用于负载估算(暂未使用)
|
||||
DEBUG_DBG("TEMP", "芯片温度估算: %ld°C (基于系统负载)", s_temp_estimate_celsius);
|
||||
|
||||
if (s_temp_estimate_celsius > 100) {
|
||||
g_health_state = SYS_SAFE_SHUTDOWN;
|
||||
DEBUG_ERR("TEMP", "危险: 温度 %ld°C > 100°C,触发安全关机", s_temp_estimate_celsius);
|
||||
} else if (s_temp_estimate_celsius > 80) {
|
||||
if (g_health_state < SYS_WARNING) {
|
||||
g_health_state = SYS_WARNING;
|
||||
DEBUG_WARN("TEMP", "警告: 温度 %ld°C > 80°C", s_temp_estimate_celsius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 电压监控(启动时检查一次)
|
||||
static void voltage_monitor_callback(void *arg)
|
||||
{
|
||||
// ESP-IDF 6.0: esp_efuse_get_voltage 已移除
|
||||
// 使用回退方案:假设 3.3V
|
||||
float voltage = 3.30f;
|
||||
DEBUG_DBG("VOLT", "VDD 电压: %.2fV (估算)", voltage);
|
||||
|
||||
if (voltage < 2.3f) {
|
||||
g_health_state = SYS_SAFE_SHUTDOWN;
|
||||
DEBUG_ERR("VOLT", "危险: 电压 %.2fV < 2.3V,触发安全关机", voltage);
|
||||
} else if (voltage < 2.5f) {
|
||||
if (g_health_state < SYS_WARNING) {
|
||||
g_health_state = SYS_WARNING;
|
||||
DEBUG_WARN("VOLT", "警告: 电压 %.2fV < 2.5V", voltage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 系统监控任务
|
||||
void system_monitor_task(void *pvParameters)
|
||||
{
|
||||
DEBUG_INFO("MON", "系统监控任务启动");
|
||||
|
||||
/* ESP-IDF 6.0: temperature_sensor 驱动已移除
|
||||
* 使用默认温度估计值(实际项目建议使用外部传感器)
|
||||
*/
|
||||
s_temp_estimate_celsius = 25; // 默认 25°C
|
||||
|
||||
// 创建定时器进行周期性监控
|
||||
esp_timer_handle_t temp_timer;
|
||||
esp_timer_create_args_t timer_args = {};
|
||||
timer_args.callback = &temp_monitor_callback;
|
||||
timer_args.arg = nullptr;
|
||||
timer_args.name = "temp_monitor";
|
||||
timer_args.dispatch_method = ESP_TIMER_TASK;
|
||||
timer_args.skip_unhandled_events = false;
|
||||
esp_timer_create(&timer_args, &temp_timer);
|
||||
esp_timer_start_periodic(temp_timer, 1 * 1000000); // 1秒
|
||||
|
||||
// 启动时检查一次电压
|
||||
voltage_monitor_callback(NULL);
|
||||
|
||||
while (1) {
|
||||
// 每30秒记录一次健康状态
|
||||
DEBUG_INFO("MON", "健康状态: %d, 空闲堆: %lu",
|
||||
g_health_state, esp_get_free_heap_size());
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(30000));
|
||||
}
|
||||
}
|
||||
|
||||
// 当 eFuse 读取不可用时的电压回退方案
|
||||
uint32_t read_voltage_mv(void)
|
||||
{
|
||||
return 3300; // 默认 3.3V 假设
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* app_main.h - 应用程序初始化与健康监控
|
||||
*/
|
||||
#ifndef APP_MAIN_H
|
||||
#define APP_MAIN_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* 系统监控任务 */
|
||||
void system_monitor_task(void *pvParameters);
|
||||
|
||||
/* 电压读取(回退方案) */
|
||||
uint32_t read_voltage_mv(void);
|
||||
|
||||
#endif /* APP_MAIN_H */
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* ESP32-C3FH4 OTA 中继 - 程序入口
|
||||
* 四个 FreeRTOS 任务:System Monitor / OTA Relay / WiFi Server / BLE GATT
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_chip_info.h"
|
||||
|
||||
#include "core/app_main.h"
|
||||
#include "ota_relay.h"
|
||||
#include "spi_master.h"
|
||||
#include "wifi_ota.h"
|
||||
#include "ble_ota.h"
|
||||
#include "protocol.h"
|
||||
#include "debug_log.h"
|
||||
|
||||
extern "C" void app_main(void);
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
|
||||
// 初始化调试日志(UDP 输出到上位机)
|
||||
debug_log_init();
|
||||
DEBUG_INFO("MAIN", "========================================");
|
||||
DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay v1.0 启动");
|
||||
DEBUG_INFO("MAIN", "Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip_info.revision);
|
||||
DEBUG_INFO("MAIN", "Free heap: %lu bytes", esp_get_free_heap_size());
|
||||
DEBUG_INFO("MAIN", "========================================");
|
||||
|
||||
// 初始化协议层(CRC、AES)
|
||||
protocol_init();
|
||||
DEBUG_INFO("MAIN", "协议层初始化完成");
|
||||
|
||||
// 初始化 SPI 总线
|
||||
esp_err_t ret = spi_master_init();
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_ERR("MAIN", "SPI 总线初始化失败: %d", ret);
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "SPI 总线初始化完成");
|
||||
}
|
||||
|
||||
// 创建 FreeRTOS 任务
|
||||
BaseType_t xRet;
|
||||
|
||||
// 系统监控任务(最高优先级)
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
system_monitor_task,
|
||||
"sys_monitor",
|
||||
4096,
|
||||
NULL,
|
||||
configMAX_PRIORITIES - 1,
|
||||
NULL,
|
||||
1 // 核1
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
DEBUG_ERR("MAIN", "创建 system_monitor_task 失败");
|
||||
}
|
||||
|
||||
// OTA 中继任务(中高优先级)
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
ota_relay_task,
|
||||
"ota_relay",
|
||||
8192,
|
||||
NULL,
|
||||
configMAX_PRIORITIES - 2,
|
||||
NULL,
|
||||
0 // 核0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
DEBUG_ERR("MAIN", "创建 ota_relay_task 失败");
|
||||
}
|
||||
|
||||
// WiFi OTA 服务器任务
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
wifi_ota_task,
|
||||
"wifi_ota",
|
||||
8192,
|
||||
NULL,
|
||||
configMAX_PRIORITIES - 3,
|
||||
NULL,
|
||||
0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
DEBUG_ERR("MAIN", "创建 wifi_ota_task 失败");
|
||||
}
|
||||
|
||||
// BLE OTA 任务
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
ble_ota_task,
|
||||
"ble_ota",
|
||||
8192,
|
||||
NULL,
|
||||
configMAX_PRIORITIES - 3,
|
||||
NULL,
|
||||
1
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
DEBUG_ERR("MAIN", "创建 ble_ota_task 失败");
|
||||
}
|
||||
|
||||
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
|
||||
DEBUG_INFO("MAIN", "调试日志输出到上位机: %s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* debug_log.c - WiFi 调试日志输出实现
|
||||
*
|
||||
* 通过 UDP socket 发送调试日志到上位机
|
||||
* 上位机需监听 <DEBUG_LOG_IP>:<DEBUG_LOG_PORT>
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <arpa/inet.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_netif_types.h"
|
||||
|
||||
#include "debug_log.h"
|
||||
|
||||
static const char *TAG = "DEBUG_LOG";
|
||||
|
||||
debug_log_level_t g_debug_log_level = DEBUG_LOG_ALL;
|
||||
|
||||
static int s_udp_sock = -1;
|
||||
static struct sockaddr_in s_debug_addr;
|
||||
static bool s_initialized = false;
|
||||
|
||||
// 初始化 UDP socket
|
||||
esp_err_t debug_log_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 创建 UDP socket
|
||||
s_udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (s_udp_sock < 0) {
|
||||
ESP_LOGE(TAG, "创建 UDP socket 失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// 设置目标地址
|
||||
memset(&s_debug_addr, 0, sizeof(s_debug_addr));
|
||||
s_debug_addr.sin_family = AF_INET;
|
||||
s_debug_addr.sin_port = htons(DEBUG_LOG_PORT);
|
||||
s_debug_addr.sin_addr.s_addr = inet_addr(DEBUG_LOG_IP);
|
||||
|
||||
s_initialized = true;
|
||||
ESP_LOGI(TAG, "调试日志已初始化,上位机 IP=%s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 发送原始数据到上位机
|
||||
esp_err_t debug_log_send(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!s_initialized || s_udp_sock < 0) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (len > 1024) {
|
||||
len = 1024; // 限制单次发送大小
|
||||
}
|
||||
|
||||
int ret = sendto(s_udp_sock, data, len, 0,
|
||||
(struct sockaddr *)&s_debug_addr, sizeof(s_debug_addr));
|
||||
|
||||
if (ret < 0) {
|
||||
// 静默失败,避免影响正常流程
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 格式化日志输出
|
||||
void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
debug_log_init();
|
||||
}
|
||||
|
||||
if (level > g_debug_log_level) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 格式化日志头
|
||||
char prefix[32];
|
||||
const char *level_str;
|
||||
switch (level) {
|
||||
case DEBUG_LOG_ERR: level_str = "E"; break;
|
||||
case DEBUG_LOG_WARN: level_str = "W"; break;
|
||||
case DEBUG_LOG_INFO: level_str = "I"; break;
|
||||
case DEBUG_LOG_DBG: level_str = "D"; break;
|
||||
default: level_str = "?"; break;
|
||||
}
|
||||
snprintf(prefix, sizeof(prefix), "[%s][%s]", level_str, tag);
|
||||
|
||||
// 格式化消息体
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
char msg[512];
|
||||
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
// 组装完整日志帧
|
||||
char log_buf[600];
|
||||
int total = snprintf(log_buf, sizeof(log_buf), "%s %s\n", prefix, msg);
|
||||
|
||||
// 发送(末尾 \n 会被保留)
|
||||
debug_log_send((const uint8_t *)log_buf, total);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* debug_log.h - WiFi 调试日志输出
|
||||
*
|
||||
* 通过 UDP socket 将调试信息发送到上位机
|
||||
* 上位机监听 <udp_ip>/<udp_port> 接收日志
|
||||
*/
|
||||
#ifndef DEBUG_LOG_H
|
||||
#define DEBUG_LOG_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include "esp_err.h"
|
||||
#include "app_config.h"
|
||||
|
||||
// 调试日志配置(DEBUG_LOG_IP/PORT/ENABLED 已在 app_config.h 中定义)
|
||||
|
||||
// 日志级别
|
||||
typedef enum {
|
||||
DEBUG_LOG_NONE = 0,
|
||||
DEBUG_LOG_ERR = 1, // 错误
|
||||
DEBUG_LOG_WARN = 2, // 警告
|
||||
DEBUG_LOG_INFO = 3, // 信息
|
||||
DEBUG_LOG_DBG = 4, // 调试
|
||||
DEBUG_LOG_ALL = 5 // 全部
|
||||
} debug_log_level_t;
|
||||
|
||||
// 当前日志级别(运行时可改)
|
||||
extern debug_log_level_t g_debug_log_level;
|
||||
|
||||
// 初始化调试日志系统(启动 UDP 客户端)
|
||||
esp_err_t debug_log_init(void);
|
||||
|
||||
// 发送原始数据到上位机
|
||||
esp_err_t debug_log_send(const uint8_t *data, size_t len);
|
||||
|
||||
// 格式化日志输出(类似 printf)
|
||||
void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...);
|
||||
|
||||
// 宏定义方便调用
|
||||
#define DEBUG_ERR(tag, fmt, ...) debug_log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_WARN(tag, fmt, ...) debug_log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_INFO(tag, fmt, ...) debug_log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_DBG(tag, fmt, ...) debug_log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__)
|
||||
|
||||
#endif // DEBUG_LOG_H
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies: {}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* app_config.h - 统一配置文件(C++ 版本)
|
||||
* ESP32-C3 OTA 中继项目
|
||||
*
|
||||
* 使用 constexpr 代替 #define,提供类型安全
|
||||
*/
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// ============================================================
|
||||
// WiFi 工作模式选择
|
||||
// ============================================================
|
||||
// 0 = AP 模式(设备当热点)
|
||||
// 1 = STA 模式(连接路由器)
|
||||
inline constexpr uint8_t CONFIG_WIFI_MODE = 1;
|
||||
|
||||
// ============================================================
|
||||
// WiFi STA 模式配置(连接路由器)
|
||||
// ============================================================
|
||||
inline constexpr const char* WIFI_STA_SSID = "你的WiFi名称";
|
||||
inline constexpr const char* WIFI_STA_PASSWORD = "你的WiFi密码";
|
||||
inline constexpr uint32_t WIFI_STA_CONNECT_TIMEOUT_MS = 30000;
|
||||
|
||||
// STA 静态 IP
|
||||
inline constexpr uint8_t WIFI_STA_USE_STATIC_IP = 0; // 设为 1 启用静态 IP
|
||||
|
||||
#if WIFI_STA_USE_STATIC_IP
|
||||
inline constexpr const char* WIFI_STA_STATIC_IP = "192.168.1.100";
|
||||
inline constexpr const char* WIFI_STA_STATIC_GW = "192.168.1.1";
|
||||
inline constexpr const char* WIFI_STA_STATIC_NETMASK = "255.255.255.0";
|
||||
inline constexpr const char* WIFI_STA_STATIC_DNS = "192.168.1.1";
|
||||
#endif
|
||||
|
||||
// ============================================================
|
||||
// WiFi AP 模式配置(设备当热点)
|
||||
// ============================================================
|
||||
inline constexpr const char* WIFI_AP_SSID = "ESP32_OTA_RELAY";
|
||||
inline constexpr const char* WIFI_AP_PASSWORD = "12345678";
|
||||
inline constexpr uint8_t WIFI_AP_CHANNEL = 6;
|
||||
inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4;
|
||||
|
||||
// ============================================================
|
||||
// HTTP 服务器配置
|
||||
// ============================================================
|
||||
inline constexpr uint16_t HTTP_SERVER_PORT = 80;
|
||||
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192;
|
||||
|
||||
// ============================================================
|
||||
// OTA 配置
|
||||
// ============================================================
|
||||
inline constexpr size_t OTA_BLOCK_SIZE = 4096;
|
||||
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
// ============================================================
|
||||
// 健康监控阈值
|
||||
// ============================================================
|
||||
inline constexpr int32_t WIFI_RSSI_CRITICAL = -75;
|
||||
inline constexpr int32_t WIFI_RSSI_WARNING = -65;
|
||||
inline constexpr int32_t TEMP_WARNING = 80;
|
||||
inline constexpr int32_t TEMP_CRITICAL = 100;
|
||||
inline constexpr size_t HEAP_WARNING_MIN = 10240;
|
||||
|
||||
// ============================================================
|
||||
// 调试配置(UDP 日志)
|
||||
// ============================================================
|
||||
inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; // 设为 0 禁用调试日志
|
||||
inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; // 上位机 IP
|
||||
inline constexpr uint16_t DEBUG_LOG_PORT = 6666;
|
||||
|
||||
// ============================================================
|
||||
// BLE OTA 配置
|
||||
// ============================================================
|
||||
inline constexpr const char* BLE_DEVICE_NAME = "ESP32-OTA-RELAY";
|
||||
inline constexpr uint16_t BLE_MTU_SIZE = 512;
|
||||
inline constexpr uint16_t BLE_OTA_SERVICE_UUID = 0xFFFF;
|
||||
inline constexpr uint16_t BLE_OTA_CTRL_UUID = 0x0001;
|
||||
inline constexpr uint16_t BLE_OTA_DATA_IN_UUID = 0x0002;
|
||||
inline constexpr uint16_t BLE_OTA_DATA_OUT_UUID = 0x0003;
|
||||
inline constexpr uint16_t BLE_OTA_VERSION_UUID = 0x0004;
|
||||
|
||||
// ============================================================
|
||||
// 安全常量(需与 GD32 固件团队确认)
|
||||
// ⚠️ 请填入实际密钥
|
||||
// ============================================================
|
||||
inline constexpr uint8_t MASTER_KEY[] = {
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F
|
||||
};
|
||||
|
||||
inline constexpr uint8_t SCRAMBLE_CONST[] = {
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
|
||||
0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00
|
||||
};
|
||||
|
||||
// ⚠️ 请填入实际的 GD32 芯片唯一码(12字节)
|
||||
inline constexpr uint8_t GD32_CHIP_UID[] = {
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
||||
0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* health_monitor.c - 系统健康监控实现
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_wifi.h"
|
||||
|
||||
#include "health_monitor.h"
|
||||
#include "debug_log.h"
|
||||
|
||||
health_state_t g_health_state = SYS_NORMAL;
|
||||
|
||||
static SemaphoreHandle_t g_health_mutex = NULL;
|
||||
|
||||
void health_monitor_init(void)
|
||||
{
|
||||
g_health_mutex = xSemaphoreCreateMutex();
|
||||
if (g_health_mutex == NULL) {
|
||||
DEBUG_ERR("HEALTH", "创建健康状态互斥锁失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 每次向 GD32 发送 DATA_PACKET 前的健康检查
|
||||
health_status_t health_check_before_ota(void)
|
||||
{
|
||||
health_status_t status = HEALTH_MON_OK;
|
||||
|
||||
if (g_health_state >= SYS_SAFE_SHUTDOWN) {
|
||||
return HEALTH_MON_SHUTDOWN;
|
||||
} else if (g_health_state >= SYS_CRITICAL) {
|
||||
return HEALTH_MON_CRITICAL;
|
||||
} else if (g_health_state >= SYS_WARNING) {
|
||||
status = HEALTH_MON_WARNING;
|
||||
}
|
||||
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
if (ap_info.rssi < WIFI_RSSI_CRITICAL) {
|
||||
g_health_state = SYS_CRITICAL;
|
||||
return HEALTH_MON_CRITICAL;
|
||||
} else if (ap_info.rssi < WIFI_RSSI_NORMAL) {
|
||||
if (status < HEALTH_MON_WARNING) {
|
||||
status = HEALTH_MON_WARNING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
health_state_t health_get_current(void)
|
||||
{
|
||||
health_state_t state;
|
||||
if (g_health_mutex) {
|
||||
xSemaphoreTake(g_health_mutex, portMAX_DELAY);
|
||||
}
|
||||
state = g_health_state;
|
||||
if (g_health_mutex) {
|
||||
xSemaphoreGive(g_health_mutex);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// 健康事件的 Flash 日志写入
|
||||
void health_log_event(const char *event, int value)
|
||||
{
|
||||
DEBUG_WARN("HEALTH", "[健康日志] %s: %d", event, value);
|
||||
}
|
||||
|
||||
health_status_t health_update_from_wifi_rssi(int8_t rssi)
|
||||
{
|
||||
if (rssi < WIFI_RSSI_CRITICAL) {
|
||||
g_health_state = SYS_CRITICAL;
|
||||
health_log_event("WIFI危险", rssi);
|
||||
return HEALTH_MON_CRITICAL;
|
||||
} else if (rssi < WIFI_RSSI_NORMAL) {
|
||||
if (g_health_state < SYS_WARNING) {
|
||||
g_health_state = SYS_WARNING;
|
||||
health_log_event("WIFI警告", rssi);
|
||||
}
|
||||
return HEALTH_MON_WARNING;
|
||||
}
|
||||
|
||||
if (g_health_state == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
|
||||
g_health_state = SYS_NORMAL;
|
||||
health_log_event("WIFI恢复", rssi);
|
||||
}
|
||||
|
||||
return HEALTH_MON_OK;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* health_monitor.h - 系统健康监控头文件
|
||||
*/
|
||||
#ifndef HEALTH_MONITOR_H
|
||||
#define HEALTH_MONITOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "app_config.h"
|
||||
|
||||
// T9.3: 健康状态等级
|
||||
typedef enum {
|
||||
SYS_NORMAL = 0, // 正常
|
||||
SYS_WARNING = 1, // 警告
|
||||
SYS_CRITICAL = 2, // 危险
|
||||
SYS_SAFE_SHUTDOWN = 3 // 安全关机
|
||||
} health_state_t;
|
||||
|
||||
typedef enum {
|
||||
HEALTH_MON_OK = 0,
|
||||
HEALTH_MON_WARNING = 1,
|
||||
HEALTH_MON_CRITICAL = 2,
|
||||
HEALTH_MON_SHUTDOWN = 3
|
||||
} health_status_t;
|
||||
|
||||
// 外部共享状态(互斥锁保护)
|
||||
extern health_state_t g_health_state;
|
||||
|
||||
// WiFi RSSI 阈值(来自 app_config.h)
|
||||
#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING
|
||||
#define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL
|
||||
|
||||
// 函数声明
|
||||
extern health_status_t health_check_before_ota(void);
|
||||
extern void health_log_event(const char *event, int value);
|
||||
extern health_state_t health_get_current(void);
|
||||
extern health_status_t health_update_from_wifi_rssi(int8_t rssi);
|
||||
|
||||
#endif // HEALTH_MONITOR_H
|
||||
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* ota_relay.c - OTA 中继核心状态机实现
|
||||
* T2.3, T2.4, T3.1 - T3.7
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_partition.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#include "ota_relay.h"
|
||||
#include "spi_master.h"
|
||||
#include "protocol.h"
|
||||
#include "health_monitor.h"
|
||||
#include "debug_log.h"
|
||||
|
||||
relay_state_t g_relay_state = RELAY_IDLE;
|
||||
|
||||
static const esp_partition_t *g_gd32_partition = NULL;
|
||||
static session_ctx_t g_session_ctx;
|
||||
|
||||
QueueHandle_t g_ota_dispatch_queue = NULL;
|
||||
QueueHandle_t g_ota_result_queue = NULL;
|
||||
|
||||
static ota_result_t send_handshake_req(void);
|
||||
static ota_result_t send_nonce_and_authenticate(void);
|
||||
static ota_result_t send_firmware_size(uint32_t size);
|
||||
static ota_result_t relay_packets_from_flash(void);
|
||||
static ota_result_t finish_ota(void);
|
||||
|
||||
// 从 Flash 加载元数据
|
||||
esp_err_t meta_load(ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
memset(meta, 0, sizeof(ota_meta_t));
|
||||
|
||||
FILE *f = fopen(META_FILE, "r");
|
||||
if (!f) {
|
||||
DEBUG_WARN("OTA", "元数据文件不存在,返回默认值");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char buf[256];
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
|
||||
buf[n] = '\0';
|
||||
fclose(f);
|
||||
|
||||
cJSON *json = cJSON_Parse(buf);
|
||||
if (!json) {
|
||||
DEBUG_ERR("OTA", "元数据 JSON 解析失败");
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
cJSON *item = cJSON_GetObjectItem(json, "total_size");
|
||||
if (item) meta->total_size = (uint32_t)item->valueint;
|
||||
|
||||
item = cJSON_GetObjectItem(json, "stored_size");
|
||||
if (item) meta->stored_size = (uint32_t)item->valueint;
|
||||
|
||||
item = cJSON_GetObjectItem(json, "last_seq");
|
||||
if (item) meta->last_seq = (uint32_t)item->valueint;
|
||||
|
||||
item = cJSON_GetObjectItem(json, "chunk_count");
|
||||
if (item) meta->chunk_count = (uint32_t)item->valueint;
|
||||
|
||||
cJSON_Delete(json);
|
||||
DEBUG_INFO("OTA", "元数据已加载: total=%u, stored=%u, seq=%u",
|
||||
meta->total_size, meta->stored_size, meta->last_seq);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 保存元数据到 Flash
|
||||
esp_err_t meta_save(const ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
FILE *f = fopen(META_FILE, "w");
|
||||
if (!f) {
|
||||
DEBUG_ERR("OTA", "无法打开元数据文件写入");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
fprintf(f,
|
||||
"{\n"
|
||||
" \"total_size\": %" PRIu32 ",\n"
|
||||
" \"stored_size\": %" PRIu32 ",\n"
|
||||
" \"last_seq\": %" PRIu32 ",\n"
|
||||
" \"chunk_count\": %" PRIu32 "\n"
|
||||
"}",
|
||||
meta->total_size, meta->stored_size, meta->last_seq, meta->chunk_count);
|
||||
|
||||
fclose(f);
|
||||
DEBUG_INFO("OTA", "元数据已保存到 Flash");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 传输过程中更新元数据
|
||||
esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq)
|
||||
{
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = meta_load(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
meta.stored_size = stored_size;
|
||||
meta.last_seq = last_seq;
|
||||
|
||||
return meta_save(&meta);
|
||||
}
|
||||
|
||||
// OTA 完成后或中止时清除元数据
|
||||
esp_err_t meta_clear(void)
|
||||
{
|
||||
DEBUG_INFO("OTA", "清除 OTA 元数据");
|
||||
ota_meta_t meta = {};
|
||||
return meta_save(&meta);
|
||||
}
|
||||
|
||||
// Flash 写入接口
|
||||
esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!g_gd32_partition) {
|
||||
g_gd32_partition = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota");
|
||||
if (!g_gd32_partition) {
|
||||
DEBUG_ERR("OTA", "找不到 gd32_ota 分区");
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
return esp_partition_write(g_gd32_partition, offset, data, len);
|
||||
}
|
||||
|
||||
esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len)
|
||||
{
|
||||
if (!g_gd32_partition) {
|
||||
g_gd32_partition = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota");
|
||||
if (!g_gd32_partition) {
|
||||
DEBUG_ERR("OTA", "找不到 gd32_ota 分区");
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
return esp_partition_read(g_gd32_partition, offset, data, len);
|
||||
}
|
||||
|
||||
// 阶段1 - 发起与 GD32 的握手
|
||||
static ota_result_t send_handshake_req(void)
|
||||
{
|
||||
DEBUG_INFO("OTA", "=== 阶段1: 握手开始 ===");
|
||||
|
||||
uint8_t tx_frame[32];
|
||||
uint8_t rx_frame[32];
|
||||
|
||||
uint8_t payload[] = {0x00, 0xAC};
|
||||
frame_build(CMD_HANDSHAKE_REQ, payload, sizeof(payload), tx_frame);
|
||||
|
||||
g_relay_state = RELAY_HANDSHAKE_SENT;
|
||||
|
||||
esp_err_t err = spi_sync_transfer(CMD_HANDSHAKE_REQ, payload, sizeof(payload),
|
||||
rx_frame, sizeof(rx_frame), 3000);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "HANDSHAKE_REQ 失败");
|
||||
g_relay_state = RELAY_ERROR;
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "握手请求已发送,等待 GD32 响应...");
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
// 发送随机数并与 GD32 相互认证
|
||||
static ota_result_t send_nonce_and_authenticate(void)
|
||||
{
|
||||
uint8_t tx_frame[64];
|
||||
uint8_t rx_frame[64];
|
||||
uint8_t nonce_server[16];
|
||||
|
||||
uint32_t timestamp = xTaskGetTickCount();
|
||||
uint32_t mem_addr = (uint32_t)nonce_server;
|
||||
uint32_t rand_seed = 0x12345678;
|
||||
|
||||
scramble_generate(timestamp, mem_addr, rand_seed, nonce_server);
|
||||
|
||||
DEBUG_INFO("OTA", "发送 CHALLENGE,包含 nonce_server (16B)");
|
||||
g_relay_state = RELAY_CHALLENGE_SENT;
|
||||
|
||||
frame_build(CMD_CHALLENGE, nonce_server, 16, tx_frame);
|
||||
|
||||
esp_err_t err = spi_sync_transfer(CMD_CHALLENGE, nonce_server, 16,
|
||||
rx_frame, sizeof(rx_frame), 5000);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "CHALLENGE 发送失败");
|
||||
g_relay_state = RELAY_ERROR;
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
g_relay_state = RELAY_CHALLENGE_RESPOND;
|
||||
DEBUG_INFO("OTA", "已收到 GD32 握手数据包");
|
||||
|
||||
// 发送 SESSION_KEY(带变换后的随机数)
|
||||
uint8_t transformed_nonce[16];
|
||||
memcpy(transformed_nonce, nonce_server, 16);
|
||||
|
||||
DEBUG_INFO("OTA", "发送 SESSION_KEY(带变换后的随机数)");
|
||||
g_relay_state = RELAY_SESSION_KEY_SENT;
|
||||
|
||||
frame_build(CMD_SESSION_KEY, transformed_nonce, 16, tx_frame);
|
||||
esp_err_t err2 = spi_sync_transfer(CMD_SESSION_KEY, transformed_nonce, 16,
|
||||
rx_frame, sizeof(rx_frame), 3000);
|
||||
if (err2 != ESP_OK) {
|
||||
g_relay_state = RELAY_ERROR;
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
g_relay_state = RELAY_AUTH_COMPLETE;
|
||||
DEBUG_INFO("OTA", "=== 阶段1 认证完成: 安全通道已建立 ===");
|
||||
|
||||
uint8_t k_session[16] = {0};
|
||||
session_key_init(&g_session_ctx, k_session);
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
// 向 GD32 发送固件总大小
|
||||
static ota_result_t send_firmware_size(uint32_t size)
|
||||
{
|
||||
uint8_t tx_frame[32];
|
||||
uint8_t rx_frame[32];
|
||||
|
||||
DEBUG_INFO("OTA", "=== 阶段2: 发送大小 ===");
|
||||
|
||||
uint8_t size_payload[4];
|
||||
size_payload[0] = (uint8_t)(size & 0xFF);
|
||||
size_payload[1] = (uint8_t)((size >> 8) & 0xFF);
|
||||
size_payload[2] = (uint8_t)((size >> 16) & 0xFF);
|
||||
size_payload[3] = (uint8_t)((size >> 24) & 0xFF);
|
||||
|
||||
g_relay_state = RELAY_SIZE_SENT;
|
||||
|
||||
frame_build(CMD_SEND_SIZE, size_payload, 4, tx_frame);
|
||||
esp_err_t err = spi_sync_transfer(CMD_SEND_SIZE, size_payload, 4,
|
||||
rx_frame, sizeof(rx_frame), 3000);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "SEND_SIZE 失败");
|
||||
g_relay_state = RELAY_ERROR;
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "大小已发送: %u 字节,等待 GD32 确认...", size);
|
||||
g_relay_state = RELAY_SIZE_ACK;
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
// 从 Flash 向 GD32 转发存储的固件(48B/包)
|
||||
static ota_result_t relay_packets_from_flash(void)
|
||||
{
|
||||
DEBUG_INFO("OTA", "=== 阶段3: 从 Flash 转发固件 ===");
|
||||
|
||||
ota_meta_t meta;
|
||||
if (meta_load(&meta) != ESP_OK) {
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
uint32_t total_packets = (meta.total_size + 47) / 48;
|
||||
uint32_t flash_offset = 0;
|
||||
uint8_t tx_buf[64];
|
||||
uint8_t rx_buf[32];
|
||||
ota_result_t result = OTA_RESULT_OK;
|
||||
|
||||
DEBUG_INFO("OTA", "准备转发 %u 包,每包 48 字节", total_packets);
|
||||
|
||||
for (uint32_t seq = 0; seq < total_packets; seq++) {
|
||||
// 发送每个包前检查健康状态
|
||||
health_status_t health = health_check_before_ota();
|
||||
if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
|
||||
DEBUG_ERR("OTA", "健康检查危险,中止 OTA");
|
||||
return OTA_RESULT_ERR_HEALTH;
|
||||
}
|
||||
|
||||
size_t chunk_size = 48;
|
||||
if (seq == total_packets - 1) {
|
||||
chunk_size = meta.total_size - (seq * 48);
|
||||
}
|
||||
|
||||
esp_err_t err = flash_read_chunk(flash_offset, &tx_buf[2], chunk_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "Flash 读取失败,偏移=%u", flash_offset);
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
tx_buf[0] = (uint8_t)(seq & 0xFF);
|
||||
tx_buf[1] = (uint8_t)((seq >> 8) & 0xFF);
|
||||
|
||||
uint16_t crc = crc16_modbus(tx_buf, 2 + chunk_size);
|
||||
tx_buf[2 + chunk_size] = (uint8_t)(crc & 0xFF);
|
||||
tx_buf[2 + chunk_size + 1] = (uint8_t)((crc >> 8) & 0xFF);
|
||||
|
||||
g_relay_state = RELAY_PACKET_SENT;
|
||||
|
||||
uint8_t cmd = (seq == total_packets - 1) ? CMD_DATA_END : CMD_DATA_PACKET;
|
||||
size_t tx_len = 2 + chunk_size + 2;
|
||||
|
||||
err = spi_sync_transfer(cmd, tx_buf, tx_len, rx_buf, sizeof(rx_buf), 3000);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN("OTA", "数据包 %u 发送失败,重试中...", seq);
|
||||
result = OTA_RESULT_ERR_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
|
||||
flash_offset += chunk_size;
|
||||
g_relay_state = RELAY_PACKET_ACK_OK;
|
||||
|
||||
// 每 10% 记录一次进度
|
||||
if (seq % (total_packets / 10 + 1) == 0) {
|
||||
DEBUG_INFO("OTA", "转发进度: %u/%u 包 (%u%%)",
|
||||
seq, total_packets, (seq * 100) / total_packets);
|
||||
}
|
||||
}
|
||||
|
||||
g_relay_state = RELAY_RECEIVE_DONE;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 结束 OTA - 发送 0x0A AC 并清理
|
||||
static ota_result_t finish_ota(void)
|
||||
{
|
||||
DEBUG_INFO("OTA", "=== 阶段4: 结束 OTA ===");
|
||||
|
||||
uint8_t tx_frame[16];
|
||||
uint8_t rx_frame[16];
|
||||
|
||||
uint8_t payload[] = {0x0A, 0xAC};
|
||||
frame_build(CMD_FINISH_OTA, payload, sizeof(payload), tx_frame);
|
||||
|
||||
g_relay_state = RELAY_FINISH_SENT;
|
||||
|
||||
esp_err_t err = spi_sync_transfer(CMD_FINISH_OTA, payload, sizeof(payload),
|
||||
rx_frame, sizeof(rx_frame), 5000);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "FINISH_OTA 失败");
|
||||
g_relay_state = RELAY_ERROR;
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "等待 GD32 跳转确认...");
|
||||
|
||||
session_key_clear(&g_session_ctx);
|
||||
memset(&g_session_ctx, 0, sizeof(session_ctx_t));
|
||||
|
||||
g_relay_state = RELAY_DONE;
|
||||
DEBUG_INFO("OTA", "=== OTA 完成 ===");
|
||||
|
||||
meta_clear();
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
// 启动完整 OTA 流程的主入口
|
||||
ota_result_t ota_relay_start_handshake(void)
|
||||
{
|
||||
ota_result_t res;
|
||||
|
||||
res = send_handshake_req();
|
||||
if (res != OTA_RESULT_OK) return res;
|
||||
|
||||
res = send_nonce_and_authenticate();
|
||||
if (res != OTA_RESULT_OK) return res;
|
||||
|
||||
ota_meta_t meta;
|
||||
meta_load(&meta);
|
||||
res = send_firmware_size(meta.total_size);
|
||||
if (res != OTA_RESULT_OK) return res;
|
||||
|
||||
res = relay_packets_from_flash();
|
||||
if (res != OTA_RESULT_OK) return res;
|
||||
|
||||
res = finish_ota();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// 从 Flash 向 GD32 转发固件
|
||||
ota_result_t ota_relay_forward_firmware(void)
|
||||
{
|
||||
ota_meta_t meta;
|
||||
if (meta_load(&meta) != ESP_OK) {
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "开始固件转发: %u 字节, %u 块",
|
||||
meta.total_size, meta.chunk_count);
|
||||
|
||||
ota_result_t res = send_handshake_req();
|
||||
if (res != OTA_RESULT_OK) return res;
|
||||
|
||||
res = send_nonce_and_authenticate();
|
||||
if (res != OTA_RESULT_OK) return res;
|
||||
|
||||
res = send_firmware_size(meta.total_size);
|
||||
if (res != OTA_RESULT_OK) return res;
|
||||
|
||||
res = relay_packets_from_flash();
|
||||
if (res != OTA_RESULT_OK) return res;
|
||||
|
||||
res = finish_ota();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// 获取当前状态
|
||||
relay_state_t ota_relay_get_state(void)
|
||||
{
|
||||
return g_relay_state;
|
||||
}
|
||||
|
||||
// 重置状态机
|
||||
void ota_relay_reset(void)
|
||||
{
|
||||
g_relay_state = RELAY_IDLE;
|
||||
session_key_clear(&g_session_ctx);
|
||||
}
|
||||
|
||||
// OTA 中继 FreeRTOS 任务
|
||||
void ota_relay_task(void *pvParameters)
|
||||
{
|
||||
DEBUG_INFO("OTA", "OTA 中继任务启动");
|
||||
|
||||
g_ota_dispatch_queue = xQueueCreate(4, sizeof(uint32_t));
|
||||
g_ota_result_queue = xQueueCreate(4, sizeof(uint32_t));
|
||||
|
||||
uint32_t msg;
|
||||
BaseType_t ret = xQueueReceive(g_ota_dispatch_queue, &msg, portMAX_DELAY);
|
||||
|
||||
if (ret == pdTRUE) {
|
||||
DEBUG_INFO("OTA", "收到 OTA 调度信号,开始转发...");
|
||||
ota_result_t result = ota_relay_forward_firmware();
|
||||
|
||||
uint32_t result_msg = (uint32_t)result;
|
||||
xQueueSend(g_ota_result_queue, &result_msg, pdMS_TO_TICKS(100));
|
||||
|
||||
if (result == OTA_RESULT_OK) {
|
||||
DEBUG_INFO("OTA", "OTA 转发成功完成");
|
||||
} else {
|
||||
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "OTA 中继任务结束");
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* ota_relay.h - OTA 中继核心状态机
|
||||
* T2.3, T3.1 - T3.7
|
||||
*/
|
||||
#ifndef OTA_RELAY_H
|
||||
#define OTA_RELAY_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
|
||||
// T4.3: OTA 元数据结构,用于 Flash 存储
|
||||
#define META_FILE "/spiffs/ota_meta.json"
|
||||
|
||||
typedef struct {
|
||||
uint32_t total_size; // 固件总大小(字节)
|
||||
uint32_t stored_size; // 当前已存储大小(字节)
|
||||
uint32_t last_seq; // 最后成功存储的序列号
|
||||
uint32_t chunk_count; // 预期总块数
|
||||
uint8_t checksum[32]; // 固件 SHA256 校验(可选)
|
||||
} ota_meta_t;
|
||||
|
||||
// OTA 中继状态(对应 GD32 四阶段协议)
|
||||
typedef enum {
|
||||
// 阶段0: 空闲
|
||||
RELAY_IDLE = 0,
|
||||
|
||||
// 阶段1: 安全握手
|
||||
RELAY_HANDSHAKE_IDLE, // 等待 HANDSHAKE_REQ
|
||||
RELAY_HANDSHAKE_SENT, // 已发送 0x00 AC,等待 GD32 响应
|
||||
RELAY_CHALLENGE_SENT, // 已发送 nonce_server (16B),等待响应
|
||||
RELAY_CHALLENGE_RESPOND, // 已收到 GD32 握手数据包
|
||||
RELAY_SESSION_KEY_SENT, // 已发送变换后的随机数,等待 0xBA C0
|
||||
RELAY_AUTH_COMPLETE, // 已收到 0xBA C0,安全通道建立完成
|
||||
|
||||
// 阶段2: 传输准备
|
||||
RELAY_SIZE_SENT, // 已发送固件大小(4B LE)
|
||||
RELAY_SIZE_ACK, // 已收到 0xBA C0 + seq
|
||||
|
||||
// 阶段3: 数据传输
|
||||
RELAY_RECEIVING, // 接收并存储固件到 Flash
|
||||
RELAY_RELAY_ACTIVE, // 存储完成,开始向 GD32 转发
|
||||
RELAY_PACKET_SENT, // 已发送 DATA_PACKET,等待 GD32 ACK
|
||||
RELAY_PACKET_ACK_OK, // ACK 正常,继续下一包
|
||||
RELAY_PACKET_ACK_RETRY, // ACK 要求重发,重新发送当前包
|
||||
RELAY_RECEIVE_DONE, // 已发送 DATA_END,等待 TRANSFER_DONE
|
||||
|
||||
// 阶段4: 结束跳转
|
||||
RELAY_TRANSFER_DONE, // 已收到 0xBA C0 + FF FF FF FF
|
||||
RELAY_FINISH_SENT, // 已发送 0x0A AC,等待 GD32 确认
|
||||
RELAY_DONE, // GD32 回复 0x0A AC,OTA 完成
|
||||
|
||||
// 错误状态
|
||||
RELAY_ERROR = 0xFF
|
||||
} relay_state_t;
|
||||
|
||||
// OTA 结果码
|
||||
typedef enum {
|
||||
OTA_RESULT_OK = 0,
|
||||
OTA_RESULT_ERR_TIMEOUT = -1, // GD32 超时无响应
|
||||
OTA_RESULT_ERR_CRC = -2, // CRC 校验失败
|
||||
OTA_RESULT_ERR_AUTH = -3, // 认证失败
|
||||
OTA_RESULT_ERR_STORAGE = -4, // Flash 存储错误
|
||||
OTA_RESULT_ERR_GD32 = -5, // GD32 返回错误
|
||||
OTA_RESULT_ERR_HEALTH = -6, // 健康状态异常中止
|
||||
OTA_RESULT_ERR_USER_ABORT = -7 // 用户中止
|
||||
} ota_result_t;
|
||||
|
||||
// 外部队列句柄(在 app_main 中创建)
|
||||
extern QueueHandle_t g_ota_dispatch_queue; // 从 WiFi/BLE 到 OTA 中继
|
||||
extern QueueHandle_t g_ota_result_queue; // OTA 结果到 WiFi/BLE
|
||||
|
||||
// OTA 中继任务入口
|
||||
void ota_relay_task(void *pvParameters);
|
||||
|
||||
// T2.3: 发起与 GD32 的握手
|
||||
ota_result_t ota_relay_start_handshake(void);
|
||||
|
||||
// T3.4: 从 Flash 向 GD32 转发存储的固件(48B/包)
|
||||
ota_result_t ota_relay_forward_firmware(void);
|
||||
|
||||
// T3.7: 获取当前中继状态
|
||||
relay_state_t ota_relay_get_state(void);
|
||||
|
||||
// T3.7: 重置中继状态机
|
||||
void ota_relay_reset(void);
|
||||
|
||||
// T4.2: Flash 写入接口
|
||||
esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len);
|
||||
esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len);
|
||||
|
||||
// T3.3: 元数据文件管理
|
||||
esp_err_t meta_load(ota_meta_t *meta);
|
||||
esp_err_t meta_save(const ota_meta_t *meta);
|
||||
esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq);
|
||||
esp_err_t meta_clear(void);
|
||||
|
||||
// 当前中继状态(供健康监控外部访问)
|
||||
extern relay_state_t g_relay_state;
|
||||
|
||||
#endif // OTA_RELAY_H
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* ble_ota.c - BLE OTA GATT 服务器实现
|
||||
* T5.1 - T5.5
|
||||
*
|
||||
* 注意: ESP-IDF 6.0 中蓝牙组件有较大变化
|
||||
* 此文件暂时提供空实现,待 ESP-IDF 6.0 蓝牙 API 稳定后再适配
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "ble_ota.h"
|
||||
#include "ota_relay.h"
|
||||
#include "health_monitor.h"
|
||||
#include "debug_log.h"
|
||||
|
||||
static bool s_ble_connected = false;
|
||||
|
||||
/* ESP-IDF 6.0: BLE OTA 功能暂时禁用,以下函数保留供后续适配使用 */
|
||||
#if 0
|
||||
// 处理写入控制点
|
||||
static void handle_ctrl_point_write(uint8_t *data, size_t len)
|
||||
{
|
||||
if (len < 1) return;
|
||||
|
||||
uint8_t cmd = data[0];
|
||||
DEBUG_DBG("BLE", "控制点写入: cmd=0x%02X, len=%d", cmd, len);
|
||||
|
||||
if (cmd == 0xF7 && s_ble_connected) {
|
||||
ota_meta_t meta;
|
||||
meta_load(&meta);
|
||||
if (meta.stored_size >= meta.total_size) {
|
||||
ble_ota_signal_relay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理写入数据输入(大固件分块)
|
||||
static void handle_data_in_write(uint8_t *data, size_t len)
|
||||
{
|
||||
if (len < 7) return;
|
||||
|
||||
uint16_t seq = data[1] | (data[2] << 8);
|
||||
uint32_t total_size = data[3] | (data[4] << 8) | (data[5] << 16) | (data[6] << 24);
|
||||
|
||||
if (seq == 0 && total_size > 0) {
|
||||
ota_meta_t meta = {
|
||||
.total_size = total_size,
|
||||
.stored_size = 0,
|
||||
.last_seq = 0,
|
||||
.chunk_count = (total_size + 4095) / 4096
|
||||
};
|
||||
meta_save(&meta);
|
||||
DEBUG_INFO("BLE", "开始固件存储: %" PRIu32 " 字节", total_size);
|
||||
}
|
||||
|
||||
size_t data_offset = 7;
|
||||
size_t data_len = len - data_offset - 4;
|
||||
|
||||
uint32_t flash_offset = seq * 4096;
|
||||
esp_err_t err = flash_write_chunk(flash_offset, &data[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("BLE", "Flash 写入失败 at seq %" PRIu16, seq);
|
||||
}
|
||||
|
||||
meta_update(flash_offset + data_len, seq);
|
||||
|
||||
DEBUG_DBG("BLE", "已存储分块 %" PRIu16 ", offset=%" PRIu32, seq, flash_offset);
|
||||
|
||||
ota_meta_t meta;
|
||||
meta_load(&meta);
|
||||
if (meta.stored_size >= meta.total_size) {
|
||||
DEBUG_INFO("BLE", "全部固件已存储,触发转发");
|
||||
ble_ota_signal_relay();
|
||||
}
|
||||
}
|
||||
#endif /* ESP_IDF_COMPILATION */
|
||||
|
||||
// 启动 BLE GATT 服务器
|
||||
// 注意: 此函数在 ESP-IDF 6.0 中暂时返回 ESP_ERR_NOT_SUPPORTED
|
||||
esp_err_t ble_ota_server_start(void)
|
||||
{
|
||||
DEBUG_WARN("BLE", "BLE OTA 功能在 ESP-IDF 6.0 中暂时不可用");
|
||||
DEBUG_WARN("BLE", "请使用 WiFi OTA 功能或等待 BLE API 适配");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// 通过 BLE 通知 APP GD32 的响应
|
||||
esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len)
|
||||
{
|
||||
(void)data;
|
||||
(void)len;
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
bool ble_ota_is_connected(void)
|
||||
{
|
||||
return s_ble_connected;
|
||||
}
|
||||
|
||||
// 信号通知 OTA 中继开始转发
|
||||
void ble_ota_signal_relay(void)
|
||||
{
|
||||
if (g_ota_dispatch_queue) {
|
||||
uint32_t msg = 1;
|
||||
BaseType_t ret = xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100));
|
||||
if (ret == pdTRUE) {
|
||||
DEBUG_INFO("BLE", "OTA 调度信号已通过 BLE 发送");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BLE OTA FreeRTOS 任务
|
||||
void ble_ota_task(void *pvParameters)
|
||||
{
|
||||
(void)pvParameters;
|
||||
DEBUG_WARN("BLE", "BLE OTA 任务启动,但功能暂时不可用");
|
||||
|
||||
// 等待调度信号
|
||||
while (1) {
|
||||
uint32_t msg;
|
||||
BaseType_t ret = xQueueReceive(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(1000));
|
||||
if (ret == pdTRUE) {
|
||||
DEBUG_INFO("BLE", "收到 OTA 调度信号,但 BLE 功能不可用");
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* ble_ota.h - BLE OTA GATT 服务器
|
||||
* T5.1 - T5.5
|
||||
*/
|
||||
#ifndef BLE_OTA_H
|
||||
#define BLE_OTA_H
|
||||
|
||||
#include "ota_relay.h"
|
||||
|
||||
// BLE OTA 任务入口
|
||||
void ble_ota_task(void *pvParameters);
|
||||
|
||||
// 启动 BLE GATT 服务器
|
||||
esp_err_t ble_ota_server_start(void);
|
||||
|
||||
// 通过 BLE 通知 APP GD32 的响应
|
||||
esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len);
|
||||
|
||||
// 检查 BLE 是否已连接
|
||||
bool ble_ota_is_connected(void);
|
||||
|
||||
// 信号通知中继开始(存储完成后调用)
|
||||
void ble_ota_signal_relay(void);
|
||||
|
||||
#endif // BLE_OTA_H
|
||||
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
* wifi_ota.c - WiFi OTA 服务器实现
|
||||
* T4.1 - T4.5
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "lwip/netdb.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_netif_types.h"
|
||||
|
||||
#include "app_config.h"
|
||||
#include "wifi_ota.h"
|
||||
#include "ota_relay.h"
|
||||
#include "health_monitor.h"
|
||||
#include "debug_log.h"
|
||||
|
||||
static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP;
|
||||
static bool s_ota_storage_done = false;
|
||||
static bool s_relay_triggered = false;
|
||||
static bool s_sta_connected = false;
|
||||
|
||||
static httpd_handle_t s_http_server = NULL;
|
||||
|
||||
// ----------------------------------------
|
||||
// WiFi AP 模式初始化
|
||||
// ----------------------------------------
|
||||
static esp_err_t wifi_init_ap(void)
|
||||
{
|
||||
DEBUG_INFO("WIFI", "初始化 WiFi AP 模式...");
|
||||
|
||||
s_wifi_mode = OTA_WIFI_MODE_AP;
|
||||
|
||||
wifi_config_t wifi_config = {};
|
||||
memcpy(wifi_config.ap.ssid, WIFI_AP_SSID, strlen(WIFI_AP_SSID));
|
||||
wifi_config.ap.ssid_len = strlen(WIFI_AP_SSID);
|
||||
memcpy(wifi_config.ap.password, WIFI_AP_PASSWORD, strlen(WIFI_AP_PASSWORD));
|
||||
wifi_config.ap.max_connection = WIFI_AP_MAX_CONNECT;
|
||||
wifi_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
||||
wifi_config.ap.channel = WIFI_AP_CHANNEL;
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
DEBUG_INFO("WIFI", "WiFi AP 已启动: %s", WIFI_AP_SSID);
|
||||
DEBUG_INFO("WIFI", "AP IP 地址: 192.168.4.1");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// WiFi STA 模式初始化
|
||||
// ----------------------------------------
|
||||
static esp_err_t wifi_init_sta(void)
|
||||
{
|
||||
DEBUG_INFO("WIFI", "初始化 WiFi STA 模式...");
|
||||
|
||||
s_wifi_mode = OTA_WIFI_MODE_STA;
|
||||
|
||||
wifi_config_t wifi_config = {};
|
||||
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
||||
wifi_config.sta.pmf_cfg.capable = true;
|
||||
wifi_config.sta.pmf_cfg.required = false;
|
||||
memcpy(wifi_config.sta.ssid, WIFI_STA_SSID, sizeof(wifi_config.sta.ssid));
|
||||
memcpy(wifi_config.sta.password, WIFI_STA_PASSWORD, sizeof(wifi_config.sta.password));
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
DEBUG_INFO("WIFI", "WiFi STA 连接中, SSID: %s...", WIFI_STA_SSID);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 等待 STA 连接成功并获取 IP
|
||||
static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
|
||||
{
|
||||
int retry = 0;
|
||||
while (retry < 60) { // 最多等 6 秒
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
s_sta_connected = true;
|
||||
esp_netif_ip_info_t ip_info;
|
||||
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (netif != NULL && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
|
||||
if (ip_str) {
|
||||
snprintf(ip_str, len, IPSTR, IP2STR(&ip_info.ip));
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
retry++;
|
||||
}
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
// 设置 STA 静态 IP
|
||||
static void wifi_set_static_ip(void)
|
||||
{
|
||||
#if WIFI_STA_USE_STATIC_IP
|
||||
esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (sta_netif) {
|
||||
ESP_ERROR_CHECK(esp_netif_dhcpc_stop(sta_netif));
|
||||
|
||||
esp_netif_ip_info_t info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
inet_pton(AF_INET, WIFI_STA_STATIC_IP, &info.ip);
|
||||
inet_pton(AF_INET, WIFI_STA_STATIC_GW, &info.gw);
|
||||
inet_pton(AF_INET, WIFI_STA_STATIC_NETMASK, &info.netmask);
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_set_ip_info(sta_netif, &info));
|
||||
DEBUG_INFO("WIFI", "静态 IP 已设置: %s", WIFI_STA_STATIC_IP);
|
||||
}
|
||||
#else
|
||||
DEBUG_INFO("WIFI", "使用 DHCP 获取 IP");
|
||||
#endif
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// HTTP POST /ota/upload 处理器
|
||||
// ----------------------------------------
|
||||
static esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
{
|
||||
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d", req->content_len);
|
||||
|
||||
if (req->content_len > OTA_MAX_FIRMWARE_SIZE) {
|
||||
DEBUG_ERR("WIFI", "固件太大: %d 字节", req->content_len);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
uint8_t buf[OTA_BLOCK_SIZE];
|
||||
int received = 0;
|
||||
int first_chunk = 1;
|
||||
|
||||
while (received < req->content_len) {
|
||||
int remaining = req->content_len - received;
|
||||
int to_read = remaining < sizeof(buf) ? remaining : sizeof(buf);
|
||||
|
||||
int ret = httpd_req_recv(req, (char *)buf, to_read);
|
||||
if (ret <= 0) {
|
||||
DEBUG_ERR("WIFI", "请求接收失败");
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (first_chunk && ret >= 6) {
|
||||
uint16_t seq = buf[0] | (buf[1] << 8);
|
||||
uint32_t total_size = buf[2] | (buf[3] << 8) | (buf[4] << 16) | (buf[5] << 24);
|
||||
|
||||
if (seq == 0) {
|
||||
ota_meta_t meta = {
|
||||
.total_size = total_size,
|
||||
.stored_size = 0,
|
||||
.last_seq = 0,
|
||||
.chunk_count = (total_size + 4095) / 4096,
|
||||
.checksum = {0}
|
||||
};
|
||||
meta_save(&meta);
|
||||
DEBUG_INFO("WIFI", "开始固件存储: %u 字节", total_size);
|
||||
}
|
||||
first_chunk = 0;
|
||||
}
|
||||
|
||||
uint32_t flash_offset = (received / OTA_BLOCK_SIZE) * OTA_BLOCK_SIZE;
|
||||
esp_err_t err = flash_write_chunk(flash_offset, buf, ret);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "Flash 写入失败 at offset %u", flash_offset);
|
||||
}
|
||||
|
||||
meta_update(flash_offset + ret, received / OTA_BLOCK_SIZE);
|
||||
|
||||
received += ret;
|
||||
DEBUG_DBG("WIFI", "已接收: %d / %d 字节", received, req->content_len);
|
||||
}
|
||||
|
||||
DEBUG_INFO("WIFI", "固件存储完成: %d 字节", received);
|
||||
|
||||
ota_meta_t meta;
|
||||
meta_load(&meta);
|
||||
if (meta.stored_size >= meta.total_size) {
|
||||
s_ota_storage_done = true;
|
||||
DEBUG_INFO("WIFI", "全部固件已存储,触发转发...");
|
||||
wifi_ota_signal_relay();
|
||||
}
|
||||
|
||||
const char resp[] = "{\"status\": \"ok\", \"progress\": 100}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// HTTP POST /ota/relay 处理器
|
||||
static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||
{
|
||||
(void)req;
|
||||
DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
|
||||
wifi_ota_signal_relay();
|
||||
|
||||
const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA relay triggered\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// HTTP GET /ota/status 处理器
|
||||
static esp_err_t ota_status_handler(httpd_req_t *req)
|
||||
{
|
||||
ota_meta_t meta;
|
||||
meta_load(&meta);
|
||||
|
||||
char resp[256];
|
||||
int len = snprintf(resp, sizeof(resp),
|
||||
"{\"total_size\": %lu, \"stored_size\": %lu, \"last_seq\": %lu, "
|
||||
"\"storage_done\": %s, \"relay_state\": %d}",
|
||||
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
|
||||
(unsigned long)meta.last_seq,
|
||||
s_ota_storage_done ? "true" : "false",
|
||||
g_relay_state);
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, len);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// HTTP GET /health 处理器
|
||||
static esp_err_t health_handler(httpd_req_t *req)
|
||||
{
|
||||
health_state_t state = health_get_current();
|
||||
|
||||
char resp[128];
|
||||
int len = snprintf(resp, sizeof(resp),
|
||||
"{\"health\": %d, \"free_heap\": %lu}",
|
||||
state, (unsigned long)esp_get_free_heap_size());
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, len);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// HTTP GET /wifi/status 处理器
|
||||
static esp_err_t wifi_status_handler(httpd_req_t *req)
|
||||
{
|
||||
char resp[256];
|
||||
char ip_str[16] = "0.0.0.0";
|
||||
int8_t rssi = -127;
|
||||
|
||||
if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
|
||||
esp_netif_ip_info_t ip_info;
|
||||
esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
|
||||
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
|
||||
}
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
rssi = ap_info.rssi;
|
||||
}
|
||||
}
|
||||
|
||||
int len = snprintf(resp, sizeof(resp),
|
||||
"{\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}",
|
||||
s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
|
||||
ip_str, rssi,
|
||||
(s_wifi_mode == OTA_WIFI_MODE_STA) ? (s_sta_connected ? "true" : "false") : "N/A");
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, len);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// 启动 HTTP OTA 服务器
|
||||
// ----------------------------------------
|
||||
static esp_err_t http_ota_server_start(void)
|
||||
{
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = HTTP_SERVER_PORT;
|
||||
config.stack_size = HTTP_SERVER_STACK_SIZE;
|
||||
|
||||
DEBUG_INFO("WIFI", "启动 HTTP 服务器,端口 %d...", config.server_port);
|
||||
|
||||
esp_err_t err = httpd_start(&s_http_server, &config);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "HTTP 服务器启动失败: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
httpd_uri_t upload_uri = {
|
||||
.uri = "/ota/upload",
|
||||
.method = HTTP_POST,
|
||||
.handler = ota_upload_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(s_http_server, &upload_uri);
|
||||
|
||||
httpd_uri_t relay_uri = {
|
||||
.uri = "/ota/relay",
|
||||
.method = HTTP_POST,
|
||||
.handler = ota_relay_trigger_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(s_http_server, &relay_uri);
|
||||
|
||||
httpd_uri_t status_uri = {
|
||||
.uri = "/ota/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = ota_status_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(s_http_server, &status_uri);
|
||||
|
||||
httpd_uri_t health_uri = {
|
||||
.uri = "/health",
|
||||
.method = HTTP_GET,
|
||||
.handler = health_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(s_http_server, &health_uri);
|
||||
|
||||
httpd_uri_t wifi_status_uri = {
|
||||
.uri = "/wifi/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = wifi_status_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(s_http_server, &wifi_status_uri);
|
||||
|
||||
DEBUG_INFO("WIFI", "HTTP 服务器已启动");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// 公开 API
|
||||
// ----------------------------------------
|
||||
void wifi_ota_signal_relay(void)
|
||||
{
|
||||
if (s_relay_triggered) {
|
||||
DEBUG_WARN("WIFI", "转发已经触发过");
|
||||
return;
|
||||
}
|
||||
|
||||
s_relay_triggered = true;
|
||||
|
||||
if (g_ota_dispatch_queue) {
|
||||
uint32_t msg = 1;
|
||||
xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100));
|
||||
DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
|
||||
}
|
||||
}
|
||||
|
||||
ota_wifi_mode_t wifi_get_mode(void)
|
||||
{
|
||||
return s_wifi_mode;
|
||||
}
|
||||
|
||||
int8_t wifi_get_rssi(void)
|
||||
{
|
||||
if (s_wifi_mode != OTA_WIFI_MODE_STA || !s_sta_connected) {
|
||||
return -127;
|
||||
}
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
return ap_info.rssi;
|
||||
}
|
||||
return -127;
|
||||
}
|
||||
|
||||
bool wifi_is_connected(void)
|
||||
{
|
||||
return s_sta_connected;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// WiFi OTA FreeRTOS 任务
|
||||
// ----------------------------------------
|
||||
void wifi_ota_task(void *pvParameters)
|
||||
{
|
||||
(void)pvParameters;
|
||||
|
||||
DEBUG_INFO("WIFI", "WiFi OTA 任务启动");
|
||||
|
||||
// 初始化 NVS
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// 初始化网络接口
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
// STA 优先,失败自动切 AP
|
||||
#ifdef CONFIG_WIFI_MODE_STA
|
||||
err = wifi_init_sta();
|
||||
if (err == ESP_OK) {
|
||||
char ip_str[16] = {0};
|
||||
|
||||
// 设置静态 IP(如果配置了)
|
||||
wifi_set_static_ip();
|
||||
|
||||
// 等待连接成功
|
||||
err = wifi_wait_sta_connected(ip_str, sizeof(ip_str));
|
||||
if (err == ESP_OK) {
|
||||
DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str);
|
||||
} else {
|
||||
DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
|
||||
esp_wifi_stop();
|
||||
s_wifi_mode = OTA_WIFI_MODE_AP;
|
||||
wifi_init_ap();
|
||||
}
|
||||
}
|
||||
#else
|
||||
wifi_init_ap();
|
||||
#endif
|
||||
|
||||
// 启动 HTTP 服务器
|
||||
err = http_ota_server_start();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "HTTP 服务器启动失败");
|
||||
}
|
||||
|
||||
DEBUG_INFO("WIFI", "WiFi OTA 服务器就绪");
|
||||
|
||||
// 定期检查 WiFi 状态
|
||||
while (1) {
|
||||
int8_t rssi = wifi_get_rssi();
|
||||
if (rssi != -127) {
|
||||
health_update_from_wifi_rssi(rssi);
|
||||
|
||||
if (rssi < WIFI_RSSI_CRITICAL) {
|
||||
DEBUG_WARN("WIFI", "WiFi RSSI 危险: %d dBm", rssi);
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* wifi_ota.h - WiFi OTA 服务器
|
||||
* T4.1 - T4.5
|
||||
*/
|
||||
#ifndef WIFI_OTA_H
|
||||
#define WIFI_OTA_H
|
||||
|
||||
#include "ota_relay.h"
|
||||
|
||||
// WiFi 模式: AP 或 STA
|
||||
typedef enum {
|
||||
OTA_WIFI_MODE_AP, // AP 模式(ESP32 作为热点)
|
||||
OTA_WIFI_MODE_STA // STA 模式(连接路由器)
|
||||
} ota_wifi_mode_t;
|
||||
|
||||
// WiFi OTA 任务入口
|
||||
void wifi_ota_task(void *pvParameters);
|
||||
|
||||
// 信号通知 OTA 中继开始(存储完成后调用)
|
||||
void wifi_ota_signal_relay(void);
|
||||
|
||||
// 获取当前 WiFi 模式
|
||||
ota_wifi_mode_t wifi_get_mode(void);
|
||||
|
||||
// 获取 WiFi RSSI
|
||||
int8_t wifi_get_rssi(void);
|
||||
|
||||
// STA 是否已连接
|
||||
bool wifi_is_connected(void);
|
||||
|
||||
#endif // WIFI_OTA_H
|
||||
@@ -0,0 +1,117 @@
|
||||
/* WiFi softAP Example
|
||||
|
||||
This example code is in the Public Domain (or CC0 licensed, at your option.)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, this
|
||||
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied.
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#include "lwip/err.h"
|
||||
#include "lwip/sys.h"
|
||||
|
||||
/* The examples use WiFi configuration that you can set via project configuration menu.
|
||||
|
||||
If you'd rather not, just change the below entries to strings with
|
||||
the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
|
||||
*/
|
||||
#define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID
|
||||
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
|
||||
#define EXAMPLE_ESP_WIFI_CHANNEL CONFIG_ESP_WIFI_CHANNEL
|
||||
#define EXAMPLE_MAX_STA_CONN CONFIG_ESP_MAX_STA_CONN
|
||||
|
||||
#if CONFIG_ESP_GTK_REKEYING_ENABLE
|
||||
#define EXAMPLE_GTK_REKEY_INTERVAL CONFIG_ESP_GTK_REKEY_INTERVAL
|
||||
#else
|
||||
#define EXAMPLE_GTK_REKEY_INTERVAL 0
|
||||
#endif
|
||||
|
||||
static const char *TAG = "wifi softAP";
|
||||
|
||||
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void* event_data)
|
||||
{
|
||||
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
|
||||
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
|
||||
ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
|
||||
MAC2STR(event->mac), event->aid);
|
||||
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
|
||||
ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d, reason=%d",
|
||||
MAC2STR(event->mac), event->aid, event->reason);
|
||||
}
|
||||
}
|
||||
|
||||
void wifi_init_softap(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_netif_create_default_wifi_ap();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
|
||||
ESP_EVENT_ANY_ID,
|
||||
&wifi_event_handler,
|
||||
NULL,
|
||||
NULL));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.ap = {
|
||||
.ssid = EXAMPLE_ESP_WIFI_SSID,
|
||||
.ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
|
||||
.channel = EXAMPLE_ESP_WIFI_CHANNEL,
|
||||
.password = EXAMPLE_ESP_WIFI_PASS,
|
||||
.max_connection = EXAMPLE_MAX_STA_CONN,
|
||||
#ifdef CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT
|
||||
.authmode = WIFI_AUTH_WPA3_PSK,
|
||||
.sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
|
||||
#else /* CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT */
|
||||
.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
#endif
|
||||
.pmf_cfg = {
|
||||
.required = true,
|
||||
},
|
||||
#ifdef CONFIG_ESP_WIFI_BSS_MAX_IDLE_SUPPORT
|
||||
.bss_max_idle_cfg = {
|
||||
.period = WIFI_AP_DEFAULT_MAX_IDLE_PERIOD,
|
||||
.protected_keep_alive = 1,
|
||||
},
|
||||
#endif
|
||||
.gtk_rekey_interval = EXAMPLE_GTK_REKEY_INTERVAL,
|
||||
},
|
||||
};
|
||||
if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
|
||||
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d",
|
||||
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS, EXAMPLE_ESP_WIFI_CHANNEL);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
//Initialize NVS
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
ESP_LOGI(TAG, "ESP_WIFI_MODE_AP");
|
||||
wifi_init_softap();
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
/*
|
||||
* protocol.c - 协议层实现
|
||||
* T2.1, T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用软件 AES 实现 + 硬件 RNG
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "esp_random.h"
|
||||
#include "app_config.h"
|
||||
#include "protocol.h"
|
||||
#include "debug_log.h"
|
||||
|
||||
static bool s_protocol_initialized = false;
|
||||
|
||||
// ========== 软件 AES-128 实现 ==========
|
||||
|
||||
// AES S-Box
|
||||
static const uint8_t sbox[256] = {
|
||||
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
|
||||
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
|
||||
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
|
||||
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
|
||||
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
|
||||
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
|
||||
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
|
||||
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
|
||||
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
|
||||
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
|
||||
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
|
||||
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
|
||||
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
|
||||
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
|
||||
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
|
||||
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
|
||||
};
|
||||
|
||||
// AES 逆 S-Box
|
||||
static const uint8_t rsbox[256] = {
|
||||
0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
|
||||
0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
|
||||
0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
|
||||
0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
|
||||
0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
|
||||
0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
|
||||
0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
|
||||
0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
|
||||
0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
|
||||
0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
|
||||
0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
|
||||
0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
|
||||
0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
|
||||
0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
|
||||
0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
|
||||
0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d
|
||||
};
|
||||
|
||||
// AES Rcon
|
||||
static const uint8_t Rcon[11] = {
|
||||
0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36
|
||||
};
|
||||
|
||||
// AES 状态操作
|
||||
#define AES_BLOCK_SIZE 16
|
||||
|
||||
typedef struct {
|
||||
uint8_t round_key[176]; // 10 rounds * 16 bytes + initial key
|
||||
} aes128_context_t;
|
||||
|
||||
// GF(2^8) 乘法
|
||||
static uint8_t gmul(uint8_t a, uint8_t b)
|
||||
{
|
||||
uint8_t p = 0, hbit;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (b & 1) {
|
||||
p ^= a;
|
||||
}
|
||||
hbit = a & 0x80;
|
||||
a <<= 1;
|
||||
if (hbit) {
|
||||
a ^= 0x1b; // 模不可约多项式 x^8 + x^4 + x^3 + x + 1
|
||||
}
|
||||
b >>= 1;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// 密钥扩展
|
||||
static void aes128_key_expansion(const uint8_t *key, aes128_context_t *ctx)
|
||||
{
|
||||
uint8_t temp[4];
|
||||
memcpy(ctx->round_key, key, 16);
|
||||
|
||||
for (int i = 4; i < 44; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
temp[j] = ctx->round_key[(i - 1) * 4 + j];
|
||||
}
|
||||
|
||||
if (i % 4 == 0) {
|
||||
// RotWord
|
||||
uint8_t t = temp[0];
|
||||
temp[0] = temp[1];
|
||||
temp[1] = temp[2];
|
||||
temp[2] = temp[3];
|
||||
temp[3] = t;
|
||||
|
||||
// SubWord
|
||||
temp[0] = sbox[temp[0]];
|
||||
temp[1] = sbox[temp[1]];
|
||||
temp[2] = sbox[temp[2]];
|
||||
temp[3] = sbox[temp[3]];
|
||||
|
||||
temp[0] ^= Rcon[i / 4];
|
||||
}
|
||||
|
||||
for (int j = 0; j < 4; j++) {
|
||||
ctx->round_key[i * 4 + j] = ctx->round_key[(i - 4) * 4 + j] ^ temp[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 字节替换
|
||||
static void sub_bytes(uint8_t *state)
|
||||
{
|
||||
for (int i = 0; i < 16; i++) {
|
||||
state[i] = sbox[state[i]];
|
||||
}
|
||||
}
|
||||
|
||||
// 逆字节替换
|
||||
static void inv_sub_bytes(uint8_t *state)
|
||||
{
|
||||
for (int i = 0; i < 16; i++) {
|
||||
state[i] = rsbox[state[i]];
|
||||
}
|
||||
}
|
||||
|
||||
// 行移位
|
||||
static void shift_rows(uint8_t *state)
|
||||
{
|
||||
uint8_t temp;
|
||||
|
||||
// 第1行左移1位
|
||||
temp = state[1];
|
||||
state[1] = state[5];
|
||||
state[5] = state[9];
|
||||
state[9] = state[13];
|
||||
state[13] = temp;
|
||||
|
||||
// 第2行左移2位
|
||||
temp = state[2];
|
||||
state[2] = state[10];
|
||||
state[10] = temp;
|
||||
temp = state[6];
|
||||
state[6] = state[14];
|
||||
state[14] = temp;
|
||||
|
||||
// 第3行左移3位
|
||||
temp = state[15];
|
||||
state[15] = state[11];
|
||||
state[11] = state[7];
|
||||
state[7] = state[3];
|
||||
state[3] = temp;
|
||||
}
|
||||
|
||||
// 逆行移位
|
||||
static void inv_shift_rows(uint8_t *state)
|
||||
{
|
||||
uint8_t temp;
|
||||
|
||||
// 第1行右移1位
|
||||
temp = state[13];
|
||||
state[13] = state[9];
|
||||
state[9] = state[5];
|
||||
state[5] = state[1];
|
||||
state[1] = temp;
|
||||
|
||||
// 第2行右移2位
|
||||
temp = state[2];
|
||||
state[2] = state[10];
|
||||
state[10] = temp;
|
||||
temp = state[6];
|
||||
state[6] = state[14];
|
||||
state[14] = temp;
|
||||
|
||||
// 第3行右移3位
|
||||
temp = state[3];
|
||||
state[3] = state[7];
|
||||
state[7] = state[11];
|
||||
state[11] = state[15];
|
||||
state[15] = temp;
|
||||
}
|
||||
|
||||
// 列混合
|
||||
static void mix_columns(uint8_t *state)
|
||||
{
|
||||
uint8_t temp[16];
|
||||
memcpy(temp, state, 16);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int c = i * 4;
|
||||
state[c] = gmul(2, temp[c]) ^ gmul(3, temp[c + 1]) ^ temp[c + 2] ^ temp[c + 3];
|
||||
state[c + 1] = temp[c] ^ gmul(2, temp[c + 1]) ^ gmul(3, temp[c + 2]) ^ temp[c + 3];
|
||||
state[c + 2] = temp[c] ^ temp[c + 1] ^ gmul(2, temp[c + 2]) ^ gmul(3, temp[c + 3]);
|
||||
state[c + 3] = gmul(3, temp[c]) ^ temp[c + 1] ^ temp[c + 2] ^ gmul(2, temp[c + 3]);
|
||||
}
|
||||
}
|
||||
|
||||
// 逆列混合
|
||||
static void inv_mix_columns(uint8_t *state)
|
||||
{
|
||||
uint8_t temp[16];
|
||||
memcpy(temp, state, 16);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int c = i * 4;
|
||||
state[c] = gmul(14, temp[c]) ^ gmul(11, temp[c + 1]) ^ gmul(13, temp[c + 2]) ^ gmul(9, temp[c + 3]);
|
||||
state[c + 1] = gmul(9, temp[c]) ^ gmul(14, temp[c + 1]) ^ gmul(11, temp[c + 2]) ^ gmul(13, temp[c + 3]);
|
||||
state[c + 2] = gmul(13, temp[c]) ^ gmul(9, temp[c + 1]) ^ gmul(14, temp[c + 2]) ^ gmul(11, temp[c + 3]);
|
||||
state[c + 3] = gmul(11, temp[c]) ^ gmul(13, temp[c + 1]) ^ gmul(9, temp[c + 2]) ^ gmul(14, temp[c + 3]);
|
||||
}
|
||||
}
|
||||
|
||||
// 轮密钥加
|
||||
static void add_round_key(uint8_t *state, const uint8_t *round_key)
|
||||
{
|
||||
for (int i = 0; i < 16; i++) {
|
||||
state[i] ^= round_key[i];
|
||||
}
|
||||
}
|
||||
|
||||
// AES-128 加密单个块
|
||||
static void aes128_encrypt_block(const uint8_t *input, uint8_t *output, const aes128_context_t *ctx)
|
||||
{
|
||||
uint8_t state[16];
|
||||
|
||||
// 复制输入到状态(列优先顺序)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
state[i * 4 + j] = input[j * 4 + i];
|
||||
}
|
||||
}
|
||||
|
||||
// 初始轮密钥加
|
||||
add_round_key(state, ctx->round_key);
|
||||
|
||||
// 9轮加密
|
||||
for (int round = 1; round < 10; round++) {
|
||||
sub_bytes(state);
|
||||
shift_rows(state);
|
||||
mix_columns(state);
|
||||
add_round_key(state, ctx->round_key + round * 16);
|
||||
}
|
||||
|
||||
// 最后一轮(无列混合)
|
||||
sub_bytes(state);
|
||||
shift_rows(state);
|
||||
add_round_key(state, ctx->round_key + 10 * 16);
|
||||
|
||||
// 复制状态到输出(列优先顺序)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
output[j * 4 + i] = state[i * 4 + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AES-128 解密单个块
|
||||
static void aes128_decrypt_block(const uint8_t *input, uint8_t *output, const aes128_context_t *ctx)
|
||||
{
|
||||
uint8_t state[16];
|
||||
|
||||
// 复制输入到状态(列优先顺序)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
state[i * 4 + j] = input[j * 4 + i];
|
||||
}
|
||||
}
|
||||
|
||||
// 初始轮密钥加
|
||||
add_round_key(state, ctx->round_key + 10 * 16);
|
||||
|
||||
// 9轮解密
|
||||
for (int round = 9; round >= 1; round--) {
|
||||
inv_shift_rows(state);
|
||||
inv_sub_bytes(state);
|
||||
add_round_key(state, ctx->round_key + round * 16);
|
||||
inv_mix_columns(state);
|
||||
}
|
||||
|
||||
// 最后一轮(无逆列混合)
|
||||
inv_shift_rows(state);
|
||||
inv_sub_bytes(state);
|
||||
add_round_key(state, ctx->round_key);
|
||||
|
||||
// 复制状态到输出(列优先顺序)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
output[j * 4 + i] = state[i * 4 + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CBC 模式加密
|
||||
static void aes128_cbc_encrypt(const uint8_t *input, uint8_t *output, size_t length,
|
||||
const uint8_t *iv, const aes128_context_t *ctx)
|
||||
{
|
||||
uint8_t prev[16];
|
||||
memcpy(prev, iv, 16);
|
||||
|
||||
for (size_t i = 0; i < length; i += 16) {
|
||||
uint8_t block[16];
|
||||
memcpy(block, input + i, 16);
|
||||
|
||||
// XOR with previous ciphertext (or IV for first block)
|
||||
for (int j = 0; j < 16; j++) {
|
||||
block[j] ^= prev[j];
|
||||
}
|
||||
|
||||
aes128_encrypt_block(block, output + i, ctx);
|
||||
memcpy(prev, output + i, 16);
|
||||
}
|
||||
}
|
||||
|
||||
// CBC 模式解密
|
||||
static void aes128_cbc_decrypt(const uint8_t *input, uint8_t *output, size_t length,
|
||||
const uint8_t *iv, const aes128_context_t *ctx)
|
||||
{
|
||||
uint8_t prev[16];
|
||||
memcpy(prev, iv, 16);
|
||||
|
||||
for (size_t i = 0; i < length; i += 16) {
|
||||
aes128_decrypt_block(input + i, output + i, ctx);
|
||||
|
||||
// XOR with previous ciphertext (or IV for first block)
|
||||
for (int j = 0; j < 16; j++) {
|
||||
output[i + j] ^= prev[j];
|
||||
}
|
||||
|
||||
memcpy(prev, input + i, 16);
|
||||
}
|
||||
}
|
||||
|
||||
void protocol_init(void)
|
||||
{
|
||||
if (s_protocol_initialized) return;
|
||||
|
||||
DEBUG_INFO("PROT", "初始化协议层(CRC16、软件AES)...");
|
||||
|
||||
s_protocol_initialized = true;
|
||||
DEBUG_INFO("PROT", "协议层就绪");
|
||||
}
|
||||
|
||||
// CRC16 Modbus(LE,低字节在前)
|
||||
uint16_t crc16_modbus(const uint8_t *data, size_t len)
|
||||
{
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (crc & 0x0001) {
|
||||
crc = (crc >> 1) ^ 0xA001;
|
||||
} else {
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (crc >> 8) | (crc << 8);
|
||||
}
|
||||
|
||||
// 构建 SPI 帧: SOF + CMD + LEN(2LE) + PAYLOAD + CRC16(2LE)
|
||||
int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame)
|
||||
{
|
||||
if (len > FRAME_MAX_PAYLOAD) {
|
||||
DEBUG_ERR("PROT", "Payload 太大: %zu > %d", len, FRAME_MAX_PAYLOAD);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t idx = 0;
|
||||
out_frame[idx++] = FRAME_SOF;
|
||||
out_frame[idx++] = cmd;
|
||||
out_frame[idx++] = (uint8_t)(len & 0xFF);
|
||||
out_frame[idx++] = (uint8_t)((len >> 8) & 0xFF);
|
||||
|
||||
if (payload && len > 0) {
|
||||
memcpy(&out_frame[idx], payload, len);
|
||||
idx += len;
|
||||
}
|
||||
|
||||
uint16_t crc = crc16_modbus(out_frame, idx);
|
||||
out_frame[idx++] = (uint8_t)(crc & 0xFF);
|
||||
out_frame[idx++] = (uint8_t)((crc >> 8) & 0xFF);
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
// 解析 SPI 帧
|
||||
int frame_parse(const uint8_t *frame, size_t frame_len,
|
||||
uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len)
|
||||
{
|
||||
if (frame_len < 6) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t idx = 0;
|
||||
|
||||
if (frame[idx++] != FRAME_SOF) {
|
||||
DEBUG_WARN("PROT", "无效 SOF: 0x%02X", frame[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t cmd = frame[idx++];
|
||||
|
||||
size_t payload_len = frame[idx] | (frame[idx + 1] << 8);
|
||||
idx += 2;
|
||||
|
||||
if (payload_len > FRAME_MAX_PAYLOAD) {
|
||||
DEBUG_ERR("PROT", "Payload 长度太大: %zu", payload_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t expected_len = 1 + 1 + 2 + payload_len + 2;
|
||||
if (frame_len < expected_len) {
|
||||
DEBUG_WARN("PROT", "帧太短: %zu < %zu", frame_len, expected_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint16_t received_crc = frame[idx + payload_len] | (frame[idx + payload_len + 1] << 8);
|
||||
uint16_t calc_crc = crc16_modbus(frame, idx + payload_len);
|
||||
|
||||
if (received_crc != calc_crc) {
|
||||
DEBUG_WARN("PROT", "CRC 不匹配: 0x%04X vs 0x%04X", received_crc, calc_crc);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (out_payload && out_len) {
|
||||
memcpy(out_payload, &frame[idx], payload_len);
|
||||
*out_len = payload_len;
|
||||
}
|
||||
if (out_cmd) {
|
||||
*out_cmd = cmd;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// AES-128 CBC 加密
|
||||
// 注意: 函数名带 protocol_ 前缀以避免与 ESP-IDF wpa_supplicant 组件冲突
|
||||
int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *ciphertext_out)
|
||||
{
|
||||
size_t padded_len = ((len + 15) / 16) * 16;
|
||||
uint8_t padded[256];
|
||||
|
||||
memcpy(padded, plaintext, len);
|
||||
uint8_t pad = (uint8_t)(16 - (len % 16));
|
||||
memset(&padded[len], pad, pad);
|
||||
|
||||
aes128_context_t ctx;
|
||||
aes128_key_expansion(key, &ctx);
|
||||
aes128_cbc_encrypt(padded, ciphertext_out, padded_len, iv, &ctx);
|
||||
|
||||
return padded_len;
|
||||
}
|
||||
|
||||
// AES-128 CBC 解密
|
||||
// 注意: 函数名带 protocol_ 前缀以避免与 ESP-IDF wpa_supplicant 组件冲突
|
||||
int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *plaintext_out)
|
||||
{
|
||||
if (len % 16 != 0) {
|
||||
DEBUG_ERR("PROT", "Ciphertext 长度不是 16 的倍数: %zu", len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
aes128_context_t ctx;
|
||||
aes128_key_expansion(key, &ctx);
|
||||
aes128_cbc_decrypt(ciphertext, plaintext_out, len, iv, &ctx);
|
||||
|
||||
// 移除 PKCS#7 填充
|
||||
uint8_t pad = plaintext_out[len - 1];
|
||||
if (pad > 16) return -1;
|
||||
return len - pad;
|
||||
}
|
||||
|
||||
// 搅伴算法 - 生成随机数
|
||||
void scramble_generate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out)
|
||||
{
|
||||
uint8_t state[16];
|
||||
|
||||
memcpy(&state[0], ×tamp, 4);
|
||||
memcpy(&state[4], &mem_addr, 4);
|
||||
memcpy(&state[8], &rand_seed, 4);
|
||||
memcpy(&state[12], SCRAMBLE_CONST, 4);
|
||||
|
||||
for (int round = 0; round < 3; round++) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
state[i] ^= SCRAMBLE_CONST[i];
|
||||
state[i] = (state[i] << 1) | (state[i] >> 7);
|
||||
}
|
||||
}
|
||||
|
||||
for (int round = 0; round < 2; round++) {
|
||||
uint32_t *words = (uint32_t *)state;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
words[i] ^= SCRAMBLE_CONST[i * 4 % 16] | ((uint32_t)SCRAMBLE_CONST[i * 4 + 1] << 8);
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(nonce_out, state, 16);
|
||||
}
|
||||
|
||||
// 会话密钥初始化
|
||||
int session_key_init(session_ctx_t *sess, const uint8_t *k_session)
|
||||
{
|
||||
if (!sess || !k_session) return -1;
|
||||
|
||||
memset(sess, 0, sizeof(session_ctx_t));
|
||||
memcpy(sess->key, k_session, 16);
|
||||
memset(sess->iv, 0, 16);
|
||||
sess->used = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void session_key_clear(session_ctx_t *sess)
|
||||
{
|
||||
if (sess) {
|
||||
memset(sess->key, 0, 16);
|
||||
memset(sess->iv, 0, 16);
|
||||
sess->used = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out)
|
||||
{
|
||||
if (!sess || !in || !out) return -1;
|
||||
|
||||
uint8_t iv_copy[16];
|
||||
memcpy(iv_copy, sess->iv, 16);
|
||||
|
||||
int ret = protocol_aes_encrypt(in, len, sess->key, iv_copy, out);
|
||||
|
||||
if (ret > 0) {
|
||||
memcpy(sess->iv, &out[ret - 16], 16);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out)
|
||||
{
|
||||
if (!sess || !in || !out) return -1;
|
||||
|
||||
uint8_t iv_copy[16];
|
||||
memcpy(iv_copy, sess->iv, 16);
|
||||
|
||||
int ret = protocol_aes_decrypt(in, len, sess->key, iv_copy, out);
|
||||
|
||||
if (ret > 0) {
|
||||
memcpy(sess->iv, &in[len - 16], 16);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// 使用 ESP-IDF 硬件 RNG 生成随机数
|
||||
void generate_random_nonce(uint8_t *nonce, size_t len)
|
||||
{
|
||||
esp_fill_random(nonce, len);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* protocol.h - 帧格式、CRC16、AES 加密解密
|
||||
* T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用软件 AES 实现 + 硬件 RNG
|
||||
*/
|
||||
#ifndef PROTOCOL_H
|
||||
#define PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "app_config.h"
|
||||
|
||||
// 帧格式: SOF(1B) + CMD(1B) + LEN(2B LE) + PAYLOAD(LEN) + CRC16(2B LE)
|
||||
#define FRAME_SOF (0xEB) // 帧起始标记
|
||||
#define FRAME_MAX_PAYLOAD (64) // 安全最大 payload
|
||||
#define FRAME_OVERHEAD (6) // SOF + CMD + LEN(2) + CRC16(2)
|
||||
|
||||
// 命令码(对应 GD32 OTA 协议)
|
||||
#define CMD_HANDSHAKE_REQ (0xF0) // 握手请求
|
||||
#define CMD_CHALLENGE (0xF1) // 挑战/随机数
|
||||
#define CMD_SESSION_KEY (0xF2) // 会话密钥
|
||||
#define CMD_SEND_SIZE (0xF3) // 发送固件大小
|
||||
#define CMD_DATA_PACKET (0xF4) // 数据包
|
||||
#define CMD_DATA_END (0xF5) // 数据结束
|
||||
#define CMD_TRANSFER_DONE (0xF6) // 传输完成
|
||||
#define CMD_FINISH_OTA (0xF7) // 结束 OTA
|
||||
#define CMD_ACK (0x80) // 通用 ACK
|
||||
#define CMD_ERROR (0x81) // 通用错误
|
||||
|
||||
// ACK 状态码
|
||||
#define ACK_OK (0x00) // 成功
|
||||
#define ACK_UNSUPPORTED (0x01) // 命令不支持
|
||||
#define ACK_AUTH_FAILED (0x02) // 认证失败
|
||||
#define ACK_SIZE_OOR (0x03) // 大小超出范围
|
||||
#define ACK_CRC_FAILED (0x04) // CRC 校验失败
|
||||
#define ACK_RETRY_EXCEED (0x05) // 重试次数超限
|
||||
#define ACK_SESSION_INVALID (0x06) // 会话密钥无效
|
||||
#define ACK_FLASH_FAILED (0x07) // Flash 写入失败
|
||||
|
||||
// 安全常量(定义在 app_config.h 中)
|
||||
inline constexpr size_t MASTER_KEY_HERE_LEN = 16;
|
||||
inline constexpr size_t SCRAMBLE_CONST_HERE_LEN = 16;
|
||||
inline constexpr size_t CHIP_UID_LEN = 12;
|
||||
|
||||
// AES 会话上下文
|
||||
typedef struct {
|
||||
uint8_t key[16]; // 会话密钥 K_session
|
||||
uint8_t iv[16]; // CBC 模式 IV
|
||||
int used;
|
||||
} session_ctx_t;
|
||||
|
||||
// 初始化协议层
|
||||
void protocol_init(void);
|
||||
|
||||
// CRC16 Modbus 计算(低字节在前,LE)
|
||||
uint16_t crc16_modbus(const uint8_t *data, size_t len);
|
||||
|
||||
// 构建帧: 返回总帧长度
|
||||
// 输出格式: SOF + CMD + LEN(2) + PAYLOAD + CRC16(2)
|
||||
int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame);
|
||||
|
||||
// 解析帧: 返回 payload 起始偏移,失败返回 -1
|
||||
// 检查 SOF,验证 CRC16,提取 CMD 和 LEN
|
||||
int frame_parse(const uint8_t *frame, size_t frame_len,
|
||||
uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len);
|
||||
|
||||
// T2.2: AES-128 CBC 加密
|
||||
// 使用预设主密钥或会话密钥
|
||||
// 注意: 函数名带 protocol_ 前缀以避免与 ESP-IDF wpa_supplicant 组件冲突
|
||||
int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *ciphertext_out);
|
||||
|
||||
int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *plaintext_out);
|
||||
|
||||
// T2.1: 搅伴算法 - 生成随机数
|
||||
// 输入: 时间戳 + 内存地址 + rand_seed
|
||||
// 输出: 16 字节 nonce_server
|
||||
void scramble_generate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out);
|
||||
|
||||
// T2.4: 会话密钥管理
|
||||
int session_key_init(session_ctx_t *sess, const uint8_t *k_session);
|
||||
void session_key_clear(session_ctx_t *sess);
|
||||
int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out);
|
||||
int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out);
|
||||
|
||||
// 使用 ESP-IDF 硬件 RNG 生成随机数
|
||||
void generate_random_nonce(uint8_t *nonce, size_t len);
|
||||
|
||||
#endif // PROTOCOL_H
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* spi_master.c - SPI Master 驱动实现
|
||||
* T1.1 - T1.4
|
||||
* ESP-IDF 6.0 适配版本
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "soc/spi_struct.h"
|
||||
|
||||
#include "spi_master.h"
|
||||
#include "debug_log.h"
|
||||
|
||||
spi_device_handle_t g_spi_device = NULL;
|
||||
|
||||
// SPI2 总线初始化
|
||||
esp_err_t spi_master_init(void)
|
||||
{
|
||||
DEBUG_INFO("SPI", "初始化 SPI2 (VSPI)...");
|
||||
|
||||
spi_bus_config_t bus_cfg = {};
|
||||
bus_cfg.mosi_io_num = 8;
|
||||
bus_cfg.miso_io_num = 7;
|
||||
bus_cfg.sclk_io_num = 6;
|
||||
bus_cfg.quadwp_io_num = -1;
|
||||
bus_cfg.quadhd_io_num = -1;
|
||||
bus_cfg.max_transfer_sz = 256;
|
||||
|
||||
esp_err_t ret = spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO);
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_ERR("SPI", "SPI 总线初始化失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
spi_device_interface_config_t dev_cfg = {};
|
||||
dev_cfg.clock_speed_hz = SPI_CLOCK_HZ;
|
||||
dev_cfg.mode = SPI_MODE;
|
||||
dev_cfg.spics_io_num = SPI_MASTER_CS_GPIO;
|
||||
dev_cfg.queue_size = SPI_QUEUE_SIZE;
|
||||
dev_cfg.command_bits = 0;
|
||||
dev_cfg.address_bits = 0;
|
||||
dev_cfg.dummy_bits = 0;
|
||||
dev_cfg.cs_ena_posttrans = 0;
|
||||
dev_cfg.cs_ena_pretrans = 0;
|
||||
|
||||
ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &g_spi_device);
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_ERR("SPI", "添加 SPI 设备失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
DEBUG_INFO("SPI", "SPI2 已初始化: CLK=40MHz, CS=GPIO4");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// 带超时的同步 SPI 传输
|
||||
// ESP-IDF 6.0: spi_device_transmit() 不再接受超时参数
|
||||
// 使用 spi_device_queue_trans() + spi_device_get_trans_result() 实现超时
|
||||
esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms)
|
||||
{
|
||||
if (g_spi_device == NULL) {
|
||||
DEBUG_ERR("SPI", "SPI 设备未初始化");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
spi_transaction_t t = {};
|
||||
t.cmd = cmd;
|
||||
t.length = payload_len * 8;
|
||||
t.rxlength = recv_len * 8;
|
||||
t.tx_buffer = payload;
|
||||
t.rx_buffer = recv_buf;
|
||||
t.user = (void *)(uintptr_t)cmd;
|
||||
|
||||
/* ESP-IDF 6.0: 使用 spi_device_transmit() 无超时参数
|
||||
* 如需超时控制,使用 spi_device_queue_trans() + spi_device_get_trans_result()
|
||||
*/
|
||||
esp_err_t ret = spi_device_transmit(g_spi_device, &t);
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_WARN("SPI", "SPI 发送 cmd=0x%02X 失败: %s", cmd, esp_err_to_name(ret));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// 仅发送
|
||||
esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len)
|
||||
{
|
||||
return spi_sync_transfer(cmd, payload, len, NULL, 0, 3000);
|
||||
}
|
||||
|
||||
// 仅接收
|
||||
esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms)
|
||||
{
|
||||
uint8_t dummy[64] = {0};
|
||||
memset(dummy, 0xFF, sizeof(dummy));
|
||||
return spi_sync_transfer(cmd, dummy, len, recv_buf, len, timeout_ms);
|
||||
}
|
||||
|
||||
// 带重试的 SPI 传输
|
||||
esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, int max_retries)
|
||||
{
|
||||
esp_err_t ret;
|
||||
int retry_count = 0;
|
||||
|
||||
while (retry_count < max_retries) {
|
||||
ret = spi_sync_transfer(cmd, payload, payload_len,
|
||||
recv_buf, recv_len, 3000);
|
||||
if (ret == ESP_OK) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
retry_count++;
|
||||
DEBUG_WARN("SPI", "SPI 重试 %d/%d, cmd=0x%02X", retry_count, max_retries, cmd);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
DEBUG_ERR("SPI", "SPI 传输 %d 次重试后失败", max_retries);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* spi_master.h - SPI Master 驱动头文件
|
||||
*/
|
||||
#ifndef SPI_MASTER_H
|
||||
#define SPI_MASTER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
// SPI 配置常量
|
||||
#define SPI_MASTER_CS_GPIO (4) // CS 片选引脚
|
||||
#define SPI_CLOCK_HZ (40 * 1000 * 1000) // 40MHz
|
||||
#define SPI_MODE (0) // CPOL=0, CPHA=0
|
||||
#define SPI_QUEUE_SIZE (1)
|
||||
|
||||
// SPI 设备句柄(外部引用)
|
||||
extern spi_device_handle_t g_spi_device;
|
||||
|
||||
// 初始化 SPI2 (VSPI) 总线和设备
|
||||
esp_err_t spi_master_init(void);
|
||||
|
||||
// 同步 SPI 传输(阻塞式)
|
||||
// 发送 cmd + payload,接收响应到 recv_buf
|
||||
// 返回: ESP_OK 成功, ESP_ERR_TIMEOUT 超时
|
||||
esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms);
|
||||
|
||||
// 仅发送(不期望接收)
|
||||
esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len);
|
||||
|
||||
// 仅接收(发送哑数据)
|
||||
esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms);
|
||||
|
||||
#endif // SPI_MASTER_H
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
# build xxx_default
|
||||
CONFIG_ESP_WIFI_SSID="ssid_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
CONFIG_ESP_WIFI_PASSWORD="password_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
CONFIG_IDF_TARGET="esp32c3"
|
||||
CONFIG_ESP32C3_REV_MIN_101=y
|
||||
CONFIG_ESP_WIFI_SSID="ssid_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
CONFIG_ESP_WIFI_PASSWORD="password_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
CONFIG_IDF_TARGET="esp32c2"
|
||||
CONFIG_XTAL_FREQ_26=y
|
||||
CONFIG_ESP_WIFI_SSID="ssid_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
CONFIG_ESP_WIFI_PASSWORD="password_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
CONFIG_IDF_TARGET="esp32c2"
|
||||
CONFIG_XTAL_FREQ_26=y
|
||||
CONFIG_ESP32C2_REV_MIN_200=y
|
||||
CONFIG_ESP_WIFI_SSID="ssid_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
CONFIG_ESP_WIFI_PASSWORD="password_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=n
|
||||
CONFIG_ESP_WIFI_SSID="ssid_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
CONFIG_ESP_WIFI_PASSWORD="password_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
@@ -0,0 +1,656 @@
|
||||
"""
|
||||
ESP32-C3 OTA 上位机工具
|
||||
通过 WiFi 连接 ESP32,发送 .hex 固件文件并触发 OTA
|
||||
|
||||
依赖: Python 3.8+, tkinter (标准库), requests
|
||||
|
||||
用法:
|
||||
python ota_host_tool.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import socket
|
||||
import threading
|
||||
import traceback
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
# 尝试导入 tkinter,失败时给出提示
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
except ImportError:
|
||||
print("错误: 缺少 tkinter 模块")
|
||||
print("Linux/Mac 请安装: pip install tkinter")
|
||||
print("Windows 通常自带 tkinter")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("错误: 缺少 requests 模块")
|
||||
print("请安装: pip install requests")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ===================== 配置 =====================
|
||||
ESP32_IP = "192.168.4.1" # ESP32 在 AP 模式下的 IP
|
||||
ESP32_PORT = 80 # HTTP 端口
|
||||
DEBUG_LOG_IP = "0.0.0.0" # 监听所有接口
|
||||
DEBUG_LOG_PORT = 6666 # UDP 日志监听端口
|
||||
TARGET_CHIP = "GD32E508" # 目标芯片(用于显示)
|
||||
CHUNK_SIZE = 4096 # 每包大小(字节)
|
||||
# =============================================
|
||||
|
||||
|
||||
class LogHandler:
|
||||
"""UDP 日志接收器"""
|
||||
|
||||
def __init__(self, text_widget: 'tk.Text', port: int = DEBUG_LOG_PORT):
|
||||
self.text_widget = text_widget
|
||||
self.port = port
|
||||
self.running = False
|
||||
self.sock: Optional[socket.socket] = None
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
|
||||
# 颜色标签
|
||||
self.tag_configs = {
|
||||
'[E]': {'foreground': '#FF4444', 'background': '#FFE4E4'},
|
||||
'[W]': {'foreground': '#FF8800', 'background': '#FFF4E4'},
|
||||
'[I]': {'foreground': '#0088FF', 'background': '#E4F0FF'},
|
||||
'[D]': {'foreground': '#888888', 'background': '#F0F0F0'},
|
||||
'[C]': {'foreground': '#00CC66', 'background': '#E4FFE4'},
|
||||
}
|
||||
|
||||
def start(self):
|
||||
"""启动日志接收线程"""
|
||||
self.running = True
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.sock.bind((DEBUG_LOG_IP, self.port))
|
||||
self.sock.settimeout(0.5)
|
||||
|
||||
self.thread = threading.Thread(target=self._receive_loop, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""停止日志接收"""
|
||||
self.running = False
|
||||
if self.sock:
|
||||
self.sock.close()
|
||||
if self.thread:
|
||||
self.thread.join(timeout=1)
|
||||
|
||||
def _receive_loop(self):
|
||||
"""日志接收循环"""
|
||||
while self.running:
|
||||
try:
|
||||
data, addr = self.sock.recvfrom(1024)
|
||||
msg = data.decode('utf-8', errors='replace').strip()
|
||||
if msg:
|
||||
self._append_log(msg)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _append_log(self, msg: str):
|
||||
"""在 Text 组件中追加日志"""
|
||||
# 根据日志级别应用颜色
|
||||
applied = False
|
||||
for prefix, config in self.tag_configs.items():
|
||||
if f'{prefix}' in msg or prefix in msg:
|
||||
self.text_widget.insert('end', msg + '\n', prefix)
|
||||
applied = True
|
||||
break
|
||||
|
||||
if not applied:
|
||||
self.text_widget.insert('end', msg + '\n')
|
||||
|
||||
# 自动滚动到底部
|
||||
self.text_widget.see('end')
|
||||
|
||||
|
||||
class IntelHexParser:
|
||||
"""Intel HEX 文件解析器
|
||||
|
||||
支持标准 Intel HEX 格式(:BB AAAA TT DD... CC)
|
||||
"""
|
||||
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
self.data: List[Optional[bytes]] = [] # 按地址索引的字节数据
|
||||
self.max_addr = 0
|
||||
self.min_addr = 0xFFFFFFFF
|
||||
|
||||
def parse(self) -> Tuple[bool, str]:
|
||||
"""解析 HEX 文件,返回 (成功标志, 消息)"""
|
||||
try:
|
||||
with open(self.filepath, 'r', encoding='utf-8') as f:
|
||||
for line_no, line in enumerate(f, 1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
ok, msg = self._parse_line(line, line_no)
|
||||
if not ok:
|
||||
return False, msg
|
||||
|
||||
# 计算实际数据范围
|
||||
if self.min_addr == 0xFFFFFFFF:
|
||||
return False, "HEX 文件为空或不包含数据"
|
||||
|
||||
return True, f"解析成功,地址范围: 0x{self.min_addr:06X} - 0x{self.max_addr:06X},共 {self.max_addr - self.min_addr + 1} 字节"
|
||||
|
||||
except FileNotFoundError:
|
||||
return False, f"文件不存在: {self.filepath}"
|
||||
except Exception as e:
|
||||
return False, f"解析失败: {e}"
|
||||
|
||||
def _parse_line(self, line: str, line_no: int) -> Tuple[bool, str]:
|
||||
"""解析单行 HEX"""
|
||||
if not line.startswith(':'):
|
||||
return False, f"第 {line_no} 行格式错误,缺少 ':' 前缀"
|
||||
|
||||
if len(line) < 11:
|
||||
return False, f"第 {line_no} 行长度不足"
|
||||
|
||||
try:
|
||||
# 解析头部
|
||||
byte_count = int(line[1:3], 16)
|
||||
address = int(line[3:7], 16)
|
||||
record_type = int(line[7:9], 16)
|
||||
|
||||
# 验证数据长度
|
||||
expected_len = 9 + byte_count * 2 + 2
|
||||
if len(line) != expected_len:
|
||||
return False, f"第 {line_no} 行长度不匹配,期望 {expected_len} 实际 {len(line)}"
|
||||
|
||||
# 解析数据
|
||||
data_bytes = bytes.fromhex(line[9:9 + byte_count * 2])
|
||||
|
||||
# 验证 CRC
|
||||
checksum = int(line[-2:], 16)
|
||||
calc_checksum = self._calc_checksum(line[1:-2])
|
||||
if calc_checksum != checksum:
|
||||
return False, f"第 {line_no} 行 CRC 校验失败"
|
||||
|
||||
except ValueError as e:
|
||||
return False, f"第 {line_no} 行数据解析错误: {e}"
|
||||
|
||||
# 处理不同类型的记录
|
||||
if record_type == 0x00:
|
||||
# 数据记录
|
||||
abs_addr = address
|
||||
if self.min_addr == 0xFFFFFFFF or abs_addr < self.min_addr:
|
||||
self.min_addr = abs_addr
|
||||
if abs_addr + byte_count - 1 > self.max_addr:
|
||||
self.max_addr = abs_addr + byte_count - 1
|
||||
|
||||
# 存储数据(简化处理:直接存储)
|
||||
if len(self.data) < abs_addr + byte_count:
|
||||
self.data.extend([None] * (abs_addr + byte_count - len(self.data)))
|
||||
for i, b in enumerate(data_bytes):
|
||||
self.data[abs_addr + i] = b
|
||||
|
||||
elif record_type == 0x01:
|
||||
# 文件结束记录
|
||||
pass
|
||||
|
||||
elif record_type == 0x02:
|
||||
# 扩展段地址
|
||||
pass
|
||||
|
||||
elif record_type == 0x04:
|
||||
# 扩展线性地址
|
||||
pass
|
||||
|
||||
return True, ""
|
||||
|
||||
def _calc_checksum(self, hex_str: str) -> int:
|
||||
"""计算校验和"""
|
||||
return sum(int(hex_str[i:i+2], 16) for i in range(0, len(hex_str), 2)) & 0xFF
|
||||
|
||||
def get_data_segment(self, start: int, length: int) -> bytes:
|
||||
"""获取指定范围的数据,不足部分填充 0xFF"""
|
||||
result = bytearray()
|
||||
for i in range(length):
|
||||
addr = start + i
|
||||
if addr < len(self.data) and self.data[addr] is not None:
|
||||
result.append(self.data[addr])
|
||||
else:
|
||||
result.append(0xFF)
|
||||
return bytes(result)
|
||||
|
||||
def get_total_size(self) -> int:
|
||||
"""获取总数据大小"""
|
||||
if self.min_addr == 0xFFFFFFFF:
|
||||
return 0
|
||||
return self.max_addr - self.min_addr + 1
|
||||
|
||||
|
||||
class OTAClient:
|
||||
"""OTA 客户端,与 ESP32 通信"""
|
||||
|
||||
def __init__(self, esp_ip: str = ESP32_IP, esp_port: int = ESP32_PORT):
|
||||
self.esp_ip = esp_ip
|
||||
self.esp_port = esp_port
|
||||
self.base_url = f"http://{esp_ip}:{esp_port}"
|
||||
|
||||
def upload_firmware(self, filepath: str,
|
||||
progress_callback=None,
|
||||
chunk_size: int = CHUNK_SIZE) -> Tuple[bool, str]:
|
||||
"""上传固件到 ESP32
|
||||
|
||||
Args:
|
||||
filepath: HEX 文件路径
|
||||
progress_callback: 进度回调函数,参数为 (当前字节, 总字节)
|
||||
|
||||
Returns:
|
||||
(成功标志, 消息)
|
||||
"""
|
||||
# 解析 HEX 文件
|
||||
parser = IntelHexParser(filepath)
|
||||
ok, msg = parser.parse()
|
||||
if not ok:
|
||||
return False, msg
|
||||
|
||||
total_size = parser.get_total_size()
|
||||
print(f"[OTA] 固件大小: {total_size} 字节")
|
||||
|
||||
# 分块发送
|
||||
addr = parser.min_addr
|
||||
sent_bytes = 0
|
||||
seq = 0
|
||||
|
||||
while addr <= parser.max_addr:
|
||||
# 计算本块大小
|
||||
remaining = parser.max_addr - addr + 1
|
||||
chunk = min(chunk_size, remaining)
|
||||
|
||||
# 读取数据
|
||||
data = parser.get_data_segment(addr, chunk)
|
||||
|
||||
# 构造数据包: SEQ(2B) + SIZE(4B) + DATA
|
||||
packet = struct.pack('<H', seq) + struct.pack('<I', total_size) + data
|
||||
|
||||
# 发送
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.base_url}/ota/upload",
|
||||
data=packet,
|
||||
timeout=10,
|
||||
headers={'Content-Type': 'application/octet-stream'}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return False, f"上传失败,HTTP {response.status_code}: {response.text}"
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return False, f"网络错误: {e}"
|
||||
|
||||
sent_bytes += chunk
|
||||
addr += chunk
|
||||
seq += 1
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(sent_bytes, total_size)
|
||||
|
||||
# 检查存储完成状态
|
||||
status_ok, status_msg = self.check_status()
|
||||
if not status_ok:
|
||||
return False, f"固件已发送但状态检查失败: {status_msg}"
|
||||
|
||||
return True, f"固件上传完成,共 {seq} 包,{sent_bytes} 字节"
|
||||
|
||||
def trigger_relay(self) -> Tuple[bool, str]:
|
||||
"""触发 ESP32 开始 OTA 中继"""
|
||||
try:
|
||||
response = requests.post(f"{self.base_url}/ota/relay", timeout=5)
|
||||
if response.status_code == 200:
|
||||
return True, "OTA 中继已触发"
|
||||
return False, f"触发失败: HTTP {response.status_code}"
|
||||
except Exception as e:
|
||||
return False, f"网络错误: {e}"
|
||||
|
||||
def check_status(self) -> Tuple[bool, str]:
|
||||
"""查询 OTA 状态"""
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/ota/status", timeout=5)
|
||||
if response.status_code == 200:
|
||||
return True, response.text
|
||||
return False, f"查询失败: HTTP {response.status_code}"
|
||||
except Exception as e:
|
||||
return False, f"网络错误: {e}"
|
||||
|
||||
def health_check(self) -> Tuple[bool, str]:
|
||||
"""查询 ESP32 健康状态"""
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/health", timeout=5)
|
||||
if response.status_code == 200:
|
||||
return True, response.text
|
||||
return False, f"查询失败: HTTP {response.status_code}"
|
||||
except Exception as e:
|
||||
return False, f"网络错误: {e}"
|
||||
|
||||
|
||||
class OTAToolGUI:
|
||||
"""OTA 上位机主界面"""
|
||||
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title(f"ESP32 OTA 上位机工具 - {TARGET_CHIP}")
|
||||
self.root.geometry("900x700")
|
||||
|
||||
# ESP32 连接配置
|
||||
self.esp_ip = tk.StringVar(value=ESP32_IP)
|
||||
|
||||
# OTA 客户端
|
||||
self.ota_client = OTAClient()
|
||||
|
||||
# 日志接收器
|
||||
self.log_handler: Optional[LogHandler] = None
|
||||
|
||||
# 当前的 HEX 文件
|
||||
self.current_hex_file = None
|
||||
|
||||
self._create_widgets()
|
||||
self._start_log_receiver()
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建界面组件"""
|
||||
# ----- 顶部:连接配置 -----
|
||||
frame_conn = tk.LabelFrame(self.root, text="ESP32 连接配置", padx=10, pady=5)
|
||||
frame_conn.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
tk.Label(frame_conn, text="ESP32 IP:").grid(row=0, column=0, sticky='e')
|
||||
tk.Entry(frame_conn, textvariable=self.esp_ip, width=20).grid(row=0, column=1, padx=5)
|
||||
|
||||
tk.Button(frame_conn, text="连接测试", command=self._test_connection).grid(row=0, column=2, padx=5)
|
||||
tk.Button(frame_conn, text="健康检查", command=self._health_check).grid(row=0, column=3, padx=5)
|
||||
|
||||
# ----- 中部左侧:文件选择 -----
|
||||
frame_file = tk.LabelFrame(self.root, text="固件文件", padx=10, pady=5)
|
||||
frame_file.pack(fill='both', expand=True, side='left', padx=10, pady=5)
|
||||
|
||||
self.file_path_var = tk.StringVar(value="未选择文件")
|
||||
tk.Label(frame_file, textvariable=self.file_path_var, fg='blue', wraplength=300).pack(fill='x')
|
||||
|
||||
tk.Button(frame_file, text="选择 .hex 文件", command=self._select_hex_file).pack(pady=5)
|
||||
tk.Button(frame_file, text="选择 .bin 文件", command=self._select_bin_file).pack(pady=5)
|
||||
|
||||
# 文件信息
|
||||
self.file_info_var = tk.StringVar(value="")
|
||||
tk.Label(frame_file, textvariable=self.file_info_var, fg='gray').pack(fill='x')
|
||||
|
||||
# 上传按钮
|
||||
self.upload_btn = tk.Button(frame_file, text="开始上传固件", bg='green', fg='white',
|
||||
font=('Arial', 12, 'bold'), state='disabled',
|
||||
command=self._start_upload)
|
||||
self.upload_btn.pack(fill='x', pady=10)
|
||||
|
||||
# 进度条
|
||||
self.progress_var = tk.DoubleVar(value=0)
|
||||
self.progress_bar = ttk.Progressbar(frame_file, variable=self.progress_var, maximum=100)
|
||||
self.progress_bar.pack(fill='x', pady=5)
|
||||
|
||||
self.progress_label = tk.Label(frame_file, text="", fg='gray')
|
||||
self.progress_label.pack()
|
||||
|
||||
# 触发 OTA 中继
|
||||
tk.Button(frame_file, text="触发 ESP32 OTA 中继", bg='orange',
|
||||
command=self._trigger_relay).pack(fill='x', pady=5)
|
||||
|
||||
# ----- 中部右侧:调试日志 -----
|
||||
frame_log = tk.LabelFrame(self.root, text="调试日志 (UDP:6666)", padx=5, pady=5)
|
||||
frame_log.pack(fill='both', expand=True, side='right', padx=10, pady=5)
|
||||
|
||||
# 日志文本框
|
||||
self.log_text = tk.Text(frame_log, height=25, width=70, font=('Courier New', 9))
|
||||
self.log_text.pack(fill='both', expand=True)
|
||||
|
||||
# 配置日志颜色标签
|
||||
self.log_text.tag_config('[E]', foreground='#FF4444')
|
||||
self.log_text.tag_config('[W]', foreground='#FF8800')
|
||||
self.log_text.tag_config('[I]', foreground='#0088FF')
|
||||
self.log_text.tag_config('[D]', foreground='#888888')
|
||||
|
||||
# 滚动条
|
||||
scrollbar = tk.Scrollbar(self.log_text, command=self.log_text.yview)
|
||||
scrollbar.pack(side='right', fill='y')
|
||||
self.log_text.config(yscrollcommand=scrollbar.set)
|
||||
|
||||
# 清除日志按钮
|
||||
tk.Button(frame_log, text="清除日志", command=lambda: self.log_text.delete('1.0', 'end')).pack()
|
||||
|
||||
def _start_log_receiver(self):
|
||||
"""启动 UDP 日志接收"""
|
||||
self.log_handler = LogHandler(self.log_text, DEBUG_LOG_PORT)
|
||||
self.log_handler.start()
|
||||
self._log_info("上位机启动", "UDP 日志接收已启动,监听 0.0.0.0:6666")
|
||||
|
||||
def _test_connection(self):
|
||||
"""测试与 ESP32 的连接"""
|
||||
ip = self.esp_ip.get().strip()
|
||||
if not ip:
|
||||
messagebox.showerror("错误", "请输入 ESP32 的 IP 地址")
|
||||
return
|
||||
|
||||
self.ota_client = OTAClient(esp_ip=ip)
|
||||
ok, msg = self.ota_client.health_check()
|
||||
if ok:
|
||||
messagebox.showinfo("连接成功", f"ESP32 连接正常\n{msg}")
|
||||
self._log_info("连接", f"ESP32 ({ip}) 连接成功")
|
||||
else:
|
||||
messagebox.showerror("连接失败", f"无法连接到 ESP32\n{msg}")
|
||||
self._log_error("连接", f"ESP32 ({ip}) 连接失败: {msg}")
|
||||
|
||||
def _health_check(self):
|
||||
"""查询 ESP32 健康状态"""
|
||||
ok, msg = self.ota_client.health_check()
|
||||
if ok:
|
||||
self._log_info("健康检查", msg)
|
||||
messagebox.showinfo("健康状态", msg)
|
||||
else:
|
||||
self._log_error("健康检查", f"查询失败: {msg}")
|
||||
messagebox.showerror("健康检查", f"查询失败\n{msg}")
|
||||
|
||||
def _select_hex_file(self):
|
||||
"""选择 HEX 文件"""
|
||||
filepath = filedialog.askopenfilename(
|
||||
title="选择 HEX 文件",
|
||||
filetypes=[("HEX 文件", "*.hex"), ("所有文件", "*.*")]
|
||||
)
|
||||
if filepath:
|
||||
self.current_hex_file = (filepath, 'hex')
|
||||
self.file_path_var.set(os.path.basename(filepath))
|
||||
self._log_info("文件", f"已选择: {filepath}")
|
||||
|
||||
# 解析文件信息
|
||||
parser = IntelHexParser(filepath)
|
||||
ok, msg = parser.parse()
|
||||
if ok:
|
||||
self.file_info_var.set(msg)
|
||||
self.upload_btn.config(state='normal')
|
||||
else:
|
||||
self.file_info_var.set(f"解析失败: {msg}")
|
||||
self.upload_btn.config(state='disabled')
|
||||
|
||||
def _select_bin_file(self):
|
||||
"""选择 BIN 文件(简化支持)"""
|
||||
filepath = filedialog.askopenfilename(
|
||||
title="选择 BIN 文件",
|
||||
filetypes=[("BIN 文件", "*.bin"), ("所有文件", "*.*")]
|
||||
)
|
||||
if filepath:
|
||||
self.current_hex_file = (filepath, 'bin')
|
||||
self.file_path_var.set(os.path.basename(filepath))
|
||||
self._log_info("文件", f"已选择: {filepath}")
|
||||
|
||||
# 获取文件大小
|
||||
size = os.path.getsize(filepath)
|
||||
self.file_info_var.set(f"BIN 文件,大小: {size} 字节 ({size/1024:.1f} KB)")
|
||||
self.upload_btn.config(state='normal')
|
||||
|
||||
def _start_upload(self):
|
||||
"""开始上传固件"""
|
||||
if not self.current_hex_file:
|
||||
messagebox.showerror("错误", "请先选择固件文件")
|
||||
return
|
||||
|
||||
filepath, file_type = self.current_hex_file
|
||||
|
||||
self.upload_btn.config(state='disabled', text="上传中...")
|
||||
self.progress_var.set(0)
|
||||
self.progress_label.config(text="正在连接 ESP32...")
|
||||
|
||||
# 在后台线程执行上传
|
||||
threading.Thread(target=self._upload_worker, args=(filepath, file_type), daemon=True).start()
|
||||
|
||||
def _upload_worker(self, filepath: str, file_type: str):
|
||||
"""上传工作线程"""
|
||||
try:
|
||||
if file_type == 'hex':
|
||||
ok, msg = self._upload_hex(filepath)
|
||||
else:
|
||||
ok, msg = self._upload_bin(filepath)
|
||||
|
||||
if ok:
|
||||
self.root.after(0, lambda: self.progress_label.config(text="上传完成"))
|
||||
self.root.after(0, lambda: messagebox.showinfo("完成", msg))
|
||||
self._log_info("上传", msg)
|
||||
else:
|
||||
self.root.after(0, lambda: self.progress_label.config(text="上传失败"))
|
||||
self.root.after(0, lambda: messagebox.showerror("上传失败", msg))
|
||||
self._log_error("上传", f"失败: {msg}")
|
||||
|
||||
except Exception as e:
|
||||
self.root.after(0, lambda: self.progress_label.config(text="上传异常"))
|
||||
self.root.after(0, lambda: messagebox.showerror("异常", f"上传异常: {e}\n{traceback.format_exc()}"))
|
||||
self._log_error("上传", f"异常: {e}")
|
||||
|
||||
finally:
|
||||
self.root.after(0, lambda: self.upload_btn.config(state='normal', text="开始上传固件"))
|
||||
|
||||
def _upload_hex(self, filepath: str) -> Tuple[bool, str]:
|
||||
"""上传 HEX 文件"""
|
||||
parser = IntelHexParser(filepath)
|
||||
ok, msg = parser.parse()
|
||||
if not ok:
|
||||
return False, msg
|
||||
|
||||
total_size = parser.get_total_size()
|
||||
addr = parser.min_addr
|
||||
seq = 0
|
||||
|
||||
while addr <= parser.max_addr:
|
||||
remaining = parser.max_addr - addr + 1
|
||||
chunk = min(CHUNK_SIZE, remaining)
|
||||
data = parser.get_data_segment(addr, chunk)
|
||||
|
||||
packet = struct.pack('<H', seq) + struct.pack('<I', total_size) + data
|
||||
|
||||
response = requests.post(
|
||||
f"http://{self.esp_ip.get()}:{ESP32_PORT}/ota/upload",
|
||||
data=packet,
|
||||
timeout=10,
|
||||
headers={'Content-Type': 'application/octet-stream'}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return False, f"HTTP {response.status_code}: {response.text}"
|
||||
|
||||
sent = min(addr - parser.min_addr + chunk, total_size)
|
||||
progress = (sent / total_size) * 100
|
||||
self.root.after(0, lambda p=progress, s=sent, t=total_size:
|
||||
[self.progress_var.set(p),
|
||||
self.progress_label.config(text=f"上传中: {s}/{t} 字节 ({p:.1f}%)")])
|
||||
|
||||
addr += chunk
|
||||
seq += 1
|
||||
|
||||
return True, f"HEX 文件上传完成,共 {seq} 包"
|
||||
|
||||
def _upload_bin(self, filepath: str) -> Tuple[bool, str]:
|
||||
"""上传 BIN 文件(直接二进制)"""
|
||||
file_size = os.path.getsize(filepath)
|
||||
|
||||
with open(filepath, 'rb') as f:
|
||||
seq = 0
|
||||
offset = 0
|
||||
while offset < file_size:
|
||||
chunk = f.read(CHUNK_SIZE)
|
||||
packet = struct.pack('<H', seq) + struct.pack('<I', file_size) + chunk
|
||||
|
||||
response = requests.post(
|
||||
f"http://{self.esp_ip.get()}:{ESP32_PORT}/ota/upload",
|
||||
data=packet,
|
||||
timeout=10,
|
||||
headers={'Content-Type': 'application/octet-stream'}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return False, f"HTTP {response.status_code}: {response.text}"
|
||||
|
||||
offset += len(chunk)
|
||||
seq += 1
|
||||
|
||||
progress = (offset / file_size) * 100
|
||||
self.root.after(0, lambda p=progress, s=offset, t=file_size:
|
||||
[self.progress_var.set(p),
|
||||
self.progress_label.config(text=f"上传中: {s}/{t} 字节 ({p:.1f}%)")])
|
||||
|
||||
return True, f"BIN 文件上传完成,共 {seq} 包"
|
||||
|
||||
def _trigger_relay(self):
|
||||
"""触发 OTA 中继"""
|
||||
ok, msg = self.ota_client.trigger_relay()
|
||||
if ok:
|
||||
self._log_info("触发", msg)
|
||||
messagebox.showinfo("成功", msg)
|
||||
else:
|
||||
self._log_error("触发", f"失败: {msg}")
|
||||
messagebox.showerror("失败", msg)
|
||||
|
||||
def _log_info(self, tag: str, msg: str):
|
||||
"""记录信息日志"""
|
||||
self._log(tag, msg, '[I]')
|
||||
|
||||
def _log_error(self, tag: str, msg: str):
|
||||
"""记录错误日志"""
|
||||
self._log(tag, msg, '[E]')
|
||||
|
||||
def _log(self, tag: str, msg: str, level: str):
|
||||
"""追加日志到文本框"""
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
log_line = f"[{timestamp}]{level}[{tag}] {msg}"
|
||||
self.log_handler._append_log(log_line)
|
||||
|
||||
def run(self):
|
||||
"""启动界面主循环"""
|
||||
self.root.mainloop()
|
||||
|
||||
def cleanup(self):
|
||||
"""清理资源"""
|
||||
if self.log_handler:
|
||||
self.log_handler.stop()
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print(f"="*50)
|
||||
print(f"ESP32 OTA 上位机工具")
|
||||
print(f"ESP32 IP: {ESP32_IP}:{ESP32_PORT}")
|
||||
print(f"日志监听: {DEBUG_LOG_IP}:{DEBUG_LOG_PORT}")
|
||||
print(f"="*50)
|
||||
|
||||
app = OTAToolGUI()
|
||||
try:
|
||||
app.run()
|
||||
finally:
|
||||
app.cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user