SEC_Warehouse/utils/ble/core/bleCore.js

563 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createEventBus } from './eventBus'
import { createBleState, BLE_STAGE } from './state'
import { createBleError, getBleErrorText, normalizeBleError } from './errors'
import { uint8ArrayToArrayBuffer, toUint8Array } from '../utils/bytes'
import { toFullUuid, uuidEquals, uuidInList } from '../utils/uuid'
import { runWithRetry } from './retry'
import { getBleProfile, DEVICE_PROFILE } from '../protocols/profiles'
// 把 uni 的 success/fail 回调风格统一成 Promise便于串联 async 流程。
function promisifyUniApi(apiName, params = {}) {
return new Promise((resolve, reject) => {
uni[apiName]({
...params,
success: (res) => resolve(res),
fail: (err) => reject(err)
})
})
}
/**
* BLE 核心能力封装:
* 负责适配器生命周期、扫描、连接、服务/特征发现、读写与通知监听。
*/
export function createBleCore(options = {}) {
const {
reconnect = {
enabled: true,
retries: 2,
delay: 1200
},
logger = console,
profileId = DEVICE_PROFILE.UNKNOWN
} = options
const eventBus = createEventBus()
const state = createBleState()
let currentProfile = getBleProfile(profileId)
let deviceFoundHandler = null
let adapterStateHandler = null
let connectionStateHandler = null
let characteristicValueHandler = null
// 返回状态快照,避免调用方直接改写内部状态对象。
function snapshotState() {
return {
...state,
auth: { ...state.auth },
deviceMap: { ...state.deviceMap },
characteristics: { ...state.characteristics }
}
}
// 统一 patch 状态并广播 state:change保证 UI 状态来源单一。
function patchState(partialState) {
Object.assign(state, partialState)
eventBus.emit('state:change', snapshotState())
}
function setStage(stage) {
patchState({ stage })
}
function setProfile(nextProfileId) {
currentProfile = getBleProfile(nextProfileId)
patchState({ profileId: currentProfile.id })
}
function getProfile() {
return currentProfile
}
function setAuthState(partialAuth = {}) {
patchState({
auth: {
...state.auth,
...partialAuth
}
})
}
function emit(eventName, payload) {
eventBus.emit(eventName, payload)
}
function emitError(error) {
const normalized = normalizeBleError(error)
patchState({ lastError: normalized })
eventBus.emit('error', normalized)
return normalized
}
async function openAdapter() {
try {
await promisifyUniApi('openBluetoothAdapter')
patchState({ available: true })
setStage(BLE_STAGE.ADAPTER_OPENED)
return true
} catch (error) {
throw emitError(error)
}
}
async function closeAdapter() {
try {
await promisifyUniApi('closeBluetoothAdapter')
} catch (error) {
logger.warn('[BLE] close adapter failed', error)
}
patchState({
stage: BLE_STAGE.IDLE,
available: false,
discovering: false,
connected: false,
deviceId: '',
serviceId: '',
characteristics: {}
})
}
async function getAdapterState() {
try {
const result = await promisifyUniApi('getBluetoothAdapterState')
patchState({
available: Boolean(result.available),
discovering: Boolean(result.discovering)
})
return result
} catch (error) {
throw emitError(error)
}
}
function watchAdapterState() {
if (adapterStateHandler) {
return
}
adapterStateHandler = (result) => {
patchState({
available: Boolean(result.available),
discovering: Boolean(result.discovering)
})
eventBus.emit('adapter:state', result)
if (!result.available) {
setStage(BLE_STAGE.DISCONNECTED)
}
}
uni.onBluetoothAdapterStateChange(adapterStateHandler)
}
function unwatchAdapterState() {
if (!adapterStateHandler) {
return
}
if (uni.offBluetoothAdapterStateChange) {
uni.offBluetoothAdapterStateChange(adapterStateHandler)
}
adapterStateHandler = null
}
/**
* 启动设备扫描。
* services 传入时会做 UUID 标准化,用于只扫描目标服务设备。
*/
async function startDiscovery(params = {}) {
const {
services = [],
allowDuplicatesKey = true,
interval = 0,
powerLevel = 'high'
} = params
try {
if (!state.available) {
await openAdapter()
}
const options = {
allowDuplicatesKey,
interval
}
if (services.length) {
options.services = services.map((uuid) => toFullUuid(uuid) || uuid)
}
if (powerLevel) {
options.powerLevel = powerLevel
}
await promisifyUniApi('startBluetoothDevicesDiscovery', options)
patchState({ discovering: true, deviceMap: {} })
setStage(BLE_STAGE.DISCOVERING)
return true
} catch (error) {
throw emitError(error)
}
}
async function stopDiscovery() {
try {
await promisifyUniApi('stopBluetoothDevicesDiscovery')
} catch (error) {
logger.warn('[BLE] stop discovery failed', error)
}
patchState({ discovering: false })
}
function watchDeviceFound() {
if (deviceFoundHandler) {
return
}
deviceFoundHandler = (result) => {
const list = Array.isArray(result.devices) ? result.devices : [result]
const nextMap = { ...state.deviceMap }
// 基于 deviceId 做去重与增量更新,避免列表无限叠加。
list.forEach((device) => {
if (!device || !device.deviceId) {
return
}
const previous = nextMap[device.deviceId] || {}
nextMap[device.deviceId] = {
...previous,
...device,
showName: device.name || device.localName || previous.showName || '未命名设备'
}
})
patchState({ deviceMap: nextMap })
eventBus.emit('device:found', Object.values(nextMap))
}
uni.onBluetoothDeviceFound(deviceFoundHandler)
}
function unwatchDeviceFound() {
if (!deviceFoundHandler) {
return
}
if (uni.offBluetoothDeviceFound) {
uni.offBluetoothDeviceFound(deviceFoundHandler)
}
deviceFoundHandler = null
}
async function connect(deviceId, options = {}) {
if (!deviceId) {
throw emitError(createBleError(10013, 'deviceId 不能为空'))
}
const {
timeout = 15000,
autoReconnect = reconnect.enabled
} = options
const runConnect = async () => {
setStage(BLE_STAGE.CONNECTING)
await promisifyUniApi('createBLEConnection', { deviceId, timeout })
patchState({ connected: true, deviceId })
setStage(BLE_STAGE.CONNECTED)
return true
}
try {
if (!autoReconnect) {
return await runConnect()
}
return await runWithRetry(runConnect, {
retries: reconnect.retries,
delay: reconnect.delay,
shouldRetry: (error) => {
const normalized = normalizeBleError(error)
return [10003, 10006, 10012].includes(normalized.code)
}
})
} catch (error) {
throw emitError(error)
}
}
async function disconnect(deviceId = state.deviceId) {
if (!deviceId) {
return
}
try {
await promisifyUniApi('closeBLEConnection', { deviceId })
} catch (error) {
logger.warn('[BLE] close connection failed', error)
}
patchState({ connected: false, deviceId: '', serviceId: '', characteristics: {} })
setStage(BLE_STAGE.DISCONNECTED)
}
function watchConnectionChange() {
if (connectionStateHandler) {
return
}
connectionStateHandler = (result) => {
if (state.deviceId && result.deviceId !== state.deviceId) {
return
}
patchState({ connected: Boolean(result.connected) })
eventBus.emit('connection:change', result)
if (!result.connected) {
setStage(BLE_STAGE.DISCONNECTED)
}
}
uni.onBLEConnectionStateChange(connectionStateHandler)
}
function unwatchConnectionChange() {
if (!connectionStateHandler) {
return
}
if (uni.offBLEConnectionStateChange) {
uni.offBLEConnectionStateChange(connectionStateHandler)
}
connectionStateHandler = null
}
function getServiceCandidates() {
const profileService = currentProfile?.uuids?.service
const candidates = [
profileService,
toFullUuid('00F4'),
toFullUuid('F400'),
toFullUuid('FFF4')
]
return candidates.filter(Boolean)
}
/**
* 发现并锁定目标服务。
* 文档主服务为 0x00F4同时兼容部分固件 0xF400 / 0xFFF4。
*/
async function discoverServices(deviceId = state.deviceId) {
if (!deviceId) {
throw emitError(createBleError(10013, 'discoverServices 缺少 deviceId'))
}
try {
const result = await promisifyUniApi('getBLEDeviceServices', { deviceId })
const services = result.services || []
const candidates = getServiceCandidates()
const targetService = services.find((service) => candidates.some((uuid) => uuidEquals(service.uuid, uuid)))
if (!targetService) {
throw createBleError(10004, '设备未暴露目标服务(00F4/F400/FFF4)')
}
patchState({ serviceId: targetService.uuid })
setStage(BLE_STAGE.SERVICE_DISCOVERED)
return {
services,
targetService
}
} catch (error) {
throw emitError(error)
}
}
async function discoverCharacteristics(options = {}) {
const deviceId = options.deviceId || state.deviceId
const serviceId = options.serviceId || state.serviceId
if (!deviceId || !serviceId) {
throw emitError(createBleError(10013, 'discoverCharacteristics 缺少 deviceId/serviceId'))
}
try {
const result = await promisifyUniApi('getBLEDeviceCharacteristics', { deviceId, serviceId })
const list = result.characteristics || []
const profile = getProfile()
// 缓存业务侧常用特征,减少后续每次操作时的查找成本。
const characteristicMap = {
wifi: list.find((item) => uuidEquals(item.uuid, profile.uuids.wifi)) || null,
key: list.find((item) => uuidEquals(item.uuid, profile.uuids.key)) || null,
cmd: list.find((item) => uuidEquals(item.uuid, profile.uuids.cmd)) || null,
msg: list.find((item) => uuidEquals(item.uuid, profile.uuids.msg)) || null,
state: list.find((item) => uuidEquals(item.uuid, profile.uuids.state)) || null,
radar: list.find((item) => uuidEquals(item.uuid, profile.uuids.radar)) || null,
all: list
}
patchState({ characteristics: characteristicMap })
return characteristicMap
} catch (error) {
throw emitError(error)
}
}
async function readCharacteristic(characteristicId, options = {}) {
const deviceId = options.deviceId || state.deviceId
const serviceId = options.serviceId || state.serviceId
if (!deviceId || !serviceId || !characteristicId) {
throw emitError(createBleError(10013, 'readCharacteristic 参数缺失'))
}
try {
await promisifyUniApi('readBLECharacteristicValue', {
deviceId,
serviceId,
characteristicId
})
return true
} catch (error) {
throw emitError(error)
}
}
async function writeCharacteristic(characteristicId, value, options = {}) {
const deviceId = options.deviceId || state.deviceId
const serviceId = options.serviceId || state.serviceId
if (!deviceId || !serviceId || !characteristicId) {
throw emitError(createBleError(10013, 'writeCharacteristic 参数缺失'))
}
try {
await promisifyUniApi('writeBLECharacteristicValue', {
deviceId,
serviceId,
characteristicId,
value: uint8ArrayToArrayBuffer(toUint8Array(value))
})
return true
} catch (error) {
throw emitError(error)
}
}
async function notifyCharacteristic(characteristicId, stateFlag = true, options = {}) {
const deviceId = options.deviceId || state.deviceId
const serviceId = options.serviceId || state.serviceId
if (!deviceId || !serviceId || !characteristicId) {
throw emitError(createBleError(10013, 'notifyCharacteristic 参数缺失'))
}
try {
await promisifyUniApi('notifyBLECharacteristicValueChange', {
deviceId,
serviceId,
characteristicId,
state: Boolean(stateFlag)
})
return true
} catch (error) {
throw emitError(error)
}
}
function watchCharacteristicValue() {
if (characteristicValueHandler) {
return
}
// 统一转为 Uint8Array简化上层解析逻辑。
characteristicValueHandler = (result) => {
eventBus.emit('characteristic:value', {
...result,
value: toUint8Array(result.value)
})
}
uni.onBLECharacteristicValueChange(characteristicValueHandler)
}
function unwatchCharacteristicValue() {
if (!characteristicValueHandler) {
return
}
if (uni.offBLECharacteristicValueChange) {
uni.offBLECharacteristicValueChange(characteristicValueHandler)
}
characteristicValueHandler = null
}
function listDiscoveredDevices() {
return Object.values(state.deviceMap)
}
function pickDeviceByService(serviceUuid = currentProfile.uuids.service) {
const list = listDiscoveredDevices()
return list.find((device) => uuidInList(serviceUuid, device.advertisServiceUUIDs || [])) || null
}
async function cleanup() {
// 清理顺序:先停扫描与连接,再移除监听,最后关闭适配器。
await stopDiscovery()
await disconnect()
unwatchDeviceFound()
unwatchAdapterState()
unwatchConnectionChange()
unwatchCharacteristicValue()
await closeAdapter()
eventBus.clear()
}
return {
openAdapter,
closeAdapter,
getAdapterState,
watchAdapterState,
unwatchAdapterState,
startDiscovery,
stopDiscovery,
watchDeviceFound,
unwatchDeviceFound,
connect,
disconnect,
watchConnectionChange,
unwatchConnectionChange,
discoverServices,
discoverCharacteristics,
readCharacteristic,
writeCharacteristic,
notifyCharacteristic,
watchCharacteristicValue,
unwatchCharacteristicValue,
pickDeviceByService,
listDiscoveredDevices,
getState: snapshotState,
setStage,
setProfile,
getProfile,
setAuthState,
emit,
on: eventBus.on,
once: eventBus.once,
cleanup,
getBleErrorText
}
}