feat(ble): 支持多种蓝牙服务UUID并优化调试日志

- 新增 FFF4 服务UUID兼容性支持,适配不同固件版本
- 重构服务发现逻辑,支持多候选UUID匹配
- 添加详细的蓝牙协议状态调试日志
- 优化WiFi配置模块的日志输出和预延时机制
- 更新设备发现过滤器以支持新UUID格式
- 添加AGENTS.md和CLAUDE.md项目指导文件
- 重构认证模块增加调试日志功能
This commit is contained in:
ozh 2026-04-22 17:50:20 +08:00
parent bcf2285b65
commit 099349258b
11 changed files with 160 additions and 68 deletions

67
.claude/rules/frontend.md Normal file
View File

@ -0,0 +1,67 @@
# Project Overview
This is a uni-app Vue3 smart home (智慧养老) application using uview-plus component library.
- Use the new features of es6
- Use the uniapp framework for small program development
- Added uview-plus component as UI component
# Build & Run Commands
Install dependencies with `npm install`.
This repository does not define npm scripts; builds and previews are normally run through HBuilderX. Use HBuilderX for the active target platform:
```bash
npm install
```
Typical local workflow:
- Open the project in HBuilderX and run to WeChat Mini Program or Android emulator/device.
- Use the built-in packaging flow for release builds.
# Architecture
## Module Organization
App entry files live at the repository root: `main.js`, `App.vue`, `pages.json`, and `manifest.json`. Feature pages are under `pages/` (`login`, `index`, `event`, `mine`, `device`). Shared business code is split across `api/`, `constants/`, `hooks/`, and `utils/`. Bluetooth logic is centralized in `utils/ble/`, with subfolders for `core/`, `modules/`, `parsers/`, `protocols/`, and `utils/`. Static assets go in `static/`; design and protocol notes live in `docs/`.
## BLE Module (`utils/ble/`)
If you are unclear about the functions of the BLE Module, please refer to the `docs/unified-ble-flow.md` file.
Unified Bluetooth Low Energy module for ED713/ED719 devices. Key files:
- `core/bleCore.js` - Bluetooth core capabilities (connect/discover/read/write/notify/reconnect)
- `core/controller.js` - Standard flow orchestration (scan→connect→discover→auth)
- `core/packet.js` - A7 packet fragmentation
- `modules/auth.js` - Authentication flow (read MSG + write KEY)
- `modules/wifi.js` - WiFi configuration
- `modules/radar.js` - Radar control (start/stop/params)
- `protocols/profiles.js` - ED713/ED719 device profile differences
The unified flow: `standardConnectFlow({ deviceId, key })``wifi.configureWifi(ssid, password)``radar.startRadarStream()`
Profile differences:
- ED713: radar signature `0x7C`, supports narrow mode
- ED719: radar signature `0x67`, supports fall param 67
If modifications are made to the BLE Module, they shall be added to the `docs/unified-ble-flow.md` file
## Authentication (`utils/auth.js`)
Route guard system with `setupAuthInterceptors()`. Currently `AUTH_ENABLED = false` for development. Routes are defined in `constants/routes.js`.
## Pages
- `pages/login/index.vue` - Login page
- `pages/index/index.vue` - Home (tab)
- `pages/event/index.vue` - Events (tab)
- `pages/mine/index.vue` - Profile (tab)
- `pages/device/add.vue` - Add device
- `pages/device/config.vue` - Device config with BLE search + WiFi panel
## Component Auto-import
uview-plus components auto-imported via `pages.json` easycom config: `^u-(.*)``uview-plus/components/u-$1/u-$1.vue`
## Coding Style & Naming Conventions
Follow the existing Vue SFC structure: `<template>`, `<script>`, `<style>`. JavaScript and Vue blocks use 2-space indentation; keep generated JSON files (`pages.json`, `manifest.json`) consistent with the existing HBuilderX formatting. Use `camelCase` for variables and functions, `UPPER_SNAKE_CASE` for constants, and keep page paths aligned with route names such as `pages/device/config.vue`. Prefer small reusable utilities in `utils/` or `hooks/` over duplicating BLE or navigation logic.

