| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631 |
- const transport = require('../../transport/ble-core.js')
- const {
- PROGRAM_CHUNK_SIZE,
- alignBootloaderBuffer,
- assertBootloaderAck,
- buildExitFrame,
- buildFlashCheckFrame,
- buildHandshakeFrame,
- buildPageEraseFrame,
- buildProgramFrame,
- buildUnlockFrame,
- calculateBootloaderCrc,
- formatBootloaderCrc,
- getBootloaderExpectedLength,
- parseBootloaderResponse,
- toHex
- } = require('../../protocols/bootloader/frame.js')
- const {
- delay
- } = require('../../utils/base-utils.js')
- const {
- isCancelError,
- loadSelectedFile
- } = require('../../repositories/file.js')
- const {
- formatBytes
- } = require('../../utils/binary-utils.js')
- 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 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 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 getHandshakeDetail(response) {
- if (!response) return '--'
- return `${response.versionText || '--'} / ${response.chipIdText || '--'}`
- }
- function waitForResponse(kind, timeout, options = {}) {
- const expectedLength = getBootloaderExpectedLength(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 = parseBootloaderResponse(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 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: '握手中'
- })
- 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 = formatBootloaderCrc(calculateBootloaderCrc(firmwareBytes))
- 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: '升级中'
- })
- assertBootloaderAck(await sendBootloaderFrame(buildUnlockFrame(), 'Bootloader解锁', 'unlock'), '编程解锁')
- setState({
- bootloaderDetailText: '开启页擦除',
- bootloaderStatusText: '升级中'
- })
- assertBootloaderAck(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
- )
- 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 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
- }
|