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 }