View File

@ -0,0 +1,5 @@
# Commit & Pull Request Guidelines
Pull requests should include a short summary, impacted pages or modules, manual test steps, linked issues when available, and screenshots for UI changes. For BLE-related changes, call out protocol assumptions, permissions touched, and any platform-specific risk.
# Security & Configuration Tips
Do not commit production secrets, API tokens, or real device credentials. Review `manifest.json` permission changes carefully, since they affect Android packaging and device capabilities.

2
.claude/rules/test.md Normal file
View File

@ -0,0 +1,2 @@
# Testing Guidelines
No automated test framework is configured yet. Validate changes with manual testing on the affected target, especially BLE discovery, authentication, and Wi-Fi configuration flows in `pages/device/config.vue` and `utils/ble/`. When submitting changes, include clear reproduction and verification steps, plus device model notes if behavior differs between ED713 and ED719.

7
AGENTS.md Normal file
View File

@ -0,0 +1,7 @@
# Repository Guidelines
This file provides guidance to Codex when working with code in this repository.
@.claude/rules/frontend.md
@.claude/rules/git-workflow.md
@.claude/rules/test.md

7
CLAUDE.md Normal file
View File

@ -0,0 +1,7 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
@.claude/rules/frontend.md
@.claude/rules/git-workflow.md
@.claude/rules/test.md

View File

@ -1,15 +1,23 @@
/** /**
* 目标服务 UUID用于优先识别特定设备 * 目标服务 UUID用于优先识别特定设备
* 协议文档0x00F4部分固件实现会使用 0xF400 * 协议文档主服务是 0x00F4部分固件会使用 0xF400 / 0xFFF4
*/ */
export const TARGET_SERVICE_UUID = '00F4' export const TARGET_SERVICE_UUID = '00F4'
export const TARGET_SERVICE_UUID_FULL = '000000F4-0000-1000-8000-00805F9B34FB' export const TARGET_SERVICE_UUID_FULL = '000000F4-0000-1000-8000-00805F9B34FB'
export const TARGET_SERVICE_UUID_ALT = 'F400' export const TARGET_SERVICE_UUID_ALT = 'F400'
export const TARGET_SERVICE_UUID_ALT_FULL = '0000F400-0000-1000-8000-00805F9B34FB' export const TARGET_SERVICE_UUID_ALT_FULL = '0000F400-0000-1000-8000-00805F9B34FB'
export const TARGET_SERVICE_UUID_COMPAT = 'FFF4'
export const TARGET_SERVICE_UUID_COMPAT_FULL = '0000FFF4-0000-1000-8000-00805F9B34FB'
export const TARGET_SHORT_UUID = '00F4' export const TARGET_SHORT_UUID = TARGET_SERVICE_UUID
export const TARGET_SHORT_UUID_CANDIDATES = Object.freeze([
TARGET_SERVICE_UUID,
TARGET_SERVICE_UUID_ALT,
TARGET_SERVICE_UUID_COMPAT
])
export const DISCOVERY_FILTER_SERVICE_UUIDS = Object.freeze([ export const DISCOVERY_FILTER_SERVICE_UUIDS = Object.freeze([
TARGET_SERVICE_UUID_COMPAT_FULL,
TARGET_SERVICE_UUID_FULL, TARGET_SERVICE_UUID_FULL,
TARGET_SERVICE_UUID_ALT_FULL TARGET_SERVICE_UUID_ALT_FULL
]) ])

View File

