feat(ble-ota): 优化BLE OTA传输性能和可靠性

- 调整NimBLE配置恢复默认缓冲区大小以支持窗口内突发包接收
- 实现BLE连接参数动态调整,支持Windows兼容模式
- 优化OTA数据队列长度与确认窗口匹配
- 改进reorder buffer管理,添加乱序包缓存统计
- 修复ACK触发逻辑,确保按累计进度而非单包序列确认
- 添加GD32 OTA元数据本地缓存避免频繁NVS访问
- 优化前端JavaScript实现,改进Web Bluetooth写入策略

BREAKING CHANGE: BLE连接参数协商机制变更可能影响部分设备兼容性
This commit is contained in:
小龙虾
2026-07-12 03:38:20 +08:00
parent c0ca4e2a9f
commit 0b363492ed
7 changed files with 169 additions and 108 deletions
+11 -7
View File
@@ -37,12 +37,12 @@ CONFIG_BTDM_CTRL_BLE_MAX_CONN_EFF=1
CONFIG_BTDM_BLE_MAX_CONN_EFF_EFF=1
# BLE 连接事件长度优化(允许单 CE 内传多包,提升 OTA 吞吐)
CONFIG_BT_CTRL_CE_LENGTH_TYPE_CE=y
CONFIG_BT_CTRL_CE_LENGTH_TYPE_ORIG=y
# NimBLE mbuf 池(单连接 OTA 场景缩减,省 ~7 KB)
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=8
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=16
CONFIG_BT_NIMBLE_MSYS1_BLOCK_COUNT=8
# NimBLE OTA 窗口突发接收容量(恢复 7feef70 对应的 IDF 6.0 默认值)
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=12
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=24
CONFIG_BT_NIMBLE_MSYS1_BLOCK_COUNT=12
# NimBLE: 禁用未使用的标准 GATT 服务(只保留自定义 OTA service,省 ~8-15 KB)
CONFIG_BT_NIMBLE_PROX_SERVICE=n
@@ -58,11 +58,15 @@ CONFIG_BT_NIMBLE_HR_SERVICE=n
CONFIG_BT_NIMBLE_BAS_SERVICE=n
CONFIG_BT_NIMBLE_DIS_SERVICE=n
# NimBLE: 传输缓冲区缩减
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=6
# NimBLE: 保留足够 ACL 缓冲承接窗口内突发包
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=16
CONFIG_BT_NIMBLE_ATT_MAX_PREP_ENTRIES=16
CONFIG_BT_NIMBLE_GATT_MAX_PROCS=2
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_MIN_CONN_INTERVAL=12
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_MAX_CONN_INTERVAL=16
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SLAVE_LATENCY=0
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SUPERVISION_TMO=200
# mbedTLS
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
+4 -4
View File
@@ -4,11 +4,11 @@
// 固件版本
#define FW_VERSION_MAJOR 0
#define FW_VERSION_MINOR 3
#define FW_VERSION_PATCH 40
#define FW_VERSION_STRING "0.3.40"
#define FW_VERSION_PATCH 41
#define FW_VERSION_STRING "0.3.41"
// 编译时间
#define BUILD_TIME "2026-07-11 17:47:55"
#define BUILD_TIME "2026-07-12 03:33:39"
// Git 信息
#define GIT_HASH "fedb08e"
#define GIT_HASH "c0ca4e2"
+130 -65
View File
@@ -65,7 +65,7 @@
// ── 数据队列:将 OTA 数据处理从 NimBLE host task 卸载到工作线程 ──
// 避免 flash 操作(esp_ota_begin / esp_ota_write)阻塞 BLE 协议栈导致连接断开。
#define BLE_OTA_DATA_QUEUE_LEN 16
#define BLE_OTA_DATA_QUEUE_LEN (BLE_OTA_ACK_WINDOW + 4)
struct BleOtaDataBatch {
uint8_t *data;
@@ -319,15 +319,62 @@ struct Esp32OtaReorderBuf {
uint8_t **slots = nullptr; ///< slots[i]:窗口内第 i 个包的 payload 拷贝(nullptr=未占)
size_t *lens = nullptr; ///< lens[i]:slots[i] 的 payload 长度
size_t payloadCap = 0; ///< 每个 slot 的容量(= BLE_OTA_CHUNK_PAYLOAD)
uint32_t cachedCount = 0; ///< 当前已缓存的乱序包数
};
static Esp32OtaReorderBuf s_reorder;
static uint32_t s_fallback_ooo_count = 0; // 回退模式下自上次 ACK 以来的乱序包计数
static uint32_t s_gd32_ooo_count = 0; // GD32 路径自上次 ACK 以来的乱序/重复包计数
static ota_meta_t s_ble_gd32_meta = {};
static bool s_ble_gd32_meta_active = false;
static bool s_conn_fallback_requested = false;
// 前向声明(alloc/free 互相引用)
static void esp32_reorder_free();
static uint16_t ble_ota_log_conn_params(uint16_t conn_handle, const char *phase)
{
struct ble_gap_conn_desc desc = {};
int rc = ble_gap_conn_find(conn_handle, &desc);
if (rc != 0) {
DEBUG_WARN(TAG, "%s读取 BLE 连接参数失败: rc=%d", phase, rc);
return 0;
}
DEBUG_INFO(TAG, "%sBLE 连接参数: interval=%.2fms latency=%u supervision=%ums",
phase,
desc.conn_itvl * 1.25f,
(unsigned)desc.conn_latency,
(unsigned)desc.supervision_timeout * 10U);
return desc.conn_itvl;
}
static int ble_ota_request_conn_params(uint16_t conn_handle, bool windows_compatible)
{
struct ble_gap_upd_params params = {};
if (windows_compatible) {
params.itvl_min = 12;
params.itvl_max = 16;
params.min_ce_len = 0;
params.max_ce_len = 0;
} else {
params.itvl_min = 6;
params.itvl_max = 6;
params.min_ce_len = 12;
params.max_ce_len = 12;
}
params.latency = 0;
params.supervision_timeout = 200;
int rc = ble_gap_update_params(conn_handle, &params);
DEBUG_INFO(TAG, "请求 BLE 连接参数(%s): interval=%.1f..%.1fms rc=%d",
windows_compatible ? "Windows兼容" : "高速",
params.itvl_min * 1.25f,
params.itvl_max * 1.25f,
rc);
return rc;
}
/**
* @brief 分配 reorder buffer(slots 指针/长度数组)。
* 在 ble_esp32_ota_begin() 中调用。分配失败则置 fallback=true,
@@ -344,6 +391,7 @@ static void esp32_reorder_alloc()
s_reorder.windowBase = 0;
s_reorder.windowSize = (uint32_t)BLE_OTA_REORDER_WINDOW;
s_reorder.payloadCap = BLE_OTA_CHUNK_PAYLOAD;
s_reorder.cachedCount = 0;
s_reorder.slots = (uint8_t **)calloc(s_reorder.windowSize, sizeof(uint8_t *));
s_reorder.lens = (size_t *)calloc(s_reorder.windowSize, sizeof(size_t));
s_fallback_ooo_count = 0;
@@ -394,6 +442,7 @@ static void esp32_reorder_free()
s_reorder.fallback = false;
s_reorder.windowSize = 0;
s_reorder.payloadCap = 0;
s_reorder.cachedCount = 0;
s_fallback_ooo_count = 0;
}
@@ -418,6 +467,9 @@ static uint32_t esp32_reorder_flush()
free(s_reorder.slots[idx]);
s_reorder.slots[idx] = nullptr;
s_reorder.lens[idx] = 0;
if (s_reorder.cachedCount > 0) {
s_reorder.cachedCount--;
}
if (err != ESP_OK) {
DEBUG_ERR(TAG, "Reorder flush 写入失败: seq=%u err=%s",
@@ -505,8 +557,8 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
s_reorder.expected++;
uint32_t flushed = esp32_reorder_flush();
if (flushed > 0) {
DEBUG_DBG(TAG, "Reorder flush: seq=%u 后连续写入 %u 包 (expected→%u)",
seq, (unsigned)flushed, (unsigned)s_reorder.expected);
DEBUG_INFO(TAG, "Reorder 缺口已补齐: seq=%u,连续 flush=%u 包,expected→%u",
seq, (unsigned)flushed, (unsigned)s_reorder.expected);
}
return ESP_OK;
}
@@ -536,6 +588,11 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
memcpy(copy, data, data_len);
s_reorder.slots[idx] = copy;
s_reorder.lens[idx] = data_len;
s_reorder.cachedCount++;
if (s_reorder.cachedCount == 1U) {
DEBUG_INFO(TAG, "Reorder 检测到缺口: expected=%u,开始缓存 seq=%u",
(unsigned)s_reorder.expected, seq);
}
DEBUG_DBG(TAG, "Reorder 缓存乱序包: seq=%u idx=%u (expected=%u)",
seq, (unsigned)idx, (unsigned)s_reorder.expected);
return ESP_OK;
@@ -617,6 +674,8 @@ static void ble_ota_session_reset()
esp32_reorder_free();
// 复位 GD32 路径乱序包计数器(节流 ACK 计数),保证下次 GD32 上传从干净状态开始
s_gd32_ooo_count = 0;
s_ble_gd32_meta = {};
s_ble_gd32_meta_active = false;
}
/**
@@ -1083,39 +1142,18 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
return true;
}
// ★ 会话判定:若已有活跃 ESP32 OTA 会话,根据已接收字节数区分两种情况:
// ① m_received == 0:seq=0 尚未写入 flash,说明是客户端 ACK 超时后
// 重传的 seq=0(同一会话续传),跳过 begin。
// ② m_received > 0:seq=0 已写入且有后续数据,说明这是新的上传会话
// (前端 abort 后 BLE 断开未完成就发起了新传输),需 abort 旧会话
// 再 begin,否则 reorder buffer 以旧 expected 去重导致全部丢弃。
// 活跃 BLE 会话中的 seq=0 一律按重传处理。窗口 0 的 ACK 丢失时,
// 前端会从累计字节 0 续发;此时重置 OTA 会丢掉已缓存的后续包。
// 新上传由 0xFB/断连先清理旧会话,不能用 seq=0 猜测会话边界。
if (Esp32OtaCore::instance().isActive()) {
if (Esp32OtaCore::instance().getReceived() > 0) {
// ★ 跨通道保护:当前活跃会话可能是 WiFi 通道发起的,
// BLE 不应无条件 abort 别人的会话。只有当活跃会话也是 BLE
// 通道时才允许 abort + rebegin(同会话重传/新 BLE 会话场景);
// 否则直接互斥拒绝,让前端看到"被其他通道占用"。
ota_channel_t curCh = OtaCoordinator::instance().getOtaChannel();
if (curCh != OTA_CHANNEL_BLE) {
DEBUG_WARN(TAG, "ESP32 OTA 被其他通道占用(ch=%d),BLE 拒绝抢占",
(int)curCh);
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
return true;
}
DEBUG_WARN(TAG, "ESP32 OTA 新会话检测: received=%u > 0, "
"中止旧会话并重新开始",
(unsigned)Esp32OtaCore::instance().getReceived());
ble_esp32_ota_abort();
esp_err_t err = ble_esp32_ota_begin(total_size);
if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) {
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
}
return true;
}
} else {
DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,跳过 begin");
ota_channel_t curCh = OtaCoordinator::instance().getOtaChannel();
if (curCh != OTA_CHANNEL_BLE) {
DEBUG_WARN(TAG, "ESP32 OTA 被其他通道占用(ch=%d),BLE 拒绝抢占",
(int)curCh);
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
return true;
}
DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,保留 reorder 状态");
} else {
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
esp_err_t err = ble_esp32_ota_begin(total_size);
@@ -1165,17 +1203,25 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
bool transferComplete = (total_size > 0 && afterReceived >= total_size);
// ACK 触发条件:仅在实际写入新字节时触发窗口边界 ACK,
// 避免重复包/乱序缓存包在窗口边界发送陈旧 ACK
// ACK 按累计连续进度触发,而不是按当前到达包的 seq 触发。缺口包到达后
// reorder 可能一次 flush 到下个窗口边界,例如 seq=99 使 expected 直达128;
// 此时必须立即 ACK 127,否则后续重复包都不产生 progress,前端只能超时。
uint32_t completedPackets = (afterReceived + BLE_OTA_CHUNK_PAYLOAD - 1U) /
BLE_OTA_CHUNK_PAYLOAD;
bool reachedAckBoundary = completedPackets > 0U &&
(completedPackets % BLE_OTA_ACK_WINDOW) == 0U;
if (progressMade &&
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
(reachedAckBoundary ||
transferComplete ||
isLastPacket)) {
if (isLastPacket || transferComplete) {
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
seq, data_len, (unsigned int)afterReceived, total_size);
}
sendWindowAck(cmd, seq, afterReceived);
uint16_t ackSeq = s_reorder.expected > 0U
? (uint16_t)(s_reorder.expected - 1U)
: seq;
sendWindowAck(cmd, ackSeq, afterReceived);
}
if (transferComplete) {
@@ -1218,14 +1264,17 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 ", payload=%zu",
total_size, data_len);
s_ble_gd32_meta = {};
s_ble_gd32_meta.total_size = total_size;
s_ble_gd32_meta_active = true;
ble_ota_boost_wifi_ps();
}
ota_meta_t meta = {};
if (OtaCoordinator::instance().loadGd32Meta(&meta) != ESP_OK || meta.total_size == 0) {
if (!s_ble_gd32_meta_active || s_ble_gd32_meta.total_size == 0) {
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
return true;
}
ota_meta_t &meta = s_ble_gd32_meta;
// ── 顺序检查:GD32 Flash 必须按序写入,无 reorder buffer ──
// 改进:乱序/重复包不再中止整个 batch,而是丢弃当前包 + 节流 ACK 报告缺口,
@@ -1287,14 +1336,24 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
}
uint32_t stored_size = flash_offset + (uint32_t)data_len;
err = OtaCoordinator::instance().updateGd32Meta(stored_size, seq);
if (err != ESP_OK) {
DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq);
}
meta.stored_size = stored_size;
meta.last_seq = seq;
const bool transfer_complete = stored_size >= meta.total_size;
const bool checkpoint =
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U) ||
transfer_complete ||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD);
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
(meta.total_size > 0 && stored_size >= meta.total_size) ||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD)) {
// NVS commit 是毫秒级操作,不能每 480 字节执行一次。仅在 ACK 窗口边界
// 和尾包持久化;正常完成时尾包一定落盘,后续 SHA/中继读取到的语义不变。
if (checkpoint) {
err = OtaCoordinator::instance().updateGd32Meta(stored_size, seq);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 元数据 checkpoint 失败 (seq=%u): %s",
seq, esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)err);
return true;
}
sendWindowAck(cmd, seq, stored_size);
}
@@ -1302,11 +1361,10 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
seq, flash_offset, stored_size);
// --- 检查 GD32 固件是否接收完毕 ---
if (OtaCoordinator::instance().loadGd32Meta(&meta) == ESP_OK &&
meta.total_size > 0 &&
meta.stored_size >= meta.total_size) {
if (transfer_complete) {
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
meta.stored_size, meta.total_size, seq + 1);
s_ble_gd32_meta_active = false;
OtaCoordinator::instance().completeGd32Storage();
ble_ota_restore_wifi_ps();
esp_err_t sha_err = OtaCoordinator::instance().finalizeGd32Checksum();
@@ -1509,7 +1567,7 @@ static const struct ble_gatt_chr_def gatt_ota_chrs[] = {
{
.uuid = &GATT_DATA_IN_UUID.u,
.access_cb = ble_ota_data_in_access,
.flags = BLE_GATT_CHR_F_WRITE,
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
},
{
.uuid = &GATT_DATA_OUT_UUID.u,
@@ -1557,23 +1615,15 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
if (event->connect.status == 0) {
// 连接成功
BleOta::instance().m_bleConnected = true;
BleOta::instance().m_connHandle = event->connect.conn_handle;
BleOta::instance().m_connHandle = event->connect.conn_handle;
s_conn_fallback_requested = false;
DEBUG_INFO(TAG, "BLE 客户端已连接: handle=%d", BleOta::instance().m_connHandle);
// 向对端请求更快的连接参数,提升 OTA 吞吐
struct ble_gap_upd_params up = {};
up.itvl_min = 6; // 7.5 ms 最小连接间隔
up.itvl_max = 6; // 7.5 ms 固定间隔(避免对端选大值)
up.latency = 0; // 无延迟
up.supervision_timeout = 2000; // 2 秒监控超时
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);
ble_ota_log_conn_params(BleOta::instance().m_connHandle, "初始");
int rc = ble_ota_request_conn_params(BleOta::instance().m_connHandle, false);
if (rc != 0) {
DEBUG_WARN(TAG, "请求更新连接参数失败: rc=%d(对端不一定接受,继续运行)", rc);
} else {
DEBUG_INFO(TAG, "已请求更快的连接参数: itvl=%.1fms ce_len=%.1fms latency=0",
up.itvl_min * 1.25f, up.min_ce_len * 0.625f);
s_conn_fallback_requested = true;
ble_ota_request_conn_params(BleOta::instance().m_connHandle, true);
}
} else {
DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
@@ -1595,6 +1645,7 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
BleOta::instance().m_bleConnected = false;
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
s_conn_fallback_requested = false;
// ★ BLE 断开时恢复 WiFi 射频
ble_ota_restore_wifi_ps();
@@ -1638,8 +1689,22 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
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_INFO(TAG, "BLE 连接参数更新完成: status=%d", event->conn_update.status);
uint16_t interval = ble_ota_log_conn_params(event->conn_update.conn_handle, "更新后");
if (!s_conn_fallback_requested &&
(event->conn_update.status != 0 || interval == 0 || interval > 16)) {
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);
+9 -7
View File
@@ -39,16 +39,18 @@ CONFIG_BT_NIMBLE_HR_SERVICE=n
CONFIG_BT_NIMBLE_BAS_SERVICE=n
CONFIG_BT_NIMBLE_DIS_SERVICE=n
# ── NimBLE: 缩减内存池(单连接 OTA 场景,不需要大池子)──
# MSYS pool 1: 256B blocks, 16→8 (省 ~2 KB)
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=8
# MSYS pool 2: 320B blocks, 32→16 (省 ~5 KB)
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=16
# ACL transport buffers: 12→6 (省 ~1.5 KB)
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=6
# ── NimBLE: OTA 窗口突发接收容量(恢复 7feef70 对应的 IDF 6.0 默认值)──
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=12
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=24
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24
# Event buffers: 30→16 (省 ~1 KB)
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=16
# Prepared write entries: 64→16 (省 ~1 KB)
CONFIG_BT_NIMBLE_ATT_MAX_PREP_ENTRIES=16
# GATT procedures: 4→2 (单连接够用)
CONFIG_BT_NIMBLE_GATT_MAX_PROCS=2
CONFIG_BT_CTRL_CE_LENGTH_TYPE_ORIG=y
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_MIN_CONN_INTERVAL=12
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_MAX_CONN_INTERVAL=16
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SLAVE_LATENCY=0
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SUPERVISION_TMO=200
+5
View File
@@ -728,6 +728,11 @@ function updateSystemStatusUi(data) {
mOtaCache.className = 'v';
}
}
// 共享进度条按当前连接模式归属:BLE 模式下由 ble_ota.js 的设备 ACK
// 驱动包数、阶段和完成状态,HTTP 状态轮询不能覆盖这套实时进度。
if (window.connectionMode === 'ble') {
return;
}
// relay state - skip GD32 relay UI when ESP32 OTA is active
if (currentOtaTarget === 'esp32' && esp32OtaPhase > 0) {
return;
+8 -23
View File
@@ -10,7 +10,7 @@
version: 0x0004,
};
const UI_BUILD = '2026-06-15-ble-merged-v2';
const UI_BUILD = '2026-07-12-ble-reorder-v6';
const BLE_OTA_NOTIFY_ACK = 0xA1;
const BLE_OTA_NOTIFY_FINISH = 0xB1;
const BLE_OTA_NOTIFY_SHA256 = 0xC1;
@@ -1228,17 +1228,16 @@
state.activePacketTotal = packetTotal;
els.sentCount.textContent = '0';
setProgress(0, `[${cfg.label}] 等待设备确认 0/${packetTotal} 包`);
bleLog('OTA', `开始写入 DATA_IN: target=${cfg.label}, file=${file.name}, total=${totalSize}, packets=${packetTotal}, payload=${payloadSize}, windowAck=${BLE_OTA_ACK_WINDOW}`);
bleLog('OTA', `开始写入 DATA_IN: build=${UI_BUILD}, target=${cfg.label}, file=${file.name}, total=${totalSize}, packets=${packetTotal}, payload=${payloadSize}, windowAck=${BLE_OTA_ACK_WINDOW}`);
const MAX_WINDOW_RETRIES = 3;
let bleWriteTs = 0; // 上一次 BLE 写入时间戳,用于节流
const BLE_WRITE_INTERVAL = 2; // 最小写入间隔 (ms),避免 BLE 栈过载
for (let windowStart = 0; windowStart < packetTotal; windowStart += BLE_OTA_ACK_WINDOW) {
if (state.abortRequested) throw new Error('用户中止');
const endSeq = Math.min(windowStart + BLE_OTA_ACK_WINDOW, packetTotal) - 1;
// 流水线发送:不逐包 await,减少 BLE 栈 per-packet 开销
// Windows Web Bluetooth 同一时刻只允许一个 GATT 操作。逐包等待仅保证
// 提交顺序;实际吞吐由设备请求的高速 BLE 连接参数决定。
// ★ 不再在循环内 setProgress:进度完全由设备 ACK 后的 state.lastAckBytes 驱动
for (let seq = windowStart; seq <= endSeq; seq++) {
// ★ 中止检查放在循环内:abortTask 可能在本窗口发送过程中触发
@@ -1249,13 +1248,6 @@
const payload = fileBytes.slice(start, end);
const packet = buildPacket(seq, totalSize, payload, cfg.packetCmd);
// 节流:避免过快地向 BLE 栈提交写入
const now = performance.now();
const gap = now - bleWriteTs;
if (gap < BLE_WRITE_INTERVAL) {
await sleep(BLE_WRITE_INTERVAL - gap);
}
// ★ seq=0 必须用 writeValueWithResponse(带响应):
// seq=0 触发设备端 esp_ota_begin()(flash 擦除 + OTA 会话创建),
// 若用 writeValueWithoutResponse 在 BLE 链路层丢失,
@@ -1271,9 +1263,8 @@
// seq=0 写入后等待设备完成 esp_ota_begin()(flash 擦除较耗时)
await sleep(200);
} else {
state.dataInChar.writeValueWithoutResponse(packet); // fire-and-forget
await state.dataInChar.writeValueWithoutResponse(packet);
}
bleWriteTs = performance.now();
}
state.ackGeneration++;
@@ -1296,8 +1287,7 @@
// ★ 进度检测:如果重试后字节数没有增长,说明续传无效
if (retryCount > 0 && state.lastAckBytes <= prevAckBytes) {
bleLog('OTA', `重试无进展: ackBytes=${state.lastAckBytes} 未增长 (前次=${prevAckBytes}),放弃`, 'E');
break;
bleLog('OTA', `上次重试暂未推进: ackBytes=${state.lastAckBytes} (前次=${prevAckBytes}),继续按缺口重试`, 'W');
}
prevAckBytes = state.lastAckBytes;
@@ -1322,10 +1312,6 @@
const end = Math.min(start + payloadSize, totalSize);
const payload = fileBytes.slice(start, end);
const packet = buildPacket(seq, totalSize, payload, cfg.packetCmd);
// 流水线发送 + 节流
const now = performance.now();
const gap = now - bleWriteTs;
if (gap < BLE_WRITE_INTERVAL) await sleep(BLE_WRITE_INTERVAL - gap);
// ★ 重试路径中 seq=0 也必须用 writeValueWithResponse
if (seq === 0) {
try {
@@ -1336,9 +1322,8 @@
}
await sleep(200);
} else {
state.dataInChar.writeValueWithoutResponse(packet);
await state.dataInChar.writeValueWithoutResponse(packet);
}
bleWriteTs = performance.now();
}
state.ackGeneration++;
@@ -1352,7 +1337,7 @@
if (state.lastAckBytes < expectedBytes) {
const missing = expectedBytes - state.lastAckBytes;
throw new Error(`BLE 丢包检测: seq=${endSeq} 期望${expectedBytes}字节 实际${state.lastAckBytes}字节 丢失${missing}字节(${(missing / payloadSize).toFixed(1)}包) 已重试${MAX_WINDOW_RETRIES}次`);
throw new Error(`BLE 丢包检测: seq=${endSeq} 期望${expectedBytes}字节 实际${state.lastAckBytes}字节 丢失${missing}字节(${(missing / payloadSize).toFixed(1)}包) 已重试${retryCount}次`);
}
}
+2 -2
View File
@@ -427,7 +427,7 @@
<span id="toastMsg">操作成功</span>
</div>
<script src="app.js?v=20260705b"></script>
<script src="app.js?v=20260712a"></script>
<script>
window.connectionMode = window.connectionMode || 'wifi';
window.flashTarget = window.flashTarget || 'esp32';
@@ -670,6 +670,6 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
applySharedModeUi();
})();
</script>
<script src="ble_ota.js?v=20260626a"></script>
<script src="ble_ota.js?v=20260712d"></script>
</body>
</html>