feat(ble): 支持多种蓝牙服务UUID并优化调试日志
- 新增 FFF4 服务UUID兼容性支持,适配不同固件版本 - 重构服务发现逻辑,支持多候选UUID匹配 - 添加详细的蓝牙协议状态调试日志 - 优化WiFi配置模块的日志输出和预延时机制 - 更新设备发现过滤器以支持新UUID格式 - 添加AGENTS.md和CLAUDE.md项目指导文件 - 重构认证模块增加调试日志功能
This commit is contained in:
parent
bcf2285b65
commit
099349258b
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,15 +1,23 @@
|
|||
/**
|
||||
* 目标服务 UUID(用于优先识别特定设备)。
|
||||
* 协议文档为 0x00F4,部分固件实现会使用 0xF400。
|
||||
* 协议文档主服务是 0x00F4,部分固件会使用 0xF400 / 0xFFF4。
|
||||
*/
|
||||
export const TARGET_SERVICE_UUID = '00F4'
|
||||
export const TARGET_SERVICE_UUID_FULL = '000000F4-0000-1000-8000-00805F9B34FB'
|
||||
export const TARGET_SERVICE_UUID_ALT = 'F400'
|
||||
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([
|
||||
TARGET_SERVICE_UUID_COMPAT_FULL,
|
||||
TARGET_SERVICE_UUID_FULL,
|
||||
TARGET_SERVICE_UUID_ALT_FULL
|
||||
])
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import {
|
|||
DISCOVERY_FILTER_SERVICE_UUIDS,
|
||||
TARGET_SERVICE_UUID_FULL,
|
||||
TARGET_SERVICE_UUID_ALT_FULL,
|
||||
TARGET_SHORT_UUID
|
||||
TARGET_SERVICE_UUID_COMPAT_FULL,
|
||||
TARGET_SHORT_UUID_CANDIDATES
|
||||
} from '@/constants/bluetooth'
|
||||
import { DEVICE_PROFILE, createUnifiedBleController, shortUuid, uuidInList } from '@/utils/ble'
|
||||
import { showToast } from '@/utils/toast'
|
||||
|
|
@ -121,7 +122,7 @@ export function createBluetoothDiscovery(options = {}) {
|
|||
return DEVICE_PROFILE.ED719
|
||||
}
|
||||
|
||||
if (text.includes('713WQ')) {
|
||||
if (text.includes('ED713') || text.includes('713WQ')) {
|
||||
return DEVICE_PROFILE.ED713
|
||||
}
|
||||
|
||||
|
|
@ -176,11 +177,13 @@ export function createBluetoothDiscovery(options = {}) {
|
|||
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)
|
||||
|| serviceList.some((item) => shortUuid(item) === TARGET_SHORT_UUID)
|
||||
|| adShortUuids.includes(TARGET_SHORT_UUID)
|
||||
|| adShortUuids.includes('F400')
|
||||
|| TARGET_SHORT_UUID_CANDIDATES.some((uuid) => serviceShortList.includes(uuid))
|
||||
|| TARGET_SHORT_UUID_CANDIDATES.some((uuid) => adShortUuids.includes(uuid))
|
||||
}
|
||||
|
||||
function bufferToPrintableText(buffer) {
|
||||
|
|
@ -401,10 +404,12 @@ export function createBluetoothDiscovery(options = {}) {
|
|||
})
|
||||
|
||||
offProtocolState = controller.ble.on('protocol:state', (payload) => {
|
||||
try { console.debug('[BLE][STATE]', 'code=', payload?.stateCode, 'char=', payload?.characteristicId) } catch (e) {}
|
||||
onProtocolState(payload)
|
||||
})
|
||||
|
||||
offProtocolMsg = controller.ble.on('protocol:msg', (payload) => {
|
||||
try { console.debug('[BLE][MSG]', 'len=', payload?.value?.length || 0) } catch (e) {}
|
||||
onProtocolMsg(payload)
|
||||
if (payload?.deviceInfo) {
|
||||
lastDeviceInfo = payload.deviceInfo
|
||||
|
|
@ -421,6 +426,7 @@ export function createBluetoothDiscovery(options = {}) {
|
|||
})
|
||||
|
||||
offError = controller.ble.on('error', (error) => {
|
||||
try { console.debug('[BLE][ERR]', error?.message || error?.errMsg || String(error)) } catch (e) {}
|
||||
onError(error)
|
||||
})
|
||||
}
|
||||
|
|
@ -562,7 +568,7 @@ export function createBluetoothDiscovery(options = {}) {
|
|||
|
||||
/**
|
||||
* 设备搜索:
|
||||
* 优先按 0x00F4/0xF400 过滤;若无结果自动切到兜底扫描并轮询 getBluetoothDevices。
|
||||
* 优先按 0xFFF4/0x00F4/0xF400 过滤;若无结果自动切到兜底扫描并轮询 getBluetoothDevices。
|
||||
*/
|
||||
async function startSearch() {
|
||||
if (isSearching) {
|
||||
|
|
@ -751,7 +757,9 @@ export function createBluetoothDiscovery(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() {
|
||||
|
|
|
|||
|
|
@ -338,8 +338,21 @@ export function createBleCore(options = {}) {
|
|||
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) {
|
||||
if (!deviceId) {
|
||||
|
|
@ -349,10 +362,11 @@ export function createBleCore(options = {}) {
|
|||
try {
|
||||
const result = await promisifyUniApi('getBLEDeviceServices', { deviceId })
|
||||
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) {
|
||||
throw createBleError(10004, '设备未暴露目标服务 0x00F4')
|
||||
throw createBleError(10004, '设备未暴露目标服务(00F4/F400/FFF4)')
|
||||
}
|
||||
|
||||
patchState({ serviceId: targetService.uuid })
|
||||
|
|
|
|||
|
|
@ -127,11 +127,11 @@ export function createUnifiedBleController(options = {}) {
|
|||
const chars = await ble.discoverCharacteristics()
|
||||
|
||||
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) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ function generateRandomKey(length = 16) {
|
|||
return result
|
||||
}
|
||||
|
||||
const authLog = (...args) => { try { console.debug('[BLE][AUTH]', ...args) } catch (e) {} }
|
||||
|
||||
function normalizeMacForKey(macText = '') {
|
||||
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 = {}) {
|
||||
const keyValue = await writePacketsToKeyCharacteristic(ble, keyText)
|
||||
|
||||
const stateEvent = await waitForProtocolState(ble, {
|
||||
const waitPromise = waitForProtocolState(ble, {
|
||||
allowedCodes: expectedStateCodes,
|
||||
rejectOnUnexpected: true,
|
||||
timeout: options.timeout || 6000,
|
||||
timeoutMessage: '等待 UUID_STATE 超时',
|
||||
unexpectedMessage: '密钥流程状态异常'
|
||||
})
|
||||
|
||||
const stateCode = Number(stateEvent.stateCode)
|
||||
|
||||
authLog('STATE wait before writeKey, expect=', expectedStateCodes)
|
||||
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.setAuthState({
|
||||
keyMatched: true,
|
||||
lastStateCode: stateCode
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stateCode,
|
||||
key: keyValue
|
||||
}
|
||||
ble.setAuthState({ keyMatched: true, lastStateCode: stateCode })
|
||||
return { success: true, stateCode, key: keyValue }
|
||||
}
|
||||
|
||||
async function ensureAuthorizedWithDeviceInfo(ble, info, keyText, options = {}) {
|
||||
|
|
|
|||
|
|
@ -39,44 +39,24 @@ function requireAuthorized(ble) {
|
|||
* WiFi 配网模块:负责打包发送 ssid|password,并等待 STATE 回执。
|
||||
*/
|
||||
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 = {}) {
|
||||
const profile = ble.getProfile()
|
||||
const targetSsid = String(ssid || '').trim()
|
||||
const targetPass = String(password || '').trim()
|
||||
|
||||
validateWifi(profile, targetSsid, targetPass)
|
||||
requireAuthorized(ble)
|
||||
|
||||
const { wifi } = ble.getState().characteristics
|
||||
if (!wifi || !wifi.uuid) {
|
||||
throw new Error('未发现 UUID_WIFI 特征值')
|
||||
const s = String(ssid || '').trim(), p = String(password || '').trim()
|
||||
validateWifi(profile, s, p); requireAuthorized(ble)
|
||||
const { wifi } = ble.getState().characteristics; if (!wifi || !wifi.uuid) throw new Error('未发现 UUID_WIFI 特征值')
|
||||
const waitPromise = waitForProtocolState(ble, { allowedCodes: WIFI_FINAL_STATE_CODES, rejectOnUnexpected: false, timeout: options.timeout || 12000, timeoutMessage: '等待 WiFi 状态超时' })
|
||||
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 sent = await writeA7Payload(ble, wifi.uuid, `${s}|${p}`, profile.packet, { packetInterval: Number(options.packetInterval) || 0 })
|
||||
log('write WiFi done, packets=', sent)
|
||||
const code = Number((await waitPromise).stateCode); log('STATE(final) received for WiFi=', code)
|
||||
return { success: code === 3, stateCode: code }
|
||||
}
|
||||
|
||||
const payload = `${targetSsid}|${targetPass}`
|
||||
|
||||
// 协议要求 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
|
||||
}
|
||||
return { configureWifi, requireAuthorized }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue