SEC_Warehouse/hooks/useBluetoothDiscovery.js

379 lines
8.5 KiB
JavaScript

import {
BLUETOOTH_ERROR_MESSAGES,
DEFAULT_BLUETOOTH_ERROR_MESSAGE,
DISCOVERY_FILTER_SERVICE_UUIDS,
TARGET_SERVICE_UUID_FULL,
TARGET_SERVICE_UUID_ALT_FULL,
TARGET_SHORT_UUID
} from '@/constants/bluetooth'
import { DEVICE_PROFILE, createUnifiedBleController } from '@/utils/ble'
import { showToast } from '@/utils/toast'
const FILTER_SCAN_TIMEOUT_MS = 4000
/**
* 蓝牙设备搜索逻辑封装。
* 基于 utils/ble 统一蓝牙控制器实现。
*/
export function createBluetoothDiscovery(options = {}) {
const {
onSearchingChange = () => {},
onDeviceMapChange = () => {},
onDeviceListChange = () => {}
} = options
const controller = createUnifiedBleController({
profileId: DEVICE_PROFILE.UNKNOWN
})
let isSearching = false
let deviceMap = {}
let initialized = false
let filterFallbackTimer = null
let offDeviceFound = null
let offAdapterState = null
let searchSessionId = 0
let acceptingDeviceEvents = false
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
}
const aSignal = typeof a.RSSI === 'number' ? a.RSSI : -999
const bSignal = typeof b.RSSI === 'number' ? b.RSSI : -999
return bSignal - aSignal
})
onDeviceListChange(list)
}
function setDeviceMap(nextMap) {
deviceMap = nextMap
onDeviceMapChange({ ...deviceMap })
emitDeviceList()
}
function resetDeviceMap() {
setDeviceMap({})
}
function normalizeUuid(uuid) {
return String(uuid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
}
function hasTargetService(device) {
if (!Array.isArray(device.advertisServiceUUIDs)) {
return false
}
const serviceSet = new Set([
normalizeUuid(TARGET_SERVICE_UUID_FULL),
normalizeUuid(TARGET_SERVICE_UUID_ALT_FULL)
])
return device.advertisServiceUUIDs.some((uuid) => {
const value = normalizeUuid(uuid)
if (value.endsWith(TARGET_SHORT_UUID)) {
return true
}
return serviceSet.has(value)
})
}
function hasTargetDeviceInMap() {
return Object.values(deviceMap).some((item) => item.hasTargetService)
}
function getDeviceStableSignature(device = {}) {
return [
device.deviceId || '',
device.RSSI ?? '',
device.name || '',
device.localName || '',
Array.isArray(device.advertisServiceUUIDs) ? device.advertisServiceUUIDs.map((item) => normalizeUuid(item)).sort().join('|') : '',
device.advertisData ? JSON.stringify(Array.from(new Uint8Array(device.advertisData))) : ''
].join('#')
}
function clearFallbackTimer() {
if (!filterFallbackTimer) {
return
}
clearTimeout(filterFallbackTimer)
filterFallbackTimer = null
}
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function ensureAndroidLocationPermission() {
let systemInfo = {}
try {
systemInfo = uni.getSystemInfoSync() || {}
} catch (error) {
return true
}
const platform = String(systemInfo.platform || '').toLowerCase()
if (platform !== 'android') {
return true
}
try {
const settings = await new Promise((resolve, reject) => {
uni.getSetting({
success: resolve,
fail: reject
})
})
const authSetting = settings.authSetting || {}
const locationSetting = authSetting['scope.userLocation']
if (locationSetting === true) {
return true
}
await new Promise((resolve, reject) => {
uni.authorize({
scope: 'scope.userLocation',
success: resolve,
fail: reject
})
})
return true
} catch (error) {
showToast('Android建议开启定位权限/定位开关(已继续尝试搜索)')
return false
}
}
function upsertDevice(rawDevice) {
if (!rawDevice || !rawDevice.deviceId) {
return
}
const targetMatched = hasTargetService(rawDevice)
if (!targetMatched) {
return
}
const existing = deviceMap[rawDevice.deviceId] || {}
const merged = {
...existing,
...rawDevice,
showName: rawDevice.name || rawDevice.localName || existing.showName || '未命名设备',
hasTargetService: true
}
const prevSignature = getDeviceStableSignature(existing)
const nextSignature = getDeviceStableSignature(merged)
if (prevSignature === nextSignature) {
return
}
setDeviceMap({
...deviceMap,
[rawDevice.deviceId]: merged
})
}
async function ensureInitialized() {
if (initialized) {
return
}
await controller.init()
controller.ble.watchDeviceFound()
offDeviceFound = controller.ble.on('device:found', (devices) => {
if (!acceptingDeviceEvents) {
return
}
const currentSession = searchSessionId
devices.forEach((device) => {
if (!acceptingDeviceEvents || currentSession !== searchSessionId) {
return
}
upsertDevice(device)
})
})
offAdapterState = controller.ble.on('adapter:state', (state) => {
if (!state || state.available !== false) {
return
}
acceptingDeviceEvents = false
setSearching(false)
showToast('蓝牙已关闭,请先开启手机蓝牙')
})
initialized = true
}
async function startDiscoveryWithServices(services = []) {
await controller.ble.startDiscovery({
services,
allowDuplicatesKey: true,
interval: 0
})
}
async function stopDiscoverySilently() {
try {
await controller.ble.stopDiscovery()
} catch (error) {
}
}
async function fallbackToUnfilteredDiscoveryIfNeeded(sessionId) {
clearFallbackTimer()
filterFallbackTimer = setTimeout(async () => {
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId || hasTargetDeviceInMap()) {
return
}
try {
await stopDiscoverySilently()
await delay(120)
if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) {
return
}
await startDiscoveryWithServices([])
} catch (error) {
}
}, 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})`
}
async function startSearch() {
if (isSearching) {
return
}
clearFallbackTimer()
searchSessionId += 1
const sessionId = searchSessionId
resetDeviceMap()
try {
await ensureAndroidLocationPermission()
await ensureInitialized()
const adapterState = await controller.ble.getAdapterState()
if (adapterState && adapterState.available === false) {
throw { errCode: 10001 }
}
await stopDiscoverySilently()
acceptingDeviceEvents = true
try {
await startDiscoveryWithServices(DISCOVERY_FILTER_SERVICE_UUIDS)
} catch (error) {
await startDiscoveryWithServices([])
}
if (sessionId !== searchSessionId) {
return
}
setSearching(true)
await fallbackToUnfilteredDiscoveryIfNeeded(sessionId)
} catch (error) {
acceptingDeviceEvents = false
setSearching(false)
showToast(getBluetoothErrorText(error))
throw error
}
}
function stopSearch(showStopToast = true) {
return new Promise((resolve) => {
clearFallbackTimer()
searchSessionId += 1
acceptingDeviceEvents = false
controller.ble.stopDiscovery()
.catch(() => {})
.finally(() => {
setSearching(false)
if (showStopToast) {
showToast('已停止搜索')
}
resolve()
})
})
}
function cleanupDiscovery() {
clearFallbackTimer()
searchSessionId += 1
acceptingDeviceEvents = false
setSearching(false)
if (offDeviceFound) {
offDeviceFound()
offDeviceFound = null
}
if (offAdapterState) {
offAdapterState()
offAdapterState = null
}
if (!initialized) {
return
}
initialized = false
controller.cleanup().catch(() => {})
}
return {
startSearch,
stopSearch,
cleanupDiscovery
}
}