SEC_Warehouse/utils/ble/core/bleCore.js

670 lines
18 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;
}
/** 打开蓝牙适配器,成功后标记可用并进入 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 参数缺失'));
}
// 取特征 properties用于诊断写入类型是否匹配并按需指定 writeType
const chars = state.characteristics || {};
const matched = Object.values(chars).find(
(item) => item && uuidEquals(item.uuid, characteristicId),
);
const props = matched?.properties || {};
const supportsWrite = Boolean(props.write);
const supportsWriteNoResp = Boolean(props.writeWithoutResponse);
// 默认优先有响应写入(可靠);仅当不支持有响应时退化为无响应写入
const writeType =
options.writeType ||
(supportsWrite
? 'write'
: supportsWriteNoResp
? 'writeWithoutResponse'
: undefined);
try {
console.debug('[BLE][WRITE]', characteristicId, {
writeType,
supportsWrite,
supportsWriteNoResp,
valueLen: value?.length
});
const params = {
deviceId,
serviceId,
characteristicId,
value: uint8ArrayToArrayBuffer(toUint8Array(value)),
};
if (writeType) {
params.writeType = writeType;
}
await promisifyUniApi('writeBLECharacteristicValue', params);
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) => {
const value = toUint8Array(result.value);
// 原始 trace定位 notify 是否投递时,确认每个特征值变更的来源与内容
try {
const hex = Array.from(value)
.map((b) => b.toString(16).padStart(2, '0'))
.join(' ');
console.debug('[BLE][RAW]', result.characteristicId, 'len=', value.length, 'hex=', hex);
} catch (e) {
}
eventBus.emit('characteristic:value', {
...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,
};
}