diff --git a/config/sdkconfig.defaults b/config/sdkconfig.defaults
index d52d44e..552e851 100644
--- a/config/sdkconfig.defaults
+++ b/config/sdkconfig.defaults
@@ -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
diff --git a/main/include/version.h b/main/include/version.h
index 7f0d6b3..37a07e3 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 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"
diff --git a/main/server/ble_ota.cpp b/main/server/ble_ota.cpp
index 64cbe2c..ba175fe 100644
--- a/main/server/ble_ota.cpp
+++ b/main/server/ble_ota.cpp
@@ -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);
diff --git a/sdkconfig.defaults b/sdkconfig.defaults
index 7cd5f3e..f922bd4 100644
--- a/sdkconfig.defaults
+++ b/sdkconfig.defaults
@@ -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
diff --git a/tools/app.js b/tools/app.js
index e62f999..8435bc6 100644
--- a/tools/app.js
+++ b/tools/app.js
@@ -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;
diff --git a/tools/ble_ota.js b/tools/ble_ota.js
index 8262336..2651ce9 100644
--- a/tools/ble_ota.js
+++ b/tools/ble_ota.js
@@ -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}次`);
}
}
diff --git a/tools/index.html b/tools/index.html
index f13fff4..6c8a71a 100644
--- a/tools/index.html
+++ b/tools/index.html
@@ -427,7 +427,7 @@
操作成功
-
+
-
+