feat: 添加蓝牙调试页面的初始实现
- 新增 BLVRQPage.json 配置文件,设置导航栏样式。 - 新增 BLVRQPage.wxml 文件,构建蓝牙设备信息展示和操作界面。 - 新增 BLVRQPage.wxss 文件,定义页面样式和布局。
This commit is contained in:
773
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js
Normal file
773
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js
Normal file
@@ -0,0 +1,773 @@
|
||||
const app = getApp()
|
||||
|
||||
function ab2hex(buffer) {
|
||||
const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer || [])
|
||||
return Array.from(view).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ')
|
||||
}
|
||||
|
||||
function asciiToBytes(text) {
|
||||
const bytes = []
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
bytes.push(text.charCodeAt(i) & 0xFF)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
function bytesToAscii(bytes) {
|
||||
return String.fromCharCode(...(bytes || []).filter(v => v > 0))
|
||||
}
|
||||
|
||||
function crc16Modbus(bytes) {
|
||||
let crc = 0xFFFF
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
crc ^= bytes[i]
|
||||
for (let j = 0; j < 8; j += 1) {
|
||||
const lsb = crc & 0x0001
|
||||
crc >>= 1
|
||||
if (lsb) crc ^= 0xA001
|
||||
}
|
||||
}
|
||||
return crc & 0xFFFF
|
||||
}
|
||||
|
||||
function buildPacket(frameType, params = []) {
|
||||
const payload = Array.isArray(params) ? params.slice() : Array.from(params || [])
|
||||
const len = 6 + payload.length
|
||||
const bytes = [0xDD, 0xD0, len & 0xFF, 0x00, 0x00, frameType & 0xFF, ...payload]
|
||||
const crc = crc16Modbus(bytes)
|
||||
bytes[3] = crc & 0xFF
|
||||
bytes[4] = (crc >> 8) & 0xFF
|
||||
return Uint8Array.from(bytes)
|
||||
}
|
||||
|
||||
function parsePacket(buffer) {
|
||||
const bytes = Array.from(new Uint8Array(buffer || []))
|
||||
if (bytes.length < 6) return null
|
||||
if (bytes[0] !== 0xDD || bytes[1] !== 0xD0) return null
|
||||
const len = bytes[2]
|
||||
if (len !== bytes.length) return null
|
||||
const inputCrc = bytes[3] | (bytes[4] << 8)
|
||||
const crcBytes = bytes.slice()
|
||||
crcBytes[3] = 0x00
|
||||
crcBytes[4] = 0x00
|
||||
const calcCrc = crc16Modbus(crcBytes)
|
||||
if (inputCrc !== calcCrc) {
|
||||
return { invalid: true, bytes, reason: 'CRC校验失败' }
|
||||
}
|
||||
return {
|
||||
bytes,
|
||||
len,
|
||||
frameType: bytes[5],
|
||||
params: bytes.slice(6)
|
||||
}
|
||||
}
|
||||
|
||||
function nowText() {
|
||||
const d = new Date()
|
||||
const pad = (n, l = 2) => String(n).padStart(l, '0')
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`
|
||||
}
|
||||
|
||||
function formatBleError(err) {
|
||||
if (!err) return '未知错误'
|
||||
const parts = []
|
||||
if (typeof err.errCode !== 'undefined') parts.push(`errCode=${err.errCode}`)
|
||||
if (err.errMsg) parts.push(err.errMsg)
|
||||
if (err.message && err.message !== err.errMsg) parts.push(err.message)
|
||||
return parts.length ? parts.join(' | ') : String(err)
|
||||
}
|
||||
|
||||
function createFunctionOptions() {
|
||||
const names = {
|
||||
0x00: '不设置',
|
||||
0x01: '普通按键功能',
|
||||
0x02: '清理按键',
|
||||
0x03: '投诉按键'
|
||||
}
|
||||
return Array.from({ length: 16 }, (_, i) => ({
|
||||
code: i,
|
||||
label: `${i}: ${names[i] || '无效'}`
|
||||
}))
|
||||
}
|
||||
|
||||
function createKeys(count) {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
hwIndex: i + 1,
|
||||
mappedKey: i + 1,
|
||||
mappedKeyIndex: i + 1,
|
||||
originalMappedKey: i + 1,
|
||||
functionCode: 0x01,
|
||||
functionIndex: 1
|
||||
}))
|
||||
}
|
||||
|
||||
function createKeySlots(keys, totalSlots = 8) {
|
||||
const list = Array.isArray(keys) ? keys : []
|
||||
return Array.from({ length: totalSlots }, (_, i) => {
|
||||
const key = list[i]
|
||||
if (key) {
|
||||
return {
|
||||
slotIndex: i,
|
||||
isPlaceholder: false,
|
||||
...key
|
||||
}
|
||||
}
|
||||
return {
|
||||
slotIndex: i,
|
||||
isPlaceholder: true,
|
||||
hwIndex: i + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
bleInfo: {
|
||||
devName: '',
|
||||
mac: '',
|
||||
serviceId: '',
|
||||
txCharId: '',
|
||||
rxCharId: '',
|
||||
readCharId: '',
|
||||
writeCharId: '',
|
||||
connected: false,
|
||||
rssi: null,
|
||||
signalText: '-'
|
||||
},
|
||||
bleNameInput: '',
|
||||
canSetBleName: false,
|
||||
deviceInfo: {
|
||||
model: 'BLV_RQ',
|
||||
softwareVersion: '-',
|
||||
hardwareVersion: '-'
|
||||
},
|
||||
keyConfig: {
|
||||
selectedKeyCount: 0,
|
||||
selectedKeyTypeIndex: 0,
|
||||
keyCountOptions: ['请选择按键类型', '1键', '2键', '3键', '4键', '5键', '6键', '7键', '8键'],
|
||||
keyCountValues: [0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
keys: [],
|
||||
keySlots: createKeySlots([])
|
||||
},
|
||||
functionOptions: createFunctionOptions(),
|
||||
functionOptionLabels: createFunctionOptions().map(item => item.label),
|
||||
canOperateKeyConfig: false,
|
||||
isConnecting: false,
|
||||
readingVersion: false,
|
||||
readingKeyConfig: false,
|
||||
writingKeyConfig: false,
|
||||
settingBleName: false,
|
||||
keySendingIndex: -1,
|
||||
logs: [],
|
||||
logScrollTo: '',
|
||||
confirmDialog: {
|
||||
visible: false,
|
||||
message: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const devName = options && options.DevName ? decodeURIComponent(options.DevName) : 'BLV_RQ设备'
|
||||
const mac = options && options.mac ? decodeURIComponent(options.mac) : ''
|
||||
const connected = options && (options.connected === '1' || options.connected === 'true')
|
||||
const serviceId = options && options.serviceId ? decodeURIComponent(options.serviceId) : ''
|
||||
const txCharId = options && options.txCharId ? decodeURIComponent(options.txCharId) : ''
|
||||
const rxCharId = options && options.rxCharId ? decodeURIComponent(options.rxCharId) : ''
|
||||
const readCharId = options && options.readCharId ? decodeURIComponent(options.readCharId) : rxCharId
|
||||
const writeCharId = options && options.writeCharId ? decodeURIComponent(options.writeCharId) : txCharId
|
||||
const resumeConnectedSession = connected || !!(mac && serviceId && txCharId && rxCharId)
|
||||
this.setData({
|
||||
bleInfo: {
|
||||
devName,
|
||||
mac,
|
||||
serviceId,
|
||||
txCharId,
|
||||
rxCharId,
|
||||
readCharId,
|
||||
writeCharId,
|
||||
connected: resumeConnectedSession,
|
||||
rssi: null,
|
||||
signalText: ''
|
||||
},
|
||||
canOperateKeyConfig: false
|
||||
})
|
||||
this.appendLog('CFG', `进入BLV_RQ页面:${devName}`)
|
||||
this.updateSetBleNameState('')
|
||||
if (app.globalData && app.globalData.pendingBleNavigation && app.globalData.pendingBleNavigation.target === 'BLV_RQ') {
|
||||
app.globalData.pendingBleNavigation = null
|
||||
}
|
||||
if (resumeConnectedSession) {
|
||||
this.activateConnectedSession('页面初始化')
|
||||
} else {
|
||||
this.ensureBleReady('页面初始化')
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.bleInfo.connected) {
|
||||
this.bindConnectionStateListener()
|
||||
this.startBleNotifications()
|
||||
this.startStatusPolling()
|
||||
this.refreshBleStatus()
|
||||
return
|
||||
}
|
||||
this.ensureBleReady('页面显示')
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.stopStatusPolling()
|
||||
this.teardownBleNotifications()
|
||||
this.teardownConnectionStateListener()
|
||||
},
|
||||
|
||||
appendLog(type, text, extra = {}) {
|
||||
const map = { TX: 'tx', RX: 'rx', WARN: 'warn', ERR: 'err', UI: 'ui', CFG: 'cfg' }
|
||||
let displayText = text
|
||||
let hexText = ''
|
||||
let noteText = text
|
||||
if (type === 'TX' && text.includes('|')) {
|
||||
const parts = text.split('|')
|
||||
noteText = parts[0].trim()
|
||||
hexText = parts.slice(1).join('|').trim()
|
||||
displayText = hexText || noteText
|
||||
} else if (type === 'RX') {
|
||||
hexText = text.replace(/^\[[^\]]+\]\s*/, '').trim()
|
||||
displayText = hexText
|
||||
}
|
||||
const item = {
|
||||
id: Date.now() + Math.random(),
|
||||
time: nowText(),
|
||||
type,
|
||||
typeClass: map[type] || 'cfg',
|
||||
text: extra.cmd ? `[${extra.cmd}] ${text}` : text,
|
||||
displayText,
|
||||
hexText,
|
||||
noteText
|
||||
}
|
||||
const logs = [...this.data.logs, item].slice(-300)
|
||||
this.setData({ logs, logScrollTo: `log-${item.id}` })
|
||||
},
|
||||
|
||||
updateSetBleNameState(value) {
|
||||
const can = /^[A-Za-z0-9]{1,8}$/.test(value || '') && !!this.data.bleInfo.connected
|
||||
this.setData({ bleNameInput: value, canSetBleName: can })
|
||||
},
|
||||
|
||||
onBleNameInput(e) {
|
||||
const raw = (e.detail.value || '').replace(/[^A-Za-z0-9]/g, '').slice(0, 8)
|
||||
this.updateSetBleNameState(raw)
|
||||
},
|
||||
|
||||
onKeyCountChange(e) {
|
||||
const selectedKeyTypeIndex = Number(e.detail.value || 0)
|
||||
const count = this.data.keyConfig.keyCountValues[selectedKeyTypeIndex] || 0
|
||||
const keys = createKeys(count)
|
||||
this.setData({
|
||||
'keyConfig.selectedKeyTypeIndex': selectedKeyTypeIndex,
|
||||
'keyConfig.selectedKeyCount': count,
|
||||
'keyConfig.keys': keys,
|
||||
'keyConfig.keySlots': createKeySlots(keys),
|
||||
canOperateKeyConfig: !!this.data.bleInfo.connected && count > 0
|
||||
})
|
||||
this.appendLog('UI', count ? `选择按键类型:${count}键` : '清空按键类型选择')
|
||||
},
|
||||
|
||||
onFunctionChange(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const value = Number(e.detail.value || 0)
|
||||
const keys = [...this.data.keyConfig.keys]
|
||||
if (!keys[index]) return
|
||||
keys[index].functionIndex = value
|
||||
keys[index].functionCode = this.data.functionOptions[value].code
|
||||
this.setData({ 'keyConfig.keys': keys, 'keyConfig.keySlots': createKeySlots(keys) })
|
||||
},
|
||||
|
||||
async onReadVersion() {
|
||||
if (!this.data.bleInfo.connected) return
|
||||
this.setData({ readingVersion: true })
|
||||
await this.sendCommand(0x01, [0x01], '读取版本')
|
||||
this.setData({ readingVersion: false })
|
||||
},
|
||||
|
||||
async onSetBleName() {
|
||||
if (!this.data.canSetBleName) {
|
||||
wx.showToast({ title: '名称不合法', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const bytes = asciiToBytes(this.data.bleNameInput)
|
||||
this.setData({ settingBleName: true })
|
||||
await this.sendCommand(0x05, [bytes.length, ...bytes], '设置蓝牙名称')
|
||||
this.setData({ settingBleName: false })
|
||||
},
|
||||
|
||||
async onReadKeyConfig() {
|
||||
if (!this.data.bleInfo.connected) {
|
||||
wx.showToast({ title: '请先连接设备', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ readingKeyConfig: true })
|
||||
await this.sendCommand(0x04, [0x01], '读取按键配置')
|
||||
this.setData({ readingKeyConfig: false })
|
||||
},
|
||||
|
||||
onSubmitKeyConfig() {
|
||||
const validation = this.validateKeyConfig()
|
||||
if (validation.blocked) {
|
||||
wx.showModal({ title: '配置校验失败', content: validation.message, showCancel: false })
|
||||
return
|
||||
}
|
||||
const message = `确认下发当前${validation.keyCount}键的按键功能配置?`
|
||||
this._pendingSubmit = validation
|
||||
this.setData({ 'confirmDialog.visible': true, 'confirmDialog.message': message })
|
||||
},
|
||||
|
||||
onCancelSubmit() {
|
||||
this._pendingSubmit = null
|
||||
this.setData({ 'confirmDialog.visible': false, 'confirmDialog.message': '' })
|
||||
},
|
||||
|
||||
async onConfirmSubmit() {
|
||||
const validation = this._pendingSubmit || this.validateKeyConfig()
|
||||
this.onCancelSubmit()
|
||||
const keys = validation.keys
|
||||
const keyCount = validation.keyCount
|
||||
const functionPayload = [keyCount]
|
||||
for (let i = 0; i < keyCount; i += 1) {
|
||||
const item = keys[i]
|
||||
functionPayload.push(item ? item.functionCode : 0x00)
|
||||
}
|
||||
this.setData({ writingKeyConfig: true })
|
||||
const step1 = await this.sendCommand(0x02, functionPayload, '设置按键功能', { expectResponse: true, silentError: true })
|
||||
if (!step1) {
|
||||
this.setData({ writingKeyConfig: false })
|
||||
wx.showToast({ title: '功能设置失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ writingKeyConfig: false })
|
||||
wx.showToast({ title: '设置成功', icon: 'success' })
|
||||
},
|
||||
|
||||
async onTriggerKey(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const item = this.data.keyConfig.keys[index]
|
||||
if (!item || !item.mappedKey) {
|
||||
wx.showToast({ title: '当前按键未配置映射', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const keyCount = Number(this.data.keyConfig.selectedKeyCount || 0)
|
||||
if (!keyCount) {
|
||||
wx.showToast({ title: '请先选择按键类型', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const bitmap = 1 << (item.mappedKey - 1)
|
||||
this.setData({ keySendingIndex: index })
|
||||
await this.sendCommand(0x06, [keyCount, bitmap], `触发按键${item.hwIndex} -> 键值${item.mappedKey}(${keyCount}键)`)
|
||||
this.setData({ keySendingIndex: -1 })
|
||||
},
|
||||
|
||||
onClearLogs() {
|
||||
this.setData({ logs: [], logScrollTo: '' })
|
||||
},
|
||||
|
||||
onExportLogs() {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager && wx.getFileSystemManager()
|
||||
if (!fs || !wx.env || !wx.env.USER_DATA_PATH) {
|
||||
wx.showToast({ title: '当前环境不支持导出', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const name = `BLV_RQ_Log_${this.getExportTimestamp()}.txt`
|
||||
const path = `${wx.env.USER_DATA_PATH}/${name}`
|
||||
const content = this.buildLogExportText()
|
||||
fs.writeFileSync(path, content, 'utf8')
|
||||
this.appendLog('UI', `日志已导出:${name}`)
|
||||
if (typeof wx.openDocument === 'function') {
|
||||
wx.openDocument({ filePath: path, showMenu: true })
|
||||
} else {
|
||||
wx.showToast({ title: '日志已导出', icon: 'success' })
|
||||
}
|
||||
} catch (err) {
|
||||
this.appendLog('ERR', `导出日志失败:${(err && err.message) || err}`)
|
||||
wx.showToast({ title: '导出失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
getExportTimestamp() {
|
||||
const d = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
|
||||
},
|
||||
|
||||
buildLogExportText() {
|
||||
const info = this.data.bleInfo
|
||||
const device = this.data.deviceInfo
|
||||
const lines = [
|
||||
'BLV_RQ 通信日志导出',
|
||||
`导出时间:${new Date().toLocaleString()}`,
|
||||
`蓝牙名称:${info.devName || '-'}`,
|
||||
`MAC:${info.mac || '-'}`,
|
||||
`连接状态:${info.connected ? '已连接' : '未连接'}`,
|
||||
`信号值:${info.signalText || '-'}`,
|
||||
`设备型号:${device.model || '-'}`,
|
||||
`软件版本:${device.softwareVersion || '-'}`,
|
||||
`硬件版本:${device.hardwareVersion || '-'}`,
|
||||
'----------------------------------------'
|
||||
]
|
||||
this.data.logs.forEach(item => {
|
||||
if (item.type === 'TX') {
|
||||
lines.push(`[${item.time}] TX -> ${item.displayText || item.text}`)
|
||||
} else if (item.type === 'RX') {
|
||||
lines.push(`[${item.time}] RX <- ${item.displayText || item.text}`)
|
||||
} else {
|
||||
lines.push(`[${item.time}] [${item.type}] ${item.text}`)
|
||||
}
|
||||
})
|
||||
return lines.join('\n')
|
||||
},
|
||||
|
||||
onDisconnect(showToast = false) {
|
||||
const { mac } = this.data.bleInfo
|
||||
if (!mac || typeof wx.closeBLEConnection !== 'function') return
|
||||
this.stopStatusPolling()
|
||||
wx.closeBLEConnection({
|
||||
deviceId: mac,
|
||||
complete: () => {
|
||||
this.setData({
|
||||
'bleInfo.connected': false,
|
||||
'bleInfo.rssi': null,
|
||||
'bleInfo.signalText': '',
|
||||
canOperateKeyConfig: false
|
||||
})
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
this.appendLog('CFG', '蓝牙连接已断开')
|
||||
if (showToast) wx.showToast({ title: '已断开连接', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onConnectionAction() {
|
||||
if (this.data.isConnecting) return
|
||||
if (this.data.bleInfo.connected) {
|
||||
this.onDisconnect(true)
|
||||
return
|
||||
}
|
||||
this.onReconnect()
|
||||
},
|
||||
|
||||
onReconnect() {
|
||||
this.appendLog('UI', '请求重连蓝牙')
|
||||
this.ensureBleReady('手动重连', true)
|
||||
},
|
||||
|
||||
validateKeyConfig() {
|
||||
const keys = (this.data.keyConfig.keys || []).map(item => ({ ...item }))
|
||||
const selectedCount = this.data.keyConfig.selectedKeyCount
|
||||
if (!selectedCount) {
|
||||
return {
|
||||
blocked: true,
|
||||
message: '请先选择按键类型',
|
||||
keys,
|
||||
keyCount: 0
|
||||
}
|
||||
}
|
||||
return {
|
||||
blocked: false,
|
||||
message: '',
|
||||
keys,
|
||||
keyCount: selectedCount
|
||||
}
|
||||
},
|
||||
|
||||
async sendCommand(frameType, params, text, options = {}) {
|
||||
const { mac, serviceId, txCharId, writeCharId } = this.data.bleInfo
|
||||
const targetWriteCharId = writeCharId || txCharId
|
||||
if (!mac || !serviceId || !targetWriteCharId) {
|
||||
wx.showToast({ title: '蓝牙通道未就绪', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
const packet = buildPacket(frameType, params)
|
||||
this.appendLog('TX', `${text} | ${ab2hex(packet)}`, { cmd: `0x${frameType.toString(16).toUpperCase().padStart(2, '0')}` })
|
||||
return new Promise((resolve) => {
|
||||
this._pendingResponse = {
|
||||
frameType,
|
||||
resolve,
|
||||
silentError: !!options.silentError,
|
||||
timer: setTimeout(() => {
|
||||
this._pendingResponse = null
|
||||
this.appendLog('WARN', `${text}超时`, { cmd: `0x${frameType.toString(16).toUpperCase().padStart(2, '0')}` })
|
||||
resolve(false)
|
||||
}, 4000)
|
||||
}
|
||||
wx.writeBLECharacteristicValue({
|
||||
deviceId: mac,
|
||||
serviceId,
|
||||
characteristicId: targetWriteCharId,
|
||||
value: packet.buffer,
|
||||
fail: (err) => {
|
||||
if (this._pendingResponse && this._pendingResponse.timer) clearTimeout(this._pendingResponse.timer)
|
||||
this._pendingResponse = null
|
||||
this.appendLog('ERR', `${text}发送失败:${(err && err.errMsg) || '未知错误'}`)
|
||||
if (!options.silentError) wx.showToast({ title: '发送失败', icon: 'none' })
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
startBleNotifications() {
|
||||
const { mac, serviceId, txCharId, rxCharId } = this.data.bleInfo
|
||||
const notifyTargets = [txCharId, rxCharId].filter(Boolean)
|
||||
if (!mac || !serviceId || !notifyTargets.length || typeof wx.notifyBLECharacteristicValueChange !== 'function') return
|
||||
let pending = notifyTargets.length
|
||||
notifyTargets.forEach((characteristicId) => {
|
||||
wx.notifyBLECharacteristicValueChange({
|
||||
state: true,
|
||||
deviceId: mac,
|
||||
serviceId,
|
||||
characteristicId,
|
||||
complete: () => {
|
||||
pending -= 1
|
||||
if (pending <= 0) {
|
||||
this.bindBleListener()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
ensureBleReady(source = '状态校验', showToast = false) {
|
||||
const { mac } = this.data.bleInfo
|
||||
if (!mac || typeof wx.createBLEConnection !== 'function') return
|
||||
if (this._connecting) return
|
||||
this._connecting = true
|
||||
this.setData({ isConnecting: true })
|
||||
this.appendLog('CFG', `${source}:开始校验蓝牙连接`)
|
||||
wx.createBLEConnection({
|
||||
deviceId: mac,
|
||||
success: () => {
|
||||
this._connecting = false
|
||||
this.activateConnectedSession(source, showToast)
|
||||
},
|
||||
fail: (err) => {
|
||||
this._connecting = false
|
||||
const reason = formatBleError(err)
|
||||
const message = String(reason || '').toLowerCase()
|
||||
if (message.includes('already connect') || message.includes('already connected') || reason === '已连接' || reason === '设备已连接') {
|
||||
this.appendLog('CFG', `${source}检测到设备已连接,复用当前蓝牙会话`)
|
||||
this.activateConnectedSession(source, showToast)
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
isConnecting: false,
|
||||
'bleInfo.connected': false,
|
||||
'bleInfo.rssi': null,
|
||||
'bleInfo.signalText': '',
|
||||
canOperateKeyConfig: false
|
||||
})
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
this.appendLog('ERR', `${source}失败:${reason}`)
|
||||
if (showToast) wx.showToast({ title: '重连失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
activateConnectedSession(source = '状态校验', showToast = false) {
|
||||
this.setData({
|
||||
isConnecting: false,
|
||||
'bleInfo.connected': true,
|
||||
canOperateKeyConfig: this.data.keyConfig.selectedKeyCount > 0
|
||||
})
|
||||
this.bindConnectionStateListener()
|
||||
this.startBleNotifications()
|
||||
this.startStatusPolling()
|
||||
this.refreshBleStatus()
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
this.appendLog('CFG', `${source}成功:蓝牙已连接`)
|
||||
this.autoLoadDeviceData()
|
||||
if (showToast) wx.showToast({ title: '重连成功', icon: 'success' })
|
||||
},
|
||||
|
||||
async autoLoadDeviceData() {
|
||||
await this.onReadVersion()
|
||||
},
|
||||
|
||||
bindConnectionStateListener() {
|
||||
if (this._connectionStateHandler || typeof wx.onBLEConnectionStateChange !== 'function') return
|
||||
this._connectionStateHandler = (res) => {
|
||||
if (!res || res.deviceId !== this.data.bleInfo.mac) return
|
||||
const connected = !!res.connected
|
||||
this.setData({
|
||||
isConnecting: false,
|
||||
'bleInfo.connected': connected,
|
||||
'bleInfo.signalText': connected ? this.data.bleInfo.signalText : '',
|
||||
'bleInfo.rssi': connected ? this.data.bleInfo.rssi : null,
|
||||
canOperateKeyConfig: connected && this.data.keyConfig.selectedKeyCount > 0
|
||||
})
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
if (connected) {
|
||||
this.startStatusPolling()
|
||||
this.refreshBleStatus()
|
||||
this.appendLog('CFG', '蓝牙连接状态:已连接')
|
||||
} else {
|
||||
this.stopStatusPolling()
|
||||
this.appendLog('WARN', '蓝牙连接状态:已断开')
|
||||
}
|
||||
}
|
||||
wx.onBLEConnectionStateChange(this._connectionStateHandler)
|
||||
},
|
||||
|
||||
teardownConnectionStateListener() {
|
||||
if (this._connectionStateHandler && typeof wx.offBLEConnectionStateChange === 'function') {
|
||||
wx.offBLEConnectionStateChange(this._connectionStateHandler)
|
||||
}
|
||||
this._connectionStateHandler = null
|
||||
},
|
||||
|
||||
startStatusPolling() {
|
||||
this.stopStatusPolling()
|
||||
this._statusTimer = setInterval(() => {
|
||||
this.refreshBleStatus()
|
||||
}, 3000)
|
||||
},
|
||||
|
||||
stopStatusPolling() {
|
||||
if (this._statusTimer) {
|
||||
clearInterval(this._statusTimer)
|
||||
this._statusTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
bindBleListener() {
|
||||
if (this._bleChangeHandler || typeof wx.onBLECharacteristicValueChange !== 'function') return
|
||||
this._bleChangeHandler = (res) => {
|
||||
const packet = parsePacket(res && res.value)
|
||||
if (!packet) return
|
||||
if (packet.invalid) {
|
||||
this.appendLog('WARN', `收到异常数据:${ab2hex(packet.bytes)}`)
|
||||
return
|
||||
}
|
||||
this.appendLog('RX', ab2hex(packet.bytes), { cmd: `0x${packet.frameType.toString(16).toUpperCase().padStart(2, '0')}` })
|
||||
this.handlePacket(packet)
|
||||
}
|
||||
wx.onBLECharacteristicValueChange(this._bleChangeHandler)
|
||||
},
|
||||
|
||||
teardownBleNotifications() {
|
||||
if (this._bleChangeHandler && typeof wx.offBLECharacteristicValueChange === 'function') {
|
||||
wx.offBLECharacteristicValueChange(this._bleChangeHandler)
|
||||
}
|
||||
this._bleChangeHandler = null
|
||||
},
|
||||
|
||||
handlePacket(packet) {
|
||||
switch (packet.frameType) {
|
||||
case 0x01:
|
||||
this.handleVersionPacket(packet.params)
|
||||
break
|
||||
case 0x04:
|
||||
this.handleKeyConfigPacket(packet.params)
|
||||
break
|
||||
case 0x05:
|
||||
this.handleSimpleAck(packet.params, '设置蓝牙名称')
|
||||
break
|
||||
case 0x02:
|
||||
this.handleSimpleAck(packet.params, '设置按键配置')
|
||||
break
|
||||
case 0x06:
|
||||
this.handleSimpleAck(packet.params, '按键控制')
|
||||
break
|
||||
default:
|
||||
this.appendLog('CFG', `收到未处理命令:0x${packet.frameType.toString(16).toUpperCase().padStart(2, '0')}`)
|
||||
break
|
||||
}
|
||||
if (this._pendingResponse && this._pendingResponse.frameType === packet.frameType) {
|
||||
clearTimeout(this._pendingResponse.timer)
|
||||
const resolve = this._pendingResponse.resolve
|
||||
this._pendingResponse = null
|
||||
resolve(true)
|
||||
}
|
||||
},
|
||||
|
||||
handleVersionPacket(params = []) {
|
||||
const softwareVersion = params.length > 0 ? String(params[0]) : '-'
|
||||
const hardwareVersion = params.length > 1 ? String(params[1]) : '-'
|
||||
this.setData({
|
||||
'deviceInfo.softwareVersion': softwareVersion,
|
||||
'deviceInfo.hardwareVersion': hardwareVersion
|
||||
})
|
||||
this.appendLog('CFG', `版本更新:软件${softwareVersion},硬件${hardwareVersion}`)
|
||||
},
|
||||
|
||||
handleKeyConfigPacket(params = []) {
|
||||
const count = Number(params[0] || 0)
|
||||
const safeCount = count > 0 && count <= 8 ? count : this.data.keyConfig.selectedKeyCount
|
||||
const keys = createKeys(safeCount)
|
||||
for (let i = 0; i < safeCount; i += 1) {
|
||||
const cfg = params[i + 1] || 0
|
||||
const mappedKey = cfg & 0x0F
|
||||
const functionCode = (cfg >> 4) & 0x0F
|
||||
keys[i].mappedKey = mappedKey || null
|
||||
keys[i].originalMappedKey = mappedKey || null
|
||||
keys[i].mappedKeyIndex = mappedKey || 0
|
||||
keys[i].functionCode = functionCode
|
||||
keys[i].functionIndex = functionCode
|
||||
}
|
||||
const keyTypeIndex = this.data.keyConfig.keyCountValues.indexOf(safeCount)
|
||||
this.setData({
|
||||
readingKeyConfig: false,
|
||||
'keyConfig.selectedKeyTypeIndex': keyTypeIndex >= 0 ? keyTypeIndex : 0,
|
||||
'keyConfig.selectedKeyCount': safeCount,
|
||||
'keyConfig.keys': keys,
|
||||
'keyConfig.keySlots': createKeySlots(keys),
|
||||
canOperateKeyConfig: true
|
||||
})
|
||||
if (count && count !== this.data.keyConfig.selectedKeyCount) {
|
||||
wx.showToast({ title: `已按设备返回${count}键修正`, icon: 'none' })
|
||||
}
|
||||
this.appendLog('CFG', `读取到按键配置:${safeCount}键`)
|
||||
},
|
||||
|
||||
handleSimpleAck(params = [], text) {
|
||||
const ok = Number(params[0] || 0) === 0x01
|
||||
this.appendLog(ok ? 'CFG' : 'WARN', `${text}${ok ? '成功' : '失败'}`)
|
||||
if (text === '设置蓝牙名称' && ok) {
|
||||
this.setData({ 'bleInfo.devName': this.data.bleNameInput })
|
||||
}
|
||||
if (text === '设置蓝牙名称') this.setData({ settingBleName: false })
|
||||
if (text === '按键控制') this.setData({ keySendingIndex: -1 })
|
||||
},
|
||||
|
||||
refreshBleStatus() {
|
||||
const { mac } = this.data.bleInfo
|
||||
if (!mac || typeof wx.getBLEDeviceRSSI !== 'function') return
|
||||
wx.getBLEDeviceRSSI({
|
||||
deviceId: mac,
|
||||
success: (res) => {
|
||||
const rssi = typeof res.RSSI === 'number' ? res.RSSI : null
|
||||
const signalText = rssi == null ? '-' : `${rssi} dBm`
|
||||
this.setData({
|
||||
'bleInfo.rssi': rssi,
|
||||
'bleInfo.signalText': signalText,
|
||||
'bleInfo.connected': true,
|
||||
canOperateKeyConfig: this.data.keyConfig.selectedKeyCount > 0
|
||||
})
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
},
|
||||
fail: (err) => {
|
||||
const reason = formatBleError(err)
|
||||
const connected = !!this.data.bleInfo.connected
|
||||
this.setData({
|
||||
'bleInfo.connected': connected,
|
||||
'bleInfo.rssi': null,
|
||||
'bleInfo.signalText': connected ? '-' : '',
|
||||
canOperateKeyConfig: connected && this.data.keyConfig.selectedKeyCount > 0
|
||||
})
|
||||
if (!connected) {
|
||||
this.stopStatusPolling()
|
||||
}
|
||||
this.appendLog('WARN', `信号刷新失败:${reason}`)
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
8
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json
Normal file
8
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"cu-custom": "/colorui/components/cu-custom"
|
||||
}
|
||||
}
|
||||
129
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml
Normal file
129
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml
Normal file
@@ -0,0 +1,129 @@
|
||||
<view class="container">
|
||||
<cu-custom bgColor="bg-gradual-blue" isBack="true">
|
||||
<view slot="content">{{bleInfo.devName || 'BLV_RQ设备'}}</view>
|
||||
</cu-custom>
|
||||
|
||||
<view class="page-body content">
|
||||
<view class="panel ble-panel device-row">
|
||||
<view class="panel-head inline-head blank-head"></view>
|
||||
|
||||
<view class="console-row name-action-row keep-inline-row">
|
||||
<view class="console-cell device-name-cell">
|
||||
<text class="device-icon">🔵</text>
|
||||
<text class="cell-label">蓝牙名称:</text>
|
||||
<text class="cell-value">{{bleInfo.devName || '-'}}</text>
|
||||
</view>
|
||||
<view class="console-cell action-cell">
|
||||
<button class="action-btn {{bleInfo.connected ? 'warn' : 'primary'}} full-btn" bindtap="onConnectionAction" disabled="{{isConnecting || !bleInfo.mac}}">{{isConnecting ? '连接中...' : (bleInfo.connected ? '断开连接' : '重新连接')}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="console-row status-signal-row keep-inline-row">
|
||||
<view class="console-cell status-cell">
|
||||
<text class="cell-label">连接状态:</text>
|
||||
<view class="status-chip {{isConnecting ? 'pending' : (bleInfo.connected ? 'online' : 'offline')}}">{{isConnecting ? '连接中' : (bleInfo.connected ? '已连接' : '未连接')}}</view>
|
||||
</view>
|
||||
<view class="console-cell signal-cell">
|
||||
<text class="cell-label">信号强度:</text>
|
||||
<text class="cell-value">{{bleInfo.signalText || '-'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="console-row mac-row">
|
||||
<view class="console-cell full-line">
|
||||
<text class="cell-label">MAC地址:</text>
|
||||
<text class="cell-value">{{bleInfo.mac || '-'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="console-row rename-console-row keep-inline-row">
|
||||
<view class="console-cell input-cell wide">
|
||||
<text class="cell-label">蓝牙名称:</text>
|
||||
<input class="name-input {{bleInfo.connected ? '' : 'is-disabled'}}" placeholder="输入1-8位英文或数字" maxlength="8" value="{{bleNameInput}}" bindinput="onBleNameInput" disabled="{{!bleInfo.connected || settingBleName}}" />
|
||||
</view>
|
||||
<view class="console-cell button-cell">
|
||||
<button class="action-btn primary full-btn" bindtap="onSetBleName" loading="{{settingBleName}}" disabled="{{!canSetBleName || settingBleName}}">设置名称</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel device-panel cfg-card">
|
||||
<view class="panel-head inline-head blank-head"></view>
|
||||
|
||||
<view class="console-row firmware-row keep-inline-row">
|
||||
<view class="console-cell firmware-cell software-version-cell">
|
||||
<text class="cell-label">软件版本:</text>
|
||||
<text class="cell-value">{{deviceInfo.softwareVersion}}</text>
|
||||
</view>
|
||||
<view class="console-cell firmware-cell hardware-version-cell">
|
||||
<text class="cell-label">硬件版本:</text>
|
||||
<text class="cell-value">{{deviceInfo.hardwareVersion}}</text>
|
||||
</view>
|
||||
<view class="console-cell action-cell-inline read-version-cell">
|
||||
<button class="action-btn default full-btn" bindtap="onReadVersion" loading="{{readingVersion}}" disabled="{{!bleInfo.connected || readingVersion || readingKeyConfig || writingKeyConfig}}">读取版本</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="console-row config-action-row keep-inline-row">
|
||||
<view class="console-cell select-cell key-type-cell">
|
||||
<text class="cell-label">按键类型:</text>
|
||||
<picker class="count-picker" mode="selector" range="{{keyConfig.keyCountOptions}}" value="{{keyConfig.selectedKeyTypeIndex}}" bindchange="onKeyCountChange" disabled="{{!bleInfo.connected || writingKeyConfig}}">
|
||||
<view class="picker-view {{bleInfo.connected ? '' : 'is-disabled'}}">{{keyConfig.keyCountOptions[keyConfig.selectedKeyTypeIndex]}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="console-cell action-cell-inline">
|
||||
<button class="action-btn primary full-btn" bindtap="onReadKeyConfig" loading="{{readingKeyConfig}}" disabled="{{!bleInfo.connected || readingKeyConfig || writingKeyConfig}}">读取配置</button>
|
||||
</view>
|
||||
<view class="console-cell action-cell-inline">
|
||||
<button class="action-btn success full-btn" bindtap="onSubmitKeyConfig" loading="{{writingKeyConfig}}" disabled="{{!canOperateKeyConfig || writingKeyConfig || readingKeyConfig}}">写入配置</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="key-card-wrap">
|
||||
<view class="key-grid strict-grid">
|
||||
<view class="key-card-inline {{item.isPlaceholder ? 'reserve-card' : (bleInfo.connected ? '' : 'is-disabled')}}" wx:for="{{keyConfig.keySlots}}" wx:key="slotIndex">
|
||||
<view wx:if="{{!item.isPlaceholder}}" class="key-card-content">
|
||||
<view class="inline-key-label">按键{{item.hwIndex}}</view>
|
||||
<picker class="inline-key-picker" mode="selector" range="{{functionOptionLabels}}" value="{{item.functionIndex}}" bindchange="onFunctionChange" data-index="{{index}}" disabled="{{!bleInfo.connected || writingKeyConfig}}">
|
||||
<view class="picker-view compact {{bleInfo.connected ? '' : 'is-disabled'}}">{{functionOptionLabels[item.functionIndex] || '请选择功能'}}</view>
|
||||
</picker>
|
||||
<button class="action-btn default inline-send-btn" bindtap="onTriggerKey" data-index="{{index}}" loading="{{keySendingIndex === index}}" disabled="{{!bleInfo.connected || !item.mappedKey || keySendingIndex === index}}">{{keySendingIndex === index ? '发送中...' : '发送'}}</button>
|
||||
</view>
|
||||
<view wx:if="{{item.isPlaceholder}}" class="reserve-card-empty">
|
||||
<text class="reserve-card-text">预留{{item.hwIndex}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel log-panel log-card">
|
||||
<view class="panel-head inline-head log-inline-head keep-inline-head blank-head"></view>
|
||||
<view class="log-toolbar-row keep-inline-actions-row">
|
||||
<view class="log-toolbar-title-spacer"></view>
|
||||
<view class="toolbar-actions right-actions keep-inline-actions">
|
||||
<button class="action-btn default" bindtap="onClearLogs">清空日志</button>
|
||||
<button class="action-btn primary" bindtap="onExportLogs">导出日志</button>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="log-scroll" scroll-y="true" scroll-into-view="{{logScrollTo}}">
|
||||
<view class="log-item simple-log {{item.typeClass}}" wx:for="{{logs}}" wx:key="id" id="{{'log-' + item.id}}">
|
||||
<text class="log-time">{{item.time}}</text>
|
||||
<text class="log-dir {{item.typeClass}}">{{item.type === 'TX' ? 'TX ->' : (item.type === 'RX' ? 'RX <-' : '[' + item.type + ']')}}</text>
|
||||
<text class="log-text {{item.type === 'TX' || item.type === 'RX' ? 'packet' : 'plain'}}">{{(item.type === 'TX' || item.type === 'RX') ? (item.displayText || item.hexText || item.text) : item.text}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="dialog-mask" wx:if="{{confirmDialog.visible}}">
|
||||
<view class="dialog-card">
|
||||
<view class="dialog-title">设置确认</view>
|
||||
<view class="dialog-content">{{confirmDialog.message}}</view>
|
||||
<view class="dialog-actions">
|
||||
<button class="action-btn default dialog-btn" bindtap="onCancelSubmit">取消</button>
|
||||
<button class="action-btn primary dialog-btn" bindtap="onConfirmSubmit">继续下发</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
715
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss
Normal file
715
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss
Normal file
@@ -0,0 +1,715 @@
|
||||
page {
|
||||
background: #f5f6f8;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100vh;
|
||||
background: #f5f6f8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-body,
|
||||
.content {
|
||||
height: calc(100vh - 88rpx);
|
||||
min-height: 0;
|
||||
padding: 8rpx 10rpx 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel,
|
||||
.cfg-card,
|
||||
.log-card,
|
||||
.device-row {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border: none;
|
||||
border-radius: 14rpx;
|
||||
padding: 12rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.ble-panel,
|
||||
.device-panel,
|
||||
.log-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ble-panel,
|
||||
.device-panel {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.log-panel,
|
||||
.log-card {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.blank-head {
|
||||
min-height: 0;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.panel-title.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.console-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.console-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.console-cell {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.device-name-cell {
|
||||
flex: 0.98;
|
||||
}
|
||||
|
||||
.status-cell,
|
||||
.signal-cell {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-cell,
|
||||
.button-cell,
|
||||
.action-cell-inline {
|
||||
width: 140rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.read-version-cell {
|
||||
width: 108rpx;
|
||||
}
|
||||
|
||||
.full-line {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-cell.wide,
|
||||
.select-cell {
|
||||
flex: 0.88;
|
||||
}
|
||||
|
||||
.firmware-cell {
|
||||
flex: 1.54;
|
||||
}
|
||||
|
||||
.software-version-cell,
|
||||
.hardware-version-cell {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.software-version-cell .cell-label,
|
||||
.hardware-version-cell .cell-label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.software-version-cell .cell-value,
|
||||
.hardware-version-cell .cell-value {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.key-type-cell {
|
||||
flex: 0.62;
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.cell-label {
|
||||
font-size: 26rpx;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cell-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 27rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
min-width: 120rpx;
|
||||
height: 100%;
|
||||
min-height: 48rpx;
|
||||
align-self: stretch;
|
||||
padding: 0 12rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.status-chip.online {
|
||||
color: #047857;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border: 1rpx solid rgba(16, 185, 129, 0.28);
|
||||
}
|
||||
|
||||
.status-chip.offline {
|
||||
color: #b45309;
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
border: 1rpx solid rgba(245, 158, 11, 0.26);
|
||||
}
|
||||
|
||||
.status-chip.pending {
|
||||
color: #1d4ed8;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
border: 1rpx solid rgba(37, 99, 235, 0.26);
|
||||
}
|
||||
|
||||
.name-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 48rpx;
|
||||
align-self: stretch;
|
||||
padding: 0 12rpx;
|
||||
background: #f7f8fa;
|
||||
border: 1rpx solid #e5e9f2;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
border-radius: 12rpx;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
padding: 0 12rpx;
|
||||
font-size: 28rpx;
|
||||
height: 100%;
|
||||
min-height: 52rpx;
|
||||
align-self: stretch;
|
||||
line-height: 1.2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.read-version-cell .action-btn {
|
||||
font-size: 26rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.action-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #0ea5e9;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2rpx 6rpx rgba(14, 165, 233, 0.35);
|
||||
}
|
||||
|
||||
.action-btn.success {
|
||||
background: #21c161;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn.warn {
|
||||
background: #ff8f00;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn.default {
|
||||
background: #ffffff;
|
||||
color: #2bab99;
|
||||
border: 1rpx solid rgba(43, 171, 153, 0.18);
|
||||
}
|
||||
|
||||
.full-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.count-picker,
|
||||
.inline-key-picker {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
min-height: 48rpx;
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
padding: 0 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f7f8fa;
|
||||
border: 1rpx solid #e5e9f2;
|
||||
border-radius: 10rpx;
|
||||
font-size: 23rpx;
|
||||
color: #0f172a;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.picker-view.compact {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
opacity: 0.48;
|
||||
filter: grayscale(0.2);
|
||||
}
|
||||
|
||||
.name-input.is-disabled,
|
||||
.picker-view.is-disabled {
|
||||
background: #f1f5f9;
|
||||
border-color: #e2e8f0;
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
.key-card-wrap {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.key-grid.strict-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.key-card-inline {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
background: #ffffff;
|
||||
border: none;
|
||||
border-radius: 14rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.reserve-card {
|
||||
border-style: dashed;
|
||||
border-color: rgba(148, 163, 184, 0.5);
|
||||
background: rgba(248, 250, 252, 0.55);
|
||||
min-height: 86rpx;
|
||||
opacity: 0.72;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.key-card-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reserve-card-empty {
|
||||
width: 100%;
|
||||
min-height: 86rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.reserve-card-text {
|
||||
font-size: 24rpx;
|
||||
color: #94a3b8;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.inline-key-label {
|
||||
min-width: 70rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inline-send-btn {
|
||||
min-width: 104rpx;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.log-inline-head {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.log-toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.log-toolbar-title-spacer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.log-scroll {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
background: #fdfdfd;
|
||||
border: 1rpx solid #e5e9f2;
|
||||
border-radius: 12rpx;
|
||||
padding: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.log-item.simple-log {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8rpx;
|
||||
padding: 0 0 8rpx 0;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.log-item.simple-log:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #9aa0a6;
|
||||
font-size: 22rpx;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
min-width: 78rpx;
|
||||
}
|
||||
|
||||
.log-dir {
|
||||
font-size: 22rpx;
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
min-width: 52rpx;
|
||||
}
|
||||
|
||||
.log-dir.tx {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.log-dir.rx {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.log-dir.warn {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.log-dir.err {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.log-dir.ui,
|
||||
.log-dir.cfg {
|
||||
color: #c4b5fd;
|
||||
}
|
||||
|
||||
.log-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #333333;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.log-text.packet {
|
||||
color: #333333;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.45;
|
||||
font-family: inherit;
|
||||
word-break: break-all;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 10rpx;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.log-text.plain {
|
||||
color: #333333;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.45;
|
||||
font-family: inherit;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.dialog-card {
|
||||
width: 78%;
|
||||
background: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2d3d;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
font-size: 26rpx;
|
||||
color: #475467;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 16rpx;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.page-body {
|
||||
padding: 12rpx;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.console-row,
|
||||
.panel-head,
|
||||
.toolbar-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.keep-inline-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.keep-inline-head {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.keep-inline-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.keep-inline-row .cell-label {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.keep-inline-row .cell-value,
|
||||
.keep-inline-row .name-input,
|
||||
.keep-inline-row .picker-view,
|
||||
.keep-inline-row .inline-key-label {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.name-action-row .device-name-cell {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name-action-row .action-cell {
|
||||
flex: 0 0 140rpx;
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-signal-row .status-cell,
|
||||
.status-signal-row .signal-cell,
|
||||
.firmware-row .firmware-cell,
|
||||
.rename-console-row .input-cell.wide,
|
||||
.rename-console-row .button-cell,
|
||||
.config-action-row .select-cell,
|
||||
.config-action-row .action-cell-inline {
|
||||
flex: 1 1 0;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rename-console-row .input-cell.wide {
|
||||
flex: 1.08;
|
||||
}
|
||||
|
||||
.rename-console-row .button-cell,
|
||||
.config-action-row .action-cell-inline {
|
||||
flex: 0 0 128rpx;
|
||||
width: 128rpx;
|
||||
}
|
||||
|
||||
.firmware-row .firmware-cell {
|
||||
flex: 1.08;
|
||||
}
|
||||
|
||||
.config-action-row .select-cell {
|
||||
flex: 0.78;
|
||||
}
|
||||
|
||||
.config-action-row .action-cell-inline {
|
||||
flex: 0 0 128rpx;
|
||||
width: 128rpx;
|
||||
}
|
||||
|
||||
.firmware-row .read-version-cell {
|
||||
flex: 0 0 112rpx;
|
||||
width: 112rpx;
|
||||
}
|
||||
|
||||
.status-signal-row .status-cell {
|
||||
flex: 0 0 230rpx;
|
||||
}
|
||||
|
||||
.status-signal-row .signal-cell {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.keep-inline-row .action-btn,
|
||||
.keep-inline-actions .action-btn {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.name-action-row .action-btn,
|
||||
.rename-console-row .action-btn,
|
||||
.config-action-row .action-btn,
|
||||
.firmware-row .action-btn {
|
||||
padding: 0 6rpx;
|
||||
}
|
||||
|
||||
.firmware-row .read-version-cell .action-btn {
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.keep-inline-head .panel-title {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.read-version-cell {
|
||||
width: 96rpx;
|
||||
}
|
||||
|
||||
.read-version-cell .action-btn {
|
||||
font-size: 24rpx;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.keep-inline-actions {
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.key-grid.strict-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.key-card-inline {
|
||||
min-height: 82rpx;
|
||||
}
|
||||
|
||||
.reserve-card-empty {
|
||||
min-height: 82rpx;
|
||||
}
|
||||
|
||||
.inline-key-label {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.log-item.simple-log {
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
min-width: 74rpx;
|
||||
}
|
||||
|
||||
.log-dir {
|
||||
min-width: 50rpx;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,18 @@
|
||||
const app = getApp()
|
||||
|
||||
const FIXED_CONNECT_CMD = new Uint8Array([0xCC, 0xC0, 0x0C, 0x00, 0x4F, 0xC0, 0x01, 0x00, 0x02, 0x00, 0x0C, 0x08])
|
||||
const DEVICE_EXPIRE_MS = 15000
|
||||
const SEARCH_DURATION_MS = 6000
|
||||
const SEARCH_RESTART_DELAY_MS = 350
|
||||
const RESUME_BACKGROUND_SEARCH_DELAY_MS = 2500
|
||||
|
||||
Page({
|
||||
data: {
|
||||
ConnectedDevName:"",
|
||||
activeTab: 'W13', // 默认选中W13
|
||||
deviceTypeLabels: ['请选择设备类型', '主机', 'W13', 'BLV_RQ'],
|
||||
deviceTypeValues: ['NONE', 'HOST', 'W13', 'BLV_RQ'],
|
||||
deviceTypeIndex: 0,
|
||||
activeTab: 'NONE',
|
||||
autho: null,
|
||||
Hotelinfo: {},
|
||||
deviceList: [],
|
||||
@@ -14,6 +21,150 @@ Page({
|
||||
coid:0
|
||||
},
|
||||
|
||||
getSignalByRSSI(rssi) {
|
||||
if (typeof rssi !== 'number' || Number.isNaN(rssi)) return 0
|
||||
return Math.max(0, Math.min(100, 100 + rssi))
|
||||
},
|
||||
|
||||
getConnectedDevice() {
|
||||
return (this.data.deviceList || []).find(item => item.connected)
|
||||
},
|
||||
|
||||
markDeviceDisconnected(deviceId, options = {}) {
|
||||
if (!deviceId) return
|
||||
const now = Date.now()
|
||||
const removeIfStale = !!options.removeIfStale
|
||||
const nextList = (this.data.deviceList || [])
|
||||
.map(item => item.id === deviceId
|
||||
? {
|
||||
...item,
|
||||
connected: false,
|
||||
RSSI: null,
|
||||
signal: 0,
|
||||
disconnectedAt: now
|
||||
}
|
||||
: item)
|
||||
.filter(item => {
|
||||
if (!removeIfStale) return true
|
||||
if (item.id !== deviceId) return true
|
||||
const lastSeenAt = item.lastSeenAt || 0
|
||||
return now - lastSeenAt <= DEVICE_EXPIRE_MS
|
||||
})
|
||||
const nextData = { deviceList: nextList }
|
||||
if (this.data.currentDeviceId === deviceId) {
|
||||
nextData.currentDeviceId = null
|
||||
}
|
||||
this.setData(nextData)
|
||||
},
|
||||
|
||||
reconcileConnectedDevices(connectedDevices = [], deviceType = this.data.activeTab) {
|
||||
const connectedIds = new Set((connectedDevices || []).map(item => item.deviceId))
|
||||
const now = Date.now()
|
||||
const nextList = (this.data.deviceList || []).map(item => {
|
||||
const name = item.name || item.localName || ''
|
||||
if (!this.matchDeviceType(name, deviceType)) return item
|
||||
if (connectedIds.has(item.id)) return item
|
||||
if (!item.connected) return item
|
||||
return {
|
||||
...item,
|
||||
connected: false,
|
||||
RSSI: null,
|
||||
signal: 0,
|
||||
disconnectedAt: now
|
||||
}
|
||||
})
|
||||
const hasCurrentConnected = this.data.currentDeviceId && connectedIds.has(this.data.currentDeviceId)
|
||||
this.setData({
|
||||
deviceList: nextList,
|
||||
currentDeviceId: hasCurrentConnected ? this.data.currentDeviceId : null
|
||||
})
|
||||
},
|
||||
|
||||
bindSearchPageBleStateListener() {
|
||||
if (this._bleStateChangeHandler || typeof wx.onBLEConnectionStateChange !== 'function') return
|
||||
this._bleStateChangeHandler = (res) => {
|
||||
if (!res || !res.deviceId) return
|
||||
if (res.connected) {
|
||||
this.refreshConnectedDeviceRSSI(res.deviceId)
|
||||
return
|
||||
}
|
||||
this.markDeviceDisconnected(res.deviceId, { removeIfStale: true })
|
||||
}
|
||||
wx.onBLEConnectionStateChange(this._bleStateChangeHandler)
|
||||
},
|
||||
|
||||
teardownSearchPageBleStateListener() {
|
||||
if (this._bleStateChangeHandler && typeof wx.offBLEConnectionStateChange === 'function') {
|
||||
wx.offBLEConnectionStateChange(this._bleStateChangeHandler)
|
||||
}
|
||||
this._bleStateChangeHandler = null
|
||||
},
|
||||
|
||||
startConnectedDeviceMonitor() {
|
||||
if (this._connectedDeviceMonitorTimer) return
|
||||
this._connectedDeviceMonitorTimer = setInterval(() => {
|
||||
const connectedDevice = this.getConnectedDevice()
|
||||
if (!connectedDevice) return
|
||||
this.refreshConnectedDeviceRSSI(connectedDevice.id)
|
||||
}, 3000)
|
||||
},
|
||||
|
||||
stopConnectedDeviceMonitor() {
|
||||
if (this._connectedDeviceMonitorTimer) {
|
||||
clearInterval(this._connectedDeviceMonitorTimer)
|
||||
this._connectedDeviceMonitorTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
applyDeviceTypeChange(selectedType, index) {
|
||||
const nextList = (this.data.deviceList || []).filter((item) => {
|
||||
const name = item.name || item.localName || ''
|
||||
return this.matchDeviceType(name, selectedType)
|
||||
})
|
||||
|
||||
this.setData({
|
||||
deviceTypeIndex: index,
|
||||
activeTab: selectedType,
|
||||
deviceList: nextList
|
||||
})
|
||||
|
||||
if (this._discoveryStopTimer) {
|
||||
clearTimeout(this._discoveryStopTimer)
|
||||
this._discoveryStopTimer = null
|
||||
}
|
||||
if (this._resumeBackgroundSearchTimer) {
|
||||
clearTimeout(this._resumeBackgroundSearchTimer)
|
||||
this._resumeBackgroundSearchTimer = null
|
||||
}
|
||||
this._searchToken = (this._searchToken || 0) + 1
|
||||
this._pauseBackgroundSearch = true
|
||||
this.stopBluetoothDiscovery()
|
||||
this.syncScanTimerByType(selectedType)
|
||||
|
||||
if (this.isSearchableDeviceType(selectedType)) {
|
||||
this.searchBluetooth({ source: 'selector', restart: true, clearList: false, showLoading: true })
|
||||
}
|
||||
},
|
||||
|
||||
refreshConnectedDeviceRSSI(deviceId) {
|
||||
if (!deviceId || typeof wx.getBLEDeviceRSSI !== 'function') return
|
||||
wx.getBLEDeviceRSSI({
|
||||
deviceId,
|
||||
success: (res) => {
|
||||
const rssi = typeof res.RSSI === 'number' ? res.RSSI : null
|
||||
if (rssi == null) return
|
||||
const signal = this.getSignalByRSSI(rssi)
|
||||
const list = (this.data.deviceList || []).map(item => item.id === deviceId
|
||||
? { ...item, connected: true, RSSI: rssi, signal, lastSeenAt: Date.now() }
|
||||
: item)
|
||||
this.setData({ deviceList: list })
|
||||
},
|
||||
fail: () => {
|
||||
this.markDeviceDisconnected(deviceId, { removeIfStale: true })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
// 返回前主动断开当前BLE连接,避免连接遗留
|
||||
@@ -23,32 +174,67 @@ Page({
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
// 切换导航选项卡
|
||||
switchTab(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
const current = this.data.activeTab
|
||||
if (tab === current) {
|
||||
markPendingBleNavigation(target, deviceId, session = {}) {
|
||||
if (!app.globalData) return
|
||||
app.globalData.pendingBleNavigation = {
|
||||
target,
|
||||
deviceId: deviceId || '',
|
||||
session: { ...session },
|
||||
keepConnection: true,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
},
|
||||
|
||||
scheduleResumeBackgroundSearch() {
|
||||
if (this._resumeBackgroundSearchTimer) {
|
||||
clearTimeout(this._resumeBackgroundSearchTimer)
|
||||
this._resumeBackgroundSearchTimer = null
|
||||
}
|
||||
this._resumeBackgroundSearchTimer = setTimeout(() => {
|
||||
this._resumeBackgroundSearchTimer = null
|
||||
this._pauseBackgroundSearch = false
|
||||
}, RESUME_BACKGROUND_SEARCH_DELAY_MS)
|
||||
},
|
||||
|
||||
syncSearchLoadingByList(searchToken = this._searchToken) {
|
||||
const hasDevices = Array.isArray(this.data.deviceList) && this.data.deviceList.length > 0
|
||||
if (hasDevices) {
|
||||
this._shouldShowSearching = false
|
||||
if (this._searchToken === searchToken) {
|
||||
wx.hideLoading()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const hasConnected = this.data.deviceList.some(d => d.connected)
|
||||
if (hasConnected) {
|
||||
if (this._shouldShowSearching && this._searchToken === searchToken) {
|
||||
wx.showLoading({ title: '搜索中...', mask: true })
|
||||
}
|
||||
},
|
||||
|
||||
onDeviceTypeChange(e) {
|
||||
const index = Number(e.detail.value || 0)
|
||||
const selectedType = this.data.deviceTypeValues[index] || 'NONE'
|
||||
if (selectedType === this.data.activeTab) {
|
||||
return
|
||||
}
|
||||
const connectedDevice = this.getConnectedDevice()
|
||||
if (connectedDevice) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '当前有已连接的蓝牙设备,切换将断开并重新搜索,是否继续?',
|
||||
title: '切换设备类型',
|
||||
content: '当前有蓝牙设备处于已连接状态,切换设备类型后将断开该连接,是否继续?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.disconnectAllDevices()
|
||||
this.setData({ activeTab: tab })
|
||||
// 切换后立即搜索一次
|
||||
this.searchBluetooth()
|
||||
if (!res.confirm) {
|
||||
this.setData({ deviceTypeIndex: this.data.deviceTypeValues.indexOf(this.data.activeTab) })
|
||||
return
|
||||
}
|
||||
try { this.disconnectCurrentDevice() } catch (e) {}
|
||||
this.applyDeviceTypeChange(selectedType, index)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.setData({ activeTab: tab })
|
||||
this.searchBluetooth()
|
||||
return
|
||||
}
|
||||
|
||||
this.applyDeviceTypeChange(selectedType, index)
|
||||
},
|
||||
|
||||
disconnectAllDevices() {
|
||||
@@ -88,21 +274,52 @@ Page({
|
||||
},
|
||||
|
||||
// 搜索蓝牙设备
|
||||
searchBluetooth() {
|
||||
const filterPrefix = this.data.activeTab
|
||||
searchBluetooth(options = {}) {
|
||||
const deviceType = this.data.activeTab
|
||||
const source = options.source || 'manual'
|
||||
const restart = options.restart !== false
|
||||
const clearList = !!options.clearList
|
||||
const showLoading = !!options.showLoading
|
||||
if (!this.isSearchableDeviceType(deviceType)) {
|
||||
if (deviceType === 'HOST') {
|
||||
wx.showToast({ title: '该设备暂未发布', icon: 'none' })
|
||||
} else {
|
||||
wx.showToast({ title: '未选择设备类型', icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (clearList) {
|
||||
this.setData({ deviceList: [] })
|
||||
}
|
||||
|
||||
// 先断开当前连接设备(如果有)
|
||||
// this.disconnectCurrentDevice()
|
||||
if (source !== 'timer') {
|
||||
this._manualSearchInProgress = true
|
||||
this._pauseBackgroundSearch = true
|
||||
if (this._resumeBackgroundSearchTimer) {
|
||||
clearTimeout(this._resumeBackgroundSearchTimer)
|
||||
this._resumeBackgroundSearchTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// // 清空旧列表并启动搜索
|
||||
// this.setData({ deviceList: [] })
|
||||
this._shouldShowSearching = showLoading
|
||||
this._hasFoundDeviceInCurrentSearch = false
|
||||
this._searchToken = (this._searchToken || 0) + 1
|
||||
const currentSearchToken = this._searchToken
|
||||
this.syncSearchLoadingByList(currentSearchToken)
|
||||
|
||||
this.ensureBluetoothReady()
|
||||
.then(() => this.startBluetoothDevicesDiscovery(filterPrefix))
|
||||
.then(() => this.startBluetoothDevicesDiscovery(deviceType, { restart, source, searchToken: currentSearchToken }))
|
||||
.catch((err) => {
|
||||
this._shouldShowSearching = false
|
||||
if (source !== 'timer') {
|
||||
this._manualSearchInProgress = false
|
||||
this.scheduleResumeBackgroundSearch()
|
||||
}
|
||||
console.error('蓝牙初始化失败', err)
|
||||
wx.hideLoading()
|
||||
if (this._searchToken === currentSearchToken) {
|
||||
wx.hideLoading()
|
||||
}
|
||||
wx.showToast({ title: '请开启蓝牙和定位权限', icon: 'none' })
|
||||
})
|
||||
},
|
||||
@@ -135,10 +352,18 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
startBluetoothDevicesDiscovery(prefix) {
|
||||
startBluetoothDevicesDiscovery(deviceType, options = {}) {
|
||||
const restart = options.restart !== false
|
||||
const source = options.source || 'manual'
|
||||
const searchToken = options.searchToken || 0
|
||||
// 先取消旧的发现监听,避免多次注册造成干扰
|
||||
this.teardownDeviceFoundListener()
|
||||
if (this._discoveryStopTimer) {
|
||||
clearTimeout(this._discoveryStopTimer)
|
||||
this._discoveryStopTimer = null
|
||||
}
|
||||
this._foundCount = 0
|
||||
this._scanDeviceType = deviceType
|
||||
const now = Date.now()
|
||||
// 防护:避免短时间内频繁触发扫描(系统限制)
|
||||
if (this._lastScanAt && now - this._lastScanAt < 2000) {
|
||||
@@ -146,33 +371,58 @@ Page({
|
||||
return
|
||||
}
|
||||
this._lastScanAt = now
|
||||
console.info('[BLE] start scan, prefix:', prefix || 'ALL')
|
||||
console.info('[BLE] start scan, deviceType:', deviceType || 'NONE')
|
||||
|
||||
const doStart = () => {
|
||||
const beginDiscovery = () => {
|
||||
wx.startBluetoothDevicesDiscovery({
|
||||
allowDuplicatesKey: true, // 允许重复上报,提升二次搜索的发现率
|
||||
success: () => {
|
||||
this._isDiscoveringSearch = true
|
||||
this.setupDeviceFoundListener(deviceType)
|
||||
// 定时停止,避免长时间占用
|
||||
this._discoveryStopTimer = setTimeout(() => {
|
||||
this._discoveryStopTimer = null
|
||||
this.stopBluetoothDiscovery()
|
||||
}, SEARCH_DURATION_MS)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('开始搜索蓝牙设备失败', err)
|
||||
// 若为系统提示搜索过于频繁,可稍后重试一次
|
||||
const code = err && (err.errCode || (err.errMsg && Number((err.errMsg.match(/\d+/)||[])[0])))
|
||||
const message = (err && (err.errMsg || err.message) || '').toLowerCase()
|
||||
if (code === 10008) {
|
||||
try { this.appendLog && this.appendLog('WARN', 'startBluetoothDevicesDiscovery failed: scanning too frequently, retrying shortly') } catch (e) {}
|
||||
setTimeout(() => { try { doStart() } catch (e) {} }, 1500)
|
||||
return
|
||||
}
|
||||
if ((code === 1509008 || message.indexOf('location permission is denied') >= 0) && restart) {
|
||||
setTimeout(() => { try { beginDiscovery() } catch (e) {} }, 800)
|
||||
return
|
||||
}
|
||||
if (source !== 'timer') {
|
||||
this._manualSearchInProgress = false
|
||||
this.scheduleResumeBackgroundSearch()
|
||||
}
|
||||
this._shouldShowSearching = false
|
||||
if (this._searchToken === searchToken) {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!restart) {
|
||||
beginDiscovery()
|
||||
return
|
||||
}
|
||||
|
||||
// 先停止可能已有的搜索,待停止完成后再启动,避免竞态
|
||||
wx.stopBluetoothDevicesDiscovery({
|
||||
complete: () => {
|
||||
wx.startBluetoothDevicesDiscovery({
|
||||
allowDuplicatesKey: true, // 允许重复上报,提升二次搜索的发现率
|
||||
success: () => {
|
||||
this.setupDeviceFoundListener(prefix)
|
||||
// 定时停止,避免长时间占用
|
||||
setTimeout(() => {
|
||||
this.stopBluetoothDiscovery()
|
||||
}, 6000)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('开始搜索蓝牙设备失败', err)
|
||||
// 若为系统提示搜索过于频繁,可稍后重试一次
|
||||
const code = err && (err.errCode || (err.errMsg && Number((err.errMsg.match(/\d+/)||[])[0])))
|
||||
if (code === 10008) {
|
||||
try { this.appendLog && this.appendLog('WARN', 'startBluetoothDevicesDiscovery failed: scanning too frequently, retrying shortly') } catch (e) {}
|
||||
setTimeout(() => { try { doStart() } catch (e) {} }, 1500)
|
||||
return
|
||||
}
|
||||
wx.hideLoading()
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
beginDiscovery()
|
||||
}, SEARCH_RESTART_DELAY_MS)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -183,8 +433,7 @@ Page({
|
||||
success: (res) => {
|
||||
if (res && res.discovering) {
|
||||
try { this.appendLog && this.appendLog('CFG', 'adapter already discovering, attach listener') } catch (e) {}
|
||||
this.setupDeviceFoundListener(prefix)
|
||||
wx.hideLoading()
|
||||
this.setupDeviceFoundListener(deviceType)
|
||||
return
|
||||
}
|
||||
doStart()
|
||||
@@ -198,11 +447,15 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
setupDeviceFoundListener(prefix) {
|
||||
setupDeviceFoundListener(deviceType) {
|
||||
this._deviceFoundHandler = (res) => {
|
||||
const devices = (res && res.devices) || []
|
||||
if (devices.length && this._shouldShowSearching && !this._hasFoundDeviceInCurrentSearch) {
|
||||
this._hasFoundDeviceInCurrentSearch = true
|
||||
this.scheduleResumeBackgroundSearch()
|
||||
}
|
||||
if (devices.length) this._foundCount = (this._foundCount || 0) + devices.length
|
||||
this.handleDeviceFound(devices, prefix)
|
||||
this.handleDeviceFound(devices, deviceType)
|
||||
}
|
||||
if (typeof wx.onBluetoothDeviceFound === 'function') {
|
||||
wx.onBluetoothDeviceFound(this._deviceFoundHandler)
|
||||
@@ -222,8 +475,9 @@ Page({
|
||||
try { this.appendLog && this.appendLog('CFG', 'startScanTimer') } catch (e) {}
|
||||
this._scanTimer = setInterval(() => {
|
||||
try {
|
||||
if (this._manualSearchInProgress || this._pauseBackgroundSearch) return
|
||||
// 触发一次搜索,内部有防抖保护
|
||||
this.searchBluetooth()
|
||||
this.searchBluetooth({ source: 'timer', restart: false, clearList: false, showLoading: false })
|
||||
} catch (e) { /* ignore */ }
|
||||
}, 3000)
|
||||
},
|
||||
@@ -236,51 +490,111 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
handleDeviceFound(devices, prefix) {
|
||||
syncScanTimerByType(deviceType) {
|
||||
const type = deviceType || this.data.activeTab
|
||||
if (this.isSearchableDeviceType(type)) {
|
||||
this.startScanTimer()
|
||||
} else {
|
||||
this.stopScanTimer()
|
||||
}
|
||||
},
|
||||
|
||||
isSearchableDeviceType(deviceType) {
|
||||
return deviceType === 'W13' || deviceType === 'BLV_RQ'
|
||||
},
|
||||
|
||||
matchDeviceType(name, deviceType) {
|
||||
const value = (name || '').trim()
|
||||
if (!value) return false
|
||||
if (deviceType === 'W13') {
|
||||
return /^BLV_(W13|C13)/i.test(value)
|
||||
}
|
||||
if (deviceType === 'BLV_RQ') {
|
||||
return /^BLV_RQ/i.test(value)
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
handleDeviceFound(devices, deviceType) {
|
||||
const list = [...this.data.deviceList]
|
||||
devices.forEach((dev) => {
|
||||
const name = dev.name || dev.localName || ''
|
||||
if (!name) return
|
||||
const isW13 = this.data.activeTab === 'W13'
|
||||
const matched = isW13 ? /^BLV_(W13|C13)_.+$/i.test(name) : (prefix ? name.startsWith(prefix) : true)
|
||||
const matched = this.matchDeviceType(name, deviceType || this.data.activeTab)
|
||||
if (!matched) return
|
||||
|
||||
const existsIndex = list.findIndex((d) => d.id === dev.deviceId)
|
||||
const rssi = dev.RSSI || 0
|
||||
const signal = Math.max(0, Math.min(100, 100 + rssi))
|
||||
const oldItem = existsIndex >= 0 ? list[existsIndex] : null
|
||||
const hasFreshRSSI = typeof dev.RSSI === 'number' && dev.RSSI !== 0
|
||||
const rssi = hasFreshRSSI ? dev.RSSI : (oldItem && typeof oldItem.RSSI === 'number' ? oldItem.RSSI : (typeof dev.RSSI === 'number' ? dev.RSSI : null))
|
||||
const signal = this.getSignalByRSSI(rssi)
|
||||
const mapped = {
|
||||
id: dev.deviceId,
|
||||
name,
|
||||
mac: dev.deviceId,
|
||||
signal,
|
||||
connected: false,
|
||||
connected: oldItem ? !!oldItem.connected : false,
|
||||
RSSI: rssi,
|
||||
localName: dev.localName || '',
|
||||
serviceUUIDs: dev.serviceUUIDs || []
|
||||
serviceUUIDs: dev.serviceUUIDs || [],
|
||||
lastSeenAt: Date.now()
|
||||
}
|
||||
|
||||
if (existsIndex >= 0) {
|
||||
// 设备已存在时仅更新信号值与 RSSI,避免覆盖其它已保存字段
|
||||
list[existsIndex] = {
|
||||
...list[existsIndex],
|
||||
name,
|
||||
mac: dev.deviceId,
|
||||
signal,
|
||||
RSSI: rssi
|
||||
connected: typeof list[existsIndex].connected === 'boolean' ? list[existsIndex].connected : false,
|
||||
RSSI: rssi,
|
||||
localName: dev.localName || list[existsIndex].localName || '',
|
||||
serviceUUIDs: dev.serviceUUIDs || list[existsIndex].serviceUUIDs || [],
|
||||
lastSeenAt: Date.now()
|
||||
}
|
||||
} else {
|
||||
list.push(mapped)
|
||||
console.log('[BluetoothDebugging] 新增设备', {
|
||||
name: mapped.name,
|
||||
mac: mapped.mac,
|
||||
serviceUUIDs: mapped.serviceUUIDs
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.setData({ deviceList: list })
|
||||
this.syncSearchLoadingByList()
|
||||
},
|
||||
|
||||
// 停止蓝牙搜索
|
||||
stopBluetoothDiscovery() {
|
||||
const stoppingToken = this._searchToken || 0
|
||||
wx.stopBluetoothDevicesDiscovery({
|
||||
complete: () => {
|
||||
console.info('[BLE] stop scan, found events:', this._foundCount || 0, 'list size:', this.data.deviceList.length)
|
||||
wx.hideLoading()
|
||||
const count = this.data.deviceList.length
|
||||
this._isDiscoveringSearch = false
|
||||
this._manualSearchInProgress = false
|
||||
this.scheduleResumeBackgroundSearch()
|
||||
this._shouldShowSearching = false
|
||||
if (this._searchToken === stoppingToken) {
|
||||
wx.hideLoading()
|
||||
}
|
||||
const scanType = this._scanDeviceType || this.data.activeTab
|
||||
const now = Date.now()
|
||||
const nextList = (this.data.deviceList || []).filter((item) => {
|
||||
const name = item.name || item.localName || ''
|
||||
const isCurrentType = this.matchDeviceType(name, scanType)
|
||||
if (!isCurrentType) return true
|
||||
if (item.connected) {
|
||||
return item.id === this.data.currentDeviceId && now - (item.lastSeenAt || 0) <= DEVICE_EXPIRE_MS
|
||||
}
|
||||
const lastSeenAt = item.lastSeenAt || 0
|
||||
return now - lastSeenAt <= DEVICE_EXPIRE_MS
|
||||
})
|
||||
this.setData({ deviceList: nextList })
|
||||
this.syncSearchLoadingByList(stoppingToken)
|
||||
this._scanDeviceType = null
|
||||
// 取消自动显示搜索完成提示,避免打扰
|
||||
}
|
||||
})
|
||||
@@ -291,6 +605,16 @@ Page({
|
||||
// this.teardownDeviceFoundListener()
|
||||
// 页面卸载时停止定时扫描
|
||||
try { this.stopScanTimer && this.stopScanTimer() } catch (e) {}
|
||||
if (this._discoveryStopTimer) {
|
||||
clearTimeout(this._discoveryStopTimer)
|
||||
this._discoveryStopTimer = null
|
||||
}
|
||||
if (this._resumeBackgroundSearchTimer) {
|
||||
clearTimeout(this._resumeBackgroundSearchTimer)
|
||||
this._resumeBackgroundSearchTimer = null
|
||||
}
|
||||
this._pauseBackgroundSearch = false
|
||||
this._manualSearchInProgress = false
|
||||
if (typeof wx.stopBluetoothDevicesDiscovery === 'function') {
|
||||
wx.stopBluetoothDevicesDiscovery({ complete: () => {} })
|
||||
}
|
||||
@@ -298,6 +622,8 @@ Page({
|
||||
clearInterval(this._fixedLoopTimer)
|
||||
this._fixedLoopTimer = null
|
||||
}
|
||||
this.stopConnectedDeviceMonitor()
|
||||
this.teardownSearchPageBleStateListener()
|
||||
},
|
||||
|
||||
// 连接设备
|
||||
@@ -324,10 +650,21 @@ Page({
|
||||
: base
|
||||
console.log('navigateTo:', withParams)
|
||||
try { this._navigatingToB13 = true } catch (e) { /* ignore */ }
|
||||
this.markPendingBleNavigation('B13', mac, { devName, serviceId: svc, txCharId: tx, rxCharId: rx })
|
||||
wx.navigateTo({ url: withParams })
|
||||
} else if (this.data.activeTab === 'BLV_RQ') {
|
||||
const devName = device.name || 'BLV_RQ设备'
|
||||
const mac = this.data.currentDeviceId || device.id || ''
|
||||
const svc = this.data.currentServiceId || device.serviceId || ''
|
||||
const tx = this.data.currentTxCharId || device.txCharId || ''
|
||||
const rx = this.data.currentRxCharId || device.rxCharId || ''
|
||||
const base = `/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(mac)}&connected=1`
|
||||
const withParams = (svc && tx && rx)
|
||||
? `${base}&serviceId=${encodeURIComponent(svc)}&txCharId=${encodeURIComponent(tx)}&rxCharId=${encodeURIComponent(rx)}`
|
||||
: base
|
||||
try { this._navigatingToBLVRQ = true } catch (e) { /* ignore */ }
|
||||
this.markPendingBleNavigation('BLV_RQ', mac, { devName, serviceId: svc, txCharId: tx, rxCharId: rx })
|
||||
wx.navigateTo({ url: withParams })
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
wx.showToast({ title: '已连接当前设备', icon: 'none' })
|
||||
}
|
||||
@@ -376,6 +713,7 @@ Page({
|
||||
success: () => {
|
||||
const list = this.data.deviceList.map((d) => ({ ...d, connected: d.id === device.id }))
|
||||
this.setData({ deviceList: list, currentDeviceId: device.id })
|
||||
this.startConnectedDeviceMonitor()
|
||||
// 设置MTU为256,提升传输效率(若支持)
|
||||
if (typeof wx.setBLEMTU === 'function') {
|
||||
wx.setBLEMTU({
|
||||
@@ -386,7 +724,17 @@ Page({
|
||||
})
|
||||
}
|
||||
// 连接成功后发现服务与特征
|
||||
this.discoverBleChannels(device)
|
||||
if (this.data.activeTab === 'BLV_RQ') {
|
||||
this.markPendingBleNavigation('BLV_RQ', device.id, {
|
||||
devName: device.name || 'BLV_RQ设备',
|
||||
serviceId: this.data.currentServiceId,
|
||||
txCharId: this.data.currentTxCharId,
|
||||
rxCharId: this.data.currentRxCharId
|
||||
})
|
||||
this.discoverBLVRQChannels(device)
|
||||
} else {
|
||||
this.discoverBleChannels(device)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.hideLoading()
|
||||
@@ -397,6 +745,7 @@ Page({
|
||||
try { this.appendLog && this.appendLog('CFG', 'createBLEConnection: already connected, treating as connected') } catch (e) {}
|
||||
const list = this.data.deviceList.map((d) => ({ ...d, connected: d.id === device.id }))
|
||||
this.setData({ deviceList: list, currentDeviceId: device.id })
|
||||
this.startConnectedDeviceMonitor()
|
||||
// 继续发现服务与特征以恢复页面状态
|
||||
try { this.discoverBleChannels(device) } catch (e) {}
|
||||
return
|
||||
@@ -468,6 +817,11 @@ Page({
|
||||
try { this._navigatingToB13 = true } catch (e) { /* ignore */ }
|
||||
wx.navigateTo({ url })
|
||||
// this.sendFixedCommand(deviceId, serviceId, ffe1.uuid)
|
||||
} else if (this.data.activeTab === 'BLV_RQ') {
|
||||
const devName = device.name || 'BLV_RQ设备'
|
||||
const url = `/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(deviceId)}&connected=1&serviceId=${encodeURIComponent(serviceId)}&txCharId=${encodeURIComponent(ffe1.uuid)}&rxCharId=${encodeURIComponent(ffe2.uuid)}`
|
||||
try { this._navigatingToBLVRQ = true } catch (e) { /* ignore */ }
|
||||
wx.navigateTo({ url })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -494,6 +848,91 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
discoverBLVRQChannels(device) {
|
||||
const deviceId = device.id
|
||||
wx.getBLEDeviceServices({
|
||||
deviceId,
|
||||
success: (srvRes) => {
|
||||
const services = srvRes.services || []
|
||||
const targetService = services.find((s) => {
|
||||
const uuid = String(s.uuid || '').toLowerCase()
|
||||
return uuid.startsWith('0000fff0')
|
||||
})
|
||||
|
||||
if (!targetService) {
|
||||
wx.hideLoading()
|
||||
wx.showModal({ title: '提示', content: '未找到FFF0目标服务', showCancel: false })
|
||||
return
|
||||
}
|
||||
|
||||
wx.getBLEDeviceCharacteristics({
|
||||
deviceId,
|
||||
serviceId: targetService.uuid,
|
||||
success: (chRes) => {
|
||||
const chars = chRes.characteristics || []
|
||||
const fff1 = chars.find(c => this._matchUuid(c.uuid, 'FFF1'))
|
||||
const fff2 = chars.find(c => this._matchUuid(c.uuid, 'FFF2'))
|
||||
const fff3 = chars.find(c => this._matchUuid(c.uuid, 'FFF3'))
|
||||
|
||||
if (!fff1 || !fff2 || !fff3) {
|
||||
wx.hideLoading()
|
||||
wx.showModal({ title: '提示', content: '未找到FFF1/FFF2/FFF3完整特征', showCancel: false })
|
||||
return
|
||||
}
|
||||
|
||||
const notifyTargets = [fff1, fff2].filter(c => c.properties && c.properties.notify)
|
||||
let pending = notifyTargets.length
|
||||
const finalize = () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '连接成功', icon: 'success' })
|
||||
const devName = device.name || 'BLV_RQ设备'
|
||||
this.setData({
|
||||
currentServiceId: targetService.uuid,
|
||||
currentTxCharId: fff1.uuid,
|
||||
currentRxCharId: fff2.uuid,
|
||||
currentReadCharId: fff2.uuid,
|
||||
currentWriteCharId: fff1.uuid
|
||||
})
|
||||
const url = `/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(deviceId)}&connected=1&serviceId=${encodeURIComponent(targetService.uuid)}&txCharId=${encodeURIComponent(fff1.uuid)}&rxCharId=${encodeURIComponent(fff2.uuid)}&readCharId=${encodeURIComponent(fff2.uuid)}&writeCharId=${encodeURIComponent(fff1.uuid)}`
|
||||
this.markPendingBleNavigation('BLV_RQ', deviceId, { devName, serviceId: targetService.uuid, txCharId: fff1.uuid, rxCharId: fff2.uuid })
|
||||
wx.navigateTo({ url })
|
||||
}
|
||||
|
||||
if (!pending) {
|
||||
finalize()
|
||||
return
|
||||
}
|
||||
|
||||
notifyTargets.forEach((charItem) => {
|
||||
wx.notifyBLECharacteristicValueChange({
|
||||
state: true,
|
||||
deviceId,
|
||||
serviceId: targetService.uuid,
|
||||
characteristicId: charItem.uuid,
|
||||
complete: () => {
|
||||
pending -= 1
|
||||
if (pending <= 0) {
|
||||
finalize()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.hideLoading()
|
||||
console.error('获取BLV_RQ特征失败', err)
|
||||
wx.showToast({ title: '获取特征失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.hideLoading()
|
||||
console.error('获取BLV_RQ服务失败', err)
|
||||
wx.showToast({ title: '获取服务失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
_matchUuid(uuid, needle) {
|
||||
if (!uuid || !needle) return false
|
||||
const u = String(uuid).replace(/-/g, '').toUpperCase()
|
||||
@@ -572,7 +1011,7 @@ Page({
|
||||
const idx = this.data.deviceList.findIndex(d => d.connected)
|
||||
if (idx >= 0) {
|
||||
// 标记断开状态
|
||||
const list = this.data.deviceList.map((d, i) => ({ ...d, connected: false }))
|
||||
const list = this.data.deviceList.map((d, i) => ({ ...d, connected: false, RSSI: null, signal: 0 }))
|
||||
this.setData({ deviceList: list })
|
||||
}
|
||||
// 如果保留了设备ID,尝试调用系统断开
|
||||
@@ -585,6 +1024,7 @@ Page({
|
||||
}
|
||||
this.setData({ currentDeviceId: null })
|
||||
}
|
||||
this.stopConnectedDeviceMonitor()
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -613,10 +1053,12 @@ Page({
|
||||
wx.setNavigationBarTitle({ title: '蓝牙调试' })
|
||||
}
|
||||
|
||||
// 页面加载时,根据当前选中的选项卡加载设备
|
||||
this.loadDevicesByTab(this.data.activeTab)
|
||||
// 同步执行一次蓝牙搜索(按 W13 过滤规则)
|
||||
this.searchBluetooth()
|
||||
this.setData({
|
||||
deviceTypeIndex: 0,
|
||||
activeTab: 'NONE',
|
||||
deviceList: []
|
||||
})
|
||||
this.syncScanTimerByType('NONE')
|
||||
},
|
||||
|
||||
onShow() {
|
||||
@@ -625,60 +1067,85 @@ Page({
|
||||
// 已从 B13 返回:清理导航标记但仍继续执行恢复流程,确保已连接设备状态可被恢复并展示
|
||||
this._navigatingToB13 = false
|
||||
}
|
||||
if (this._navigatingToBLVRQ) {
|
||||
this._navigatingToBLVRQ = false
|
||||
}
|
||||
|
||||
try { this.appendLog && this.appendLog('CFG', 'onShow: resume, ensuring adapter and forcing discovery') } catch (e) {}
|
||||
// 息屏唤醒后可能适配器被关闭,先确保打开后短延迟再搜索一次
|
||||
const deviceType = this.data.activeTab
|
||||
this.bindSearchPageBleStateListener()
|
||||
this.syncScanTimerByType(deviceType)
|
||||
this.startConnectedDeviceMonitor()
|
||||
|
||||
if (!this.isSearchableDeviceType(deviceType)) {
|
||||
try { this.appendLog && this.appendLog('CFG', 'onShow: no searchable device type, skip discovery') } catch (e) {}
|
||||
return
|
||||
}
|
||||
|
||||
try { this.appendLog && this.appendLog('CFG', 'onShow: resume adapter and restore devices without auto scan') } catch (e) {}
|
||||
this.ensureBluetoothReady()
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.startBluetoothDevicesDiscovery(this.data.activeTab)
|
||||
} catch (e) { /* ignore */ }
|
||||
}, 300)
|
||||
|
||||
// 尝试获取系统已连接设备,优先恢复之前连接的设备显示(防止已连接但不广播的设备无法被扫描到)
|
||||
try {
|
||||
const svc = this.data.currentServiceId
|
||||
const isW13 = this.data.activeTab === 'W13'
|
||||
const prefix = this.data.activeTab
|
||||
const matchByTab = (name) => {
|
||||
const n = name || ''
|
||||
if (isW13) return /^BLV_(W13|C13)_.+$/i.test(n)
|
||||
return prefix ? n.startsWith(prefix) : true
|
||||
}
|
||||
const matchByType = (name) => this.matchDeviceType(name, deviceType)
|
||||
|
||||
if (typeof wx.getConnectedBluetoothDevices === 'function' && svc) {
|
||||
wx.getConnectedBluetoothDevices({ services: [svc], success: (res) => {
|
||||
const devices = (res && res.devices) || []
|
||||
this.reconcileConnectedDevices(devices, deviceType)
|
||||
if (devices.length) {
|
||||
// 将已连接设备合并到列表并按过滤规则筛选
|
||||
const list = [...this.data.deviceList]
|
||||
devices.forEach(d => {
|
||||
|
||||
const name = d.name || d.localName || ''
|
||||
if (!matchByTab(name)) return
|
||||
if (!matchByType(name)) return
|
||||
const idx = list.findIndex(x => x.id === d.deviceId)
|
||||
const mapped = { id: d.deviceId, name, mac: d.deviceId, connected: true, RSSI: d.RSSI || 0, serviceUUIDs: d.serviceUUIDs || [] }
|
||||
const prev = idx >= 0 ? list[idx] : null
|
||||
const hasSystemRSSI = typeof d.RSSI === 'number' && d.RSSI !== 0
|
||||
const rssi = hasSystemRSSI ? d.RSSI : (prev && typeof prev.RSSI === 'number' ? prev.RSSI : null)
|
||||
const mapped = {
|
||||
id: d.deviceId,
|
||||
name,
|
||||
mac: d.deviceId,
|
||||
connected: true,
|
||||
RSSI: rssi,
|
||||
signal: this.getSignalByRSSI(rssi),
|
||||
serviceUUIDs: d.serviceUUIDs || []
|
||||
}
|
||||
mapped.lastSeenAt = Date.now()
|
||||
if (idx >= 0) list[idx] = { ...list[idx], ...mapped }
|
||||
else list.unshift(mapped)
|
||||
this.refreshConnectedDeviceRSSI(d.deviceId)
|
||||
})
|
||||
this.setData({ deviceList: list })
|
||||
try { this.appendLog && this.appendLog('CFG', 'restored connected devices from system') } catch (e) {}
|
||||
}
|
||||
}})
|
||||
} else if (typeof wx.getBluetoothDevices === 'function') {
|
||||
// 作为兜底,查询最近缓存的设备并按过滤规则合并
|
||||
wx.getBluetoothDevices({ success: (res) => {
|
||||
const devices = (res && res.devices) || []
|
||||
const connectedDevices = devices.filter(d => !!d.connected)
|
||||
this.reconcileConnectedDevices(connectedDevices, deviceType)
|
||||
if (devices.length) {
|
||||
const list = [...this.data.deviceList]
|
||||
devices.forEach(d => {
|
||||
const name = d.name || d.localName || ''
|
||||
if (!matchByTab(name)) return
|
||||
if (!matchByType(name)) return
|
||||
const idx = list.findIndex(x => x.id === d.deviceId)
|
||||
const mapped = { id: d.deviceId, name, mac: d.deviceId, connected: !!d.connected, RSSI: d.RSSI || 0, serviceUUIDs: d.serviceUUIDs || [] }
|
||||
const prev = idx >= 0 ? list[idx] : null
|
||||
const hasSystemRSSI = typeof d.RSSI === 'number' && d.RSSI !== 0
|
||||
const rssi = hasSystemRSSI ? d.RSSI : (prev && typeof prev.RSSI === 'number' ? prev.RSSI : null)
|
||||
const mapped = {
|
||||
id: d.deviceId,
|
||||
name,
|
||||
mac: d.deviceId,
|
||||
connected: !!d.connected,
|
||||
RSSI: rssi,
|
||||
signal: this.getSignalByRSSI(rssi),
|
||||
serviceUUIDs: d.serviceUUIDs || []
|
||||
}
|
||||
mapped.lastSeenAt = Date.now()
|
||||
if (idx >= 0) list[idx] = { ...list[idx], ...mapped }
|
||||
else list.push(mapped)
|
||||
if (mapped.connected) this.refreshConnectedDeviceRSSI(d.deviceId)
|
||||
})
|
||||
this.setData({ deviceList: list })
|
||||
}
|
||||
@@ -689,42 +1156,43 @@ Page({
|
||||
.catch((err) => {
|
||||
try { this.appendLog && this.appendLog('WARN', 'onShow ensureBluetoothReady failed') } catch (e) {}
|
||||
})
|
||||
// 进入页面时启动定时扫描(每3秒一次)
|
||||
try { this.startScanTimer && this.startScanTimer() } catch (e) {}
|
||||
},
|
||||
|
||||
onHide() {
|
||||
// 如果正在导航到 B13 页面,保留连接会话,但应停止扫描/发现以节省资源
|
||||
if (this._navigatingToB13) {
|
||||
try { this.appendLog && this.appendLog('CFG', 'onHide during navigation to B13: stop scan but keep connection') } catch (e) {}
|
||||
// 停止定时扫描
|
||||
try { this.stopScanTimer && this.stopScanTimer() } catch (e) {}
|
||||
// 停止发现(但不断开已连接设备),随后直接返回以保留连接状态
|
||||
try {
|
||||
if (typeof wx.stopBluetoothDevicesDiscovery === 'function') {
|
||||
wx.stopBluetoothDevicesDiscovery({ complete: () => { try { this.appendLog && this.appendLog('CFG', 'stopBluetoothDevicesDiscovery onHide (navigating to B13)') } catch (e) {} } })
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return
|
||||
}
|
||||
|
||||
// 非导航到 B13 的离开:停止定时扫描并断开连接、关闭适配器以重置状态
|
||||
this.stopConnectedDeviceMonitor()
|
||||
try { this.stopScanTimer && this.stopScanTimer() } catch (e) {}
|
||||
|
||||
if (this._discoveryStopTimer) {
|
||||
clearTimeout(this._discoveryStopTimer)
|
||||
this._discoveryStopTimer = null
|
||||
}
|
||||
if (this._resumeBackgroundSearchTimer) {
|
||||
clearTimeout(this._resumeBackgroundSearchTimer)
|
||||
this._resumeBackgroundSearchTimer = null
|
||||
}
|
||||
this._pauseBackgroundSearch = false
|
||||
this._manualSearchInProgress = false
|
||||
try {
|
||||
// 停止发现,避免后台扫描
|
||||
if (typeof wx.stopBluetoothDevicesDiscovery === 'function') {
|
||||
wx.stopBluetoothDevicesDiscovery({ complete: () => { try { this.appendLog && this.appendLog('CFG', 'stopBluetoothDevicesDiscovery onHide') } catch (e) {} } })
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
const pendingBleNavigation = app.globalData && app.globalData.pendingBleNavigation
|
||||
const keepBleSession = this._navigatingToB13 || this._navigatingToBLVRQ || (
|
||||
pendingBleNavigation &&
|
||||
pendingBleNavigation.keepConnection &&
|
||||
Date.now() - (pendingBleNavigation.timestamp || 0) < 5000
|
||||
)
|
||||
if (keepBleSession) {
|
||||
try { this.appendLog && this.appendLog('CFG', 'onHide during navigation: keep connection and skip disconnect') } catch (e) {}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 断开当前连接(如果有)
|
||||
this.disconnectCurrentDevice()
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
try {
|
||||
// 关闭蓝牙适配器以重置底层状态(部分 Android 机型息屏后需要此步骤)
|
||||
if (typeof wx.closeBluetoothAdapter === 'function') {
|
||||
wx.closeBluetoothAdapter({ complete: () => { try { this.appendLog && this.appendLog('CFG', 'closeBluetoothAdapter called onHide') } catch (e) {} } })
|
||||
}
|
||||
|
||||
@@ -7,25 +7,13 @@
|
||||
|
||||
<!-- 内容栏顶部 -->
|
||||
<view class="content-header">
|
||||
<!-- 左侧导航栏 -->
|
||||
<view class="nav-tabs">
|
||||
<view
|
||||
class="nav-tab {{activeTab === 'host' ? 'active' : ''}}"
|
||||
data-tab="host"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
主机
|
||||
<picker class="device-type-picker" mode="selector" range="{{deviceTypeLabels}}" value="{{deviceTypeIndex}}" bindchange="onDeviceTypeChange">
|
||||
<view class="device-type-display {{deviceTypeIndex === 0 ? 'placeholder' : ''}}">
|
||||
<text class="device-type-text">{{deviceTypeLabels[deviceTypeIndex]}}</text>
|
||||
<text class="device-type-arrow">▼</text>
|
||||
</view>
|
||||
<view
|
||||
class="nav-tab {{activeTab === 'W13' ? 'active' : ''}}"
|
||||
data-tab="W13"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
W13
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧搜索按钮 -->
|
||||
</picker>
|
||||
|
||||
<view class="search-btn" bindtap="searchBluetooth">
|
||||
搜索蓝牙
|
||||
</view>
|
||||
|
||||
@@ -14,32 +14,43 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
/* 左侧导航栏 */
|
||||
.nav-tabs {
|
||||
.device-type-picker {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.device-type-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
min-height: 70rpx;
|
||||
padding: 0 22rpx;
|
||||
background: #ffffff;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 50rpx;
|
||||
padding: 2rpx;
|
||||
gap: 2rpx;
|
||||
border: 1rpx solid #dce3ef;
|
||||
border-radius: 18rpx;
|
||||
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.04);
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
padding: 12rpx 26rpx;
|
||||
border-radius: 50rpx;
|
||||
.device-type-display.placeholder {
|
||||
color: #8c9399;
|
||||
}
|
||||
|
||||
.device-type-text {
|
||||
font-size: 28rpx;
|
||||
transition: all 0.3s ease;
|
||||
color: #666666;
|
||||
background-color: #ffffff;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background: linear-gradient(90deg, #00C6FF, #0072FF);
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 114, 255, 0.3);
|
||||
.device-type-arrow {
|
||||
font-size: 20rpx;
|
||||
color: #7b8794;
|
||||
}
|
||||
|
||||
/* 右侧搜索按钮 */
|
||||
|
||||
Reference in New Issue
Block a user