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