const transport = require('../../transport/ble-core.js') const { PROGRAM_CHUNK_SIZE, assertBootloaderAck, buildExitFrame, buildFlashCheckFrame, buildHandshakeFrame, buildPageEraseFrame, buildProgramFrame, buildUnlockFrame, calculateBootloaderCrc, formatBootloaderCrc, toHex } = require('../../protocols/bootloader/index.js') const { delay } = require('../../utils/base-utils.js') const { isCancelError, loadSelectedFile } = require('../../repositories/file.js') const { formatBytes } = require('../../utils/binary-utils.js') const firmware = require('./firmware.js') const bootloaderTransport = require('./transport.js') const HANDSHAKE_INTERVAL_MS = 200 const HANDSHAKE_ATTEMPTS = 10 const HANDSHAKE_TIMEOUT_MS = HANDSHAKE_INTERVAL_MS * HANDSHAKE_ATTEMPTS const PROGRAM_RESPONSE_TIMEOUT_MS = 6000 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 const subscribers = [] function getState() { return { ...state } } function setState(changedData) { Object.assign(state, changedData) subscribers.slice().forEach((subscriber) => { subscriber(getState()) }) } 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) { bootloaderTransport.abortActiveResponseWaiter('蓝牙已断开') } if (!transportState.connectedDevice && state.isBootloaderBusy) { transport.showCommandAlert('BootLoader', '蓝牙已断开,升级已停止') setState({ bootloaderDetailText: '蓝牙已断开,升级已停止', bootloaderStatusText: '升级失败', isBootloaderBusy: false }) } }) initialized = true } function getFlashLayout() { return firmware.getFlashLayout(state.chipModel, state.firmwareSize) } function setChipModel(chipModel) { const nextChipModel = firmware.normalizeModel(chipModel) || '--' const validation = firmware.getFirmwareValidation(state.firmwareSize, nextChipModel) setState({ chipModel: nextChipModel, bootloaderDetailText: validation.text, firmwareValidText: validation.text, isFirmwareReady: validation.isReady }) } function getHandshakeDetail(response) { if (!response) return '--' return `${response.versionText || '--'} / ${response.chipIdText || '--'}` } 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 = bootloaderTransport.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 bootloaderTransport.sendRawFrame(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) { bootloaderTransport.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: '握手中' }) let lastError = null let finished = false const responsePromise = bootloaderTransport.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 bootloaderTransport.sendRawFrame(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 = formatBootloaderCrc(calculateBootloaderCrc(firmwareBytes)) const firmwareSizeText = formatBytes(firmwareBytes.length) const validation = firmware.getFirmwareValidation(firmwareBytes.length, state.chipModel) 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 || `请先选择 ${firmware.FLASH_SIZE_TEXT} .bin 文件`) return false } const layout = getFlashLayout() if (!layout) { transport.showCommandAlert('固件大小', `请选择 ${firmware.FLASH_SIZE_TEXT} .bin 文件`) return false } setState({ bootloaderDetailText: '', bootloaderProgress: 0, bootloaderStatusText: '握手中', isBootloaderBusy: true }) try { await handshakeUntilReady() setState({ bootloaderDetailText: '编程解锁', bootloaderStatusText: '升级中' }) assertBootloaderAck(await bootloaderTransport.sendFrame(buildUnlockFrame(), 'Bootloader解锁', 'unlock'), '编程解锁') setState({ bootloaderDetailText: '开启页擦除', bootloaderStatusText: '升级中' }) assertBootloaderAck(await bootloaderTransport.sendFrame(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 bootloaderTransport.sendFrame( buildProgramFrame(address, chunk), `编程 0x${toHex(address, 4)}`, 'program', PROGRAM_RESPONSE_TIMEOUT_MS ) assertBootloaderAck(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 bootloaderTransport.sendFrame(buildFlashCheckFrame(), '全Flash校验', 'flashCheck') await bootloaderTransport.sendFrame(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 bootloaderTransport.sendFrame(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 bootloaderTransport.sendFrame(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 }