Files
Wx_BLWConfigTools_V02_Prod/pages/basics/BluetoothDebugging/BluetoothDebugging.js
chenzhihao f94cf21e7a feat: 添加蓝牙调试页面的初始实现
- 新增 BLVRQPage.json 配置文件,设置导航栏样式。
- 新增 BLVRQPage.wxml 文件,构建蓝牙设备信息展示和操作界面。
- 新增 BLVRQPage.wxss 文件,定义页面样式和布局。
2026-03-20 18:18:49 +08:00

1201 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// pages/bluetooth-connect/bluetooth-connect.js
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:"",
deviceTypeLabels: ['请选择设备类型', '主机', 'W13', 'BLV_RQ'],
deviceTypeValues: ['NONE', 'HOST', 'W13', 'BLV_RQ'],
deviceTypeIndex: 0,
activeTab: 'NONE',
autho: null,
Hotelinfo: {},
deviceList: [],
currentDeviceId: null,
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连接避免连接遗留
try {
this.disconnectCurrentDevice()
} catch (e) {}
wx.navigateBack()
},
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
}
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: '当前有蓝牙设备处于已连接状态,切换设备类型后将断开该连接,是否继续?',
success: (res) => {
if (!res.confirm) {
this.setData({ deviceTypeIndex: this.data.deviceTypeValues.indexOf(this.data.activeTab) })
return
}
try { this.disconnectCurrentDevice() } catch (e) {}
this.applyDeviceTypeChange(selectedType, index)
}
})
return
}
this.applyDeviceTypeChange(selectedType, index)
},
disconnectAllDevices() {
const list = this.data.deviceList.map(d => ({ ...d, connected: false }))
// 按 signal 从高到低排序,避免空值影响
list.sort((a, b) => (b.signal || 0) - (a.signal || 0))
this.setData({ deviceList: list })
},
// 根据选项卡加载设备数据
loadDevicesByTab(tab) {
// 这里可以根据tab从服务器获取对应的设备列表
console.log('加载设备列表,选项卡:', tab)
// 模拟不同选项卡的设备数据
let deviceList = []
if (tab === 'host') {
deviceList = [
{
id: 1,
name: '主机设备1',
signal: 95,
connected: true
},
{
id: 2,
name: '主机设备2',
signal: 80,
connected: false
}
]
} else {
deviceList = this.data.deviceList
}
this.setData({ deviceList })
},
// 搜索蓝牙设备
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: [] })
}
if (source !== 'timer') {
this._manualSearchInProgress = true
this._pauseBackgroundSearch = true
if (this._resumeBackgroundSearchTimer) {
clearTimeout(this._resumeBackgroundSearchTimer)
this._resumeBackgroundSearchTimer = null
}
}
this._shouldShowSearching = showLoading
this._hasFoundDeviceInCurrentSearch = false
this._searchToken = (this._searchToken || 0) + 1
const currentSearchToken = this._searchToken
this.syncSearchLoadingByList(currentSearchToken)
this.ensureBluetoothReady()
.then(() => this.startBluetoothDevicesDiscovery(deviceType, { restart, source, searchToken: currentSearchToken }))
.catch((err) => {
this._shouldShowSearching = false
if (source !== 'timer') {
this._manualSearchInProgress = false
this.scheduleResumeBackgroundSearch()
}
console.error('蓝牙初始化失败', err)
if (this._searchToken === currentSearchToken) {
wx.hideLoading()
}
wx.showToast({ title: '请开启蓝牙和定位权限', icon: 'none' })
})
},
ensureBluetoothReady() {
return new Promise((resolve, reject) => {
wx.openBluetoothAdapter({
mode: 'central',
success: () => {
resolve()
},
fail: (err) => {
// 10001 系统蓝牙未打开10002 无权限
if (err && err.errCode === 10001) {
wx.showModal({
title: '蓝牙未开启',
content: '请先打开手机蓝牙后重试',
showCancel: false
})
} else {
wx.showModal({
title: '权限提示',
content: '请授权蓝牙与定位权限后重试',
showCancel: false
})
}
reject(err)
}
})
})
},
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) {
try { this.appendLog && this.appendLog('WARN', 'skip startBluetoothDevicesDiscovery: throttled') } catch (e) {}
return
}
this._lastScanAt = now
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: () => {
setTimeout(() => {
beginDiscovery()
}, SEARCH_RESTART_DELAY_MS)
}
})
}
// 优先查询适配器状态,若系统正在扫描则直接注册监听并返回
if (typeof wx.getBluetoothAdapterState === 'function') {
wx.getBluetoothAdapterState({
success: (res) => {
if (res && res.discovering) {
try { this.appendLog && this.appendLog('CFG', 'adapter already discovering, attach listener') } catch (e) {}
this.setupDeviceFoundListener(deviceType)
return
}
doStart()
},
fail: () => {
doStart()
}
})
} else {
doStart()
}
},
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, deviceType)
}
if (typeof wx.onBluetoothDeviceFound === 'function') {
wx.onBluetoothDeviceFound(this._deviceFoundHandler)
}
},
teardownDeviceFoundListener() {
if (this._deviceFoundHandler && typeof wx.offBluetoothDeviceFound === 'function') {
wx.offBluetoothDeviceFound(this._deviceFoundHandler)
}
this._deviceFoundHandler = null
},
// 启动/停止定时扫描每3秒一次
startScanTimer() {
if (this._scanTimer) return
try { this.appendLog && this.appendLog('CFG', 'startScanTimer') } catch (e) {}
this._scanTimer = setInterval(() => {
try {
if (this._manualSearchInProgress || this._pauseBackgroundSearch) return
// 触发一次搜索,内部有防抖保护
this.searchBluetooth({ source: 'timer', restart: false, clearList: false, showLoading: false })
} catch (e) { /* ignore */ }
}, 3000)
},
stopScanTimer() {
if (this._scanTimer) {
clearInterval(this._scanTimer)
this._scanTimer = null
try { this.appendLog && this.appendLog('CFG', 'stopScanTimer') } catch (e) {}
}
},
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 matched = this.matchDeviceType(name, deviceType || this.data.activeTab)
if (!matched) return
const existsIndex = list.findIndex((d) => d.id === dev.deviceId)
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: oldItem ? !!oldItem.connected : false,
RSSI: rssi,
localName: dev.localName || '',
serviceUUIDs: dev.serviceUUIDs || [],
lastSeenAt: Date.now()
}
if (existsIndex >= 0) {
// 设备已存在时仅更新信号值与 RSSI避免覆盖其它已保存字段
list[existsIndex] = {
...list[existsIndex],
name,
mac: dev.deviceId,
signal,
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)
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
// 取消自动显示搜索完成提示,避免打扰
}
})
},
onUnload() {
// 页面卸载时清理蓝牙扫描与监听
// 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: () => {} })
}
if (this._fixedLoopTimer) {
clearInterval(this._fixedLoopTimer)
this._fixedLoopTimer = null
}
this.stopConnectedDeviceMonitor()
this.teardownSearchPageBleStateListener()
},
// 连接设备
onDeviceTap(e) {
const index = e.currentTarget.dataset.index
const device = this.data.deviceList[index]
let coid= this.data.coid
if (!device) return
const currentIndex = this.data.deviceList.findIndex(d => d.connected)
// 如果点击的就是已连接设备直接进入对应页面并携带已保存的BLE参数
if (currentIndex === index && currentIndex >= 0) {
if (this.data.activeTab === 'W13') {
const devName = device.name || 'W13设备'
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 || ''
// 至少携带 mac若已持有 svc/tx/rx 则一并带上,避免重复发现
const base = `/pages/basics/BluetoothDebugging/B13page/B13page?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(mac)}`
const withParams = (svc && tx && rx)
? `${base}&serviceId=${encodeURIComponent(svc)}&txCharId=${encodeURIComponent(tx)}&rxCharId=${encodeURIComponent(rx)}`
: 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' })
}
return
}
if (currentIndex >= 0 && currentIndex !== index) {
wx.showModal({
title: '切换设备',
content: '已连接其他设备,是否切换到当前设备?',
success: (res) => {
if (res.confirm) {
this.connectToDevice(index)
}
}
})
} else if (currentIndex < 0) {
wx.showModal({
title: '连接设备',
content: '是否连接此蓝牙设备?',
success: (res) => {
if (res.confirm) {
this.connectToDevice(index)
}
}
})
} else {
this.connectToDevice(index)
}
},
connectToDevice(index) {
const device = this.data.deviceList[index]
if (!device || !device.id) {
wx.showToast({ title: '设备信息缺失', icon: 'none' })
return
}
wx.showLoading({ title: '连接中...', mask: true })
// 在尝试 createBLEConnection 前确保适配器已打开(处理息屏后 closeBluetoothAdapter 场景)
this.ensureBluetoothReady()
.then(() => {
// 使用 BLE 直连
wx.createBLEConnection({
deviceId: device.id,
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({
deviceId: device.id,
mtu: 500,
fail: () => console.warn('[BLE] set MTU 256 failed'),
success: () => console.info('[BLE] set MTU 256 success')
})
}
// 连接成功后发现服务与特征
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()
console.error('BLE 连接失败', err)
const errmsg = (err && (err.errMsg || err.message)) || ''
const isAlreadyConnected = errmsg.indexOf('already connect') >= 0 || errmsg.indexOf('already connected') >= 0 || (err && (err.errCode === -1 || err.errno === 1509007))
if (isAlreadyConnected) {
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
}
wx.showToast({ title: '连接失败', icon: 'none' })
}
})
})
.catch((err) => {
wx.hideLoading()
console.error('蓝牙未初始化,无法连接', err)
wx.showToast({ title: '蓝牙未初始化', icon: 'none' })
})
},
// 发现包含 FFE1(写) / FFE2(订阅) 的服务与特征,并启用 FFE2 通知
discoverBleChannels(device) {
const deviceId = device.id
wx.getBLEDeviceServices({
deviceId,
success: (srvRes) => {
const services = srvRes.services || []
if (!services.length) {
wx.hideLoading()
wx.showToast({ title: '未发现服务', icon: 'none' })
return
}
let found = false
let pending = services.length
// 优先自定义/未知服务UUID 含 FFE其余按原顺序
const score = (s) => {
const u = (s.uuid || '').toUpperCase()
return u.includes('FFE') ? 2 : (s.isPrimary === true ? 1 : 0)
}
const sorted = services.slice().sort((a, b) => score(b) - score(a))
sorted.forEach(s => {
const serviceId = s.uuid
wx.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (chRes) => {
const chars = chRes.characteristics || []
const ffe1 = chars.find(c => this._matchUuid(c.uuid, 'FFE1'))
const ffe2 = chars.find(c => this._matchUuid(c.uuid, 'FFE2'))
if (!found && ffe1 && ffe2) {
found = true
// 启用FFE2通知
wx.notifyBLECharacteristicValueChange({
state: true,
deviceId,
serviceId,
characteristicId: ffe2.uuid,
complete: () => {
wx.hideLoading()
wx.showToast({ title: '连接成功', icon: 'success' })
// 保存当前服务与特征,供已连接设备直接进入页面使用
this.setData({
currentServiceId: serviceId,
currentTxCharId: ffe1.uuid,
currentRxCharId: ffe2.uuid
})
// 连接成功后发送指定命令帧
// this.sendFixedCommand(deviceId, serviceId, ffe1.uuid)
if (this.data.activeTab === 'W13') {
const devName = device.name || 'W13设备'
const url = `/pages/basics/BluetoothDebugging/B13page/B13page?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(deviceId)}&serviceId=${encodeURIComponent(serviceId)}&txCharId=${encodeURIComponent(ffe1.uuid)}&rxCharId=${encodeURIComponent(ffe2.uuid)}`
console.log(url)
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 })
}
}
})
}
},
fail: () => {
// 不中断流程,继续其他服务
},
complete: () => {
pending -= 1
if (!found && pending === 0) {
wx.hideLoading()
wx.showModal({ title: '提示', content: '未找到FFE1/FFE2特征', showCancel: false })
}
}
})
})
},
fail: (err) => {
wx.hideLoading()
console.error('获取服务失败', err)
wx.showToast({ title: '获取服务失败', icon: 'none' })
}
})
},
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()
const n = String(needle).toUpperCase().replace(/^0X/, '')
// 模糊匹配包含指定段即可兼容16/128位 UUID
return u.includes(n)
},
sendFixedCommand(deviceId, serviceId, txCharId) {
if (!deviceId || !serviceId || !txCharId || typeof wx.writeBLECharacteristicValue !== 'function') return
console.info(`[BLE] sendFixedCommand params device=${deviceId} svc=${serviceId} tx=${txCharId}`)
try {
wx.writeBLECharacteristicValue({
deviceId,
serviceId,
characteristicId: txCharId,
value: FIXED_CONNECT_CMD.buffer,
complete: () => {
console.info(`[BLE] sent fixed cmd device=${deviceId} svc=${serviceId} tx=${txCharId}`)
}
})
} catch (e) {
console.warn('[BLE] send fixed cmd failed', e)
}
},
// 测试函数每5秒发送一次固定命令
startFixedCmdLoop() {
const deviceId = this.data.currentDeviceId
const serviceId = this.data.currentServiceId
const txCharId = this.data.currentTxCharId
if (!deviceId || !serviceId || !txCharId) {
wx.showToast({ title: '未连接BLE', icon: 'none' })
return
}
if (this._fixedLoopTimer) {
clearInterval(this._fixedLoopTimer)
this._fixedLoopTimer = null
}
const sendOnce = () => {
try {
wx.writeBLECharacteristicValue({
deviceId,
serviceId,
characteristicId: txCharId,
value: FIXED_CONNECT_CMD.buffer,
fail: (err) => {
console.warn('[BLE] loop fixed cmd fail', err && (err.errMsg || err.message) || err)
}
})
} catch (e) {
console.warn('[BLE] loop fixed cmd exception', e && (e.errMsg || e.message) || e)
}
}
sendOnce()
this._fixedLoopTimer = setInterval(sendOnce, 5000)
wx.showToast({ title: '已启动固定命令循环', icon: 'none' })
},
// 可选:停止循环发送
stopFixedCmdLoop() {
if (this._fixedLoopTimer) {
clearInterval(this._fixedLoopTimer)
this._fixedLoopTimer = null
wx.showToast({ title: '已停止循环', icon: 'none' })
}
},
// 断开当前连接设备(如果有真实连接)
disconnectCurrentDevice() {
// 如果正在导航到 B13 页面,避免主动断开连接以保持会话
if (this._navigatingToB13) {
try { this.appendLog && this.appendLog('CFG', 'skip disconnectCurrentDevice during navigate to B13') } catch (e) {}
return
}
const idx = this.data.deviceList.findIndex(d => d.connected)
if (idx >= 0) {
// 标记断开状态
const list = this.data.deviceList.map((d, i) => ({ ...d, connected: false, RSSI: null, signal: 0 }))
this.setData({ deviceList: list })
}
// 如果保留了设备ID尝试调用系统断开
const devId = this.data.currentDeviceId
if (devId && typeof wx.closeBLEConnection === 'function') {
try {
wx.closeBLEConnection({ deviceId: devId, complete: () => {} })
} catch (e) {
// 忽略断开异常,继续搜索
}
this.setData({ currentDeviceId: null })
}
this.stopConnectedDeviceMonitor()
},
onLoad() {
const { autho } = app.globalData || {}
let currentHotel = null
if (autho && Array.isArray(autho) && autho.length > 0) {
// 优先取第一个分组里的第一个酒店;后续可按需要改为 options.HotelId 精确匹配
const firstGroup = autho[0]
if (firstGroup && Array.isArray(firstGroup.Hotels) && firstGroup.Hotels.length > 0) {
currentHotel = firstGroup.Hotels[0]
}
}
if (currentHotel) {
this.setData({
autho,
Hotelinfo: currentHotel
})
const title = currentHotel.HotelName
? `${currentHotel.HotelName}${currentHotel.Code ? ' (' + currentHotel.Code + ')' : ''}`
: '蓝牙调试'
wx.setNavigationBarTitle({ title })
} else {
wx.setNavigationBarTitle({ title: '蓝牙调试' })
}
this.setData({
deviceTypeIndex: 0,
activeTab: 'NONE',
deviceList: []
})
this.syncScanTimerByType('NONE')
},
onShow() {
// 返回到此页面时,清理导航标记
if (this._navigatingToB13) {
// 已从 B13 返回:清理导航标记但仍继续执行恢复流程,确保已连接设备状态可被恢复并展示
this._navigatingToB13 = false
}
if (this._navigatingToBLVRQ) {
this._navigatingToBLVRQ = false
}
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(() => {
try {
const svc = this.data.currentServiceId
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 (!matchByType(name)) return
const idx = list.findIndex(x => x.id === d.deviceId)
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 (!matchByType(name)) return
const idx = list.findIndex(x => x.id === d.deviceId)
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 })
}
}})
}
} catch (e) { /* ignore */ }
})
.catch((err) => {
try { this.appendLog && this.appendLog('WARN', 'onShow ensureBluetoothReady failed') } catch (e) {}
})
},
onHide() {
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 {
if (typeof wx.closeBluetoothAdapter === 'function') {
wx.closeBluetoothAdapter({ complete: () => { try { this.appendLog && this.appendLog('CFG', 'closeBluetoothAdapter called onHide') } catch (e) {} } })
}
} catch (e) { /* ignore */ }
}
})