const transport = require('./ble-transport') const { BYTE_ORDER_HIGH, crc16Ccitt, appendCrc16Ccitt, hasValidCrc16Ccitt } = require('./crc') const { BOOTLOADER_HEAD } = require('./bootloader-frame') const { softReset } = require('./motor-control-protocol') const { delay } = require('./base-utils') const { formatBytes, isCancelError, loadSelectedFile } = require('./file-service') const HEAD = BOOTLOADER_HEAD const BOOTLOADER_CRC_OPTIONS = { byteOrder: BYTE_ORDER_HIGH } const ACK = 0x06 const NAK = 0x15 const HANDSHAKE_INTERVAL_MS = 200 const HANDSHAKE_ATTEMPTS = 10 const HANDSHAKE_TIMEOUT_MS = HANDSHAKE_INTERVAL_MS * HANDSHAKE_ATTEMPTS const RESPONSE_TIMEOUT_MS = 3000 const PROGRAM_RESPONSE_TIMEOUT_MS = 6000 const PROGRAM_CHUNK_SIZE = 128 const FILE_SIZES = { 16: 16 * 1024, 32: 32 * 1024 } const FLASH_LAYOUTS = { 16: { capacity: FILE_SIZES[16], startAddress: 0x0400, endAddress: 0x4000 }, 32: { capacity: FILE_SIZES[32], startAddress: 0x0800, endAddress: 0x8000 } } const FLASH_SIZE_TEXT = Object.keys(FILE_SIZES) .map((sizeKb) => formatBytes(FILE_SIZES[sizeKb])) .join(' 或 ') const CHIP_FLASH_SIZE_KB = { FU6572L: 32, FU6572N: 32, FU6572T: 32, FU6565N: 32, FU6565T: 32, FU6563N: 32, FU6562L: 32, FU6562LA: 32, FU6562Q: 32, FU6562S: 32, FU6562T: 32, FU6532N: 32, FU6532T: 32, FU6522L: 32, FU6522N: 32, FU6522T: 32, FU6812L2: 16, FU6812N2: 16, FU6812S2: 16, FU6812V: 16, FU6861Q2: 16, FU6861N2: 16, FU6861NF2: 16, FU6861L2: 16, FU6862L: 16, FU6862Q: 16, FU6872P: 16 } const CHIP_FAMILY_FLASH_SIZE_KB = { 65: 32, 68: 16 } const state = { bootloaderChipId: '--', bootloaderDetailText: '', bootloaderProgress: 0, bootloaderStatusText: '', bootloaderVersion: '--', chipModel: '--', deviceProgramCrcText: '--', firmwareChecksumText: '--', firmwareName: '', firmwareSize: 0, firmwareSizeText: '--', firmwareValidText: '未选择', isBootloaderBusy: false, isFirmwareReady: false } let firmwareBytes = null let initialized = false let unsubscribeTransport = null let activeResponseWaiter = null const subscribers = [] function getState() { return { ...state } } function setState(changedData) { Object.assign(state, changedData) subscribers.slice().forEach((subscriber) => { subscriber(getState()) }) } function abortActiveResponseWaiter(message) { if (!activeResponseWaiter) return false const waiter = activeResponseWaiter activeResponseWaiter = null waiter.abort(new Error(message || '蓝牙已断开')) return true } function subscribe(subscriber) { if (typeof subscriber !== 'function') return () => {} subscribers.push(subscriber) subscriber(getState()) return () => { const index = subscribers.indexOf(subscriber) if (index >= 0) subscribers.splice(index, 1) } } function init() { transport.init() if (initialized) return unsubscribeTransport = transport.subscribe((transportState) => { if (!transportState.connectedDevice) { abortActiveResponseWaiter('蓝牙已断开') } if (!transportState.connectedDevice && state.isBootloaderBusy) { transport.showCommandAlert('BootLoader', '蓝牙已断开,升级已停止') setState({ bootloaderDetailText: '蓝牙已断开,升级已停止', bootloaderStatusText: '升级失败', isBootloaderBusy: false }) } }) initialized = true } function toHex(value, length = 2) { return Number(value || 0).toString(16).toUpperCase().padStart(length, '0') } function formatCrc(value) { return `0x${toHex(value, 4)}` } function normalizeModel(value) { const text = String(value || '').trim() return text && text !== '--' ? text : '' } function extractChipModels(chipModel) { const model = normalizeModel(chipModel).toUpperCase() return model.match(/FU\d{4}[A-Z0-9]*/g) || [] } function inferFlashSizeKb(chipModel) { const models = extractChipModels(chipModel) for (const model of models) { if (CHIP_FLASH_SIZE_KB[model]) return CHIP_FLASH_SIZE_KB[model] const family = model.slice(2, 4) if (CHIP_FAMILY_FLASH_SIZE_KB[family]) return CHIP_FAMILY_FLASH_SIZE_KB[family] } const text = normalizeModel(chipModel).toUpperCase() if (/(^|[^0-9])32\s*K(B)?([^0-9]|$)/.test(text)) return 32 if (/(^|[^0-9])16\s*K(B)?([^0-9]|$)/.test(text)) return 16 return null } function inferFlashSizeKbFromBytes(byteLength) { return Number(Object.keys(FILE_SIZES).find((sizeKb) => FILE_SIZES[sizeKb] === byteLength)) || null } function inferUpgradeFlashSizeKb(chipModel, byteLength) { return inferFlashSizeKb(chipModel) || inferFlashSizeKbFromBytes(byteLength) } function getFlashLayout() { const sizeKb = inferUpgradeFlashSizeKb(state.chipModel, state.firmwareSize) return sizeKb ? FLASH_LAYOUTS[sizeKb] : null } function getFirmwareValidation(byteLength = state.firmwareSize, chipModel = state.chipModel) { const chipSizeKb = inferFlashSizeKb(chipModel) const firmwareSizeKb = inferFlashSizeKbFromBytes(byteLength) const sizeKb = chipSizeKb || firmwareSizeKb const layout = sizeKb ? FLASH_LAYOUTS[sizeKb] : null const normalizedChipModel = normalizeModel(chipModel) if (!byteLength) { return { isReady: false, text: chipSizeKb ? `需要 ${formatBytes(FLASH_LAYOUTS[chipSizeKb].capacity)} .bin` : `请选择 ${FLASH_SIZE_TEXT} .bin` } } if (!layout) { return { isReady: false, text: `文件 ${formatBytes(byteLength)},应为 ${FLASH_SIZE_TEXT}` } } if (byteLength !== layout.capacity) { return { isReady: false, text: `文件 ${formatBytes(byteLength)},应为 ${formatBytes(layout.capacity)}` } } return { isReady: true, text: chipSizeKb ? `匹配 ${formatBytes(layout.capacity)}` : `${normalizedChipModel ? `${normalizedChipModel} 未识别,` : '未读到芯片型号,'}按 ${formatBytes(layout.capacity)} 尝试` } } function setChipModel(chipModel) { const nextChipModel = normalizeModel(chipModel) || '--' const validation = getFirmwareValidation(state.firmwareSize, nextChipModel) setState({ chipModel: nextChipModel, bootloaderDetailText: validation.text, firmwareValidText: validation.text, isFirmwareReady: validation.isReady }) } function buildFrame(payload) { return new Uint8Array(appendCrc16Ccitt(HEAD.concat(payload), BOOTLOADER_CRC_OPTIONS)) } function buildHandshakeFrame() { return buildFrame([0x39, 0x42, 0x4C]) } function buildUnlockFrame() { return buildFrame([0x08, 0x4E, 0x00]) } function buildProgramFrame(address, dataBytes) { const payload = [ 0x44, address & 0xFF, (address >> 8) & 0xFF ] const data = Array.prototype.slice.call(dataBytes || []).slice(0, PROGRAM_CHUNK_SIZE) while (data.length < PROGRAM_CHUNK_SIZE) { data.push(0x00) } return buildFrame(payload.concat(data)) } function buildFlashCheckFrame() { return buildFrame([0x19, 0x43, 0x43]) } function buildPageEraseFrame(enabled) { return buildFrame([0x08, 0x50, enabled ? 0x45 : 0x44]) } function buildExitFrame() { return buildFrame([0x08, 0x42, 0x42]) } function parseAsciiField(bytes, offset, length) { const chars = [] for (let index = 0; index < length; index += 1) { const byte = bytes[offset + index] & 0xFF if (byte === 0x00 || byte === 0xFF) continue if (byte >= 0x20 && byte <= 0x7E) { chars.push(String.fromCharCode(byte)) } } return chars.join('').trim() || '--' } function getHandshakeDetail(response) { if (!response) return '--' return `${response.versionText || '--'} / ${response.chipIdText || '--'}` } function alignBootloaderBuffer(buffer) { let headIndex = -1 for (let index = 0; index < buffer.length - 1; index += 1) { if (buffer[index] === HEAD[0] && buffer[index + 1] === HEAD[1]) { headIndex = index break } } if (headIndex > 0) { buffer.splice(0, headIndex) } else if (headIndex < 0 && buffer.length > 1) { buffer.splice(0, buffer.length - 1) } } function parseResponse(bytes, kind) { if (!hasValidCrc16Ccitt(bytes, BOOTLOADER_CRC_OPTIONS)) { throw new Error('Bootloader 返回帧 CRC 校验失败') } if (kind === 'handshake') { if (bytes.length !== 15 || bytes[2] !== 0x39 || bytes[3] !== 0x42 || bytes[4] !== 0x4C) { throw new Error('握手反馈帧不匹配') } const versionText = parseAsciiField(bytes, 5, 4) const chipIdText = parseAsciiField(bytes, 9, 4) return { chipId: chipIdText, chipIdText, version: versionText, versionText } } if (kind === 'unlock') { if (bytes.length !== 8 || bytes[2] !== 0x08 || bytes[3] !== 0x4E || bytes[4] !== 0x00) { throw new Error('解锁反馈帧不匹配') } return { ack: bytes[5] } } if (kind === 'program') { if (bytes.length !== 8 || bytes[2] !== 0x44) { throw new Error('编程反馈帧不匹配') } return { ack: bytes[5], address: (bytes[3] & 0xFF) | ((bytes[4] & 0xFF) << 8) } } if (kind === 'flashCheck') { if (bytes.length !== 9 || bytes[2] !== 0x19 || bytes[3] !== 0x43 || bytes[4] !== 0x43) { throw new Error('全 Flash 校验反馈帧不匹配') } const flashCrc = ((bytes[5] & 0xFF) << 8) | (bytes[6] & 0xFF) return { flashCrc, flashCrcText: formatCrc(flashCrc) } } if (kind === 'pageErase') { if (bytes.length !== 8 || bytes[2] !== 0x08 || bytes[3] !== 0x50) { throw new Error('页擦除反馈帧不匹配') } return { ack: bytes[5], enabled: bytes[4] === 0x45 } } return {} } function assertAck(response, label) { if (!response || response.ack === ACK) return if (response.ack === NAK) throw new Error(`${label}失败:设备返回 NAK`) throw new Error(`${label}失败:未知 ACK 0x${toHex(response.ack)}`) } function getExpectedLength(kind) { if (kind === 'handshake') return 15 if (kind === 'flashCheck') return 9 return 8 } function waitForResponse(kind, timeout, options = {}) { const expectedLength = getExpectedLength(kind) const buffer = [] return new Promise((resolve, reject) => { let settled = false let timer = null let unsubscribe = () => {} const waiter = { abort: (error) => { cleanup() reject(error) } } abortActiveResponseWaiter('新的 BootLoader 响应等待已开始') activeResponseWaiter = waiter unsubscribe = transport.subscribeRawResponse((bytes) => { buffer.push.apply(buffer, bytes) alignBootloaderBuffer(buffer) if (buffer.length < expectedLength) return const frame = buffer.slice(0, expectedLength) try { const response = parseResponse(frame, kind) cleanup() resolve(response) } catch (error) { if (options.ignoreInvalid) { buffer.shift() return } cleanup() reject(error) } }) timer = setTimeout(() => { cleanup() reject(new Error(`${kind} 响应超时`)) }, timeout || RESPONSE_TIMEOUT_MS) function cleanup() { if (settled) return settled = true clearTimeout(timer) if (activeResponseWaiter === waiter) { activeResponseWaiter = null } unsubscribe() } }) } async function sendBootloaderFrame(frame, label, kind, timeout) { const responsePromise = kind ? waitForResponse(kind, timeout) : null const sent = await transport.sendRawFrameExact(frame, label) if (!sent) { if (responsePromise) { responsePromise.catch(() => {}) abortActiveResponseWaiter(`${label}发送失败`) } throw new Error(`${label}发送失败`) } return responsePromise ? responsePromise : true } async function sendSoftReset() { return softReset({ kind: 'bootloader-soft-reset' }) } async function sendHandshakeKeepAlive() { if (state.isBootloaderBusy) return false const frame = buildHandshakeFrame() let finished = false setState({ bootloaderDetailText: `0/${HANDSHAKE_ATTEMPTS}`, bootloaderProgress: 0, bootloaderStatusText: '握手中', isBootloaderBusy: true }) const responsePromise = waitForResponse('handshake', HANDSHAKE_TIMEOUT_MS, { ignoreInvalid: true }).then((response) => { finished = true return response }).catch((error) => { finished = true throw error }) responsePromise.catch(() => {}) try { for (let attempt = 0; attempt < HANDSHAKE_ATTEMPTS && !finished; attempt += 1) { const sent = await transport.sendRawFrameExact(frame, 'Bootloader握手') if (!sent) throw new Error('握手帧发送失败') setState({ bootloaderDetailText: `${attempt + 1}/${HANDSHAKE_ATTEMPTS}`, bootloaderProgress: Math.round((attempt + 1) / HANDSHAKE_ATTEMPTS * 100) }) if (attempt < HANDSHAKE_ATTEMPTS - 1) { await delay(HANDSHAKE_INTERVAL_MS) } } const response = await responsePromise setState({ bootloaderChipId: response.chipIdText, bootloaderDetailText: getHandshakeDetail(response), bootloaderProgress: 100, bootloaderStatusText: '握手成功', bootloaderVersion: response.versionText, isBootloaderBusy: false }) return true } catch (error) { abortActiveResponseWaiter('握手已停止') const message = error && error.message ? error.message : '握手失败' transport.showCommandAlert('BootLoader握手', message) setState({ bootloaderDetailText: message, bootloaderStatusText: '握手失败', isBootloaderBusy: false }) return false } } async function handshakeUntilReady() { const frame = buildHandshakeFrame() setState({ bootloaderDetailText: '软复位', bootloaderProgress: 0, bootloaderStatusText: '握手中' }) const resetResponse = await sendSoftReset() setState({ bootloaderDetailText: resetResponse ? '等待握手' : '软复位未响应,继续握手' }) let lastError = null let finished = false const responsePromise = waitForResponse('handshake', HANDSHAKE_TIMEOUT_MS, { ignoreInvalid: true }).then((response) => { finished = true return response }).catch((error) => { finished = true throw error }) for (let attempt = 0; attempt < HANDSHAKE_ATTEMPTS && !finished; attempt += 1) { try { await transport.sendRawFrameExact(frame, 'Bootloader握手') } catch (error) { lastError = error } if (!finished && attempt < HANDSHAKE_ATTEMPTS - 1) { await delay(HANDSHAKE_INTERVAL_MS) } } try { const response = await responsePromise setState({ bootloaderChipId: response.chipIdText, bootloaderDetailText: getHandshakeDetail(response), bootloaderVersion: response.versionText, bootloaderStatusText: '握手成功' }) return response } catch (error) { throw new Error(lastError ? `Bootloader握手失败:${lastError.message}` : `Bootloader握手失败:${error.message}`) } } async function chooseFirmwareFile(source = 'message') { if (state.isBootloaderBusy) return false try { const file = await loadSelectedFile(source, { extensionMessage: '请选择 .bin 固件文件', extensions: ['bin'], fallbackName: 'firmware.bin' }) firmwareBytes = file.bytes const firmwareCrcText = formatCrc(crc16Ccitt( Array.prototype.slice.call(firmwareBytes), BOOTLOADER_CRC_OPTIONS )) const firmwareSizeText = formatBytes(firmwareBytes.length) const validation = getFirmwareValidation(firmwareBytes.length) setState({ bootloaderDetailText: validation.text, bootloaderProgress: 0, bootloaderStatusText: validation.isReady ? '固件已加载' : '固件不匹配', deviceProgramCrcText: '--', firmwareChecksumText: firmwareCrcText, firmwareName: file.name || 'firmware.bin', firmwareSize: firmwareBytes.length, firmwareSizeText, firmwareValidText: validation.text, isFirmwareReady: validation.isReady }) return true } catch (error) { const message = error && (error.errMsg || error.message) ? (error.errMsg || error.message) : '读取固件失败' if (!isCancelError(error)) { transport.showCommandAlert('固件文件', message) } return false } } async function startUpgrade() { if (state.isBootloaderBusy) return false if (!firmwareBytes || !state.isFirmwareReady) { transport.showCommandAlert('固件不匹配', state.firmwareValidText || `请先选择 ${FLASH_SIZE_TEXT} .bin 文件`) return false } const layout = getFlashLayout() if (!layout) { transport.showCommandAlert('固件大小', `请选择 ${FLASH_SIZE_TEXT} .bin 文件`) return false } setState({ bootloaderDetailText: '', bootloaderProgress: 0, bootloaderStatusText: '握手中', isBootloaderBusy: true }) try { await handshakeUntilReady() setState({ bootloaderDetailText: '编程解锁', bootloaderStatusText: '升级中' }) assertAck(await sendBootloaderFrame(buildUnlockFrame(), 'Bootloader解锁', 'unlock'), '编程解锁') setState({ bootloaderDetailText: '开启页擦除', bootloaderStatusText: '升级中' }) assertAck(await sendBootloaderFrame(buildPageEraseFrame(true), '页擦除使能', 'pageErase'), '页擦除使能') const totalBytes = layout.endAddress - layout.startAddress let programmedBytes = 0 for (let address = layout.startAddress; address < layout.endAddress; address += PROGRAM_CHUNK_SIZE) { const chunk = firmwareBytes.slice(address, address + PROGRAM_CHUNK_SIZE) const response = await sendBootloaderFrame( buildProgramFrame(address, chunk), `编程 0x${toHex(address, 4)}`, 'program', PROGRAM_RESPONSE_TIMEOUT_MS ) assertAck(response, `编程 0x${toHex(address, 4)}`) if (response.address !== address) { throw new Error(`编程地址反馈不匹配:0x${toHex(response.address, 4)}`) } programmedBytes = Math.min(totalBytes, programmedBytes + PROGRAM_CHUNK_SIZE) const progress = Math.min(99, Math.round(programmedBytes / totalBytes * 100)) setState({ bootloaderDetailText: `0x${toHex(address, 4)}`, bootloaderProgress: progress, bootloaderStatusText: `升级中 ${progress}%` }) } const checkResponse = await sendBootloaderFrame(buildFlashCheckFrame(), '全Flash校验', 'flashCheck') await sendBootloaderFrame(buildExitFrame(), '退出Bootloader') setState({ bootloaderDetailText: '校验通过', bootloaderProgress: 100, bootloaderStatusText: '升级完成', deviceProgramCrcText: checkResponse.flashCrcText, isBootloaderBusy: false }) return true } catch (error) { const message = error && error.message ? error.message : '升级失败' transport.showCommandAlert('Bootloader升级', message) setState({ bootloaderDetailText: message, bootloaderStatusText: '升级失败', isBootloaderBusy: false }) return false } } async function readProgramChecksum() { if (state.isBootloaderBusy) return false setState({ bootloaderDetailText: '', bootloaderStatusText: '读取中' }) try { const response = await sendBootloaderFrame(buildFlashCheckFrame(), '读取程序校验码', 'flashCheck') setState({ bootloaderDetailText: '程序校验码已读取', bootloaderStatusText: '读取完成', deviceProgramCrcText: response.flashCrcText }) return true } catch (error) { const message = error && error.message ? error.message : '读取程序校验码失败' transport.showCommandAlert('程序校验码', message) setState({ bootloaderDetailText: message, bootloaderStatusText: '读取失败' }) return false } } async function exitBootloader() { if (state.isBootloaderBusy) return false try { const sent = await sendBootloaderFrame(buildExitFrame(), '退出BootLoader') if (!sent) throw new Error('退出命令发送失败') setState({ bootloaderDetailText: '', bootloaderStatusText: '已退出 BootLoader' }) return true } catch (error) { const message = error && error.message ? error.message : '退出 BootLoader 失败' transport.showCommandAlert('退出 BootLoader', message) setState({ bootloaderDetailText: message, bootloaderStatusText: '退出失败' }) return false } } module.exports = { chooseFirmwareFile, getState, init, readProgramChecksum, sendHandshakeKeepAlive, setChipModel, exitBootloader, startUpgrade, subscribe }