1
0

ble-core.js 30 KB

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