fix(ble): 成功搜索到ED719设备

This commit is contained in:
ozh 2026-04-17 17:17:39 +08:00
parent 640772ef0c
commit fd617947f1
1 changed files with 526 additions and 80 deletions

View File

@ -6,55 +6,98 @@ import {
TARGET_SERVICE_UUID_ALT_FULL, TARGET_SERVICE_UUID_ALT_FULL,
TARGET_SHORT_UUID TARGET_SHORT_UUID
} from '@/constants/bluetooth' } from '@/constants/bluetooth'
import { DEVICE_PROFILE, createUnifiedBleController } from '@/utils/ble' import { DEVICE_PROFILE, createUnifiedBleController, shortUuid, uuidInList } from '@/utils/ble'
import { showToast } from '@/utils/toast' import { showToast } from '@/utils/toast'
const FILTER_SCAN_TIMEOUT_MS = 4000 const FILTER_SCAN_TIMEOUT_MS = 4000
const RESTART_SCAN_DELAY_MS = 120
const DISCOVERY_POLL_INTERVAL_MS = 1200
const TARGET_NAME_KEYWORDS = Object.freeze(['ED713', 'ED719'])
function promisifyUniApi(apiName, params = {}) {
return new Promise((resolve, reject) => {
uni[apiName]({
...params,
success: (res) => resolve(res),
fail: (err) => reject(err)
})
})
}
/** /**
* 蓝牙设备搜索逻辑封装 * 统一蓝牙接入层ED713/ED719
* 基于 utils/ble 统一蓝牙控制器实现 * 1) 搜索过滤去重可停止
* 2) 连接 + 服务/特征发现 + 订阅
* 3) 鉴权读取设备信息WiFi 配置雷达控制
*/ */
export function createBluetoothDiscovery(options = {}) { export function createBluetoothDiscovery(options = {}) {
const { const {
onSearchingChange = () => {}, onSearchingChange = () => {},
onDeviceMapChange = () => {}, onDeviceMapChange = () => {},
onDeviceListChange = () => {} onDeviceListChange = () => {},
onConnectedChange = () => {},
onProfileChange = () => {},
onStageChange = () => {},
onProtocolState = () => {},
onProtocolMsg = () => {},
onRadarData = () => {},
onDeviceInfoChange = () => {},
onAuthChange = () => {},
onError = () => {}
} = options } = options
const controller = createUnifiedBleController({ const controller = createUnifiedBleController({
profileId: DEVICE_PROFILE.UNKNOWN profileId: DEVICE_PROFILE.UNKNOWN
}) })
let initialized = false
let isSearching = false let isSearching = false
let isConnected = false
let deviceMap = {} let deviceMap = {}
let initialized = false let currentDeviceId = ''
let filterFallbackTimer = null let currentProfileId = DEVICE_PROFILE.UNKNOWN
let lastDeviceInfo = null
let offDeviceFound = null let lastAuthResult = null
let offAdapterState = null
let searchSessionId = 0 let searchSessionId = 0
let acceptingDeviceEvents = false let acceptingDeviceEvents = false
let filterFallbackTimer = null
let discoveryPollTimer = null
let offDeviceFound = null
let offAdapterState = null
let offConnectionChange = null
let offStateChange = null
let offProtocolState = null
let offProtocolMsg = null
let offProtocolRadar = null
let offError = null
function setSearching(value) { function setSearching(value) {
isSearching = Boolean(value) isSearching = Boolean(value)
onSearchingChange(isSearching) onSearchingChange(isSearching)
} }
function emitDeviceList() { function setConnected(value) {
const list = Object.values(deviceMap).sort((a, b) => { isConnected = Boolean(value)
if (a.hasTargetService !== b.hasTargetService) { onConnectedChange(isConnected)
return a.hasTargetService ? -1 : 1
} }
const aSignal = typeof a.RSSI === 'number' ? a.RSSI : -999 function getSortedDeviceList(mapValue = deviceMap) {
const bSignal = typeof b.RSSI === 'number' ? b.RSSI : -999 return Object.values(mapValue).sort((left, right) => {
return bSignal - aSignal if (left.hasTargetService !== right.hasTargetService) {
}) return left.hasTargetService ? -1 : 1
}
onDeviceListChange(list) const leftSignal = typeof left.RSSI === 'number' ? left.RSSI : -999
const rightSignal = typeof right.RSSI === 'number' ? right.RSSI : -999
return rightSignal - leftSignal
})
}
function emitDeviceList() {
onDeviceListChange(getSortedDeviceList())
} }
function setDeviceMap(nextMap) { function setDeviceMap(nextMap) {
@ -71,29 +114,117 @@ export function createBluetoothDiscovery(options = {}) {
return String(uuid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase() return String(uuid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
} }
function inferProfileByName(device) {
const text = `${device?.name || ''} ${device?.localName || ''}`.toUpperCase()
if (text.includes('ED719')) {
return DEVICE_PROFILE.ED719
}
if (text.includes('ED713')) {
return DEVICE_PROFILE.ED713
}
return DEVICE_PROFILE.UNKNOWN
}
/**
* 从广播包中解析 16-bit service UUIDAD Type 0x02/0x03
*/
function extractShortUuidsFromAdvertisData(advertisData) {
if (!advertisData) {
return []
}
const bytes = advertisData instanceof Uint8Array ? advertisData : new Uint8Array(advertisData)
const shortUuids = []
let offset = 0
while (offset < bytes.length) {
const len = bytes[offset]
if (!len) {
break
}
const typeIndex = offset + 1
const dataStart = offset + 2
const dataEnd = offset + 1 + len
if (dataEnd > bytes.length) {
break
}
const type = bytes[typeIndex]
if (type === 0x02 || type === 0x03) {
for (let index = dataStart; index + 1 < dataEnd; index += 2) {
const value = ((bytes[index + 1] << 8) | bytes[index]).toString(16).toUpperCase().padStart(4, '0')
shortUuids.push(value)
}
}
offset += len + 1
}
return shortUuids
}
function hasTargetService(device) { function hasTargetService(device) {
if (!Array.isArray(device.advertisServiceUUIDs)) { const serviceList = Array.isArray(device?.advertisServiceUUIDs) ? device.advertisServiceUUIDs : []
const adShortUuids = extractShortUuidsFromAdvertisData(device?.advertisData)
if (!serviceList.length && !adShortUuids.length) {
return false return false
} }
const serviceSet = new Set([ return uuidInList(TARGET_SERVICE_UUID_FULL, serviceList)
normalizeUuid(TARGET_SERVICE_UUID_FULL), || uuidInList(TARGET_SERVICE_UUID_ALT_FULL, serviceList)
normalizeUuid(TARGET_SERVICE_UUID_ALT_FULL) || serviceList.some((item) => shortUuid(item) === TARGET_SHORT_UUID)
]) || adShortUuids.includes(TARGET_SHORT_UUID)
|| adShortUuids.includes('F400')
return device.advertisServiceUUIDs.some((uuid) => {
const value = normalizeUuid(uuid)
if (value.endsWith(TARGET_SHORT_UUID)) {
return true
} }
return serviceSet.has(value) function bufferToPrintableText(buffer) {
}) if (!buffer) {
return ''
}
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
return Array.from(bytes)
.map((value) => (value >= 32 && value <= 126 ? String.fromCharCode(value) : ' '))
.join(' ')
.toUpperCase()
}
function hasTargetKeywordText(text) {
return TARGET_NAME_KEYWORDS.some((keyword) => text.includes(keyword))
}
function hasTargetName(device) {
const text = `${String(device?.name || '')} ${String(device?.localName || '')}`.toUpperCase()
return hasTargetKeywordText(text)
}
function hasTargetKeywordInPayload(device) {
const adText = bufferToPrintableText(device?.advertisData)
const manufacturerText = bufferToPrintableText(device?.manufacturerData)
return hasTargetKeywordText(`${adText} ${manufacturerText}`)
}
function matchTargetDevice(device) {
const byService = hasTargetService(device)
const byName = hasTargetName(device)
const byPayload = hasTargetKeywordInPayload(device)
return {
isTarget: byService || byName || byPayload,
byService,
byName,
byPayload
}
} }
function hasTargetDeviceInMap() { function hasTargetDeviceInMap() {
return Object.values(deviceMap).some((item) => item.hasTargetService) return Object.values(deviceMap).some((item) => item.isTargetDevice)
} }
function getDeviceStableSignature(device = {}) { function getDeviceStableSignature(device = {}) {
@ -102,7 +233,9 @@ export function createBluetoothDiscovery(options = {}) {
device.RSSI ?? '', device.RSSI ?? '',
device.name || '', device.name || '',
device.localName || '', device.localName || '',
Array.isArray(device.advertisServiceUUIDs) ? device.advertisServiceUUIDs.map((item) => normalizeUuid(item)).sort().join('|') : '', Array.isArray(device.advertisServiceUUIDs)
? device.advertisServiceUUIDs.map((item) => normalizeUuid(item)).sort().join('|')
: '',
device.advertisData ? JSON.stringify(Array.from(new Uint8Array(device.advertisData))) : '' device.advertisData ? JSON.stringify(Array.from(new Uint8Array(device.advertisData))) : ''
].join('#') ].join('#')
} }
@ -116,6 +249,15 @@ export function createBluetoothDiscovery(options = {}) {
filterFallbackTimer = null filterFallbackTimer = null
} }
function clearDiscoveryPollTimer() {
if (!discoveryPollTimer) {
return
}
clearTimeout(discoveryPollTimer)
discoveryPollTimer = null
}
function delay(ms) { function delay(ms) {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(resolve, ms) setTimeout(resolve, ms)
@ -144,9 +286,7 @@ export function createBluetoothDiscovery(options = {}) {
}) })
}) })
const authSetting = settings.authSetting || {} const locationSetting = settings?.authSetting?.['scope.userLocation']
const locationSetting = authSetting['scope.userLocation']
if (locationSetting === true) { if (locationSetting === true) {
return true return true
} }
@ -171,8 +311,8 @@ export function createBluetoothDiscovery(options = {}) {
return return
} }
const targetMatched = hasTargetService(rawDevice) const matched = matchTargetDevice(rawDevice)
if (!targetMatched) { if (!matched.isTarget) {
return return
} }
@ -181,7 +321,11 @@ export function createBluetoothDiscovery(options = {}) {
...existing, ...existing,
...rawDevice, ...rawDevice,
showName: rawDevice.name || rawDevice.localName || existing.showName || '未命名设备', showName: rawDevice.name || rawDevice.localName || existing.showName || '未命名设备',
hasTargetService: true isTargetDevice: true,
hasTargetService: matched.byService,
matchedByName: matched.byName,
matchedByPayload: matched.byPayload,
profileHint: inferProfileByName(rawDevice)
} }
const prevSignature = getDeviceStableSignature(existing) const prevSignature = getDeviceStableSignature(existing)
@ -197,13 +341,30 @@ export function createBluetoothDiscovery(options = {}) {
}) })
} }
async function ensureInitialized() { function getBluetoothErrorText(error) {
if (initialized) { const errCode = error?.errCode ?? error?.code
const text = BLUETOOTH_ERROR_MESSAGES[errCode] || DEFAULT_BLUETOOTH_ERROR_MESSAGE
if (errCode === undefined || errCode === null) {
return text
}
return `${text}(${errCode})`
}
function bindControllerEvents() {
if (offStateChange) {
return return
} }
await controller.init() offStateChange = controller.ble.on('state:change', (state) => {
controller.ble.watchDeviceFound() onStageChange(state.stage)
if (state.profileId && state.profileId !== currentProfileId) {
currentProfileId = state.profileId
onProfileChange(currentProfileId)
}
})
offDeviceFound = controller.ble.on('device:found', (devices) => { offDeviceFound = controller.ble.on('device:found', (devices) => {
if (!acceptingDeviceEvents) { if (!acceptingDeviceEvents) {
@ -221,23 +382,113 @@ export function createBluetoothDiscovery(options = {}) {
}) })
offAdapterState = controller.ble.on('adapter:state', (state) => { offAdapterState = controller.ble.on('adapter:state', (state) => {
if (!state || state.available !== false) { if (state?.available !== false) {
return return
} }
acceptingDeviceEvents = false acceptingDeviceEvents = false
setSearching(false) setSearching(false)
setConnected(false)
showToast('蓝牙已关闭,请先开启手机蓝牙') showToast('蓝牙已关闭,请先开启手机蓝牙')
}) })
offConnectionChange = controller.ble.on('connection:change', (payload) => {
setConnected(Boolean(payload?.connected))
if (!payload?.connected) {
currentDeviceId = ''
}
})
offProtocolState = controller.ble.on('protocol:state', (payload) => {
onProtocolState(payload)
})
offProtocolMsg = controller.ble.on('protocol:msg', (payload) => {
onProtocolMsg(payload)
if (payload?.deviceInfo) {
lastDeviceInfo = payload.deviceInfo
onDeviceInfoChange(lastDeviceInfo)
}
})
offProtocolRadar = controller.ble.on('protocol:radar', (payload) => {
const parsed = controller.radar.parseNotifyPayload(payload.value)
onRadarData({
...payload,
parsed
})
})
offError = controller.ble.on('error', (error) => {
onError(error)
})
}
function unbindControllerEvents() {
if (offDeviceFound) {
offDeviceFound()
offDeviceFound = null
}
if (offAdapterState) {
offAdapterState()
offAdapterState = null
}
if (offConnectionChange) {
offConnectionChange()
offConnectionChange = null
}
if (offStateChange) {
offStateChange()
offStateChange = null
}
if (offProtocolState) {
offProtocolState()
offProtocolState = null
}
if (offProtocolMsg) {
offProtocolMsg()
offProtocolMsg = null
}
if (offProtocolRadar) {
offProtocolRadar()
offProtocolRadar = null
}
if (offError) {
offError()
offError = null
}
}
async function ensureInitialized() {
if (initialized) {
return
}
await controller.init()
controller.ble.watchDeviceFound()
bindControllerEvents()
initialized = true initialized = true
currentProfileId = controller.ble.getProfile().id
onProfileChange(currentProfileId)
} }
async function startDiscoveryWithServices(services = []) { async function startDiscoveryWithServices(services = []) {
await controller.ble.startDiscovery({ await controller.ble.startDiscovery({
services, services,
allowDuplicatesKey: true, allowDuplicatesKey: true,
interval: 0 interval: 0,
// 兼容低版本客户端:不强制传 powerLevel。
powerLevel: null
}) })
} }
@ -248,6 +499,45 @@ export function createBluetoothDiscovery(options = {}) {
} }
} }
async function pullDiscoveredDevicesOnce(sessionId) {
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) {
return
}
try {
const result = await promisifyUniApi('getBluetoothDevices')
const list = Array.isArray(result?.devices) ? result.devices : []
list.forEach((device) => {
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) {
return
}
upsertDevice(device)
})
} catch (error) {
}
}
function startDiscoveryPolling(sessionId) {
clearDiscoveryPollTimer()
const run = async () => {
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) {
return
}
await pullDiscoveredDevicesOnce(sessionId)
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) {
return
}
discoveryPollTimer = setTimeout(run, DISCOVERY_POLL_INTERVAL_MS)
}
discoveryPollTimer = setTimeout(run, 300)
}
async function fallbackToUnfilteredDiscoveryIfNeeded(sessionId) { async function fallbackToUnfilteredDiscoveryIfNeeded(sessionId) {
clearFallbackTimer() clearFallbackTimer()
@ -258,7 +548,7 @@ export function createBluetoothDiscovery(options = {}) {
try { try {
await stopDiscoverySilently() await stopDiscoverySilently()
await delay(120) await delay(RESTART_SCAN_DELAY_MS)
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) { if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) {
return return
@ -270,23 +560,18 @@ export function createBluetoothDiscovery(options = {}) {
}, FILTER_SCAN_TIMEOUT_MS) }, FILTER_SCAN_TIMEOUT_MS)
} }
function getBluetoothErrorText(error) { /**
const errCode = (error && (error.errCode ?? error.code)) * 设备搜索
const text = BLUETOOTH_ERROR_MESSAGES[errCode] || DEFAULT_BLUETOOTH_ERROR_MESSAGE * 优先按 0x00F4/0xF400 过滤若无结果自动切到兜底扫描并轮询 getBluetoothDevices
*/
if (errCode === undefined || errCode === null) {
return text
}
return `${text}(${errCode})`
}
async function startSearch() { async function startSearch() {
if (isSearching) { if (isSearching) {
return return
} }
clearFallbackTimer() clearFallbackTimer()
clearDiscoveryPollTimer()
searchSessionId += 1 searchSessionId += 1
const sessionId = searchSessionId const sessionId = searchSessionId
@ -297,7 +582,7 @@ export function createBluetoothDiscovery(options = {}) {
await ensureInitialized() await ensureInitialized()
const adapterState = await controller.ble.getAdapterState() const adapterState = await controller.ble.getAdapterState()
if (adapterState && adapterState.available === false) { if (adapterState?.available === false) {
throw { errCode: 10001 } throw { errCode: 10001 }
} }
@ -316,6 +601,7 @@ export function createBluetoothDiscovery(options = {}) {
} }
setSearching(true) setSearching(true)
startDiscoveryPolling(sessionId)
await fallbackToUnfilteredDiscoveryIfNeeded(sessionId) await fallbackToUnfilteredDiscoveryIfNeeded(sessionId)
} catch (error) { } catch (error) {
acceptingDeviceEvents = false acceptingDeviceEvents = false
@ -325,54 +611,214 @@ export function createBluetoothDiscovery(options = {}) {
} }
} }
function stopSearch(showStopToast = true) { async function stopSearch(showStopToast = true) {
return new Promise((resolve) => {
clearFallbackTimer() clearFallbackTimer()
clearDiscoveryPollTimer()
searchSessionId += 1 searchSessionId += 1
acceptingDeviceEvents = false acceptingDeviceEvents = false
controller.ble.stopDiscovery() await stopDiscoverySilently()
.catch(() => {})
.finally(() => {
setSearching(false) setSearching(false)
if (showStopToast) { if (showStopToast) {
showToast('已停止搜索') showToast('已停止搜索')
} }
resolve() }
})
function pickPreferredDeviceId(deviceId) {
if (deviceId) {
return deviceId
}
const list = getSortedDeviceList()
return list[0]?.deviceId || ''
}
function applyProfileHintByDevice(deviceId) {
if (!deviceId) {
return
}
const hint = deviceMap[deviceId]?.profileHint
if (!hint || hint === DEVICE_PROFILE.UNKNOWN) {
return
}
const currentProfile = controller.ble.getProfile().id
if (currentProfile === DEVICE_PROFILE.UNKNOWN) {
controller.ble.setProfile(hint)
}
}
/**
* 仅连接与发现阶段连接 -> 发现服务0x00F4 -> 发现特征 -> 订阅 F301/F302
*/
async function connectDevice(deviceId) {
await ensureInitialized()
await stopSearch(false)
const targetDeviceId = pickPreferredDeviceId(deviceId)
if (!targetDeviceId) {
throw new Error('未找到可连接设备')
}
applyProfileHintByDevice(targetDeviceId)
const characteristics = await controller.connectAndDiscover(targetDeviceId)
currentDeviceId = targetDeviceId
setConnected(true)
return {
deviceId: targetDeviceId,
characteristics,
profile: controller.ble.getProfile(),
state: controller.ble.getState()
}
}
/**
* 鉴权流程严格遵循协议绑定逻辑读取 UUID_MSG bindStatus 后写 KEY
*/
async function authorizeDevice(key, options = {}) {
if (!currentDeviceId) {
throw new Error('请先连接设备后再鉴权')
}
const result = await controller.auth.ensureAuthorized(key, options)
lastAuthResult = result
onAuthChange(result)
return result
}
/**
* 标准连接流程连接 -> 发现 -> 订阅 -> 鉴权
*/
async function standardConnectFlow(params = {}) {
await ensureInitialized()
await stopSearch(false)
const targetDeviceId = pickPreferredDeviceId(params.deviceId)
if (!targetDeviceId) {
throw new Error('未找到可连接设备')
}
applyProfileHintByDevice(targetDeviceId)
const result = await controller.standardConnectFlow({
...params,
deviceId: targetDeviceId
}) })
currentDeviceId = result.deviceId
setConnected(true)
lastAuthResult = result.authResult
onAuthChange(result.authResult)
return result
}
async function readDeviceInfo() {
const info = await controller.auth.readDeviceInfo()
lastDeviceInfo = info
onDeviceInfoChange(info)
return info
}
async function configureWifi(ssid, password, options = {}) {
return controller.wifi.configureWifi(ssid, password, options)
}
async function startRadar() {
return controller.radar.startRadarStream()
}
async function stopRadar() {
return controller.radar.stopRadarStream()
}
async function setNarrowMode(enabled) {
return controller.radar.setNarrowMode(Boolean(enabled))
}
async function writeFallParams(rawParams) {
return controller.radar.writeFallParams(rawParams)
}
async function sendRadarCommand(commandByte) {
return controller.radar.sendCommand(commandByte)
}
async function disconnectDevice() {
await controller.ble.disconnect(currentDeviceId)
currentDeviceId = ''
setConnected(false)
}
function getState() {
const bleState = controller.ble.getState()
return {
isSearching,
isConnected,
currentDeviceId,
currentProfileId: controller.ble.getProfile().id,
deviceMap: { ...deviceMap },
deviceList: getSortedDeviceList(),
lastDeviceInfo,
lastAuthResult,
bleState
}
} }
function cleanupDiscovery() { function cleanupDiscovery() {
clearFallbackTimer() clearFallbackTimer()
clearDiscoveryPollTimer()
searchSessionId += 1 searchSessionId += 1
acceptingDeviceEvents = false acceptingDeviceEvents = false
setSearching(false) setSearching(false)
setConnected(false)
if (offDeviceFound) { currentDeviceId = ''
offDeviceFound() lastAuthResult = null
offDeviceFound = null lastDeviceInfo = null
}
if (offAdapterState) { unbindControllerEvents()
offAdapterState()
offAdapterState = null
}
if (!initialized) { if (!initialized) {
return return
} }
initialized = false initialized = false
controller.cleanup().catch(() => {}) controller.cleanup().catch(() => {})
} }
return { return {
// 搜索能力(兼容原页面入口)。
startSearch, startSearch,
stopSearch, stopSearch,
cleanupDiscovery cleanupDiscovery,
// 连接与协议流程能力。
connectDevice,
authorizeDevice,
standardConnectFlow,
disconnectDevice,
// 协议业务能力。
readDeviceInfo,
configureWifi,
startRadar,
stopRadar,
setNarrowMode,
writeFallParams,
sendRadarCommand,
// 调试/状态读取。
getState
} }
} }