增加中文日志输出 优化导出日志功能

This commit is contained in:
2026-01-19 16:25:49 +08:00
parent 75806e6962
commit 8d244cb316
9 changed files with 24711 additions and 94 deletions

View File

@@ -84,8 +84,8 @@ PC -> MCU
- 0: 不判断
- 1: 触发
- 2: 释放
- 3: 关至
- 4: 开至
- 3: 开至关
- 4: 关至开
注意条件组超时时间必须大于或等于条件判定时间P11~P12 >= P3~P4否则设备应判定为参数错误。

View File

@@ -2,6 +2,7 @@
"pages": [
"pages/autho/index",
"pages/basics/BluetoothDebugging/B13page/B13page",
"pages/login/login",
"pages/index/index",
"pages/mycenter/index",
@@ -16,8 +17,7 @@
"pages/basics/FacialDeviceBinding/FacialDeviceBinding",
"pages/basics/progress/progress",
"pages/basics/progress/RoomTypeControlLog/RoomTypeControlLog",
"pages/basics/BluetoothDebugging/BluetoothDebugging",
"pages/basics/BluetoothDebugging/B13page/B13page"
"pages/basics/BluetoothDebugging/BluetoothDebugging"
],
"window": {
"backgroundTextStyle": "light",

92
lib/gbkDecoder.js Normal file
View File

@@ -0,0 +1,92 @@
// Lightweight GBK/GB18030 decoder helper
// Strategy:
// 1. Prefer native TextDecoder('gb18030'/'gbk') if available
// 2. Fallback to a reverse table built from utils/ecUnicodeToGBK.js (if present)
// 3. Final fallback: byte-by-byte ASCII/replace decoding
function toUint8Array(input) {
if (!input) return new Uint8Array(0)
if (input instanceof Uint8Array) return input
if (input.buffer && input.buffer instanceof ArrayBuffer) return new Uint8Array(input.buffer)
return new Uint8Array(input)
}
let reverseMap = null
function buildReverseFromTable(table) {
try {
const rev = Object.create(null)
for (let i = 0; i < table.length; i++) {
const v = table[i]
if (v) rev[String(v).toUpperCase()] = Number(i)
}
return rev
} catch (e) { return null }
}
function decodeWithTable(u8, table) {
if (!table) return null
if (!reverseMap) reverseMap = buildReverseFromTable(table)
if (!reverseMap) return null
let out = ''
for (let i = 0; i < u8.length; i++) {
const b = u8[i]
if (b <= 0x7F) { out += String.fromCharCode(b); continue }
const b2 = u8[i + 1]
if (typeof b2 !== 'number') { out += '\uFFFD'; continue }
const hex = b.toString(16).padStart(2, '0').toUpperCase() + b2.toString(16).padStart(2, '0').toUpperCase()
const cp = reverseMap[hex]
if (cp != null) {
try { out += String.fromCodePoint(cp) } catch (e) { out += '\uFFFD' }
} else {
out += '\uFFFD'
}
i += 1
}
return out
}
function decode(u8in) {
const u8 = toUint8Array(u8in)
// 1) Native TextDecoder if environment supports gb18030/gbk
try {
if (typeof TextDecoder === 'function') {
try { return new TextDecoder('gb18030').decode(u8) } catch (e) { /* ignore */ }
try { return new TextDecoder('gbk').decode(u8) } catch (e) { /* ignore */ }
}
} catch (e) { /* ignore */ }
// 2) Try a local Encoding lib if available (bundles may expose Encoding.convert)
try {
const enc = (typeof Encoding !== 'undefined') ? Encoding : (typeof window !== 'undefined' ? window.Encoding : null)
if (enc && typeof enc.convert === 'function') {
try { return enc.convert(u8, { to: 'UNICODE', from: 'GB18030', type: 'string' }) } catch (e) { /* ignore */ }
try { return enc.convert(u8, { to: 'UTF8', from: 'GB18030', type: 'string' }) } catch (e) { /* ignore */ }
}
} catch (e) { /* ignore */ }
// 3) Try building reverse map from project mapping utils (utils/ecUnicodeToGBK.js)
try {
const tableMod = require('../utils/ecUnicodeToGBK.js')
const table = tableMod && (tableMod.table || tableMod.t || tableMod)
if (table) {
const s = decodeWithTable(u8, table)
if (s && s.length) return s
}
} catch (e) { /* ignore */ }
// 4) Final fallback: best-effort ASCII/replace pass
try {
let out = ''
for (let i = 0; i < u8.length; i++) {
const b = u8[i]
if (b <= 0x7F) out += String.fromCharCode(b)
else {
const b2 = u8[i + 1]
if (typeof b2 === 'number') { out += '\uFFFD'; i += 1 } else { out += '\uFFFD' }
}
}
return out
} catch (e) { return '' }
}
module.exports = { decode }

11
lib/test_gbk.js Normal file
View File

@@ -0,0 +1,11 @@
// Quick test for gbkDecoder
const path = require('path')
const { decode } = require('./gbkDecoder')
const hex = '2D D7 B4 CC AC 3A 32 CC F5 BC FE D7 E9 3A 31 C5 D0 B6 A8 32 2C 32 2C 32 2C 32 2C 32 2C 54 69 6D 65 3A 34 34 30 35 2D 34 34 30 35'
const bytes = hex.split(/\s+/).map(h => parseInt(h,16))
const u8 = Uint8Array.from(bytes)
const out = decode(u8)
console.log('DECODED:', out)
console.log('EXPECTED:', '- 状态:2 条件组:1 判定-2,2,2,2,2 Time:4405-4405')

View File

@@ -1,6 +1,15 @@
const { buildCommand, buildReadVersion, buildSetDoorBathEvent, 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
try {
EncodingLib = require('../../../../utils/ecUnicodeToGBK.js')
} catch (e) {
try { EncodingLib = (typeof Encoding !== 'undefined' ? Encoding : null) } catch (e2) { EncodingLib = null }
}
let GBKDecoder = null
try { GBKDecoder = require('../../../../lib/gbkDecoder.js') } catch (e) { GBKDecoder = null }
Page({
data: {
TabCur: 1, // 1: 蓝牙调试 2: 蓝牙升级
@@ -37,19 +46,19 @@ Page({
],
// 条件“有无人标记”选项,参考截图:无人至有人/短暂离开/长时间离开/有人至无人
tagOptions: ['无人至有人', '短暂离开', '长时间离开', '有人至无人'],
stateOptions: ['不判断', '触发', '释放', '关至开', '开至关'],
// 门磁专用选项与其他端口不同0=不判断,1=关门,2=开门,3=关至开,4=开至关
doorMagOptions: ['不判断', '关门', '开门', '关至开', '开至关'],
stateOptions: ['不判断', '触发', '释放', '开至关', '关至开'],
// 门磁专用选项与其他端口不同0=不判断,1=关门,2=开门,3=开至关,4=关至开
doorMagOptions: ['不判断', '关门', '开门', '开至关', '关至开'],
// 默认条件:由用户指定的条件组与条件
conditions: [
// 组1: timeout 2秒包含1个条件
{ group: 1, seq: 1, tag: 0, cardPower: 0, doorMag: 3, irHall: 0, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 0, timeout: 2, timeoutUnit: 0 },
{ group: 1, seq: 1, tag: 0, cardPower: 0, doorMag: 4, irHall: 0, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 0, timeout: 2, timeoutUnit: 0 },
// 组2: timeout 20秒包含3个条件
{ group: 2, seq: 1, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 0, timeout: 20, timeoutUnit: 0 },
{ group: 2, seq: 2, tag: 0, cardPower: 0, doorMag: 0, irHall: 0, bathRadar: 1, bathroomRadar: 0, judgeTime: 0, judgeUnit: 0, timeout: 20, timeoutUnit: 0 },
{ group: 2, seq: 3, tag: 0, cardPower: 0, doorMag: 0, irHall: 0, bathRadar: 0, bathroomRadar: 1, judgeTime: 0, judgeUnit: 0, timeout: 20, timeoutUnit: 0 },
// 组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 },
{ group: 3, seq: 1, tag: 1, cardPower: 0, doorMag:3, 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: 1, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 },
// 组5: timeout 10分包含1个条件
@@ -100,6 +109,8 @@ Page({
bathTriggerIndex: 0,
bathReleaseIndex: 0,
pressedMask: 0,
// 是否将非协议数据按 GBK 解码为中文日志
showChineseLogs: false,
// 删除模态框状态
deleteDialogVisible: false,
deleteDialogMode: '', // 'group' | 'condition'
@@ -237,6 +248,21 @@ Page({
onShow() {
// 页面显示时也打印一次,方便返回/二次进入场景
this.logBleStatus()
// 尝试在页面恢复时重建 BLE 通道并恢复通知/订阅
try {
// 小延时以让系统先恢复蓝牙状态
setTimeout(() => {
this.ensureBleConnectedAndReconnect(() => {
// 确保找到 service/char 并开启 notify
this.ensureBleChannels(() => {
try { this.enableNotify() } catch (e) { /* ignore */ }
try { this.setupBleListener() } catch (e) { /* ignore */ }
// 重新启动雷达读取(若页面之前在读)
try { this.sendRadarStatusCommand(true) } catch (e) { /* ignore */ }
})
})
}, 250)
} catch (e) { /* ignore */ }
},
onUnload() {
@@ -270,7 +296,16 @@ Page({
// === 门磁控件事件处理 ===
onDoorFieldInput(e) {
const field = e.currentTarget.dataset.field
let val = Number(e.detail.value || 0)
const raw = (e.detail && e.detail.value != null) ? String(e.detail.value).trim() : ''
// Allow empty input while user is editing (don't enforce min/max on empty)
if (raw === '') {
const updates = {}
if (field === 'triggerDelay') updates.doorTriggerDelay = ''
if (field === 'releaseDelay') updates.doorReleaseDelay = ''
this.setData(updates)
return
}
let val = Number(raw)
if (isNaN(val)) val = 0
// 单位index 0 => 秒 (max 60), index 1 => 分 (max 30)
const unitIndex = (field === 'triggerDelay') ? (this.data.doorTriggerUnitIndex || 0) : (this.data.doorReleaseUnitIndex || 0)
@@ -323,7 +358,15 @@ Page({
// === 卫浴控件事件处理 ===
onBathFieldInput(e) {
const field = e.currentTarget.dataset.field
let val = Number(e.detail.value || 0)
const raw = (e.detail && e.detail.value != null) ? String(e.detail.value).trim() : ''
if (raw === '') {
const updates = {}
if (field === 'triggerDelay') updates.bathTriggerDelay = ''
if (field === 'releaseDelay') updates.bathReleaseDelay = ''
this.setData(updates)
return
}
let val = Number(raw)
if (isNaN(val)) val = 0
const unitIndex = (field === 'triggerDelay') ? (this.data.bathTriggerUnitIndex || 0) : (this.data.bathReleaseUnitIndex || 0)
const maxAllowed = unitIndex === 0 ? 60 : 30
@@ -459,6 +502,100 @@ Page({
}
},
// 导出日志:将当前 logList 文本
onExportLogs(){
const list = this.data.logList || []
if (!list || list.length === 0) {
// this.appendLog('WARN', '导出失败:无日志可导出')
wx.showToast({ title: '无日志可导出', icon: 'none' })
return
}
const txt = list.map(i => (i.time ? i.time + ' ' : '') + (i.text || '')).join('\n')
try {
if (typeof wx.getFileSystemManager !== 'function') {
this.appendLog('WARN', '环境不支持 wx.getFileSystemManager回退为剪贴板')
if (typeof wx.setClipboardData === 'function') {
wx.setClipboardData({ data: txt, success: () => { wx.showToast({ title: '日志已复制到剪贴板', icon: 'none' }); this.appendLog('UI', 'clipboard fallback success') }, fail: () => { wx.showToast({ title: '导出失败', icon: 'none' }) } })
} else {
wx.showToast({ title: '当前环境不支持导出', icon: 'none' })
}
return
}
const fsm = wx.getFileSystemManager()
const now = new Date()
const pad = (n) => String(n).padStart(2, '0')
const name = `comm_logs_${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.txt`
const base = (wx.env && wx.env.USER_DATA_PATH) ? wx.env.USER_DATA_PATH : null
const filePath = base ? `${base}/${name}` : `/tmp/${name}`
fsm.writeFile({
filePath,
data: txt,
encoding: 'utf8',
success: () => {
this.appendLog('UI', `导出文件: ${filePath}`)
// 写入后弹出确认对话,用户点击“分享”视为手势再调用分享接口(不打开文档)
wx.showModal({
title: '导出完成',
content: '文件已导出,是否现在分享给微信联系人?',
confirmText: '分享',
cancelText: '取消',
success: (res) => {
if (res && res.confirm) {
try {
// this.appendLog('DEBUG', '用户确认分享,尝试调用分享接口')
if (typeof wx.shareFileMessage === 'function') {
wx.shareFileMessage({ filePath, success: () => { wx.showToast({ title: '已打开分享面板', icon: 'success' }); this.appendLog('UI', 'shareFileMessage success') }, fail: (e) => { this.appendLog('WARN', `shareFileMessage fail ${e && (e.errMsg||e)}`); if (typeof wx.setClipboardData === 'function') { wx.setClipboardData({ data: txt, success: () => wx.showToast({ title: '已复制到剪贴板,可粘贴发送', icon: 'none' }) }) } } })
return
}
if (typeof wx.shareFile === 'function') {
wx.shareFile({ filePath, success: () => { wx.showToast({ title: '已打开分享面板', icon: 'success' });
// this.appendLog('UI', 'shareFile success')
}, fail: (e) => { this.appendLog('WARN', `shareFile fail ${e && (e.errMsg||e)}`); if (typeof wx.setClipboardData === 'function') { wx.setClipboardData({ data: txt, success: () => wx.showToast({ title: '已复制到剪贴板,可粘贴发送', icon: 'none' }) }) } } })
return
}
// 无分享接口,回退为剪贴板
if (typeof wx.setClipboardData === 'function') {
wx.setClipboardData({ data: txt, success: () => { wx.showToast({ title: '日志已复制到剪贴板,可在聊天中粘贴发送', icon: 'none' });
// this.appendLog('UI', 'clipboard fallback success')
} })
} else {
wx.showToast({ title: '导出完成,但无法直接分享', icon: 'none' })
}
} catch (ex) {
this.appendLog('WARN', `用户触发分享时出错: ${ex && (ex.message||ex)}`)
if (typeof wx.setClipboardData === 'function') {
wx.setClipboardData({ data: txt, success: () => { wx.showToast({ title: '日志已复制到剪贴板', icon: 'none' });
// this.appendLog('UI', 'clipboard fallback success')
} })
}
}
} else {
this.appendLog('UI', '用户取消分享,文件已保存')
wx.showToast({ title: '文件已保存', icon: 'none' })
}
},
fail: (e) => {
this.appendLog('WARN', `showModal failed: ${e && (e.errMsg||e)}`)
// 弹窗失败时直接回退为剪贴板
if (typeof wx.setClipboardData === 'function') {
wx.setClipboardData({ data: txt, success: () => { wx.showToast({ title: '日志已复制到剪贴板', icon: 'none' }); this.appendLog('UI', 'clipboard fallback success after showModal fail') } })
}
}
})
},
fail: (err) => {
console.error('writeFile fail', err)
wx.showToast({ title: '写入文件失败', icon: 'none' })
this.appendLog('WARN', `写入文件失败 ${err && (err.errMsg||err)}`)
}
})
} catch (e) {
console.error(e)
wx.showToast({ title: '导出失败', icon: 'none' })
this.appendLog('WARN', `导出异常 ${e && (e.message||e)}`)
}
},
// 开始OTA升级命令0x0B, P0=0x01
onStartOta() {
try {
@@ -1078,6 +1215,20 @@ Page({
// 按显示偏好记录日志HEX 或原始文本)
const viewText = this.data.hexShow ? hex : `[${hex}]`
this.appendLog('RX', viewText)
// 若开启中文日志且数据不符合协议头,则尝试用 GBK 解码生成中文日志
try {
const isProtocol = u8 && u8.length >= 2 && u8[0] === 0xCC && u8[1] === 0xC0
if (!isProtocol && this.data.showChineseLogs) {
const decoded = this._tryDecodeGbk(u8)
if (decoded && decoded.trim()) {
this.appendLog('RX-CN', decoded)
}
}
} catch (e) {
// ignore decode errors
}
// 将规范化数据交给协议解析器,进行业务处理与 UI 更新
this.handleIncomingPacket(u8)
}
@@ -1101,6 +1252,46 @@ Page({
wx.showModal({ title: '提示', content: '下载链接:' + url, showCancel: false });
}
},
// Toggle Chinese logs: when enabled, non-protocol BLE data will be decoded via GBK
onToggleChineseLogs() {
const next = !this.data.showChineseLogs
this.setData({ showChineseLogs: next })
wx.showToast({ title: next ? '中文日志已开启' : '中文日志已关闭', icon: 'none' })
if (next) {
// 发送固定数据包以通知设备(要求:中文日志开启时发送)
const pkt = new Uint8Array([0xCC, 0xC0, 0x0C, 0x00, 0x4F, 0xC5, 0x01, 0x00, 0x02, 0x00, 0x0C, 0x04])
try {
this.appendLog('TX', `中文日志开关包: ${this.toHex(pkt)}`)
this.writeBleBytes(pkt, '中文日志开关')
} catch (e) {
this.appendLog('WARN', `发送中文日志开关包失败: ${e && (e.message || e)}`)
}
}
},
// 尝试用 GBK 解码 Uint8Array若环境支持 TextDecoder('gbk') 则使用之,失败则回退到 utf-8 或简单字节->字符串
_tryDecodeGbk(u8) {
try {
if (GBKDecoder && typeof GBKDecoder.decode === 'function') {
const r = GBKDecoder.decode(u8)
if (r != null) return r
}
} catch (e) { /* ignore */ }
// Fallback: best-effort ASCII/pass-through
try {
let s = ''
for (let i = 0; i < u8.length; i++) {
const b = u8[i]
if (b <= 0x7F) s += String.fromCharCode(b)
else {
const b2 = u8[i + 1]
if (typeof b2 === 'number') { s += '\uFFFD'; i += 1 } else s += '\uFFFD'
}
}
return s
} catch (e) { return '' }
},
teardownBleListener() {
if (this._onBleChange && typeof wx.offBLECharacteristicValueChange === 'function') {
wx.offBLECharacteristicValueChange(this._onBleChange)
@@ -2494,11 +2685,15 @@ Page({
const now = new Date()
const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
const finalText = this.data.withTimestamp ? `[${timeStr}] ${direction}: ${text}` : `${direction}: ${text}`
try {
// 同步输出到控制台,方便开发者在调试时查看
try {
// 始终在控制台输出,便于调试;但若 direction 为 UI/DEBUG 则不加入页面日志列表以减少冗余
console.log(finalText)
} catch (e) { /* ignore console errors */ }
if (String(direction) === 'UI' || String(direction) === 'DEBUG') {
// 仅打印到控制台,不更新页面 logList
return
}
const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`
// 限制日志长度,避免无限增长导致 setData 负担和内存问题保留最新200条
try {

View File

@@ -12,6 +12,8 @@
<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 class="cu-item {{4==TabCur?'text-blue cur':''}}" bindtap="tabSelect" data-id="4">设备通信日志</view>
</view>
</scroll-view>
@@ -25,12 +27,15 @@
<button size="mini" type="primary" bindtap="onStartOta">OTA升级</button>
</view> -->
<!-- 顶部设备分类 -->
<view class="grid">
<view class="grid-item" wx:for="{{radarLights}}" wx:key="key">
<view class="circle {{item.colorClass}}"></view>
<text class="label">{{item.label}}</text>
<!-- 顶部设备分类(含读雷达按钮) -->
<view class="grid-and-actions">
<view class="grid">
<view class="grid-item" wx:for="{{radarLights}}" wx:key="key">
<view class="circle {{item.colorClass}}"></view>
<text class="label">{{item.label}}</text>
</view>
</view>
</view>
<!-- 状态功能块:已改为测试按键按钮,点击发送测试按键命令 -->
@@ -75,7 +80,7 @@
</view>
<!-- 门磁/卫浴事件设置(替代原延时滑块) -->
<view class="cfg-card">
<view class="cfg-card tall-controls">
<view class="cfg-head head-row">
<text>门磁开廊灯事件设置</text>
<view class="head-actions">
@@ -85,17 +90,13 @@
<view class="form-row">
<view class="form-inline">
<text class="label">开门延时</text>
<picker class="inline-picker" mode="selector" range="{{doorTriggerOptions}}" value="{{doorTriggerIndex}}" bindchange="onDoorTriggerPickerChange">
<view class="picker-text">{{doorTriggerOptions[doorTriggerIndex]}}</view>
</picker>
<input class="picker-text" placeholder="输入延时" value="{{doorTriggerDelay}}" type="number" data-field="triggerDelay" bindinput="onDoorFieldInput" />
<text class="label">单位</text>
<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>
<picker class="inline-picker" mode="selector" range="{{doorReleaseOptions}}" value="{{doorReleaseIndex}}" bindchange="onDoorReleasePickerChange">
<view class="picker-text">{{doorReleaseOptions[doorReleaseIndex]}}</view>
</picker>
<input class="picker-text" placeholder="输入延时" value="{{doorReleaseDelay}}" type="number" data-field="releaseDelay" bindinput="onDoorFieldInput" />
<text class="label">单位</text>
<picker class="inline-picker" mode="selector" range="{{eventUnits}}" value="{{doorReleaseUnitIndex}}" bindchange="onDoorUnitChange" data-field="releaseUnit">
<view class="picker-text">{{eventUnits[doorReleaseUnitIndex]}}</view>
@@ -104,7 +105,7 @@
</view>
</view>
<view class="cfg-card">
<view class="cfg-card tall-controls">
<view class="cfg-head head-row">
<text>卫浴雷达开卫浴灯事件设置</text>
<view class="head-actions">
@@ -114,17 +115,13 @@
<view class="form-row">
<view class="form-inline">
<text class="label">触发延迟</text>
<picker class="inline-picker" mode="selector" range="{{bathTriggerOptions}}" value="{{bathTriggerIndex}}" bindchange="onBathTriggerPickerChange">
<view class="picker-text">{{bathTriggerOptions[bathTriggerIndex]}}</view>
</picker>
<input class="picker-text" placeholder="输入延时" value="{{bathTriggerDelay}}" type="number" data-field="triggerDelay" bindinput="onBathFieldInput" />
<text class="label">单位</text>
<picker class="inline-picker" mode="selector" range="{{eventUnits}}" value="{{bathTriggerUnitIndex}}" bindchange="onBathUnitChange" data-field="triggerUnit">
<view class="picker-text">{{eventUnits[bathTriggerUnitIndex]}}</view>
</picker>
<text class="label">释放延迟</text>
<picker class="inline-picker" mode="selector" range="{{bathReleaseOptions}}" value="{{bathReleaseIndex}}" bindchange="onBathReleasePickerChange">
<view class="picker-text">{{bathReleaseOptions[bathReleaseIndex]}}</view>
</picker>
<input class="picker-text" placeholder="输入延时" value="{{bathReleaseDelay}}" type="number" data-field="releaseDelay" bindinput="onBathFieldInput" />
<text class="label">单位</text>
<picker class="inline-picker" mode="selector" range="{{eventUnits}}" value="{{bathReleaseUnitIndex}}" bindchange="onBathUnitChange" data-field="releaseUnit">
<view class="picker-text">{{eventUnits[bathReleaseUnitIndex]}}</view>
@@ -139,7 +136,7 @@
<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" size="mini" type="default" bindtap="onClearLogs">清空</button>
<button class="clear-btn" style="display:none;" size="mini" type="default" bindtap="onClearLogs">清空</button>
</view>
</view>
@@ -163,7 +160,32 @@
<button class="send-btn" size="mini" type="primary" bindtap="onSend">发送</button>
</view>
<scroll-view class="log-scroll" scroll-y="true">
<!-- <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>
@@ -410,25 +432,42 @@
<!-- Tab: 设备升级 -->
<view wx:if="{{TabCur==3}}" class="content">
<!-- OTA 升级卡片(顶部) -->
<!-- OTA 升级说明卡片 -->
<view class="cfg-card ota-card">
<view class="cfg-head head-row">
<text>OTA 升级工具</text>
<text>设备升级说明</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 class="ota-article">
<view class="section">
<text class="section-title">前提条件</text>
<view class="section-body">
<text>1手机已安装 “OTA升级工具”。</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>
</view>
<view class="section">
<text class="section-title">注意事项</text>
<view class="section-body">
<text>• 升级过程中请保持设备与手机的蓝牙连接稳定,勿中断电源。</text>
<text>• 升级失败后请重试一次,仍失败请联系技术支持并提供日志。</text>
<text>• 点击“下载工具”将复制下载链接到剪贴板,需在浏览器中打开并下载。</text>
</view>
</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 File

@@ -7,6 +7,9 @@
.table.cond-item-content .tr { display:grid; grid-template-columns: 1.1fr 1.1fr 1.1fr 1fr 1fr 1.1fr; gap: 6rpx; padding: 6rpx 0; }
/* pages/basics/BluetoothDebugging/B13page/B13page.wxss */
.container { background: #f5f6f8; min-height: 100vh; display: flex; flex-direction: column; }
.nav { padding: 4rpx 6rpx; }
.nav .flex { gap: 6rpx; }
.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; }
@@ -44,11 +47,11 @@
/* 日志头按钮组:读雷达 与 清空 按钮并列样式 */
.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;
padding: 0 18rpx;
height: 56rpx;
line-height: 56rpx;
border-radius: 12rpx;
font-size: 22rpx;
font-size: 26rpx;
}
.log-head .log-actions .clear-btn {
padding: 0 16rpx;
@@ -215,6 +218,20 @@
.cond-card .group-time .unit-picker .picker-text { width: 100rpx; height: 44rpx; padding: 0 8rpx; font-size: 22rpx; white-space: nowrap; }
.cfg-actions { display:flex; justify-content:flex-end; margin-top: 10rpx; }
.action-row { display:flex; flex-wrap: wrap; gap: 12rpx; }
/* OTA 文案样式:用于设备升级的分段说明(前提条件 / 操作步骤 / 注意事项) */
.ota-article .section { margin-bottom: 12rpx; }
.ota-article .section-title { font-size: 36rpx; font-weight: 800; color:#222; margin-bottom: 10rpx; display: block; }
.ota-article .section-body { font-size: 32rpx; color: #444; padding-left: 8rpx; line-height: 46rpx; }
.ota-article .section-body text { display: block; margin-bottom: 10rpx; }
.ota-article .section-body .mono { font-family: monospace; background: #f5f6f8; padding: 2rpx 6rpx; border-radius: 6rpx; }
.ota-article .note { color: #d9534f; }
/* Full-screen log view for TabCur==4: ensure scroll area fills from toolbar bottom to screen bottom */
.full-log-view { display:flex; flex-direction: column; flex: 1; min-height: 0; }
.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; }
.log-item { margin-bottom: 10rpx; }
.log-item:last-child { margin-bottom: 0; }
.log-time { display: block; font-size: 22rpx; color: #9aa0a6; margin-bottom: 4rpx; }
@@ -251,3 +268,27 @@
.inline-input { width: 90rpx; }
.inline-picker .picker-text { width: 80rpx; }
}
/* 增大门磁/卫浴卡片中的控件高度,便于触控 */
.cfg-card.tall-controls .picker-text,
.cfg-card.tall-controls input.picker-text,
.cfg-card.tall-controls .inline-picker .picker-text,
.cfg-card.tall-controls .inline-input {
height: 56rpx !important;
line-height: 56rpx !important;
font-size: 26rpx !important;
padding: 0 12rpx !important;
}
.cfg-card.tall-controls .form-inline { gap: 14rpx; }
.cfg-card.tall-controls .label { font-size: 24rpx; }
/* 全屏日志视图:增大工具栏中“清空”按钮宽度,提升点击目标 */
.full-log-view .toolbar .toolbar-actions button:last-child {
min-width: 150rpx;
padding: 0 16rpx;
}
/* 中文日志开关按钮:开-绿色,关-红色 */
.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; }

View File

@@ -141,27 +141,65 @@ Page({
// 先取消旧的发现监听,避免多次注册造成干扰
this.teardownDeviceFoundListener()
this._foundCount = 0
const now = Date.now()
// 防护:避免短时间内频繁触发扫描(系统限制)
if (this._lastScanAt && now - this._lastScanAt < 2000) {
try { this.appendLog && this.appendLog('WARN', 'skip startBluetoothDevicesDiscovery: throttled') } catch (e) {}
return
}
this._lastScanAt = now
console.info('[BLE] start scan, prefix:', prefix || 'ALL')
// 先停止可能已有的搜索,待停止完成后再启动,避免竞态
wx.stopBluetoothDevicesDiscovery({
complete: () => {
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: true, // 允许重复上报,提升二次搜索的发现率
success: () => {
const doStart = () => {
// 先停止可能已有的搜索,待停止完成后再启动,避免竞态
wx.stopBluetoothDevicesDiscovery({
complete: () => {
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: true, // 允许重复上报,提升二次搜索的发现率
success: () => {
this.setupDeviceFoundListener(prefix)
// 定时停止,避免长时间占用
setTimeout(() => {
this.stopBluetoothDiscovery()
}, 6000)
},
fail: (err) => {
console.error('开始搜索蓝牙设备失败', err)
// 若为系统提示搜索过于频繁,可稍后重试一次
const code = err && (err.errCode || (err.errMsg && Number((err.errMsg.match(/\d+/)||[])[0])))
if (code === 10008) {
try { this.appendLog && this.appendLog('WARN', 'startBluetoothDevicesDiscovery failed: scanning too frequently, retrying shortly') } catch (e) {}
setTimeout(() => { try { doStart() } catch (e) {} }, 1500)
return
}
wx.hideLoading()
wx.showToast({ title: '搜索失败', icon: 'none' })
}
})
}
})
}
// 优先查询适配器状态,若系统正在扫描则直接注册监听并返回
if (typeof wx.getBluetoothAdapterState === 'function') {
wx.getBluetoothAdapterState({
success: (res) => {
if (res && res.discovering) {
try { this.appendLog && this.appendLog('CFG', 'adapter already discovering, attach listener') } catch (e) {}
this.setupDeviceFoundListener(prefix)
// 定时停止,避免长时间占用
setTimeout(() => {
this.stopBluetoothDiscovery()
}, 6000)
},
fail: (err) => {
console.error('开始搜索蓝牙设备失败', err)
wx.hideLoading()
wx.showToast({ title: '搜索失败', icon: 'none' })
wx.showToast({ title: '正在搜索中', icon: 'none' })
return
}
})
}
})
doStart()
},
fail: () => {
doStart()
}
})
} else {
doStart()
}
},
setupDeviceFoundListener(prefix) {
@@ -262,6 +300,7 @@ Page({
? `${base}&serviceId=${encodeURIComponent(svc)}&txCharId=${encodeURIComponent(tx)}&rxCharId=${encodeURIComponent(rx)}`
: base
console.log('navigateTo:', withParams)
try { this._navigatingToB13 = true } catch (e) { /* ignore */ }
wx.navigateTo({ url: withParams })
@@ -305,30 +344,49 @@ Page({
}
wx.showLoading({ title: '连接中...', mask: true })
// 使用 BLE 直连,不再使用模拟延迟
wx.createBLEConnection({
deviceId: device.id,
success: () => {
const list = this.data.deviceList.map((d, i) => ({ ...d, connected: i === index }))
this.setData({ deviceList: list, currentDeviceId: device.id })
// 设置MTU为256提升传输效率
if (typeof wx.setBLEMTU === 'function') {
wx.setBLEMTU({
deviceId: device.id,
mtu: 500,
fail: () => console.warn('[BLE] set MTU 256 failed'),
success: () => console.info('[BLE] set MTU 256 success')
})
}
// 连接成功后发现服务与特征
this.discoverBleChannels(device)
},
fail: (err) => {
// 在尝试 createBLEConnection 前确保适配器已打开(处理息屏后 closeBluetoothAdapter 场景)
this.ensureBluetoothReady()
.then(() => {
// 使用 BLE 直连
wx.createBLEConnection({
deviceId: device.id,
success: () => {
const list = this.data.deviceList.map((d, i) => ({ ...d, connected: i === index }))
this.setData({ deviceList: list, currentDeviceId: device.id })
// 设置MTU为256提升传输效率若支持
if (typeof wx.setBLEMTU === 'function') {
wx.setBLEMTU({
deviceId: device.id,
mtu: 500,
fail: () => console.warn('[BLE] set MTU 256 failed'),
success: () => console.info('[BLE] set MTU 256 success')
})
}
// 连接成功后发现服务与特征
this.discoverBleChannels(device)
},
fail: (err) => {
wx.hideLoading()
console.error('BLE 连接失败', err)
const errmsg = (err && (err.errMsg || err.message)) || ''
const isAlreadyConnected = errmsg.indexOf('already connect') >= 0 || errmsg.indexOf('already connected') >= 0 || (err && (err.errCode === -1 || err.errno === 1509007))
if (isAlreadyConnected) {
try { this.appendLog && this.appendLog('CFG', 'createBLEConnection: already connected, treating as connected') } catch (e) {}
const list = this.data.deviceList.map((d, i) => ({ ...d, connected: i === index }))
this.setData({ deviceList: list, currentDeviceId: device.id })
// 继续发现服务与特征以恢复页面状态
try { this.discoverBleChannels(device) } catch (e) {}
return
}
wx.showToast({ title: '连接失败', icon: 'none' })
}
})
})
.catch((err) => {
wx.hideLoading()
console.error('BLE 连接失败', err)
wx.showToast({ title: '连接失败', icon: 'none' })
}
})
console.error('蓝牙未初始化,无法连接', err)
wx.showToast({ title: '蓝牙未初始化', icon: 'none' })
})
},
// 发现包含 FFE1(写) / FFE2(订阅) 的服务与特征,并启用 FFE2 通知
@@ -384,7 +442,8 @@ Page({
const devName = device.name || 'W13设备'
const url = `/pages/basics/BluetoothDebugging/B13page/B13page?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(deviceId)}&serviceId=${encodeURIComponent(serviceId)}&txCharId=${encodeURIComponent(ffe1.uuid)}&rxCharId=${encodeURIComponent(ffe2.uuid)}`
console.log(url)
wx.navigateTo({ url })
try { this._navigatingToB13 = true } catch (e) { /* ignore */ }
wx.navigateTo({ url })
// this.sendFixedCommand(deviceId, serviceId, ffe1.uuid)
}
}
@@ -482,6 +541,11 @@ Page({
// 断开当前连接设备(如果有真实连接)
disconnectCurrentDevice() {
// 如果正在导航到 B13 页面,避免主动断开连接以保持会话
if (this._navigatingToB13) {
try { this.appendLog && this.appendLog('CFG', 'skip disconnectCurrentDevice during navigate to B13') } catch (e) {}
return
}
const idx = this.data.deviceList.findIndex(d => d.connected)
if (idx >= 0) {
// 标记断开状态
@@ -530,5 +594,105 @@ Page({
this.loadDevicesByTab(this.data.activeTab)
// 同步执行一次蓝牙搜索(按 W13 过滤规则)
this.searchBluetooth()
},
onShow() {
// 返回到此页面时,清理导航标记
if (this._navigatingToB13) {
this._navigatingToB13 = false
return
}
try { this.appendLog && this.appendLog('CFG', 'onShow: resume, ensuring adapter and forcing discovery') } catch (e) {}
// 息屏唤醒后可能适配器被关闭,先确保打开后短延迟再搜索一次
this.ensureBluetoothReady()
.then(() => {
setTimeout(() => {
try {
this.startBluetoothDevicesDiscovery(this.data.activeTab)
} catch (e) { /* ignore */ }
}, 300)
// 尝试获取系统已连接设备,优先恢复之前连接的设备显示(防止已连接但不广播的设备无法被扫描到)
try {
const svc = this.data.currentServiceId
const isW13 = this.data.activeTab === 'W13'
const prefix = this.data.activeTab
const matchByTab = (name) => {
const n = name || ''
if (isW13) return /^BLV_(W13|C13)_.+$/i.test(n)
return prefix ? n.startsWith(prefix) : true
}
if (typeof wx.getConnectedBluetoothDevices === 'function' && svc) {
wx.getConnectedBluetoothDevices({ services: [svc], success: (res) => {
const devices = (res && res.devices) || []
if (devices.length) {
// 将已连接设备合并到列表并按过滤规则筛选
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)
const mapped = { id: d.deviceId, name, mac: d.deviceId, connected: true, RSSI: d.RSSI || 0, serviceUUIDs: d.serviceUUIDs || [] }
if (idx >= 0) list[idx] = { ...list[idx], ...mapped }
else list.unshift(mapped)
})
this.setData({ deviceList: list })
try { this.appendLog && this.appendLog('CFG', 'restored connected devices from system') } catch (e) {}
}
}})
} else if (typeof wx.getBluetoothDevices === 'function') {
// 作为兜底,查询最近缓存的设备并按过滤规则合并
wx.getBluetoothDevices({ success: (res) => {
const devices = (res && res.devices) || []
if (devices.length) {
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)
const mapped = { id: d.deviceId, name, mac: d.deviceId, connected: !!d.connected, RSSI: d.RSSI || 0, serviceUUIDs: d.serviceUUIDs || [] }
if (idx >= 0) list[idx] = { ...list[idx], ...mapped }
else list.push(mapped)
})
this.setData({ deviceList: list })
}
}})
}
} catch (e) { /* ignore */ }
})
.catch((err) => {
try { this.appendLog && this.appendLog('WARN', 'onShow ensureBluetoothReady failed') } catch (e) {}
})
},
onHide() {
// 如果正在导航到 B13 页面,跳过 onHide 的断开/重置流程,保留连接
if (this._navigatingToB13) {
try { this.appendLog && this.appendLog('CFG', 'onHide skipped due to navigation to B13') } catch (e) {}
// 清理标记,后续返回时 onShow 会再次执行
this._navigatingToB13 = false
return
}
try {
// 停止发现,避免后台扫描
if (typeof wx.stopBluetoothDevicesDiscovery === 'function') {
wx.stopBluetoothDevicesDiscovery({ complete: () => { try { this.appendLog && this.appendLog('CFG', 'stopBluetoothDevicesDiscovery onHide') } catch (e) {} } })
}
} catch (e) { /* ignore */ }
try {
// 断开当前连接(如果有)
this.disconnectCurrentDevice()
} catch (e) { /* ignore */ }
try {
// 关闭蓝牙适配器以重置底层状态(部分 Android 机型息屏后需要此步骤)
if (typeof wx.closeBluetoothAdapter === 'function') {
wx.closeBluetoothAdapter({ complete: () => { try { this.appendLog && this.appendLog('CFG', 'closeBluetoothAdapter called onHide') } catch (e) {} } })
}
} catch (e) { /* ignore */ }
}
})

24075
utils/ecUnicodeToGBK.js Normal file

File diff suppressed because it is too large Load Diff