service.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. const transport = require('../../transport/ble-core.js')
  2. const {
  3. PROGRAM_CHUNK_SIZE,
  4. alignBootloaderBuffer,
  5. assertBootloaderAck,
  6. buildExitFrame,
  7. buildFlashCheckFrame,
  8. buildHandshakeFrame,
  9. buildPageEraseFrame,
  10. buildProgramFrame,
  11. buildUnlockFrame,
  12. calculateBootloaderCrc,
  13. formatBootloaderCrc,
  14. getBootloaderExpectedLength,
  15. parseBootloaderResponse,
  16. toHex
  17. } = require('../../protocols/bootloader/frame.js')
  18. const {
  19. delay
  20. } = require('../../utils/base-utils.js')
  21. const {
  22. isCancelError,
  23. loadSelectedFile
  24. } = require('../../repositories/file.js')
  25. const {
  26. formatBytes
  27. } = require('../../utils/binary-utils.js')
  28. const HANDSHAKE_INTERVAL_MS = 200
  29. const HANDSHAKE_ATTEMPTS = 10
  30. const HANDSHAKE_TIMEOUT_MS = HANDSHAKE_INTERVAL_MS * HANDSHAKE_ATTEMPTS
  31. const RESPONSE_TIMEOUT_MS = 3000
  32. const PROGRAM_RESPONSE_TIMEOUT_MS = 6000
  33. const FILE_SIZES = {
  34. 16: 16 * 1024,
  35. 32: 32 * 1024
  36. }
  37. const FLASH_LAYOUTS = {
  38. 16: {
  39. capacity: FILE_SIZES[16],
  40. startAddress: 0x0400,
  41. endAddress: 0x4000
  42. },
  43. 32: {
  44. capacity: FILE_SIZES[32],
  45. startAddress: 0x0800,
  46. endAddress: 0x8000
  47. }
  48. }
  49. const FLASH_SIZE_TEXT = Object.keys(FILE_SIZES)
  50. .map((sizeKb) => formatBytes(FILE_SIZES[sizeKb]))
  51. .join(' 或 ')
  52. const CHIP_FLASH_SIZE_KB = {
  53. FU6572L: 32,
  54. FU6572N: 32,
  55. FU6572T: 32,
  56. FU6565N: 32,
  57. FU6565T: 32,
  58. FU6563N: 32,
  59. FU6562L: 32,
  60. FU6562LA: 32,
  61. FU6562Q: 32,
  62. FU6562S: 32,
  63. FU6562T: 32,
  64. FU6532N: 32,
  65. FU6532T: 32,
  66. FU6522L: 32,
  67. FU6522N: 32,
  68. FU6522T: 32,
  69. FU6812L2: 16,
  70. FU6812N2: 16,
  71. FU6812S2: 16,
  72. FU6812V: 16,
  73. FU6861Q2: 16,
  74. FU6861N2: 16,
  75. FU6861NF2: 16,
  76. FU6861L2: 16,
  77. FU6862L: 16,
  78. FU6862Q: 16,
  79. FU6872P: 16
  80. }
  81. const CHIP_FAMILY_FLASH_SIZE_KB = {
  82. 65: 32,
  83. 68: 16
  84. }
  85. const state = {
  86. bootloaderChipId: '--',
  87. bootloaderDetailText: '',
  88. bootloaderProgress: 0,
  89. bootloaderStatusText: '',
  90. bootloaderVersion: '--',
  91. chipModel: '--',
  92. deviceProgramCrcText: '--',
  93. firmwareChecksumText: '--',
  94. firmwareName: '',
  95. firmwareSize: 0,
  96. firmwareSizeText: '--',
  97. firmwareValidText: '未选择',
  98. isBootloaderBusy: false,
  99. isFirmwareReady: false
  100. }
  101. let firmwareBytes = null
  102. let initialized = false
  103. let unsubscribeTransport = null
  104. let activeResponseWaiter = null
  105. const subscribers = []
  106. function getState() {
  107. return {
  108. ...state
  109. }
  110. }
  111. function setState(changedData) {
  112. Object.assign(state, changedData)
  113. subscribers.slice().forEach((subscriber) => {
  114. subscriber(getState())
  115. })
  116. }
  117. function abortActiveResponseWaiter(message) {
  118. if (!activeResponseWaiter) return false
  119. const waiter = activeResponseWaiter
  120. activeResponseWaiter = null
  121. waiter.abort(new Error(message || '蓝牙已断开'))
  122. return true
  123. }
  124. function subscribe(subscriber) {
  125. if (typeof subscriber !== 'function') return () => {}
  126. subscribers.push(subscriber)
  127. subscriber(getState())
  128. return () => {
  129. const index = subscribers.indexOf(subscriber)
  130. if (index >= 0) subscribers.splice(index, 1)
  131. }
  132. }
  133. function init() {
  134. transport.init()
  135. if (initialized) return
  136. unsubscribeTransport = transport.subscribe((transportState) => {
  137. if (!transportState.connectedDevice) {
  138. abortActiveResponseWaiter('蓝牙已断开')
  139. }
  140. if (!transportState.connectedDevice && state.isBootloaderBusy) {
  141. transport.showCommandAlert('BootLoader', '蓝牙已断开,升级已停止')
  142. setState({
  143. bootloaderDetailText: '蓝牙已断开,升级已停止',
  144. bootloaderStatusText: '升级失败',
  145. isBootloaderBusy: false
  146. })
  147. }
  148. })
  149. initialized = true
  150. }
  151. function normalizeModel(value) {
  152. const text = String(value || '').trim()
  153. return text && text !== '--' ? text : ''
  154. }
  155. function extractChipModels(chipModel) {
  156. const model = normalizeModel(chipModel).toUpperCase()
  157. return model.match(/FU\d{4}[A-Z0-9]*/g) || []
  158. }
  159. function inferFlashSizeKb(chipModel) {
  160. const models = extractChipModels(chipModel)
  161. for (const model of models) {
  162. if (CHIP_FLASH_SIZE_KB[model]) return CHIP_FLASH_SIZE_KB[model]
  163. const family = model.slice(2, 4)
  164. if (CHIP_FAMILY_FLASH_SIZE_KB[family]) return CHIP_FAMILY_FLASH_SIZE_KB[family]
  165. }
  166. const text = normalizeModel(chipModel).toUpperCase()
  167. if (/(^|[^0-9])32\s*K(B)?([^0-9]|$)/.test(text)) return 32
  168. if (/(^|[^0-9])16\s*K(B)?([^0-9]|$)/.test(text)) return 16
  169. return null
  170. }
  171. function inferFlashSizeKbFromBytes(byteLength) {
  172. return Number(Object.keys(FILE_SIZES).find((sizeKb) => FILE_SIZES[sizeKb] === byteLength)) || null
  173. }
  174. function inferUpgradeFlashSizeKb(chipModel, byteLength) {
  175. return inferFlashSizeKb(chipModel) || inferFlashSizeKbFromBytes(byteLength)
  176. }
  177. function getFlashLayout() {
  178. const sizeKb = inferUpgradeFlashSizeKb(state.chipModel, state.firmwareSize)
  179. return sizeKb ? FLASH_LAYOUTS[sizeKb] : null
  180. }
  181. function getFirmwareValidation(byteLength = state.firmwareSize, chipModel = state.chipModel) {
  182. const chipSizeKb = inferFlashSizeKb(chipModel)
  183. const firmwareSizeKb = inferFlashSizeKbFromBytes(byteLength)
  184. const sizeKb = chipSizeKb || firmwareSizeKb
  185. const layout = sizeKb ? FLASH_LAYOUTS[sizeKb] : null
  186. const normalizedChipModel = normalizeModel(chipModel)
  187. if (!byteLength) {
  188. return {
  189. isReady: false,
  190. text: chipSizeKb
  191. ? `需要 ${formatBytes(FLASH_LAYOUTS[chipSizeKb].capacity)} .bin`
  192. : `请选择 ${FLASH_SIZE_TEXT} .bin`
  193. }
  194. }
  195. if (!layout) {
  196. return {
  197. isReady: false,
  198. text: `文件 ${formatBytes(byteLength)},应为 ${FLASH_SIZE_TEXT}`
  199. }
  200. }
  201. if (byteLength !== layout.capacity) {
  202. return {
  203. isReady: false,
  204. text: `文件 ${formatBytes(byteLength)},应为 ${formatBytes(layout.capacity)}`
  205. }
  206. }
  207. return {
  208. isReady: true,
  209. text: chipSizeKb
  210. ? `匹配 ${formatBytes(layout.capacity)}`
  211. : `${normalizedChipModel ? `${normalizedChipModel} 未识别,` : '未读到芯片型号,'}按 ${formatBytes(layout.capacity)} 尝试`
  212. }
  213. }
  214. function setChipModel(chipModel) {
  215. const nextChipModel = normalizeModel(chipModel) || '--'
  216. const validation = getFirmwareValidation(state.firmwareSize, nextChipModel)
  217. setState({
  218. chipModel: nextChipModel,
  219. bootloaderDetailText: validation.text,
  220. firmwareValidText: validation.text,
  221. isFirmwareReady: validation.isReady
  222. })
  223. }
  224. function getHandshakeDetail(response) {
  225. if (!response) return '--'
  226. return `${response.versionText || '--'} / ${response.chipIdText || '--'}`
  227. }
  228. function waitForResponse(kind, timeout, options = {}) {
  229. const expectedLength = getBootloaderExpectedLength(kind)
  230. const buffer = []
  231. return new Promise((resolve, reject) => {
  232. let settled = false
  233. let timer = null
  234. let unsubscribe = () => {}
  235. const waiter = {
  236. abort: (error) => {
  237. cleanup()
  238. reject(error)
  239. }
  240. }
  241. abortActiveResponseWaiter('新的 BootLoader 响应等待已开始')
  242. activeResponseWaiter = waiter
  243. unsubscribe = transport.subscribeRawResponse((bytes) => {
  244. buffer.push.apply(buffer, bytes)
  245. alignBootloaderBuffer(buffer)
  246. if (buffer.length < expectedLength) return
  247. const frame = buffer.slice(0, expectedLength)
  248. try {
  249. const response = parseBootloaderResponse(frame, kind)
  250. cleanup()
  251. resolve(response)
  252. } catch (error) {
  253. if (options.ignoreInvalid) {
  254. buffer.shift()
  255. return
  256. }
  257. cleanup()
  258. reject(error)
  259. }
  260. })
  261. timer = setTimeout(() => {
  262. cleanup()
  263. reject(new Error(`${kind} 响应超时`))
  264. }, timeout || RESPONSE_TIMEOUT_MS)
  265. function cleanup() {
  266. if (settled) return
  267. settled = true
  268. clearTimeout(timer)
  269. if (activeResponseWaiter === waiter) {
  270. activeResponseWaiter = null
  271. }
  272. unsubscribe()
  273. }
  274. })
  275. }
  276. async function sendBootloaderFrame(frame, label, kind, timeout) {
  277. const responsePromise = kind ? waitForResponse(kind, timeout) : null
  278. const sent = await transport.sendRawFrameExact(frame, label)
  279. if (!sent) {
  280. if (responsePromise) {
  281. responsePromise.catch(() => {})
  282. abortActiveResponseWaiter(`${label}发送失败`)
  283. }
  284. throw new Error(`${label}发送失败`)
  285. }
  286. return responsePromise ? responsePromise : true
  287. }
  288. async function sendHandshakeKeepAlive() {
  289. if (state.isBootloaderBusy) return false
  290. const frame = buildHandshakeFrame()
  291. let finished = false
  292. setState({
  293. bootloaderDetailText: `0/${HANDSHAKE_ATTEMPTS}`,
  294. bootloaderProgress: 0,
  295. bootloaderStatusText: '握手中',
  296. isBootloaderBusy: true
  297. })
  298. const responsePromise = waitForResponse('handshake', HANDSHAKE_TIMEOUT_MS, {
  299. ignoreInvalid: true
  300. }).then((response) => {
  301. finished = true
  302. return response
  303. }).catch((error) => {
  304. finished = true
  305. throw error
  306. })
  307. responsePromise.catch(() => {})
  308. try {
  309. for (let attempt = 0; attempt < HANDSHAKE_ATTEMPTS && !finished; attempt += 1) {
  310. const sent = await transport.sendRawFrameExact(frame, 'Bootloader握手')
  311. if (!sent) throw new Error('握手帧发送失败')
  312. setState({
  313. bootloaderDetailText: `${attempt + 1}/${HANDSHAKE_ATTEMPTS}`,
  314. bootloaderProgress: Math.round((attempt + 1) / HANDSHAKE_ATTEMPTS * 100)
  315. })
  316. if (attempt < HANDSHAKE_ATTEMPTS - 1) {
  317. await delay(HANDSHAKE_INTERVAL_MS)
  318. }
  319. }
  320. const response = await responsePromise
  321. setState({
  322. bootloaderChipId: response.chipIdText,
  323. bootloaderDetailText: getHandshakeDetail(response),
  324. bootloaderProgress: 100,
  325. bootloaderStatusText: '握手成功',
  326. bootloaderVersion: response.versionText,
  327. isBootloaderBusy: false
  328. })
  329. return true
  330. } catch (error) {
  331. abortActiveResponseWaiter('握手已停止')
  332. const message = error && error.message ? error.message : '握手失败'
  333. transport.showCommandAlert('BootLoader握手', message)
  334. setState({
  335. bootloaderDetailText: message,
  336. bootloaderStatusText: '握手失败',
  337. isBootloaderBusy: false
  338. })
  339. return false
  340. }
  341. }
  342. async function handshakeUntilReady() {
  343. const frame = buildHandshakeFrame()
  344. setState({
  345. bootloaderDetailText: '等待握手',
  346. bootloaderProgress: 0,
  347. bootloaderStatusText: '握手中'
  348. })
  349. let lastError = null
  350. let finished = false
  351. const responsePromise = waitForResponse('handshake', HANDSHAKE_TIMEOUT_MS, {
  352. ignoreInvalid: true
  353. }).then((response) => {
  354. finished = true
  355. return response
  356. }).catch((error) => {
  357. finished = true
  358. throw error
  359. })
  360. for (let attempt = 0; attempt < HANDSHAKE_ATTEMPTS && !finished; attempt += 1) {
  361. try {
  362. await transport.sendRawFrameExact(frame, 'Bootloader握手')
  363. } catch (error) {
  364. lastError = error
  365. }
  366. if (!finished && attempt < HANDSHAKE_ATTEMPTS - 1) {
  367. await delay(HANDSHAKE_INTERVAL_MS)
  368. }
  369. }
  370. try {
  371. const response = await responsePromise
  372. setState({
  373. bootloaderChipId: response.chipIdText,
  374. bootloaderDetailText: getHandshakeDetail(response),
  375. bootloaderVersion: response.versionText,
  376. bootloaderStatusText: '握手成功'
  377. })
  378. return response
  379. } catch (error) {
  380. throw new Error(lastError ? `Bootloader握手失败:${lastError.message}` : `Bootloader握手失败:${error.message}`)
  381. }
  382. }
  383. async function chooseFirmwareFile(source = 'message') {
  384. if (state.isBootloaderBusy) return false
  385. try {
  386. const file = await loadSelectedFile(source, {
  387. extensionMessage: '请选择 .bin 固件文件',
  388. extensions: ['bin'],
  389. fallbackName: 'firmware.bin'
  390. })
  391. firmwareBytes = file.bytes
  392. const firmwareCrcText = formatBootloaderCrc(calculateBootloaderCrc(firmwareBytes))
  393. const firmwareSizeText = formatBytes(firmwareBytes.length)
  394. const validation = getFirmwareValidation(firmwareBytes.length)
  395. setState({
  396. bootloaderDetailText: validation.text,
  397. bootloaderProgress: 0,
  398. bootloaderStatusText: validation.isReady ? '固件已加载' : '固件不匹配',
  399. deviceProgramCrcText: '--',
  400. firmwareChecksumText: firmwareCrcText,
  401. firmwareName: file.name || 'firmware.bin',
  402. firmwareSize: firmwareBytes.length,
  403. firmwareSizeText,
  404. firmwareValidText: validation.text,
  405. isFirmwareReady: validation.isReady
  406. })
  407. return true
  408. } catch (error) {
  409. const message = error && (error.errMsg || error.message)
  410. ? (error.errMsg || error.message)
  411. : '读取固件失败'
  412. if (!isCancelError(error)) {
  413. transport.showCommandAlert('固件文件', message)
  414. }
  415. return false
  416. }
  417. }
  418. async function startUpgrade() {
  419. if (state.isBootloaderBusy) return false
  420. if (!firmwareBytes || !state.isFirmwareReady) {
  421. transport.showCommandAlert('固件不匹配', state.firmwareValidText || `请先选择 ${FLASH_SIZE_TEXT} .bin 文件`)
  422. return false
  423. }
  424. const layout = getFlashLayout()
  425. if (!layout) {
  426. transport.showCommandAlert('固件大小', `请选择 ${FLASH_SIZE_TEXT} .bin 文件`)
  427. return false
  428. }
  429. setState({
  430. bootloaderDetailText: '',
  431. bootloaderProgress: 0,
  432. bootloaderStatusText: '握手中',
  433. isBootloaderBusy: true
  434. })
  435. try {
  436. await handshakeUntilReady()
  437. setState({
  438. bootloaderDetailText: '编程解锁',
  439. bootloaderStatusText: '升级中'
  440. })
  441. assertBootloaderAck(await sendBootloaderFrame(buildUnlockFrame(), 'Bootloader解锁', 'unlock'), '编程解锁')
  442. setState({
  443. bootloaderDetailText: '开启页擦除',
  444. bootloaderStatusText: '升级中'
  445. })
  446. assertBootloaderAck(await sendBootloaderFrame(buildPageEraseFrame(true), '页擦除使能', 'pageErase'), '页擦除使能')
  447. const totalBytes = layout.endAddress - layout.startAddress
  448. let programmedBytes = 0
  449. for (let address = layout.startAddress; address < layout.endAddress; address += PROGRAM_CHUNK_SIZE) {
  450. const chunk = firmwareBytes.slice(address, address + PROGRAM_CHUNK_SIZE)
  451. const response = await sendBootloaderFrame(
  452. buildProgramFrame(address, chunk),
  453. `编程 0x${toHex(address, 4)}`,
  454. 'program',
  455. PROGRAM_RESPONSE_TIMEOUT_MS
  456. )
  457. assertBootloaderAck(response, `编程 0x${toHex(address, 4)}`)
  458. if (response.address !== address) {
  459. throw new Error(`编程地址反馈不匹配:0x${toHex(response.address, 4)}`)
  460. }
  461. programmedBytes = Math.min(totalBytes, programmedBytes + PROGRAM_CHUNK_SIZE)
  462. const progress = Math.min(99, Math.round(programmedBytes / totalBytes * 100))
  463. setState({
  464. bootloaderDetailText: `0x${toHex(address, 4)}`,
  465. bootloaderProgress: progress,
  466. bootloaderStatusText: `升级中 ${progress}%`
  467. })
  468. }
  469. const checkResponse = await sendBootloaderFrame(buildFlashCheckFrame(), '全Flash校验', 'flashCheck')
  470. await sendBootloaderFrame(buildExitFrame(), '退出Bootloader')
  471. setState({
  472. bootloaderDetailText: '校验通过',
  473. bootloaderProgress: 100,
  474. bootloaderStatusText: '升级完成',
  475. deviceProgramCrcText: checkResponse.flashCrcText,
  476. isBootloaderBusy: false
  477. })
  478. return true
  479. } catch (error) {
  480. const message = error && error.message ? error.message : '升级失败'
  481. transport.showCommandAlert('Bootloader升级', message)
  482. setState({
  483. bootloaderDetailText: message,
  484. bootloaderStatusText: '升级失败',
  485. isBootloaderBusy: false
  486. })
  487. return false
  488. }
  489. }
  490. async function readProgramChecksum() {
  491. if (state.isBootloaderBusy) return false
  492. setState({
  493. bootloaderDetailText: '',
  494. bootloaderStatusText: '读取中'
  495. })
  496. try {
  497. const response = await sendBootloaderFrame(buildFlashCheckFrame(), '读取程序校验码', 'flashCheck')
  498. setState({
  499. bootloaderDetailText: '程序校验码已读取',
  500. bootloaderStatusText: '读取完成',
  501. deviceProgramCrcText: response.flashCrcText
  502. })
  503. return true
  504. } catch (error) {
  505. const message = error && error.message ? error.message : '读取程序校验码失败'
  506. transport.showCommandAlert('程序校验码', message)
  507. setState({
  508. bootloaderDetailText: message,
  509. bootloaderStatusText: '读取失败'
  510. })
  511. return false
  512. }
  513. }
  514. async function exitBootloader() {
  515. if (state.isBootloaderBusy) return false
  516. try {
  517. const sent = await sendBootloaderFrame(buildExitFrame(), '退出BootLoader')
  518. if (!sent) throw new Error('退出命令发送失败')
  519. setState({
  520. bootloaderDetailText: '',
  521. bootloaderStatusText: '已退出 BootLoader'
  522. })
  523. return true
  524. } catch (error) {
  525. const message = error && error.message ? error.message : '退出 BootLoader 失败'
  526. transport.showCommandAlert('退出 BootLoader', message)
  527. setState({
  528. bootloaderDetailText: message,
  529. bootloaderStatusText: '退出失败'
  530. })
  531. return false
  532. }
  533. }
  534. module.exports = {
  535. chooseFirmwareFile,
  536. getState,
  537. init,
  538. readProgramChecksum,
  539. sendHandshakeKeepAlive,
  540. setChipModel,
  541. exitBootloader,
  542. startUpgrade,
  543. subscribe
  544. }