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 } }