| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- const DEFAULT_PACKET_SIZE = 20
- const DEFAULT_MAX_FRAME_BYTES = 64
- const TARGET_BLE_UUIDS = ['FFE0', 'FFE1']
- const MODULE_PACKET_SIZES = [
- {
- packetSize: 0,
- patterns: [/HC[-_ ]?05/i]
- },
- {
- packetSize: 320,
- patterns: [/BT[-_ ]?24/i, /\bBT24\b/i]
- }
- ]
- const bluetoothErrorMap = {
- 10000: '蓝牙模块未初始化,请重新扫描',
- 10001: '蓝牙不可用,请开启手机蓝牙',
- 10002: '未找到指定设备,请重新扫描',
- 10003: '连接失败,请靠近设备后重试',
- 10004: '未发现设备服务',
- 10005: '未发现设备特征值',
- 10006: '当前连接已断开',
- 10007: '当前特征值不支持此操作',
- 10008: '系统蓝牙异常,请稍后重试',
- 10009: '当前系统不支持 BLE',
- 10012: '蓝牙操作超时,请重试',
- 10013: '设备 ID 无效,请重新扫描'
- }
- function formatBluetoothError(error) {
- if (!error) return '操作失败'
- const message = bluetoothErrorMap[error.errCode]
- if (message) return message
- return error.errMsg || error.message || '蓝牙操作失败'
- }
- function formatSignalText(RSSI) {
- return typeof RSSI === 'number' ? `${RSSI} dBm` : '--'
- }
- function formatTime(timestamp) {
- const date = new Date(timestamp)
- const pad = (value, length = 2) => String(value).padStart(length, '0')
- return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`
- }
- function inferPacketSize(device = {}) {
- const text = [device.displayName, device.name, device.localName]
- .map((value) => String(value || ''))
- .join(' ')
- .toUpperCase()
- for (const item of MODULE_PACKET_SIZES) {
- const matchedPattern = (item.patterns || []).some((pattern) => pattern.test(text))
- if (matchedPattern) {
- return item.packetSize
- }
- }
- return DEFAULT_PACKET_SIZE
- }
- function resolvePacketSize(packetSize, frameLength) {
- if (packetSize === 0) return frameLength || DEFAULT_PACKET_SIZE
- if (Number.isInteger(packetSize) && packetSize > 0) return packetSize
- return DEFAULT_PACKET_SIZE
- }
- function normalizeUuid(value) {
- return String(value || '').replace(/-/g, '').toUpperCase()
- }
- function isTargetUuid(value) {
- const uuid = normalizeUuid(value)
- return TARGET_BLE_UUIDS.some((target) => uuid.indexOf(target) >= 0)
- }
- function hasTargetAdvertisedUuid(device) {
- return (device.advertisServiceUUIDs || []).some(isTargetUuid)
- }
- function mergeAdvertisedServiceUUIDs(left = [], right = []) {
- const uuidMap = {}
- const uuids = []
- left.concat(right).forEach((uuid) => {
- const key = normalizeUuid(uuid)
- if (!key || uuidMap[key]) return
- uuidMap[key] = true
- uuids.push(uuid)
- })
- return uuids
- }
- function normalizeDevice(device, options = {}) {
- const advertisServiceUUIDs = device.advertisServiceUUIDs || []
- const displayName = String(device.name || device.localName || '').trim() || '未命名设备'
- const packetSize = inferPacketSize({
- displayName,
- localName: device.localName,
- name: device.name
- })
- const isTargetAdvertised = hasTargetAdvertisedUuid({
- advertisServiceUUIDs
- })
- const now = Number(options.now) || Date.now()
- return {
- deviceId: device.deviceId,
- name: device.name || '',
- localName: device.localName || '',
- RSSI: device.RSSI,
- advertisServiceUUIDs,
- displayName,
- isTargetAdvertised,
- packetSize,
- signalText: formatSignalText(device.RSSI),
- serviceText: advertisServiceUUIDs.length ? advertisServiceUUIDs.join(', ') : '未广播服务',
- targetText: isTargetAdvertised ? '广播含目标 UUID' : '',
- lastSeenAt: now
- }
- }
- function normalizeHex(value) {
- return String(value || '')
- .replace(/0x/gi, '')
- .replace(/[\s,;:_-]/g, '')
- .toUpperCase()
- }
- function validateHex(value) {
- const trimmed = String(value || '').trim()
- const withoutPrefix = trimmed.replace(/0x/gi, '')
- const compact = normalizeHex(trimmed)
- if (!compact) return '请输入要发送的十六进制数据'
- if (/[^0-9a-fA-F\s,;:_-]/.test(withoutPrefix)) return '只支持十六进制字符'
- if (compact.length % 2 !== 0) return '十六进制长度必须为偶数'
- return ''
- }
- function normalizeMaxFrameBytes(maxFrameBytes) {
- const numberValue = Number(maxFrameBytes)
- if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0
- if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue)
- return DEFAULT_MAX_FRAME_BYTES
- }
- function hexToArrayBuffer(hexText) {
- const hex = normalizeHex(hexText)
- const buffer = new ArrayBuffer(hex.length / 2)
- const view = new Uint8Array(buffer)
- for (let index = 0; index < view.length; index += 1) {
- view[index] = parseInt(hex.substr(index * 2, 2), 16)
- }
- return buffer
- }
- function arrayBufferToHex(buffer) {
- if (!buffer) return ''
- return Array.prototype.map.call(new Uint8Array(buffer), (item) => item.toString(16).padStart(2, '0')).join(' ').toUpperCase()
- }
- function formatFrameHex(bytes) {
- return Array.prototype.map.call(bytes || [], (item) => Number(item || 0).toString(16).padStart(2, '0')).join(' ').toUpperCase()
- }
- function getCharacteristicRole(properties = {}) {
- const canWrite = !!(properties.write || properties.writeNoResponse)
- const canNotify = !!(properties.notify || properties.indicate)
- if (canWrite && canNotify) return '收发'
- if (canWrite) return '发送'
- if (canNotify) return '接收'
- if (properties.read) return '读取'
- return '其他'
- }
- function buildCharacteristicText(serviceId, characteristicId) {
- if (!serviceId || !characteristicId) return '未选择'
- return `${serviceId.slice(0, 8)} / ${characteristicId.slice(0, 8)}`
- }
- function hasTargetCharacteristic(discovery) {
- return (discovery.services || []).some((service) => (
- isTargetUuid(service.uuid) || (service.characteristics || []).some((item) => isTargetUuid(item.uuid))
- ))
- }
- function isConnectionLostError(error) {
- if (!error) return false
- if ([10000, 10001, 10006, 10013].includes(error.errCode)) return true
- const message = String(error.errMsg || error.message || '').toLowerCase()
- return message.includes('disconnect') || message.includes('not connected')
- }
- module.exports = {
- DEFAULT_MAX_FRAME_BYTES,
- DEFAULT_PACKET_SIZE,
- arrayBufferToHex,
- buildCharacteristicText,
- formatBluetoothError,
- formatFrameHex,
- formatSignalText,
- formatTime,
- getCharacteristicRole,
- hasTargetAdvertisedUuid,
- hasTargetCharacteristic,
- hexToArrayBuffer,
- inferPacketSize,
- isConnectionLostError,
- isTargetUuid,
- mergeAdvertisedServiceUUIDs,
- normalizeDevice,
- normalizeHex,
- normalizeMaxFrameBytes,
- normalizeUuid,
- resolvePacketSize,
- validateHex
- }
|