const { notifyPageToast } = require('../utils/page-toast.js') const SCAN_TIMEOUT = 15000 const CONNECT_TIMEOUT = 10000 const RSSI_REFRESH_INTERVAL = 2000 const DEFAULT_PACKET_SIZE = 20 const MODULE_PACKET_SIZES = [ { packetSize: 0, patterns: [/HC[-_ ]?05/i] }, { packetSize: 320, patterns: [/BT[-_ ]?24/i, /\bBT24\b/i] } ] const RESPONSE_TIMEOUT = 1000 const MAX_RESPONSE_BUFFER_BYTES = 128 const DEFAULT_MAX_FRAME_BYTES = 64 const MAX_LOG_COUNT = 100 const TARGET_BLE_UUIDS = ['FFE0', 'FFE1'] const bluetoothErrorMap = { 10000: '蓝牙模块未初始化,请重新扫描', 10001: '蓝牙不可用,请开启手机蓝牙', 10002: '未找到指定设备,请重新扫描', 10003: '连接失败,请靠近设备后重试', 10004: '未发现设备服务', 10005: '未发现设备特征值', 10006: '当前连接已断开', 10007: '当前特征值不支持此操作', 10008: '系统蓝牙异常,请稍后重试', 10009: '当前系统不支持 BLE', 10012: '蓝牙操作超时,请重试', 10013: '设备 ID 无效,请重新扫描' } const state = { adapterAvailable: false, adapterOpened: false, characteristicText: '未选择', connectedDevice: null, connectedServiceCount: 0, connectingDeviceId: '', devices: [], errorText: '', isAwaitingResponse: false, isConnecting: false, isDiscovering: false, isSending: false, logScrollTarget: '', logs: [], rxCount: 0, sendHex: '', sendQueueLength: 0, systemTip: '', txCount: 0, writeCharacteristicId: '', writeServiceId: '', writeType: '' } let initialized = false let scanTimer = null let rssiTimer = null let isReadingRssi = false let pendingRequest = null let sendQueue = [] let isProcessingSendQueue = false let sendQueueGeneration = 0 let sendJobSequence = 0 let deviceMap = {} let deviceSequence = 0 let logSequence = 0 const subscribers = [] const rawResponseSubscribers = [] const protocolHelpers = { getResponseBufferHint: null, inspectReceivedBytes: null, parseSendExpected: null, readResponseFromBuffer: null } let protocolHelpersLoaded = false let protocolHelpersLoader = null function applyProtocolHelpers(helpers = {}) { Object.assign(protocolHelpers, { getResponseBufferHint: typeof helpers.getResponseBufferHint === 'function' ? helpers.getResponseBufferHint : null, inspectReceivedBytes: typeof helpers.inspectReceivedBytes === 'function' ? helpers.inspectReceivedBytes : null, parseSendExpected: typeof helpers.parseSendExpected === 'function' ? helpers.parseSendExpected : null, readResponseFromBuffer: typeof helpers.readResponseFromBuffer === 'function' ? helpers.readResponseFromBuffer : null }) } function configureProtocolHelpers(helpers = {}) { if (typeof helpers === 'function') { protocolHelpersLoader = helpers protocolHelpersLoaded = false return } protocolHelpersLoader = null protocolHelpersLoaded = true applyProtocolHelpers(helpers) } function ensureProtocolHelpers() { if (protocolHelpersLoaded || typeof protocolHelpersLoader !== 'function') return protocolHelpersLoaded = true applyProtocolHelpers(protocolHelpersLoader() || {}) } function setState(changedData) { Object.assign(state, changedData) subscribers.slice().forEach((subscriber) => { subscriber(getState()) }) } function getState() { return { ...state, devices: state.devices.slice(), logs: state.logs.slice() } } function subscribe(subscriber) { if (typeof subscriber !== 'function') return () => {} subscribers.push(subscriber) subscriber(getState()) return () => { const index = subscribers.indexOf(subscriber) if (index >= 0) subscribers.splice(index, 1) } } function subscribeRawResponse(subscriber) { if (typeof subscriber !== 'function') return () => {} rawResponseSubscribers.push(subscriber) return () => { const index = rawResponseSubscribers.indexOf(subscriber) if (index >= 0) rawResponseSubscribers.splice(index, 1) } } function callWx(apiName, params = {}) { return new Promise((resolve, reject) => { const api = wx[apiName] if (typeof api !== 'function') { reject(new Error(`${apiName} 不可用`)) return } api({ ...params, success: resolve, fail: reject }) }) } function formatBluetoothError(error) { if (!error) return '操作失败' const message = bluetoothErrorMap[error.errCode] if (message) return message return error.errMsg || error.message || '蓝牙操作失败' } function normalizeDevice(device) { 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 }) 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: Date.now() } } function formatSignalText(RSSI) { return typeof RSSI === 'number' ? `${RSSI} dBm` : '--' } 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 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 getResponseBufferHint(expected, options = {}) { if (Number.isFinite(Number(options.responseBufferHint))) { return Math.max(0, Math.round(Number(options.responseBufferHint))) } ensureProtocolHelpers() if (typeof protocolHelpers.getResponseBufferHint === 'function') { return Math.max(0, Math.round(Number(protocolHelpers.getResponseBufferHint(expected)) || 0)) } return 0 } function getResponseBufferLimit(expected, options = {}) { const responseLength = getResponseBufferHint(expected, options) const maxFrameBytes = options.maxFrameBytes const frameLimit = normalizeMaxFrameBytes(maxFrameBytes) if (frameLimit === 0) { return Math.max(MAX_RESPONSE_BUFFER_BYTES, responseLength + 8) } return Math.max(MAX_RESPONSE_BUFFER_BYTES, frameLimit + 8, responseLength + 8) } 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 validateDmaFrameLength(bytes, options = {}) { const maxFrameBytes = normalizeMaxFrameBytes(options.maxFrameBytes) if (maxFrameBytes === 0) return '' if (bytes.length > maxFrameBytes) { return `发送帧长度 ${bytes.length} 字节,超过最大包长 ${maxFrameBytes} 字节限制` } if (!options.expected) return '' const responseLength = getResponseBufferHint(options.expected, options) if (responseLength > maxFrameBytes) { return `预计返回帧长度 ${responseLength} 字节,超过最大包长 ${maxFrameBytes} 字节限制` } return '' } 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 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 addLog(direction, payload, note = '') { logSequence += 1 const logItem = { id: `log-${Date.now()}-${logSequence}`, direction, note, payload, time: formatTime(Date.now()) } const nextLogs = state.logs.concat(logItem).slice(-MAX_LOG_COUNT) setState({ logScrollTarget: logItem.id, logs: nextLogs }) } function getReceiveCrcState(rawBytes) { if (!rawBytes || rawBytes.length < 4) return '' ensureProtocolHelpers() if (typeof protocolHelpers.inspectReceivedBytes === 'function') { const note = protocolHelpers.inspectReceivedBytes(rawBytes, { pendingRequest: !!pendingRequest }) if (note) return note } return pendingRequest ? '片段' : '' } function showCommandAlert(title, content) { const message = content || title || '操作失败' notifyPageToast(message, 'error') setState({ errorText: message }) } function clearScanTimer() { if (!scanTimer) return clearTimeout(scanTimer) scanTimer = null } async function stopScan() { clearScanTimer() try { await callWx('stopBluetoothDevicesDiscovery') } catch (error) { if (error.errCode !== 10000) { setState({ errorText: formatBluetoothError(error) }) } } setState({ isDiscovering: false }) } function resetScanTimer() { clearScanTimer() scanTimer = setTimeout(() => { stopScan() if (!state.devices.length) { setState({ systemTip: '安卓真机请确认系统定位已开启,并允许微信使用附近设备或位置信息。' }) } }, SCAN_TIMEOUT) } function mergeDevices(devices) { if (!devices.length) return devices.forEach((device) => { if (!device.deviceId) return const previousDevice = deviceMap[device.deviceId] || {} const nextDevice = normalizeDevice(device) const advertisServiceUUIDs = mergeAdvertisedServiceUUIDs( previousDevice.advertisServiceUUIDs, nextDevice.advertisServiceUUIDs ) const isTargetAdvertised = !!previousDevice.isTargetAdvertised || hasTargetAdvertisedUuid({ advertisServiceUUIDs }) const isTargetDevice = !!previousDevice.isTargetDevice const seenIndex = previousDevice.seenIndex || (deviceSequence += 1) deviceMap[device.deviceId] = { ...previousDevice, ...nextDevice, advertisServiceUUIDs, displayName: nextDevice.displayName === '未命名设备' && previousDevice.displayName ? previousDevice.displayName : nextDevice.displayName, isTargetAdvertised, isTargetDevice, packetSize: nextDevice.packetSize || previousDevice.packetSize || DEFAULT_PACKET_SIZE, seenIndex, serviceText: advertisServiceUUIDs.length ? advertisServiceUUIDs.join(', ') : '未广播服务', targetText: isTargetDevice ? '已发现目标特征' : (isTargetAdvertised ? '广播含目标 UUID' : '') } }) refreshDeviceList() } function refreshDeviceList() { const deviceList = getSortedDeviceList() setState({ devices: deviceList.slice(0, 30) }) } function getSortedDeviceList() { return Object.keys(deviceMap) .map((deviceId) => deviceMap[deviceId]) .sort((left, right) => { const leftIndex = Number(left.seenIndex) || 0 const rightIndex = Number(right.seenIndex) || 0 return leftIndex - rightIndex }) } function clearPendingRequest() { if (!pendingRequest) return null const pending = pendingRequest clearTimeout(pendingRequest.timer) pendingRequest = null setState({ isAwaitingResponse: false }) return pending } function cancelPendingRequest() { const pending = clearPendingRequest() if (pending) { pending.resolve(false) } } function clearSendQueue() { if (!sendQueue.length) return const queuedJobs = sendQueue.splice(0) queuedJobs.forEach((job) => { job.resolve(false) }) setState({ sendQueueLength: 0 }) } function resetSendRuntimeState() { sendQueueGeneration += 1 cancelPendingRequest() clearSendQueue() isProcessingSendQueue = false setState({ isAwaitingResponse: false, isSending: false, sendQueueLength: 0 }) } function clearConnectedState(changedData = {}) { stopRssiRefresh() resetSendRuntimeState() setState({ characteristicText: '未选择', connectedDevice: null, connectedServiceCount: 0, connectingDeviceId: '', isConnecting: false, writeCharacteristicId: '', writeServiceId: '', writeType: '', ...changedData }) } function stopRssiRefresh() { if (rssiTimer) { clearInterval(rssiTimer) rssiTimer = null } isReadingRssi = false } function applyRssiUpdate(deviceId, rssi) { if (!state.connectedDevice || state.connectedDevice.deviceId !== deviceId || typeof rssi !== 'number') { return } const signalText = formatSignalText(rssi) const updatedDevice = { ...state.connectedDevice, RSSI: rssi, lastSeenAt: Date.now(), signalText } deviceMap[deviceId] = { ...(deviceMap[deviceId] || {}), RSSI: rssi, lastSeenAt: updatedDevice.lastSeenAt, signalText } setState({ connectedDevice: updatedDevice, devices: getSortedDeviceList().slice(0, 30) }) } async function refreshConnectedRssi() { const { connectedDevice } = state if (!connectedDevice || typeof wx === 'undefined' || typeof wx.getBLEDeviceRSSI !== 'function') return if (isReadingRssi) return isReadingRssi = true try { const result = await callWx('getBLEDeviceRSSI', { deviceId: connectedDevice.deviceId }) if (!state.connectedDevice || state.connectedDevice.deviceId !== connectedDevice.deviceId) return applyRssiUpdate(connectedDevice.deviceId, result && result.RSSI) } catch (error) { if (isConnectionLostError(error)) { clearConnectedState({ errorText: formatBluetoothError(error) }) } } finally { isReadingRssi = false } } function startRssiRefresh() { stopRssiRefresh() if (!state.connectedDevice || typeof wx === 'undefined' || typeof wx.getBLEDeviceRSSI !== 'function') { return } refreshConnectedRssi() rssiTimer = setInterval(() => { refreshConnectedRssi() }, RSSI_REFRESH_INTERVAL) } 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') } function finishPendingRequest(resolveValue) { const pending = clearPendingRequest() if (pending) { pending.resolve(resolveValue) } } function consumePendingResponseBuffer() { const pending = pendingRequest if (!pending || !Array.isArray(pending.responseBuffer)) return ensureProtocolHelpers() const responseReader = pending.responseReader || protocolHelpers.readResponseFromBuffer if (typeof responseReader !== 'function') { const content = `${pending.label} 未配置响应解析器,已丢弃` addLog('SYS', content) finishPendingRequest(false) if (pending.showModal) { showCommandAlert('通讯异常', content) } return } const result = responseReader(pending.responseBuffer, pending.expected, { maxFrameBytes: pending.expected && pending.expected.maxFrameBytes }) if (!result || result.status === 'pending') return if (result.status === 'frame-too-long') { const content = `${pending.label} 返回帧长度 ${result.responseLength} 字节,超过最大包长 ${result.frameLimit} 字节限制,已丢弃` addLog('SYS', content) finishPendingRequest(false) if (pending.showModal) { showCommandAlert('通讯异常', content) } return } if (result.status === 'invalid') { const content = `${pending.label} 收到无效响应帧,已丢弃` addLog('SYS', content) finishPendingRequest(false) if (pending.showModal) { showCommandAlert('通讯异常', content) } return } if (result.status === 'exception') { const content = result.message || `${pending.label} 收到异常响应帧` addLog('SYS', content) finishPendingRequest(false) if (pending.showModal) { showCommandAlert('设备返回故障帧', content) } return } if (result.status === 'mismatch') { const content = `${pending.label} 收到不匹配响应,已丢弃` addLog('SYS', content) finishPendingRequest(false) if (pending.showModal) { showCommandAlert('通讯异常', content) } return } if (result.status !== 'complete') { const content = `${pending.label} 收到未知响应状态,已丢弃` addLog('SYS', content) finishPendingRequest(false) if (pending.showModal) { showCommandAlert('通讯异常', content) } return } finishPendingRequest(result.response) if (pending.responseBuffer.length) { consumePendingResponseBuffer() } } function handleModbusResponse(bytes) { if (!pendingRequest || !Array.isArray(bytes) || !bytes.length) return pendingRequest.responseBuffer = pendingRequest.responseBuffer.concat(bytes) const bufferLimit = pendingRequest.responseBufferLimit || MAX_RESPONSE_BUFFER_BYTES if (pendingRequest.responseBuffer.length > bufferLimit) { const pending = pendingRequest const content = `${pending.label} 返回数据超过缓冲区,已丢弃` addLog('SYS', content) finishPendingRequest(false) if (pending.showModal) { showCommandAlert('通讯异常', content) } return } consumePendingResponseBuffer() } function createPendingRequest(label, expected, options = {}) { return new Promise((resolve) => { const timer = setTimeout(() => { const pending = clearPendingRequest() if (!pending) return addLog('SYS', `${label} 超时`) if (options.showModal !== false) { showCommandAlert('通讯超时', `${label} 1秒内没有收到回复`) } resolve(false) }, options.timeout || RESPONSE_TIMEOUT) pendingRequest = { expected, label, resolve, timer, responseBufferLimit: getResponseBufferLimit(expected, options), responseReader: typeof options.responseReader === 'function' ? options.responseReader : null, showModal: options.showModal !== false, responseBuffer: [] } setState({ isAwaitingResponse: true }) }) } function init() { if (initialized) return wx.onBluetoothDeviceFound((res) => { mergeDevices(res.devices || []) }) wx.onBluetoothAdapterStateChange((res) => { setState({ adapterAvailable: !!res.available, isDiscovering: !!res.discovering }) if (!res.available) { clearScanTimer() clearConnectedState({ adapterAvailable: false, adapterOpened: false, errorText: '请开启手机蓝牙后重新扫描', isDiscovering: false, sendQueueLength: 0 }) } }) wx.onBLEConnectionStateChange((res) => { const { connectedDevice } = state if (!connectedDevice || connectedDevice.deviceId !== res.deviceId) return if (!res.connected) { addLog('SYS', '连接已断开') clearConnectedState({ errorText: '', sendQueueLength: 0 }) } }) wx.onBLECharacteristicValueChange((res) => { const hex = arrayBufferToHex(res.value) const byteLength = res.value ? res.value.byteLength : 0 const rawBytes = Array.prototype.slice.call(new Uint8Array(res.value || new ArrayBuffer(0))) const crcState = getReceiveCrcState(rawBytes) setState({ rxCount: state.rxCount + byteLength }) addLog('RX', hex, crcState) rawResponseSubscribers.slice().forEach((subscriber) => { subscriber(rawBytes, res) }) handleModbusResponse(rawBytes) }) initialized = true } async function getAuthSetting() { return callWx('getSetting') .then((res) => res.authSetting || {}) .catch(() => ({})) } function showPermissionModal(title, content) { return new Promise((resolve, reject) => { wx.showModal({ title, content, confirmText: '去设置', success: async (res) => { if (!res.confirm) { reject(new Error('用户取消授权')) return } try { await callWx('openSetting') resolve() } catch (error) { reject(error) } }, fail: reject }) }) } async function ensureBluetoothAuthorized() { const authSetting = await getAuthSetting() if (authSetting['scope.bluetooth']) return if (authSetting['scope.bluetooth'] === false) { await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。') return } try { await callWx('authorize', { scope: 'scope.bluetooth' }) } catch (error) { await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。') } } async function ensureAndroidLocationAuthorized() { const systemInfo = wx.getSystemInfoSync ? wx.getSystemInfoSync() : wx.getDeviceInfo() if (systemInfo.platform !== 'android') return const authSetting = await getAuthSetting() if (authSetting['scope.userLocation']) return setState({ systemTip: '安卓系统扫描 BLE 设备通常需要开启系统定位权限。' }) if (authSetting['scope.userLocation'] === false) { await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。') return } try { await callWx('authorize', { scope: 'scope.userLocation' }) } catch (error) { await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。') } } async function openAdapter() { if (state.adapterOpened) { try { const adapterState = await callWx('getBluetoothAdapterState') setState({ adapterAvailable: !!adapterState.available, isDiscovering: !!adapterState.discovering }) if (adapterState.available) return } catch (error) { setState({ adapterAvailable: false, adapterOpened: false }) } } try { await callWx('openBluetoothAdapter', { mode: 'central' }) const adapterState = await callWx('getBluetoothAdapterState') setState({ adapterAvailable: !!adapterState.available, adapterOpened: true, isDiscovering: !!adapterState.discovering }) if (!adapterState.available) { throw { errCode: 10001, errMsg: 'bluetooth adapter not available' } } } catch (error) { if (error.errCode === 10001) { setState({ adapterOpened: true, adapterAvailable: false }) } throw error } } async function startDiscovery() { try { await callWx('startBluetoothDevicesDiscovery', { allowDuplicatesKey: true, interval: 600, powerLevel: 'high' }) } catch (error) { await callWx('startBluetoothDevicesDiscovery', { allowDuplicatesKey: true, interval: 600 }) } } async function startScan() { if (state.isConnecting) return deviceMap = {} deviceSequence = 0 setState({ devices: [], errorText: '' }) try { init() await ensureBluetoothAuthorized() await ensureAndroidLocationAuthorized() await openAdapter() await startDiscovery() setState({ isDiscovering: true }) resetScanTimer() addLog('SYS', '开始扫描 BLE 设备') } catch (error) { clearScanTimer() setState({ isDiscovering: false, errorText: formatBluetoothError(error) }) } } function clearDevices() { deviceMap = {} deviceSequence = 0 setState({ devices: [], errorText: '' }) } async function closeConnectedDevice(nextDeviceId, options = {}) { const { connectedDevice } = state if (!connectedDevice) { resetSendRuntimeState() return } if (connectedDevice.deviceId === nextDeviceId && !options.force) return resetSendRuntimeState() try { await callWx('closeBLEConnection', { deviceId: connectedDevice.deviceId }) } catch (error) { if (error.errCode !== 10006) throw error } clearConnectedState() } async function discoverCharacteristics(deviceId) { const serviceResult = await callWx('getBLEDeviceServices', { deviceId }) const services = [] let writeServiceId = '' let writeCharacteristicId = '' let writeType = '' let notifyServiceId = '' let notifyCharacteristicId = '' for (const service of serviceResult.services || []) { const characteristicResult = await callWx('getBLEDeviceCharacteristics', { deviceId, serviceId: service.uuid }) const characteristics = (characteristicResult.characteristics || []).map((item) => ({ uuid: item.uuid, role: getCharacteristicRole(item.properties), properties: item.properties || {} })) services.push({ uuid: service.uuid, primary: service.isPrimary, characteristics }) characteristics.forEach((item) => { const isPreferredService = isTargetUuid(service.uuid) const isPreferredCharacteristic = isTargetUuid(item.uuid) const canWrite = item.properties.write || item.properties.writeNoResponse const canNotify = item.properties.notify || item.properties.indicate if (isPreferredService && isPreferredCharacteristic && canWrite) { writeServiceId = service.uuid writeCharacteristicId = item.uuid writeType = item.properties.write ? 'write' : 'writeNoResponse' } if (isPreferredService && isPreferredCharacteristic && canNotify) { notifyServiceId = service.uuid notifyCharacteristicId = item.uuid } if (!writeCharacteristicId && canWrite) { writeServiceId = service.uuid writeCharacteristicId = item.uuid writeType = item.properties.write ? 'write' : 'writeNoResponse' } if (!notifyCharacteristicId && canNotify) { notifyServiceId = service.uuid notifyCharacteristicId = item.uuid } }) } return { services, writeServiceId, writeCharacteristicId, writeType, notifyServiceId, notifyCharacteristicId } } async function enableNotify(deviceId, serviceId, characteristicId) { try { await callWx('notifyBLECharacteristicValueChange', { deviceId, serviceId, characteristicId, state: true }) addLog('SYS', `已开启通知 ${characteristicId}`) return true } catch (error) { addLog('SYS', `开启通知失败:${formatBluetoothError(error)}`) if (isConnectionLostError(error)) { throw error } return false } } async function connectDeviceById(deviceId) { const device = deviceMap[deviceId] if (!device || state.isConnecting) return resetSendRuntimeState() setState({ connectingDeviceId: deviceId, errorText: '', isConnecting: true }) try { await stopScan() await closeConnectedDevice(deviceId, { force: state.connectedDevice && state.connectedDevice.deviceId === deviceId }) await openAdapter() await callWx('createBLEConnection', { deviceId, timeout: CONNECT_TIMEOUT }) const discovery = await discoverCharacteristics(deviceId) const notifyEnabled = discovery.notifyServiceId && discovery.notifyCharacteristicId ? await enableNotify(deviceId, discovery.notifyServiceId, discovery.notifyCharacteristicId) : false const isTargetDevice = hasTargetCharacteristic(discovery) const connectedDevice = { ...device, isTargetDevice, packetSize: device.packetSize || inferPacketSize(device), targetText: isTargetDevice ? '已发现目标特征' : device.targetText } deviceMap[deviceId] = connectedDevice refreshDeviceList() setState({ characteristicText: buildCharacteristicText(discovery.writeServiceId, discovery.writeCharacteristicId), connectedDevice, connectedServiceCount: discovery.services.length, connectingDeviceId: '', errorText: discovery.writeServiceId ? (notifyEnabled ? '' : '已连接,但未成功开启通知,可能收不到设备回复') : '已连接,但未找到可写特征值', isConnecting: false, writeCharacteristicId: discovery.writeCharacteristicId, writeServiceId: discovery.writeServiceId, writeType: discovery.writeType }) startRssiRefresh() addLog('SYS', `已连接 ${device.displayName}`) } catch (error) { resetSendRuntimeState() setState({ connectingDeviceId: '', isConnecting: false, errorText: formatBluetoothError(error) }) } } async function disconnectDevice() { const { connectedDevice } = state if (!connectedDevice) return try { await callWx('closeBLEConnection', { deviceId: connectedDevice.deviceId }) } catch (error) { if (error.errCode !== 10006) { setState({ errorText: formatBluetoothError(error) }) return } } addLog('SYS', '主动断开连接') clearConnectedState({ errorText: '', sendQueueLength: 0 }) } async function refreshNativeConnectionState() { if (!state.connectedDevice || typeof wx.getConnectedBluetoothDevices !== 'function') return true try { const services = state.writeServiceId ? [state.writeServiceId] : [] const result = await callWx('getConnectedBluetoothDevices', { services }) const isConnected = (result.devices || []).some((device) => device.deviceId === state.connectedDevice.deviceId) if (isConnected) return true addLog('SYS', '蓝牙连接状态已失效') clearConnectedState({ errorText: '蓝牙连接已失效,请重新连接' }) return false } catch (error) { if (isConnectionLostError(error)) { clearConnectedState({ errorText: formatBluetoothError(error) }) return false } return true } } function handleAppHide() { clearScanTimer() stopRssiRefresh() resetSendRuntimeState() if (state.isDiscovering) { stopScan() } } async function handleAppShow() { if (!state.connectedDevice) return init() const connected = await refreshNativeConnectionState() if (connected && state.connectedDevice) { startRssiRefresh() } } function setSendHex(sendHex) { setState({ sendHex, errorText: '' }) } function clearInput() { setState({ sendHex: '', errorText: '' }) } function clearLogs() { setState({ logScrollTarget: '', logs: [], rxCount: 0, txCount: 0 }) } function enqueueSendFrame(hexFrame, source, options = {}) { if (!state.connectedDevice) { setState({ errorText: '请先连接蓝牙透传设备' }) return Promise.resolve(false) } if (!state.writeServiceId || !state.writeCharacteristicId) { setState({ errorText: '当前设备没有可写特征值' }) return Promise.resolve(false) } const errorText = validateHex(hexFrame) if (errorText) { setState({ errorText }) return Promise.resolve(false) } const buffer = hexToArrayBuffer(hexFrame) const bytes = new Uint8Array(buffer) const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options) if (dmaFrameLengthError) { setState({ errorText: dmaFrameLengthError }) return Promise.resolve(false) } return new Promise((resolve) => { sendJobSequence += 1 sendQueue.push({ id: sendJobSequence, hexFrame, options, resolve, source }) setState({ sendQueueLength: sendQueue.length }) processSendQueue() }) } async function processSendQueue() { if (isProcessingSendQueue) return const generation = sendQueueGeneration isProcessingSendQueue = true try { while (sendQueue.length && generation === sendQueueGeneration) { const job = sendQueue.shift() setState({ sendQueueLength: sendQueue.length }) let result = false try { result = await executeSendFrame(job.hexFrame, job.source, job.options) } catch (error) { cancelPendingRequest() setState({ errorText: error.message || '发送失败' }) } job.resolve(result) if (!state.connectedDevice) { clearSendQueue() break } } } finally { if (generation === sendQueueGeneration) { isProcessingSendQueue = false } } } async function executeSendFrame(hexFrame, source, options = {}) { const { connectedDevice, writeCharacteristicId, writeServiceId, writeType } = state const errorText = validateHex(hexFrame) if (!connectedDevice) { setState({ errorText: '请先连接蓝牙透传设备' }) return false } if (!writeServiceId || !writeCharacteristicId) { setState({ errorText: '当前设备没有可写特征值' }) return false } if (errorText) { setState({ errorText }) return false } const buffer = hexToArrayBuffer(hexFrame) const bytes = new Uint8Array(buffer) const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options) if (dmaFrameLengthError) { setState({ errorText: dmaFrameLengthError }) return false } const chunkSize = resolvePacketSize( options.chunkSize === undefined ? connectedDevice.packetSize : options.chunkSize, bytes.length ) const waitResponse = !!options.expected const responsePromise = waitResponse ? createPendingRequest(source, options.expected, options) : null setState({ isSending: true, errorText: '' }) try { for (let offset = 0; offset < bytes.length; offset += chunkSize) { const chunk = bytes.slice(offset, offset + chunkSize) await callWx('writeBLECharacteristicValue', { deviceId: connectedDevice.deviceId, serviceId: writeServiceId, characteristicId: writeCharacteristicId, value: chunk.buffer, writeType }) } setState({ txCount: state.txCount + bytes.length }) addLog('TX', arrayBufferToHex(buffer), source) if (waitResponse) { return responsePromise } return true } catch (error) { if (waitResponse) { cancelPendingRequest() } if (isConnectionLostError(error)) { clearConnectedState({ errorText: formatBluetoothError(error) }) } else { setState({ errorText: formatBluetoothError(error) }) } return false } finally { setState({ isSending: false }) } } function sendManagedFrame(frameBytes, label, expected, options = {}) { return enqueueSendFrame(formatFrameHex(frameBytes), label, { expected: expected ? { ...expected, maxFrameBytes: options.maxFrameBytes === undefined ? DEFAULT_MAX_FRAME_BYTES : options.maxFrameBytes } : expected, responseBufferHint: options.responseBufferHint, responseReader: options.responseReader, showModal: options.showModal !== false, timeout: options.timeout || RESPONSE_TIMEOUT, maxFrameBytes: options.maxFrameBytes === undefined ? DEFAULT_MAX_FRAME_BYTES : options.maxFrameBytes }) } function sendRawFrameExact(frameBytes, source) { const bytes = frameBytes instanceof Uint8Array ? frameBytes : new Uint8Array(frameBytes || []) return enqueueSendFrame(formatFrameHex(Array.prototype.slice.call(bytes)), source, { chunkSize: 0, skipDmaCheck: true }) } function sendHexFrame() { const errorText = validateHex(state.sendHex) ensureProtocolHelpers() const expected = errorText || typeof protocolHelpers.parseSendExpected !== 'function' ? null : protocolHelpers.parseSendExpected(Array.prototype.slice.call(new Uint8Array(hexToArrayBuffer(state.sendHex)))) return enqueueSendFrame(state.sendHex, 'HEX', expected ? { expected } : {}) } module.exports = { clearDevices, clearInput, clearLogs, configureProtocolHelpers, connectDeviceById, disconnectDevice, enqueueSendFrame, getState, handleAppHide, handleAppShow, init, sendHexFrame, sendManagedFrame, sendRawFrameExact, setSendHex, showCommandAlert, startScan, stopScan, subscribe, subscribeRawResponse }