670 lines
18 KiB
JavaScript
670 lines
18 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 参数缺失'));
|
||
}
|
||
|
||
// 取特征 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,
|
||
};
|
||
}
|