优化微信小程序 持续接收雷达状态后卡住问题

This commit is contained in:
2026-01-17 09:43:38 +08:00
parent bbc4f205a3
commit 75806e6962
4 changed files with 374 additions and 67 deletions

View File

@@ -38,6 +38,8 @@ Page({
// 条件“有无人标记”选项,参考截图:无人至有人/短暂离开/长时间离开/有人至无人
tagOptions: ['无人至有人', '短暂离开', '长时间离开', '有人至无人'],
stateOptions: ['不判断', '触发', '释放', '关至开', '开至关'],
// 门磁专用选项与其他端口不同0=不判断,1=关门,2=开门,3=关至开,4=开至关
doorMagOptions: ['不判断', '关门', '开门', '关至开', '开至关'],
// 默认条件:由用户指定的条件组与条件
conditions: [
// 组1: timeout 2秒包含1个条件
@@ -49,11 +51,11 @@ Page({
// 组3: timeout 2秒包含1个条件
{ group: 3, seq: 1, tag: 1, cardPower: 0, doorMag: 4, irHall: 0, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 0, timeout: 2, timeoutUnit: 0 },
// 组4: timeout 10分包含1个条件
{ group: 4, seq: 1, tag: 1, cardPower: 0, doorMag: 3, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 },
{ group: 4, seq: 1, tag: 1, cardPower: 0, doorMag: 1, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 },
// 组5: timeout 10分包含1个条件
{ group: 5, seq: 1, tag: 2, cardPower: 0, doorMag: 3, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 },
{ group: 5, seq: 1, tag: 2, cardPower: 0, doorMag: 1, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 },
// 组6: timeout 10分包含1个条件
{ group: 6, seq: 1, tag: 3, cardPower: 0, doorMag: 3, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 }
{ group: 6, seq: 1, tag: 3, cardPower: 0, doorMag: 1, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 }
],
// 二级菜单:按组折叠
condGroups: [],
@@ -78,7 +80,7 @@ Page({
// 门磁/卫浴事件控件数据(仅保留 秒 / 分)
eventUnits: ['秒', '分'],
// 雷达读取状态false=未读取, true=正在读取
radarReading: false,
radarReading: true,
// 门磁
doorTriggerDelay: 0,
doorTriggerUnitIndex: 0,
@@ -249,6 +251,13 @@ Page({
if (id === 2) {
this.onSendReadVersion()
}
// 切换到设备升级标签data-id === 3默认发送一包 OTA 升级命令
if (id === 3) {
// 给用户一点缓冲时间再发送(确保 UI 切换完成)
// setTimeout(() => {
// this.onStartOta()
// }, 200)
}
},
onOpenDelayChange(e) {
@@ -294,9 +303,10 @@ Page({
// 通过 picker index 推导真实数值index + 1
const opts = {
door: {
triggerDelay: Number((d.doorTriggerIndex || 0) + 1),
// use the actual delay value (can be 0) instead of index+1
triggerDelay: Number(d.doorTriggerDelay || 0),
triggerUnit: mapUnit(d.doorTriggerUnitIndex),
releaseDelay: Number((d.doorReleaseIndex || 0) + 1),
releaseDelay: Number(d.doorReleaseDelay || 0),
releaseUnit: mapUnit(d.doorReleaseUnitIndex)
},
bath: null
@@ -344,9 +354,9 @@ Page({
const opts = {
door: null,
bath: {
triggerDelay: Number((d.bathTriggerIndex || 0) + 1),
triggerDelay: Number(d.bathTriggerDelay || 0),
triggerUnit: mapUnit(d.bathTriggerUnitIndex),
releaseDelay: Number((d.bathReleaseIndex || 0) + 1),
releaseDelay: Number(d.bathReleaseDelay || 0),
releaseUnit: mapUnit(d.bathReleaseUnitIndex)
}
}
@@ -851,7 +861,7 @@ Page({
// 开启订阅若已传入rx特征保证能接收数据
this.enableNotify()
this.setupBleListener()
// this.sendRadarStatusCommand(true)
this.sendRadarStatusCommand(true)
},
enableNotify() {
@@ -1033,6 +1043,7 @@ Page({
*/
setupBleListener() {
// 移除旧监听,防止重复触发(页面重复进入或多次初始化的场景)
this.teardownBleListener()
// 运行环境不支持通知回调则直接返回(避免报错)
if (typeof wx.onBLECharacteristicValueChange !== 'function') return
@@ -1073,7 +1084,23 @@ Page({
// 注册系统 BLE 通知回调
wx.onBLECharacteristicValueChange(this._onBleChange)
},
onDownloadOtaTool() {
const url = 'https://www.baidu.com/';
if (wx && wx.setClipboardData) {
wx.setClipboardData({
data: url,
success() {
wx.showToast({ title: '下载链接已复制到剪贴板', icon: 'none' });
},
fail() {
wx.showModal({ title: '提示', content: '无法复制链接,请手动访问:' + url, showCancel: false });
}
});
} else {
// fallback
wx.showModal({ title: '提示', content: '下载链接:' + url, showCancel: false });
}
},
teardownBleListener() {
if (this._onBleChange && typeof wx.offBLECharacteristicValueChange === 'function') {
wx.offBLECharacteristicValueChange(this._onBleChange)
@@ -1145,39 +1172,61 @@ Page({
},
updateRadarLights(bits) {
// 更新雷达指示灯数组(保持原有展示)
const next = (this.data.radarLights || []).map((it, idx) => {
const triggered = ((bits >> idx) & 0x01) === 1
return {
...it,
triggered,
colorClass: triggered ? 'red' : 'green'
// 节流更新:合并高频上报,避免频繁 setData 导致 UI 卡顿
try {
const now = Date.now()
const throttleMs = 200
if (!this._lastRadarUpdateTime || (now - this._lastRadarUpdateTime) >= throttleMs) {
// 直接更新
this._lastRadarUpdateTime = now
const next = (this.data.radarLights || []).map((it, idx) => {
const triggered = ((bits >> idx) & 0x01) === 1
return Object.assign({}, it, { triggered, colorClass: triggered ? 'red' : 'green' })
})
const doorTriggered = ((bits >> 0) & 0x01) === 1
const bathTriggered = ((bits >> 1) & 0x01) === 1
const bedTriggered = ((bits >> 2) & 0x01) === 1
const hallTriggered = ((bits >> 3) & 0x01) === 1
const cardClasses = {
roomHasPeople: (bedTriggered || hallTriggered) ? 'orange' : 'gray',
roomNoPeople: (!bedTriggered && !hallTriggered) ? 'green' : 'gray',
doorOpen: (!doorTriggered) ? 'red' : 'gray',
doorClose: (doorTriggered) ? 'green' : 'gray',
bathHasPeople: (bathTriggered) ? 'blue' : 'gray',
bathNoPeople: (!bathTriggered) ? 'green' : 'gray'
}
this.setData({ radarLights: next, cardClasses })
} else {
// 延迟更新到节流窗口结束,使用最新 bits
this._pendingRadarBits = bits
if (this._radarUpdateTimer) clearTimeout(this._radarUpdateTimer)
this._radarUpdateTimer = setTimeout(() => {
try {
const b = this._pendingRadarBits || 0
this._lastRadarUpdateTime = Date.now()
const next = (this.data.radarLights || []).map((it, idx) => {
const triggered = ((b >> idx) & 0x01) === 1
return Object.assign({}, it, { triggered, colorClass: triggered ? 'red' : 'green' })
})
const doorTriggered = ((b >> 0) & 0x01) === 1
const bathTriggered = ((b >> 1) & 0x01) === 1
const bedTriggered = ((b >> 2) & 0x01) === 1
const hallTriggered = ((b >> 3) & 0x01) === 1
const cardClasses = {
roomHasPeople: (bedTriggered || hallTriggered) ? 'orange' : 'gray',
roomNoPeople: (!bedTriggered && !hallTriggered) ? 'green' : 'gray',
doorOpen: (!doorTriggered) ? 'red' : 'gray',
doorClose: (doorTriggered) ? 'green' : 'gray',
bathHasPeople: (bathTriggered) ? 'blue' : 'gray',
bathNoPeople: (!bathTriggered) ? 'green' : 'gray'
}
this.setData({ radarLights: next, cardClasses })
} catch (e) { /* ignore */ }
}, throttleMs)
}
})
// 解析位值约定bit0=门磁, bit1=卫浴, bit2=卧室, bit3=走廊)
const doorTriggered = ((bits >> 0) & 0x01) === 1
const bathTriggered = ((bits >> 1) & 0x01) === 1
const bedTriggered = ((bits >> 2) & 0x01) === 1
const hallTriggered = ((bits >> 3) & 0x01) === 1
// 计算卡片显示规则:默认灰色,按需求将对应卡片设色
const cardClasses = {
// 房间有人:卧室或走廊任一触发 -> 橙色,否则灰色
roomHasPeople: (bedTriggered || hallTriggered) ? 'orange' : 'gray',
// 房间无人:卧室和走廊均处于释放 -> 绿色,否则灰色
roomNoPeople: (!bedTriggered && !hallTriggered) ? 'green' : 'gray',
// 门开:门磁释放 -> 红色,否则灰色
doorOpen: (!doorTriggered) ? 'red' : 'gray',
// 门关:门磁触发 -> 绿色,否则灰色
doorClose: (doorTriggered) ? 'green' : 'gray',
// 卫浴有人:卫浴触发 -> 蓝色,否则灰色
bathHasPeople: (bathTriggered) ? 'blue' : 'gray',
// 卫浴无人:卫浴释放 -> 绿色,否则灰色
bathNoPeople: (!bathTriggered) ? 'green' : 'gray'
} catch (e) {
try { console.warn('updateRadarLights error', e) } catch (xx) { /* ignore */ }
}
this.setData({ radarLights: next, cardClasses })
},
// 数值约束
@@ -1400,6 +1449,94 @@ Page({
wx.showToast({ title: '端口配置已发送', icon: 'success' })
},
// 仅下发端口配置(参考一键下发,但只发送 0x09 端口包)
onSendPorts() {
const sendPorts = async () => {
try {
this.appendLog('UI', '操作: 端口下发(开始)')
// 标准化 ports缺失 deviceType/deviceAddr 时写入默认值
try {
const rawPorts = Array.isArray(this.data.ports) ? this.data.ports : []
let changed = false
const normalized = rawPorts.map((p) => {
const np = { ...p }
if (np.deviceType == null || np.deviceType === 0) {
if (np.deviceType !== 2) { np.deviceType = 2; changed = true }
}
if (np.deviceAddr == null || np.deviceAddr === 0) {
if (np.deviceAddr !== 1) { np.deviceAddr = 1; changed = true }
}
return np
})
if (changed) {
try { this.setData({ ports: normalized }) } catch (e) { /* ignore */ }
}
} catch (e) { /* ignore normalization error */ }
const ports = Array.isArray(this.data.ports) ? this.data.ports : []
const portPkts = []
for (let i = 0; i < ports.length; i++) {
const p = ports[i]
const P0 = (p && p.deviceType) ? (p.deviceType & 0xFF) : 0
const P1 = (p && p.deviceAddr) ? (p.deviceAddr & 0xFF) : 0
const loopLE = [((p && p.loop) || 0) & 0xFF, (((p && p.loop) || 0) >>> 8) & 0xFF]
const P4 = ((p && p.thresholdDown) || 0) & 0xFF
const P5 = this.getVirtualPort(p, i)
const P6 = (p && p.enabled) ? 0x01 : 0x00
const dtLE = [((p && p.detectTime) || 0) & 0xFF, ((((p && p.detectTime) || 0) >>> 8) & 0xFF)]
const P9 = (this.normalizeUnitForPacket ? this.normalizeUnitForPacket((p && p.detectUnit) || 0) : this.unitIndexToProtocolValue((p && p.detectUnit) || 0)) & 0xFF
const P10 = ((p && p.thresholdUp) || 0) & 0xFF
const payload = [P0, P1, ...loopLE, P4, P5, P6, ...dtLE, P9, P10]
const pkt = buildCommand(COMMANDS.SET_CONDITION_2, payload, { frame: i + 1, framNum: ports.length })
portPkts.push({ pkt, label: `端口 ${p && p.name || (i+1)}` })
this.appendLog('TX', `端口配置[${p && p.name || (i+1)}]: ${this.toHex(pkt)}`)
}
try { this.enableNotify() } catch (e) { /* ignore */ }
const sendWithRetry = async (pktObj, cmdType) => {
const maxRetries = 3
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const waitPromise = this.waitForResponse(cmdType, 3000)
try { await this.writeRawBytes(pktObj.pkt, pktObj.label) } catch (writeErr) {
if (this._pendingResponse && typeof this._pendingResponse.reject === 'function') {
try { this._pendingResponse.reject(new Error('写入失败')) } catch (e) { /* ignore */ }
this._pendingResponse = null
}
throw writeErr
}
const params = await waitPromise
if (params && params.length > 0 && (params[0] & 0xFF) === 0x01) {
this.appendLog('UI', `${pktObj.label} 下发成功 (attempt ${attempt})`)
await new Promise(r => setTimeout(r, 100))
return true
} else {
throw new Error('设备返回失败或参数不正确')
}
} catch (err) {
this.appendLog('WARN', `${pktObj.label} 发送 attempt ${attempt} 失败: ${err && (err.message || err)}`)
if (attempt < maxRetries) { await new Promise(r => setTimeout(r, 200)); continue }
throw new Error(`${pktObj.label} 下发失败`)
}
}
}
for (let i = 0; i < portPkts.length; i++) {
const obj = portPkts[i]
await sendWithRetry(obj, COMMANDS.SET_CONDITION_2 & 0xFF)
}
wx.showToast({ title: '端口下发完成', icon: 'success' })
this.appendLog('UI', '端口下发完成')
} catch (err) {
wx.showToast({ title: '端口下发失败', icon: 'none' })
this.appendLog('WARN', `端口下发失败: ${err && (err.message || err)}`)
}
}
sendPorts()
},
// 读取端口配置(占位示例,后续可接入真实读取命令)
onReadPorts() {
this.appendLog('UI', '请求读取端口配置')
@@ -1473,6 +1610,89 @@ Page({
wx.showToast({ title: '条件配置已发送', icon: 'success' })
},
// 仅下发条件配置(参考一键下发,但只发送 0x08 条件包)
onSendConditions() {
const sendConds = async () => {
try {
this.appendLog('UI', '操作: 条件下发(开始)')
// 扁平化 condGroups 并兼容旧字段名
const flat = []
;(this.data.condGroups || []).forEach(grp => {
(grp.items || []).forEach(it => {
const item = { ...it, group: grp.group, timeout: grp.timeout, timeoutUnit: grp.timeoutUnit }
if (item.tag == null && (item.cardPower != null)) item.tag = item.cardPower
if (item.judgeUnit == null) item.judgeUnit = (item.judgeUnit === 0 ? 0 : 2)
if (item.timeoutUnit == null) item.timeoutUnit = (item.timeoutUnit === 0 ? 0 : 2)
flat.push(item)
})
})
const condPkts = []
for (let i = 0; i < flat.length; i++) {
const c = flat[i]
const P0 = (typeof this.tagIndexToProtocolValue === 'function' ? this.tagIndexToProtocolValue((c && c.tag) || 0) : (((c && c.tag) || 0) + 1)) & 0xFF
const P1 = ((c && c.group) || 0) & 0xFF
const P2 = ((c && c.seq) || 0) & 0xFF
const jtLE = [((c && c.judgeTime) || 0) & 0xFF, (((c && c.judgeTime) || 0) >>> 8) & 0xFF]
const P5 = (typeof this.normalizeUnitForPacket === 'function') ? (this.normalizeUnitForPacket((c && c.judgeUnit) || 0) & 0xFF) : (((c && c.judgeUnit) || 0) & 0xFF)
const P6 = ((c && c.cardPower) || 0) & 0xFF
const P7 = ((c && c.doorMag) || 0) & 0xFF
const P8 = ((c && c.bathRadar) || 0) & 0xFF
const P9 = ((c && c.bathroomRadar) || 0) & 0xFF
const P10 = ((c && c.irHall) || 0) & 0xFF
const toLE = [((c && c.timeout) || 0) & 0xFF, ((((c && c.timeout) || 0) >>> 8) & 0xFF)]
const P13 = (typeof this.normalizeUnitForPacket === 'function') ? (this.normalizeUnitForPacket((c && c.timeoutUnit) || 0) & 0xFF) : (((c && c.timeoutUnit) || 0) & 0xFF)
const payload = [P0, P1, P2, ...jtLE, P5, P6, P7, P8, P9, P10, ...toLE, P13]
const pkt = buildCommand(COMMANDS.SET_CONDITION_1, payload, { frame: i + 1, framNum: flat.length })
condPkts.push({ pkt, label: `条件 ${c && c.group || ''}-${c && c.seq || i+1}` })
this.appendLog('TX', `条件配置[组${c.group}/序${c.seq}]: ${this.toHex(pkt)}`)
}
try { this.enableNotify() } catch (e) { /* ignore */ }
const sendWithRetry = async (pktObj, cmdType) => {
const maxRetries = 3
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const waitPromise = this.waitForResponse(cmdType, 3000)
try { await this.writeRawBytes(pktObj.pkt, pktObj.label) } catch (writeErr) {
if (this._pendingResponse && typeof this._pendingResponse.reject === 'function') {
try { this._pendingResponse.reject(new Error('写入失败')) } catch (e) { /* ignore */ }
this._pendingResponse = null
}
throw writeErr
}
const params = await waitPromise
if (params && params.length > 0 && (params[0] & 0xFF) === 0x01) {
this.appendLog('UI', `${pktObj.label} 下发成功 (attempt ${attempt})`)
await new Promise(r => setTimeout(r, 100))
return true
} else {
throw new Error('设备返回失败或参数不正确')
}
} catch (err) {
this.appendLog('WARN', `${pktObj.label} 发送 attempt ${attempt} 失败: ${err && (err.message || err)}`)
if (attempt < maxRetries) { await new Promise(r => setTimeout(r, 200)); continue }
throw new Error(`${pktObj.label} 下发失败`)
}
}
}
for (let i = 0; i < condPkts.length; i++) {
const obj = condPkts[i]
await sendWithRetry(obj, COMMANDS.SET_CONDITION_1 & 0xFF)
}
wx.showToast({ title: '条件下发完成', icon: 'success' })
this.appendLog('UI', '条件下发完成')
} catch (err) {
wx.showToast({ title: '条件下发失败', icon: 'none' })
this.appendLog('WARN', `条件下发失败: ${err && (err.message || err)}`)
}
}
sendConds()
},
// 功能栏:删除条件组
onDeleteCondGroup() {
const groups = (this.data.condGroups || []).map(g => String(g.group))
@@ -2280,8 +2500,16 @@ Page({
} catch (e) { /* ignore console errors */ }
const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`
const next = [{ id, time: timeStr, text: finalText }, ...this.data.logList]
this.setData({ logList: next })
// 限制日志长度,避免无限增长导致 setData 负担和内存问题保留最新200条
try {
const maxLogs = 200
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 })
} catch (e) {
// 若 setData 出现异常,仍保证不抛到外层
try { console.warn('appendLog setData failed', e) } catch (xx) { /* ignore */ }
}
}
})

View File

@@ -9,23 +9,21 @@
<!-- 顶部标签:参考主机升级页面的切换方式 -->
<scroll-view scroll-x class="bg-white nav text-center text-bold text-xl">
<view class="flex text-center">
<view class="cu-item {{1==TabCur?'text-blue cur':''}}" bindtap="tabSelect" data-id="1">蓝牙调试</view>
<view class="cu-item {{2==TabCur?'text-blue cur':''}}" bindtap="tabSelect" data-id="2">蓝牙升级</view>
<view class="cu-item {{1==TabCur?'text-blue cur':''}}" bindtap="tabSelect" data-id="1">设备测试</view>
<view class="cu-item {{2==TabCur?'text-blue cur':''}}" bindtap="tabSelect" data-id="2">设备配置</view>
<view class="cu-item {{3==TabCur?'text-blue cur':''}}" bindtap="tabSelect" data-id="3">设备升级</view>
</view>
</scroll-view>
<!-- Tab: 蓝牙调试 -->
<!-- Tab: 设备测试 -->
<view wx:if="{{TabCur==1}}" class="content">
<!-- 顶部操作按钮 -->
<!-- 顶部操作按钮
<view class="top-actions" style="display:flex; gap:6rpx; padding:4rpx 6rpx;">
<button size="mini" type="primary" bindtap="sendDoorEvent">设置门磁延时</button>
<button size="mini" type="primary" bindtap="sendBathEvent">设置卫浴延时</button>
<button size="mini" type="{{radarReading ? 'warn' : 'primary'}}" bindtap="toggleRadarRead">{{radarReading ? '停止读取' : '读雷达状态'}}</button>
<button size="mini" type="primary" bindtap="onStartOta">OTA升级</button>
</view>
<button size="mini" type="primary" bindtap="onStartOta">OTA升级</button>
</view> -->
<!-- 顶部设备分类 -->
<view class="grid">
@@ -79,11 +77,14 @@
<!-- 门磁/卫浴事件设置(替代原延时滑块) -->
<view class="cfg-card">
<view class="cfg-head head-row">
<text>门磁开廊灯事件设置</text>
<text>门磁开廊灯事件设置</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="sendDoorEvent">设置门磁延时</button>
</view>
</view>
<view class="form-row">
<view class="form-inline">
<text class="label">触发延迟</text>
<text class="label">开门延时</text>
<picker class="inline-picker" mode="selector" range="{{doorTriggerOptions}}" value="{{doorTriggerIndex}}" bindchange="onDoorTriggerPickerChange">
<view class="picker-text">{{doorTriggerOptions[doorTriggerIndex]}}</view>
</picker>
@@ -91,7 +92,7 @@
<picker class="inline-picker" mode="selector" range="{{eventUnits}}" value="{{doorTriggerUnitIndex}}" bindchange="onDoorUnitChange" data-field="triggerUnit">
<view class="picker-text">{{eventUnits[doorTriggerUnitIndex]}}</view>
</picker>
<text class="label">释放延迟</text>
<text class="label">关门延时</text>
<picker class="inline-picker" mode="selector" range="{{doorReleaseOptions}}" value="{{doorReleaseIndex}}" bindchange="onDoorReleasePickerChange">
<view class="picker-text">{{doorReleaseOptions[doorReleaseIndex]}}</view>
</picker>
@@ -105,7 +106,10 @@
<view class="cfg-card">
<view class="cfg-head head-row">
<text>卫浴雷达开卫浴灯事件设置</text>
<text>卫浴雷达开卫浴灯事件设置</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="sendBathEvent">设置卫浴延时</button>
</view>
</view>
<view class="form-row">
<view class="form-inline">
@@ -133,7 +137,10 @@
<view class="log-card">
<view class="log-head">
<text class="title">通讯日志</text>
<text class="action" bindtap="onClearLogs">清空</text>
<view class="log-actions">
<button class="radar-btn" size="mini" type="{{radarReading ? 'warn' : 'primary'}}" bindtap="toggleRadarRead">{{radarReading ? '停止读取' : '读雷达状态'}}</button>
<button class="clear-btn" size="mini" type="default" bindtap="onClearLogs">清空</button>
</view>
</view>
<view class="log-options">
@@ -168,8 +175,9 @@
</view>
</view>
<!-- Tab: 蓝牙升级 -->
<!-- Tab: 设备升级 -->
<view wx:if="{{TabCur==2}}" class="content">
<!-- 设备信息栏 -->
<view class="device-row">
<view class="device-left">
<view class="device-line">
@@ -205,6 +213,7 @@
<view class="cfg-head head-row">
<text>端口配置</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="onSendPorts">端口下发</button>
<button class="hide-btn" size="mini" type="default" bindtap="onReadPorts">读取配置</button>
<button class="hide-btn" size="mini" type="primary" bindtap="onSavePorts">保存配置</button>
</view>
@@ -252,6 +261,7 @@
<view class="cfg-head head-row">
<text>条件配置</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="onSendConditions">条件下发</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>
@@ -275,7 +285,7 @@
</view>
</view>
<view class="head-actions">
<button size="mini" type="default" bindtap="onToggleGroup" data-idx="{{gidx}}">{{grp.expanded?'收起':'展开'}}</button>
<button class="group-toggle" size="mini" type="default" bindtap="onToggleGroup" data-idx="{{gidx}}" title="{{grp.expanded ? '收起' : '展开'}}">{{grp.expanded ? '▴' : '▾'}}</button>
</view>
</view>
@@ -322,8 +332,8 @@
</picker>
</view>
<view class="td">
<picker mode="selector" range="{{stateOptions}}" value="{{it.doorMag}}" bindchange="onItemPickerChange" data-gidx="{{gidx}}" data-iidx="{{iidx}}" data-field="doorMag">
<view class="picker-text">{{stateOptions[it.doorMag]}}</view>
<picker mode="selector" range="{{doorMagOptions}}" value="{{it.doorMag}}" bindchange="onItemPickerChange" data-gidx="{{gidx}}" data-iidx="{{iidx}}" data-field="doorMag">
<view class="picker-text">{{doorMagOptions[it.doorMag]}}</view>
</picker>
</view>
<view class="td">
@@ -397,5 +407,29 @@
</view>
</view>
</view>
<!-- Tab: 设备升级 -->
<view wx:if="{{TabCur==3}}" class="content">
<!-- OTA 升级卡片(顶部) -->
<view class="cfg-card ota-card">
<view class="cfg-head head-row">
<text>OTA 升级工具</text>
<view class="head-actions">
<button size="mini" type="primary" bindtap="onStartOta">发送 OTA 升级命令</button>
</view>
</view>
<view class="ota-steps">
<view class="step">1. 升级工具下载:
<button class="link-btn" size="mini" bindtap="onDownloadOtaTool">下载工具</button>
</view>
<view class="step">2. 安装软件</view>
<view class="step">3. 点击 OTA 按钮,发送 OTA 升级命令</view>
<view class="step">4. 打开下载的 【OTA 升级工具】</view>
<view class="step">5. 蓝牙连接名称:<text class="mono">OTAOTA_OTAOTA_OTA</text></view>
<view class="step">6. 连接后点击 <text class="mono">GETINFO</text>,再点击 <text class="mono">IMAGEA</text></view>
<view class="step">7. 选择升级固件后,点击最下面的 <text class="mono">START</text> 按钮,再选择芯片 “CH573” 开始升级</view>
<view class="note">下载链接: https://www.baidu.com/ (点击“下载工具”将复制链接)</view>
</view>
</view>
</view>
</view>

View File

@@ -26,7 +26,7 @@
.icon.gray { background: #9aa0a6; }
.icon { overflow: hidden; display:flex; align-items:center; justify-content:center; }
.icon-img { width: 100%; height: 100%; display: block; }
.card-title { font-size: 24rpx; color: #555; }
.card-title { font-size: 22rpx; color: #555; }
/* 按下效果 */
.card.pressed { transform: translateY(4rpx) scale(0.985); filter: brightness(0.94); box-shadow: none; }
@@ -41,6 +41,28 @@
.log-head .title { font-size: 26rpx; color: #333; }
.log-head .action { font-size: 24rpx; color: #2bab99; }
/* 日志头按钮组:读雷达 与 清空 按钮并列样式 */
.log-head .log-actions { display:flex; align-items:center; gap: 8rpx; }
.log-head .log-actions .radar-btn {
padding: 0 16rpx;
height: 48rpx;
line-height: 48rpx;
border-radius: 12rpx;
font-size: 22rpx;
}
.log-head .log-actions .clear-btn {
padding: 0 16rpx;
min-width: 130rpx;
height: 48rpx;
line-height: 48rpx;
border-radius: 12rpx;
font-size: 22rpx;
background: #ffffff !important;
color: #2bab99 !important;
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; }
.log-options checkbox { margin-right: 10rpx; transform: scale(0.9); }
@@ -62,6 +84,13 @@
.dr-btn-view:active { opacity: 0.85; }
/* 顶部操作按钮:紧凑内间距,适配工具栏顶部的多个小按钮 */
.top-actions {
display: flex;
justify-content: flex-end;
gap: 6rpx;
padding: 4rpx 6rpx;
}
.top-actions button {
padding: 0 10rpx;
height: 58rpx;
@@ -192,6 +221,23 @@
.log-text { display: block; font-size: 24rpx; color: #333; word-break: break-all; }
.log-empty { font-size: 24rpx; color: #9aa0a6; text-align: center; padding: 20rpx 0; }
/* group toggle button (triangle) styling */
.group-toggle {
padding: 0 8rpx !important;
width: 52rpx !important;
height: 40rpx !important;
line-height: 40rpx !important;
font-size: 26rpx !important;
border-radius: 8rpx !important;
background: transparent !important;
color: #0ea5e9 !important;
border: 1rpx solid rgba(14,165,233,0.14) !important;
}
.group-toggle:active { background: rgba(14,165,233,0.06) !important; }
/* 收窄条件组超时输入宽度 */
.cond-card .group-time input.picker-text { width: 80rpx; padding: 0 6rpx; box-sizing: border-box; }
/* Inline form for trigger/release delay - keep controls on one row on mobile */
.form-row { display:block; }
.form-inline { display:flex; align-items:center; gap: 10rpx; flex-wrap: nowrap; }