285 lines
8.5 KiB
JavaScript
285 lines
8.5 KiB
JavaScript
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。
|
||
* 雷达帧还能自动推断设备 profile(ED713/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,
|
||
};
|
||
}
|