fix(ble): 恢复过滤扫描优先+4s兜底全扫,并加搜索排障日志

真机验证"直接全频段扫描"方案后设备反而搜不出(修改前过滤扫描可搜到),
说明该机型下无过滤扫描路径存在问题(可能为机型对无 services 参数的
startBluetoothDevicesDiscovery 兼容差异)。回退为迁移原版策略:

- 恢复 DISCOVERY_FILTER_SERVICE_UUIDS 常量与 fallbackToUnfilteredDiscoveryIfNeeded
  兜底机制:先按 0xFFF4/0x00F4/0xF400 过滤扫描,4s 无结果自动切全扫
- upsertDevice 改为返回布尔值,event/poll 两条通道均打印排障日志
  ([BLE][SCAN] 原生采集数与命中数、[BLE][FOUND] 事件命中数、
  启动/兜底切换日志),真机 vConsole 可直接观察各阶段状态
This commit is contained in:
ozh 2026-09-10 11:11:30 +08:00
parent 5e5cd2bf47
commit 2238e965b9
2 changed files with 85 additions and 9 deletions

View File

@ -1,6 +1,7 @@
import { import {
BLUETOOTH_ERROR_MESSAGES, BLUETOOTH_ERROR_MESSAGES,
DEFAULT_BLUETOOTH_ERROR_MESSAGE, DEFAULT_BLUETOOTH_ERROR_MESSAGE,
DISCOVERY_FILTER_SERVICE_UUIDS,
TARGET_SERVICE_UUID_FULL, TARGET_SERVICE_UUID_FULL,
TARGET_SERVICE_UUID_ALT_FULL, TARGET_SERVICE_UUID_ALT_FULL,
TARGET_SERVICE_UUID_COMPAT_FULL, TARGET_SERVICE_UUID_COMPAT_FULL,
@ -13,6 +14,8 @@ import Taro from '@tarojs/taro'
const RESTART_SCAN_DELAY_MS = 120 const RESTART_SCAN_DELAY_MS = 120
const DISCOVERY_POLL_INTERVAL_MS = 1200 const DISCOVERY_POLL_INTERVAL_MS = 1200
const SCAN_DURATION_MS = 30000 const SCAN_DURATION_MS = 30000
// 过滤扫描无结果多久后切到无过滤全扫
const FILTER_SCAN_TIMEOUT_MS = 4000
// 设备列表 UI 刷新最小间隔:搜索中 allowDuplicatesKey=true,目标设备每秒广播 // 设备列表 UI 刷新最小间隔:搜索中 allowDuplicatesKey=true,目标设备每秒广播
// 数次,不节流会以每秒数十次 setState 打满小程序渲染线程,点击被排队卡死 // 数次,不节流会以每秒数十次 setState 打满小程序渲染线程,点击被排队卡死
const DEVICE_LIST_EMIT_INTERVAL_MS = 300 const DEVICE_LIST_EMIT_INTERVAL_MS = 300
@ -81,6 +84,7 @@ export function createBluetoothDiscovery(options = {}) {
let discoveryPollTimer = null let discoveryPollTimer = null
let autoStopTimer = null let autoStopTimer = null
let deviceListEmitTimer = null let deviceListEmitTimer = null
let filterFallbackTimer = null
let offDeviceFound = null let offDeviceFound = null
let offAdapterState = null let offAdapterState = null
@ -282,6 +286,19 @@ export function createBluetoothDiscovery(options = {}) {
].join('#') ].join('#')
} }
function hasTargetDeviceInMap() {
return Object.values(deviceMap).some((item) => item.isTargetDevice)
}
function clearFallbackTimer() {
if (!filterFallbackTimer) {
return
}
clearTimeout(filterFallbackTimer)
filterFallbackTimer = null
}
function clearDiscoveryPollTimer() { function clearDiscoveryPollTimer() {
if (!discoveryPollTimer) { if (!discoveryPollTimer) {
return return
@ -350,12 +367,12 @@ export function createBluetoothDiscovery(options = {}) {
function upsertDevice(rawDevice) { function upsertDevice(rawDevice) {
if (!rawDevice || !rawDevice.deviceId) { if (!rawDevice || !rawDevice.deviceId) {
return return false
} }
const matched = matchTargetDevice(rawDevice) const matched = matchTargetDevice(rawDevice)
if (!matched.isTarget) { if (!matched.isTarget) {
return return false
} }
const existing = deviceMap[rawDevice.deviceId] || {} const existing = deviceMap[rawDevice.deviceId] || {}
@ -374,13 +391,14 @@ export function createBluetoothDiscovery(options = {}) {
const nextSignature = getDeviceStableSignature(merged) const nextSignature = getDeviceStableSignature(merged)
if (prevSignature === nextSignature) { if (prevSignature === nextSignature) {
return return false
} }
setDeviceMap({ setDeviceMap({
...deviceMap, ...deviceMap,
[rawDevice.deviceId]: merged [rawDevice.deviceId]: merged
}) })
return true
} }
function getBluetoothErrorText(error) { function getBluetoothErrorText(error) {
@ -415,13 +433,19 @@ export function createBluetoothDiscovery(options = {}) {
} }
const currentSession = searchSessionId const currentSession = searchSessionId
let hitCount = 0
devices.forEach((device) => { devices.forEach((device) => {
if (!acceptingDeviceEvents || currentSession !== searchSessionId) { if (!acceptingDeviceEvents || currentSession !== searchSessionId) {
return 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) => { offAdapterState = controller.ble.on('adapter:state', (state) => {
@ -562,13 +586,18 @@ export function createBluetoothDiscovery(options = {}) {
try { try {
const result = await promisifyTaroApi('getBluetoothDevices') const result = await promisifyTaroApi('getBluetoothDevices')
const list = Array.isArray(result?.devices) ? result.devices : [] const list = Array.isArray(result?.devices) ? result.devices : []
// 排障日志:原生层到底采到了多少设备、命中了多少目标(真机 vConsole 可见)
let hitCount = 0
list.forEach((device) => { list.forEach((device) => {
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) { if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) {
return return
} }
upsertDevice(device) if (upsertDevice(device)) {
hitCount += 1
}
}) })
console.debug('[BLE][SCAN]', 'native devices=', list.length, 'newHits=', hitCount)
} catch (error) { } catch (error) {
} }
} }
@ -593,10 +622,36 @@ export function createBluetoothDiscovery(options = {}) {
discoveryPollTimer = setTimeout(run, 300) 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, * 优先按 0xFFF4/0x00F4/0xF400 过滤扫描(快、列表干净);
* 对 16 位广播 UUID(0x00F4 等)的设备经常过滤过头导致搜不到,故不再走过滤扫描。 * 4s 无结果自动切到无过滤全扫(兜住 16 位短 UUID 设备)并轮询 getBluetoothDevices。
*/ */
async function startSearch() { async function startSearch() {
if (isSearching) { if (isSearching) {
@ -606,6 +661,7 @@ export function createBluetoothDiscovery(options = {}) {
// 立即标记搜索中,防止 await 期间用户重复点击或 stopSearch 被覆盖 // 立即标记搜索中,防止 await 期间用户重复点击或 stopSearch 被覆盖
isSearching = true isSearching = true
onSearchingChange(true) onSearchingChange(true)
clearFallbackTimer()
clearDiscoveryPollTimer() clearDiscoveryPollTimer()
clearAutoStopTimer() clearAutoStopTimer()
@ -634,13 +690,21 @@ export function createBluetoothDiscovery(options = {}) {
acceptingDeviceEvents = true 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) { if (sessionId !== searchSessionId) {
return return
} }
startDiscoveryPolling(sessionId) startDiscoveryPolling(sessionId)
await fallbackToUnfilteredDiscoveryIfNeeded(sessionId)
// 30s 自动停止搜索 // 30s 自动停止搜索
autoStopTimer = setTimeout(() => { autoStopTimer = setTimeout(() => {
@ -658,6 +722,7 @@ export function createBluetoothDiscovery(options = {}) {
} }
async function stopSearch(showStopToast = true) { async function stopSearch(showStopToast = true) {
clearFallbackTimer()
clearDiscoveryPollTimer() clearDiscoveryPollTimer()
clearAutoStopTimer() clearAutoStopTimer()
@ -863,6 +928,7 @@ export function createBluetoothDiscovery(options = {}) {
} }
function cleanupDiscovery() { function cleanupDiscovery() {
clearFallbackTimer()
clearDiscoveryPollTimer() clearDiscoveryPollTimer()
clearDeviceListEmitTimer() clearDeviceListEmitTimer()

View File

@ -16,6 +16,16 @@ export const TARGET_SHORT_UUID_CANDIDATES = Object.freeze([
TARGET_SERVICE_UUID_COMPAT 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
])
/** /**
* 蓝牙常见错误提示映射。 * 蓝牙常见错误提示映射。
*/ */