From 2238e965b94916ea6a47a88cf5c56815edccbe2c Mon Sep 17 00:00:00 2001 From: ozh Date: Thu, 10 Sep 2026 11:11:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(ble):=20=E6=81=A2=E5=A4=8D=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E6=89=AB=E6=8F=8F=E4=BC=98=E5=85=88+4s=E5=85=9C?= =?UTF-8?q?=E5=BA=95=E5=85=A8=E6=89=AB=EF=BC=8C=E5=B9=B6=E5=8A=A0=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=8E=92=E9=9A=9C=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机验证"直接全频段扫描"方案后设备反而搜不出(修改前过滤扫描可搜到), 说明该机型下无过滤扫描路径存在问题(可能为机型对无 services 参数的 startBluetoothDevicesDiscovery 兼容差异)。回退为迁移原版策略: - 恢复 DISCOVERY_FILTER_SERVICE_UUIDS 常量与 fallbackToUnfilteredDiscoveryIfNeeded 兜底机制:先按 0xFFF4/0x00F4/0xF400 过滤扫描,4s 无结果自动切全扫 - upsertDevice 改为返回布尔值,event/poll 两条通道均打印排障日志 ([BLE][SCAN] 原生采集数与命中数、[BLE][FOUND] 事件命中数、 启动/兜底切换日志),真机 vConsole 可直接观察各阶段状态 --- src/ble/discovery.js | 84 ++++++++++++++++++++++++++++++++++---- src/constants/bluetooth.js | 10 +++++ 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/ble/discovery.js b/src/ble/discovery.js index 9d77669..696810b 100644 --- a/src/ble/discovery.js +++ b/src/ble/discovery.js @@ -1,6 +1,7 @@ import { BLUETOOTH_ERROR_MESSAGES, DEFAULT_BLUETOOTH_ERROR_MESSAGE, + DISCOVERY_FILTER_SERVICE_UUIDS, TARGET_SERVICE_UUID_FULL, TARGET_SERVICE_UUID_ALT_FULL, TARGET_SERVICE_UUID_COMPAT_FULL, @@ -13,6 +14,8 @@ import Taro from '@tarojs/taro' const RESTART_SCAN_DELAY_MS = 120 const DISCOVERY_POLL_INTERVAL_MS = 1200 const SCAN_DURATION_MS = 30000 +// 过滤扫描无结果多久后切到无过滤全扫 +const FILTER_SCAN_TIMEOUT_MS = 4000 // 设备列表 UI 刷新最小间隔:搜索中 allowDuplicatesKey=true,目标设备每秒广播 // 数次,不节流会以每秒数十次 setState 打满小程序渲染线程,点击被排队卡死 const DEVICE_LIST_EMIT_INTERVAL_MS = 300 @@ -81,6 +84,7 @@ export function createBluetoothDiscovery(options = {}) { let discoveryPollTimer = null let autoStopTimer = null let deviceListEmitTimer = null + let filterFallbackTimer = null let offDeviceFound = null let offAdapterState = null @@ -282,6 +286,19 @@ export function createBluetoothDiscovery(options = {}) { ].join('#') } + function hasTargetDeviceInMap() { + return Object.values(deviceMap).some((item) => item.isTargetDevice) + } + + function clearFallbackTimer() { + if (!filterFallbackTimer) { + return + } + + clearTimeout(filterFallbackTimer) + filterFallbackTimer = null + } + function clearDiscoveryPollTimer() { if (!discoveryPollTimer) { return @@ -350,12 +367,12 @@ export function createBluetoothDiscovery(options = {}) { function upsertDevice(rawDevice) { if (!rawDevice || !rawDevice.deviceId) { - return + return false } const matched = matchTargetDevice(rawDevice) if (!matched.isTarget) { - return + return false } const existing = deviceMap[rawDevice.deviceId] || {} @@ -374,13 +391,14 @@ export function createBluetoothDiscovery(options = {}) { const nextSignature = getDeviceStableSignature(merged) if (prevSignature === nextSignature) { - return + return false } setDeviceMap({ ...deviceMap, [rawDevice.deviceId]: merged }) + return true } function getBluetoothErrorText(error) { @@ -415,13 +433,19 @@ export function createBluetoothDiscovery(options = {}) { } const currentSession = searchSessionId + let hitCount = 0 devices.forEach((device) => { if (!acceptingDeviceEvents || currentSession !== searchSessionId) { return } - upsertDevice(device) + if (upsertDevice(device)) { + hitCount += 1 + } }) + if (hitCount) { + console.debug('[BLE][FOUND]', 'event devices=', devices.length, 'newHits=', hitCount) + } }) offAdapterState = controller.ble.on('adapter:state', (state) => { @@ -562,13 +586,18 @@ export function createBluetoothDiscovery(options = {}) { try { const result = await promisifyTaroApi('getBluetoothDevices') const list = Array.isArray(result?.devices) ? result.devices : [] + // 排障日志:原生层到底采到了多少设备、命中了多少目标(真机 vConsole 可见) + let hitCount = 0 list.forEach((device) => { if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) { return } - upsertDevice(device) + if (upsertDevice(device)) { + hitCount += 1 + } }) + console.debug('[BLE][SCAN]', 'native devices=', list.length, 'newHits=', hitCount) } catch (error) { } } @@ -593,10 +622,36 @@ export function createBluetoothDiscovery(options = {}) { discoveryPollTimer = setTimeout(run, 300) } + // 过滤扫描超过 4s 仍无目标设备:停扫 → 等待 → 切无过滤全频段扫描。 + // 微信 services 过滤只按 128 位完整 UUID 匹配广播包,16 位短 UUID 设备 + // 会被过滤掉,必须靠兜底全扫 + JS 层 matchTargetDevice 兜住 + async function fallbackToUnfilteredDiscoveryIfNeeded(sessionId) { + clearFallbackTimer() + + filterFallbackTimer = setTimeout(async () => { + if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId || hasTargetDeviceInMap()) { + return + } + + try { + console.debug('[BLE][SCAN]', 'filtered scan found nothing, fallback to unfiltered') + await stopDiscoverySilently() + await delay(RESTART_SCAN_DELAY_MS) + + if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) { + return + } + + await startDiscoveryWithServices([]) + } catch (error) { + } + }, FILTER_SCAN_TIMEOUT_MS) + } + /** - * 设备搜索:先无过滤全频段扫描(onBluetoothDeviceFound 事件驱动 + getBluetoothDevices 轮询双通道), - * 命中关键词/服务 UUID 即入列。微信的 services 过滤只匹配 128 位完整 UUID, - * 对 16 位广播 UUID(0x00F4 等)的设备经常过滤过头导致搜不到,故不再走过滤扫描。 + * 设备搜索: + * 优先按 0xFFF4/0x00F4/0xF400 过滤扫描(快、列表干净); + * 4s 无结果自动切到无过滤全扫(兜住 16 位短 UUID 设备)并轮询 getBluetoothDevices。 */ async function startSearch() { if (isSearching) { @@ -606,6 +661,7 @@ export function createBluetoothDiscovery(options = {}) { // 立即标记搜索中,防止 await 期间用户重复点击或 stopSearch 被覆盖 isSearching = true onSearchingChange(true) + clearFallbackTimer() clearDiscoveryPollTimer() clearAutoStopTimer() @@ -634,13 +690,21 @@ export function createBluetoothDiscovery(options = {}) { acceptingDeviceEvents = true - await startDiscoveryWithServices([]) + try { + await startDiscoveryWithServices(DISCOVERY_FILTER_SERVICE_UUIDS) + console.debug('[BLE][SCAN]', 'filtered discovery started, sessionId=', sessionId) + } catch (startError) { + console.debug('[BLE][SCAN]', 'filtered start failed, retry unfiltered', startError) + await startDiscoveryWithServices([]) + console.debug('[BLE][SCAN]', 'unfiltered discovery started (after filter fail), sessionId=', sessionId) + } if (sessionId !== searchSessionId) { return } startDiscoveryPolling(sessionId) + await fallbackToUnfilteredDiscoveryIfNeeded(sessionId) // 30s 自动停止搜索 autoStopTimer = setTimeout(() => { @@ -658,6 +722,7 @@ export function createBluetoothDiscovery(options = {}) { } async function stopSearch(showStopToast = true) { + clearFallbackTimer() clearDiscoveryPollTimer() clearAutoStopTimer() @@ -863,6 +928,7 @@ export function createBluetoothDiscovery(options = {}) { } function cleanupDiscovery() { + clearFallbackTimer() clearDiscoveryPollTimer() clearDeviceListEmitTimer() diff --git a/src/constants/bluetooth.js b/src/constants/bluetooth.js index 9876619..4984659 100644 --- a/src/constants/bluetooth.js +++ b/src/constants/bluetooth.js @@ -16,6 +16,16 @@ export const TARGET_SHORT_UUID_CANDIDATES = Object.freeze([ TARGET_SERVICE_UUID_COMPAT ]) +/** + * 过滤扫描用的 128 位完整 UUID 列表(与 advertisServiceUUIDs 匹配主服务)。 + * 微信 services 过滤只认完整 UUID;广播 16 位短 UUID 的机型由无过滤兜底全扫 + JS 层匹配覆盖。 + */ +export const DISCOVERY_FILTER_SERVICE_UUIDS = Object.freeze([ + TARGET_SERVICE_UUID_COMPAT_FULL, + TARGET_SERVICE_UUID_FULL, + TARGET_SERVICE_UUID_ALT_FULL +]) + /** * 蓝牙常见错误提示映射。 */