599 lines
16 KiB
JavaScript
599 lines
16 KiB
JavaScript
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
|
||
}
|
||
|
||
/** 打开蓝牙适配器,成功后标记可用并进入 ADAPTER_OPENED 阶段。 */
|
||
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)
|
||
}
|
||
|
||
// 重置原生 handler 引用,以便 openAdapter 后 init() 可重新注册
|
||
adapterStateHandler = null
|
||
connectionStateHandler = null
|
||
characteristicValueHandler = null
|
||
deviceFoundHandler = null
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
/** 注册适配器状态变更监听,蓝牙关闭时自动切换到 DISCONNECTED。 */
|
||
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 })
|
||
}
|
||
|
||
/** 注册设备发现监听,按 deviceId 去重合并后广播 device:found。 */
|
||
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
|
||
}
|
||
|
||
/**
|
||
* 建立BLE连接。
|
||
* @param {string} deviceId 设备ID
|
||
* @param {object} options
|
||
* @param {number} options.timeout 连接超时(ms),默认15000
|
||
* @param {boolean} options.autoReconnect 是否自动重试,默认跟随初始化配置
|
||
*/
|
||
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)
|
||
}
|
||
}
|
||
|
||
/** 断开BLE连接并重置连接相关状态。 */
|
||
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)
|
||
}
|
||
|
||
/** 注册连接状态变更监听,断开时自动切换到 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
|
||
}
|
||
|
||
/** 获取候选服务UUID列表:profile主服务 + 兼容固件的 00F4/F400/FFF4。 */
|
||
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)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发现特征值并按业务用途缓存(wifi/key/cmd/msg/state/radar)。
|
||
* @param {object} options 可选覆盖 deviceId/serviceId
|
||
* @returns {object} characteristicMap 各业务特征 + all 全量列表
|
||
*/
|
||
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)
|
||
}
|
||
}
|
||
|
||
/** 读取指定特征值(触发 onBLECharacteristicValueChange 回调)。 */
|
||
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)
|
||
}
|
||
}
|
||
|
||
/** 向指定特征值写入数据,自动将输入转为 ArrayBuffer。 */
|
||
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)
|
||
}
|
||
|
||
/** 从已发现设备中,按广播服务UUID匹配并返回第一个设备。 */
|
||
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
|
||
}
|
||
}
|