1
0

ble-core.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278
  1. const {
  2. notifyPageToast
  3. } = require('../utils/page-toast.js')
  4. const {
  5. bytesToUtf8Text
  6. } = require('../utils/binary-utils.js')
  7. const {
  8. DEFAULT_MAX_FRAME_BYTES,
  9. arrayBufferToHex,
  10. buildCharacteristicText,
  11. formatBluetoothError,
  12. formatFrameHex,
  13. getCharacteristicRole,
  14. hasTargetCharacteristic,
  15. hexToArrayBuffer,
  16. inferPacketSize,
  17. isConnectionLostError,
  18. isTargetUuid,
  19. normalizeMaxFrameBytes,
  20. resolvePacketSize,
  21. validateHex
  22. } = require('./ble-utils.js')
  23. const {
  24. DEFAULT_MAX_LOG_COUNT,
  25. appendLog,
  26. createClearLogsState,
  27. createLogItem
  28. } = require('./ble-logs.js')
  29. const {
  30. createProtocolHelperRegistry
  31. } = require('./protocol-helper-registry.js')
  32. const {
  33. createBleDeviceRegistry
  34. } = require('./ble-device-registry.js')
  35. const SCAN_TIMEOUT = 15000
  36. const CONNECT_TIMEOUT = 10000
  37. const RSSI_REFRESH_INTERVAL = 2000
  38. const RESPONSE_TIMEOUT = 1000
  39. const MAX_RESPONSE_BUFFER_BYTES = 128
  40. const state = {
  41. adapterAvailable: false,
  42. adapterOpened: false,
  43. characteristicText: '未选择',
  44. connectedDevice: null,
  45. connectedServiceCount: 0,
  46. connectingDeviceId: '',
  47. devices: [],
  48. errorText: '',
  49. isAwaitingResponse: false,
  50. isConnecting: false,
  51. isDiscovering: false,
  52. isSending: false,
  53. logScrollTarget: '',
  54. logs: [],
  55. rxCount: 0,
  56. sendHex: '',
  57. sendQueueLength: 0,
  58. systemTip: '',
  59. signalText: '',
  60. txCount: 0,
  61. writeCharacteristicId: '',
  62. writeServiceId: '',
  63. writeType: ''
  64. }
  65. let initialized = false
  66. let scanTimer = null
  67. let rssiTimer = null
  68. let isReadingRssi = false
  69. let pendingRequest = null
  70. let sendQueue = []
  71. let isProcessingSendQueue = false
  72. let sendQueueGeneration = 0
  73. let sendJobSequence = 0
  74. let logSequence = 0
  75. const subscribers = []
  76. const rawResponseSubscribers = []
  77. const protocolHelperRegistry = createProtocolHelperRegistry()
  78. const deviceRegistry = createBleDeviceRegistry()
  79. function configureProtocolHelpers(helpers = {}) {
  80. protocolHelperRegistry.configure(helpers)
  81. }
  82. function setState(changedData) {
  83. Object.assign(state, changedData)
  84. subscribers.slice().forEach((subscriber) => {
  85. subscriber(getState())
  86. })
  87. }
  88. function getState() {
  89. return {
  90. ...state,
  91. devices: state.devices.slice(),
  92. logs: state.logs.slice()
  93. }
  94. }
  95. function subscribe(subscriber) {
  96. if (typeof subscriber !== 'function') return () => {}
  97. subscribers.push(subscriber)
  98. subscriber(getState())
  99. return () => {
  100. const index = subscribers.indexOf(subscriber)
  101. if (index >= 0) subscribers.splice(index, 1)
  102. }
  103. }
  104. function subscribeRawResponse(subscriber) {
  105. if (typeof subscriber !== 'function') return () => {}
  106. rawResponseSubscribers.push(subscriber)
  107. return () => {
  108. const index = rawResponseSubscribers.indexOf(subscriber)
  109. if (index >= 0) rawResponseSubscribers.splice(index, 1)
  110. }
  111. }
  112. function callWx(apiName, params = {}) {
  113. return new Promise((resolve, reject) => {
  114. const api = wx[apiName]
  115. if (typeof api !== 'function') {
  116. reject(new Error(`${apiName} 不可用`))
  117. return
  118. }
  119. api({
  120. ...params,
  121. success: resolve,
  122. fail: reject
  123. })
  124. })
  125. }
  126. function getResponseBufferHint(expected, options = {}) {
  127. if (Number.isFinite(Number(options.responseBufferHint))) {
  128. return Math.max(0, Math.round(Number(options.responseBufferHint)))
  129. }
  130. const getHint = protocolHelperRegistry.get('getResponseBufferHint')
  131. if (typeof getHint === 'function') {
  132. return Math.max(0, Math.round(Number(getHint(expected)) || 0))
  133. }
  134. return 0
  135. }
  136. function getResponseBufferLimit(expected, options = {}) {
  137. const responseLength = getResponseBufferHint(expected, options)
  138. const maxFrameBytes = options.maxFrameBytes
  139. const frameLimit = normalizeMaxFrameBytes(maxFrameBytes)
  140. if (frameLimit === 0) {
  141. return Math.max(MAX_RESPONSE_BUFFER_BYTES, responseLength + 8)
  142. }
  143. return Math.max(MAX_RESPONSE_BUFFER_BYTES, frameLimit + 8, responseLength + 8)
  144. }
  145. function validateDmaFrameLength(bytes, options = {}) {
  146. const maxFrameBytes = normalizeMaxFrameBytes(options.maxFrameBytes)
  147. if (maxFrameBytes === 0) return ''
  148. if (bytes.length > maxFrameBytes) {
  149. return `发送帧长度 ${bytes.length} 字节,超过最大包长 ${maxFrameBytes} 字节限制`
  150. }
  151. if (!options.expected) return ''
  152. const responseLength = getResponseBufferHint(options.expected, options)
  153. if (responseLength > maxFrameBytes) {
  154. return `预计返回帧长度 ${responseLength} 字节,超过最大包长 ${maxFrameBytes} 字节限制`
  155. }
  156. return ''
  157. }
  158. function addLog(direction, payload, note = '', extras = {}) {
  159. logSequence += 1
  160. const logItem = createLogItem(direction, payload, note, extras, logSequence)
  161. const nextLogs = appendLog(state.logs, logItem, DEFAULT_MAX_LOG_COUNT)
  162. setState({
  163. logScrollTarget: logItem.id,
  164. logs: nextLogs
  165. })
  166. }
  167. function getReceiveCrcState(rawBytes) {
  168. if (!rawBytes || rawBytes.length < 4) return ''
  169. const inspectReceivedBytes = protocolHelperRegistry.get('inspectReceivedBytes')
  170. if (typeof inspectReceivedBytes === 'function') {
  171. const note = inspectReceivedBytes(rawBytes, {
  172. pendingRequest: pendingRequest ? pendingRequest.expected : null
  173. })
  174. if (note) return note
  175. }
  176. return pendingRequest ? '片段' : ''
  177. }
  178. function showCommandAlert(title, content) {
  179. const message = content || title || '操作失败'
  180. notifyPageToast(message, 'error')
  181. setState({
  182. errorText: message
  183. })
  184. }
  185. function clearScanTimer() {
  186. if (!scanTimer) return
  187. clearTimeout(scanTimer)
  188. scanTimer = null
  189. }
  190. async function stopScan() {
  191. clearScanTimer()
  192. try {
  193. await callWx('stopBluetoothDevicesDiscovery')
  194. } catch (error) {
  195. if (error.errCode !== 10000) {
  196. setState({
  197. errorText: formatBluetoothError(error)
  198. })
  199. }
  200. }
  201. setState({
  202. isDiscovering: false
  203. })
  204. }
  205. function resetScanTimer() {
  206. clearScanTimer()
  207. scanTimer = setTimeout(() => {
  208. stopScan()
  209. if (!state.devices.length) {
  210. setState({
  211. systemTip: '安卓真机请确认系统定位已开启,并允许微信使用附近设备或位置信息。'
  212. })
  213. }
  214. }, SCAN_TIMEOUT)
  215. }
  216. function mergeDevices(devices) {
  217. const changed = deviceRegistry.mergeDevices(devices)
  218. if (!changed) return
  219. setState({
  220. devices: deviceRegistry.getDeviceList()
  221. })
  222. }
  223. function clearPendingRequest() {
  224. if (!pendingRequest) return null
  225. const pending = pendingRequest
  226. clearTimeout(pendingRequest.timer)
  227. pendingRequest = null
  228. setState({
  229. isAwaitingResponse: false
  230. })
  231. return pending
  232. }
  233. function cancelPendingRequest() {
  234. const pending = clearPendingRequest()
  235. if (pending) {
  236. pending.resolve(false)
  237. }
  238. }
  239. function clearSendQueue() {
  240. if (!sendQueue.length) return
  241. const queuedJobs = sendQueue.splice(0)
  242. queuedJobs.forEach((job) => {
  243. job.resolve(false)
  244. })
  245. setState({
  246. sendQueueLength: 0
  247. })
  248. }
  249. function resetSendRuntimeState() {
  250. sendQueueGeneration += 1
  251. cancelPendingRequest()
  252. clearSendQueue()
  253. isProcessingSendQueue = false
  254. setState({
  255. isAwaitingResponse: false,
  256. isSending: false,
  257. sendQueueLength: 0
  258. })
  259. }
  260. function clearConnectedState(changedData = {}) {
  261. stopRssiRefresh()
  262. resetSendRuntimeState()
  263. setState({
  264. characteristicText: '未选择',
  265. connectedDevice: null,
  266. connectedServiceCount: 0,
  267. connectingDeviceId: '',
  268. isConnecting: false,
  269. signalText: '',
  270. writeCharacteristicId: '',
  271. writeServiceId: '',
  272. writeType: '',
  273. ...changedData
  274. })
  275. }
  276. function stopRssiRefresh() {
  277. if (rssiTimer) {
  278. clearInterval(rssiTimer)
  279. rssiTimer = null
  280. }
  281. isReadingRssi = false
  282. }
  283. function applyRssiUpdate(deviceId, rssi) {
  284. if (!state.connectedDevice || state.connectedDevice.deviceId !== deviceId || typeof rssi !== 'number') {
  285. return
  286. }
  287. const result = deviceRegistry.applyRssiUpdate(deviceId, rssi, state.connectedDevice)
  288. if (!result || !result.updatedDevice) return
  289. setState({
  290. connectedDevice: result.updatedDevice,
  291. signalText: result.updatedDevice.signalText || '',
  292. devices: result.deviceList
  293. })
  294. }
  295. async function refreshConnectedRssi() {
  296. const { connectedDevice } = state
  297. if (!connectedDevice || typeof wx === 'undefined' || typeof wx.getBLEDeviceRSSI !== 'function') return
  298. if (isReadingRssi) return
  299. isReadingRssi = true
  300. try {
  301. const result = await callWx('getBLEDeviceRSSI', {
  302. deviceId: connectedDevice.deviceId
  303. })
  304. if (!state.connectedDevice || state.connectedDevice.deviceId !== connectedDevice.deviceId) return
  305. applyRssiUpdate(connectedDevice.deviceId, result && result.RSSI)
  306. } catch (error) {
  307. if (isConnectionLostError(error)) {
  308. clearConnectedState({
  309. errorText: formatBluetoothError(error)
  310. })
  311. }
  312. } finally {
  313. isReadingRssi = false
  314. }
  315. }
  316. function startRssiRefresh() {
  317. stopRssiRefresh()
  318. if (!state.connectedDevice || typeof wx === 'undefined' || typeof wx.getBLEDeviceRSSI !== 'function') {
  319. return
  320. }
  321. refreshConnectedRssi()
  322. rssiTimer = setInterval(() => {
  323. refreshConnectedRssi()
  324. }, RSSI_REFRESH_INTERVAL)
  325. }
  326. function finishPendingRequest(resolveValue) {
  327. const pending = clearPendingRequest()
  328. if (pending) {
  329. pending.resolve(resolveValue)
  330. }
  331. }
  332. function consumePendingResponseBuffer() {
  333. const pending = pendingRequest
  334. if (!pending || !Array.isArray(pending.responseBuffer)) return
  335. const responseReader = pending.responseReader || protocolHelperRegistry.get('readResponseFromBuffer')
  336. if (typeof responseReader !== 'function') {
  337. const content = `${pending.label} 未配置响应解析器,已丢弃`
  338. addLog('SYS', content)
  339. finishPendingRequest(false)
  340. if (pending.showModal) {
  341. showCommandAlert('通讯异常', content)
  342. }
  343. return
  344. }
  345. const result = responseReader(pending.responseBuffer, pending.expected, {
  346. maxFrameBytes: pending.expected && pending.expected.maxFrameBytes
  347. })
  348. if (!result || result.status === 'pending') return
  349. if (result.status === 'frame-too-long') {
  350. const content = `${pending.label} 返回帧长度 ${result.responseLength} 字节,超过最大包长 ${result.frameLimit} 字节限制,已丢弃`
  351. addLog('SYS', content)
  352. finishPendingRequest(false)
  353. if (pending.showModal) {
  354. showCommandAlert('通讯异常', content)
  355. }
  356. return
  357. }
  358. if (result.status === 'invalid') {
  359. const content = result.message
  360. ? `${pending.label} 收到无效响应帧:${result.message}`
  361. : `${pending.label} 收到无效响应帧,已丢弃`
  362. addLog('SYS', content)
  363. finishPendingRequest(false)
  364. if (pending.showModal) {
  365. showCommandAlert('通讯异常', content)
  366. }
  367. return
  368. }
  369. if (result.status === 'exception') {
  370. const content = result.message || `${pending.label} 收到异常响应帧`
  371. addLog('SYS', content)
  372. finishPendingRequest(false)
  373. if (pending.showModal) {
  374. showCommandAlert('设备返回故障帧', content)
  375. }
  376. return
  377. }
  378. if (result.status === 'mismatch') {
  379. const content = `${pending.label} 收到不匹配响应,已丢弃`
  380. addLog('SYS', content)
  381. finishPendingRequest(false)
  382. if (pending.showModal) {
  383. showCommandAlert('通讯异常', content)
  384. }
  385. return
  386. }
  387. if (result.status !== 'complete') {
  388. const content = `${pending.label} 收到未知响应状态,已丢弃`
  389. addLog('SYS', content)
  390. finishPendingRequest(false)
  391. if (pending.showModal) {
  392. showCommandAlert('通讯异常', content)
  393. }
  394. return
  395. }
  396. finishPendingRequest(result.response)
  397. if (pending.responseBuffer.length) {
  398. consumePendingResponseBuffer()
  399. }
  400. }
  401. function handleReceivedResponseBytes(bytes) {
  402. if (!pendingRequest || !Array.isArray(bytes) || !bytes.length) return
  403. pendingRequest.responseBuffer = pendingRequest.responseBuffer.concat(bytes)
  404. const bufferLimit = pendingRequest.responseBufferLimit || MAX_RESPONSE_BUFFER_BYTES
  405. if (pendingRequest.responseBuffer.length > bufferLimit) {
  406. const pending = pendingRequest
  407. const content = `${pending.label} 返回数据超过缓冲区,已丢弃`
  408. addLog('SYS', content)
  409. finishPendingRequest(false)
  410. if (pending.showModal) {
  411. showCommandAlert('通讯异常', content)
  412. }
  413. return
  414. }
  415. consumePendingResponseBuffer()
  416. }
  417. function createPendingRequest(label, expected, options = {}) {
  418. return new Promise((resolve) => {
  419. const timer = setTimeout(() => {
  420. const pending = clearPendingRequest()
  421. if (!pending) return
  422. addLog('SYS', `${label} 超时`)
  423. if (options.showModal !== false) {
  424. showCommandAlert('通讯超时', `${label} 1秒内没有收到回复`)
  425. }
  426. resolve(false)
  427. }, options.timeout || RESPONSE_TIMEOUT)
  428. pendingRequest = {
  429. expected,
  430. label,
  431. resolve,
  432. timer,
  433. responseBufferLimit: getResponseBufferLimit(expected, options),
  434. responseReader: typeof options.responseReader === 'function' ? options.responseReader : null,
  435. showModal: options.showModal !== false,
  436. responseBuffer: []
  437. }
  438. setState({
  439. isAwaitingResponse: true
  440. })
  441. })
  442. }
  443. function init() {
  444. if (initialized) return
  445. wx.onBluetoothDeviceFound((res) => {
  446. mergeDevices(res.devices || [])
  447. })
  448. wx.onBluetoothAdapterStateChange((res) => {
  449. setState({
  450. adapterAvailable: !!res.available,
  451. isDiscovering: !!res.discovering
  452. })
  453. if (!res.available) {
  454. clearScanTimer()
  455. clearConnectedState({
  456. adapterAvailable: false,
  457. adapterOpened: false,
  458. errorText: '请开启手机蓝牙后重新扫描',
  459. isDiscovering: false,
  460. sendQueueLength: 0
  461. })
  462. }
  463. })
  464. wx.onBLEConnectionStateChange((res) => {
  465. const { connectedDevice } = state
  466. if (!connectedDevice || connectedDevice.deviceId !== res.deviceId) return
  467. if (!res.connected) {
  468. addLog('SYS', '连接已断开')
  469. clearConnectedState({
  470. errorText: '',
  471. sendQueueLength: 0
  472. })
  473. }
  474. })
  475. wx.onBLECharacteristicValueChange((res) => {
  476. const hex = arrayBufferToHex(res.value)
  477. const byteLength = res.value ? res.value.byteLength : 0
  478. const rawBytes = Array.prototype.slice.call(new Uint8Array(res.value || new ArrayBuffer(0)))
  479. const crcState = getReceiveCrcState(rawBytes)
  480. setState({
  481. rxCount: state.rxCount + byteLength
  482. })
  483. addLog('RX', hex, crcState, {
  484. payloadBytes: rawBytes,
  485. payloadText: bytesToUtf8Text(rawBytes)
  486. })
  487. rawResponseSubscribers.slice().forEach((subscriber) => {
  488. subscriber(rawBytes, res)
  489. })
  490. handleReceivedResponseBytes(rawBytes)
  491. })
  492. initialized = true
  493. }
  494. async function getAuthSetting() {
  495. return callWx('getSetting')
  496. .then((res) => res.authSetting || {})
  497. .catch(() => ({}))
  498. }
  499. function showPermissionModal(title, content) {
  500. return new Promise((resolve, reject) => {
  501. wx.showModal({
  502. title,
  503. content,
  504. confirmText: '去设置',
  505. success: async (res) => {
  506. if (!res.confirm) {
  507. reject(new Error('用户取消授权'))
  508. return
  509. }
  510. try {
  511. await callWx('openSetting')
  512. resolve()
  513. } catch (error) {
  514. reject(error)
  515. }
  516. },
  517. fail: reject
  518. })
  519. })
  520. }
  521. async function ensureBluetoothAuthorized() {
  522. const authSetting = await getAuthSetting()
  523. if (authSetting['scope.bluetooth']) return
  524. if (authSetting['scope.bluetooth'] === false) {
  525. await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。')
  526. return
  527. }
  528. try {
  529. await callWx('authorize', {
  530. scope: 'scope.bluetooth'
  531. })
  532. } catch (error) {
  533. await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。')
  534. }
  535. }
  536. async function ensureAndroidLocationAuthorized() {
  537. const systemInfo = wx.getSystemInfoSync ? wx.getSystemInfoSync() : wx.getDeviceInfo()
  538. if (systemInfo.platform !== 'android') return
  539. const authSetting = await getAuthSetting()
  540. if (authSetting['scope.userLocation']) return
  541. setState({
  542. systemTip: '安卓系统扫描 BLE 设备通常需要开启系统定位权限。'
  543. })
  544. if (authSetting['scope.userLocation'] === false) {
  545. await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。')
  546. return
  547. }
  548. try {
  549. await callWx('authorize', {
  550. scope: 'scope.userLocation'
  551. })
  552. } catch (error) {
  553. await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。')
  554. }
  555. }
  556. async function openAdapter() {
  557. if (state.adapterOpened) {
  558. try {
  559. const adapterState = await callWx('getBluetoothAdapterState')
  560. setState({
  561. adapterAvailable: !!adapterState.available,
  562. isDiscovering: !!adapterState.discovering
  563. })
  564. if (adapterState.available) return
  565. } catch (error) {
  566. setState({
  567. adapterAvailable: false,
  568. adapterOpened: false
  569. })
  570. }
  571. }
  572. try {
  573. await callWx('openBluetoothAdapter', {
  574. mode: 'central'
  575. })
  576. const adapterState = await callWx('getBluetoothAdapterState')
  577. setState({
  578. adapterAvailable: !!adapterState.available,
  579. adapterOpened: true,
  580. isDiscovering: !!adapterState.discovering
  581. })
  582. if (!adapterState.available) {
  583. throw {
  584. errCode: 10001,
  585. errMsg: 'bluetooth adapter not available'
  586. }
  587. }
  588. } catch (error) {
  589. if (error.errCode === 10001) {
  590. setState({
  591. adapterOpened: true,
  592. adapterAvailable: false
  593. })
  594. }
  595. throw error
  596. }
  597. }
  598. async function startDiscovery() {
  599. try {
  600. await callWx('startBluetoothDevicesDiscovery', {
  601. allowDuplicatesKey: true,
  602. interval: 600,
  603. powerLevel: 'high'
  604. })
  605. } catch (error) {
  606. await callWx('startBluetoothDevicesDiscovery', {
  607. allowDuplicatesKey: true,
  608. interval: 600
  609. })
  610. }
  611. }
  612. async function startScan() {
  613. if (state.isConnecting) return
  614. deviceRegistry.clear()
  615. setState({
  616. devices: [],
  617. errorText: ''
  618. })
  619. try {
  620. init()
  621. await ensureBluetoothAuthorized()
  622. await ensureAndroidLocationAuthorized()
  623. await openAdapter()
  624. await startDiscovery()
  625. setState({
  626. isDiscovering: true
  627. })
  628. resetScanTimer()
  629. addLog('SYS', '开始扫描 BLE 设备')
  630. } catch (error) {
  631. clearScanTimer()
  632. setState({
  633. isDiscovering: false,
  634. errorText: formatBluetoothError(error)
  635. })
  636. }
  637. }
  638. function clearDevices() {
  639. deviceRegistry.clear()
  640. setState({
  641. devices: [],
  642. errorText: ''
  643. })
  644. }
  645. async function closeConnectedDevice(nextDeviceId, options = {}) {
  646. const { connectedDevice } = state
  647. if (!connectedDevice) {
  648. resetSendRuntimeState()
  649. return
  650. }
  651. if (connectedDevice.deviceId === nextDeviceId && !options.force) return
  652. resetSendRuntimeState()
  653. try {
  654. await callWx('closeBLEConnection', {
  655. deviceId: connectedDevice.deviceId
  656. })
  657. } catch (error) {
  658. if (error.errCode !== 10006) throw error
  659. }
  660. clearConnectedState()
  661. }
  662. async function discoverCharacteristics(deviceId) {
  663. const serviceResult = await callWx('getBLEDeviceServices', {
  664. deviceId
  665. })
  666. const services = []
  667. let writeServiceId = ''
  668. let writeCharacteristicId = ''
  669. let writeType = ''
  670. let notifyServiceId = ''
  671. let notifyCharacteristicId = ''
  672. for (const service of serviceResult.services || []) {
  673. const characteristicResult = await callWx('getBLEDeviceCharacteristics', {
  674. deviceId,
  675. serviceId: service.uuid
  676. })
  677. const characteristics = (characteristicResult.characteristics || []).map((item) => ({
  678. uuid: item.uuid,
  679. role: getCharacteristicRole(item.properties),
  680. properties: item.properties || {}
  681. }))
  682. services.push({
  683. uuid: service.uuid,
  684. primary: service.isPrimary,
  685. characteristics
  686. })
  687. characteristics.forEach((item) => {
  688. const isPreferredService = isTargetUuid(service.uuid)
  689. const isPreferredCharacteristic = isTargetUuid(item.uuid)
  690. const canWrite = item.properties.write || item.properties.writeNoResponse
  691. const canNotify = item.properties.notify || item.properties.indicate
  692. if (isPreferredService && isPreferredCharacteristic && canWrite) {
  693. writeServiceId = service.uuid
  694. writeCharacteristicId = item.uuid
  695. writeType = item.properties.write ? 'write' : 'writeNoResponse'
  696. }
  697. if (isPreferredService && isPreferredCharacteristic && canNotify) {
  698. notifyServiceId = service.uuid
  699. notifyCharacteristicId = item.uuid
  700. }
  701. if (!writeCharacteristicId && canWrite) {
  702. writeServiceId = service.uuid
  703. writeCharacteristicId = item.uuid
  704. writeType = item.properties.write ? 'write' : 'writeNoResponse'
  705. }
  706. if (!notifyCharacteristicId && canNotify) {
  707. notifyServiceId = service.uuid
  708. notifyCharacteristicId = item.uuid
  709. }
  710. })
  711. }
  712. return {
  713. services,
  714. writeServiceId,
  715. writeCharacteristicId,
  716. writeType,
  717. notifyServiceId,
  718. notifyCharacteristicId
  719. }
  720. }
  721. async function enableNotify(deviceId, serviceId, characteristicId) {
  722. try {
  723. await callWx('notifyBLECharacteristicValueChange', {
  724. deviceId,
  725. serviceId,
  726. characteristicId,
  727. state: true
  728. })
  729. addLog('SYS', `已开启通知 ${characteristicId}`)
  730. return true
  731. } catch (error) {
  732. addLog('SYS', `开启通知失败:${formatBluetoothError(error)}`)
  733. if (isConnectionLostError(error)) {
  734. throw error
  735. }
  736. return false
  737. }
  738. }
  739. async function connectDeviceById(deviceId) {
  740. const device = deviceRegistry.getDevice(deviceId)
  741. if (!device || state.isConnecting) return
  742. resetSendRuntimeState()
  743. setState({
  744. connectingDeviceId: deviceId,
  745. errorText: '',
  746. isConnecting: true
  747. })
  748. try {
  749. await stopScan()
  750. await closeConnectedDevice(deviceId, {
  751. force: state.connectedDevice && state.connectedDevice.deviceId === deviceId
  752. })
  753. await openAdapter()
  754. await callWx('createBLEConnection', {
  755. deviceId,
  756. timeout: CONNECT_TIMEOUT
  757. })
  758. const discovery = await discoverCharacteristics(deviceId)
  759. const notifyEnabled = discovery.notifyServiceId && discovery.notifyCharacteristicId
  760. ? await enableNotify(deviceId, discovery.notifyServiceId, discovery.notifyCharacteristicId)
  761. : false
  762. const isTargetDevice = hasTargetCharacteristic(discovery)
  763. const connectedDevice = deviceRegistry.markConnectedDevice(deviceId, {
  764. isTargetDevice
  765. }) || {
  766. ...device,
  767. isTargetDevice,
  768. packetSize: device.packetSize || inferPacketSize(device),
  769. targetText: isTargetDevice ? '已发现目标特征' : device.targetText
  770. }
  771. setState({
  772. characteristicText: buildCharacteristicText(discovery.writeServiceId, discovery.writeCharacteristicId),
  773. connectedDevice,
  774. connectedServiceCount: discovery.services.length,
  775. connectingDeviceId: '',
  776. errorText: discovery.writeServiceId
  777. ? (notifyEnabled ? '' : '已连接,但未成功开启通知,可能收不到设备回复')
  778. : '已连接,但未找到可写特征值',
  779. isConnecting: false,
  780. writeCharacteristicId: discovery.writeCharacteristicId,
  781. writeServiceId: discovery.writeServiceId,
  782. writeType: discovery.writeType,
  783. devices: deviceRegistry.getDeviceList()
  784. })
  785. startRssiRefresh()
  786. addLog('SYS', `已连接 ${device.displayName}`)
  787. } catch (error) {
  788. resetSendRuntimeState()
  789. setState({
  790. connectingDeviceId: '',
  791. isConnecting: false,
  792. errorText: formatBluetoothError(error)
  793. })
  794. }
  795. }
  796. async function disconnectDevice() {
  797. const { connectedDevice } = state
  798. if (!connectedDevice) return
  799. try {
  800. await callWx('closeBLEConnection', {
  801. deviceId: connectedDevice.deviceId
  802. })
  803. } catch (error) {
  804. if (error.errCode !== 10006) {
  805. setState({
  806. errorText: formatBluetoothError(error)
  807. })
  808. return
  809. }
  810. }
  811. addLog('SYS', '主动断开连接')
  812. clearConnectedState({
  813. errorText: '',
  814. sendQueueLength: 0
  815. })
  816. }
  817. async function refreshNativeConnectionState() {
  818. if (!state.connectedDevice || typeof wx.getConnectedBluetoothDevices !== 'function') return true
  819. try {
  820. const services = state.writeServiceId ? [state.writeServiceId] : []
  821. const result = await callWx('getConnectedBluetoothDevices', {
  822. services
  823. })
  824. const isConnected = (result.devices || []).some((device) => device.deviceId === state.connectedDevice.deviceId)
  825. if (isConnected) return true
  826. addLog('SYS', '蓝牙连接状态已失效')
  827. clearConnectedState({
  828. errorText: '蓝牙连接已失效,请重新连接'
  829. })
  830. return false
  831. } catch (error) {
  832. if (isConnectionLostError(error)) {
  833. clearConnectedState({
  834. errorText: formatBluetoothError(error)
  835. })
  836. return false
  837. }
  838. return true
  839. }
  840. }
  841. function handleAppHide() {
  842. clearScanTimer()
  843. stopRssiRefresh()
  844. resetSendRuntimeState()
  845. if (state.isDiscovering) {
  846. stopScan()
  847. }
  848. }
  849. async function handleAppShow() {
  850. if (!state.connectedDevice) return
  851. init()
  852. const connected = await refreshNativeConnectionState()
  853. if (connected && state.connectedDevice) {
  854. startRssiRefresh()
  855. }
  856. }
  857. function setSendHex(sendHex) {
  858. setState({
  859. sendHex,
  860. errorText: ''
  861. })
  862. }
  863. function clearInput() {
  864. setState({
  865. sendHex: '',
  866. errorText: ''
  867. })
  868. }
  869. function clearLogs() {
  870. setState(createClearLogsState())
  871. }
  872. function enqueueSendFrame(hexFrame, source, options = {}) {
  873. if (!state.connectedDevice) {
  874. setState({
  875. errorText: '请先连接蓝牙透传设备'
  876. })
  877. return Promise.resolve(false)
  878. }
  879. if (!state.writeServiceId || !state.writeCharacteristicId) {
  880. setState({
  881. errorText: '当前设备没有可写特征值'
  882. })
  883. return Promise.resolve(false)
  884. }
  885. const errorText = validateHex(hexFrame)
  886. if (errorText) {
  887. setState({
  888. errorText
  889. })
  890. return Promise.resolve(false)
  891. }
  892. const buffer = hexToArrayBuffer(hexFrame)
  893. const bytes = new Uint8Array(buffer)
  894. const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options)
  895. if (dmaFrameLengthError) {
  896. setState({
  897. errorText: dmaFrameLengthError
  898. })
  899. return Promise.resolve(false)
  900. }
  901. return new Promise((resolve) => {
  902. sendJobSequence += 1
  903. sendQueue.push({
  904. id: sendJobSequence,
  905. hexFrame,
  906. options,
  907. resolve,
  908. source
  909. })
  910. setState({
  911. sendQueueLength: sendQueue.length
  912. })
  913. processSendQueue()
  914. })
  915. }
  916. async function processSendQueue() {
  917. if (isProcessingSendQueue) return
  918. const generation = sendQueueGeneration
  919. isProcessingSendQueue = true
  920. try {
  921. while (sendQueue.length && generation === sendQueueGeneration) {
  922. const job = sendQueue.shift()
  923. setState({
  924. sendQueueLength: sendQueue.length
  925. })
  926. let result = false
  927. try {
  928. result = await executeSendFrame(job.hexFrame, job.source, job.options)
  929. } catch (error) {
  930. cancelPendingRequest()
  931. setState({
  932. errorText: error.message || '发送失败'
  933. })
  934. }
  935. job.resolve(result)
  936. if (!state.connectedDevice) {
  937. clearSendQueue()
  938. break
  939. }
  940. }
  941. } finally {
  942. if (generation === sendQueueGeneration) {
  943. isProcessingSendQueue = false
  944. }
  945. }
  946. }
  947. async function executeSendFrame(hexFrame, source, options = {}) {
  948. const {
  949. connectedDevice,
  950. writeCharacteristicId,
  951. writeServiceId,
  952. writeType
  953. } = state
  954. const errorText = validateHex(hexFrame)
  955. if (!connectedDevice) {
  956. setState({
  957. errorText: '请先连接蓝牙透传设备'
  958. })
  959. return false
  960. }
  961. if (!writeServiceId || !writeCharacteristicId) {
  962. setState({
  963. errorText: '当前设备没有可写特征值'
  964. })
  965. return false
  966. }
  967. if (errorText) {
  968. setState({
  969. errorText
  970. })
  971. return false
  972. }
  973. const buffer = hexToArrayBuffer(hexFrame)
  974. const bytes = new Uint8Array(buffer)
  975. const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options)
  976. if (dmaFrameLengthError) {
  977. setState({
  978. errorText: dmaFrameLengthError
  979. })
  980. return false
  981. }
  982. const chunkSize = resolvePacketSize(
  983. options.chunkSize === undefined ? connectedDevice.packetSize : options.chunkSize,
  984. bytes.length
  985. )
  986. const waitResponse = !!options.expected
  987. const responsePromise = waitResponse
  988. ? createPendingRequest(source, options.expected, options)
  989. : null
  990. setState({
  991. isSending: true,
  992. errorText: ''
  993. })
  994. try {
  995. for (let offset = 0; offset < bytes.length; offset += chunkSize) {
  996. const chunk = bytes.slice(offset, offset + chunkSize)
  997. await callWx('writeBLECharacteristicValue', {
  998. deviceId: connectedDevice.deviceId,
  999. serviceId: writeServiceId,
  1000. characteristicId: writeCharacteristicId,
  1001. value: chunk.buffer,
  1002. writeType
  1003. })
  1004. }
  1005. setState({
  1006. txCount: state.txCount + bytes.length
  1007. })
  1008. addLog('TX', arrayBufferToHex(buffer), source, {
  1009. payloadBytes: Array.prototype.slice.call(bytes),
  1010. payloadText: bytesToUtf8Text(bytes)
  1011. })
  1012. if (waitResponse) {
  1013. return responsePromise
  1014. }
  1015. return true
  1016. } catch (error) {
  1017. if (waitResponse) {
  1018. cancelPendingRequest()
  1019. }
  1020. if (isConnectionLostError(error)) {
  1021. clearConnectedState({
  1022. errorText: formatBluetoothError(error)
  1023. })
  1024. } else {
  1025. setState({
  1026. errorText: formatBluetoothError(error)
  1027. })
  1028. }
  1029. return false
  1030. } finally {
  1031. setState({
  1032. isSending: false
  1033. })
  1034. }
  1035. }
  1036. function sendManagedFrame(frameBytes, label, expected, options = {}) {
  1037. return enqueueSendFrame(formatFrameHex(frameBytes), label, {
  1038. expected: expected ? {
  1039. ...expected,
  1040. maxFrameBytes: options.maxFrameBytes === undefined ? DEFAULT_MAX_FRAME_BYTES : options.maxFrameBytes
  1041. } : expected,
  1042. responseBufferHint: options.responseBufferHint,
  1043. responseReader: options.responseReader,
  1044. showModal: options.showModal !== false,
  1045. timeout: options.timeout || RESPONSE_TIMEOUT,
  1046. maxFrameBytes: options.maxFrameBytes === undefined ? DEFAULT_MAX_FRAME_BYTES : options.maxFrameBytes
  1047. })
  1048. }
  1049. function sendRawFrameExact(frameBytes, source) {
  1050. const bytes = frameBytes instanceof Uint8Array
  1051. ? frameBytes
  1052. : new Uint8Array(frameBytes || [])
  1053. return enqueueSendFrame(formatFrameHex(Array.prototype.slice.call(bytes)), source, {
  1054. chunkSize: 0,
  1055. skipDmaCheck: true
  1056. })
  1057. }
  1058. function sendHexFrame() {
  1059. const errorText = validateHex(state.sendHex)
  1060. const parseSendExpected = protocolHelperRegistry.get('parseSendExpected')
  1061. const expected = errorText || typeof parseSendExpected !== 'function'
  1062. ? null
  1063. : parseSendExpected(Array.prototype.slice.call(new Uint8Array(hexToArrayBuffer(state.sendHex))))
  1064. return enqueueSendFrame(state.sendHex, 'HEX', expected ? {
  1065. expected
  1066. } : {})
  1067. }
  1068. module.exports = {
  1069. clearDevices,
  1070. clearInput,
  1071. clearLogs,
  1072. configureProtocolHelpers,
  1073. connectDeviceById,
  1074. disconnectDevice,
  1075. enqueueSendFrame,
  1076. getState,
  1077. handleAppHide,
  1078. handleAppShow,
  1079. init,
  1080. sendHexFrame,
  1081. sendManagedFrame,
  1082. sendRawFrameExact,
  1083. setSendHex,
  1084. showCommandAlert,
  1085. startScan,
  1086. stopScan,
  1087. subscribe,
  1088. subscribeRawResponse
  1089. }