feat(ble): 完善BLE OTA架构并优化性能
- 实现BLE OTA数据路径分离:添加ble_ota_data.cpp处理重排序/工作线程/DATA_IN, 和ble_ota_host.cpp处理GATT/GAP/广播/主机功能 - 将滑动ACK窗口从32增大到64,实测Windows WebBT环境下固件传输速度提升约10% (从40KB/s提升到44KB/s),内存消耗约30KB重排序缓冲区 - 优化调试日志级别,将OTA相关信息从INFO调整为DBG以减少日志冗余 refactor(can): 改进CAN反馈监听机制 - 将CAN反馈任务从1Hz主动轮询模式改为被动事件监听模式, 使用SpiMaster::takeEvent()替代主动发送0xC1轮询请求 - 更新相关头文件注释说明架构变更,将"周期性轮询"改为"被动接收推送" - 修改调试日志级别,将暂停/恢复状态日志从INFO调整为DBG
This commit is contained in:
+4
-2
@@ -9,10 +9,12 @@ idf_component_register(
|
||||
"storage/gd32_flash_store.cpp" # GD32 Flash 存储抽象层(H1 提取)
|
||||
"server/wifi_ota.cpp" # WiFi HTTP OTA 服务器
|
||||
"server/http_handlers.cpp" # HTTP 路由处理(H2 从 wifi_ota 提取)
|
||||
"server/ble_ota.cpp" # BLE GATT OTA 服务器
|
||||
"server/ble_ota.cpp" # BLE OTA 公开 API / 收尾 / 共存
|
||||
"server/ble_ota_data.cpp" # BLE OTA 数据路径(reorder/worker/DATA_IN)
|
||||
"server/ble_ota_host.cpp" # BLE OTA GATT/GAP/广播/Host
|
||||
"monitor/health_monitor.cpp" # 系统健康监控(温度/RSSI/堆内存/栈水位主动巡检)
|
||||
"monitor/cpu_stats.cpp" # 1Hz FreeRTOS CPU 占用采样任务
|
||||
"monitor/can_feedback_task.cpp" # CAN 1Hz 反馈轮询任务(0xC1)
|
||||
"monitor/can_feedback_task.cpp" # CAN 反馈被动监听(0xC1 takeEvent)
|
||||
"monitor/debug_log.cpp" # 三路调试日志(UART/UDP/环形缓冲)
|
||||
"monitor/task_stack_check.cpp" # 任务栈水位指标收集(health_mon 巡检 + /system/status 共享)
|
||||
"monitor/stack_overflow_hook.cpp" # FreeRTOS 栈溢出钩子(canary 触发时 UART 硬写告警)
|
||||
|
||||
@@ -60,7 +60,7 @@ inline constexpr uint32_t TASK_STACK_BLE_WORKER = 3072; ///< ble_ota_worker
|
||||
inline constexpr uint32_t TASK_STACK_HEALTH_MON = 3072; ///< health_mon:peak~1.2KB
|
||||
inline constexpr uint32_t TASK_STACK_CPU_STATS = 3072; ///< cpu_stats:peak~2KB
|
||||
inline constexpr uint32_t TASK_STACK_WIFI_RECONNECT = 2560; ///< wifi_reconnect:peak~1KB
|
||||
inline constexpr uint32_t TASK_STACK_CAN_FB = 3072; ///< can_fb:1Hz 轮询
|
||||
inline constexpr uint32_t TASK_STACK_CAN_FB = 3072; ///< can_fb:被动 takeEvent(0xC1)
|
||||
inline constexpr uint32_t TASK_STACK_BLE_FINISH = 4096; ///< 按需:SHA finalize(保持)
|
||||
inline constexpr uint32_t TASK_STACK_BLE_GD32_WAIT = 3072; ///< 按需:GD32 中继等待
|
||||
inline constexpr uint32_t TASK_STACK_BLE_RESTART = 2048; ///< 按需:延时重启
|
||||
@@ -130,9 +130,13 @@ inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00; ///< BLE 数
|
||||
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01; ///< BLE 数据通道:ESP32 固件包标识
|
||||
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480; ///< BLE 单包数据载荷大小(字节)
|
||||
// 滑动 ACK 窗口(须与前端 tools/js/ble/constants.js BLE_OTA_ACK_WINDOW 保持一致)
|
||||
inline constexpr size_t BLE_OTA_ACK_WINDOW = 32;
|
||||
// ★ 64:实测 Windows WebBT 窗口内单包间隔 0.4-2.9ms(2M PHY 多包/事件),
|
||||
// 瓶颈在每窗口的 ACK 往返。窗口 32→64 提速 ~10%(40→44 KB/s),再试 96 无收益
|
||||
// (ACK 往返在 64 已摊薄,稳态瓶颈是 Windows GATT 每包 ~10.6ms resolve 延迟)。
|
||||
// 内存:64×480B ≈ 30KB reorder buffer,分配失败有 fallback 丢弃策略兜底。
|
||||
inline constexpr size_t BLE_OTA_ACK_WINDOW = 64;
|
||||
// Reorder buffer 窗口大小(吸收 BLE 窗口内乱序到达的包,须 >= BLE_OTA_ACK_WINDOW)
|
||||
inline constexpr size_t BLE_OTA_REORDER_WINDOW = 32;
|
||||
inline constexpr size_t BLE_OTA_REORDER_WINDOW = 64;
|
||||
// 数据队列深度:略大于窗口,seq=0 预擦期间多扛几批
|
||||
inline constexpr size_t BLE_OTA_DATA_QUEUE_LEN = (BLE_OTA_ACK_WINDOW * 2 + 16);
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 50
|
||||
#define FW_VERSION_STRING "0.3.50"
|
||||
#define FW_VERSION_PATCH 51
|
||||
#define FW_VERSION_STRING "0.3.51"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-07-17 02:02:16"
|
||||
#define BUILD_TIME "2026-07-18 00:40:04"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "6364eac"
|
||||
#define GIT_HASH "1b10f89"
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
*
|
||||
* 架构(GD32 主动推送,ESP32 被动接收):
|
||||
* GD32 APP 在主循环中调用 canFeedbackPushStep(),循环遍历 3 通道 × 2 页,
|
||||
* 通过 requestEsp32Read(0xC1) 下拉 SPIready 主动按铃,将每页反馈 payload
|
||||
* 推送给 ESP32。ESP32 本任务调用 checkGd32Request() 阻塞等待按铃,读到
|
||||
* 一帧后用 canFdParseFeedbackPage() 解析并累积到 s_work[channel],再
|
||||
* publish() 到 s_latest[channel] 供 Web/WebSocket 读取。
|
||||
* 通过 requestEsp32Read(0xC1) 下拉 DATA_READY 主动按铃,将每页反馈 payload
|
||||
* 推送给 ESP32。ESP32 本任务 SpiMaster::takeEvent() 阻塞等待按铃/事件,
|
||||
* 读到一帧后用 canFdParseFeedbackPage() 解析并累积到 s_work[channel],再
|
||||
* publish() 到 s_latest[channel] 供 Web 读取。
|
||||
*
|
||||
* 替代了原先 ESP32 1Hz 主动发 0xC1 轮询的逻辑,避免占用主线 SPI 带宽。
|
||||
*
|
||||
@@ -161,9 +161,9 @@ void pauseCanFeedbackTask(bool pause)
|
||||
if (s_paused == pause) return; // 幂等:状态未变时不重复日志
|
||||
s_paused = pause;
|
||||
if (pause) {
|
||||
DEBUG_INFO(kTag, "CAN 监听已暂停(OTA 进行中)");
|
||||
DEBUG_DBG(kTag, "CAN 监听已暂停(OTA 进行中)");
|
||||
} else {
|
||||
DEBUG_INFO(kTag, "CAN 监听已恢复");
|
||||
DEBUG_DBG(kTag, "CAN 监听已恢复");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/*
|
||||
* can_feedback_task.h - 周期性轮询 GD32 CAN 反馈快照的任务接口
|
||||
/*
|
||||
* can_feedback_task.h - 被动接收 GD32 主动推送的 CAN 反馈快照
|
||||
*
|
||||
* ESP32 侧不主动 1Hz 轮询 0xC1;由 GD32 按铃推送,本任务 takeEvent 接收。
|
||||
*/
|
||||
#ifndef CAN_FEEDBACK_TASK_H
|
||||
#define CAN_FEEDBACK_TASK_H
|
||||
@@ -7,7 +9,7 @@
|
||||
#include <stdint.h>
|
||||
#include "transport/canfd_bridge.h"
|
||||
|
||||
/** @brief 暂停/恢复 CAN 反馈轮询(OTA 期间调用 pause=true)。 */
|
||||
/** @brief 暂停/恢复 CAN 反馈监听(OTA 期间调用 pause=true)。 */
|
||||
void pauseCanFeedbackTask(bool pause);
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -15,7 +17,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief 启动 1Hz CAN 反馈轮询任务(幂等)。
|
||||
* @brief 启动 CAN 反馈被动监听任务(幂等)。
|
||||
* @return true 表示任务已存在或创建成功。
|
||||
*/
|
||||
bool startCanFeedbackTask();
|
||||
|
||||
@@ -144,7 +144,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
}
|
||||
|
||||
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
|
||||
DEBUG_INFO(TAG, "当前启动分区: %s, size=%u, address=0x%lx",
|
||||
DEBUG_DBG(TAG, "当前启动分区: %s, size=%u, address=0x%lx",
|
||||
boot_partition->label, boot_partition->size, boot_partition->address);
|
||||
|
||||
// 选择非当前启动的 OTA 分区(OTA_0 → OTA_1 fallback)
|
||||
@@ -161,7 +161,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
return beginCleanupAndReturn(ESP_FAIL);
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "OTA 目标分区: %s, size=%u, address=0x%lx",
|
||||
DEBUG_DBG(TAG, "OTA 目标分区: %s, size=%u, address=0x%lx",
|
||||
m_partition->label, m_partition->size, m_partition->address);
|
||||
|
||||
if (total_size > m_partition->size) {
|
||||
@@ -191,7 +191,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
esp_err_to_name(err), (long long)((t1 - t0) / 1000));
|
||||
return beginCleanupAndReturn(err);
|
||||
}
|
||||
DEBUG_INFO(TAG, "esp_ota_begin(SEQUENTIAL) 完成 耗时 %lld ms free=%lu",
|
||||
DEBUG_DBG(TAG, "esp_ota_begin(SEQUENTIAL) 完成 耗时 %lld ms free=%lu",
|
||||
(long long)((t1 - t0) / 1000),
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
|
||||
@@ -201,7 +201,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
m_ended = false;
|
||||
memset(m_sha256, 0, sizeof(m_sha256));
|
||||
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节 (sequential erase-on-write)",
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节 (sequential erase-on-write)",
|
||||
m_partition->label, (unsigned)total_size);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -241,7 +241,7 @@ esp_err_t Esp32OtaCore::computeShaFromPartition()
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "从 OTA 分区读回计算 SHA-256 (%u 字节)...", (unsigned)m_received);
|
||||
DEBUG_DBG(TAG, "从 OTA 分区读回计算 SHA-256 (%u 字节)...", (unsigned)m_received);
|
||||
|
||||
/* static context + 小读缓冲:勿占 httpd 栈,也勿再静态吃 4KB BSS */
|
||||
static mbedtls_sha256_context s_sha_ctx;
|
||||
@@ -279,7 +279,7 @@ esp_err_t Esp32OtaCore::computeShaFromPartition()
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02x", m_sha256[i]);
|
||||
}
|
||||
DEBUG_INFO(TAG, "设备端 SHA-256: %s", hex);
|
||||
DEBUG_DBG(TAG, "设备端 SHA-256: %s", hex);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
|
||||
// ── 先读回分区计算 SHA-256,并与前端文件 SHA 比对 ──
|
||||
if (frontend_sha_hex != nullptr && strlen(frontend_sha_hex) == 64) {
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 校验 SHA-256...");
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 校验 SHA-256...");
|
||||
esp_err_t sha_err = computeShaFromPartition();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "SHA 计算失败: %s,中止", esp_err_to_name(sha_err));
|
||||
@@ -351,7 +351,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
}
|
||||
|
||||
if (sha_match) {
|
||||
DEBUG_INFO(TAG, "SHA-256 校验通过");
|
||||
DEBUG_DBG(TAG, "SHA-256 校验通过");
|
||||
} else {
|
||||
DEBUG_ERR(TAG, "SHA-256 不匹配: frontend=%s device=%s",
|
||||
frontend_sha_hex, device_hex);
|
||||
|
||||
@@ -182,7 +182,7 @@ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t ch
|
||||
m_sysState = OTA_SYS_GD32_STORING;
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING (epoch=%u, channel=%d)",
|
||||
DEBUG_DBG(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING (epoch=%u, channel=%d)",
|
||||
total_size, m_otaEpoch, (int)channel);
|
||||
}
|
||||
unlock(h);
|
||||
@@ -248,7 +248,7 @@ void OtaCoordinator::completeGd32Storage()
|
||||
m_gd32StorageDone = true;
|
||||
m_busySinceTick = 0; // ★ STORED 是非忙状态
|
||||
resume_ble = (m_otaChannel == OTA_CHANNEL_WIFI);
|
||||
DEBUG_INFO(TAG, "GD32 固件存储完成, 系统状态 → GD32_STORED");
|
||||
DEBUG_DBG(TAG, "GD32 固件存储完成, 系统状态 → GD32_STORED");
|
||||
} else if (h) {
|
||||
DEBUG_WARN(TAG, "completeGd32Storage 在非预期状态: %d", (int)m_sysState);
|
||||
}
|
||||
@@ -324,7 +324,7 @@ esp_err_t OtaCoordinator::triggerGd32Relay()
|
||||
|
||||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||||
m_busySinceTick = xTaskGetTickCount();
|
||||
DEBUG_INFO(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
|
||||
DEBUG_DBG(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
|
||||
unlock(h);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -337,7 +337,7 @@ void OtaCoordinator::notifyRelayStarted()
|
||||
if (m_sysState != OTA_SYS_GD32_RELAYING) {
|
||||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||||
m_busySinceTick = xTaskGetTickCount();
|
||||
DEBUG_INFO(TAG, "中继已开始(兜底), 系统状态 → GD32_RELAYING");
|
||||
DEBUG_DBG(TAG, "中继已开始(兜底), 系统状态 → GD32_RELAYING");
|
||||
}
|
||||
pauseCanFeedbackTask(true);
|
||||
}
|
||||
@@ -521,7 +521,7 @@ void OtaCoordinator::setEsp32StateLocked(ota_system_state_t s, const char *dbg_m
|
||||
} else if (s == OTA_SYS_ESP32_FINALIZING && m_busySinceTick == 0) {
|
||||
m_busySinceTick = xTaskGetTickCount();
|
||||
}
|
||||
DEBUG_INFO(TAG, "%s", dbg_msg);
|
||||
DEBUG_DBG(TAG, "%s", dbg_msg);
|
||||
}
|
||||
|
||||
/** @brief 通知协调器:ESP32 固件接收完毕,状态转为 FINALIZING。 */
|
||||
|
||||
+20
-1797
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,701 @@
|
||||
/*
|
||||
* ble_ota_host.cpp - BLE OTA GATT/GAP/广播/NimBLE Host
|
||||
*/
|
||||
#include "ble_ota_priv.h"
|
||||
|
||||
uint16_t s_data_out_val_handle = 0;
|
||||
|
||||
int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
(void)conn_handle;
|
||||
(void)attr_handle;
|
||||
(void)arg;
|
||||
|
||||
if (BleOta::instance().isSuspendedForWifiOta()) {
|
||||
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
uint8_t buf[36] = {0};
|
||||
int len = ble_ota_mbuf_to_buf(ctxt->om, buf, sizeof(buf));
|
||||
if (len < 1) {
|
||||
DEBUG_WARN(TAG, "CTRL 数据为空");
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "CTRL 写入: cmd=0x%02X len=%d", buf[0], len);
|
||||
|
||||
if (buf[0] == BLE_OTA_CTRL_TRIGGER_GD32) {
|
||||
uint16_t session_id = 0;
|
||||
if (len >= 35) {
|
||||
session_id = (uint16_t)(buf[33] | (buf[34] << 8));
|
||||
} else if (len >= 3) {
|
||||
session_id = (uint16_t)(buf[1] | (buf[2] << 8));
|
||||
}
|
||||
|
||||
if (len < 35) {
|
||||
DEBUG_WARN(TAG, "CTRL 0xF7 数据长度不足 35(前端 SHA-256 必传),拒绝触发 GD32 中继");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH,
|
||||
packBleFinishValue(session_id, 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 检查固件是否存储完毕 → 通过协调器触发 GD32 OTA 中继
|
||||
ota_meta_t meta = {};
|
||||
esp_err_t err = OtaCoordinator::instance().loadGd32Meta(&meta);
|
||||
if (err == ESP_OK &&
|
||||
meta.total_size > 0 &&
|
||||
meta.stored_size >= meta.total_size) {
|
||||
const uint8_t *frontend_sha = &buf[1];
|
||||
char frontend_sha_hex[65] = {0};
|
||||
char device_sha_hex[65] = {0};
|
||||
shaBytesToHexUpper(frontend_sha, frontend_sha_hex);
|
||||
|
||||
// ★ 统一走共享 helper:计算设备端 SHA → 比对前端 SHA
|
||||
uint8_t device_sha[32] = {0};
|
||||
err = OtaCoordinator::instance().verifyGd32Firmware(frontend_sha, device_sha);
|
||||
shaBytesToHexUpper(device_sha, device_sha_hex);
|
||||
sendSha256Notify(device_sha, session_id);
|
||||
DEBUG_DBG(TAG, "GD32 前端 SHA-256: %s, session=%u", frontend_sha_hex, session_id);
|
||||
DEBUG_DBG(TAG, "GD32 缓存 SHA-256: %s", device_sha_hex);
|
||||
|
||||
if (err == ESP_ERR_INVALID_CRC) {
|
||||
DEBUG_ERR(TAG, "GD32 缓存 SHA-256 与前端文件不匹配,拒绝触发 SPI 中继");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH,
|
||||
packBleFinishValue(session_id, 0));
|
||||
return 0;
|
||||
} else if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 SHA-256 计算/读取失败: %s", esp_err_to_name(err));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 3));
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "GD32 固件就绪且 SHA-256 匹配 (%" PRIu32 " / %" PRIu32 " 字节),通过协调器触发中继",
|
||||
meta.stored_size, meta.total_size);
|
||||
if (OtaRelay::getResultQueue()) {
|
||||
uint32_t stale = 0;
|
||||
while (xQueueReceive(OtaRelay::getResultQueue(), &stale, 0) == pdTRUE) {
|
||||
DEBUG_WARN(TAG, "清理旧 GD32 中继结果: %" PRIu32, stale);
|
||||
}
|
||||
}
|
||||
esp_err_t trig_err = OtaCoordinator::instance().triggerGd32Relay();
|
||||
if (trig_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 中继触发失败: %s", esp_err_to_name(trig_err));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_START,
|
||||
packBleFinishValue(session_id, 0));
|
||||
DEBUG_INFO(TAG, "已通知前端: GD32 中继已开始 (stage=0x10, session=%u)", session_id);
|
||||
|
||||
BleGd32RelayWaitCtx *ctx = (BleGd32RelayWaitCtx *)malloc(sizeof(BleGd32RelayWaitCtx));
|
||||
if (!ctx) {
|
||||
DEBUG_ERR(TAG, "GD32 中继等待任务上下文分配失败");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 2));
|
||||
return 0;
|
||||
}
|
||||
ctx->session_id = session_id;
|
||||
BaseType_t ok = xTaskCreate(ble_gd32_relay_wait_task,
|
||||
"ble_gd32_wait",
|
||||
TASK_STACK_BLE_GD32_WAIT,
|
||||
ctx,
|
||||
5,
|
||||
NULL);
|
||||
if (ok != pdPASS) {
|
||||
DEBUG_ERR(TAG, "创建 GD32 中继等待任务失败");
|
||||
free(ctx);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 2));
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "GD32 固件未就绪 (stored=%" PRIu32 ", total=%" PRIu32 ")",
|
||||
meta.stored_size, meta.total_size);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 1));
|
||||
}
|
||||
} else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) {
|
||||
uint16_t session_id = 0;
|
||||
if (len >= 35) {
|
||||
session_id = (uint16_t)(buf[33] | (buf[34] << 8));
|
||||
}
|
||||
if (!Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, 1));
|
||||
return 0;
|
||||
}
|
||||
if (BleOta::instance().m_esp32RebootPending) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 收尾已在进行中,忽略重复 CTRL");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 强制要求 32B 前端 SHA-256:与 WiFi 流程对齐,设备端读回分区计算并比对,
|
||||
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH。
|
||||
if (len < 33) {
|
||||
DEBUG_WARN(TAG, "CTRL 0xF8 数据长度不足 33(前端 SHA-256 必传),拒绝触发");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)ESP_ERR_INVALID_ARG);
|
||||
return 0;
|
||||
}
|
||||
|
||||
BleEsp32FinishCtx *finish_ctx = (BleEsp32FinishCtx *)malloc(sizeof(BleEsp32FinishCtx));
|
||||
if (!finish_ctx) {
|
||||
DEBUG_ERR(TAG, "CTRL 0xF8 申请内存失败");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, (uint16_t)ESP_ERR_NO_MEM));
|
||||
return 0;
|
||||
}
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
snprintf(&finish_ctx->sha_hex[i * 2], 3, "%02x", buf[1 + i]);
|
||||
}
|
||||
finish_ctx->sha_hex[64] = '\0';
|
||||
finish_ctx->session_id = session_id;
|
||||
DEBUG_DBG(TAG, "前端 SHA-256: %s, session=%u", finish_ctx->sha_hex, session_id);
|
||||
|
||||
BleOta::instance().m_esp32RebootPending = true;
|
||||
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
|
||||
"ble_ota_finish",
|
||||
TASK_STACK_BLE_FINISH,
|
||||
finish_ctx, // heap context; task 负责 free
|
||||
5,
|
||||
NULL);
|
||||
if (ok != pdPASS) {
|
||||
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
|
||||
free(finish_ctx);
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, (uint16_t)ESP_ERR_NO_MEM));
|
||||
} else {
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 收尾任务已创建(前端 SHA 已传入)");
|
||||
}
|
||||
} else if (buf[0] == BLE_OTA_CTRL_RESTART_ESP32) {
|
||||
// 重启 ESP32:GATT 回调中仅推送 FINISH,延时 + 重启交给独立 task,避免卡 NimBLE host
|
||||
DEBUG_INFO(TAG, "收到 0xFA 重启请求,创建 restart task(500ms 后 esp_restart())");
|
||||
if (BleOta::instance().m_esp32RebootPending) {
|
||||
DEBUG_WARN(TAG, "0xFA 重启已在进行中,忽略重复 CTRL");
|
||||
return 0;
|
||||
}
|
||||
BleOta::instance().m_esp32RebootPending = true;
|
||||
BaseType_t ok = xTaskCreate(ble_esp32_restart_task,
|
||||
"ble_ota_restart",
|
||||
TASK_STACK_BLE_RESTART,
|
||||
NULL,
|
||||
5,
|
||||
NULL);
|
||||
if (ok != pdPASS) {
|
||||
DEBUG_ERR(TAG, "创建 0xFA 重启任务失败");
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, 0);
|
||||
}
|
||||
} else if (buf[0] == BLE_OTA_CTRL_ABORT_OTA) {
|
||||
// 0xFB: 中止当前 OTA(保持 BLE 连接)
|
||||
// - 数据接收阶段(GD32_STORING / ESP32_RECEIVING):立即中止 + 清队列 + 回复 ABORTED
|
||||
// - 中继/收尾阶段(GD32_RELAYING / ESP32_FINALIZING):拒绝中止(SPI 转发不可中断)
|
||||
// - IDLE/已完成状态:幂等回复 ABORTED
|
||||
uint16_t session_id = 0;
|
||||
if (len >= 3) {
|
||||
session_id = (uint16_t)(buf[1] | (buf[2] << 8));
|
||||
}
|
||||
|
||||
ota_system_state_t cur_state = OtaCoordinator::instance().getSystemState();
|
||||
DEBUG_WARN(TAG, "收到 0xFB 中止请求: state=%s session=%u",
|
||||
OtaCoordinator::stateToString(cur_state), session_id);
|
||||
|
||||
if (cur_state == OTA_SYS_GD32_RELAYING) {
|
||||
DEBUG_WARN(TAG, "GD32 SPI 中继进行中,无法中止(请等待完成或断开 BLE)");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 2));
|
||||
return 0;
|
||||
}
|
||||
if (cur_state == OTA_SYS_ESP32_FINALIZING) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 收尾进行中,无法中止");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── 可中止状态:清队列 + abort core + 复位协调器 ──
|
||||
if (s_otaDataQueue) {
|
||||
BleOtaDataBatch stale;
|
||||
size_t dropped = 0;
|
||||
while (xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) {
|
||||
free(stale.data);
|
||||
dropped++;
|
||||
}
|
||||
if (dropped > 0) {
|
||||
DEBUG_INFO(TAG, "中止时清理 OTA 数据队列: %zu 项", dropped);
|
||||
}
|
||||
}
|
||||
|
||||
// abortCurrent 内部按状态分发:GD32_STORING→metaClear, ESP32_RECEIVING→core.abort, IDLE→空操作
|
||||
// ble_ota_session_reset() 统一负责 abortCurrent + core.abort 兜底 + 会话状态复位
|
||||
// (m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending/reorder/s_gd32_ooo_count)
|
||||
ble_ota_session_reset();
|
||||
|
||||
ble_ota_restore_wifi_ps();
|
||||
|
||||
DEBUG_INFO(TAG, "OTA 已中止(BLE 保持连接),通知前端 (stage=0x20)");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ABORTED,
|
||||
packBleFinishValue(session_id, 0));
|
||||
}
|
||||
// 0xF7 = 触发 GD32 中继
|
||||
// 0xF8 = 触发 ESP32 本机 OTA 收尾:设备端读 OTA 分区算 SHA 并与前端 32B SHA 比对,
|
||||
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH(不重启)
|
||||
// 0xFA = 重启 ESP32(配套前端 expectedReboot 自动重连)
|
||||
// 0xFB = 中止当前 OTA(不断开 BLE):仅数据接收阶段有效,中继/收尾阶段拒绝
|
||||
|
||||
return 0; // BLE_ATT_ERR_OK 在 NimBLE 中就是 0
|
||||
}
|
||||
|
||||
/*
|
||||
* DATA_IN (0x0002) - Write
|
||||
* 数据块格式:
|
||||
* [0] cmd (0x00=数据)
|
||||
* [1-2] seq uint16 LE - 分块序号
|
||||
* [3-6] total_size uint32 LE - 固件总大小 (仅 seq=0 时有效)
|
||||
* [7..n] payload 固件数据
|
||||
* 注: 已移除CRC字段(BLE链路层已有CRC校验)
|
||||
*/
|
||||
/**
|
||||
* @brief GATT DATA_IN 特征访问回调,接收并处理 OTA 数据块。
|
||||
* @param conn_handle BLE 连接句柄。
|
||||
* @param attr_handle GATT 属性句柄。
|
||||
* @param ctxt GATT 访问上下文。
|
||||
* @param arg 回调用户参数。
|
||||
* @return NimBLE ATT 错误码,0 表示成功。
|
||||
*/
|
||||
|
||||
int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
(void)conn_handle;
|
||||
(void)attr_handle;
|
||||
(void)arg;
|
||||
|
||||
if (ctxt->op != BLE_GATT_ACCESS_OP_READ_CHR) {
|
||||
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
const char *version = FW_VERSION;
|
||||
// 版本字符串: "vX.Y.Z YYYY-MM-DD HH:MM:SS"
|
||||
char ver_buf[64];
|
||||
int ver_len = snprintf(ver_buf, sizeof(ver_buf), "%s %s", FW_VERSION, FW_BUILD_TIME);
|
||||
if (ver_len < 0 || ver_len >= (int)sizeof(ver_buf)) {
|
||||
ver_len = strlen(version);
|
||||
int rc = os_mbuf_append(ctxt->om, version, ver_len);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
int rc = os_mbuf_append(ctxt->om, ver_buf, ver_len);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/*
|
||||
* DATA_OUT (0x0003) - Notify only
|
||||
* 访问回调不做读写处理,通知由 BleOta::instance().notifyResponse() 发送
|
||||
*/
|
||||
/**
|
||||
* @brief GATT DATA_OUT 特征访问回调(Notify-only,占位实现)。
|
||||
* @param conn_handle BLE 连接句柄。
|
||||
* @param attr_handle GATT 属性句柄。
|
||||
* @param ctxt GATT 访问上下文。
|
||||
* @param arg 回调用户参数。
|
||||
* @return 0。
|
||||
*/
|
||||
int ble_ota_data_out_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
(void)conn_handle;
|
||||
(void)attr_handle;
|
||||
(void)ctxt;
|
||||
(void)arg;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GATT 服务定义
|
||||
// ============================================================
|
||||
|
||||
// BLE_UUID16_DECLARE 在 C++ 中产生 "taking address of rvalue" 编译错误。
|
||||
// 改用 static ble_uuid16_t 变量,再取 .u 地址。
|
||||
/** @brief OTA GATT 服务 UUID */
|
||||
const ble_uuid16_t GATT_SVC_UUID = BLE_UUID16_INIT(BLE_OTA_SERVICE_UUID);
|
||||
/** @brief CTRL 特征 UUID,写入 0xF7 触发中继 */
|
||||
const ble_uuid16_t GATT_CTRL_UUID = BLE_UUID16_INIT(BLE_OTA_CTRL_UUID);
|
||||
/** @brief DATA_IN 特征 UUID,接收固件数据块 */
|
||||
const ble_uuid16_t GATT_DATA_IN_UUID = BLE_UUID16_INIT(BLE_OTA_DATA_IN_UUID);
|
||||
/** @brief DATA_OUT 特征 UUID,Notify 推送 GD32 响应 */
|
||||
const ble_uuid16_t GATT_DATA_OUT_UUID = BLE_UUID16_INIT(BLE_OTA_DATA_OUT_UUID);
|
||||
/** @brief VERSION 特征 UUID,返回固件版本字符串 */
|
||||
const ble_uuid16_t GATT_VERSION_UUID = BLE_UUID16_INIT(BLE_OTA_VERSION_UUID);
|
||||
|
||||
// NimBLE C 结构体在 C++ designated initializer 不能显式初始化全部字段。
|
||||
// 用 pragma 局部关闭 -Wmissing-field-initializers 警告。
|
||||
#ifdef __cplusplus
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
|
||||
#endif
|
||||
|
||||
const struct ble_gatt_chr_def gatt_ota_chrs[] = {
|
||||
{
|
||||
.uuid = &GATT_CTRL_UUID.u,
|
||||
.access_cb = ble_ota_ctrl_access,
|
||||
.flags = BLE_GATT_CHR_F_WRITE,
|
||||
},
|
||||
{
|
||||
.uuid = &GATT_DATA_IN_UUID.u,
|
||||
.access_cb = ble_ota_data_in_access,
|
||||
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
|
||||
},
|
||||
{
|
||||
.uuid = &GATT_DATA_OUT_UUID.u,
|
||||
.access_cb = ble_ota_data_out_access,
|
||||
.flags = BLE_GATT_CHR_F_NOTIFY,
|
||||
.val_handle = &s_data_out_val_handle,
|
||||
},
|
||||
{
|
||||
.uuid = &GATT_VERSION_UUID.u,
|
||||
.access_cb = ble_ota_version_access,
|
||||
.flags = BLE_GATT_CHR_F_READ,
|
||||
},
|
||||
{} // 终止符
|
||||
};
|
||||
|
||||
const struct ble_gatt_svc_def gatt_ota_svcs[] = {
|
||||
{
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &GATT_SVC_UUID.u,
|
||||
.characteristics = (struct ble_gatt_chr_def *)gatt_ota_chrs,
|
||||
},
|
||||
{} // 终止符
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
// ============================================================
|
||||
// GAP 事件回调
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief BLE GAP 事件回调,处理连接和断开事件。
|
||||
* @param event
|
||||
* @param arg
|
||||
* @return 0 成功。
|
||||
*/
|
||||
int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
switch (event->type) {
|
||||
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
if (event->connect.status == 0) {
|
||||
if (BleOta::instance().isSuspendedForWifiOta()) {
|
||||
/* WiFi OTA 中拒绝 BLE 连接,立刻踢掉 */
|
||||
DEBUG_WARN(TAG, "WiFi OTA 中,拒绝 BLE 连接 handle=%d",
|
||||
event->connect.conn_handle);
|
||||
(void)ble_gap_terminate(event->connect.conn_handle,
|
||||
BLE_ERR_REM_USER_CONN_TERM);
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
return 0;
|
||||
}
|
||||
// 连接成功
|
||||
BleOta::instance().m_bleConnected = true;
|
||||
BleOta::instance().m_connHandle = event->connect.conn_handle;
|
||||
s_conn_fallback_requested = false;
|
||||
DEBUG_INFO(TAG, "BLE 客户端已连接: handle=%d", BleOta::instance().m_connHandle);
|
||||
|
||||
ble_ota_log_conn_params(BleOta::instance().m_connHandle, "初始");
|
||||
int rc = ble_ota_request_conn_params(BleOta::instance().m_connHandle, false);
|
||||
if (rc != 0) {
|
||||
s_conn_fallback_requested = true;
|
||||
ble_ota_request_conn_params(BleOta::instance().m_connHandle, true);
|
||||
}
|
||||
/* 优先 LE 2M PHY(空中速率约翻倍);对端不支持则保持 1M,失败仅打日志 */
|
||||
{
|
||||
const uint8_t phy_mask = BLE_GAP_LE_PHY_1M_MASK | BLE_GAP_LE_PHY_2M_MASK;
|
||||
int phy_rc = ble_gap_set_prefered_le_phy(
|
||||
BleOta::instance().m_connHandle, phy_mask, phy_mask,
|
||||
BLE_GAP_LE_PHY_CODED_ANY);
|
||||
DEBUG_DBG(TAG, "请求 LE PHY 1M|2M: rc=%d", phy_rc);
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
DEBUG_INFO(TAG, "BLE 客户端已断开: reason=%d",
|
||||
event->disconnect.reason);
|
||||
|
||||
// ★ 断开时清空数据队列,丢弃属于旧会话的残留包
|
||||
if (s_otaDataQueue) {
|
||||
drainOtaDataQueue();
|
||||
}
|
||||
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
s_conn_fallback_requested = false;
|
||||
|
||||
/* WiFi OTA 软挂起期间:主动踢连触发的 disconnect 不得 abort WiFi OTA,也不得再开广播 */
|
||||
if (BleOta::instance().isSuspendedForWifiOta()) {
|
||||
DEBUG_INFO(TAG, "WiFi OTA 挂起中:忽略 BLE 断开对 OTA 状态的影响");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ★ BLE 断开时恢复 WiFi 射频
|
||||
ble_ota_restore_wifi_ps();
|
||||
|
||||
// ★ 断开时清理"BLE 数据接收阶段"残留的 OTA 会话状态,避免下次刷写被
|
||||
// 系统 busy 假拒绝。注意区分可中止 / 不可中止状态:
|
||||
// - GD32_STORING / ESP32_RECEIVING / ESP32_FINALIZING:BLE 是唯一数据源,
|
||||
// 断开即等价于中止,必须 abortCurrent() + 清理底层 core。
|
||||
// - GD32_RELAYING / ESP32_REBOOTING:SPI 中继/重启不依赖 BLE,不能打断。
|
||||
// 这些状态保留,等中继/重启自然结束。
|
||||
// - IDLE / STORED / COMPLETE / ERROR:abortCurrent 幂等安全,直接调即可。
|
||||
{
|
||||
ota_system_state_t cur = OtaCoordinator::instance().getSystemState();
|
||||
if (cur == OTA_SYS_GD32_RELAYING || cur == OTA_SYS_ESP32_REBOOTING) {
|
||||
DEBUG_WARN(TAG, "BLE 断开但 %s 进行中,保留状态等待自然结束",
|
||||
OtaCoordinator::stateToString(cur));
|
||||
} else if (cur == OTA_SYS_ESP32_RECEIVING ||
|
||||
cur == OTA_SYS_ESP32_FINALIZING ||
|
||||
cur == OTA_SYS_GD32_STORING) {
|
||||
/* 仅当当前 OTA 通道是 BLE 时才因断连中止;WiFi 通道的 RECEIVING 不能被误杀 */
|
||||
ota_channel_t ch = OTA_CHANNEL_WIFI;
|
||||
{
|
||||
/* get channel via snapshot if available */
|
||||
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
|
||||
ch = snap.channel;
|
||||
}
|
||||
if (ch == OTA_CHANNEL_BLE) {
|
||||
DEBUG_WARN(TAG, "BLE 断开,中止 BLE 通道 OTA (state=%s)",
|
||||
OtaCoordinator::stateToString(cur));
|
||||
ble_ota_session_reset();
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "BLE 断开但当前为 WiFi OTA,不中止会话");
|
||||
}
|
||||
} else {
|
||||
ble_ota_session_reset();
|
||||
}
|
||||
}
|
||||
// 恢复广播以接受新连接
|
||||
ble_ota_start_advertising();
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||
if (BleOta::instance().isSuspendedForWifiOta()) {
|
||||
return 0; /* WiFi OTA 中不要自动再开广播 */
|
||||
}
|
||||
DEBUG_INFO(TAG, "广播周期结束,重新开始");
|
||||
ble_ota_start_advertising();
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
DEBUG_INFO(TAG, "CCC 描述符更新: conn=%d attr=%d notify=%d indicate=%d",
|
||||
event->subscribe.conn_handle,
|
||||
event->subscribe.attr_handle,
|
||||
event->subscribe.cur_notify,
|
||||
event->subscribe.cur_indicate);
|
||||
if (event->subscribe.cur_notify) {
|
||||
ble_ota_log_conn_params(event->subscribe.conn_handle, "订阅时");
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_CONN_UPDATE: {
|
||||
DEBUG_DBG(TAG, "BLE 连接参数更新完成: status=%d", event->conn_update.status);
|
||||
uint16_t interval = ble_ota_log_conn_params(event->conn_update.conn_handle, "更新后");
|
||||
/* 回退阈值:高速分支目标是 itvl ≤ 8(≤10ms)。若协商失败或落在 > 8(>10ms),
|
||||
* 说明高速区间被对端拒绝/降级,尝试 Windows 兼容回退路径(10..12.5ms)。
|
||||
* 原阈值 > 16(>20ms)过宽松,收窄后让回退更早触发、避免停在 15ms 这种"慢但不算失败"的区间。 */
|
||||
if (!s_conn_fallback_requested &&
|
||||
(event->conn_update.status != 0 || interval == 0 || interval > 8)) {
|
||||
s_conn_fallback_requested = true;
|
||||
ble_ota_request_conn_params(event->conn_update.conn_handle, true);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case BLE_GAP_EVENT_MTU:
|
||||
DEBUG_INFO(TAG, "MTU 协商: conn=%d mtu=%d",
|
||||
event->mtu.conn_handle, event->mtu.value);
|
||||
return 0;
|
||||
|
||||
#ifdef BLE_GAP_EVENT_PHY_UPDATE_COMPLETE
|
||||
case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE: {
|
||||
/* NimBLE: event type 名 *_COMPLETE,union 字段 phy_updated(非 phy_update)。
|
||||
* 2M 生效才能翻倍空中速率;Chrome/Win 常仍停在 1M。 */
|
||||
const uint8_t rx = event->phy_updated.rx_phy;
|
||||
const uint8_t tx = event->phy_updated.tx_phy;
|
||||
const char *rx_phy = (rx == BLE_GAP_LE_PHY_2M) ? "2M" :
|
||||
(rx == BLE_GAP_LE_PHY_1M) ? "1M" :
|
||||
(rx == BLE_GAP_LE_PHY_CODED) ? "CODED" : "?";
|
||||
const char *tx_phy = (tx == BLE_GAP_LE_PHY_2M) ? "2M" :
|
||||
(tx == BLE_GAP_LE_PHY_1M) ? "1M" :
|
||||
(tx == BLE_GAP_LE_PHY_CODED) ? "CODED" : "?";
|
||||
DEBUG_INFO(TAG, "PHY 更新: status=%d rx=%s tx=%s",
|
||||
event->phy_updated.status, rx_phy, tx_phy);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BLE 广播
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 启动 BLE 广播,使设备可被发现和连接。
|
||||
*/
|
||||
void ble_ota_start_advertising(void) // kept as free function for BLE callback compatibility
|
||||
{
|
||||
if (BleOta::instance().isSuspendedForWifiOta()) {
|
||||
DEBUG_DBG(TAG, "WiFi OTA 挂起中,跳过广播");
|
||||
return;
|
||||
}
|
||||
|
||||
int rc;
|
||||
|
||||
// 自动推断可用地址类型(优先 Public,不可用则生成 Random)
|
||||
uint8_t own_addr_type;
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
DEBUG_ERR(TAG, "推断地址类型失败: rc=%d, 尝试生成随机地址", rc);
|
||||
// 生成不可解析随机地址作为 fallback
|
||||
ble_addr_t rnd_addr;
|
||||
rc = ble_hs_id_gen_rnd(0, &rnd_addr);
|
||||
if (rc != 0) {
|
||||
DEBUG_ERR(TAG, "生成随机地址也失败: rc=%d, BLE 广播无法启动", rc);
|
||||
return;
|
||||
}
|
||||
own_addr_type = rnd_addr.type;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "使用地址类型: %s", own_addr_type == BLE_OWN_ADDR_PUBLIC ? "Public" : "Random");
|
||||
|
||||
// --- 构建广播数据 ---
|
||||
uint8_t adv_data[31] = {0};
|
||||
int adv_pos = 0;
|
||||
|
||||
// AD Flags: LE General Discoverable + BR/EDR Not Supported
|
||||
adv_data[adv_pos++] = 0x02; // 长度
|
||||
adv_data[adv_pos++] = 0x01; // 类型 = Flags
|
||||
adv_data[adv_pos++] = 0x06; // 值
|
||||
|
||||
// Complete Local Name
|
||||
const char *name = BLE_DEVICE_NAME;
|
||||
size_t name_len = strlen(name);
|
||||
if (name_len > 29 - adv_pos - 2) {
|
||||
name_len = 29 - adv_pos - 2; // 截断以适配广播包
|
||||
}
|
||||
adv_data[adv_pos++] = (uint8_t)(name_len + 1);
|
||||
adv_data[adv_pos++] = 0x09; // 类型 = Complete Local Name
|
||||
memcpy(&adv_data[adv_pos], name, name_len);
|
||||
adv_pos += name_len;
|
||||
|
||||
rc = ble_gap_adv_set_data(adv_data, adv_pos);
|
||||
if (rc != 0) {
|
||||
DEBUG_ERR(TAG, "设置广播数据失败: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
// 扫描响应: 包含 Service UUID 以帮助发现
|
||||
uint8_t scan_rsp[31] = {0};
|
||||
int rsp_pos = 0;
|
||||
scan_rsp[rsp_pos++] = 0x11; // 长度 (2 + 15)
|
||||
scan_rsp[rsp_pos++] = 0x09; // 类型 = Complete Local Name
|
||||
name_len = strlen(name);
|
||||
if (name_len > (size_t)(31 - rsp_pos)) {
|
||||
name_len = 31 - rsp_pos;
|
||||
}
|
||||
memcpy(&scan_rsp[rsp_pos], name, name_len);
|
||||
rsp_pos += name_len;
|
||||
|
||||
rc = ble_gap_adv_rsp_set_data(scan_rsp, rsp_pos);
|
||||
if (rc != 0) {
|
||||
DEBUG_WARN(TAG, "设置扫描响应数据失败: %d", rc);
|
||||
}
|
||||
|
||||
// 开始广播
|
||||
struct ble_gap_adv_params adv_params = {};
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
// own_addr_type 已在上面由 ble_hs_id_infer_auto 推断
|
||||
|
||||
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
|
||||
&adv_params, ble_ota_gap_event, NULL);
|
||||
if (rc != 0) {
|
||||
DEBUG_ERR(TAG, "启动广播失败: %d", rc);
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "BLE 广播已启动: \"%s\"", name);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NimBLE 同步/重置回调
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief NimBLE 协议栈同步完成回调,初始化广播。
|
||||
*/
|
||||
void ble_ota_on_sync(void)
|
||||
{
|
||||
DEBUG_INFO(TAG, "NimBLE 协议栈同步完成");
|
||||
|
||||
// GATT 服务已在 serverStart() 中注册,不再重复注册
|
||||
// 重复调用 ble_gatts_count_cfg / ble_gatts_add_svcs 会导致 BLE_HS_EBUSY
|
||||
|
||||
DEBUG_INFO(TAG, "DATA_OUT 值句柄: %u", s_data_out_val_handle);
|
||||
if (s_data_out_val_handle == 0) {
|
||||
DEBUG_WARN(TAG, "DATA_OUT 值句柄为 0,通知功能不可用");
|
||||
}
|
||||
|
||||
BleOta::instance().m_bleReady = true;
|
||||
ble_ota_start_advertising();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief NimBLE 协议栈重置回调。
|
||||
* @param reason
|
||||
*/
|
||||
void ble_ota_on_reset(int reason)
|
||||
{
|
||||
DEBUG_WARN(TAG, "NimBLE 协议栈重置: reason=%d", reason);
|
||||
BleOta::instance().m_bleReady = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NimBLE Host 任务(独立 FreeRTOS 任务)
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief NimBLE 主机任务入口,处理协议栈事件循环。
|
||||
* @param param
|
||||
*/
|
||||
void ble_ota_nimble_host_task(void *param)
|
||||
{
|
||||
(void)param;
|
||||
DEBUG_INFO(TAG, "NimBLE Host 任务启动");
|
||||
nimble_port_run();
|
||||
// 正常情况下不会到达这里
|
||||
DEBUG_INFO(TAG, "NimBLE Host 任务退出");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 公开 API
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 启动 BLE GATT 服务器并注册 OTA 服务。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* ble_ota_priv.h - BLE OTA 内部共享声明(仅 server/ble_ota*.cpp 使用)
|
||||
*
|
||||
* 拆分:
|
||||
* ble_ota.cpp — 单例/会话/收尾/公开 API
|
||||
* ble_ota_data.cpp — 乱序缓冲、数据写路径、worker
|
||||
* ble_ota_host.cpp — GATT/GAP/广播/Host
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_wifi.h"
|
||||
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_gap.h"
|
||||
#include "nimble/ble.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
#include "services/gatt/ble_svc_gatt.h"
|
||||
#include "host/ble_att.h"
|
||||
|
||||
#include "app_config.h"
|
||||
#include "version.h"
|
||||
#include "ble_ota.h"
|
||||
#include "esp32_ota_core.h"
|
||||
#include "ota_coordinator.h"
|
||||
#include "ota_relay.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "health_monitor.h"
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
#define TAG "BLE_OTA"
|
||||
#define BLE_OTA_NOTIFY_ACK 0xA1
|
||||
#define BLE_OTA_NOTIFY_FINISH 0xB1
|
||||
#define BLE_OTA_NOTIFY_SHA256 0xC1
|
||||
#define BLE_OTA_FINISH_DONE 0x02
|
||||
#define BLE_OTA_FINISH_REBOOT_NOW 0x07
|
||||
#define BLE_OTA_FINISH_GD32_RELAY_START 0x10
|
||||
#define BLE_OTA_FINISH_GD32_RELAY_DONE 0x11
|
||||
#define BLE_OTA_FINISH_ABORTED 0x20
|
||||
#define BLE_ATT_ERR_OTA_BUSY 0x80
|
||||
#define BLE_OTA_FINISH_ERR_FINALIZE 0x81
|
||||
#define BLE_OTA_FINISH_ERR_MUTEX 0x83
|
||||
#define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84
|
||||
#define BLE_OTA_FINISH_ERR_SHA_MISMATCH 0x85
|
||||
#define BLE_OTA_FINISH_ERR_GD32_RELAY 0x86
|
||||
|
||||
struct BleOtaDataBatch {
|
||||
uint8_t *data;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
struct BleGd32RelayWaitCtx {
|
||||
uint16_t session_id;
|
||||
};
|
||||
|
||||
struct BleEsp32FinishCtx {
|
||||
char sha_hex[65];
|
||||
uint16_t session_id;
|
||||
};
|
||||
|
||||
struct BleFinishCtxDeleter {
|
||||
BleEsp32FinishCtx *p;
|
||||
explicit BleFinishCtxDeleter(BleEsp32FinishCtx *ctx) : p(ctx) {}
|
||||
~BleFinishCtxDeleter() { if (p) { free(p); p = nullptr; } }
|
||||
BleFinishCtxDeleter(const BleFinishCtxDeleter&) = delete;
|
||||
BleFinishCtxDeleter& operator=(const BleFinishCtxDeleter&) = delete;
|
||||
};
|
||||
|
||||
extern QueueHandle_t s_otaDataQueue;
|
||||
extern uint16_t s_data_out_val_handle;
|
||||
extern bool s_wifi_ps_boosted;
|
||||
/** GAP 连接参数回退请求标志(定义在 ble_ota_data.cpp,host 连接事件读写) */
|
||||
extern bool s_conn_fallback_requested;
|
||||
|
||||
void drainOtaDataQueue();
|
||||
void ble_ota_start_advertising(void);
|
||||
void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes);
|
||||
void sendFinishEvent(uint8_t stage, uint32_t value);
|
||||
uint32_t packBleFinishValue(uint16_t session_id, uint16_t code);
|
||||
void shaBytesToHexUpper(const uint8_t sha[32], char out_hex[65]);
|
||||
void sendSha256Notify(const uint8_t sha[32], uint16_t session_id);
|
||||
void esp32_reorder_free();
|
||||
void ble_ota_session_reset(void);
|
||||
void ble_ota_restore_wifi_ps(void);
|
||||
void ble_ota_boost_wifi_ps(void);
|
||||
void ble_ota_data_worker_task(void *pvParameters);
|
||||
void ble_gd32_relay_wait_task(void *pvParameters);
|
||||
int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len);
|
||||
void ble_ota_on_sync(void);
|
||||
void ble_ota_on_reset(int reason);
|
||||
void ble_ota_nimble_host_task(void *param);
|
||||
int ble_ota_gap_event(struct ble_gap_event *event, void *arg);
|
||||
uint16_t ble_ota_log_conn_params(uint16_t conn_handle, const char *phase);
|
||||
int ble_ota_request_conn_params(uint16_t conn_handle, bool windows_compatible);
|
||||
void ble_esp32_restart_task(void *pvParameters);
|
||||
void ble_esp32_finish_and_reboot_task(void *pvParameters);
|
||||
int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
int ble_ota_data_out_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
|
||||
extern const struct ble_gatt_svc_def gatt_ota_svcs[];
|
||||
@@ -630,7 +630,7 @@ esp_err_t logs_handler(httpd_req_t *req)
|
||||
|
||||
|
||||
// ============================================================
|
||||
// GET /can/feedback — 三路 CAN 1Hz 反馈快照
|
||||
// GET /can/feedback — 三路 CAN 反馈快照(GD32 推送,ESP32 缓存)
|
||||
// ============================================================
|
||||
esp_err_t can_feedback_handler(httpd_req_t *req)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ esp_err_t spi_send_handler(httpd_req_t *req);
|
||||
esp_err_t system_status_handler(httpd_req_t *req);
|
||||
esp_err_t system_probe_handler(httpd_req_t *req);
|
||||
|
||||
// CAN 1Hz 反馈快照与接收模式
|
||||
// CAN 反馈快照(被动 0xC1)与接收模式
|
||||
esp_err_t can_feedback_handler(httpd_req_t *req);
|
||||
esp_err_t can_mode_handler(httpd_req_t *req);
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ typedef struct {
|
||||
} id_snapshot_entry_t;
|
||||
|
||||
// ============================================================
|
||||
// CAN 接收过滤模式 & 1Hz 反馈快照
|
||||
// CAN 接收过滤模式 & 反馈快照(GD32 约 1Hz 刷新;ESP32 被动收 0xC1)
|
||||
// 与 GD32 DCU_APP SPI 协议帧 0xC1/0xC2 配套
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
@@ -87,7 +87,7 @@ typedef enum {
|
||||
} can_rx_mode_t;
|
||||
|
||||
/**
|
||||
* @brief 单通道 1Hz 反馈统计。
|
||||
* @brief 单通道 CAN 反馈统计(快照字段;非 ESP32 侧 1Hz 轮询)。
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t channel;
|
||||
|
||||
@@ -409,9 +409,13 @@ void SpiMaster::workerLoop()
|
||||
/**
|
||||
* @brief GPIO 边沿中断回调(DATA_READY / PB6)。
|
||||
*
|
||||
* 仅处理下降沿:GD32 有待读数据(按铃)。若本端 IDLE 则置
|
||||
* m_gd32RequestPending 并唤醒 SPI worker。上升沿不再使用(旧 waitSpiReady 已删)。
|
||||
* 仅处理下降沿:GD32 有待读数据(按铃)。上升沿不再使用(旧的"等上升沿
|
||||
* ready"逻辑已删除,现由 waitSpiDataReady 做电平轮询)。
|
||||
* ISR 内仅做标志翻转 + 任务通知,不做 SPI 传输。
|
||||
*
|
||||
* 本端忙时(acceptRing()==false):记下 pending 但不 notify,避免抢断
|
||||
* 当前 SPI 事务;事务结束 worker 下一轮会立即消费此标志,省去最多 25ms
|
||||
* 的回扫延迟。PB6 在被读走前保持低、不会产生新下降沿,不记 pending 即丢唤醒。
|
||||
*/
|
||||
void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg)
|
||||
{
|
||||
@@ -426,6 +430,10 @@ void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg)
|
||||
return;
|
||||
}
|
||||
if (!self->m_linkState.acceptRing()) {
|
||||
/* 本端忙:记下 pending,不 notify 抢断当前事务;
|
||||
* 事务结束 worker 下一轮会消费此标志(PB6 保持低、无新下降沿,
|
||||
* 不记则只能等 25ms 回扫兜底)。 */
|
||||
self->m_gd32RequestPending = true;
|
||||
return;
|
||||
}
|
||||
self->m_gd32RequestPending = true;
|
||||
|
||||
Reference in New Issue
Block a user