feat(version): 更新固件版本号至 0.3.12,修正编译时间和 Git 哈希
feat(ble): 增加 BLE OTA 窗口确认机制,优化数据包处理 fix(html): 更新链接以在新标签页中打开,改善用户体验
This commit is contained in:
@@ -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()
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
+35
-22
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+23
-43
@@ -356,7 +356,7 @@
|
||||
</div>
|
||||
<div class="sub-actions">
|
||||
<button id="versionBtn" class="btn btn-secondary" disabled>读取版本</button>
|
||||
<a class="btn btn-secondary" href="index.html">回 WiFi 页面</a>
|
||||
<a class="btn btn-secondary" href="index.html" target="_blank">回 WiFi 页面 ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 {
|
||||
|
||||
+2
-2
@@ -126,8 +126,8 @@
|
||||
</div>
|
||||
<div style="margin-top:12px;">
|
||||
<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 class="btn btn-outline btn-block" href="ble_ota.html?v=20260509-0046" target="_blank" style="margin-top:10px;text-decoration:none;">
|
||||
切换到 BLE 蓝牙刷写 ↗
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user