增加读取门磁、卫浴按钮

This commit is contained in:
2026-01-23 10:14:55 +08:00
parent 8cceee566d
commit 1043e83bd3
9 changed files with 671 additions and 116 deletions

View File

@@ -1,5 +1,5 @@
const { buildCommand, buildReadVersion, buildSetDoorBathEvent, COMMANDS, verifyHexPacket } = require('../../../../utils/w13Packet.js')
const { buildCommand, buildReadVersion, buildSetDoorBathEvent, buildReadDoorBathEvent, COMMANDS, verifyHexPacket } = require('../../../../utils/w13Packet.js')
const FIXED_CONNECT_CMD = new Uint8Array([0xCC, 0xC0, 0x0C, 0x00, 0x4F, 0xC0, 0x01, 0x00, 0x02, 0x00, 0x0C, 0x08])
// Optional encoding library for robust GBK/GB18030 decoding
let EncodingLib = null
@@ -38,6 +38,12 @@ Page({
txCharId: '',
rxCharId: '',
logList: [],
logScrollTo: '',
// 日志滚动区高度px由页面显示时计算并设置
logListHeight: 200,
debugDeviceInfoLogs: false,
isReconnecting: false,
showLastKnown: true,
timeUnits: ['秒', '分', '时'],
hourValues: Array.from({ length: 24 }, (_, i) => i + 1),
msValues: Array.from({ length: 60 }, (_, i) => i + 1),
@@ -128,6 +134,25 @@ Page({
addDialogSelectedGroupIndex: 0,
},
// 动态计算日志区域高度(像素)
// 在页面显示或切换到日志 Tab 时调用,计算公式:高度 = 窗口高度 - 日志卡片顶部
// 若计算结果过小则使用最小值保障可见性
// 中间层:在执行测试按键动作前检查连接状态
maybeOnTestKeyTap(e) {
try {
if (!this.data.isConnected) {
wx.showToast({ title: '未连接设备', icon: 'none' })
this.appendLog('UI', '尝试操作测试按键但设备未连接')
return
}
if (typeof this.onTestKeyTap === 'function') return this.onTestKeyTap(e)
} catch (err) {
// swallow
}
},
onLoad(options) {
const raw = options && (options.DevName || options.name) || ''
// 处理通过 URL 传递的编码,避免中文显示为乱码
@@ -167,50 +192,94 @@ Page({
// 更新当前设备的 RSSI / signal / 连接状态(基于 deviceId
updateDeviceInfo() {
//console.log('开始获取信号值')
const deviceId = this.data.deviceId
if (!deviceId) {
this.setData({ bleSignal: '-', bleRSSI: '-', isConnected: false })
return
}
const setUnknown = () => this.setData({ bleSignal: '-', bleRSSI: '-', isConnected: false })
const setUnknown = () => {
this.setData({ bleSignal: '-', bleRSSI: '-', isConnected: false })
}
try {
// 优先使用 getConnectedBluetoothDevices 查询当前已连接设备
// 优先使用 getBLEDeviceRSSI 查询当前已连接设备
const svc = this.data.serviceId
if (typeof wx.getConnectedBluetoothDevices === 'function' && svc) {
wx.getConnectedBluetoothDevices({ services: [svc], success: (res) => {
const devices = (res && res.devices) || []
const found = devices.find(d => d.deviceId === deviceId)
if (found) {
const rssi = (typeof found.RSSI === 'number') ? found.RSSI : (found.RSSI ? Number(found.RSSI) : 0)
//console.log('开始获取信号值1')
if (typeof wx.getBLEDeviceRSSI === 'function' && svc) {
//console.log('开始获取信号值2')
wx.getBLEDeviceRSSI({ deviceId: deviceId, success: (res) => {
//console.log('开始获取信号值3')
////console.log(res)
if (res) {
//console.log('开始获取信号值4')
//console.log(res.RSSI)
const rssi = (typeof res.RSSI === 'number') ? res.RSSI : (found.RSSI ? Number(res.RSSI) : 0)
const sigText = isNaN(rssi) ? '-' : `${rssi} dBm`
// 记录最后已知 RSSI
try { this._lastKnownRSSI = isNaN(rssi) ? null : rssi; this._lastKnownAt = Date.now() } catch (e) { /* ignore */ }
this.setData({ bleRSSI: isNaN(rssi) ? '-' : rssi, bleSignal: sigText, isConnected: true })
return
}
// 未连接
// 若存在最后已知RSSI则展示最后已知值否则展示未知
if (this._lastKnownRSSI != null) {
//console.log('开始获取信号值5')
const age = Math.floor((Date.now() - (this._lastKnownAt || 0)) / 1000)
const sigText = `${this._lastKnownRSSI} dBm (最后已知 ${age}s)`
this.setData({ bleRSSI: this._lastKnownRSSI, bleSignal: sigText, isConnected: false })
} else {
//console.log('开始获取信号值6')
setUnknown()
}
}, fail: () => {
//console.log('开始获取信号值7')
setUnknown()
}, fail: () => setUnknown() })
} })
return
}
//console.log('开始获取信号值8')
// 兜底使用 getBluetoothDevices 查询缓存设备
if (typeof wx.getBluetoothDevices === 'function') {
//console.log('开始获取信号值9')
wx.getBluetoothDevices({ success: (res) => {
//console.log('开始获取信号值10')
const devices = (res && res.devices) || []
const found = devices.find(d => d.deviceId === deviceId)
if (found) {
//console.log('开始获取信号值11')
const rssi = (typeof found.RSSI === 'number') ? found.RSSI : (found.RSSI ? Number(found.RSSI) : 0)
const sigText = isNaN(rssi) ? '-' : `${rssi} dBm`
// connected 字段在缓存中可能为 true/false
this.setData({ bleRSSI: isNaN(rssi) ? '-' : rssi, bleSignal: sigText, isConnected: !!found.connected })
try { this._lastKnownRSSI = isNaN(rssi) ? null : rssi; this._lastKnownAt = Date.now() } catch (e) { /* ignore */ }
this.setData({ bleRSSI: isNaN(rssi) ? '-' : rssi, bleSignal: sigText, isConnected: !!found.connected })
return
}
setUnknown()
}, fail: () => setUnknown() })
//console.log('开始获取信号值12')
if (this._lastKnownRSSI != null) {
//console.log('开始获取信号值13')
const age = Math.floor((Date.now() - (this._lastKnownAt || 0)) / 1000)
const sigText = `${this._lastKnownRSSI} dBm (最后已知 ${age}s)`
this.setData({ bleRSSI: this._lastKnownRSSI, bleSignal: sigText, isConnected: false })
} else {
//console.log('开始获取信号值14')
setUnknown()
}
}, fail: () =>{
//console.log('开始获取信号值15')
setUnknown()
} })
return
}
//console.log('开始获取信号值16')
setUnknown()
} catch (e) {
setUnknown()
@@ -322,6 +391,41 @@ Page({
}, 250)
} catch (e) { /* ignore */ }
try { this.updateDeviceInfo && this.updateDeviceInfo() } catch (e) {}
// 启动周期性刷新设备信息(信号/连接状态)
try { this.startDeviceInfoTimer && this.startDeviceInfoTimer() } catch (e) { /* ignore */ }
// 延迟计算日志区域高度,确保布局完成后获取正确位置
try { setTimeout(() => { try { this.updateLogListHeight && this.updateLogListHeight() } catch (e) {} }, 250) } catch (e) {}
},
onHide() {
// 页面隐藏时不停止设备信息轮询,保留以便断开后继续刷新(如需可在此处暂停)
},
updateLogListHeight() {
// 1. 拿到屏幕可用高度px
const sys = wx.getWindowInfo();
const screenHeight = sys.windowHeight; // px
// 2. 拿到 scroll-view 的 toppx
wx.createSelectorQuery()
.in(this)
.select('#cnmdgdx')
.boundingClientRect(rect => {
if (rect) {
const topPx = rect.top; // px
const bottomPx = 10 / 2; // 10rpx → 5px2倍屏
const heightPx = screenHeight - topPx - bottomPx;
// 3. 转 rpx 并写入
this.setData({
logListHeight: heightPx * 2 // px→rpx
});
}
})
.exec();
},
onUnload() {
@@ -334,6 +438,7 @@ Page({
} catch (e) { /* ignore */ }
this._onBleConnChange = null
try { this.stopReconnectTimer && this.stopReconnectTimer() } catch (e) {}
try { this.stopDeviceInfoTimer && this.stopDeviceInfoTimer() } catch (e) { /* ignore */ }
},
// 顶部标签切换
@@ -351,6 +456,10 @@ Page({
// this.onStartOta()
// }, 200)
}
if (id === 4) {
// 切换到通信日志页时计算日志区域高度
try { setTimeout(() => { try { this.updateLogListHeight && this.updateLogListHeight() } catch (e) {} }, 120) } catch (e) {}
}
},
onOpenDelayChange(e) {
@@ -479,6 +588,77 @@ Page({
}
},
// 发送读取门磁/卫浴事件参数请求
sendReadDoorBathEvent() {
try {
const pkt = buildReadDoorBathEvent()
// 设置一个短期的 pendingResponse若收到匹配类型则由 handleIncomingPacket 解析并回调
const self = this
// 清理旧的 pending
try { if (this._pendingResponse && this._pendingResponse._timeout) clearTimeout(this._pendingResponse._timeout) } catch (e) {}
this._pendingResponse = {
expectedType: COMMANDS.READ_DOOR_BATH_EVENT & 0xFF,
resolve(params) {
try {
// params 应为至少 8 字节P0..P7 四个 16-bit 小端数(单位:秒)
if (!params || params.length < 8) {
self.appendLog('PARSE', '读取门卫事件返回长度不足')
wx.showToast({ title: '读取返回长度异常', icon: 'none' })
return
}
const toU16 = (off) => (params[off] & 0xFF) | ((params[off + 1] & 0xFF) << 8)
const doorTriggerSec = toU16(0)
const doorReleaseSec = toU16(2)
const bathTriggerSec = toU16(4)
const bathReleaseSec = toU16(6)
const conv = (sec) => {
const n = Number(sec || 0)
if (n <= 60) return { val: n, unitIdx: 0 }
const mins = Math.max(1, Math.round(n / 60))
return { val: Math.min(mins, 30), unitIdx: 1 }
}
const dTrig = conv(doorTriggerSec)
const dRel = conv(doorReleaseSec)
const bTrig = conv(bathTriggerSec)
const bRel = conv(bathReleaseSec)
self.setData({
doorTriggerDelay: dTrig.val,
doorTriggerUnitIndex: dTrig.unitIdx,
doorReleaseDelay: dRel.val,
doorReleaseUnitIndex: dRel.unitIdx,
bathTriggerDelay: bTrig.val,
bathTriggerUnitIndex: bTrig.unitIdx,
bathReleaseDelay: bRel.val,
bathReleaseUnitIndex: bRel.unitIdx
})
self.appendLog('PARSE', `读取门卫事件: 门触发=${doorTriggerSec}s, 门释放=${doorReleaseSec}s, 卫触发=${bathTriggerSec}s, 卫释放=${bathReleaseSec}s`)
wx.showToast({ title: '已更新事件参数', icon: 'success' })
} catch (ex) {
self.appendLog('WARN', `解析门卫事件返回异常 ${ex && (ex.message||ex)}`)
}
}
}
// 设置超时3s后清理 pending
this._pendingResponse._timeout = setTimeout(() => { try { this._pendingResponse = null } catch (e) {} }, 3000)
this.transmitPacket(pkt, '读取门卫事件')
} catch (err) {
wx.showToast({ title: '构包失败', icon: 'none' })
}
},
onReadDoorEvent() {
if (!this.data.isConnected) { wx.showToast({ title: '未连接设备', icon: 'none' }); return }
this.sendReadDoorBathEvent()
},
onReadBathEvent() {
if (!this.data.isConnected) { wx.showToast({ title: '未连接设备', icon: 'none' }); return }
this.sendReadDoorBathEvent()
},
// 切换雷达读取状态:开始/停止
toggleRadarRead() {
const reading = !!this.data.radarReading
@@ -1250,6 +1430,7 @@ Page({
// 移除旧监听,防止重复触发(页面重复进入或多次初始化的场景)
this.teardownBleListener()
try { if (this.data.debugDeviceInfoLogs) this.appendLog('DBG', 'setupBleListener: re-registering listeners') } catch (e) {}
// 运行环境不支持通知回调则直接返回(避免报错)
if (typeof wx.onBLECharacteristicValueChange !== 'function') return
// 定义并缓存通知回调,便于后续 off 解绑
@@ -1312,12 +1493,20 @@ Page({
if (connected) {
// 连接成功,停止重连计时并启用发送按钮
this.setData({ isConnected: true })
try { if (this.data.debugDeviceInfoLogs) this.appendLog('DBG', 'onBLEConnectionStateChange: connected') } catch (e) {}
this.setData({ isReconnecting: false })
try { this.stopReconnectTimer && this.stopReconnectTimer() } catch (e) {}
try { this.ensureBleChannels(() => {}) } catch (e) {}
try { this.updateDeviceInfo && this.updateDeviceInfo() } catch (e) { /* ignore */ }
try { this.startDeviceInfoTimer && this.startDeviceInfoTimer() } catch (e) { /* ignore */ }
} else {
// 断开,禁用发送并启动重连计时
this.setData({ isConnected: false })
try { if (this.data.debugDeviceInfoLogs) this.appendLog('DBG', 'onBLEConnectionStateChange: disconnected') } catch (e) {}
this.setData({ isReconnecting: true })
try { this.startReconnectTimer && this.startReconnectTimer() } catch (e) {}
try { this.updateDeviceInfo && this.updateDeviceInfo() } catch (e) { /* ignore */ }
try { this.startDeviceInfoTimer && this.startDeviceInfoTimer() } catch (e) { /* ignore */ }
}
} catch (e) { /* ignore */ }
}
@@ -1330,6 +1519,7 @@ Page({
if (this._reconnectTimer) return
const deviceId = this.data.deviceId
if (!deviceId) return
try { this.setData({ isReconnecting: true }) } catch (e) { /* ignore */ }
this._reconnectTimer = setInterval(() => {
try {
if (typeof wx.createBLEConnection === 'function') {
@@ -1337,7 +1527,14 @@ Page({
// createBLEConnection 成功即认为已连接
this.setData({ isConnected: true })
try { this.stopReconnectTimer() } catch (e) {}
try { this.ensureBleChannels(() => {}) } catch (e) {}
try { this.setData({ isReconnecting: false }) } catch (e) { /* ignore */ }
try {
this.ensureBleChannels(() => {
try { this.setupBleListener() } catch (e) { /* ignore */ }
})
} catch (e) { /* ignore */ }
try { this.updateDeviceInfo && this.updateDeviceInfo() } catch (e) { /* ignore */ }
try { this.startDeviceInfoTimer && this.startDeviceInfoTimer() } catch (e) { /* ignore */ }
}, fail: () => {
// ignore failure, will retry
} })
@@ -1351,6 +1548,29 @@ Page({
clearInterval(this._reconnectTimer)
this._reconnectTimer = null
}
try { this.setData({ isReconnecting: false }) } catch (e) { /* ignore */ }
},
// 设备信息轮询管理:每 intervalMs 刷新一次设备信息(幂等)
startDeviceInfoTimer(intervalMs = 1000) {
try {
if (this._deviceInfoTimer) return
const ms = Number(intervalMs) || 1000
this._deviceInfoTimer = setInterval(() => {
try {
if (this.data.debugDeviceInfoLogs) this.appendLog('DBG', 'deviceInfoTimer tick')
this.updateDeviceInfo && this.updateDeviceInfo()
} catch (e) { /* ignore */ }
}, ms)
} catch (e) { /* ignore */ }
},
stopDeviceInfoTimer() {
try {
if (this._deviceInfoTimer) {
clearInterval(this._deviceInfoTimer)
this._deviceInfoTimer = null
}
} catch (e) { /* ignore */ }
},
onDownloadOtaTool() {
const url = 'https://www.baidu.com/';
@@ -1410,10 +1630,20 @@ Page({
} catch (e) { return '' }
},
teardownBleListener() {
if (this._onBleChange && typeof wx.offBLECharacteristicValueChange === 'function') {
wx.offBLECharacteristicValueChange(this._onBleChange)
}
try {
if (this._onBleChange && typeof wx.offBLECharacteristicValueChange === 'function') {
try { if (this.data.debugDeviceInfoLogs) this.appendLog('DBG', 'teardownBleListener: offBLECharacteristicValueChange') } catch (e) {}
wx.offBLECharacteristicValueChange(this._onBleChange)
}
} catch (e) { /* ignore */ }
this._onBleChange = null
try {
if (this._onBleConnChange && typeof wx.offBLEConnectionStateChange === 'function') {
try { if (this.data.debugDeviceInfoLogs) this.appendLog('DBG', 'teardownBleListener: offBLEConnectionStateChange') } catch (e) {}
try { wx.offBLEConnectionStateChange(this._onBleConnChange) } catch (e) { /* ignore */ }
}
} catch (e) { /* ignore */ }
this._onBleConnChange = null
},
handleIncomingPacket(u8) {
@@ -1458,6 +1688,53 @@ Page({
this.appendLog('WARN', '解析读版本响应失败')
}
}
// 读取门磁/卫浴事件参数响应 (Frame_Type = 0x17)
if (frameType === (COMMANDS.READ_DOOR_BATH_EVENT & 0xFF)) {
try {
const params = u8.slice(11) || []
if (params.length >= 8) {
const toU16 = (off) => (params[off] & 0xFF) | ((params[off + 1] & 0xFF) << 8)
const doorTriggerSec = toU16(0)
const doorReleaseSec = toU16(2)
const bathTriggerSec = toU16(4)
const bathReleaseSec = toU16(6)
const conv = (sec) => {
const n = Number(sec || 0)
if (n <= 60) return { val: n, unitIdx: 0 }
const mins = Math.max(1, Math.round(n / 60))
return { val: Math.min(mins, 30), unitIdx: 1 }
}
const dTrig = conv(doorTriggerSec)
const dRel = conv(doorReleaseSec)
const bTrig = conv(bathTriggerSec)
const bRel = conv(bathReleaseSec)
this.setData({
doorTriggerDelay: dTrig.val,
doorTriggerUnitIndex: dTrig.unitIdx,
doorReleaseDelay: dRel.val,
doorReleaseUnitIndex: dRel.unitIdx,
bathTriggerDelay: bTrig.val,
bathTriggerUnitIndex: bTrig.unitIdx,
bathReleaseDelay: bRel.val,
bathReleaseUnitIndex: bRel.unitIdx
})
this.appendLog('PARSE', `读取门卫事件: 门触发=${doorTriggerSec}s, 门释放=${doorReleaseSec}s, 卫触发=${bathTriggerSec}s, 卫释放=${bathReleaseSec}s`)
} else {
this.appendLog('PARSE', '读取门卫事件:返回数据长度不足')
}
// 若存在 pendingResponse交由其 resolveavoid duplicate parsing
try {
if (this._pendingResponse && this._pendingResponse.expectedType === (COMMANDS.READ_DOOR_BATH_EVENT & 0xFF)) {
const params = u8.slice(11) || []
try { this._pendingResponse.resolve(params) } catch (e) { /* ignore */ }
try { if (this._pendingResponse._timeout) clearTimeout(this._pendingResponse._timeout) } catch (e) {}
this._pendingResponse = null
}
} catch (e) { /* ignore */ }
} catch (e) {
this.appendLog('WARN', '解析读取门卫事件响应异常')
}
}
// 如果存在挂起的等待响应onOneKeySend 使用),并且类型匹配,则回调并清理
try {
if (this._pendingResponse && this._pendingResponse.expectedType != null) {
@@ -2733,9 +3010,56 @@ Page({
const key = e.currentTarget.dataset.key
if (!key) return
const checked = (e.detail.value || []).includes(key)
// 合并逻辑:当是 hexShow 时,同时维护 showChineseLogs 并发送协议命令
if (key === 'hexShow') {
// hexShow 为 true 时表示以 HEX 形式展示,需关闭中文日志;反之打开中文日志
const showChinese = !checked
this.setData({ hexShow: checked, showChineseLogs: showChinese })
try { this.sendChineseLogCommand(showChinese) } catch (err) { this.appendLog('WARN', `发送中文日志命令失败: ${err && (err.message || err)}`) }
return
}
this.setData({ [key]: checked })
},
// 发送“中文日志 开/关”命令enable=true 表示开启中文日志设备返回中文false 表示关闭中文日志
sendChineseLogCommand(enable) {
try {
const flag = enable ? 1 : 0
const pkt = buildCommand(COMMANDS.ENABLE_BLE_LOG, [flag])
// 记录发送日志
try { this.appendLog('TX', `中文日志 ${enable ? '开' : '关'} -> ${this.toHex ? this.toHex(pkt) : ''}`) } catch (e) { /* ignore */ }
// 优先使用已存在的写入接口
if (typeof this.writeRawBytes === 'function') {
try { this.writeRawBytes(pkt, '中文日志开关') } catch (e) { /* ignore */ }
return
}
if (typeof this.writeBleBytes === 'function') {
try { this.writeBleBytes(pkt, '中文日志开关') } catch (e) { /* ignore */ }
return
}
// 回退到原生 API若页面保存了 deviceId/serviceId/txCharId
const deviceId = this.data.deviceId || ''
const serviceId = this.data.serviceId || ''
const charId = this.data.txCharId || this.data.txChar || ''
if (deviceId && serviceId && charId && typeof wx.writeBLECharacteristicValue === 'function') {
const ab = new Uint8Array(pkt).buffer
try {
wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: charId, value: ab, success: () => { this.appendLog('UI', '中文日志命令写入成功') }, fail: (e) => { this.appendLog('WARN', `写入中文日志命令失败 ${e && e.errMsg}`) } })
} catch (e) {
this.appendLog('WARN', `写入中文日志命令异常 ${e && (e.message || e)}`)
}
return
}
// 无可用写入路径
this.appendLog('WARN', '未找到可用的写入接口以发送中文日志命令')
} catch (err) {
this.appendLog('WARN', `构造中文日志命令失败: ${err && (err.message || err)}`)
}
},
onInputChange(e) {
this.setData({ sendText: e.detail.value })
},
@@ -2818,7 +3142,8 @@ Page({
const cur = Array.isArray(this.data.logList) ? this.data.logList : []
const next = [{ id, time: timeStr, text: finalText }, ...cur]
if (next.length > maxLogs) next.length = maxLogs
this.setData({ logList: next })
// 更新日志并触发滚动到最新项
this.setData({ logList: next, logScrollTo: id })
} catch (e) {
// 若 setData 出现异常,仍保证不抛到外层
try { console.warn('appendLog setData failed', e) } catch (xx) { /* ignore */ }

View File

@@ -19,7 +19,8 @@
<!-- 公共设备信息栏:放在顶部标签下,所有 Tab 共用 -->
<view class="device-row common-device-row">
<view class="device-left">
<view class="flex" style="width: 100%;" >
<view class="flex-6">
<view class="device-line">
<text class="dr-label">蓝牙名称:</text>
<text class="dr-value">{{bleName || '-'}}</text>
@@ -27,19 +28,30 @@
<view class="device-line">
<text class="dr-label">MAC</text>
<text class="dr-value">{{bleMac || '-'}}</text>
<text class="dr-label">版本:</text>
<text class="dr-value">{{bleVersion || '-'}}</text>
</view>
<view class="device-line">
<text class="dr-label">信号:</text>
<text class="dr-value">{{bleSignal}}</text>
<text class="dr-label">RSSI</text>
<text class="dr-value">{{bleRSSI}}</text>
<text class="dr-label">状态:</text>
<text class="dr-value">{{isConnected ? '已连接' : '未连接'}}</text>
</view>
</view>
</view>
<view class="dr-btn dr-btn-view dr-btn-small" bindtap="onSendReadVersion">读取蓝牙信息</view>
<view class="flex-xis ">
<button class="dr-btn dr-btn-view dr-btn-small" bindtap="onSendReadVersion" disabled="{{!isConnected}}">读取蓝牙信息</button>
</view>
</view>
<view class="flex" style="width: 100%;">
<view class="flex-sub" >
<text class="dr-label">信号:</text>
<text class="dr-value" style="color: {{bleRSSI >= -60 ? 'green' : (bleRSSI >= -80 ? 'orange' : 'red')}};">{{bleSignal}}</text>
</view>
<view class="flex-sub" >
<text class="dr-label">版本:</text>
<text class="dr-value">{{bleVersion || '-'}}</text>
</view>
<view class="flex-sub" >
<text class="dr-label">状态:</text>
<text class="dr-value" style="color: {{isConnected ? 'green':'red'}};">{{isConnected ? '已连接' : '未连接'}}</text>
</view>
</view>
</view>
<!-- Tab: 设备测试 -->
@@ -52,19 +64,17 @@
<button size="mini" type="primary" bindtap="onStartOta">OTA升级</button>
</view> -->
<!-- 顶部设备分类(含读雷达按钮) -->
<!-- 雷达状态卡片(含读雷达按钮) -->
<view class="grid-and-actions">
<view class="grid">
<view class="grid-item" wx:for="{{radarLights}}" wx:key="key">
<!-- 文字先于指示灯显示 -->
<text class="label">
{{
<text class="label"> {{
item.key === 'door' ? (item.triggered ? '门磁(开门)' : '门磁(关门)') :
item.key === 'bath' ? (item.triggered ? '卫浴(触发)' : '卫浴(释放)') :
item.key === 'bed' ? (item.triggered ? '卧室(触发)' : '卧室(释放)') :
item.key === 'hall' ? (item.triggered ? '走廊(触发)' : '走廊(释放)') : item.label
}}
</text>
}} </text>
<view class="circle {{item.colorClass}}"></view>
</view>
</view>
@@ -73,38 +83,38 @@
<!-- 状态功能块:已改为测试按键按钮,点击发送测试按键命令 -->
<view class="cards">
<view class="card {{pressedMask==1?'pressed':''}}" bindtap="onTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x01" data-masknum="1">
<view class="card {{pressedMask==1?'pressed':''}} {{!isConnected?'disabled':''}}" bindtap="maybeOnTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x01" data-masknum="1">
<view class="icon">
<image class="icon-img" src="../../../../images/tabbar/jinmen.png" mode="aspectFill" />
</view>
<text class="card-title">房间有人</text>
</view>
<view class="card {{pressedMask==2?'pressed':''}}" bindtap="onTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x02" data-masknum="2">
<view class="card {{pressedMask==2?'pressed':''}} {{!isConnected?'disabled':''}}" bindtap="maybeOnTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x02" data-masknum="2">
<view class="icon">
<image class="icon-img" src="../../../../images/tabbar/chumen.png" mode="aspectFill" />
</view>
<text class="card-title">房间无人</text>
</view>
<view class="card {{pressedMask==4?'pressed':''}}" bindtap="onTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x04" data-masknum="4">
<view class="card {{pressedMask==4?'pressed':''}} {{!isConnected?'disabled':''}}" bindtap="maybeOnTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x04" data-masknum="4">
<view class="icon">
<image class="icon-img" src="../../../../images/tabbar/menkaiqi.png" mode="aspectFill" />
</view>
<text class="card-title">门开</text>
</view>
<view class="card {{pressedMask==8?'pressed':''}}" bindtap="onTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x08" data-masknum="8">
<view class="card {{pressedMask==8?'pressed':''}} {{!isConnected?'disabled':''}}" bindtap="maybeOnTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x08" data-masknum="8">
<view class="icon">
<image class="icon-img" src="../../../../images/tabbar/menguanbi.png" mode="aspectFill" />
</view>
<text class="card-title">门关</text>
</view>
<view class="card {{pressedMask==16?'pressed':''}}" bindtap="onTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x10" data-masknum="16">
<view class="card {{pressedMask==16?'pressed':''}} {{!isConnected?'disabled':''}}" bindtap="maybeOnTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x10" data-masknum="16">
<view class="icon">
<image class="icon-img" src="../../../../images/tabbar/youren.png" mode="aspectFill" />
</view>
<text class="card-title">卫浴有人</text>
</view>
<view class="card {{pressedMask==32?'pressed':''}}" bindtap="onTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x20" data-masknum="32">
<view class="card {{pressedMask==32?'pressed':''}} {{!isConnected?'disabled':''}}" bindtap="maybeOnTestKeyTap" bindtouchstart="onTestKeyTouchStart" bindtouchend="onTestKeyTouchEnd" bindtouchcancel="onTestKeyTouchEnd" data-mask="0x20" data-masknum="32">
<view class="icon">
<image class="icon-img" src="../../../../images/tabbar/shiyourenkou.png" mode="aspectFill" />
</view>
@@ -116,8 +126,9 @@
<view class="cfg-card tall-controls">
<view class="cfg-head head-row">
<text>门磁开廊灯事件设置</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="sendDoorEvent">设置门磁延时</button>
<view class="head-actions">
<button size="mini" type="primary" bindtap="sendDoorEvent" disabled="{{!isConnected}}">设置门磁延时</button>
<button size="mini" type="default" bindtap="onReadDoorEvent" disabled="{{!isConnected}}">读取门磁延时</button>
</view>
</view>
<view class="form-row">
@@ -142,7 +153,8 @@
<view class="cfg-head head-row">
<text>卫浴雷达开卫浴灯事件设置</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="sendBathEvent">设置卫浴延时</button>
<button size="mini" type="primary" bindtap="sendBathEvent" disabled="{{!isConnected}}">设置卫浴延时</button>
<button size="mini" type="default" bindtap="onReadBathEvent" disabled="{{!isConnected}}">读取卫浴延时</button>
</view>
</view>
<view class="form-row">
@@ -163,22 +175,32 @@
</view>
</view>
<!-- 通讯日志 -->
<view class="log-card">
<view class="log-head">
<text class="title">通讯日志</text>
<view class="log-actions">
<button class="radar-btn" size="mini" type="{{radarReading ? 'warn' : 'primary'}}" bindtap="toggleRadarRead">{{radarReading ? '停止读取' : '读雷达状态'}}</button>
<button class="clear-btn" style="display:none;" size="mini" type="default" bindtap="onClearLogs">清空</button>
<!-- 已移至“设备通信日志”Tab -->
</view>
<!-- Tab: 设备通信日志(独立全屏日志视图) -->
<view wx:if="{{TabCur==4}}" class="content full-log-view">
<view class="cfg-card">
<view class="toolbar">
通信日志
<view class="toolbar-actions">
<button class="radar-btn" size="mini" type="{{radarReading ? 'warn' : 'primary'}}" bindtap="toggleRadarRead" disabled="{{!isConnected}}" >{{radarReading ? '读取雷达状态(关)' : '读取雷达状态(开)'}}</button>
<!-- 中文日志开关已合并到日志选项区域,与 HEX 显示复选框绑定 -->
</view>
</view>
</view>
<!-- 将通讯日志的选项与发送控件移至此处 -->
<view >
<view class="log-options">
<checkbox-group class="option" bindchange="onCheckboxChange" data-key="hexSend">
<label><checkbox value="hexSend" checked="{{hexSend}}" />HEX发送</label>
</checkbox-group>
<checkbox-group class="option" bindchange="onCheckboxChange" data-key="hexShow">
<label><checkbox value="hexShow" checked="{{hexShow}}" />HEX显示</label>
<label><checkbox value="hexShow" checked="{{hexShow}}" disabled="{{!isConnected}}"/>HEX显示</label>
</checkbox-group>
<checkbox-group class="option" bindchange="onCheckboxChange" data-key="withTimestamp">
<label><checkbox value="withTimestamp" checked="{{withTimestamp}}" />时间戳</label>
@@ -192,41 +214,18 @@
<textarea class="log-input" placeholder="输入要发送的数据" value="{{sendText}}" bindinput="onInputChange" disable-default-padding="true" />
<button class="send-btn" size="mini" type="primary" bindtap="onSend" disabled="{{!isConnected}}">发送</button>
</view>
<!-- <scroll-view class="log-scroll" scroll-y="true">
<block wx:if="{{logList.length > 0}}">
<view class="log-item" wx:for="{{logList}}" wx:key="id">
<text class="log-time">{{item.time}}</text>
<text class="log-text">{{item.text}}</text>
</view>
</block>
<view class="log-empty" wx:else>暂无日志记录</view>
</scroll-view> -->
</view>
</view>
<!-- Tab: 设备通信日志(独立全屏日志视图) -->
<view wx:if="{{TabCur==4}}" class="content full-log-view">
<view class="cfg-card">
<view class="toolbar">
通信日志
<view class="toolbar-actions">
<button size="mini" type="default" bindtap="onExportLogs">导出日志</button>
<button size="mini" class="chinese-log-btn {{showChineseLogs ? 'on' : 'off'}}" bindtap="onToggleChineseLogs">{{showChineseLogs ? '中文日志*关' : '中文日志*开'}}</button>
<button size="mini" type="default" bindtap="onClearLogs">清空</button>
</view>
</view>
<!-- 日志显示卡片 -->
<view class="log-card" style="display:flex; flex-direction:column; gap:8rpx; flex:1;">
<view >
<button size="mini" type="default" bindtap="onClearLogs">清空</button>
<button style="margin-left: 20rpx;" size="mini" type="default" bindtap="onExportLogs">导出日志</button>
</view>
<view class="log-card" style="display:flex; flex-direction:column; gap:8rpx; flex:1;">
<scroll-view class="log-scroll" scroll-y="true" style="flex:1;">
<block wx:if="{{logList.length > 0}}">
<view class="log-item" wx:for="{{logList}}" wx:key="id">
<text class="log-time">{{item.time}}</text>
<text class="log-text">{{item.text}}</text>
</view>
</block>
<view class="log-empty" wx:else>暂无日志记录</view>
</scroll-view>
<scroll-view id="cnmdgdx" scroll-y="true" scroll-into-view="{{logScrollTo}}" style="height:{{logListHeight-50}}rpx;" >
<block style="height:95%" wx:for="{{logList}}" wx:key="id">
<view id="{{item.id}}" class="log-item">{{item.time}} - {{item.text}}</view>
</block>
</scroll-view>
</view>
</view>
@@ -242,7 +241,7 @@
<view class="toolbar-actions">
<button size="mini" type="default" bindtap="onExport">导出文件</button>
<button size="mini" type="default" bindtap="onImport">导入文件</button>
<button size="mini" type="primary" bindtap="onOneKeySend">一键下发</button>
<button size="mini" type="primary" bindtap="onOneKeySend" disabled="{{!isConnected}}">一键下发</button>
</view>
</view>
</view>
@@ -251,8 +250,8 @@
<view class="cfg-card">
<view class="cfg-head head-row">
<text>端口配置</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="onSendPorts">端口下发</button>
<view class="head-actions">
<button size="mini" type="primary" bindtap="onSendPorts" disabled="{{!isConnected}}">端口下发</button>
<button class="hide-btn" size="mini" type="default" bindtap="onReadPorts">读取配置</button>
<button class="hide-btn" size="mini" type="primary" bindtap="onSavePorts">保存配置</button>
</view>
@@ -300,7 +299,7 @@
<view class="cfg-head head-row">
<text>条件配置</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="onSendConditions">条件下发</button>
<button size="mini" type="primary" bindtap="onSendConditions" disabled="{{!isConnected}}">条件下发</button>
<button class="hide-btn" size="mini" type="warn" bindtap="onDeleteCondGroup">删除条件组</button>
<button class="hide-btn" size="mini" type="warn" bindtap="onDeleteCondition">删除条件</button>
<button class="hide-btn" size="mini" type="primary" bindtap="onAddCondGroup">添加条件组</button>
@@ -316,7 +315,8 @@
<text class="group-seq">{{grp.group}}</text>
<view class="group-time">
<text class="label">超时:</text>
<input class="picker-text" placeholder="超时时间" value="{{grp.timeout || 0}}" type="number" bindinput="onGroupNumberInput" data-idx="{{gidx}}" data-field="timeout" />
<input class="picker-text" placeholder="超时时间" value="{{grp.timeout || 0}}" type="number" bindinput="onGroupNumberInput" data-idx="{{gidx}}" data-field="timeout" />
<text class="label">单位:</text>
<picker class="unit-picker" mode="selector" range="{{timeUnits}}" value="{{grp.timeoutUnit}}" bindchange="onGroupPickerChange" data-idx="{{gidx}}" data-field="timeoutUnit">
<view class="picker-text">{{timeUnits[grp.timeoutUnit]}}</view>
@@ -454,35 +454,43 @@
<view class="cfg-head head-row">
<text>设备升级说明</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="onStartOta">发送 OTA 升级命令</button>
<button size="mini" type="primary" bindtap="onStartOta" disabled="{{!isConnected}}">发送 OTA 升级命令</button>
</view>
</view>
<view class="ota-article">
<view class="section">
<text class="section-title">前提条件</text>
<view class="section-body">
<text>1手机已安装 “OTA升级工具”</text>
<view class="tucontainer">
<text>1手机已安装<text class="bold-text">“OTA升级工具”</text></text>
</view>
</view>
<view class="section">
<text class="section-title">操作步骤</text>
<view class="section-body steps">
<text>1. 在本页面点击“发送 OTA 升级命令”。</text>
<text>2. 打开已下载并安装的 OTA 升级工具。</text>
<text>3. 手机蓝牙扫描并连接名称为:<text class="mono ">OTAOTA_OTAOTA_OTA</text> 的设备。</text>
<text>4. 连接后依次点击 GETINFO → IMAGEA 。</text>
<text>5. 选择升级固件文件后,点击 START ,并选择芯片类型 “CH573” 开始升级。</text>
<text class="section-title">操作步骤</text>
<view class="tucontainer">
<text>1. 在本页面点击<text class="bold-text">“发送 OTA 升级命令”</text>。</text>
</view>
<view class="tucontainer">
<text>2. 打开已下载并安装的 OTA 升级工具。</text>
</view>
<view class="tucontainer">
<text>3. 手机蓝牙扫描并连接名称为:<text class="bold-text">“OTAOTA_OTAOTA_OTA”</text>的设备。</text>
</view>
<view class="tucontainer">
<text>4. 连接后依次点击<text class="bold-text">GETINFO</text>→<text class="bold-text">IMAGEA</text>。</text>
</view>
<view class="tucontainer">
<text>5. 选择升级固件文件后,点击<text class="bold-text">START</text>,并选择芯片类型<text class="bold-text">“CH573”</text>开始升级。</text>
</view>
</view>
<view class="section">
<text class="section-title">注意事项</text>
<view class="section-body">
<text>• 升级过程中请保持设备与手机的蓝牙连接稳定,勿中断电源。</text>
<text>• 升级失败后请重试一次,仍失败请联系技术支持并提供日志。</text>
<text>• 点击“下载工具”将复制下载链接到剪贴板,需在浏览器中打开并下载。</text>
<text>• 升级失败后请重试一次,仍失败请联系技术支持并提供日志。</text>
</view>
</view>
</view>

View File

@@ -9,15 +9,27 @@
.container { background: #f5f6f8; min-height: 100vh; display: flex; flex-direction: column; }
.nav { padding: 4rpx 6rpx; }
.nav .flex { gap: 6rpx; }
.tucontainer {
font-size: 16px;
line-height: 1.5;
}
.bold-text {
font-weight: bold;
color: #0291f7e5; /* 红色,可以根据需要修改 */
/* 或者使用其他颜色 */
/* color: #3498db; 蓝色 */
/* color: #2ecc71; 绿色 */
/* color: #f39c12; 橙色 */
}
.nav .cu-item { padding: 6rpx 8rpx; margin-right: 4rpx; font-size: 32rpx; }
.content { padding: 8rpx 10rpx 0rpx; box-sizing: border-box; display: flex; flex-direction: column; gap: 8rpx; flex: 1; }
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10rpx; margin-bottom: 12rpx; }
.grid-item { background: #fff; border-radius: 14rpx; padding: 10rpx 0; display: flex; flex-direction: column; align-items: center; }
.circle { width: 40rpx; height: 40rpx; border-radius: 50%; background: #e9ecef; margin-bottom: 6rpx; }
.grid-item { background: #fff; border-radius: 14rpx; padding: 20rpx 22rpx; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; box-sizing: border-box; }
.circle { width: 40rpx; height: 40rpx; border-radius: 50%; background: #e9ecef; margin-top: 8rpx; margin-bottom: 6rpx; }
.circle.green { background: #21c161; }
.circle.red { background: #ff3b30; }
.circle.gray { background: #e9ecef; }
.label { font-size: 24rpx; color: #606266; }
.label { font-size: 24rpx; color: #606266; text-align:center; }
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8rpx; margin-bottom: 10rpx; }
.card { background: #fff; border-radius: 14rpx; padding: 24rpx 8rpx; display: flex; flex-direction: column; align-items: center; min-height: 140rpx; box-sizing: border-box; transition: transform 120ms ease, filter 120ms ease; }
@@ -34,6 +46,9 @@
/* 按下效果 */
.card.pressed { transform: translateY(4rpx) scale(0.985); filter: brightness(0.94); box-shadow: none; }
/* 禁用样式:视为不可点击并降低视觉优先级 */
.card.disabled { opacity: 0.5; }
.slider-card { background: #fff; border-radius: 14rpx; padding: 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); margin-bottom: 10rpx; }
.slider-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6rpx; }
.slider-head .title { font-size: 26rpx; color: #333; }
@@ -65,6 +80,7 @@
border: 1rpx solid rgba(43,171,153,0.18) !important;
}
.log-head .log-actions .clear-btn:active { opacity: 0.9 }
.log-options { display: flex; flex-wrap: nowrap; gap: 14rpx; }
.log-options .option { display: flex; align-items: center; font-size: 24rpx; color: #555; white-space: nowrap; }
@@ -78,7 +94,8 @@
/* 设备信息行 */
.device-row { display:flex; align-items:flex-start; justify-content: space-between; gap: 12rpx; background:#fff; border-radius: 14rpx; padding: 10rpx 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); flex-wrap: wrap; row-gap: 6rpx; }
.device-left { display:flex; flex-direction: column; gap: 6rpx; flex: 1; min-width: 60%; }
.device-left { display:flex; flex-direction: column; gap: 6rpx; min-width: 60%; }
.device-leftNt { display:flex; flex-direction: column; gap: 4rpx; min-width: 40%; }
.device-line { display:flex; align-items:center; gap: 8rpx; flex-wrap: wrap; }
.device-row .dr-label { color:#606266; font-size: 24rpx; }
.device-row .dr-value { color:#333; font-size: 26rpx; }
@@ -232,6 +249,13 @@
.full-log-view .cfg-card { flex: none; }
.full-log-view .log-card { flex: 1; min-height: 0; }
.full-log-view .log-scroll { flex: 1; min-height: 0; max-height: none; }
.full-log-view .log-list { flex: 1; min-height: 0; max-height: none; /* ensure flex sizing */
/* height:0 helps flex children not expand the parent when content grows */
height: 0; overflow: hidden;
}
/* Ensure individual log items don't force parent to grow */
.full-log-view .log-list .log-item { display: block; box-sizing: border-box; }
.log-item { margin-bottom: 10rpx; }
.log-item:last-child { margin-bottom: 0; }
.log-time { display: block; font-size: 22rpx; color: #9aa0a6; margin-bottom: 4rpx; }
@@ -264,9 +288,9 @@
/* If screen too narrow, allow wrapping but keep label + control groups together */
@media (max-width: 360px) {
.form-inline { flex-wrap: wrap; }
.inline-input { width: 90rpx; }
.inline-picker .picker-text { width: 80rpx; }
.form-inline { flex-wrap: wrap; }
.inline-input { width: 90rpx; }
.inline-picker .picker-text { width: 72rpx; }
}
/* 增大门磁/卫浴卡片中的控件高度,便于触控 */
@@ -282,6 +306,21 @@
.cfg-card.tall-controls .form-inline { gap: 14rpx; }
.cfg-card.tall-controls .label { font-size: 24rpx; }
/* 调整门磁/卫浴事件设置卡片的内间距与控件布局 */
.cfg-card.tall-controls {
padding: 18rpx !important;
}
.cfg-card.tall-controls .cfg-head {
padding-bottom: 10rpx;
}
.cfg-card.tall-controls .form-row { padding: 6rpx 0 0 0; }
.cfg-card.tall-controls .form-inline { align-items: center; gap: 16rpx; }
.cfg-card.tall-controls .picker-text { padding: 0 4rpx;min-width: 60rpx; }
.cfg-card.tall-controls .inline-picker .picker-text { padding: 0 4rpx; width: 80rpx; text-align: center; }
.cfg-card.tall-controls .label { margin-right: 8rpx; color: #525252; }
.cfg-card.tall-controls .picker-text, .cfg-card.tall-controls input.picker-text { border-radius: 10rpx; }
/* 全屏日志视图:增大工具栏中“清空”按钮宽度,提升点击目标 */
.full-log-view .toolbar .toolbar-actions button:last-child {
min-width: 150rpx;
@@ -292,3 +331,13 @@
.chinese-log-btn { padding: 0 12rpx; height: 54rpx; line-height: 54rpx; border-radius: 10rpx; font-size: 24rpx; color: #ffffff; border: none; }
.chinese-log-btn.on { background: #ff3b30 !important; }
.chinese-log-btn.off { background: #21c161 !important; }
/* 使门磁/卫浴卡片头部的操作按钮更紧凑(减少内间距与高度) */
.cfg-card.tall-controls .head-actions { gap: 6rpx; }
.cfg-card.tall-controls .head-actions button {
padding: 0 10rpx !important;
height: 52rpx !important;
line-height: 52rpx !important;
font-size: 24rpx !important;
border-radius: 9rpx !important;
}

View File

@@ -0,0 +1,148 @@
# B13page 页面描述文档
> 文件位置: pages/basics/BluetoothDebugging/B13page/B13page_描述.md
## 一、页面目标
B13page 为 B13 设备提供蓝牙调试与设备交互入口,目标包括:
- 显示设备基本信息名称、MAC、版本
- 实时展示连接状态与信号RSSI/Signal
- 提供设备测试操作(模拟按键、门磁/卫浴事件下发)。
- 支持设备配置(端口、条件等)与通信协议日志查看/发送。
- 支持 OTA 升级入口及调试日志导出。
## 二、页面显示结构(按视觉区域划分)
1. 顶部导航(横向 scroll-tabs
- Tab 1: 设备测试(默认)
- Tab 2: 设备配置
- Tab 3: 设备升级
- Tab 4: 设备通信日志(独立全屏日志视图)
2. 公共设备信息栏(位于标签下方、所有 Tab 共用)
- 显示字段:蓝牙名称(`bleName`)、MAC(`bleMac`)、版本(`bleVersion`)、信号(`bleSignal`)、RSSI(`bleRSSI`)、状态(`isConnected`)。
- 操作按钮:快速读取蓝牙信息(`onSendReadVersion`)。
3. Tab 1 - 设备测试
- 雷达指示灯网格(四项:门磁/卫浴/卧室/走廊,显示触发状态)。
- 测试按键卡片组(房间有人/无人、门开/关、卫浴有人/无人),支持长按/短按行为并发送测试指令。
- 门磁/卫浴事件设置面板(延时与单位选择,并有“设置”按钮)。
- (原)通讯日志卡片已移除,放置于 Tab 4避免重复
4. Tab 2 - 设备配置
- 端口配置表(表格形式,支持编辑端口号、阈值、启用开关、检测时间与单位)。
- 条件配置(折叠的条件组卡片,支持添加/删除、下发/读取/保存操作)。
5. Tab 3 - 设备升级
- 文件导入导出、OTA 一键下发等入口。
6. Tab 4 - 设备通信日志(全屏)
- 工具栏:导出日志、中文日志开关、清空。
- 日志选项:`HEX发送``HEX显示``时间戳``回车换行`
- 发送面板:多行输入与 `发送` 按钮(受 `isConnected` 控制)。
- 日志滚动区:按时间序列展示 `logList`(每条包含时间与文本)。
## 三、数据模型(重要字段与说明)
- `bleName` (string):显示的设备名称。
- `bleMac` (string):设备 MAC / deviceId。
- `bleVersion` (string):蓝牙固件/协议版本。
- `bleSignal` (string):可读文本形式的信号描述(例如 `-42 dBm``-`)。
- `bleRSSI` (number|string)RSSI 原始值或 `-`
- `isConnected` (boolean)当前连接状态UI 显示“已连接/未连接”)。
- `deviceId`, `serviceId`, `txCharId`, `rxCharId`BLE 通信上下文。
- `logList` (array):日志项数组,每项结构至少包含 `{ time, text }`
- `sendText` (string):发送框内容。
- `hexSend`, `hexShow`, `withTimestamp`, `wrapCRLF` (boolean):日志/发送选项。
## 四、主要交互与事件(函数与触发条件)
- 页面生命周期
- `onLoad(options)`:解析入参,初始化字段、构建条件组、发现特征并可能启动雷达读取。
- `onShow()`:尝试恢复或重建 BLE 通道、重新订阅通知并触发一次设备信息更新。
- `onUnload()`:解绑通知、移除连接监听、清理定时器。
- 用户交互事件
- `tabSelect(e)`:切换 Tab。
- `onTestKeyTap/onTestKeyTouch*`:测试按键事件,发送协议命令并记录日志。
- `sendDoorEvent/sendBathEvent`:下发门磁/卫浴延时配置到设备。
- `onInputChange``onSend`:日志发送输入与发送执行。
- `onExportLogs``onClearLogs``onToggleChineseLogs`:日志导出/清空/中文切换。
- BLE/通信事件
- `setupBleListener()`:注册 `wx.onBLECharacteristicValueChange`(处理 ArrayBuffer/hex并注册 `wx.onBLEConnectionStateChange` 回调。
- `teardownBleListener()`:解绑上述回调。
- `updateDeviceInfo()`:查询并更新 `bleSignal`/`bleRSSI`/`isConnected`(优先 `getConnectedBluetoothDevices`,兜底 `getBluetoothDevices`)。
- `startReconnectTimer()` / `stopReconnectTimer()`:断开时每 N 秒尝试 `createBLEConnection`
## 五、功能需求(详细)
1. 信号与连接实时性
- 页面应定期(配置间隔)调用 `updateDeviceInfo()` 刷新 `bleSignal`/`bleRSSI`/`isConnected`
- 当发生连接/断开事件时,应立即触发一次 `updateDeviceInfo()` 并在 UI 上反映变化。
2. 日志管理
- `logList` 应支持追加 RX/TX/调试条目;日志视图支持滚动与清空/导出。
- 支持 HEX 显示与 GBK 解码(可选中文日志)。
3. 监听与清理
- 每次进入页面或成功建立连接后,应确保只有一次 `onBLECharacteristicValueChange``onBLEConnectionStateChange` 注册。
- 页面卸载时必须移除所有监听与定时器,避免内存泄漏。
4. 重连与恢复
- 在断连时启动重连机制(间隔可配置),重连成功后恢复 notify 与读取流程。
- 在重连期间保留“最后已知 RSSI/Signal”以便用户参考而非立即显示 `-`)。
5. 可配置性与调试
- 支持 debug 模式(短轮询间隔、更多日志),生产模式下使用更长间隔以节省电量。
## 六、非功能需求
- 兼容性:兼容不同微信/小程序基础库版本的 BLE API优先使用 `getConnectedBluetoothDevices`,回落 `getBluetoothDevices`)。
- 性能:避免过短轮询导致 UI 卡顿或耗电;默认发布轮询建议 2-5 秒。
- 可维护性:关键逻辑(轮询/回调/重连)封装为方法,利于单元测试与复用。
## 七、验收与测试要点
- 断连/重连场景验证:断开后仍能按间隔调用 `updateDeviceInfo()`,连接恢复时立即更新并恢复 notify。
- 日志功能验收:发送/接收日志正确记录、导出与中文解析功能可切换。
- 资源管理:多次进入/离开页面不重复注册回调,无残留定时器。
---
文档生成:开发助手
日期2026-01-22
## 分角色讨论(补充)
以下为与主要角色(开发、测试、产品、架构/安全、运维)深入讨论后的结论与可执行项,已合并到页面描述内,便于对齐实施优先级与责任人。
### 开发(实现者)
- 关注点:稳定的轮询逻辑、避免重复注册回调、兼容不同基础库 BLE API、异常安全处理。
- 可执行项:
- A1: 封装 `startDeviceInfoTimer(intervalMs)` / `stopDeviceInfoTimer()`,默认调试 1s发布 2-5s。
- A2: 在 `setupBleListener()` 确保 `onBLECharacteristicValueChange``onBLEConnectionStateChange` 只注册一次,并在 `teardownBleListener()` 中完全解绑。
- A3: 在 `onBLEConnectionStateChange` 的连接与断开分支都立即调用 `updateDeviceInfo()` 并确保轮询器处于运行状态;在连接成功后立即恢复 notify 流程(`ensureBleChannels()` / `enableNotify()`)。
### 测试QA
- 关注点:真机复现性、基础库差异与边界场景。
- 可执行项:
- Q1: 制定一套标准化用例(见文档第七节)并使用 debug 日志验证 `updateDeviceInfo()` 调用频率与返回。
- Q2: 在不同小程序基础库版本与 Android/iOS 设备上验证 `getConnectedBluetoothDevices``getBluetoothDevices` 的行为差异。
### 产品UX
- 关注点:状态的可理解性与节电策略。
- 可执行项:
- P1: 在 UI 上显示“最后已知信号xx 秒前)”或“正在重连”提示,避免断连时直接显示 `-` 导致误解。
- P2: 将轮询间隔作为设置或根据“调试模式”自动切换(调试=1s普通=2-5s
### 架构 / 安全
- 关注点:资源泄漏、异常路径、统一日志。
- 可执行项:
- S1: 所有外部 API 调用加 try/catch失败写入调试日志并不抛出致命错误。
- S2: 页面卸载时必须清理所有定时器与回调(`onUnload` 明确调用 `stopDeviceInfoTimer()``teardownBleListener()`)。
### 运维 / 支持
- 关注点:定位线上问题、快速回滚。
- 可执行项:
- O1: 日志导出保持易读(时间戳、方向 RX/TX、HEX/文本),并在 Bug 报告中附带。
- O2: 发布前将轮询间隔恢复为节电安全值2-5s并在内测版本保留 1s 调试开关。
### 责任与优先级建议
- 优先级(高): A1, A3, S2 — 先保证轮询与连接恢复逻辑正确且无泄漏。
- 优先级(中): Q1, P1, S1 — 改善可测试性与 UX增加异常保护。
- 优先级(低): O1, P2 — 运营与配置相关,发布前调整。
本节已写入页面描述文档,便于团队按角色分配实现与验证任务。

View File

@@ -654,6 +654,7 @@ Page({
// 将已连接设备合并到列表并按过滤规则筛选
const list = [...this.data.deviceList]
devices.forEach(d => {
const name = d.name || d.localName || ''
if (!matchByTab(name)) return
const idx = list.findIndex(x => x.id === d.deviceId)