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, 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', '713WQ', 'ED719','EP832']) function promisifyUniApi(apiName, params = {}) { return new Promise((resolve, reject) => { uni[apiName]({ ...params, success: (res) => resolve(res), fail: (err) => reject(err) }) }) } /** * 统一蓝牙接入层(ED713/ED719): * 1) 搜索(过滤、去重、可停止) * 2) 连接 + 服务/特征发现 + 订阅 * 3) 鉴权、读取设备信息、WiFi 配置、雷达控制 */ export function createBluetoothDiscovery(options = {}) { const { onSearchingChange = () => {}, onDeviceMapChange = () => {}, 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 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 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 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) { deviceMap = nextMap onDeviceMapChange({ ...deviceMap }) emitDeviceList() } function resetDeviceMap() { setDeviceMap({}) } function normalizeUuid(uuid) { 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('713WQ')) { 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) { const serviceList = Array.isArray(device?.advertisServiceUUIDs) ? device.advertisServiceUUIDs : [] const adShortUuids = extractShortUuidsFromAdvertisData(device?.advertisData) if (!serviceList.length && !adShortUuids.length) { return false } 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') } 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() { return Object.values(deviceMap).some((item) => item.isTargetDevice) } 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 clearDiscoveryPollTimer() { if (!discoveryPollTimer) { return } clearTimeout(discoveryPollTimer) discoveryPollTimer = 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 locationSetting = settings?.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 matched = matchTargetDevice(rawDevice) if (!matched.isTarget) { return } const existing = deviceMap[rawDevice.deviceId] || {} const merged = { ...existing, ...rawDevice, showName: rawDevice.name || rawDevice.localName || existing.showName || '未命名设备', isTargetDevice: true, hasTargetService: matched.byService, matchedByName: matched.byName, matchedByPayload: matched.byPayload, profileHint: inferProfileByName(rawDevice) } const prevSignature = getDeviceStableSignature(existing) const nextSignature = getDeviceStableSignature(merged) if (prevSignature === nextSignature) { return } setDeviceMap({ ...deviceMap, [rawDevice.deviceId]: merged }) } 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 } 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) { return } const currentSession = searchSessionId devices.forEach((device) => { if (!acceptingDeviceEvents || currentSession !== searchSessionId) { return } upsertDevice(device) }) }) offAdapterState = controller.ble.on('adapter:state', (state) => { 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, // 兼容低版本客户端:不强制传 powerLevel。 powerLevel: null }) } async function stopDiscoverySilently() { try { await controller.ble.stopDiscovery() } catch (error) { } } 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() filterFallbackTimer = setTimeout(async () => { if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId || hasTargetDeviceInMap()) { return } try { await stopDiscoverySilently() await delay(RESTART_SCAN_DELAY_MS) if (!isSearching || !acceptingDeviceEvents || sessionId !== searchSessionId) { return } await startDiscoveryWithServices([]) } catch (error) { } }, FILTER_SCAN_TIMEOUT_MS) } /** * 设备搜索: * 优先按 0x00F4/0xF400 过滤;若无结果自动切到兜底扫描并轮询 getBluetoothDevices。 */ async function startSearch() { if (isSearching) { return } clearFallbackTimer() clearDiscoveryPollTimer() searchSessionId += 1 const sessionId = searchSessionId resetDeviceMap() try { await ensureAndroidLocationPermission() await ensureInitialized() const adapterState = await controller.ble.getAdapterState() if (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) startDiscoveryPolling(sessionId) await fallbackToUnfilteredDiscoveryIfNeeded(sessionId) } catch (error) { acceptingDeviceEvents = false setSearching(false) showToast(getBluetoothErrorText(error)) throw error } } async function stopSearch(showStopToast = true) { clearFallbackTimer() clearDiscoveryPollTimer() searchSessionId += 1 acceptingDeviceEvents = false 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 } /** * MAC固定规则鉴权:密钥=MAC(去分隔符)+0000。 */ async function authorizeDeviceByMac(options = {}) { if (!currentDeviceId) { throw new Error('请先连接设备后再鉴权') } const result = await controller.auth.ensureAuthorizedByMac(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 } /** * WiFi配置专用流程:连接 -> 发现 -> 订阅 -> 按 MAC+0000 完成鉴权。 */ async function prepareWifiConfig(deviceId, options = {}) { await connectDevice(deviceId) return authorizeDeviceByMac(options) } 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) currentDeviceId = '' lastAuthResult = null lastDeviceInfo = null unbindControllerEvents() if (!initialized) { return } initialized = false controller.cleanup().catch(() => {}) } return { // 搜索能力(兼容原页面入口)。 startSearch, stopSearch, cleanupDiscovery, // 连接与协议流程能力。 connectDevice, authorizeDevice, authorizeDeviceByMac, standardConnectFlow, prepareWifiConfig, disconnectDevice, // 协议业务能力。 readDeviceInfo, configureWifi, startRadar, stopRadar, setNarrowMode, writeFallParams, sendRadarCommand, // 调试/状态读取。 getState } }