1
0

ble-utils.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. const DEFAULT_PACKET_SIZE = 20
  2. const DEFAULT_MAX_FRAME_BYTES = 64
  3. const TARGET_BLE_UUIDS = ['FFE0', 'FFE1']
  4. const MODULE_PACKET_SIZES = [
  5. {
  6. packetSize: 0,
  7. patterns: [/HC[-_ ]?05/i]
  8. },
  9. {
  10. packetSize: 320,
  11. patterns: [/BT[-_ ]?24/i, /\bBT24\b/i]
  12. }
  13. ]
  14. const bluetoothErrorMap = {
  15. 10000: '蓝牙模块未初始化,请重新扫描',
  16. 10001: '蓝牙不可用,请开启手机蓝牙',
  17. 10002: '未找到指定设备,请重新扫描',
  18. 10003: '连接失败,请靠近设备后重试',
  19. 10004: '未发现设备服务',
  20. 10005: '未发现设备特征值',
  21. 10006: '当前连接已断开',
  22. 10007: '当前特征值不支持此操作',
  23. 10008: '系统蓝牙异常,请稍后重试',
  24. 10009: '当前系统不支持 BLE',
  25. 10012: '蓝牙操作超时,请重试',
  26. 10013: '设备 ID 无效,请重新扫描'
  27. }
  28. function formatBluetoothError(error) {
  29. if (!error) return '操作失败'
  30. const message = bluetoothErrorMap[error.errCode]
  31. if (message) return message
  32. return error.errMsg || error.message || '蓝牙操作失败'
  33. }
  34. function formatSignalText(RSSI) {
  35. return typeof RSSI === 'number' ? `${RSSI} dBm` : '--'
  36. }
  37. function formatTime(timestamp) {
  38. const date = new Date(timestamp)
  39. const pad = (value, length = 2) => String(value).padStart(length, '0')
  40. return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`
  41. }
  42. function inferPacketSize(device = {}) {
  43. const text = [device.displayName, device.name, device.localName]
  44. .map((value) => String(value || ''))
  45. .join(' ')
  46. .toUpperCase()
  47. for (const item of MODULE_PACKET_SIZES) {
  48. const matchedPattern = (item.patterns || []).some((pattern) => pattern.test(text))
  49. if (matchedPattern) {
  50. return item.packetSize
  51. }
  52. }
  53. return DEFAULT_PACKET_SIZE
  54. }
  55. function resolvePacketSize(packetSize, frameLength) {
  56. if (packetSize === 0) return frameLength || DEFAULT_PACKET_SIZE
  57. if (Number.isInteger(packetSize) && packetSize > 0) return packetSize
  58. return DEFAULT_PACKET_SIZE
  59. }
  60. function normalizeUuid(value) {
  61. return String(value || '').replace(/-/g, '').toUpperCase()
  62. }
  63. function isTargetUuid(value) {
  64. const uuid = normalizeUuid(value)
  65. return TARGET_BLE_UUIDS.some((target) => uuid.indexOf(target) >= 0)
  66. }
  67. function hasTargetAdvertisedUuid(device) {
  68. return (device.advertisServiceUUIDs || []).some(isTargetUuid)
  69. }
  70. function mergeAdvertisedServiceUUIDs(left = [], right = []) {
  71. const uuidMap = {}
  72. const uuids = []
  73. left.concat(right).forEach((uuid) => {
  74. const key = normalizeUuid(uuid)
  75. if (!key || uuidMap[key]) return
  76. uuidMap[key] = true
  77. uuids.push(uuid)
  78. })
  79. return uuids
  80. }
  81. function normalizeDevice(device, options = {}) {
  82. const advertisServiceUUIDs = device.advertisServiceUUIDs || []
  83. const displayName = String(device.name || device.localName || '').trim() || '未命名设备'
  84. const packetSize = inferPacketSize({
  85. displayName,
  86. localName: device.localName,
  87. name: device.name
  88. })
  89. const isTargetAdvertised = hasTargetAdvertisedUuid({
  90. advertisServiceUUIDs
  91. })
  92. const now = Number(options.now) || Date.now()
  93. return {
  94. deviceId: device.deviceId,
  95. name: device.name || '',
  96. localName: device.localName || '',
  97. RSSI: device.RSSI,
  98. advertisServiceUUIDs,
  99. displayName,
  100. isTargetAdvertised,
  101. packetSize,
  102. signalText: formatSignalText(device.RSSI),
  103. serviceText: advertisServiceUUIDs.length ? advertisServiceUUIDs.join(', ') : '未广播服务',
  104. targetText: isTargetAdvertised ? '广播含目标 UUID' : '',
  105. lastSeenAt: now
  106. }
  107. }
  108. function normalizeHex(value) {
  109. return String(value || '')
  110. .replace(/0x/gi, '')
  111. .replace(/[\s,;:_-]/g, '')
  112. .toUpperCase()
  113. }
  114. function validateHex(value) {
  115. const trimmed = String(value || '').trim()
  116. const withoutPrefix = trimmed.replace(/0x/gi, '')
  117. const compact = normalizeHex(trimmed)
  118. if (!compact) return '请输入要发送的十六进制数据'
  119. if (/[^0-9a-fA-F\s,;:_-]/.test(withoutPrefix)) return '只支持十六进制字符'
  120. if (compact.length % 2 !== 0) return '十六进制长度必须为偶数'
  121. return ''
  122. }
  123. function normalizeMaxFrameBytes(maxFrameBytes) {
  124. const numberValue = Number(maxFrameBytes)
  125. if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0
  126. if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue)
  127. return DEFAULT_MAX_FRAME_BYTES
  128. }
  129. function hexToArrayBuffer(hexText) {
  130. const hex = normalizeHex(hexText)
  131. const buffer = new ArrayBuffer(hex.length / 2)
  132. const view = new Uint8Array(buffer)
  133. for (let index = 0; index < view.length; index += 1) {
  134. view[index] = parseInt(hex.substr(index * 2, 2), 16)
  135. }
  136. return buffer
  137. }
  138. function arrayBufferToHex(buffer) {
  139. if (!buffer) return ''
  140. return Array.prototype.map.call(new Uint8Array(buffer), (item) => item.toString(16).padStart(2, '0')).join(' ').toUpperCase()
  141. }
  142. function formatFrameHex(bytes) {
  143. return Array.prototype.map.call(bytes || [], (item) => Number(item || 0).toString(16).padStart(2, '0')).join(' ').toUpperCase()
  144. }
  145. function getCharacteristicRole(properties = {}) {
  146. const canWrite = !!(properties.write || properties.writeNoResponse)
  147. const canNotify = !!(properties.notify || properties.indicate)
  148. if (canWrite && canNotify) return '收发'
  149. if (canWrite) return '发送'
  150. if (canNotify) return '接收'
  151. if (properties.read) return '读取'
  152. return '其他'
  153. }
  154. function buildCharacteristicText(serviceId, characteristicId) {
  155. if (!serviceId || !characteristicId) return '未选择'
  156. return `${serviceId.slice(0, 8)} / ${characteristicId.slice(0, 8)}`
  157. }
  158. function hasTargetCharacteristic(discovery) {
  159. return (discovery.services || []).some((service) => (
  160. isTargetUuid(service.uuid) || (service.characteristics || []).some((item) => isTargetUuid(item.uuid))
  161. ))
  162. }
  163. function isConnectionLostError(error) {
  164. if (!error) return false
  165. if ([10000, 10001, 10006, 10013].includes(error.errCode)) return true
  166. const message = String(error.errMsg || error.message || '').toLowerCase()
  167. return message.includes('disconnect') || message.includes('not connected')
  168. }
  169. module.exports = {
  170. DEFAULT_MAX_FRAME_BYTES,
  171. DEFAULT_PACKET_SIZE,
  172. arrayBufferToHex,
  173. buildCharacteristicText,
  174. formatBluetoothError,
  175. formatFrameHex,
  176. formatSignalText,
  177. formatTime,
  178. getCharacteristicRole,
  179. hasTargetAdvertisedUuid,
  180. hasTargetCharacteristic,
  181. hexToArrayBuffer,
  182. inferPacketSize,
  183. isConnectionLostError,
  184. isTargetUuid,
  185. mergeAdvertisedServiceUUIDs,
  186. normalizeDevice,
  187. normalizeHex,
  188. normalizeMaxFrameBytes,
  189. normalizeUuid,
  190. resolvePacketSize,
  191. validateHex
  192. }