SEC_Warehouse/utils/ble/core/controller.js

285 lines
8.5 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 { createBleCore } from './bleCore';
import { createAuthModule } from '../modules/auth';
import { createWifiModule } from '../modules/wifi';
import { createRadarModule } from '../modules/radar';
import { parseStateCode, parseDeviceInfo } from '../parsers';
import {
DEVICE_PROFILE,
guessProfileByRadarFrame,
} from '../protocols/profiles';
import { uuidEquals } from '../utils/uuid';
/**
* 订阅 F301 前先读一次 UUID_MSG(F501),满足厂家强制时序。
* 先注册 protocol:msg 监听再触发读取,避免回调早于监听丢失数据。
*/
async function readMsgOnce(ble, chars, timeout = 4000) {
if (!chars || !chars.msg || !chars.msg.uuid) {
console.debug('[BLE][MSG]', 'readMsgOnce skipped: no MSG characteristic');
return null;
}
let timer = null;
let off = null;
try {
const payload = await new Promise((resolve, reject) => {
const finish = (cb) => {
if (timer) clearTimeout(timer);
if (off) off();
cb();
};
off = ble.on('protocol:msg', (data) => finish(() => resolve(data)));
timer = setTimeout(
() => finish(() => reject(new Error('读取 UUID_MSG 超时'))),
timeout
);
ble.readCharacteristic(chars.msg.uuid).catch((e) => finish(() => reject(e)));
});
const info = parseDeviceInfo(payload.value, ble.getProfile().id);
console.debug('[BLE][MSG]', 'readMsgOnce ok', JSON.stringify(info));
return info;
} catch (e) {
// 读 MSG 失败不阻断后续(仅作时序前置),但记录日志便于排查
console.debug('[BLE][MSG]', 'readMsgOnce failed', e?.message || e);
return null;
}
}
/**
* 创建特征值通知分发器。
* 根据特征UUID将通知分发到对应协议事件state / msg / radar / raw。
* 雷达帧还能自动推断设备 profileED713/ED719
*/
function createCharacteristicDispatcher(ble) {
return (event) => {
const state = ble.getState();
const profile = ble.getProfile();
const current = state.characteristics;
if (
current.state &&
uuidEquals(event.characteristicId, current.state.uuid)
) {
const stateCode = parseStateCode(event.value);
ble.setAuthState({
lastStateCode: stateCode,
keyMatched: stateCode === 7 || stateCode === 5 || state.auth.keyMatched,
});
ble.emit('protocol:state', {
...event,
profileId: profile.id,
stateCode,
});
return;
}
if (current.msg && uuidEquals(event.characteristicId, current.msg.uuid)) {
ble.emit('protocol:msg', {
...event,
profileId: profile.id,
deviceInfo: parseDeviceInfo(event.value, profile.id),
});
return;
}
if (
current.radar &&
uuidEquals(event.characteristicId, current.radar.uuid)
) {
const guessed = guessProfileByRadarFrame(event.value);
if (
state.profileId === DEVICE_PROFILE.UNKNOWN &&
guessed !== DEVICE_PROFILE.UNKNOWN
) {
ble.setProfile(guessed);
}
ble.emit('protocol:radar', {
...event,
profileId: ble.getProfile().id,
});
return;
}
ble.emit('protocol:raw', {
...event,
profileId: profile.id,
});
};
}
/**
* 统一BLE控制器整合核心能力 + 认证/WiFi/雷达三大业务模块。
* 页面层应通过此控制器编排完整业务流程。
* @param {object} options 透传给 createBleCore 的配置(重试策略/日志/profileId等
*/
export function createUnifiedBleController(options = {}) {
const ble = createBleCore(options);
const auth = createAuthModule(ble);
const wifi = createWifiModule(ble);
const radar = createRadarModule(ble);
const characteristicDispatcher = createCharacteristicDispatcher(ble);
let offCharacteristic = null;
/** 绑定特征值分发器到 ble 事件总线。 */
function bindDispatcher() {
if (offCharacteristic) {
return;
}
offCharacteristic = ble.on(
'characteristic:value',
characteristicDispatcher,
);
}
/** 解绑特征值分发器。 */
function unbindDispatcher() {
if (!offCharacteristic) {
return;
}
offCharacteristic();
offCharacteristic = null;
}
/** 初始化蓝牙适配器并注册所有监听(适配器状态/连接/特征值/分发器)。 */
async function init() {
await ble.openAdapter();
ble.watchAdapterState();
ble.watchConnectionChange();
ble.watchCharacteristicValue();
bindDispatcher();
}
/**
* 扫描并选取目标设备。
* 先按服务UUID过滤扫描无结果时可退化为无过滤扫描。
* @param {object} params
* @param {string} params.serviceUuid 目标服务UUID默认取 profile 配置
* @param {number} params.scanDuration 扫描时长(ms)默认5000
* @param {boolean} params.fallbackWithoutFilter 首次无结果时是否退化为无过滤扫描默认true
* @returns {object|null} 匹配到的设备对象,未找到返回 null
*/
async function scanAndPickDevice({
serviceUuid,
scanDuration = 5000,
fallbackWithoutFilter = true,
} = {}) {
const profile = ble.getProfile();
const targetService = serviceUuid || profile.uuids.service;
ble.watchDeviceFound();
await ble.startDiscovery({ services: [targetService] });
await new Promise((resolve) => setTimeout(resolve, scanDuration));
await ble.stopDiscovery();
let selected = ble.pickDeviceByService(targetService);
if (!selected && fallbackWithoutFilter) {
await ble.startDiscovery({ services: [] });
await new Promise((resolve) => setTimeout(resolve, scanDuration));
await ble.stopDiscovery();
selected =
ble.pickDeviceByService(targetService) ||
ble.listDiscoveredDevices()[0] ||
null;
}
return selected;
}
/**
* 连接后严格执行协议前置(厂家要求顺序):
* 1) 发现服务和特征
* 2) 先读取 UUID_MSG(F501) —— 必须在订阅 F301 之前
* 3) 订阅 UUID_STATE(F301) / UUID_RADAR(F302)
*
* 厂家说明:订阅 F301 前必须先读 F501否则后续设备不回 STATE 通知,
* 表现为写 KEY/WIFI 后 F301 零回执、等待超时。
*/
async function connectAndDiscover(deviceId) {
await ble.connect(deviceId);
await ble.discoverServices(deviceId);
const chars = await ble.discoverCharacteristics();
// 先读 UUID_MSG(F501),再订阅 F301 —— 厂家强制顺序
await readMsgOnce(ble, chars);
if (chars.state && chars.state.uuid) {
// 打印 STATE 特征属性,区分 notify / indicate定位通知不投递问题
console.debug('[BLE][SUB]', 'STATE props', JSON.stringify(chars.state.properties || {}));
// STATE 是鉴权/WiFi 的唯一回执通道,订阅失败必须显式抛出,
// 否则后续写 KEY 后设备零回执,表现为"等待 UUID_STATE 超时"难以定位。
try {
await ble.notifyCharacteristic(chars.state.uuid, true);
console.debug('[BLE][SUB]', 'notify STATE ok', chars.state.uuid);
} catch (e) {
throw new Error(`订阅 UUID_STATE(F301) 失败:${e?.message || e}`);
}
} else {
throw new Error('未发现 UUID_STATE(F301) 特征值,无法接收设备回执');
}
if (chars.radar && chars.radar.uuid) {
try {
await ble.notifyCharacteristic(chars.radar.uuid, true);
console.debug('[BLE][SUB]', 'notify RADAR ok', chars.radar.uuid);
} catch (e) {
console.debug('[BLE][SUB]', 'notify RADAR fail', e?.message || e);
}
}
return chars;
}
/**
* 标准流程:连接 -> 订阅 -> 鉴权。
* 鉴权内部会按 bindStatus 分支处理绑定/匹配。
*/
async function standardConnectFlow({ deviceId, key }) {
await init();
let targetDeviceId = deviceId;
if (!targetDeviceId) {
const device = await scanAndPickDevice();
if (!device) {
throw new Error('未找到可连接设备');
}
targetDeviceId = device.deviceId;
}
await connectAndDiscover(targetDeviceId);
const authResult = await auth.ensureAuthorized(key);
return {
deviceId: targetDeviceId,
authResult,
state: ble.getState(),
profile: ble.getProfile(),
};
}
/** 释放所有资源:解绑分发器 + 清理核心BLE停扫描/断连接/移监听/关适配器)。 */
async function cleanup() {
unbindDispatcher();
await ble.cleanup();
}
return {
ble,
auth,
wifi,
radar,
init,
scanAndPickDevice,
connectAndDiscover,
standardConnectFlow,
cleanup,
};
}