ble-core.js 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276
  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 = `${pending.label} 收到无效响应帧,已丢弃`
  360. addLog('SYS', content)
  361. finishPendingRequest(false)
  362. if (pending.showModal) {
  363. showCommandAlert('通讯异常', content)
  364. }
  365. return
  366. }
  367. if (result.status === 'exception') {
  368. const content = result.message || `${pending.label} 收到异常响应帧`
  369. addLog('SYS', content)
  370. finishPendingRequest(false)
  371. if (pending.showModal) {
  372. showCommandAlert('设备返回故障帧', content)
  373. }
  374. return
  375. }
  376. if (result.status === 'mismatch') {
  377. const content = `${pending.label} 收到不匹配响应,已丢弃`
  378. addLog('SYS', content)
  379. finishPendingRequest(false)
  380. if (pending.showModal) {
  381. showCommandAlert('通讯异常', content)
  382. }
  383. return
  384. }
  385. if (result.status !== 'complete') {
  386. const content = `${pending.label} 收到未知响应状态,已丢弃`
  387. addLog('SYS', content)
  388. finishPendingRequest(false)
  389. if (pending.showModal) {
  390. showCommandAlert('通讯异常', content)
  391. }
  392. return
  393. }
  394. finishPendingRequest(result.response)
  395. if (pending.responseBuffer.length) {
  396. consumePendingResponseBuffer()
  397. }
  398. }
  399. function handleReceivedResponseBytes(bytes) {
  400. if (!pendingRequest || !Array.isArray(bytes) || !bytes.length) return
  401. pendingRequest.responseBuffer = pendingRequest.responseBuffer.concat(bytes)
  402. const bufferLimit = pendingRequest.responseBufferLimit || MAX_RESPONSE_BUFFER_BYTES
  403. if (pendingRequest.responseBuffer.length > bufferLimit) {
  404. const pending = pendingRequest
  405. const content = `${pending.label} 返回数据超过缓冲区,已丢弃`
  406. addLog('SYS', content)
  407. finishPendingRequest(false)
  408. if (pending.showModal) {
  409. showCommandAlert('通讯异常', content)
  410. }
  411. return
  412. }
  413. consumePendingResponseBuffer()
  414. }
  415. function createPendingRequest(label, expected, options = {}) {
  416. return new Promise((resolve) => {
  417. const timer = setTimeout(() => {
  418. const pending = clearPendingRequest()
  419. if (!pending) return
  420. addLog('SYS', `${label} 超时`)
  421. if (options.showModal !== false) {
  422. showCommandAlert('通讯超时', `${label} 1秒内没有收到回复`)
  423. }
  424. resolve(false)
  425. }, options.timeout || RESPONSE_TIMEOUT)
  426. pendingRequest = {
  427. expected,
  428. label,
  429. resolve,
  430. timer,
  431. responseBufferLimit: getResponseBufferLimit(expected, options),
  432. responseReader: typeof options.responseReader === 'function' ? options.responseReader : null,
  433. showModal: options.showModal !== false,
  434. responseBuffer: []
  435. }
  436. setState({
  437. isAwaitingResponse: true
  438. })
  439. })
  440. }
  441. function init() {
  442. if (initialized) return
  443. wx.onBluetoothDeviceFound((res) => {
  444. mergeDevices(res.devices || [])
  445. })
  446. wx.onBluetoothAdapterStateChange((res) => {
  447. setState({
  448. adapterAvailable: !!res.available,
  449. isDiscovering: !!res.discovering
  450. })
  451. if (!res.available) {
  452. clearScanTimer()
  453. clearConnectedState({
  454. adapterAvailable: false,
  455. adapterOpened: false,
  456. errorText: '请开启手机蓝牙后重新扫描',
  457. isDiscovering: false,
  458. sendQueueLength: 0
  459. })
  460. }
  461. })
  462. wx.onBLEConnectionStateChange((res) => {
  463. const { connectedDevice } = state
  464. if (!connectedDevice || connectedDevice.deviceId !== res.deviceId) return
  465. if (!res.connected) {
  466. addLog('SYS', '连接已断开')
  467. clearConnectedState({
  468. errorText: '',
  469. sendQueueLength: 0
  470. })
  471. }
  472. })
  473. wx.onBLECharacteristicValueChange((res) => {
  474. const hex = arrayBufferToHex(res.value)
  475. const byteLength = res.value ? res.value.byteLength : 0
  476. const rawBytes = Array.prototype.slice.call(new Uint8Array(res.value || new ArrayBuffer(0)))
  477. const crcState = getReceiveCrcState(rawBytes)
  478. setState({
  479. rxCount: state.rxCount + byteLength
  480. })
  481. addLog('RX', hex, crcState, {
  482. payloadBytes: rawBytes,
  483. payloadText: bytesToUtf8Text(rawBytes)
  484. })
  485. rawResponseSubscribers.slice().forEach((subscriber) => {
  486. subscriber(rawBytes, res)
  487. })
  488. handleReceivedResponseBytes(rawBytes)
  489. })
  490. initialized = true
  491. }
  492. async function getAuthSetting() {
  493. return callWx('getSetting')
  494. .then((res) => res.authSetting || {})
  495. .catch(() => ({}))
  496. }
  497. function showPermissionModal(title, content) {
  498. return new Promise((resolve, reject) => {
  499. wx.showModal({
  500. title,
  501. content,
  502. confirmText: '去设置',
  503. success: async (res) => {
  504. if (!res.confirm) {
  505. reject(new Error('用户取消授权'))
  506. return
  507. }
  508. try {
  509. await callWx('openSetting')
  510. resolve()
  511. } catch (error) {
  512. reject(error)
  513. }
  514. },
  515. fail: reject
  516. })
  517. })
  518. }
  519. async function ensureBluetoothAuthorized() {
  520. const authSetting = await getAuthSetting()
  521. if (authSetting['scope.bluetooth']) return
  522. if (authSetting['scope.bluetooth'] === false) {
  523. await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。')
  524. return
  525. }
  526. try {
  527. await callWx('authorize', {
  528. scope: 'scope.bluetooth'
  529. })
  530. } catch (error) {
  531. await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。')
  532. }
  533. }
  534. async function ensureAndroidLocationAuthorized() {
  535. const systemInfo = wx.getSystemInfoSync ? wx.getSystemInfoSync() : wx.getDeviceInfo()
  536. if (systemInfo.platform !== 'android') return
  537. const authSetting = await getAuthSetting()
  538. if (authSetting['scope.userLocation']) return
  539. setState({
  540. systemTip: '安卓系统扫描 BLE 设备通常需要开启系统定位权限。'
  541. })
  542. if (authSetting['scope.userLocation'] === false) {
  543. await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。')
  544. return
  545. }
  546. try {
  547. await callWx('authorize', {
  548. scope: 'scope.userLocation'
  549. })
  550. } catch (error) {
  551. await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。')
  552. }
  553. }
  554. async function openAdapter() {
  555. if (state.adapterOpened) {
  556. try {
  557. const adapterState = await callWx('getBluetoothAdapterState')
  558. setState({
  559. adapterAvailable: !!adapterState.available,
  560. isDiscovering: !!adapterState.discovering
  561. })
  562. if (adapterState.available) return
  563. } catch (error) {
  564. setState({
  565. adapterAvailable: false,
  566. adapterOpened: false
  567. })
  568. }
  569. }
  570. try {
  571. await callWx('openBluetoothAdapter', {
  572. mode: 'central'
  573. })
  574. const adapterState = await callWx('getBluetoothAdapterState')
  575. setState({
  576. adapterAvailable: !!adapterState.available,
  577. adapterOpened: true,
  578. isDiscovering: !!adapterState.discovering
  579. })
  580. if (!adapterState.available) {
  581. throw {
  582. errCode: 10001,
  583. errMsg: 'bluetooth adapter not available'
  584. }
  585. }
  586. } catch (error) {
  587. if (error.errCode === 10001) {
  588. setState({
  589. adapterOpened: true,
  590. adapterAvailable: false
  591. })
  592. }
  593. throw error
  594. }
  595. }
  596. async function startDiscovery() {
  597. try {
  598. await callWx('startBluetoothDevicesDiscovery', {
  599. allowDuplicatesKey: true,
  600. interval: 600,
  601. powerLevel: 'high'
  602. })
  603. } catch (error) {
  604. await callWx('startBluetoothDevicesDiscovery', {
  605. allowDuplicatesKey: true,
  606. interval: 600
  607. })
  608. }
  609. }
  610. async function startScan() {
  611. if (state.isConnecting) return
  612. deviceRegistry.clear()
  613. setState({
  614. devices: [],
  615. errorText: ''
  616. })
  617. try {
  618. init()
  619. await ensureBluetoothAuthorized()
  620. await ensureAndroidLocationAuthorized()
  621. await openAdapter()
  622. await startDiscovery()
  623. setState({
  624. isDiscovering: true
  625. })
  626. resetScanTimer()
  627. addLog('SYS', '开始扫描 BLE 设备')
  628. } catch (error) {
  629. clearScanTimer()
  630. setState({
  631. isDiscovering: false,
  632. errorText: formatBluetoothError(error)
  633. })
  634. }
  635. }
  636. function clearDevices() {
  637. deviceRegistry.clear()
  638. setState({
  639. devices: [],
  640. errorText: ''
  641. })
  642. }
  643. async function closeConnectedDevice(nextDeviceId, options = {}) {
  644. const { connectedDevice } = state
  645. if (!connectedDevice) {
  646. resetSendRuntimeState()
  647. return
  648. }
  649. if (connectedDevice.deviceId === nextDeviceId && !options.force) return
  650. resetSendRuntimeState()
  651. try {
  652. await callWx('closeBLEConnection', {
  653. deviceId: connectedDevice.deviceId
  654. })
  655. } catch (error) {
  656. if (error.errCode !== 10006) throw error
  657. }
  658. clearConnectedState()
  659. }
  660. async function discoverCharacteristics(deviceId) {
  661. const serviceResult = await callWx('getBLEDeviceServices', {
  662. deviceId
  663. })
  664. const services = []
  665. let writeServiceId = ''
  666. let writeCharacteristicId = ''
  667. let writeType = ''
  668. let notifyServiceId = ''
  669. let notifyCharacteristicId = ''
  670. for (const service of serviceResult.services || []) {
  671. const characteristicResult = await callWx('getBLEDeviceCharacteristics', {
  672. deviceId,
  673. serviceId: service.uuid
  674. })
  675. const characteristics = (characteristicResult.characteristics || []).map((item) => ({
  676. uuid: item.uuid,
  677. role: getCharacteristicRole(item.properties),
  678. properties: item.properties || {}
  679. }))
  680. services.push({
  681. uuid: service.uuid,
  682. primary: service.isPrimary,
  683. characteristics
  684. })
  685. characteristics.forEach((item) => {
  686. const isPreferredService = isTargetUuid(service.uuid)
  687. const isPreferredCharacteristic = isTargetUuid(item.uuid)
  688. const canWrite = item.properties.write || item.properties.writeNoResponse
  689. const canNotify = item.properties.notify || item.properties.indicate
  690. if (isPreferredService && isPreferredCharacteristic && canWrite) {
  691. writeServiceId = service.uuid
  692. writeCharacteristicId = item.uuid
  693. writeType = item.properties.write ? 'write' : 'writeNoResponse'
  694. }
  695. if (isPreferredService && isPreferredCharacteristic && canNotify) {
  696. notifyServiceId = service.uuid
  697. notifyCharacteristicId = item.uuid
  698. }
  699. if (!writeCharacteristicId && canWrite) {
  700. writeServiceId = service.uuid
  701. writeCharacteristicId = item.uuid
  702. writeType = item.properties.write ? 'write' : 'writeNoResponse'
  703. }
  704. if (!notifyCharacteristicId && canNotify) {
  705. notifyServiceId = service.uuid
  706. notifyCharacteristicId = item.uuid
  707. }
  708. })
  709. }
  710. return {
  711. services,
  712. writeServiceId,
  713. writeCharacteristicId,
  714. writeType,
  715. notifyServiceId,
  716. notifyCharacteristicId
  717. }
  718. }
  719. async function enableNotify(deviceId, serviceId, characteristicId) {
  720. try {
  721. await callWx('notifyBLECharacteristicValueChange', {
  722. deviceId,
  723. serviceId,
  724. characteristicId,
  725. state: true
  726. })
  727. addLog('SYS', `已开启通知 ${characteristicId}`)
  728. return true
  729. } catch (error) {
  730. addLog('SYS', `开启通知失败:${formatBluetoothError(error)}`)
  731. if (isConnectionLostError(error)) {
  732. throw error
  733. }
  734. return false
  735. }
  736. }
  737. async function connectDeviceById(deviceId) {
  738. const device = deviceRegistry.getDevice(deviceId)
  739. if (!device || state.isConnecting) return
  740. resetSendRuntimeState()
  741. setState({
  742. connectingDeviceId: deviceId,
  743. errorText: '',
  744. isConnecting: true
  745. })
  746. try {
  747. await stopScan()
  748. await closeConnectedDevice(deviceId, {
  749. force: state.connectedDevice && state.connectedDevice.deviceId === deviceId
  750. })
  751. await openAdapter()
  752. await callWx('createBLEConnection', {
  753. deviceId,
  754. timeout: CONNECT_TIMEOUT
  755. })
  756. const discovery = await discoverCharacteristics(deviceId)
  757. const notifyEnabled = discovery.notifyServiceId && discovery.notifyCharacteristicId
  758. ? await enableNotify(deviceId, discovery.notifyServiceId, discovery.notifyCharacteristicId)
  759. : false
  760. const isTargetDevice = hasTargetCharacteristic(discovery)
  761. const connectedDevice = deviceRegistry.markConnectedDevice(deviceId, {
  762. isTargetDevice
  763. }) || {
  764. ...device,
  765. isTargetDevice,
  766. packetSize: device.packetSize || inferPacketSize(device),
  767. targetText: isTargetDevice ? '已发现目标特征' : device.targetText
  768. }
  769. setState({
  770. characteristicText: buildCharacteristicText(discovery.writeServiceId, discovery.writeCharacteristicId),
  771. connectedDevice,
  772. connectedServiceCount: discovery.services.length,
  773. connectingDeviceId: '',
  774. errorText: discovery.writeServiceId
  775. ? (notifyEnabled ? '' : '已连接,但未成功开启通知,可能收不到设备回复')
  776. : '已连接,但未找到可写特征值',
  777. isConnecting: false,
  778. writeCharacteristicId: discovery.writeCharacteristicId,
  779. writeServiceId: discovery.writeServiceId,
  780. writeType: discovery.writeType,
  781. devices: deviceRegistry.getDeviceList()
  782. })
  783. startRssiRefresh()
  784. addLog('SYS', `已连接 ${device.displayName}`)
  785. } catch (error) {
  786. resetSendRuntimeState()
  787. setState({
  788. connectingDeviceId: '',
  789. isConnecting: false,
  790. errorText: formatBluetoothError(error)
  791. })
  792. }
  793. }
  794. async function disconnectDevice() {
  795. const { connectedDevice } = state
  796. if (!connectedDevice) return
  797. try {
  798. await callWx('closeBLEConnection', {
  799. deviceId: connectedDevice.deviceId
  800. })
  801. } catch (error) {
  802. if (error.errCode !== 10006) {
  803. setState({
  804. errorText: formatBluetoothError(error)
  805. })
  806. return
  807. }
  808. }
  809. addLog('SYS', '主动断开连接')
  810. clearConnectedState({
  811. errorText: '',
  812. sendQueueLength: 0
  813. })
  814. }
  815. async function refreshNativeConnectionState() {
  816. if (!state.connectedDevice || typeof wx.getConnectedBluetoothDevices !== 'function') return true
  817. try {
  818. const services = state.writeServiceId ? [state.writeServiceId] : []
  819. const result = await callWx('getConnectedBluetoothDevices', {
  820. services
  821. })
  822. const isConnected = (result.devices || []).some((device) => device.deviceId === state.connectedDevice.deviceId)
  823. if (isConnected) return true
  824. addLog('SYS', '蓝牙连接状态已失效')
  825. clearConnectedState({
  826. errorText: '蓝牙连接已失效,请重新连接'
  827. })
  828. return false
  829. } catch (error) {
  830. if (isConnectionLostError(error)) {
  831. clearConnectedState({
  832. errorText: formatBluetoothError(error)
  833. })
  834. return false
  835. }
  836. return true
  837. }
  838. }
  839. function handleAppHide() {
  840. clearScanTimer()
  841. stopRssiRefresh()
  842. resetSendRuntimeState()
  843. if (state.isDiscovering) {
  844. stopScan()
  845. }
  846. }
  847. async function handleAppShow() {
  848. if (!state.connectedDevice) return
  849. init()
  850. const connected = await refreshNativeConnectionState()
  851. if (connected && state.connectedDevice) {
  852. startRssiRefresh()
  853. }
  854. }
  855. function setSendHex(sendHex) {
  856. setState({
  857. sendHex,
  858. errorText: ''
  859. })
  860. }
  861. function clearInput() {
  862. setState({
  863. sendHex: '',
  864. errorText: ''
  865. })
  866. }
  867. function clearLogs() {
  868. setState(createClearLogsState())
  869. }
  870. function enqueueSendFrame(hexFrame, source, options = {}) {
  871. if (!state.connectedDevice) {
  872. setState({
  873. errorText: '请先连接蓝牙透传设备'
  874. })
  875. return Promise.resolve(false)
  876. }
  877. if (!state.writeServiceId || !state.writeCharacteristicId) {
  878. setState({
  879. errorText: '当前设备没有可写特征值'
  880. })
  881. return Promise.resolve(false)
  882. }
  883. const errorText = validateHex(hexFrame)
  884. if (errorText) {
  885. setState({
  886. errorText
  887. })
  888. return Promise.resolve(false)
  889. }
  890. const buffer = hexToArrayBuffer(hexFrame)
  891. const bytes = new Uint8Array(buffer)
  892. const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options)
  893. if (dmaFrameLengthError) {
  894. setState({
  895. errorText: dmaFrameLengthError
  896. })
  897. return Promise.resolve(false)
  898. }
  899. return new Promise((resolve) => {
  900. sendJobSequence += 1
  901. sendQueue.push({
  902. id: sendJobSequence,
  903. hexFrame,
  904. options,
  905. resolve,
  906. source
  907. })
  908. setState({
  909. sendQueueLength: sendQueue.length
  910. })
  911. processSendQueue()
  912. })
  913. }
  914. async function processSendQueue() {
  915. if (isProcessingSendQueue) return
  916. const generation = sendQueueGeneration
  917. isProcessingSendQueue = true
  918. try {
  919. while (sendQueue.length && generation === sendQueueGeneration) {
  920. const job = sendQueue.shift()
  921. setState({
  922. sendQueueLength: sendQueue.length
  923. })
  924. let result = false
  925. try {
  926. result = await executeSendFrame(job.hexFrame, job.source, job.options)
  927. } catch (error) {
  928. cancelPendingRequest()
  929. setState({
  930. errorText: error.message || '发送失败'
  931. })
  932. }
  933. job.resolve(result)
  934. if (!state.connectedDevice) {
  935. clearSendQueue()
  936. break
  937. }
  938. }
  939. } finally {
  940. if (generation === sendQueueGeneration) {
  941. isProcessingSendQueue = false
  942. }
  943. }
  944. }
  945. async function executeSendFrame(hexFrame, source, options = {}) {
  946. const {
  947. connectedDevice,
  948. writeCharacteristicId,
  949. writeServiceId,
  950. writeType
  951. } = state
  952. const errorText = validateHex(hexFrame)
  953. if (!connectedDevice) {
  954. setState({
  955. errorText: '请先连接蓝牙透传设备'
  956. })
  957. return false
  958. }
  959. if (!writeServiceId || !writeCharacteristicId) {
  960. setState({
  961. errorText: '当前设备没有可写特征值'
  962. })
  963. return false
  964. }
  965. if (errorText) {
  966. setState({
  967. errorText
  968. })
  969. return false
  970. }
  971. const buffer = hexToArrayBuffer(hexFrame)
  972. const bytes = new Uint8Array(buffer)
  973. const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options)
  974. if (dmaFrameLengthError) {
  975. setState({
  976. errorText: dmaFrameLengthError
  977. })
  978. return false
  979. }
  980. const chunkSize = resolvePacketSize(
  981. options.chunkSize === undefined ? connectedDevice.packetSize : options.chunkSize,
  982. bytes.length
  983. )
  984. const waitResponse = !!options.expected
  985. const responsePromise = waitResponse
  986. ? createPendingRequest(source, options.expected, options)
  987. : null
  988. setState({
  989. isSending: true,
  990. errorText: ''
  991. })
  992. try {
  993. for (let offset = 0; offset < bytes.length; offset += chunkSize) {
  994. const chunk = bytes.slice(offset, offset + chunkSize)
  995. await callWx('writeBLECharacteristicValue', {
  996. deviceId: connectedDevice.deviceId,
  997. serviceId: writeServiceId,
  998. characteristicId: writeCharacteristicId,
  999. value: chunk.buffer,
  1000. writeType
  1001. })
  1002. }
  1003. setState({
  1004. txCount: state.txCount + bytes.length
  1005. })
  1006. addLog('TX', arrayBufferToHex(buffer), source, {
  1007. payloadBytes: Array.prototype.slice.call(bytes),
  1008. payloadText: bytesToUtf8Text(bytes)
  1009. })
  1010. if (waitResponse) {
  1011. return responsePromise
  1012. }
  1013. return true
  1014. } catch (error) {
  1015. if (waitResponse) {
  1016. cancelPendingRequest()
  1017. }
  1018. if (isConnectionLostError(error)) {
  1019. clearConnectedState({
  1020. errorText: formatBluetoothError(error)
  1021. })
  1022. } else {
  1023. setState({
  1024. errorText: formatBluetoothError(error)
  1025. })
  1026. }
  1027. return false
  1028. } finally {
  1029. setState({
  1030. isSending: false
  1031. })
  1032. }
  1033. }
  1034. function sendManagedFrame(frameBytes, label, expected, options = {}) {
  1035. return enqueueSendFrame(formatFrameHex(frameBytes), label, {
  1036. expected: expected ? {
  1037. ...expected,
  1038. maxFrameBytes: options.maxFrameBytes === undefined ? DEFAULT_MAX_FRAME_BYTES : options.maxFrameBytes
  1039. } : expected,
  1040. responseBufferHint: options.responseBufferHint,
  1041. responseReader: options.responseReader,
  1042. showModal: options.showModal !== false,
  1043. timeout: options.timeout || RESPONSE_TIMEOUT,
  1044. maxFrameBytes: options.maxFrameBytes === undefined ? DEFAULT_MAX_FRAME_BYTES : options.maxFrameBytes
  1045. })
  1046. }
  1047. function sendRawFrameExact(frameBytes, source) {
  1048. const bytes = frameBytes instanceof Uint8Array
  1049. ? frameBytes
  1050. : new Uint8Array(frameBytes || [])
  1051. return enqueueSendFrame(formatFrameHex(Array.prototype.slice.call(bytes)), source, {
  1052. chunkSize: 0,
  1053. skipDmaCheck: true
  1054. })
  1055. }
  1056. function sendHexFrame() {
  1057. const errorText = validateHex(state.sendHex)
  1058. const parseSendExpected = protocolHelperRegistry.get('parseSendExpected')
  1059. const expected = errorText || typeof parseSendExpected !== 'function'
  1060. ? null
  1061. : parseSendExpected(Array.prototype.slice.call(new Uint8Array(hexToArrayBuffer(state.sendHex))))
  1062. return enqueueSendFrame(state.sendHex, 'HEX', expected ? {
  1063. expected
  1064. } : {})
  1065. }
  1066. module.exports = {
  1067. clearDevices,
  1068. clearInput,
  1069. clearLogs,
  1070. configureProtocolHelpers,
  1071. connectDeviceById,
  1072. disconnectDevice,
  1073. enqueueSendFrame,
  1074. getState,
  1075. handleAppHide,
  1076. handleAppShow,
  1077. init,
  1078. sendHexFrame,
  1079. sendManagedFrame,
  1080. sendRawFrameExact,
  1081. setSendHex,
  1082. showCommandAlert,
  1083. startScan,
  1084. stopScan,
  1085. subscribe,
  1086. subscribeRawResponse
  1087. }