feat: ESP32-C3 OTA 中继项目 initial commit

This commit is contained in:
小龙虾
2026-04-27 00:42:26 +08:00
commit bcfe130451
48 changed files with 9705 additions and 0 deletions
Executable
+2
View File
@@ -0,0 +1,2 @@
CompileFlags:
Remove: [-f*, -m*]
+13
View File
@@ -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"]
+19
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
Vendored Executable
+18
View File
@@ -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"
}
]
}
Vendored Executable
+17
View File
@@ -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
View File
@@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ESP32_COMM)
Executable
+60
View File
@@ -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.
+8
View File
@@ -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
+306
View File
@@ -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
+7
View File
@@ -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,
1 # ESP32-C3FH4 OTA Relay - Partition Table
2 # Name, Type, SubType, Offset, Size, Label
3 otadata, data, ota, 0x9000, 0x2000,
4 gd32_ota, data, spiffs, 0xB000, 0x40000,
5 app0, app, ota_0, 0x4B000, 0x330000,
6 app1, app, ota_1, 0x3DC000, 0x330000,
7 spiffs, data, spiffs, 0x670000, 0x190000,
+71
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
dependencies:
idf:
source:
type: idf
version: 6.0.0
manifest_hash: 85718ddc7aec44f55b1140a54bca97140b212bcf5abafbf43632bc4dc01988d8
target: esp32c3
version: 3.0.0
+126
View File
@@ -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` 标注,不得自行假定。
**开发过程中遇到任何问题,请继续尝试解决,不要停止。**
+43
View File
@@ -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
+698
View File
@@ -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
View File
@@ -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侧协商
+32
View File
@@ -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
)
+20
View File
@@ -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
+97
View File
@@ -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 假设
}
+15
View File
@@ -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 */
+107
View File
@@ -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);
}
+111
View File
@@ -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);
}
+45
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
## IDF Component Manager Manifest File
dependencies: {}
+99
View File
@@ -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
};
+92
View File
@@ -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;
}
+38
View File
@@ -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
+458
View File
@@ -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);
}
+101
View File
@@ -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
+131
View File
@@ -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));
}
}
+25
View File
@@ -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
+452
View File
@@ -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));
}
}
+31
View File
@@ -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
+117
View File
@@ -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();
}
+572
View File
@@ -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], &timestamp, 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);
}
+93
View File
@@ -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
+124
View File
@@ -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;
}
+35
View File
@@ -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
View File
@@ -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}"
+4
View File
@@ -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}"
+4
View File
@@ -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}"
+5
View File
@@ -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}"
+3
View File
@@ -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}"
+656
View File
@@ -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