fix(ble): 成功搜索到ED719设备
This commit is contained in:
parent
640772ef0c
commit
fd617947f1
|
|
@ -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
|
}
|
||||||
|
|
||||||
|
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 leftSignal = typeof left.RSSI === 'number' ? left.RSSI : -999
|
||||||
const bSignal = typeof b.RSSI === 'number' ? b.RSSI : -999
|
const rightSignal = typeof right.RSSI === 'number' ? right.RSSI : -999
|
||||||
return bSignal - aSignal
|
return rightSignal - leftSignal
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onDeviceListChange(list)
|
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 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) {
|
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) => {
|
function bufferToPrintableText(buffer) {
|
||||||
const value = normalizeUuid(uuid)
|
if (!buffer) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
if (value.endsWith(TARGET_SHORT_UUID)) {
|
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
|
||||||
return true
|
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() {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue