| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278 |
- 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: '',
- signalText: '',
- 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,
- signalText: '',
- 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,
- signalText: result.updatedDevice.signalText || '',
- 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 = result.message
- ? `${pending.label} 收到无效响应帧:${result.message}`
- : `${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
- }
|