@ -4,7 +4,8 @@ import {
DISCOVERY_FILTER_SERVICE_UUIDS, DISCOVERY_FILTER_SERVICE_UUIDS,
TARGET_SERVICE_UUID_FULL, TARGET_SERVICE_UUID_FULL,
TARGET_SERVICE_UUID_ALT_FULL, TARGET_SERVICE_UUID_ALT_FULL,
TARGET_SHORT_UUID TARGET_SERVICE_UUID_COMPAT_FULL,
TARGET_SHORT_UUID_CANDIDATES
} from '@/constants/bluetooth' } from '@/constants/bluetooth'
import { DEVICE_PROFILE, createUnifiedBleController, shortUuid, uuidInList } from '@/utils/ble' import { DEVICE_PROFILE, createUnifiedBleController, shortUuid, uuidInList } from '@/utils/ble'
import { showToast } from '@/utils/toast' import { showToast } from '@/utils/toast'
@ -121,7 +122,7 @@ export function createBluetoothDiscovery(options = {}) {
return DEVICE_PROFILE.ED719 return DEVICE_PROFILE.ED719
} }
if (text.includes('713WQ')) { if (text.includes('ED713') || text.includes('713WQ')) {
return DEVICE_PROFILE.ED713 return DEVICE_PROFILE.ED713
} }
@ -176,11 +177,13 @@ export function createBluetoothDiscovery(options = {}) {
return false return false
} }
return uuidInList(TARGET_SERVICE_UUID_FULL, serviceList) const serviceShortList = serviceList.map((item) => shortUuid(item))
return uuidInList(TARGET_SERVICE_UUID_COMPAT_FULL, serviceList)
|| uuidInList(TARGET_SERVICE_UUID_FULL, serviceList)
|| uuidInList(TARGET_SERVICE_UUID_ALT_FULL, serviceList) || uuidInList(TARGET_SERVICE_UUID_ALT_FULL, serviceList)
|| serviceList.some((item) => shortUuid(item) === TARGET_SHORT_UUID) || TARGET_SHORT_UUID_CANDIDATES.some((uuid) => serviceShortList.includes(uuid))
|| adShortUuids.includes(TARGET_SHORT_UUID) || TARGET_SHORT_UUID_CANDIDATES.some((uuid) => adShortUuids.includes(uuid))
|| adShortUuids.includes('F400')
} }
function bufferToPrintableText(buffer) { function bufferToPrintableText(buffer) {
@ -401,10 +404,12 @@ export function createBluetoothDiscovery(options = {}) {
}) })
offProtocolState = controller.ble.on('protocol:state', (payload) => { offProtocolState = controller.ble.on('protocol:state', (payload) => {
try { console.debug('[BLE][STATE]', 'code=', payload?.stateCode, 'char=', payload?.characteristicId) } catch (e) {}
onProtocolState(payload) onProtocolState(payload)
}) })
offProtocolMsg = controller.ble.on('protocol:msg', (payload) => { offProtocolMsg = controller.ble.on('protocol:msg', (payload) => {
try { console.debug('[BLE][MSG]', 'len=', payload?.value?.length || 0) } catch (e) {}
onProtocolMsg(payload) onProtocolMsg(payload)
if (payload?.deviceInfo) { if (payload?.deviceInfo) {
lastDeviceInfo = payload.deviceInfo lastDeviceInfo = payload.deviceInfo
@ -421,6 +426,7 @@ export function createBluetoothDiscovery(options = {}) {
}) })
offError = controller.ble.on('error', (error) => { offError = controller.ble.on('error', (error) => {
try { console.debug('[BLE][ERR]', error?.message || error?.errMsg || String(error)) } catch (e) {}
onError(error) onError(error)
}) })
} }
@ -562,7 +568,7 @@ export function createBluetoothDiscovery(options = {}) {
/** /**
* 设备搜索 * 设备搜索
* 优先按 0x00F4/0xF400 过滤若无结果自动切到兜底扫描并轮询 getBluetoothDevices * 优先按 0xFFF4/0x00F4/0xF400 过滤若无结果自动切到兜底扫描并轮询 getBluetoothDevices
*/ */
async function startSearch() { async function startSearch() {
if (isSearching) { if (isSearching) {
@ -751,7 +757,9 @@ export function createBluetoothDiscovery(options = {}) {
} }
async function configureWifi(ssid, password, options = {}) { async function configureWifi(ssid, password, options = {}) {
return controller.wifi.configureWifi(ssid, password, options) const profileId = controller.ble.getProfile().id
const preDelayMs = options.preDelayMs ?? (profileId === DEVICE_PROFILE.ED713 ? 120 : 0)
return controller.wifi.configureWifi(ssid, password, { ...options, preDelayMs })
} }
async function startRadar() { async function startRadar() {

View File

@ -338,8 +338,21 @@ export function createBleCore(options = {}) {
connectionStateHandler = null connectionStateHandler = null
} }
function getServiceCandidates() {
const profileService = currentProfile?.uuids?.service
const candidates = [
profileService,
toFullUuid('00F4'),
toFullUuid('F400'),
toFullUuid('FFF4')
]
return candidates.filter(Boolean)
}
/** /**
* 发现并锁定目标服务协议要求 0x00F4 * 发现并锁定目标服务
* 文档主服务为 0x00F4同时兼容部分固件 0xF400 / 0xFFF4
*/ */
async function discoverServices(deviceId = state.deviceId) { async function discoverServices(deviceId = state.deviceId) {
if (!deviceId) { if (!deviceId) {
@ -349,10 +362,11 @@ export function createBleCore(options = {}) {
try { try {
const result = await promisifyUniApi('getBLEDeviceServices', { deviceId }) const result = await promisifyUniApi('getBLEDeviceServices', { deviceId })
const services = result.services || [] const services = result.services || []
const targetService = services.find((service) => uuidEquals(service.uuid, currentProfile.uuids.service)) const candidates = getServiceCandidates()
const targetService = services.find((service) => candidates.some((uuid) => uuidEquals(service.uuid, uuid)))
if (!targetService) { if (!targetService) {
throw createBleError(10004, '设备未暴露目标服务 0x00F4') throw createBleError(10004, '设备未暴露目标服务(00F4/F400/FFF4)')
} }
patchState({ serviceId: targetService.uuid }) patchState({ serviceId: targetService.uuid })

View File

@ -127,11 +127,11 @@ export function createUnifiedBleController(options = {}) {
const chars = await ble.discoverCharacteristics() const chars = await ble.discoverCharacteristics()
if (chars.state && chars.state.uuid) { if (chars.state && chars.state.uuid) {
await ble.notifyCharacteristic(chars.state.uuid, true) try { await ble.notifyCharacteristic(chars.state.uuid, true); console.debug('[BLE][SUB]', 'notify STATE ok', chars.state.uuid) } catch (e) { console.debug('[BLE][SUB]', 'notify STATE fail', e?.message || e) }
} }
if (chars.radar && chars.radar.uuid) { if (chars.radar && chars.radar.uuid) {
await ble.notifyCharacteristic(chars.radar.uuid, true) 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 return chars

View File

@ -15,6 +15,8 @@ function generateRandomKey(length = 16) {
return result return result
} }
const authLog = (...args) => { try { console.debug('[BLE][AUTH]', ...args) } catch (e) {} }
function normalizeMacForKey(macText = '') { function normalizeMacForKey(macText = '') {
return String(macText || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase() return String(macText || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
} }
@ -63,29 +65,21 @@ async function writePacketsToKeyCharacteristic(ble, keyText) {
} }
async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options = {}) { async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options = {}) {
const keyValue = await writePacketsToKeyCharacteristic(ble, keyText) const waitPromise = waitForProtocolState(ble, {
const stateEvent = await waitForProtocolState(ble, {
allowedCodes: expectedStateCodes, allowedCodes: expectedStateCodes,
rejectOnUnexpected: true, rejectOnUnexpected: true,
timeout: options.timeout || 6000, timeout: options.timeout || 6000,
timeoutMessage: '等待 UUID_STATE 超时', timeoutMessage: '等待 UUID_STATE 超时',
unexpectedMessage: '密钥流程状态异常' unexpectedMessage: '密钥流程状态异常'
}) })
authLog('STATE wait before writeKey, expect=', expectedStateCodes)
const stateCode = Number(stateEvent.stateCode) const keyValue = await writePacketsToKeyCharacteristic(ble, keyText)
authLog('key written, length=', keyValue)
const stateCode = Number((await waitPromise).stateCode)
authLog('STATE received for KEY=', stateCode)
ble.setStage(BLE_STAGE.AUTHORIZED) ble.setStage(BLE_STAGE.AUTHORIZED)
ble.setAuthState({ ble.setAuthState({ keyMatched: true, lastStateCode: stateCode })
keyMatched: true, return { success: true, stateCode, key: keyValue }
lastStateCode: stateCode
})
return {
success: true,
stateCode,
key: keyValue
}
} }
async function ensureAuthorizedWithDeviceInfo(ble, info, keyText, options = {}) { async function ensureAuthorizedWithDeviceInfo(ble, info, keyText, options = {}) {

View File

@ -39,44 +39,24 @@ function requireAuthorized(ble) {
* WiFi 配网模块负责打包发送 ssid|password并等待 STATE 回执 * WiFi 配网模块负责打包发送 ssid|password并等待 STATE 回执
*/ */
export function createWifiModule(ble) { export function createWifiModule(ble) {
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, Number(ms) || 0))
}
const log = (...args) => { try { console.debug('[BLE][WiFi]', ...args) } catch (e) {} }
async function configureWifi(ssid, password, options = {}) { async function configureWifi(ssid, password, options = {}) {
const profile = ble.getProfile() const profile = ble.getProfile()
const targetSsid = String(ssid || '').trim() const s = String(ssid || '').trim(), p = String(password || '').trim()
const targetPass = String(password || '').trim() validateWifi(profile, s, p); requireAuthorized(ble)
const { wifi } = ble.getState().characteristics; if (!wifi || !wifi.uuid) throw new Error('未发现 UUID_WIFI 特征值')
validateWifi(profile, targetSsid, targetPass) const waitPromise = waitForProtocolState(ble, { allowedCodes: WIFI_FINAL_STATE_CODES, rejectOnUnexpected: false, timeout: options.timeout || 12000, timeoutMessage: '等待 WiFi 状态超时' })
requireAuthorized(ble) const pre = options.preDelayMs ?? (ble.getProfile().id === 'ED713' ? 120 : 0); if (pre) { log('preDelay before WiFi write(ms)=', pre); await delay(pre) }
log('write WiFi start', { service: ble.getState().serviceId, char: wifi.uuid })
const { wifi } = ble.getState().characteristics const sent = await writeA7Payload(ble, wifi.uuid, `${s}|${p}`, profile.packet, { packetInterval: Number(options.packetInterval) || 0 })
if (!wifi || !wifi.uuid) { log('write WiFi done, packets=', sent)
throw new Error('未发现 UUID_WIFI 特征值') const code = Number((await waitPromise).stateCode); log('STATE(final) received for WiFi=', code)
return { success: code === 3, stateCode: code }
} }
const payload = `${targetSsid}|${targetPass}` return { configureWifi, requireAuthorized }
// 协议要求 WiFi 参数按 0xA7 分包发送到 UUID_WIFI(F401)。
await writeA7Payload(ble, wifi.uuid, payload, profile.packet, {
packetInterval: Number(options.packetInterval) || 0
})
const stateEvent = await waitForProtocolState(ble, {
// 仅等待最终态,避免在“连接中(4)”时过早返回导致页面提前结束流程。
allowedCodes: WIFI_FINAL_STATE_CODES,
rejectOnUnexpected: false,
timeout: options.timeout || 12000,
timeoutMessage: '等待 WiFi 状态超时'
})
const stateCode = Number(stateEvent.stateCode)
return {
success: stateCode === 3,
stateCode
}
}
return {
configureWifi,
requireAuthorized
}
} }