1
0

service.js 17 KB

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