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:
@@ -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,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
@@ -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, ¶ms);
|
||||
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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user