增加读取门磁、卫浴按钮
This commit is contained in:
@@ -190,8 +190,19 @@ MCU -> PC
|
||||
| PC→MCU | 0x16 | P0: 控制位<br> bit0: 设置门磁开关廊灯事件<br> bit1: 设置卫浴灯开关事件<br><br>门磁开关廊灯事件:<br> P1: 事件触发延迟时间数值<br> P2: 时间单位,1=秒 2=分 3=时<br> P3: 事件释放延迟时间<br> P4: 时间单位,1=秒 2=分 3=时<br><br>卫浴灯开关事件:<br> P5: 事件触发延迟时间数值<br> P6: 时间单位,1=秒 2=分 3=时<br> P7: 事件释放延迟时间<br> P8: 时间单位,1=秒 2=分 3=时 | 设置门磁与卫浴雷达的触发/释放时序参数 |
|
||||
| MCU→PC | 0x16 | P0:<br> 0x01: 参数正确<br> 0x02: 参数错误 | 返回设置结果 |
|
||||
|
||||
## 4. 命令交互流程图
|
||||
|
||||
|
||||
### 3.9 读取门磁/卫浴事件触发/释放参数
|
||||
| 方向 | 命令字 | 参数 | 备注 |
|
||||
|------|-------|------|------|
|
||||
| PC→MCU | 0x17 | P0: 0x01 (读取参数) | 请求设备返回门磁与卫浴雷达的触发/释放延时参数(低地址在前) |
|
||||
| MCU→PC | 0x17 | P0~P1: 门磁事件触发延迟时间(16-bit,单位:秒,低地址在前)<br>P2~P3: 门磁事件释放延迟时间(16-bit,低地址在前)<br>P4~P5: 卫浴雷达事件触发延迟时间(16-bit,低地址在前)<br>P6~P7: 卫浴雷达事件释放延迟时间(16-bit,低地址在前) | 设备返回各项时序参数,均为 16-bit 小端格式,单位为秒。 |
|
||||
|
||||
说明:
|
||||
- 请求示例(PC→MCU):Frame_Type=0x17,P0=0x01。
|
||||
- 响应示例(MCU→PC):Frame_Type=0x17,参数区域例如 P0~P7 = [0x0A,0x00, 0x14,0x00, 0x05,0x00, 0x08,0x00] 表示:门磁触发延时 10s,门磁释放延时 20s,卫浴触发 5s,卫浴释放 8s(均为小端)。
|
||||
|
||||
## 4. 命令交互流程图
|
||||
### 4.1 读版本号流程
|
||||
```mermaid
|
||||
flowchart TD
|
||||
|
||||
@@ -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: () => setUnknown() })
|
||||
}
|
||||
}, fail: () => {
|
||||
//console.log('开始获取信号值7')
|
||||
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
|
||||
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
|
||||
}
|
||||
//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: () => 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 的 top(px)
|
||||
|
||||
wx.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#cnmdgdx')
|
||||
.boundingClientRect(rect => {
|
||||
if (rect) {
|
||||
const topPx = rect.top; // px
|
||||
const bottomPx = 10 / 2; // 10rpx → 5px(2倍屏)
|
||||
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() {
|
||||
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,交由其 resolve(avoid 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 */ }
|
||||
|
||||
@@ -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>
|
||||
|
||||
</view>
|
||||
</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="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>
|
||||
<view class="flex-sub" >
|
||||
<text class="dr-label">状态:</text>
|
||||
<text class="dr-value">{{isConnected ? '已连接' : '未连接'}}</text>
|
||||
<text class="dr-value" style="color: {{isConnected ? 'green':'red'}};">{{isConnected ? '已连接' : '未连接'}}</text>
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
<view class="dr-btn dr-btn-view dr-btn-small" bindtap="onSendReadVersion">读取蓝牙信息</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>
|
||||
@@ -117,7 +127,8 @@
|
||||
<view class="cfg-head head-row">
|
||||
<text>门磁开廊灯事件设置</text>
|
||||
<view class="head-actions">
|
||||
<button size="mini" type="primary" bindtap="sendDoorEvent">设置门磁延时</button>
|
||||
<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,40 +214,17 @@
|
||||
<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>
|
||||
|
||||
<!-- 日志显示卡片 -->
|
||||
<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 >
|
||||
<button size="mini" type="default" bindtap="onClearLogs">清空</button>
|
||||
<button style="margin-left: 20rpx;" size="mini" type="default" bindtap="onExportLogs">导出日志</button>
|
||||
</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>
|
||||
<view class="log-empty" wx:else>暂无日志记录</view>
|
||||
</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>
|
||||
@@ -252,7 +251,7 @@
|
||||
<view class="cfg-head head-row">
|
||||
<text>端口配置</text>
|
||||
<view class="head-actions">
|
||||
<button size="mini" type="primary" bindtap="onSendPorts">端口下发</button>
|
||||
<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>
|
||||
@@ -317,6 +316,7 @@
|
||||
<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" />
|
||||
|
||||
<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,27 +454,36 @@
|
||||
<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>
|
||||
<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">
|
||||
@@ -482,7 +491,6 @@
|
||||
<view class="section-body">
|
||||
<text>• 升级过程中请保持设备与手机的蓝牙连接稳定,勿中断电源。</text>
|
||||
<text>• 升级失败后请重试一次,仍失败请联系技术支持并提供日志。</text>
|
||||
<text>• 点击“下载工具”将复制下载链接到剪贴板,需在浏览器中打开并下载。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -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; }
|
||||
@@ -66,6 +81,7 @@
|
||||
}
|
||||
.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; }
|
||||
.log-options checkbox { margin-right: 10rpx; transform: scale(0.9); }
|
||||
@@ -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; }
|
||||
@@ -266,7 +290,7 @@
|
||||
@media (max-width: 360px) {
|
||||
.form-inline { flex-wrap: wrap; }
|
||||
.inline-input { width: 90rpx; }
|
||||
.inline-picker .picker-text { width: 80rpx; }
|
||||
.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;
|
||||
}
|
||||
|
||||
148
pages/basics/BluetoothDebugging/B13page/B13page_描述.md
Normal file
148
pages/basics/BluetoothDebugging/B13page/B13page_描述.md
Normal 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 — 运营与配置相关,发布前调整。
|
||||
|
||||
本节已写入页面描述文档,便于团队按角色分配实现与验证任务。
|
||||
@@ -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)
|
||||
|
||||
@@ -213,7 +213,7 @@ if (Nres.Status==200) {
|
||||
},
|
||||
calcScrollHeight() {
|
||||
// 1. 拿到屏幕可用高度(px)
|
||||
const sys = wx.getSystemInfoSync();
|
||||
const sys = wx.getWindowInfo();
|
||||
const screenHeight = sys.windowHeight; // px
|
||||
|
||||
// 2. 拿到 scroll-view 的 top(px)
|
||||
|
||||
@@ -12,6 +12,8 @@ export const COMMANDS = {
|
||||
ENABLE_BLE_LOG: 0x0C,
|
||||
// 设置门磁/卫浴事件触发/释放参数
|
||||
SET_DOOR_BATH_EVENT: 0x16,
|
||||
// 读取门磁/卫浴事件触发/释放参数
|
||||
READ_DOOR_BATH_EVENT: 0x17,
|
||||
RADAR_STATUS: 0x11,
|
||||
TEST_KEYS: 0x13,
|
||||
};
|
||||
@@ -225,6 +227,17 @@ export function buildSetDoorBathEvent(opts = {}, options = {}) {
|
||||
return buildCommand(COMMANDS.SET_DOOR_BATH_EVENT, payload, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造:读取门磁/卫浴事件触发/释放参数 (Frame_Type=0x17)
|
||||
* PC -> MCU: P0 = 0x01 (读请求)
|
||||
* 返回:MCU -> PC 帧中 P0..P7 为四个 16-bit 小端延时值(单位:秒)
|
||||
* @param {{frame?:number, framNum?:number, crcType?:('CCITT'|'MODBUS'), head?:number[]}} [options]
|
||||
*/
|
||||
export function buildReadDoorBathEvent(options = {}) {
|
||||
// 按文档,发送 P0 = 0x01 表示读请求
|
||||
return buildCommand(COMMANDS.READ_DOOR_BATH_EVENT, [0x01], options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证十六进制字符串包并计算/写入 CRC(默认 MODBUS),返回完整包与CRC值
|
||||
* @param {string} hexStr 如 'CC C0 0C 00 00 00 01 00 02 00 0C 08' 或连续hex
|
||||
|
||||
Reference in New Issue
Block a user