revert(ble): 回滚到迁移完成基线,隔离真机搜索问题成因

- 逐一尝试 09-09/09-10 的节流、全频段扫描、过滤恢复+排障日志及
  ble-config 超时兜底等改动后真机仍搜不到设备,整体 checkout 回
  8494285(Taro 迁移收尾提交)基线
- discovery.js 恢复过滤扫描优先+4s 兜底全扫原实现,移除 UI 刷新节流
  与排障日志,stopSearch 恢复先停原生再翻状态的原顺序
- ble-config.tsx 恢复初版:仅 useEffect 卸载清理,无超时兜底与
  失败补断开
- docs/unified-ble-flow.md 补记回滚决策与目的
This commit is contained in:
ozh 2026-09-14 14:56:31 +08:00
parent 3f52b493e0
commit 51747499ed
4 changed files with 30 additions and 122 deletions

View File

@ -132,6 +132,14 @@ async function runStandardFlow({ key, ssid, password }) {
## 6. 修改记录 ## 6. 修改记录
### 2026-09-11 回滚到迁移完成基线(真机仍搜不到设备)
- 2026-09-09/09-10 的全部搜索策略改动(`ac4cb3b` 节流、`01fefe3` 全频段扫描、`2238e96` 恢复过滤+排障日志)与 `58d0ee8`(ble-config 页 useDidHide/超时兜底/safeDisconnect)逐一尝试后真机仍无法搜出设备,`git checkout 8494285 --` 整体回滚到"Taro 迁移收尾提交"的基线状态:
- `src/ble/discovery.js`:恢复过滤扫描优先 + 4s 兜底全扫的原始实现,移除 300ms UI 刷新节流与 `[BLE][SCAN]`/`[BLE][FOUND]` 排障日志,`stopSearch` 恢复"先停原生扫描再翻转状态"的原顺序
- `src/constants/bluetooth.js`:恢复原注释(内容未变)
- `src/pages/device/ble-config.tsx`:恢复 `1f3c9c8` 初版——仅 useEffect 卸载清理(无 useDidHide)、`prepareWifiConfig` 无超时兜底、连接失败不补 disconnect
- 回滚后基线 = 迁移完成时用户尚未报告任何搜索问题的状态,用于隔离"迁移本身引入"与"后续修复引入"两类原因
### 2026-09-10 恢复过滤扫描优先 + 搜索排障日志 ### 2026-09-10 恢复过滤扫描优先 + 搜索排障日志
#### src/ble/discovery.js #### src/ble/discovery.js

View File

@ -11,14 +11,10 @@ import {DEVICE_PROFILE, createUnifiedBleController, shortUuid, uuidInList} from
import {showToast} from '@/utils/toast' import {showToast} from '@/utils/toast'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
const FILTER_SCAN_TIMEOUT_MS = 4000
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,目标设备每秒广播
// 数次,不节流会以每秒数十次 setState 打满小程序渲染线程,点击被排队卡死
const DEVICE_LIST_EMIT_INTERVAL_MS = 300
const TARGET_NAME_KEYWORDS = Object.freeze(['ED713', '713WQ', 'ED719', 'EP832']) const TARGET_NAME_KEYWORDS = Object.freeze(['ED713', '713WQ', 'ED719', 'EP832'])
function promisifyTaroApi(apiName, params = {}) { function promisifyTaroApi(apiName, params = {}) {
@ -81,10 +77,9 @@ export function createBluetoothDiscovery(options = {}) {
let searchSessionId = 0 let searchSessionId = 0
let acceptingDeviceEvents = false let acceptingDeviceEvents = false
let filterFallbackTimer = null
let discoveryPollTimer = null let discoveryPollTimer = null
let autoStopTimer = null let autoStopTimer = null
let deviceListEmitTimer = null
let filterFallbackTimer = null
let offDeviceFound = null let offDeviceFound = null
let offAdapterState = null let offAdapterState = null
@ -121,41 +116,14 @@ export function createBluetoothDiscovery(options = {}) {
onDeviceListChange(getSortedDeviceList()) onDeviceListChange(getSortedDeviceList())
} }
// 广播包持续到达时(RSSI 每包都变,签名判重挡不住),节流 UI 刷新;
// 数据仍实时合入 deviceMap,只是通知 UI 的频率被压到每 300ms 一次。
// 末包到达后补一次尾部刷新,保证最后一帧数据不丢
function emitDeviceListThrottled() {
if (deviceListEmitTimer) {
return
}
deviceListEmitTimer = setTimeout(() => {
deviceListEmitTimer = null
emitDeviceList()
}, DEVICE_LIST_EMIT_INTERVAL_MS)
}
function clearDeviceListEmitTimer() {
if (!deviceListEmitTimer) {
return
}
clearTimeout(deviceListEmitTimer)
deviceListEmitTimer = null
}
function setDeviceMap(nextMap) { function setDeviceMap(nextMap) {
deviceMap = nextMap deviceMap = nextMap
onDeviceMapChange({...deviceMap}) onDeviceMapChange({...deviceMap})
emitDeviceListThrottled() emitDeviceList()
} }
function resetDeviceMap() { function resetDeviceMap() {
clearDeviceListEmitTimer() setDeviceMap({})
deviceMap = {}
onDeviceMapChange({...deviceMap})
// 重置列表必须立即通知:清空操作不能等 300ms 节流窗口
emitDeviceList()
} }
function normalizeUuid(uuid) { function normalizeUuid(uuid) {
@ -273,6 +241,10 @@ export function createBluetoothDiscovery(options = {}) {
} }
} }
function hasTargetDeviceInMap() {
return Object.values(deviceMap).some((item) => item.isTargetDevice)
}
function getDeviceStableSignature(device = {}) { function getDeviceStableSignature(device = {}) {
return [ return [
device.deviceId || '', device.deviceId || '',
@ -286,10 +258,6 @@ export function createBluetoothDiscovery(options = {}) {
].join('#') ].join('#')
} }
function hasTargetDeviceInMap() {
return Object.values(deviceMap).some((item) => item.isTargetDevice)
}
function clearFallbackTimer() { function clearFallbackTimer() {
if (!filterFallbackTimer) { if (!filterFallbackTimer) {
return return
@ -367,12 +335,12 @@ export function createBluetoothDiscovery(options = {}) {
function upsertDevice(rawDevice) { function upsertDevice(rawDevice) {
if (!rawDevice || !rawDevice.deviceId) { if (!rawDevice || !rawDevice.deviceId) {
return false return
} }
const matched = matchTargetDevice(rawDevice) const matched = matchTargetDevice(rawDevice)
if (!matched.isTarget) { if (!matched.isTarget) {
return false return
} }
const existing = deviceMap[rawDevice.deviceId] || {} const existing = deviceMap[rawDevice.deviceId] || {}
@ -391,14 +359,13 @@ export function createBluetoothDiscovery(options = {}) {
const nextSignature = getDeviceStableSignature(merged) const nextSignature = getDeviceStableSignature(merged)
if (prevSignature === nextSignature) { if (prevSignature === nextSignature) {
return false return
} }
setDeviceMap({ setDeviceMap({
...deviceMap, ...deviceMap,
[rawDevice.deviceId]: merged [rawDevice.deviceId]: merged
}) })
return true
} }
function getBluetoothErrorText(error) { function getBluetoothErrorText(error) {
@ -433,19 +400,13 @@ 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
} }
if (upsertDevice(device)) { 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) => {
@ -586,18 +547,13 @@ 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
} }
if (upsertDevice(device)) { upsertDevice(device)
hitCount += 1
}
}) })
console.debug('[BLE][SCAN]', 'native devices=', list.length, 'newHits=', hitCount)
} catch (error) { } catch (error) {
} }
} }
@ -622,9 +578,6 @@ export function createBluetoothDiscovery(options = {}) {
discoveryPollTimer = setTimeout(run, 300) discoveryPollTimer = setTimeout(run, 300)
} }
// 过滤扫描超过 4s 仍无目标设备:停扫 → 等待 → 切无过滤全频段扫描。
// 微信 services 过滤只按 128 位完整 UUID 匹配广播包,16 位短 UUID 设备
// 会被过滤掉,必须靠兜底全扫 + JS 层 matchTargetDevice 兜住
async function fallbackToUnfilteredDiscoveryIfNeeded(sessionId) { async function fallbackToUnfilteredDiscoveryIfNeeded(sessionId) {
clearFallbackTimer() clearFallbackTimer()
@ -634,7 +587,6 @@ export function createBluetoothDiscovery(options = {}) {
} }
try { try {
console.debug('[BLE][SCAN]', 'filtered scan found nothing, fallback to unfiltered')
await stopDiscoverySilently() await stopDiscoverySilently()
await delay(RESTART_SCAN_DELAY_MS) await delay(RESTART_SCAN_DELAY_MS)
@ -650,8 +602,7 @@ export function createBluetoothDiscovery(options = {}) {
/** /**
* 设备搜索: * 设备搜索:
* 优先按 0xFFF4/0x00F4/0xF400 过滤扫描(快、列表干净); * 优先按 0xFFF4/0x00F4/0xF400 过滤;若无结果自动切到兜底扫描并轮询 getBluetoothDevices。
* 4s 无结果自动切到无过滤全扫(兜住 16 位短 UUID 设备)并轮询 getBluetoothDevices。
*/ */
async function startSearch() { async function startSearch() {
if (isSearching) { if (isSearching) {
@ -692,11 +643,8 @@ export function createBluetoothDiscovery(options = {}) {
try { try {
await startDiscoveryWithServices(DISCOVERY_FILTER_SERVICE_UUIDS) await startDiscoveryWithServices(DISCOVERY_FILTER_SERVICE_UUIDS)
console.debug('[BLE][SCAN]', 'filtered discovery started, sessionId=', sessionId) } catch (error) {
} catch (startError) {
console.debug('[BLE][SCAN]', 'filtered start failed, retry unfiltered', startError)
await startDiscoveryWithServices([]) await startDiscoveryWithServices([])
console.debug('[BLE][SCAN]', 'unfiltered discovery started (after filter fail), sessionId=', sessionId)
} }
if (sessionId !== searchSessionId) { if (sessionId !== searchSessionId) {
@ -729,20 +677,13 @@ export function createBluetoothDiscovery(options = {}) {
searchSessionId += 1 searchSessionId += 1
acceptingDeviceEvents = false acceptingDeviceEvents = false
// 先同步翻转搜索态,再异步停原生扫描: await stopDiscoverySilently()
// stopBluetoothDevicesDiscovery 在部分机型回调慢,若等它完成才 setSearching(false),
// 期间按钮仍显示"搜索中"、连接/二次搜索等后续操作全部被串行拖住(感知为卡顿)。 // 暂停搜索保留已发现的设备数据,不做 resetDeviceMap
// searchSessionId 已递增、事件已关闭,翻转状态不会引入新的扫描事件
setSearching(false) setSearching(false)
if (showStopToast) { if (showStopToast) {
showToast('已停止搜索') showToast('已停止搜索')
} }
await stopDiscoverySilently()
// 停搜后立即冲刷最后一帧列表,避免 pending 的节流定时器延迟刷新
clearDeviceListEmitTimer()
emitDeviceList()
} }
function pickPreferredDeviceId(deviceId) { function pickPreferredDeviceId(deviceId) {
@ -776,11 +717,7 @@ export function createBluetoothDiscovery(options = {}) {
async function connectDevice(deviceId) { async function connectDevice(deviceId) {
await ensureInitialized() await ensureInitialized()
// 搜索进行中点击"网络配置":立即停搜索(状态同步翻转,不等原生回调),
// 否则 createBLEConnection 会被扫描信道占用拖慢甚至失败
if (isSearching) {
await stopSearch(false) await stopSearch(false)
}
const targetDeviceId = pickPreferredDeviceId(deviceId) const targetDeviceId = pickPreferredDeviceId(deviceId)
if (!targetDeviceId) { if (!targetDeviceId) {
@ -930,7 +867,6 @@ export function createBluetoothDiscovery(options = {}) {
function cleanupDiscovery() { function cleanupDiscovery() {
clearFallbackTimer() clearFallbackTimer()
clearDiscoveryPollTimer() clearDiscoveryPollTimer()
clearDeviceListEmitTimer()
searchSessionId += 1 searchSessionId += 1
acceptingDeviceEvents = false acceptingDeviceEvents = false

View File

@ -16,10 +16,6 @@ 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([ export const DISCOVERY_FILTER_SERVICE_UUIDS = Object.freeze([
TARGET_SERVICE_UUID_COMPAT_FULL, TARGET_SERVICE_UUID_COMPAT_FULL,
TARGET_SERVICE_UUID_FULL, TARGET_SERVICE_UUID_FULL,

View File

@ -1,35 +1,20 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Taro, { useDidHide } from '@tarojs/taro' import Taro from '@tarojs/taro'
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components'
import { Navbar } from '@/components/navbar' import { Navbar } from '@/components/navbar'
import { useBle } from '@/ble/hooks/useBle' import { useBle } from '@/ble/hooks/useBle'
import { navigateTo } from '@/utils/navigation' import { navigateTo } from '@/utils/navigation'
import { ROUTES } from '@/constants/routes' import { ROUTES } from '@/constants/routes'
import { getErrorMessage } from '@/utils/error' import { getErrorMessage } from '@/utils/error'
import { showToast } from '@/utils/toast'
import { DEVICE_ACTION_KEYS, ACTION_REGISTRY, isEp832Device } from '@/constants/deviceActions' import { DEVICE_ACTION_KEYS, ACTION_REGISTRY, isEp832Device } from '@/constants/deviceActions'
import type { BleDevice } from '@/ble/hooks/useBle' import type { BleDevice } from '@/ble/hooks/useBle'
import styles from './ble-config.module.scss' import styles from './ble-config.module.scss'
// createBLEConnection 最多 3 次尝试(15s 超时 + 1.2s 重试间隔)约 47s,
// 兜底覆盖 discoverServices/notify 无原生超时的场景,保证 loading 遮罩必然撤除
const PREPARE_WIFI_TIMEOUT_MS = 60000
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(message)), ms)
})
])
}
export default function DeviceBleConfig() { export default function DeviceBleConfig() {
const { state, discovery } = useBle() const { state, discovery } = useBle()
const [preparingId, setPreparingId] = useState('') const [preparingId, setPreparingId] = useState('')
// 页面隐藏(navigateTo 配网页/切后台)与卸载(返回)时停搜索,连接保持供配网页使用。 // 页面隐藏/卸载时清理搜索(对齐旧版 onHide/onUnload),连接保持供配网页使用
// useEffect 的清理只在卸载触发,压栈后台时组件不卸载,必须补 useDidHide(对齐旧版 onHide/onUnload)
useEffect(() => { useEffect(() => {
return () => { return () => {
if (discovery.getState().isSearching) { if (discovery.getState().isSearching) {
@ -39,12 +24,6 @@ export default function DeviceBleConfig() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useDidHide(() => {
if (discovery.getState().isSearching) {
discovery.stopSearch(false)
}
})
const startScan = async () => { const startScan = async () => {
if (state.searching) { if (state.searching) {
await discovery.stopSearch() await discovery.stopSearch()
@ -67,19 +46,10 @@ export default function DeviceBleConfig() {
let prepError: unknown = null let prepError: unknown = null
try { try {
// createBLEConnection 部分机型失败后不回调(微信基础库缺陷), await discovery.prepareWifiConfig(deviceId)
// discoverServices/notify 也无原生超时;不设兜底会导致 loading 永不消失、按钮永久失效
await withTimeout(
discovery.prepareWifiConfig(deviceId),
PREPARE_WIFI_TIMEOUT_MS,
`连接设备超时(${PREPARE_WIFI_TIMEOUT_MS / 1000}s),请重试`
)
navigateTo(ROUTES.DEVICE_CONFIG) navigateTo(ROUTES.DEVICE_CONFIG)
} catch (error) { } catch (error) {
prepError = error prepError = error
// 对齐旧版 safeDisconnect:连接半途失败时清掉 OS 层残留连接,
// 否则后续重试会被"已连接(10010)"错误卡死
discovery.disconnectDevice().catch(() => {})
} finally { } finally {
setPreparingId('') setPreparingId('')
// showLoading 与 showToast 共用同一遮罩层,必须先 hideLoading 再弹提示 // showLoading 与 showToast 共用同一遮罩层,必须先 hideLoading 再弹提示
@ -94,8 +64,6 @@ export default function DeviceBleConfig() {
showCancel: false, showCancel: false,
confirmText: '知道了' confirmText: '知道了'
}) })
} else {
showToast('设备已连接,请填写WiFi信息')
} }
} }