| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273 |
- 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
- }
|