feat(ble): 重构蓝牙认证模块并新增WiFi配置功能

- 新增buildKeyFromMac函数,按协议生成MAC+0000固定规则密钥
- 在设备列表页添加WiFi配置弹层界面
- 实现WiFi名称密码输入、当前WiFi获取、历史配置导入功能
- 添加prepareWifiConfig流程,专用于WiFi配置前的MAC鉴权
- 更新configureWifi方法,使用新的状态等待机制并验证授权状态
This commit is contained in:
ozh 2026-04-20 15:11:00 +08:00
parent 599a01cd73
commit aa0264ba19
9 changed files with 729 additions and 136 deletions

BIN
docs/ED713-ble.pdf Normal file

Binary file not shown.

BIN
docs/ED719-ble.docx Normal file

Binary file not shown.

View File

@ -692,6 +692,20 @@ export function createBluetoothDiscovery(options = {}) {
return result
}
/**
* MAC固定规则鉴权密钥=MAC(去分隔符)+0000
*/
async function authorizeDeviceByMac(options = {}) {
if (!currentDeviceId) {
throw new Error('请先连接设备后再鉴权')
}
const result = await controller.auth.ensureAuthorizedByMac(options)
lastAuthResult = result
onAuthChange(result)
return result
}
/**
* 标准连接流程连接 -> 发现 -> 订阅 -> 鉴权
*/
@ -721,6 +735,14 @@ export function createBluetoothDiscovery(options = {}) {
return result
}
/**
* WiFi配置专用流程连接 -> 发现 -> 订阅 -> MAC+0000 完成鉴权
*/
async function prepareWifiConfig(deviceId, options = {}) {
await connectDevice(deviceId)
return authorizeDeviceByMac(options)
}
async function readDeviceInfo() {
const info = await controller.auth.readDeviceInfo()
lastDeviceInfo = info
@ -806,7 +828,9 @@ export function createBluetoothDiscovery(options = {}) {
// 连接与协议流程能力。
connectDevice,
authorizeDevice,
authorizeDeviceByMac,
standardConnectFlow,
prepareWifiConfig,
disconnectDevice,
// 协议业务能力。

View File

@ -31,19 +31,106 @@
</view>
<text class="device-id">{{ device.deviceId }}</text>
<view class="device-actions">
<view class="action-btn">网络配置</view>
<view class="action-btn" :class="{ disabled: wifiPreparing }" @click.stop="openWifiPanel(device)">
{{ wifiPreparing ? '鉴权中' : '网络配置' }}
</view>
<view class="action-btn">参数配置</view>
<view class="action-btn">服务器配置</view>
</view>
</view>
</view>
</view>
<view v-if="wifiPanelVisible" class="wifi-mask" @click="closeWifiPanel">
<view class="wifi-dialog" @click.stop>
<view class="wifi-dialog-header">
<text class="wifi-dialog-title">设备网络设置</text>
<text class="wifi-dialog-close" @click="closeWifiPanel">×</text>
</view>
<view class="wifi-device-row">
<text class="wifi-device-label">设备</text>
<text class="wifi-device-value">{{ selectedDeviceName }}</text>
</view>
<view class="wifi-row wifi-row-ssid">
<text class="wifi-label"><text class="wifi-required">*</text>WiFi名称</text>
<input
v-model.trim="wifiForm.ssid"
class="wifi-input wifi-input-ssid"
placeholder="请输入 WiFi 名称"
:maxlength="32"
confirm-type="done"
/>
<view class="wifi-current-btn" :class="{ disabled: fetchingCurrentWifi }" @click.stop="fetchCurrentWifiSsid">
{{ fetchingCurrentWifi ? '获取中...' : '一键获取当前连接WiFi' }}
</view>
</view>
<view class="wifi-row">
<text class="wifi-label"><text class="wifi-required">*</text>WiFi密码</text>
<input
v-model.trim="wifiForm.password"
class="wifi-input"
placeholder="请输入密码"
password
:maxlength="63"
confirm-type="done"
/>
</view>
<view class="wifi-import-row" @click="importLastWifiConfig">
一键导入上次 WiFi 配置
</view>
<view class="wifi-submit-btn" :class="{ disabled: wifiSending }" @click="confirmWifiSettings">
{{ wifiSending ? '发送中...' : '发送' }}
</view>
</view>
</view>
</view>
</template>
<script>
import { createBluetoothDiscovery } from '@/hooks/useBluetoothDiscovery'
import { navigateBack } from '@/utils/navigation'
import { showToast } from '@/utils/toast'
const WIFI_HISTORY_STORAGE_KEY = 'SMARTHOME_LAST_WIFI_CONFIG'
const WIFI_STATE_MESSAGES = Object.freeze({
1: 'WiFi 参数校验失败,请检查名称与密码',
2: '设备连接该 WiFi 失败,请确认密码是否正确',
3: 'WiFi 配置成功',
4: '设备正在连接 WiFi请稍候'
})
function getErrorMessage(error) {
if (!error) {
return '操作失败,请重试'
}
if (typeof error === 'string') {
return error
}
return error.message || error.errMsg || '操作失败,请重试'
}
function promisifyUniApi(apiName, params = {}) {
return new Promise((resolve, reject) => {
if (typeof uni[apiName] !== 'function') {
reject(new Error(`当前环境不支持 ${apiName}`))
return
}
uni[apiName]({
...params,
success: (res) => resolve(res),
fail: (err) => reject(err)
})
})
}
export default {
data() {
@ -51,17 +138,37 @@ export default {
isSearching: false,
deviceList: [],
deviceMap: {},
discoveryController: null
discoveryController: null,
// WiFi
wifiPanelVisible: false,
wifiPreparing: false,
wifiSending: false,
fetchingCurrentWifi: false,
wifiPrepared: false,
selectedWifiDevice: null,
wifiForm: {
ssid: '',
password: ''
}
}
},
computed: {
searchBtnText() {
return this.isSearching ? '搜索中' : '搜索'
},
selectedDeviceName() {
if (!this.selectedWifiDevice) {
return '未选择设备'
}
return this.selectedWifiDevice.showName || this.selectedWifiDevice.deviceId || '未命名设备'
}
},
created() {
/**
* 蓝牙搜索控制器统一处理设备发现排序清理逻辑
* 蓝牙搜索控制器统一处理设备发现连接鉴权WiFi 发送等流程
*/
this.discoveryController = createBluetoothDiscovery({
onSearchingChange: (value) => {
@ -82,11 +189,38 @@ export default {
this.cleanupDiscovery()
},
methods: {
async safeDisconnect() {
if (!this.discoveryController) {
return
}
try {
await this.discoveryController.disconnectDevice()
} catch (error) {
}
},
cleanupWifiPanelState() {
this.wifiPanelVisible = false
this.wifiPrepared = false
this.wifiPreparing = false
this.fetchingCurrentWifi = false
this.selectedWifiDevice = null
this.resetWifiForm()
},
resetWifiForm() {
//
this.wifiForm.ssid = ''
this.wifiForm.password = ''
},
cleanupDiscovery() {
if (!this.discoveryController) {
return
}
this.cleanupWifiPanelState()
this.discoveryController.cleanupDiscovery()
},
@ -109,6 +243,194 @@ export default {
await this.discoveryController.startSearch()
} catch (error) {
}
},
async openWifiPanel(device) {
if (this.wifiPreparing || this.wifiSending) {
return
}
if (!this.discoveryController) {
return
}
if (!device || !device.deviceId) {
showToast('未选择可配置设备')
return
}
this.wifiPreparing = true
this.wifiPrepared = false
this.selectedWifiDevice = device
uni.showLoading({
title: '密钥鉴权中',
mask: true
})
try {
/**
* 按需求先执行 MAC+0000 密钥鉴权成功后再允许进入 WiFi 弹层
*/
await this.discoveryController.prepareWifiConfig(device.deviceId, {
suffix: '0000',
timeout: 8000
})
this.wifiPrepared = true
this.wifiPanelVisible = true
showToast('密钥鉴权成功请填写WiFi信息')
} catch (error) {
showToast(getErrorMessage(error))
await this.safeDisconnect()
} finally {
this.wifiPreparing = false
uni.hideLoading()
}
},
async closeWifiPanel(force = false) {
if (!force && (this.wifiSending || this.wifiPreparing)) {
return
}
this.cleanupWifiPanelState()
await this.safeDisconnect()
},
async fetchCurrentWifiSsid() {
if (this.fetchingCurrentWifi) {
return
}
this.fetchingCurrentWifi = true
try {
// WiFi getConnectedWifi startWifi
if (typeof uni.startWifi === 'function') {
try {
await promisifyUniApi('startWifi')
} catch (error) {
//
}
}
const result = await promisifyUniApi('getConnectedWifi')
const ssid = String(result?.wifi?.SSID || result?.wifi?.ssid || result?.SSID || '').trim()
if (!ssid || ssid.toLowerCase().includes('unknown')) {
throw new Error('未获取到当前连接 WiFi 名称')
}
this.wifiForm.ssid = ssid
showToast('已填入当前连接WiFi')
} catch (error) {
showToast(getErrorMessage(error))
} finally {
this.fetchingCurrentWifi = false
}
},
importLastWifiConfig() {
try {
const cached = uni.getStorageSync(WIFI_HISTORY_STORAGE_KEY)
if (!cached || typeof cached !== 'object') {
showToast('暂无上次 WiFi 配置')
return
}
const ssid = String(cached.ssid || '').trim()
const password = String(cached.password || '').trim()
if (!ssid || !password) {
showToast('暂无可用的历史 WiFi 配置')
return
}
this.wifiForm.ssid = ssid
this.wifiForm.password = password
showToast('已导入上次 WiFi 配置')
} catch (error) {
showToast('读取历史 WiFi 配置失败')
}
},
cacheWifiConfig(ssid, password) {
try {
uni.setStorageSync(WIFI_HISTORY_STORAGE_KEY, {
ssid,
password,
updatedAt: Date.now()
})
} catch (error) {
}
},
getWifiStateMessage(stateCode) {
return WIFI_STATE_MESSAGES[stateCode] || `WiFi 状态码异常: ${stateCode}`
},
async confirmWifiSettings() {
if (this.wifiSending) {
return
}
if (!this.wifiPrepared) {
showToast('请先完成密钥鉴权')
return
}
const ssid = String(this.wifiForm.ssid || '').trim()
const password = String(this.wifiForm.password || '').trim()
if (!ssid) {
showToast('请输入 WiFi 名称')
return
}
if (!password) {
showToast('请输入 WiFi 密码')
return
}
this.wifiSending = true
let needAutoClose = false
let needCacheWifi = false
let tipMessage = ''
try {
/**
* 发送后必须等待设备通过 UUID_STATE 返回最终结果1/2/3
* 才进行提示并自动关闭弹层
*/
const wifiResult = await this.discoveryController.configureWifi(ssid, password, {
timeout: 12000
})
const stateMessage = this.getWifiStateMessage(wifiResult.stateCode)
tipMessage = stateMessage
needAutoClose = true
needCacheWifi = Boolean(wifiResult.success)
} catch (error) {
// /
tipMessage = getErrorMessage(error)
} finally {
this.wifiSending = false
if (tipMessage) {
showToast(tipMessage)
}
if (needCacheWifi) {
this.cacheWifiConfig(ssid, password)
}
if (needAutoClose) {
await this.closeWifiPanel(true)
}
}
}
}
}
@ -301,4 +623,159 @@ export default {
border-radius: 6rpx;
background: #f8fffc;
}
.action-btn.disabled {
border-color: #9fdcc3;
color: #7db89e;
background: #f5faf7;
}
.wifi-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.48);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 999;
}
.wifi-dialog {
width: 100%;
background: #ffffff;
border-radius: 28rpx 28rpx 0 0;
overflow: hidden;
}
.wifi-dialog-header {
height: 108rpx;
border-bottom: 1rpx solid #efefef;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.wifi-dialog-title {
font-size: 42rpx;
color: #2d2f33;
font-weight: 700;
}
.wifi-dialog-close {
position: absolute;
right: 26rpx;
top: 18rpx;
width: 72rpx;
height: 72rpx;
text-align: center;
line-height: 72rpx;
font-size: 56rpx;
color: #b8bcc3;
}
.wifi-device-row {
min-height: 74rpx;
display: flex;
align-items: center;
gap: 14rpx;
padding: 0 30rpx;
border-bottom: 1rpx solid #f4f4f4;
}
.wifi-device-label {
font-size: 28rpx;
color: #8a8f98;
}
.wifi-device-value {
flex: 1;
min-width: 0;
font-size: 27rpx;
color: #4a4f56;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wifi-row {
min-height: 102rpx;
border-bottom: 1rpx solid #f2f2f2;
display: flex;
align-items: center;
padding: 0 30rpx;
gap: 20rpx;
}
.wifi-row-ssid {
align-items: center;
}
.wifi-label {
width: 170rpx;
font-size: 34rpx;
color: #363b45;
}
.wifi-required {
color: #ff4f4f;
margin-right: 8rpx;
}
.wifi-input {
flex: 1;
min-width: 0;
height: 80rpx;
font-size: 32rpx;
color: #1f2329;
}
.wifi-input-ssid {
max-width: 260rpx;
}
.wifi-current-btn {
flex-shrink: 0;
min-width: 190rpx;
height: 62rpx;
line-height: 62rpx;
border: 1rpx solid #74dcb2;
border-radius: 8rpx;
color: #24b478;
background: #effcf5;
text-align: center;
font-size: 24rpx;
padding: 0 12rpx;
}
.wifi-current-btn.disabled {
border-color: #b8d8ca;
color: #9bbbae;
background: #f3f6f4;
}
.wifi-import-row {
min-height: 100rpx;
border-bottom: 1rpx solid #f2f2f2;
font-size: 34rpx;
color: #3bc58d;
display: flex;
align-items: center;
justify-content: center;
}
.wifi-submit-btn {
height: 100rpx;
background: #11c462;
color: #ffffff;
font-size: 36rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.wifi-submit-btn.disabled {
background: #8edcae;
}
</style>

View File

@ -32,6 +32,34 @@ export function buildA7Packets(payload, packetConfig = {}) {
})
}
/**
* payload A7 规则分包后顺序写入目标特征
* 统一 auth/wifi 分包发送逻辑避免多处重复实现
*/
export async function writeA7Payload(ble, characteristicId, payload, packetConfig = {}, options = {}) {
if (!ble || typeof ble.writeCharacteristic !== 'function') {
throw new Error('writeA7Payload 缺少有效的 ble 实例')
}
if (!characteristicId) {
throw new Error('writeA7Payload 缺少 characteristicId')
}
const { packetInterval = 0 } = options
const packets = buildA7Packets(payload, packetConfig)
for (const packet of packets) {
await ble.writeCharacteristic(characteristicId, packet)
if (packetInterval > 0) {
// 部分固件在连续写入时需要极短间隔,默认 0 兼容原行为。
await new Promise((resolve) => setTimeout(resolve, packetInterval))
}
}
return packets.length
}
/**
* 合并多个 A7 分包数据体去掉前两字节头信息后拼接
*/

View File

@ -0,0 +1,54 @@
/**
* 统一等待 UUID_STATE 通知
*
* @param {object} ble createBleCore 实例
* @param {object} options 等待配置
* @param {number[]} options.allowedCodes 允许通过的状态码为空则接收任意状态码
* @param {boolean} options.rejectOnUnexpected 收到非 allowedCodes 状态码时是否立刻失败
* @param {number} options.timeout 超时时间(ms)
* @param {string} options.timeoutMessage 超时提示
* @param {string} options.unexpectedMessage 非预期状态提示前缀
*/
export function waitForProtocolState(ble, options = {}) {
const {
allowedCodes = [],
rejectOnUnexpected = false,
timeout = 6000,
timeoutMessage = '等待 UUID_STATE 超时',
unexpectedMessage = '收到非预期 UUID_STATE 状态码'
} = options
return new Promise((resolve, reject) => {
let timer = null
let off = null
const finish = (callback) => {
if (timer) {
clearTimeout(timer)
}
if (off) {
off()
}
callback()
}
timer = setTimeout(() => {
finish(() => reject(new Error(timeoutMessage)))
}, timeout)
off = ble.on('protocol:state', (payload) => {
const stateCode = Number(payload?.stateCode)
if (!allowedCodes.length || allowedCodes.includes(stateCode)) {
finish(() => resolve(payload))
return
}
if (rejectOnUnexpected) {
finish(() => reject(new Error(`${unexpectedMessage}: ${stateCode}`)))
}
})
})
}

View File

@ -12,7 +12,8 @@ export { createRadarModule } from './modules/radar'
export { BLE_STAGE } from './core/state'
export { DEVICE_PROFILE, getBleProfile, guessProfileByRadarFrame } from './protocols/profiles'
export { buildA7Packets } from './core/packet'
export { buildA7Packets, writeA7Payload } from './core/packet'
export { waitForProtocolState } from './core/stateWait'
export { bytesToHex, hexToBytes, utf8ToBytes, bytesToUtf8, bytesToMac } from './utils/hex'
export { splitToLowHigh, joinLowHigh, readUint16LE, readUint32LE, toSignedInt8 } from './utils/bytes'

View File

@ -1,34 +1,8 @@
import { buildA7Packets } from '../core/packet'
import { writeA7Payload } from '../core/packet'
import { waitForProtocolState } from '../core/stateWait'
import { parseDeviceInfo } from '../parsers'
import { BLE_STAGE } from '../core/state'
function waitForStateCode(ble, timeout = 6000) {
return new Promise((resolve, reject) => {
let timer = null
let off = null
const finish = (callback) => {
if (timer) {
clearTimeout(timer)
}
if (off) {
off()
}
callback()
}
timer = setTimeout(() => {
finish(() => reject(new Error('等待 UUID_STATE 超时')))
}, timeout)
off = ble.on('protocol:state', (payload) => {
finish(() => resolve(payload))
})
})
}
function generateRandomKey(length = 16) {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'
let result = ''
@ -41,6 +15,34 @@ function generateRandomKey(length = 16) {
return result
}
function normalizeMacForKey(macText = '') {
return String(macText || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
}
/**
* 按协议约定生成设备密钥MAC(去分隔符) + 固定后缀
* 默认后缀为 "0000"对应 12 MAC + 4 位后缀 = 16 位密钥
*/
export function buildKeyFromMac(macText, suffix = '0000', expectedLength = 16) {
const normalizedMac = normalizeMacForKey(macText)
const normalizedSuffix = String(suffix || '').trim()
if (!normalizedSuffix) {
throw new Error('密钥后缀不能为空')
}
const requiredMacLength = expectedLength - normalizedSuffix.length
if (requiredMacLength <= 0) {
throw new Error('密钥长度配置异常')
}
if (normalizedMac.length !== requiredMacLength) {
throw new Error(`设备 MAC 长度异常,期望 ${requiredMacLength} 位,实际 ${normalizedMac.length}`)
}
return `${normalizedMac}${normalizedSuffix}`
}
async function writePacketsToKeyCharacteristic(ble, keyText) {
const { key } = ble.getState().characteristics
@ -55,22 +57,23 @@ async function writePacketsToKeyCharacteristic(ble, keyText) {
throw new Error(`密钥长度必须为 ${profile.auth.keyLength}`)
}
const packets = buildA7Packets(keyValue, profile.packet)
for (const packet of packets) {
await ble.writeCharacteristic(key.uuid, packet)
}
await writeA7Payload(ble, key.uuid, keyValue, profile.packet)
return keyValue
}
async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options = {}) {
const keyValue = await writePacketsToKeyCharacteristic(ble, keyText)
const stateEvent = await waitForStateCode(ble, options.timeout || 6000)
const stateCode = stateEvent.stateCode
if (!expectedStateCodes.includes(stateCode)) {
throw new Error(`密钥流程状态异常,期望=${expectedStateCodes.join('/')} 实际=${stateCode}`)
}
const stateEvent = await waitForProtocolState(ble, {
allowedCodes: expectedStateCodes,
rejectOnUnexpected: true,
timeout: options.timeout || 6000,
timeoutMessage: '等待 UUID_STATE 超时',
unexpectedMessage: '密钥流程状态异常'
})
const stateCode = Number(stateEvent.stateCode)
ble.setStage(BLE_STAGE.AUTHORIZED)
ble.setAuthState({
@ -85,6 +88,58 @@ async function writeKeyAndExpectStates(ble, keyText, expectedStateCodes, options
}
}
async function ensureAuthorizedWithDeviceInfo(ble, info, keyText, options = {}) {
const bindStatus = Number(info.bindStatus)
const profile = ble.getProfile()
ble.setAuthState({
bound: Number.isNaN(bindStatus) ? null : bindStatus,
keyMatched: false
})
if (bindStatus === 0) {
const keyToBind = String(keyText || '').trim() || generateRandomKey(profile.auth.keyLength)
const authResult = await writeKeyAndExpectStates(ble, keyToBind, [5], options)
return {
mode: 'BIND',
deviceInfo: info,
authResult,
key: keyToBind
}
}
if (bindStatus === 1) {
const keyToMatch = String(keyText || '').trim()
if (!keyToMatch) {
throw new Error('设备已绑定,必须提供已绑定密钥用于匹配')
}
const authResult = await writeKeyAndExpectStates(ble, keyToMatch, [7], options)
return {
mode: 'MATCH',
deviceInfo: info,
authResult,
key: keyToMatch
}
}
const fallbackKey = String(keyText || '').trim()
if (!fallbackKey) {
throw new Error(`无法识别的绑定状态: ${info.bindStatus},且未提供密钥`)
}
const fallbackResult = await writeKeyAndExpectStates(ble, fallbackKey, [7, 5], options)
return {
mode: 'UNKNOWN_BIND_STATE_FALLBACK',
deviceInfo: info,
authResult: fallbackResult,
key: fallbackKey
}
}
export function createAuthModule(ble) {
async function readDeviceInfo() {
const { msg } = ble.getState().characteristics
@ -108,73 +163,41 @@ export function createAuthModule(ble) {
/**
* 严格遵循协议1.2绑定流程
* 1) 先读取 UUID_MSG 获取 bindStatus
* 2) bindStatus=0 -> 写随机密钥并等待 UUID_STATE=5(绑定成功)
* 3) bindStatus=1 -> 写已知密钥并等待 UUID_STATE=7(密钥匹配成功)
*
* 兼容说明当某些固件 MSG 未明确返回 0/1 回退为已知密钥匹配优先
* 允许状态 7(匹配成功) 5(设备首次绑定成功)
* 1) 读取 UUID_MSG bindStatus
* 2) bindStatus=0 写随机/指定密钥并等待 STATE=5
* 3) bindStatus=1 写已知密钥并等待 STATE=7
*/
async function ensureAuthorized(keyText, options = {}) {
ble.setStage(BLE_STAGE.AUTHORIZING)
const info = await readDeviceInfo()
return ensureAuthorizedWithDeviceInfo(ble, info, keyText, options)
}
/**
* 固定规则鉴权密钥 = 设备MAC(去分隔符) + "0000"
* 用于先鉴权再进行 WiFi 配置的页面流程
*/
async function ensureAuthorizedByMac(options = {}) {
ble.setStage(BLE_STAGE.AUTHORIZING)
const info = await readDeviceInfo()
const bindStatus = Number(info.bindStatus)
const profile = ble.getProfile()
const keyByMac = buildKeyFromMac(info.mac, options.suffix || '0000', profile.auth.keyLength)
ble.setAuthState({
bound: Number.isNaN(bindStatus) ? null : bindStatus,
keyMatched: false
})
// 未绑定:按协议执行绑定。
if (bindStatus === 0) {
const keyToBind = String(keyText || '').trim() || generateRandomKey(ble.getProfile().auth.keyLength)
const authResult = await writeKeyAndExpectStates(ble, keyToBind, [5], options)
return {
mode: 'BIND',
deviceInfo: info,
authResult,
key: keyToBind
}
}
// 已绑定:按协议执行密钥匹配。
if (bindStatus === 1) {
const keyToMatch = String(keyText || '').trim()
if (!keyToMatch) {
throw new Error('设备已绑定,必须提供已绑定密钥用于匹配')
}
const authResult = await writeKeyAndExpectStates(ble, keyToMatch, [7], options)
return {
mode: 'MATCH',
deviceInfo: info,
authResult,
key: keyToMatch
}
}
// 绑定状态不明确时,执行兼容分支(优先匹配,允许绑定成功)。
const fallbackKey = String(keyText || '').trim()
if (!fallbackKey) {
throw new Error(`无法识别的绑定状态: ${info.bindStatus},且未提供密钥`)
}
const fallbackResult = await writeKeyAndExpectStates(ble, fallbackKey, [7, 5], options)
const authResult = await ensureAuthorizedWithDeviceInfo(ble, info, keyByMac, options)
return {
mode: 'UNKNOWN_BIND_STATE_FALLBACK',
deviceInfo: info,
authResult: fallbackResult,
key: fallbackKey
...authResult,
key: keyByMac,
keyRule: 'MAC+0000'
}
}
return {
readDeviceInfo,
ensureAuthorized,
ensureAuthorizedByMac,
buildKeyFromMac,
generateRandomKey
}
}

View File

@ -1,4 +1,7 @@
import { buildA7Packets } from '../core/packet'
import { writeA7Payload } from '../core/packet'
import { waitForProtocolState } from '../core/stateWait'
const WIFI_FINAL_STATE_CODES = Object.freeze([1, 2, 3])
/**
* 校验 WiFi 入参规则来源于 profile.wifi
@ -24,50 +27,25 @@ function validateWifi(profile, ssid, password) {
}
}
/**
* 监听协议状态通知等待 WiFi 配网状态码返回
*/
function waitForWifiState(ble, timeout = 8000) {
return new Promise((resolve, reject) => {
let timer = null
let off = null
function requireAuthorized(ble) {
const state = ble.getState()
const clearAll = () => {
if (timer) {
clearTimeout(timer)
}
if (off) {
off()
}
}
timer = setTimeout(() => {
clearAll()
reject(new Error('等待 WiFi 状态超时'))
}, timeout)
off = ble.on('protocol:state', (payload) => {
if (![1, 2, 3, 4].includes(payload.stateCode)) {
return
}
clearAll()
resolve(payload.stateCode)
})
})
if (!state.auth || state.auth.keyMatched !== true) {
throw new Error('请先完成密钥鉴权,再发送 WiFi 配置')
}
}
/**
* WiFi 配网模块负责打包发送 ssid|password随后等待状态通知
* WiFi 配网模块负责打包发送 ssid|password并等待 STATE 回执
*/
export function createWifiModule(ble) {
async function configureWifi(ssid, password, options = {}) {
const profile = ble.getProfile()
const targetSsid = String(ssid || '')
const targetPass = String(password || '')
const targetSsid = String(ssid || '').trim()
const targetPass = String(password || '').trim()
validateWifi(profile, targetSsid, targetPass)
requireAuthorized(ble)
const { wifi } = ble.getState().characteristics
if (!wifi || !wifi.uuid) {
@ -75,14 +53,21 @@ export function createWifiModule(ble) {
}
const payload = `${targetSsid}|${targetPass}`
const packets = buildA7Packets(payload, profile.packet)
// 按分包顺序逐包发送,保持设备端解析一致性
for (const packet of packets) {
await ble.writeCharacteristic(wifi.uuid, packet)
}
// 协议要求 WiFi 参数按 0xA7 分包发送到 UUID_WIFI(F401)
await writeA7Payload(ble, wifi.uuid, payload, profile.packet, {
packetInterval: Number(options.packetInterval) || 0
})
const stateCode = await waitForWifiState(ble, options.timeout || 8000)
const stateEvent = await waitForProtocolState(ble, {
// 仅等待最终态,避免在“连接中(4)”时过早返回导致页面提前结束流程。
allowedCodes: WIFI_FINAL_STATE_CODES,
rejectOnUnexpected: false,
timeout: options.timeout || 12000,
timeoutMessage: '等待 WiFi 状态超时'
})
const stateCode = Number(stateEvent.stateCode)
return {
success: stateCode === 3,
@ -91,6 +76,7 @@ export function createWifiModule(ble) {
}
return {
configureWifi
configureWifi,
requireAuthorized
}
}