const { notifyPageToast } = require('../utils/page-toast.js') const { bytesToUtf8Text } = require('../utils/binary-utils.js') const { DEFAULT_MAX_FRAME_BYTES, arrayBufferToHex, buildCharacteristicText, formatBluetoothError, formatFrameHex, getCharacteristicRole, hasTargetCharacteristic, hexToArrayBuffer, inferPacketSize, isConnectionLostError, isTargetUuid, normalizeMaxFrameBytes, resolvePacketSize, validateHex } = require('./ble-utils.js') const { DEFAULT_MAX_LOG_COUNT, appendLog, createClearLogsState, createLogItem } = require('./ble-logs.js') const { createProtocolHelperRegistry } = require('./protocol-helper-registry.js') const { createBleDeviceRegistry } = require('./ble-device-registry.js') const SCAN_TIMEOUT = 15000 const CONNECT_TIMEOUT = 10000 const RSSI_REFRESH_INTERVAL = 2000 const RESPONSE_TIMEOUT = 1000 const MAX_RESPONSE_BUFFER_BYTES = 128 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 logSequence = 0 const subscribers = [] const rawResponseSubscribers = [] const protocolHelperRegistry = createProtocolHelperRegistry() const deviceRegistry = createBleDeviceRegistry() function configureProtocolHelpers(helpers = {}) { protocolHelperRegistry.configure(helpers) } 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 getResponseBufferHint(expected, options = {}) { if (Number.isFinite(Number(options.responseBufferHint))) { return Math.max(0, Math.round(Number(options.responseBufferHint))) } const getHint = protocolHelperRegistry.get('getResponseBufferHint') if (typeof getHint === 'function') { return Math.max(0, Math.round(Number(getHint(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 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 addLog(direction, payload, note = '', extras = {}) { logSequence += 1 const logItem = createLogItem(direction, payload, note, extras, logSequence) const nextLogs = appendLog(state.logs, logItem, DEFAULT_MAX_LOG_COUNT) setState({ logScrollTarget: logItem.id, logs: nextLogs }) } function getReceiveCrcState(rawBytes) { if (!rawBytes || rawBytes.length < 4) return '' const inspectReceivedBytes = protocolHelperRegistry.get('inspectReceivedBytes') if (typeof inspectReceivedBytes === 'function') { const note = inspectReceivedBytes(rawBytes, { pendingRequest: pendingRequest ? pendingRequest.expected : null }) 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) { const changed = deviceRegistry.mergeDevices(devices) if (!changed) return setState({ devices: deviceRegistry.getDeviceList() }) } 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 result = deviceRegistry.applyRssiUpdate(deviceId, rssi, state.connectedDevice) if (!result || !result.updatedDevice) return setState({ connectedDevice: result.updatedDevice, devices: result.deviceList }) } 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 finishPendingRequest(resolveValue) { const pending = clearPendingRequest() if (pending) { pending.resolve(resolveValue) } } function consumePendingResponseBuffer() { const pending = pendingRequest if (!pending || !Array.isArray(pending.responseBuffer)) return const responseReader = pending.responseReader || protocolHelperRegistry.get('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 handleReceivedResponseBytes(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, { payloadBytes: rawBytes, payloadText: bytesToUtf8Text(rawBytes) }) rawResponseSubscribers.slice().forEach((subscriber) => { subscriber(rawBytes, res) }) handleReceivedResponseBytes(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 deviceRegistry.clear() 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() { deviceRegistry.clear() 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 = deviceRegistry.getDevice(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 = deviceRegistry.markConnectedDevice(deviceId, { isTargetDevice }) || { ...device, isTargetDevice, packetSize: device.packetSize || inferPacketSize(device), targetText: isTargetDevice ? '已发现目标特征' : device.targetText } 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, devices: deviceRegistry.getDeviceList() }) 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(createClearLogsState()) } 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, { payloadBytes: Array.prototype.slice.call(bytes), payloadText: bytesToUtf8Text(bytes) }) 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) const parseSendExpected = protocolHelperRegistry.get('parseSendExpected') const expected = errorText || typeof parseSendExpected !== 'function' ? null : 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 }