diff --git a/cmake/gen_version.cmake b/cmake/gen_version.cmake deleted file mode 100644 index 3068ff0..0000000 --- a/cmake/gen_version.cmake +++ /dev/null @@ -1,54 +0,0 @@ -# gen_version.cmake - 自动生成版本信息头文件 -# 包含版本号、编译时间、Git 信息 - -# 版本号(可手动修改) -set(FW_VERSION_MAJOR 0) -set(FW_VERSION_MINOR 3) -set(FW_VERSION_PATCH 10) - -# 获取编译时间 -string(TIMESTAMP BUILD_TIME "%Y-%m-%d %H:%M:%S") - -# 获取 Git 信息(如果可用) -find_package(Git QUIET) -if(GIT_FOUND) - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET - ) -else() - set(GIT_HASH "unknown") -endif() - -# 生成版本头文件 -set(VERSION_HEADER_CONTENT -"// Auto-generated version info - DO NOT EDIT -#pragma once - -// 固件版本 -#define FW_VERSION_MAJOR ${FW_VERSION_MAJOR} -#define FW_VERSION_MINOR ${FW_VERSION_MINOR} -#define FW_VERSION_PATCH ${FW_VERSION_PATCH} -#define FW_VERSION_STRING \"${FW_VERSION_MAJOR}.${FW_VERSION_MINOR}.${FW_VERSION_PATCH}\" - -// 编译时间 -#define BUILD_TIME \"${BUILD_TIME}\" - -// Git 信息 -#define GIT_HASH \"${GIT_HASH}\" -") - -# 检查文件是否需要更新 -if(EXISTS ${VERSION_HEADER_PATH}) - file(READ ${VERSION_HEADER_PATH} EXISTING_CONTENT) - if(NOT "${EXISTING_CONTENT}" STREQUAL "${VERSION_HEADER_CONTENT}") - file(WRITE ${VERSION_HEADER_PATH} "${VERSION_HEADER_CONTENT}") - message(STATUS "Version header updated: ${VERSION_HEADER_PATH}") - endif() -else() - file(WRITE ${VERSION_HEADER_PATH} "${VERSION_HEADER_CONTENT}") - message(STATUS "Version header generated: ${VERSION_HEADER_PATH}") -endif() diff --git a/cmake/gen_version_script.cmake b/cmake/gen_version_script.cmake index e27ecfd..705bc34 100644 --- a/cmake/gen_version_script.cmake +++ b/cmake/gen_version_script.cmake @@ -8,7 +8,7 @@ # 版本号(可手动修改) set(FW_VERSION_MAJOR 0) set(FW_VERSION_MINOR 3) -set(FW_VERSION_PATCH 11) +set(FW_VERSION_PATCH 12) # 获取编译时间 string(TIMESTAMP BUILD_TIME "%Y-%m-%d %H:%M:%S") diff --git a/main/include/version.h b/main/include/version.h index 13fecce..b0d545d 100644 --- a/main/include/version.h +++ b/main/include/version.h @@ -4,11 +4,11 @@ // 固件版本 #define FW_VERSION_MAJOR 0 #define FW_VERSION_MINOR 3 -#define FW_VERSION_PATCH 11 -#define FW_VERSION_STRING "0.3.11" +#define FW_VERSION_PATCH 12 +#define FW_VERSION_STRING "0.3.12" // 编译时间 -#define BUILD_TIME "2026-06-03 03:00:47" +#define BUILD_TIME "2026-06-13 01:22:52" // Git 信息 -#define GIT_HASH "a59364e" +#define GIT_HASH "d8e4222" diff --git a/main/server/ble_ota.cpp b/main/server/ble_ota.cpp index 8cc4962..bd0a9fe 100644 --- a/main/server/ble_ota.cpp +++ b/main/server/ble_ota.cpp @@ -52,20 +52,15 @@ #define BLE_OTA_FINISH_REBOOT_NOW 0x07 #define BLE_OTA_FINISH_ERR_OTA_END 0x81 #define BLE_OTA_FINISH_ERR_SET_BOOT 0x82 -#define BLE_OTA_ACK_WINDOW 8 +#define BLE_OTA_ACK_WINDOW 32 // ============================================================ -// 前向声明(GAP event handler 在广播函数之前定义,需要声明) +// 前向声明 // ============================================================ static void ble_ota_start_advertising(void); static void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes); static void sendFinishEvent(uint8_t stage, uint32_t value); -// ============================================================ -// 静态状态 -// ============================================================ - - // ============================================================ BleOta& BleOta::instance() { @@ -229,7 +224,11 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size) return ESP_FAIL; } - esp_err_t err = esp_ota_begin(BleOta::instance().m_esp32UpdatePartition, total_size, &BleOta::instance().m_esp32OtaHandle); + // 用分区大小初始化 OTA,而非包头声明的 total_size + // esp_ota_end 会从固件镜像头读取实际大小来校验,不依赖此处传入的 size + esp_err_t err = esp_ota_begin(BleOta::instance().m_esp32UpdatePartition, + BleOta::instance().m_esp32UpdatePartition->size, + &BleOta::instance().m_esp32OtaHandle); if (err != ESP_OK) { DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err)); BleOta::instance().m_esp32UpdatePartition = NULL; @@ -486,14 +485,23 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u", seq, (unsigned int)BleOta::instance().m_esp32Received); + // 尾包检测: data_len < 标准负载说明是最后一包(不满一个完整 payload) + bool isLastPacket = (data_len < BLE_OTA_CHUNK_PAYLOAD); + if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U || - (total_size > 0 && BleOta::instance().m_esp32Received >= total_size)) { + (total_size > 0 && BleOta::instance().m_esp32Received >= total_size) || + isLastPacket) { + if (isLastPacket) { + DEBUG_INFO(TAG, "ESP32 尾包检测: seq=%u data_len=%zu received=%u total=%" PRIu32, + seq, data_len, (unsigned int)BleOta::instance().m_esp32Received, total_size); + } sendWindowAck(cmd, seq, (uint32_t)BleOta::instance().m_esp32Received); } - if (total_size > 0 && BleOta::instance().m_esp32Received >= total_size) { - DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)", - (unsigned int)BleOta::instance().m_esp32Received, total_size, seq + 1); + if ((total_size > 0 && BleOta::instance().m_esp32Received >= total_size) || isLastPacket) { + DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)%s", + (unsigned int)BleOta::instance().m_esp32Received, total_size, seq + 1, + isLastPacket ? " [尾包]" : ""); BleOta::instance().m_esp32LastCmd = cmd; BleOta::instance().m_esp32LastSeq = seq; @@ -578,7 +586,8 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, } if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U || - (meta.total_size > 0 && stored_size >= meta.total_size)) { + (meta.total_size > 0 && stored_size >= meta.total_size) || + (data_len < BLE_OTA_CHUNK_PAYLOAD)) { sendWindowAck(cmd, seq, stored_size); } @@ -733,17 +742,17 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg) // 向对端请求更快的连接参数,提升 OTA 吞吐 struct ble_gap_upd_params up = {}; up.itvl_min = 6; // 7.5 ms 最小连接间隔 - up.itvl_max = 12; // 15 ms 最大连接间隔 + up.itvl_max = 6; // 7.5 ms 固定间隔(避免对端选大值) up.latency = 0; // 无延迟 up.supervision_timeout = 2000; // 2 秒监控超时 - up.min_ce_len = 0; // 控制器自行决定 - up.max_ce_len = 0; + up.min_ce_len = 12; // 最小连接事件长度 12×0.625=7.5ms,尽量多传包 + up.max_ce_len = 12; int rc = ble_gap_update_params(BleOta::instance().m_connHandle, &up); if (rc != 0) { DEBUG_WARN(TAG, "请求更新连接参数失败: rc=%d(对端不一定接受,继续运行)", rc); } else { - DEBUG_INFO(TAG, "已请求更快的连接参数: itvl=%.1f-%.1fms latency=0", - up.itvl_min * 1.25f, up.itvl_max * 1.25f); + DEBUG_INFO(TAG, "已请求更快的连接参数: itvl=%.1fms ce_len=%.1fms latency=0", + up.itvl_min * 1.25f, up.min_ce_len * 0.625f); } } else { DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status); @@ -883,6 +892,11 @@ static void ble_ota_on_sync(void) // 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(); } @@ -973,8 +987,7 @@ esp_err_t BleOta::serverStart() nimble_port_freertos_init(ble_ota_nimble_host_task); - // Sync val_handle to class member - BleOta::instance().mDataOutValHandle = s_data_out_val_handle; + // s_data_out_val_handle 由 on_sync 回调填充,此处不复制 DEBUG_INFO(TAG, "BLE OTA 服务器初始化完成"); return ESP_OK; } @@ -990,7 +1003,7 @@ esp_err_t BleOta::notifyResponse(const uint8_t *data, size_t len) if (!BleOta::instance().m_bleConnected) { return ESP_ERR_NOT_SUPPORTED; } - if (BleOta::instance().mDataOutValHandle == 0) { + if (s_data_out_val_handle == 0) { DEBUG_WARN(TAG, "DATA_OUT 值句柄未设置"); return ESP_ERR_INVALID_STATE; } @@ -1003,7 +1016,7 @@ esp_err_t BleOta::notifyResponse(const uint8_t *data, size_t len) return ESP_ERR_NO_MEM; } - int rc = ble_gattc_notify_custom(BleOta::instance().m_connHandle, BleOta::instance().mDataOutValHandle, om); + int rc = ble_gattc_notify_custom(BleOta::instance().m_connHandle, s_data_out_val_handle, om); if (rc != 0) { DEBUG_WARN(TAG, "通知发送失败: rc=%d", rc); return ESP_FAIL; diff --git a/main/server/ble_ota.h b/main/server/ble_ota.h index 9f0dfd7..e4bebc1 100644 --- a/main/server/ble_ota.h +++ b/main/server/ble_ota.h @@ -23,7 +23,6 @@ public: bool m_bleReady = false; bool m_bleConnected = false; uint16_t m_connHandle = 0xFFFF; - uint16_t mDataOutValHandle = 0; bool m_esp32OtaActive = false; esp_ota_handle_t m_esp32OtaHandle = 0; diff --git a/tools/ble_ota.html b/tools/ble_ota.html index 83ec302..27fc6cb 100644 --- a/tools/ble_ota.html +++ b/tools/ble_ota.html @@ -356,7 +356,7 @@
- 回 WiFi 页面 + 回 WiFi 页面 ↗
@@ -481,7 +481,7 @@ const UI_BUILD = '2026-05-08-ble-console-v1'; const BLE_OTA_NOTIFY_ACK = 0xA1; const BLE_OTA_NOTIFY_FINISH = 0xB1; - const BLE_OTA_ACK_WINDOW = 8; + const BLE_OTA_ACK_WINDOW = 32; const TARGETS = { gd32: { @@ -512,8 +512,6 @@ versionChar: null, notificationsEnabled: false, selectedFile: null, - selectedFileBytes: null, - selectedFileKey: '', selectedFileHandle: null, // FileSystemFileHandle for hot reload abortRequested: false, uploading: false, @@ -869,8 +867,6 @@ els.packetCount.textContent = String(packets); els.sentCount.textContent = '0'; - state.selectedFileBytes = null; - state.selectedFileKey = file ? `${file.name}|${file.size}|${file.lastModified || 0}` : ''; state.selectedFile = file || null; state.lastUploadStatus = 'idle'; @@ -883,15 +879,14 @@ if (file) { setProgress(0, `已选择 ${file.name}`); log('FILE', `已选择: ${file.name} (${formatBytes(file.size)}), 目标=${currentTarget().label}`); - preloadSelectedFileBytes().catch((err) => { - log('FILE', `预读文件失败: ${err.message}`, 'W'); - }); } else { setProgress(0, '等待选择文件'); } } function syncSelectedFileFromInput() { + // FSAPI 选择的文件以 selectedFileHandle 为准,不从 input 元素同步,避免切标签页回来误清空 + if (state.selectedFileHandle) return; const file = (els.firmwareFile && els.firmwareFile.files && els.firmwareFile.files[0]) || null; updateFileInfo(file); } @@ -925,8 +920,6 @@ function clearSelectedFile() { state.selectedFile = null; - state.selectedFileBytes = null; - state.selectedFileKey = ''; state.selectedFileHandle = null; state.lastUploadStatus = 'idle'; if (els.firmwareFile) els.firmwareFile.value = ''; @@ -934,19 +927,6 @@ log('FILE', '文件已清除'); } - async function preloadSelectedFileBytes() { - if (!state.selectedFile) { - throw new Error('未选择固件文件'); - } - if (state.selectedFileBytes && state.selectedFileBytes.length === state.selectedFile.size) { - return state.selectedFileBytes; - } - const bytes = new Uint8Array(await state.selectedFile.arrayBuffer()); - state.selectedFileBytes = bytes; - log('FILE', `固件已缓存到内存: ${bytes.length} bytes`); - return bytes; - } - function buildPacket(seq, totalSize, payload, cmd) { const packet = new Uint8Array(1 + 2 + 4 + payload.length + 4); packet[0] = cmd; @@ -964,9 +944,6 @@ return new Promise(r => setTimeout(r, ms)); } - async function drainGattQueue() { - await sleep(120); - } async function uploadFirmware() { // 每次刷写前尝试重新从本地读取文件 @@ -986,8 +963,6 @@ const cfg = currentTarget(); const payloadSize = Math.max(32, Math.min(498, Number(els.payloadSize.value) || 480)); const file = state.selectedFile; - const totalSize = file.size; - const packetTotal = Math.ceil(totalSize / payloadSize); state.abortRequested = false; state.uploading = true; @@ -1013,14 +988,16 @@ throw new Error('DATA_OUT 通知未开启,无法使用 ACK 模式'); } - log('OTA', `开始准备固件: target=${cfg.label}, file=${file.name}, total=${totalSize}, packets=${packetTotal}, payload=${payloadSize}, cmd=0x${cfg.packetCmd.toString(16).padStart(2, '0').toUpperCase()}, ackWindow=${BLE_OTA_ACK_WINDOW}`); - let fileBytes = state.selectedFileBytes; - if (!fileBytes || fileBytes.length !== totalSize) { - setProgress(0, `[${cfg.label}] 正在读取固件文件到内存...`); - fileBytes = await preloadSelectedFileBytes(); - } else { - setProgress(0, `[${cfg.label}] 使用已缓存的固件数据...`); - log('OTA', `使用缓存固件: ${fileBytes.length} bytes`); + log('OTA', `开始准备固件: target=${cfg.label}, file=${file.name}, payload=${payloadSize}, cmd=0x${cfg.packetCmd.toString(16).padStart(2, '0').toUpperCase()}, ackWindow=${BLE_OTA_ACK_WINDOW}`); + setProgress(0, `[${cfg.label}] 正在读取固件文件...`); + // 直接读取同一个 File 对象的 arrayBuffer,不经过 state 中间变量 + // 保证 totalSize、包头 total_size、发送字节数三者完全一致 + const fileBytes = new Uint8Array(await file.arrayBuffer()); + const totalSize = fileBytes.length; + const packetTotal = Math.ceil(totalSize / payloadSize); + log('OTA', `固件已读取: totalSize=${totalSize}, packetTotal=${packetTotal}, file.size=${file.size}`); + if (totalSize !== file.size) { + log('OTA', `⚠️ file.size(${file.size}) ≠ arrayBuffer(${totalSize}),以 arrayBuffer 为准,请确认页面已 Ctrl+F5 刷新`, 'W'); } log('OTA', `开始写入 DATA_IN: target=${cfg.label}, file=${file.name}, total=${totalSize}, packets=${packetTotal}, payload=${payloadSize}, windowAck=${BLE_OTA_ACK_WINDOW}`); @@ -1038,14 +1015,17 @@ const percent = ((seq + 1) / packetTotal) * 100; els.sentCount.textContent = String(seq + 1); - setProgress(percent, `[${cfg.label}] 已写 ${seq + 1}/${packetTotal} 包,等待窗口 ACK`); + setProgress(percent, `[${cfg.label}] 已写 ${seq + 1}/${packetTotal} 包`); } - const ack = await waitForAck(cfg.packetCmd, endSeq, 10000); - if (ack.seq + 1 === packetTotal || ((ack.seq + 1) % (BLE_OTA_ACK_WINDOW * 5)) === 0) { - log('OTA', `${cfg.label} 窗口 ACK 已确认: seq=${ack.seq}, bytes=${ack.bytes}`); + await waitForAck(cfg.packetCmd, endSeq, 10000); + + // ACK 字节数校验: 设备报告的累计字节必须匹配预期 + const expectedBytes = Math.min((endSeq + 1) * payloadSize, totalSize); + if (state.lastAckBytes < expectedBytes) { + const missing = expectedBytes - state.lastAckBytes; + throw new Error(`BLE 丢包检测: seq=${endSeq} 期望${expectedBytes}字节 实际${state.lastAckBytes}字节 丢失${missing}字节(${(missing / payloadSize).toFixed(1)}包)`); } - await sleep(0); } setProgress(100, `[${cfg.label}] 固件数据已全部写入 DATA_IN`); @@ -1053,7 +1033,7 @@ if (cfg.label === 'ESP32') { return true; } - await drainGattQueue(); + await sleep(30); const cmdHex = els.ctrlHex.value || cfg.ctrlHex; const cmd = hexToBytes(cmdHex); try { diff --git a/tools/index.html b/tools/index.html index 5bbe92b..1153994 100644 --- a/tools/index.html +++ b/tools/index.html @@ -126,8 +126,8 @@
- - 切换到 BLE 蓝牙刷写 + + 切换到 BLE 蓝牙刷写 ↗