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/
|
.hbuilderx/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.codex-tasks/
|
.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 = {}) {
|
async function prepareWifiConfig(deviceId, options = {}) {
|
||||||
await connectDevice(deviceId)
|
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() {
|
async function readDeviceInfo() {
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@
|
||||||
"setting" : {
|
"setting" : {
|
||||||
"urlCheck" : false
|
"urlCheck" : false
|
||||||
},
|
},
|
||||||
"usingComponents" : true
|
"usingComponents" : true,
|
||||||
|
"libVersion" : "latest"
|
||||||
},
|
},
|
||||||
"mp-alipay" : {
|
"mp-alipay" : {
|
||||||
"usingComponents" : true
|
"usingComponents" : true
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,10 @@
|
||||||
一键导入上次 WiFi 配置
|
一键导入上次 WiFi 配置
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="wifiErrorTip" class="wifi-error-tip">
|
||||||
|
{{ wifiErrorTip }}
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="wifi-submit-btn" :class="{ disabled: wifiSending }" @click="confirmWifiSettings">
|
<view class="wifi-submit-btn" :class="{ disabled: wifiSending }" @click="confirmWifiSettings">
|
||||||
{{ wifiSending ? '发送中...' : '发送' }}
|
{{ wifiSending ? '发送中...' : '发送' }}
|
||||||
</view>
|
</view>
|
||||||
|
|
@ -155,6 +159,7 @@ export default {
|
||||||
wifiSending: false,
|
wifiSending: false,
|
||||||
fetchingCurrentWifi: false,
|
fetchingCurrentWifi: false,
|
||||||
wifiPrepared: false,
|
wifiPrepared: false,
|
||||||
|
wifiErrorTip: '',
|
||||||
devicePreparingMap: {},
|
devicePreparingMap: {},
|
||||||
selectedWifiDevice: null,
|
selectedWifiDevice: null,
|
||||||
wifiForm: {
|
wifiForm: {
|
||||||
|
|
@ -216,6 +221,7 @@ export default {
|
||||||
this.wifiPreparing = false
|
this.wifiPreparing = false
|
||||||
this.activePreparingDeviceId = ''
|
this.activePreparingDeviceId = ''
|
||||||
this.fetchingCurrentWifi = false
|
this.fetchingCurrentWifi = false
|
||||||
|
this.wifiErrorTip = ''
|
||||||
this.selectedWifiDevice = null
|
this.selectedWifiDevice = null
|
||||||
this.resetWifiForm()
|
this.resetWifiForm()
|
||||||
},
|
},
|
||||||
|
|
@ -329,6 +335,7 @@ export default {
|
||||||
mask: true
|
mask: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let authError = null
|
||||||
try {
|
try {
|
||||||
/**
|
/**
|
||||||
* 先按项目固定已知密钥规则(MAC+0000)完成鉴权,成功后再允许进入 WiFi 弹层。
|
* 先按项目固定已知密钥规则(MAC+0000)完成鉴权,成功后再允许进入 WiFi 弹层。
|
||||||
|
|
@ -343,16 +350,29 @@ export default {
|
||||||
this.wifiPanelVisible = true
|
this.wifiPanelVisible = true
|
||||||
// 确保 Vue 完成 DOM 更新后再调原生 toast,避免原生层覆盖 Vue 弹层
|
// 确保 Vue 完成 DOM 更新后再调原生 toast,避免原生层覆盖 Vue 弹层
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
showToast('密钥鉴权成功,请填写WiFi信息')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(getErrorMessage(error))
|
authError = error
|
||||||
await this.safeDisconnect()
|
await this.safeDisconnect()
|
||||||
} finally {
|
} finally {
|
||||||
this.setDevicePreparing(targetDeviceId, false)
|
this.setDevicePreparing(targetDeviceId, false)
|
||||||
this.activePreparingDeviceId = ''
|
this.activePreparingDeviceId = ''
|
||||||
this.wifiPreparing = false
|
this.wifiPreparing = false
|
||||||
|
// showLoading 与 showToast 共用同一遮罩层,必须先 hideLoading 再弹提示,
|
||||||
|
// 否则 hideLoading 会把 catch 里的 toast 一起清掉(表现为弹窗瞬间消失)。
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authError) {
|
||||||
|
// 鉴权失败用模态弹窗,需用户主动确认,确保错误诊断信息可见可读
|
||||||
|
uni.showModal({
|
||||||
|
title: '密钥鉴权失败',
|
||||||
|
content: getErrorMessage(authError),
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '知道了'
|
||||||
|
})
|
||||||
|
} else if (this.wifiPanelVisible) {
|
||||||
|
showToast('密钥鉴权成功,请填写WiFi信息')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async closeWifiPanel(force = false) {
|
async closeWifiPanel(force = false) {
|
||||||
|
|
@ -461,6 +481,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wifiSending = true
|
this.wifiSending = true
|
||||||
|
this.wifiErrorTip = ''
|
||||||
|
|
||||||
let needAutoClose = false
|
let needAutoClose = false
|
||||||
let needCacheWifi = false
|
let needCacheWifi = false
|
||||||
|
|
@ -472,7 +493,7 @@ export default {
|
||||||
* 才进行提示并自动关闭弹层。
|
* 才进行提示并自动关闭弹层。
|
||||||
*/
|
*/
|
||||||
const wifiResult = await this.discoveryController.configureWifi(ssid, password, {
|
const wifiResult = await this.discoveryController.configureWifi(ssid, password, {
|
||||||
timeout: 12000
|
timeout: 60000
|
||||||
})
|
})
|
||||||
|
|
||||||
const stateMessage = this.getWifiStateMessage(wifiResult.stateCode)
|
const stateMessage = this.getWifiStateMessage(wifiResult.stateCode)
|
||||||
|
|
@ -480,8 +501,9 @@ export default {
|
||||||
needAutoClose = true
|
needAutoClose = true
|
||||||
needCacheWifi = Boolean(wifiResult.success)
|
needCacheWifi = Boolean(wifiResult.success)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 超时/异常场景表示尚未拿到有效设备结果,保持弹层打开。
|
// 超时/异常场景:错误信息持久展示在弹层内,避免瞬态 toast 被覆盖看不清
|
||||||
tipMessage = getErrorMessage(error)
|
tipMessage = getErrorMessage(error)
|
||||||
|
this.wifiErrorTip = tipMessage
|
||||||
} finally {
|
} finally {
|
||||||
this.wifiSending = false
|
this.wifiSending = false
|
||||||
|
|
||||||
|
|
@ -830,6 +852,16 @@ export default {
|
||||||
justify-content: center;
|
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 {
|
.wifi-submit-btn {
|
||||||
height: 100rpx;
|
height: 100rpx;
|
||||||
background: #11c462;
|
background: #11c462;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { createEventBus } from './eventBus'
|
import { createEventBus } from './eventBus';
|
||||||
import { createBleState, BLE_STAGE } from './state'
|
import { createBleState, BLE_STAGE } from './state';
|
||||||
import { createBleError, getBleErrorText, normalizeBleError } from './errors'
|
import { createBleError, getBleErrorText, normalizeBleError } from './errors';
|
||||||
import { uint8ArrayToArrayBuffer, toUint8Array } from '../utils/bytes'
|
import { uint8ArrayToArrayBuffer, toUint8Array } from '../utils/bytes';
|
||||||
import { toFullUuid, uuidEquals, uuidInList } from '../utils/uuid'
|
import { toFullUuid, uuidEquals, uuidInList } from '../utils/uuid';
|
||||||
import { runWithRetry } from './retry'
|
import { runWithRetry } from './retry';
|
||||||
import { getBleProfile, DEVICE_PROFILE } from '../protocols/profiles'
|
import { getBleProfile, DEVICE_PROFILE } from '../protocols/profiles';
|
||||||
|
|
||||||
// 把 uni 的 success/fail 回调风格统一成 Promise,便于串联 async 流程。
|
// 把 uni 的 success/fail 回调风格统一成 Promise,便于串联 async 流程。
|
||||||
function promisifyUniApi(apiName, params = {}) {
|
function promisifyUniApi(apiName, params = {}) {
|
||||||
|
|
@ -12,9 +12,9 @@ function promisifyUniApi(apiName, params = {}) {
|
||||||
uni[apiName]({
|
uni[apiName]({
|
||||||
...params,
|
...params,
|
||||||
success: (res) => resolve(res),
|
success: (res) => resolve(res),
|
||||||
fail: (err) => reject(err)
|
fail: (err) => reject(err),
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,19 +26,19 @@ export function createBleCore(options = {}) {
|
||||||
reconnect = {
|
reconnect = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
retries: 2,
|
retries: 2,
|
||||||
delay: 1200
|
delay: 1200,
|
||||||
},
|
},
|
||||||
logger = console,
|
logger = console,
|
||||||
profileId = DEVICE_PROFILE.UNKNOWN
|
profileId = DEVICE_PROFILE.UNKNOWN,
|
||||||
} = options
|
} = options;
|
||||||
|
|
||||||
const eventBus = createEventBus()
|
const eventBus = createEventBus();
|
||||||
const state = createBleState()
|
const state = createBleState();
|
||||||
let currentProfile = getBleProfile(profileId)
|
let currentProfile = getBleProfile(profileId);
|
||||||
let deviceFoundHandler = null
|
let deviceFoundHandler = null;
|
||||||
let adapterStateHandler = null
|
let adapterStateHandler = null;
|
||||||
let connectionStateHandler = null
|
let connectionStateHandler = null;
|
||||||
let characteristicValueHandler = null
|
let characteristicValueHandler = null;
|
||||||
|
|
||||||
// 返回状态快照,避免调用方直接改写内部状态对象。
|
// 返回状态快照,避免调用方直接改写内部状态对象。
|
||||||
function snapshotState() {
|
function snapshotState() {
|
||||||
|
|
@ -46,74 +46,74 @@ export function createBleCore(options = {}) {
|
||||||
...state,
|
...state,
|
||||||
auth: { ...state.auth },
|
auth: { ...state.auth },
|
||||||
deviceMap: { ...state.deviceMap },
|
deviceMap: { ...state.deviceMap },
|
||||||
characteristics: { ...state.characteristics }
|
characteristics: { ...state.characteristics },
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一 patch 状态并广播 state:change,保证 UI 状态来源单一。
|
// 统一 patch 状态并广播 state:change,保证 UI 状态来源单一。
|
||||||
function patchState(partialState) {
|
function patchState(partialState) {
|
||||||
Object.assign(state, partialState)
|
Object.assign(state, partialState);
|
||||||
eventBus.emit('state:change', snapshotState())
|
eventBus.emit('state:change', snapshotState());
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStage(stage) {
|
function setStage(stage) {
|
||||||
patchState({ stage })
|
patchState({ stage });
|
||||||
}
|
}
|
||||||
|
|
||||||
function setProfile(nextProfileId) {
|
function setProfile(nextProfileId) {
|
||||||
currentProfile = getBleProfile(nextProfileId)
|
currentProfile = getBleProfile(nextProfileId);
|
||||||
patchState({ profileId: currentProfile.id })
|
patchState({ profileId: currentProfile.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProfile() {
|
function getProfile() {
|
||||||
return currentProfile
|
return currentProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAuthState(partialAuth = {}) {
|
function setAuthState(partialAuth = {}) {
|
||||||
patchState({
|
patchState({
|
||||||
auth: {
|
auth: {
|
||||||
...state.auth,
|
...state.auth,
|
||||||
...partialAuth
|
...partialAuth,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function emit(eventName, payload) {
|
function emit(eventName, payload) {
|
||||||
eventBus.emit(eventName, payload)
|
eventBus.emit(eventName, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitError(error) {
|
function emitError(error) {
|
||||||
const normalized = normalizeBleError(error)
|
const normalized = normalizeBleError(error);
|
||||||
patchState({ lastError: normalized })
|
patchState({ lastError: normalized });
|
||||||
eventBus.emit('error', normalized)
|
eventBus.emit('error', normalized);
|
||||||
return normalized
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开蓝牙适配器,成功后标记可用并进入 ADAPTER_OPENED 阶段。 */
|
/** 打开蓝牙适配器,成功后标记可用并进入 ADAPTER_OPENED 阶段。 */
|
||||||
async function openAdapter() {
|
async function openAdapter() {
|
||||||
try {
|
try {
|
||||||
await promisifyUniApi('openBluetoothAdapter')
|
await promisifyUniApi('openBluetoothAdapter');
|
||||||
patchState({ available: true })
|
patchState({ available: true });
|
||||||
setStage(BLE_STAGE.ADAPTER_OPENED)
|
setStage(BLE_STAGE.ADAPTER_OPENED);
|
||||||
return true
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw emitError(error)
|
throw emitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 关闭蓝牙适配器并重置全部运行时状态。 */
|
/** 关闭蓝牙适配器并重置全部运行时状态。 */
|
||||||
async function closeAdapter() {
|
async function closeAdapter() {
|
||||||
try {
|
try {
|
||||||
await promisifyUniApi('closeBluetoothAdapter')
|
await promisifyUniApi('closeBluetoothAdapter');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('[BLE] close adapter failed', error)
|
logger.warn('[BLE] close adapter failed', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置原生 handler 引用,以便 openAdapter 后 init() 可重新注册
|
// 重置原生 handler 引用,以便 openAdapter 后 init() 可重新注册
|
||||||
adapterStateHandler = null
|
adapterStateHandler = null;
|
||||||
connectionStateHandler = null
|
connectionStateHandler = null;
|
||||||
characteristicValueHandler = null
|
characteristicValueHandler = null;
|
||||||
deviceFoundHandler = null
|
deviceFoundHandler = null;
|
||||||
|
|
||||||
patchState({
|
patchState({
|
||||||
stage: BLE_STAGE.IDLE,
|
stage: BLE_STAGE.IDLE,
|
||||||
|
|
@ -122,56 +122,56 @@ export function createBleCore(options = {}) {
|
||||||
connected: false,
|
connected: false,
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
serviceId: '',
|
serviceId: '',
|
||||||
characteristics: {}
|
characteristics: {},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 读取当前适配器状态(可用/搜索中),同步到内部状态。 */
|
/** 读取当前适配器状态(可用/搜索中),同步到内部状态。 */
|
||||||
async function getAdapterState() {
|
async function getAdapterState() {
|
||||||
try {
|
try {
|
||||||
const result = await promisifyUniApi('getBluetoothAdapterState')
|
const result = await promisifyUniApi('getBluetoothAdapterState');
|
||||||
patchState({
|
patchState({
|
||||||
available: Boolean(result.available),
|
available: Boolean(result.available),
|
||||||
discovering: Boolean(result.discovering)
|
discovering: Boolean(result.discovering),
|
||||||
})
|
});
|
||||||
return result
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw emitError(error)
|
throw emitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注册适配器状态变更监听,蓝牙关闭时自动切换到 DISCONNECTED。 */
|
/** 注册适配器状态变更监听,蓝牙关闭时自动切换到 DISCONNECTED。 */
|
||||||
function watchAdapterState() {
|
function watchAdapterState() {
|
||||||
if (adapterStateHandler) {
|
if (adapterStateHandler) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
adapterStateHandler = (result) => {
|
adapterStateHandler = (result) => {
|
||||||
patchState({
|
patchState({
|
||||||
available: Boolean(result.available),
|
available: Boolean(result.available),
|
||||||
discovering: Boolean(result.discovering)
|
discovering: Boolean(result.discovering),
|
||||||
})
|
});
|
||||||
eventBus.emit('adapter:state', result)
|
eventBus.emit('adapter:state', result);
|
||||||
|
|
||||||
if (!result.available) {
|
if (!result.available) {
|
||||||
setStage(BLE_STAGE.DISCONNECTED)
|
setStage(BLE_STAGE.DISCONNECTED);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
uni.onBluetoothAdapterStateChange(adapterStateHandler)
|
uni.onBluetoothAdapterStateChange(adapterStateHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 移除适配器状态变更监听。 */
|
/** 移除适配器状态变更监听。 */
|
||||||
function unwatchAdapterState() {
|
function unwatchAdapterState() {
|
||||||
if (!adapterStateHandler) {
|
if (!adapterStateHandler) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uni.offBluetoothAdapterStateChange) {
|
if (uni.offBluetoothAdapterStateChange) {
|
||||||
uni.offBluetoothAdapterStateChange(adapterStateHandler)
|
uni.offBluetoothAdapterStateChange(adapterStateHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
adapterStateHandler = null
|
adapterStateHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -183,89 +183,93 @@ export function createBleCore(options = {}) {
|
||||||
services = [],
|
services = [],
|
||||||
allowDuplicatesKey = true,
|
allowDuplicatesKey = true,
|
||||||
interval = 0,
|
interval = 0,
|
||||||
powerLevel = 'high'
|
powerLevel = 'high',
|
||||||
} = params
|
} = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!state.available) {
|
if (!state.available) {
|
||||||
await openAdapter()
|
await openAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
allowDuplicatesKey,
|
allowDuplicatesKey,
|
||||||
interval
|
interval,
|
||||||
}
|
};
|
||||||
|
|
||||||
if (services.length) {
|
if (services.length) {
|
||||||
options.services = services.map((uuid) => toFullUuid(uuid) || uuid)
|
options.services = services.map((uuid) => toFullUuid(uuid) || uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (powerLevel) {
|
if (powerLevel) {
|
||||||
options.powerLevel = powerLevel
|
options.powerLevel = powerLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
await promisifyUniApi('startBluetoothDevicesDiscovery', options)
|
await promisifyUniApi('startBluetoothDevicesDiscovery', options);
|
||||||
patchState({ discovering: true, deviceMap: {} })
|
patchState({ discovering: true, deviceMap: {} });
|
||||||
setStage(BLE_STAGE.DISCOVERING)
|
setStage(BLE_STAGE.DISCOVERING);
|
||||||
return true
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw emitError(error)
|
throw emitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 停止设备扫描。 */
|
/** 停止设备扫描。 */
|
||||||
async function stopDiscovery() {
|
async function stopDiscovery() {
|
||||||
try {
|
try {
|
||||||
await promisifyUniApi('stopBluetoothDevicesDiscovery')
|
await promisifyUniApi('stopBluetoothDevicesDiscovery');
|
||||||
} catch (error) {
|
} 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。 */
|
/** 注册设备发现监听,按 deviceId 去重合并后广播 device:found。 */
|
||||||
function watchDeviceFound() {
|
function watchDeviceFound() {
|
||||||
if (deviceFoundHandler) {
|
if (deviceFoundHandler) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceFoundHandler = (result) => {
|
deviceFoundHandler = (result) => {
|
||||||
const list = Array.isArray(result.devices) ? result.devices : [result]
|
const list = Array.isArray(result.devices) ? result.devices : [result];
|
||||||
const nextMap = { ...state.deviceMap }
|
const nextMap = { ...state.deviceMap };
|
||||||
|
|
||||||
// 基于 deviceId 做去重与增量更新,避免列表无限叠加。
|
// 基于 deviceId 做去重与增量更新,避免列表无限叠加。
|
||||||
list.forEach((device) => {
|
list.forEach((device) => {
|
||||||
if (!device || !device.deviceId) {
|
if (!device || !device.deviceId) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previous = nextMap[device.deviceId] || {}
|
const previous = nextMap[device.deviceId] || {};
|
||||||
nextMap[device.deviceId] = {
|
nextMap[device.deviceId] = {
|
||||||
...previous,
|
...previous,
|
||||||
...device,
|
...device,
|
||||||
showName: device.name || device.localName || previous.showName || '未命名设备'
|
showName:
|
||||||
}
|
device.name ||
|
||||||
})
|
device.localName ||
|
||||||
|
previous.showName ||
|
||||||
|
'未命名设备',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
patchState({ deviceMap: nextMap })
|
patchState({ deviceMap: nextMap });
|
||||||
eventBus.emit('device:found', Object.values(nextMap))
|
eventBus.emit('device:found', Object.values(nextMap));
|
||||||
}
|
};
|
||||||
|
|
||||||
uni.onBluetoothDeviceFound(deviceFoundHandler)
|
uni.onBluetoothDeviceFound(deviceFoundHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 移除设备发现监听。 */
|
/** 移除设备发现监听。 */
|
||||||
function unwatchDeviceFound() {
|
function unwatchDeviceFound() {
|
||||||
if (!deviceFoundHandler) {
|
if (!deviceFoundHandler) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uni.offBluetoothDeviceFound) {
|
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 = {}) {
|
async function connect(deviceId, options = {}) {
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
throw emitError(createBleError(10013, 'deviceId 不能为空'))
|
throw emitError(createBleError(10013, 'deviceId 不能为空'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { timeout = 15000, autoReconnect = reconnect.enabled } = options;
|
||||||
timeout = 15000,
|
|
||||||
autoReconnect = reconnect.enabled
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const runConnect = async () => {
|
const runConnect = async () => {
|
||||||
setStage(BLE_STAGE.CONNECTING)
|
setStage(BLE_STAGE.CONNECTING);
|
||||||
await promisifyUniApi('createBLEConnection', { deviceId, timeout })
|
await promisifyUniApi('createBLEConnection', { deviceId, timeout });
|
||||||
patchState({ connected: true, deviceId })
|
patchState({ connected: true, deviceId });
|
||||||
setStage(BLE_STAGE.CONNECTED)
|
setStage(BLE_STAGE.CONNECTED);
|
||||||
return true
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!autoReconnect) {
|
if (!autoReconnect) {
|
||||||
return await runConnect()
|
return await runConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await runWithRetry(runConnect, {
|
return await runWithRetry(runConnect, {
|
||||||
retries: reconnect.retries,
|
retries: reconnect.retries,
|
||||||
delay: reconnect.delay,
|
delay: reconnect.delay,
|
||||||
shouldRetry: (error) => {
|
shouldRetry: (error) => {
|
||||||
const normalized = normalizeBleError(error)
|
const normalized = normalizeBleError(error);
|
||||||
return [10003, 10006, 10012].includes(normalized.code)
|
return [10003, 10006, 10012].includes(normalized.code);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw emitError(error)
|
throw emitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 断开BLE连接并重置连接相关状态。 */
|
/** 断开BLE连接并重置连接相关状态。 */
|
||||||
async function disconnect(deviceId = state.deviceId) {
|
async function disconnect(deviceId = state.deviceId) {
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await promisifyUniApi('closeBLEConnection', { deviceId })
|
await promisifyUniApi('closeBLEConnection', { deviceId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('[BLE] close connection failed', error)
|
logger.warn('[BLE] close connection failed', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
patchState({ connected: false, deviceId: '', serviceId: '', characteristics: {} })
|
patchState({
|
||||||
setStage(BLE_STAGE.DISCONNECTED)
|
connected: false,
|
||||||
|
deviceId: '',
|
||||||
|
serviceId: '',
|
||||||
|
characteristics: {},
|
||||||
|
});
|
||||||
|
setStage(BLE_STAGE.DISCONNECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注册连接状态变更监听,断开时自动切换到 DISCONNECTED。 */
|
/** 注册连接状态变更监听,断开时自动切换到 DISCONNECTED。 */
|
||||||
function watchConnectionChange() {
|
function watchConnectionChange() {
|
||||||
if (connectionStateHandler) {
|
if (connectionStateHandler) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionStateHandler = (result) => {
|
connectionStateHandler = (result) => {
|
||||||
if (state.deviceId && result.deviceId !== state.deviceId) {
|
if (state.deviceId && result.deviceId !== state.deviceId) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
patchState({ connected: Boolean(result.connected) })
|
patchState({ connected: Boolean(result.connected) });
|
||||||
eventBus.emit('connection:change', result)
|
eventBus.emit('connection:change', result);
|
||||||
|
|
||||||
if (!result.connected) {
|
if (!result.connected) {
|
||||||
setStage(BLE_STAGE.DISCONNECTED)
|
setStage(BLE_STAGE.DISCONNECTED);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
uni.onBLEConnectionStateChange(connectionStateHandler)
|
uni.onBLEConnectionStateChange(connectionStateHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 移除连接状态变更监听。 */
|
/** 移除连接状态变更监听。 */
|
||||||
function unwatchConnectionChange() {
|
function unwatchConnectionChange() {
|
||||||
if (!connectionStateHandler) {
|
if (!connectionStateHandler) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uni.offBLEConnectionStateChange) {
|
if (uni.offBLEConnectionStateChange) {
|
||||||
uni.offBLEConnectionStateChange(connectionStateHandler)
|
uni.offBLEConnectionStateChange(connectionStateHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionStateHandler = null
|
connectionStateHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取候选服务UUID列表:profile主服务 + 兼容固件的 00F4/F400/FFF4。 */
|
/** 获取候选服务UUID列表:profile主服务 + 兼容固件的 00F4/F400/FFF4。 */
|
||||||
function getServiceCandidates() {
|
function getServiceCandidates() {
|
||||||
const profileService = currentProfile?.uuids?.service
|
const profileService = currentProfile?.uuids?.service;
|
||||||
const candidates = [
|
const candidates = [
|
||||||
profileService,
|
profileService,
|
||||||
toFullUuid('00F4'),
|
toFullUuid('00F4'),
|
||||||
toFullUuid('F400'),
|
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) {
|
async function discoverServices(deviceId = state.deviceId) {
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
throw emitError(createBleError(10013, 'discoverServices 缺少 deviceId'))
|
throw emitError(createBleError(10013, 'discoverServices 缺少 deviceId'));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await promisifyUniApi('getBLEDeviceServices', { deviceId })
|
const result = await promisifyUniApi('getBLEDeviceServices', {
|
||||||
const services = result.services || []
|
deviceId,
|
||||||
const candidates = getServiceCandidates()
|
});
|
||||||
const targetService = services.find((service) => candidates.some((uuid) => uuidEquals(service.uuid, uuid)))
|
const services = result.services || [];
|
||||||
|
const candidates = getServiceCandidates();
|
||||||
|
const targetService = services.find((service) =>
|
||||||
|
candidates.some((uuid) => uuidEquals(service.uuid, uuid)),
|
||||||
|
);
|
||||||
|
|
||||||
if (!targetService) {
|
if (!targetService) {
|
||||||
throw createBleError(10004, '设备未暴露目标服务(00F4/F400/FFF4)')
|
throw createBleError(10004, '设备未暴露目标服务(00F4/F400/FFF4)');
|
||||||
}
|
}
|
||||||
|
|
||||||
patchState({ serviceId: targetService.uuid })
|
patchState({ serviceId: targetService.uuid });
|
||||||
setStage(BLE_STAGE.SERVICE_DISCOVERED)
|
setStage(BLE_STAGE.SERVICE_DISCOVERED);
|
||||||
return {
|
return {
|
||||||
services,
|
services,
|
||||||
targetService
|
targetService,
|
||||||
}
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw emitError(error)
|
throw emitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,86 +421,134 @@ export function createBleCore(options = {}) {
|
||||||
* @returns {object} characteristicMap 各业务特征 + all 全量列表
|
* @returns {object} characteristicMap 各业务特征 + all 全量列表
|
||||||
*/
|
*/
|
||||||
async function discoverCharacteristics(options = {}) {
|
async function discoverCharacteristics(options = {}) {
|
||||||
const deviceId = options.deviceId || state.deviceId
|
const deviceId = options.deviceId || state.deviceId;
|
||||||
const serviceId = options.serviceId || state.serviceId
|
const serviceId = options.serviceId || state.serviceId;
|
||||||
|
|
||||||
if (!deviceId || !serviceId) {
|
if (!deviceId || !serviceId) {
|
||||||
throw emitError(createBleError(10013, 'discoverCharacteristics 缺少 deviceId/serviceId'))
|
throw emitError(
|
||||||
|
createBleError(
|
||||||
|
10013,
|
||||||
|
'discoverCharacteristics 缺少 deviceId/serviceId',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await promisifyUniApi('getBLEDeviceCharacteristics', { deviceId, serviceId })
|
const result = await promisifyUniApi('getBLEDeviceCharacteristics', {
|
||||||
const list = result.characteristics || []
|
deviceId,
|
||||||
const profile = getProfile()
|
serviceId,
|
||||||
|
});
|
||||||
|
const list = result.characteristics || [];
|
||||||
|
const profile = getProfile();
|
||||||
|
|
||||||
// 缓存业务侧常用特征,减少后续每次操作时的查找成本。
|
// 缓存业务侧常用特征,减少后续每次操作时的查找成本。
|
||||||
const characteristicMap = {
|
const characteristicMap = {
|
||||||
wifi: list.find((item) => uuidEquals(item.uuid, profile.uuids.wifi)) || null,
|
wifi:
|
||||||
key: list.find((item) => uuidEquals(item.uuid, profile.uuids.key)) || null,
|
list.find((item) => uuidEquals(item.uuid, profile.uuids.wifi)) ||
|
||||||
cmd: list.find((item) => uuidEquals(item.uuid, profile.uuids.cmd)) || null,
|
null,
|
||||||
msg: list.find((item) => uuidEquals(item.uuid, profile.uuids.msg)) || null,
|
key:
|
||||||
state: list.find((item) => uuidEquals(item.uuid, profile.uuids.state)) || null,
|
list.find((item) => uuidEquals(item.uuid, profile.uuids.key)) || null,
|
||||||
radar: list.find((item) => uuidEquals(item.uuid, profile.uuids.radar)) || null,
|
cmd:
|
||||||
all: list
|
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 })
|
patchState({ characteristics: characteristicMap });
|
||||||
return characteristicMap
|
return characteristicMap;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw emitError(error)
|
throw emitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 读取指定特征值(触发 onBLECharacteristicValueChange 回调)。 */
|
/** 读取指定特征值(触发 onBLECharacteristicValueChange 回调)。 */
|
||||||
async function readCharacteristic(characteristicId, options = {}) {
|
async function readCharacteristic(characteristicId, options = {}) {
|
||||||
const deviceId = options.deviceId || state.deviceId
|
const deviceId = options.deviceId || state.deviceId;
|
||||||
const serviceId = options.serviceId || state.serviceId
|
const serviceId = options.serviceId || state.serviceId;
|
||||||
|
|
||||||
if (!deviceId || !serviceId || !characteristicId) {
|
if (!deviceId || !serviceId || !characteristicId) {
|
||||||
throw emitError(createBleError(10013, 'readCharacteristic 参数缺失'))
|
throw emitError(createBleError(10013, 'readCharacteristic 参数缺失'));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await promisifyUniApi('readBLECharacteristicValue', {
|
await promisifyUniApi('readBLECharacteristicValue', {
|
||||||
deviceId,
|
deviceId,
|
||||||
serviceId,
|
serviceId,
|
||||||
characteristicId
|
characteristicId,
|
||||||
})
|
});
|
||||||
return true
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw emitError(error)
|
throw emitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 向指定特征值写入数据,自动将输入转为 ArrayBuffer。 */
|
/** 向指定特征值写入数据,自动将输入转为 ArrayBuffer。 */
|
||||||
async function writeCharacteristic(characteristicId, value, options = {}) {
|
async function writeCharacteristic(characteristicId, value, options = {}) {
|
||||||
const deviceId = options.deviceId || state.deviceId
|
const deviceId = options.deviceId || state.deviceId;
|
||||||
const serviceId = options.serviceId || state.serviceId
|
const serviceId = options.serviceId || state.serviceId;
|
||||||
|
|
||||||
if (!deviceId || !serviceId || !characteristicId) {
|
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 {
|
try {
|
||||||
await promisifyUniApi('writeBLECharacteristicValue', {
|
console.debug('[BLE][WRITE]', characteristicId, {
|
||||||
|
writeType,
|
||||||
|
supportsWrite,
|
||||||
|
supportsWriteNoResp,
|
||||||
|
valueLen: value?.length
|
||||||
|
});
|
||||||
|
const params = {
|
||||||
deviceId,
|
deviceId,
|
||||||
serviceId,
|
serviceId,
|
||||||
characteristicId,
|
characteristicId,
|
||||||
value: uint8ArrayToArrayBuffer(toUint8Array(value))
|
value: uint8ArrayToArrayBuffer(toUint8Array(value)),
|
||||||
})
|
};
|
||||||
return true
|
if (writeType) {
|
||||||
|
params.writeType = writeType;
|
||||||
|
}
|
||||||
|
await promisifyUniApi('writeBLECharacteristicValue', params);
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw emitError(error)
|
throw emitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 订阅/取消订阅指定特征值的通知。 */
|
/** 订阅/取消订阅指定特征值的通知。 */
|
||||||
async function notifyCharacteristic(characteristicId, stateFlag = true, options = {}) {
|
async function notifyCharacteristic(
|
||||||
const deviceId = options.deviceId || state.deviceId
|
characteristicId,
|
||||||
const serviceId = options.serviceId || state.serviceId
|
stateFlag = true,
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
|
const deviceId = options.deviceId || state.deviceId;
|
||||||
|
const serviceId = options.serviceId || state.serviceId;
|
||||||
|
|
||||||
if (!deviceId || !serviceId || !characteristicId) {
|
if (!deviceId || !serviceId || !characteristicId) {
|
||||||
throw emitError(createBleError(10013, 'notifyCharacteristic 参数缺失'))
|
throw emitError(createBleError(10013, 'notifyCharacteristic 参数缺失'));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -498,67 +556,80 @@ export function createBleCore(options = {}) {
|
||||||
deviceId,
|
deviceId,
|
||||||
serviceId,
|
serviceId,
|
||||||
characteristicId,
|
characteristicId,
|
||||||
state: Boolean(stateFlag)
|
state: Boolean(stateFlag),
|
||||||
})
|
});
|
||||||
return true
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw emitError(error)
|
throw emitError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function watchCharacteristicValue() {
|
function watchCharacteristicValue() {
|
||||||
if (characteristicValueHandler) {
|
if (characteristicValueHandler) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一转为 Uint8Array,简化上层解析逻辑。
|
// 统一转为 Uint8Array,简化上层解析逻辑。
|
||||||
characteristicValueHandler = (result) => {
|
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', {
|
eventBus.emit('characteristic:value', {
|
||||||
...result,
|
...result,
|
||||||
value: toUint8Array(result.value)
|
value,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
uni.onBLECharacteristicValueChange(characteristicValueHandler)
|
uni.onBLECharacteristicValueChange(characteristicValueHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 移除特征值变更监听。 */
|
/** 移除特征值变更监听。 */
|
||||||
function unwatchCharacteristicValue() {
|
function unwatchCharacteristicValue() {
|
||||||
if (!characteristicValueHandler) {
|
if (!characteristicValueHandler) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uni.offBLECharacteristicValueChange) {
|
if (uni.offBLECharacteristicValueChange) {
|
||||||
uni.offBLECharacteristicValueChange(characteristicValueHandler)
|
uni.offBLECharacteristicValueChange(characteristicValueHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
characteristicValueHandler = null
|
characteristicValueHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 返回已发现设备列表。 */
|
/** 返回已发现设备列表。 */
|
||||||
function listDiscoveredDevices() {
|
function listDiscoveredDevices() {
|
||||||
return Object.values(state.deviceMap)
|
return Object.values(state.deviceMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从已发现设备中,按广播服务UUID匹配并返回第一个设备。 */
|
/** 从已发现设备中,按广播服务UUID匹配并返回第一个设备。 */
|
||||||
function pickDeviceByService(serviceUuid = currentProfile.uuids.service) {
|
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() {
|
async function cleanup() {
|
||||||
// 清理顺序:先停扫描与连接,再移除监听,最后关闭适配器。
|
// 清理顺序:先停扫描与连接,再移除监听,最后关闭适配器。
|
||||||
await stopDiscovery()
|
await stopDiscovery();
|
||||||
await disconnect()
|
await disconnect();
|
||||||
|
|
||||||
unwatchDeviceFound()
|
unwatchDeviceFound();
|
||||||
unwatchAdapterState()
|
unwatchAdapterState();
|
||||||
unwatchConnectionChange()
|
unwatchConnectionChange();
|
||||||
unwatchCharacteristicValue()
|
unwatchCharacteristicValue();
|
||||||
|
|
||||||
await closeAdapter()
|
await closeAdapter();
|
||||||
eventBus.clear()
|
eventBus.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -593,6 +664,6 @@ export function createBleCore(options = {}) {
|
||||||
on: eventBus.on,
|
on: eventBus.on,
|
||||||
once: eventBus.once,
|
once: eventBus.once,
|
||||||
cleanup,
|
cleanup,
|
||||||
getBleErrorText
|
getBleErrorText,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,49 @@
|
||||||
import {createBleCore} from './bleCore'
|
import { createBleCore } from './bleCore';
|
||||||
import {createAuthModule} from '../modules/auth'
|
import { createAuthModule } from '../modules/auth';
|
||||||
import {createWifiModule} from '../modules/wifi'
|
import { createWifiModule } from '../modules/wifi';
|
||||||
import {createRadarModule} from '../modules/radar'
|
import { createRadarModule } from '../modules/radar';
|
||||||
import {parseStateCode, parseDeviceInfo} from '../parsers'
|
import { parseStateCode, parseDeviceInfo } from '../parsers';
|
||||||
import {DEVICE_PROFILE, guessProfileByRadarFrame} from '../protocols/profiles'
|
import {
|
||||||
import {uuidEquals} from '../utils/uuid'
|
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) {
|
function createCharacteristicDispatcher(ble) {
|
||||||
return (event) => {
|
return (event) => {
|
||||||
const state = ble.getState()
|
const state = ble.getState();
|
||||||
const profile = ble.getProfile()
|
const profile = ble.getProfile();
|
||||||
const current = state.characteristics
|
const current = state.characteristics;
|
||||||
|
|
||||||
if (current.state && uuidEquals(event.characteristicId, current.state.uuid)) {
|
if (
|
||||||
const stateCode = parseStateCode(event.value)
|
current.state &&
|
||||||
|
uuidEquals(event.characteristicId, current.state.uuid)
|
||||||
|
) {
|
||||||
|
const stateCode = parseStateCode(event.value);
|
||||||
ble.setAuthState({
|
ble.setAuthState({
|
||||||
lastStateCode: stateCode,
|
lastStateCode: stateCode,
|
||||||
keyMatched: stateCode === 7 || stateCode === 5 || state.auth.keyMatched
|
keyMatched: stateCode === 7 || stateCode === 5 || state.auth.keyMatched,
|
||||||
})
|
});
|
||||||
|
|
||||||
ble.emit('protocol:state', {
|
ble.emit('protocol:state', {
|
||||||
...event,
|
...event,
|
||||||
profileId: profile.id,
|
profileId: profile.id,
|
||||||
stateCode
|
stateCode,
|
||||||
})
|
});
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.msg && uuidEquals(event.characteristicId, current.msg.uuid)) {
|
if (current.msg && uuidEquals(event.characteristicId, current.msg.uuid)) {
|
||||||
ble.emit('protocol:msg', {
|
ble.emit('protocol:msg', {
|
||||||
...event,
|
...event,
|
||||||
profileId: profile.id,
|
profileId: profile.id,
|
||||||
deviceInfo: parseDeviceInfo(event.value, profile.id)
|
deviceInfo: parseDeviceInfo(event.value, profile.id),
|
||||||
})
|
});
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.radar && uuidEquals(event.characteristicId, current.radar.uuid)) {
|
if (
|
||||||
const guessed = guessProfileByRadarFrame(event.value)
|
current.radar &&
|
||||||
if (state.profileId === DEVICE_PROFILE.UNKNOWN && guessed !== DEVICE_PROFILE.UNKNOWN) {
|
uuidEquals(event.characteristicId, current.radar.uuid)
|
||||||
ble.setProfile(guessed)
|
) {
|
||||||
|
const guessed = guessProfileByRadarFrame(event.value);
|
||||||
|
if (
|
||||||
|
state.profileId === DEVICE_PROFILE.UNKNOWN &&
|
||||||
|
guessed !== DEVICE_PROFILE.UNKNOWN
|
||||||
|
) {
|
||||||
|
ble.setProfile(guessed);
|
||||||
}
|
}
|
||||||
|
|
||||||
ble.emit('protocol:radar', {
|
ble.emit('protocol:radar', {
|
||||||
...event,
|
...event,
|
||||||
profileId: ble.getProfile().id
|
profileId: ble.getProfile().id,
|
||||||
})
|
});
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ble.emit('protocol:raw', {
|
ble.emit('protocol:raw', {
|
||||||
...event,
|
...event,
|
||||||
profileId: profile.id
|
profileId: profile.id,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -67,40 +115,43 @@ function createCharacteristicDispatcher(ble) {
|
||||||
* @param {object} options 透传给 createBleCore 的配置(重试策略/日志/profileId等)
|
* @param {object} options 透传给 createBleCore 的配置(重试策略/日志/profileId等)
|
||||||
*/
|
*/
|
||||||
export function createUnifiedBleController(options = {}) {
|
export function createUnifiedBleController(options = {}) {
|
||||||
const ble = createBleCore(options)
|
const ble = createBleCore(options);
|
||||||
const auth = createAuthModule(ble)
|
const auth = createAuthModule(ble);
|
||||||
const wifi = createWifiModule(ble)
|
const wifi = createWifiModule(ble);
|
||||||
const radar = createRadarModule(ble)
|
const radar = createRadarModule(ble);
|
||||||
|
|
||||||
const characteristicDispatcher = createCharacteristicDispatcher(ble)
|
const characteristicDispatcher = createCharacteristicDispatcher(ble);
|
||||||
let offCharacteristic = null
|
let offCharacteristic = null;
|
||||||
|
|
||||||
/** 绑定特征值分发器到 ble 事件总线。 */
|
/** 绑定特征值分发器到 ble 事件总线。 */
|
||||||
function bindDispatcher() {
|
function bindDispatcher() {
|
||||||
if (offCharacteristic) {
|
if (offCharacteristic) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
offCharacteristic = ble.on('characteristic:value', characteristicDispatcher)
|
offCharacteristic = ble.on(
|
||||||
|
'characteristic:value',
|
||||||
|
characteristicDispatcher,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解绑特征值分发器。 */
|
/** 解绑特征值分发器。 */
|
||||||
function unbindDispatcher() {
|
function unbindDispatcher() {
|
||||||
if (!offCharacteristic) {
|
if (!offCharacteristic) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
offCharacteristic()
|
offCharacteristic();
|
||||||
offCharacteristic = null
|
offCharacteristic = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化蓝牙适配器并注册所有监听(适配器状态/连接/特征值/分发器)。 */
|
/** 初始化蓝牙适配器并注册所有监听(适配器状态/连接/特征值/分发器)。 */
|
||||||
async function init() {
|
async function init() {
|
||||||
await ble.openAdapter()
|
await ble.openAdapter();
|
||||||
ble.watchAdapterState()
|
ble.watchAdapterState();
|
||||||
ble.watchConnectionChange()
|
ble.watchConnectionChange();
|
||||||
ble.watchCharacteristicValue()
|
ble.watchCharacteristicValue();
|
||||||
bindDispatcher()
|
bindDispatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -115,92 +166,108 @@ export function createUnifiedBleController(options = {}) {
|
||||||
async function scanAndPickDevice({
|
async function scanAndPickDevice({
|
||||||
serviceUuid,
|
serviceUuid,
|
||||||
scanDuration = 5000,
|
scanDuration = 5000,
|
||||||
fallbackWithoutFilter = true
|
fallbackWithoutFilter = true,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const profile = ble.getProfile()
|
const profile = ble.getProfile();
|
||||||
const targetService = serviceUuid || profile.uuids.service
|
const targetService = serviceUuid || profile.uuids.service;
|
||||||
|
|
||||||
ble.watchDeviceFound()
|
ble.watchDeviceFound();
|
||||||
|
|
||||||
await ble.startDiscovery({services: [targetService]})
|
await ble.startDiscovery({ services: [targetService] });
|
||||||
await new Promise((resolve) => setTimeout(resolve, scanDuration))
|
await new Promise((resolve) => setTimeout(resolve, scanDuration));
|
||||||
await ble.stopDiscovery()
|
await ble.stopDiscovery();
|
||||||
|
|
||||||
let selected = ble.pickDeviceByService(targetService)
|
let selected = ble.pickDeviceByService(targetService);
|
||||||
|
|
||||||
if (!selected && fallbackWithoutFilter) {
|
if (!selected && fallbackWithoutFilter) {
|
||||||
await ble.startDiscovery({services: []})
|
await ble.startDiscovery({ services: [] });
|
||||||
await new Promise((resolve) => setTimeout(resolve, scanDuration))
|
await new Promise((resolve) => setTimeout(resolve, scanDuration));
|
||||||
await ble.stopDiscovery()
|
await ble.stopDiscovery();
|
||||||
selected = ble.pickDeviceByService(targetService) || ble.listDiscoveredDevices()[0] || null
|
selected =
|
||||||
|
ble.pickDeviceByService(targetService) ||
|
||||||
|
ble.listDiscoveredDevices()[0] ||
|
||||||
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return selected
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接后严格执行协议前置:
|
* 连接后严格执行协议前置(厂家要求顺序):
|
||||||
* - 发现服务和特征
|
* 1) 发现服务和特征
|
||||||
* - 订阅 UUID_STATE / UUID_RADAR
|
* 2) 先读取 UUID_MSG(F501) —— 必须在订阅 F301 之前
|
||||||
|
* 3) 订阅 UUID_STATE(F301) / UUID_RADAR(F302)
|
||||||
|
*
|
||||||
|
* 厂家说明:订阅 F301 前必须先读 F501,否则后续设备不回 STATE 通知,
|
||||||
|
* 表现为写 KEY/WIFI 后 F301 零回执、等待超时。
|
||||||
*/
|
*/
|
||||||
async function connectAndDiscover(deviceId) {
|
async function connectAndDiscover(deviceId) {
|
||||||
await ble.connect(deviceId)
|
await ble.connect(deviceId);
|
||||||
await ble.discoverServices(deviceId)
|
await ble.discoverServices(deviceId);
|
||||||
const chars = await ble.discoverCharacteristics()
|
const chars = await ble.discoverCharacteristics();
|
||||||
|
|
||||||
|
// 先读 UUID_MSG(F501),再订阅 F301 —— 厂家强制顺序
|
||||||
|
await readMsgOnce(ble, chars);
|
||||||
|
|
||||||
if (chars.state && chars.state.uuid) {
|
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 {
|
try {
|
||||||
await ble.notifyCharacteristic(chars.state.uuid, true);
|
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) {
|
} 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) {
|
if (chars.radar && chars.radar.uuid) {
|
||||||
try {
|
try {
|
||||||
await ble.notifyCharacteristic(chars.radar.uuid, true);
|
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) {
|
} 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 分支处理绑定/匹配。
|
* 鉴权内部会按 bindStatus 分支处理绑定/匹配。
|
||||||
*/
|
*/
|
||||||
async function standardConnectFlow({deviceId, key}) {
|
async function standardConnectFlow({ deviceId, key }) {
|
||||||
await init()
|
await init();
|
||||||
|
|
||||||
let targetDeviceId = deviceId
|
let targetDeviceId = deviceId;
|
||||||
if (!targetDeviceId) {
|
if (!targetDeviceId) {
|
||||||
const device = await scanAndPickDevice()
|
const device = await scanAndPickDevice();
|
||||||
if (!device) {
|
if (!device) {
|
||||||
throw new Error('未找到可连接设备')
|
throw new Error('未找到可连接设备');
|
||||||
}
|
}
|
||||||
|
|
||||||
targetDeviceId = device.deviceId
|
targetDeviceId = device.deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await connectAndDiscover(targetDeviceId)
|
await connectAndDiscover(targetDeviceId);
|
||||||
const authResult = await auth.ensureAuthorized(key)
|
const authResult = await auth.ensureAuthorized(key);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deviceId: targetDeviceId,
|
deviceId: targetDeviceId,
|
||||||
authResult,
|
authResult,
|
||||||
state: ble.getState(),
|
state: ble.getState(),
|
||||||
profile: ble.getProfile()
|
profile: ble.getProfile(),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 释放所有资源:解绑分发器 + 清理核心BLE(停扫描/断连接/移监听/关适配器)。 */
|
/** 释放所有资源:解绑分发器 + 清理核心BLE(停扫描/断连接/移监听/关适配器)。 */
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
unbindDispatcher()
|
unbindDispatcher();
|
||||||
await ble.cleanup()
|
await ble.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -212,6 +279,6 @@ export function createUnifiedBleController(options = {}) {
|
||||||
scanAndPickDevice,
|
scanAndPickDevice,
|
||||||
connectAndDiscover,
|
connectAndDiscover,
|
||||||
standardConnectFlow,
|
standardConnectFlow,
|
||||||
cleanup
|
cleanup,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,18 @@ const ERROR_MESSAGES = Object.freeze({
|
||||||
10017: 'startBluetoothDevicesDiscovery: 未找到蓝牙适配器',
|
10017: 'startBluetoothDevicesDiscovery: 未找到蓝牙适配器',
|
||||||
10018: 'stopBluetoothDevicesDiscovery: 调用失败',
|
10018: 'stopBluetoothDevicesDiscovery: 调用失败',
|
||||||
10019: 'getBluetoothDevices: 调用失败',
|
10019: 'getBluetoothDevices: 调用失败',
|
||||||
10020: 'getConnectedBluetoothDevices: 调用失败'
|
10020: 'getConnectedBluetoothDevices: 调用失败',
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造统一 BLE Error,方便上层直接读取 code/message。
|
* 构造统一 BLE Error,方便上层直接读取 code/message。
|
||||||
*/
|
*/
|
||||||
export function createBleError(code, message, payload = {}) {
|
export function createBleError(code, message, payload = {}) {
|
||||||
const err = new Error(message || ERROR_MESSAGES[code] || '蓝牙操作失败')
|
const err = new Error(message || ERROR_MESSAGES[code] || '蓝牙操作失败');
|
||||||
err.name = 'BleError'
|
err.name = 'BleError';
|
||||||
err.code = code
|
err.code = code;
|
||||||
Object.assign(err, payload)
|
Object.assign(err, payload);
|
||||||
return err
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -39,16 +39,16 @@ export function createBleError(code, message, payload = {}) {
|
||||||
*/
|
*/
|
||||||
export function normalizeBleError(error, fallbackMessage = '蓝牙操作失败') {
|
export function normalizeBleError(error, fallbackMessage = '蓝牙操作失败') {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return createBleError(-1, fallbackMessage)
|
return createBleError(-1, fallbackMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.name === 'BleError') {
|
if (error.name === 'BleError') {
|
||||||
return error
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errCode = typeof error.errCode === 'number' ? error.errCode : -1
|
const errCode = typeof error.errCode === 'number' ? error.errCode : -1;
|
||||||
const message = error.errMsg || ERROR_MESSAGES[errCode] || fallbackMessage
|
const message = error.errMsg || ERROR_MESSAGES[errCode] || fallbackMessage;
|
||||||
return createBleError(errCode, message, { raw: error })
|
return createBleError(errCode, message, { raw: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -56,8 +56,8 @@ export function normalizeBleError(error, fallbackMessage = '蓝牙操作失败')
|
||||||
*/
|
*/
|
||||||
export function getBleErrorText(error, fallbackText = '蓝牙操作失败') {
|
export function getBleErrorText(error, fallbackText = '蓝牙操作失败') {
|
||||||
if (!error) {
|
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 与业务模块之间的事件解耦。
|
* 轻量事件总线:用于 BLE Core 与业务模块之间的事件解耦。
|
||||||
*/
|
*/
|
||||||
export function createEventBus() {
|
export function createEventBus() {
|
||||||
const listeners = {}
|
const listeners = {};
|
||||||
|
|
||||||
/** 订阅事件,返回取消订阅函数。 */
|
/** 订阅事件,返回取消订阅函数。 */
|
||||||
function on(eventName, handler) {
|
function on(eventName, handler) {
|
||||||
if (!listeners[eventName]) {
|
if (!listeners[eventName]) {
|
||||||
listeners[eventName] = new Set()
|
listeners[eventName] = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
listeners[eventName].add(handler)
|
listeners[eventName].add(handler);
|
||||||
|
|
||||||
// 返回取消订阅函数,方便调用方在页面卸载时回收监听。
|
// 返回取消订阅函数,方便调用方在页面卸载时回收监听。
|
||||||
return () => {
|
return () => {
|
||||||
off(eventName, handler)
|
off(eventName, handler);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单次订阅:触发一次后自动取消。 */
|
/** 单次订阅:触发一次后自动取消。 */
|
||||||
function once(eventName, handler) {
|
function once(eventName, handler) {
|
||||||
const unsubscribe = on(eventName, (payload) => {
|
const unsubscribe = on(eventName, (payload) => {
|
||||||
unsubscribe()
|
unsubscribe();
|
||||||
handler(payload)
|
handler(payload);
|
||||||
})
|
});
|
||||||
|
|
||||||
return unsubscribe
|
return unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取消指定事件的某个监听。 */
|
/** 取消指定事件的某个监听。 */
|
||||||
function off(eventName, handler) {
|
function off(eventName, handler) {
|
||||||
if (!listeners[eventName]) {
|
if (!listeners[eventName]) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
listeners[eventName].delete(handler)
|
listeners[eventName].delete(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 广播事件,所有监听器同步执行(内部已 try-catch 隔离)。 */
|
/** 广播事件,所有监听器同步执行(内部已 try-catch 隔离)。 */
|
||||||
function emit(eventName, payload) {
|
function emit(eventName, payload) {
|
||||||
if (!listeners[eventName]) {
|
if (!listeners[eventName]) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
listeners[eventName].forEach((handler) => {
|
listeners[eventName].forEach((handler) => {
|
||||||
try {
|
try {
|
||||||
handler(payload)
|
handler(payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BLE event handler error]', eventName, error)
|
console.error('[BLE event handler error]', eventName, error);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清空所有事件监听。 */
|
/** 清空所有事件监听。 */
|
||||||
function clear() {
|
function clear() {
|
||||||
Object.keys(listeners).forEach((eventName) => {
|
Object.keys(listeners).forEach((eventName) => {
|
||||||
listeners[eventName].clear()
|
listeners[eventName].clear();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -64,6 +64,6 @@ export function createEventBus() {
|
||||||
once,
|
once,
|
||||||
off,
|
off,
|
||||||
emit,
|
emit,
|
||||||
clear
|
clear,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@
|
||||||
* @param {number} options.timeout 超时时间(ms)
|
* @param {number} options.timeout 超时时间(ms)
|
||||||
* @param {string} options.timeoutMessage 超时提示
|
* @param {string} options.timeoutMessage 超时提示
|
||||||
* @param {string} options.unexpectedMessage 非预期状态提示前缀
|
* @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 = {}) {
|
export function waitForProtocolState(ble, options = {}) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -15,14 +21,20 @@ export function waitForProtocolState(ble, options = {}) {
|
||||||
rejectOnUnexpected = false,
|
rejectOnUnexpected = false,
|
||||||
timeout = 6000,
|
timeout = 6000,
|
||||||
timeoutMessage = '等待 UUID_STATE 超时',
|
timeoutMessage = '等待 UUID_STATE 超时',
|
||||||
unexpectedMessage = '收到非预期 UUID_STATE 状态码'
|
unexpectedMessage = '收到非预期 UUID_STATE 状态码',
|
||||||
|
refreshOnCodes = [],
|
||||||
|
refreshTimeout = timeout
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let timer = null
|
let timer = null
|
||||||
let off = null
|
let off = null
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,13 +50,20 @@ export function waitForProtocolState(ble, options = {}) {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const armTimer = (duration) => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
console.debug('[BLE][STATEWAIT]', 'timeout')
|
console.debug('[BLE][STATEWAIT]', 'timeout')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
finish(() => reject(new Error(timeoutMessage)))
|
finish(() => reject(new Error(timeoutMessage)))
|
||||||
}, timeout)
|
}, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
armTimer(timeout)
|
||||||
|
|
||||||
off = ble.on('protocol:state', (payload) => {
|
off = ble.on('protocol:state', (payload) => {
|
||||||
const stateCode = Number(payload?.stateCode)
|
const stateCode = Number(payload?.stateCode)
|
||||||
|
|
@ -58,6 +77,16 @@ export function waitForProtocolState(ble, options = {}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 命中中间态刷新:重置超时窗口,继续等待后续最终态
|
||||||
|
if (refreshOnCodes.length && refreshOnCodes.includes(stateCode)) {
|
||||||
|
try {
|
||||||
|
console.debug('[BLE][STATEWAIT]', 'refresh', stateCode)
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
armTimer(refreshTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (rejectOnUnexpected) {
|
if (rejectOnUnexpected) {
|
||||||
try {
|
try {
|
||||||
console.debug('[BLE][STATEWAIT]', 'reject', stateCode)
|
console.debug('[BLE][STATEWAIT]', 'reject', stateCode)
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,32 @@
|
||||||
import {writeA7Payload} from '../core/packet'
|
import { writeA7Payload } from '../core/packet';
|
||||||
import {waitForProtocolState} from '../core/stateWait'
|
import { waitForProtocolState } from '../core/stateWait';
|
||||||
import {parseDeviceInfo} from '../parsers'
|
import { parseDeviceInfo } from '../parsers';
|
||||||
import {BLE_STAGE} from '../core/state'
|
import { BLE_STAGE } from '../core/state';
|
||||||
|
|
||||||
/** 生成指定长度的随机密钥(排除易混淆字符 0/O/1/I/l)。 */
|
/** 生成指定长度的随机密钥(排除易混淆字符 0/O/1/I/l)。 */
|
||||||
function generateRandomKey(length = 16) {
|
function generateRandomKey(length = 16) {
|
||||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
|
||||||
let result = ''
|
let result = '';
|
||||||
|
|
||||||
for (let index = 0; index < length; index += 1) {
|
for (let index = 0; index < length; index += 1) {
|
||||||
const random = Math.floor(Math.random() * chars.length)
|
const random = Math.floor(Math.random() * chars.length);
|
||||||
result += chars[random]
|
result += chars[random];
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authLog = (...args) => {
|
const authLog = (...args) => {
|
||||||
try {
|
try {
|
||||||
console.debug('[BLE][AUTH]', ...args)
|
console.debug('[BLE][AUTH]', ...args);
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/** 将 MAC 地址去除非十六进制字符并转大写,用于密钥拼接。 */
|
/** 将 MAC 地址去除非十六进制字符并转大写,用于密钥拼接。 */
|
||||||
function normalizeMacForKey(macText = '') {
|
function normalizeMacForKey(macText = '') {
|
||||||
return String(macText || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
|
return String(macText || '')
|
||||||
|
.replace(/[^0-9a-fA-F]/g, '')
|
||||||
|
.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,23 +34,23 @@ function normalizeMacForKey(macText = '') {
|
||||||
* 默认后缀为 "0000",对应 12 位 MAC + 4 位后缀 = 16 位密钥。
|
* 默认后缀为 "0000",对应 12 位 MAC + 4 位后缀 = 16 位密钥。
|
||||||
*/
|
*/
|
||||||
export function buildKeyFromMac(macText, suffix = '0000', expectedLength = 16) {
|
export function buildKeyFromMac(macText, suffix = '0000', expectedLength = 16) {
|
||||||
const normalizedMac = normalizeMacForKey(macText)
|
const normalizedMac = normalizeMacForKey(macText);
|
||||||
const normalizedSuffix = String(suffix || '').trim()
|
const normalizedSuffix = String(suffix || '').trim();
|
||||||
|
|
||||||
if (!normalizedSuffix) {
|
if (!normalizedSuffix) {
|
||||||
throw new Error('密钥后缀不能为空')
|
throw new Error('密钥后缀不能为空');
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiredMacLength = expectedLength - normalizedSuffix.length
|
const requiredMacLength = expectedLength - normalizedSuffix.length;
|
||||||
if (requiredMacLength <= 0) {
|
if (requiredMacLength <= 0) {
|
||||||
throw new Error('密钥长度配置异常')
|
throw new Error('密钥长度配置异常');
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (normalizedMac.length !== requiredMacLength) {
|
// if (normalizedMac.length !== requiredMacLength) {
|
||||||
// throw new Error(`设备 MAC 长度异常,期望 ${requiredMacLength} 位,实际 ${normalizedMac.length} 位`)
|
// 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} 实际写入的密钥值
|
* @returns {string} 实际写入的密钥值
|
||||||
*/
|
*/
|
||||||
async function writePacketsToKeyCharacteristic(ble, keyText) {
|
async function writePacketsToKeyCharacteristic(ble, keyText) {
|
||||||
const {key} = ble.getState().characteristics
|
const { key } = ble.getState().characteristics;
|
||||||
|
|
||||||
if (!key || !key.uuid) {
|
if (!key || !key.uuid) {
|
||||||
throw new Error('未发现 UUID_KEY 特征值')
|
throw new Error('未发现 UUID_KEY 特征值');
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = ble.getProfile()
|
const profile = ble.getProfile();
|
||||||
const keyValue = String(keyText || '').trim()
|
const keyValue = String(keyText || '').trim();
|
||||||
|
|
||||||
// if (keyValue.length !== profile.auth.keyLength) {
|
// if (keyValue.length !== profile.auth.keyLength) {
|
||||||
// throw new Error(`密钥长度必须为 ${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 {number[]} expectedStateCodes 期望的状态码列表
|
||||||
* @param {object} options 超时等配置
|
* @param {object} options 超时等配置
|
||||||
*/
|
*/
|
||||||
async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options = {}) {
|
async function writeKeyAndExpectStates(
|
||||||
|
ble,
|
||||||
|
keyText,
|
||||||
|
expectedStateCodes,
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
// 设备订阅 STATE 后可能先发送 -1(未写密钥)/0(空闲) 等中间态,
|
// 设备订阅 STATE 后可能先发送 -1(未写密钥)/0(空闲) 等中间态,
|
||||||
// 若 rejectOnUnexpected:true 会导致中间态直接 reject,阻断鉴权流程。
|
// 若 rejectOnUnexpected:true 会导致中间态直接 reject,阻断鉴权流程。
|
||||||
const waitPromise = waitForProtocolState(ble, {
|
const waitPromise = waitForProtocolState(ble, {
|
||||||
|
|
@ -93,16 +99,32 @@ async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options
|
||||||
rejectOnUnexpected: false,
|
rejectOnUnexpected: false,
|
||||||
timeout: options.timeout || 6000,
|
timeout: options.timeout || 6000,
|
||||||
timeoutMessage: '等待 UUID_STATE 超时',
|
timeoutMessage: '等待 UUID_STATE 超时',
|
||||||
unexpectedMessage: '密钥流程状态异常'
|
unexpectedMessage: '密钥流程状态异常',
|
||||||
})
|
});
|
||||||
authLog('STATE wait before writeKey, expect=', expectedStateCodes)
|
authLog('STATE wait before writeKey, expect=', expectedStateCodes);
|
||||||
const keyValue = await writePacketsToKeyCharacteristic(ble, keyText)
|
const keyValue = await writePacketsToKeyCharacteristic(ble, keyText);
|
||||||
authLog('key written, length=', keyValue)
|
authLog('key written, length=', keyValue);
|
||||||
const stateCode = Number((await waitPromise).stateCode)
|
try {
|
||||||
authLog('STATE received for KEY=', stateCode)
|
const stateCode = Number((await waitPromise).stateCode);
|
||||||
ble.setStage(BLE_STAGE.AUTHORIZED)
|
authLog('STATE received for KEY=', stateCode);
|
||||||
ble.setAuthState({keyMatched: true, lastStateCode: stateCode})
|
ble.setStage(BLE_STAGE.AUTHORIZED);
|
||||||
return {success: true, stateCode, key: keyValue}
|
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
|
* - bindStatus=1(已绑定):使用已知密钥匹配,等待 STATE=7
|
||||||
* - 其他状态:尝试用提供的密钥匹配,等待 STATE=7 或 5
|
* - 其他状态:尝试用提供的密钥匹配,等待 STATE=7 或 5
|
||||||
*/
|
*/
|
||||||
async function ensureAuthorizedWithDeviceInfo(ble, info, keyText, options = {}) {
|
async function ensureAuthorizedWithDeviceInfo(
|
||||||
const bindStatus = info.bindStatus !== null ? Number(info.bindStatus) : null
|
ble,
|
||||||
const profile = ble.getProfile()
|
info,
|
||||||
|
keyText,
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
|
const bindStatus = info.bindStatus !== null ? Number(info.bindStatus) : null;
|
||||||
|
const profile = ble.getProfile();
|
||||||
|
|
||||||
ble.setAuthState({
|
ble.setAuthState({
|
||||||
bound: Number.isNaN(bindStatus) ? null : bindStatus,
|
bound: Number.isNaN(bindStatus) ? null : bindStatus,
|
||||||
keyMatched: false
|
keyMatched: false,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (bindStatus === 0) {
|
if (bindStatus === 0) {
|
||||||
const keyToBind = String(keyText || '').trim() || generateRandomKey(profile.auth.keyLength)
|
const keyToBind =
|
||||||
const authResult = await writeKeyAndExpectStates(ble, keyToBind, [5], options)
|
String(keyText || '').trim() || generateRandomKey(profile.auth.keyLength);
|
||||||
|
const authResult = await writeKeyAndExpectStates(
|
||||||
|
ble,
|
||||||
|
keyToBind,
|
||||||
|
[5],
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode: 'BIND',
|
mode: 'BIND',
|
||||||
deviceInfo: info,
|
deviceInfo: info,
|
||||||
authResult,
|
authResult,
|
||||||
key: keyToBind
|
key: keyToBind,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bindStatus === 1) {
|
if (bindStatus === 1) {
|
||||||
const keyToMatch = String(keyText || '').trim()
|
const keyToMatch = String(keyText || '').trim();
|
||||||
if (!keyToMatch) {
|
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 {
|
return {
|
||||||
mode: 'MATCH',
|
mode: 'MATCH',
|
||||||
deviceInfo: info,
|
deviceInfo: info,
|
||||||
authResult,
|
authResult,
|
||||||
key: keyToMatch
|
key: keyToMatch,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackKey = String(keyText || '').trim()
|
const fallbackKey = String(keyText || '').trim();
|
||||||
if (!fallbackKey) {
|
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 {
|
return {
|
||||||
mode: 'UNKNOWN_BIND_STATE_FALLBACK',
|
mode: 'UNKNOWN_BIND_STATE_FALLBACK',
|
||||||
deviceInfo: info,
|
deviceInfo: info,
|
||||||
authResult: fallbackResult,
|
authResult: fallbackResult,
|
||||||
key: fallbackKey
|
key: fallbackKey,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -173,35 +216,48 @@ export function createAuthModule(ble) {
|
||||||
* 先订阅 protocol:msg 事件,再触发 read,保证不丢通知。
|
* 先订阅 protocol:msg 事件,再触发 read,保证不丢通知。
|
||||||
*/
|
*/
|
||||||
async function readDeviceInfo(options = {}) {
|
async function readDeviceInfo(options = {}) {
|
||||||
const {msg} = ble.getState().characteristics
|
const { msg } = ble.getState().characteristics;
|
||||||
|
|
||||||
if (!msg || !msg.uuid) {
|
if (!msg || !msg.uuid) {
|
||||||
throw new Error('未发现 UUID_MSG 特征值')
|
throw new Error('未发现 UUID_MSG 特征值');
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = Number(options.msgTimeout) || 4000
|
const timeout = Number(options.msgTimeout) || 4000;
|
||||||
let timer = null
|
let timer = null;
|
||||||
let off = null
|
let off = null;
|
||||||
|
|
||||||
const valuePromise = new Promise((resolve, reject) => {
|
const valuePromise = new Promise((resolve, reject) => {
|
||||||
const finish = (callback) => {
|
const finish = (callback) => {
|
||||||
if (timer) clearTimeout(timer)
|
if (timer) clearTimeout(timer);
|
||||||
if (off) off()
|
if (off) off();
|
||||||
callback()
|
callback();
|
||||||
}
|
};
|
||||||
|
|
||||||
off = ble.on('protocol:msg', (payload) => finish(() => resolve(payload)))
|
off = ble.on('protocol:msg', (payload) => finish(() => resolve(payload)));
|
||||||
timer = setTimeout(() => finish(() => reject(new Error('读取 UUID_MSG 超时'))), timeout)
|
timer = setTimeout(
|
||||||
})
|
() => finish(() => reject(new Error('读取 UUID_MSG 超时'))),
|
||||||
|
timeout,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ble.readCharacteristic(msg.uuid)
|
await ble.readCharacteristic(msg.uuid);
|
||||||
const payload = await valuePromise
|
const payload = await valuePromise;
|
||||||
return parseDeviceInfo(payload.value, ble.getProfile().id)
|
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) {
|
} catch (error) {
|
||||||
if (timer) clearTimeout(timer)
|
if (timer) clearTimeout(timer);
|
||||||
if (off) off()
|
if (off) off();
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,9 +268,9 @@ export function createAuthModule(ble) {
|
||||||
* 3) bindStatus=1 写已知密钥并等待 STATE=7。
|
* 3) bindStatus=1 写已知密钥并等待 STATE=7。
|
||||||
*/
|
*/
|
||||||
async function ensureAuthorized(keyText, options = {}) {
|
async function ensureAuthorized(keyText, options = {}) {
|
||||||
ble.setStage(BLE_STAGE.AUTHORIZING)
|
ble.setStage(BLE_STAGE.AUTHORIZING);
|
||||||
const info = await readDeviceInfo(options)
|
const info = await readDeviceInfo(options);
|
||||||
return ensureAuthorizedWithDeviceInfo(ble, info, keyText, options)
|
return ensureAuthorizedWithDeviceInfo(ble, info, keyText, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -222,19 +278,28 @@ export function createAuthModule(ble) {
|
||||||
* 用于”先鉴权再进行 WiFi 配置”的页面流程。
|
* 用于”先鉴权再进行 WiFi 配置”的页面流程。
|
||||||
*/
|
*/
|
||||||
async function ensureAuthorizedByMac(options = {}) {
|
async function ensureAuthorizedByMac(options = {}) {
|
||||||
ble.setStage(BLE_STAGE.AUTHORIZING)
|
ble.setStage(BLE_STAGE.AUTHORIZING);
|
||||||
|
|
||||||
const info = await readDeviceInfo(options)
|
const info = await readDeviceInfo(options);
|
||||||
const profile = ble.getProfile()
|
const profile = ble.getProfile();
|
||||||
const keyByMac = buildKeyFromMac(info.mac, options.suffix || '0000', profile.auth.keyLength)
|
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 {
|
return {
|
||||||
...authResult,
|
...authResult,
|
||||||
key: keyByMac,
|
key: keyByMac,
|
||||||
keyRule: 'MAC+0000'
|
keyRule: 'MAC+0000',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -242,6 +307,6 @@ export function createAuthModule(ble) {
|
||||||
ensureAuthorized,
|
ensureAuthorized,
|
||||||
ensureAuthorizedByMac,
|
ensureAuthorizedByMac,
|
||||||
buildKeyFromMac,
|
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) {
|
export function createRadarModule(ble) {
|
||||||
function requireAuthorized() {
|
function requireAuthorized() {
|
||||||
const state = ble.getState()
|
const state = ble.getState();
|
||||||
|
|
||||||
// 协议要求:获取雷达校准数据前必须先完成密钥匹配。
|
// 协议要求:获取雷达校准数据前必须先完成密钥匹配。
|
||||||
if (!state.auth || state.auth.keyMatched !== true) {
|
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 停止)
|
* @param {number} commandByte 指令字节(如 0xA1 启动、0xA2 停止)
|
||||||
*/
|
*/
|
||||||
async function sendCommand(commandByte) {
|
async function sendCommand(commandByte) {
|
||||||
const { cmd } = ble.getState().characteristics
|
const { cmd } = ble.getState().characteristics;
|
||||||
|
|
||||||
if (!cmd || !cmd.uuid) {
|
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)。 */
|
/** 启动雷达数据流(写入 CMD = 0xA1)。 */
|
||||||
async function startRadarStream() {
|
async function startRadarStream() {
|
||||||
const profile = ble.getProfile()
|
const profile = ble.getProfile();
|
||||||
|
|
||||||
// 协议1.2.2:先密钥匹配,再向 UUID_CMD 写 0xA1。
|
// 协议1.2.2:先密钥匹配,再向 UUID_CMD 写 0xA1。
|
||||||
requireAuthorized()
|
requireAuthorized();
|
||||||
await sendCommand(profile.radar.startCommand)
|
await sendCommand(profile.radar.startCommand);
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 停止雷达数据流(写入 CMD = 0xA2)。 */
|
/** 停止雷达数据流(写入 CMD = 0xA2)。 */
|
||||||
async function stopRadarStream() {
|
async function stopRadarStream() {
|
||||||
const profile = ble.getProfile()
|
const profile = ble.getProfile();
|
||||||
|
|
||||||
// 停止同样要求处于鉴权通过态,避免误发到未鉴权连接。
|
// 停止同样要求处于鉴权通过态,避免误发到未鉴权连接。
|
||||||
requireAuthorized()
|
requireAuthorized();
|
||||||
await sendCommand(profile.radar.stopCommand)
|
await sendCommand(profile.radar.stopCommand);
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,20 +55,23 @@ export function createRadarModule(ble) {
|
||||||
* @param {boolean} enabled true=开启,false=关闭
|
* @param {boolean} enabled true=开启,false=关闭
|
||||||
*/
|
*/
|
||||||
async function setNarrowMode(enabled) {
|
async function setNarrowMode(enabled) {
|
||||||
const profile = ble.getProfile()
|
const profile = ble.getProfile();
|
||||||
|
|
||||||
if (!profile.cmd.supportsNarrowMode) {
|
if (!profile.cmd.supportsNarrowMode) {
|
||||||
throw new Error('当前设备不支持窄床模式设置')
|
throw new Error('当前设备不支持窄床模式设置');
|
||||||
}
|
}
|
||||||
|
|
||||||
requireAuthorized()
|
requireAuthorized();
|
||||||
|
|
||||||
// 协议规定:写入 [0x7C, flag] 两字节
|
// 协议规定:写入 [0x7C, flag] 两字节
|
||||||
const { cmd } = ble.getState().characteristics
|
const { cmd } = ble.getState().characteristics;
|
||||||
if (!cmd || !cmd.uuid) {
|
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 跌倒参数字节
|
* @param {Uint8Array|number[]} rawParams 跌倒参数字节
|
||||||
*/
|
*/
|
||||||
async function writeFallParams(rawParams) {
|
async function writeFallParams(rawParams) {
|
||||||
const profile = ble.getProfile()
|
const profile = ble.getProfile();
|
||||||
|
|
||||||
if (!profile.cmd.supportsFallParam67) {
|
if (!profile.cmd.supportsFallParam67) {
|
||||||
throw new Error('当前设备不支持 0x67 跌倒参数写入')
|
throw new Error('当前设备不支持 0x67 跌倒参数写入');
|
||||||
}
|
}
|
||||||
|
|
||||||
requireAuthorized()
|
requireAuthorized();
|
||||||
|
|
||||||
const payload = rawParams instanceof Uint8Array ? rawParams : new Uint8Array(rawParams)
|
const payload =
|
||||||
const packet = new Uint8Array(1 + payload.length)
|
rawParams instanceof Uint8Array ? rawParams : new Uint8Array(rawParams);
|
||||||
packet[0] = 0x67
|
const packet = new Uint8Array(1 + payload.length);
|
||||||
packet.set(payload, 1)
|
packet[0] = 0x67;
|
||||||
|
packet.set(payload, 1);
|
||||||
|
|
||||||
const { cmd } = ble.getState().characteristics
|
const { cmd } = ble.getState().characteristics;
|
||||||
if (!cmd || !cmd.uuid) {
|
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 解析雷达通知数据。 */
|
/** 按当前 profile 解析雷达通知数据。 */
|
||||||
function parseNotifyPayload(payload) {
|
function parseNotifyPayload(payload) {
|
||||||
const profile = ble.getProfile()
|
const profile = ble.getProfile();
|
||||||
return parseRadarData(payload, profile.id)
|
return parseRadarData(payload, profile.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -110,6 +114,6 @@ export function createRadarModule(ble) {
|
||||||
setNarrowMode,
|
setNarrowMode,
|
||||||
writeFallParams,
|
writeFallParams,
|
||||||
parseNotifyPayload,
|
parseNotifyPayload,
|
||||||
requireAuthorized
|
requireAuthorized,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,16 @@ import { waitForProtocolState } from '../core/stateWait'
|
||||||
|
|
||||||
const WIFI_FINAL_STATE_CODES = Object.freeze([1, 2, 3])
|
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。
|
* 校验 WiFi 入参,规则来源于 profile.wifi。
|
||||||
*/
|
*/
|
||||||
|
|
@ -27,12 +37,15 @@ function validateWifi(profile, ssid, password) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 校验鉴权状态,未完成密钥匹配则抛出异常。 */
|
/**
|
||||||
|
* 校验鉴权状态。
|
||||||
|
* 厂家确认密钥流程已不启用,此处仅记录未鉴权状态,不阻断 WiFi 配置。
|
||||||
|
*/
|
||||||
function requireAuthorized(ble) {
|
function requireAuthorized(ble) {
|
||||||
const state = ble.getState()
|
const state = ble.getState()
|
||||||
|
|
||||||
if (!state.auth || state.auth.keyMatched !== true) {
|
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} ssid WiFi名称
|
||||||
* @param {string} password WiFi密码
|
* @param {string} password WiFi密码
|
||||||
* @param {object} options
|
* @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.preDelayMs 写入前延迟(ms),ED713 默认120ms
|
||||||
* @param {number} options.packetInterval 分包写入间隔(ms)
|
* @param {number} options.packetInterval 分包写入间隔(ms)
|
||||||
* @returns {{ success: boolean, stateCode: number }} stateCode=3 表示连接成功
|
* @returns {{ success: boolean, stateCode: number }} stateCode=3 表示连接成功
|
||||||
|
|
@ -70,12 +83,16 @@ export function createWifiModule(ble) {
|
||||||
throw new Error('未发现 UUID_WIFI 特征值')
|
throw new Error('未发现 UUID_WIFI 特征值')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalTimeout = options.timeout || WIFI_TOTAL_TIMEOUT_MS
|
||||||
// 先注册 STATE 等待,再写入,确保不漏回执
|
// 先注册 STATE 等待,再写入,确保不漏回执
|
||||||
|
// 收到 STATE=4(连接中) 时刷新窗口,避免固定上限短于真实 DHCP 时长误判超时
|
||||||
const waitPromise = waitForProtocolState(ble, {
|
const waitPromise = waitForProtocolState(ble, {
|
||||||
allowedCodes: WIFI_FINAL_STATE_CODES,
|
allowedCodes: WIFI_FINAL_STATE_CODES,
|
||||||
rejectOnUnexpected: false,
|
rejectOnUnexpected: false,
|
||||||
timeout: options.timeout || 12000,
|
timeout: totalTimeout,
|
||||||
timeoutMessage: '等待 WiFi 状态超时'
|
timeoutMessage: '等待 WiFi 状态超时',
|
||||||
|
refreshOnCodes: WIFI_CONNECTING_STATE_CODES,
|
||||||
|
refreshTimeout: WIFI_REFRESH_TIMEOUT_MS
|
||||||
})
|
})
|
||||||
|
|
||||||
// ED713 固件需要写入前延迟,否则可能丢包
|
// ED713 固件需要写入前延迟,否则可能丢包
|
||||||
|
|
@ -86,12 +103,23 @@ export function createWifiModule(ble) {
|
||||||
}
|
}
|
||||||
|
|
||||||
log('write WiFi start', { service: ble.getState().serviceId, char: wifi.uuid })
|
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, {
|
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)
|
log('STATE(final) received for WiFi=', code)
|
||||||
|
|
||||||
// STATE=3 表示 WiFi 连接成功
|
// STATE=3 表示 WiFi 连接成功
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue