825 lines
20 KiB
JavaScript
825 lines
20 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, 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'])
|
||
|
||
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('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) {
|
||
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
|
||
}
|
||
|
||
/**
|
||
* 标准连接流程:连接 -> 发现 -> 订阅 -> 鉴权。
|
||
*/
|
||
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)
|
||
|
||
currentDeviceId = ''
|
||
lastAuthResult = null
|
||
lastDeviceInfo = null
|
||
|
||
unbindControllerEvents()
|
||
|
||
if (!initialized) {
|
||
return
|
||
}
|
||
|
||
initialized = false
|
||
controller.cleanup().catch(() => {})
|
||
}
|
||
|
||
return {
|
||
// 搜索能力(兼容原页面入口)。
|
||
startSearch,
|
||
stopSearch,
|
||
cleanupDiscovery,
|
||
|
||
// 连接与协议流程能力。
|
||
connectDevice,
|
||
authorizeDevice,
|
||
standardConnectFlow,
|
||
disconnectDevice,
|
||
|
||
// 协议业务能力。
|
||
readDeviceInfo,
|
||
configureWifi,
|
||
startRadar,
|
||
stopRadar,
|
||
setNarrowMode,
|
||
writeFallParams,
|
||
sendRadarCommand,
|
||
|
||
// 调试/状态读取。
|
||
getState
|
||
}
|
||
}
|