Compare commits
3
Commits
adb7dc65ca
...
6f4be9189c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f4be9189c | ||
|
|
235b9c0420 | ||
|
|
e014ca7704 |
Vendored
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"idf.openOcdConfigs": [
|
"idf.openOcdConfigs": [
|
||||||
"board/esp32c3-bridge.cfg"
|
"board/esp32c3-bridge.cfg"
|
||||||
],
|
],
|
||||||
"idf.portWin": "COM13",
|
"idf.portWin": "COM15",
|
||||||
"idf.currentSetup": "F:\\software\\.espressif\\v6.0\\esp-idf",
|
"idf.currentSetup": "F:\\software\\.espressif\\v6.0\\esp-idf",
|
||||||
"idf.customExtraVars": {
|
"idf.customExtraVars": {
|
||||||
"IDF_TARGET": "esp32c3"
|
"IDF_TARGET": "esp32c3"
|
||||||
|
|||||||
+1
-105
@@ -1,8 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ESP32-C3FH4 OTA 中继 - 程序入口
|
* ESP32-C3FH4 OTA 中继 - 程序入口
|
||||||
* 四个 FreeRTOS 任务:System Monitor / OTA Relay / WiFi Server / BLE GATT
|
* 三个 FreeRTOS 任务:OTA Relay / WiFi Server / BLE GATT
|
||||||
*
|
|
||||||
* 新增:周期性状态监控任务,每 10 秒打印系统状态
|
|
||||||
*/
|
*/
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
@@ -20,12 +18,6 @@
|
|||||||
#include "transport/protocol.h"
|
#include "transport/protocol.h"
|
||||||
#include "monitor/debug_log.h"
|
#include "monitor/debug_log.h"
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 状态监控任务
|
|
||||||
* @param pvParameters
|
|
||||||
*/
|
|
||||||
static void status_monitor_task(void *pvParameters);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。
|
* @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。
|
||||||
*/
|
*/
|
||||||
@@ -130,22 +122,6 @@ extern "C" void app_main(void)
|
|||||||
DEBUG_INFO("MAIN", "BLE OTA 任务已创建");
|
DEBUG_INFO("MAIN", "BLE OTA 任务已创建");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态监控任务(低优先级,周期性打印系统状态)
|
|
||||||
xRet = xTaskCreatePinnedToCore(
|
|
||||||
status_monitor_task,
|
|
||||||
"status_mon",
|
|
||||||
4096,
|
|
||||||
NULL,
|
|
||||||
configMAX_PRIORITIES - 5,
|
|
||||||
NULL,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
if (xRet != pdPASS) {
|
|
||||||
DEBUG_ERR("MAIN", "创建 status_monitor_task 失败");
|
|
||||||
} else {
|
|
||||||
DEBUG_INFO("MAIN", "状态监控任务已创建");
|
|
||||||
}
|
|
||||||
|
|
||||||
DEBUG_INFO("MAIN", "========================================");
|
DEBUG_INFO("MAIN", "========================================");
|
||||||
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
|
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
|
||||||
DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)",
|
DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)",
|
||||||
@@ -153,83 +129,3 @@ extern "C" void app_main(void)
|
|||||||
DEBUG_INFO("MAIN", "========================================");
|
DEBUG_INFO("MAIN", "========================================");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 状态监控任务 - 每 10 秒打印系统状态
|
|
||||||
/**
|
|
||||||
* @brief 周期性打印系统运行状态,包括内存、WiFi 和 OTA 中继信息。
|
|
||||||
* @param pvParameters
|
|
||||||
*/
|
|
||||||
static void status_monitor_task(void *pvParameters)
|
|
||||||
{
|
|
||||||
(void)pvParameters;
|
|
||||||
DEBUG_INFO("STAT", "状态监控任务启动");
|
|
||||||
|
|
||||||
uint32_t loop_count = 0;
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
loop_count++;
|
|
||||||
|
|
||||||
uint32_t free_heap = esp_get_free_heap_size();
|
|
||||||
uint32_t min_free_heap = esp_get_minimum_free_heap_size();
|
|
||||||
health_state_t health = health_get_current();
|
|
||||||
relay_state_t relay = ota_relay_get_state();
|
|
||||||
int8_t rssi = wifi_get_rssi();
|
|
||||||
bool wifi_conn = wifi_is_connected();
|
|
||||||
|
|
||||||
// 打印状态头
|
|
||||||
DEBUG_INFO("STAT", "========== 系统状态 #%lu ==========", (unsigned long)loop_count);
|
|
||||||
|
|
||||||
// 内存状态
|
|
||||||
DEBUG_INFO("STAT", "内存: 空闲=%lu 字节, 最低=%lu 字节",
|
|
||||||
(unsigned long)free_heap, (unsigned long)min_free_heap);
|
|
||||||
|
|
||||||
// 健康状态
|
|
||||||
const char *health_str;
|
|
||||||
switch (health) {
|
|
||||||
case SYS_NORMAL: health_str = "正常"; break;
|
|
||||||
case SYS_WARNING: health_str = "警告"; break;
|
|
||||||
case SYS_CRITICAL: health_str = "危险"; break;
|
|
||||||
case SYS_SAFE_SHUTDOWN: health_str = "安全关机"; break;
|
|
||||||
default: health_str = "未知"; break;
|
|
||||||
}
|
|
||||||
DEBUG_INFO("STAT", "健康状态: %s (%d)", health_str, health);
|
|
||||||
|
|
||||||
// WiFi 状态
|
|
||||||
if (wifi_conn) {
|
|
||||||
DEBUG_INFO("STAT", "WiFi: 已连接, RSSI=%d dBm", rssi);
|
|
||||||
} else {
|
|
||||||
DEBUG_INFO("STAT", "WiFi: 未连接");
|
|
||||||
}
|
|
||||||
|
|
||||||
// OTA 中继状态
|
|
||||||
const char *relay_str;
|
|
||||||
switch (relay) {
|
|
||||||
case RELAY_IDLE: relay_str = "空闲"; break;
|
|
||||||
case RELAY_HANDSHAKE_IDLE: relay_str = "等待握手"; break;
|
|
||||||
case RELAY_HANDSHAKE_SENT: relay_str = "握手请求已发送"; break;
|
|
||||||
case RELAY_CHALLENGE_SENT: relay_str = "挑战已发送"; break;
|
|
||||||
case RELAY_CHALLENGE_RESPOND: relay_str = "收到握手响应"; break;
|
|
||||||
case RELAY_SESSION_KEY_SENT: relay_str = "会话密钥已发送"; break;
|
|
||||||
case RELAY_AUTH_COMPLETE: relay_str = "认证完成"; break;
|
|
||||||
case RELAY_SIZE_SENT: relay_str = "大小已发送"; break;
|
|
||||||
case RELAY_SIZE_ACK: relay_str = "大小确认"; break;
|
|
||||||
case RELAY_RECEIVING: relay_str = "接收中"; break;
|
|
||||||
case RELAY_RELAY_ACTIVE: relay_str = "转发中"; break;
|
|
||||||
case RELAY_PACKET_SENT: relay_str = "数据包已发送"; break;
|
|
||||||
case RELAY_PACKET_ACK_OK: relay_str = "ACK 正常"; break;
|
|
||||||
case RELAY_PACKET_ACK_RETRY: relay_str = "需要重试"; break;
|
|
||||||
case RELAY_RECEIVE_DONE: relay_str = "接收完成"; break;
|
|
||||||
case RELAY_TRANSFER_DONE: relay_str = "传输完成"; break;
|
|
||||||
case RELAY_FINISH_SENT: relay_str = "结束已发送"; break;
|
|
||||||
case RELAY_DONE: relay_str = "OTA 完成"; break;
|
|
||||||
case RELAY_ERROR: relay_str = "错误"; break;
|
|
||||||
default: relay_str = "未知"; break;
|
|
||||||
}
|
|
||||||
DEBUG_INFO("STAT", "OTA 中继: %s (%d)", relay_str, relay);
|
|
||||||
|
|
||||||
DEBUG_INFO("STAT", "======================================");
|
|
||||||
|
|
||||||
// 每 10 秒打印一次
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* app_config.h - 统一配置文件(C++ 版本)
|
* app_config.h - 统一配置文件(C++ 版本)
|
||||||
* ESP32-C3 OTA 中继项目
|
* ESP32-C3 OTA 中继项目
|
||||||
*
|
*
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// 固件版本
|
// 固件版本
|
||||||
// ============================================================
|
// ============================================================
|
||||||
inline constexpr const char *FW_VERSION = "0.2.0";
|
inline constexpr const char *FW_VERSION = "0.2.10";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// WiFi 工作模式选择
|
// WiFi 工作模式选择
|
||||||
@@ -98,8 +98,8 @@ inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11; // cmd(1)+seq(2)+siz
|
|||||||
// ⚠️ 请填入实际密钥
|
// ⚠️ 请填入实际密钥
|
||||||
// ============================================================
|
// ============================================================
|
||||||
inline constexpr uint8_t MASTER_KEY[] = {
|
inline constexpr uint8_t MASTER_KEY[] = {
|
||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
|
||||||
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F
|
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr uint8_t SCRAMBLE_CONST[] = {
|
inline constexpr uint8_t SCRAMBLE_CONST[] = {
|
||||||
@@ -107,7 +107,11 @@ inline constexpr uint8_t SCRAMBLE_CONST[] = {
|
|||||||
0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00
|
0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⚠️ 请填入实际的 GD32 芯片唯一码(12字节)
|
// GD32_CHIP_UID 仅保留为默认占位,运行时以握手包解出的芯片地址 UID 为准。
|
||||||
|
inline constexpr uint32_t DEVICE_SERIAL_ADDR_LSB = 0x1FFFF7E8U;
|
||||||
|
inline constexpr uint32_t DEVICE_SERIAL_ADDR_MID = 0x1FFFF7ECU;
|
||||||
|
inline constexpr uint32_t DEVICE_SERIAL_ADDR_MSB = 0x1FFFF7F0U;
|
||||||
|
|
||||||
inline constexpr uint8_t GD32_CHIP_UID[] = {
|
inline constexpr uint8_t GD32_CHIP_UID[] = {
|
||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
||||||
0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
|
0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
static const char *TAG = "DEBUG_LOG";
|
static const char *TAG = "DEBUG_LOG";
|
||||||
|
|
||||||
/** @brief 当前调试日志过滤级别,运行时可动态调整。 */
|
/** @brief 当前调试日志过滤级别,运行时可动态调整。 */
|
||||||
debug_log_level_t g_debug_log_level = DEBUG_LOG_ALL;
|
debug_log_level_t g_debug_log_level = DEBUG_LOG_INFO;
|
||||||
|
|
||||||
/** @brief UDP 调试日志 socket 句柄,首次发送时延迟创建。 */
|
/** @brief UDP 调试日志 socket 句柄,首次发送时延迟创建。 */
|
||||||
static int s_udp_sock = -1;
|
static int s_udp_sock = -1;
|
||||||
@@ -67,6 +67,11 @@ int debug_log_ring_count(void)
|
|||||||
|
|
||||||
// 读取并清空日志缓冲区,返回 JSON 数组字符串
|
// 读取并清空日志缓冲区,返回 JSON 数组字符串
|
||||||
// 调用者负责 free 返回值
|
// 调用者负责 free 返回值
|
||||||
|
/**
|
||||||
|
* @brief 读取并清空环形日志缓冲区,返回 JSON 数组字符串。
|
||||||
|
* @param out_len 输出 JSON 字符串长度(不含末尾 '\0')。
|
||||||
|
* @return 成功返回需由调用者 free 的字符串;无日志或失败返回 NULL。
|
||||||
|
*/
|
||||||
char *debug_log_ring_drain_json(int *out_len)
|
char *debug_log_ring_drain_json(int *out_len)
|
||||||
{
|
{
|
||||||
if (!s_log_ring_mutex) {
|
if (!s_log_ring_mutex) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* health_monitor.cpp - 系统健康监控
|
* health_monitor.cpp - 系统健康监控
|
||||||
* 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控
|
* 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控
|
||||||
*/
|
*/
|
||||||
@@ -71,6 +71,12 @@ float health_get_last_temperature(void)
|
|||||||
return s_last_temperature;
|
return s_last_temperature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取最近一次采样到的 WiFi RSSI 及其年龄。
|
||||||
|
* @param out_rssi 输出 RSSI 指针,可为 NULL。
|
||||||
|
* @param out_age_ms 输出采样距今毫秒数指针,可为 NULL。
|
||||||
|
* @return 有有效采样返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
bool health_get_last_wifi_rssi(int8_t *out_rssi, uint32_t *out_age_ms)
|
bool health_get_last_wifi_rssi(int8_t *out_rssi, uint32_t *out_age_ms)
|
||||||
{
|
{
|
||||||
if (s_last_wifi_rssi_ms <= 0) {
|
if (s_last_wifi_rssi_ms <= 0) {
|
||||||
@@ -233,6 +239,36 @@ health_status_t health_check_before_ota(void)
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
health_status_t health_check_before_ota_relay(void)
|
||||||
|
{
|
||||||
|
health_status_t status = HEALTH_MON_OK;
|
||||||
|
|
||||||
|
if (g_health_state >= SYS_SAFE_SHUTDOWN) {
|
||||||
|
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_SAFE_SHUTDOWN,禁止传输");
|
||||||
|
return HEALTH_MON_SHUTDOWN;
|
||||||
|
} else if (g_health_state >= SYS_CRITICAL) {
|
||||||
|
/* SYS_CRITICAL 可能是由 WiFi RSSI 过低导致的,
|
||||||
|
* 但 SPI 中继阶段不依赖 WiFi,只检查温度/硬件等本地状态。
|
||||||
|
* 温度达到 TEMP_CRITICAL(100°C) 时 g_health_state 会被设为
|
||||||
|
* SYS_SAFE_SHUTDOWN,在上面的分支已经拦截。
|
||||||
|
* 所以 SYS_CRITICAL 在中继阶段只可能是 WiFi 导致的,允许继续。 */
|
||||||
|
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_CRITICAL (WiFi 相关),SPI 中继允许继续");
|
||||||
|
} else if (g_health_state >= SYS_WARNING) {
|
||||||
|
status = HEALTH_MON_WARNING;
|
||||||
|
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_WARNING,继续传输");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 跳过 WiFi 连接状态和 RSSI 检查:
|
||||||
|
* SPI 中继阶段固件已存储在 ESP32 Flash 中,
|
||||||
|
* 不依赖 WiFi 连接,无需检查 WiFi 状态。 */
|
||||||
|
|
||||||
|
if (status == HEALTH_MON_OK) {
|
||||||
|
DEBUG_DBG("HEALTH", "OTA 中继检查: 正常");
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 根据 WiFi RSSI 更新健康状态并返回检查结果。
|
* @brief 根据 WiFi RSSI 更新健康状态并返回检查结果。
|
||||||
* @param rssi 当前 WiFi 信号强度(dBm)。
|
* @param rssi 当前 WiFi 信号强度(dBm)。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* health_monitor.h - 系统健康监控头文件
|
* health_monitor.h - 系统健康监控头文件
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -45,6 +45,15 @@ void health_monitor_task(void *pvParameters);
|
|||||||
*/
|
*/
|
||||||
health_status_t health_check_before_ota(void);
|
health_status_t health_check_before_ota(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在 OTA 中继转发阶段执行健康检查,跳过 WiFi 相关检查。
|
||||||
|
*
|
||||||
|
* SPI 中继阶段固件已存储在 ESP32 Flash 中,不依赖 WiFi 连接,
|
||||||
|
* 因此跳过 WiFi 连接状态和 RSSI 检查,只检查温度等本地状态。
|
||||||
|
* @return 健康检查结果,决定是否允许 OTA。
|
||||||
|
*/
|
||||||
|
health_status_t health_check_before_ota_relay(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 记录一条健康相关事件日志,便于故障追踪与状态回溯。
|
* @brief 记录一条健康相关事件日志,便于故障追踪与状态回溯。
|
||||||
* @param event 事件名称字符串。
|
* @param event 事件名称字符串。
|
||||||
|
|||||||
+579
-89
@@ -9,7 +9,8 @@
|
|||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_partition.h"
|
#include "esp_partition.h"
|
||||||
#include "cJSON.h"
|
#include "nvs_flash.h"
|
||||||
|
#include "nvs.h"
|
||||||
#include "aes/esp_aes.h"
|
#include "aes/esp_aes.h"
|
||||||
|
|
||||||
#include "ota_relay.h"
|
#include "ota_relay.h"
|
||||||
@@ -20,12 +21,17 @@
|
|||||||
|
|
||||||
/** @brief 当前 OTA 中继状态机状态。 */
|
/** @brief 当前 OTA 中继状态机状态。 */
|
||||||
relay_state_t g_relay_state = RELAY_IDLE;
|
relay_state_t g_relay_state = RELAY_IDLE;
|
||||||
|
volatile bool g_gd32_app_alive = false;
|
||||||
|
|
||||||
static const esp_partition_t *g_gd32_partition = NULL;
|
static const esp_partition_t *g_gd32_partition = NULL;
|
||||||
/** @brief OTA 认证会话上下文,包含会话密钥和 IV。 */
|
/** @brief OTA 认证会话上下文,包含会话密钥和 IV。 */
|
||||||
static session_ctx_t g_session_ctx;
|
static session_ctx_t g_session_ctx;
|
||||||
static uint8_t g_nonce_server[16] = {0};
|
static uint8_t g_nonce_server[16] = {0};
|
||||||
static uint8_t g_k_session[16] = {0};
|
static uint8_t g_k_session[16] = {0};
|
||||||
|
static uint8_t g_gd32_uid_runtime[12] = {0};
|
||||||
|
|
||||||
|
static constexpr size_t GD32_UID_LEN = 12;
|
||||||
|
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
|
||||||
|
|
||||||
/** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */
|
/** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */
|
||||||
QueueHandle_t g_ota_dispatch_queue = NULL;
|
QueueHandle_t g_ota_dispatch_queue = NULL;
|
||||||
@@ -62,7 +68,259 @@ static ota_result_t relay_packets_from_flash(void);
|
|||||||
* @return 操作结果码。
|
* @return 操作结果码。
|
||||||
*/
|
*/
|
||||||
static ota_result_t finish_ota(void);
|
static ota_result_t finish_ota(void);
|
||||||
|
static int aes128_ecb_encrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out);
|
||||||
|
static int aes128_ecb_decrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out);
|
||||||
|
static void log_resp_payload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len);
|
||||||
|
static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type,
|
||||||
|
uint8_t *resp_payload, size_t resp_max,
|
||||||
|
size_t *resp_len, uint32_t timeout_ms,
|
||||||
|
uint32_t max_wait_rounds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断响应负载是否为带序号的结构化 ACK。
|
||||||
|
* @param payload 响应负载指针。
|
||||||
|
* @param len 负载长度。
|
||||||
|
* @param expected_seq 期望的序号。
|
||||||
|
* @return 匹配返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool is_structured_ack_with_seq(const uint8_t *payload, size_t len, uint32_t expected_seq)
|
||||||
|
{
|
||||||
|
if (!payload || len < 6) return false;
|
||||||
|
if (payload[0] != 0xBA || payload[1] != 0xC0) return false;
|
||||||
|
uint32_t seq = (uint32_t)payload[2] | ((uint32_t)payload[3] << 8) |
|
||||||
|
((uint32_t)payload[4] << 16) | ((uint32_t)payload[5] << 24);
|
||||||
|
return seq == expected_seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析结构化错误响应中的序号和错误码。
|
||||||
|
* @param payload 响应负载指针。
|
||||||
|
* @param len 负载长度。
|
||||||
|
* @param seq_out 输出错误对应序号,可为 NULL。
|
||||||
|
* @param err_out 输出错误码,可为 NULL。
|
||||||
|
* @return 解析成功返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool parse_structured_error(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out)
|
||||||
|
{
|
||||||
|
if (!payload || len < 7) return false;
|
||||||
|
if (payload[0] != 0xBA || payload[1] != 0xC0) return false;
|
||||||
|
if (seq_out) {
|
||||||
|
*seq_out = (uint32_t)payload[2] | ((uint32_t)payload[3] << 8) |
|
||||||
|
((uint32_t)payload[4] << 16) | ((uint32_t)payload[5] << 24);
|
||||||
|
}
|
||||||
|
if (err_out) *err_out = payload[6];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断缓冲区是否全部由指定字节值组成。
|
||||||
|
* @param buf 输入缓冲区。
|
||||||
|
* @param len 缓冲区长度。
|
||||||
|
* @param value 目标字节值。
|
||||||
|
* @return 全部匹配返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool is_handshake_echo_ack(const uint8_t *payload, size_t len)
|
||||||
|
{
|
||||||
|
return payload && len >= 2U && payload[0] == 0x00U && payload[1] == 0xACU;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_challenge_payload_ack(const uint8_t *payload, size_t len)
|
||||||
|
{
|
||||||
|
return payload && len == GD32_CHALLENGE_RESPONSE_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_confirm_plain_ack(const uint8_t *payload, size_t len)
|
||||||
|
{
|
||||||
|
return payload && len == 2U && payload[0] == 0xBAU && payload[1] == 0xC0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_size_seq0_ack(const uint8_t *payload, size_t len)
|
||||||
|
{
|
||||||
|
return is_structured_ack_with_seq(payload, len, 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_finish_plain_ack(const uint8_t *payload, size_t len)
|
||||||
|
{
|
||||||
|
return payload && len >= 2U && payload[0] == 0x0AU && payload[1] == 0xACU;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief FINISH 阶段匹配器:接受 0x0A 0xAC 明文 ACK 或结构化错误帧。
|
||||||
|
*
|
||||||
|
* BOOT 在 FINISH 成功时回 2B 明文 ACK {0x0A, 0xAC};
|
||||||
|
* 失败时回 7B 结构化错误帧 {0xBA,0xC0, seq(4B), err_code}。
|
||||||
|
* 两种都需要识别,否则 ESP32 会把错误帧当成残留 ACK 继续轮询。
|
||||||
|
*/
|
||||||
|
static bool is_finish_ack_or_error(const uint8_t *payload, size_t len)
|
||||||
|
{
|
||||||
|
if (!payload) return false;
|
||||||
|
/* 明文 ACK: 0x0A 0xAC */
|
||||||
|
if (len >= 2U && payload[0] == 0x0AU && payload[1] == 0xACU) return true;
|
||||||
|
/* 结构化错误帧: 0xBA 0xC0 + seq(4B) + err_code */
|
||||||
|
if (len >= 7U && payload[0] == 0xBAU && payload[1] == 0xC0U) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按小端序读取 4 字节无符号整数。
|
||||||
|
* @param p 输入字节指针(至少 4 字节)。
|
||||||
|
* @return 读取出的 uint32_t 数值。
|
||||||
|
*/
|
||||||
|
static bool bytes_all_value(const uint8_t *buf, size_t len, uint8_t value)
|
||||||
|
{
|
||||||
|
if (!buf || len == 0) return false;
|
||||||
|
for (size_t i = 0; i < len; ++i) {
|
||||||
|
if (buf[i] != value) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 继续轮询,直到拿到符合当前阶段预期形状的 ACK,或遇到结构化错误/超时。
|
||||||
|
*/
|
||||||
|
static esp_err_t poll_until_expected_ack_shape(const char *stage,
|
||||||
|
uint8_t *resp_type,
|
||||||
|
uint8_t *resp_payload,
|
||||||
|
size_t resp_max,
|
||||||
|
size_t *resp_len,
|
||||||
|
uint32_t poll_timeout_ms,
|
||||||
|
uint32_t wait_rounds,
|
||||||
|
uint32_t maxExtraPolls,
|
||||||
|
bool (*matcher)(const uint8_t *, size_t))
|
||||||
|
{
|
||||||
|
if (!stage || !resp_type || !resp_payload || !resp_len || !matcher) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = ESP_OK;
|
||||||
|
if (*resp_type == SPI_OTA_RSP_WAIT) {
|
||||||
|
err = wait_until_not_wait(stage, resp_type, resp_payload, resp_max, resp_len,
|
||||||
|
poll_timeout_ms, wait_rounds);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t extra = 0; extra < maxExtraPolls; ++extra) {
|
||||||
|
if (*resp_type != SPI_OTA_RSP_ACK) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (matcher(resp_payload, *resp_len)) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t err_seq = 0;
|
||||||
|
uint8_t err_code = 0;
|
||||||
|
if (parse_structured_error(resp_payload, *resp_len, &err_seq, &err_code)) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_WARN("OTA", "%s 收到上一阶段残留ACK/非目标ACK,继续轮询: len=%u", stage, (unsigned)*resp_len);
|
||||||
|
*resp_len = 0;
|
||||||
|
err = spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, poll_timeout_ms);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
log_resp_payload(stage, *resp_type, resp_payload, *resp_len);
|
||||||
|
if (*resp_type == SPI_OTA_RSP_WAIT) {
|
||||||
|
err = wait_until_not_wait(stage, resp_type, resp_payload, resp_max, resp_len,
|
||||||
|
poll_timeout_ms, wait_rounds);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析并校验 GD32 Challenge 响应,提取会话密钥和 UID。
|
||||||
|
* @param resp_payload Challenge 响应数据。
|
||||||
|
* @param resp_len 响应长度。
|
||||||
|
* @param out_k_session 输出会话密钥(16 字节)。
|
||||||
|
* @param out_uid 输出 UID(GD32_UID_LEN 字节)。
|
||||||
|
* @return 校验通过返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool parse_and_verify_challenge_payload(const uint8_t *resp_payload, size_t resp_len,
|
||||||
|
uint8_t *out_k_session, uint8_t *out_uid)
|
||||||
|
{
|
||||||
|
if (!resp_payload || !out_k_session || !out_uid) return false;
|
||||||
|
|
||||||
|
if (resp_len != GD32_CHALLENGE_RESPONSE_LEN) {
|
||||||
|
DEBUG_ERR("OTA", "CHALLENGE validate failed: invalid payload length=%u", (unsigned)resp_len);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GD32 响应格式 (36 bytes):
|
||||||
|
* [0..11] = UID (12 bytes)
|
||||||
|
* [12..19] = nonce_server[8:16] (8 bytes, nonce 后半段)
|
||||||
|
* [20..35] = encrypted_Ksession (16 bytes) = AES(K_master, K_session)
|
||||||
|
*
|
||||||
|
* 其中 K_session = AES(K_master, nonce_server)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const uint8_t *uid = resp_payload;
|
||||||
|
const uint8_t *nonce_frag = resp_payload + 12;
|
||||||
|
const uint8_t *encrypted_ksession = resp_payload + 20;
|
||||||
|
|
||||||
|
// 校验 nonce 片段: 应与 nonce_server[8:16] 一致
|
||||||
|
if (memcmp(nonce_frag, g_nonce_server + 8, 8) != 0) {
|
||||||
|
DEBUG_ERR("OTA", "CHALLENGE校验失败: nonce片段不匹配");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 派生 K_session = AES(K_master, nonce_server)
|
||||||
|
uint8_t K_session[16] = {0};
|
||||||
|
if (aes128_ecb_encrypt_block_local(MASTER_KEY, g_nonce_server, K_session) != 0) {
|
||||||
|
DEBUG_ERR("OTA", "CHALLENGE校验失败: 派生K_session失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 encrypted_Ksession: AES(K_master, K_session) 应匹配
|
||||||
|
uint8_t expected_encrypted[16] = {0};
|
||||||
|
if (aes128_ecb_encrypt_block_local(MASTER_KEY, K_session, expected_encrypted) != 0) {
|
||||||
|
DEBUG_ERR("OTA", "CHALLENGE校验失败: 加密K_session失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memcmp(encrypted_ksession, expected_encrypted, 16) != 0) {
|
||||||
|
DEBUG_ERR("OTA", "CHALLENGE校验失败: encrypted_Ksession不匹配");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验通过,输出 K_session 和 UID
|
||||||
|
memcpy(out_k_session, K_session, 16);
|
||||||
|
memcpy(out_uid, uid, GD32_UID_LEN);
|
||||||
|
|
||||||
|
if (bytes_all_value(out_uid, GD32_UID_LEN, 0x00U) ||
|
||||||
|
bytes_all_value(out_uid, GD32_UID_LEN, 0xFFU)) {
|
||||||
|
DEBUG_ERR("OTA", "CHALLENGE校验失败: UID无效(全00/全FF)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char uid_hex[GD32_UID_LEN * 2 + 1] = {0};
|
||||||
|
for (size_t i = 0; i < GD32_UID_LEN; ++i) {
|
||||||
|
snprintf(&uid_hex[i * 2], sizeof(uid_hex) - i * 2, "%02X", out_uid[i]);
|
||||||
|
}
|
||||||
|
DEBUG_INFO("OTA",
|
||||||
|
"GD32 UID(%uB @0x%08" PRIX32 "/0x%08" PRIX32 "/0x%08" PRIX32 "): %s",
|
||||||
|
(unsigned)GD32_UID_LEN,
|
||||||
|
DEVICE_SERIAL_ADDR_LSB, DEVICE_SERIAL_ADDR_MID, DEVICE_SERIAL_ADDR_MSB,
|
||||||
|
uid_hex);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使用 AES-128 ECB 加密单个 16 字节分组。
|
||||||
|
* @param key 16 字节密钥。
|
||||||
|
* @param in 输入明文分组(16 字节)。
|
||||||
|
* @param out 输出密文分组(16 字节)。
|
||||||
|
* @return 0 表示成功,非 0 表示失败。
|
||||||
|
*/
|
||||||
static int aes128_ecb_encrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out)
|
static int aes128_ecb_encrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out)
|
||||||
{
|
{
|
||||||
esp_aes_context aes;
|
esp_aes_context aes;
|
||||||
@@ -75,6 +333,13 @@ static int aes128_ecb_encrypt_block_local(const uint8_t *key, const uint8_t *in,
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使用 AES-128 ECB 解密单个 16 字节分组。
|
||||||
|
* @param key 16 字节密钥。
|
||||||
|
* @param in 输入密文分组(16 字节)。
|
||||||
|
* @param out 输出明文分组(16 字节)。
|
||||||
|
* @return 0 表示成功,非 0 表示失败。
|
||||||
|
*/
|
||||||
static int aes128_ecb_decrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out)
|
static int aes128_ecb_decrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out)
|
||||||
{
|
{
|
||||||
esp_aes_context aes;
|
esp_aes_context aes;
|
||||||
@@ -87,6 +352,13 @@ static int aes128_ecb_decrypt_block_local(const uint8_t *key, const uint8_t *in,
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按 3 个分组对 48 字节数据执行 AES-128 ECB 加密。
|
||||||
|
* @param key 16 字节密钥。
|
||||||
|
* @param in 输入明文(48 字节)。
|
||||||
|
* @param out 输出密文(48 字节)。
|
||||||
|
* @return 全部分组加密成功返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
static bool aes128_ecb_encrypt_48bytes_local(const uint8_t *key, const uint8_t *in, uint8_t *out)
|
static bool aes128_ecb_encrypt_48bytes_local(const uint8_t *key, const uint8_t *in, uint8_t *out)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 3; ++i) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
@@ -97,6 +369,12 @@ static bool aes128_ecb_encrypt_48bytes_local(const uint8_t *key, const uint8_t *
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算带 key 前缀的 CRC32(key + 48B 数据),输出小端 4 字节。
|
||||||
|
* @param key 16 字节会话密钥。
|
||||||
|
* @param data48 输入数据(48 字节)。
|
||||||
|
* @param output 输出 CRC32(4 字节)。
|
||||||
|
*/
|
||||||
static void calculate_crc32_with_key_local(const uint8_t *key, const uint8_t *data48, uint8_t *output)
|
static void calculate_crc32_with_key_local(const uint8_t *key, const uint8_t *data48, uint8_t *output)
|
||||||
{
|
{
|
||||||
uint32_t crc = 0xFFFFFFFFU;
|
uint32_t crc = 0xFFFFFFFFU;
|
||||||
@@ -119,6 +397,13 @@ static void calculate_crc32_with_key_local(const uint8_t *key, const uint8_t *da
|
|||||||
output[3] = (uint8_t)((crc >> 24) & 0xFFU);
|
output[3] = (uint8_t)((crc >> 24) & 0xFFU);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 记录响应类型和负载十六进制摘要,便于 OTA 阶段诊断。
|
||||||
|
* @param stage 当前阶段名称。
|
||||||
|
* @param resp_type 响应类型。
|
||||||
|
* @param payload 响应负载。
|
||||||
|
* @param len 响应负载长度。
|
||||||
|
*/
|
||||||
static void log_resp_payload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len)
|
static void log_resp_payload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len)
|
||||||
{
|
{
|
||||||
char hex[193] = {0};
|
char hex[193] = {0};
|
||||||
@@ -126,12 +411,23 @@ static void log_resp_payload(const char *stage, uint8_t resp_type, const uint8_t
|
|||||||
for (size_t i = 0; i < n; ++i) {
|
for (size_t i = 0; i < n; ++i) {
|
||||||
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", payload[i]);
|
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", payload[i]);
|
||||||
}
|
}
|
||||||
DEBUG_INFO("OTA", "%s 响应: type=0x%02X len=%u payload[%u]=%s%s",
|
DEBUG_DBG("OTA", "%s 响应: type=0x%02X len=%u payload[%u]=%s%s",
|
||||||
stage, resp_type, (unsigned)len, (unsigned)n,
|
stage, resp_type, (unsigned)len, (unsigned)n,
|
||||||
n > 0 ? hex : "<empty>",
|
n > 0 ? hex : "<empty>",
|
||||||
len > n ? "..." : "");
|
len > n ? "..." : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 当收到 WAIT 响应时进行只读轮询,直到响应变为非 WAIT 或达到轮询上限。
|
||||||
|
* @param stage 当前阶段名称(用于日志)。
|
||||||
|
* @param resp_type 输入/输出响应类型。
|
||||||
|
* @param resp_payload 输出响应负载缓冲区。
|
||||||
|
* @param resp_max 响应缓冲区大小。
|
||||||
|
* @param resp_len 输出响应长度指针,可为 NULL。
|
||||||
|
* @param timeout_ms 每次轮询的超时毫秒。
|
||||||
|
* @param max_wait_rounds 最多轮询次数。
|
||||||
|
* @return ESP_OK 或底层 SPI 轮询错误码。
|
||||||
|
*/
|
||||||
static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type,
|
static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type,
|
||||||
uint8_t *resp_payload, size_t resp_max,
|
uint8_t *resp_payload, size_t resp_max,
|
||||||
size_t *resp_len, uint32_t timeout_ms, uint32_t max_wait_rounds)
|
size_t *resp_len, uint32_t timeout_ms, uint32_t max_wait_rounds)
|
||||||
@@ -141,7 +437,22 @@ static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type,
|
|||||||
while (*resp_type == SPI_OTA_RSP_WAIT && round < max_wait_rounds) {
|
while (*resp_type == SPI_OTA_RSP_WAIT && round < max_wait_rounds) {
|
||||||
++round;
|
++round;
|
||||||
DEBUG_WARN("OTA", "%s 收到 WAIT,开始第 %u 次只读轮询...", stage, round);
|
DEBUG_WARN("OTA", "%s 收到 WAIT,开始第 %u 次只读轮询...", stage, round);
|
||||||
esp_err_t err = spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
|
/* GD32 擦除 Flash 期间 (__disable_irq) 无法响应 SPI,
|
||||||
|
* ESP32 的 poll 会持续拿到 0xFF 或 WAIT 帧残留字节,
|
||||||
|
* 导致 spi_ota_poll_response 超时返回 ESP_ERR_TIMEOUT。
|
||||||
|
*
|
||||||
|
* 这是正常现象——只要还在 WAIT 轮询范围内,超时不代表协议失败,
|
||||||
|
* 只是 GD32 还在忙。继续重试即可。
|
||||||
|
*
|
||||||
|
* 用 100ms 短超时做每次 poll,避免擦除期间长阻塞。
|
||||||
|
* 300 轮 × 100ms = 30s,远超 2-3 秒的 Flash 擦除时间。
|
||||||
|
* 实际上擦除完成后 ACK 会立即返回,不会真的等 30s。 */
|
||||||
|
esp_err_t err = spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, 100);
|
||||||
|
if (err == ESP_ERR_TIMEOUT) {
|
||||||
|
/* 擦除期间 SPI 无响应 → 保持 WAIT 状态继续轮询 */
|
||||||
|
*resp_type = SPI_OTA_RSP_WAIT;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -158,41 +469,22 @@ static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type,
|
|||||||
esp_err_t meta_load(ota_meta_t *meta)
|
esp_err_t meta_load(ota_meta_t *meta)
|
||||||
{
|
{
|
||||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||||
|
|
||||||
memset(meta, 0, sizeof(ota_meta_t));
|
memset(meta, 0, sizeof(ota_meta_t));
|
||||||
|
|
||||||
FILE *f = fopen(META_FILE, "r");
|
nvs_handle_t handle;
|
||||||
if (!f) {
|
esp_err_t err = nvs_open(OTA_META_NVS_NAMESPACE, NVS_READONLY, &handle);
|
||||||
// 文件不存在是正常状态(首次使用,未刷过 GD32)
|
if (err != ESP_OK) {
|
||||||
|
// NVS 中没有元数据是正常状态(首次使用)
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
char buf[256];
|
uint32_t val = 0;
|
||||||
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
|
if (nvs_get_u32(handle, "total_size", &val) == ESP_OK) meta->total_size = val;
|
||||||
buf[n] = '\0';
|
if (nvs_get_u32(handle, "stored_size", &val) == ESP_OK) meta->stored_size = val;
|
||||||
fclose(f);
|
if (nvs_get_u32(handle, "last_seq", &val) == ESP_OK) meta->last_seq = val;
|
||||||
|
if (nvs_get_u32(handle, "chunk_count", &val) == ESP_OK) meta->chunk_count = val;
|
||||||
|
|
||||||
cJSON *json = cJSON_Parse(buf);
|
nvs_close(handle);
|
||||||
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;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,24 +497,24 @@ esp_err_t meta_save(const ota_meta_t *meta)
|
|||||||
{
|
{
|
||||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||||
|
|
||||||
FILE *f = fopen(META_FILE, "w");
|
nvs_handle_t handle;
|
||||||
if (!f) {
|
esp_err_t err = nvs_open(OTA_META_NVS_NAMESPACE, NVS_READWRITE, &handle);
|
||||||
DEBUG_ERR("OTA", "无法打开元数据文件写入");
|
if (err != ESP_OK) {
|
||||||
return ESP_FAIL;
|
DEBUG_ERR("OTA", "无法打开 NVS 写入: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(f,
|
nvs_set_u32(handle, "total_size", meta->total_size);
|
||||||
"{\n"
|
nvs_set_u32(handle, "stored_size", meta->stored_size);
|
||||||
" \"total_size\": %" PRIu32 ",\n"
|
nvs_set_u32(handle, "last_seq", meta->last_seq);
|
||||||
" \"stored_size\": %" PRIu32 ",\n"
|
nvs_set_u32(handle, "chunk_count", meta->chunk_count);
|
||||||
" \"last_seq\": %" PRIu32 ",\n"
|
err = nvs_commit(handle);
|
||||||
" \"chunk_count\": %" PRIu32 "\n"
|
nvs_close(handle);
|
||||||
"}",
|
|
||||||
meta->total_size, meta->stored_size, meta->last_seq, meta->chunk_count);
|
|
||||||
|
|
||||||
fclose(f);
|
if (err != ESP_OK) {
|
||||||
DEBUG_INFO("OTA", "元数据已保存到 Flash");
|
DEBUG_ERR("OTA", "NVS commit 失败: %s", esp_err_to_name(err));
|
||||||
return ESP_OK;
|
}
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,35 +592,80 @@ esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len)
|
|||||||
* @brief 阶段1 - 发起与 GD32 的握手
|
* @brief 阶段1 - 发起与 GD32 的握手
|
||||||
* @return 操作结果码。
|
* @return 操作结果码。
|
||||||
*/
|
*/
|
||||||
|
static void request_gd32_app_enter_bootloader(void)
|
||||||
|
{
|
||||||
|
DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口");
|
||||||
|
esp_err_t err = gd32_app_request_bootloader(500);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
/* 收到确认:可能来自 APP(即将跳转 BOOT)或 BOOT 本身(已在 BOOT)。
|
||||||
|
* 如果来自 APP,GD32 需要复位并重新初始化 SPI 外设,需等 ~1-2s;
|
||||||
|
* 如果已在 BOOT,SPI 已经就绪,200ms 足够。
|
||||||
|
* 等待 1000ms 给 GD32 足够时间完成系统复位和 Bootloader 初始化。 */
|
||||||
|
DEBUG_INFO("OTA", "GD32 已确认进入 Bootloader,等待就绪");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
} else {
|
||||||
|
DEBUG_WARN("OTA", "GD32 APP 未确认进入 Bootloader,继续尝试直接握手: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static ota_result_t send_handshake_req(void)
|
static ota_result_t send_handshake_req(void)
|
||||||
{
|
{
|
||||||
DEBUG_INFO("OTA", "=== 阶段1: 握手开始 ===");
|
DEBUG_INFO("OTA", "=== 阶段1: 握手开始 ===");
|
||||||
DEBUG_INFO("OTA", "发送 HANDSHAKE_REQ (0x00 0xAC)");
|
DEBUG_INFO("OTA", "发送 HANDSHAKE_REQ (0x00 0xAC)");
|
||||||
|
|
||||||
uint8_t payload[] = {0x00, 0xAC};
|
uint8_t payload[] = {0x00, 0xAC};
|
||||||
uint8_t resp_type = 0;
|
uint8_t last_resp_type = 0U;
|
||||||
uint8_t resp_payload[64] = {0};
|
size_t last_resp_len = 0U;
|
||||||
size_t resp_len = 0;
|
esp_err_t last_err = ESP_OK;
|
||||||
|
constexpr uint32_t kHandshakeAttempts = 10U;
|
||||||
|
constexpr uint32_t kHandshakeReqTimeoutMs = 200U;
|
||||||
|
constexpr uint32_t kHandshakePollTimeoutMs = 200U;
|
||||||
|
constexpr uint32_t kHandshakeRetryGapMs = 100U;
|
||||||
|
|
||||||
/** @brief 当前 OTA 中继状态机状态。 */
|
/** @brief 当前 OTA 中继状态机状态。 */
|
||||||
g_relay_state = RELAY_HANDSHAKE_SENT;
|
g_relay_state = RELAY_HANDSHAKE_SENT;
|
||||||
DEBUG_INFO("OTA", "状态: RELAY_HANDSHAKE_SENT");
|
DEBUG_INFO("OTA", "状态: RELAY_HANDSHAKE_SENT");
|
||||||
|
|
||||||
|
for (uint32_t attempt = 0U; attempt < kHandshakeAttempts; ++attempt) {
|
||||||
|
uint8_t resp_type = 0U;
|
||||||
|
uint8_t resp_payload[64] = {0};
|
||||||
|
size_t resp_len = 0U;
|
||||||
|
|
||||||
esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_REQ, payload, sizeof(payload),
|
esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_REQ, payload, sizeof(payload),
|
||||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||||
log_resp_payload("HANDSHAKE_REQ", resp_type, resp_payload, resp_len);
|
kHandshakeReqTimeoutMs);
|
||||||
|
last_err = err;
|
||||||
|
last_resp_type = resp_type;
|
||||||
|
last_resp_len = resp_len;
|
||||||
|
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
err = wait_until_not_wait("HANDSHAKE_REQ", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 8);
|
log_resp_payload("HANDSHAKE_REQ", resp_type, resp_payload, resp_len);
|
||||||
|
err = poll_until_expected_ack_shape("HANDSHAKE_REQ", &resp_type, resp_payload,
|
||||||
|
sizeof(resp_payload), &resp_len,
|
||||||
|
kHandshakePollTimeoutMs, 2, 2, is_handshake_echo_ack);
|
||||||
|
last_err = err;
|
||||||
|
last_resp_type = resp_type;
|
||||||
|
last_resp_len = resp_len;
|
||||||
|
|
||||||
|
if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK && is_handshake_echo_ack(resp_payload, resp_len)) {
|
||||||
|
DEBUG_INFO("OTA", "握手请求已确认 (attempt=%lu)", (unsigned long)(attempt + 1U));
|
||||||
|
return OTA_RESULT_OK;
|
||||||
}
|
}
|
||||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 2 ||
|
|
||||||
resp_payload[0] != 0x00 || resp_payload[1] != 0xAC) {
|
|
||||||
DEBUG_ERR("OTA", "HANDSHAKE_REQ 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
|
|
||||||
g_relay_state = RELAY_ERROR;
|
|
||||||
return OTA_RESULT_ERR_TIMEOUT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_INFO("OTA", "握手请求已确认");
|
DEBUG_WARN("OTA", "HANDSHAKE_REQ 未就绪,重试 %lu/%lu: err=%d resp_type=0x%02X resp_len=%u",
|
||||||
return OTA_RESULT_OK;
|
(unsigned long)(attempt + 1U),
|
||||||
|
(unsigned long)kHandshakeAttempts,
|
||||||
|
last_err,
|
||||||
|
last_resp_type,
|
||||||
|
(unsigned)last_resp_len);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(kHandshakeRetryGapMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_ERR("OTA", "HANDSHAKE_REQ 失败, err=%d, resp_type=0x%02X, resp_len=%zu",
|
||||||
|
last_err, last_resp_type, last_resp_len);
|
||||||
|
g_relay_state = RELAY_ERROR;
|
||||||
|
return OTA_RESULT_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -355,10 +692,10 @@ static ota_result_t send_nonce_and_authenticate(void)
|
|||||||
esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_REQ, g_nonce_server, 16,
|
esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_REQ, g_nonce_server, 16,
|
||||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 5000);
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 5000);
|
||||||
log_resp_payload("CHALLENGE", resp_type, resp_payload, resp_len);
|
log_resp_payload("CHALLENGE", resp_type, resp_payload, resp_len);
|
||||||
if (err == ESP_OK) {
|
err = poll_until_expected_ack_shape("CHALLENGE", &resp_type, resp_payload,
|
||||||
err = wait_until_not_wait("CHALLENGE", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 12);
|
sizeof(resp_payload), &resp_len,
|
||||||
}
|
1000, 12, 8, is_challenge_payload_ack);
|
||||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len != 36) {
|
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !is_challenge_payload_ack(resp_payload, resp_len)) {
|
||||||
DEBUG_ERR("OTA", "CHALLENGE 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
|
DEBUG_ERR("OTA", "CHALLENGE 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
|
||||||
g_relay_state = RELAY_ERROR;
|
g_relay_state = RELAY_ERROR;
|
||||||
return OTA_RESULT_ERR_TIMEOUT;
|
return OTA_RESULT_ERR_TIMEOUT;
|
||||||
@@ -368,8 +705,8 @@ static ota_result_t send_nonce_and_authenticate(void)
|
|||||||
DEBUG_INFO("OTA", "已收到 GD32 握手数据包");
|
DEBUG_INFO("OTA", "已收到 GD32 握手数据包");
|
||||||
DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_RESPOND");
|
DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_RESPOND");
|
||||||
|
|
||||||
if (aes128_ecb_encrypt_block_local(MASTER_KEY, g_nonce_server, g_k_session) != 0) {
|
if (!parse_and_verify_challenge_payload(resp_payload, resp_len, g_k_session, g_gd32_uid_runtime)) {
|
||||||
DEBUG_ERR("OTA", "计算 K_session 失败");
|
DEBUG_ERR("OTA", "CHALLENGE 解包/校验失败");
|
||||||
g_relay_state = RELAY_ERROR;
|
g_relay_state = RELAY_ERROR;
|
||||||
return OTA_RESULT_ERR_AUTH;
|
return OTA_RESULT_ERR_AUTH;
|
||||||
}
|
}
|
||||||
@@ -393,16 +730,25 @@ static ota_result_t send_nonce_and_authenticate(void)
|
|||||||
err = spi_ota_transfer(SPI_OTA_CMD_REQ, encrypted_confirm, 16,
|
err = spi_ota_transfer(SPI_OTA_CMD_REQ, encrypted_confirm, 16,
|
||||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||||
log_resp_payload("CONFIRM", resp_type, resp_payload, resp_len);
|
log_resp_payload("CONFIRM", resp_type, resp_payload, resp_len);
|
||||||
if (err == ESP_OK) {
|
err = poll_until_expected_ack_shape("CONFIRM", &resp_type, resp_payload,
|
||||||
err = wait_until_not_wait("CONFIRM", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 8);
|
sizeof(resp_payload), &resp_len,
|
||||||
}
|
1000, 8, 8, is_confirm_plain_ack);
|
||||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 2 ||
|
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !is_confirm_plain_ack(resp_payload, resp_len)) {
|
||||||
resp_payload[0] != 0xBA || resp_payload[1] != 0xC0) {
|
|
||||||
DEBUG_ERR("OTA", "SESSION_KEY/CONFIRM 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
|
DEBUG_ERR("OTA", "SESSION_KEY/CONFIRM 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
|
||||||
g_relay_state = RELAY_ERROR;
|
g_relay_state = RELAY_ERROR;
|
||||||
return OTA_RESULT_ERR_TIMEOUT;
|
return OTA_RESULT_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 诊断: 打印 confirm 响应完整内容,确认是 2B 还是意外读到了 36B challenge 响应 */
|
||||||
|
{
|
||||||
|
char hex[73] = {0};
|
||||||
|
size_t n = (resp_len > 36U) ? 36U : resp_len;
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", resp_payload[i]);
|
||||||
|
}
|
||||||
|
DEBUG_INFO("OTA", "CONFIRM 验证通过, resp_len=%zu, payload=%s", resp_len, hex);
|
||||||
|
}
|
||||||
|
|
||||||
g_relay_state = RELAY_AUTH_COMPLETE;
|
g_relay_state = RELAY_AUTH_COMPLETE;
|
||||||
DEBUG_INFO("OTA", "状态: RELAY_AUTH_COMPLETE");
|
DEBUG_INFO("OTA", "状态: RELAY_AUTH_COMPLETE");
|
||||||
DEBUG_INFO("OTA", "=== 阶段1 认证完成: 安全通道已建立 ===");
|
DEBUG_INFO("OTA", "=== 阶段1 认证完成: 安全通道已建立 ===");
|
||||||
@@ -437,11 +783,11 @@ static ota_result_t send_firmware_size(uint32_t size)
|
|||||||
esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload),
|
esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload),
|
||||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||||
log_resp_payload("SEND_SIZE", resp_type, resp_payload, resp_len);
|
log_resp_payload("SEND_SIZE", resp_type, resp_payload, resp_len);
|
||||||
if (err == ESP_OK) {
|
DEBUG_WARN("OTA", "SEND_SIZE poll config: poll_timeout_ms=2000 wait_rounds=300 maxExtraPolls=24");
|
||||||
err = wait_until_not_wait("SEND_SIZE", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1500, 12);
|
err = poll_until_expected_ack_shape("SEND_SIZE", &resp_type, resp_payload,
|
||||||
}
|
sizeof(resp_payload), &resp_len,
|
||||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 2 ||
|
2000, 300, 24, is_size_seq0_ack);
|
||||||
resp_payload[0] != 0xBA || resp_payload[1] != 0xC0) {
|
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !is_size_seq0_ack(resp_payload, resp_len)) {
|
||||||
DEBUG_ERR("OTA", "SEND_SIZE 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
|
DEBUG_ERR("OTA", "SEND_SIZE 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
|
||||||
g_relay_state = RELAY_ERROR;
|
g_relay_state = RELAY_ERROR;
|
||||||
return OTA_RESULT_ERR_TIMEOUT;
|
return OTA_RESULT_ERR_TIMEOUT;
|
||||||
@@ -481,8 +827,9 @@ static ota_result_t relay_packets_from_flash(void)
|
|||||||
g_relay_state = RELAY_RELAY_ACTIVE;
|
g_relay_state = RELAY_RELAY_ACTIVE;
|
||||||
|
|
||||||
for (uint32_t seq = 0; seq < total_packets; seq++) {
|
for (uint32_t seq = 0; seq < total_packets; seq++) {
|
||||||
|
uint32_t retry_count = 0;
|
||||||
if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == total_packets - 1) {
|
if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == total_packets - 1) {
|
||||||
health_status_t health = health_check_before_ota();
|
health_status_t health = health_check_before_ota_relay();
|
||||||
if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
|
if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
|
||||||
DEBUG_ERR("OTA", "健康检查危险,中止 OTA");
|
DEBUG_ERR("OTA", "健康检查危险,中止 OTA");
|
||||||
return OTA_RESULT_ERR_HEALTH;
|
return OTA_RESULT_ERR_HEALTH;
|
||||||
@@ -520,12 +867,13 @@ static ota_result_t relay_packets_from_flash(void)
|
|||||||
tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU);
|
tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU);
|
||||||
calculate_crc32_with_key_local(g_k_session, plain48, &tx_buf[60]);
|
calculate_crc32_with_key_local(g_k_session, plain48, &tx_buf[60]);
|
||||||
|
|
||||||
DEBUG_INFO("OTA", "包 %u: chunk=%u if_end=0x%08X crc32=%02X%02X%02X%02X",
|
DEBUG_DBG("OTA", "包 %u: chunk=%u if_end=0x%08X crc32=%02X%02X%02X%02X",
|
||||||
seq, (unsigned)chunk_size, (unsigned)if_end,
|
seq, (unsigned)chunk_size, (unsigned)if_end,
|
||||||
tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]);
|
tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]);
|
||||||
|
|
||||||
g_relay_state = RELAY_PACKET_SENT;
|
g_relay_state = RELAY_PACKET_SENT;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
resp_len = 0;
|
resp_len = 0;
|
||||||
err = spi_ota_transfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf),
|
err = spi_ota_transfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf),
|
||||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||||
@@ -533,25 +881,88 @@ static ota_result_t relay_packets_from_flash(void)
|
|||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
err = wait_until_not_wait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10);
|
err = wait_until_not_wait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10);
|
||||||
}
|
}
|
||||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 4) {
|
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||||
DEBUG_WARN("OTA", "数据包 %u 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
|
DEBUG_WARN("OTA", "数据包 %u 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
|
||||||
result = OTA_RESULT_ERR_TIMEOUT;
|
result = OTA_RESULT_ERR_TIMEOUT;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t ack_value = (uint32_t)resp_payload[0] |
|
|
||||||
((uint32_t)resp_payload[1] << 8) |
|
|
||||||
((uint32_t)resp_payload[2] << 16) |
|
|
||||||
((uint32_t)resp_payload[3] << 24);
|
|
||||||
uint32_t expected_ack = (seq == total_packets - 1) ? 0xFFFFFFFFU : (seq + 1U);
|
uint32_t expected_ack = (seq == total_packets - 1) ? 0xFFFFFFFFU : (seq + 1U);
|
||||||
if (ack_value != expected_ack) {
|
|
||||||
DEBUG_ERR("OTA", "包 %u ACK 不匹配: got=0x%08X expected=0x%08X", seq, ack_value, expected_ack);
|
/* 跳过上一阶段残留的、seq 不匹配的结构化 ACK。
|
||||||
|
*
|
||||||
|
* 典型场景:SIZE 阶段的 ACK(seq=0) 仍挂在 DMA 上,
|
||||||
|
* 包 0 发出后第一次 poll 读到 seq=0 而非期望的 seq=1。
|
||||||
|
* 继续轮询直到拿到正确的 ACK 或超时。 */
|
||||||
|
if (!is_structured_ack_with_seq(resp_payload, resp_len, expected_ack)) {
|
||||||
|
uint32_t err_seq = 0;
|
||||||
|
uint8_t err_code = 0;
|
||||||
|
if (parse_structured_error(resp_payload, resp_len, &err_seq, &err_code)) {
|
||||||
|
if (err_seq == seq) {
|
||||||
|
DEBUG_WARN("OTA", "包 %u 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||||
|
g_relay_state = RELAY_PACKET_ACK_RETRY;
|
||||||
|
if (++retry_count >= 3U) {
|
||||||
|
DEBUG_ERR("OTA", "包 %u 重发次数超限", seq);
|
||||||
|
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue; /* 重发本包 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 非期望 ACK,也非错误帧 → 可能是残留 ACK,循环 poll 直到拿到正确的 */
|
||||||
|
bool got_expected = false;
|
||||||
|
for (uint32_t retry = 0; retry < 5; ++retry) {
|
||||||
|
DEBUG_WARN("OTA", "包 %u 收到非目标ACK(seq不匹配),继续轮询(%u/5)...", seq, retry + 1);
|
||||||
|
err = spi_ota_poll_response(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
|
||||||
|
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||||
|
err = wait_until_not_wait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10);
|
||||||
|
}
|
||||||
|
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (is_structured_ack_with_seq(resp_payload, resp_len, expected_ack)) {
|
||||||
|
got_expected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (parse_structured_error(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||||
|
break; /* 下面统一处理 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (got_expected) {
|
||||||
|
/* 匹配成功 */
|
||||||
|
flash_offset += (uint32_t)chunk_size;
|
||||||
|
g_relay_state = RELAY_PACKET_ACK_OK;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 最终检查是否为错误帧 */
|
||||||
|
if (parse_structured_error(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||||
|
DEBUG_WARN("OTA", "包 %u 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||||
|
g_relay_state = RELAY_PACKET_ACK_RETRY;
|
||||||
|
if (++retry_count >= 3U) {
|
||||||
|
DEBUG_ERR("OTA", "包 %u 重发次数超限", seq);
|
||||||
|
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue; /* 重发本包 */
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_ERR("OTA", "包 %u ACK/ERROR 不匹配,len=%zu", seq, resp_len);
|
||||||
result = OTA_RESULT_ERR_GD32;
|
result = OTA_RESULT_ERR_GD32;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 匹配成功 */
|
||||||
flash_offset += (uint32_t)chunk_size;
|
flash_offset += (uint32_t)chunk_size;
|
||||||
g_relay_state = RELAY_PACKET_ACK_OK;
|
g_relay_state = RELAY_PACKET_ACK_OK;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != OTA_RESULT_OK) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (seq % (total_packets / 10 + 1) == 0) {
|
if (seq % (total_packets / 10 + 1) == 0) {
|
||||||
uint32_t progress = (seq * 100U) / total_packets;
|
uint32_t progress = (seq * 100U) / total_packets;
|
||||||
@@ -560,7 +971,7 @@ static ota_result_t relay_packets_from_flash(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
g_relay_state = RELAY_RECEIVE_DONE;
|
g_relay_state = RELAY_RECEIVE_DONE;
|
||||||
DEBUG_INFO("OTA", "状态: RELAY_RECEIVE_DONE");
|
DEBUG_DBG("OTA", "状态: RELAY_RECEIVE_DONE");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,18 +982,88 @@ static ota_result_t relay_packets_from_flash(void)
|
|||||||
static ota_result_t finish_ota(void)
|
static ota_result_t finish_ota(void)
|
||||||
{
|
{
|
||||||
DEBUG_INFO("OTA", "=== 阶段4: 结束 OTA ===");
|
DEBUG_INFO("OTA", "=== 阶段4: 结束 OTA ===");
|
||||||
DEBUG_INFO("OTA", "GD32 当前实现已在最后一个 DATA 包后自行 ACK 并跳转,无需额外 FINISH 命令");
|
DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC),等待 GD32 确认即将跳转 APP");
|
||||||
|
|
||||||
|
uint8_t resp_type = 0;
|
||||||
|
uint8_t resp_payload[32] = {0};
|
||||||
|
size_t resp_len = 0;
|
||||||
|
uint8_t finish_payload[2] = {0x0A, 0xAC};
|
||||||
|
|
||||||
|
g_relay_state = RELAY_FINISH_SENT;
|
||||||
|
esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload),
|
||||||
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||||
|
log_resp_payload("FINISH_OTA", resp_type, resp_payload, resp_len);
|
||||||
|
err = poll_until_expected_ack_shape("FINISH_OTA", &resp_type, resp_payload,
|
||||||
|
sizeof(resp_payload), &resp_len,
|
||||||
|
1000, 10, 8, is_finish_ack_or_error);
|
||||||
|
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK ||
|
||||||
|
!is_finish_ack_or_error(resp_payload, resp_len)) {
|
||||||
|
DEBUG_ERR("OTA", "FINISH_OTA 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len);
|
||||||
|
g_relay_state = RELAY_ERROR;
|
||||||
|
return OTA_RESULT_ERR_GD32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 检查是否为结构化错误帧(BOOT validateApplication 失败等) */
|
||||||
|
uint32_t err_seq = 0;
|
||||||
|
uint8_t err_code = 0;
|
||||||
|
if (parse_structured_error(resp_payload, resp_len, &err_seq, &err_code)) {
|
||||||
|
DEBUG_ERR("OTA", "FINISH_OTA BOOT 返回错误: seq=0x%08lX err=0x%02X (%s)",
|
||||||
|
(unsigned long)err_seq, err_code,
|
||||||
|
err_code == 0x02U ? "FLASH_ERROR/validateApp失败" :
|
||||||
|
err_code == 0x04U ? "INVALID_PACKET" :
|
||||||
|
err_code == 0x03U ? "INVALID_KEY" :
|
||||||
|
"未知错误");
|
||||||
|
g_relay_state = RELAY_ERROR;
|
||||||
|
return OTA_RESULT_ERR_GD32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 正常明文 ACK: 0x0A 0xAC */
|
||||||
|
if (!is_finish_plain_ack(resp_payload, resp_len)) {
|
||||||
|
DEBUG_ERR("OTA", "FINISH_OTA 响应格式异常: len=%zu payload[0]=0x%02X", resp_len, resp_payload[0]);
|
||||||
|
g_relay_state = RELAY_ERROR;
|
||||||
|
return OTA_RESULT_ERR_GD32;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_relay_state = RELAY_DONE;
|
||||||
session_key_clear(&g_session_ctx);
|
session_key_clear(&g_session_ctx);
|
||||||
memset(&g_session_ctx, 0, sizeof(session_ctx_t));
|
memset(&g_session_ctx, 0, sizeof(session_ctx_t));
|
||||||
memset(g_k_session, 0, sizeof(g_k_session));
|
memset(g_k_session, 0, sizeof(g_k_session));
|
||||||
memset(g_nonce_server, 0, sizeof(g_nonce_server));
|
memset(g_nonce_server, 0, sizeof(g_nonce_server));
|
||||||
|
memset(g_gd32_uid_runtime, 0, sizeof(g_gd32_uid_runtime));
|
||||||
|
|
||||||
g_relay_state = RELAY_DONE;
|
|
||||||
DEBUG_INFO("OTA", "状态: RELAY_DONE");
|
DEBUG_INFO("OTA", "状态: RELAY_DONE");
|
||||||
DEBUG_INFO("OTA", "=== OTA 完成 ===");
|
DEBUG_INFO("OTA", "=== OTA 完成,等待 GD32 APP 启动 ===");
|
||||||
|
|
||||||
meta_clear();
|
meta_clear();
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 阶段5: APP 存活验证
|
||||||
|
*
|
||||||
|
* GD32 BOOT 处理完 FINISH_OTA 后会跳转到 APP。
|
||||||
|
* APP 启动需要初始化外设和 SPI,大约需要 1-2 秒。
|
||||||
|
* 我们以 500ms 间隔反复 ping APP,最多等 15 秒。
|
||||||
|
* 收到 APP_ALIVE 回复才认为 OTA 真正成功。
|
||||||
|
* ============================================================ */
|
||||||
|
g_relay_state = RELAY_APP_VERIFY;
|
||||||
|
g_gd32_app_alive = false;
|
||||||
|
|
||||||
|
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
uint32_t uptime = 0;
|
||||||
|
esp_err_t ping_err = gd32_app_ping(1000, &uptime);
|
||||||
|
if (ping_err == ESP_OK) {
|
||||||
|
g_gd32_app_alive = true;
|
||||||
|
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%lu sec", (unsigned long)uptime);
|
||||||
|
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%u/30)", attempt + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_gd32_app_alive) {
|
||||||
|
DEBUG_WARN("OTA", "GD32 APP 存活确认超时,OTA 传输已完成但 APP 未启动");
|
||||||
|
}
|
||||||
|
|
||||||
return OTA_RESULT_OK;
|
return OTA_RESULT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,6 +1075,8 @@ ota_result_t ota_relay_start_handshake(void)
|
|||||||
{
|
{
|
||||||
ota_result_t res;
|
ota_result_t res;
|
||||||
|
|
||||||
|
request_gd32_app_enter_bootloader();
|
||||||
|
|
||||||
res = send_handshake_req();
|
res = send_handshake_req();
|
||||||
if (res != OTA_RESULT_OK) return res;
|
if (res != OTA_RESULT_OK) return res;
|
||||||
|
|
||||||
@@ -627,6 +1110,8 @@ ota_result_t ota_relay_forward_firmware(void)
|
|||||||
DEBUG_INFO("OTA", "开始固件转发: %u 字节, %u 块",
|
DEBUG_INFO("OTA", "开始固件转发: %u 字节, %u 块",
|
||||||
meta.total_size, meta.chunk_count);
|
meta.total_size, meta.chunk_count);
|
||||||
|
|
||||||
|
request_gd32_app_enter_bootloader();
|
||||||
|
|
||||||
ota_result_t res = send_handshake_req();
|
ota_result_t res = send_handshake_req();
|
||||||
if (res != OTA_RESULT_OK) return res;
|
if (res != OTA_RESULT_OK) return res;
|
||||||
|
|
||||||
@@ -662,6 +1147,10 @@ void ota_relay_reset(void)
|
|||||||
/** @brief 当前 OTA 中继状态机状态。 */
|
/** @brief 当前 OTA 中继状态机状态。 */
|
||||||
g_relay_state = RELAY_IDLE;
|
g_relay_state = RELAY_IDLE;
|
||||||
session_key_clear(&g_session_ctx);
|
session_key_clear(&g_session_ctx);
|
||||||
|
memset(&g_session_ctx, 0, sizeof(session_ctx_t));
|
||||||
|
memset(g_k_session, 0, sizeof(g_k_session));
|
||||||
|
memset(g_nonce_server, 0, sizeof(g_nonce_server));
|
||||||
|
memset(g_gd32_uid_runtime, 0, sizeof(g_gd32_uid_runtime));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -698,6 +1187,7 @@ void ota_relay_task(void *pvParameters)
|
|||||||
|
|
||||||
if (result == OTA_RESULT_OK) {
|
if (result == OTA_RESULT_OK) {
|
||||||
DEBUG_INFO("OTA", "OTA 转发成功完成");
|
DEBUG_INFO("OTA", "OTA 转发成功完成");
|
||||||
|
ota_relay_reset();
|
||||||
} else {
|
} else {
|
||||||
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
|
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
|
||||||
ota_relay_reset();
|
ota_relay_reset();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* ota_relay.h - OTA 中继核心状态机
|
* ota_relay.h - OTA 中继核心状态机
|
||||||
* T2.3, T3.1 - T3.7
|
* T2.3, T3.1 - T3.7
|
||||||
*/
|
*/
|
||||||
@@ -9,8 +9,9 @@
|
|||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
|
|
||||||
// T4.3: OTA 元数据结构,用于 Flash 存储
|
// T4.3: OTA 元数据结构,用于 NVS 存储
|
||||||
#define META_FILE "/spiffs/ota_meta.json"
|
#define OTA_META_NVS_NAMESPACE "ota_meta"
|
||||||
|
#define OTA_META_NVS_KEY "meta"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t total_size; // 固件总大小(字节)
|
uint32_t total_size; // 固件总大小(字节)
|
||||||
@@ -49,6 +50,7 @@ typedef enum {
|
|||||||
RELAY_TRANSFER_DONE, // 已收到 0xBA C0 + FF FF FF FF
|
RELAY_TRANSFER_DONE, // 已收到 0xBA C0 + FF FF FF FF
|
||||||
RELAY_FINISH_SENT, // 已发送 0x0A AC,等待 GD32 确认
|
RELAY_FINISH_SENT, // 已发送 0x0A AC,等待 GD32 确认
|
||||||
RELAY_DONE, // GD32 回复 0x0A AC,OTA 完成
|
RELAY_DONE, // GD32 回复 0x0A AC,OTA 完成
|
||||||
|
RELAY_APP_VERIFY, // 等待 GD32 APP 启动并回复存活确认
|
||||||
|
|
||||||
// 错误状态
|
// 错误状态
|
||||||
RELAY_ERROR = 0xFF
|
RELAY_ERROR = 0xFF
|
||||||
@@ -145,6 +147,9 @@ esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq);
|
|||||||
*/
|
*/
|
||||||
esp_err_t meta_clear(void);
|
esp_err_t meta_clear(void);
|
||||||
|
|
||||||
|
// GD32 APP 存活标志(OTA 完成后由 APP PING 确认)
|
||||||
|
extern volatile bool g_gd32_app_alive;
|
||||||
|
|
||||||
// 当前中继状态(供健康监控外部访问)
|
// 当前中继状态(供健康监控外部访问)
|
||||||
extern relay_state_t g_relay_state;
|
extern relay_state_t g_relay_state;
|
||||||
|
|
||||||
|
|||||||
+62
-3
@@ -79,6 +79,10 @@ static bool s_esp32_reboot_pending = false;
|
|||||||
static uint8_t s_esp32_last_cmd = 0;
|
static uint8_t s_esp32_last_cmd = 0;
|
||||||
static uint16_t s_esp32_last_seq = 0xFFFFU;
|
static uint16_t s_esp32_last_seq = 0xFFFFU;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 异步完成 ESP32 OTA 收尾并触发重启。
|
||||||
|
* @param pvParameters 任务参数,当前未使用。
|
||||||
|
*/
|
||||||
static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||||
{
|
{
|
||||||
(void)pvParameters;
|
(void)pvParameters;
|
||||||
@@ -179,6 +183,11 @@ static int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len)
|
|||||||
return (int)copy_len;
|
return (int)copy_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化 ESP32 本机 OTA 写入会话并选择目标分区。
|
||||||
|
* @param total_size 固件总大小(字节)。
|
||||||
|
* @return ESP_OK 成功,其他为失败错误码。
|
||||||
|
*/
|
||||||
static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||||
{
|
{
|
||||||
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
|
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
|
||||||
@@ -238,6 +247,12 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 向 ESP32 OTA 分区写入一段固件数据。
|
||||||
|
* @param data 待写入数据指针。
|
||||||
|
* @param len 写入长度(字节)。
|
||||||
|
* @return ESP_OK 成功,其他为失败错误码。
|
||||||
|
*/
|
||||||
static esp_err_t ble_esp32_ota_write(const uint8_t *data, size_t len)
|
static esp_err_t ble_esp32_ota_write(const uint8_t *data, size_t len)
|
||||||
{
|
{
|
||||||
if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
|
if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
|
||||||
@@ -250,6 +265,12 @@ static esp_err_t ble_esp32_ota_write(const uint8_t *data, size_t len)
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 发送窗口 ACK 通知,回报已处理的分块序号与累计字节数。
|
||||||
|
* @param cmd 对应的数据命令字节。
|
||||||
|
* @param seq 当前确认的分块序号。
|
||||||
|
* @param completed_bytes 已完成写入的累计字节数。
|
||||||
|
*/
|
||||||
static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t completed_bytes)
|
static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t completed_bytes)
|
||||||
{
|
{
|
||||||
uint8_t ack[8] = {0};
|
uint8_t ack[8] = {0};
|
||||||
@@ -272,6 +293,11 @@ static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t complete
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 发送收尾阶段事件通知(开始/完成/失败/重启等)。
|
||||||
|
* @param stage 阶段标识码。
|
||||||
|
* @param value 阶段附带值(错误码或耗时等)。
|
||||||
|
*/
|
||||||
static void ble_ota_send_finish_event(uint8_t stage, uint32_t value)
|
static void ble_ota_send_finish_event(uint8_t stage, uint32_t value)
|
||||||
{
|
{
|
||||||
uint8_t pkt[6] = {0};
|
uint8_t pkt[6] = {0};
|
||||||
@@ -297,6 +323,14 @@ static void ble_ota_send_finish_event(uint8_t stage, uint32_t value)
|
|||||||
* CTRL (0x0001) - Write
|
* CTRL (0x0001) - Write
|
||||||
* 写入 0xF7 触发 OTA 中继
|
* 写入 0xF7 触发 OTA 中继
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @brief GATT CTRL 特征访问回调,处理控制命令写入。
|
||||||
|
* @param conn_handle BLE 连接句柄。
|
||||||
|
* @param attr_handle GATT 属性句柄。
|
||||||
|
* @param ctxt GATT 访问上下文。
|
||||||
|
* @param arg 回调用户参数。
|
||||||
|
* @return NimBLE ATT 错误码,0 表示成功。
|
||||||
|
*/
|
||||||
static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||||
{
|
{
|
||||||
@@ -368,6 +402,14 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
* [7..n-4] payload 固件数据
|
* [7..n-4] payload 固件数据
|
||||||
* [n-4..n] crc 校验 (预留)
|
* [n-4..n] crc 校验 (预留)
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @brief GATT DATA_IN 特征访问回调,接收并处理 OTA 数据块。
|
||||||
|
* @param conn_handle BLE 连接句柄。
|
||||||
|
* @param attr_handle GATT 属性句柄。
|
||||||
|
* @param ctxt GATT 访问上下文。
|
||||||
|
* @param arg 回调用户参数。
|
||||||
|
* @return NimBLE ATT 错误码,0 表示成功。
|
||||||
|
*/
|
||||||
static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||||
{
|
{
|
||||||
@@ -479,9 +521,10 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
|
|
||||||
// --- GD32 OTA:首个分块时创建存储元数据 ---
|
// --- GD32 OTA:首个分块时创建存储元数据 ---
|
||||||
if (seq == 0 && total_size > 0) {
|
if (seq == 0 && total_size > 0) {
|
||||||
if (total_size > OTA_MAX_FIRMWARE_SIZE) {
|
uint32_t rounded_size = (uint32_t)(((total_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
||||||
DEBUG_ERR(TAG, "固件太大: %" PRIu32 " > %zu",
|
if (total_size == 0U || total_size > OTA_MAX_FIRMWARE_SIZE || rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
||||||
total_size, OTA_MAX_FIRMWARE_SIZE);
|
DEBUG_ERR(TAG, "GD32 固件大小非法: raw=%" PRIu32 " rounded48=%" PRIu32 " max=%zu",
|
||||||
|
total_size, rounded_size, OTA_MAX_FIRMWARE_SIZE);
|
||||||
return BLE_ATT_ERR_UNLIKELY;
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,6 +601,14 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
* VERSION (0x0004) - Read
|
* VERSION (0x0004) - Read
|
||||||
* 返回固件版本字符串
|
* 返回固件版本字符串
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @brief GATT VERSION 特征访问回调,返回固件版本字符串。
|
||||||
|
* @param conn_handle BLE 连接句柄。
|
||||||
|
* @param attr_handle GATT 属性句柄。
|
||||||
|
* @param ctxt GATT 访问上下文。
|
||||||
|
* @param arg 回调用户参数。
|
||||||
|
* @return NimBLE ATT 错误码,0 表示成功。
|
||||||
|
*/
|
||||||
static int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle,
|
static int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||||
{
|
{
|
||||||
@@ -578,6 +629,14 @@ static int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
* DATA_OUT (0x0003) - Notify only
|
* DATA_OUT (0x0003) - Notify only
|
||||||
* 访问回调不做读写处理,通知由 ble_ota_notify_response() 发送
|
* 访问回调不做读写处理,通知由 ble_ota_notify_response() 发送
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @brief GATT DATA_OUT 特征访问回调(Notify-only,占位实现)。
|
||||||
|
* @param conn_handle BLE 连接句柄。
|
||||||
|
* @param attr_handle GATT 属性句柄。
|
||||||
|
* @param ctxt GATT 访问上下文。
|
||||||
|
* @param arg 回调用户参数。
|
||||||
|
* @return 0。
|
||||||
|
*/
|
||||||
static int ble_ota_data_out_access(uint16_t conn_handle, uint16_t attr_handle,
|
static int ble_ota_data_out_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||||
{
|
{
|
||||||
|
|||||||
+363
-65
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* wifi_ota.c - WiFi OTA 服务器实现
|
* wifi_ota.c - WiFi OTA 服务器实现
|
||||||
* T4.1 - T4.5
|
* T4.1 - T4.5
|
||||||
*/
|
*/
|
||||||
@@ -30,12 +30,16 @@
|
|||||||
|
|
||||||
/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
|
/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
|
||||||
static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP;
|
static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP;
|
||||||
|
/** @brief 冷启动标志,上电复位或掉电复位时为 true。 */
|
||||||
|
static bool s_cold_boot = false;
|
||||||
/** @brief OTA 中继是否已触发标志。 */
|
/** @brief OTA 中继是否已触发标志。 */
|
||||||
static bool s_relay_triggered = false;
|
static bool s_relay_triggered = false;
|
||||||
/** @brief STA 模式连接状态。 */
|
/** @brief STA 模式连接状态。 */
|
||||||
static bool s_sta_connected = false;
|
static bool s_sta_connected = false;
|
||||||
/** @brief STA 连接重试次数。 */
|
/** @brief STA 连接重试次数。 */
|
||||||
static int s_sta_retry_count = 0;
|
static int s_sta_retry_count = 0;
|
||||||
|
/** @brief STA 重试已耗尽标志,用于加速 wifi_ota_task 中的 AP 切换。 */
|
||||||
|
static bool s_sta_retry_exhausted = false;
|
||||||
/** @brief STA 连接成功信号量。 */
|
/** @brief STA 连接成功信号量。 */
|
||||||
static SemaphoreHandle_t s_sta_connected_sem = NULL;
|
static SemaphoreHandle_t s_sta_connected_sem = NULL;
|
||||||
/** @brief STA 获取 IP 信号量。 */
|
/** @brief STA 获取 IP 信号量。 */
|
||||||
@@ -55,6 +59,13 @@ static bool s_gd32_storage_done = false;
|
|||||||
/** @brief HTTP 服务器句柄。 */
|
/** @brief HTTP 服务器句柄。 */
|
||||||
static httpd_handle_t s_http_server = NULL;
|
static httpd_handle_t s_http_server = NULL;
|
||||||
|
|
||||||
|
static constexpr uint32_t GD32_OTA_FLASH_SECTOR_SIZE = 4096U;
|
||||||
|
|
||||||
|
static uint32_t align_up_u32(uint32_t value, uint32_t alignment)
|
||||||
|
{
|
||||||
|
return ((value + alignment - 1U) / alignment) * alignment;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 预先声明:为 STA 模式配置静态 IP、网关和子网掩码。
|
* @brief 预先声明:为 STA 模式配置静态 IP、网关和子网掩码。
|
||||||
*/
|
*/
|
||||||
@@ -88,6 +99,13 @@ static esp_err_t http_options_handler(httpd_req_t *req)
|
|||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// WiFi/IP 事件回调(事件驱动连接)
|
// WiFi/IP 事件回调(事件驱动连接)
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
/**
|
||||||
|
* @brief WiFi/IP 事件回调,处理 STA 启停、重连和获取 IP 等状态变化。
|
||||||
|
* @param arg 回调用户参数。
|
||||||
|
* @param event_base 事件基类(WIFI_EVENT 或 IP_EVENT)。
|
||||||
|
* @param event_id 具体事件 ID。
|
||||||
|
* @param event_data 事件附带数据。
|
||||||
|
*/
|
||||||
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
|
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
|
||||||
int32_t event_id, void* event_data)
|
int32_t event_id, void* event_data)
|
||||||
{
|
{
|
||||||
@@ -104,6 +122,7 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base,
|
|||||||
s_sta_connected = true;
|
s_sta_connected = true;
|
||||||
/** @brief STA 连接重试次数。 */
|
/** @brief STA 连接重试次数。 */
|
||||||
s_sta_retry_count = 0;
|
s_sta_retry_count = 0;
|
||||||
|
s_sta_retry_exhausted = false; // ★ 连接成功,重置耗尽标志
|
||||||
if (s_sta_connected_sem) {
|
if (s_sta_connected_sem) {
|
||||||
xSemaphoreGive(s_sta_connected_sem);
|
xSemaphoreGive(s_sta_connected_sem);
|
||||||
}
|
}
|
||||||
@@ -117,10 +136,21 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base,
|
|||||||
|
|
||||||
if (s_sta_retry_count < WIFI_STA_MAX_RETRY) {
|
if (s_sta_retry_count < WIFI_STA_MAX_RETRY) {
|
||||||
s_sta_retry_count++;
|
s_sta_retry_count++;
|
||||||
|
// ★ 冷启动首次连接失败时增加退避延迟
|
||||||
|
// 长时间断电后路由器可能还在初始化(ARP/DHCP/信道选择)
|
||||||
|
// reason 201=NO_AP_FOUND, 15=AUTH_FAIL, 2=AUTH_EXPIRE, 204=HANDSHAKE_TIMEOUT
|
||||||
|
if (s_cold_boot && s_sta_retry_count <= 3) {
|
||||||
|
int delay_ms = 2000 * s_sta_retry_count; // 2s, 4s, 6s 递增退避
|
||||||
|
DEBUG_INFO("WIFI", "冷启动退避重连第 %d 次 (reason=%d, 等 %dms)...",
|
||||||
|
s_sta_retry_count, disconn->reason, delay_ms);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(delay_ms));
|
||||||
|
} else {
|
||||||
DEBUG_INFO("WIFI", "重连第 %d 次...", s_sta_retry_count);
|
DEBUG_INFO("WIFI", "重连第 %d 次...", s_sta_retry_count);
|
||||||
|
}
|
||||||
esp_wifi_connect();
|
esp_wifi_connect();
|
||||||
} else {
|
} else {
|
||||||
DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", s_sta_retry_count);
|
DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", s_sta_retry_count);
|
||||||
|
s_sta_retry_exhausted = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -226,6 +256,8 @@ static esp_err_t wifi_init_sta(void)
|
|||||||
s_wifi_mode = OTA_WIFI_MODE_STA;
|
s_wifi_mode = OTA_WIFI_MODE_STA;
|
||||||
/** @brief STA 连接重试次数。 */
|
/** @brief STA 连接重试次数。 */
|
||||||
s_sta_retry_count = 0;
|
s_sta_retry_count = 0;
|
||||||
|
/** @brief STA 重试已耗尽标志。 */
|
||||||
|
s_sta_retry_exhausted = false;
|
||||||
|
|
||||||
// ★ 设置国家代码:中国,允许信道 1-13
|
// ★ 设置国家代码:中国,允许信道 1-13
|
||||||
// ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13
|
// ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13
|
||||||
@@ -253,6 +285,15 @@ static esp_err_t wifi_init_sta(void)
|
|||||||
|
|
||||||
wifi_set_static_ip();
|
wifi_set_static_ip();
|
||||||
|
|
||||||
|
// ★ 冷启动时在 wifi_start 前增加延迟
|
||||||
|
// 长时间断电后首次上电,WiFi PHY 校准需要额外时间
|
||||||
|
// esp_wifi_start() 内部会触发 PHY 初始化和校准
|
||||||
|
// 如果校准未完成就启动射频,可能导致连接失败
|
||||||
|
if (s_cold_boot) {
|
||||||
|
DEBUG_INFO("WIFI", "冷启动: 等待 PHY 就绪...");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
}
|
||||||
|
|
||||||
ESP_ERROR_CHECK(esp_wifi_start());
|
ESP_ERROR_CHECK(esp_wifi_start());
|
||||||
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
||||||
|
|
||||||
@@ -277,8 +318,18 @@ static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
|
|||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 使用轮询等待,检查 s_sta_retry_exhausted 标志加速退出
|
||||||
|
// 不再一次性等完整超时,而是每秒检查一次重试耗尽状态
|
||||||
TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_STA_CONNECT_TIMEOUT_MS);
|
TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_STA_CONNECT_TIMEOUT_MS);
|
||||||
if (xSemaphoreTake(wait_sem, timeout_ticks) == pdTRUE) {
|
TickType_t start_tick = xTaskGetTickCount();
|
||||||
|
TickType_t deadline = start_tick + timeout_ticks;
|
||||||
|
|
||||||
|
while (xTaskGetTickCount() < deadline) {
|
||||||
|
// 每次最多等 1 秒
|
||||||
|
TickType_t wait_until = xTaskGetTickCount() + pdMS_TO_TICKS(1000);
|
||||||
|
if (wait_until > deadline) wait_until = deadline;
|
||||||
|
|
||||||
|
if (xSemaphoreTake(wait_sem, wait_until - xTaskGetTickCount()) == pdTRUE) {
|
||||||
esp_netif_ip_info_t ip_info;
|
esp_netif_ip_info_t ip_info;
|
||||||
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
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 (netif != NULL && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
|
||||||
@@ -292,6 +343,13 @@ static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 重试已耗尽,不再等待
|
||||||
|
if (s_sta_retry_exhausted) {
|
||||||
|
DEBUG_ERR("WIFI", "STA 重试已耗尽 (%d 次),立即切换 AP", s_sta_retry_count);
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, s_sta_retry_count);
|
DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, s_sta_retry_count);
|
||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
@@ -502,7 +560,7 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
|
|||||||
int depth = 1; // 开头的 '{' 已被 peek 消费,从 1 开始
|
int depth = 1; // 开头的 '{' 已被 peek 消费,从 1 开始
|
||||||
while (total < buf_size - 1) {
|
while (total < buf_size - 1) {
|
||||||
int ret = httpd_req_recv(req, buf + total, 1); // 逐字节读,精确找到 JSON 结尾
|
int ret = httpd_req_recv(req, buf + total, 1); // 逐字节读,精确找到 JSON 结尾
|
||||||
if (ret <= 0) return 0;
|
if (ret <= 0) return total;
|
||||||
total += ret;
|
total += ret;
|
||||||
buf[total] = '\0';
|
buf[total] = '\0';
|
||||||
if (buf[total - 1] == '{') depth++;
|
if (buf[total - 1] == '{') depth++;
|
||||||
@@ -511,7 +569,7 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
|
|||||||
if (depth == 0) return total; // JSON 头读完
|
if (depth == 0) return total; // JSON 头读完
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0; // JSON 头太长,超出缓冲区
|
return total; // fallback: return consumed bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
@@ -538,6 +596,9 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
bool is_esp32_ota = false;
|
bool is_esp32_ota = false;
|
||||||
uint32_t total_size = 0;
|
uint32_t total_size = 0;
|
||||||
int json_header_len = 0;
|
int json_header_len = 0;
|
||||||
|
uint8_t pre_read_buf[256] = {0};
|
||||||
|
int pre_read_len = 1;
|
||||||
|
pre_read_buf[0] = (uint8_t)peek;
|
||||||
|
|
||||||
if (peek == '{') {
|
if (peek == '{') {
|
||||||
// JSON 模式:逐字节读完 JSON 头
|
// JSON 模式:逐字节读完 JSON 头
|
||||||
@@ -547,6 +608,9 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
if (json_header_len > 0) {
|
if (json_header_len > 0) {
|
||||||
json_header_len++; // 加上已读的 peek 字节
|
json_header_len++; // 加上已读的 peek 字节
|
||||||
json_buf[json_header_len] = '\0';
|
json_buf[json_header_len] = '\0';
|
||||||
|
pre_read_len = json_header_len;
|
||||||
|
if (pre_read_len > (int)sizeof(pre_read_buf)) pre_read_len = (int)sizeof(pre_read_buf);
|
||||||
|
memcpy(pre_read_buf, json_buf, pre_read_len);
|
||||||
|
|
||||||
cJSON *root = cJSON_Parse(json_buf);
|
cJSON *root = cJSON_Parse(json_buf);
|
||||||
if (root) {
|
if (root) {
|
||||||
@@ -623,9 +687,12 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- GD32 OTA(原逻辑)---
|
// --- GD32 OTA(原逻辑)---
|
||||||
if (req->content_len > OTA_MAX_FIRMWARE_SIZE) {
|
const uint32_t gd32_fw_size = (uint32_t)req->content_len;
|
||||||
DEBUG_ERR("WIFI", "固件太大: %d > %d", req->content_len, OTA_MAX_FIRMWARE_SIZE);
|
const uint32_t gd32_rounded_size = (uint32_t)(((gd32_fw_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大");
|
if (gd32_fw_size == 0U || gd32_fw_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
||||||
|
DEBUG_ERR("WIFI", "固件大小非法: raw=%u rounded48=%u max=%u",
|
||||||
|
(unsigned)gd32_fw_size, (unsigned)gd32_rounded_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大或48B对齐后越界");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,10 +710,22 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "找不到 gd32_ota 分区");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "找不到 gd32_ota 分区");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
esp_err_t erase_err = esp_partition_erase_range(gd32_partition, 0, gd32_partition->size);
|
const uint32_t gd32_erase_size = align_up_u32(gd32_fw_size, GD32_OTA_FLASH_SECTOR_SIZE);
|
||||||
|
if (gd32_erase_size > gd32_partition->size) {
|
||||||
|
DEBUG_ERR("WIFI", "gd32_ota 分区空间不足: erase=%u partition=%u",
|
||||||
|
(unsigned)gd32_erase_size, (unsigned)gd32_partition->size);
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "gd32_ota partition too small");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_INFO("WIFI", "擦除 gd32_ota 分区: %u bytes (fw=%u, partition=%u)",
|
||||||
|
(unsigned)gd32_erase_size, (unsigned)gd32_fw_size, (unsigned)gd32_partition->size);
|
||||||
|
esp_err_t erase_err = esp_partition_erase_range(gd32_partition, 0, gd32_erase_size);
|
||||||
if (erase_err != ESP_OK) {
|
if (erase_err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "擦除 gd32_ota 分区失败: %s", esp_err_to_name(erase_err));
|
DEBUG_ERR("WIFI", "擦除 gd32_ota 分区失败: %s", esp_err_to_name(erase_err));
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "擦除分区失败");
|
char err_msg[96];
|
||||||
|
snprintf(err_msg, sizeof(err_msg), "erase gd32_ota failed: %s", esp_err_to_name(erase_err));
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg);
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
meta_clear();
|
meta_clear();
|
||||||
@@ -659,77 +738,73 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
|
|
||||||
int received = 0;
|
int received = 0;
|
||||||
|
|
||||||
// peek 字节是 GD32 数据的第一字节,先放回缓冲区
|
// GD32 路径与网页上传协议对齐:网页上传的是 raw binary(不带 6B 头)。
|
||||||
uint8_t gd32_header[6];
|
uint32_t expected_fw_size_raw = gd32_fw_size;
|
||||||
gd32_header[0] = (uint8_t)peek;
|
ota_meta_t meta_raw = {
|
||||||
int header_read = 1;
|
.total_size = expected_fw_size_raw,
|
||||||
// 读剩余 5 字节凑齐 6 字节 GD32 头
|
.stored_size = 0,
|
||||||
if (req->content_len >= 6) {
|
.last_seq = 0,
|
||||||
int need = 6 - header_read;
|
.chunk_count = (expected_fw_size_raw + OTA_BLOCK_SIZE - 1U) / OTA_BLOCK_SIZE,
|
||||||
int got = httpd_req_recv(req, (char *)gd32_header + header_read, need);
|
.checksum = {0}
|
||||||
if (got > 0) header_read += got;
|
};
|
||||||
}
|
meta_save(&meta_raw);
|
||||||
|
DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw);
|
||||||
|
|
||||||
uint32_t expected_fw_size = 0;
|
int first_chunk_len = pre_read_len;
|
||||||
if (header_read >= 6) {
|
if (first_chunk_len > req->content_len) first_chunk_len = req->content_len;
|
||||||
uint16_t seq = gd32_header[0] | (gd32_header[1] << 8);
|
if (first_chunk_len > 0) {
|
||||||
uint32_t fw_size = gd32_header[2] | (gd32_header[3] << 8) | (gd32_header[4] << 16) | (gd32_header[5] << 24);
|
esp_err_t err = flash_write_chunk(0, pre_read_buf, first_chunk_len);
|
||||||
expected_fw_size = fw_size;
|
if (err != ESP_OK) {
|
||||||
if (fw_size > OTA_MAX_FIRMWARE_SIZE) {
|
DEBUG_ERR("WIFI", "GD32 first chunk write failed: len=%d err=%s",
|
||||||
DEBUG_ERR("WIFI", "GD32 固件头声明大小超限: %u > %u", fw_size, OTA_MAX_FIRMWARE_SIZE);
|
first_chunk_len, esp_err_to_name(err));
|
||||||
free(buf);
|
free(buf);
|
||||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "GD32 固件大小超限");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
if (seq == 0) {
|
received = first_chunk_len;
|
||||||
ota_meta_t meta = { .total_size = fw_size, .stored_size = 0, .last_seq = 0,
|
meta_update((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE));
|
||||||
.chunk_count = (fw_size + OTA_BLOCK_SIZE - 1) / OTA_BLOCK_SIZE, .checksum = {0} };
|
|
||||||
meta_save(&meta);
|
|
||||||
DEBUG_INFO("WIFI", "开始 GD32 固件存储: %u 字节", fw_size);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首包已经读走 6 字节头,因此后续 body 全部按 header_read 为偏移基准顺序落盘
|
while (received < req->content_len) {
|
||||||
while (received + header_read < req->content_len) {
|
size_t to_read = req->content_len - received;
|
||||||
size_t to_read = req->content_len - received - header_read;
|
|
||||||
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
|
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
|
||||||
|
|
||||||
int ret = httpd_req_recv(req, (char *)buf, to_read);
|
int ret = httpd_req_recv(req, (char *)buf, to_read);
|
||||||
if (ret <= 0) break;
|
if (ret <= 0) {
|
||||||
|
DEBUG_ERR("WIFI", "GD32 upload interrupted: received=%d expected=%d",
|
||||||
|
received, req->content_len);
|
||||||
|
free(buf);
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "upload interrupted");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t flash_offset = (uint32_t)received;
|
uint32_t flash_offset = (uint32_t)received;
|
||||||
esp_err_t err = flash_write_chunk(flash_offset, buf, ret);
|
esp_err_t err = flash_write_chunk(flash_offset, buf, ret);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "Flash 写入失败: offset=%u len=%d err=%s",
|
DEBUG_ERR("WIFI", "GD32 flash write failed: offset=%u len=%d err=%s",
|
||||||
flash_offset, ret, esp_err_to_name(err));
|
flash_offset, ret, esp_err_to_name(err));
|
||||||
free(buf);
|
free(buf);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "写入 gd32_ota 失败");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
received += ret;
|
received += ret;
|
||||||
if (expected_fw_size > 0) {
|
|
||||||
meta_update((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE));
|
meta_update((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE));
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t progress = expected_fw_size > 0
|
|
||||||
? ((uint32_t)received * 100U) / expected_fw_size
|
|
||||||
: ((uint32_t)(received + header_read) * 100U) / (uint32_t)req->content_len;
|
|
||||||
if (progress % 10 == 0) {
|
|
||||||
DEBUG_INFO("WIFI", "GD32 上传进度: %d / %u (%u%%)",
|
|
||||||
received, expected_fw_size, progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(buf);
|
free(buf);
|
||||||
DEBUG_INFO("WIFI", "GD32 固件存储完成: payload=%d 字节, 头=%d 字节", received, header_read);
|
DEBUG_INFO("WIFI", "GD32 raw upload done: %d / %u bytes", received, expected_fw_size_raw);
|
||||||
|
|
||||||
ota_meta_t meta;
|
ota_meta_t final_meta;
|
||||||
meta_load(&meta);
|
meta_load(&final_meta);
|
||||||
if (meta.total_size > 0 && meta.stored_size >= meta.total_size) {
|
if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) {
|
||||||
/** @brief GD32 固件存储完成标志。 */
|
|
||||||
s_gd32_storage_done = true;
|
s_gd32_storage_done = true;
|
||||||
DEBUG_INFO("WIFI", "GD32 固件已存储完成,等待触发转发...");
|
DEBUG_INFO("WIFI", "GD32 firmware fully stored, auto-triggering relay");
|
||||||
|
// 自动触发中继
|
||||||
|
if (g_ota_dispatch_queue) {
|
||||||
|
uint32_t cmd = 1; // 1 = start relay
|
||||||
|
xQueueSend(g_ota_dispatch_queue, &cmd, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}";
|
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}";
|
||||||
@@ -745,11 +820,36 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
*/
|
*/
|
||||||
static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||||
{
|
{
|
||||||
(void)req;
|
|
||||||
http_set_cors_headers(req);
|
http_set_cors_headers(req);
|
||||||
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ===");
|
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ===");
|
||||||
DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
|
DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
|
||||||
|
|
||||||
|
ota_meta_t meta;
|
||||||
|
memset(&meta, 0, sizeof(meta));
|
||||||
|
if (meta_load(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "firmware not fully stored");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_relay_triggered && g_relay_state != RELAY_IDLE &&
|
||||||
|
g_relay_state != RELAY_DONE && g_relay_state != RELAY_ERROR) {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay already triggered");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
/* 上一轮中继已结束/失败/空闲,重置标志允许重新触发 */
|
||||||
|
s_relay_triggered = false;
|
||||||
|
s_gd32_storage_done = false;
|
||||||
|
|
||||||
|
if (!g_ota_dispatch_queue) {
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "relay queue not ready");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
wifi_ota_signal_relay();
|
wifi_ota_signal_relay();
|
||||||
|
if (!s_relay_triggered) {
|
||||||
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "relay trigger failed");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA relay triggered\"}";
|
const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA relay triggered\"}";
|
||||||
httpd_resp_set_type(req, "application/json");
|
httpd_resp_set_type(req, "application/json");
|
||||||
@@ -774,15 +874,17 @@ static esp_err_t ota_status_handler(httpd_req_t *req)
|
|||||||
char resp[256];
|
char resp[256];
|
||||||
int len = snprintf(resp, sizeof(resp),
|
int len = snprintf(resp, sizeof(resp),
|
||||||
"{\"total_size\": %lu, \"stored_size\": %lu, \"last_seq\": %lu, "
|
"{\"total_size\": %lu, \"stored_size\": %lu, \"last_seq\": %lu, "
|
||||||
"\"storage_done\": %s, \"relay_state\": %d}",
|
"\"storage_done\": %s, \"relay_state\": %d, \"gd32_app_alive\": %s}",
|
||||||
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
|
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
|
||||||
(unsigned long)meta.last_seq,
|
(unsigned long)meta.last_seq,
|
||||||
s_gd32_storage_done ? "true" : "false",
|
s_gd32_storage_done ? "true" : "false",
|
||||||
g_relay_state);
|
g_relay_state,
|
||||||
|
g_gd32_app_alive ? "true" : "false");
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s",
|
DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s, app_alive=%s",
|
||||||
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
|
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
|
||||||
s_gd32_storage_done ? "true" : "false");
|
s_gd32_storage_done ? "true" : "false",
|
||||||
|
g_gd32_app_alive ? "true" : "false");
|
||||||
|
|
||||||
httpd_resp_set_type(req, "application/json");
|
httpd_resp_set_type(req, "application/json");
|
||||||
httpd_resp_send(req, resp, len);
|
httpd_resp_send(req, resp, len);
|
||||||
@@ -923,6 +1025,98 @@ static esp_err_t can_send_handler(httpd_req_t *req)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP POST /spi/send 处理器
|
||||||
|
// 网页传 type + payload,ESP32 自动构建 SOF+TYPE+LEN(L/H)+PAYLOAD+CRC 帧
|
||||||
|
// 并通过 spi_ota_transfer 完成完整事务(发送+轮询响应帧+解析)。
|
||||||
|
//
|
||||||
|
// 请求体: {"type": "01", "data": "01020304", "timeout_ms": 1000}
|
||||||
|
// type: 帧类型 (hex 1 字节,如 01=REQ, 02=DATA, B0=BOOT)
|
||||||
|
// data: 负载数据 (hex 字符串,可选)
|
||||||
|
// timeout_ms: 超时毫秒 (可选, 默认 1000)
|
||||||
|
/**
|
||||||
|
* @brief HTTP POST /spi/send 处理器,转发上位机请求到 SPI 并返回结果。
|
||||||
|
* @param req HTTP 请求对象。
|
||||||
|
* @return ESP_OK 成功,ESP_FAIL 表示请求处理失败。
|
||||||
|
*/
|
||||||
|
static esp_err_t spi_send_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
http_set_cors_headers(req);
|
||||||
|
char buf[512] = {0};
|
||||||
|
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||||
|
if (ret <= 0) {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
buf[ret] = '\0';
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(buf);
|
||||||
|
if (!root) {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *type_item = cJSON_GetObjectItem(root, "type");
|
||||||
|
cJSON *data_item = cJSON_GetObjectItem(root, "data");
|
||||||
|
cJSON *timeout_item = cJSON_GetObjectItem(root, "timeout_ms");
|
||||||
|
|
||||||
|
if (!type_item || !cJSON_IsString(type_item)) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 type 字段");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
const char *type_hex = cJSON_GetStringValue(type_item);
|
||||||
|
uint8_t frame_type = (uint8_t)strtol(type_hex, NULL, 16);
|
||||||
|
|
||||||
|
uint32_t timeout_ms = timeout_item ? (uint32_t)cJSON_GetNumberValue(timeout_item) : 1000;
|
||||||
|
|
||||||
|
// 解析 payload (hex 字符串 → 字节数组)
|
||||||
|
uint8_t payload_data[SPI_OTA_FRAME_MAX] = {0};
|
||||||
|
size_t payload_len = 0;
|
||||||
|
if (data_item && cJSON_IsString(data_item)) {
|
||||||
|
const char *hex_str = cJSON_GetStringValue(data_item);
|
||||||
|
size_t hex_len = strlen(hex_str);
|
||||||
|
for (size_t i = 0; i < hex_len / 2 && i < SPI_OTA_FRAME_MAX; i++) {
|
||||||
|
char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
|
||||||
|
payload_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
|
||||||
|
payload_len++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_INFO("SPI", "WEB SPI: type=0x%02X, payload=%zu 字节",
|
||||||
|
frame_type, payload_len);
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
char resp[1024];
|
||||||
|
uint8_t resp_type = 0;
|
||||||
|
uint8_t resp_payload[SPI_OTA_FRAME_MAX] = {0};
|
||||||
|
size_t resp_len = 0;
|
||||||
|
esp_err_t spi_err = spi_ota_transfer(frame_type, payload_data, payload_len,
|
||||||
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||||
|
timeout_ms);
|
||||||
|
|
||||||
|
if (spi_err == ESP_OK) {
|
||||||
|
char resp_hex[SPI_OTA_FRAME_MAX * 2 + 1] = {0};
|
||||||
|
for (size_t i = 0; i < resp_len; i++) {
|
||||||
|
sprintf(&resp_hex[i * 2], "%02X", resp_payload[i]);
|
||||||
|
}
|
||||||
|
snprintf(resp, sizeof(resp),
|
||||||
|
"{\"status\": \"ok\", \"type\": \"%02X\", \"payload_len\": %zu, \"resp_type\": \"%02X\", \"resp_len\": %zu, \"recv\": \"%s\"}",
|
||||||
|
frame_type, payload_len, resp_type, resp_len, resp_hex);
|
||||||
|
DEBUG_INFO("SPI", "WEB SPI 响应: type=0x%02X len=%zu payload=%s",
|
||||||
|
resp_type, resp_len, resp_hex);
|
||||||
|
} else {
|
||||||
|
snprintf(resp, sizeof(resp),
|
||||||
|
"{\"status\": \"error\", \"code\": %d, \"message\": \"%s\"}",
|
||||||
|
spi_err, esp_err_to_name(spi_err));
|
||||||
|
DEBUG_ERR("SPI", "WEB SPI 失败: %s", esp_err_to_name(spi_err));
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
httpd_resp_send(req, resp, strlen(resp));
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief HTTP GET /can/status 处理器
|
* @brief HTTP GET /can/status 处理器
|
||||||
* @param req
|
* @param req
|
||||||
@@ -1041,6 +1235,8 @@ static esp_err_t http_ota_server_start(void)
|
|||||||
config.server_port = HTTP_SERVER_PORT;
|
config.server_port = HTTP_SERVER_PORT;
|
||||||
config.stack_size = HTTP_SERVER_STACK_SIZE;
|
config.stack_size = HTTP_SERVER_STACK_SIZE;
|
||||||
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
|
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
|
||||||
|
config.recv_wait_timeout = 30; // GD32 上传前会擦除 Flash,5 秒默认值过紧
|
||||||
|
config.send_wait_timeout = 30;
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
|
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
|
||||||
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
|
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
|
||||||
@@ -1051,6 +1247,16 @@ static esp_err_t http_ota_server_start(void)
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 NVS 恢复固件存储状态(重启后保留)
|
||||||
|
{
|
||||||
|
ota_meta_t boot_meta;
|
||||||
|
if (meta_load(&boot_meta) == ESP_OK &&
|
||||||
|
boot_meta.total_size > 0 && boot_meta.stored_size >= boot_meta.total_size) {
|
||||||
|
s_gd32_storage_done = true;
|
||||||
|
DEBUG_INFO("WIFI", "从 NVS 恢复: 固件已就绪 (%u bytes)", boot_meta.total_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
httpd_uri_t upload_uri = {
|
httpd_uri_t upload_uri = {
|
||||||
.uri = "/ota/upload",
|
.uri = "/ota/upload",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_POST,
|
||||||
@@ -1233,7 +1439,23 @@ static esp_err_t http_ota_server_start(void)
|
|||||||
httpd_register_uri_handler(s_http_server, &can_status_options_uri);
|
httpd_register_uri_handler(s_http_server, &can_status_options_uri);
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "HTTP 服务器已启动");
|
DEBUG_INFO("WIFI", "HTTP 服务器已启动");
|
||||||
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status");
|
// SPI 直接发送
|
||||||
|
httpd_uri_t spi_send_uri = {
|
||||||
|
.uri = "/spi/send",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = spi_send_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(s_http_server, &spi_send_uri);
|
||||||
|
httpd_uri_t spi_send_options_uri = {
|
||||||
|
.uri = "/spi/send",
|
||||||
|
.method = HTTP_OPTIONS,
|
||||||
|
.handler = http_options_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(s_http_server, &spi_send_options_uri);
|
||||||
|
|
||||||
|
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status, /spi/send");
|
||||||
|
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -1253,18 +1475,27 @@ void wifi_ota_signal_relay(void)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s_relay_triggered) {
|
if (s_relay_triggered && g_relay_state != RELAY_IDLE &&
|
||||||
DEBUG_WARN("WIFI", "转发已经触发过");
|
g_relay_state != RELAY_DONE && g_relay_state != RELAY_ERROR) {
|
||||||
|
DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", g_relay_state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* 上一轮中继已结束/失败/空闲,重置标志 */
|
||||||
|
s_relay_triggered = false;
|
||||||
|
s_gd32_storage_done = false;
|
||||||
|
|
||||||
/** @brief OTA 中继是否已触发标志。 */
|
/** @brief OTA 中继是否已触发标志。 */
|
||||||
s_relay_triggered = true;
|
s_relay_triggered = true;
|
||||||
|
|
||||||
if (g_ota_dispatch_queue) {
|
if (g_ota_dispatch_queue) {
|
||||||
uint32_t msg = 1;
|
uint32_t msg = 1;
|
||||||
xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100));
|
BaseType_t ret = xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100));
|
||||||
|
if (ret == pdTRUE) {
|
||||||
DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
|
DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
|
||||||
|
} else {
|
||||||
|
DEBUG_ERR("WIFI", "OTA 调度信号发送失败");
|
||||||
|
s_relay_triggered = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
DEBUG_ERR("WIFI", "OTA 中继队列尚未就绪");
|
DEBUG_ERR("WIFI", "OTA 中继队列尚未就绪");
|
||||||
s_relay_triggered = false;
|
s_relay_triggered = false;
|
||||||
@@ -1319,19 +1550,64 @@ bool wifi_early_init(void)
|
|||||||
{
|
{
|
||||||
DEBUG_INFO("WIFI", "=== WiFi 早期初始化 (调度器启动前) ===");
|
DEBUG_INFO("WIFI", "=== WiFi 早期初始化 (调度器启动前) ===");
|
||||||
|
|
||||||
|
// ★ 冷启动检测:判断是否为上电复位
|
||||||
|
esp_reset_reason_t reset_reason = esp_reset_reason();
|
||||||
|
bool is_cold_boot = (reset_reason == ESP_RST_POWERON || reset_reason == ESP_RST_BROWNOUT);
|
||||||
|
s_cold_boot = is_cold_boot; // 保存到模块级标志,供事件回调使用
|
||||||
|
DEBUG_INFO("WIFI", "复位原因: %d (%s), 冷启动: %s",
|
||||||
|
reset_reason,
|
||||||
|
(reset_reason == ESP_RST_POWERON) ? "POWER_ON" :
|
||||||
|
(reset_reason == ESP_RST_BROWNOUT) ? "BROWNOUT" :
|
||||||
|
(reset_reason == ESP_RST_SW) ? "SOFTWARE" :
|
||||||
|
(reset_reason == ESP_RST_PANIC) ? "PANIC" :
|
||||||
|
(reset_reason == ESP_RST_INT_WDT) ? "INT_WDT" :
|
||||||
|
(reset_reason == ESP_RST_TASK_WDT) ? "TASK_WDT" :
|
||||||
|
(reset_reason == ESP_RST_DEEPSLEEP) ? "DEEP_SLEEP" :
|
||||||
|
"OTHER",
|
||||||
|
is_cold_boot ? "是" : "否");
|
||||||
|
|
||||||
esp_err_t err = nvs_flash_init();
|
esp_err_t err = nvs_flash_init();
|
||||||
|
bool nvs_erased = false;
|
||||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||||
// NVS 分区损坏或版本不匹配,擦除后重试
|
// NVS 分区损坏或版本不匹配,擦除后重试
|
||||||
DEBUG_WARN("WIFI", "NVS 分区异常 (%s),擦除后重试", esp_err_to_name(err));
|
DEBUG_WARN("WIFI", "NVS 分区异常 (%s),擦除后重试", esp_err_to_name(err));
|
||||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||||
|
nvs_erased = true;
|
||||||
err = nvs_flash_init();
|
err = nvs_flash_init();
|
||||||
}
|
}
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
|
DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
|
||||||
} else {
|
} else {
|
||||||
|
if (nvs_erased) {
|
||||||
|
DEBUG_WARN("WIFI", "NVS 已擦除重建,WiFi PHY 校准数据已清除,将重新校准");
|
||||||
|
}
|
||||||
DEBUG_INFO("WIFI", "NVS 初始化成功");
|
DEBUG_INFO("WIFI", "NVS 初始化成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 冷启动时强制清除 WiFi NVS 缓存,避免残留脏数据
|
||||||
|
// 长时间断电后 NVS 中的 WiFi 配置缓存可能不一致
|
||||||
|
if (is_cold_boot) {
|
||||||
|
nvs_handle_t nvs_handle;
|
||||||
|
err = nvs_open("nvs.net80211", NVS_READWRITE, &nvs_handle);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
// 清除 WiFi 驱动内部 NVS 缓存键
|
||||||
|
nvs_erase_all(nvs_handle);
|
||||||
|
nvs_commit(nvs_handle);
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
DEBUG_INFO("WIFI", "冷启动: 已清除 WiFi NVS 缓存 (nvs.net80211)");
|
||||||
|
} else {
|
||||||
|
DEBUG_INFO("WIFI", "冷启动: 无 WiFi NVS 缓存分区,跳过清除");
|
||||||
|
}
|
||||||
|
// 同样清除 PHY 校准数据分区,强制下次全量校准
|
||||||
|
err = nvs_open("phy", NVS_READWRITE, &nvs_handle);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
nvs_erase_all(nvs_handle);
|
||||||
|
nvs_commit(nvs_handle);
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
DEBUG_INFO("WIFI", "冷启动: 已清除 PHY 校准缓存,将重新校准");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @brief STA 连接成功信号量。 */
|
/** @brief STA 连接成功信号量。 */
|
||||||
s_sta_connected_sem = xSemaphoreCreateBinary();
|
s_sta_connected_sem = xSemaphoreCreateBinary();
|
||||||
/** @brief STA 获取 IP 信号量。 */
|
/** @brief STA 获取 IP 信号量。 */
|
||||||
@@ -1401,7 +1677,29 @@ void wifi_ota_task(void *pvParameters)
|
|||||||
DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str);
|
DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str);
|
||||||
} else {
|
} else {
|
||||||
DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
|
DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
|
||||||
esp_wifi_stop(); // 先停 STA,再切 AP
|
// ★ 完整的 STA→AP 切换流程:
|
||||||
|
// 1. 停止 WiFi
|
||||||
|
// 2. 反初始化 WiFi 驱动(释放内部资源,包括 NVS 缓存)
|
||||||
|
// 3. 重新初始化 WiFi 驱动
|
||||||
|
// 4. 重新注册事件回调
|
||||||
|
// 5. 以 AP 模式启动
|
||||||
|
esp_wifi_stop();
|
||||||
|
esp_err_t deinit_err = esp_wifi_deinit();
|
||||||
|
if (deinit_err != ESP_OK) {
|
||||||
|
DEBUG_WARN("WIFI", "WiFi deinit 失败: %s,仅 stop 后切 AP", esp_err_to_name(deinit_err));
|
||||||
|
} else {
|
||||||
|
DEBUG_INFO("WIFI", "WiFi 驱动已反初始化,重新初始化...");
|
||||||
|
wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||||
|
deinit_err = esp_wifi_init(&wifi_cfg);
|
||||||
|
if (deinit_err != ESP_OK) {
|
||||||
|
DEBUG_ERR("WIFI", "WiFi 驱动重新初始化失败: %s", esp_err_to_name(deinit_err));
|
||||||
|
} else {
|
||||||
|
// ★ deinit 后事件回调会丢失,必须重新注册
|
||||||
|
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL);
|
||||||
|
esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL);
|
||||||
|
DEBUG_INFO("WIFI", "WiFi/IP 事件回调已重新注册");
|
||||||
|
}
|
||||||
|
}
|
||||||
/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
|
/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
|
||||||
s_wifi_mode = OTA_WIFI_MODE_AP;
|
s_wifi_mode = OTA_WIFI_MODE_AP;
|
||||||
wifi_init_ap();
|
wifi_init_ap();
|
||||||
|
|||||||
@@ -88,7 +88,15 @@ int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_fr
|
|||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 SPI 帧
|
/**
|
||||||
|
* @brief 解析并校验一帧 SPI 协议数据。
|
||||||
|
* @param frame 输入帧缓冲区。
|
||||||
|
* @param frame_len 输入帧长度。
|
||||||
|
* @param out_cmd 输出命令字节,可为 NULL。
|
||||||
|
* @param out_payload 输出负载缓冲区,可为 NULL。
|
||||||
|
* @param out_len 输出负载长度指针,可为 NULL。
|
||||||
|
* @return 0 成功,-1 失败。
|
||||||
|
*/
|
||||||
int frame_parse(const uint8_t *frame, size_t frame_len,
|
int frame_parse(const uint8_t *frame, size_t frame_len,
|
||||||
uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len)
|
uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len)
|
||||||
{
|
{
|
||||||
@@ -141,7 +149,15 @@ int frame_parse(const uint8_t *frame, size_t frame_len,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AES-128 CBC 加密(使用 ESP32-C3 硬件 AES)
|
/**
|
||||||
|
* @brief 使用 AES-128 CBC 对明文进行加密(PKCS#7 填充)。
|
||||||
|
* @param plaintext 明文数据指针。
|
||||||
|
* @param len 明文长度。
|
||||||
|
* @param key 16 字节密钥。
|
||||||
|
* @param iv 16 字节初始向量。
|
||||||
|
* @param ciphertext_out 输出密文缓冲区。
|
||||||
|
* @return >=0 为密文长度,-1 表示失败。
|
||||||
|
*/
|
||||||
int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
|
int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
|
||||||
const uint8_t *key, const uint8_t *iv,
|
const uint8_t *key, const uint8_t *iv,
|
||||||
uint8_t *ciphertext_out)
|
uint8_t *ciphertext_out)
|
||||||
@@ -189,7 +205,15 @@ int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AES-128 CBC 解密(使用 ESP32-C3 硬件 AES)
|
/**
|
||||||
|
* @brief 使用 AES-128 CBC 解密密文并移除 PKCS#7 填充。
|
||||||
|
* @param ciphertext 密文数据指针。
|
||||||
|
* @param len 密文长度(必须是 16 的倍数)。
|
||||||
|
* @param key 16 字节密钥。
|
||||||
|
* @param iv 16 字节初始向量。
|
||||||
|
* @param plaintext_out 输出明文缓冲区。
|
||||||
|
* @return >=0 为明文长度,-1 表示失败。
|
||||||
|
*/
|
||||||
int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len,
|
int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len,
|
||||||
const uint8_t *key, const uint8_t *iv,
|
const uint8_t *key, const uint8_t *iv,
|
||||||
uint8_t *plaintext_out)
|
uint8_t *plaintext_out)
|
||||||
@@ -242,7 +266,13 @@ int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len,
|
|||||||
return orig_len;
|
return orig_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搅伴算法 - 生成随机数
|
/**
|
||||||
|
* @brief 根据时间戳、地址和随机种子生成 16 字节 nonce。
|
||||||
|
* @param timestamp 时间戳输入。
|
||||||
|
* @param mem_addr 地址输入。
|
||||||
|
* @param rand_seed 随机种子输入。
|
||||||
|
* @param nonce_out 输出 nonce 缓冲区(16 字节)。
|
||||||
|
*/
|
||||||
void scramble_generate(uint32_t timestamp, uint32_t mem_addr,
|
void scramble_generate(uint32_t timestamp, uint32_t mem_addr,
|
||||||
uint32_t rand_seed, uint8_t *nonce_out)
|
uint32_t rand_seed, uint8_t *nonce_out)
|
||||||
{
|
{
|
||||||
|
|||||||
+405
-52
@@ -15,6 +15,106 @@
|
|||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include "monitor/debug_log.h"
|
#include "monitor/debug_log.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr size_t kSpiTransferMaxBytes = 256U;
|
||||||
|
|
||||||
|
static esp_err_t spi_transfer_clocked(const uint8_t *tx_buf, size_t tx_len,
|
||||||
|
uint8_t *rx_buf, size_t rx_len)
|
||||||
|
{
|
||||||
|
if (g_spi_device == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((tx_len > 0U && tx_buf == NULL) || (rx_len > 0U && rx_buf == NULL)) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t total_len = (tx_len > rx_len) ? tx_len : rx_len;
|
||||||
|
if (total_len == 0U) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (total_len > kSpiTransferMaxBytes) {
|
||||||
|
return ESP_ERR_INVALID_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t tx_staging[kSpiTransferMaxBytes];
|
||||||
|
uint8_t rx_staging[kSpiTransferMaxBytes];
|
||||||
|
const uint8_t *tx_ptr = tx_buf;
|
||||||
|
uint8_t *rx_ptr = rx_buf;
|
||||||
|
|
||||||
|
if (tx_len < total_len) {
|
||||||
|
memset(tx_staging, 0xFF, total_len);
|
||||||
|
if (tx_buf != NULL && tx_len > 0U) {
|
||||||
|
memcpy(tx_staging, tx_buf, tx_len);
|
||||||
|
}
|
||||||
|
tx_ptr = tx_staging;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rx_len > 0U && rx_len < total_len) {
|
||||||
|
rx_ptr = rx_staging;
|
||||||
|
}
|
||||||
|
|
||||||
|
spi_transaction_t t = {};
|
||||||
|
t.length = total_len * 8U;
|
||||||
|
t.rxlength = (rx_len > 0U) ? (total_len * 8U) : 0U;
|
||||||
|
t.tx_buffer = tx_ptr;
|
||||||
|
t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL;
|
||||||
|
|
||||||
|
esp_err_t ret = spi_device_transmit(g_spi_device, &t);
|
||||||
|
if (ret == ESP_OK && rx_len > 0U && rx_ptr != rx_buf) {
|
||||||
|
memcpy(rx_buf, rx_ptr, rx_len);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t spi_transfer_request_response(uint8_t cmd,
|
||||||
|
const uint8_t *payload, size_t payload_len,
|
||||||
|
uint8_t *rx_buf, size_t rx_len)
|
||||||
|
{
|
||||||
|
if ((payload_len > 0U && payload == NULL) || (rx_len > 0U && rx_buf == NULL)) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t cmd_len = (cmd != 0U) ? 1U : 0U;
|
||||||
|
const size_t total_len = cmd_len + payload_len + rx_len;
|
||||||
|
if (total_len == 0U) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
if (total_len > kSpiTransferMaxBytes) {
|
||||||
|
return ESP_ERR_INVALID_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t tx_staging[kSpiTransferMaxBytes];
|
||||||
|
uint8_t rx_staging[kSpiTransferMaxBytes];
|
||||||
|
size_t tx_pos = 0U;
|
||||||
|
|
||||||
|
if (cmd_len > 0U) {
|
||||||
|
tx_staging[tx_pos++] = cmd;
|
||||||
|
}
|
||||||
|
if (payload_len > 0U) {
|
||||||
|
memcpy(&tx_staging[tx_pos], payload, payload_len);
|
||||||
|
tx_pos += payload_len;
|
||||||
|
}
|
||||||
|
if (tx_pos < total_len) {
|
||||||
|
memset(&tx_staging[tx_pos], 0xFF, total_len - tx_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ret = spi_transfer_clocked(tx_staging, total_len,
|
||||||
|
(rx_len > 0U) ? rx_staging : NULL,
|
||||||
|
(rx_len > 0U) ? total_len : 0U);
|
||||||
|
if (ret == ESP_OK && rx_len > 0U) {
|
||||||
|
memcpy(rx_buf, &rx_staging[cmd_len + payload_len], rx_len);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算 SPI OTA 帧头和负载的异或 CRC8。
|
||||||
|
* @param data 输入数据指针。
|
||||||
|
* @param len 输入长度。
|
||||||
|
* @return 计算得到的 CRC8 值。
|
||||||
|
*/
|
||||||
static uint8_t spi_ota_crc8(const uint8_t *data, size_t len)
|
static uint8_t spi_ota_crc8(const uint8_t *data, size_t len)
|
||||||
{
|
{
|
||||||
uint8_t crc = 0x00;
|
uint8_t crc = 0x00;
|
||||||
@@ -24,6 +124,15 @@ static uint8_t spi_ota_crc8(const uint8_t *data, size_t len)
|
|||||||
return crc;
|
return crc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 构建 SPI OTA 协议帧(SOF + TYPE + LEN + PAYLOAD + CRC)。
|
||||||
|
* @param type 帧类型。
|
||||||
|
* @param payload 负载数据指针,可为 NULL。
|
||||||
|
* @param payload_len 负载长度。
|
||||||
|
* @param out 输出帧缓冲区。
|
||||||
|
* @param out_len 输出帧长度。
|
||||||
|
* @return 构建成功返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
static bool spi_ota_build_frame(uint8_t type, const uint8_t *payload, size_t payload_len,
|
static bool spi_ota_build_frame(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||||
uint8_t *out, size_t *out_len)
|
uint8_t *out, size_t *out_len)
|
||||||
{
|
{
|
||||||
@@ -42,6 +151,15 @@ static bool spi_ota_build_frame(uint8_t type, const uint8_t *payload, size_t pay
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析并校验一帧 SPI OTA 数据。
|
||||||
|
* @param frame 输入帧指针。
|
||||||
|
* @param frame_len 输入帧缓冲区长度。
|
||||||
|
* @param type 输出帧类型,可为 NULL。
|
||||||
|
* @param payload 输出负载指针,可为 NULL。
|
||||||
|
* @param payload_len 输出负载长度,可为 NULL。
|
||||||
|
* @return 解析并校验成功返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
static bool spi_ota_parse_frame(const uint8_t *frame, size_t frame_len,
|
static bool spi_ota_parse_frame(const uint8_t *frame, size_t frame_len,
|
||||||
uint8_t *type, const uint8_t **payload, size_t *payload_len)
|
uint8_t *type, const uint8_t **payload, size_t *payload_len)
|
||||||
{
|
{
|
||||||
@@ -57,6 +175,16 @@ static bool spi_ota_parse_frame(const uint8_t *frame, size_t frame_len,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在接收缓冲区中扫描并定位第一帧有效 SPI OTA 数据。
|
||||||
|
* @param buf 输入缓冲区。
|
||||||
|
* @param buf_len 缓冲区长度。
|
||||||
|
* @param type 输出帧类型,可为 NULL。
|
||||||
|
* @param payload 输出负载指针,可为 NULL。
|
||||||
|
* @param payload_len 输出负载长度,可为 NULL。
|
||||||
|
* @param frame_offset 输出帧起始偏移,可为 NULL。
|
||||||
|
* @return 找到有效帧返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
static bool spi_ota_find_frame(const uint8_t *buf, size_t buf_len,
|
static bool spi_ota_find_frame(const uint8_t *buf, size_t buf_len,
|
||||||
uint8_t *type, const uint8_t **payload, size_t *payload_len,
|
uint8_t *type, const uint8_t **payload, size_t *payload_len,
|
||||||
size_t *frame_offset)
|
size_t *frame_offset)
|
||||||
@@ -64,6 +192,15 @@ static bool spi_ota_find_frame(const uint8_t *buf, size_t buf_len,
|
|||||||
if (!buf || buf_len < 5) return false;
|
if (!buf || buf_len < 5) return false;
|
||||||
for (size_t i = 0; i + 5 <= buf_len; ++i) {
|
for (size_t i = 0; i + 5 <= buf_len; ++i) {
|
||||||
if (buf[i] != SPI_OTA_SOF) continue;
|
if (buf[i] != SPI_OTA_SOF) continue;
|
||||||
|
size_t len_field = (size_t)buf[i+2] | ((size_t)buf[i+3] << 8);
|
||||||
|
size_t needed = 5 + len_field;
|
||||||
|
if (i + needed > buf_len) continue;
|
||||||
|
uint8_t crc = spi_ota_crc8(&buf[i], 4 + len_field);
|
||||||
|
if (crc != buf[i + 4 + len_field]) {
|
||||||
|
DEBUG_DBG("SPI", "帧@%u CRC错误: calc=0x%02X recv=0x%02X type=0x%02X len=%u",
|
||||||
|
(unsigned)i, crc, buf[i + 4 + len_field], buf[i+1], (unsigned)len_field);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (spi_ota_parse_frame(&buf[i], buf_len - i, type, payload, payload_len)) {
|
if (spi_ota_parse_frame(&buf[i], buf_len - i, type, payload, payload_len)) {
|
||||||
if (frame_offset) *frame_offset = i;
|
if (frame_offset) *frame_offset = i;
|
||||||
return true;
|
return true;
|
||||||
@@ -84,14 +221,14 @@ esp_err_t spi_master_init(void)
|
|||||||
DEBUG_INFO("SPI", "=== 初始化 SPI2 (VSPI) ===");
|
DEBUG_INFO("SPI", "=== 初始化 SPI2 (VSPI) ===");
|
||||||
|
|
||||||
spi_bus_config_t bus_cfg = {};
|
spi_bus_config_t bus_cfg = {};
|
||||||
bus_cfg.mosi_io_num = 8;
|
bus_cfg.mosi_io_num = 5;
|
||||||
bus_cfg.miso_io_num = 7;
|
bus_cfg.miso_io_num = 7;
|
||||||
bus_cfg.sclk_io_num = 6;
|
bus_cfg.sclk_io_num = 6;
|
||||||
bus_cfg.quadwp_io_num = -1;
|
bus_cfg.quadwp_io_num = -1;
|
||||||
bus_cfg.quadhd_io_num = -1;
|
bus_cfg.quadhd_io_num = -1;
|
||||||
bus_cfg.max_transfer_sz = 256;
|
bus_cfg.max_transfer_sz = 256;
|
||||||
|
|
||||||
DEBUG_INFO("SPI", "SPI2 引脚: MOSI=GPIO8, MISO=GPIO7, SCLK=GPIO6");
|
DEBUG_INFO("SPI", "SPI2 引脚: MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6");
|
||||||
DEBUG_INFO("SPI", "SPI2 配置: CLK=%d MHz, CS=GPIO%d, 最大传输=%d 字节",
|
DEBUG_INFO("SPI", "SPI2 配置: CLK=%d MHz, CS=GPIO%d, 最大传输=%d 字节",
|
||||||
SPI_CLOCK_HZ / 1000000, SPI_MASTER_CS_GPIO, bus_cfg.max_transfer_sz);
|
SPI_CLOCK_HZ / 1000000, SPI_MASTER_CS_GPIO, bus_cfg.max_transfer_sz);
|
||||||
|
|
||||||
@@ -109,8 +246,36 @@ esp_err_t spi_master_init(void)
|
|||||||
dev_cfg.command_bits = 0;
|
dev_cfg.command_bits = 0;
|
||||||
dev_cfg.address_bits = 0;
|
dev_cfg.address_bits = 0;
|
||||||
dev_cfg.dummy_bits = 0;
|
dev_cfg.dummy_bits = 0;
|
||||||
dev_cfg.cs_ena_posttrans = 0;
|
/* CS↑ 后保持 CS 高的时间(SPI 时钟周期数)。
|
||||||
dev_cfg.cs_ena_pretrans = 0;
|
*
|
||||||
|
* 0 = 最小 CS 高时间(仅满足 SPI 时序)。
|
||||||
|
* 设大一点让 GD32 EXTI ISR 有足够时间完成 onCsRelease() 处理:
|
||||||
|
* 切 internal NSS HIGH + 清 RBNE FIFO + 帧解析 + 准备响应帧。
|
||||||
|
*
|
||||||
|
* 10 × 250ns = 2.5µs,加上 SPI 驱动本身的 CS 间隮(~1µs),
|
||||||
|
* GD32 实际有 ~3.5µs 的 CS 高窗口。足够完成关键操作。
|
||||||
|
*/
|
||||||
|
dev_cfg.cs_ena_posttrans = 10;
|
||||||
|
/* CS↓ 后等待 20 个 SPI 时钟周期再开始发 SCK。
|
||||||
|
*
|
||||||
|
* 4MHz SPI → 20×250ns = 5µs 延时。
|
||||||
|
*
|
||||||
|
* GD32 SPI 从机在 CS↓ 时需要 EXTI ISR 执行 onCsAssert():
|
||||||
|
* 设置 s_csActive=true → 清空接收缓冲 → 切 internal NSS LOW → 配置 DMA。
|
||||||
|
*
|
||||||
|
* 但 SPI0 RBNE 中断优先级 (1,0) 高于 EXTI (1,1),
|
||||||
|
* 首字节 RBNE 会打断正在执行的 EXTI ISR →
|
||||||
|
* s_csActive 还没设为 true → s_rxStreamLen 还没清零 →
|
||||||
|
* 首字节存到旧位置 → EXTI 返回后清零 → 首字节丢失。
|
||||||
|
*
|
||||||
|
* cs_ena_pretrans=20 让 ESP32 在 CS↓ 后多等 5µs,
|
||||||
|
* GD32 EXTI ISR 只需 ~1µs 就能完成关键初始化,
|
||||||
|
* 5µs 足够让 onCsAssert() 执行完 s_csActive=true + s_rxStreamLen=0U。
|
||||||
|
*
|
||||||
|
* 代价:每次 SPI 事务多 5µs 延时。
|
||||||
|
* 一次 OTA 事务 = 1次请求 + ~10次轮询 = ~55µs 额外开销,可接受。
|
||||||
|
*/
|
||||||
|
dev_cfg.cs_ena_pretrans = 20;
|
||||||
|
|
||||||
ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &g_spi_device);
|
ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &g_spi_device);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
@@ -124,13 +289,21 @@ esp_err_t spi_master_init(void)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 带超时的同步 SPI 传输
|
/**
|
||||||
// ESP-IDF 6.0: spi_device_transmit() 不再接受超时参数
|
* @brief 执行一次同步 SPI 事务,先发送请求,再补时钟读取响应尾部。
|
||||||
// 使用 spi_device_queue_trans() + spi_device_get_trans_result() 实现超时
|
* @param cmd SPI 命令码;为 0 时不额外插入命令字节,直接发送 payload。
|
||||||
|
* @param payload 发送数据指针。
|
||||||
|
* @param payload_len 发送数据长度。
|
||||||
|
* @param recv_buf 接收缓冲区指针。
|
||||||
|
* @param recv_len 期望接收的响应字节数。
|
||||||
|
* @param timeout_ms 预留超时参数(当前实现未直接使用)。
|
||||||
|
* @return ESP_OK 成功,其他为失败错误码。
|
||||||
|
*/
|
||||||
esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
|
esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
|
||||||
size_t payload_len, uint8_t *recv_buf,
|
size_t payload_len, uint8_t *recv_buf,
|
||||||
size_t recv_len, uint32_t timeout_ms)
|
size_t recv_len, uint32_t timeout_ms)
|
||||||
{
|
{
|
||||||
|
(void)timeout_ms;
|
||||||
if (g_spi_device == NULL) {
|
if (g_spi_device == NULL) {
|
||||||
DEBUG_ERR("SPI", "SPI 设备未初始化");
|
DEBUG_ERR("SPI", "SPI 设备未初始化");
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
@@ -139,18 +312,7 @@ esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
|
|||||||
DEBUG_DBG("SPI", "SPI 传输: cmd=0x%02X, 发送=%zu 字节, 接收=%zu 字节",
|
DEBUG_DBG("SPI", "SPI 传输: cmd=0x%02X, 发送=%zu 字节, 接收=%zu 字节",
|
||||||
cmd, payload_len, recv_len);
|
cmd, payload_len, recv_len);
|
||||||
|
|
||||||
spi_transaction_t t = {};
|
esp_err_t ret = spi_transfer_request_response(cmd, payload, payload_len, recv_buf, recv_len);
|
||||||
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) {
|
if (ret != ESP_OK) {
|
||||||
DEBUG_WARN("SPI", "SPI 发送 cmd=0x%02X 失败: %s", cmd, esp_err_to_name(ret));
|
DEBUG_WARN("SPI", "SPI 发送 cmd=0x%02X 失败: %s", cmd, esp_err_to_name(ret));
|
||||||
} else {
|
} else {
|
||||||
@@ -184,11 +346,18 @@ 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)
|
esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms)
|
||||||
{
|
{
|
||||||
uint8_t dummy[64] = {0};
|
return spi_sync_transfer(cmd, NULL, 0, recv_buf, len, timeout_ms);
|
||||||
memset(dummy, 0xFF, sizeof(dummy));
|
|
||||||
return spi_sync_transfer(cmd, dummy, len, recv_buf, len, timeout_ms);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 轮询读取 SPI OTA 响应帧,不重复发送请求帧。
|
||||||
|
* @param resp_type 输出响应类型,可为 NULL。
|
||||||
|
* @param resp_payload 输出响应负载缓冲区,可为 NULL。
|
||||||
|
* @param resp_max 响应缓冲区最大长度。
|
||||||
|
* @param resp_len 输出响应长度,可为 NULL。
|
||||||
|
* @param timeout_ms 轮询超时时间(毫秒)。
|
||||||
|
* @return ESP_OK 成功,ESP_ERR_TIMEOUT 超时,其他为底层错误码。
|
||||||
|
*/
|
||||||
esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload,
|
esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload,
|
||||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
||||||
{
|
{
|
||||||
@@ -207,15 +376,10 @@ esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload,
|
|||||||
while (xTaskGetTickCount() <= deadline) {
|
while (xTaskGetTickCount() <= deadline) {
|
||||||
++attempt;
|
++attempt;
|
||||||
memset(rx_raw, 0xFF, sizeof(rx_raw));
|
memset(rx_raw, 0xFF, sizeof(rx_raw));
|
||||||
spi_transaction_t rsp = {};
|
esp_err_t ret = spi_transfer_clocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw));
|
||||||
rsp.length = SPI_OTA_FRAME_MAX * 8;
|
|
||||||
rsp.rxlength = SPI_OTA_FRAME_MAX * 8;
|
|
||||||
rsp.tx_buffer = dummy;
|
|
||||||
rsp.rx_buffer = rx_raw;
|
|
||||||
esp_err_t ret = spi_device_transmit(g_spi_device, &rsp);
|
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
DEBUG_WARN("SPI", "SPI OTA 响应读取失败[%u]: %s", attempt, esp_err_to_name(ret));
|
DEBUG_WARN("SPI", "SPI OTA 响应读取失败[%u]: %s", attempt, esp_err_to_name(ret));
|
||||||
vTaskDelay(pdMS_TO_TICKS(10));
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +388,7 @@ esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload,
|
|||||||
size_t parsed_len = 0;
|
size_t parsed_len = 0;
|
||||||
size_t frame_offset = 0;
|
size_t frame_offset = 0;
|
||||||
if (spi_ota_find_frame(rx_raw, sizeof(rx_raw), &parsed_type, &parsed_payload, &parsed_len, &frame_offset)) {
|
if (spi_ota_find_frame(rx_raw, sizeof(rx_raw), &parsed_type, &parsed_payload, &parsed_len, &frame_offset)) {
|
||||||
DEBUG_INFO("SPI", "OTA响应: type=0x%02X len=%u offset=%u attempt=%u",
|
DEBUG_DBG("SPI", "OTA响应: type=0x%02X len=%u offset=%u attempt=%u",
|
||||||
parsed_type, (unsigned)parsed_len, (unsigned)frame_offset, (unsigned)attempt);
|
parsed_type, (unsigned)parsed_len, (unsigned)frame_offset, (unsigned)attempt);
|
||||||
if (resp_type) *resp_type = parsed_type;
|
if (resp_type) *resp_type = parsed_type;
|
||||||
if (resp_payload && resp_max > 0) {
|
if (resp_payload && resp_max > 0) {
|
||||||
@@ -244,14 +408,30 @@ esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DEBUG_DBG("SPI", "OTA响应未就绪[%u]: %s", attempt, all_ff ? "all-FF" : "no-valid-SOF");
|
DEBUG_DBG("SPI", "OTA响应未就绪[%u]: %s first10=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
|
||||||
vTaskDelay(pdMS_TO_TICKS(10));
|
attempt, all_ff ? "all-FF" : "no-valid-SOF",
|
||||||
|
rx_raw[0], rx_raw[1], rx_raw[2], rx_raw[3],
|
||||||
|
rx_raw[4], rx_raw[5], rx_raw[6], rx_raw[7],
|
||||||
|
rx_raw[8], rx_raw[9]);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_ERR("SPI", "OTA响应超时: timeout=%ums", (unsigned)timeout_ms);
|
DEBUG_DBG("SPI", "OTA响应超时: timeout=%ums", (unsigned)timeout_ms);
|
||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 执行一次 SPI OTA 请求事务并等待响应帧。
|
||||||
|
* @param type 请求帧类型。
|
||||||
|
* @param payload 请求负载。
|
||||||
|
* @param payload_len 负载长度。
|
||||||
|
* @param resp_type 输出响应类型,可为 NULL。
|
||||||
|
* @param resp_payload 输出响应负载缓冲区,可为 NULL。
|
||||||
|
* @param resp_max 响应负载缓冲区大小。
|
||||||
|
* @param resp_len 输出响应长度,可为 NULL。
|
||||||
|
* @param timeout_ms 响应等待超时时间(毫秒)。
|
||||||
|
* @return ESP_OK 成功,其他为失败错误码。
|
||||||
|
*/
|
||||||
esp_err_t spi_ota_transfer(uint8_t type, const uint8_t *payload, size_t payload_len,
|
esp_err_t spi_ota_transfer(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||||
uint8_t *resp_type, uint8_t *resp_payload,
|
uint8_t *resp_type, uint8_t *resp_payload,
|
||||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
||||||
@@ -268,23 +448,61 @@ esp_err_t spi_ota_transfer(uint8_t type, const uint8_t *payload, size_t payload_
|
|||||||
return ESP_ERR_INVALID_ARG;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_INFO("SPI", "OTA事务: req_type=0x%02X req_len=%u", type, (unsigned)payload_len);
|
DEBUG_DBG("SPI", "OTA事务: req_type=0x%02X req_len=%u", type, (unsigned)payload_len);
|
||||||
|
|
||||||
spi_transaction_t req = {};
|
/* ============================================================
|
||||||
req.length = tx_len * 8;
|
* 第1次 CS 周期:发送请求帧(只发不收)
|
||||||
req.tx_buffer = tx_frame;
|
*
|
||||||
esp_err_t ret = spi_device_transmit(g_spi_device, &req);
|
* ESP32 发完请求帧后 CS↑,GD32 在 CS↑ 的 onCsRelease 中
|
||||||
|
* 或 RBNE 中断的 tryProcessCompleteFrame 中处理帧,
|
||||||
|
* 将 ACK 写入 s_txBuf 并启动 DMA。
|
||||||
|
*
|
||||||
|
* 注意:不能在同一个 CS 周期内发请求+读响应,因为:
|
||||||
|
* 1. SPI 全双工:ESP32 发请求时 MISO 同时在输出旧数据
|
||||||
|
* 2. GD32 收到请求帧后才能准备 ACK,需重启 DMA
|
||||||
|
* 3. DMA 重启期间 MISO 会输出 0x00/0xFF,导致帧断裂
|
||||||
|
* ============================================================ */
|
||||||
|
esp_err_t ret = spi_transfer_clocked(tx_frame, tx_len, NULL, 0);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
DEBUG_ERR("SPI", "SPI OTA 请求事务失败: %s", esp_err_to_name(ret));
|
DEBUG_ERR("SPI", "SPI OTA 请求发送失败: %s", esp_err_to_name(ret));
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(2));
|
/* 等 GD32 处理帧 + 准备响应。
|
||||||
|
*
|
||||||
|
* GD32 在 CS↑ 的 onCsRelease() 中处理帧:
|
||||||
|
* isrProcessFrame() → OTA handler 回调 → queueTxFrame() → DMA 启动
|
||||||
|
* 总耗时约 10-20µs(ISR 内执行,无调度延迟)。
|
||||||
|
*
|
||||||
|
* 但 EXTI ISR 可能被更高优先级的 RBNE 中断短暂抢占,
|
||||||
|
* 实际完成时间可能到 30-50µs。
|
||||||
|
*
|
||||||
|
* 500µs 提供了 10x 以上的安全余量,
|
||||||
|
* 同时对 OTA 事务延迟影响可忽略(一次 OTA 事务 ~1ms 级别)。 */
|
||||||
|
esp_rom_delay_us(500);
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 第2次 CS 周期:发 0xFF dummy 产生时钟,读 MISO 上的响应帧
|
||||||
|
*
|
||||||
|
* GD32 的 onCsAssert 会重启 DMA,将 s_txBuf(ACK帧)
|
||||||
|
* 推到 MISO,ESP32 在 96 字节中扫描定位响应帧。
|
||||||
|
* ============================================================ */
|
||||||
return spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
|
return spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CANFD 帧发送
|
// CANFD 帧发送
|
||||||
// 帧格式: [通道(1B)] [扩展帧ID(4B BE)] [数据(1~64B)]
|
// 帧格式: [通道(1B)] [扩展帧ID(4B BE)] [数据(1~64B)]
|
||||||
|
/**
|
||||||
|
* @brief 发送一帧 CANFD 数据到 GD32,并可选等待响应。
|
||||||
|
* @param channel CAN 通道号。
|
||||||
|
* @param ext_id 扩展帧 ID(29-bit)。
|
||||||
|
* @param data 发送负载指针。
|
||||||
|
* @param dlc 发送数据长度。
|
||||||
|
* @param recv_buf 接收缓冲区,可为 NULL。
|
||||||
|
* @param recv_max 接收缓冲区最大长度。
|
||||||
|
* @param timeout_ms 等待响应超时(毫秒)。
|
||||||
|
* @return >=0 为接收字节数,<0 为错误码。
|
||||||
|
*/
|
||||||
static int can_frame_send(can_channel_t channel, uint32_t ext_id,
|
static int can_frame_send(can_channel_t channel, uint32_t ext_id,
|
||||||
const uint8_t *data, uint8_t dlc,
|
const uint8_t *data, uint8_t dlc,
|
||||||
uint8_t *recv_buf, uint8_t recv_max,
|
uint8_t *recv_buf, uint8_t recv_max,
|
||||||
@@ -319,13 +537,8 @@ static int can_frame_send(can_channel_t channel, uint32_t ext_id,
|
|||||||
size_t tx_len = 5 + dlc;
|
size_t tx_len = 5 + dlc;
|
||||||
uint8_t rx_buf[5 + CAN_MAX_DLC] = {0};
|
uint8_t rx_buf[5 + CAN_MAX_DLC] = {0};
|
||||||
|
|
||||||
spi_transaction_t t = {};
|
const size_t rx_len = (recv_buf != NULL && recv_max > 0U) ? (size_t)(5U + recv_max) : 0U;
|
||||||
t.tx_buffer = tx_buf;
|
esp_err_t ret = spi_transfer_clocked(tx_buf, tx_len, rx_buf, rx_len);
|
||||||
t.rx_buffer = rx_buf;
|
|
||||||
t.length = tx_len * 8;
|
|
||||||
t.rxlength = (recv_max > 0) ? (5 + recv_max) * 8 : 0;
|
|
||||||
|
|
||||||
esp_err_t ret = spi_device_transmit(g_spi_device, &t);
|
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
DEBUG_ERR("SPI", "CAN[ch%d] ID=0x%08X 发送失败: %s",
|
DEBUG_ERR("SPI", "CAN[ch%d] ID=0x%08X 发送失败: %s",
|
||||||
channel, ext_id, esp_err_to_name(ret));
|
channel, ext_id, esp_err_to_name(ret));
|
||||||
@@ -352,26 +565,166 @@ static int can_frame_send(can_channel_t channel, uint32_t ext_id,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对外暴露的 GD32 CAN 发送接口,会在必要时修正默认 DLC。
|
/**
|
||||||
|
* @brief 对外暴露的 GD32 CAN 发送接口,必要时修正默认 DLC。
|
||||||
|
* @param channel CAN 通道。
|
||||||
|
* @param ext_id 扩展帧 ID。
|
||||||
|
* @param data 发送数据指针。
|
||||||
|
* @param dlc 数据长度。
|
||||||
|
* @param recv_buf 接收缓冲区,可为 NULL。
|
||||||
|
* @param recv_max 接收缓冲区大小。
|
||||||
|
* @param timeout_ms 等待响应超时(毫秒)。
|
||||||
|
* @return >=0 为接收字节数,<0 为错误码。
|
||||||
|
*/
|
||||||
int gd32_can_send(can_channel_t channel, uint32_t ext_id,
|
int gd32_can_send(can_channel_t channel, uint32_t ext_id,
|
||||||
const uint8_t *data, uint8_t dlc,
|
const uint8_t *data, uint8_t dlc,
|
||||||
uint8_t *recv_buf, uint8_t recv_max,
|
uint8_t *recv_buf, uint8_t recv_max,
|
||||||
uint32_t timeout_ms)
|
uint32_t timeout_ms)
|
||||||
{
|
{
|
||||||
if (dlc == 0) {
|
|
||||||
dlc = (data != NULL) ? 64 : 0; // 避免 dlc=0 的情况
|
|
||||||
}
|
|
||||||
return can_frame_send(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms);
|
return can_frame_send(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对外暴露的无响应 GD32 CAN 发送接口。
|
/**
|
||||||
|
* @brief 发送无需响应的 GD32 CAN 帧。
|
||||||
|
* @param channel CAN 通道。
|
||||||
|
* @param ext_id 扩展帧 ID。
|
||||||
|
* @param data 发送数据指针。
|
||||||
|
* @param dlc 数据长度。
|
||||||
|
* @return 0 表示已发送完成,<0 为错误码。
|
||||||
|
*/
|
||||||
int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id,
|
int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id,
|
||||||
const uint8_t *data, uint8_t dlc)
|
const uint8_t *data, uint8_t dlc)
|
||||||
{
|
{
|
||||||
return can_frame_send(channel, ext_id, data, dlc, NULL, 0, 0);
|
return can_frame_send(channel, ext_id, data, dlc, NULL, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 向正在运行的 GD32 DCU_APP 发送进入 Bootloader 的短 SPI 命令。
|
||||||
|
* @param timeout_ms 等待 APP 返回确认的最长时间。
|
||||||
|
* @return ESP_OK 表示 APP 已接收命令;超时则通常表示 GD32 已不在 APP 或 SPI 未就绪。
|
||||||
|
*/
|
||||||
|
esp_err_t gd32_app_request_bootloader(uint32_t timeout_ms)
|
||||||
|
{
|
||||||
|
uint8_t cmd = SPI_APP_ENTER_BOOT_CMD;
|
||||||
|
esp_err_t ret = spi_transfer_clocked(&cmd, 1U, NULL, 0U);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
DEBUG_WARN("SPI", "APP进入BOOT命令发送失败: %s", esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t rx[8] = {0};
|
||||||
|
uint8_t dummy[8];
|
||||||
|
memset(dummy, 0xFF, sizeof(dummy));
|
||||||
|
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||||||
|
while (xTaskGetTickCount() <= deadline) {
|
||||||
|
memset(rx, 0xFF, sizeof(rx));
|
||||||
|
ret = spi_transfer_clocked(dummy, sizeof(dummy), rx, sizeof(rx));
|
||||||
|
if (ret == ESP_OK) {
|
||||||
|
for (size_t i = 0; i + 3U < sizeof(rx); ++i) {
|
||||||
|
if (rx[i] == SPI_APP_ENTER_BOOT_CMD && rx[i + 1U] == SPI_APP_ENTER_BOOT_ACK_OK) {
|
||||||
|
DEBUG_INFO("SPI", "GD32 APP确认进入Bootloader OTA窗口");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_WARN("SPI", "GD32 APP进入Bootloader确认超时");
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec)
|
||||||
|
{
|
||||||
|
/* APP 侧 SPI 从机当前是中断逐字节收发,不适合像 BOOT 那样一次读 96B 长轮询。
|
||||||
|
* 这里改为:每次都完整重发一帧 PING 请求,然后只读一个短窗口寻找 ACK。
|
||||||
|
* 这样即便某次响应窗口错过,也会在下一次重发时重新生成 APP_ALIVE。 */
|
||||||
|
constexpr uint8_t kAppPingType = 0xA1U;
|
||||||
|
constexpr size_t kAppPingPollBytes = 32U;
|
||||||
|
|
||||||
|
uint8_t tx_frame[8] = {0};
|
||||||
|
size_t tx_len = 0U;
|
||||||
|
if (!spi_ota_build_frame(kAppPingType, NULL, 0U, tx_frame, &tx_len)) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||||||
|
uint32_t attempt = 0U;
|
||||||
|
|
||||||
|
while (xTaskGetTickCount() <= deadline) {
|
||||||
|
++attempt;
|
||||||
|
|
||||||
|
esp_err_t err = spi_transfer_clocked(tx_frame, tx_len, NULL, 0U);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
DEBUG_DBG("SPI", "APP PING 请求发送失败[%lu]: %s",
|
||||||
|
(unsigned long)attempt, esp_err_to_name(err));
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_rom_delay_us(200);
|
||||||
|
|
||||||
|
uint8_t rx_raw[kAppPingPollBytes];
|
||||||
|
uint8_t dummy[kAppPingPollBytes];
|
||||||
|
memset(dummy, 0xFF, sizeof(dummy));
|
||||||
|
memset(rx_raw, 0xFF, sizeof(rx_raw));
|
||||||
|
|
||||||
|
err = spi_transfer_clocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
DEBUG_DBG("SPI", "APP PING 响应读取失败[%lu]: %s",
|
||||||
|
(unsigned long)attempt, esp_err_to_name(err));
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t resp_type = 0U;
|
||||||
|
const uint8_t *resp_payload = nullptr;
|
||||||
|
size_t resp_len = 0U;
|
||||||
|
size_t frame_offset = 0U;
|
||||||
|
if (spi_ota_find_frame(rx_raw, sizeof(rx_raw),
|
||||||
|
&resp_type, &resp_payload, &resp_len, &frame_offset)) {
|
||||||
|
if (resp_type == SPI_OTA_RSP_ACK && resp_len >= 3U && resp_payload[0] == kAppPingType) {
|
||||||
|
if (uptime_sec) {
|
||||||
|
*uptime_sec = (uint32_t)resp_payload[1] | ((uint32_t)resp_payload[2] << 8);
|
||||||
|
}
|
||||||
|
DEBUG_INFO("SPI", "APP PING 成功: uptime=%lu sec, attempt=%lu, offset=%u",
|
||||||
|
uptime_sec ? (unsigned long)*uptime_sec : 0UL,
|
||||||
|
(unsigned long)attempt,
|
||||||
|
(unsigned)frame_offset);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_DBG("SPI", "APP PING 收到无效帧[%lu]: type=0x%02X len=%u offset=%u payload0=0x%02X",
|
||||||
|
(unsigned long)attempt,
|
||||||
|
resp_type,
|
||||||
|
(unsigned)resp_len,
|
||||||
|
(unsigned)frame_offset,
|
||||||
|
resp_len > 0U ? resp_payload[0] : 0xFFU);
|
||||||
|
} else {
|
||||||
|
DEBUG_DBG("SPI", "APP PING 未找到有效帧[%lu]: first8=%02X%02X%02X%02X%02X%02X%02X%02X",
|
||||||
|
(unsigned long)attempt,
|
||||||
|
rx_raw[0], rx_raw[1], rx_raw[2], rx_raw[3],
|
||||||
|
rx_raw[4], rx_raw[5], rx_raw[6], rx_raw[7]);
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_DBG("SPI", "APP PING 超时: timeout=%ums", (unsigned)timeout_ms);
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
// 带重试的 SPI 传输
|
// 带重试的 SPI 传输
|
||||||
|
/**
|
||||||
|
* @brief 执行带重试的 SPI 同步传输。
|
||||||
|
* @param cmd SPI 命令码。
|
||||||
|
* @param payload 发送数据指针。
|
||||||
|
* @param payload_len 发送长度。
|
||||||
|
* @param recv_buf 接收缓冲区。
|
||||||
|
* @param recv_len 接收长度。
|
||||||
|
* @param max_retries 最大重试次数。
|
||||||
|
* @return ESP_OK 成功,重试耗尽返回 ESP_ERR_TIMEOUT。
|
||||||
|
*/
|
||||||
esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload,
|
esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload,
|
||||||
size_t payload_len, uint8_t *recv_buf,
|
size_t payload_len, uint8_t *recv_buf,
|
||||||
size_t recv_len, int max_retries)
|
size_t recv_len, int max_retries)
|
||||||
|
|||||||
+26
-10
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
// SPI 配置常量
|
// SPI 配置常量
|
||||||
#define SPI_MASTER_CS_GPIO (4) // CS 片选引脚
|
#define SPI_MASTER_CS_GPIO (4) // CS 片选引脚
|
||||||
#define SPI_CLOCK_HZ (4 * 1000 * 1000) // 先降到 4MHz 便于联调
|
#define SPI_CLOCK_HZ (4 * 1000 * 1000) // 4MHz
|
||||||
#define SPI_MODE (0) // CPOL=0, CPHA=0
|
#define SPI_MODE (0) // CPOL=0, CPHA=0
|
||||||
#define SPI_QUEUE_SIZE (1)
|
#define SPI_QUEUE_SIZE (1)
|
||||||
#define SPI_OTA_FRAME_MAX (96)
|
#define SPI_OTA_FRAME_MAX (96)
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
#define SPI_OTA_CMD_REQ (0x01)
|
#define SPI_OTA_CMD_REQ (0x01)
|
||||||
#define SPI_OTA_CMD_DATA (0x02)
|
#define SPI_OTA_CMD_DATA (0x02)
|
||||||
#define SPI_OTA_CMD_CANCEL (0x03)
|
#define SPI_OTA_CMD_CANCEL (0x03)
|
||||||
|
#define SPI_APP_ENTER_BOOT_CMD (0xB0)
|
||||||
|
#define SPI_APP_ENTER_BOOT_ACK_OK (0xA5)
|
||||||
#define SPI_OTA_RSP_ACK (0x80)
|
#define SPI_OTA_RSP_ACK (0x80)
|
||||||
#define SPI_OTA_RSP_WAIT (0x81)
|
#define SPI_OTA_RSP_WAIT (0x81)
|
||||||
#define SPI_OTA_RSP_ERROR (0x82)
|
#define SPI_OTA_RSP_ERROR (0x82)
|
||||||
@@ -57,7 +59,7 @@ typedef struct {
|
|||||||
* @param channel CAN 通道(0~2)。
|
* @param channel CAN 通道(0~2)。
|
||||||
* @param ext_id 29 位扩展帧 ID。
|
* @param ext_id 29 位扩展帧 ID。
|
||||||
* @param data 数据负载指针。
|
* @param data 数据负载指针。
|
||||||
* @param dlc 数据长度,0 则自动推断。
|
* @param dlc 数据长度,范围 1~64。
|
||||||
* @param recv_buf 接收缓冲区,可为 NULL。
|
* @param recv_buf 接收缓冲区,可为 NULL。
|
||||||
* @param recv_max 期望最大接收字节数。
|
* @param recv_max 期望最大接收字节数。
|
||||||
* @param timeout_ms 超时时间(毫秒)。
|
* @param timeout_ms 超时时间(毫秒)。
|
||||||
@@ -73,7 +75,7 @@ int gd32_can_send(can_channel_t channel, uint32_t ext_id,
|
|||||||
* @param channel CAN 通道(0~2)。
|
* @param channel CAN 通道(0~2)。
|
||||||
* @param ext_id 29 位扩展帧 ID。
|
* @param ext_id 29 位扩展帧 ID。
|
||||||
* @param data 数据负载指针。
|
* @param data 数据负载指针。
|
||||||
* @param dlc 数据长度。
|
* @param dlc 数据长度,范围 1~64。
|
||||||
* @return >= 0 成功,< 0 失败。
|
* @return >= 0 成功,< 0 失败。
|
||||||
*/
|
*/
|
||||||
int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id,
|
int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id,
|
||||||
@@ -86,12 +88,12 @@ int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id,
|
|||||||
esp_err_t spi_master_init(void);
|
esp_err_t spi_master_init(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 执行一次同步 SPI 传输,发送命令和负载并可选接收返回数据。
|
* @brief 执行一次同步 SPI 事务,先发送请求,再读取请求之后的响应字节。
|
||||||
* @param cmd SPI 命令码。
|
* @param cmd SPI 命令码;为 0 时不额外插入命令字节。
|
||||||
* @param payload 发送数据指针。
|
* @param payload 发送数据指针。
|
||||||
* @param payload_len 发送数据长度。
|
* @param payload_len 发送数据长度。
|
||||||
* @param recv_buf 接收缓冲区,可为 NULL。
|
* @param recv_buf 接收缓冲区,可为 NULL。
|
||||||
* @param recv_len 期望接收长度。
|
* @param recv_len 期望接收的响应长度。
|
||||||
* @param timeout_ms 超时时间(毫秒)。
|
* @param timeout_ms 超时时间(毫秒)。
|
||||||
* @return ESP_OK 成功,其他失败。
|
* @return ESP_OK 成功,其他失败。
|
||||||
*/
|
*/
|
||||||
@@ -100,8 +102,8 @@ esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
|
|||||||
size_t recv_len, uint32_t timeout_ms);
|
size_t recv_len, uint32_t timeout_ms);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 仅发送 SPI 数据,不关心返回内容。
|
* @brief 仅发送 SPI 请求数据,不关心返回内容。
|
||||||
* @param cmd SPI 命令码。
|
* @param cmd SPI 命令码;为 0 时仅发送 payload。
|
||||||
* @param payload 发送数据指针。
|
* @param payload 发送数据指针。
|
||||||
* @param len 发送数据长度。
|
* @param len 发送数据长度。
|
||||||
* @return ESP_OK 成功,其他失败。
|
* @return ESP_OK 成功,其他失败。
|
||||||
@@ -109,8 +111,8 @@ esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
|
|||||||
esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len);
|
esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 仅接收 SPI 数据,底层会发送哑数据产生时钟。
|
* @brief 仅接收 SPI 数据,必要时先发送命令字节,再发送哑数据产生时钟。
|
||||||
* @param cmd SPI 命令码。
|
* @param cmd SPI 命令码;为 0 时只发送哑数据。
|
||||||
* @param recv_buf 接收缓冲区。
|
* @param recv_buf 接收缓冲区。
|
||||||
* @param len 接收长度。
|
* @param len 接收长度。
|
||||||
* @param timeout_ms 超时时间(毫秒)。
|
* @param timeout_ms 超时时间(毫秒)。
|
||||||
@@ -139,4 +141,18 @@ esp_err_t spi_ota_transfer(uint8_t type, const uint8_t *payload, size_t payload_
|
|||||||
esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload,
|
esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload,
|
||||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 通知正在运行的 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。
|
||||||
|
* @return ESP_OK 表示 APP 已确认;ESP_ERR_TIMEOUT 常表示 GD32 已在 Bootloader 或通信未就绪。
|
||||||
|
*/
|
||||||
|
esp_err_t gd32_app_request_bootloader(uint32_t timeout_ms);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 向 GD32 APP 发送存活探测命令 (TYPE=0xA1),等待 APP 回复 ALIVE ACK。
|
||||||
|
* @param timeout_ms 超时时间(毫秒)。
|
||||||
|
* @param uptime_sec 输出 APP 运行秒数,可为 NULL。
|
||||||
|
* @return ESP_OK APP 存活并回复;ESP_ERR_TIMEOUT 超时无响应。
|
||||||
|
*/
|
||||||
|
esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec);
|
||||||
|
|
||||||
#endif // SPI_MASTER_H
|
#endif // SPI_MASTER_H
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
*** Begin Patch
|
||||||
|
*** Update File: G:\Robot_v2\DCU\ESP32_COMM\main\transport\spi_master.h
|
||||||
|
@@ #define SPI_CLOCK_HZ (4 * 1000 * 1000) // 4MHz
|
||||||
|
- #define SPI_CLOCK_HZ (4 * 1000 * 1000) // 4MHz
|
||||||
|
+ #define SPI_CLOCK_HZ (10 * 1000 * 1000) // 10MHz
|
||||||
|
@@ #define SPI_QUEUE_SIZE (1)
|
||||||
|
- #define SPI_QUEUE_SIZE (1)
|
||||||
|
+ #define SPI_QUEUE_SIZE (3)
|
||||||
|
@@ #define SPI_OTA_FRAME_MAX (96)
|
||||||
|
- #define SPI_OTA_FRAME_MAX (96)
|
||||||
|
+ #define SPI_OTA_FRAME_MAX (128)
|
||||||
|
*** End Patch
|
||||||
+795
-338
File diff suppressed because it is too large
Load Diff
+621
-289
File diff suppressed because it is too large
Load Diff
+205
-221
@@ -1,279 +1,263 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>ESP32 OTA 中继系统 - 上位机</title>
|
<title>ESP32 OTA 中继控制台</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- 顶部标题栏 -->
|
|
||||||
<header class="header">
|
<!-- ====== HEADER: compact status bar ====== -->
|
||||||
<div class="logo-section">
|
<header class="header-bar">
|
||||||
<div class="logo-icon">📡</div>
|
<div class="header-brand">
|
||||||
<div class="logo-text">
|
<span class="dot-mark"></span>
|
||||||
<h1>ESP32 OTA 中继系统</h1>
|
<strong>ESP32 OTA 中继控制台</strong>
|
||||||
<p>GD32 固件空中升级管理平台</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="status-badges">
|
|
||||||
<div class="badge disconnected" id="connectionBadge">
|
|
||||||
<span class="dot"></span>
|
|
||||||
<span id="connectionText">未连接</span>
|
|
||||||
</div>
|
|
||||||
<div class="badge" id="healthBadge">
|
|
||||||
<span class="dot"></span>
|
|
||||||
<span id="healthText">系统正常</span>
|
|
||||||
</div>
|
|
||||||
<div class="badge">
|
|
||||||
<span id="fwVersion">v--</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-status">
|
||||||
|
<span class="pill offline" id="pillConn"><span class="indicator"></span><span id="pillConnTxt">未连接</span></span>
|
||||||
|
<span class="pill" id="pillVer">v--</span>
|
||||||
|
<span class="pill" id="pillIP">--</span>
|
||||||
|
<span class="pill" id="pillRSSI">--</span>
|
||||||
|
<span class="pill idle" id="pillRelay"><span class="indicator"></span><span id="pillRelayTxt">待机</span></span>
|
||||||
|
<span class="pill" id="pillUptime">运行 --</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 主网格布局 -->
|
<!-- ====== MAIN LAYOUT ====== -->
|
||||||
<div class="main-grid">
|
<div class="main-layout">
|
||||||
<!-- 左侧面板 -->
|
|
||||||
<div class="left-panel">
|
<!-- ====== LEFT: workflow ====== -->
|
||||||
<!-- 连接配置 -->
|
<div>
|
||||||
<div class="card connection-card">
|
|
||||||
<div class="card-header">
|
<!-- 1. Connection -->
|
||||||
<div class="card-title">
|
<div class="card">
|
||||||
<div class="icon">🔗</div>
|
<div class="card-head"><span class="ico">🔗</span> 设备连接</div>
|
||||||
连接配置
|
<div class="card-body">
|
||||||
</div>
|
<div class="form-row">
|
||||||
</div>
|
<label class="form-label" for="espIp">ESP32 IP 地址</label>
|
||||||
<div class="form-group">
|
|
||||||
<label>ESP32 IP 地址</label>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" id="espIp" value="192.168.31.127" placeholder="输入 IP 地址">
|
<input class="input" type="text" id="espIp" value="192.168.31.127" placeholder="输入 IP 地址">
|
||||||
<button class="btn btn-secondary" onclick="testConnection()">测试</button>
|
<button class="btn btn-outline" onclick="testConnection()">测试</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="conn-indicator err" id="connStatus">
|
||||||
<label>目标芯片</label>
|
<span class="light"></span>
|
||||||
<select id="targetChip" onchange="onTargetChipChange()" style="width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text-primary);font-size:13px;">
|
<span id="connStatusTxt">等待连接</span>
|
||||||
<option value="gd32">GD32(Flash 中继)</option>
|
|
||||||
<option value="esp32">ESP32(自身升级)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" style="width:100%; margin-top:8px;" onclick="restartEsp32()">
|
</div>
|
||||||
🔄 重启 ESP32
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. ESP32 OTA -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head"><span class="ico">📦</span> ESP32 自身升级</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="font-size:12px;color:var(--text-dim);margin-bottom:10px;">上传固件并执行 OTA,设备将自动重启</div>
|
||||||
|
<div class="upload-zone upload-zone-sm" id="uploadZoneEsp32" onclick="if(window.showOpenFilePicker){pickFileViaFSAPI('esp32')}else{document.getElementById('fileInputEsp32').click()}">
|
||||||
|
<div class="upload-ico">📤</div>
|
||||||
|
<h4>点击或拖拽 ESP32 固件</h4>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="fileInputEsp32" accept=".bin" onchange="handleFileSelect(event, 'esp32')" style="display:none;">
|
||||||
|
<div class="file-info" id="fileInfoEsp32">
|
||||||
|
<div class="file-info-row">
|
||||||
|
<span style="font-weight:600;">📄 <span id="fileNameEsp32">-</span></span>
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick="clearFile('esp32')">清除</button>
|
||||||
|
</div>
|
||||||
|
<div class="file-info-grid">
|
||||||
|
<div class="file-meta-item"><span>类型</span><b id="fileTypeEsp32">-</b></div>
|
||||||
|
<div class="file-meta-item"><span>大小</span><b id="fileSizeEsp32">-</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-block" id="uploadEsp32Btn" onclick="startUpload('esp32')" disabled style="margin-top:10px;">
|
||||||
|
上传并升级 ESP32
|
||||||
</button>
|
</button>
|
||||||
<a class="btn btn-secondary" href="ble_ota.html" style="width:100%; margin-top:8px; text-decoration:none; display:flex; align-items:center; justify-content:center;">
|
</div>
|
||||||
📶 打开 BLE 刷写页面
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. GD32 OTA -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head"><span class="ico">📦</span> GD32 SPI 中继</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="font-size:12px;color:var(--text-dim);margin-bottom:10px;">上传固件到缓存,再触发 SPI 中继刷写<br><span style="color:var(--accent);">支持 .bin 和 .hex(自动转换)</span></div>
|
||||||
|
<div class="upload-zone upload-zone-sm" id="uploadZoneGd32" onclick="if(window.showOpenFilePicker){pickFileViaFSAPI('gd32')}else{document.getElementById('fileInputGd32').click()}">
|
||||||
|
<div class="upload-ico">📤</div>
|
||||||
|
<h4>点击或拖拽 GD32 固件</h4>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="fileInputGd32" accept=".hex,.bin" onchange="handleFileSelect(event, 'gd32')" style="display:none;">
|
||||||
|
<div class="file-info" id="fileInfoGd32">
|
||||||
|
<div class="file-info-row">
|
||||||
|
<span style="font-weight:600;">📄 <span id="fileNameGd32">-</span></span>
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick="clearFile('gd32')">清除</button>
|
||||||
|
</div>
|
||||||
|
<div class="file-info-grid">
|
||||||
|
<div class="file-meta-item"><span>类型</span><b id="fileTypeGd32">-</b></div>
|
||||||
|
<div class="file-meta-item"><span>大小</span><b id="fileSizeGd32">-</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-success btn-block" id="uploadGd32Btn" onclick="startUpload('gd32')" disabled style="margin-top:10px;margin-bottom:8px;">
|
||||||
|
上传固件到缓存
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline btn-block" id="relayBtn" onclick="triggerRelay()">
|
||||||
|
触发 GD32 OTA 中继
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. Common -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head"><span class="ico">⚡</span> 通用操作</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="progress-wrap" id="progressWrap">
|
||||||
|
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<span id="progressTxt">准备上传...</span>
|
||||||
|
<span id="progressPct">0%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline btn-block" onclick="restartEsp32()">重启 ESP32</button>
|
||||||
|
<a class="btn btn-outline btn-block" href="ble_ota.html?v=20260509-0046" style="margin-top:10px;text-decoration:none;">
|
||||||
|
切换到 BLE 蓝牙刷写
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 文件上传 -->
|
</div>
|
||||||
|
|
||||||
|
<!-- ====== RIGHT: status + tools ====== -->
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<!-- Primary status -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-head"><span class="ico">📡</span> 设备状态</div>
|
||||||
<div class="card-title">
|
<div class="card-body">
|
||||||
<div class="icon">📁</div>
|
<div class="badge-grid">
|
||||||
固件文件
|
<div class="badge-stat">
|
||||||
|
<div class="v err" id="bConn">离线</div>
|
||||||
|
<div class="k">连接状态</div>
|
||||||
|
</div>
|
||||||
|
<div class="badge-stat">
|
||||||
|
<div class="v warn" id="bOTAProgress">0%</div>
|
||||||
|
<div class="k">OTA 进度</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="upload-zone" id="uploadZone" onclick="document.getElementById('fileInput').click()">
|
<!-- Secondary metrics -->
|
||||||
<div class="upload-icon">📤</div>
|
|
||||||
<h3>点击或拖拽文件到此处</h3>
|
|
||||||
<p>支持 .hex 和 .bin 格式</p>
|
|
||||||
</div>
|
|
||||||
<input type="file" id="fileInput" class="file-input" accept=".hex,.bin" onchange="handleFileSelect(event)">
|
|
||||||
|
|
||||||
<div class="file-info" id="fileInfo">
|
|
||||||
<div class="file-header">
|
|
||||||
<div class="file-name">
|
|
||||||
<span class="icon">📄</span>
|
|
||||||
<span id="fileName">-</span>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-secondary" onclick="clearFile()" style="padding:6px 12px;">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="file-meta">
|
|
||||||
<div class="meta-item"><span>类型:</span> <b id="fileType">-</b></div>
|
|
||||||
<div class="meta-item"><span>大小:</span> <b id="fileSize">-</b></div>
|
|
||||||
<div class="meta-item"><span>数据包:</span> <b id="fileChunks">-</b></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="progress-section" id="progressSection">
|
|
||||||
<div class="progress-bar-container">
|
|
||||||
<div class="progress-bar" id="progressBar"></div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-info">
|
|
||||||
<span id="progressText">准备上传...</span>
|
|
||||||
<span id="progressPercent">0%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button class="btn btn-primary" id="uploadBtn" onclick="startUpload()" disabled>
|
|
||||||
⬆️ 上传固件
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-success" id="relayBtn" onclick="triggerRelay()">
|
|
||||||
🚀 触发 OTA
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="esp32Actions" style="display:none; margin-top:12px;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- OTA 阶段指示 -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-head"><span class="ico">📊</span> 运行指标</div>
|
||||||
<div class="card-title">
|
<div class="card-body">
|
||||||
<div class="icon">📊</div>
|
<div class="metric-grid">
|
||||||
OTA 流程阶段
|
<div class="metric"><div class="k">温度</div><div class="v" id="mTemp">--</div></div>
|
||||||
</div>
|
<div class="metric"><div class="k">WiFi / IP</div><div class="v" id="mWifi" style="font-size:14px;">--</div></div>
|
||||||
</div>
|
<div class="metric"><div class="k">信号强度</div><div class="v" id="mRSSI">--</div></div>
|
||||||
<div class="stage-indicator">
|
<div class="metric"><div class="k">空闲堆</div><div class="v" id="mHeap" style="font-size:16px;">--</div></div>
|
||||||
<div class="stage" id="stage1">
|
<div class="metric"><div class="k">最低空闲堆</div><div class="v" id="mMinHeap" style="font-size:16px;">--</div></div>
|
||||||
<div class="stage-icon">1️⃣</div>
|
<div class="metric"><div class="k">固件版本</div><div class="v" id="mVersion" style="font-size:16px;">--</div></div>
|
||||||
<div class="stage-label">握手认证</div>
|
<div class="metric"><div class="k">OTA 缓存</div><div class="v" id="mOtaCache" style="font-size:13px;">--</div></div>
|
||||||
</div>
|
|
||||||
<div class="stage" id="stage2">
|
|
||||||
<div class="stage-icon">2️⃣</div>
|
|
||||||
<div class="stage-label">传输准备</div>
|
|
||||||
</div>
|
|
||||||
<div class="stage" id="stage3">
|
|
||||||
<div class="stage-icon">3️⃣</div>
|
|
||||||
<div class="stage-label">数据转发</div>
|
|
||||||
</div>
|
|
||||||
<div class="stage" id="stage4">
|
|
||||||
<div class="stage-icon">4️⃣</div>
|
|
||||||
<div class="stage-label">完成跳转</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧面板 -->
|
<!-- OTA stage flow -->
|
||||||
<div class="right-panel">
|
|
||||||
<!-- 实时状态 -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-head"><span class="ico">🔀</span> OTA 流程阶段</div>
|
||||||
<div class="card-title">
|
<div class="card-body">
|
||||||
<div class="icon">📈</div>
|
<div class="stage-flow">
|
||||||
实时状态
|
<div class="stage-node" id="s1"><div class="stage-dot">1</div><div class="stage-label">握手认证</div></div>
|
||||||
<span style="font-size:12px;color:var(--text-muted);font-weight:normal;margin-left:8px;">运行: <span id="statUptime">--</span></span>
|
<div class="stage-node" id="s2"><div class="stage-dot">2</div><div class="stage-label">传输准备</div></div>
|
||||||
</div>
|
<div class="stage-node" id="s3"><div class="stage-dot">3</div><div class="stage-label">数据转发</div></div>
|
||||||
</div>
|
<div class="stage-node" id="s4"><div class="stage-dot">4</div><div class="stage-label">完成跳转</div></div>
|
||||||
<div class="status-grid">
|
|
||||||
<div class="status-item">
|
|
||||||
<div class="value" id="statTemperature">--</div>
|
|
||||||
<div class="label">芯片温度</div>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<div class="value" id="statWifi">--</div>
|
|
||||||
<div class="label">WiFi 状态</div>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<div class="value" id="statSignal">--</div>
|
|
||||||
<div class="label">信号强度</div>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<div class="value" id="statHeap">0</div>
|
|
||||||
<div class="label">空闲堆</div>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<div class="value" id="statMinHeap">0</div>
|
|
||||||
<div class="label">最低空闲堆</div>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<div class="value" id="statProgress">0%</div>
|
|
||||||
<div class="label">上传进度</div>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<div class="value" id="statRelayState">待机</div>
|
|
||||||
<div class="label">中继状态</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CAN 帧发送面板 -->
|
<!-- CAN panel -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-head"><span class="ico">📨</span> CAN 帧发送</div>
|
||||||
<div class="card-title">
|
<div class="card-body">
|
||||||
<div class="icon">📨</div>
|
<div class="can-grid">
|
||||||
CAN 帧发送
|
<div class="form-row" style="margin:0"><label class="form-label">通道</label>
|
||||||
|
<select class="input" id="canChannel"><option value="0">CH0</option><option value="1">CH1</option><option value="2">CH2</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin:0"><label class="form-label">扩展帧 ID</label>
|
||||||
|
<input class="input" type="text" id="canId" value="0x18DA00F1">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin:0"><label class="form-label">DLC</label>
|
||||||
|
<input class="input" type="number" id="canDlc" value="8" min="1" max="64">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin:0"><label class="form-label">数据 (HEX)</label>
|
||||||
|
<input class="input" type="text" id="canData" value="0102030405060708" placeholder="01020304...">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:grid; grid-template-columns:80px 1fr 80px 1fr; gap:12px; margin-bottom:16px;">
|
<div class="helper-note" id="canHint">输入十六进制数据后会自动检查字节数是否与 DLC 一致。</div>
|
||||||
<div class="form-group" style="margin:0;">
|
<div class="btn-row">
|
||||||
<label style="font-size:11px;color:var(--text-muted);margin-bottom:6px;display:block;">通道</label>
|
<button class="btn btn-primary" onclick="sendCanFrame()">发送</button>
|
||||||
<select id="canChannel" style="width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text-primary);font-size:13px;">
|
<button class="btn btn-outline" onclick="clearCanFields()">清空</button>
|
||||||
<option value="0">CH0</option>
|
|
||||||
<option value="1">CH1</option>
|
|
||||||
<option value="2">CH2</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin:0;">
|
<div id="canResp" style="display:none;margin-top:12px;padding:12px;background:var(--bg-elevated);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:12px;">
|
||||||
<label style="font-size:11px;color:var(--text-muted);margin-bottom:6px;display:block;">扩展帧 ID</label>
|
<div style="color:var(--text-label);margin-bottom:4px;">响应</div>
|
||||||
<input type="text" id="canId" value="0x18DA00F1" placeholder="例如: 0x18DA00F1" style="width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text-primary);font-size:13px;">
|
<div id="canRespText" style="color:var(--accent);"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin:0;">
|
|
||||||
<label style="font-size:11px;color:var(--text-muted);margin-bottom:6px;display:block;">DLC</label>
|
|
||||||
<input type="number" id="canDlc" value="8" min="1" max="64" style="width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text-primary);font-size:13px;">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin:0;">
|
|
||||||
<label style="font-size:11px;color:var(--text-muted);margin-bottom:6px;display:block;">数据 (HEX)</label>
|
|
||||||
<input type="text" id="canData" value="0102030405060708" placeholder="01020304..." style="width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text-primary);font-size:13px;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:12px;">
|
|
||||||
<button class="btn btn-primary" style="flex:1;" onclick="sendCanFrame()">📤 发送</button>
|
|
||||||
<button class="btn btn-secondary" onclick="clearCanFields()">清空</button>
|
|
||||||
</div>
|
|
||||||
<div id="canResponse" style="margin-top:16px;background:var(--bg-secondary);border-radius:10px;padding:12px 16px;font-family:monospace;font-size:12px;display:none;">
|
|
||||||
<div style="color:var(--text-muted);margin-bottom:6px;">响应:</div>
|
|
||||||
<div id="canRespContent" style="color:var(--accent);"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 调试日志 -->
|
<!-- SPI panel -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head"><span class="ico">⚡</span> SPI 发送</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
|
||||||
|
<input class="input" type="text" id="spiType" value="01" maxlength="2" placeholder="type" title="帧类型 (hex)" style="width:52px;flex:none;">
|
||||||
|
<input class="input" type="text" id="spiData" value="" placeholder="payload hex..." style="flex:1;min-width:100px;">
|
||||||
|
<input class="input" type="number" id="spiTimeout" value="1000" min="100" max="10000" title="超时 ms" style="width:68px;flex:none;">
|
||||||
|
<span style="font-size:11px;color:var(--text-dim);">ms</span>
|
||||||
|
<button class="btn btn-primary" onclick="sendSpiFrame()" style="padding:4px 14px;">发送</button>
|
||||||
|
<button class="btn btn-outline" onclick="clearSpiFields()" style="padding:4px 10px;">清空</button>
|
||||||
|
</div>
|
||||||
|
<div class="helper-note" id="spiHint">type(hex) + payload → 自动构建 SOF+LEN+CRC 帧</div>
|
||||||
|
<div id="spiResp" style="display:none;margin-top:8px;padding:8px 10px;background:var(--bg-elevated);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:12px;">
|
||||||
|
<span id="spiRespText" style="color:var(--accent);"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log panel -->
|
||||||
<div class="card log-panel">
|
<div class="card log-panel">
|
||||||
<div class="card-header">
|
<div class="card-head" style="justify-content:space-between;">
|
||||||
<div class="card-title">
|
<span><span class="ico">🖥️</span> 调试日志</span>
|
||||||
<div class="icon">🖥️</div>
|
<span style="font-size:11px;font-weight:400;color:var(--text-label);text-transform:none;letter-spacing:0;">UDP :6666</span>
|
||||||
调试日志
|
|
||||||
<span style="font-size:12px;color:var(--text-muted);font-weight:normal;margin-left:8px;">UDP:6666</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="log-header">
|
<div class="log-toolbar">
|
||||||
<span style="font-size:13px;color:var(--text-muted);">实时日志输出</span>
|
|
||||||
<div class="log-filters">
|
<div class="log-filters">
|
||||||
<button class="filter-btn active" onclick="toggleFilter('all', this)">全部</button>
|
<button class="log-filter active" onclick="setLogFilter('all', this)">全部</button>
|
||||||
<button class="filter-btn" onclick="toggleFilter('E', this)">错误</button>
|
<button class="log-filter" onclick="setLogFilter('E', this)">错误</button>
|
||||||
<button class="filter-btn" onclick="toggleFilter('W', this)">警告</button>
|
<button class="log-filter" onclick="setLogFilter('W', this)">警告</button>
|
||||||
<button class="filter-btn" onclick="toggleFilter('I', this)">信息</button>
|
<button class="log-filter" onclick="setLogFilter('I', this)">信息</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="log-content" id="logContent">
|
|
||||||
<div class="log-entry">
|
|
||||||
<span class="log-time">[--:--:--]</span>
|
|
||||||
<span class="log-level I">I</span>
|
|
||||||
<span class="log-tag">[系统]</span>
|
|
||||||
<span class="log-msg">等待上位机连接...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="log-footer">
|
|
||||||
<span class="log-stats"><span id="logCount">0</span> 条日志</span>
|
|
||||||
<button class="btn btn-secondary" onclick="clearLogs()" style="padding:6px 12px;font-size:12px;">清除日志</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick="toggleAutoScroll()" id="autoScrollBtn" title="切换自动滚动">📌 自动滚动</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="log-scroll" id="logScroll">
|
||||||
|
<div class="log-line"><span class="lt">[--:--:--]</span><span class="lv I">I</span><span class="lg">[系统]</span><span class="lm">等待上位机连接...</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="log-foot">
|
||||||
|
<span>共 <span id="logCount">0</span> 条</span>
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick="clearLogs()">清除日志</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast 提示 -->
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast -->
|
||||||
<div class="toast" id="toast">
|
<div class="toast" id="toast">
|
||||||
<span class="toast-icon" id="toastIcon">✓</span>
|
<span class="toast-ico" id="toastIco">✓</span>
|
||||||
<span class="toast-message" id="toastMessage">操作成功</span>
|
<span id="toastMsg">操作成功</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js?v=20260529"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+349
-614
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user