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 +]) + /** * 蓝牙常见错误提示映射。 */