refactor(ble): BLE 模块重构与协议时序修复
- 从 git 追踪中移除 .idea、.claude/rules、CLAUDE.md、AGENTS.md - 更新 .gitignore,添加 prettier 配置 - BLE 核心模块重构(bleCore/controller/auth/radar/wifi/errors/eventBus/stateWait) - 修复协议时序:订阅 F301 前先读 F501(readMsgOnce) - 设备配置页优化:鉴权失败改用模态弹窗,WiFi 错误持久展示 - 密钥流程已废弃,鉴权失败不阻断后续配网流程 - pages.json 格式化(tab → 空格)
This commit is contained in:
parent
cc02eb5998
commit
82b287dd92
|
|
@ -1,67 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -3,3 +3,23 @@ unpackage/
|
|||
.hbuilderx/
|
||||
.DS_Store
|
||||
.codex-tasks/
|
||||
|
||||
.idea/
|
||||
|
||||
.vscode/
|
||||
.prettierignore
|
||||
.prettierrc
|
||||
|
||||
# Graphify (generated)
|
||||
graphify-out/
|
||||
|
||||
# Claude Code && Codex
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
|
||||
# Codegraph
|
||||
.codegraph/
|
||||
|
||||
# Project-specific
|
||||
docs/superpowers/
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/smarthome.iml" filepath="$PROJECT_DIR$/.idea/smarthome.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
{
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -799,7 +799,15 @@ export function createBluetoothDiscovery(options = {}) {
|
|||
*/
|
||||
async function prepareWifiConfig(deviceId, options = {}) {
|
||||
await connectDevice(deviceId)
|
||||
return authorizeDeviceByMac(options)
|
||||
|
||||
// 厂家确认:密钥流程已不启用,写 KEY 后设备不回 STATE,鉴权必然超时。
|
||||
// 这里尽力尝试鉴权,失败则忽略并继续,由 WiFi 配网直接走(写 F401 后等 STATE=3)。
|
||||
try {
|
||||
return await authorizeDeviceByMac(options)
|
||||
} catch (error) {
|
||||
console.debug('[BLE][AUTH]', '鉴权失败已忽略(密钥流程已废弃)', error?.message || error)
|
||||
return { mode: 'AUTH_SKIPPED', error: error?.message || String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
async function readDeviceInfo() {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@
|
|||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
"usingComponents" : true,
|
||||
"libVersion" : "latest"
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
|
|
|
|||
144
pages.json
144
pages.json
|
|
@ -1,74 +1,74 @@
|
|||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/device/add",
|
||||
"style": {
|
||||
"navigationBarTitleText": "添加设备"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/device/config",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "智慧养老",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F1F1F4"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#666666",
|
||||
"selectedColor": "#2CCB98",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/event/index",
|
||||
"text": "事件"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/index",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {},
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^u-(.*)": "uview-plus/components/u-$1/u-$1.vue",
|
||||
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue"
|
||||
}
|
||||
}
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/device/add",
|
||||
"style": {
|
||||
"navigationBarTitleText": "添加设备"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/device/config",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "智慧养老",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F1F1F4"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#666666",
|
||||
"selectedColor": "#2CCB98",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/event/index",
|
||||
"text": "事件"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/index",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {},
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^u-(.*)": "uview-plus/components/u-$1/u-$1.vue",
|
||||
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@
|
|||
一键导入上次 WiFi 配置
|
||||
</view>
|
||||
|
||||
<view v-if="wifiErrorTip" class="wifi-error-tip">
|
||||
{{ wifiErrorTip }}
|
||||
</view>
|
||||
|
||||
<view class="wifi-submit-btn" :class="{ disabled: wifiSending }" @click="confirmWifiSettings">
|
||||
{{ wifiSending ? '发送中...' : '发送' }}
|
||||
</view>
|
||||
|
|
@ -155,6 +159,7 @@ export default {
|
|||
wifiSending: false,
|
||||
fetchingCurrentWifi: false,
|
||||
wifiPrepared: false,
|
||||
wifiErrorTip: '',
|
||||
devicePreparingMap: {},
|
||||
selectedWifiDevice: null,
|
||||
wifiForm: {
|
||||
|
|
@ -216,6 +221,7 @@ export default {
|
|||
this.wifiPreparing = false
|
||||
this.activePreparingDeviceId = ''
|
||||
this.fetchingCurrentWifi = false
|
||||
this.wifiErrorTip = ''
|
||||
this.selectedWifiDevice = null
|
||||
this.resetWifiForm()
|
||||
},
|
||||
|
|
@ -329,6 +335,7 @@ export default {
|
|||
mask: true
|
||||
})
|
||||
|
||||
let authError = null
|
||||
try {
|
||||
/**
|
||||
* 先按项目固定已知密钥规则(MAC+0000)完成鉴权,成功后再允许进入 WiFi 弹层。
|
||||
|
|
@ -343,16 +350,29 @@ export default {
|
|||
this.wifiPanelVisible = true
|
||||
// 确保 Vue 完成 DOM 更新后再调原生 toast,避免原生层覆盖 Vue 弹层
|
||||
await this.$nextTick()
|
||||
showToast('密钥鉴权成功,请填写WiFi信息')
|
||||
} catch (error) {
|
||||
showToast(getErrorMessage(error))
|
||||
authError = error
|
||||
await this.safeDisconnect()
|
||||
} finally {
|
||||
this.setDevicePreparing(targetDeviceId, false)
|
||||
this.activePreparingDeviceId = ''
|
||||
this.wifiPreparing = false
|
||||
// showLoading 与 showToast 共用同一遮罩层,必须先 hideLoading 再弹提示,
|
||||
// 否则 hideLoading 会把 catch 里的 toast 一起清掉(表现为弹窗瞬间消失)。
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
if (authError) {
|
||||
// 鉴权失败用模态弹窗,需用户主动确认,确保错误诊断信息可见可读
|
||||
uni.showModal({
|
||||
title: '密钥鉴权失败',
|
||||
content: getErrorMessage(authError),
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
} else if (this.wifiPanelVisible) {
|
||||
showToast('密钥鉴权成功,请填写WiFi信息')
|
||||
}
|
||||
},
|
||||
|
||||
async closeWifiPanel(force = false) {
|
||||
|
|
@ -461,6 +481,7 @@ export default {
|
|||
}
|
||||
|
||||
this.wifiSending = true
|
||||
this.wifiErrorTip = ''
|
||||
|
||||
let needAutoClose = false
|
||||
let needCacheWifi = false
|
||||
|
|
@ -472,7 +493,7 @@ export default {
|
|||
* 才进行提示并自动关闭弹层。
|
||||
*/
|
||||
const wifiResult = await this.discoveryController.configureWifi(ssid, password, {
|
||||
timeout: 12000
|
||||
timeout: 60000
|
||||
})
|
||||
|
||||
const stateMessage = this.getWifiStateMessage(wifiResult.stateCode)
|
||||
|
|
@ -480,8 +501,9 @@ export default {
|
|||
needAutoClose = true
|
||||
needCacheWifi = Boolean(wifiResult.success)
|
||||
} catch (error) {
|
||||
// 超时/异常场景表示尚未拿到有效设备结果,保持弹层打开。
|
||||
// 超时/异常场景:错误信息持久展示在弹层内,避免瞬态 toast 被覆盖看不清
|
||||
tipMessage = getErrorMessage(error)
|
||||
this.wifiErrorTip = tipMessage
|
||||
} finally {
|
||||
this.wifiSending = false
|
||||
|
||||
|
|
@ -830,6 +852,16 @@ export default {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.wifi-error-tip {
|
||||
min-height: 80rpx;
|
||||
padding: 16rpx 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: #d33b1d;
|
||||
background: #fff3f1;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.wifi-submit-btn {
|
||||
height: 100rpx;
|
||||
background: #11c462;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { createEventBus } from './eventBus'
|
||||
import { createBleState, BLE_STAGE } from './state'
|
||||
import { createBleError, getBleErrorText, normalizeBleError } from './errors'
|
||||
import { uint8ArrayToArrayBuffer, toUint8Array } from '../utils/bytes'
|
||||
import { toFullUuid, uuidEquals, uuidInList } from '../utils/uuid'
|
||||
import { runWithRetry } from './retry'
|
||||
import { getBleProfile, DEVICE_PROFILE } from '../protocols/profiles'
|
||||
import { createEventBus } from './eventBus';
|
||||
import { createBleState, BLE_STAGE } from './state';
|
||||
import { createBleError, getBleErrorText, normalizeBleError } from './errors';
|
||||
import { uint8ArrayToArrayBuffer, toUint8Array } from '../utils/bytes';
|
||||
import { toFullUuid, uuidEquals, uuidInList } from '../utils/uuid';
|
||||
import { runWithRetry } from './retry';
|
||||
import { getBleProfile, DEVICE_PROFILE } from '../protocols/profiles';
|
||||
|
||||
// 把 uni 的 success/fail 回调风格统一成 Promise,便于串联 async 流程。
|
||||
function promisifyUniApi(apiName, params = {}) {
|
||||
|
|
@ -12,9 +12,9 @@ function promisifyUniApi(apiName, params = {}) {
|
|||
uni[apiName]({
|
||||
...params,
|
||||
success: (res) => resolve(res),
|
||||
fail: (err) => reject(err)
|
||||
})
|
||||
})
|
||||
fail: (err) => reject(err),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -26,19 +26,19 @@ export function createBleCore(options = {}) {
|
|||
reconnect = {
|
||||
enabled: true,
|
||||
retries: 2,
|
||||
delay: 1200
|
||||
delay: 1200,
|
||||
},
|
||||
logger = console,
|
||||
profileId = DEVICE_PROFILE.UNKNOWN
|
||||
} = options
|
||||
profileId = DEVICE_PROFILE.UNKNOWN,
|
||||
} = options;
|
||||
|
||||
const eventBus = createEventBus()
|
||||
const state = createBleState()
|
||||
let currentProfile = getBleProfile(profileId)
|
||||
let deviceFoundHandler = null
|
||||
let adapterStateHandler = null
|
||||
let connectionStateHandler = null
|
||||
let characteristicValueHandler = null
|
||||
const eventBus = createEventBus();
|
||||
const state = createBleState();
|
||||
let currentProfile = getBleProfile(profileId);
|
||||
let deviceFoundHandler = null;
|
||||
let adapterStateHandler = null;
|
||||
let connectionStateHandler = null;
|
||||
let characteristicValueHandler = null;
|
||||
|
||||
// 返回状态快照,避免调用方直接改写内部状态对象。
|
||||
function snapshotState() {
|
||||
|
|
@ -46,74 +46,74 @@ export function createBleCore(options = {}) {
|
|||
...state,
|
||||
auth: { ...state.auth },
|
||||
deviceMap: { ...state.deviceMap },
|
||||
characteristics: { ...state.characteristics }
|
||||
}
|
||||
characteristics: { ...state.characteristics },
|
||||
};
|
||||
}
|
||||
|
||||
// 统一 patch 状态并广播 state:change,保证 UI 状态来源单一。
|
||||
function patchState(partialState) {
|
||||
Object.assign(state, partialState)
|
||||
eventBus.emit('state:change', snapshotState())
|
||||
Object.assign(state, partialState);
|
||||
eventBus.emit('state:change', snapshotState());
|
||||
}
|
||||
|
||||
function setStage(stage) {
|
||||
patchState({ stage })
|
||||
patchState({ stage });
|
||||
}
|
||||
|
||||
function setProfile(nextProfileId) {
|
||||
currentProfile = getBleProfile(nextProfileId)
|
||||
patchState({ profileId: currentProfile.id })
|
||||
currentProfile = getBleProfile(nextProfileId);
|
||||
patchState({ profileId: currentProfile.id });
|
||||
}
|
||||
|
||||
function getProfile() {
|
||||
return currentProfile
|
||||
return currentProfile;
|
||||
}
|
||||
|
||||
function setAuthState(partialAuth = {}) {
|
||||
patchState({
|
||||
auth: {
|
||||
...state.auth,
|
||||
...partialAuth
|
||||
}
|
||||
})
|
||||
...partialAuth,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function emit(eventName, payload) {
|
||||
eventBus.emit(eventName, payload)
|
||||
eventBus.emit(eventName, payload);
|
||||
}
|
||||
|
||||
function emitError(error) {
|
||||
const normalized = normalizeBleError(error)
|
||||
patchState({ lastError: normalized })
|
||||
eventBus.emit('error', normalized)
|
||||
return normalized
|
||||
const normalized = normalizeBleError(error);
|
||||
patchState({ lastError: normalized });
|
||||
eventBus.emit('error', normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** 打开蓝牙适配器,成功后标记可用并进入 ADAPTER_OPENED 阶段。 */
|
||||
async function openAdapter() {
|
||||
try {
|
||||
await promisifyUniApi('openBluetoothAdapter')
|
||||
patchState({ available: true })
|
||||
setStage(BLE_STAGE.ADAPTER_OPENED)
|
||||
return true
|
||||
await promisifyUniApi('openBluetoothAdapter');
|
||||
patchState({ available: true });
|
||||
setStage(BLE_STAGE.ADAPTER_OPENED);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw emitError(error)
|
||||
throw emitError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭蓝牙适配器并重置全部运行时状态。 */
|
||||
async function closeAdapter() {
|
||||
try {
|
||||
await promisifyUniApi('closeBluetoothAdapter')
|
||||
await promisifyUniApi('closeBluetoothAdapter');
|
||||
} catch (error) {
|
||||
logger.warn('[BLE] close adapter failed', error)
|
||||
logger.warn('[BLE] close adapter failed', error);
|
||||
}
|
||||
|
||||
// 重置原生 handler 引用,以便 openAdapter 后 init() 可重新注册
|
||||
adapterStateHandler = null
|
||||
connectionStateHandler = null
|
||||
characteristicValueHandler = null
|
||||
deviceFoundHandler = null
|
||||
adapterStateHandler = null;
|
||||
connectionStateHandler = null;
|
||||
characteristicValueHandler = null;
|
||||
deviceFoundHandler = null;
|
||||
|
||||
patchState({
|
||||
stage: BLE_STAGE.IDLE,
|
||||
|
|
@ -122,56 +122,56 @@ export function createBleCore(options = {}) {
|
|||
connected: false,
|
||||
deviceId: '',
|
||||
serviceId: '',
|
||||
characteristics: {}
|
||||
})
|
||||
characteristics: {},
|
||||
});
|
||||
}
|
||||
|
||||
/** 读取当前适配器状态(可用/搜索中),同步到内部状态。 */
|
||||
async function getAdapterState() {
|
||||
try {
|
||||
const result = await promisifyUniApi('getBluetoothAdapterState')
|
||||
const result = await promisifyUniApi('getBluetoothAdapterState');
|
||||
patchState({
|
||||
available: Boolean(result.available),
|
||||
discovering: Boolean(result.discovering)
|
||||
})
|
||||
return result
|
||||
discovering: Boolean(result.discovering),
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw emitError(error)
|
||||
throw emitError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 注册适配器状态变更监听,蓝牙关闭时自动切换到 DISCONNECTED。 */
|
||||
function watchAdapterState() {
|
||||
if (adapterStateHandler) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
adapterStateHandler = (result) => {
|
||||
patchState({
|
||||
available: Boolean(result.available),
|
||||
discovering: Boolean(result.discovering)
|
||||
})
|
||||
eventBus.emit('adapter:state', result)
|
||||
discovering: Boolean(result.discovering),
|
||||
});
|
||||
eventBus.emit('adapter:state', result);
|
||||
|
||||
if (!result.available) {
|
||||
setStage(BLE_STAGE.DISCONNECTED)
|
||||
setStage(BLE_STAGE.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
uni.onBluetoothAdapterStateChange(adapterStateHandler)
|
||||
uni.onBluetoothAdapterStateChange(adapterStateHandler);
|
||||
}
|
||||
|
||||
/** 移除适配器状态变更监听。 */
|
||||
function unwatchAdapterState() {
|
||||
if (!adapterStateHandler) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (uni.offBluetoothAdapterStateChange) {
|
||||
uni.offBluetoothAdapterStateChange(adapterStateHandler)
|
||||
uni.offBluetoothAdapterStateChange(adapterStateHandler);
|
||||
}
|
||||
|
||||
adapterStateHandler = null
|
||||
adapterStateHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -183,89 +183,93 @@ export function createBleCore(options = {}) {
|
|||
services = [],
|
||||
allowDuplicatesKey = true,
|
||||
interval = 0,
|
||||
powerLevel = 'high'
|
||||
} = params
|
||||
powerLevel = 'high',
|
||||
} = params;
|
||||
|
||||
try {
|
||||
if (!state.available) {
|
||||
await openAdapter()
|
||||
await openAdapter();
|
||||
}
|
||||
|
||||
const options = {
|
||||
allowDuplicatesKey,
|
||||
interval
|
||||
}
|
||||
interval,
|
||||
};
|
||||
|
||||
if (services.length) {
|
||||
options.services = services.map((uuid) => toFullUuid(uuid) || uuid)
|
||||
options.services = services.map((uuid) => toFullUuid(uuid) || uuid);
|
||||
}
|
||||
|
||||
if (powerLevel) {
|
||||
options.powerLevel = powerLevel
|
||||
options.powerLevel = powerLevel;
|
||||
}
|
||||
|
||||
await promisifyUniApi('startBluetoothDevicesDiscovery', options)
|
||||
patchState({ discovering: true, deviceMap: {} })
|
||||
setStage(BLE_STAGE.DISCOVERING)
|
||||
return true
|
||||
await promisifyUniApi('startBluetoothDevicesDiscovery', options);
|
||||
patchState({ discovering: true, deviceMap: {} });
|
||||
setStage(BLE_STAGE.DISCOVERING);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw emitError(error)
|
||||
throw emitError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止设备扫描。 */
|
||||
async function stopDiscovery() {
|
||||
try {
|
||||
await promisifyUniApi('stopBluetoothDevicesDiscovery')
|
||||
await promisifyUniApi('stopBluetoothDevicesDiscovery');
|
||||
} catch (error) {
|
||||
logger.warn('[BLE] stop discovery failed', error)
|
||||
logger.warn('[BLE] stop discovery failed', error);
|
||||
}
|
||||
|
||||
patchState({ discovering: false })
|
||||
patchState({ discovering: false });
|
||||
}
|
||||
|
||||
/** 注册设备发现监听,按 deviceId 去重合并后广播 device:found。 */
|
||||
function watchDeviceFound() {
|
||||
if (deviceFoundHandler) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
deviceFoundHandler = (result) => {
|
||||
const list = Array.isArray(result.devices) ? result.devices : [result]
|
||||
const nextMap = { ...state.deviceMap }
|
||||
const list = Array.isArray(result.devices) ? result.devices : [result];
|
||||
const nextMap = { ...state.deviceMap };
|
||||
|
||||
// 基于 deviceId 做去重与增量更新,避免列表无限叠加。
|
||||
list.forEach((device) => {
|
||||
if (!device || !device.deviceId) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = nextMap[device.deviceId] || {}
|
||||
const previous = nextMap[device.deviceId] || {};
|
||||
nextMap[device.deviceId] = {
|
||||
...previous,
|
||||
...device,
|
||||
showName: device.name || device.localName || previous.showName || '未命名设备'
|
||||
}
|
||||
})
|
||||
showName:
|
||||
device.name ||
|
||||
device.localName ||
|
||||
previous.showName ||
|
||||
'未命名设备',
|
||||
};
|
||||
});
|
||||
|
||||
patchState({ deviceMap: nextMap })
|
||||
eventBus.emit('device:found', Object.values(nextMap))
|
||||
}
|
||||
patchState({ deviceMap: nextMap });
|
||||
eventBus.emit('device:found', Object.values(nextMap));
|
||||
};
|
||||
|
||||
uni.onBluetoothDeviceFound(deviceFoundHandler)
|
||||
uni.onBluetoothDeviceFound(deviceFoundHandler);
|
||||
}
|
||||
|
||||
/** 移除设备发现监听。 */
|
||||
function unwatchDeviceFound() {
|
||||
if (!deviceFoundHandler) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (uni.offBluetoothDeviceFound) {
|
||||
uni.offBluetoothDeviceFound(deviceFoundHandler)
|
||||
uni.offBluetoothDeviceFound(deviceFoundHandler);
|
||||
}
|
||||
|
||||
deviceFoundHandler = null
|
||||
deviceFoundHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -277,102 +281,104 @@ export function createBleCore(options = {}) {
|
|||
*/
|
||||
async function connect(deviceId, options = {}) {
|
||||
if (!deviceId) {
|
||||
throw emitError(createBleError(10013, 'deviceId 不能为空'))
|
||||
throw emitError(createBleError(10013, 'deviceId 不能为空'));
|
||||
}
|
||||
|
||||
const {
|
||||
timeout = 15000,
|
||||
autoReconnect = reconnect.enabled
|
||||
} = options
|
||||
const { timeout = 15000, autoReconnect = reconnect.enabled } = options;
|
||||
|
||||
const runConnect = async () => {
|
||||
setStage(BLE_STAGE.CONNECTING)
|
||||
await promisifyUniApi('createBLEConnection', { deviceId, timeout })
|
||||
patchState({ connected: true, deviceId })
|
||||
setStage(BLE_STAGE.CONNECTED)
|
||||
return true
|
||||
}
|
||||
setStage(BLE_STAGE.CONNECTING);
|
||||
await promisifyUniApi('createBLEConnection', { deviceId, timeout });
|
||||
patchState({ connected: true, deviceId });
|
||||
setStage(BLE_STAGE.CONNECTED);
|
||||
return true;
|
||||
};
|
||||
|
||||
try {
|
||||
if (!autoReconnect) {
|
||||
return await runConnect()
|
||||
return await runConnect();
|
||||
}
|
||||
|
||||
return await runWithRetry(runConnect, {
|
||||
retries: reconnect.retries,
|
||||
delay: reconnect.delay,
|
||||
shouldRetry: (error) => {
|
||||
const normalized = normalizeBleError(error)
|
||||
return [10003, 10006, 10012].includes(normalized.code)
|
||||
}
|
||||
})
|
||||
const normalized = normalizeBleError(error);
|
||||
return [10003, 10006, 10012].includes(normalized.code);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
throw emitError(error)
|
||||
throw emitError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 断开BLE连接并重置连接相关状态。 */
|
||||
async function disconnect(deviceId = state.deviceId) {
|
||||
if (!deviceId) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await promisifyUniApi('closeBLEConnection', { deviceId })
|
||||
await promisifyUniApi('closeBLEConnection', { deviceId });
|
||||
} catch (error) {
|
||||
logger.warn('[BLE] close connection failed', error)
|
||||
logger.warn('[BLE] close connection failed', error);
|
||||
}
|
||||
|
||||
patchState({ connected: false, deviceId: '', serviceId: '', characteristics: {} })
|
||||
setStage(BLE_STAGE.DISCONNECTED)
|
||||
patchState({
|
||||
connected: false,
|
||||
deviceId: '',
|
||||
serviceId: '',
|
||||
characteristics: {},
|
||||
});
|
||||
setStage(BLE_STAGE.DISCONNECTED);
|
||||
}
|
||||
|
||||
/** 注册连接状态变更监听,断开时自动切换到 DISCONNECTED。 */
|
||||
function watchConnectionChange() {
|
||||
if (connectionStateHandler) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
connectionStateHandler = (result) => {
|
||||
if (state.deviceId && result.deviceId !== state.deviceId) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
patchState({ connected: Boolean(result.connected) })
|
||||
eventBus.emit('connection:change', result)
|
||||
patchState({ connected: Boolean(result.connected) });
|
||||
eventBus.emit('connection:change', result);
|
||||
|
||||
if (!result.connected) {
|
||||
setStage(BLE_STAGE.DISCONNECTED)
|
||||
setStage(BLE_STAGE.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
uni.onBLEConnectionStateChange(connectionStateHandler)
|
||||
uni.onBLEConnectionStateChange(connectionStateHandler);
|
||||
}
|
||||
|
||||
/** 移除连接状态变更监听。 */
|
||||
function unwatchConnectionChange() {
|
||||
if (!connectionStateHandler) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (uni.offBLEConnectionStateChange) {
|
||||
uni.offBLEConnectionStateChange(connectionStateHandler)
|
||||
uni.offBLEConnectionStateChange(connectionStateHandler);
|
||||
}
|
||||
|
||||
connectionStateHandler = null
|
||||
connectionStateHandler = null;
|
||||
}
|
||||
|
||||
/** 获取候选服务UUID列表:profile主服务 + 兼容固件的 00F4/F400/FFF4。 */
|
||||
function getServiceCandidates() {
|
||||
const profileService = currentProfile?.uuids?.service
|
||||
const profileService = currentProfile?.uuids?.service;
|
||||
const candidates = [
|
||||
profileService,
|
||||
toFullUuid('00F4'),
|
||||
toFullUuid('F400'),
|
||||
toFullUuid('FFF4')
|
||||
]
|
||||
toFullUuid('FFF4'),
|
||||
];
|
||||
|
||||
return candidates.filter(Boolean)
|
||||
return candidates.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -381,27 +387,31 @@ export function createBleCore(options = {}) {
|
|||
*/
|
||||
async function discoverServices(deviceId = state.deviceId) {
|
||||
if (!deviceId) {
|
||||
throw emitError(createBleError(10013, 'discoverServices 缺少 deviceId'))
|
||||
throw emitError(createBleError(10013, 'discoverServices 缺少 deviceId'));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await promisifyUniApi('getBLEDeviceServices', { deviceId })
|
||||
const services = result.services || []
|
||||
const candidates = getServiceCandidates()
|
||||
const targetService = services.find((service) => candidates.some((uuid) => uuidEquals(service.uuid, uuid)))
|
||||
const result = await promisifyUniApi('getBLEDeviceServices', {
|
||||
deviceId,
|
||||
});
|
||||
const services = result.services || [];
|
||||
const candidates = getServiceCandidates();
|
||||
const targetService = services.find((service) =>
|
||||
candidates.some((uuid) => uuidEquals(service.uuid, uuid)),
|
||||
);
|
||||
|
||||
if (!targetService) {
|
||||
throw createBleError(10004, '设备未暴露目标服务(00F4/F400/FFF4)')
|
||||
throw createBleError(10004, '设备未暴露目标服务(00F4/F400/FFF4)');
|
||||
}
|
||||
|
||||
patchState({ serviceId: targetService.uuid })
|
||||
setStage(BLE_STAGE.SERVICE_DISCOVERED)
|
||||
patchState({ serviceId: targetService.uuid });
|
||||
setStage(BLE_STAGE.SERVICE_DISCOVERED);
|
||||
return {
|
||||
services,
|
||||
targetService
|
||||
}
|
||||
targetService,
|
||||
};
|
||||
} catch (error) {
|
||||
throw emitError(error)
|
||||
throw emitError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,86 +421,134 @@ export function createBleCore(options = {}) {
|
|||
* @returns {object} characteristicMap 各业务特征 + all 全量列表
|
||||
*/
|
||||
async function discoverCharacteristics(options = {}) {
|
||||
const deviceId = options.deviceId || state.deviceId
|
||||
const serviceId = options.serviceId || state.serviceId
|
||||
const deviceId = options.deviceId || state.deviceId;
|
||||
const serviceId = options.serviceId || state.serviceId;
|
||||
|
||||
if (!deviceId || !serviceId) {
|
||||
throw emitError(createBleError(10013, 'discoverCharacteristics 缺少 deviceId/serviceId'))
|
||||
throw emitError(
|
||||
createBleError(
|
||||
10013,
|
||||
'discoverCharacteristics 缺少 deviceId/serviceId',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await promisifyUniApi('getBLEDeviceCharacteristics', { deviceId, serviceId })
|
||||
const list = result.characteristics || []
|
||||
const profile = getProfile()
|
||||
const result = await promisifyUniApi('getBLEDeviceCharacteristics', {
|
||||
deviceId,
|
||||
serviceId,
|
||||
});
|
||||
const list = result.characteristics || [];
|
||||
const profile = getProfile();
|
||||
|
||||
// 缓存业务侧常用特征,减少后续每次操作时的查找成本。
|
||||
const characteristicMap = {
|
||||
wifi: list.find((item) => uuidEquals(item.uuid, profile.uuids.wifi)) || null,
|
||||
key: list.find((item) => uuidEquals(item.uuid, profile.uuids.key)) || null,
|
||||
cmd: list.find((item) => uuidEquals(item.uuid, profile.uuids.cmd)) || null,
|
||||
msg: list.find((item) => uuidEquals(item.uuid, profile.uuids.msg)) || null,
|
||||
state: list.find((item) => uuidEquals(item.uuid, profile.uuids.state)) || null,
|
||||
radar: list.find((item) => uuidEquals(item.uuid, profile.uuids.radar)) || null,
|
||||
all: list
|
||||
}
|
||||
wifi:
|
||||
list.find((item) => uuidEquals(item.uuid, profile.uuids.wifi)) ||
|
||||
null,
|
||||
key:
|
||||
list.find((item) => uuidEquals(item.uuid, profile.uuids.key)) || null,
|
||||
cmd:
|
||||
list.find((item) => uuidEquals(item.uuid, profile.uuids.cmd)) || null,
|
||||
msg:
|
||||
list.find((item) => uuidEquals(item.uuid, profile.uuids.msg)) || null,
|
||||
state:
|
||||
list.find((item) => uuidEquals(item.uuid, profile.uuids.state)) ||
|
||||
null,
|
||||
radar:
|
||||
list.find((item) => uuidEquals(item.uuid, profile.uuids.radar)) ||
|
||||
null,
|
||||
all: list,
|
||||
};
|
||||
|
||||
patchState({ characteristics: characteristicMap })
|
||||
return characteristicMap
|
||||
patchState({ characteristics: characteristicMap });
|
||||
return characteristicMap;
|
||||
} catch (error) {
|
||||
throw emitError(error)
|
||||
throw emitError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取指定特征值(触发 onBLECharacteristicValueChange 回调)。 */
|
||||
async function readCharacteristic(characteristicId, options = {}) {
|
||||
const deviceId = options.deviceId || state.deviceId
|
||||
const serviceId = options.serviceId || state.serviceId
|
||||
const deviceId = options.deviceId || state.deviceId;
|
||||
const serviceId = options.serviceId || state.serviceId;
|
||||
|
||||
if (!deviceId || !serviceId || !characteristicId) {
|
||||
throw emitError(createBleError(10013, 'readCharacteristic 参数缺失'))
|
||||
throw emitError(createBleError(10013, 'readCharacteristic 参数缺失'));
|
||||
}
|
||||
|
||||
try {
|
||||
await promisifyUniApi('readBLECharacteristicValue', {
|
||||
deviceId,
|
||||
serviceId,
|
||||
characteristicId
|
||||
})
|
||||
return true
|
||||
characteristicId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw emitError(error)
|
||||
throw emitError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 向指定特征值写入数据,自动将输入转为 ArrayBuffer。 */
|
||||
async function writeCharacteristic(characteristicId, value, options = {}) {
|
||||
const deviceId = options.deviceId || state.deviceId
|
||||
const serviceId = options.serviceId || state.serviceId
|
||||
const deviceId = options.deviceId || state.deviceId;
|
||||
const serviceId = options.serviceId || state.serviceId;
|
||||
|
||||
if (!deviceId || !serviceId || !characteristicId) {
|
||||
throw emitError(createBleError(10013, 'writeCharacteristic 参数缺失'))
|
||||
throw emitError(createBleError(10013, 'writeCharacteristic 参数缺失'));
|
||||
}
|
||||
|
||||
// 取特征 properties,用于诊断写入类型是否匹配,并按需指定 writeType
|
||||
const chars = state.characteristics || {};
|
||||
const matched = Object.values(chars).find(
|
||||
(item) => item && uuidEquals(item.uuid, characteristicId),
|
||||
);
|
||||
const props = matched?.properties || {};
|
||||
const supportsWrite = Boolean(props.write);
|
||||
const supportsWriteNoResp = Boolean(props.writeWithoutResponse);
|
||||
// 默认优先有响应写入(可靠);仅当不支持有响应时退化为无响应写入
|
||||
const writeType =
|
||||
options.writeType ||
|
||||
(supportsWrite
|
||||
? 'write'
|
||||
: supportsWriteNoResp
|
||||
? 'writeWithoutResponse'
|
||||
: undefined);
|
||||
|
||||
try {
|
||||
await promisifyUniApi('writeBLECharacteristicValue', {
|
||||
console.debug('[BLE][WRITE]', characteristicId, {
|
||||
writeType,
|
||||
supportsWrite,
|
||||
supportsWriteNoResp,
|
||||
valueLen: value?.length
|
||||
});
|
||||
const params = {
|
||||
deviceId,
|
||||
serviceId,
|
||||
characteristicId,
|
||||
value: uint8ArrayToArrayBuffer(toUint8Array(value))
|
||||
})
|
||||
return true
|
||||
value: uint8ArrayToArrayBuffer(toUint8Array(value)),
|
||||
};
|
||||
if (writeType) {
|
||||
params.writeType = writeType;
|
||||
}
|
||||
await promisifyUniApi('writeBLECharacteristicValue', params);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw emitError(error)
|
||||
throw emitError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 订阅/取消订阅指定特征值的通知。 */
|
||||
async function notifyCharacteristic(characteristicId, stateFlag = true, options = {}) {
|
||||
const deviceId = options.deviceId || state.deviceId
|
||||
const serviceId = options.serviceId || state.serviceId
|
||||
async function notifyCharacteristic(
|
||||
characteristicId,
|
||||
stateFlag = true,
|
||||
options = {},
|
||||
) {
|
||||
const deviceId = options.deviceId || state.deviceId;
|
||||
const serviceId = options.serviceId || state.serviceId;
|
||||
|
||||
if (!deviceId || !serviceId || !characteristicId) {
|
||||
throw emitError(createBleError(10013, 'notifyCharacteristic 参数缺失'))
|
||||
throw emitError(createBleError(10013, 'notifyCharacteristic 参数缺失'));
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -498,67 +556,80 @@ export function createBleCore(options = {}) {
|
|||
deviceId,
|
||||
serviceId,
|
||||
characteristicId,
|
||||
state: Boolean(stateFlag)
|
||||
})
|
||||
return true
|
||||
state: Boolean(stateFlag),
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw emitError(error)
|
||||
throw emitError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function watchCharacteristicValue() {
|
||||
if (characteristicValueHandler) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// 统一转为 Uint8Array,简化上层解析逻辑。
|
||||
characteristicValueHandler = (result) => {
|
||||
const value = toUint8Array(result.value);
|
||||
// 原始 trace:定位 notify 是否投递时,确认每个特征值变更的来源与内容
|
||||
try {
|
||||
const hex = Array.from(value)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join(' ');
|
||||
console.debug('[BLE][RAW]', result.characteristicId, 'len=', value.length, 'hex=', hex);
|
||||
} catch (e) {
|
||||
}
|
||||
eventBus.emit('characteristic:value', {
|
||||
...result,
|
||||
value: toUint8Array(result.value)
|
||||
})
|
||||
}
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
uni.onBLECharacteristicValueChange(characteristicValueHandler)
|
||||
uni.onBLECharacteristicValueChange(characteristicValueHandler);
|
||||
}
|
||||
|
||||
/** 移除特征值变更监听。 */
|
||||
function unwatchCharacteristicValue() {
|
||||
if (!characteristicValueHandler) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (uni.offBLECharacteristicValueChange) {
|
||||
uni.offBLECharacteristicValueChange(characteristicValueHandler)
|
||||
uni.offBLECharacteristicValueChange(characteristicValueHandler);
|
||||
}
|
||||
|
||||
characteristicValueHandler = null
|
||||
characteristicValueHandler = null;
|
||||
}
|
||||
|
||||
/** 返回已发现设备列表。 */
|
||||
function listDiscoveredDevices() {
|
||||
return Object.values(state.deviceMap)
|
||||
return Object.values(state.deviceMap);
|
||||
}
|
||||
|
||||
/** 从已发现设备中,按广播服务UUID匹配并返回第一个设备。 */
|
||||
function pickDeviceByService(serviceUuid = currentProfile.uuids.service) {
|
||||
const list = listDiscoveredDevices()
|
||||
const list = listDiscoveredDevices();
|
||||
|
||||
return list.find((device) => uuidInList(serviceUuid, device.advertisServiceUUIDs || [])) || null
|
||||
return (
|
||||
list.find((device) =>
|
||||
uuidInList(serviceUuid, device.advertisServiceUUIDs || []),
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
// 清理顺序:先停扫描与连接,再移除监听,最后关闭适配器。
|
||||
await stopDiscovery()
|
||||
await disconnect()
|
||||
await stopDiscovery();
|
||||
await disconnect();
|
||||
|
||||
unwatchDeviceFound()
|
||||
unwatchAdapterState()
|
||||
unwatchConnectionChange()
|
||||
unwatchCharacteristicValue()
|
||||
unwatchDeviceFound();
|
||||
unwatchAdapterState();
|
||||
unwatchConnectionChange();
|
||||
unwatchCharacteristicValue();
|
||||
|
||||
await closeAdapter()
|
||||
eventBus.clear()
|
||||
await closeAdapter();
|
||||
eventBus.clear();
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -593,6 +664,6 @@ export function createBleCore(options = {}) {
|
|||
on: eventBus.on,
|
||||
once: eventBus.once,
|
||||
cleanup,
|
||||
getBleErrorText
|
||||
}
|
||||
getBleErrorText,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,49 @@
|
|||
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'
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建特征值通知分发器。
|
||||
|
|
@ -13,52 +52,61 @@ import {uuidEquals} from '../utils/uuid'
|
|||
*/
|
||||
function createCharacteristicDispatcher(ble) {
|
||||
return (event) => {
|
||||
const state = ble.getState()
|
||||
const profile = ble.getProfile()
|
||||
const current = state.characteristics
|
||||
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)
|
||||
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
|
||||
})
|
||||
keyMatched: stateCode === 7 || stateCode === 5 || state.auth.keyMatched,
|
||||
});
|
||||
|
||||
ble.emit('protocol:state', {
|
||||
...event,
|
||||
profileId: profile.id,
|
||||
stateCode
|
||||
})
|
||||
return
|
||||
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
|
||||
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)
|
||||
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
|
||||
profileId: ble.getProfile().id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ble.emit('protocol:raw', {
|
||||
...event,
|
||||
profileId: profile.id
|
||||
})
|
||||
}
|
||||
profileId: profile.id,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,40 +115,43 @@ function createCharacteristicDispatcher(ble) {
|
|||
* @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 ble = createBleCore(options);
|
||||
const auth = createAuthModule(ble);
|
||||
const wifi = createWifiModule(ble);
|
||||
const radar = createRadarModule(ble);
|
||||
|
||||
const characteristicDispatcher = createCharacteristicDispatcher(ble)
|
||||
let offCharacteristic = null
|
||||
const characteristicDispatcher = createCharacteristicDispatcher(ble);
|
||||
let offCharacteristic = null;
|
||||
|
||||
/** 绑定特征值分发器到 ble 事件总线。 */
|
||||
function bindDispatcher() {
|
||||
if (offCharacteristic) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
offCharacteristic = ble.on('characteristic:value', characteristicDispatcher)
|
||||
offCharacteristic = ble.on(
|
||||
'characteristic:value',
|
||||
characteristicDispatcher,
|
||||
);
|
||||
}
|
||||
|
||||
/** 解绑特征值分发器。 */
|
||||
function unbindDispatcher() {
|
||||
if (!offCharacteristic) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
offCharacteristic()
|
||||
offCharacteristic = null
|
||||
offCharacteristic();
|
||||
offCharacteristic = null;
|
||||
}
|
||||
|
||||
/** 初始化蓝牙适配器并注册所有监听(适配器状态/连接/特征值/分发器)。 */
|
||||
async function init() {
|
||||
await ble.openAdapter()
|
||||
ble.watchAdapterState()
|
||||
ble.watchConnectionChange()
|
||||
ble.watchCharacteristicValue()
|
||||
bindDispatcher()
|
||||
await ble.openAdapter();
|
||||
ble.watchAdapterState();
|
||||
ble.watchConnectionChange();
|
||||
ble.watchCharacteristicValue();
|
||||
bindDispatcher();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -113,94 +164,110 @@ export function createUnifiedBleController(options = {}) {
|
|||
* @returns {object|null} 匹配到的设备对象,未找到返回 null
|
||||
*/
|
||||
async function scanAndPickDevice({
|
||||
serviceUuid,
|
||||
scanDuration = 5000,
|
||||
fallbackWithoutFilter = true
|
||||
} = {}) {
|
||||
const profile = ble.getProfile()
|
||||
const targetService = serviceUuid || profile.uuids.service
|
||||
serviceUuid,
|
||||
scanDuration = 5000,
|
||||
fallbackWithoutFilter = true,
|
||||
} = {}) {
|
||||
const profile = ble.getProfile();
|
||||
const targetService = serviceUuid || profile.uuids.service;
|
||||
|
||||
ble.watchDeviceFound()
|
||||
ble.watchDeviceFound();
|
||||
|
||||
await ble.startDiscovery({services: [targetService]})
|
||||
await new Promise((resolve) => setTimeout(resolve, scanDuration))
|
||||
await ble.stopDiscovery()
|
||||
await ble.startDiscovery({ services: [targetService] });
|
||||
await new Promise((resolve) => setTimeout(resolve, scanDuration));
|
||||
await ble.stopDiscovery();
|
||||
|
||||
let selected = ble.pickDeviceByService(targetService)
|
||||
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
|
||||
await ble.startDiscovery({ services: [] });
|
||||
await new Promise((resolve) => setTimeout(resolve, scanDuration));
|
||||
await ble.stopDiscovery();
|
||||
selected =
|
||||
ble.pickDeviceByService(targetService) ||
|
||||
ble.listDiscoveredDevices()[0] ||
|
||||
null;
|
||||
}
|
||||
|
||||
return selected
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接后严格执行协议前置:
|
||||
* - 发现服务和特征
|
||||
* - 订阅 UUID_STATE / UUID_RADAR
|
||||
* 连接后严格执行协议前置(厂家要求顺序):
|
||||
* 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()
|
||||
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)
|
||||
console.debug('[BLE][SUB]', 'notify STATE ok', chars.state.uuid);
|
||||
} catch (e) {
|
||||
console.debug('[BLE][SUB]', 'notify STATE fail', e?.message || 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)
|
||||
console.debug('[BLE][SUB]', 'notify RADAR ok', chars.radar.uuid);
|
||||
} catch (e) {
|
||||
console.debug('[BLE][SUB]', 'notify RADAR fail', e?.message || e)
|
||||
console.debug('[BLE][SUB]', 'notify RADAR fail', e?.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
return chars
|
||||
return chars;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准流程:连接 -> 订阅 -> 鉴权。
|
||||
* 鉴权内部会按 bindStatus 分支处理绑定/匹配。
|
||||
*/
|
||||
async function standardConnectFlow({deviceId, key}) {
|
||||
await init()
|
||||
async function standardConnectFlow({ deviceId, key }) {
|
||||
await init();
|
||||
|
||||
let targetDeviceId = deviceId
|
||||
let targetDeviceId = deviceId;
|
||||
if (!targetDeviceId) {
|
||||
const device = await scanAndPickDevice()
|
||||
const device = await scanAndPickDevice();
|
||||
if (!device) {
|
||||
throw new Error('未找到可连接设备')
|
||||
throw new Error('未找到可连接设备');
|
||||
}
|
||||
|
||||
targetDeviceId = device.deviceId
|
||||
targetDeviceId = device.deviceId;
|
||||
}
|
||||
|
||||
await connectAndDiscover(targetDeviceId)
|
||||
const authResult = await auth.ensureAuthorized(key)
|
||||
await connectAndDiscover(targetDeviceId);
|
||||
const authResult = await auth.ensureAuthorized(key);
|
||||
|
||||
return {
|
||||
deviceId: targetDeviceId,
|
||||
authResult,
|
||||
state: ble.getState(),
|
||||
profile: ble.getProfile()
|
||||
}
|
||||
profile: ble.getProfile(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 释放所有资源:解绑分发器 + 清理核心BLE(停扫描/断连接/移监听/关适配器)。 */
|
||||
async function cleanup() {
|
||||
unbindDispatcher()
|
||||
await ble.cleanup()
|
||||
unbindDispatcher();
|
||||
await ble.cleanup();
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -212,6 +279,6 @@ export function createUnifiedBleController(options = {}) {
|
|||
scanAndPickDevice,
|
||||
connectAndDiscover,
|
||||
standardConnectFlow,
|
||||
cleanup
|
||||
}
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,18 +20,18 @@ const ERROR_MESSAGES = Object.freeze({
|
|||
10017: 'startBluetoothDevicesDiscovery: 未找到蓝牙适配器',
|
||||
10018: 'stopBluetoothDevicesDiscovery: 调用失败',
|
||||
10019: 'getBluetoothDevices: 调用失败',
|
||||
10020: 'getConnectedBluetoothDevices: 调用失败'
|
||||
})
|
||||
10020: 'getConnectedBluetoothDevices: 调用失败',
|
||||
});
|
||||
|
||||
/**
|
||||
* 构造统一 BLE Error,方便上层直接读取 code/message。
|
||||
*/
|
||||
export function createBleError(code, message, payload = {}) {
|
||||
const err = new Error(message || ERROR_MESSAGES[code] || '蓝牙操作失败')
|
||||
err.name = 'BleError'
|
||||
err.code = code
|
||||
Object.assign(err, payload)
|
||||
return err
|
||||
const err = new Error(message || ERROR_MESSAGES[code] || '蓝牙操作失败');
|
||||
err.name = 'BleError';
|
||||
err.code = code;
|
||||
Object.assign(err, payload);
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -39,16 +39,16 @@ export function createBleError(code, message, payload = {}) {
|
|||
*/
|
||||
export function normalizeBleError(error, fallbackMessage = '蓝牙操作失败') {
|
||||
if (!error) {
|
||||
return createBleError(-1, fallbackMessage)
|
||||
return createBleError(-1, fallbackMessage);
|
||||
}
|
||||
|
||||
if (error.name === 'BleError') {
|
||||
return error
|
||||
return error;
|
||||
}
|
||||
|
||||
const errCode = typeof error.errCode === 'number' ? error.errCode : -1
|
||||
const message = error.errMsg || ERROR_MESSAGES[errCode] || fallbackMessage
|
||||
return createBleError(errCode, message, { raw: error })
|
||||
const errCode = typeof error.errCode === 'number' ? error.errCode : -1;
|
||||
const message = error.errMsg || ERROR_MESSAGES[errCode] || fallbackMessage;
|
||||
return createBleError(errCode, message, { raw: error });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -56,8 +56,8 @@ export function normalizeBleError(error, fallbackMessage = '蓝牙操作失败')
|
|||
*/
|
||||
export function getBleErrorText(error, fallbackText = '蓝牙操作失败') {
|
||||
if (!error) {
|
||||
return fallbackText
|
||||
return fallbackText;
|
||||
}
|
||||
|
||||
return error.message || ERROR_MESSAGES[error.code] || fallbackText
|
||||
return error.message || ERROR_MESSAGES[error.code] || fallbackText;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,61 +2,61 @@
|
|||
* 轻量事件总线:用于 BLE Core 与业务模块之间的事件解耦。
|
||||
*/
|
||||
export function createEventBus() {
|
||||
const listeners = {}
|
||||
const listeners = {};
|
||||
|
||||
/** 订阅事件,返回取消订阅函数。 */
|
||||
function on(eventName, handler) {
|
||||
if (!listeners[eventName]) {
|
||||
listeners[eventName] = new Set()
|
||||
listeners[eventName] = new Set();
|
||||
}
|
||||
|
||||
listeners[eventName].add(handler)
|
||||
listeners[eventName].add(handler);
|
||||
|
||||
// 返回取消订阅函数,方便调用方在页面卸载时回收监听。
|
||||
return () => {
|
||||
off(eventName, handler)
|
||||
}
|
||||
off(eventName, handler);
|
||||
};
|
||||
}
|
||||
|
||||
/** 单次订阅:触发一次后自动取消。 */
|
||||
function once(eventName, handler) {
|
||||
const unsubscribe = on(eventName, (payload) => {
|
||||
unsubscribe()
|
||||
handler(payload)
|
||||
})
|
||||
unsubscribe();
|
||||
handler(payload);
|
||||
});
|
||||
|
||||
return unsubscribe
|
||||
return unsubscribe;
|
||||
}
|
||||
|
||||
/** 取消指定事件的某个监听。 */
|
||||
function off(eventName, handler) {
|
||||
if (!listeners[eventName]) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
listeners[eventName].delete(handler)
|
||||
listeners[eventName].delete(handler);
|
||||
}
|
||||
|
||||
/** 广播事件,所有监听器同步执行(内部已 try-catch 隔离)。 */
|
||||
function emit(eventName, payload) {
|
||||
if (!listeners[eventName]) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
listeners[eventName].forEach((handler) => {
|
||||
try {
|
||||
handler(payload)
|
||||
handler(payload);
|
||||
} catch (error) {
|
||||
console.error('[BLE event handler error]', eventName, error)
|
||||
console.error('[BLE event handler error]', eventName, error);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/** 清空所有事件监听。 */
|
||||
function clear() {
|
||||
Object.keys(listeners).forEach((eventName) => {
|
||||
listeners[eventName].clear()
|
||||
})
|
||||
listeners[eventName].clear();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -64,6 +64,6 @@ export function createEventBus() {
|
|||
once,
|
||||
off,
|
||||
emit,
|
||||
clear
|
||||
}
|
||||
clear,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
* @param {number} options.timeout 超时时间(ms)
|
||||
* @param {string} options.timeoutMessage 超时提示
|
||||
* @param {string} options.unexpectedMessage 非预期状态提示前缀
|
||||
* @param {number[]} options.refreshOnCodes 收到这些中间态状态码时重置超时计时器
|
||||
* @param {number} options.refreshTimeout 重置后的窗口时长(ms),未传则回退到 timeout
|
||||
*
|
||||
* 中间态刷新机制:设备连接 WiFi 时会先发 STATE=4(连接中) 再发最终态,
|
||||
* 若总超时短于真实 DHCP 时长会误判超时。收到 refreshOnCodes 内的中间态时
|
||||
* 重置倒计时,最多延长至 refreshTimeout 指定的单次窗口时长。
|
||||
*/
|
||||
export function waitForProtocolState(ble, options = {}) {
|
||||
const {
|
||||
|
|
@ -15,14 +21,20 @@ export function waitForProtocolState(ble, options = {}) {
|
|||
rejectOnUnexpected = false,
|
||||
timeout = 6000,
|
||||
timeoutMessage = '等待 UUID_STATE 超时',
|
||||
unexpectedMessage = '收到非预期 UUID_STATE 状态码'
|
||||
unexpectedMessage = '收到非预期 UUID_STATE 状态码',
|
||||
refreshOnCodes = [],
|
||||
refreshTimeout = timeout
|
||||
} = options
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timer = null
|
||||
let off = null
|
||||
try {
|
||||
console.debug('[BLE][STATEWAIT]', 'start', {allowed: allowedCodes.join(',') || 'ANY', timeout})
|
||||
console.debug('[BLE][STATEWAIT]', 'start', {
|
||||
allowed: allowedCodes.join(',') || 'ANY',
|
||||
timeout,
|
||||
refreshOn: refreshOnCodes.join(',') || 'NONE'
|
||||
})
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
|
|
@ -38,13 +50,20 @@ export function waitForProtocolState(ble, options = {}) {
|
|||
callback()
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
try {
|
||||
console.debug('[BLE][STATEWAIT]', 'timeout')
|
||||
} catch (e) {
|
||||
const armTimer = (duration) => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
finish(() => reject(new Error(timeoutMessage)))
|
||||
}, timeout)
|
||||
timer = setTimeout(() => {
|
||||
try {
|
||||
console.debug('[BLE][STATEWAIT]', 'timeout')
|
||||
} catch (e) {
|
||||
}
|
||||
finish(() => reject(new Error(timeoutMessage)))
|
||||
}, duration)
|
||||
}
|
||||
|
||||
armTimer(timeout)
|
||||
|
||||
off = ble.on('protocol:state', (payload) => {
|
||||
const stateCode = Number(payload?.stateCode)
|
||||
|
|
@ -58,6 +77,16 @@ export function waitForProtocolState(ble, options = {}) {
|
|||
return
|
||||
}
|
||||
|
||||
// 命中中间态刷新:重置超时窗口,继续等待后续最终态
|
||||
if (refreshOnCodes.length && refreshOnCodes.includes(stateCode)) {
|
||||
try {
|
||||
console.debug('[BLE][STATEWAIT]', 'refresh', stateCode)
|
||||
} catch (e) {
|
||||
}
|
||||
armTimer(refreshTimeout)
|
||||
return
|
||||
}
|
||||
|
||||
if (rejectOnUnexpected) {
|
||||
try {
|
||||
console.debug('[BLE][STATEWAIT]', 'reject', stateCode)
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
import {writeA7Payload} from '../core/packet'
|
||||
import {waitForProtocolState} from '../core/stateWait'
|
||||
import {parseDeviceInfo} from '../parsers'
|
||||
import {BLE_STAGE} from '../core/state'
|
||||
import { writeA7Payload } from '../core/packet';
|
||||
import { waitForProtocolState } from '../core/stateWait';
|
||||
import { parseDeviceInfo } from '../parsers';
|
||||
import { BLE_STAGE } from '../core/state';
|
||||
|
||||
/** 生成指定长度的随机密钥(排除易混淆字符 0/O/1/I/l)。 */
|
||||
function generateRandomKey(length = 16) {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'
|
||||
let result = ''
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
|
||||
let result = '';
|
||||
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const random = Math.floor(Math.random() * chars.length)
|
||||
result += chars[random]
|
||||
const random = Math.floor(Math.random() * chars.length);
|
||||
result += chars[random];
|
||||
}
|
||||
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
const authLog = (...args) => {
|
||||
try {
|
||||
console.debug('[BLE][AUTH]', ...args)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
console.debug('[BLE][AUTH]', ...args);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
/** 将 MAC 地址去除非十六进制字符并转大写,用于密钥拼接。 */
|
||||
function normalizeMacForKey(macText = '') {
|
||||
return String(macText || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
|
||||
return String(macText || '')
|
||||
.replace(/[^0-9a-fA-F]/g, '')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -33,23 +34,23 @@ function normalizeMacForKey(macText = '') {
|
|||
* 默认后缀为 "0000",对应 12 位 MAC + 4 位后缀 = 16 位密钥。
|
||||
*/
|
||||
export function buildKeyFromMac(macText, suffix = '0000', expectedLength = 16) {
|
||||
const normalizedMac = normalizeMacForKey(macText)
|
||||
const normalizedSuffix = String(suffix || '').trim()
|
||||
const normalizedMac = normalizeMacForKey(macText);
|
||||
const normalizedSuffix = String(suffix || '').trim();
|
||||
|
||||
if (!normalizedSuffix) {
|
||||
throw new Error('密钥后缀不能为空')
|
||||
throw new Error('密钥后缀不能为空');
|
||||
}
|
||||
|
||||
const requiredMacLength = expectedLength - normalizedSuffix.length
|
||||
const requiredMacLength = expectedLength - normalizedSuffix.length;
|
||||
if (requiredMacLength <= 0) {
|
||||
throw new Error('密钥长度配置异常')
|
||||
throw new Error('密钥长度配置异常');
|
||||
}
|
||||
|
||||
// if (normalizedMac.length !== requiredMacLength) {
|
||||
// throw new Error(`设备 MAC 长度异常,期望 ${requiredMacLength} 位,实际 ${normalizedMac.length} 位`)
|
||||
// }
|
||||
|
||||
return `${normalizedMac}${normalizedSuffix}`
|
||||
return `${normalizedMac}${normalizedSuffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,22 +60,22 @@ export function buildKeyFromMac(macText, suffix = '0000', expectedLength = 16) {
|
|||
* @returns {string} 实际写入的密钥值
|
||||
*/
|
||||
async function writePacketsToKeyCharacteristic(ble, keyText) {
|
||||
const {key} = ble.getState().characteristics
|
||||
const { key } = ble.getState().characteristics;
|
||||
|
||||
if (!key || !key.uuid) {
|
||||
throw new Error('未发现 UUID_KEY 特征值')
|
||||
throw new Error('未发现 UUID_KEY 特征值');
|
||||
}
|
||||
|
||||
const profile = ble.getProfile()
|
||||
const keyValue = String(keyText || '').trim()
|
||||
const profile = ble.getProfile();
|
||||
const keyValue = String(keyText || '').trim();
|
||||
|
||||
// if (keyValue.length !== profile.auth.keyLength) {
|
||||
// throw new Error(`密钥长度必须为 ${profile.auth.keyLength}`)
|
||||
// }
|
||||
|
||||
await writeA7Payload(ble, key.uuid, keyValue, profile.packet)
|
||||
await writeA7Payload(ble, key.uuid, keyValue, profile.packet);
|
||||
|
||||
return keyValue
|
||||
return keyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -85,7 +86,12 @@ async function writePacketsToKeyCharacteristic(ble, keyText) {
|
|||
* @param {number[]} expectedStateCodes 期望的状态码列表
|
||||
* @param {object} options 超时等配置
|
||||
*/
|
||||
async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options = {}) {
|
||||
async function writeKeyAndExpectStates(
|
||||
ble,
|
||||
keyText,
|
||||
expectedStateCodes,
|
||||
options = {},
|
||||
) {
|
||||
// 设备订阅 STATE 后可能先发送 -1(未写密钥)/0(空闲) 等中间态,
|
||||
// 若 rejectOnUnexpected:true 会导致中间态直接 reject,阻断鉴权流程。
|
||||
const waitPromise = waitForProtocolState(ble, {
|
||||
|
|
@ -93,16 +99,32 @@ async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options
|
|||
rejectOnUnexpected: false,
|
||||
timeout: options.timeout || 6000,
|
||||
timeoutMessage: '等待 UUID_STATE 超时',
|
||||
unexpectedMessage: '密钥流程状态异常'
|
||||
})
|
||||
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}
|
||||
unexpectedMessage: '密钥流程状态异常',
|
||||
});
|
||||
authLog('STATE wait before writeKey, expect=', expectedStateCodes);
|
||||
const keyValue = await writePacketsToKeyCharacteristic(ble, keyText);
|
||||
authLog('key written, length=', keyValue);
|
||||
try {
|
||||
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 };
|
||||
} catch (error) {
|
||||
// 鉴权超时多为黑盒:密钥不匹配(STATE=8)、F301 订阅失败、或写入丢失均表现为超时。
|
||||
// 这里带上绑定状态与设备最后上报的 STATE,区分根因方向。
|
||||
const authState = ble.getState().auth || {};
|
||||
const lastState = authState.lastStateCode;
|
||||
const bound = authState.bound;
|
||||
const expectedStr = expectedStateCodes.join('/');
|
||||
const boundHint = (bound === null || bound === undefined)
|
||||
? '绑定状态未知'
|
||||
: `绑定状态=${bound}`;
|
||||
const lastHint = (lastState === null || lastState === undefined)
|
||||
? '设备未上报任何 STATE(可能 F301 订阅失败或密钥写入丢失)'
|
||||
: `设备最后上报 STATE=${lastState}(期望 ${expectedStr},可能密钥不匹配)`;
|
||||
throw new Error(`密钥鉴权超时(${boundHint},${lastHint})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -111,56 +133,77 @@ async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options
|
|||
* - bindStatus=1(已绑定):使用已知密钥匹配,等待 STATE=7
|
||||
* - 其他状态:尝试用提供的密钥匹配,等待 STATE=7 或 5
|
||||
*/
|
||||
async function ensureAuthorizedWithDeviceInfo(ble, info, keyText, options = {}) {
|
||||
const bindStatus = info.bindStatus !== null ? Number(info.bindStatus) : null
|
||||
const profile = ble.getProfile()
|
||||
async function ensureAuthorizedWithDeviceInfo(
|
||||
ble,
|
||||
info,
|
||||
keyText,
|
||||
options = {},
|
||||
) {
|
||||
const bindStatus = info.bindStatus !== null ? Number(info.bindStatus) : null;
|
||||
const profile = ble.getProfile();
|
||||
|
||||
ble.setAuthState({
|
||||
bound: Number.isNaN(bindStatus) ? null : bindStatus,
|
||||
keyMatched: false
|
||||
})
|
||||
keyMatched: false,
|
||||
});
|
||||
|
||||
if (bindStatus === 0) {
|
||||
const keyToBind = String(keyText || '').trim() || generateRandomKey(profile.auth.keyLength)
|
||||
const authResult = await writeKeyAndExpectStates(ble, keyToBind, [5], options)
|
||||
const keyToBind =
|
||||
String(keyText || '').trim() || generateRandomKey(profile.auth.keyLength);
|
||||
const authResult = await writeKeyAndExpectStates(
|
||||
ble,
|
||||
keyToBind,
|
||||
[5],
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
mode: 'BIND',
|
||||
deviceInfo: info,
|
||||
authResult,
|
||||
key: keyToBind
|
||||
}
|
||||
key: keyToBind,
|
||||
};
|
||||
}
|
||||
|
||||
if (bindStatus === 1) {
|
||||
const keyToMatch = String(keyText || '').trim()
|
||||
const keyToMatch = String(keyText || '').trim();
|
||||
if (!keyToMatch) {
|
||||
throw new Error('设备已绑定,必须提供已绑定密钥用于匹配')
|
||||
throw new Error('设备已绑定,必须提供已绑定密钥用于匹配');
|
||||
}
|
||||
|
||||
const authResult = await writeKeyAndExpectStates(ble, keyToMatch, [7], options)
|
||||
const authResult = await writeKeyAndExpectStates(
|
||||
ble,
|
||||
keyToMatch,
|
||||
[7],
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
mode: 'MATCH',
|
||||
deviceInfo: info,
|
||||
authResult,
|
||||
key: keyToMatch
|
||||
}
|
||||
key: keyToMatch,
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackKey = String(keyText || '').trim()
|
||||
const fallbackKey = String(keyText || '').trim();
|
||||
if (!fallbackKey) {
|
||||
throw new Error(`无法识别的绑定状态: ${info.bindStatus},且未提供密钥`)
|
||||
throw new Error(`无法识别的绑定状态: ${info.bindStatus},且未提供密钥`);
|
||||
}
|
||||
|
||||
const fallbackResult = await writeKeyAndExpectStates(ble, fallbackKey, [7, 5], options)
|
||||
const fallbackResult = await writeKeyAndExpectStates(
|
||||
ble,
|
||||
fallbackKey,
|
||||
[7, 5],
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
mode: 'UNKNOWN_BIND_STATE_FALLBACK',
|
||||
deviceInfo: info,
|
||||
authResult: fallbackResult,
|
||||
key: fallbackKey
|
||||
}
|
||||
key: fallbackKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,35 +216,48 @@ export function createAuthModule(ble) {
|
|||
* 先订阅 protocol:msg 事件,再触发 read,保证不丢通知。
|
||||
*/
|
||||
async function readDeviceInfo(options = {}) {
|
||||
const {msg} = ble.getState().characteristics
|
||||
const { msg } = ble.getState().characteristics;
|
||||
|
||||
if (!msg || !msg.uuid) {
|
||||
throw new Error('未发现 UUID_MSG 特征值')
|
||||
throw new Error('未发现 UUID_MSG 特征值');
|
||||
}
|
||||
|
||||
const timeout = Number(options.msgTimeout) || 4000
|
||||
let timer = null
|
||||
let off = null
|
||||
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()
|
||||
}
|
||||
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)
|
||||
})
|
||||
off = ble.on('protocol:msg', (payload) => finish(() => resolve(payload)));
|
||||
timer = setTimeout(
|
||||
() => finish(() => reject(new Error('读取 UUID_MSG 超时'))),
|
||||
timeout,
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await ble.readCharacteristic(msg.uuid)
|
||||
const payload = await valuePromise
|
||||
return parseDeviceInfo(payload.value, ble.getProfile().id)
|
||||
await ble.readCharacteristic(msg.uuid);
|
||||
const payload = await valuePromise;
|
||||
const info = parseDeviceInfo(payload.value, ble.getProfile().id);
|
||||
// 打印 MSG 原始字节与解析结果,验证 bindStatus/MAC 是否解析正确
|
||||
try {
|
||||
const rawBytes = Array.from(payload.value || []);
|
||||
const hex = rawBytes
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join(' ');
|
||||
console.debug('[BLE][AUTH]', 'MSG raw hex=', hex, 'parsed=', JSON.stringify(info));
|
||||
} catch (e) {
|
||||
}
|
||||
return info;
|
||||
} catch (error) {
|
||||
if (timer) clearTimeout(timer)
|
||||
if (off) off()
|
||||
throw error
|
||||
if (timer) clearTimeout(timer);
|
||||
if (off) off();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,9 +268,9 @@ export function createAuthModule(ble) {
|
|||
* 3) bindStatus=1 写已知密钥并等待 STATE=7。
|
||||
*/
|
||||
async function ensureAuthorized(keyText, options = {}) {
|
||||
ble.setStage(BLE_STAGE.AUTHORIZING)
|
||||
const info = await readDeviceInfo(options)
|
||||
return ensureAuthorizedWithDeviceInfo(ble, info, keyText, options)
|
||||
ble.setStage(BLE_STAGE.AUTHORIZING);
|
||||
const info = await readDeviceInfo(options);
|
||||
return ensureAuthorizedWithDeviceInfo(ble, info, keyText, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -222,19 +278,28 @@ export function createAuthModule(ble) {
|
|||
* 用于”先鉴权再进行 WiFi 配置”的页面流程。
|
||||
*/
|
||||
async function ensureAuthorizedByMac(options = {}) {
|
||||
ble.setStage(BLE_STAGE.AUTHORIZING)
|
||||
ble.setStage(BLE_STAGE.AUTHORIZING);
|
||||
|
||||
const info = await readDeviceInfo(options)
|
||||
const profile = ble.getProfile()
|
||||
const keyByMac = buildKeyFromMac(info.mac, options.suffix || '0000', profile.auth.keyLength)
|
||||
const info = await readDeviceInfo(options);
|
||||
const profile = ble.getProfile();
|
||||
const keyByMac = buildKeyFromMac(
|
||||
info.mac,
|
||||
options.suffix || '0000',
|
||||
profile.auth.keyLength,
|
||||
);
|
||||
|
||||
const authResult = await ensureAuthorizedWithDeviceInfo(ble, info, keyByMac, options)
|
||||
const authResult = await ensureAuthorizedWithDeviceInfo(
|
||||
ble,
|
||||
info,
|
||||
keyByMac,
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
...authResult,
|
||||
key: keyByMac,
|
||||
keyRule: 'MAC+0000'
|
||||
}
|
||||
keyRule: 'MAC+0000',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -242,6 +307,6 @@ export function createAuthModule(ble) {
|
|||
ensureAuthorized,
|
||||
ensureAuthorizedByMac,
|
||||
buildKeyFromMac,
|
||||
generateRandomKey
|
||||
}
|
||||
generateRandomKey,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { parseRadarData } from '../parsers'
|
||||
import { parseRadarData } from '../parsers';
|
||||
|
||||
/**
|
||||
* 雷达控制模块:管理雷达启停、窄床模式、跌倒参数写入。
|
||||
|
|
@ -7,11 +7,11 @@ import { parseRadarData } from '../parsers'
|
|||
*/
|
||||
export function createRadarModule(ble) {
|
||||
function requireAuthorized() {
|
||||
const state = ble.getState()
|
||||
const state = ble.getState();
|
||||
|
||||
// 协议要求:获取雷达校准数据前必须先完成密钥匹配。
|
||||
if (!state.auth || state.auth.keyMatched !== true) {
|
||||
throw new Error('请先完成密钥匹配,再执行雷达命令')
|
||||
throw new Error('请先完成密钥匹配,再执行雷达命令');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -20,33 +20,33 @@ export function createRadarModule(ble) {
|
|||
* @param {number} commandByte 指令字节(如 0xA1 启动、0xA2 停止)
|
||||
*/
|
||||
async function sendCommand(commandByte) {
|
||||
const { cmd } = ble.getState().characteristics
|
||||
const { cmd } = ble.getState().characteristics;
|
||||
|
||||
if (!cmd || !cmd.uuid) {
|
||||
throw new Error('未发现 UUID_CMD 特征值')
|
||||
throw new Error('未发现 UUID_CMD 特征值');
|
||||
}
|
||||
|
||||
await ble.writeCharacteristic(cmd.uuid, new Uint8Array([commandByte]))
|
||||
await ble.writeCharacteristic(cmd.uuid, new Uint8Array([commandByte]));
|
||||
}
|
||||
|
||||
/** 启动雷达数据流(写入 CMD = 0xA1)。 */
|
||||
async function startRadarStream() {
|
||||
const profile = ble.getProfile()
|
||||
const profile = ble.getProfile();
|
||||
|
||||
// 协议1.2.2:先密钥匹配,再向 UUID_CMD 写 0xA1。
|
||||
requireAuthorized()
|
||||
await sendCommand(profile.radar.startCommand)
|
||||
return true
|
||||
requireAuthorized();
|
||||
await sendCommand(profile.radar.startCommand);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 停止雷达数据流(写入 CMD = 0xA2)。 */
|
||||
async function stopRadarStream() {
|
||||
const profile = ble.getProfile()
|
||||
const profile = ble.getProfile();
|
||||
|
||||
// 停止同样要求处于鉴权通过态,避免误发到未鉴权连接。
|
||||
requireAuthorized()
|
||||
await sendCommand(profile.radar.stopCommand)
|
||||
return true
|
||||
requireAuthorized();
|
||||
await sendCommand(profile.radar.stopCommand);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -55,20 +55,23 @@ export function createRadarModule(ble) {
|
|||
* @param {boolean} enabled true=开启,false=关闭
|
||||
*/
|
||||
async function setNarrowMode(enabled) {
|
||||
const profile = ble.getProfile()
|
||||
const profile = ble.getProfile();
|
||||
|
||||
if (!profile.cmd.supportsNarrowMode) {
|
||||
throw new Error('当前设备不支持窄床模式设置')
|
||||
throw new Error('当前设备不支持窄床模式设置');
|
||||
}
|
||||
|
||||
requireAuthorized()
|
||||
requireAuthorized();
|
||||
|
||||
// 协议规定:写入 [0x7C, flag] 两字节
|
||||
const { cmd } = ble.getState().characteristics
|
||||
const { cmd } = ble.getState().characteristics;
|
||||
if (!cmd || !cmd.uuid) {
|
||||
throw new Error('未发现 UUID_CMD 特征值')
|
||||
throw new Error('未发现 UUID_CMD 特征值');
|
||||
}
|
||||
await ble.writeCharacteristic(cmd.uuid, new Uint8Array([0x7C, enabled ? 1 : 0]))
|
||||
await ble.writeCharacteristic(
|
||||
cmd.uuid,
|
||||
new Uint8Array([0x7c, enabled ? 1 : 0]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -76,31 +79,32 @@ export function createRadarModule(ble) {
|
|||
* @param {Uint8Array|number[]} rawParams 跌倒参数字节
|
||||
*/
|
||||
async function writeFallParams(rawParams) {
|
||||
const profile = ble.getProfile()
|
||||
const profile = ble.getProfile();
|
||||
|
||||
if (!profile.cmd.supportsFallParam67) {
|
||||
throw new Error('当前设备不支持 0x67 跌倒参数写入')
|
||||
throw new Error('当前设备不支持 0x67 跌倒参数写入');
|
||||
}
|
||||
|
||||
requireAuthorized()
|
||||
requireAuthorized();
|
||||
|
||||
const payload = rawParams instanceof Uint8Array ? rawParams : new Uint8Array(rawParams)
|
||||
const packet = new Uint8Array(1 + payload.length)
|
||||
packet[0] = 0x67
|
||||
packet.set(payload, 1)
|
||||
const payload =
|
||||
rawParams instanceof Uint8Array ? rawParams : new Uint8Array(rawParams);
|
||||
const packet = new Uint8Array(1 + payload.length);
|
||||
packet[0] = 0x67;
|
||||
packet.set(payload, 1);
|
||||
|
||||
const { cmd } = ble.getState().characteristics
|
||||
const { cmd } = ble.getState().characteristics;
|
||||
if (!cmd || !cmd.uuid) {
|
||||
throw new Error('未发现 UUID_CMD 特征值')
|
||||
throw new Error('未发现 UUID_CMD 特征值');
|
||||
}
|
||||
|
||||
await ble.writeCharacteristic(cmd.uuid, packet)
|
||||
await ble.writeCharacteristic(cmd.uuid, packet);
|
||||
}
|
||||
|
||||
/** 按当前 profile 解析雷达通知数据。 */
|
||||
function parseNotifyPayload(payload) {
|
||||
const profile = ble.getProfile()
|
||||
return parseRadarData(payload, profile.id)
|
||||
const profile = ble.getProfile();
|
||||
return parseRadarData(payload, profile.id);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -110,6 +114,6 @@ export function createRadarModule(ble) {
|
|||
setNarrowMode,
|
||||
writeFallParams,
|
||||
parseNotifyPayload,
|
||||
requireAuthorized
|
||||
}
|
||||
requireAuthorized,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@ import { waitForProtocolState } from '../core/stateWait'
|
|||
|
||||
const WIFI_FINAL_STATE_CODES = Object.freeze([1, 2, 3])
|
||||
|
||||
// STATE=4 为 WiFi 连接中中间态,收到后用于刷新等待窗口
|
||||
const WIFI_CONNECTING_STATE_CODES = Object.freeze([4])
|
||||
|
||||
// 总等待上限:覆盖弱信号/5G/企业级路由真实 DHCP 时长
|
||||
const WIFI_TOTAL_TIMEOUT_MS = 60000
|
||||
// 收到 STATE=4 后的单次刷新窗口
|
||||
const WIFI_REFRESH_TIMEOUT_MS = 30000
|
||||
// ED713 分包写入间隔:连续 writeWithoutResponse 易丢包,固件需留处理时间
|
||||
const ED713_PACKET_INTERVAL_MS = 50
|
||||
|
||||
/**
|
||||
* 校验 WiFi 入参,规则来源于 profile.wifi。
|
||||
*/
|
||||
|
|
@ -27,12 +37,15 @@ function validateWifi(profile, ssid, password) {
|
|||
}
|
||||
}
|
||||
|
||||
/** 校验鉴权状态,未完成密钥匹配则抛出异常。 */
|
||||
/**
|
||||
* 校验鉴权状态。
|
||||
* 厂家确认密钥流程已不启用,此处仅记录未鉴权状态,不阻断 WiFi 配置。
|
||||
*/
|
||||
function requireAuthorized(ble) {
|
||||
const state = ble.getState()
|
||||
|
||||
if (!state.auth || state.auth.keyMatched !== true) {
|
||||
throw new Error('请先完成密钥鉴权,再发送 WiFi 配置')
|
||||
console.debug('[BLE][WiFi]', '密钥未匹配(密钥流程已废弃,继续配网)')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +66,7 @@ export function createWifiModule(ble) {
|
|||
* @param {string} ssid WiFi名称
|
||||
* @param {string} password WiFi密码
|
||||
* @param {object} options
|
||||
* @param {number} options.timeout 等待状态回执超时(ms),默认12000
|
||||
* @param {number} options.timeout 等待状态回执总超时(ms),默认60000
|
||||
* @param {number} options.preDelayMs 写入前延迟(ms),ED713 默认120ms
|
||||
* @param {number} options.packetInterval 分包写入间隔(ms)
|
||||
* @returns {{ success: boolean, stateCode: number }} stateCode=3 表示连接成功
|
||||
|
|
@ -70,12 +83,16 @@ export function createWifiModule(ble) {
|
|||
throw new Error('未发现 UUID_WIFI 特征值')
|
||||
}
|
||||
|
||||
const totalTimeout = options.timeout || WIFI_TOTAL_TIMEOUT_MS
|
||||
// 先注册 STATE 等待,再写入,确保不漏回执
|
||||
// 收到 STATE=4(连接中) 时刷新窗口,避免固定上限短于真实 DHCP 时长误判超时
|
||||
const waitPromise = waitForProtocolState(ble, {
|
||||
allowedCodes: WIFI_FINAL_STATE_CODES,
|
||||
rejectOnUnexpected: false,
|
||||
timeout: options.timeout || 12000,
|
||||
timeoutMessage: '等待 WiFi 状态超时'
|
||||
timeout: totalTimeout,
|
||||
timeoutMessage: '等待 WiFi 状态超时',
|
||||
refreshOnCodes: WIFI_CONNECTING_STATE_CODES,
|
||||
refreshTimeout: WIFI_REFRESH_TIMEOUT_MS
|
||||
})
|
||||
|
||||
// ED713 固件需要写入前延迟,否则可能丢包
|
||||
|
|
@ -86,12 +103,23 @@ export function createWifiModule(ble) {
|
|||
}
|
||||
|
||||
log('write WiFi start', { service: ble.getState().serviceId, char: wifi.uuid })
|
||||
const packetInterval = Number(options.packetInterval) || (profile.id === 'ED713' ? ED713_PACKET_INTERVAL_MS : 0)
|
||||
const sent = await writeA7Payload(ble, wifi.uuid, `${s}|${p}`, profile.packet, {
|
||||
packetInterval: Number(options.packetInterval) || 0
|
||||
packetInterval
|
||||
})
|
||||
log('write WiFi done, packets=', sent)
|
||||
log('write WiFi done, packets=', sent, 'interval=', packetInterval)
|
||||
|
||||
const code = Number((await waitPromise).stateCode)
|
||||
let code
|
||||
try {
|
||||
code = Number((await waitPromise).stateCode)
|
||||
} catch (error) {
|
||||
// 超时时带上设备最后上报的 STATE 码,便于区分"设备未响应"与"设备回了非预期态"
|
||||
const lastState = ble.getState().auth?.lastStateCode
|
||||
const hint = (lastState === undefined || lastState === null)
|
||||
? '(设备未上报任何 STATE,可能 WiFi 分包写入丢失或固件未处理)'
|
||||
: `(设备最后上报 STATE=${lastState},未收到最终态 1/2/3)`
|
||||
throw new Error(`${error.message}${hint}`)
|
||||
}
|
||||
log('STATE(final) received for WiFi=', code)
|
||||
|
||||
// STATE=3 表示 WiFi 连接成功
|
||||
|
|
|
|||
Loading…
Reference in New Issue