From fd617947f19f508f71c4838ff05c92f40355fa7a Mon Sep 17 00:00:00 2001 From: ozh Date: Fri, 17 Apr 2026 17:17:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(ble):=20=E6=88=90=E5=8A=9F=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=88=B0ED719=E8=AE=BE=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks/useBluetoothDiscovery.js | 606 ++++++++++++++++++++++++++++----- 1 file changed, 526 insertions(+), 80 deletions(-) diff --git a/hooks/useBluetoothDiscovery.js b/hooks/useBluetoothDiscovery.js index 8d748aa..bc03737 100644 --- a/hooks/useBluetoothDiscovery.js +++ b/hooks/useBluetoothDiscovery.js @@ -6,55 +6,98 @@ import { TARGET_SERVICE_UUID_ALT_FULL, TARGET_SHORT_UUID } from '@/constants/bluetooth' -import { DEVICE_PROFILE, createUnifiedBleController } from '@/utils/ble' +import { DEVICE_PROFILE, createUnifiedBleController, shortUuid, uuidInList } from '@/utils/ble' import { showToast } from '@/utils/toast' 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) + }) + }) +} /** - * 蓝牙设备搜索逻辑封装。 - * 基于 utils/ble 统一蓝牙控制器实现。 + * 统一蓝牙接入层(ED713/ED719): + * 1) 搜索(过滤、去重、可停止) + * 2) 连接 + 服务/特征发现 + 订阅 + * 3) 鉴权、读取设备信息、WiFi 配置、雷达控制 */ export function createBluetoothDiscovery(options = {}) { const { onSearchingChange = () => {}, onDeviceMapChange = () => {}, - onDeviceListChange = () => {} + onDeviceListChange = () => {}, + onConnectedChange = () => {}, + onProfileChange = () => {}, + onStageChange = () => {}, + onProtocolState = () => {}, + onProtocolMsg = () => {}, + onRadarData = () => {}, + onDeviceInfoChange = () => {}, + onAuthChange = () => {}, + onError = () => {} } = options const controller = createUnifiedBleController({ profileId: DEVICE_PROFILE.UNKNOWN }) + let initialized = false let isSearching = false + let isConnected = false let deviceMap = {} - let initialized = false - let filterFallbackTimer = null - - let offDeviceFound = null - let offAdapterState = null + let currentDeviceId = '' + let currentProfileId = DEVICE_PROFILE.UNKNOWN + let lastDeviceInfo = null + let lastAuthResult = null let searchSessionId = 0 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) { isSearching = Boolean(value) onSearchingChange(isSearching) } - function emitDeviceList() { - const list = Object.values(deviceMap).sort((a, b) => { - if (a.hasTargetService !== b.hasTargetService) { - return a.hasTargetService ? -1 : 1 + function setConnected(value) { + isConnected = Boolean(value) + onConnectedChange(isConnected) + } + + function getSortedDeviceList(mapValue = deviceMap) { + return Object.values(mapValue).sort((left, right) => { + if (left.hasTargetService !== right.hasTargetService) { + return left.hasTargetService ? -1 : 1 } - const aSignal = typeof a.RSSI === 'number' ? a.RSSI : -999 - const bSignal = typeof b.RSSI === 'number' ? b.RSSI : -999 - return bSignal - aSignal + const leftSignal = typeof left.RSSI === 'number' ? left.RSSI : -999 + const rightSignal = typeof right.RSSI === 'number' ? right.RSSI : -999 + return rightSignal - leftSignal }) + } - onDeviceListChange(list) + function emitDeviceList() { + onDeviceListChange(getSortedDeviceList()) } function setDeviceMap(nextMap) { @@ -71,29 +114,117 @@ export function createBluetoothDiscovery(options = {}) { 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 UUID(AD 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) { - 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 } - const serviceSet = new Set([ - normalizeUuid(TARGET_SERVICE_UUID_FULL), - normalizeUuid(TARGET_SERVICE_UUID_ALT_FULL) - ]) + return uuidInList(TARGET_SERVICE_UUID_FULL, serviceList) + || uuidInList(TARGET_SERVICE_UUID_ALT_FULL, serviceList) + || 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) + function bufferToPrintableText(buffer) { + if (!buffer) { + return '' + } - if (value.endsWith(TARGET_SHORT_UUID)) { - return true - } + const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer) + return Array.from(bytes) + .map((value) => (value >= 32 && value <= 126 ? String.fromCharCode(value) : ' ')) + .join(' ') + .toUpperCase() + } - return serviceSet.has(value) - }) + 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() { - return Object.values(deviceMap).some((item) => item.hasTargetService) + return Object.values(deviceMap).some((item) => item.isTargetDevice) } function getDeviceStableSignature(device = {}) { @@ -102,7 +233,9 @@ export function createBluetoothDiscovery(options = {}) { device.RSSI ?? '', device.name || '', 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))) : '' ].join('#') } @@ -116,6 +249,15 @@ export function createBluetoothDiscovery(options = {}) { filterFallbackTimer = null } + function clearDiscoveryPollTimer() { + if (!discoveryPollTimer) { + return + } + + clearTimeout(discoveryPollTimer) + discoveryPollTimer = null + } + function delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms) @@ -144,9 +286,7 @@ export function createBluetoothDiscovery(options = {}) { }) }) - const authSetting = settings.authSetting || {} - const locationSetting = authSetting['scope.userLocation'] - + const locationSetting = settings?.authSetting?.['scope.userLocation'] if (locationSetting === true) { return true } @@ -171,8 +311,8 @@ export function createBluetoothDiscovery(options = {}) { return } - const targetMatched = hasTargetService(rawDevice) - if (!targetMatched) { + const matched = matchTargetDevice(rawDevice) + if (!matched.isTarget) { return } @@ -181,7 +321,11 @@ export function createBluetoothDiscovery(options = {}) { ...existing, ...rawDevice, 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) @@ -197,13 +341,30 @@ export function createBluetoothDiscovery(options = {}) { }) } - async function ensureInitialized() { - if (initialized) { + function getBluetoothErrorText(error) { + 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 } - await controller.init() - controller.ble.watchDeviceFound() + offStateChange = controller.ble.on('state:change', (state) => { + onStageChange(state.stage) + + if (state.profileId && state.profileId !== currentProfileId) { + currentProfileId = state.profileId + onProfileChange(currentProfileId) + } + }) offDeviceFound = controller.ble.on('device:found', (devices) => { if (!acceptingDeviceEvents) { @@ -221,23 +382,113 @@ export function createBluetoothDiscovery(options = {}) { }) offAdapterState = controller.ble.on('adapter:state', (state) => { - if (!state || state.available !== false) { + if (state?.available !== false) { return } acceptingDeviceEvents = false setSearching(false) + setConnected(false) 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 + + currentProfileId = controller.ble.getProfile().id + onProfileChange(currentProfileId) } async function startDiscoveryWithServices(services = []) { await controller.ble.startDiscovery({ services, 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) { clearFallbackTimer() @@ -258,7 +548,7 @@ export function createBluetoothDiscovery(options = {}) { try { await stopDiscoverySilently() - await delay(120) + await delay(RESTART_SCAN_DELAY_MS) if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) { return @@ -270,23 +560,18 @@ export function createBluetoothDiscovery(options = {}) { }, FILTER_SCAN_TIMEOUT_MS) } - function getBluetoothErrorText(error) { - const errCode = (error && (error.errCode ?? error.code)) - const text = BLUETOOTH_ERROR_MESSAGES[errCode] || DEFAULT_BLUETOOTH_ERROR_MESSAGE - - if (errCode === undefined || errCode === null) { - return text - } - - return `${text}(${errCode})` - } - + /** + * 设备搜索: + * 优先按 0x00F4/0xF400 过滤;若无结果自动切到兜底扫描并轮询 getBluetoothDevices。 + */ async function startSearch() { if (isSearching) { return } clearFallbackTimer() + clearDiscoveryPollTimer() + searchSessionId += 1 const sessionId = searchSessionId @@ -297,7 +582,7 @@ export function createBluetoothDiscovery(options = {}) { await ensureInitialized() const adapterState = await controller.ble.getAdapterState() - if (adapterState && adapterState.available === false) { + if (adapterState?.available === false) { throw { errCode: 10001 } } @@ -316,6 +601,7 @@ export function createBluetoothDiscovery(options = {}) { } setSearching(true) + startDiscoveryPolling(sessionId) await fallbackToUnfilteredDiscoveryIfNeeded(sessionId) } catch (error) { acceptingDeviceEvents = false @@ -325,54 +611,214 @@ export function createBluetoothDiscovery(options = {}) { } } - function stopSearch(showStopToast = true) { - return new Promise((resolve) => { - clearFallbackTimer() + async function stopSearch(showStopToast = true) { + clearFallbackTimer() + clearDiscoveryPollTimer() - searchSessionId += 1 - acceptingDeviceEvents = false + searchSessionId += 1 + acceptingDeviceEvents = false - controller.ble.stopDiscovery() - .catch(() => {}) - .finally(() => { - setSearching(false) - if (showStopToast) { - showToast('已停止搜索') - } - resolve() - }) + await stopDiscoverySilently() + + setSearching(false) + if (showStopToast) { + showToast('已停止搜索') + } + } + + 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() { clearFallbackTimer() + clearDiscoveryPollTimer() searchSessionId += 1 acceptingDeviceEvents = false setSearching(false) + setConnected(false) - if (offDeviceFound) { - offDeviceFound() - offDeviceFound = null - } + currentDeviceId = '' + lastAuthResult = null + lastDeviceInfo = null - if (offAdapterState) { - offAdapterState() - offAdapterState = null - } + unbindControllerEvents() if (!initialized) { return } initialized = false - controller.cleanup().catch(() => {}) } return { + // 搜索能力(兼容原页面入口)。 startSearch, stopSearch, - cleanupDiscovery + cleanupDiscovery, + + // 连接与协议流程能力。 + connectDevice, + authorizeDevice, + standardConnectFlow, + disconnectDevice, + + // 协议业务能力。 + readDeviceInfo, + configureWifi, + startRadar, + stopRadar, + setNarrowMode, + writeFallParams, + sendRadarCommand, + + // 调试/状态读取。 + getState } }