- 新增 /can/send_burst HTTP接口,支持一次SPI事务批量发送多帧CAN消息 - 实现canFdSendBurst函数,通过0xC7 SPI类型实现背靠背装载各通道邮箱 - 添加canBurstCapacity函数计算单次突发最大帧数限制 - 突发发送期间暂停can_feedback_task避免SPI抢占冲突 refactor(can_feedback_task): 改造为周期查询+事件消费模式 - 0xC4/0xC5从GD32主动推送改为ESP32周期查询(250ms/1s) - 0xC6/0xD2仍保留事件驱动推送模式,由任务消费处理 - 消除OTA期间GD32悬空按铃导致的超时刷屏问题 - 电机OTA期间也暂停任务避免与0xD1抢夺SPI资源 perf(ota_relay): 增加重发次数上限提升传输可靠性 - 将kMaxDataSends从3次增加到5次,提高数据包传输成功率 chore(version): 更新固件版本号至0.3.78 - 版本查询checkVersionWait移到tickInFlight释放后异步触发 - 避免/motor/version请求拖长tick导致tickStuck误报通道闪黄
199 lines
7.1 KiB
JavaScript
199 lines
7.1 KiB
JavaScript
// ==================== BLE OTA 状态与元素缓存 ====================
|
|
import { TARGETS } from './constants.js';
|
|
import { addLog } from '../common/log.js';
|
|
import { updateBleStageLabels, resetBleStages } from './stages.js';
|
|
import { hideBleVerifyInfo } from './verify.js';
|
|
export const state = {
|
|
device: null,
|
|
server: null,
|
|
service: null,
|
|
ctrlChar: null,
|
|
dataInChar: null,
|
|
dataOutChar: null,
|
|
versionChar: null,
|
|
notificationsEnabled: false,
|
|
selectedFile: null,
|
|
selectedFileHandle: null, // FileSystemFileHandle for hot reload
|
|
abortRequested: false,
|
|
uploading: false,
|
|
autoFlashing: false,
|
|
lastUploadStatus: 'idle',
|
|
lastNotificationHex: '',
|
|
lastAckCmd: null,
|
|
lastAckSeq: -1,
|
|
lastAckBytes: 0,
|
|
ackWaiter: null,
|
|
otaResultWaiter: null,
|
|
otaRunId: 0,
|
|
lastEsp32DoneSession: null,
|
|
fileSha256: null, // 当前 BLE 会话用的 SHA(与对应芯片槽同步)
|
|
deviceSha256: null,
|
|
expectedReboot: false, // 发送 0xFA 后置 true,重连后清
|
|
reconnecting: false, // 自动重连中
|
|
reconnectAttempts: 0, // 重连尝试次数
|
|
uploadTargetKey: null,
|
|
activeTotalSize: 0,
|
|
activePacketTotal: 0,
|
|
};
|
|
|
|
export const $ = (id) => document.getElementById(id);
|
|
export const els = {
|
|
bleConnectToggle: $('bleConnectToggle'),
|
|
autoFlashBtn: $('autoFlashBtn'),
|
|
abortBtn: $('abortBtn'),
|
|
uploadZone: $('uploadZone'),
|
|
firmwareFile: $('firmwareFile'),
|
|
connState: $('connState'),
|
|
connBadge: $('connBadge'),
|
|
deviceName: $('deviceName'),
|
|
deviceId: $('deviceId'),
|
|
gattState: $('gattState'),
|
|
versionText: $('versionText'),
|
|
fileInfo: $('fileInfo'),
|
|
clearFileBtn: $('clearFileBtn'),
|
|
progressFill: $('progressFill'), // 共享进度条
|
|
progressTxt: $('progressTxt'), // 共享进度文本
|
|
progressPct: $('progressPct'), // 共享进度百分比
|
|
fileName: $('fileName'),
|
|
fileType: $('fileType'),
|
|
fileSize: $('fileSize'),
|
|
packetCount: $('packetCount'),
|
|
sentCount: $('sentCount'),
|
|
logScroll: $('logScroll'), // 共享日志面板
|
|
};
|
|
|
|
export function currentTarget() {
|
|
return TARGETS[window.flashTarget] || TARGETS.esp32;
|
|
}
|
|
|
|
/** BLE 通道是否正在上传/自动刷写。集中判定,避免各处重复 state.uploading || state.autoFlashing。 */
|
|
export function isBleUploading() {
|
|
return !!(state.uploading || state.autoFlashing);
|
|
}
|
|
|
|
export function currentPayloadSize() {
|
|
const cfg = currentTarget();
|
|
return Math.max(32, Math.min(498, Number(cfg.recommendedPayload) || 480));
|
|
}
|
|
|
|
export function flowTarget() {
|
|
return TARGETS[state.uploadTargetKey] || currentTarget();
|
|
}
|
|
|
|
export function bleLog(tag, message, level) {
|
|
level = level || 'I';
|
|
if (typeof addLog === 'function') {
|
|
addLog(level, 'BLE-' + tag, message);
|
|
}
|
|
}
|
|
|
|
export function toHex(uint8) {
|
|
return Array.from(uint8).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
|
|
}
|
|
|
|
export function hexToBytes(hex) {
|
|
const clean = hex.replace(/[^0-9a-fA-F]/g, '');
|
|
if (clean.length === 0 || clean.length % 2 !== 0) throw new Error('HEX 长度必须为偶数');
|
|
const out = new Uint8Array(clean.length / 2);
|
|
for (let i = 0; i < clean.length; i += 2) out[i / 2] = parseInt(clean.slice(i, i + 2), 16);
|
|
return out;
|
|
}
|
|
|
|
export async function sleep(ms) {
|
|
return new Promise(r => setTimeout(r, ms));
|
|
}
|
|
|
|
export function setReadyHint(html) {
|
|
const el = $('readyHint');
|
|
if (el) el.innerHTML = html;
|
|
}
|
|
|
|
export function setConnectionState(connected) {
|
|
const idle = connected && state.selectedFile && !state.uploading;
|
|
// BLE 模式:连接状态只写顶栏胶囊,避免下方重复文案
|
|
if (typeof connectionMode !== 'undefined' && connectionMode === 'ble') {
|
|
var pc = document.getElementById('pillConn');
|
|
var pct = document.getElementById('pillConnTxt');
|
|
if (pc) pc.className = connected ? 'pill online' : 'pill offline';
|
|
if (pct) pct.textContent = connected ? 'BLE 已连接' : 'BLE 未连接';
|
|
if (els.connState) {
|
|
els.connState.textContent = connected ? '已连接' : '未连接';
|
|
if (els.connState.parentElement) {
|
|
els.connState.parentElement.className = connected ? 'pill online' : 'pill offline';
|
|
}
|
|
}
|
|
if (els.connBadge) {
|
|
els.connBadge.textContent = connected ? 'online' : 'offline';
|
|
els.connBadge.className = `v ${connected ? 'ok' : 'bad'}`;
|
|
}
|
|
if (els.gattState) els.gattState.textContent = connected ? 'GATT 已就绪' : '未就绪';
|
|
}
|
|
// launch 风格按钮:checked 与 data-ble-state 由连接结果驱动
|
|
// connecting 态由点击时设置;此处仅在连接结束(成功→connected / 失败→idle)时收口
|
|
if (els.bleConnectToggle) {
|
|
if (els.bleConnectToggle.checked !== connected) {
|
|
els.bleConnectToggle.checked = !!connected;
|
|
}
|
|
}
|
|
var btn = document.querySelector('.ble-launch-btn');
|
|
if (btn) btn.setAttribute('data-ble-state', connected ? 'connected' : 'idle');
|
|
els.autoFlashBtn.disabled = !idle;
|
|
els.abortBtn.disabled = !state.uploading;
|
|
|
|
if (!connected) {
|
|
setReadyHint('当前未连接设备。建议顺序:连接设备 → 选择固件 → 开始刷写。');
|
|
} else if (!state.notificationsEnabled) {
|
|
setReadyHint('设备已连接,正在自动准备刷写通道。若长时间无响应,请断开后重连。');
|
|
} else if (!state.selectedFile) {
|
|
setReadyHint('设备已连接。下一步请选择目标芯片与固件文件。');
|
|
} else {
|
|
setReadyHint('设备与文件都已就绪,可以直接开始刷写或执行一键刷写。');
|
|
}
|
|
}
|
|
|
|
export function resetUiAfterDisconnect() {
|
|
state.server = null;
|
|
state.service = null;
|
|
state.ctrlChar = null;
|
|
state.dataInChar = null;
|
|
state.dataOutChar = null;
|
|
state.versionChar = null;
|
|
state.notificationsEnabled = false;
|
|
state.uploading = false;
|
|
state.autoFlashing = false;
|
|
state.lastUploadStatus = 'idle';
|
|
state.deviceSha256 = null;
|
|
if (typeof connectionMode === 'undefined' || connectionMode === 'ble') {
|
|
els.deviceName.textContent = '-';
|
|
els.deviceId.textContent = '-';
|
|
if (els.versionText) els.versionText.textContent = '未读取';
|
|
var pv = document.getElementById('pillVer');
|
|
if (pv) pv.textContent = 'v--';
|
|
}
|
|
resetBleStages();
|
|
hideBleVerifyInfo();
|
|
setConnectionState(false);
|
|
}
|
|
|
|
export function refreshTargetUi() {
|
|
const cfg = currentTarget();
|
|
updateBleStageLabels(cfg);
|
|
/* 与 WiFi 一致:文件由 syncBleFileFromSelectedTarget 按 flashTarget 绑定;
|
|
* 此处只刷新标签/包数/阶段说明。 */
|
|
if (state.selectedFile) {
|
|
const payloadSize = currentPayloadSize();
|
|
const packets = Math.ceil(state.selectedFile.size / payloadSize);
|
|
if (els.packetCount) els.packetCount.textContent = String(packets);
|
|
}
|
|
setConnectionState(!!state.server);
|
|
var espTab = document.getElementById('targetEsp32Tab');
|
|
var gdTab = document.getElementById('targetGd32Tab');
|
|
var isEsp = cfg.label === 'ESP32';
|
|
if (espTab) espTab.classList.toggle('active', isEsp);
|
|
if (gdTab) gdTab.classList.toggle('active', !isEsp);
|
|
var autoBtn = document.getElementById('autoFlashBtn');
|
|
if (autoBtn && !state.uploading && !state.autoFlashing) {
|
|
autoBtn.textContent = isEsp ? '一键刷写 ESP32' : '一键刷写 GD32';
|
|
}
|
|
}
|