From 2a6eeccbed576129d24549124eb9034cc0509e47 Mon Sep 17 00:00:00 2001 From: ozh Date: Thu, 30 Apr 2026 11:21:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(ble):=20=E5=8D=8F=E8=AE=AE=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7=E4=BF=AE=E5=A4=8D=E3=80=81WiFi=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E8=B6=85=E6=97=B6=E4=BF=AE=E5=A4=8D=E3=80=81=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E8=A1=8C=E4=B8=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正 ED713/ED719 MSG 解析分路与雷达 shieldNum 偏移 - 雷达窄床模式命令格式改为两字节 [0x7C, flag] - 鉴权流程 rejectOnUnexpected=false 避免中间态阻断 - readDeviceInfo 增加 msgTimeout 4秒超时保护 - config.vue prepareWifiConfig 超时修正(800000→8000) - 搜索 30s 自动停止,暂停保留设备数据 - closeAdapter 重置 handler 引用,每次搜索前完整适配器重开 --- docs/unified-ble-flow.md | 44 +++++++++++++++ hooks/useBluetoothDiscovery.js | 41 ++++++++++++-- pages/device/config.vue | 7 ++- utils/ble/core/bleCore.js | 6 +++ utils/ble/core/controller.js | 2 +- utils/ble/modules/auth.js | 51 +++++++++++------- utils/ble/modules/radar.js | 11 +++- utils/ble/parsers/commonParser.js | 90 ++++++++++++++++++++----------- 8 files changed, 195 insertions(+), 57 deletions(-) diff --git a/docs/unified-ble-flow.md b/docs/unified-ble-flow.md index 461c13e..5b291a0 100644 --- a/docs/unified-ble-flow.md +++ b/docs/unified-ble-flow.md @@ -19,6 +19,7 @@ 4. 标准业务阶段 - 读取设备信息:`read(F501)` - 鉴权:`write(F402, A7分包key)`,监听 `F301` 状态码 +- 当前项目约定:**已知密钥 = 设备 MAC 去分隔符后 + `0000`**,例如 `10:52:1C:81:64:F4` → `10521C8164F40000` - 鉴权成功后进行功能操作: - WiFi 配网:`write(F401, A7分包ssid|pass)` - 控制命令:`write(F403, [cmd])` @@ -69,6 +70,7 @@ utils/ble/ - `scanAndPickDevice/connectAndDiscover/cleanup` - 业务模块 - `createAuthModule(ble).ensureAuthorized(key)` + - `createAuthModule(ble).ensureAuthorizedByMac({ suffix: '0000' })` - `createWifiModule(ble).configureWifi(ssid, password)` - `createRadarModule(ble).startRadarStream()/stopRadarStream()/writeFallParams()` - 工具 @@ -122,3 +124,45 @@ async function runStandardFlow({ key, ssid, password }) { - `cmd.supportsNarrowMode`: ED713=true - `cmd.supportsFallParam67`: ED719=true - 未知设备先用 `UNKNOWN`,收到 `F302` 数据后按签名字节自动识别 profile。 + + +## 6. 修改记录 + +### 2026-04-30 对照协议文档第二轮修复 + +#### commonParser.js +- `parseDeviceInfo(payload, profileId)`: 重写为按 ED713/ED719 分路解析 + - ED713:MAC[0:6] + bindStatus[6] + 其它[7:10] + productId[10:12] + baseBoard[12] + coreBoard[13] + 其它[14:16] + deviceType[16] + wifiStatus[17] + cellularStatus[18] + - ED719:布局与 ED713 一致(bindStatus/productId 偏移相同,文档仅省略标注),复用 `parseEd713DeviceInfo` +- `parseEd719Radar`: 修正 `shieldNum` 从 bytes[27] → bytes[24](按文档结构体偏移累加 header[4]+event+people+num+nodata+height+mode+beeper+left+right+front+back+sensitive+nodata1+stateDelay=24) +- 新增 `wallOption`、`peopleHaveSwitch`、`peopleActivitySwitch` 字段解析 + +#### radar.js +- `setNarrowMode(enabled)`: 修正命令格式,从单字节 `0x7C`/`0x7D` 改为两字节 `[0x7C, flag]`,flag 0=关 1=开(符合协议文档 CMD=0x7C + 1byte参数) + +#### auth.js +- `readDeviceInfo(options)`: 新增 `msgTimeout` 参数与 4 秒超时保护,防止 UUID_MSG 无回包时页面挂死 +- `readDeviceInfo()`: 调用 `parseDeviceInfo` 时传入 `ble.getProfile().id` +- `ensureAuthorized`/`ensureAuthorizedByMac`: 透传 `options` 到 `readDeviceInfo` +- `writeKeyAndExpectStates`: `rejectOnUnexpected` 从 `true` 改为 `false`,避免设备中间态(-1/0)直接 reject 阻断鉴权 +- `ensureAuthorizedWithDeviceInfo`: 移除 `bindStatus === null` 分支(ED719 实际布局与 ED713 一致,bindStatus 可正常读取) + +#### controller.js +- 特征分发器 `createCharacteristicDispatcher` 中 `parseDeviceInfo` 调用传入 `profile.id` + +#### bleCore.js +- `closeAdapter()`: 新增重置 `adapterStateHandler`/`connectionStateHandler`/`characteristicValueHandler`/`deviceFoundHandler` 为 `null`,确保 `openAdapter` 后 `init()` 可重新注册所有原生监听 + +#### pages/device/config.vue +- `openWifiPanel`: `prepareWifiConfig` 超时从 `800000` 改为 `8000`,新增 `msgTimeout: 4000` +- `wifiPanelVisible = true` 后加 `await this.$nextTick()` 确保 Vue DOM 更新后再调原生 toast + +#### hooks/useBluetoothDiscovery.js +- 新增 `SCAN_DURATION_MS = 30000`,蓝牙搜索 30 秒后自动停止 +- `startSearch`: 搜索开始前同步设置 `isSearching = true`(避免异步导致无法暂停);先 `stopDiscoverySilently()` 再 `resetDeviceMap()`(新搜索刷新数据);启动 30 秒自动停止定时器 +- `startSearch`: `stopDiscoverySilently()` 后执行完整适配器重置(`unwatch*` 注销所有原生监听 → `closeAdapter` 关闭适配器 → `initialized = false`),由 `ensureInitialized` 重新打开并注册全部监听,确保每次搜索都从干净适配器状态开始,修复二次搜索找不到设备的问题 +- `stopSearch`: 移除 `resetDeviceMap()` 调用,暂停搜索时保留已发现设备数据;清除自动停止定时器 + +### 2026-04-29 对照协议文档修复 + +(初次修复,记录同上,已被 2026-04-30 条目覆盖) diff --git a/hooks/useBluetoothDiscovery.js b/hooks/useBluetoothDiscovery.js index 20fccc7..60c54b0 100644 --- a/hooks/useBluetoothDiscovery.js +++ b/hooks/useBluetoothDiscovery.js @@ -13,6 +13,7 @@ import {showToast} from '@/utils/toast' const FILTER_SCAN_TIMEOUT_MS = 4000 const RESTART_SCAN_DELAY_MS = 120 const DISCOVERY_POLL_INTERVAL_MS = 1200 +const SCAN_DURATION_MS = 30000 const TARGET_NAME_KEYWORDS = Object.freeze(['ED713', '713WQ', 'ED719', 'EP832']) function promisifyUniApi(apiName, params = {}) { @@ -77,6 +78,7 @@ export function createBluetoothDiscovery(options = {}) { let acceptingDeviceEvents = false let filterFallbackTimer = null let discoveryPollTimer = null + let autoStopTimer = null let offDeviceFound = null let offAdapterState = null @@ -273,6 +275,15 @@ export function createBluetoothDiscovery(options = {}) { discoveryPollTimer = null } + function clearAutoStopTimer() { + if (!autoStopTimer) { + return + } + + clearTimeout(autoStopTimer) + autoStopTimer = null + } + function delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms) @@ -597,8 +608,23 @@ export function createBluetoothDiscovery(options = {}) { return } + // 立即标记搜索中,防止 await 期间用户重复点击或 stopSearch 被覆盖 + isSearching = true + onSearchingChange(true) clearFallbackTimer() clearDiscoveryPollTimer() + clearAutoStopTimer() + + // 先停止上一次残留扫描 + await stopDiscoverySilently() + + // 重置 BLE 适配器:关闭并重开,确保每次搜索都是干净的初始状态 + if (initialized) { + unbindControllerEvents() + await controller.ble.closeAdapter() + await delay(RESTART_SCAN_DELAY_MS) + initialized = false + } searchSessionId += 1 const sessionId = searchSessionId @@ -614,8 +640,6 @@ export function createBluetoothDiscovery(options = {}) { throw {errCode: 10001} } - await stopDiscoverySilently() - acceptingDeviceEvents = true try { @@ -628,12 +652,19 @@ export function createBluetoothDiscovery(options = {}) { return } - setSearching(true) startDiscoveryPolling(sessionId) await fallbackToUnfilteredDiscoveryIfNeeded(sessionId) + + // 30s 自动停止搜索 + autoStopTimer = setTimeout(() => { + if (searchSessionId === sessionId) { + stopSearch(false) + } + }, SCAN_DURATION_MS) } catch (error) { acceptingDeviceEvents = false - setSearching(false) + isSearching = false + onSearchingChange(false) showToast(getBluetoothErrorText(error)) throw error } @@ -642,12 +673,14 @@ export function createBluetoothDiscovery(options = {}) { async function stopSearch(showStopToast = true) { clearFallbackTimer() clearDiscoveryPollTimer() + clearAutoStopTimer() searchSessionId += 1 acceptingDeviceEvents = false await stopDiscoverySilently() + // 暂停搜索保留已发现的设备数据,不做 resetDeviceMap setSearching(false) if (showStopToast) { showToast('已停止搜索') diff --git a/pages/device/config.vue b/pages/device/config.vue index fb568c2..61d48bb 100644 --- a/pages/device/config.vue +++ b/pages/device/config.vue @@ -331,15 +331,18 @@ export default { try { /** - * 按需求先执行 MAC+0000 密钥鉴权,成功后再允许进入 WiFi 弹层。 + * 先按项目固定已知密钥规则(MAC+0000)完成鉴权,成功后再允许进入 WiFi 弹层。 */ await this.discoveryController.prepareWifiConfig(targetDeviceId, { suffix: '0000', - timeout: 800000 + timeout: 8000, + msgTimeout: 4000 }) this.wifiPrepared = true this.wifiPanelVisible = true + // 确保 Vue 完成 DOM 更新后再调原生 toast,避免原生层覆盖 Vue 弹层 + await this.$nextTick() showToast('密钥鉴权成功,请填写WiFi信息') } catch (error) { showToast(getErrorMessage(error)) diff --git a/utils/ble/core/bleCore.js b/utils/ble/core/bleCore.js index 69c96e6..5e23f49 100644 --- a/utils/ble/core/bleCore.js +++ b/utils/ble/core/bleCore.js @@ -109,6 +109,12 @@ export function createBleCore(options = {}) { 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, diff --git a/utils/ble/core/controller.js b/utils/ble/core/controller.js index 2dbe0c2..0fae71d 100644 --- a/utils/ble/core/controller.js +++ b/utils/ble/core/controller.js @@ -36,7 +36,7 @@ function createCharacteristicDispatcher(ble) { ble.emit('protocol:msg', { ...event, profileId: profile.id, - deviceInfo: parseDeviceInfo(event.value) + deviceInfo: parseDeviceInfo(event.value, profile.id) }) return } diff --git a/utils/ble/modules/auth.js b/utils/ble/modules/auth.js index d8f7e24..19e54c9 100644 --- a/utils/ble/modules/auth.js +++ b/utils/ble/modules/auth.js @@ -86,9 +86,11 @@ async function writePacketsToKeyCharacteristic(ble, keyText) { * @param {object} options 超时等配置 */ async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options = {}) { + // 设备订阅 STATE 后可能先发送 -1(未写密钥)/0(空闲) 等中间态, + // 若 rejectOnUnexpected:true 会导致中间态直接 reject,阻断鉴权流程。 const waitPromise = waitForProtocolState(ble, { allowedCodes: expectedStateCodes, - rejectOnUnexpected: true, + rejectOnUnexpected: false, timeout: options.timeout || 6000, timeoutMessage: '等待 UUID_STATE 超时', unexpectedMessage: '密钥流程状态异常' @@ -105,12 +107,12 @@ async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options /** * 根据设备绑定状态执行鉴权流程: - * - bindStatus=0(未绑定):生成随机密钥写入,等待 STATE=5(绑定成功) - * - bindStatus=1(已绑定):使用已知密钥匹配,等待 STATE=7(匹配成功) + * - bindStatus=0(未绑定):生成随机密钥写入,等待 STATE=5 + * - bindStatus=1(已绑定):使用已知密钥匹配,等待 STATE=7 * - 其他状态:尝试用提供的密钥匹配,等待 STATE=7 或 5 */ async function ensureAuthorizedWithDeviceInfo(ble, info, keyText, options = {}) { - const bindStatus = Number(info.bindStatus) + const bindStatus = info.bindStatus !== null ? Number(info.bindStatus) : null const profile = ble.getProfile() ble.setAuthState({ @@ -170,24 +172,37 @@ export function createAuthModule(ble) { * 读取 UUID_MSG 获取设备信息(MAC/绑定状态/产品ID等)。 * 先订阅 protocol:msg 事件,再触发 read,保证不丢通知。 */ - async function readDeviceInfo() { + async function readDeviceInfo(options = {}) { const {msg} = ble.getState().characteristics if (!msg || !msg.uuid) { throw new Error('未发现 UUID_MSG 特征值') } - const valuePromise = new Promise((resolve) => { - const off = ble.on('protocol:msg', (payload) => { - off() - resolve(payload) - }) + const timeout = Number(options.msgTimeout) || 4000 + let timer = null + let off = null + + const valuePromise = new Promise((resolve, reject) => { + const finish = (callback) => { + if (timer) clearTimeout(timer) + if (off) off() + callback() + } + + off = ble.on('protocol:msg', (payload) => finish(() => resolve(payload))) + timer = setTimeout(() => finish(() => reject(new Error('读取 UUID_MSG 超时'))), timeout) }) - await ble.readCharacteristic(msg.uuid) - const payload = await valuePromise - - return parseDeviceInfo(payload.value) + try { + await ble.readCharacteristic(msg.uuid) + const payload = await valuePromise + return parseDeviceInfo(payload.value, ble.getProfile().id) + } catch (error) { + if (timer) clearTimeout(timer) + if (off) off() + throw error + } } /** @@ -198,18 +213,18 @@ export function createAuthModule(ble) { */ async function ensureAuthorized(keyText, options = {}) { ble.setStage(BLE_STAGE.AUTHORIZING) - const info = await readDeviceInfo() + const info = await readDeviceInfo(options) return ensureAuthorizedWithDeviceInfo(ble, info, keyText, options) } /** - * 固定规则鉴权:密钥 = 设备MAC(去分隔符) + "0000"。 - * 用于“先鉴权再进行 WiFi 配置”的页面流程。 + * 固定规则鉴权:密钥 = 设备MAC(去分隔符) + “0000”。 + * 用于”先鉴权再进行 WiFi 配置”的页面流程。 */ async function ensureAuthorizedByMac(options = {}) { ble.setStage(BLE_STAGE.AUTHORIZING) - const info = await readDeviceInfo() + const info = await readDeviceInfo(options) const profile = ble.getProfile() const keyByMac = buildKeyFromMac(info.mac, options.suffix || '0000', profile.auth.keyLength) diff --git a/utils/ble/modules/radar.js b/utils/ble/modules/radar.js index 2b83dbb..313beaf 100644 --- a/utils/ble/modules/radar.js +++ b/utils/ble/modules/radar.js @@ -51,7 +51,8 @@ export function createRadarModule(ble) { /** * 设置窄床模式(仅 ED713 支持)。 - * @param {boolean} enabled true=开启(0x7D),false=关闭(0x7C) + * 协议:CMD=0x7C,参数1byte(0=关, 1=开),设置后雷达重启。 + * @param {boolean} enabled true=开启,false=关闭 */ async function setNarrowMode(enabled) { const profile = ble.getProfile() @@ -61,7 +62,13 @@ export function createRadarModule(ble) { } requireAuthorized() - await sendCommand(enabled ? 0x7D : 0x7C) + + // 协议规定:写入 [0x7C, flag] 两字节 + const { cmd } = ble.getState().characteristics + if (!cmd || !cmd.uuid) { + throw new Error('未发现 UUID_CMD 特征值') + } + await ble.writeCharacteristic(cmd.uuid, new Uint8Array([0x7C, enabled ? 1 : 0])) } /** diff --git a/utils/ble/parsers/commonParser.js b/utils/ble/parsers/commonParser.js index f669207..82f4ff1 100644 --- a/utils/ble/parsers/commonParser.js +++ b/utils/ble/parsers/commonParser.js @@ -1,5 +1,6 @@ import {bytesToHex, bytesToMac} from '../utils/hex' import {joinLowHigh, readUint16LE, toUint8Array} from '../utils/bytes' +import {DEVICE_PROFILE} from '../protocols/profiles' /** * 解析 UUID_STATE 的状态字节,转为有符号 8bit。 @@ -14,41 +15,61 @@ export function parseStateCode(payload) { } /** - * 解析 UUID_MSG 设备信息(ED713/ED719 通用字段优先)。 + * ED713 MSG 解析。 + * 文档布局:MAC(6) + bindStatus(1) + 其它(3) + productId_H(1) + productId_L(1) + * + baseBoard(1) + coreBoard(1) + 其它(2) + deviceType(1) + * + wifiStatus(1) + cellularStatus(1) + 其它(1) */ -export function parseDeviceInfo(payload) { +function parseEd713DeviceInfo(bytes) { + if (!bytes.length) { + return { + raw: bytes, mac: '', bindStatus: null, productId: null, + deviceType: null, wifiStatus: null, cellularStatus: null + } + } + + return { + raw: bytes, + mac: bytesToMac(bytes.slice(0, 6)), + bindStatus: bytes.length > 6 ? bytes[6] : null, + productId: bytes.length > 11 ? joinLowHigh(bytes[11], bytes[10]) : null, + baseBoardVersion: bytes.length > 12 ? bytes[12] : null, + coreBoardVersion: bytes.length > 13 ? bytes[13] : null, + deviceType: bytes.length > 16 ? bytes[16] : null, + wifiStatus: bytes.length > 17 ? bytes[17] : null, + cellularStatus: bytes.length > 18 ? bytes[18] : null + } +} + +/** + * ED719 MSG 解析。 + * 实际偏移与 ED713 一致(文档省略了 bindStatus/productId 但底板数据布局相同): + * MAC(6) + bindStatus(1) + 其它(3) + productId_H(1) + productId_L(1) + * + baseBoard(1) + coreBoard(1) + 其它(2) + deviceType(1) + * + wifiStatus(1) + cellularStatus(1) + 其它(1) + */ +function parseEd719DeviceInfo(bytes) { + return parseEd713DeviceInfo(bytes) +} + +/** + * 解析 UUID_MSG 设备信息(按 profile 区分 ED713/ED719)。 + */ +export function parseDeviceInfo(payload, profileId = DEVICE_PROFILE.UNKNOWN) { const bytes = toUint8Array(payload) if (!bytes.length) { return { - raw: bytes, - mac: '', - bindStatus: null, - productId: null, - deviceType: null, - wifiStatus: null, - cellularStatus: null + raw: bytes, mac: '', bindStatus: null, productId: null, + deviceType: null, wifiStatus: null, cellularStatus: null } } - const macBytes = bytes.slice(0, 6) - const bindStatus = bytes.length > 6 ? bytes[6] : null - const productId = bytes.length > 10 ? joinLowHigh(bytes[10], bytes[9]) : null - const deviceType = bytes.length > 14 ? bytes[14] : null - const wifiStatus = bytes.length > 15 ? bytes[15] : null - const cellularStatus = bytes.length > 16 ? bytes[16] : null + if (profileId === DEVICE_PROFILE.ED713) return parseEd713DeviceInfo(bytes) + if (profileId === DEVICE_PROFILE.ED719) return parseEd719DeviceInfo(bytes) - return { - raw: bytes, - mac: bytesToMac(macBytes), - bindStatus, - productId, - deviceType, - wifiStatus, - cellularStatus, - baseBoardVersion: bytes.length > 11 ? bytes[11] : null, - coreBoardVersion: bytes.length > 12 ? bytes[12] : null - } + // UNKNOWN:按 ED713 布局兼容 + return parseEd713DeviceInfo(bytes) } /** @@ -86,7 +107,12 @@ export function parseEd713Radar(payload) { /** * ED719 雷达数据解析。 - * 屏蔽区数量由 shieldNum 控制,每块固定 9 字节。 + * 按文档结构体定义: + * header[4] + event(1) + people(1) + num(1) + nodata(1) + * + height(2) + mode(1) + beeper(1) + left(2) + right(2) + front(2) + back(2) + * + sensitive(1) + nodata1(1) + stateDelay(2) = 24 bytes + * shield_num 在 offset 24,屏蔽区在 offset 25 + * 屏蔽区之后:wallOption + peopleHaveSwitch + peopleActivitySwitch */ export function parseEd719Radar(payload) { const bytes = toUint8Array(payload) @@ -100,10 +126,12 @@ export function parseEd719Radar(payload) { } } - const shieldNum = bytes[27] || 0 - const shieldStart = 28 + const shieldNum = bytes[24] || 0 + const shieldStart = 25 const shieldBlockLength = Math.min(Math.max(shieldNum, 0), 4) * 9 + const wallOffset = shieldStart + shieldBlockLength + return { type: 'ED719', valid: true, @@ -122,6 +150,8 @@ export function parseEd719Radar(payload) { stateDelay: readUint16LE(bytes, 22), shieldNum, shieldZoneRaw: Array.from(bytes.slice(shieldStart, shieldStart + shieldBlockLength)), - wallOption: bytes[shieldStart + shieldBlockLength] ?? null + wallOption: bytes[wallOffset] ?? null, + peopleHaveSwitch: bytes[wallOffset + 1] ?? null, + peopleActivitySwitch: bytes[wallOffset + 2] ?? null } }