// 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 */ } } })