Files
ESP32_COMM/tools/js/ble/session.js
T

124 lines
4.6 KiB
JavaScript
Raw Normal View History

// ==================== BLE OTA 结果等待与中止 ====================
import { BLE_OTA_CTRL_ABORT_OTA } from './constants.js';
import { state, els, bleLog } from './state.js';
import { connectBle, disconnectBle } from './gatt.js';
import { autoFlash } from './transfer.js';
import { syncSelectedFileFromInput } from './file.js';
export function clearOtaResultWaiter() {
if (state.otaResultWaiter) {
clearTimeout(state.otaResultWaiter.timer);
state.otaResultWaiter = null;
}
}
export function resolveOtaResult(status, detail) {
if (!state.otaResultWaiter) return;
var waiter = state.otaResultWaiter;
state.otaResultWaiter = null;
clearTimeout(waiter.timer);
waiter.resolve({ status: status, detail: detail || {} });
}
export function rejectOtaResult(message, detail) {
if (!state.otaResultWaiter) return;
var waiter = state.otaResultWaiter;
state.otaResultWaiter = null;
clearTimeout(waiter.timer);
var err = new Error(message || 'OTA 收尾失败');
err.detail = detail || {};
waiter.reject(err);
}
export function nextOtaSessionId() {
state.otaRunId = (state.otaRunId + 1) & 0xFFFF;
if (state.otaRunId === 0) state.otaRunId = 1;
return state.otaRunId;
}
export function finishBelongsToWaiter(targetKey, value) {
if (!state.otaResultWaiter || state.otaResultWaiter.targetKey !== targetKey) return false;
return (value & 0xFFFF) === state.otaResultWaiter.sessionId;
}
export function finishSessionId(value) {
return value & 0xFFFF;
}
export function isSessionlessFinish(value) {
return finishSessionId(value) === 0;
}
export function hasCurrentWaiter(targetKey) {
return !!(state.otaResultWaiter && state.otaResultWaiter.targetKey === targetKey);
}
export function waitForOtaResult(targetKey, sessionId, timeoutMs) {
clearOtaResultWaiter();
var runId = state.otaRunId;
return new Promise(function(resolve, reject) {
var timer = setTimeout(function() {
if (state.otaResultWaiter && state.otaResultWaiter.runId === runId) {
state.otaResultWaiter = null;
}
reject(new Error(targetKey === 'gd32' ? '等待 GD32 OTA 转发结果超时' : '等待 ESP32 OTA 收尾结果超时'));
}, timeoutMs);
state.otaResultWaiter = {
targetKey: targetKey,
sessionId: sessionId,
runId: runId,
timer: timer,
resolve: resolve,
reject: reject,
};
});
}
/**
* @description 中止当前 BLE OTA 任务并清理状态。
* 改进:发 0xFB 控制命令让 ESP32 主动清理 OTA 会话,BLE 保持连接。
* 旧做法是直接断开 BLE 触发 GAP disconnect 清理,导致用户需重连。
* 若 0xFB 发送失败(如 CTRL 特征不可用),回退到设置标志等待发包循环自然退出。
*/
export async function abortTask() {
state.abortRequested = true; // 立即停止发送循环
bleLog('OTA', '已请求中止,发送 0xFB 让设备端清理(BLE 保持连接)');
if (state.ctrlChar && state.server && state.device.gatt.connected) {
try {
const session = state.sessionId || 0;
const abortCmd = new Uint8Array([BLE_OTA_CTRL_ABORT_OTA, session & 0xFF, (session >> 8) & 0xFF]);
await state.ctrlChar.writeValue(abortCmd);
bleLog('OTA', '0xFB 已发送,等待设备端回复 ABORTED (stage=0x20)');
} catch (e) {
bleLog('OTA', '0xFB 发送失败: ' + e.message + ',回退到本地中止', 'E');
// CTRL 写入失败时回退:本地立即标记失败,不再等设备端 ABORTED 回复
state.lastUploadStatus = 'failed';
if (state.ackWaiter) {
const waiter = state.ackWaiter;
state.ackWaiter = null;
clearTimeout(waiter.timer);
waiter.reject(new Error('中止失败: ' + e.message));
}
rejectOtaResult('中止失败: ' + e.message, { stage: 0xFB, value: 0 });
}
} else {
// CTRL 特征未就绪(连接已断或未完成握手),仅本地清理
bleLog('OTA', 'CTRL 特征不可用,仅本地中止', 'W');
state.lastUploadStatus = 'failed';
rejectOtaResult('CTRL 特征不可用', { stage: 0xFB, value: 0 });
}
}
export function bindEvents() {
els.connectBtn.addEventListener('click', connectBle);
els.disconnectBtn.addEventListener('click', disconnectBle);
els.autoFlashBtn.addEventListener('click', autoFlash);
els.abortBtn.addEventListener('click', abortTask);
// 上传区事件由 app.js 的 setupSharedUploadZone 统一处理
// 文件输入变化时同步 BLE 文件状态
els.firmwareFile.addEventListener('change', syncSelectedFileFromInput);
els.firmwareFile.addEventListener('input', syncSelectedFileFromInput);
window.addEventListener('focus', () => setTimeout(syncSelectedFileFromInput, 0));
}